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