cffs-image-tool 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,20 +1,22 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
2
  import { basename } from 'node:path';
3
- import { SECTOR_SIZE, FLAG_USED, DATA_START } from './constants.js';
3
+ import { SECTOR_SIZE, FLAG_USED, DATA_START, SECTORS_PER_DISK } from './constants.js';
4
4
  import { parseName, formatName } from './filename.js';
5
- import { writeSector } from './image.js';
5
+ import { writeSector, diskBaseLba, validateDisk } from './image.js';
6
6
  import { readDirectory, writeDirectory, findFile, findFreeSlot, calcNextSector, calcSectorCount, } from './directory.js';
7
7
  /**
8
- * Add a host file to the image. Optionally rename with targetName (8.3 format).
8
+ * Add a host file to the given disk. Optionally rename with targetName (8.3 format).
9
+ * Start sectors are disk-relative; a file may not spill past its disk's region.
9
10
  */
10
- export function addFile(buf, hostPath, targetName) {
11
+ export function addFile(buf, hostPath, disk = 0, targetName) {
12
+ validateDisk(buf, disk);
11
13
  const data = readFileSync(hostPath);
12
14
  if (data.length > 0xFFFF) {
13
15
  throw new Error(`File too large: ${data.length} bytes (max 65535)`);
14
16
  }
15
17
  const nameInput = targetName ?? basename(hostPath);
16
18
  const { name, ext } = parseName(nameInput);
17
- const entries = readDirectory(buf);
19
+ const entries = readDirectory(buf, disk);
18
20
  // If file with same name exists, clear old entry (overwrite behavior matches BIOS FsSaveFile)
19
21
  const existing = findFile(entries, name, ext);
20
22
  if (existing) {
@@ -24,15 +26,16 @@ export function addFile(buf, hostPath, targetName) {
24
26
  if (slotIndex === -1) {
25
27
  throw new Error('Directory is full (16 entries maximum)');
26
28
  }
27
- // Recalculate next sector after potentially clearing old entry
29
+ // Recalculate next sector (disk-relative) after potentially clearing old entry
28
30
  const startSector = calcNextSector(entries);
29
- // Check that the file data fits in the image
30
31
  const sectorCount = calcSectorCount(data.length);
31
- const endByte = (startSector + sectorCount) * SECTOR_SIZE;
32
- if (endByte > buf.length) {
33
- throw new Error(`Not enough space in image: need sector ${startSector + sectorCount - 1}, image has ${buf.length / SECTOR_SIZE} sectors`);
32
+ // Disk-full guard: the file must fit within this disk's region (matches
33
+ // BIOS FsSaveFile: end sector must be <= FS_DISK_SECTORS).
34
+ if (startSector + sectorCount > SECTORS_PER_DISK) {
35
+ throw new Error(`Not enough space on disk ${disk}: need sector ${startSector + sectorCount}, disk holds ${SECTORS_PER_DISK} sectors`);
34
36
  }
35
- // Write file data to sectors
37
+ // Write file data to absolute sectors within the disk region
38
+ const base = diskBaseLba(disk);
36
39
  for (let i = 0; i < sectorCount; i++) {
37
40
  const chunk = Buffer.alloc(SECTOR_SIZE);
38
41
  const srcOffset = i * SECTOR_SIZE;
@@ -40,7 +43,7 @@ export function addFile(buf, hostPath, targetName) {
40
43
  if (remaining > 0) {
41
44
  data.copy(chunk, 0, srcOffset, srcOffset + remaining);
42
45
  }
43
- writeSector(buf, startSector + i, chunk);
46
+ writeSector(buf, base + startSector + i, chunk);
44
47
  }
45
48
  // Update directory entry
46
49
  entries[slotIndex] = {
@@ -51,41 +54,45 @@ export function addFile(buf, hostPath, targetName) {
51
54
  fileSize: data.length,
52
55
  index: slotIndex,
53
56
  };
54
- writeDirectory(buf, entries);
57
+ writeDirectory(buf, entries, disk);
55
58
  }
56
59
  /**
57
60
  * Remove a file by name (clears flags only, no compaction — matches BIOS FsDeleteFile).
58
61
  */
59
- export function removeFile(buf, nameInput) {
62
+ export function removeFile(buf, nameInput, disk = 0) {
63
+ validateDisk(buf, disk);
60
64
  const { name, ext } = parseName(nameInput);
61
- const entries = readDirectory(buf);
65
+ const entries = readDirectory(buf, disk);
62
66
  const entry = findFile(entries, name, ext);
63
67
  if (!entry) {
64
68
  throw new Error(`File not found: ${nameInput}`);
65
69
  }
66
70
  entry.flags = 0;
67
- writeDirectory(buf, entries);
71
+ writeDirectory(buf, entries, disk);
68
72
  }
69
73
  /**
70
- * List all in-use directory entries.
74
+ * List all in-use directory entries on the given disk.
71
75
  */
72
- export function listFiles(buf) {
73
- return readDirectory(buf).filter((e) => (e.flags & FLAG_USED) !== 0);
76
+ export function listFiles(buf, disk = 0) {
77
+ validateDisk(buf, disk);
78
+ return readDirectory(buf, disk).filter((e) => (e.flags & FLAG_USED) !== 0);
74
79
  }
75
80
  /**
76
- * Extract a file from the image to the host filesystem.
81
+ * Extract a file from the given disk to the host filesystem.
77
82
  */
78
- export function extractFile(buf, nameInput, outputPath) {
83
+ export function extractFile(buf, nameInput, outputPath, disk = 0) {
84
+ validateDisk(buf, disk);
79
85
  const { name, ext } = parseName(nameInput);
80
- const entries = readDirectory(buf);
86
+ const entries = readDirectory(buf, disk);
81
87
  const entry = findFile(entries, name, ext);
82
88
  if (!entry) {
83
89
  throw new Error(`File not found: ${nameInput}`);
84
90
  }
91
+ const base = diskBaseLba(disk);
85
92
  const sectorCount = calcSectorCount(entry.fileSize);
86
93
  const output = Buffer.alloc(entry.fileSize);
87
94
  for (let i = 0; i < sectorCount; i++) {
88
- const offset = (entry.startSector + i) * SECTOR_SIZE;
95
+ const offset = (base + entry.startSector + i) * SECTOR_SIZE;
89
96
  const srcSlice = buf.subarray(offset, offset + SECTOR_SIZE);
90
97
  const dstOffset = i * SECTOR_SIZE;
91
98
  const remaining = Math.min(SECTOR_SIZE, entry.fileSize - dstOffset);
@@ -95,10 +102,13 @@ export function extractFile(buf, nameInput, outputPath) {
95
102
  writeFileSync(outPath, output);
96
103
  }
97
104
  /**
98
- * Defragment the image: sort used entries by startSector, rewrite contiguously from LBA 1.
105
+ * Defragment a disk: sort used entries by startSector, rewrite contiguously
106
+ * from the disk's first data sector, and zero the remainder of the disk region.
99
107
  */
100
- export function defragment(buf) {
101
- const entries = readDirectory(buf);
108
+ export function defragment(buf, disk = 0) {
109
+ validateDisk(buf, disk);
110
+ const base = diskBaseLba(disk);
111
+ const entries = readDirectory(buf, disk);
102
112
  const usedEntries = entries
103
113
  .filter((e) => (e.flags & FLAG_USED) !== 0)
104
114
  .sort((a, b) => a.startSector - b.startSector);
@@ -110,38 +120,40 @@ export function defragment(buf) {
110
120
  // Read file data from current location
111
121
  const fileData = Buffer.alloc(sectorCount * SECTOR_SIZE);
112
122
  for (let i = 0; i < sectorCount; i++) {
113
- const srcOffset = (entry.startSector + i) * SECTOR_SIZE;
123
+ const srcOffset = (base + entry.startSector + i) * SECTOR_SIZE;
114
124
  buf.copy(fileData, i * SECTOR_SIZE, srcOffset, srcOffset + SECTOR_SIZE);
115
125
  }
116
126
  // Write to new location
117
127
  for (let i = 0; i < sectorCount; i++) {
118
128
  const chunk = fileData.subarray(i * SECTOR_SIZE, (i + 1) * SECTOR_SIZE);
119
- writeSector(buf, currentSector + i, chunk);
129
+ writeSector(buf, base + currentSector + i, chunk);
120
130
  }
121
131
  entry.startSector = currentSector;
122
132
  }
123
133
  currentSector += sectorCount;
124
134
  }
125
- // Zero out any sectors after the last used file (reclaim space)
126
- const totalSectors = buf.length / SECTOR_SIZE;
127
- for (let s = currentSector; s < totalSectors; s++) {
128
- buf.fill(0, s * SECTOR_SIZE, (s + 1) * SECTOR_SIZE);
135
+ // Zero out any sectors after the last used file, up to this disk's boundary
136
+ for (let s = currentSector; s < SECTORS_PER_DISK; s++) {
137
+ const offset = (base + s) * SECTOR_SIZE;
138
+ buf.fill(0, offset, offset + SECTOR_SIZE);
129
139
  }
130
140
  // Write updated directory
131
- writeDirectory(buf, entries);
141
+ writeDirectory(buf, entries, disk);
132
142
  }
133
143
  /**
134
- * Clear all directory entries (zero the directory sector).
144
+ * Clear all directory entries on the given disk (zero its directory sector).
135
145
  */
136
- export function clearImage(buf) {
146
+ export function clearImage(buf, disk = 0) {
147
+ validateDisk(buf, disk);
137
148
  const sector = Buffer.alloc(SECTOR_SIZE);
138
- writeSector(buf, 0, sector);
149
+ writeSector(buf, diskBaseLba(disk), sector);
139
150
  }
140
151
  /**
141
- * Return image stats.
152
+ * Return stats for the given disk.
142
153
  */
143
- export function imageInfo(buf) {
144
- const entries = readDirectory(buf);
154
+ export function imageInfo(buf, disk = 0) {
155
+ validateDisk(buf, disk);
156
+ const entries = readDirectory(buf, disk);
145
157
  const usedEntries = entries.filter((e) => (e.flags & FLAG_USED) !== 0);
146
158
  const nextFreeSector = calcNextSector(entries);
147
159
  const totalSectors = buf.length / SECTOR_SIZE;
@@ -151,11 +163,14 @@ export function imageInfo(buf) {
151
163
  }
152
164
  return {
153
165
  totalSectors,
166
+ totalDisks: Math.floor(totalSectors / SECTORS_PER_DISK),
167
+ disk,
154
168
  usedEntries: usedEntries.length,
155
169
  freeEntries: entries.length - usedEntries.length,
156
170
  nextFreeSector,
157
171
  usedDataSectors,
158
- freeDataSectors: totalSectors - 1 - usedDataSectors, // -1 for directory sector
172
+ // Data sectors available in this disk: SECTORS_PER_DISK - 1 (directory) - used
173
+ freeDataSectors: SECTORS_PER_DISK - 1 - usedDataSectors,
159
174
  };
160
175
  }
161
176
  //# sourceMappingURL=operations.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/lib/operations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/lib/operations.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cffs-image-tool",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "CompactFlash Filesystem Image Tool for A.C. Wright 6502 Project",
5
5
  "type": "module",
6
6
  "main": "dist/cli.js",
package/src/cli.ts CHANGED
@@ -6,6 +6,7 @@ import { stdin, stdout } from 'node:process';
6
6
  import { openImage, saveImage, createImage } from './lib/image.js';
7
7
  import { formatName } from './lib/filename.js';
8
8
  import { calcSectorCount } from './lib/directory.js';
9
+ import { SECTORS_PER_DISK, MAX_DISKS } from './lib/constants.js';
9
10
  import {
10
11
  addFile, removeFile, listFiles, extractFile,
11
12
  defragment, clearImage, imageInfo,
@@ -16,7 +17,7 @@ const program = new Command();
16
17
  program
17
18
  .name('cffs')
18
19
  .description('CompactFlash Filesystem Image Tool for A.C. Wright 6502 Project')
19
- .version('1.0.0');
20
+ .version('1.1.0');
20
21
 
21
22
  /**
22
23
  * Parse a size string like "32M", "512K", "1G", or a plain number (bytes).
@@ -36,14 +37,36 @@ function parseSize(value: string): number {
36
37
  }
37
38
  }
38
39
 
40
+ /**
41
+ * Parse and validate a disk bank number (0 to MAX_DISKS-1).
42
+ */
43
+ function parseDisk(value: string): number {
44
+ const disk = parseInt(value, 10);
45
+ if (!Number.isInteger(disk) || disk < 0 || disk >= MAX_DISKS) {
46
+ throw new Error(`Invalid disk: "${value}". Must be 0-${MAX_DISKS - 1}.`);
47
+ }
48
+ return disk;
49
+ }
50
+
39
51
  // ── create ──────────────────────────────────────────────────────────────────
40
52
  program
41
53
  .command('create')
42
54
  .description('Create a blank CompactFlash image')
43
55
  .argument('<image>', 'Path to image file to create')
44
- .option('-s, --size <size>', 'Image size (e.g. 32M, 512K)', '32M')
45
- .action((image: string, opts: { size: string }) => {
46
- const totalBytes = parseSize(opts.size);
56
+ .option('-s, --size <size>', 'Image size (e.g. 1M, 512K); one disk bank is 1M', '1M')
57
+ .option('-D, --disks <count>', `Size by disk-bank count (1 MB each); overrides --size`)
58
+ .action((image: string, opts: { size: string; disks?: string }) => {
59
+ let totalBytes: number;
60
+ if (opts.disks !== undefined) {
61
+ const disks = parseInt(opts.disks, 10);
62
+ if (!Number.isInteger(disks) || disks < 1 || disks > MAX_DISKS) {
63
+ console.error(`Error: --disks must be 1-${MAX_DISKS}`);
64
+ process.exit(1);
65
+ }
66
+ totalBytes = disks * SECTORS_PER_DISK * 512;
67
+ } else {
68
+ totalBytes = parseSize(opts.size);
69
+ }
47
70
  if (totalBytes % 512 !== 0) {
48
71
  console.error('Error: size must be a multiple of 512 bytes');
49
72
  process.exit(1);
@@ -51,20 +74,23 @@ program
51
74
  const totalSectors = totalBytes / 512;
52
75
  const buf = createImage(totalSectors);
53
76
  saveImage(image, buf);
54
- console.log(`Created ${image} (${totalBytes.toLocaleString()} bytes, ${totalSectors.toLocaleString()} sectors)`);
77
+ const diskCount = Math.floor(totalSectors / SECTORS_PER_DISK);
78
+ console.log(`Created ${image} (${totalBytes.toLocaleString()} bytes, ${totalSectors.toLocaleString()} sectors, ${diskCount} disk${diskCount === 1 ? '' : 's'})`);
55
79
  });
56
80
 
57
81
  // ── list ────────────────────────────────────────────────────────────────────
58
82
  program
59
83
  .command('list')
60
- .description('List files in the image')
84
+ .description('List files on a disk in the image')
61
85
  .argument('<image>', 'Path to image file')
62
- .action((image: string) => {
86
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
87
+ .action((image: string, opts: { disk: number }) => {
63
88
  const buf = openImage(image);
64
- const files = listFiles(buf);
89
+ const files = listFiles(buf, opts.disk);
65
90
 
91
+ console.log(`Disk ${opts.disk}`);
66
92
  if (files.length === 0) {
67
- console.log('No files in image.');
93
+ console.log('No files on disk.');
68
94
  return;
69
95
  }
70
96
 
@@ -82,65 +108,70 @@ program
82
108
  // ── add ─────────────────────────────────────────────────────────────────────
83
109
  program
84
110
  .command('add')
85
- .description('Add a host file to the image')
111
+ .description('Add a host file to a disk in the image')
86
112
  .argument('<image>', 'Path to image file')
87
113
  .argument('<file>', 'Host file to add')
88
114
  .option('-n, --name <name>', 'Target 8.3 filename (default: source filename)')
89
- .action((image: string, file: string, opts: { name?: string }) => {
115
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
116
+ .action((image: string, file: string, opts: { name?: string; disk: number }) => {
90
117
  const buf = openImage(image);
91
- addFile(buf, file, opts.name);
118
+ addFile(buf, file, opts.disk, opts.name);
92
119
  saveImage(image, buf);
93
- console.log(`Added ${opts.name ?? file} to ${image}`);
120
+ console.log(`Added ${opts.name ?? file} to disk ${opts.disk} of ${image}`);
94
121
  });
95
122
 
96
123
  // ── remove ──────────────────────────────────────────────────────────────────
97
124
  program
98
125
  .command('remove')
99
- .description('Delete a file entry from the image')
126
+ .description('Delete a file entry from a disk in the image')
100
127
  .argument('<image>', 'Path to image file')
101
128
  .argument('<name>', 'Filename to remove (8.3 format)')
102
- .action((image: string, name: string) => {
129
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
130
+ .action((image: string, name: string, opts: { disk: number }) => {
103
131
  const buf = openImage(image);
104
- removeFile(buf, name);
132
+ removeFile(buf, name, opts.disk);
105
133
  saveImage(image, buf);
106
- console.log(`Removed ${name} from ${image}`);
134
+ console.log(`Removed ${name} from disk ${opts.disk} of ${image}`);
107
135
  });
108
136
 
109
137
  // ── extract ─────────────────────────────────────────────────────────────────
110
138
  program
111
139
  .command('extract')
112
- .description('Extract a file from the image to the host filesystem')
140
+ .description('Extract a file from a disk in the image to the host filesystem')
113
141
  .argument('<image>', 'Path to image file')
114
142
  .argument('<name>', 'Filename to extract (8.3 format)')
115
143
  .argument('[output]', 'Output path (default: original filename)')
116
- .action((image: string, name: string, output?: string) => {
144
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
145
+ .action((image: string, name: string, output: string | undefined, opts: { disk: number }) => {
117
146
  const buf = openImage(image);
118
- extractFile(buf, name, output);
119
- console.log(`Extracted ${name} from ${image}`);
147
+ extractFile(buf, name, output, opts.disk);
148
+ console.log(`Extracted ${name} from disk ${opts.disk} of ${image}`);
120
149
  });
121
150
 
122
151
  // ── defrag ──────────────────────────────────────────────────────────────────
123
152
  program
124
153
  .command('defrag')
125
- .description('Defragment the image (compact files)')
154
+ .description('Defragment a disk in the image (compact files)')
126
155
  .argument('<image>', 'Path to image file')
127
- .action((image: string) => {
156
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
157
+ .action((image: string, opts: { disk: number }) => {
128
158
  const buf = openImage(image);
129
- defragment(buf);
159
+ defragment(buf, opts.disk);
130
160
  saveImage(image, buf);
131
- console.log(`Defragmented ${image}`);
161
+ console.log(`Defragmented disk ${opts.disk} of ${image}`);
132
162
  });
133
163
 
134
164
  // ── clear ───────────────────────────────────────────────────────────────────
135
165
  program
136
166
  .command('clear')
137
- .description('Clear all directory entries')
167
+ .description("Clear a disk's directory entries")
138
168
  .argument('<image>', 'Path to image file')
139
169
  .option('-y, --yes', 'Skip confirmation prompt')
140
- .action(async (image: string, opts: { yes?: boolean }) => {
170
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
171
+ .action(async (image: string, opts: { yes?: boolean; disk: number }) => {
141
172
  if (!opts.yes) {
142
173
  const rl = createInterface({ input: stdin, output: stdout });
143
- const answer = await rl.question(`Clear all entries in ${image}? [y/N] `);
174
+ const answer = await rl.question(`Clear all entries on disk ${opts.disk} of ${image}? [y/N] `);
144
175
  rl.close();
145
176
  if (answer.toLowerCase() !== 'y') {
146
177
  console.log('Aborted.');
@@ -148,23 +179,31 @@ program
148
179
  }
149
180
  }
150
181
  const buf = openImage(image);
151
- clearImage(buf);
182
+ clearImage(buf, opts.disk);
152
183
  saveImage(image, buf);
153
- console.log(`Cleared all directory entries in ${image}`);
184
+ console.log(`Cleared all directory entries on disk ${opts.disk} of ${image}`);
154
185
  });
155
186
 
156
187
  // ── info ────────────────────────────────────────────────────────────────────
157
188
  program
158
189
  .command('info')
159
- .description('Display image statistics')
190
+ .description('Display statistics for a disk in the image')
160
191
  .argument('<image>', 'Path to image file')
161
- .action((image: string) => {
192
+ .option('-d, --disk <n>', 'Disk bank to operate on (0-255)', parseDisk, 0)
193
+ .action((image: string, opts: { disk: number }) => {
162
194
  const buf = openImage(image);
163
- const info = imageInfo(buf);
195
+ const info = imageInfo(buf, opts.disk);
164
196
  console.log(`Image size: ${(info.totalSectors * 512).toLocaleString()} bytes (${info.totalSectors.toLocaleString()} sectors)`);
197
+ console.log(`Disks: ${info.totalDisks} × 1 MB (showing disk ${info.disk})`);
165
198
  console.log(`Directory: ${info.usedEntries}/16 entries used, ${info.freeEntries} free`);
166
- console.log(`Data sectors: ${info.usedDataSectors} used, ${info.freeDataSectors.toLocaleString()} free`);
167
- console.log(`Next free sector: ${info.nextFreeSector}`);
199
+ console.log(`Data sectors: ${info.usedDataSectors} used, ${info.freeDataSectors.toLocaleString()} free (of ${(SECTORS_PER_DISK - 1).toLocaleString()} per disk)`);
200
+ console.log(`Next free sector: ${info.nextFreeSector} (disk-relative)`);
168
201
  });
169
202
 
170
- program.parse();
203
+ // Run, surfacing operational errors as clean one-line messages instead of
204
+ // raw Node stack traces. Commander's own usage errors are left untouched.
205
+ program.parseAsync().catch((err: unknown) => {
206
+ const message = err instanceof Error ? err.message : String(err);
207
+ console.error(`Error: ${message}`);
208
+ process.exit(1);
209
+ });
@@ -4,6 +4,14 @@ export const DATA_START = 1;
4
4
  export const MAX_FILES = 16;
5
5
  export const ENTRY_SIZE = 32;
6
6
 
7
+ // Disk banking: the CF card is divided into up to 256 "disk" banks of
8
+ // SECTORS_PER_DISK sectors (1 MB) each. Disk n's directory lives at absolute
9
+ // LBA n * SECTORS_PER_DISK, and its data sectors follow contiguously. Start
10
+ // sectors in directory entries are stored relative to the disk's base LBA, and
11
+ // a file may not spill past its disk's region (matches BIOS FS_DISK_SECTORS).
12
+ export const SECTORS_PER_DISK = 2048; // 2048 * 512 = 1 MB
13
+ export const MAX_DISKS = 256; // 256 disks = 256 MB total
14
+
7
15
  // Field offsets within a directory entry
8
16
  export const NAME_OFFSET = 0;
9
17
  export const NAME_LENGTH = 8;
@@ -5,13 +5,14 @@ import {
5
5
  DATA_START,
6
6
  } from './constants.js';
7
7
  import { DirEntry } from './types.js';
8
- import { readSector, writeSector } from './image.js';
8
+ import { readSector, writeSector, diskBaseLba } from './image.js';
9
9
 
10
10
  /**
11
- * Parse all 16 directory entries from LBA 0.
11
+ * Parse all 16 directory entries from the given disk's directory sector.
12
+ * Start sectors are stored relative to the disk's base LBA.
12
13
  */
13
- export function readDirectory(buf: Buffer): DirEntry[] {
14
- const sector = readSector(buf, DIR_LBA);
14
+ export function readDirectory(buf: Buffer, disk = 0): DirEntry[] {
15
+ const sector = readSector(buf, diskBaseLba(disk) + DIR_LBA);
15
16
  const entries: DirEntry[] = [];
16
17
 
17
18
  for (let i = 0; i < MAX_FILES; i++) {
@@ -29,9 +30,9 @@ export function readDirectory(buf: Buffer): DirEntry[] {
29
30
  }
30
31
 
31
32
  /**
32
- * Serialize directory entries back to LBA 0.
33
+ * Serialize directory entries back to the given disk's directory sector.
33
34
  */
34
- export function writeDirectory(buf: Buffer, entries: DirEntry[]): void {
35
+ export function writeDirectory(buf: Buffer, entries: DirEntry[], disk = 0): void {
35
36
  const sector = Buffer.alloc(SECTOR_SIZE);
36
37
 
37
38
  for (const entry of entries) {
@@ -49,7 +50,7 @@ export function writeDirectory(buf: Buffer, entries: DirEntry[]): void {
49
50
  // Reserved bytes stay zeroed
50
51
  }
51
52
 
52
- writeSector(buf, DIR_LBA, sector);
53
+ writeSector(buf, diskBaseLba(disk) + DIR_LBA, sector);
53
54
  }
54
55
 
55
56
  /**
package/src/lib/image.ts CHANGED
@@ -1,5 +1,31 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
- import { SECTOR_SIZE } from './constants.js';
2
+ import { SECTOR_SIZE, SECTORS_PER_DISK, MAX_DISKS } from './constants.js';
3
+
4
+ /**
5
+ * Absolute base LBA of a disk bank. Disk n starts at LBA n * SECTORS_PER_DISK,
6
+ * where its directory sector lives; data sectors follow contiguously.
7
+ */
8
+ export function diskBaseLba(disk: number): number {
9
+ return disk * SECTORS_PER_DISK;
10
+ }
11
+
12
+ /**
13
+ * Validate a disk number and ensure its region fits within the image.
14
+ * Throws with a descriptive message otherwise.
15
+ */
16
+ export function validateDisk(buf: Buffer, disk: number): void {
17
+ if (!Number.isInteger(disk) || disk < 0 || disk >= MAX_DISKS) {
18
+ throw new Error(`Invalid disk: ${disk} (must be 0-${MAX_DISKS - 1})`);
19
+ }
20
+ const totalSectors = buf.length / SECTOR_SIZE;
21
+ const base = diskBaseLba(disk);
22
+ if (base + SECTORS_PER_DISK > totalSectors) {
23
+ const availableDisks = Math.floor(totalSectors / SECTORS_PER_DISK);
24
+ throw new Error(
25
+ `Disk ${disk} does not fit in image: it holds ${availableDisks} disk(s) (0-${availableDisks - 1})`
26
+ );
27
+ }
28
+ }
3
29
 
4
30
  /**
5
31
  * Read an entire image file into a Buffer.