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