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,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 image. Optionally rename with targetName (8.3 format).
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
- 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`);
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
- return readDirectory(buf).filter((e) => (e.flags & FLAG_USED) !== 0);
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 image to the host filesystem.
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 the image: sort used entries by startSector, rewrite contiguously from LBA 1.
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
- const entries = readDirectory(buf);
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 (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);
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 the directory sector).
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, 0, sector);
183
+ writeSector(buf, diskBaseLba(disk), sector);
173
184
  }
174
185
 
175
186
  /**
176
- * Return image stats.
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
- const entries = readDirectory(buf);
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
- freeDataSectors: totalSectors - 1 - usedDataSectors, // -1 for directory sector
218
+ // Data sectors available in this disk: SECTORS_PER_DISK - 1 (directory) - used
219
+ freeDataSectors: SECTORS_PER_DISK - 1 - usedDataSectors,
203
220
  };
204
221
  }