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