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