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