openMSX
DirAsDSK.cc
Go to the documentation of this file.
1#include "DirAsDSK.hh"
2#include "DiskChanger.hh"
3#include "Scheduler.hh"
4#include "CliComm.hh"
5#include "BootBlocks.hh"
6#include "File.hh"
7#include "FileException.hh"
8#include "ReadDir.hh"
9#include "StringOp.hh"
10#include "one_of.hh"
11#include "ranges.hh"
12#include "stl.hh"
13#include "xrange.hh"
14#include <cassert>
15#include <cstring>
16#include <vector>
17
18using std::string;
19using std::vector;
20
21namespace openmsx {
22
23constexpr unsigned SECTOR_SIZE = sizeof(SectorBuffer);
24constexpr unsigned SECTORS_PER_DIR = 7;
25constexpr unsigned NUM_FATS = 2;
26constexpr unsigned NUM_TRACKS = 80;
27constexpr unsigned SECTORS_PER_CLUSTER = 2;
28constexpr unsigned SECTORS_PER_TRACK = 9;
29constexpr unsigned FIRST_FAT_SECTOR = 1;
30constexpr unsigned DIR_ENTRIES_PER_SECTOR =
31 SECTOR_SIZE / sizeof(MSXDirEntry);
32
33// First valid regular cluster number.
34constexpr unsigned FIRST_CLUSTER = 2;
35
36constexpr unsigned FREE_FAT = 0x000;
37constexpr unsigned BAD_FAT = 0xFF7;
38constexpr unsigned EOF_FAT = 0xFFF; // actually 0xFF8-0xFFF
39
40
41// Transform BAD_FAT (0xFF7) and EOF_FAT-range (0xFF8-0xFFF)
42// to a single value: EOF_FAT (0xFFF).
43[[nodiscard]] static constexpr unsigned normalizeFAT(unsigned cluster)
44{
45 return (cluster < BAD_FAT) ? cluster : EOF_FAT;
46}
47
48unsigned DirAsDSK::readFATHelper(const SectorBuffer* fatBuf, unsigned cluster) const
49{
50 assert(FIRST_CLUSTER <= cluster);
51 assert(cluster < maxCluster);
52 const auto* buf = fatBuf[0].raw;
53 const auto* p = &buf[(cluster * 3) / 2];
54 unsigned result = (cluster & 1)
55 ? (p[0] >> 4) + (p[1] << 4)
56 : p[0] + ((p[1] & 0x0F) << 8);
57 return normalizeFAT(result);
58}
59
60void DirAsDSK::writeFATHelper(SectorBuffer* fatBuf, unsigned cluster, unsigned val) const
61{
62 assert(FIRST_CLUSTER <= cluster);
63 assert(cluster < maxCluster);
64 auto* buf = fatBuf[0].raw;
65 auto* p = &buf[(cluster * 3) / 2];
66 if (cluster & 1) {
67 p[0] = (p[0] & 0x0F) + (val << 4);
68 p[1] = val >> 4;
69 } else {
70 p[0] = val;
71 p[1] = (p[1] & 0xF0) + ((val >> 8) & 0x0F);
72 }
73}
74
75SectorBuffer* DirAsDSK::fat()
76{
77 return &sectors[FIRST_FAT_SECTOR];
78}
79SectorBuffer* DirAsDSK::fat2()
80{
81 return &sectors[firstSector2ndFAT];
82}
83
84// Read entry from FAT.
85unsigned DirAsDSK::readFAT(unsigned cluster)
86{
87 return readFATHelper(fat(), cluster);
88}
89
90// Write an entry to both FAT1 and FAT2.
91void DirAsDSK::writeFAT12(unsigned cluster, unsigned val)
92{
93 writeFATHelper(fat (), cluster, val);
94 writeFATHelper(fat2(), cluster, val);
95 // An alternative is to copy FAT1 to FAT2 after changes have been made
96 // to FAT1. This is probably more like what the real disk rom does.
97}
98
99// Returns maxCluster in case of no more free clusters
100unsigned DirAsDSK::findNextFreeCluster(unsigned cluster)
101{
102 assert(cluster < maxCluster);
103 do {
104 ++cluster;
105 assert(cluster >= FIRST_CLUSTER);
106 } while ((cluster < maxCluster) && (readFAT(cluster) != FREE_FAT));
107 return cluster;
108}
109unsigned DirAsDSK::findFirstFreeCluster()
110{
111 return findNextFreeCluster(FIRST_CLUSTER - 1);
112}
113
114// Throws when there are no more free clusters.
115unsigned DirAsDSK::getFreeCluster()
116{
117 unsigned cluster = findFirstFreeCluster();
118 if (cluster == maxCluster) {
119 throw MSXException("disk full");
120 }
121 return cluster;
122}
123
124unsigned DirAsDSK::clusterToSector(unsigned cluster) const
125{
126 assert(cluster >= FIRST_CLUSTER);
127 assert(cluster < maxCluster);
128 return firstDataSector + SECTORS_PER_CLUSTER *
129 (cluster - FIRST_CLUSTER);
130}
131
132std::pair<unsigned, unsigned> DirAsDSK::sectorToClusterOffset(unsigned sector) const
133{
134 assert(sector >= firstDataSector);
135 assert(sector < nofSectors);
136 sector -= firstDataSector;
137 unsigned cluster = (sector / SECTORS_PER_CLUSTER) + FIRST_CLUSTER;
138 unsigned offset = (sector % SECTORS_PER_CLUSTER) * SECTOR_SIZE;
139 return {cluster, offset};
140}
141unsigned DirAsDSK::sectorToCluster(unsigned sector) const
142{
143 auto [cluster, offset] = sectorToClusterOffset(sector);
144 return cluster;
145}
146
147MSXDirEntry& DirAsDSK::msxDir(DirIndex dirIndex)
148{
149 assert(dirIndex.sector < nofSectors);
150 assert(dirIndex.idx < DIR_ENTRIES_PER_SECTOR);
151 return sectors[dirIndex.sector].dirEntry[dirIndex.idx];
152}
153
154// Returns -1 when there are no more sectors for this directory.
155unsigned DirAsDSK::nextMsxDirSector(unsigned sector)
156{
157 if (sector < firstDataSector) {
158 // Root directory.
159 assert(firstDirSector <= sector);
160 ++sector;
161 if (sector == firstDataSector) {
162 // Root directory has a fixed number of sectors.
163 return unsigned(-1);
164 }
165 return sector;
166 } else {
167 // Subdirectory.
168 auto [cluster, offset] = sectorToClusterOffset(sector);
169 if (offset < ((SECTORS_PER_CLUSTER - 1) * SECTOR_SIZE)) {
170 // Next sector still in same cluster.
171 return sector + 1;
172 }
173 unsigned nextCl = readFAT(cluster);
174 if ((nextCl < FIRST_CLUSTER) || (maxCluster <= nextCl)) {
175 // No next cluster, end of directory reached.
176 return unsigned(-1);
177 }
178 return clusterToSector(nextCl);
179 }
180}
181
182// Check if a msx filename is used in a specific msx (sub)directory.
183bool DirAsDSK::checkMSXFileExists(
184 const string& msxFilename, unsigned msxDirSector)
185{
186 vector<bool> visited(nofSectors, false);
187 do {
188 if (visited[msxDirSector]) {
189 // cycle detected, invalid disk, but don't crash on it
190 return false;
191 }
192 visited[msxDirSector] = true;
193
194 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
195 DirIndex dirIndex(msxDirSector, idx);
196 if (memcmp(msxDir(dirIndex).filename,
197 msxFilename.data(), 8 + 3) == 0) {
198 return true;
199 }
200 }
201 msxDirSector = nextMsxDirSector(msxDirSector);
202 } while (msxDirSector != unsigned(-1));
203
204 // Searched through all sectors of this (sub)directory.
205 return false;
206}
207
208// Returns msx directory entry for the given host file. Or -1 if the host file
209// is not mapped in the virtual disk.
210DirAsDSK::DirIndex DirAsDSK::findHostFileInDSK(std::string_view hostName)
211{
212 for (const auto& [dirIdx, mapDir] : mapDirs) {
213 if (mapDir.hostName == hostName) {
214 return dirIdx;
215 }
216 }
217 return {unsigned(-1), unsigned(-1)};
218}
219
220// Check if a host file is already mapped in the virtual disk.
221bool DirAsDSK::checkFileUsedInDSK(std::string_view hostName)
222{
223 DirIndex dirIndex = findHostFileInDSK(hostName);
224 return dirIndex.sector != unsigned(-1);
225}
226
227static string hostToMsxName(string hostName)
228{
229 // Create an MSX filename 8.3 format. TODO use vfat-like abbreviation
230 transform_in_place(hostName, [](char a) {
231 return (a == ' ') ? '_' : ::toupper(a);
232 });
233 auto [file, ext] = StringOp::splitOnLast(hostName, '.');
234 if (file.empty()) std::swap(file, ext);
235
236 string result(8 + 3, ' ');
237 memcpy(result.data() + 0, file.data(), std::min<size_t>(8, file.size()));
238 memcpy(result.data() + 8, ext .data(), std::min<size_t>(3, ext .size()));
239 ranges::replace(result, '.', '_');
240 return result;
241}
242
243static string msxToHostName(const char* msxName)
244{
245 string result;
246 for (unsigned i = 0; (i < 8) && (msxName[i] != ' '); ++i) {
247 result += char(tolower(msxName[i]));
248 }
249 if (msxName[8] != ' ') {
250 result += '.';
251 for (unsigned i = 8; (i < (8 + 3)) && (msxName[i] != ' '); ++i) {
252 result += char(tolower(msxName[i]));
253 }
254 }
255 return result;
256}
257
258
259DirAsDSK::DirAsDSK(DiskChanger& diskChanger_, CliComm& cliComm_,
260 const Filename& hostDir_, SyncMode syncMode_,
261 BootSectorType bootSectorType)
262 : SectorBasedDisk(hostDir_)
263 , diskChanger(diskChanger_)
264 , cliComm(cliComm_)
265 , hostDir(FileOperations::expandTilde(hostDir_.getResolved() + '/'))
266 , syncMode(syncMode_)
267 , lastAccess(EmuTime::zero())
268 , nofSectors((diskChanger_.isDoubleSidedDrive() ? 2 : 1) * SECTORS_PER_TRACK * NUM_TRACKS)
269 , nofSectorsPerFat((((3 * nofSectors) / (2 * SECTORS_PER_CLUSTER)) + SECTOR_SIZE - 1) / SECTOR_SIZE)
270 , firstSector2ndFAT(FIRST_FAT_SECTOR + nofSectorsPerFat)
271 , firstDirSector(FIRST_FAT_SECTOR + NUM_FATS * nofSectorsPerFat)
272 , firstDataSector(firstDirSector + SECTORS_PER_DIR)
273 , maxCluster((nofSectors - firstDataSector) / SECTORS_PER_CLUSTER + FIRST_CLUSTER)
274 , sectors(nofSectors)
275{
276 if (!FileOperations::isDirectory(hostDir)) {
277 throw MSXException("Not a directory");
278 }
279
280 // First create structure for the virtual disk.
281 byte numSides = diskChanger_.isDoubleSidedDrive() ? 2 : 1;
282 setNbSectors(nofSectors);
284 setNbSides(numSides);
285
286 // Initially the whole disk is filled with 0xE5 (at least on Philips
287 // NMS8250).
288 memset(sectors.data(), 0xE5, sizeof(SectorBuffer) * nofSectors);
289
290 // Use selected bootsector, fill-in values.
291 byte mediaDescriptor = (numSides == 2) ? 0xF9 : 0xF8;
292 const auto& protoBootSector = bootSectorType == BOOTSECTOR_DOS1
295 memcpy(&sectors[0], &protoBootSector, sizeof(protoBootSector));
296 auto& bootSector = sectors[0].bootSector;
297 bootSector.bpSector = SECTOR_SIZE;
298 bootSector.spCluster = SECTORS_PER_CLUSTER;
299 bootSector.nrFats = NUM_FATS;
300 bootSector.dirEntries = SECTORS_PER_DIR * (SECTOR_SIZE / sizeof(MSXDirEntry));
301 bootSector.nrSectors = nofSectors;
302 bootSector.descriptor = mediaDescriptor;
303 bootSector.sectorsFat = nofSectorsPerFat;
304 bootSector.sectorsTrack = SECTORS_PER_TRACK;
305 bootSector.nrSides = numSides;
306
307 // Clear FAT1 + FAT2.
308 memset(fat(), 0, SECTOR_SIZE * nofSectorsPerFat * NUM_FATS);
309 // First 3 bytes are initialized specially:
310 // 'cluster 0' contains the media descriptor
311 // 'cluster 1' is marked as EOF_FAT
312 // So cluster 2 is the first usable cluster number
313 fat ()->raw[0] = mediaDescriptor; fat ()->raw[1] = 0xFF; fat ()->raw[2] = 0xFF;
314 fat2()->raw[0] = mediaDescriptor; fat2()->raw[1] = 0xFF; fat2()->raw[2] = 0xFF;
315
316 // Assign empty directory entries.
317 memset(&sectors[firstDirSector], 0, SECTOR_SIZE * SECTORS_PER_DIR);
318
319 // No host files are mapped to this disk yet.
320 assert(mapDirs.empty());
321
322 // Import the host filesystem.
323 syncWithHost();
324}
325
327{
328 return syncMode == SYNC_READONLY;
329}
330
332{
333 // For simplicity, for now, always report content has (possibly) changed
334 // (in the future we could try to detect that no host files have
335 // actually changed).
336 // The effect is that the MSX always see a 'disk-changed-signal'.
337 // This fixes: https://github.com/openMSX/openMSX/issues/1410
338 return true;
339}
340
342{
343 bool needSync = [&] {
344 if (auto* scheduler = diskChanger.getScheduler()) {
345 auto now = scheduler->getCurrentTime();
346 auto delta = now - lastAccess;
347 return delta > EmuDuration::sec(1);
348 // Do not update lastAccess because we don't actually call
349 // syncWithHost().
350 } else {
351 // Happens when dirasdisk is used in virtual_drive.
352 return true;
353 }
354 }();
355 if (needSync) {
356 flushCaches();
357 }
358}
359
360void DirAsDSK::readSectorImpl(size_t sector, SectorBuffer& buf)
361{
362 assert(sector < nofSectors);
363
364 // 'Peek-mode' is used to periodically calculate a sha1sum for the
365 // whole disk (used by reverse). We don't want this calculation to
366 // interfer with the access time we use for normal read/writes. So in
367 // peek-mode we skip the whole sync-step.
368 if (!isPeekMode()) {
369 bool needSync = [&] {
370 if (auto* scheduler = diskChanger.getScheduler()) {
371 auto now = scheduler->getCurrentTime();
372 auto delta = now - lastAccess;
373 lastAccess = now;
374 return delta > EmuDuration::sec(1);
375 } else {
376 // Happens when dirasdisk is used in virtual_drive.
377 return true;
378 }
379 }();
380 if (needSync) {
381 syncWithHost();
382 flushCaches(); // e.g. sha1sum
383 // Let the diskdrive report the disk has been ejected.
384 // E.g. a turbor machine uses this to flush its
385 // internal disk caches.
386 diskChanger.forceDiskChange(); // maybe redundant now? (see hasChanged()).
387 }
388 }
389
390 // Simply return the sector from our virtual disk image.
391 memcpy(&buf, &sectors[sector], sizeof(buf));
392}
393
394void DirAsDSK::syncWithHost()
395{
396 // Check for removed host files. This frees up space in the virtual
397 // disk. Do this first because otherwise later actions may fail (run
398 // out of virtual disk space) for no good reason.
399 checkDeletedHostFiles();
400
401 // Next update existing files. This may enlarge or shrink virtual
402 // files. In case not all host files fit on the virtual disk it's
403 // better to update the existing files than to (partly) add a too big
404 // new file and have no space left to enlarge the existing files.
405 checkModifiedHostFiles();
406
407 // Last add new host files (this can only consume virtual disk space).
408 addNewHostFiles({}, firstDirSector);
409}
410
411void DirAsDSK::checkDeletedHostFiles()
412{
413 // This handles both host files and directories.
414 auto copy = mapDirs;
415 for (const auto& [dirIdx, mapDir] : copy) {
416 if (!mapDirs.contains(dirIdx)) {
417 // While iterating over (the copy of) mapDirs we delete
418 // entries of mapDirs (when we delete files only the
419 // current entry is deleted, when we delete
420 // subdirectories possibly many entries are deleted).
421 // At this point in the code we've reached such an
422 // entry that's still in the original (copy of) mapDirs
423 // but has already been deleted from the current
424 // mapDirs. Ignore it.
425 continue;
426 }
427 auto fullHostName = tmpStrCat(hostDir, mapDir.hostName);
428 bool isMSXDirectory = (msxDir(dirIdx).attrib &
431 if ((!FileOperations::getStat(fullHostName, fst)) ||
432 (FileOperations::isDirectory(fst) != isMSXDirectory)) {
433 // TODO also check access permission
434 // Error stat-ing file, or directory/file type is not
435 // the same on the msx and host side (e.g. a host file
436 // has been removed and a host directory with the same
437 // name has been created). In both cases delete the msx
438 // entry (if needed it will be recreated soon).
439 deleteMSXFile(dirIdx);
440 }
441 }
442}
443
444void DirAsDSK::deleteMSXFile(DirIndex dirIndex)
445{
446 // Remove mapping between host and msx file (if any).
447 mapDirs.erase(dirIndex);
448
449 char c = msxDir(dirIndex).filename[0];
450 if (c == one_of(0, char(0xE5))) {
451 // Directory entry not in use, don't need to do anything.
452 return;
453 }
454
455 if (msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY) {
456 // If we're deleting a directory then also (recursively)
457 // delete the files/directories in this directory.
458 const char* msxName = msxDir(dirIndex).filename;
459 if ((memcmp(msxName, ". ", 11) == 0) ||
460 (memcmp(msxName, ".. ", 11) == 0)) {
461 // But skip the "." and ".." entries.
462 return;
463 }
464 // Sanity check on cluster range.
465 unsigned cluster = msxDir(dirIndex).startCluster;
466 if ((FIRST_CLUSTER <= cluster) && (cluster < maxCluster)) {
467 // Recursively delete all files in this subdir.
468 deleteMSXFilesInDir(clusterToSector(cluster));
469 }
470 }
471
472 // At this point we have a regular file or an empty subdirectory.
473 // Delete it by marking the first filename char as 0xE5.
474 msxDir(dirIndex).filename[0] = char(0xE5);
475
476 // Clear the FAT chain to free up space in the virtual disk.
477 freeFATChain(msxDir(dirIndex).startCluster);
478}
479
480void DirAsDSK::deleteMSXFilesInDir(unsigned msxDirSector)
481{
482 vector<bool> visited(nofSectors, false);
483 do {
484 if (visited[msxDirSector]) {
485 // cycle detected, invalid disk, but don't crash on it
486 return;
487 }
488 visited[msxDirSector] = true;
489
490 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
491 deleteMSXFile(DirIndex(msxDirSector, idx));
492 }
493 msxDirSector = nextMsxDirSector(msxDirSector);
494 } while (msxDirSector != unsigned(-1));
495}
496
497void DirAsDSK::freeFATChain(unsigned cluster)
498{
499 // Follow a FAT chain and mark all clusters on this chain as free.
500 while ((FIRST_CLUSTER <= cluster) && (cluster < maxCluster)) {
501 unsigned nextCl = readFAT(cluster);
502 writeFAT12(cluster, FREE_FAT);
503 cluster = nextCl;
504 }
505}
506
507void DirAsDSK::checkModifiedHostFiles()
508{
509 auto copy = mapDirs;
510 for (const auto& [dirIdx, mapDir] : copy) {
511 if (!mapDirs.contains(dirIdx)) {
512 // See comment in checkDeletedHostFiles().
513 continue;
514 }
515 auto fullHostName = tmpStrCat(hostDir, mapDir.hostName);
516 bool isMSXDirectory = (msxDir(dirIdx).attrib &
519 if (FileOperations::getStat(fullHostName, fst) &&
520 (FileOperations::isDirectory(fst) == isMSXDirectory)) {
521 // Detect changes in host file.
522 // Heuristic: we use filesize and modification time to detect
523 // changes in file content.
524 // TODO do we need both filesize and mtime or is mtime alone
525 // enough?
526 // We ignore time/size changes in directories,
527 // typically such a change indicates one of the files
528 // in that directory is changed/added/removed. But such
529 // changes are handled elsewhere.
530 if (!isMSXDirectory &&
531 ((mapDir.mtime != fst.st_mtime) ||
532 (mapDir.filesize != size_t(fst.st_size)))) {
533 importHostFile(dirIdx, fst);
534 }
535 } else {
536 // Only very rarely happens (because checkDeletedHostFiles()
537 // checked this just recently).
538 deleteMSXFile(dirIdx);
539 }
540 }
541}
542
543void DirAsDSK::importHostFile(DirIndex dirIndex, FileOperations::Stat& fst)
544{
545 assert(!(msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY));
546 assert(mapDirs.contains(dirIndex));
547
548 // Set _msx_ modification time.
549 setMSXTimeStamp(dirIndex, fst);
550
551 // Set _host_ modification time (and filesize)
552 // Note: this is the _only_ place where we update the mapdir.mtime
553 // field. We do _not_ update it when the msx writes to the file. So in
554 // case the msx does write a data sector, it writes to the correct
555 // offset in the host file, but mtime is not updated. On the next
556 // host->emu sync we will resync the full file content and only then
557 // update mtime. This may seem inefficient, but it makes sure that we
558 // never miss any file changes performed by the host. E.g. the host
559 // changed part of the file short before the msx wrote to the same
560 // file. We can never guarantee that such race-scenarios work
561 // correctly, but at least with this mtime-update convention the file
562 // content will be the same on the host and the msx side after every
563 // sync.
564 size_t hostSize = fst.st_size;
565 auto& mapDir = mapDirs[dirIndex];
566 mapDir.filesize = hostSize;
567 mapDir.mtime = fst.st_mtime;
568
569 bool moreClustersInChain = true;
570 unsigned curCl = msxDir(dirIndex).startCluster;
571 // If there is no cluster assigned yet to this file (curCl == 0), then
572 // find a free cluster. Treat invalid cases in the same way (curCl == 1
573 // or curCl >= maxCluster).
574 if ((curCl < FIRST_CLUSTER) || (curCl >= maxCluster)) {
575 moreClustersInChain = false;
576 curCl = findFirstFreeCluster(); // maxCluster in case of disk-full
577 }
578
579 auto remainingSize = hostSize;
580 unsigned prevCl = 0;
581 try {
582 File file(hostDir + mapDir.hostName,
583 "rb"); // don't uncompress
584
585 while (remainingSize && (curCl < maxCluster)) {
586 unsigned logicalSector = clusterToSector(curCl);
587 for (auto i : xrange(SECTORS_PER_CLUSTER)) {
588 unsigned sector = logicalSector + i;
589 assert(sector < nofSectors);
590 auto* buf = &sectors[sector];
591 memset(buf, 0, SECTOR_SIZE); // in case (end of) file only fills partial sector
592 auto sz = std::min(remainingSize, SECTOR_SIZE);
593 file.read(buf, sz);
594 remainingSize -= sz;
595 if (remainingSize == 0) {
596 // Don't fill next sectors in this cluster
597 // if there is no data left.
598 break;
599 }
600 }
601
602 if (prevCl) {
603 writeFAT12(prevCl, curCl);
604 } else {
605 msxDir(dirIndex).startCluster = curCl;
606 }
607 prevCl = curCl;
608
609 // Check if we can follow the existing FAT chain or
610 // need to allocate a free cluster.
611 if (moreClustersInChain) {
612 curCl = readFAT(curCl);
613 if ((curCl == EOF_FAT) || // normal end
614 (curCl < FIRST_CLUSTER) || // invalid
615 (curCl >= maxCluster)) { // invalid
616 // Treat invalid FAT chain the same as
617 // a normal EOF_FAT.
618 moreClustersInChain = false;
619 curCl = findFirstFreeCluster();
620 }
621 } else {
622 curCl = findNextFreeCluster(curCl);
623 }
624 }
625 if (remainingSize != 0) {
626 cliComm.printWarning("Virtual diskimage full: ",
627 mapDir.hostName, " truncated.");
628 }
629 } catch (FileException& e) {
630 // Error opening or reading host file.
631 cliComm.printWarning("Error reading host file: ",
632 mapDir.hostName, ": ", e.getMessage(),
633 " Truncated file on MSX disk.");
634 }
635
636 // In all cases (no error / image full / host read error) we need to
637 // properly terminate the FAT chain.
638 if (prevCl) {
639 writeFAT12(prevCl, EOF_FAT);
640 } else {
641 // Filesize zero: don't allocate any cluster, write zero
642 // cluster number (checked on a MSXTurboR, DOS2 mode).
643 msxDir(dirIndex).startCluster = FREE_FAT;
644 }
645
646 // Clear remains of FAT if needed.
647 if (moreClustersInChain) {
648 freeFATChain(curCl);
649 }
650
651 // Write (possibly truncated) file size.
652 msxDir(dirIndex).size = uint32_t(hostSize - remainingSize);
653
654 // TODO in case of an error (disk image full, or host file read error),
655 // wouldn't it be better to remove the (half imported) msx file again?
656 // Sometimes when I'm using DirAsDSK I have one file that is too big
657 // (e.g. a core file, or a vim .swp file) and that one prevents
658 // DirAsDSK from importing the other (small) files in my directory.
659}
660
661void DirAsDSK::setMSXTimeStamp(DirIndex dirIndex, FileOperations::Stat& fst)
662{
663 // Use intermediate param to prevent compilation error for Android
664 time_t mtime = fst.st_mtime;
665 auto* mtim = localtime(&mtime);
666 int t1 = mtim ? (mtim->tm_sec >> 1) + (mtim->tm_min << 5) +
667 (mtim->tm_hour << 11)
668 : 0;
669 msxDir(dirIndex).time = t1;
670 int t2 = mtim ? mtim->tm_mday + ((mtim->tm_mon + 1) << 5) +
671 ((mtim->tm_year + 1900 - 1980) << 9)
672 : 0;
673 msxDir(dirIndex).date = t2;
674}
675
676// Used to add 'regular' files before 'derived' files. E.g. when editing a file
677// in a host editor, you often get backup/swap files like this:
678// myfile.txt myfile.txt~ .myfile.txt.swp
679// Currently the 1st and 2nd are mapped to the same MSX filename. If more
680// host files map to the same MSX file then (currently) one of the two is
681// ignored. Which one is ignored depends on the order in which they are added
682// to the virtual disk. This routine/heuristic tries to add 'regular' files
683// before derived files.
684static size_t weight(const string& hostName)
685{
686 // TODO this weight function can most likely be improved
687 size_t result = 0;
688 auto [file, ext] = StringOp::splitOnLast(hostName, '.');
689 // too many '.' characters
690 result += ranges::count(file, '.') * 100;
691 // too long extension
692 result += ext.size() * 10;
693 // too long file
694 result += file.size();
695 return result;
696}
697
698void DirAsDSK::addNewHostFiles(const string& hostSubDir, unsigned msxDirSector)
699{
700 assert(!hostSubDir.starts_with('/'));
701 assert(hostSubDir.empty() || hostSubDir.ends_with('/'));
702
703 vector<string> hostNames;
704 {
705 ReadDir dir(tmpStrCat(hostDir, hostSubDir));
706 while (auto* d = dir.getEntry()) {
707 hostNames.emplace_back(d->d_name);
708 }
709 }
710 ranges::sort(hostNames, {}, [](const string& n) { return weight(n); });
711
712 for (auto& hostName : hostNames) {
713 try {
714 if (hostName.starts_with('.')) {
715 // skip '.' and '..'
716 // also skip hidden files on unix
717 continue;
718 }
719 auto fullHostName = tmpStrCat(hostDir, hostSubDir, hostName);
721 if (!FileOperations::getStat(fullHostName, fst)) {
722 throw MSXException("Error accessing ", fullHostName);
723 }
725 addNewDirectory(hostSubDir, hostName, msxDirSector, fst);
726 } else if (FileOperations::isRegularFile(fst)) {
727 addNewHostFile(hostSubDir, hostName, msxDirSector, fst);
728 } else {
729 throw MSXException("Not a regular file: ", fullHostName);
730 }
731 } catch (MSXException& e) {
732 cliComm.printWarning(e.getMessage());
733 }
734 }
735}
736
737void DirAsDSK::addNewDirectory(const string& hostSubDir, const string& hostName,
738 unsigned msxDirSector, FileOperations::Stat& fst)
739{
740 DirIndex dirIndex = findHostFileInDSK(tmpStrCat(hostSubDir, hostName));
741 unsigned newMsxDirSector;
742 if (dirIndex.sector == unsigned(-1)) {
743 // MSX directory doesn't exist yet, create it.
744 // Allocate a cluster to hold the subdirectory entries.
745 unsigned cluster = getFreeCluster();
746 writeFAT12(cluster, EOF_FAT);
747
748 // Allocate and fill in directory entry.
749 try {
750 dirIndex = fillMSXDirEntry(hostSubDir, hostName, msxDirSector);
751 } catch (...) {
752 // Rollback allocation of directory cluster.
753 writeFAT12(cluster, FREE_FAT);
754 throw;
755 }
756 setMSXTimeStamp(dirIndex, fst);
757 msxDir(dirIndex).attrib = MSXDirEntry::ATT_DIRECTORY;
758 msxDir(dirIndex).startCluster = cluster;
759
760 // Initialize the new directory.
761 newMsxDirSector = clusterToSector(cluster);
762 for (auto i : xrange(SECTORS_PER_CLUSTER)) {
763 memset(&sectors[newMsxDirSector + i], 0, SECTOR_SIZE);
764 }
765 DirIndex idx0(newMsxDirSector, 0); // entry for "."
766 DirIndex idx1(newMsxDirSector, 1); // ".."
767 memset(msxDir(idx0).filename, ' ', 11);
768 memset(msxDir(idx1).filename, ' ', 11);
769 memset(msxDir(idx0).filename, '.', 1);
770 memset(msxDir(idx1).filename, '.', 2);
771 msxDir(idx0).attrib = MSXDirEntry::ATT_DIRECTORY;
772 msxDir(idx1).attrib = MSXDirEntry::ATT_DIRECTORY;
773 setMSXTimeStamp(idx0, fst);
774 setMSXTimeStamp(idx1, fst);
775 msxDir(idx0).startCluster = cluster;
776 msxDir(idx1).startCluster = msxDirSector == firstDirSector
777 ? 0 : sectorToCluster(msxDirSector);
778 } else {
779 if (!(msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY)) {
780 // Should rarely happen because checkDeletedHostFiles()
781 // recently checked this. (It could happen when a host
782 // directory is *just*recently* created with the same
783 // name as an existing msx file). Ignore, it will be
784 // corrected in the next sync.
785 return;
786 }
787 unsigned cluster = msxDir(dirIndex).startCluster;
788 if ((cluster < FIRST_CLUSTER) || (cluster >= maxCluster)) {
789 // Sanity check on cluster range.
790 return;
791 }
792 newMsxDirSector = clusterToSector(cluster);
793 }
794
795 // Recursively process this directory.
796 addNewHostFiles(strCat(hostSubDir, hostName, '/'), newMsxDirSector);
797}
798
799void DirAsDSK::addNewHostFile(const string& hostSubDir, const string& hostName,
800 unsigned msxDirSector, FileOperations::Stat& fst)
801{
802 if (checkFileUsedInDSK(tmpStrCat(hostSubDir, hostName))) {
803 // File is already present in the virtual disk, do nothing.
804 return;
805 }
806 // TODO check for available free space on disk instead of max free space
807 int diskSpace = (nofSectors - firstDataSector) * SECTOR_SIZE;
808 if (fst.st_size > diskSpace) {
809 cliComm.printWarning("File too large: ",
810 hostDir, hostSubDir, hostName);
811 return;
812 }
813
814 DirIndex dirIndex = fillMSXDirEntry(hostSubDir, hostName, msxDirSector);
815 importHostFile(dirIndex, fst);
816}
817
818DirAsDSK::DirIndex DirAsDSK::fillMSXDirEntry(
819 const string& hostSubDir, const string& hostName, unsigned msxDirSector)
820{
821 string hostPath = hostSubDir + hostName;
822 try {
823 // Get empty dir entry (possibly extends subdirectory).
824 DirIndex dirIndex = getFreeDirEntry(msxDirSector);
825
826 // Create correct MSX filename.
827 string msxFilename = hostToMsxName(hostName);
828 if (checkMSXFileExists(msxFilename, msxDirSector)) {
829 // TODO: actually should increase vfat abbreviation if possible!!
830 throw MSXException(
831 "MSX name ", msxToHostName(msxFilename.c_str()),
832 " already exists");
833 }
834
835 // Fill in hostName / msx filename.
836 assert(!hostPath.ends_with('/'));
837 mapDirs[dirIndex].hostName = hostPath;
838 memset(&msxDir(dirIndex), 0, sizeof(MSXDirEntry)); // clear entry
839 memcpy(msxDir(dirIndex).filename, msxFilename.data(), 8 + 3);
840 return dirIndex;
841 } catch (MSXException& e) {
842 throw MSXException("Couldn't add ", hostPath, ": ",
843 e.getMessage());
844 }
845}
846
847DirAsDSK::DirIndex DirAsDSK::getFreeDirEntry(unsigned msxDirSector)
848{
849 vector<bool> visited(nofSectors, false);
850 while (true) {
851 if (visited[msxDirSector]) {
852 // cycle detected, invalid disk, but don't crash on it
853 throw MSXException("cycle in FAT");
854 }
855 visited[msxDirSector] = true;
856
857 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
858 DirIndex dirIndex(msxDirSector, idx);
859 const char* msxName = msxDir(dirIndex).filename;
860 if (msxName[0] == one_of(char(0x00), char(0xE5))) {
861 // Found an unused msx entry. There shouldn't
862 // be any hostfile mapped to this entry.
863 assert(!mapDirs.contains(dirIndex));
864 return dirIndex;
865 }
866 }
867 unsigned sector = nextMsxDirSector(msxDirSector);
868 if (sector == unsigned(-1)) break;
869 msxDirSector = sector;
870 }
871
872 // No free space in existing directory.
873 if (msxDirSector == (firstDataSector - 1)) {
874 // Can't extend root directory.
875 throw MSXException("root directory full");
876 }
877
878 // Extend sub-directory: allocate and clear a new cluster, add this
879 // cluster in the existing FAT chain.
880 unsigned cluster = sectorToCluster(msxDirSector);
881 unsigned newCluster = getFreeCluster(); // throws if disk full
882 unsigned sector = clusterToSector(newCluster);
883 memset(&sectors[sector], 0, SECTORS_PER_CLUSTER * SECTOR_SIZE);
884 writeFAT12(cluster, newCluster);
885 writeFAT12(newCluster, EOF_FAT);
886
887 // First entry in this newly allocated cluster is free. Return it.
888 return {sector, 0};
889}
890
891void DirAsDSK::writeSectorImpl(size_t sector_, const SectorBuffer& buf)
892{
893 assert(sector_ < nofSectors);
894 assert(syncMode != SYNC_READONLY);
895 auto sector = unsigned(sector_);
896
897 // Update last access time.
898 if (auto* scheduler = diskChanger.getScheduler()) {
899 lastAccess = scheduler->getCurrentTime();
900 }
901
902 if (sector == 0) {
903 // Ignore. We don't allow writing to the bootsector. It would
904 // be very bad if the MSX tried to format this disk using other
905 // disk parameters than this code assumes. It's also not useful
906 // to write a different bootprogram to this disk because it
907 // will be lost when this virtual disk is ejected.
908 } else if (sector < firstSector2ndFAT) {
909 writeFATSector(sector, buf);
910 } else if (sector < firstDirSector) {
911 // Write to 2nd FAT, only buffer it. Don't interpret the data
912 // in FAT2 in any way (nor trigger any action on this write).
913 memcpy(&sectors[sector], &buf, sizeof(buf));
914 } else if (DirIndex dirDirIndex; isDirSector(sector, dirDirIndex)) {
915 // Either root- or sub-directory.
916 writeDIRSector(sector, dirDirIndex, buf);
917 } else {
918 writeDataSector(sector, buf);
919 }
920}
921
922void DirAsDSK::writeFATSector(unsigned sector, const SectorBuffer& buf)
923{
924 // Create copy of old FAT (to be able to detect changes).
925 vector<SectorBuffer> oldFAT(nofSectorsPerFat);
926 memcpy(&oldFAT[0], fat(), SECTOR_SIZE * nofSectorsPerFat);
927
928 // Update current FAT with new data.
929 memcpy(&sectors[sector], &buf, sizeof(buf));
930
931 // Look for changes.
932 for (auto i : xrange(FIRST_CLUSTER, maxCluster)) {
933 if (readFAT(i) != readFATHelper(oldFAT.data(), i)) {
934 exportFileFromFATChange(i, oldFAT.data());
935 }
936 }
937 // At this point there should be no more differences.
938 // Note: we can't use
939 // assert(memcmp(fat(), oldFAT, sizeof(oldFAT)) == 0);
940 // because exportFileFromFATChange() only updates the part of the FAT
941 // that actually contains FAT info. E.g. not the media ID at the
942 // beginning nor the unused part at the end. And for example the 'CALL
943 // FORMAT' routine also writes these parts of the FAT.
944 for (auto i : xrange(FIRST_CLUSTER, maxCluster)) {
945 assert(readFAT(i) == readFATHelper(oldFAT.data(), i)); (void)i;
946 }
947}
948
949void DirAsDSK::exportFileFromFATChange(unsigned cluster, SectorBuffer* oldFAT)
950{
951 // Get first cluster in the FAT chain that contains 'cluster'.
952 auto [startCluster, chainLength] = getChainStart(cluster);
953
954 // Copy this whole chain from FAT1 to FAT2 (so that the loop in
955 // writeFATSector() sees this part is already handled).
956 vector<bool> visited(maxCluster, false);
957 unsigned tmp = startCluster;
958 while ((FIRST_CLUSTER <= tmp) && (tmp < maxCluster)) {
959 if (visited[tmp]) {
960 // detected cycle, don't export file
961 return;
962 }
963 visited[tmp] = true;
964
965 unsigned next = readFAT(tmp);
966 writeFATHelper(oldFAT, tmp, next);
967 tmp = next;
968 }
969
970 // Find the corresponding direntry and (if found) export file based on
971 // new cluster chain.
972 DirIndex dirIndex, dirDirIndex;
973 if (getDirEntryForCluster(startCluster, dirIndex, dirDirIndex)) {
974 exportToHost(dirIndex, dirDirIndex);
975 }
976}
977
978std::pair<unsigned, unsigned> DirAsDSK::getChainStart(unsigned cluster)
979{
980 // Search for the first cluster in the chain that contains 'cluster'
981 // Note: worst case (this implementation of) the search is O(N^2), but
982 // because usually FAT chains are allocated in ascending order, this
983 // search is fast O(N).
984 unsigned chainLength = 0;
985 for (unsigned i = FIRST_CLUSTER; i < maxCluster; ++i) { // note: i changed in loop!
986 if (readFAT(i) == cluster) {
987 // Found a predecessor.
988 cluster = i;
989 ++chainLength;
990 i = FIRST_CLUSTER - 1; // restart search
991 }
992 }
993 return {cluster, chainLength};
994}
995
996// Generic helper function that walks over the whole MSX directory tree
997// (possibly it stops early so it doesn't always walk over the whole tree).
998// The action that is performed while walking depends on the functor parameter.
999template<typename FUNC> bool DirAsDSK::scanMsxDirs(FUNC func, unsigned sector)
1000{
1001 size_t rdIdx = 0;
1002 vector<unsigned> dirs; // TODO make vector of struct instead of
1003 vector<DirIndex> dirs2; // 2 parallel vectors.
1004 while (true) {
1005 do {
1006 // About to process a new directory sector.
1007 if (func.onDirSector(sector)) return true;
1008
1009 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
1010 // About to process a new directory entry.
1011 DirIndex dirIndex(sector, idx);
1012 const MSXDirEntry& entry = msxDir(dirIndex);
1013 if (func.onDirEntry(dirIndex, entry)) return true;
1014
1015 if ((entry.filename[0] == one_of(char(0x00), char(0xE5))) ||
1016 !(entry.attrib & MSXDirEntry::ATT_DIRECTORY)) {
1017 // Not a directory.
1018 continue;
1019 }
1020 unsigned cluster = msxDir(dirIndex).startCluster;
1021 if ((cluster < FIRST_CLUSTER) ||
1022 (cluster >= maxCluster)) {
1023 // Cluster=0 happens for ".." entries to
1024 // the root directory, also be robust for
1025 // bogus data.
1026 continue;
1027 }
1028 unsigned dir = clusterToSector(cluster);
1029 if (contains(dirs, dir)) {
1030 // Already found this sector. Except
1031 // for the special "." and ".."
1032 // entries, loops should not occur in
1033 // valid disk images, but don't crash
1034 // on (intentionally?) invalid images.
1035 continue;
1036 }
1037 // Found a new directory, insert in the set of
1038 // yet-to-be-processed directories.
1039 dirs.push_back(dir);
1040 dirs2.push_back(dirIndex);
1041 }
1042 sector = nextMsxDirSector(sector);
1043 } while (sector != unsigned(-1));
1044
1045 // Scan next subdirectory (if any).
1046 if (rdIdx == dirs.size()) {
1047 // Visited all directories.
1048 return false;
1049 }
1050 // About to process a new subdirectory.
1051 func.onVisitSubDir(dirs2[rdIdx]);
1052 sector = dirs[rdIdx++];
1053 }
1054}
1055
1056// Base class for functor objects to be used in scanMsxDirs().
1057// This implements all required methods with empty implementations.
1059 // Called right before we enter a new subdirectory.
1060 void onVisitSubDir(DirAsDSK::DirIndex /*subdir*/) {}
1061
1062 // Called when a new sector of a (sub)directory is being scanned.
1063 inline bool onDirSector(unsigned /*dirSector*/) {
1064 return false;
1065 }
1066
1067 // Called for each directory entry (in a sector).
1068 inline bool onDirEntry(DirAsDSK::DirIndex /*dirIndex*/,
1069 const MSXDirEntry& /*entry*/) {
1070 return false;
1071 }
1072};
1073
1074// Base class for the IsDirSector and DirEntryForCluster scanner algorithms
1075// below. This class remembers the directory entry of the last visited subdir.
1077 explicit DirScanner(DirAsDSK::DirIndex& dirDirIndex_)
1078 : dirDirIndex(dirDirIndex_)
1079 {
1080 dirDirIndex = DirAsDSK::DirIndex(0, 0); // represents entry for root dir
1081 }
1082
1083 // Called right before we enter a new subdirectory.
1084 void onVisitSubDir(DirAsDSK::DirIndex subdir) {
1085 dirDirIndex = subdir;
1086 }
1087
1088 DirAsDSK::DirIndex& dirDirIndex;
1089};
1090
1091// Figure out whether a given sector is part of the msx directory structure.
1093 IsDirSector(unsigned sector_, DirAsDSK::DirIndex& dirDirIndex_)
1094 : DirScanner(dirDirIndex_)
1095 , sector(sector_) {}
1096 [[nodiscard]] bool onDirSector(unsigned dirSector) const {
1097 return sector == dirSector;
1098 }
1099 const unsigned sector;
1100};
1101bool DirAsDSK::isDirSector(unsigned sector, DirIndex& dirDirIndex)
1102{
1103 return scanMsxDirs(IsDirSector(sector, dirDirIndex), firstDirSector);
1104}
1105
1106// Search for the directory entry that has the given startCluster.
1108 DirEntryForCluster(unsigned cluster_,
1109 DirAsDSK::DirIndex& dirIndex_,
1110 DirAsDSK::DirIndex& dirDirIndex_)
1111 : DirScanner(dirDirIndex_)
1112 , cluster(cluster_)
1113 , result(dirIndex_) {}
1114 bool onDirEntry(DirAsDSK::DirIndex dirIndex, const MSXDirEntry& entry) {
1115 if (entry.startCluster == cluster) {
1116 result = dirIndex;
1117 return true;
1118 }
1119 return false;
1120 }
1121 const unsigned cluster;
1122 DirAsDSK::DirIndex& result;
1123};
1124bool DirAsDSK::getDirEntryForCluster(unsigned cluster,
1125 DirIndex& dirIndex, DirIndex& dirDirIndex)
1126{
1127 return scanMsxDirs(DirEntryForCluster(cluster, dirIndex, dirDirIndex),
1128 firstDirSector);
1129}
1130DirAsDSK::DirIndex DirAsDSK::getDirEntryForCluster(unsigned cluster)
1131{
1132 DirIndex dirIndex, dirDirIndex;
1133 if (getDirEntryForCluster(cluster, dirIndex, dirDirIndex)) {
1134 return dirIndex;
1135 } else {
1136 return {unsigned(-1), unsigned(-1)}; // not found
1137 }
1138}
1139
1140// Remove the mapping between the msx and host for all the files/dirs in the
1141// given msx directory (+ subdirectories).
1144 : mapDirs(mapDirs_) {}
1145 bool onDirEntry(DirAsDSK::DirIndex dirIndex,
1146 const MSXDirEntry& /*entry*/) {
1147 mapDirs.erase(dirIndex);
1148 return false;
1149 }
1151};
1152void DirAsDSK::unmapHostFiles(unsigned msxDirSector)
1153{
1154 scanMsxDirs(UnmapHostFiles(mapDirs), msxDirSector);
1155}
1156
1157void DirAsDSK::exportToHost(DirIndex dirIndex, DirIndex dirDirIndex)
1158{
1159 // Handle both files and subdirectories.
1160 if (msxDir(dirIndex).attrib & MSXDirEntry::ATT_VOLUME) {
1161 // But ignore volume ID.
1162 return;
1163 }
1164 const char* msxName = msxDir(dirIndex).filename;
1165 string hostName;
1166 if (auto* v = lookup(mapDirs, dirIndex)) {
1167 // Hostname is already known.
1168 hostName = v->hostName;
1169 } else {
1170 // Host file/dir does not yet exist, create hostname from
1171 // msx name.
1172 if (msxName[0] == one_of(char(0x00), char(0xE5))) {
1173 // Invalid MSX name, don't do anything.
1174 return;
1175 }
1176 string hostSubDir;
1177 if (dirDirIndex.sector != 0) {
1178 // Not the msx root directory.
1179 auto* v2 = lookup(mapDirs, dirDirIndex);
1180 assert(v2);
1181 hostSubDir = v2->hostName;
1182 assert(!hostSubDir.ends_with('/'));
1183 hostSubDir += '/';
1184 }
1185 hostName = hostSubDir + msxToHostName(msxName);
1186 mapDirs[dirIndex].hostName = hostName;
1187 }
1188 if (msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY) {
1189 if ((memcmp(msxName, ". ", 11) == 0) ||
1190 (memcmp(msxName, ".. ", 11) == 0)) {
1191 // Don't export "." or "..".
1192 return;
1193 }
1194 exportToHostDir(dirIndex, hostName);
1195 } else {
1196 exportToHostFile(dirIndex, hostName);
1197 }
1198}
1199
1200void DirAsDSK::exportToHostDir(DirIndex dirIndex, const string& hostName)
1201{
1202 try {
1203 unsigned cluster = msxDir(dirIndex).startCluster;
1204 if ((cluster < FIRST_CLUSTER) || (cluster >= maxCluster)) {
1205 // Sanity check on cluster range.
1206 return;
1207 }
1208 unsigned msxDirSector = clusterToSector(cluster);
1209
1210 // Create the host directory.
1211 FileOperations::mkdirp(hostDir + hostName);
1212
1213 // Export all the components in this directory.
1214 vector<bool> visited(nofSectors, false);
1215 do {
1216 if (visited[msxDirSector]) {
1217 // detected cycle, invalid disk, but don't crash on it
1218 return;
1219 }
1220 visited[msxDirSector] = true;
1221
1222 if (readFAT(sectorToCluster(msxDirSector)) == FREE_FAT) {
1223 // This happens e.g. on a TurboR when a directory
1224 // is removed: first the FAT is cleared before
1225 // the directory entry is updated.
1226 return;
1227 }
1228 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
1229 exportToHost(DirIndex(msxDirSector, idx), dirIndex);
1230 }
1231 msxDirSector = nextMsxDirSector(msxDirSector);
1232 } while (msxDirSector != unsigned(-1));
1233 } catch (FileException& e) {
1234 cliComm.printWarning("Error while syncing host directory: ",
1235 hostName, ": ", e.getMessage());
1236 }
1237}
1238
1239void DirAsDSK::exportToHostFile(DirIndex dirIndex, const string& hostName)
1240{
1241 // We write a host file with length that is the minimum of:
1242 // - Length indicated in msx directory entry.
1243 // - Length of FAT-chain * cluster-size, this chain can have length=0
1244 // if startCluster is not (yet) filled in.
1245 try {
1246 unsigned curCl = msxDir(dirIndex).startCluster;
1247 unsigned msxSize = msxDir(dirIndex).size;
1248
1249 File file(hostDir + hostName, File::TRUNCATE);
1250 unsigned offset = 0;
1251 vector<bool> visited(maxCluster, false);
1252
1253 while ((FIRST_CLUSTER <= curCl) && (curCl < maxCluster)) {
1254 if (visited[curCl]) {
1255 // detected cycle, invalid disk, but don't crash on it
1256 return;
1257 }
1258 visited[curCl] = true;
1259
1260 unsigned logicalSector = clusterToSector(curCl);
1261 for (auto i : xrange(SECTORS_PER_CLUSTER)) {
1262 if (offset >= msxSize) break;
1263 unsigned sector = logicalSector + i;
1264 assert(sector < nofSectors);
1265 auto writeSize = std::min<size_t>(msxSize - offset, SECTOR_SIZE);
1266 file.write(&sectors[sector], writeSize);
1267 offset += SECTOR_SIZE;
1268 }
1269 if (offset >= msxSize) break;
1270 curCl = readFAT(curCl);
1271 }
1272 } catch (FileException& e) {
1273 cliComm.printWarning("Error while syncing host file: ",
1274 hostName, ": ", e.getMessage());
1275 }
1276}
1277
1278void DirAsDSK::writeDIRSector(unsigned sector, DirIndex dirDirIndex,
1279 const SectorBuffer& buf)
1280{
1281 // Look for changed directory entries.
1282 for (auto idx : xrange(DIR_ENTRIES_PER_SECTOR)) {
1283 const auto& newEntry = buf.dirEntry[idx];
1284 DirIndex dirIndex(sector, idx);
1285 if (memcmp(&msxDir(dirIndex), &newEntry, sizeof(newEntry)) != 0) {
1286 writeDIREntry(dirIndex, dirDirIndex, newEntry);
1287 }
1288 }
1289 // At this point sector should be updated.
1290 assert(memcmp(&sectors[sector], &buf, sizeof(buf)) == 0);
1291}
1292
1293void DirAsDSK::writeDIREntry(DirIndex dirIndex, DirIndex dirDirIndex,
1294 const MSXDirEntry& newEntry)
1295{
1296 if (memcmp(msxDir(dirIndex).filename, newEntry.filename, 8 + 3) != 0 ||
1297 ((msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY) !=
1298 ( newEntry.attrib & MSXDirEntry::ATT_DIRECTORY))) {
1299 // Name or file-type in the direntry was changed.
1300 if (auto it = mapDirs.find(dirIndex); it != end(mapDirs)) {
1301 // If there is an associated hostfile, then delete it
1302 // (in case of a rename, the file will be recreated
1303 // below).
1304 auto fullHostName = tmpStrCat(hostDir, it->second.hostName);
1305 FileOperations::deleteRecursive(fullHostName); // ignore return value
1306 // Remove mapping between msx and host file/dir.
1307 mapDirs.erase(it);
1308 if (msxDir(dirIndex).attrib & MSXDirEntry::ATT_DIRECTORY) {
1309 // In case of a directory also unmap all
1310 // sub-components.
1311 unsigned cluster = msxDir(dirIndex).startCluster;
1312 if ((FIRST_CLUSTER <= cluster) &&
1313 (cluster < maxCluster)) {
1314 unmapHostFiles(clusterToSector(cluster));
1315 }
1316 }
1317 }
1318 }
1319
1320 // Copy the new msx directory entry.
1321 memcpy(&msxDir(dirIndex), &newEntry, sizeof(newEntry));
1322
1323 // (Re-)export the full file/directory.
1324 exportToHost(dirIndex, dirDirIndex);
1325}
1326
1327void DirAsDSK::writeDataSector(unsigned sector, const SectorBuffer& buf)
1328{
1329 assert(sector >= firstDataSector);
1330 assert(sector < nofSectors);
1331
1332 // Buffer the write, whether the sector is mapped to a file or not.
1333 memcpy(&sectors[sector], &buf, sizeof(buf));
1334
1335 // Get first cluster in the FAT chain that contains this sector.
1336 auto [cluster, offset] = sectorToClusterOffset(sector);
1337 auto [startCluster, chainLength] = getChainStart(cluster);
1338 offset += (sizeof(buf) * SECTORS_PER_CLUSTER) * chainLength;
1339
1340 // Get corresponding directory entry.
1341 DirIndex dirIndex = getDirEntryForCluster(startCluster);
1342 // no need to check for 'dirIndex.sector == unsigned(-1)'
1343 auto* v = lookup(mapDirs, dirIndex);
1344 if (!v) {
1345 // This sector was not mapped to a file, nothing more to do.
1346 return;
1347 }
1348
1349 // Actually write data to host file.
1350 string fullHostName = hostDir + v->hostName;
1351 try {
1352 File file(fullHostName, "rb+"); // don't uncompress
1353 file.seek(offset);
1354 unsigned msxSize = msxDir(dirIndex).size;
1355 if (msxSize > offset) {
1356 auto writeSize = std::min<size_t>(msxSize - offset, sizeof(buf));
1357 file.write(&buf, writeSize);
1358 }
1359 } catch (FileException& e) {
1360 cliComm.printWarning("Couldn't write to file ", fullHostName,
1361 ": ", e.getMessage());
1362 }
1363}
1364
1365} // namespace openmsx
bool erase(const K &key)
Definition: hash_set.hh:483
Definition: one_of.hh:7
static const SectorBuffer dos2BootBlock
Definition: BootBlocks.hh:15
static const SectorBuffer dos1BootBlock
Definition: BootBlocks.hh:12
void printWarning(std::string_view message)
Definition: CliComm.cc:10
friend struct UnmapHostFiles
Definition: DirAsDSK.hh:114
friend struct IsDirSector
Definition: DirAsDSK.hh:112
void writeSectorImpl(size_t sector, const SectorBuffer &buf) override
Definition: DirAsDSK.cc:891
friend struct DirEntryForCluster
Definition: DirAsDSK.hh:113
bool isWriteProtectedImpl() const override
Definition: DirAsDSK.cc:326
DirAsDSK(DiskChanger &diskChanger, CliComm &cliComm, const Filename &hostDir, SyncMode syncMode, BootSectorType bootSectorType)
Definition: DirAsDSK.cc:259
void checkCaches() override
Definition: DirAsDSK.cc:341
void readSectorImpl(size_t sector, SectorBuffer &buf) override
Definition: DirAsDSK.cc:360
bool hasChanged() const override
Has the content of this disk changed, by some other means than the MSX writing to the disk.
Definition: DirAsDSK.cc:331
bool isDoubleSidedDrive() const
Definition: DiskChanger.hh:72
Scheduler * getScheduler() const
Definition: DiskChanger.hh:71
void setNbSides(unsigned num)
Definition: Disk.hh:44
void setSectorsPerTrack(unsigned num)
Definition: Disk.hh:42
static constexpr EmuDuration sec(unsigned x)
Definition: EmuDuration.hh:40
@ TRUNCATE
Definition: File.hh:20
This class represents a filename.
Definition: Filename.hh:18
static constexpr size_t SECTOR_SIZE
Abstract class for disk images that only represent the logical sector information (so not the raw tra...
void setNbSectors(size_t num)
const Value * lookup(const hash_map< Key, Value, Hasher, Equal > &map, const Key2 &key)
Definition: hash_map.hh:118
constexpr double e
Definition: Math.hh:18
std::pair< string_view, string_view > splitOnLast(string_view str, string_view chars)
Definition: StringOp.cc:108
constexpr vecN< N, T > min(const vecN< N, T > &x, const vecN< N, T > &y)
Definition: gl_vec.hh:265
string expandTilde(string path)
Expand the '~' character to the users home directory.
bool getStat(zstring_view filename, Stat &st)
Call stat() and return the stat structure.
bool isRegularFile(const Stat &st)
bool isDirectory(const Stat &st)
int deleteRecursive(zstring_view path)
Recursively delete a file or directory and (in case of a directory) all its sub-components.
void mkdirp(string path)
Acts like the unix command "mkdir -p".
This file implemented 3 utility functions:
Definition: Autofire.cc:9
constexpr unsigned FREE_FAT
Definition: DirAsDSK.cc:36
constexpr unsigned SECTORS_PER_CLUSTER
Definition: DirAsDSK.cc:27
constexpr unsigned SECTORS_PER_TRACK
Definition: DirAsDSK.cc:28
constexpr unsigned SECTOR_SIZE
Definition: DirAsDSK.cc:23
constexpr unsigned EOF_FAT
Definition: DirAsDSK.cc:38
constexpr unsigned NUM_FATS
Definition: DirAsDSK.cc:25
constexpr unsigned NUM_TRACKS
Definition: DirAsDSK.cc:26
constexpr unsigned DIR_ENTRIES_PER_SECTOR
Definition: DirAsDSK.cc:30
constexpr unsigned SECTORS_PER_DIR
Definition: DirAsDSK.cc:24
constexpr unsigned FIRST_FAT_SECTOR
Definition: DirAsDSK.cc:29
constexpr const char *const filename
constexpr unsigned BAD_FAT
Definition: DirAsDSK.cc:37
constexpr unsigned FIRST_CLUSTER
Definition: DirAsDSK.cc:34
auto count(InputRange &&range, const T &value)
Definition: ranges.hh:290
auto copy(InputRange &&range, OutputIter out)
Definition: ranges.hh:208
constexpr void replace(ForwardRange &&range, const T &old_value, const T &new_value)
Definition: ranges.hh:244
constexpr void sort(RandomAccessRange &&range)
Definition: ranges.hh:33
void swap(openmsx::MemBuffer< T > &l, openmsx::MemBuffer< T > &r) noexcept
Definition: MemBuffer.hh:202
uint32_t next(octet_iterator &it, octet_iterator end)
auto transform_in_place(ForwardRange &&range, UnaryOperation op)
Definition: stl.hh:187
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition: stl.hh:23
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:617
std::string strCat(Ts &&...ts)
Definition: strCat.hh:549
bool onDirEntry(DirAsDSK::DirIndex dirIndex, const MSXDirEntry &entry)
Definition: DirAsDSK.cc:1114
DirEntryForCluster(unsigned cluster_, DirAsDSK::DirIndex &dirIndex_, DirAsDSK::DirIndex &dirDirIndex_)
Definition: DirAsDSK.cc:1108
DirAsDSK::DirIndex & result
Definition: DirAsDSK.cc:1122
void onVisitSubDir(DirAsDSK::DirIndex subdir)
Definition: DirAsDSK.cc:1084
DirAsDSK::DirIndex & dirDirIndex
Definition: DirAsDSK.cc:1088
DirScanner(DirAsDSK::DirIndex &dirDirIndex_)
Definition: DirAsDSK.cc:1077
IsDirSector(unsigned sector_, DirAsDSK::DirIndex &dirDirIndex_)
Definition: DirAsDSK.cc:1093
const unsigned sector
Definition: DirAsDSK.cc:1099
bool onDirSector(unsigned dirSector) const
Definition: DirAsDSK.cc:1096
Endian::L16 startCluster
static constexpr byte ATT_DIRECTORY
static constexpr byte ATT_VOLUME
bool onDirEntry(DirAsDSK::DirIndex, const MSXDirEntry &)
Definition: DirAsDSK.cc:1068
void onVisitSubDir(DirAsDSK::DirIndex)
Definition: DirAsDSK.cc:1060
bool onDirSector(unsigned)
Definition: DirAsDSK.cc:1063
bool onDirEntry(DirAsDSK::DirIndex dirIndex, const MSXDirEntry &)
Definition: DirAsDSK.cc:1145
DirAsDSK::MapDirs & mapDirs
Definition: DirAsDSK.cc:1150
UnmapHostFiles(DirAsDSK::MapDirs &mapDirs_)
Definition: DirAsDSK.cc:1143
constexpr auto xrange(T e)
Definition: xrange.hh:133
constexpr auto end(const zstring_view &x)