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