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