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