cffs-image-tool 1.0.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/LICENSE +21 -0
- package/README.md +86 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +153 -0
- package/dist/cli.js.map +1 -0
- package/dist/lib/constants.d.ts +15 -0
- package/dist/lib/constants.js +18 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/directory.d.ts +28 -0
- package/dist/lib/directory.js +85 -0
- package/dist/lib/directory.js.map +1 -0
- package/dist/lib/filename.d.ts +13 -0
- package/dist/lib/filename.js +48 -0
- package/dist/lib/filename.js.map +1 -0
- package/dist/lib/image.d.ts +20 -0
- package/dist/lib/image.js +46 -0
- package/dist/lib/image.js.map +1 -0
- package/dist/lib/operations.d.ts +36 -0
- package/dist/lib/operations.js +161 -0
- package/dist/lib/operations.js.map +1 -0
- package/dist/lib/types.d.ts +8 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/package.json +31 -0
- package/src/cli.ts +170 -0
- package/src/lib/constants.ts +19 -0
- package/src/lib/directory.ts +104 -0
- package/src/lib/filename.ts +55 -0
- package/src/lib/image.ts +50 -0
- package/src/lib/operations.ts +204 -0
- package/src/lib/types.ts +8 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { basename } from 'node:path';
|
|
3
|
+
import { SECTOR_SIZE, FLAG_USED, DATA_START } from './constants.js';
|
|
4
|
+
import { DirEntry } from './types.js';
|
|
5
|
+
import { parseName, formatName } from './filename.js';
|
|
6
|
+
import { writeSector } from './image.js';
|
|
7
|
+
import {
|
|
8
|
+
readDirectory, writeDirectory, findFile, findFreeSlot,
|
|
9
|
+
calcNextSector, calcSectorCount,
|
|
10
|
+
} from './directory.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Add a host file to the image. Optionally rename with targetName (8.3 format).
|
|
14
|
+
*/
|
|
15
|
+
export function addFile(buf: Buffer, hostPath: string, targetName?: string): void {
|
|
16
|
+
const data = readFileSync(hostPath);
|
|
17
|
+
|
|
18
|
+
if (data.length > 0xFFFF) {
|
|
19
|
+
throw new Error(`File too large: ${data.length} bytes (max 65535)`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const nameInput = targetName ?? basename(hostPath);
|
|
23
|
+
const { name, ext } = parseName(nameInput);
|
|
24
|
+
|
|
25
|
+
const entries = readDirectory(buf);
|
|
26
|
+
|
|
27
|
+
// If file with same name exists, clear old entry (overwrite behavior matches BIOS FsSaveFile)
|
|
28
|
+
const existing = findFile(entries, name, ext);
|
|
29
|
+
if (existing) {
|
|
30
|
+
existing.flags = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const slotIndex = findFreeSlot(entries);
|
|
34
|
+
if (slotIndex === -1) {
|
|
35
|
+
throw new Error('Directory is full (16 entries maximum)');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Recalculate next sector after potentially clearing old entry
|
|
39
|
+
const startSector = calcNextSector(entries);
|
|
40
|
+
|
|
41
|
+
// Check that the file data fits in the image
|
|
42
|
+
const sectorCount = calcSectorCount(data.length);
|
|
43
|
+
const endByte = (startSector + sectorCount) * SECTOR_SIZE;
|
|
44
|
+
if (endByte > buf.length) {
|
|
45
|
+
throw new Error(`Not enough space in image: need sector ${startSector + sectorCount - 1}, image has ${buf.length / SECTOR_SIZE} sectors`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Write file data to sectors
|
|
49
|
+
for (let i = 0; i < sectorCount; i++) {
|
|
50
|
+
const chunk = Buffer.alloc(SECTOR_SIZE);
|
|
51
|
+
const srcOffset = i * SECTOR_SIZE;
|
|
52
|
+
const remaining = Math.min(SECTOR_SIZE, data.length - srcOffset);
|
|
53
|
+
if (remaining > 0) {
|
|
54
|
+
data.copy(chunk, 0, srcOffset, srcOffset + remaining);
|
|
55
|
+
}
|
|
56
|
+
writeSector(buf, startSector + i, chunk);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Update directory entry
|
|
60
|
+
entries[slotIndex] = {
|
|
61
|
+
name,
|
|
62
|
+
ext,
|
|
63
|
+
flags: FLAG_USED,
|
|
64
|
+
startSector,
|
|
65
|
+
fileSize: data.length,
|
|
66
|
+
index: slotIndex,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
writeDirectory(buf, entries);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Remove a file by name (clears flags only, no compaction — matches BIOS FsDeleteFile).
|
|
74
|
+
*/
|
|
75
|
+
export function removeFile(buf: Buffer, nameInput: string): void {
|
|
76
|
+
const { name, ext } = parseName(nameInput);
|
|
77
|
+
const entries = readDirectory(buf);
|
|
78
|
+
const entry = findFile(entries, name, ext);
|
|
79
|
+
|
|
80
|
+
if (!entry) {
|
|
81
|
+
throw new Error(`File not found: ${nameInput}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
entry.flags = 0;
|
|
85
|
+
writeDirectory(buf, entries);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* List all in-use directory entries.
|
|
90
|
+
*/
|
|
91
|
+
export function listFiles(buf: Buffer): DirEntry[] {
|
|
92
|
+
return readDirectory(buf).filter((e) => (e.flags & FLAG_USED) !== 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Extract a file from the image to the host filesystem.
|
|
97
|
+
*/
|
|
98
|
+
export function extractFile(buf: Buffer, nameInput: string, outputPath?: string): void {
|
|
99
|
+
const { name, ext } = parseName(nameInput);
|
|
100
|
+
const entries = readDirectory(buf);
|
|
101
|
+
const entry = findFile(entries, name, ext);
|
|
102
|
+
|
|
103
|
+
if (!entry) {
|
|
104
|
+
throw new Error(`File not found: ${nameInput}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const sectorCount = calcSectorCount(entry.fileSize);
|
|
108
|
+
const output = Buffer.alloc(entry.fileSize);
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < sectorCount; i++) {
|
|
111
|
+
const offset = (entry.startSector + i) * SECTOR_SIZE;
|
|
112
|
+
const srcSlice = buf.subarray(offset, offset + SECTOR_SIZE);
|
|
113
|
+
const dstOffset = i * SECTOR_SIZE;
|
|
114
|
+
const remaining = Math.min(SECTOR_SIZE, entry.fileSize - dstOffset);
|
|
115
|
+
srcSlice.copy(output, dstOffset, 0, remaining);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outPath = outputPath ?? formatName(entry);
|
|
119
|
+
writeFileSync(outPath, output);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Defragment the image: sort used entries by startSector, rewrite contiguously from LBA 1.
|
|
124
|
+
*/
|
|
125
|
+
export function defragment(buf: Buffer): void {
|
|
126
|
+
const entries = readDirectory(buf);
|
|
127
|
+
const usedEntries = entries
|
|
128
|
+
.filter((e) => (e.flags & FLAG_USED) !== 0)
|
|
129
|
+
.sort((a, b) => a.startSector - b.startSector);
|
|
130
|
+
|
|
131
|
+
let currentSector = DATA_START;
|
|
132
|
+
|
|
133
|
+
for (const entry of usedEntries) {
|
|
134
|
+
const sectorCount = calcSectorCount(entry.fileSize);
|
|
135
|
+
|
|
136
|
+
// Only move if not already in the right spot
|
|
137
|
+
if (entry.startSector !== currentSector) {
|
|
138
|
+
// Read file data from current location
|
|
139
|
+
const fileData = Buffer.alloc(sectorCount * SECTOR_SIZE);
|
|
140
|
+
for (let i = 0; i < sectorCount; i++) {
|
|
141
|
+
const srcOffset = (entry.startSector + i) * SECTOR_SIZE;
|
|
142
|
+
buf.copy(fileData, i * SECTOR_SIZE, srcOffset, srcOffset + SECTOR_SIZE);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Write to new location
|
|
146
|
+
for (let i = 0; i < sectorCount; i++) {
|
|
147
|
+
const chunk = fileData.subarray(i * SECTOR_SIZE, (i + 1) * SECTOR_SIZE);
|
|
148
|
+
writeSector(buf, currentSector + i, chunk);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
entry.startSector = currentSector;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
currentSector += sectorCount;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Zero out any sectors after the last used file (reclaim space)
|
|
158
|
+
const totalSectors = buf.length / SECTOR_SIZE;
|
|
159
|
+
for (let s = currentSector; s < totalSectors; s++) {
|
|
160
|
+
buf.fill(0, s * SECTOR_SIZE, (s + 1) * SECTOR_SIZE);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Write updated directory
|
|
164
|
+
writeDirectory(buf, entries);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Clear all directory entries (zero the directory sector).
|
|
169
|
+
*/
|
|
170
|
+
export function clearImage(buf: Buffer): void {
|
|
171
|
+
const sector = Buffer.alloc(SECTOR_SIZE);
|
|
172
|
+
writeSector(buf, 0, sector);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Return image stats.
|
|
177
|
+
*/
|
|
178
|
+
export function imageInfo(buf: Buffer): {
|
|
179
|
+
totalSectors: number;
|
|
180
|
+
usedEntries: number;
|
|
181
|
+
freeEntries: number;
|
|
182
|
+
nextFreeSector: number;
|
|
183
|
+
usedDataSectors: number;
|
|
184
|
+
freeDataSectors: number;
|
|
185
|
+
} {
|
|
186
|
+
const entries = readDirectory(buf);
|
|
187
|
+
const usedEntries = entries.filter((e) => (e.flags & FLAG_USED) !== 0);
|
|
188
|
+
const nextFreeSector = calcNextSector(entries);
|
|
189
|
+
const totalSectors = buf.length / SECTOR_SIZE;
|
|
190
|
+
|
|
191
|
+
let usedDataSectors = 0;
|
|
192
|
+
for (const e of usedEntries) {
|
|
193
|
+
usedDataSectors += calcSectorCount(e.fileSize);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
totalSectors,
|
|
198
|
+
usedEntries: usedEntries.length,
|
|
199
|
+
freeEntries: entries.length - usedEntries.length,
|
|
200
|
+
nextFreeSector,
|
|
201
|
+
usedDataSectors,
|
|
202
|
+
freeDataSectors: totalSectors - 1 - usedDataSectors, // -1 for directory sector
|
|
203
|
+
};
|
|
204
|
+
}
|
package/src/lib/types.ts
ADDED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"sourceMap": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|