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/src/lib/operations.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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 { DirEntry } from './types.js';
|
|
5
5
|
import { parseName, formatName } from './filename.js';
|
|
6
|
-
import { writeSector } from './image.js';
|
|
6
|
+
import { writeSector, diskBaseLba, validateDisk } from './image.js';
|
|
7
7
|
import {
|
|
8
8
|
readDirectory, writeDirectory, findFile, findFreeSlot,
|
|
9
9
|
calcNextSector, calcSectorCount,
|
|
10
10
|
} from './directory.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Add a host file to the
|
|
13
|
+
* Add a host file to the given disk. Optionally rename with targetName (8.3 format).
|
|
14
|
+
* Start sectors are disk-relative; a file may not spill past its disk's region.
|
|
14
15
|
*/
|
|
15
|
-
export function addFile(buf: Buffer, hostPath: string, targetName?: string): void {
|
|
16
|
+
export function addFile(buf: Buffer, hostPath: string, disk = 0, targetName?: string): void {
|
|
17
|
+
validateDisk(buf, disk);
|
|
16
18
|
const data = readFileSync(hostPath);
|
|
17
19
|
|
|
18
20
|
if (data.length > 0xFFFF) {
|
|
@@ -22,7 +24,7 @@ export function addFile(buf: Buffer, hostPath: string, targetName?: string): voi
|
|
|
22
24
|
const nameInput = targetName ?? basename(hostPath);
|
|
23
25
|
const { name, ext } = parseName(nameInput);
|
|
24
26
|
|
|
25
|
-
const entries = readDirectory(buf);
|
|
27
|
+
const entries = readDirectory(buf, disk);
|
|
26
28
|
|
|
27
29
|
// If file with same name exists, clear old entry (overwrite behavior matches BIOS FsSaveFile)
|
|
28
30
|
const existing = findFile(entries, name, ext);
|
|
@@ -35,17 +37,18 @@ export function addFile(buf: Buffer, hostPath: string, targetName?: string): voi
|
|
|
35
37
|
throw new Error('Directory is full (16 entries maximum)');
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Recalculate next sector after potentially clearing old entry
|
|
40
|
+
// Recalculate next sector (disk-relative) after potentially clearing old entry
|
|
39
41
|
const startSector = calcNextSector(entries);
|
|
40
|
-
|
|
41
|
-
// Check that the file data fits in the image
|
|
42
42
|
const sectorCount = calcSectorCount(data.length);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
|
|
44
|
+
// Disk-full guard: the file must fit within this disk's region (matches
|
|
45
|
+
// BIOS FsSaveFile: end sector must be <= FS_DISK_SECTORS).
|
|
46
|
+
if (startSector + sectorCount > SECTORS_PER_DISK) {
|
|
47
|
+
throw new Error(`Not enough space on disk ${disk}: need sector ${startSector + sectorCount}, disk holds ${SECTORS_PER_DISK} sectors`);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
// Write file data to sectors
|
|
50
|
+
// Write file data to absolute sectors within the disk region
|
|
51
|
+
const base = diskBaseLba(disk);
|
|
49
52
|
for (let i = 0; i < sectorCount; i++) {
|
|
50
53
|
const chunk = Buffer.alloc(SECTOR_SIZE);
|
|
51
54
|
const srcOffset = i * SECTOR_SIZE;
|
|
@@ -53,7 +56,7 @@ export function addFile(buf: Buffer, hostPath: string, targetName?: string): voi
|
|
|
53
56
|
if (remaining > 0) {
|
|
54
57
|
data.copy(chunk, 0, srcOffset, srcOffset + remaining);
|
|
55
58
|
}
|
|
56
|
-
writeSector(buf, startSector + i, chunk);
|
|
59
|
+
writeSector(buf, base + startSector + i, chunk);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
// Update directory entry
|
|
@@ -66,15 +69,16 @@ export function addFile(buf: Buffer, hostPath: string, targetName?: string): voi
|
|
|
66
69
|
index: slotIndex,
|
|
67
70
|
};
|
|
68
71
|
|
|
69
|
-
writeDirectory(buf, entries);
|
|
72
|
+
writeDirectory(buf, entries, disk);
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|
|
73
76
|
* Remove a file by name (clears flags only, no compaction — matches BIOS FsDeleteFile).
|
|
74
77
|
*/
|
|
75
|
-
export function removeFile(buf: Buffer, nameInput: string): void {
|
|
78
|
+
export function removeFile(buf: Buffer, nameInput: string, disk = 0): void {
|
|
79
|
+
validateDisk(buf, disk);
|
|
76
80
|
const { name, ext } = parseName(nameInput);
|
|
77
|
-
const entries = readDirectory(buf);
|
|
81
|
+
const entries = readDirectory(buf, disk);
|
|
78
82
|
const entry = findFile(entries, name, ext);
|
|
79
83
|
|
|
80
84
|
if (!entry) {
|
|
@@ -82,33 +86,36 @@ export function removeFile(buf: Buffer, nameInput: string): void {
|
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
entry.flags = 0;
|
|
85
|
-
writeDirectory(buf, entries);
|
|
89
|
+
writeDirectory(buf, entries, disk);
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
/**
|
|
89
|
-
* List all in-use directory entries.
|
|
93
|
+
* List all in-use directory entries on the given disk.
|
|
90
94
|
*/
|
|
91
|
-
export function listFiles(buf: Buffer): DirEntry[] {
|
|
92
|
-
|
|
95
|
+
export function listFiles(buf: Buffer, disk = 0): DirEntry[] {
|
|
96
|
+
validateDisk(buf, disk);
|
|
97
|
+
return readDirectory(buf, disk).filter((e) => (e.flags & FLAG_USED) !== 0);
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
/**
|
|
96
|
-
* Extract a file from the
|
|
101
|
+
* Extract a file from the given disk to the host filesystem.
|
|
97
102
|
*/
|
|
98
|
-
export function extractFile(buf: Buffer, nameInput: string, outputPath?: string): void {
|
|
103
|
+
export function extractFile(buf: Buffer, nameInput: string, outputPath?: string, disk = 0): void {
|
|
104
|
+
validateDisk(buf, disk);
|
|
99
105
|
const { name, ext } = parseName(nameInput);
|
|
100
|
-
const entries = readDirectory(buf);
|
|
106
|
+
const entries = readDirectory(buf, disk);
|
|
101
107
|
const entry = findFile(entries, name, ext);
|
|
102
108
|
|
|
103
109
|
if (!entry) {
|
|
104
110
|
throw new Error(`File not found: ${nameInput}`);
|
|
105
111
|
}
|
|
106
112
|
|
|
113
|
+
const base = diskBaseLba(disk);
|
|
107
114
|
const sectorCount = calcSectorCount(entry.fileSize);
|
|
108
115
|
const output = Buffer.alloc(entry.fileSize);
|
|
109
116
|
|
|
110
117
|
for (let i = 0; i < sectorCount; i++) {
|
|
111
|
-
const offset = (entry.startSector + i) * SECTOR_SIZE;
|
|
118
|
+
const offset = (base + entry.startSector + i) * SECTOR_SIZE;
|
|
112
119
|
const srcSlice = buf.subarray(offset, offset + SECTOR_SIZE);
|
|
113
120
|
const dstOffset = i * SECTOR_SIZE;
|
|
114
121
|
const remaining = Math.min(SECTOR_SIZE, entry.fileSize - dstOffset);
|
|
@@ -120,10 +127,13 @@ export function extractFile(buf: Buffer, nameInput: string, outputPath?: string)
|
|
|
120
127
|
}
|
|
121
128
|
|
|
122
129
|
/**
|
|
123
|
-
* Defragment
|
|
130
|
+
* Defragment a disk: sort used entries by startSector, rewrite contiguously
|
|
131
|
+
* from the disk's first data sector, and zero the remainder of the disk region.
|
|
124
132
|
*/
|
|
125
|
-
export function defragment(buf: Buffer): void {
|
|
126
|
-
|
|
133
|
+
export function defragment(buf: Buffer, disk = 0): void {
|
|
134
|
+
validateDisk(buf, disk);
|
|
135
|
+
const base = diskBaseLba(disk);
|
|
136
|
+
const entries = readDirectory(buf, disk);
|
|
127
137
|
const usedEntries = entries
|
|
128
138
|
.filter((e) => (e.flags & FLAG_USED) !== 0)
|
|
129
139
|
.sort((a, b) => a.startSector - b.startSector);
|
|
@@ -138,14 +148,14 @@ export function defragment(buf: Buffer): void {
|
|
|
138
148
|
// Read file data from current location
|
|
139
149
|
const fileData = Buffer.alloc(sectorCount * SECTOR_SIZE);
|
|
140
150
|
for (let i = 0; i < sectorCount; i++) {
|
|
141
|
-
const srcOffset = (entry.startSector + i) * SECTOR_SIZE;
|
|
151
|
+
const srcOffset = (base + entry.startSector + i) * SECTOR_SIZE;
|
|
142
152
|
buf.copy(fileData, i * SECTOR_SIZE, srcOffset, srcOffset + SECTOR_SIZE);
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
// Write to new location
|
|
146
156
|
for (let i = 0; i < sectorCount; i++) {
|
|
147
157
|
const chunk = fileData.subarray(i * SECTOR_SIZE, (i + 1) * SECTOR_SIZE);
|
|
148
|
-
writeSector(buf, currentSector + i, chunk);
|
|
158
|
+
writeSector(buf, base + currentSector + i, chunk);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
entry.startSector = currentSector;
|
|
@@ -154,36 +164,40 @@ export function defragment(buf: Buffer): void {
|
|
|
154
164
|
currentSector += sectorCount;
|
|
155
165
|
}
|
|
156
166
|
|
|
157
|
-
// Zero out any sectors after the last used file
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
buf.fill(0,
|
|
167
|
+
// Zero out any sectors after the last used file, up to this disk's boundary
|
|
168
|
+
for (let s = currentSector; s < SECTORS_PER_DISK; s++) {
|
|
169
|
+
const offset = (base + s) * SECTOR_SIZE;
|
|
170
|
+
buf.fill(0, offset, offset + SECTOR_SIZE);
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
// Write updated directory
|
|
164
|
-
writeDirectory(buf, entries);
|
|
174
|
+
writeDirectory(buf, entries, disk);
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
/**
|
|
168
|
-
* Clear all directory entries (zero
|
|
178
|
+
* Clear all directory entries on the given disk (zero its directory sector).
|
|
169
179
|
*/
|
|
170
|
-
export function clearImage(buf: Buffer): void {
|
|
180
|
+
export function clearImage(buf: Buffer, disk = 0): void {
|
|
181
|
+
validateDisk(buf, disk);
|
|
171
182
|
const sector = Buffer.alloc(SECTOR_SIZE);
|
|
172
|
-
writeSector(buf,
|
|
183
|
+
writeSector(buf, diskBaseLba(disk), sector);
|
|
173
184
|
}
|
|
174
185
|
|
|
175
186
|
/**
|
|
176
|
-
* Return
|
|
187
|
+
* Return stats for the given disk.
|
|
177
188
|
*/
|
|
178
|
-
export function imageInfo(buf: Buffer): {
|
|
189
|
+
export function imageInfo(buf: Buffer, disk = 0): {
|
|
179
190
|
totalSectors: number;
|
|
191
|
+
totalDisks: number;
|
|
192
|
+
disk: number;
|
|
180
193
|
usedEntries: number;
|
|
181
194
|
freeEntries: number;
|
|
182
195
|
nextFreeSector: number;
|
|
183
196
|
usedDataSectors: number;
|
|
184
197
|
freeDataSectors: number;
|
|
185
198
|
} {
|
|
186
|
-
|
|
199
|
+
validateDisk(buf, disk);
|
|
200
|
+
const entries = readDirectory(buf, disk);
|
|
187
201
|
const usedEntries = entries.filter((e) => (e.flags & FLAG_USED) !== 0);
|
|
188
202
|
const nextFreeSector = calcNextSector(entries);
|
|
189
203
|
const totalSectors = buf.length / SECTOR_SIZE;
|
|
@@ -195,10 +209,13 @@ export function imageInfo(buf: Buffer): {
|
|
|
195
209
|
|
|
196
210
|
return {
|
|
197
211
|
totalSectors,
|
|
212
|
+
totalDisks: Math.floor(totalSectors / SECTORS_PER_DISK),
|
|
213
|
+
disk,
|
|
198
214
|
usedEntries: usedEntries.length,
|
|
199
215
|
freeEntries: entries.length - usedEntries.length,
|
|
200
216
|
nextFreeSector,
|
|
201
217
|
usedDataSectors,
|
|
202
|
-
|
|
218
|
+
// Data sectors available in this disk: SECTORS_PER_DISK - 1 (directory) - used
|
|
219
|
+
freeDataSectors: SECTORS_PER_DISK - 1 - usedDataSectors,
|
|
203
220
|
};
|
|
204
221
|
}
|