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.
- package/README.md +46 -13
- package/dist/cli.js +72 -33
- package/dist/cli.js.map +1 -1
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +7 -0
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/directory.d.ts +5 -4
- package/dist/lib/directory.js +8 -7
- package/dist/lib/directory.js.map +1 -1
- package/dist/lib/image.d.ts +10 -0
- package/dist/lib/image.js +23 -1
- package/dist/lib/image.js.map +1 -1
- package/dist/lib/operations.d.ts +17 -13
- package/dist/lib/operations.js +55 -40
- package/dist/lib/operations.js.map +1 -1
- package/package.json +1 -1
- package/src/cli.ts +75 -36
- package/src/lib/constants.ts +8 -0
- package/src/lib/directory.ts +8 -7
- package/src/lib/image.ts +27 -1
- package/src/lib/operations.ts +58 -41
package/dist/lib/operations.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
buf.fill(0,
|
|
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
|
|
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,
|
|
149
|
+
writeSector(buf, diskBaseLba(disk), sector);
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
|
-
* Return
|
|
152
|
+
* Return stats for the given disk.
|
|
142
153
|
*/
|
|
143
|
-
export function imageInfo(buf) {
|
|
144
|
-
|
|
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
|
-
|
|
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":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"operations.js","sourceRoot":"","sources":["../../src/lib/operations.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
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.
|
|
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.
|
|
45
|
-
.
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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
|
-
.
|
|
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(
|
|
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
|
-
.
|
|
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
|
|
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
|
|
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
|
|
190
|
+
.description('Display statistics for a disk in the image')
|
|
160
191
|
.argument('<image>', 'Path to image file')
|
|
161
|
-
.
|
|
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
|
-
|
|
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
|
+
});
|
package/src/lib/constants.ts
CHANGED
|
@@ -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;
|
package/src/lib/directory.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|