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 
18 using std::string;
19 using std::vector;
20 
21 namespace openmsx {
22 
23 constexpr unsigned SECTOR_SIZE = sizeof(SectorBuffer);
24 constexpr unsigned SECTORS_PER_DIR = 7;
25 constexpr unsigned NUM_FATS = 2;
26 constexpr unsigned NUM_TRACKS = 80;
27 constexpr unsigned SECTORS_PER_CLUSTER = 2;
28 constexpr unsigned SECTORS_PER_TRACK = 9;
29 constexpr unsigned FIRST_FAT_SECTOR = 1;
30 constexpr unsigned DIR_ENTRIES_PER_SECTOR =
31  SECTOR_SIZE / sizeof(MSXDirEntry);
32 
33 // First valid regular cluster number.
34 constexpr unsigned FIRST_CLUSTER = 2;
35 
36 constexpr unsigned FREE_FAT = 0x000;
37 constexpr unsigned BAD_FAT = 0xFF7;
38 constexpr 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 
48 unsigned 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 
60 void 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 
75 SectorBuffer* DirAsDSK::fat()
76 {
77  return &sectors[FIRST_FAT_SECTOR];
78 }
79 SectorBuffer* DirAsDSK::fat2()
80 {
81  return &sectors[firstSector2ndFAT];
82 }
83 
84 // Read entry from FAT.
85 unsigned DirAsDSK::readFAT(unsigned cluster)
86 {
87  return readFATHelper(fat(), cluster);
88 }
89 
90 // Write an entry to both FAT1 and FAT2.
91 void 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
100 unsigned 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 }
109 unsigned DirAsDSK::findFirstFreeCluster()
110 {
111  return findNextFreeCluster(FIRST_CLUSTER - 1);
112 }
113 
114 // Throws when there are no more free clusters.
115 unsigned DirAsDSK::getFreeCluster()
116 {
117  unsigned cluster = findFirstFreeCluster();
118  if (cluster == maxCluster) {
119  throw MSXException("disk full");
120  }
121  return cluster;
122 }
123 
124 unsigned 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 
132 std::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 }
141 unsigned DirAsDSK::sectorToCluster(unsigned sector) const
142 {
143  auto [cluster, offset] = sectorToClusterOffset(sector);
144  return cluster;
145 }
146 
147 MSXDirEntry& 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.
155 unsigned 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.
183 bool 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.
210 DirAsDSK::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.
221 bool DirAsDSK::checkFileUsedInDSK(std::string_view hostName)
222 {
223  DirIndex dirIndex = findHostFileInDSK(hostName);
224  return dirIndex.sector != unsigned(-1);
225 }
226 
227 static 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 
243 static 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 
259 DirAsDSK::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 
360 void 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 
394 void 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 
411 void 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 
444 void 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 
480 void 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 
497 void 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 
507 void 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 
543 void 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 
661 void 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.
684 static 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 
698 void 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  }
724  if (FileOperations::isDirectory(fst)) {
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 
737 void 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 
799 void 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 
818 DirAsDSK::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 
847 DirAsDSK::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 
891 void 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 
922 void 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 
949 void 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 
978 std::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.
999 template<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.
1058 struct NullScanner {
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 };
1101 bool 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 };
1124 bool DirAsDSK::getDirEntryForCluster(unsigned cluster,
1125  DirIndex& dirIndex, DirIndex& dirDirIndex)
1126 {
1127  return scanMsxDirs(DirEntryForCluster(cluster, dirIndex, dirDirIndex),
1128  firstDirSector);
1129 }
1130 DirAsDSK::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).
1143  explicit UnmapHostFiles(DirAsDSK::MapDirs& mapDirs_)
1144  : mapDirs(mapDirs_) {}
1145  bool onDirEntry(DirAsDSK::DirIndex dirIndex,
1146  const MSXDirEntry& /*entry*/) {
1147  mapDirs.erase(dirIndex);
1148  return false;
1149  }
1151 };
1152 void DirAsDSK::unmapHostFiles(unsigned msxDirSector)
1153 {
1154  scanMsxDirs(UnmapHostFiles(mapDirs), msxDirSector);
1155 }
1156 
1157 void 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 
1200 void 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 
1239 void 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 
1278 void 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 
1293 void 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 
1327 void 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
void swap(openmsx::MemBuffer< T > &l, openmsx::MemBuffer< T > &r) noexcept
Definition: MemBuffer.hh:202
bool erase(const K &key)
Definition: hash_set.hh:487
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:116
friend struct IsDirSector
Definition: DirAsDSK.hh:114
void writeSectorImpl(size_t sector, const SectorBuffer &buf) override
Definition: DirAsDSK.cc:891
friend struct DirEntryForCluster
Definition: DirAsDSK.hh:115
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
Scheduler * getScheduler() const
Definition: DiskChanger.hh:71
bool isDoubleSidedDrive() const
Definition: DiskChanger.hh:72
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:39
@ 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
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:269
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
void replace(ForwardRange &&range, const T &old_value, const T &new_value)
Definition: ranges.hh:245
auto count(InputRange &&range, const T &value)
Definition: ranges.hh:291
auto copy(InputRange &&range, OutputIter out)
Definition: ranges.hh:209
void sort(RandomAccessRange &&range)
Definition: ranges.hh:34
uint32_t next(octet_iterator &it, octet_iterator end)
auto transform_in_place(ForwardRange &&range, UnaryOperation op)
Definition: stl.hh:196
constexpr bool contains(ITER first, ITER last, const VAL &val)
Check if a range contains a given value, using linear search.
Definition: stl.hh:32
TemporaryString tmpStrCat(Ts &&... ts)
Definition: strCat.hh:659
std::string strCat(Ts &&...ts)
Definition: strCat.hh:591
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:155
constexpr auto end(const zstring_view &x)