OXYGENHUB
◆ main enhanced-x16-emulator / tests / test_sdcard_fat.c
19.1 KiB · C · last changed chore(): initial commit 25 days ago source blame history raw
1
// test_sdcard_fat.c — functional tests for the SD card FAT index
2
//
3
// Synthesises small FAT32 and FAT16 images in memory, writes them to disk,
4
// builds the index, and exercises every major code path.
5
//
6
// The five file-access functions are stubbed onto stdio (no SDL, no zlib).
7
// The stubs mirror the REAL contracts in files.h / files.c exactly:
8
//   x16seek  returns the resulting stream offset (like SDL_RWseek), or -1
9
//   x16tell  returns the current offset, or -1 on error
10
//   x16read  returns the number of ITEMS read (not bytes)
11
//   x16open  returns a heap handle, or NULL on failure
12
//   x16close frees the handle (no-op on NULL)
13
// A stub that does not mirror the real contract manufactures false confidence.
14
15
#include "sdcard_fat.h"
16
#include "support/harness.h"
17
#include "files.h"   // pulls in XSEEK_* and checks stub signatures
18
19
#include <stdio.h>
20
#include <stdlib.h>
21
#include <string.h>
22
#include <stdint.h>
23
#include <stdbool.h>
24
25
// ── stdio stubs for src/files.h ───────────────────────────────────────────────
26
27
struct x16file {
28
	FILE *fp;
29
};
30
31
struct x16file *
32
x16open(const char *path, const char *attribs)
33
{
34
	FILE *fp = fopen(path, attribs);
35
	if (fp == NULL) {
36
		return NULL;
37
	}
38
	struct x16file *f = (struct x16file *)malloc(sizeof(struct x16file));
39
	if (f == NULL) {
40
		fclose(fp);
41
		return NULL;
42
	}
43
	f->fp = fp;
44
	return f;
45
}
46
47
void
48
x16close(struct x16file *f)
49
{
50
	if (f == NULL) {
51
		return;
52
	}
53
	fclose(f->fp);
54
	free(f);
55
}
56
57
int64_t
58
x16size(struct x16file *f)
59
{
60
	long here = ftell(f->fp);
61
	fseek(f->fp, 0, SEEK_END);
62
	long end  = ftell(f->fp);
63
	fseek(f->fp, here, SEEK_SET);
64
	return (int64_t)end;
65
}
66
67
// Mirrors the real contract exactly: SDL_RWseek -- and so the real x16seek --
68
// returns the resulting stream position, not a success/failure status. A stub
69
// that returned fseek()'s 0-on-success would let a parser which tested the
70
// result against zero pass here and fail against every real image.
71
int
72
x16seek(struct x16file *f, int64_t pos, int origin)
73
{
74
	int whence = (origin == XSEEK_END) ? SEEK_END
75
	           : (origin == XSEEK_CUR) ? SEEK_CUR
76
	                                   : SEEK_SET;
77
	if (fseek(f->fp, (long)pos, whence) != 0) {
78
		return -1;
79
	}
80
	long p = ftell(f->fp);
81
	return (p < 0) ? -1 : (int)p;
82
}
83
84
int64_t
85
x16tell(struct x16file *f)
86
{
87
	long p = ftell(f->fp);
88
	return (p < 0) ? -1 : (int64_t)p;
89
}
90
91
uint64_t
92
x16read(struct x16file *f, uint8_t *data, uint64_t data_size, uint64_t data_count)
93
{
94
	return (uint64_t)fread(data, (size_t)data_size, (size_t)data_count, f->fp);
95
}
96
97
// ── Tiny image builder ────────────────────────────────────────────────────────
98
99
#define SECTOR_SIZE 512u
100
101
typedef struct {
102
	uint8_t *data;
103
	uint32_t sector_count;
104
} disk_t;
105
106
static disk_t
107
disk_alloc(uint32_t sectors)
108
{
109
	disk_t d;
110
	d.sector_count = sectors;
111
	d.data         = (uint8_t *)calloc((size_t)sectors, SECTOR_SIZE);
112
	return d;
113
}
114
115
static void
116
disk_free(disk_t *d)
117
{
118
	free(d->data);
119
	d->data         = NULL;
120
	d->sector_count = 0;
121
}
122
123
static uint8_t *
124
disk_sector(disk_t *d, uint32_t lba)
125
{
126
	return d->data + (size_t)lba * SECTOR_SIZE;
127
}
128
129
static void wle16(uint8_t *p, uint16_t v)
130
{
131
	p[0] = (uint8_t)(v & 0xFFu);
132
	p[1] = (uint8_t)((v >> 8) & 0xFFu);
133
}
134
135
static void wle32(uint8_t *p, uint32_t v)
136
{
137
	p[0] = (uint8_t)(v & 0xFFu);
138
	p[1] = (uint8_t)((v >> 8)  & 0xFFu);
139
	p[2] = (uint8_t)((v >> 16) & 0xFFu);
140
	p[3] = (uint8_t)((v >> 24) & 0xFFu);
141
}
142
143
static void
144
fat32_set(disk_t *d, uint32_t fat_lba, uint32_t cluster, uint32_t val)
145
{
146
	uint32_t byte_off = cluster * 4u;
147
	uint32_t s = fat_lba + byte_off / SECTOR_SIZE;
148
	uint32_t i = byte_off % SECTOR_SIZE;
149
	wle32(disk_sector(d, s) + i, val & 0x0FFFFFFFu);
150
}
151
152
static void
153
fat16_set(disk_t *d, uint32_t fat_lba, uint32_t cluster, uint16_t val)
154
{
155
	uint32_t byte_off = cluster * 2u;
156
	uint32_t s = fat_lba + byte_off / SECTOR_SIZE;
157
	uint32_t i = byte_off % SECTOR_SIZE;
158
	wle16(disk_sector(d, s) + i, val);
159
}
160
161
static void
162
write_dirent_83(uint8_t *de, const char *name83, uint8_t attr,
163
                uint32_t first_cluster, uint32_t size)
164
{
165
	memset(de, 0, 32);
166
	memcpy(de, name83, 11);
167
	de[11] = attr;
168
	wle16(de + 20, (uint16_t)(first_cluster >> 16));
169
	wle16(de + 26, (uint16_t)(first_cluster & 0xFFFFu));
170
	wle32(de + 28, size);
171
}
172
173
static uint8_t
174
lfn_checksum(const uint8_t *name83)
175
{
176
	uint8_t sum = 0;
177
	for (int i = 0; i < 11; i++) {
178
		sum = (uint8_t)(((sum & 1u) ? 0x80u : 0x00u) + (sum >> 1) + name83[i]);
179
	}
180
	return sum;
181
}
182
183
static void
184
write_lfn_entry(uint8_t *de, uint8_t seq, bool is_last,
185
                uint8_t checksum, const uint16_t ucs2[13])
186
{
187
	static const int off[13] = {1,3,5,7,9, 14,16,18,20,22,24, 28,30};
188
	memset(de, 0, 32);
189
	de[0]  = seq | (is_last ? 0x40u : 0x00u);
190
	de[11] = 0x0F;
191
	de[13] = checksum;
192
	for (int i = 0; i < 13; i++) {
193
		wle16(de + off[i], ucs2[i]);
194
	}
195
}
196
197
static const char *
198
write_temp_disk(const disk_t *d, const char *leaf)
199
{
200
	static char path[512];
201
	snprintf(path, sizeof(path), "test_sdcard_fat_%s", leaf);
202
	FILE *f = fopen(path, "wb");
203
	if (!f) {
204
		return NULL;
205
	}
206
	fwrite(d->data, SECTOR_SIZE, (size_t)d->sector_count, f);
207
	fclose(f);
208
	return path;
209
}
210
211
// ── FAT32 image ───────────────────────────────────────────────────────────────
212
//
213
// Geometry chosen so cluster_count = 65664 >= 65525 (FAT32) exactly:
214
//   65664 clusters × 4 B = 262656 B = 513 sectors per FAT
215
//
216
// Absolute LBAs:
217
//   P_LBA      = 2048  (partition start / VBR)
218
//   FAT1_LBA   = 2054  (P_LBA + RSVD_SEC)
219
//   FAT2_LBA   = 2567  (FAT1 + FAT32_SZ)
220
//   F32_DATA   = 3080  (FAT2 + FAT32_SZ)
221
//   Cluster n  = F32_DATA + (n-2)
222
//
223
// Files:
224
//   /              dir,  cluster 2  ← FAT32 root (BUG 3 fix registers this)
225
//   /FILE1.PRG     100 B cluster 4
226
//   /SUBDIR/        dir, cluster 3
227
//   /A LONG NAME.PRG  50 B clusters 5→6 (VFAT LFN)
228
//   /SUBDIR/DEEP.PRG 200 B cluster 7
229
230
#define P_LBA        2048u
231
#define RSVD_SEC     6u
232
#define FAT32_SZ     513u
233
#define NUM_FATS     2u
234
#define F32_SPC      1u
235
#define F32_TOT_SEC  66696u
236
#define FAT1_LBA     (P_LBA + RSVD_SEC)
237
#define FAT2_LBA     (FAT1_LBA + FAT32_SZ)
238
#define F32_DATA_LBA (FAT2_LBA + FAT32_SZ)
239
#define F32_DISK_SECTORS (F32_DATA_LBA + 6u + 1u)
240
241
static disk_t
242
build_fat32_image(void)
243
{
244
	disk_t d = disk_alloc(F32_DISK_SECTORS);
245
246
	// MBR
247
	uint8_t *mbr = disk_sector(&d, 0);
248
	mbr[0] = 0xEB; mbr[1] = 0x58; mbr[2] = 0x90;
249
	uint8_t *pe = mbr + 0x1BE;
250
	pe[4] = 0x0C;
251
	wle32(pe + 8,  P_LBA);
252
	wle32(pe + 12, F32_TOT_SEC);
253
	mbr[510] = 0x55; mbr[511] = 0xAA;
254
255
	// VBR
256
	uint8_t *vbr = disk_sector(&d, P_LBA);
257
	vbr[0] = 0xEB; vbr[1] = 0x58; vbr[2] = 0x90;
258
	memcpy(vbr + 3, "MSDOS5.0", 8);
259
	wle16(vbr + 11, 512);
260
	vbr[13] = F32_SPC;
261
	wle16(vbr + 14, RSVD_SEC);
262
	vbr[16] = NUM_FATS;
263
	wle16(vbr + 17, 0);
264
	wle16(vbr + 19, 0);
265
	vbr[21] = 0xF8;
266
	wle16(vbr + 22, 0);
267
	wle16(vbr + 24, 63);
268
	wle16(vbr + 26, 255);
269
	wle32(vbr + 28, 0);
270
	wle32(vbr + 32, F32_TOT_SEC);
271
	wle32(vbr + 36, FAT32_SZ);
272
	wle16(vbr + 40, 0);
273
	wle16(vbr + 42, 0);
274
	wle32(vbr + 44, 2);   // root cluster = 2
275
	wle16(vbr + 48, 1);
276
	wle16(vbr + 50, 6);
277
	vbr[510] = 0x55; vbr[511] = 0xAA;
278
279
	// FAT entries (both copies)
280
	uint32_t fat_lbas[2] = { FAT1_LBA, FAT2_LBA };
281
	for (int fi = 0; fi < 2; fi++) {
282
		uint32_t fl = fat_lbas[fi];
283
		fat32_set(&d, fl, 0, 0x0FFFFFF8u);
284
		fat32_set(&d, fl, 1, 0x0FFFFFFFu);
285
		fat32_set(&d, fl, 2, 0x0FFFFFFFu); // root dir: EOC
286
		fat32_set(&d, fl, 3, 0x0FFFFFFFu); // SUBDIR: EOC
287
		fat32_set(&d, fl, 4, 0x0FFFFFFFu); // FILE1.PRG: EOC
288
		fat32_set(&d, fl, 5, 6u);           // LONGNAME: → 6
289
		fat32_set(&d, fl, 6, 0x0FFFFFFFu); // LONGNAME: EOC
290
		fat32_set(&d, fl, 7, 0x0FFFFFFFu); // DEEP.PRG: EOC
291
	}
292
293
	// Root directory (cluster 2 = F32_DATA_LBA)
294
	uint8_t *root = disk_sector(&d, F32_DATA_LBA + 0);
295
296
	write_dirent_83(root + 0*32, "FILE1   PRG", 0x20, 4, 100);
297
	write_dirent_83(root + 1*32, "SUBDIR     ", 0x10, 3, 0);
298
299
	// VFAT LFN for "A LONG NAME.PRG" (15 chars, 2 LFN slots)
300
	uint8_t short83[11];
301
	memcpy(short83, "ALONGN~1PRG", 11);
302
	uint8_t ck = lfn_checksum(short83);
303
304
	const char *lfn_name = "A LONG NAME.PRG";
305
	uint16_t frag1[13], frag2[13];
306
	for (int i = 0; i < 13; i++) {
307
		frag1[i] = (uint16_t)(unsigned char)lfn_name[i];
308
	}
309
	frag2[0] = (uint16_t)(unsigned char)lfn_name[13];
310
	frag2[1] = (uint16_t)(unsigned char)lfn_name[14];
311
	frag2[2] = 0x0000u;
312
	for (int i = 3; i < 13; i++) { frag2[i] = 0xFFFFu; }
313
314
	write_lfn_entry(root + 2*32, 2, true,  ck, frag2);
315
	write_lfn_entry(root + 3*32, 1, false, ck, frag1);
316
	write_dirent_83(root + 4*32, (const char *)short83, 0x20, 5, 50);
317
318
	// SUBDIR (cluster 3 = F32_DATA_LBA+1)
319
	uint8_t *sub = disk_sector(&d, F32_DATA_LBA + 1);
320
	write_dirent_83(sub + 0*32, ".          ", 0x10, 3, 0);
321
	write_dirent_83(sub + 1*32, "..         ", 0x10, 2, 0);
322
	write_dirent_83(sub + 2*32, "DEEP    PRG", 0x20, 7, 200);
323
324
	return d;
325
}
326
327
// ── FAT16 image ───────────────────────────────────────────────────────────────
328
//
329
// Superfloppy; cluster_count = 4162 (4085 <= 4162 < 65525 → FAT16).
330
//
331
//   FAT1_16  = LBA 4,  size 16 sectors
332
//   FAT2_16  = LBA 20, size 16 sectors
333
//   Root dir = LBA 36 (2 sectors for 32 entries)
334
//   Data     = LBA 38  (cluster 2 = ONE.TXT)
335
336
#define F16_RSVD     4u
337
#define F16_FATSZ    16u
338
#define F16_NFATS    2u
339
#define F16_ROOTENT  32u
340
#define F16_ROOTSEC  2u    // ceil(32*32/512) = 2
341
#define F16_SPC      1u
342
#define F16_TOT      4200u
343
#define F16_FAT1_LBA F16_RSVD
344
#define F16_FAT2_LBA (F16_FAT1_LBA + F16_FATSZ)
345
#define F16_ROOT_LBA (F16_FAT2_LBA + F16_FATSZ)
346
#define F16_DATA_LBA (F16_ROOT_LBA + F16_ROOTSEC)
347
#define F16_DISK_SECTORS (F16_DATA_LBA + 2u)
348
349
static disk_t
350
build_fat16_image(void)
351
{
352
	disk_t d = disk_alloc(F16_DISK_SECTORS);
353
354
	uint8_t *vbr = disk_sector(&d, 0);
355
	vbr[0] = 0xEB; vbr[1] = 0x3C; vbr[2] = 0x90;
356
	memcpy(vbr + 3, "MSDOS5.0", 8);
357
	wle16(vbr + 11, 512);
358
	vbr[13] = F16_SPC;
359
	wle16(vbr + 14, F16_RSVD);
360
	vbr[16] = F16_NFATS;
361
	wle16(vbr + 17, F16_ROOTENT);
362
	wle16(vbr + 19, (uint16_t)F16_TOT);
363
	vbr[21] = 0xF8;
364
	wle16(vbr + 22, (uint16_t)F16_FATSZ);
365
	wle16(vbr + 24, 63);
366
	wle16(vbr + 26, 255);
367
	wle32(vbr + 28, 0);
368
	wle32(vbr + 32, 0);
369
	vbr[510] = 0x55; vbr[511] = 0xAA;
370
371
	uint32_t fat_lbas[2] = { F16_FAT1_LBA, F16_FAT2_LBA };
372
	for (int fi = 0; fi < 2; fi++) {
373
		uint32_t fl = fat_lbas[fi];
374
		fat16_set(&d, fl, 0, 0xFFF8u);
375
		fat16_set(&d, fl, 1, 0xFFFFu);
376
		fat16_set(&d, fl, 2, 0xFFFFu);
377
	}
378
379
	uint8_t *rdir = disk_sector(&d, F16_ROOT_LBA);
380
	write_dirent_83(rdir + 0*32, "ONE     TXT", 0x20, 2, 42);
381
382
	return d;
383
}
384
385
// ── Helpers ───────────────────────────────────────────────────────────────────
386
387
static const sdcard_fat_file_t *
388
find_file(const char *path)
389
{
390
	int n = sdcard_fat_file_count();
391
	for (int i = 0; i < n; i++) {
392
		const sdcard_fat_file_t *f = sdcard_fat_file_at(i);
393
		if (f && strcmp(f->path, path) == 0) {
394
			return f;
395
		}
396
	}
397
	return NULL;
398
}
399
400
// Open a disk image, call sdcard_fat_build, run tests, then free+close.
401
// Returns the opened handle so callers can close after their tests.
402
static struct x16file *
403
open_and_build(const char *path, bool expect_ok, const char *label)
404
{
405
	struct x16file *img = x16open(path, "rb");
406
	check(img != NULL, label);
407
	if (!img) {
408
		return NULL;
409
	}
410
	bool ok = sdcard_fat_build(img);
411
	if (expect_ok) {
412
		check(ok, "sdcard_fat_build succeeds");
413
	} else {
414
		check(!ok, "sdcard_fat_build returns false (expected)");
415
	}
416
	return img;
417
}
418
419
// ── Tests ─────────────────────────────────────────────────────────────────────
420
421
static void
422
test_fat32(void)
423
{
424
	printf("\n--- FAT32 image tests ---\n");
425
426
	disk_t      d    = build_fat32_image();
427
	const char *path = write_temp_disk(&d, "fat32.img");
428
	disk_free(&d);
429
430
	check(path != NULL, "FAT32 image written to disk");
431
	if (!path) { return; }
432
433
	struct x16file *img = open_and_build(path, true, "FAT32 image opened");
434
	if (!img) { remove(path); return; }
435
436
	check(sdcard_fat_ready(),                           "sdcard_fat_ready() is true");
437
	check(strcmp(sdcard_fat_type_name(), "FAT32") == 0, "type name is FAT32");
438
439
	// "/" root entry + FILE1.PRG + SUBDIR + LONGNAME + DEEP.PRG = 5 entries
440
	check(sdcard_fat_file_count() >= 5, "file count >= 5 (root + 4 files)");
441
	printf("  file count: %d\n", sdcard_fat_file_count());
442
443
	check(find_file("/")               != NULL, "found / (FAT32 root dir)");
444
	check(find_file("/FILE1.PRG")      != NULL, "found /FILE1.PRG");
445
	check(find_file("/SUBDIR")         != NULL, "found /SUBDIR");
446
	check(find_file("/A LONG NAME.PRG") != NULL, "found /A LONG NAME.PRG via LFN");
447
	check(find_file("/SUBDIR/DEEP.PRG") != NULL, "found /SUBDIR/DEEP.PRG");
448
449
	const sdcard_fat_file_t *f1 = find_file("/FILE1.PRG");
450
	if (f1) { check_eq(f1->size, 100u, "/FILE1.PRG size == 100"); }
451
452
	// ── Region lookups ────────────────────────────────────────────────────────
453
454
	// MBR
455
	{
456
		sdcard_fat_region_t r = sdcard_fat_lookup(0, NULL, NULL);
457
		check(r == SDCARD_FAT_REGION_MBR, "LBA 0 is REGION_MBR");
458
	}
459
460
	// FAT1
461
	{
462
		sdcard_fat_region_t r = sdcard_fat_lookup(FAT1_LBA, NULL, NULL);
463
		check(r == SDCARD_FAT_REGION_FAT, "LBA FAT1_LBA is REGION_FAT");
464
	}
465
466
	// FILE1.PRG: cluster 4 = F32_DATA_LBA+2
467
	{
468
		const sdcard_fat_file_t *hf = NULL;
469
		uint64_t off = 999u;
470
		sdcard_fat_region_t r = sdcard_fat_lookup(F32_DATA_LBA + 2u, &hf, &off);
471
		check(r == SDCARD_FAT_REGION_FILE,                          "cluster 4 is REGION_FILE");
472
		check(hf != NULL && strcmp(hf->path, "/FILE1.PRG") == 0,   "  ...names /FILE1.PRG");
473
		check_eq((uint32_t)off, 0u, "  ...offset 0");
474
	}
475
476
	// LONGNAME cluster 6 = F32_DATA_LBA+4 (second cluster → offset 512)
477
	{
478
		const sdcard_fat_file_t *hf = NULL;
479
		uint64_t off = 0u;
480
		sdcard_fat_region_t r = sdcard_fat_lookup(F32_DATA_LBA + 4u, &hf, &off);
481
		check(r == SDCARD_FAT_REGION_FILE,                              "cluster 6 is REGION_FILE");
482
		check(hf != NULL && strcmp(hf->path, "/A LONG NAME.PRG") == 0, "  ...names /A LONG NAME.PRG");
483
		check_eq((uint32_t)off, 512u, "  ...offset 512 (second cluster)");
484
	}
485
486
	// BUG 3: FAT32 root directory cluster (cluster 2 = F32_DATA_LBA)
487
	// Must return REGION_DIR, NOT REGION_FREE.
488
	{
489
		const sdcard_fat_file_t *hf = NULL;
490
		uint64_t off = 999u;
491
		sdcard_fat_region_t r = sdcard_fat_lookup(F32_DATA_LBA + 0u, &hf, &off);
492
		check(r == SDCARD_FAT_REGION_DIR,
493
		      "FAT32 root dir LBA is REGION_DIR (not FREE)");
494
		check(hf != NULL && strcmp(hf->path, "/") == 0,
495
		      "  ...attributed to '/'");
496
		check_eq((uint32_t)off, 0u, "  ...at offset 0");
497
	}
498
499
	// BUG 3: write to FAT32 root dir must mark the index stale.
500
	{
501
		check(!sdcard_fat_is_stale(), "not stale before root-dir write");
502
		sdcard_fat_note_access(F32_DATA_LBA + 0u, true, 512);
503
		check(sdcard_fat_is_stale(), "stale after write to FAT32 root dir sector");
504
	}
505
506
	// Unallocated data LBA
507
	{
508
		sdcard_fat_region_t r = sdcard_fat_lookup(F32_DATA_LBA + 100u, NULL, NULL);
509
		check(r == SDCARD_FAT_REGION_FREE || r == SDCARD_FAT_REGION_UNKNOWN,
510
		      "unallocated LBA returns FREE or UNKNOWN (no crash)");
511
	}
512
513
	sdcard_fat_free();
514
	check(!sdcard_fat_ready(), "sdcard_fat_ready() is false after free");
515
516
	x16close(img);
517
	remove(path);
518
}
519
520
static void
521
test_fat16(void)
522
{
523
	printf("\n--- FAT16 image tests ---\n");
524
525
	disk_t      d    = build_fat16_image();
526
	const char *path = write_temp_disk(&d, "fat16.img");
527
	disk_free(&d);
528
529
	check(path != NULL, "FAT16 image written to disk");
530
	if (!path) { return; }
531
532
	struct x16file *img = open_and_build(path, true, "FAT16 image opened");
533
	if (!img) { remove(path); return; }
534
535
	check(sdcard_fat_ready(),                           "sdcard_fat_ready() is true");
536
	check(strcmp(sdcard_fat_type_name(), "FAT16") == 0, "type name is FAT16");
537
538
	check(find_file("/ONE.TXT") != NULL, "found /ONE.TXT");
539
	const sdcard_fat_file_t *ft = find_file("/ONE.TXT");
540
	if (ft) { check_eq(ft->size, 42u, "/ONE.TXT size == 42"); }
541
542
	// FAT region
543
	{
544
		sdcard_fat_region_t r = sdcard_fat_lookup(F16_FAT1_LBA, NULL, NULL);
545
		check(r == SDCARD_FAT_REGION_FAT, "LBA 4 is REGION_FAT");
546
	}
547
548
	// Fixed root directory
549
	{
550
		sdcard_fat_region_t r = sdcard_fat_lookup(F16_ROOT_LBA, NULL, NULL);
551
		check(r == SDCARD_FAT_REGION_ROOTDIR, "LBA 36 is REGION_ROOTDIR (FAT16 fixed)");
552
	}
553
554
	// ONE.TXT cluster 2 = F16_DATA_LBA
555
	{
556
		const sdcard_fat_file_t *hf = NULL;
557
		uint64_t off = 999u;
558
		sdcard_fat_region_t r = sdcard_fat_lookup(F16_DATA_LBA, &hf, &off);
559
		check(r == SDCARD_FAT_REGION_FILE,                      "cluster 2 is REGION_FILE");
560
		check(hf != NULL && strcmp(hf->path, "/ONE.TXT") == 0, "  ...names /ONE.TXT");
561
		check_eq((uint32_t)off, 0u, "  ...offset 0");
562
	}
563
564
	// Superfloppy: LBA 0 is reserved (VBR), not MBR
565
	{
566
		sdcard_fat_region_t r = sdcard_fat_lookup(0, NULL, NULL);
567
		check(r == SDCARD_FAT_REGION_RESERVED, "FAT16 superfloppy LBA 0 is RESERVED (VBR)");
568
	}
569
570
	sdcard_fat_free();
571
	x16close(img);
572
	remove(path);
573
}
574
575
static void
576
test_garbage_image(void)
577
{
578
	printf("\n--- Garbage image tests ---\n");
579
580
	disk_t d = disk_alloc(2u);
581
	const char *path = write_temp_disk(&d, "garbage.img");
582
	disk_free(&d);
583
584
	check(path != NULL, "garbage image written");
585
	if (!path) { return; }
586
587
	struct x16file *img = x16open(path, "rb");
588
	check(img != NULL, "garbage image opened");
589
	if (!img) { remove(path); return; }
590
591
	bool ok = sdcard_fat_build(img);
592
	check(!ok,                 "sdcard_fat_build returns false for zero image");
593
	check(!sdcard_fat_ready(), "sdcard_fat_ready() is false after failed build");
594
595
	sdcard_fat_region_t r = sdcard_fat_lookup(0, NULL, NULL);
596
	check(r == SDCARD_FAT_REGION_UNKNOWN, "lookup on unbuilt index returns UNKNOWN");
597
598
	x16close(img);
599
	remove(path);
600
}
601
602
static void
603
test_stale(void)
604
{
605
	printf("\n--- Stale tracking tests ---\n");
606
607
	disk_t      d    = build_fat32_image();
608
	const char *path = write_temp_disk(&d, "stale.img");
609
	disk_free(&d);
610
611
	if (!path) { return; }
612
613
	struct x16file *img = x16open(path, "rb");
614
	check(img != NULL, "stale test: image opened");
615
	if (!img) { remove(path); return; }
616
617
	bool ok = sdcard_fat_build(img);
618
	check(ok,                    "stale test: build succeeds");
619
	check(!sdcard_fat_is_stale(), "not stale immediately after build");
620
621
	// Write to a FAT sector must mark stale
622
	sdcard_fat_note_access(FAT1_LBA, true, 512);
623
	check(sdcard_fat_is_stale(), "stale after write to a FAT sector");
624
625
	// Rebuild; read of file data must NOT mark stale
626
	sdcard_fat_free();
627
	sdcard_fat_build(img);
628
	check(!sdcard_fat_is_stale(), "not stale after rebuild");
629
	sdcard_fat_note_access(F32_DATA_LBA + 2u, false, 512); // read FILE1.PRG
630
	check(!sdcard_fat_is_stale(), "read of file data does not mark stale");
631
632
	sdcard_fat_free();
633
	x16close(img);
634
	remove(path);
635
}
636
637
// ── main ──────────────────────────────────────────────────────────────────────
638
639
int
640
main(void)
641
{
642
	test_fat32();
643
	test_fat16();
644
	test_garbage_image();
645
	test_stale();
646
647
	return x16_test_summary("sdcard_fat");
648
}