OXYGENHUB
◆ main openmsx / src / fdc / DiskImageUtils.cc
21.7 KiB · C++ · last changed chore(): initial commit 1 month ago source blame history raw
1
#include "DiskImageUtils.hh"
2
3
#include "BootBlocks.hh"
4
#include "CommandException.hh"
5
#include "DiskPartition.hh"
6
7
#include "endian.hh"
8
#include "enumerate.hh"
9
#include "one_of.hh"
10
#include "random.hh"
11
#include "strCat.hh"
12
#include "xrange.hh"
13
14
#include <algorithm>
15
#include <bit>
16
#include <cassert>
17
#include <ctime>
18
#include <ranges>
19
20
namespace openmsx::DiskImageUtils {
21
22
static constexpr unsigned DOS1_MAX_CLUSTER_COUNT = 0x3FE;  // Max 3 sectors per FAT
23
static constexpr unsigned FAT12_MAX_CLUSTER_COUNT = 0xFF4;
24
static constexpr unsigned FAT16_MAX_CLUSTER_COUNT = 0xFFF4;
25
static constexpr unsigned SECTOR_SIZE = sizeof(SectorBuffer);
26
static constexpr unsigned DIR_ENTRIES_PER_SECTOR = SECTOR_SIZE / sizeof(MSXDirEntry);
27
28
enum class PartitionTableType : uint8_t {
29
	SUNRISE_IDE,
30
	NEXTOR
31
};
32
33
static constexpr std::array<char, 11> SUNRISE_PARTITION_TABLE_HEADER = {
34
	'\353', '\376', '\220', 'M', 'S', 'X', '_', 'I', 'D', 'E', ' '
35
};
36
static constexpr std::array<char, 11> NEXTOR_PARTITION_TABLE_HEADER = {
37
	'\353', '\376', '\220', 'N', 'E', 'X', 'T', 'O', 'R', '2', '0'
38
};
39
[[nodiscard]] static std::optional<PartitionTableType> getPartitionTableType(const SectorBuffer& buf)
40
{
41
	if (buf.ptSunrise.header == SUNRISE_PARTITION_TABLE_HEADER) {
42
		return PartitionTableType::SUNRISE_IDE;
43
	} else if (buf.ptNextor.header == NEXTOR_PARTITION_TABLE_HEADER &&
44
	           buf.ptNextor.part[0].sys_ind != 0 && // Extra checks to distinguish MBR from VBR
45
	           buf.ptNextor.part[0].start == 1) {   // since they have the same OEM signature.
46
		return PartitionTableType::NEXTOR;
47
	}
48
	return {};
49
}
50
51
bool hasPartitionTable(SectorAccessibleDisk& disk)
52
{
53
	SectorBuffer buf;
54
	disk.readSector(0, buf);
55
	return getPartitionTableType(buf).has_value();
56
}
57
58
// Get partition from Nextor extended boot record (standard EBR) chain.
59
static Partition& getPartitionNextorExtended(
60
	SectorAccessibleDisk& disk, unsigned partition, SectorBuffer& buf,
61
	unsigned remaining, unsigned ebrOuterSector)
62
{
63
	unsigned ebrSector = ebrOuterSector;
64
	while (true) {
65
		disk.readSector(ebrSector, buf);
66
67
		if (remaining == 0) {
68
			// EBR partition entry. Start is relative to *this* EBR sector.
69
			auto& p = buf.ptNextor.part[0];
70
			if (p.start == 0) {
71
				break;
72
			}
73
			// Adjust to absolute address before returning.
74
			p.start = ebrSector + p.start;
75
			return p;
76
		}
77
78
		// EBR link entry. Start is relative to *outermost* EBR sector.
79
		const auto& link = buf.ptNextor.part[1];
80
		if (link.start == 0) {
81
			break;
82
		} else if (link.sys_ind != one_of(0x05, 0x0F)) {
83
			throw CommandException("Invalid extended boot record.");
84
		}
85
		ebrSector = ebrOuterSector + link.start;
86
		remaining--;
87
	}
88
	throw CommandException("No partition number ", partition);
89
}
90
91
// Get partition from Nextor master boot record (standard MBR).
92
static Partition& getPartitionNextor(
93
	SectorAccessibleDisk& disk, unsigned partition, SectorBuffer& buf)
94
{
95
	unsigned remaining = partition - 1;
96
	for (auto& p : buf.ptNextor.part) {
97
		if (p.start == 0) {
98
			break;
99
		} else if (p.sys_ind == one_of(0x05, 0x0F)) {
100
			return getPartitionNextorExtended(disk, partition, buf, remaining, p.start);
101
		} else if (remaining == 0) {
102
			return p;
103
		}
104
		remaining--;
105
	}
106
	throw CommandException("No partition number ", partition);
107
}
108
109
// Get partition from Sunrise IDE master boot record.
110
static Partition& getPartitionSunrise(unsigned partition, SectorBuffer& buf)
111
{
112
	// check number in range
113
	if (partition < 1 || partition > buf.ptSunrise.part.size()) {
114
		throw CommandException(
115
			"Invalid partition number specified (must be 1-",
116
			buf.ptSunrise.part.size(), ").");
117
	}
118
	// check valid partition number
119
	auto& p = buf.ptSunrise.part[buf.ptSunrise.part.size() - partition];
120
	if (p.start == 0) {
121
		throw CommandException("No partition number ", partition);
122
	}
123
	return p;
124
}
125
126
Partition& getPartition(SectorAccessibleDisk& disk, unsigned partition, SectorBuffer& buf)
127
{
128
	// check drive has a partition table
129
	// check valid partition number and return the entry
130
	disk.readSector(0, buf);
131
	auto partitionTableType = getPartitionTableType(buf);
132
	if (partitionTableType == PartitionTableType::SUNRISE_IDE) {
133
		return getPartitionSunrise(partition, buf);
134
	} else if (partitionTableType == PartitionTableType::NEXTOR) {
135
		return getPartitionNextor(disk, partition, buf);
136
	} else {
137
		throw CommandException("No (or invalid) partition table.");
138
	}
139
}
140
141
void checkSupportedPartition(SectorAccessibleDisk& disk, unsigned partition)
142
{
143
	SectorBuffer buf;
144
	const Partition& p = getPartition(disk, partition, buf);
145
146
	// check partition type
147
	if (p.sys_ind != one_of(0x01, 0x04, 0x06, 0x0E)) {
148
		throw CommandException("Only FAT12 and FAT16 partitions are supported.");
149
	}
150
}
151
152
153
// Create a correct boot sector depending on the required size of the filesystem
154
struct SetBootSectorResult {
155
	unsigned sectorsPerFat;
156
	unsigned fatCount;
157
	unsigned fatStart;
158
	unsigned rootDirStart;
159
	unsigned dataStart;
160
	uint8_t descriptor;
161
	bool fat16;
162
};
163
static SetBootSectorResult setBootSector(
164
	MSXBootSector& boot, MSXBootSectorType bootType, size_t nbSectors)
165
{
166
	// start from the default boot block ..
167
	if (bootType == MSXBootSectorType::DOS1) {
168
		boot = BootBlocks::dos1BootBlock.bootSector;
169
	} else if (bootType == MSXBootSectorType::DOS2) {
170
		boot = BootBlocks::dos2BootBlock.bootSector;
171
	} else if (bootType == MSXBootSectorType::NEXTOR && nbSectors > 0x10000) {
172
		boot = BootBlocks::nextorBootBlockFAT16.bootSector;
173
	} else if (bootType == MSXBootSectorType::NEXTOR) {
174
		boot = BootBlocks::nextorBootBlockFAT12.bootSector;
175
	} else {
176
		UNREACHABLE;
177
	}
178
179
	// .. and fill-in image-size dependent parameters ..
180
	// these are the same for most formats
181
	uint8_t nbReservedSectors = 1;
182
	uint8_t nbHiddenSectors = 0;
183
	uint32_t vol_id = random_32bit() & 0x7F7F7F7F; // why are bits masked?;
184
185
	// all these are set below (but initialize here to avoid warning)
186
	uint16_t nbSides = 2;
187
	uint8_t nbFats = 2;
188
	uint16_t nbSectorsPerFat = 3;
189
	uint8_t nbSectorsPerCluster = 2;
190
	uint16_t nbDirEntry = 112;
191
	uint8_t descriptor = 0xF9;
192
	bool fat16 = false;
193
194
	// now set correct info according to size of image (in sectors!)
195
	if (bootType == MSXBootSectorType::NEXTOR && nbSectors > 0x10000) {
196
		// using the same layout as Nextor 2.1.1’s FDISK
197
		nbSides = 0;
198
		nbFats = 2;
199
		nbDirEntry = 512;
200
		descriptor = 0xF0;
201
		fat16 = true;
202
203
		// <= 128 MB: 4, <= 256 MB: 8, ..., <= 4 GB: 128
204
		nbSectorsPerCluster = narrow<uint8_t>(std::clamp(std::bit_ceil(nbSectors) >> 16, 4uz, 128uz));
205
206
		// Calculate fat size based on cluster count estimate
207
		unsigned fatStart = nbReservedSectors + nbDirEntry / DIR_ENTRIES_PER_SECTOR;
208
		auto estSectorCount = narrow<unsigned>(nbSectors - fatStart);
209
		auto estClusterCount = std::min(estSectorCount / nbSectorsPerCluster, FAT16_MAX_CLUSTER_COUNT);
210
		auto fatSize = 2 * (estClusterCount + 2);
211
		nbSectorsPerFat = narrow<uint16_t>((fatSize + SECTOR_SIZE - 1) / SECTOR_SIZE);  // round up
212
213
		// Adjust sectors count down to match cluster count
214
		auto dataStart = fatStart + nbFats * nbSectorsPerFat;
215
		auto dataSectorCount = narrow<unsigned>(nbSectors - dataStart);
216
		auto clusterCount = std::min(dataSectorCount / nbSectorsPerCluster, FAT16_MAX_CLUSTER_COUNT);
217
		nbSectors = dataStart + clusterCount * nbSectorsPerCluster;
218
	} else if (bootType == MSXBootSectorType::NEXTOR) {
219
		// using the same layout as Nextor 2.1.1’s FDISK
220
		nbSides = 0;
221
		nbFats = 2;
222
		nbDirEntry = 112;
223
		descriptor = 0xF0;
224
225
		// <= 2 MB: 1, <= 4 MB: 2, ..., <= 32 MB: 16
226
		nbSectorsPerCluster = narrow<uint8_t>(std::clamp(std::bit_ceil(nbSectors) >> 12, 1uz, 16uz));
227
228
		// Partitions <= 16 MB are defined to have at most 3 sectors per FAT,
229
		// so that they can boot DOS 1. This limits the cluster count to 1022.
230
		// And for some unknown reason, Nextor limits it to one less than that.
231
		unsigned maxClusterCount = FAT12_MAX_CLUSTER_COUNT;
232
		if (nbSectors <= 0x8000) {
233
			maxClusterCount = DOS1_MAX_CLUSTER_COUNT - 1;
234
			nbSectorsPerCluster *= 4; // <= 2 MB: 4, <= 4 MB: 8, ..., <= 16 MB: 32
235
		}
236
237
		// Calculate fat size based on cluster count estimate
238
		unsigned fatStart = nbReservedSectors + nbDirEntry / DIR_ENTRIES_PER_SECTOR;
239
		auto estSectorCount = narrow<unsigned>(nbSectors - fatStart);
240
		auto estClusterCount = std::min(estSectorCount / nbSectorsPerCluster, maxClusterCount);
241
		auto fatSize = (3 * (estClusterCount + 2) + 1) / 2;  // round up
242
		nbSectorsPerFat = narrow<uint16_t>((fatSize + SECTOR_SIZE - 1) / SECTOR_SIZE);  // round up
243
244
		// Adjust sectors count down to match cluster count
245
		auto dataStart = fatStart + nbFats * nbSectorsPerFat;
246
		auto dataSectorCount = narrow<unsigned>(nbSectors - dataStart);
247
		auto clusterCount = std::min(dataSectorCount / nbSectorsPerCluster, maxClusterCount);
248
		nbSectors = dataStart + clusterCount * nbSectorsPerCluster;
249
	} else if (bootType == MSXBootSectorType::DOS1 && nbSectors > 1440) {
250
		// DOS1 supports up to 3 sectors per FAT, limiting the cluster count to 1022.
251
		nbSides = 0;
252
		nbFats = 2;
253
		nbDirEntry = 112;
254
		descriptor = 0xF0;
255
256
		// <= 1 MB: 2, <= 2 MB: 4, ..., <= 32 MB: 64
257
		nbSectorsPerCluster = narrow<uint8_t>(std::clamp(std::bit_ceil(nbSectors) >> 10, 2uz, 64uz));
258
259
		// Calculate fat size based on cluster count estimate
260
		unsigned fatStart = nbReservedSectors + nbDirEntry / DIR_ENTRIES_PER_SECTOR;
261
		auto estSectorCount = narrow<unsigned>(nbSectors - fatStart);
262
		auto estClusterCount = std::min(estSectorCount / nbSectorsPerCluster, DOS1_MAX_CLUSTER_COUNT);
263
		auto fatSize = (3 * (estClusterCount + 2) + 1) / 2;  // round up
264
		nbSectorsPerFat = narrow<uint16_t>((fatSize + SECTOR_SIZE - 1) / SECTOR_SIZE);  // round up
265
266
		// Adjust sectors count down to match cluster count
267
		auto dataStart = fatStart + nbFats * nbSectorsPerFat;
268
		auto dataSectorCount = narrow<unsigned>(nbSectors - dataStart);
269
		auto clusterCount = std::min(dataSectorCount / nbSectorsPerCluster, DOS1_MAX_CLUSTER_COUNT);
270
		nbSectors = dataStart + clusterCount * nbSectorsPerCluster;
271
	} else if (nbSectors > 32732) {
272
		// using the same layout as used by Jon in IDEFDISK v 3.1
273
		// 32732 < nbSectors
274
		// note: this format is only valid for nbSectors <= 65536
275
		nbSides = 32;		// copied from a partition from an IDE HD
276
		nbFats = 2;
277
		nbSectorsPerFat = 12;	// copied from a partition from an IDE HD
278
		nbSectorsPerCluster = 16;
279
		nbDirEntry = 256;
280
		descriptor = 0xF0;
281
		// for a 32MB disk or greater the sectors would be >= 65536
282
		// since MSX use 16 bits for this, in case of sectors = 65536
283
		// the truncated word will be 0 -> formatted as 320 Kb disk!
284
		nbSectors = std::min(nbSectors, 65535uz); // this is the max size for fat12 :-)
285
	} else if (nbSectors > 16388) {
286
		// using the same layout as used by Jon in IDEFDISK v 3.1
287
		// 16388 < nbSectors <= 32732
288
		nbSides = 2;		// unknown yet
289
		nbFats = 2;
290
		nbSectorsPerFat = 12;
291
		nbSectorsPerCluster = 8;
292
		nbDirEntry = 256;
293
		descriptor = 0XF0;
294
	} else if (nbSectors > 8212) {
295
		// using the same layout as used by Jon in IDEFDISK v 3.1
296
		// 8212 < nbSectors <= 16388
297
		nbSides = 2;		// unknown yet
298
		nbFats = 2;
299
		nbSectorsPerFat = 12;
300
		nbSectorsPerCluster = 4;
301
		nbDirEntry = 256;
302
		descriptor = 0xF0;
303
	} else if (nbSectors > 4126) {
304
		// using the same layout as used by Jon in IDEFDISK v 3.1
305
		// 4126 < nbSectors <= 8212
306
		nbSides = 2;		// unknown yet
307
		nbFats = 2;
308
		nbSectorsPerFat = 12;
309
		nbSectorsPerCluster = 2;
310
		nbDirEntry = 256;
311
		descriptor = 0xF0;
312
	} else if (nbSectors > 2880) {
313
		// using the same layout as used by Jon in IDEFDISK v 3.1
314
		// 2880 < nbSectors <= 4126
315
		nbSides = 2;		// unknown yet
316
		nbFats = 2;
317
		nbSectorsPerFat = 6;
318
		nbSectorsPerCluster = 2;
319
		nbDirEntry = 224;
320
		descriptor = 0xF0;
321
	} else if (nbSectors > 1440) {
322
		// using the same layout as used by Jon in IDEFDISK v 3.1
323
		// 1440 < nbSectors <= 2880
324
		nbSides = 2;		// unknown yet
325
		nbFats = 2;
326
		nbSectorsPerFat = 5;
327
		nbSectorsPerCluster = 2;
328
		nbDirEntry = 112;
329
		descriptor = 0xF0;
330
	} else if (nbSectors > 720) {
331
		// normal double sided disk
332
		// 720 < nbSectors <= 1440
333
		nbSides = 2;
334
		nbFats = 2;
335
		nbSectorsPerFat = 3;
336
		nbSectorsPerCluster = 2;
337
		nbDirEntry = 112;
338
		descriptor = 0xF9;
339
		nbSectors = 1440;	// force nbSectors to 1440, why?
340
	} else {
341
		// normal single sided disk
342
		// nbSectors <= 720
343
		nbSides = 1;
344
		nbFats = 2;
345
		nbSectorsPerFat = 2;
346
		nbSectorsPerCluster = 2;
347
		nbDirEntry = 112;
348
		descriptor = 0xF8;
349
		nbSectors = 720;	// force nbSectors to 720, why?
350
	}
351
352
	assert(nbDirEntry % 16 == 0);  // Non multiples of 16 not supported.
353
354
	if (nbSectors < 0x10000) {
355
		boot.nrSectors = narrow<uint16_t>(nbSectors);
356
	} else if (bootType == MSXBootSectorType::NEXTOR && fat16) {
357
		boot.nrSectors = 0;
358
	} else {
359
		throw CommandException("Too many sectors for FAT12 ", nbSectors);
360
	}
361
362
	boot.nrSides = nbSides;
363
	boot.spCluster = nbSectorsPerCluster;
364
	boot.nrFats = nbFats;
365
	boot.sectorsFat = nbSectorsPerFat;
366
	boot.dirEntries = nbDirEntry;
367
	boot.descriptor = descriptor;
368
	boot.resvSectors = nbReservedSectors;
369
370
	if (bootType == MSXBootSectorType::DOS1) {
371
		auto& params = boot.params.dos1;
372
		params.hiddenSectors = nbHiddenSectors;
373
	} else if (bootType == MSXBootSectorType::DOS2 || (bootType == MSXBootSectorType::NEXTOR && !fat16)) {
374
		auto& params = boot.params.dos2;
375
		params.hiddenSectors = nbHiddenSectors;
376
		params.vol_id = vol_id;
377
	} else if (bootType == MSXBootSectorType::NEXTOR && fat16) {
378
		auto& params = boot.params.extended;
379
		if (nbSectors <= 0x80'0000) {
380
			params.nrSectors = narrow<unsigned>(nbSectors);
381
		} else {
382
			throw CommandException("Too many sectors for FAT16 ", nbSectors);
383
		}
384
		params.hiddenSectors = nbHiddenSectors;
385
		params.vol_id = vol_id;
386
	} else {
387
		UNREACHABLE;
388
	}
389
390
	SetBootSectorResult result;
391
	result.sectorsPerFat = nbSectorsPerFat;
392
	result.fatCount = nbFats;
393
	result.fatStart = nbReservedSectors;
394
	result.rootDirStart = result.fatStart + nbFats * nbSectorsPerFat;
395
	result.dataStart = result.rootDirStart + nbDirEntry / 16;
396
	result.descriptor = descriptor;
397
	result.fat16 = fat16;
398
	return result;
399
}
400
401
void format(SectorAccessibleDisk& disk, MSXBootSectorType bootType)
402
{
403
	format(disk, bootType, disk.getNbSectors());
404
}
405
406
void format(SectorAccessibleDisk& disk, MSXBootSectorType bootType, size_t nbSectors)
407
{
408
	// first create a boot sector for given partition size
409
	nbSectors = std::min(nbSectors, disk.getNbSectors());
410
	SectorBuffer buf;
411
	SetBootSectorResult result = setBootSector(buf.bootSector, bootType, nbSectors);
412
	disk.writeSector(0, buf);
413
414
	// write empty FAT sectors (except for first sector, see below)
415
	std::ranges::fill(buf.raw, 0);
416
	for (auto fat : xrange(result.fatCount)) {
417
		for (auto i : xrange(1u, result.sectorsPerFat)) {
418
			disk.writeSector(i + result.fatStart + fat * result.sectorsPerFat, buf);
419
		}
420
	}
421
422
	// write empty directory sectors
423
	for (auto i : xrange(result.rootDirStart, result.dataStart)) {
424
		disk.writeSector(i, buf);
425
	}
426
427
	// first FAT sector is special:
428
	//  - first byte contains the media descriptor
429
	//  - first two clusters must be marked as EOF
430
	buf.raw[0] = result.descriptor;
431
	buf.raw[1] = 0xFF;
432
	buf.raw[2] = 0xFF;
433
	if (result.fat16) {
434
		buf.raw[3] = 0xFF;
435
	}
436
	for (auto fat : xrange(result.fatCount)) {
437
		disk.writeSector(result.fatStart + fat * result.sectorsPerFat, buf);
438
	}
439
440
	// write 'empty' data sectors
441
	std::ranges::fill(buf.raw, 0xE5);
442
	for (auto i : xrange(result.dataStart, nbSectors)) {
443
		disk.writeSector(i, buf);
444
	}
445
}
446
447
struct CHS {
448
	unsigned cylinder;
449
	uint8_t head; // 0-15
450
	uint8_t sector; // 1-32
451
};
452
[[nodiscard]] static constexpr CHS logicalToCHS(unsigned logical)
453
{
454
	// This is made to fit the openMSX hard disk configuration:
455
	//  32 sectors/track   16 heads
456
	unsigned tmp = logical + 1;
457
	uint8_t sector = tmp % 32;
458
	if (sector == 0) sector = 32;
459
	tmp = (tmp - sector) / 32;
460
	uint8_t head = tmp % 16;
461
	unsigned cylinder = tmp / 16;
462
	return {.cylinder = cylinder, .head = head, .sector = sector};
463
}
464
465
static std::vector<unsigned> clampPartitionSizes(std::span<const unsigned> sizes,
466
	size_t diskSize, unsigned initialOffset, unsigned perPartitionOffset)
467
{
468
	std::vector<unsigned> clampedSizes;
469
	size_t sizeRemaining = std::min(diskSize, size_t(std::numeric_limits<unsigned>::max()));
470
471
	if (sizeRemaining >= initialOffset) {
472
		sizeRemaining -= initialOffset;
473
	} else {
474
		return clampedSizes;
475
	}
476
477
	for (auto size : sizes) {
478
		if (sizeRemaining <= perPartitionOffset) {
479
			break;
480
		}
481
		sizeRemaining -= perPartitionOffset;
482
		if (size <= perPartitionOffset) {
483
			throw CommandException("Partition size too small: ", size);
484
		}
485
		size -= perPartitionOffset;
486
		if (sizeRemaining > size) {
487
			clampedSizes.push_back(size);
488
			sizeRemaining -= size;
489
		} else {
490
			clampedSizes.push_back(narrow<unsigned>(sizeRemaining));
491
			break;
492
		}
493
	}
494
	return clampedSizes;
495
}
496
497
// Partition with standard master / extended boot record (MBR / EBR) for Nextor.
498
static std::vector<unsigned> partitionNextor(SectorAccessibleDisk& disk, std::span<const unsigned> sizes)
499
{
500
	SectorBuffer buf;
501
	auto& pt = buf.ptNextor;
502
503
	std::vector<unsigned> clampedSizes = clampPartitionSizes(sizes, disk.getNbSectors(), 0, 1);
504
505
	if (clampedSizes.empty()) {
506
		std::ranges::fill(buf.raw, 0);
507
		pt.header = NEXTOR_PARTITION_TABLE_HEADER;
508
		pt.end = 0xAA55;
509
		disk.writeSector(0, buf);
510
		return clampedSizes;
511
	}
512
513
	unsigned ptSector = 0;
514
	for (auto [i, size] : enumerate(clampedSizes)) {
515
		std::ranges::fill(buf.raw, 0);
516
		if (i == 0) {
517
			pt.header = NEXTOR_PARTITION_TABLE_HEADER;
518
		}
519
		pt.end = 0xAA55;
520
521
		// Add partition entry
522
		auto& p = pt.part[0];
523
		p.boot_ind = (i == 0) ? 0x80 : 0x00; // boot flag
524
		p.sys_ind = size > 0x1'0000 ? 0x0E : 0x01; // FAT16B (LBA), or FAT12
525
		p.start = 1;
526
		p.size = size;
527
528
		// Add link if not the last partition
529
		if (i != clampedSizes.size() - 1) {
530
			auto& link = pt.part[1];
531
			link.sys_ind = 0x05; // EBR
532
			if (i == 0) {
533
				link.start = ptSector + 1 + size;
534
				link.size = sum(std::views::drop(sizes, 1), [](unsigned s) { return 1 + s; });
535
			} else {
536
				link.start = ptSector + 1 + size - (1 + clampedSizes[0]);
537
				link.size = 1 + clampedSizes[i + 1];
538
			}
539
		}
540
541
		disk.writeSector(ptSector, buf);
542
543
		ptSector += 1 + size;
544
	}
545
	return clampedSizes;
546
}
547
548
// Partition with Sunrise IDE master boot record.
549
static std::vector<unsigned> partitionSunrise(SectorAccessibleDisk& disk, std::span<const unsigned> sizes)
550
{
551
	SectorBuffer buf;
552
	auto& pt = buf.ptSunrise;
553
554
	std::vector<unsigned> clampedSizes = clampPartitionSizes(sizes, disk.getNbSectors(), 1, 0);
555
	if (clampedSizes.size() > pt.part.size()) {
556
		clampedSizes.resize(pt.part.size());
557
	}
558
559
	std::ranges::fill(buf.raw, 0);
560
	pt.header = SUNRISE_PARTITION_TABLE_HEADER;
561
	pt.end = 0xAA55;
562
563
	unsigned partitionOffset = 1;
564
	for (auto [i, size] : enumerate(clampedSizes)) {
565
		unsigned partitionNbSectors = size;
566
		auto& p = pt.part[30 - i];
567
		auto [startCylinder, startHead, startSector] =
568
			logicalToCHS(partitionOffset);
569
		auto [endCylinder, endHead, endSector] =
570
			logicalToCHS(partitionOffset + partitionNbSectors - 1);
571
		p.boot_ind = (i == 0) ? 0x80 : 0x00; // boot flag
572
		p.head = startHead;
573
		p.sector = startSector;
574
		p.cyl = narrow_cast<uint8_t>(startCylinder); // wraps for size larger than 64MB
575
		p.sys_ind = 0x01; // FAT12
576
		p.end_head = endHead;
577
		p.end_sector = endSector;
578
		p.end_cyl = narrow_cast<uint8_t>(endCylinder);
579
		p.start = partitionOffset;
580
		p.size = partitionNbSectors;
581
		partitionOffset += partitionNbSectors;
582
	}
583
	disk.writeSector(0, buf);
584
	return clampedSizes;
585
}
586
587
// Partition with standard master boot record (MBR) for Beer IDE 1.9.
588
static std::vector<unsigned> partitionBeer(SectorAccessibleDisk& disk, std::span<const unsigned> sizes)
589
{
590
	SectorBuffer buf;
591
	auto& pt = buf.ptNextor;
592
593
	std::vector<unsigned> clampedSizes = clampPartitionSizes(sizes, disk.getNbSectors(), 1, 0);
594
	if (clampedSizes.size() > pt.part.size()) {
595
		clampedSizes.resize(pt.part.size());
596
	}
597
598
	std::ranges::fill(buf.raw, 0);
599
	pt.header = NEXTOR_PARTITION_TABLE_HEADER; // TODO: Find out BEER IDE signature
600
	pt.end = 0xAA55;
601
602
	unsigned partitionOffset = 1;
603
	for (auto [i, size] : enumerate(clampedSizes)) {
604
		auto& p = pt.part[i];
605
		p.boot_ind = (i == 0) ? 0x80 : 0x00; // boot flag
606
		p.sys_ind = 0x01; // FAT12
607
		p.start = partitionOffset;
608
		p.size = size;
609
		partitionOffset += size;
610
	}
611
612
	disk.writeSector(0, buf);
613
	return clampedSizes;
614
}
615
616
unsigned partition(SectorAccessibleDisk& disk, std::span<const unsigned> sizes, MSXBootSectorType bootType)
617
{
618
	std::vector<unsigned> clampedSizes = [&] {
619
		if (bootType == MSXBootSectorType::NEXTOR) {
620
			return partitionNextor(disk, sizes);
621
		} else if (bootType == MSXBootSectorType::DOS2) {
622
			return partitionSunrise(disk, sizes);
623
		} else if (bootType == MSXBootSectorType::DOS1) {
624
			return partitionBeer(disk, sizes);
625
		} else {
626
			UNREACHABLE;
627
		}
628
	}();
629
630
	for (auto [i, size] : enumerate(clampedSizes)) {
631
		DiskPartition diskPartition(disk, narrow<unsigned>(i + 1));
632
		format(diskPartition, bootType, size);
633
	}
634
635
	return narrow<unsigned>(clampedSizes.size());
636
}
637
638
FatTimeDate toTimeDate(time_t totalSeconds)
639
{
640
	if (const tm* mtim = localtime(&totalSeconds)) {
641
		auto time = narrow<uint16_t>(
642
			(std::min(mtim->tm_sec, 59) >> 1) + (mtim->tm_min << 5) +
643
			(mtim->tm_hour << 11));
644
		auto date = narrow<uint16_t>(
645
			mtim->tm_mday + ((mtim->tm_mon + 1) << 5) +
646
			(std::clamp(mtim->tm_year + 1900 - 1980, 0, 119) << 9));
647
		return {.time = time, .date = date};
648
	}
649
	return {.time = 0, .date = 0};
650
}
651
652
time_t fromTimeDate(FatTimeDate timeDate)
653
{
654
	struct tm tm{};
655
	tm.tm_sec  = std::clamp(((timeDate.time >>  0) & 31) * 2, 0, 60);
656
	tm.tm_min  = std::clamp(((timeDate.time >>  5) & 63), 0, 59);
657
	tm.tm_hour = std::clamp(((timeDate.time >> 11) & 31), 0, 23);
658
	tm.tm_mday = std::clamp((timeDate.date >> 0) & 31, 1, 31);
659
	tm.tm_mon  = std::clamp(((timeDate.date >> 5) & 15) - 1, 0, 11);
660
	tm.tm_year = (timeDate.date >> 9) + 1980 - 1900;
661
	tm.tm_isdst = -1;
662
	return mktime(&tm);
663
}
664
665
std::string formatAttrib(MSXDirEntry::AttribValue attrib)
666
{
667
	using enum MSXDirEntry::Attrib;
668
	return strCat((attrib & DIRECTORY ? 'd' : '-'),
669
	              (attrib & READONLY  ? 'r' : '-'),
670
	              (attrib & HIDDEN    ? 'h' : '-'),
671
	              (attrib & VOLUME    ? 'v' : '-'),  // TODO check if this is the output of files,l
672
	              (attrib & ARCHIVE   ? 'a' : '-')); // TODO check if this is the output of files,l
673
}
674
675
} // namespace openmsx::DiskImageUtils