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