OXYGENHUB
◆ main openmsx / src / fdc / MSXtar.cc
36.0 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "MSXtar.hh"
2
3
#include "SectorAccessibleDisk.hh"
4
5
#include "FileOperations.hh"
6
#include "MSXException.hh"
7
#include "MsxChar2Unicode.hh"
8
#include "foreach_file.hh"
9
10
#include "File.hh"
11
#include "StringOp.hh"
12
#include "narrow.hh"
13
#include "one_of.hh"
14
#include "ranges.hh"
15
#include "stl.hh"
16
#include "strCat.hh"
17
#include "xrange.hh"
18
19
// Note: For Mac OS X 10.3 <ctime> must be included before <utime.h>.
20
#include <ctime>
21
#ifndef _MSC_VER
22
#include <utime.h>
23
#else
24
#include <sys/utime.h>
25
#endif
26
27
#include <algorithm>
28
#include <bit>
29
#include <cassert>
30
#include <cctype>
31
#include <cstring>
32
33
namespace openmsx {
34
35
using FAT::Free;
36
using FAT::EndOfChain;
37
using FAT::Cluster;
38
using FAT::DirCluster;
39
using FAT::FatCluster;
40
using FAT::FileName;
41
42
namespace FAT {
43
	static constexpr unsigned FREE = 0x000;
44
	static constexpr unsigned FIRST_CLUSTER = 0x002;
45
}
46
47
namespace FAT12 {
48
	static constexpr unsigned BAD = 0xFF7;
49
	static constexpr unsigned END_OF_CHAIN = 0xFFF; // actually 0xFF8-0xFFF, signals EOF in FAT12
50
51
	static constexpr unsigned MAX_CLUSTER_COUNT = 0xFF4;
52
53
	// Functor to convert a FatCluster or DirCluster to a FAT12 cluster number
54
	struct ToClusterNumber {
55
		unsigned operator()(Free) const { return FAT::FREE; }
56
		unsigned operator()(EndOfChain) const { return END_OF_CHAIN; }
57
		unsigned operator()(Cluster cluster) const { return FAT::FIRST_CLUSTER + cluster.index; }
58
	};
59
}
60
61
namespace FAT16 {
62
	static constexpr unsigned BAD = 0xFFF7;
63
	static constexpr unsigned END_OF_CHAIN = 0xFFFF; // actually 0xFFF8-0xFFFF, signals EOF in FAT16
64
65
	static constexpr unsigned MAX_CLUSTER_COUNT = 0xFFF4;
66
67
	// Functor to convert a FatCluster or DirCluster to a FAT16 cluster number
68
	struct ToClusterNumber {
69
		unsigned operator()(Free) const { return FAT::FREE; }
70
		unsigned operator()(EndOfChain) const { return END_OF_CHAIN; }
71
		unsigned operator()(Cluster cluster) const { return FAT::FIRST_CLUSTER + cluster.index; }
72
	};
73
}
74
75
static constexpr unsigned SECTOR_SIZE = sizeof(SectorBuffer);
76
static constexpr unsigned DIR_ENTRIES_PER_SECTOR = SECTOR_SIZE / sizeof(MSXDirEntry);
77
78
static constexpr uint8_t EBPB_SIGNATURE = 0x29;  // Extended BIOS Parameter Block signature
79
80
// This particular combination of flags indicates that this dir entry is used
81
// to store a long Unicode file name.
82
// For details, read http://home.teleport.com/~brainy/lfn.htm
83
static constexpr MSXDirEntry::AttribValue T_MSX_LFN(0x0F); // LFN entry (long files names)
84
85
/** Transforms a cluster number towards the first sector of this cluster
86
  * The calculation uses info read fom the boot sector
87
  */
88
unsigned MSXtar::clusterToSector(Cluster cluster) const
89
{
90
	return dataStart + sectorsPerCluster * cluster.index;
91
}
92
93
/** Transforms a sector number towards it containing cluster
94
  * The calculation uses info read fom the boot sector
95
  */
96
Cluster MSXtar::sectorToCluster(unsigned sector) const
97
{
98
	// check lower bound since unsigned can't represent negative numbers
99
	// don't check upper bound since it is used in calculations like getNextSector
100
	assert(sector >= dataStart);
101
	return Cluster{(sector - dataStart) / sectorsPerCluster};
102
}
103
104
105
/** Initialize object variables by reading info from the boot sector
106
  */
107
void MSXtar::parseBootSector(const MSXBootSector& boot)
108
{
109
	fatCount          = boot.nrFats;
110
	sectorsPerFat     = boot.sectorsFat;
111
	sectorsPerCluster = boot.spCluster;
112
113
	unsigned nrSectors = boot.nrSectors;
114
	if (nrSectors == 0 && boot.params.extended.extendedBootSignature == EBPB_SIGNATURE) {
115
		nrSectors = boot.params.extended.nrSectors;
116
	}
117
118
	if (boot.bpSector != SECTOR_SIZE) {
119
		throw MSXException("Illegal sector size: ", uint16_t(boot.bpSector));
120
	}
121
	if (boot.resvSectors == 0) {
122
		throw MSXException("Illegal number of reserved sectors: ", uint16_t(boot.resvSectors));
123
	}
124
	if (fatCount == 0) {
125
		throw MSXException("Illegal number of FATs: ", fatCount);
126
	}
127
	if (sectorsPerFat == 0 || sectorsPerFat > 0x100) {
128
		throw MSXException("Illegal number of sectors per FAT: ", sectorsPerFat);
129
	}
130
	if (boot.dirEntries == 0 || boot.dirEntries % DIR_ENTRIES_PER_SECTOR != 0) {
131
		throw MSXException("Illegal number of root directory entries: ", uint16_t(boot.dirEntries));
132
	}
133
	if (!std::has_single_bit(sectorsPerCluster)) {
134
		throw MSXException("Illegal number of sectors per cluster: ", sectorsPerCluster);
135
	}
136
137
	unsigned nbRootDirSectors = boot.dirEntries / DIR_ENTRIES_PER_SECTOR;
138
	fatStart = boot.resvSectors;
139
	rootDirStart = fatStart + fatCount * sectorsPerFat;
140
	chrootSector = rootDirStart;
141
	dataStart = rootDirStart + nbRootDirSectors;
142
143
	// Whether to use FAT16 is strangely implicit, it must be derived from the
144
	// cluster count being higher than 0xFF4. Let's round that up to 0x1000,
145
	// since FAT12 uses 1.5 bytes per cluster its maximum size is 12 sectors,
146
	// whereas FAT16 uses 2 bytes per cluster so its minimum size is 16 sectors.
147
	// Therefore we can pick the correct FAT type with sectorsPerFat >= 14.
148
	constexpr unsigned fat16Threshold = (0x1000 * 3 / 2 + 0x1000 * 2) / 2 / SECTOR_SIZE;
149
	fat16 = sectorsPerFat >= fat16Threshold;
150
151
	if (dataStart + sectorsPerCluster > nrSectors) {
152
		throw MSXException("Illegal number of sectors: ", nrSectors);
153
	}
154
155
	clusterCount = std::min((nrSectors - dataStart) / sectorsPerCluster,
156
	                        fat16 ? FAT16::MAX_CLUSTER_COUNT : FAT12::MAX_CLUSTER_COUNT);
157
158
	// Some (invalid) disk images have a too small FAT to be able to address
159
	// all clusters of the image. OpenMSX SVN revisions pre-11326 even
160
	// created such invalid images for some disk sizes!!
161
	unsigned fatCapacity = (2 * SECTOR_SIZE * sectorsPerFat) / 3 - FAT::FIRST_CLUSTER;
162
	clusterCount = std::min(clusterCount, fatCapacity);
163
}
164
165
void MSXtar::writeLogicalSector(unsigned sector, const SectorBuffer& buf)
166
{
167
	assert(!fatBuffer.empty());
168
	unsigned fatSector = sector - fatStart;
169
	if (fatSector < sectorsPerFat) {
170
		// we have a cache and this is a sector of the 1st FAT
171
		//   --> update cache
172
		fatBuffer[fatSector] = buf;
173
		fatCacheDirty = true;
174
	} else {
175
		disk.writeSector(sector, buf);
176
	}
177
}
178
179
void MSXtar::readLogicalSector(unsigned sector, SectorBuffer& buf)
180
{
181
	assert(!fatBuffer.empty());
182
	unsigned fatSector = sector - fatStart;
183
	if (fatSector < sectorsPerFat) {
184
		// we have a cache and this is a sector of the 1st FAT
185
		//   --> read from cache
186
		buf = fatBuffer[fatSector];
187
	} else {
188
		disk.readSector(sector, buf);
189
	}
190
}
191
192
MSXtar::MSXtar(SectorAccessibleDisk& sectorDisk, const MsxChar2Unicode& msxChars_)
193
	: disk(sectorDisk)
194
	, msxChars(msxChars_)
195
{
196
	if (disk.getNbSectors() == 0) {
197
		throw MSXException("No disk inserted.");
198
	}
199
	try {
200
		SectorBuffer buf;
201
		disk.readSector(0, buf);
202
		parseBootSector(buf.bootSector);
203
	} catch (MSXException& e) {
204
		throw MSXException("Bad disk image: ", e.getMessage());
205
	}
206
207
	// cache complete FAT
208
	fatCacheDirty = false;
209
	fatBuffer.resize(sectorsPerFat);
210
	disk.readSectors(std::span{fatBuffer}, fatStart);
211
}
212
213
// Not used when NRVO is used (but NRVO optimization is not (yet) mandated)
214
MSXtar::MSXtar(MSXtar&& other) noexcept
215
	: disk(other.disk)
216
	, fatBuffer(std::move(other.fatBuffer))
217
	, msxChars(other.msxChars)
218
	, findFirstFreeClusterStart(other.findFirstFreeClusterStart)
219
	, clusterCount(other.clusterCount)
220
	, fatCount(other.fatCount)
221
	, sectorsPerCluster(other.sectorsPerCluster)
222
	, sectorsPerFat(other.sectorsPerFat)
223
	, fatStart(other.fatStart)
224
	, rootDirStart(other.rootDirStart)
225
	, dataStart(other.dataStart)
226
	, chrootSector(other.chrootSector)
227
	, fatCacheDirty(other.fatCacheDirty)
228
{
229
	other.fatCacheDirty = false;
230
}
231
232
MSXtar::~MSXtar()
233
{
234
	if (!fatCacheDirty) return;
235
236
	for (auto fat : xrange(fatCount)) {
237
		for (auto i : xrange(sectorsPerFat)) {
238
			try {
239
				disk.writeSector(i + fatStart + fat * sectorsPerFat, fatBuffer[i]);
240
			} catch (MSXException&) {
241
				// nothing
242
			}
243
		}
244
	}
245
}
246
247
// Get the next cluster number from the FAT chain
248
FatCluster MSXtar::readFAT(Cluster cluster) const
249
{
250
	assert(!fatBuffer.empty()); // FAT must already be cached
251
	assert(cluster.index < clusterCount);
252
253
	std::span<const uint8_t> data{fatBuffer[0].raw.data(), sectorsPerFat * size_t(SECTOR_SIZE)};
254
255
	unsigned index = FAT::FIRST_CLUSTER + cluster.index;
256
	unsigned value = [&] {
257
		if (fat16) {
258
			auto p = subspan<2>(data, index * 2);
259
			return p[0] | p[1] << 8;
260
		} else {
261
			auto p = subspan<2>(data, (index * 3) / 2);
262
			return (index & 1)
263
			     ? (p[0] >> 4) + (p[1] << 4)
264
			     : p[0] + ((p[1] & 0x0F) << 8);
265
		}
266
	}();
267
268
	// Be tolerant when reading:
269
	// * FREE is returned as FREE.
270
	// * Anything else but a valid cluster number is returned as END_OF_CHAIN.
271
	if (value == FAT::FREE) {
272
		return Free{};
273
	} else if (value >= FAT::FIRST_CLUSTER && value < FAT::FIRST_CLUSTER + clusterCount) {
274
		return Cluster{value - FAT::FIRST_CLUSTER};
275
	} else {
276
		return EndOfChain{};
277
	}
278
}
279
280
// Write an entry to the FAT
281
void MSXtar::writeFAT(Cluster cluster, FatCluster value)
282
{
283
	assert(!fatBuffer.empty()); // FAT must already be cached
284
	assert(cluster.index < clusterCount);
285
286
	// Be strict when writing:
287
	// * Anything but FREE, END_OF_CHAIN or a valid cluster number is rejected.
288
	assert(!std::holds_alternative<Cluster>(value) || std::get<Cluster>(value).index < clusterCount);
289
290
	std::span data{fatBuffer[0].raw.data(), sectorsPerFat * size_t(SECTOR_SIZE)};
291
292
	unsigned index = FAT::FIRST_CLUSTER + cluster.index;
293
294
	if (std::holds_alternative<Free>(value) && cluster < findFirstFreeClusterStart) {
295
		// update where findFirstFreeCluster() will start scanning
296
		findFirstFreeClusterStart = cluster;
297
	}
298
299
	if (fat16) {
300
		unsigned fatValue = std::visit(FAT16::ToClusterNumber{}, value);
301
		auto p = subspan<2>(data, index * 2);
302
		p[0] = narrow_cast<uint8_t>(fatValue);
303
		p[1] = narrow_cast<uint8_t>(fatValue >> 8);
304
	} else {
305
		unsigned fatValue = std::visit(FAT12::ToClusterNumber{}, value);
306
		auto p = subspan<2>(data, (index * 3) / 2);
307
		if (index & 1) {
308
			p[0] = narrow_cast<uint8_t>((p[0] & 0x0F) + (fatValue << 4));
309
			p[1] = narrow_cast<uint8_t>(fatValue >> 4);
310
		} else {
311
			p[0] = narrow_cast<uint8_t>(fatValue);
312
			p[1] = narrow_cast<uint8_t>((p[1] & 0xF0) + ((fatValue >> 8) & 0x0F));
313
		}
314
	}
315
	fatCacheDirty = true;
316
}
317
318
// Find the next cluster number marked as free in the FAT
319
// @throws When no more free clusters
320
Cluster MSXtar::findFirstFreeCluster()
321
{
322
	for (auto cluster : xrange(findFirstFreeClusterStart.index, clusterCount)) {
323
		if (readFAT({cluster}) == FatCluster(Free{})) {
324
			findFirstFreeClusterStart = {cluster};
325
			return findFirstFreeClusterStart;
326
		}
327
	}
328
	throw MSXException("Disk full.");
329
}
330
331
unsigned MSXtar::countFreeClusters() const
332
{
333
	return narrow<unsigned>(std::ranges::count_if(xrange(findFirstFreeClusterStart.index, clusterCount),
334
		[&](unsigned cluster) { return readFAT({cluster}) == FatCluster(Free{}); }));
335
}
336
337
// Get the next sector from a file or (root/sub)directory
338
// If no next sector then 0 is returned
339
unsigned MSXtar::getNextSector(unsigned sector) const
340
{
341
	assert(sector >= rootDirStart);
342
	if (sector < dataStart) {
343
		// sector is part of the root directory
344
		return (sector == dataStart - 1) ? 0 : sector + 1;
345
	}
346
	Cluster currCluster = sectorToCluster(sector);
347
	if (currCluster == sectorToCluster(sector + 1)) {
348
		// next sector of cluster
349
		return sector + 1;
350
	} else {
351
		// first sector in next cluster
352
		FatCluster nextCluster = readFAT(currCluster);
353
		return std::visit(overloaded{
354
			[](Free) { return 0u; /* Invalid entry in FAT chain. */ },
355
			[](EndOfChain) { return 0u; },
356
			[this](Cluster cluster) { return clusterToSector(cluster); }
357
		}, nextCluster);
358
	}
359
}
360
361
// Get start cluster from a directory entry.
362
DirCluster MSXtar::getStartCluster(const MSXDirEntry& entry) const
363
{
364
	// Be tolerant when reading:
365
	// * Anything but a valid cluster number is returned as FREE.
366
	unsigned cluster = entry.startCluster;
367
	if (cluster >= FAT::FIRST_CLUSTER && cluster < FAT::FIRST_CLUSTER + clusterCount) {
368
		return Cluster{cluster - FAT::FIRST_CLUSTER};
369
	} else {
370
		return Free{};
371
	}
372
}
373
374
// Set start cluster on a directory entry.
375
void MSXtar::setStartCluster(MSXDirEntry& entry, DirCluster cluster) const
376
{
377
	// Be strict when writing:
378
	// * Anything but FREE or a valid cluster number is rejected.
379
	assert(!std::holds_alternative<Cluster>(cluster) || std::get<Cluster>(cluster).index < clusterCount);
380
	if (fat16) {
381
		entry.startCluster = narrow<uint16_t>(std::visit(FAT16::ToClusterNumber{}, cluster));
382
	} else {
383
		entry.startCluster = narrow<uint16_t>(std::visit(FAT12::ToClusterNumber{}, cluster));
384
	}
385
}
386
387
// If there are no more free entries in a subdirectory, the subdir is
388
// expanded with an extra cluster. This function gets the free cluster,
389
// clears it and updates the fat for the subdir
390
// returns: the first sector in the newly appended cluster
391
// @throws When disk is full
392
unsigned MSXtar::appendClusterToSubdir(unsigned sector)
393
{
394
	Cluster nextCl = findFirstFreeCluster();
395
	unsigned nextSector = clusterToSector(nextCl);
396
397
	// clear this cluster
398
	SectorBuffer buf;
399
	std::ranges::fill(buf.raw, 0);
400
	for (auto i : xrange(sectorsPerCluster)) {
401
		writeLogicalSector(i + nextSector, buf);
402
	}
403
404
	Cluster curCl = sectorToCluster(sector);
405
	assert(readFAT(curCl) == FatCluster(EndOfChain{}));
406
	writeFAT(curCl, nextCl);
407
	writeFAT(nextCl, EndOfChain{});
408
	return nextSector;
409
}
410
411
412
// Returns the index of a free (or with deleted file) entry
413
//  In:  The current dir sector
414
//  Out: index number, if no index is found then -1 is returned
415
unsigned MSXtar::findUsableIndexInSector(unsigned sector)
416
{
417
	SectorBuffer buf;
418
	readLogicalSector(sector, buf);
419
420
	// find a not used (0x00) or delete entry (0xE5)
421
	for (auto i : xrange(DIR_ENTRIES_PER_SECTOR)) {
422
		if (buf.dirEntry[i].filename[0] == one_of(0x00, char(0xE5))) {
423
			return i;
424
		}
425
	}
426
	return unsigned(-1);
427
}
428
429
// This function returns the sector and dirIndex for a new directory entry
430
// if needed the involved subdirectory is expanded by an extra cluster
431
// returns: a DirEntry containing sector and index
432
// @throws When either root dir is full or disk is full
433
MSXtar::DirEntry MSXtar::addEntryToDir(unsigned sector)
434
{
435
	// this routine adds the msx name to a directory sector, if needed (and
436
	// possible) the directory is extened with an extra cluster
437
	DirEntry result;
438
	result.sector = sector;
439
440
	assert(sector >= rootDirStart);
441
	if (sector < dataStart) {
442
		// add to the root directory
443
		for (/* */ ; result.sector < dataStart; result.sector++) {
444
			result.index = findUsableIndexInSector(result.sector);
445
			if (result.index != unsigned(-1)) {
446
				return result;
447
			}
448
		}
449
		throw MSXException("Root directory full.");
450
451
	} else {
452
		// add to a subdir
453
		while (true) {
454
			result.index = findUsableIndexInSector(result.sector);
455
			if (result.index != unsigned(-1)) {
456
				return result;
457
			}
458
			unsigned nextSector = getNextSector(result.sector);
459
			if (nextSector == 0) {
460
				nextSector = appendClusterToSubdir(result.sector);
461
			}
462
			result.sector = nextSector;
463
		}
464
	}
465
}
466
467
// filters out unsupported characters and upper-cases
468
static char toFileNameChar(char a)
469
{
470
	if ((a >= 0x00 && a < 0x20) || a == one_of(
471
		' ', '"', '*', '+', ',', '.', '/', ':', ';', '<', '=', '>', '?', '[', '\\', ']', '|', 0x7F, 0xFF)) {
472
		a = '_';
473
	}
474
	return narrow<char>(toupper(a));
475
}
476
477
// Transform a long hostname in a 8.3 uppercase filename as used in the
478
// dirEntries on an MSX
479
FileName MSXtar::hostToMSXFileName(std::string_view hostName) const
480
{
481
	std::vector<uint8_t> hostMSXName = msxChars.utf8ToMsx(hostName, '_');
482
	std::string_view hostMSXNameView(std::bit_cast<char*>(hostMSXName.data()), hostMSXName.size());
483
	auto [hostDir, hostFile] = StringOp::splitOnLast(hostMSXNameView, '/');
484
485
	// handle special case '.' and '..' first
486
	FileName result;
487
	result.fill(' ');
488
	if (hostFile == one_of(".", "..")) {
489
		copy_to_range(hostFile, result);
490
		return result;
491
	}
492
493
	auto [file, ext] = StringOp::splitOnLast(hostFile, '.');
494
	if (file.empty()) std::swap(file, ext);
495
496
	StringOp::trimRight(file, ' ');
497
	StringOp::trimRight(ext,  ' ');
498
499
	// truncate to 8.3 characters, put in uppercase and create '_' if needed
500
	std::string fileS(file.substr(0, 8));
501
	std::string extS (ext .substr(0, 3));
502
	transform_in_place(fileS, toFileNameChar);
503
	transform_in_place(extS,  toFileNameChar);
504
505
	// add correct number of spaces
506
#if defined(__GNUC__) && !defined(__clang__)
507
#pragma GCC diagnostic push
508
#pragma GCC diagnostic ignored "-Wstringop-overflow"
509
#endif
510
	copy_to_range(fileS, subspan<8>(result, 0));
511
	copy_to_range(extS,  subspan<3>(result, 8));
512
#if defined(__GNUC__) && !defined(__clang__)
513
#pragma GCC diagnostic pop
514
#endif
515
	return result;
516
}
517
518
// This function creates a new MSX subdir with given date 'd' and time 't'
519
// in the subdir pointed at by 'sector'. In the newly
520
// created subdir the entries for '.' and '..' are created
521
// returns: the first sector of the new subdir
522
// @throws in case no directory could be created
523
unsigned MSXtar::addSubdir(
524
	const FileName& msxName, uint16_t t, uint16_t d, unsigned sector)
525
{
526
	// returns the sector for the first cluster of this subdir
527
	DirEntry result = addEntryToDir(sector);
528
529
	// load the sector
530
	SectorBuffer buf;
531
	readLogicalSector(result.sector, buf);
532
533
	auto& dirEntry = buf.dirEntry[result.index];
534
	copy_to_range(msxName, dirEntry.filename);
535
	dirEntry.attrib = MSXDirEntry::Attrib::DIRECTORY;
536
	dirEntry.time = t;
537
	dirEntry.date = d;
538
539
	// dirEntry.filesize = fsize;
540
	Cluster curCl = findFirstFreeCluster();
541
	setStartCluster(dirEntry, curCl);
542
	writeFAT(curCl, EndOfChain{});
543
544
	// save the sector again
545
	writeLogicalSector(result.sector, buf);
546
547
	// clear this cluster
548
	unsigned logicalSector = clusterToSector(curCl);
549
	std::ranges::fill(buf.raw, 0);
550
	for (auto i : xrange(sectorsPerCluster)) {
551
		writeLogicalSector(i + logicalSector, buf);
552
	}
553
554
	// now add the '.' and '..' entries!!
555
	memset(&buf.dirEntry[0], 0, sizeof(MSXDirEntry));
556
	std::ranges::fill(buf.dirEntry[0].filename, ' ');
557
	buf.dirEntry[0].filename[0] = '.';
558
	buf.dirEntry[0].attrib = MSXDirEntry::Attrib::DIRECTORY;
559
	buf.dirEntry[0].time = t;
560
	buf.dirEntry[0].date = d;
561
	setStartCluster(buf.dirEntry[0], curCl);
562
563
	memset(&buf.dirEntry[1], 0, sizeof(MSXDirEntry));
564
	std::ranges::fill(buf.dirEntry[1].filename, ' ');
565
	buf.dirEntry[1].filename[0] = '.';
566
	buf.dirEntry[1].filename[1] = '.';
567
	buf.dirEntry[1].attrib = MSXDirEntry::Attrib::DIRECTORY;
568
	buf.dirEntry[1].time = t;
569
	buf.dirEntry[1].date = d;
570
	if (sector == rootDirStart) {
571
		setStartCluster(buf.dirEntry[1], Free{});
572
	} else {
573
		setStartCluster(buf.dirEntry[1], sectorToCluster(sector));
574
	}
575
576
	// and save this in the first sector of the new subdir
577
	writeLogicalSector(logicalSector, buf);
578
579
	return logicalSector;
580
}
581
582
// Get the time/date from a host file in MSX format
583
static DiskImageUtils::FatTimeDate getTimeDate(zstring_view filename)
584
{
585
	if (auto st = FileOperations::getStat(filename)) {
586
		// Some info indicates that st.st_mtime could be useless on win32 with vfat.
587
		// On Android 'st_mtime' is 'unsigned long' instead of 'time_t'
588
		// (like on linux), so we require a reinterpret_cast. That cast
589
		// is fine (but redundant) on linux.
590
		return DiskImageUtils::toTimeDate(reinterpret_cast<time_t&>(st->st_mtime));
591
	} else {
592
		// stat failed
593
		return {.time = 0, .date = 0};
594
	}
595
}
596
597
// Add an MSXsubdir with the time properties from the HOST-OS subdir
598
// @throws when subdir could not be created
599
unsigned MSXtar::addSubdirToDSK(zstring_view hostName, const FileName& msxName,
600
                                unsigned sector)
601
{
602
	auto [time, date] = getTimeDate(hostName);
603
	return addSubdir(msxName, time, date, sector);
604
}
605
606
// This file alters the filecontent of a given file
607
// It only changes the file content (and the filesize in the msxDirEntry)
608
// It doesn't changes timestamps nor filename, filetype etc.
609
// @throws when something goes wrong
610
void MSXtar::alterFileInDSK(MSXDirEntry& msxDirEntry, const std::string& hostName)
611
{
612
	// get host file size
613
	auto st = FileOperations::getStat(hostName);
614
	if (!st) {
615
		throw MSXException("Error reading host file: ", hostName);
616
	}
617
	auto hostSize = narrow<unsigned>(st->st_size);
618
	auto remaining = hostSize;
619
620
	// open host file for reading
621
	File file(hostName, "rb");
622
623
	// copy host file to image
624
	DirCluster prevCl = Free{};
625
	FatCluster curCl = std::visit(overloaded{
626
		[](Free) -> FatCluster { return EndOfChain{}; },
627
		[](Cluster cluster) -> FatCluster { return cluster; }
628
	}, getStartCluster(msxDirEntry));
629
630
	while (remaining) {
631
		Cluster cluster;
632
		// allocate new cluster if needed
633
		try {
634
			cluster = std::visit(overloaded{
635
				[](Free) -> Cluster { throw MSXException("Invalid entry in FAT chain."); },
636
				[&](EndOfChain) {
637
					Cluster newCl = findFirstFreeCluster();
638
					std::visit(overloaded{
639
						[&](Free) { setStartCluster(msxDirEntry, newCl); },
640
						[&](Cluster cluster_) { writeFAT(cluster_, newCl); }
641
					}, prevCl);
642
					writeFAT(newCl, EndOfChain{});
643
					return newCl;
644
				},
645
				[](Cluster cluster_) { return cluster_; }
646
			}, curCl);
647
		} catch (MSXException&) {
648
			// no more free clusters or invalid entry in FAT chain
649
			break;
650
		}
651
652
		// fill cluster
653
		unsigned logicalSector = clusterToSector(cluster);
654
		for (unsigned j = 0; (j < sectorsPerCluster) && remaining; ++j) {
655
			SectorBuffer buf;
656
			unsigned chunkSize = std::min(SECTOR_SIZE, remaining);
657
			file.read(subspan(buf.raw, 0, chunkSize));
658
			std::ranges::fill(subspan(buf.raw, chunkSize), 0);
659
			writeLogicalSector(logicalSector + j, buf);
660
			remaining -= chunkSize;
661
		}
662
663
		// advance to next cluster
664
		prevCl = cluster;
665
		curCl = readFAT(cluster);
666
	}
667
668
	// terminate FAT chain
669
	std::visit(overloaded{
670
		[&](Free free) { setStartCluster(msxDirEntry, free); },
671
		[&](Cluster cluster) { writeFAT(cluster, EndOfChain{}); }
672
	}, prevCl);
673
674
	// free rest of FAT chain
675
	freeFatChain(curCl);
676
677
	// write (possibly truncated) file size
678
	msxDirEntry.size = hostSize - remaining;
679
680
	if (remaining) {
681
		throw MSXException("Disk full, ", hostName, " truncated.");
682
	}
683
}
684
685
void MSXtar::freeFatChain(FAT::FatCluster startCluster)
686
{
687
	FatCluster curCl = startCluster;
688
	while (std::holds_alternative<Cluster>(curCl)) {
689
		Cluster cluster = std::get<Cluster>(curCl);
690
		FatCluster nextCl = readFAT(cluster);
691
		writeFAT(cluster, Free{});
692
		curCl = nextCl;
693
	}
694
}
695
696
std::string MSXtar::deleteEntry(const FAT::FileName& msxName, unsigned rootSector)
697
{
698
	SectorBuffer buf;
699
	DirEntry entry = findEntryInDir(msxName, rootSector, buf);
700
	if (entry.sector == 0) {
701
		// not found
702
		return "entry not found";
703
	}
704
	deleteEntry(buf.dirEntry[entry.index]);
705
	writeLogicalSector(entry.sector, buf);
706
	return "";
707
}
708
709
void MSXtar::deleteEntry(MSXDirEntry& msxDirEntry)
710
{
711
	DirCluster startCluster = getStartCluster(msxDirEntry);
712
713
	if (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
714
		// If we're deleting a directory then also (recursively)
715
		// delete the files/directories in this directory.
716
		if (const auto& msxName = msxDirEntry.filename;
717
		    std::ranges::equal(msxName, std::string_view(".          ")) ||
718
		    std::ranges::equal(msxName, std::string_view("..         "))) {
719
			// But skip the "." and ".." entries.
720
			return;
721
		}
722
		std::visit(overloaded{
723
			[](Free) { /* Points to root, ignore. */ },
724
			[&](Cluster cluster) { deleteDirectory(clusterToSector(cluster)); }
725
		}, startCluster);
726
	}
727
728
	// At this point we have a regular file or an empty subdirectory.
729
	// Delete it by marking the first filename char as 0xE5.
730
	msxDirEntry.filename[0] = char(0xE5);
731
732
	// Free the FAT chain
733
	std::visit(overloaded{
734
		[](Free) { /* Points to root, ignore. */ },
735
		[&](Cluster cluster) { freeFatChain(cluster); }
736
	}, startCluster);
737
738
	// Changed sector will be written by parent function
739
}
740
741
void MSXtar::deleteDirectory(unsigned sector)
742
{
743
	for (/* */ ; sector != 0; sector = getNextSector(sector)) {
744
		SectorBuffer buf;
745
		readLogicalSector(sector, buf);
746
		for (auto& dirEntry : buf.dirEntry) {
747
			if (dirEntry.filename[0] == char(0x00)) {
748
				return;
749
			}
750
			if (dirEntry.filename[0] == one_of(char(0xe5), '.') || dirEntry.attrib == T_MSX_LFN) {
751
				continue;
752
			}
753
			deleteEntry(dirEntry);
754
		}
755
		writeLogicalSector(sector, buf);
756
	}
757
}
758
759
std::string MSXtar::renameItem(std::string_view currentName, std::string_view newName)
760
{
761
	SectorBuffer buf;
762
763
	FileName newMsxName = hostToMSXFileName(newName);
764
	if (auto newEntry = findEntryInDir(newMsxName, chrootSector, buf);
765
	    newEntry.sector != 0) {
766
		return "another entry with new name already exists";
767
	}
768
769
	FileName oldMsxName = hostToMSXFileName(currentName);
770
	auto oldEntry = findEntryInDir(oldMsxName, chrootSector, buf);
771
	if (oldEntry.sector == 0) {
772
		return "entry not found";
773
	}
774
775
	buf.dirEntry[oldEntry.index].filename = newMsxName;
776
	writeLogicalSector(oldEntry.sector, buf);
777
	return "";
778
}
779
780
// Find the dir entry for 'name' in subdir starting at the given 'sector'
781
// with given 'index'
782
// returns: a DirEntry with sector and index filled in
783
//          sector is 0 if no match was found
784
MSXtar::DirEntry MSXtar::findEntryInDir(
785
	const FileName& msxName, unsigned sector, SectorBuffer& buf)
786
{
787
	DirEntry result;
788
	result.sector = sector;
789
	result.index = 0; // avoid warning (only some gcc versions complain)
790
	while (result.sector) {
791
		// read sector and scan 16 entries
792
		readLogicalSector(result.sector, buf);
793
		for (result.index = 0; result.index < DIR_ENTRIES_PER_SECTOR; ++result.index) {
794
			if (std::ranges::equal(buf.dirEntry[result.index].filename, msxName)) {
795
				return result;
796
			}
797
		}
798
		// try next sector
799
		result.sector = getNextSector(result.sector);
800
	}
801
	return result;
802
}
803
804
// Add file to the MSX disk in the subdir pointed to by 'sector'
805
// @throws when file could not be added
806
std::string MSXtar::addFileToDSK(const std::string& fullHostName, unsigned rootSector, Add add)
807
{
808
	auto [directory, hostName] = StringOp::splitOnLast(fullHostName, "/\\");
809
	FileName msxName = hostToMSXFileName(hostName);
810
811
	// first find out if the filename already exists in current dir
812
	SectorBuffer dummy;
813
	if (DirEntry fullMsxDirEntry = findEntryInDir(msxName, rootSector, dummy);
814
	    fullMsxDirEntry.sector != 0) {
815
		if (add == Add::PRESERVE) {
816
			return strCat("Warning: preserving entry ", hostName, '\n');
817
		} else {
818
			// first delete entry
819
			deleteEntry(dummy.dirEntry[fullMsxDirEntry.index]);
820
			writeLogicalSector(fullMsxDirEntry.sector, dummy);
821
		}
822
	}
823
824
	SectorBuffer buf;
825
	DirEntry entry = addEntryToDir(rootSector);
826
	readLogicalSector(entry.sector, buf);
827
	auto& dirEntry = buf.dirEntry[entry.index];
828
	memset(&dirEntry, 0, sizeof(dirEntry));
829
	copy_to_range(msxName, dirEntry.filename);
830
	dirEntry.attrib = MSXDirEntry::Attrib::REGULAR;
831
832
	// compute time/date stamps
833
	auto [time, date] = getTimeDate(fullHostName);
834
	dirEntry.time = time;
835
	dirEntry.date = date;
836
837
	try {
838
		alterFileInDSK(dirEntry, fullHostName);
839
	} catch (MSXException&) {
840
		// still write directory entry
841
		writeLogicalSector(entry.sector, buf);
842
		throw;
843
	}
844
	writeLogicalSector(entry.sector, buf);
845
	return {};
846
}
847
848
std::string MSXtar::addOrCreateSubdir(zstring_view hostDirName, unsigned sector, Add add)
849
{
850
	FileName msxFileName = hostToMSXFileName(hostDirName);
851
	auto printableFilename = msxToHostFileName(msxFileName);
852
	SectorBuffer buf;
853
	if (DirEntry entry = findEntryInDir(msxFileName, sector, buf);
854
	    entry.sector != 0) {
855
		// entry already exists ..
856
		auto& msxDirEntry = buf.dirEntry[entry.index];
857
		if (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
858
			// .. and is a directory
859
			DirCluster nextCluster = getStartCluster(msxDirEntry);
860
			return std::visit(overloaded{
861
				[&](Free) { return strCat("Directory ", printableFilename, " goes to root.\n"); },
862
				[&](Cluster cluster) { return recurseDirFill(hostDirName, clusterToSector(cluster), add); }
863
			}, nextCluster);
864
		}
865
		// .. but is NOT a directory
866
		if (add == Add::PRESERVE) {
867
			return strCat("MSX file ", printableFilename, " is not a directory.\n");
868
		} else {
869
			// first delete existing file
870
			deleteEntry(msxDirEntry);
871
			writeLogicalSector(entry.sector, buf);
872
		}
873
	}
874
	// add new directory
875
	unsigned nextSector = addSubdirToDSK(hostDirName, msxFileName, sector);
876
	return recurseDirFill(hostDirName, nextSector, add);
877
}
878
879
// Transfer directory and all its subdirectories to the MSX disk image
880
// @throws when an error occurs
881
std::string MSXtar::recurseDirFill(std::string_view dirName, unsigned sector, Add add)
882
{
883
	std::string messages;
884
885
	auto fileAction = [&](const std::string& path) {
886
		// add new file
887
		messages += addFileToDSK(path, sector, add);
888
	};
889
	auto dirAction = [&](const std::string& path) {
890
		// add new directory (+ recurse)
891
		messages += addOrCreateSubdir(path, sector, add);
892
	};
893
	foreach_file_and_directory(std::string(dirName), fileAction, dirAction);
894
895
	return messages;
896
}
897
898
899
std::string MSXtar::msxToHostFileName(const FileName& msxName) const
900
{
901
	std::string result;
902
	for (unsigned i = 0; i < 8 && msxName[i] != ' '; ++i) {
903
		result += char(tolower(msxName[i]));
904
	}
905
	if (msxName[8] != ' ') {
906
		result += '.';
907
		for (unsigned i = 8; i < 11 && msxName[i] != ' '; ++i) {
908
			result += char(tolower(msxName[i]));
909
		}
910
	}
911
	std::span<const uint8_t> resultSpan(std::bit_cast<const uint8_t*>(result.data()), result.size());
912
	return msxChars.msxToUtf8(resultSpan, '_');
913
}
914
915
916
// Set the entries from dirEntry to the timestamp of resultFile
917
static void changeTime(zstring_view resultFile, const MSXDirEntry& dirEntry)
918
{
919
	unsigned t = dirEntry.time;
920
	unsigned d = dirEntry.date;
921
	struct tm mTim;
922
	struct utimbuf uTim;
923
	mTim.tm_sec   = narrow<int>((t & 0x001f) << 1);
924
	mTim.tm_min   = narrow<int>((t & 0x07e0) >> 5);
925
	mTim.tm_hour  = narrow<int>((t & 0xf800) >> 11);
926
	mTim.tm_mday  = narrow<int>( (d & 0x001f) >> 0);
927
	mTim.tm_mon   = narrow<int>(((d & 0x01e0) >> 5) - 1);
928
	mTim.tm_year  = narrow<int>(((d & 0xfe00) >> 9) + 80);
929
	mTim.tm_isdst = -1;
930
	uTim.actime  = mktime(&mTim);
931
	uTim.modtime = mktime(&mTim);
932
	utime(resultFile.c_str(), &uTim);
933
}
934
935
TclObject MSXtar::dirRaw()
936
{
937
	TclObject result;
938
	for (unsigned sector = chrootSector; sector != 0; sector = getNextSector(sector)) {
939
		SectorBuffer buf;
940
		readLogicalSector(sector, buf);
941
		for (auto& dirEntry : buf.dirEntry) {
942
			if (dirEntry.filename[0] == char(0x00)) {
943
				return result;
944
			}
945
			if (dirEntry.filename[0] == char(0xe5) || dirEntry.attrib == T_MSX_LFN) {
946
				continue;
947
			}
948
949
			auto filename = msxToHostFileName(dirEntry.filename);
950
			time_t time = DiskImageUtils::fromTimeDate({.time = dirEntry.time, .date = dirEntry.date});
951
			result.addListElement(makeTclList(filename, dirEntry.attrib.value, narrow<uint32_t>(time), dirEntry.size));
952
		}
953
	}
954
	return result;
955
}
956
957
std::string MSXtar::dir()
958
{
959
	std::string result;
960
	auto list = dirRaw();
961
	for (auto i : xrange(list.size())) {
962
		auto entry = list.getListIndexUnchecked(i);
963
		auto filename = std::string(entry.getListIndexUnchecked(0).getString());
964
		auto attrib = DiskImageUtils::formatAttrib(MSXDirEntry::AttribValue(uint8_t(entry.getListIndexUnchecked(1).getOptionalInt().value_or(0))));
965
		//time_t time = entry.getListIndexUnchecked(2).getOptionalInt().value_or(0); // ignored
966
		auto size = entry.getListIndexUnchecked(3).getOptionalInt().value_or(0);
967
968
		filename.resize(13, ' '); // filename first (in condensed form for human readability)
969
		strAppend(result, filename, attrib, "  ", size, '\n');
970
	}
971
	return result;
972
}
973
974
// routines to update the global vars: chrootSector
975
void MSXtar::chdir(std::string_view newRootDir)
976
{
977
	chroot(newRootDir, false);
978
}
979
980
void MSXtar::mkdir(std::string_view newRootDir)
981
{
982
	unsigned tmpMSXchrootSector = chrootSector;
983
	chroot(newRootDir, true);
984
	chrootSector = tmpMSXchrootSector;
985
}
986
987
void MSXtar::chroot(std::string_view newRootDir, bool createDir)
988
{
989
	if (newRootDir.starts_with('/') || newRootDir.starts_with('\\')) {
990
		// absolute path, reset chrootSector
991
		chrootSector = rootDirStart;
992
		StringOp::trimLeft(newRootDir, "/\\");
993
	}
994
995
	while (!newRootDir.empty()) {
996
		auto [firstPart, lastPart] = StringOp::splitOnFirst(newRootDir, "/\\");
997
		newRootDir = lastPart;
998
		StringOp::trimLeft(newRootDir, "/\\");
999
1000
		// find 'firstPart' directory or create it if requested
1001
		SectorBuffer buf;
1002
		FileName msxName = hostToMSXFileName(firstPart);
1003
		DirEntry entry = findEntryInDir(msxName, chrootSector, buf);
1004
		if (entry.sector == 0) {
1005
			if (!createDir) {
1006
				throw MSXException("Subdirectory ", firstPart,
1007
				                   " not found.");
1008
			}
1009
			// creat new subdir
1010
			time_t now;
1011
			time(&now);
1012
			auto [t, d] = DiskImageUtils::toTimeDate(now);
1013
			chrootSector = addSubdir(msxName, t, d, chrootSector);
1014
		} else {
1015
			const auto& dirEntry = buf.dirEntry[entry.index];
1016
			if (!(dirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY)) {
1017
				throw MSXException(firstPart, " is not a directory.");
1018
			}
1019
			DirCluster chrootCluster = getStartCluster(dirEntry);
1020
			chrootSector = std::visit(overloaded{
1021
				[this](Free) { return rootDirStart; },
1022
				[this](Cluster cluster) { return clusterToSector(cluster); }
1023
			}, chrootCluster);
1024
		}
1025
	}
1026
}
1027
1028
void MSXtar::fileExtract(const std::string& resultFile, const MSXDirEntry& dirEntry)
1029
{
1030
	unsigned size = dirEntry.size;
1031
	unsigned sector = std::visit(overloaded{
1032
		[](Free) { return 0u; },
1033
		[this](Cluster cluster) { return clusterToSector(cluster); }
1034
	}, getStartCluster(dirEntry));
1035
1036
	File file(resultFile, "wb");
1037
	while (size && sector) {
1038
		SectorBuffer buf;
1039
		readLogicalSector(sector, buf);
1040
		unsigned saveSize = std::min(size, SECTOR_SIZE);
1041
		file.write(subspan(buf.raw, 0, saveSize));
1042
		size -= saveSize;
1043
		sector = getNextSector(sector);
1044
	}
1045
	// now change the access time
1046
	changeTime(resultFile, dirEntry);
1047
}
1048
1049
// extracts a single item (file or directory) from the msx image to the host OS
1050
std::string MSXtar::singleItemExtract(std::string_view dirName, std::string_view itemName,
1051
                                 unsigned sector)
1052
{
1053
	// first find out if the filename exists in current dir
1054
	SectorBuffer buf;
1055
	FileName msxName = hostToMSXFileName(itemName);
1056
	DirEntry entry = findEntryInDir(msxName, sector, buf);
1057
	if (entry.sector == 0) {
1058
		return strCat(itemName, " not found!\n");
1059
	}
1060
1061
	const auto& msxDirEntry = buf.dirEntry[entry.index];
1062
	// create full name for local filesystem
1063
	std::string fullName = strCat(dirName, '/', msxToHostFileName(msxDirEntry.filename));
1064
1065
	// ...and extract
1066
	if  (msxDirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
1067
		// recursive extract this subdir
1068
		FileOperations::mkdirp(fullName);
1069
		DirCluster nextCluster = getStartCluster(msxDirEntry);
1070
		std::visit(overloaded{
1071
			[](Free) { /* Points to root, ignore. */},
1072
			[&](Cluster cluster) { recurseDirExtract(fullName, clusterToSector(cluster)); }
1073
		}, nextCluster);
1074
	} else {
1075
		// it is a file
1076
		fileExtract(fullName, msxDirEntry);
1077
	}
1078
	return {};
1079
}
1080
1081
1082
// extracts the contents of the directory (at sector) and all its subdirs to the host OS
1083
void MSXtar::recurseDirExtract(std::string_view dirName, unsigned sector)
1084
{
1085
	for (/* */ ; sector != 0; sector = getNextSector(sector)) {
1086
		SectorBuffer buf;
1087
		readLogicalSector(sector, buf);
1088
		for (auto& dirEntry : buf.dirEntry) {
1089
			if (dirEntry.filename[0] == char(0x00)) {
1090
				return;
1091
			}
1092
			if (dirEntry.filename[0] == one_of(char(0xe5), '.') || dirEntry.attrib == T_MSX_LFN) {
1093
				continue;
1094
			}
1095
			std::string filename = msxToHostFileName(dirEntry.filename);
1096
			std::string fullName = filename;
1097
			if (!dirName.empty()) {
1098
				fullName = strCat(dirName, '/', filename);
1099
			}
1100
			if (dirEntry.attrib & MSXDirEntry::Attrib::DIRECTORY) {
1101
				FileOperations::mkdirp(fullName);
1102
				// now change the access time
1103
				changeTime(fullName, dirEntry);
1104
				std::visit(overloaded{
1105
					[](Free) { /* Points to root, ignore. */ },
1106
					[&](Cluster cluster) { recurseDirExtract(fullName, clusterToSector(cluster)); }
1107
				}, getStartCluster(dirEntry));
1108
			} else {
1109
				fileExtract(fullName, dirEntry);
1110
			}
1111
		}
1112
	}
1113
}
1114
1115
std::string MSXtar::addItem(const std::string& hostItemName, Add add)
1116
{
1117
	if (auto stat = FileOperations::getStat(hostItemName)) {
1118
		if (FileOperations::isRegularFile(*stat)) {
1119
			return addFileToDSK(hostItemName, chrootSector, add);
1120
		} else if (FileOperations::isDirectory(hostItemName)) {
1121
			return addOrCreateSubdir(hostItemName, chrootSector, add);
1122
		}
1123
	}
1124
	return "";
1125
}
1126
1127
std::string MSXtar::addDir(std::string_view rootDirName, Add add)
1128
{
1129
	return recurseDirFill(rootDirName, chrootSector, add);
1130
}
1131
1132
std::string MSXtar::addFile(const std::string& filename, Add add)
1133
{
1134
	return addFileToDSK(filename, chrootSector, add);
1135
}
1136
1137
std::string MSXtar::getItemFromDir(std::string_view rootDirName, std::string_view itemName)
1138
{
1139
	return singleItemExtract(rootDirName, itemName, chrootSector);
1140
}
1141
1142
void MSXtar::getDir(std::string_view rootDirName)
1143
{
1144
	recurseDirExtract(rootDirName, chrootSector);
1145
}
1146
1147
std::string MSXtar::deleteItem(std::string_view itemName)
1148
{
1149
	FileName msxName = hostToMSXFileName(itemName);
1150
	return deleteEntry(msxName, chrootSector);
1151
}
1152
1153
std::string MSXtar::convertToMsxName(std::string_view name) const
1154
{
1155
	FileName msxName = hostToMSXFileName(name);
1156
	return msxToHostFileName(msxName);
1157
}
1158
1159
MSXtar::FreeSpaceResult MSXtar::getFreeSpace() const
1160
{
1161
	return {.numFreeClusters = countFreeClusters(),
1162
		.clusterSize = sectorsPerCluster * SECTOR_SIZE};
1163
}
1164
1165
} // namespace openmsx