openMSX
MSXtar.cc
Go to the documentation of this file.
1// Note: For Mac OS X 10.3 <ctime> must be included before <utime.h>.
2#include <ctime>
3#ifndef _MSC_VER
4#include <utime.h>
5#else
6#include <sys/utime.h>
7#endif
8
9#include "MSXtar.hh"
10
12#include "FileOperations.hh"
13#include "foreach_file.hh"
14#include "MSXException.hh"
15#include "MsxChar2Unicode.hh"
16
17#include "StringOp.hh"
18#include "strCat.hh"
19#include "File.hh"
20#include "narrow.hh"
21#include "one_of.hh"
22#include "ranges.hh"
23#include "stl.hh"
24#include "xrange.hh"
25
26#include <algorithm>
27#include <bit>
28#include <cstring>
29#include <cassert>
30#include <cctype>
31
32using std::string;
33using std::string_view;
34
35namespace openmsx {
36
37using FAT::Free;
38using FAT::EndOfChain;
39using FAT::Cluster;
40using FAT::DirCluster;
41using FAT::FatCluster;
42using FAT::FileName;
43
44namespace FAT {
45 static constexpr unsigned FREE = 0x000;
46 static constexpr unsigned FIRST_CLUSTER = 0x002;
47}
48
49namespace FAT12 {
50 static constexpr unsigned BAD = 0xFF7;
51 static constexpr unsigned END_OF_CHAIN = 0xFFF; // actually 0xFF8-0xFFF, signals EOF in FAT12
52
53 static constexpr unsigned MAX_CLUSTER_COUNT = 0xFF4;
54
55 // Functor to convert a FatCluster or DirCluster to a FAT12 cluster number
57 unsigned operator()(Free) const { return FAT::FREE; }
58 unsigned operator()(EndOfChain) const { return END_OF_CHAIN; }
59 unsigned operator()(Cluster cluster) const { return FAT::FIRST_CLUSTER + cluster.index; }
61}
62
63namespace FAT16 {
64 static constexpr unsigned BAD = 0xFFF7;
65 static constexpr unsigned END_OF_CHAIN = 0xFFFF; // actually 0xFFF8-0xFFFF, signals EOF in FAT16
66
67 static constexpr unsigned MAX_CLUSTER_COUNT = 0xFFF4;
68
69 // Functor to convert a FatCluster or DirCluster to a FAT16 cluster number
71 unsigned operator()(Free) const { return FAT::FREE; }
72 unsigned operator()(EndOfChain) const { return END_OF_CHAIN; }
73 unsigned operator()(Cluster cluster) const { return FAT::FIRST_CLUSTER + cluster.index; }
75}
76
77static constexpr unsigned SECTOR_SIZE = sizeof(SectorBuffer);
78static constexpr unsigned DIR_ENTRIES_PER_SECTOR = SECTOR_SIZE / sizeof(MSXDirEntry);
79
80static constexpr uint8_t EBPB_SIGNATURE = 0x29; // Extended BIOS Parameter Block signature
81
82// This particular combination of flags indicates that this dir entry is used
83// to store a long Unicode file name.
84// For details, read http://home.teleport.com/~brainy/lfn.htm
85static constexpr uint8_t T_MSX_LFN = 0x0F; // LFN entry (long files names)
86
90unsigned MSXtar::clusterToSector(Cluster cluster) const
91{
92 return dataStart + sectorsPerCluster * cluster.index;
93}
94
98Cluster MSXtar::sectorToCluster(unsigned sector) const
99{
100 // check lower bound since unsigned can't represent negative numbers
101 // don't check upper bound since it is used in calculations like getNextSector
102 assert(sector >= dataStart);
103 return Cluster{(sector - dataStart) / sectorsPerCluster};
104}
105
106
109void MSXtar::parseBootSector(const MSXBootSector& boot)
110{
111 fatCount = boot.nrFats;
112 sectorsPerFat = boot.sectorsFat;
113 sectorsPerCluster = boot.spCluster;
114
115 unsigned nrSectors = boot.nrSectors;
116 if (nrSectors == 0 && boot.params.extended.extendedBootSignature == EBPB_SIGNATURE) {
117 nrSectors = boot.params.extended.nrSectors;
118 }
119
120 if (boot.bpSector != SECTOR_SIZE) {
121 throw MSXException("Illegal sector size: ", boot.bpSector);
122 }
123 if (boot.resvSectors == 0) {
124 throw MSXException("Illegal number of reserved sectors: ", boot.resvSectors);
125 }
126 if (fatCount == 0) {
127 throw MSXException("Illegal number of FATs: ", fatCount);
128 }
129 if (sectorsPerFat == 0 || sectorsPerFat > 0x100) {
130 throw MSXException("Illegal number of sectors per FAT: ", sectorsPerFat);
131 }
132 if (boot.dirEntries == 0 || boot.dirEntries % DIR_ENTRIES_PER_SECTOR != 0) {
133 throw MSXException("Illegal number of root directory entries: ", boot.dirEntries);
134 }
135 if (!std::has_single_bit(sectorsPerCluster)) {
136 throw MSXException("Illegal number of sectors per cluster: ", sectorsPerCluster);
137 }
138
139 unsigned nbRootDirSectors = boot.dirEntries / DIR_ENTRIES_PER_SECTOR;
140 fatStart = boot.resvSectors;
141 rootDirStart = fatStart + fatCount * sectorsPerFat;
142 chrootSector = rootDirStart;
143 dataStart = rootDirStart + nbRootDirSectors;
144
145 // Whether to use FAT16 is strangely implicit, it must be derived from the
146 // cluster count being higher than 0xFF4. Let's round that up to 0x1000,
147 // since FAT12 uses 1.5 bytes per cluster its maximum size is 12 sectors,
148 // whereas FAT16 uses 2 bytes per cluster so its minimum size is 16 sectors.
149 // Therefore we can pick the correct FAT type with sectorsPerFat >= 14.
150 constexpr unsigned fat16Threshold = (0x1000 * 3 / 2 + 0x1000 * 2) / 2 / SECTOR_SIZE;
151 fat16 = sectorsPerFat >= fat16Threshold;
152
153 if (dataStart + sectorsPerCluster > nrSectors) {
154 throw MSXException("Illegal number of sectors: ", nrSectors);
155 }
156
157 clusterCount = std::min((nrSectors - dataStart) / sectorsPerCluster,
158 fat16 ? FAT16::MAX_CLUSTER_COUNT : FAT12::MAX_CLUSTER_COUNT);
159
160 // Some (invalid) disk images have a too small FAT to be able to address
161 // all clusters of the image. OpenMSX SVN revisions pre-11326 even
162 // created such invalid images for some disk sizes!!
163 unsigned fatCapacity = (2 * SECTOR_SIZE * sectorsPerFat) / 3 - FAT::FIRST_CLUSTER;
164 clusterCount = std::min(clusterCount, fatCapacity);
165}
166
167void MSXtar::writeLogicalSector(unsigned sector, const SectorBuffer& buf)
168{
169 assert(!fatBuffer.empty());
170 unsigned fatSector = sector - fatStart;
171 if (fatSector < sectorsPerFat) {
172 // we have a cache and this is a sector of the 1st FAT
173 // --> update cache
174 fatBuffer[fatSector] = buf;
175 fatCacheDirty = true;
176 } else {
177 disk.writeSector(sector, buf);
178 }
179}
180
181void MSXtar::readLogicalSector(unsigned sector, SectorBuffer& buf)
182{
183 assert(!fatBuffer.empty());
184 unsigned fatSector = sector - fatStart;
185 if (fatSector < sectorsPerFat) {
186 // we have a cache and this is a sector of the 1st FAT
187 // --> read from cache
188 buf = fatBuffer[fatSector];
189 } else {
190 disk.readSector(sector, buf);
191 }
192}
193
195 : disk(sectorDisk)
196 , msxChars(msxChars_)
197 , findFirstFreeClusterStart{0}
198{
199 if (disk.getNbSectors() == 0) {
200 throw MSXException("No disk inserted.");
201 }
202 try {
203 SectorBuffer buf;
204 disk.readSector(0, buf);
205 parseBootSector(buf.bootSector);
206 } catch (MSXException& e) {
207 throw MSXException("Bad disk image: ", e.getMessage());
208 }
209
210 // cache complete FAT
211 fatCacheDirty = false;
212 fatBuffer.resize(sectorsPerFat);
213 disk.readSectors(std::span{fatBuffer.data(), sectorsPerFat}, fatStart);
214}
215
216// Not used when NRVO is used (but NRVO optimization is not (yet) mandated)
217MSXtar::MSXtar(MSXtar&& other) noexcept
218 : disk(other.disk)
219 , fatBuffer(std::move(other.fatBuffer))
220 , msxChars(other.msxChars)
221 , findFirstFreeClusterStart(other.findFirstFreeClusterStart)
222 , clusterCount(other.clusterCount)
223 , fatCount(other.fatCount)
224 , sectorsPerCluster(other.sectorsPerCluster)
225 , sectorsPerFat(other.sectorsPerFat)
226 , fatStart(other.fatStart)
227 , rootDirStart(other.rootDirStart)
228 , dataStart(other.dataStart)
229 , chrootSector(other.chrootSector)
230 , fatCacheDirty(other.fatCacheDirty)
231{
232 other.fatCacheDirty = false;
233}
234
236{
237 if (!fatCacheDirty) return;
238
239 for (auto fat : xrange(fatCount)) {
240 for (auto i : xrange(sectorsPerFat)) {
241 try {
242 disk.writeSector(i + fatStart + fat * sectorsPerFat, fatBuffer[i]);
243 } catch (MSXException&) {
244 // nothing
245 }
246 }
247 }
248}
249
250// Get the next cluster number from the FAT chain
251FatCluster MSXtar::readFAT(Cluster cluster) const
252{
253 assert(!fatBuffer.empty()); // FAT must already be cached
254 assert(cluster.index < clusterCount);
255
256 std::span<const uint8_t> data{fatBuffer[0].raw.data(), sectorsPerFat * size_t(SECTOR_SIZE)};
257
258 unsigned index = FAT::FIRST_CLUSTER + cluster.index;
259 unsigned value = [&] {
260 if (fat16) {
261 auto p = subspan<2>(data, index * 2);
262 return p[0] | p[1] << 8;
263 } else {
264 auto p = subspan<2>(data, (index * 3) / 2);
265 return (index & 1)
266 ? (p[0] >> 4) + (p[1] << 4)
267 : p[0] + ((p[1] & 0x0F) << 8);
268 }
269 }();
270
271 // Be tolerant when reading:
272 // * FREE is returned as FREE.
273 // * Anything else but a valid cluster number is returned as END_OF_CHAIN.
274 if (value == FAT::FREE) {
275 return Free{};
276 } else if (value >= FAT::FIRST_CLUSTER && value < FAT::FIRST_CLUSTER + clusterCount) {
277 return Cluster{value - FAT::FIRST_CLUSTER};
278 } else {
279 return EndOfChain{};
280 }
281}
282
283// Write an entry to the FAT
284void MSXtar::writeFAT(Cluster cluster, FatCluster value)
285{
286 assert(!fatBuffer.empty()); // FAT must already be cached
287 assert(cluster.index < clusterCount);
288
289 // Be strict when writing:
290 // * Anything but FREE, END_OF_CHAIN or a valid cluster number is rejected.
291 assert(!std::holds_alternative<Cluster>(value) || std::get<Cluster>(value).index < clusterCount);
292
293 std::span data{fatBuffer[0].raw.data(), sectorsPerFat * size_t(SECTOR_SIZE)};
294
295 unsigned index = FAT::FIRST_CLUSTER + cluster.index;
296
297 if (std::holds_alternative<Free>(value) && cluster < findFirstFreeClusterStart) {
298 // update where findFirstFreeCluster() will start scanning
299 findFirstFreeClusterStart = cluster;
300 }
301
302 if (fat16) {
303 unsigned fatValue = std::visit(FAT16::toClusterNumber, value);
304 auto p = subspan<2>(data, index * 2);
305 p[0] = narrow_cast<uint8_t>(fatValue);
306 p[1] = narrow_cast<uint8_t>(fatValue >> 8);
307 } else {
308 unsigned fatValue = std::visit(FAT12::toClusterNumber, value);
309 auto p = subspan<2>(data, (index * 3) / 2);
310 if (index & 1) {
311 p[0] = narrow_cast<uint8_t>((p[0] & 0x0F) + (fatValue << 4));
312 p[1] = narrow_cast<uint8_t>(fatValue >> 4);
313 } else {
314 p[0] = narrow_cast<uint8_t>(fatValue);
315 p[1] = narrow_cast<uint8_t>((p[1] & 0xF0) + ((fatValue >> 8) & 0x0F));
316 }
317 }
318 fatCacheDirty = true;
319}
320
321// Find the next cluster number marked as free in the FAT
322// @throws When no more free clusters
323Cluster MSXtar::findFirstFreeCluster()
324{
325 for (auto cluster : xrange(findFirstFreeClusterStart.index, clusterCount)) {
326 if (readFAT({cluster}) == FatCluster(Free{})) {
327 findFirstFreeClusterStart = {cluster};
328 return findFirstFreeClusterStart;
329 }
330 }
331 throw MSXException("Disk full.");
332}
333
334unsigned MSXtar::countFreeClusters() const
335{
336 return narrow<unsigned>(ranges::count_if(xrange(findFirstFreeClusterStart.index, clusterCount),
337 [&](unsigned cluster) { return readFAT({cluster}) == FatCluster(Free{}); }));
338}
339
340// Get the next sector from a file or (root/sub)directory
341// If no next sector then 0 is returned
342unsigned MSXtar::getNextSector(unsigned sector)
343{
344 assert(sector >= rootDirStart);
345 if (sector < dataStart) {
346 // sector is part of the root directory
347 return (sector == dataStart - 1) ? 0 : sector + 1;
348 }
349 Cluster currCluster = sectorToCluster(sector);
350 if (currCluster == sectorToCluster(sector + 1)) {
351 // next sector of cluster
352 return sector + 1;
353 } else {
354 // first sector in next cluster
355 FatCluster nextCluster = readFAT(currCluster);
356 return std::visit(overloaded{
357 [](Free) { return 0u; /* Invalid entry in FAT chain. */ },
358 [](EndOfChain) { return 0u; },
359 [this](Cluster cluster) { return clusterToSector(cluster); }
360 }, nextCluster);
361 }
362}
363
364// Get start cluster from a directory entry.
365DirCluster MSXtar::getStartCluster(const MSXDirEntry& entry) const
366{
367 // Be tolerant when reading:
368 // * Anything but a valid cluster number is returned as FREE.
369 unsigned cluster = entry.startCluster;
370 if (cluster >= FAT::FIRST_CLUSTER && cluster < FAT::FIRST_CLUSTER + clusterCount) {
371 return Cluster{cluster - FAT::FIRST_CLUSTER};
372 } else {
373 return Free{};
374 }
375}
376
377// Set start cluster on a directory entry.
378void MSXtar::setStartCluster(MSXDirEntry& entry, DirCluster cluster) const
379{
380 // Be strict when writing:
381 // * Anything but FREE or a valid cluster number is rejected.
382 assert(!std::holds_alternative<Cluster>(cluster) || std::get<Cluster>(cluster).index < clusterCount);
383 if (fat16) {
384 entry.startCluster = narrow<uint16_t>(std::visit(FAT16::toClusterNumber, cluster));
385 } else {
386 entry.startCluster = narrow<uint16_t>(std::visit(FAT12::toClusterNumber, cluster));
387 }
388}
389
390// If there are no more free entries in a subdirectory, the subdir is
391// expanded with an extra cluster. This function gets the free cluster,
392// clears it and updates the fat for the subdir
393// returns: the first sector in the newly appended cluster
394// @throws When disk is full
395unsigned MSXtar::appendClusterToSubdir(unsigned sector)
396{
397 Cluster nextCl = findFirstFreeCluster();
398 unsigned nextSector = clusterToSector(nextCl);
399
400 // clear this cluster
401 SectorBuffer buf;
402 ranges::fill(buf.raw, 0);
403 for (auto i : xrange(sectorsPerCluster)) {
404 writeLogicalSector(i + nextSector, buf);
405 }
406
407 Cluster curCl = sectorToCluster(sector);
408 assert(readFAT(curCl) == FatCluster(EndOfChain{}));
409 writeFAT(curCl, nextCl);
410 writeFAT(nextCl, EndOfChain{});
411 return nextSector;
412}
413
414
415// Returns the index of a free (or with deleted file) entry
416// In: The current dir sector
417// Out: index number, if no index is found then -1 is returned
418unsigned MSXtar::findUsableIndexInSector(unsigned sector)
419{
420 SectorBuffer buf;
421 readLogicalSector(sector, buf);
422
423 // find a not used (0x00) or delete entry (0xE5)
424 for (auto i : xrange(DIR_ENTRIES_PER_SECTOR)) {
425 if (buf.dirEntry[i].filename[0] == one_of(0x00, char(0xE5))) {
426 return i;
427 }
428 }
429 return unsigned(-1);
430}
431
432// This function returns the sector and dirIndex for a new directory entry
433// if needed the involved subdirectory is expanded by an extra cluster
434// returns: a DirEntry containing sector and index
435// @throws When either root dir is full or disk is full
436MSXtar::DirEntry MSXtar::addEntryToDir(unsigned sector)
437{
438 // this routine adds the msx name to a directory sector, if needed (and
439 // possible) the directory is extened with an extra cluster
440 DirEntry result;
441 result.sector = sector;
442
443 assert(sector >= rootDirStart);
444 if (sector < dataStart) {
445 // add to the root directory
446 for (/* */ ; result.sector < dataStart; result.sector++) {
447 result.index = findUsableIndexInSector(result.sector);
448 if (result.index != unsigned(-1)) {
449 return result;
450 }
451 }
452 throw MSXException("Root directory full.");
453
454 } else {
455 // add to a subdir
456 while (true) {
457 result.index = findUsableIndexInSector(result.sector);
458 if (result.index != unsigned(-1)) {
459 return result;
460 }
461 unsigned nextSector = getNextSector(result.sector);
462 if (nextSector == 0) {
463 nextSector = appendClusterToSubdir(result.sector);
464 }
465 result.sector = nextSector;
466 }
467 }
468}
469
470// filters out unsupported characters and upper-cases
471static char toFileNameChar(char a)
472{
473 if ((a >= 0x00 && a < 0x20) || a == one_of(
474 ' ', '"', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '[', '\\', ']', '|', 0x7F, 0xFF)) {
475 a = '_';
476 }
477 return narrow<char>(toupper(a));
478}
479
480// Transform a long hostname in a 8.3 uppercase filename as used in the
481// dirEntries on an MSX
482FileName MSXtar::hostToMSXFileName(string_view hostName) const
483{
484 std::vector<uint8_t> hostMSXName = msxChars.utf8ToMsx(hostName, '_');
485 std::string_view hostMSXNameView(reinterpret_cast<char*>(hostMSXName.data()), hostMSXName.size());
486 auto [hostDir, hostFile] = StringOp::splitOnLast(hostMSXNameView, '/');
487
488 // handle special case '.' and '..' first
489 FileName result;
490 result.fill(' ');
491 if (hostFile == one_of(".", "..")) {
492 ranges::copy(hostFile, result);
493 return result;
494 }
495
496 auto [file, ext] = StringOp::splitOnLast(hostFile, '.');
497 if (file.empty()) std::swap(file, ext);
498
499 StringOp::trimRight(file, ' ');
500 StringOp::trimRight(ext, ' ');
501
502 // put in major case and create '_' if needed
503 string fileS(file.data(), std::min<size_t>(8, file.size()));
504 string extS (ext .data(), std::min<size_t>(3, ext .size()));
505 transform_in_place(fileS, toFileNameChar);
506 transform_in_place(extS, toFileNameChar);
507
508 // add correct number of spaces
509 ranges::copy(fileS, subspan<8>(result, 0));
510 ranges::copy(extS, subspan<3>(result, 8));
511 return result;
512}
513
514// This function creates a new MSX subdir with given date 'd' and time 't'
515// in the subdir pointed at by 'sector'. In the newly
516// created subdir the entries for '.' and '..' are created
517// returns: the first sector of the new subdir
518// @throws in case no directory could be created
519unsigned MSXtar::addSubdir(
520 const FileName& msxName, uint16_t t, uint16_t d, unsigned sector)
521{
522 // returns the sector for the first cluster of this subdir
523 DirEntry result = addEntryToDir(sector);
524
525 // load the sector
526 SectorBuffer buf;
527 readLogicalSector(result.sector, buf);
528
529 auto& dirEntry = buf.dirEntry[result.index];
530 ranges::copy(msxName, dirEntry.filename);
531 dirEntry.attrib = MSXDirEntry::Attrib::DIRECTORY;
532 dirEntry.time = t;
533 dirEntry.date = d;
534
535 // dirEntry.filesize = fsize;
536 Cluster curCl = findFirstFreeCluster();
537 setStartCluster(dirEntry, curCl);
538 writeFAT(curCl, EndOfChain{});
539
540 // save the sector again
541 writeLogicalSector(result.sector, buf);
542
543 // clear this cluster
544 unsigned logicalSector = clusterToSector(curCl);
545 ranges::fill(buf.raw, 0);
546 for (auto i : xrange(sectorsPerCluster)) {
547 writeLogicalSector(i + logicalSector, buf);
548 }
549
550 // now add the '.' and '..' entries!!
551 memset(&buf.dirEntry[0], 0, sizeof(MSXDirEntry));
552 ranges::fill(buf.dirEntry[0].filename, ' ');
553 buf.dirEntry[0].filename[0] = '.';
554 buf.dirEntry[0].attrib = MSXDirEntry::Attrib::DIRECTORY;
555 buf.dirEntry[0].time = t;
556 buf.dirEntry[0].date = d;
557 setStartCluster(buf.dirEntry[0], curCl);
558
559 memset(&buf.dirEntry[1], 0, sizeof(MSXDirEntry));
560 ranges::fill(buf.dirEntry[1].filename, ' ');
561 buf.dirEntry[1].filename[0] = '.';
562 buf.dirEntry[1].filename[1] = '.';
563 buf.dirEntry[1].attrib = MSXDirEntry::Attrib::DIRECTORY;
564 buf.dirEntry[1].time = t;
565 buf.dirEntry[1].date = d;
566 if (sector == rootDirStart) {
567 setStartCluster(buf.dirEntry[1], Free{});
568 } else {
569 setStartCluster(buf.dirEntry[1], sectorToCluster(sector));
570 }
571
572 // and save this in the first sector of the new subdir
573 writeLogicalSector(logicalSector, buf);
574
575 return logicalSector;
576}
577
578// Get the time/date from a host file in MSX format
579static DiskImageUtils::FatTimeDate getTimeDate(zstring_view filename)
580{
581 if (auto st = FileOperations::getStat(filename)) {
582 // Some info indicates that st.st_mtime could be useless on win32 with vfat.
583 // On Android 'st_mtime' is 'unsigned long' instead of 'time_t'
584 // (like on linux), so we require a reinterpret_cast. That cast
585 // is fine (but redundant) on linux.
586 return DiskImageUtils::toTimeDate(reinterpret_cast<time_t&>(st->st_mtime));
587 } else {
588 // stat failed
589 return {0, 0};
590 }
591}
592
593// Add an MSXsubdir with the time properties from the HOST-OS subdir
594// @throws when subdir could not be created
595unsigned MSXtar::addSubdirToDSK(zstring_view hostName, const FileName& msxName,
596 unsigned sector)
597{
598 auto [time, date] = getTimeDate(hostName);
599 return addSubdir(msxName, time, date, sector);
600}
601
602// This file alters the filecontent of a given file
603// It only changes the file content (and the filesize in the msxDirEntry)
604// It doesn't changes timestamps nor filename, filetype etc.
605// @throws when something goes wrong
606void MSXtar::alterFileInDSK(MSXDirEntry& msxDirEntry, const string& hostName)
607{
608 // get host file size
609 auto st = FileOperations::getStat(hostName);
610 if (!st) {
611 throw MSXException("Error reading host file: ", hostName);
612 }
613 unsigned hostSize = narrow<unsigned>(st->st_size);
614 unsigned remaining = hostSize;
615
616 // open host file for reading
617 File file(hostName, "rb");
618
619 // copy host file to image
620 DirCluster prevCl = Free{};
621 FatCluster curCl = std::visit(overloaded{
622 [](Free) -> FatCluster { return EndOfChain{}; },
623 [](Cluster cluster) -> FatCluster { return cluster; }
624 }, getStartCluster(msxDirEntry));
625
626 while (remaining) {
627 Cluster cluster;
628 // allocate new cluster if needed
629 try {
630 cluster = std::visit(overloaded{
631 [](Free) -> Cluster { throw new MSXException("Invalid entry in FAT chain."); },
632 [&](EndOfChain) {
633 Cluster newCl = findFirstFreeCluster();
634 std::visit(overloaded{
635 [&](Free) { setStartCluster(msxDirEntry, newCl); },
636 [&](Cluster cluster_) { writeFAT(cluster_, newCl); }
637 }, prevCl);
638 writeFAT(newCl, EndOfChain{});
639 return newCl;
640 },
641 [](Cluster cluster_) { return cluster_; }
642 }, curCl);
643 } catch (MSXException&) {
644 // no more free clusters or invalid entry in FAT chain
645 break;
646 }
647
648 // fill cluster
649 unsigned logicalSector = clusterToSector(cluster);
650 for (unsigned j = 0; (j < sectorsPerCluster) && remaining; ++j) {
651 SectorBuffer buf;
652 unsigned chunkSize = std::min(SECTOR_SIZE, remaining);
653 file.read(subspan(buf.raw, 0, chunkSize));
654 ranges::fill(subspan(buf.raw, chunkSize), 0);
655 writeLogicalSector(logicalSector + j, buf);
656 remaining -= chunkSize;
657 }
658
659 // advance to next cluster
660 prevCl = cluster;
661 curCl = readFAT(cluster);
662 }
663
664 // terminate FAT chain
665 std::visit(overloaded{
666 [&](Free free) { setStartCluster(msxDirEntry, free); },
667 [&](Cluster cluster) { writeFAT(cluster, EndOfChain{}); }
668 }, prevCl);
669
670 // free rest of FAT chain
671 freeFatChain(curCl);
672
673 // write (possibly truncated) file size
674 msxDirEntry.size = hostSize - remaining;
675
676 if (remaining) {
677 throw MSXException("Disk full, ", hostName, " truncated.");
678 }
679}
680
681void MSXtar::freeFatChain(FAT::FatCluster startCluster)
682{
683 FatCluster curCl = startCluster;
684 while (std::holds_alternative<Cluster>(curCl)) {
685 Cluster cluster = std::get<Cluster>(curCl);
686 FatCluster nextCl = readFAT(cluster);
687 writeFAT(cluster, Free{});
688 curCl = nextCl;
689 }
690}
691
692std::string MSXtar::deleteEntry(const FAT::FileName& msxName, unsigned rootSector)
693{
694 SectorBuffer buf;
695 DirEntry entry = findEntryInDir(msxName, rootSector, buf);
696 if (entry.sector == 0) {
697 // not found
698 return "entry not found";
699 }
700 deleteEntry(buf.dirEntry[entry.index]);
701 writeLogicalSector(entry.sector, buf);
702 return "";
703}
704
705void MSXtar::deleteEntry(MSXDirEntry& msxDirEntry)
706{
707 DirCluster startCluster = getStartCluster(msxDirEntry);
708
709 if (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
710 // If we're deleting a directory then also (recursively)
711 // delete the files/directories in this directory.
712 const auto& msxName = msxDirEntry.filename;
713 if (ranges::equal(msxName, std::string_view(". ")) ||
714 ranges::equal(msxName, std::string_view(".. "))) {
715 // But skip the "." and ".." entries.
716 return;
717 }
718 std::visit(overloaded{
719 [](Free) { /* Points to root, ignore. */ },
720 [&](Cluster cluster) { deleteDirectory(clusterToSector(cluster)); }
721 }, startCluster);
722 }
723
724 // At this point we have a regular file or an empty subdirectory.
725 // Delete it by marking the first filename char as 0xE5.
726 msxDirEntry.filename[0] = char(0xE5);
727
728 // Free the FAT chain
729 std::visit(overloaded{
730 [](Free) { /* Points to root, ignore. */ },
731 [&](Cluster cluster) { freeFatChain(cluster); }
732 }, startCluster);
733
734 // Changed sector will be written by parent function
735}
736
737void MSXtar::deleteDirectory(unsigned sector)
738{
739 for (/* */ ; sector != 0; sector = getNextSector(sector)) {
740 SectorBuffer buf;
741 readLogicalSector(sector, buf);
742 for (auto& dirEntry : buf.dirEntry) {
743 if (dirEntry.filename[0] == char(0x00)) {
744 return;
745 }
746 if (dirEntry.filename[0] == one_of(char(0xe5), '.') || dirEntry.attrib == T_MSX_LFN) {
747 continue;
748 }
749 deleteEntry(dirEntry);
750 }
751 writeLogicalSector(sector, buf);
752 }
753}
754
755std::string MSXtar::renameItem(std::string_view currentName, std::string_view newName)
756{
757 SectorBuffer buf;
758
759 FileName newMsxName = hostToMSXFileName(newName);
760 auto newEntry = findEntryInDir(newMsxName, chrootSector, buf);
761 if (newEntry.sector != 0) {
762 return "another entry with new name already exists";
763 }
764
765 FileName oldMsxName = hostToMSXFileName(currentName);
766 auto oldEntry = findEntryInDir(oldMsxName, chrootSector, buf);
767 if (oldEntry.sector == 0) {
768 return "entry not found";
769 }
770
771 buf.dirEntry[oldEntry.index].filename = newMsxName;
772 writeLogicalSector(oldEntry.sector, buf);
773 return "";
774}
775
776// Find the dir entry for 'name' in subdir starting at the given 'sector'
777// with given 'index'
778// returns: a DirEntry with sector and index filled in
779// sector is 0 if no match was found
780MSXtar::DirEntry MSXtar::findEntryInDir(
781 const FileName& msxName, unsigned sector, SectorBuffer& buf)
782{
783 DirEntry result;
784 result.sector = sector;
785 result.index = 0; // avoid warning (only some gcc versions complain)
786 while (result.sector) {
787 // read sector and scan 16 entries
788 readLogicalSector(result.sector, buf);
789 for (result.index = 0; result.index < DIR_ENTRIES_PER_SECTOR; ++result.index) {
790 if (ranges::equal(buf.dirEntry[result.index].filename, msxName)) {
791 return result;
792 }
793 }
794 // try next sector
795 result.sector = getNextSector(result.sector);
796 }
797 return result;
798}
799
800// Add file to the MSX disk in the subdir pointed to by 'sector'
801// @throws when file could not be added
802string MSXtar::addFileToDSK(const string& fullHostName, unsigned rootSector, Add add)
803{
804 auto [directory, hostName] = StringOp::splitOnLast(fullHostName, "/\\");
805 FileName msxName = hostToMSXFileName(hostName);
806
807 // first find out if the filename already exists in current dir
808 SectorBuffer dummy;
809 DirEntry fullMsxDirEntry = findEntryInDir(msxName, rootSector, dummy);
810 if (fullMsxDirEntry.sector != 0) {
811 if (add == Add::PRESERVE) {
812 return strCat("Warning: preserving entry ", hostName, '\n');
813 } else {
814 // first delete entry
815 deleteEntry(dummy.dirEntry[fullMsxDirEntry.index]);
816 writeLogicalSector(fullMsxDirEntry.sector, dummy);
817 }
818 }
819
820 SectorBuffer buf;
821 DirEntry entry = addEntryToDir(rootSector);
822 readLogicalSector(entry.sector, buf);
823 auto& dirEntry = buf.dirEntry[entry.index];
824 memset(&dirEntry, 0, sizeof(dirEntry));
825 ranges::copy(msxName, dirEntry.filename);
826 dirEntry.attrib = MSXDirEntry::Attrib::REGULAR;
827
828 // compute time/date stamps
829 auto [time, date] = getTimeDate(fullHostName);
830 dirEntry.time = time;
831 dirEntry.date = date;
832
833 try {
834 alterFileInDSK(dirEntry, fullHostName);
835 } catch (MSXException&) {
836 // still write directory entry
837 writeLogicalSector(entry.sector, buf);
838 throw;
839 }
840 writeLogicalSector(entry.sector, buf);
841 return {};
842}
843
844std::string MSXtar::addOrCreateSubdir(zstring_view hostDirName, unsigned sector, Add add)
845{
846 FileName msxFileName = hostToMSXFileName(hostDirName);
847 auto printableFilename = msxToHostFileName(msxFileName);
848 SectorBuffer buf;
849 DirEntry entry = findEntryInDir(msxFileName, sector, buf);
850 if (entry.sector != 0) {
851 // entry already exists ..
852 auto& msxDirEntry = buf.dirEntry[entry.index];
853 if (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
854 // .. and is a directory
855 DirCluster nextCluster = getStartCluster(msxDirEntry);
856 return std::visit(overloaded{
857 [&](Free) { return strCat("Directory ", printableFilename, " goes to root.\n"); },
858 [&](Cluster cluster) { return recurseDirFill(hostDirName, clusterToSector(cluster), add); }
859 }, nextCluster);
860 return "";
861 }
862 // .. but is NOT a directory
863 if (add == Add::PRESERVE) {
864 return strCat("MSX file ", printableFilename, " is not a directory.\n");
865 } else {
866 // first delete existing file
867 deleteEntry(msxDirEntry);
868 writeLogicalSector(entry.sector, buf);
869 }
870 }
871 // add new directory
872 unsigned nextSector = addSubdirToDSK(hostDirName, msxFileName, sector);
873 return recurseDirFill(hostDirName, nextSector, add);
874}
875
876// Transfer directory and all its subdirectories to the MSX disk image
877// @throws when an error occurs
878string MSXtar::recurseDirFill(string_view dirName, unsigned sector, Add add)
879{
880 string messages;
881
882 auto fileAction = [&](const string& path) {
883 // add new file
884 messages += addFileToDSK(path, sector, add);
885 };
886 auto dirAction = [&](const string& path) {
887 // add new directory (+ recurse)
888 messages += addOrCreateSubdir(path, sector, add);
889 };
890 foreach_file_and_directory(std::string(dirName), fileAction, dirAction);
891
892 return messages;
893}
894
895
896string MSXtar::msxToHostFileName(const FileName& msxName) const
897{
898 string result;
899 for (unsigned i = 0; i < 8 && msxName[i] != ' '; ++i) {
900 result += char(tolower(msxName[i]));
901 }
902 if (msxName[8] != ' ') {
903 result += '.';
904 for (unsigned i = 8; i < 11 && msxName[i] != ' '; ++i) {
905 result += char(tolower(msxName[i]));
906 }
907 }
908 std::span<const uint8_t> resultSpan(reinterpret_cast<const uint8_t*>(result.data()), result.size());
909 return msxChars.msxToUtf8(resultSpan, '_');
910}
911
912
913// Set the entries from dirEntry to the timestamp of resultFile
914static void changeTime(zstring_view resultFile, const MSXDirEntry& dirEntry)
915{
916 unsigned t = dirEntry.time;
917 unsigned d = dirEntry.date;
918 struct tm mTim;
919 struct utimbuf uTim;
920 mTim.tm_sec = narrow<int>((t & 0x001f) << 1);
921 mTim.tm_min = narrow<int>((t & 0x07e0) >> 5);
922 mTim.tm_hour = narrow<int>((t & 0xf800) >> 11);
923 mTim.tm_mday = narrow<int>( (d & 0x001f));
924 mTim.tm_mon = narrow<int>(((d & 0x01e0) >> 5) - 1);
925 mTim.tm_year = narrow<int>(((d & 0xfe00) >> 9) + 80);
926 mTim.tm_isdst = -1;
927 uTim.actime = mktime(&mTim);
928 uTim.modtime = mktime(&mTim);
929 utime(resultFile.c_str(), &uTim);
930}
931
932TclObject MSXtar::dirRaw()
933{
934 TclObject result;
935 for (unsigned sector = chrootSector; sector != 0; sector = getNextSector(sector)) {
936 SectorBuffer buf;
937 readLogicalSector(sector, buf);
938 for (auto& dirEntry : buf.dirEntry) {
939 if (dirEntry.filename[0] == char(0x00)) {
940 return result;
941 }
942 if (dirEntry.filename[0] == char(0xe5) || dirEntry.attrib == T_MSX_LFN) {
943 continue;
944 }
945
946 auto filename = msxToHostFileName(dirEntry.filename);
947 time_t time = DiskImageUtils::fromTimeDate(DiskImageUtils::FatTimeDate{dirEntry.time, dirEntry.date});
948 result.addListElement(makeTclList(filename, dirEntry.attrib, narrow<uint32_t>(time), dirEntry.size));
949 }
950 }
951 return result;
952}
953
954std::string MSXtar::dir()
955{
956 std::string result;
957 auto list = dirRaw();
958 auto num = list.size();
959 for (unsigned i = 0; i < num; ++i) {
960 auto entry = list.getListIndexUnchecked(i);
961 auto filename = std::string(entry.getListIndexUnchecked(0).getString());
962 auto attrib = DiskImageUtils::formatAttrib(narrow<uint8_t>(entry.getListIndexUnchecked(1).getOptionalInt().value_or(0)));
963 //time_t time = entry.getListIndexUnchecked(2).getOptionalInt().value_or(0); // ignored
964 auto size = entry.getListIndexUnchecked(3).getOptionalInt().value_or(0);
965
966 filename.resize(13, ' '); // filename first (in condensed form for human readability)
967 strAppend(result, filename, attrib, " ", size, '\n');
968 }
969 return result;
970}
971
972// routines to update the global vars: chrootSector
973void MSXtar::chdir(string_view newRootDir)
974{
975 chroot(newRootDir, false);
976}
977
978void MSXtar::mkdir(string_view newRootDir)
979{
980 unsigned tmpMSXchrootSector = chrootSector;
981 chroot(newRootDir, true);
982 chrootSector = tmpMSXchrootSector;
983}
984
985void MSXtar::chroot(string_view newRootDir, bool createDir)
986{
987 if (newRootDir.starts_with('/') || newRootDir.starts_with('\\')) {
988 // absolute path, reset chrootSector
989 chrootSector = rootDirStart;
990 StringOp::trimLeft(newRootDir, "/\\");
991 }
992
993 while (!newRootDir.empty()) {
994 auto [firstPart, lastPart] = StringOp::splitOnFirst(newRootDir, "/\\");
995 newRootDir = lastPart;
996 StringOp::trimLeft(newRootDir, "/\\");
997
998 // find 'firstPart' directory or create it if requested
999 SectorBuffer buf;
1000 FileName msxName = hostToMSXFileName(firstPart);
1001 DirEntry entry = findEntryInDir(msxName, chrootSector, buf);
1002 if (entry.sector == 0) {
1003 if (!createDir) {
1004 throw MSXException("Subdirectory ", firstPart,
1005 " not found.");
1006 }
1007 // creat new subdir
1008 time_t now;
1009 time(&now);
1010 auto [t, d] = DiskImageUtils::toTimeDate(now);
1011 chrootSector = addSubdir(msxName, t, d, chrootSector);
1012 } else {
1013 auto& dirEntry = buf.dirEntry[entry.index];
1014 if (!(dirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY)) {
1015 throw MSXException(firstPart, " is not a directory.");
1016 }
1017 DirCluster chrootCluster = getStartCluster(dirEntry);
1018 chrootSector = std::visit(overloaded{
1019 [this](Free) { return rootDirStart; },
1020 [this](Cluster cluster) { return clusterToSector(cluster); }
1021 }, chrootCluster);
1022 }
1023 }
1024}
1025
1026void MSXtar::fileExtract(const string& resultFile, const MSXDirEntry& dirEntry)
1027{
1028 unsigned size = dirEntry.size;
1029 unsigned sector = std::visit(overloaded{
1030 [](Free) { return 0u; },
1031 [this](Cluster cluster) { return clusterToSector(cluster); }
1032 }, getStartCluster(dirEntry));
1033
1034 File file(resultFile, "wb");
1035 while (size && sector) {
1036 SectorBuffer buf;
1037 readLogicalSector(sector, buf);
1038 unsigned saveSize = std::min(size, SECTOR_SIZE);
1039 file.write(subspan(buf.raw, 0, saveSize));
1040 size -= saveSize;
1041 sector = getNextSector(sector);
1042 }
1043 // now change the access time
1044 changeTime(resultFile, dirEntry);
1045}
1046
1047// extracts a single item (file or directory) from the msx image to the host OS
1048string MSXtar::singleItemExtract(string_view dirName, string_view itemName,
1049 unsigned sector)
1050{
1051 // first find out if the filename exists in current dir
1052 SectorBuffer buf;
1053 FileName msxName = hostToMSXFileName(itemName);
1054 DirEntry entry = findEntryInDir(msxName, sector, buf);
1055 if (entry.sector == 0) {
1056 return strCat(itemName, " not found!\n");
1057 }
1058
1059 auto& msxDirEntry = buf.dirEntry[entry.index];
1060 // create full name for local filesystem
1061 string fullName = strCat(dirName, '/', msxToHostFileName(msxDirEntry.filename));
1062
1063 // ...and extract
1064 if (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
1065 // recursive extract this subdir
1066 FileOperations::mkdirp(fullName);
1067 DirCluster nextCluster = getStartCluster(msxDirEntry);
1068 std::visit(overloaded{
1069 [](Free) { /* Points to root, ignore. */},
1070 [&](Cluster cluster) { recurseDirExtract(fullName, clusterToSector(cluster)); }
1071 }, nextCluster);
1072 } else {
1073 // it is a file
1074 fileExtract(fullName, msxDirEntry);
1075 }
1076 return {};
1077}
1078
1079
1080// extracts the contents of the directory (at sector) and all its subdirs to the host OS
1081void MSXtar::recurseDirExtract(string_view dirName, unsigned sector)
1082{
1083 for (/* */ ; sector != 0; sector = getNextSector(sector)) {
1084 SectorBuffer buf;
1085 readLogicalSector(sector, buf);
1086 for (auto& dirEntry : buf.dirEntry) {
1087 if (dirEntry.filename[0] == char(0x00)) {
1088 return;
1089 }
1090 if (dirEntry.filename[0] == one_of(char(0xe5), '.') || dirEntry.attrib == T_MSX_LFN) {
1091 continue;
1092 }
1093 string filename = msxToHostFileName(dirEntry.filename);
1094 string fullName = filename;
1095 if (!dirName.empty()) {
1096 fullName = strCat(dirName, '/', filename);
1097 }
1098 if (dirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
1099 FileOperations::mkdirp(fullName);
1100 // now change the access time
1101 changeTime(fullName, dirEntry);
1102 std::visit(overloaded{
1103 [](Free) { /* Points to root, ignore. */ },
1104 [&](Cluster cluster) { recurseDirExtract(fullName, clusterToSector(cluster)); }
1105 }, getStartCluster(dirEntry));
1106 } else {
1107 fileExtract(fullName, dirEntry);
1108 }
1109 }
1110 }
1111}
1112
1113std::string MSXtar::addItem(const std::string& hostItemName, Add add)
1114{
1115 if (auto stat = FileOperations::getStat(hostItemName)) {
1116 if (FileOperations::isRegularFile(*stat)) {
1117 return addFileToDSK(hostItemName, chrootSector, add);
1118 } else if (FileOperations::isDirectory(hostItemName)) {
1119 return addOrCreateSubdir(hostItemName, chrootSector, add);
1120 }
1121 }
1122 return "";
1123}
1124
1125string MSXtar::addDir(string_view rootDirName, Add add)
1126{
1127 return recurseDirFill(rootDirName, chrootSector, add);
1128}
1129
1130string MSXtar::addFile(const string& filename, Add add)
1131{
1132 return addFileToDSK(filename, chrootSector, add);
1133}
1134
1135string MSXtar::getItemFromDir(string_view rootDirName, string_view itemName)
1136{
1137 return singleItemExtract(rootDirName, itemName, chrootSector);
1138}
1139
1140void MSXtar::getDir(string_view rootDirName)
1141{
1142 recurseDirExtract(rootDirName, chrootSector);
1143}
1144
1145std::string MSXtar::deleteItem(std::string_view itemName)
1146{
1147 FileName msxName = hostToMSXFileName(itemName);
1148 return deleteEntry(msxName, chrootSector);
1149}
1150
1151std::string MSXtar::convertToMsxName(std::string_view name) const
1152{
1153 FileName msxName = hostToMSXFileName(name);
1154 return msxToHostFileName(msxName);
1155}
1156
1157MSXtar::FreeSpaceResult MSXtar::getFreeSpace() const
1158{
1159 return {countFreeClusters(), sectorsPerCluster * SECTOR_SIZE};
1160}
1161
1162} // namespace openmsx
TclObject t
MSXtar(SectorAccessibleDisk &disk, const MsxChar2Unicode &msxChars_)
Definition MSXtar.cc:194
void readSector(size_t sector, SectorBuffer &buf)
void readSectors(std::span< SectorBuffer > buffers, size_t startSector)
void writeSector(size_t sector, const SectorBuffer &buf)
void addListElement(const T &t)
Definition TclObject.hh:127
Like std::string_view, but with the extra guarantee that it refers to a zero-terminated string.
constexpr auto size() const
constexpr const char * c_str() const
std::pair< string_view, string_view > splitOnLast(string_view str, string_view chars)
Definition StringOp.cc:108
void trimRight(string &str, const char *chars)
Definition StringOp.cc:29
std::pair< string_view, string_view > splitOnFirst(string_view str, string_view chars)
Definition StringOp.cc:91
void trimLeft(string &str, const char *chars)
Definition StringOp.cc:58
struct openmsx::FAT12::ToClusterNumber toClusterNumber
struct openmsx::FAT16::ToClusterNumber toClusterNumber
std::variant< Free, Cluster > DirCluster
Definition MSXtar.hh:38
std::variant< Free, EndOfChain, Cluster > FatCluster
Definition MSXtar.hh:39
decltype(MSXDirEntry::filename) FileName
Definition MSXtar.hh:41
This file implemented 3 utility functions:
Definition Autofire.cc:9
TclObject makeTclList(Args &&... args)
Definition TclObject.hh:289
bool foreach_file_and_directory(std::string path, FileAction fileAction, DirAction dirAction)
constexpr void fill(ForwardRange &&range, const T &value)
Definition ranges.hh:305
auto copy(InputRange &&range, OutputIter out)
Definition ranges.hh:250
bool equal(InputRange1 &&range1, InputRange2 &&range2, Pred pred={}, Proj1 proj1={}, Proj2 proj2={})
Definition ranges.hh:368
auto count_if(InputRange &&range, UnaryPredicate pred)
Definition ranges.hh:345
size_t size(std::string_view utf8)
constexpr auto subspan(Range &&range, size_t offset, size_t count=std::dynamic_extent)
Definition ranges.hh:471
auto transform_in_place(ForwardRange &&range, UnaryOperation op)
Definition stl.hh:196
std::string strCat()
Definition strCat.hh:703
void strAppend(std::string &result, Ts &&...ts)
Definition strCat.hh:752
unsigned operator()(Free) const
Definition MSXtar.cc:57
unsigned operator()(Cluster cluster) const
Definition MSXtar.cc:59
unsigned operator()(EndOfChain) const
Definition MSXtar.cc:58
unsigned operator()(EndOfChain) const
Definition MSXtar.cc:72
unsigned operator()(Cluster cluster) const
Definition MSXtar.cc:73
unsigned operator()(Free) const
Definition MSXtar.cc:71
std::array< MSXDirEntry, 16 > dirEntry
constexpr auto xrange(T e)
Definition xrange.hh:132