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