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