@kduma-oss/pcf 0.0.1
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 +116 -0
- package/dist/consts.d.ts +42 -0
- package/dist/consts.d.ts.map +1 -0
- package/dist/consts.js +44 -0
- package/dist/consts.js.map +1 -0
- package/dist/container.d.ts +124 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +441 -0
- package/dist/container.js.map +1 -0
- package/dist/crc.d.ts +15 -0
- package/dist/crc.d.ts.map +1 -0
- package/dist/crc.js +62 -0
- package/dist/crc.js.map +1 -0
- package/dist/entry.d.ts +44 -0
- package/dist/entry.d.ts.map +1 -0
- package/dist/entry.js +118 -0
- package/dist/entry.js.map +1 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +88 -0
- package/dist/errors.js.map +1 -0
- package/dist/hash.d.ts +59 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +151 -0
- package/dist/hash.js.map +1 -0
- package/dist/header.d.ts +20 -0
- package/dist/header.d.ts.map +1 -0
- package/dist/header.js +38 -0
- package/dist/header.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +42 -0
- package/dist/index.js.map +1 -0
- package/dist/node-storage.d.ts +24 -0
- package/dist/node-storage.d.ts.map +1 -0
- package/dist/node-storage.js +52 -0
- package/dist/node-storage.js.map +1 -0
- package/dist/storage.d.ts +35 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +66 -0
- package/dist/storage.js.map +1 -0
- package/dist/table.d.ts +28 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +48 -0
- package/dist/table.js.map +1 -0
- package/package.json +60 -0
- package/src/consts.ts +50 -0
- package/src/container.ts +575 -0
- package/src/crc.ts +69 -0
- package/src/entry.ts +152 -0
- package/src/errors.ts +124 -0
- package/src/hash.ts +165 -0
- package/src/header.ts +50 -0
- package/src/index.ts +68 -0
- package/src/node-storage.ts +75 -0
- package/src/storage.ts +85 -0
- package/src/table.ts +67 -0
package/src/container.ts
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The high-level {@link Container} type: reading and writing whole PCF files.
|
|
3
|
+
*
|
|
4
|
+
* `Container` is backed by any {@link Storage}, so it works equally with an
|
|
5
|
+
* in-memory {@link MemoryStorage} and a file-backed `NodeFileStorage`.
|
|
6
|
+
*
|
|
7
|
+
* # Reader vs. writer scope
|
|
8
|
+
*
|
|
9
|
+
* The *reader* side (`open`, `entries`, `readPartitionData`, `verify`) is fully
|
|
10
|
+
* general: it accepts any conforming file, including arbitrary region placement
|
|
11
|
+
* and overflow-block chains.
|
|
12
|
+
*
|
|
13
|
+
* The *writer* side implements one documented placement strategy (the format
|
|
14
|
+
* deliberately leaves layout to the writer, spec section 12 / A7, A9):
|
|
15
|
+
*
|
|
16
|
+
* - The first table block sits immediately after the header and is created with
|
|
17
|
+
* reserved capacity for `firstBlockCapacity` entries, so entries can be
|
|
18
|
+
* appended in place without moving data.
|
|
19
|
+
* - Partition data is appended at a growing end-of-data cursor; each partition
|
|
20
|
+
* may reserve `extraReserve` spare bytes for later in-place growth.
|
|
21
|
+
* - When every known block is full, a new overflow block is appended and linked
|
|
22
|
+
* into the chain.
|
|
23
|
+
* - Block capacity is *not* stored in the file (spec A9); it is tracked only in
|
|
24
|
+
* memory for the lifetime of this handle. After {@link Container.open}, blocks
|
|
25
|
+
* are treated as having no spare capacity, so subsequent additions go into
|
|
26
|
+
* fresh overflow blocks. {@link Container.compactedImage} rebuilds a tightly
|
|
27
|
+
* packed file.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
ENTRY_SIZE,
|
|
32
|
+
HEADER_SIZE,
|
|
33
|
+
MAX_ENTRIES_PER_BLOCK,
|
|
34
|
+
NIL_UID,
|
|
35
|
+
TABLE_HEADER_SIZE,
|
|
36
|
+
TYPE_RESERVED,
|
|
37
|
+
VERSION_MAJOR,
|
|
38
|
+
VERSION_MINOR,
|
|
39
|
+
} from "./consts.js";
|
|
40
|
+
import {
|
|
41
|
+
encodeLabel,
|
|
42
|
+
entryFromBytes,
|
|
43
|
+
entryToBytes,
|
|
44
|
+
type PartitionEntry,
|
|
45
|
+
validateEntry,
|
|
46
|
+
} from "./entry.js";
|
|
47
|
+
import { PcfError } from "./errors.js";
|
|
48
|
+
import { computeHashField, digestLen, HashAlgo, verifies } from "./hash.js";
|
|
49
|
+
import {
|
|
50
|
+
type FileHeader,
|
|
51
|
+
headerFromBytes,
|
|
52
|
+
headerToBytes,
|
|
53
|
+
} from "./header.js";
|
|
54
|
+
import { MemoryStorage, type Storage } from "./storage.js";
|
|
55
|
+
import {
|
|
56
|
+
computeTableHash,
|
|
57
|
+
tableHeaderFromBytes,
|
|
58
|
+
tableHeaderToBytes,
|
|
59
|
+
type TableBlockHeader,
|
|
60
|
+
} from "./table.js";
|
|
61
|
+
|
|
62
|
+
/** In-memory bookkeeping for one table block (not stored on disk). */
|
|
63
|
+
interface BlockInfo {
|
|
64
|
+
offset: number;
|
|
65
|
+
capacity: number;
|
|
66
|
+
count: number;
|
|
67
|
+
algo: HashAlgo;
|
|
68
|
+
next: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One table block read from disk: its absolute `offset`, its parsed
|
|
73
|
+
* {@link TableBlockHeader} (including `tableHash` and `nextTableOffset`), and
|
|
74
|
+
* its {@link PartitionEntry} list.
|
|
75
|
+
*
|
|
76
|
+
* Returned by {@link Container.readBlockAt}. It lets code layered on PCF group
|
|
77
|
+
* blocks, inspect each block's `tableHash`, and follow non-default
|
|
78
|
+
* `nextTableOffset` chains, instead of {@link Container.entries} which flattens
|
|
79
|
+
* the whole chain.
|
|
80
|
+
*/
|
|
81
|
+
export interface BlockView {
|
|
82
|
+
/** Absolute file offset of the table block. */
|
|
83
|
+
offset: number;
|
|
84
|
+
/** Parsed 74-byte block header. */
|
|
85
|
+
header: TableBlockHeader;
|
|
86
|
+
/** The block's entries, in stored order. */
|
|
87
|
+
entries: PartitionEntry[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
|
|
91
|
+
if (a.length !== b.length) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
for (let i = 0; i < a.length; i++) {
|
|
95
|
+
if (a[i] !== b[i]) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** A PCF container backed by a {@link Storage}. */
|
|
103
|
+
export class Container {
|
|
104
|
+
private storage: Storage;
|
|
105
|
+
private fileHeader: FileHeader;
|
|
106
|
+
private blocks: BlockInfo[];
|
|
107
|
+
private dataEof: number;
|
|
108
|
+
private defaultCapacity: number;
|
|
109
|
+
private tableHashAlgo: HashAlgo;
|
|
110
|
+
|
|
111
|
+
private constructor(
|
|
112
|
+
storage: Storage,
|
|
113
|
+
fileHeader: FileHeader,
|
|
114
|
+
blocks: BlockInfo[],
|
|
115
|
+
dataEof: number,
|
|
116
|
+
defaultCapacity: number,
|
|
117
|
+
tableHashAlgo: HashAlgo,
|
|
118
|
+
) {
|
|
119
|
+
this.storage = storage;
|
|
120
|
+
this.fileHeader = fileHeader;
|
|
121
|
+
this.blocks = blocks;
|
|
122
|
+
this.dataEof = dataEof;
|
|
123
|
+
this.defaultCapacity = defaultCapacity;
|
|
124
|
+
this.tableHashAlgo = tableHashAlgo;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- construction ------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create an empty container with sensible defaults (first block capacity 16,
|
|
131
|
+
* table hashing with SHA-256). Defaults to an in-memory store.
|
|
132
|
+
*/
|
|
133
|
+
static create(storage: Storage = new MemoryStorage()): Container {
|
|
134
|
+
return Container.createWith(storage, 16, HashAlgo.Sha256);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create an empty container, choosing the first block's reserved capacity and
|
|
139
|
+
* the table-hash algorithm.
|
|
140
|
+
*/
|
|
141
|
+
static createWith(
|
|
142
|
+
storage: Storage,
|
|
143
|
+
firstBlockCapacity: number,
|
|
144
|
+
tableHashAlgo: HashAlgo,
|
|
145
|
+
): Container {
|
|
146
|
+
const cap = Math.min(
|
|
147
|
+
Math.max(firstBlockCapacity, 1),
|
|
148
|
+
MAX_ENTRIES_PER_BLOCK,
|
|
149
|
+
);
|
|
150
|
+
const header: FileHeader = {
|
|
151
|
+
versionMajor: VERSION_MAJOR,
|
|
152
|
+
versionMinor: VERSION_MINOR,
|
|
153
|
+
partitionTableOffset: BigInt(HEADER_SIZE),
|
|
154
|
+
};
|
|
155
|
+
storage.writeAt(0, headerToBytes(header));
|
|
156
|
+
|
|
157
|
+
const th = computeTableHash(tableHashAlgo, 0n, []);
|
|
158
|
+
const bh: TableBlockHeader = {
|
|
159
|
+
partitionCount: 0,
|
|
160
|
+
nextTableOffset: 0n,
|
|
161
|
+
tableHashAlgo,
|
|
162
|
+
tableHash: th,
|
|
163
|
+
};
|
|
164
|
+
storage.writeAt(HEADER_SIZE, tableHeaderToBytes(bh));
|
|
165
|
+
|
|
166
|
+
const dataEof = HEADER_SIZE + TABLE_HEADER_SIZE + cap * ENTRY_SIZE;
|
|
167
|
+
const blocks: BlockInfo[] = [
|
|
168
|
+
{ offset: HEADER_SIZE, capacity: cap, count: 0, algo: tableHashAlgo, next: 0 },
|
|
169
|
+
];
|
|
170
|
+
return new Container(storage, header, blocks, dataEof, cap, tableHashAlgo);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Open an existing container, validating the header (spec C1, C2). */
|
|
174
|
+
static open(storage: Storage): Container {
|
|
175
|
+
const hb = storage.readAt(0, HEADER_SIZE);
|
|
176
|
+
const header = headerFromBytes(hb);
|
|
177
|
+
|
|
178
|
+
const me = new Container(storage, header, [], 0, 16, HashAlgo.Sha256);
|
|
179
|
+
|
|
180
|
+
const blocks: BlockInfo[] = [];
|
|
181
|
+
let off = Number(header.partitionTableOffset);
|
|
182
|
+
while (off !== 0) {
|
|
183
|
+
const [h] = me.readBlock(off);
|
|
184
|
+
blocks.push({
|
|
185
|
+
offset: off,
|
|
186
|
+
capacity: h.partitionCount, // no known spare after open
|
|
187
|
+
count: h.partitionCount,
|
|
188
|
+
algo: h.tableHashAlgo,
|
|
189
|
+
next: Number(h.nextTableOffset),
|
|
190
|
+
});
|
|
191
|
+
off = Number(h.nextTableOffset);
|
|
192
|
+
}
|
|
193
|
+
if (blocks.length > 0) {
|
|
194
|
+
me.tableHashAlgo = blocks[0]!.algo;
|
|
195
|
+
}
|
|
196
|
+
me.blocks = blocks;
|
|
197
|
+
me.dataEof = storage.size();
|
|
198
|
+
return me;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Consume the container and return the backing store. */
|
|
202
|
+
intoStorage(): Storage {
|
|
203
|
+
return this.storage;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** The parsed file header. */
|
|
207
|
+
header(): FileHeader {
|
|
208
|
+
return this.fileHeader;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- low-level I/O ------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
private readBlock(off: number): [TableBlockHeader, PartitionEntry[]] {
|
|
214
|
+
const hb = this.storage.readAt(off, TABLE_HEADER_SIZE);
|
|
215
|
+
const h = tableHeaderFromBytes(hb);
|
|
216
|
+
const entries: PartitionEntry[] = [];
|
|
217
|
+
for (let i = 0; i < h.partitionCount; i++) {
|
|
218
|
+
const eb = this.storage.readAt(
|
|
219
|
+
off + TABLE_HEADER_SIZE + i * ENTRY_SIZE,
|
|
220
|
+
ENTRY_SIZE,
|
|
221
|
+
);
|
|
222
|
+
entries.push(entryFromBytes(eb));
|
|
223
|
+
}
|
|
224
|
+
return [h, entries];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private writeBlock(
|
|
228
|
+
off: number,
|
|
229
|
+
next: number,
|
|
230
|
+
algo: HashAlgo,
|
|
231
|
+
entries: readonly PartitionEntry[],
|
|
232
|
+
): void {
|
|
233
|
+
const hash = computeTableHash(algo, BigInt(next), entries);
|
|
234
|
+
const header: TableBlockHeader = {
|
|
235
|
+
partitionCount: entries.length,
|
|
236
|
+
nextTableOffset: BigInt(next),
|
|
237
|
+
tableHashAlgo: algo,
|
|
238
|
+
tableHash: hash,
|
|
239
|
+
};
|
|
240
|
+
this.storage.writeAt(off, tableHeaderToBytes(header));
|
|
241
|
+
const buf = new Uint8Array(entries.length * ENTRY_SIZE);
|
|
242
|
+
let p = 0;
|
|
243
|
+
for (const e of entries) {
|
|
244
|
+
buf.set(entryToBytes(e), p);
|
|
245
|
+
p += ENTRY_SIZE;
|
|
246
|
+
}
|
|
247
|
+
this.storage.writeAt(off + TABLE_HEADER_SIZE, buf);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- reading -----------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/** All live partition entries, in chain order. */
|
|
253
|
+
entries(): PartitionEntry[] {
|
|
254
|
+
const out: PartitionEntry[] = [];
|
|
255
|
+
let off = Number(this.fileHeader.partitionTableOffset);
|
|
256
|
+
while (off !== 0) {
|
|
257
|
+
const [h, entries] = this.readBlock(off);
|
|
258
|
+
out.push(...entries);
|
|
259
|
+
off = Number(h.nextTableOffset);
|
|
260
|
+
}
|
|
261
|
+
return out;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Read a single table block at an absolute `offset`, returning its parsed
|
|
266
|
+
* header (including `tableHash`) and entries. Unlike {@link entries}, which
|
|
267
|
+
* flattens the whole chain, this exposes one block at a time so a caller can
|
|
268
|
+
* follow an arbitrary `nextTableOffset` chain and inspect each block's
|
|
269
|
+
* `tableHash`. It is a read-only operation and does not alter the container.
|
|
270
|
+
*/
|
|
271
|
+
readBlockAt(offset: number): BlockView {
|
|
272
|
+
const [header, entries] = this.readBlock(offset);
|
|
273
|
+
return { offset, header, entries };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Read a partition's used data. */
|
|
277
|
+
readPartitionData(entry: PartitionEntry): Uint8Array {
|
|
278
|
+
const used = Number(entry.usedBytes);
|
|
279
|
+
if (used === 0) {
|
|
280
|
+
return new Uint8Array(0);
|
|
281
|
+
}
|
|
282
|
+
return this.storage.readAt(Number(entry.startOffset), used);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private locate(uid: Uint8Array): [number, number, PartitionEntry] {
|
|
286
|
+
let off = Number(this.fileHeader.partitionTableOffset);
|
|
287
|
+
while (off !== 0) {
|
|
288
|
+
const [h, entries] = this.readBlock(off);
|
|
289
|
+
for (let i = 0; i < entries.length; i++) {
|
|
290
|
+
if (bytesEqual(entries[i]!.uid, uid)) {
|
|
291
|
+
return [off, i, entries[i]!];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
off = Number(h.nextTableOffset);
|
|
295
|
+
}
|
|
296
|
+
throw PcfError.notFound();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private blockIndex(offset: number): number {
|
|
300
|
+
const i = this.blocks.findIndex((b) => b.offset === offset);
|
|
301
|
+
if (i < 0) {
|
|
302
|
+
throw new Error("block offset must be tracked");
|
|
303
|
+
}
|
|
304
|
+
return i;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- writing -----------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Add a new partition. The data is appended at the end-of-data cursor and
|
|
311
|
+
* reserves `extraReserve` spare bytes for later in-place growth.
|
|
312
|
+
*/
|
|
313
|
+
addPartition(
|
|
314
|
+
partitionType: number,
|
|
315
|
+
uid: Uint8Array,
|
|
316
|
+
label: string,
|
|
317
|
+
data: Uint8Array,
|
|
318
|
+
extraReserve: number | bigint = 0,
|
|
319
|
+
dataHashAlgo: HashAlgo = HashAlgo.Sha256,
|
|
320
|
+
): void {
|
|
321
|
+
if ((partitionType >>> 0) === TYPE_RESERVED) {
|
|
322
|
+
throw PcfError.reservedType();
|
|
323
|
+
}
|
|
324
|
+
if (bytesEqual(uid, NIL_UID)) {
|
|
325
|
+
throw PcfError.nilUid();
|
|
326
|
+
}
|
|
327
|
+
if (this.entries().some((e) => bytesEqual(e.uid, uid))) {
|
|
328
|
+
throw PcfError.duplicateUid();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const labelBytes = encodeLabel(label);
|
|
332
|
+
const used = data.length;
|
|
333
|
+
const max = used + Number(extraReserve);
|
|
334
|
+
const start = this.dataEof;
|
|
335
|
+
if (used > 0) {
|
|
336
|
+
this.storage.writeAt(start, data);
|
|
337
|
+
}
|
|
338
|
+
this.dataEof += max;
|
|
339
|
+
const dataHash = computeHashField(dataHashAlgo, data);
|
|
340
|
+
|
|
341
|
+
const entry: PartitionEntry = {
|
|
342
|
+
partitionType: partitionType >>> 0,
|
|
343
|
+
uid: uid.slice(),
|
|
344
|
+
label: labelBytes,
|
|
345
|
+
startOffset: BigInt(start),
|
|
346
|
+
maxLength: BigInt(max),
|
|
347
|
+
usedBytes: BigInt(used),
|
|
348
|
+
dataHashAlgo,
|
|
349
|
+
dataHash,
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
// Find an existing block with reserved room.
|
|
353
|
+
const target = this.blocks.findIndex(
|
|
354
|
+
(b) => b.count < b.capacity && b.count < MAX_ENTRIES_PER_BLOCK,
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (target >= 0) {
|
|
358
|
+
const boff = this.blocks[target]!.offset;
|
|
359
|
+
const [, entries] = this.readBlock(boff);
|
|
360
|
+
entries.push(entry);
|
|
361
|
+
const algo = this.blocks[target]!.algo;
|
|
362
|
+
const next = this.blocks[target]!.next;
|
|
363
|
+
this.writeBlock(boff, next, algo, entries);
|
|
364
|
+
this.blocks[target]!.count += 1;
|
|
365
|
+
} else {
|
|
366
|
+
// Allocate a new overflow block at the end-of-data cursor.
|
|
367
|
+
const newOff = this.dataEof;
|
|
368
|
+
const cap = Math.min(
|
|
369
|
+
Math.max(this.defaultCapacity, 1),
|
|
370
|
+
MAX_ENTRIES_PER_BLOCK,
|
|
371
|
+
);
|
|
372
|
+
this.dataEof = newOff + TABLE_HEADER_SIZE + cap * ENTRY_SIZE;
|
|
373
|
+
const algo = this.tableHashAlgo;
|
|
374
|
+
this.writeBlock(newOff, 0, algo, [entry]);
|
|
375
|
+
|
|
376
|
+
// Re-link the previous tail block to point at the new block.
|
|
377
|
+
const tail = this.blocks[this.blocks.length - 1]!;
|
|
378
|
+
const [, tentries] = this.readBlock(tail.offset);
|
|
379
|
+
this.writeBlock(tail.offset, newOff, tail.algo, tentries);
|
|
380
|
+
tail.next = newOff;
|
|
381
|
+
this.blocks.push({
|
|
382
|
+
offset: newOff,
|
|
383
|
+
capacity: cap,
|
|
384
|
+
count: 1,
|
|
385
|
+
algo,
|
|
386
|
+
next: 0,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Replace a partition's data in place (spec section 8.5, hash cascade).
|
|
393
|
+
* Fails if `newData` exceeds the partition's reservation.
|
|
394
|
+
*/
|
|
395
|
+
updatePartitionData(uid: Uint8Array, newData: Uint8Array): void {
|
|
396
|
+
const [boff, slot, entry] = this.locate(uid);
|
|
397
|
+
if (BigInt(newData.length) > entry.maxLength) {
|
|
398
|
+
throw PcfError.dataTooLarge();
|
|
399
|
+
}
|
|
400
|
+
if (newData.length > 0) {
|
|
401
|
+
this.storage.writeAt(Number(entry.startOffset), newData);
|
|
402
|
+
}
|
|
403
|
+
entry.usedBytes = BigInt(newData.length);
|
|
404
|
+
entry.dataHash = computeHashField(entry.dataHashAlgo, newData);
|
|
405
|
+
|
|
406
|
+
const [, entries] = this.readBlock(boff);
|
|
407
|
+
entries[slot] = entry;
|
|
408
|
+
const bi = this.blockIndex(boff);
|
|
409
|
+
const next = this.blocks[bi]!.next;
|
|
410
|
+
const algo = this.blocks[bi]!.algo;
|
|
411
|
+
this.writeBlock(boff, next, algo, entries);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Remove a partition. Entries after it in the same block shift down; the
|
|
416
|
+
* freed data region becomes dead space until {@link Container.compactedImage}
|
|
417
|
+
* reclaims it (spec section 11.4).
|
|
418
|
+
*/
|
|
419
|
+
removePartition(uid: Uint8Array): void {
|
|
420
|
+
const [boff, slot] = this.locate(uid);
|
|
421
|
+
const [, entries] = this.readBlock(boff);
|
|
422
|
+
entries.splice(slot, 1);
|
|
423
|
+
const bi = this.blockIndex(boff);
|
|
424
|
+
const next = this.blocks[bi]!.next;
|
|
425
|
+
const algo = this.blocks[bi]!.algo;
|
|
426
|
+
this.writeBlock(boff, next, algo, entries);
|
|
427
|
+
this.blocks[bi]!.count -= 1;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---- integrity ---------------------------------------------------------
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Verify every table block and every partition's data against its stored
|
|
434
|
+
* hash, and run the per-entry conformance checks (spec section 12).
|
|
435
|
+
*/
|
|
436
|
+
verify(): void {
|
|
437
|
+
let off = Number(this.fileHeader.partitionTableOffset);
|
|
438
|
+
while (off !== 0) {
|
|
439
|
+
const [h, entries] = this.readBlock(off);
|
|
440
|
+
if (verifies(h.tableHashAlgo)) {
|
|
441
|
+
const computed = computeTableHash(
|
|
442
|
+
h.tableHashAlgo,
|
|
443
|
+
h.nextTableOffset,
|
|
444
|
+
entries,
|
|
445
|
+
);
|
|
446
|
+
const n = digestLen(h.tableHashAlgo);
|
|
447
|
+
for (let i = 0; i < n; i++) {
|
|
448
|
+
if (computed[i] !== h.tableHash[i]) {
|
|
449
|
+
throw PcfError.tableHashMismatch();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
for (const e of entries) {
|
|
454
|
+
validateEntry(e);
|
|
455
|
+
const data = this.readPartitionData(e);
|
|
456
|
+
if (!verifyDataHash(e, data)) {
|
|
457
|
+
throw PcfError.dataHashMismatch();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
off = Number(h.nextTableOffset);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ---- compaction --------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Build a freshly compacted image: all dead space removed, every `maxLength`
|
|
468
|
+
* trimmed to `usedBytes`, partitions placed contiguously after a tightly
|
|
469
|
+
* packed table (spec section 11.5). The current handle is left unchanged;
|
|
470
|
+
* write the bytes to a new store and re-open it.
|
|
471
|
+
*/
|
|
472
|
+
compactedImage(): Uint8Array {
|
|
473
|
+
// Gather live entries and their data, in chain order.
|
|
474
|
+
const live: Array<{ entry: PartitionEntry; data: Uint8Array }> = [];
|
|
475
|
+
let off = Number(this.fileHeader.partitionTableOffset);
|
|
476
|
+
while (off !== 0) {
|
|
477
|
+
const [h, entries] = this.readBlock(off);
|
|
478
|
+
for (const e of entries) {
|
|
479
|
+
live.push({ entry: e, data: this.readPartitionData(e) });
|
|
480
|
+
}
|
|
481
|
+
off = Number(h.nextTableOffset);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const algo = this.tableHashAlgo;
|
|
485
|
+
const n = live.length;
|
|
486
|
+
const numBlocks = n === 0 ? 1 : Math.ceil(n / MAX_ENTRIES_PER_BLOCK);
|
|
487
|
+
|
|
488
|
+
const counts: number[] = [];
|
|
489
|
+
let rem = n;
|
|
490
|
+
for (let i = 0; i < numBlocks; i++) {
|
|
491
|
+
const c = Math.min(rem, MAX_ENTRIES_PER_BLOCK);
|
|
492
|
+
counts.push(c);
|
|
493
|
+
rem -= c;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const blockOffsets: number[] = [];
|
|
497
|
+
let o = HEADER_SIZE;
|
|
498
|
+
for (const c of counts) {
|
|
499
|
+
blockOffsets.push(o);
|
|
500
|
+
o += TABLE_HEADER_SIZE + c * ENTRY_SIZE;
|
|
501
|
+
}
|
|
502
|
+
const dataStart = o;
|
|
503
|
+
|
|
504
|
+
// Assign contiguous data offsets; trim reservations to used size.
|
|
505
|
+
let d = dataStart;
|
|
506
|
+
for (const item of live) {
|
|
507
|
+
const len = item.data.length;
|
|
508
|
+
item.entry = {
|
|
509
|
+
...item.entry,
|
|
510
|
+
startOffset: BigInt(d),
|
|
511
|
+
usedBytes: BigInt(len),
|
|
512
|
+
maxLength: BigInt(len),
|
|
513
|
+
// dataHash is unchanged because the content is unchanged.
|
|
514
|
+
};
|
|
515
|
+
d += len;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Serialise.
|
|
519
|
+
const image = new Uint8Array(d);
|
|
520
|
+
const header: FileHeader = {
|
|
521
|
+
versionMajor: VERSION_MAJOR,
|
|
522
|
+
versionMinor: VERSION_MINOR,
|
|
523
|
+
partitionTableOffset: BigInt(HEADER_SIZE),
|
|
524
|
+
};
|
|
525
|
+
image.set(headerToBytes(header), 0);
|
|
526
|
+
|
|
527
|
+
let idx = 0;
|
|
528
|
+
for (let bi = 0; bi < counts.length; bi++) {
|
|
529
|
+
const c = counts[bi]!;
|
|
530
|
+
const next = bi + 1 < numBlocks ? blockOffsets[bi + 1]! : 0;
|
|
531
|
+
const slice = live.slice(idx, idx + c).map((x) => x.entry);
|
|
532
|
+
const th = computeTableHash(algo, BigInt(next), slice);
|
|
533
|
+
const bh: TableBlockHeader = {
|
|
534
|
+
partitionCount: c,
|
|
535
|
+
nextTableOffset: BigInt(next),
|
|
536
|
+
tableHashAlgo: algo,
|
|
537
|
+
tableHash: th,
|
|
538
|
+
};
|
|
539
|
+
let p = blockOffsets[bi]!;
|
|
540
|
+
image.set(tableHeaderToBytes(bh), p);
|
|
541
|
+
p += TABLE_HEADER_SIZE;
|
|
542
|
+
for (const e of slice) {
|
|
543
|
+
image.set(entryToBytes(e), p);
|
|
544
|
+
p += ENTRY_SIZE;
|
|
545
|
+
}
|
|
546
|
+
idx += c;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
let dp = dataStart;
|
|
550
|
+
for (const item of live) {
|
|
551
|
+
image.set(item.data, dp);
|
|
552
|
+
dp += item.data.length;
|
|
553
|
+
}
|
|
554
|
+
return image;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Write a compacted copy of the container to `out`. */
|
|
558
|
+
compactInto(out: Storage): void {
|
|
559
|
+
out.writeAt(0, this.compactedImage());
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function verifyDataHash(e: PartitionEntry, data: Uint8Array): boolean {
|
|
564
|
+
if (!verifies(e.dataHashAlgo)) {
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
const computed = computeHashField(e.dataHashAlgo, data);
|
|
568
|
+
const n = digestLen(e.dataHashAlgo);
|
|
569
|
+
for (let i = 0; i < n; i++) {
|
|
570
|
+
if (computed[i] !== e.dataHash[i]) {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return true;
|
|
575
|
+
}
|
package/src/crc.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-TypeScript CRC implementations used by the PCF hash registry
|
|
3
|
+
* (spec section 8.1). `@noble/hashes` does not provide CRCs, so the three
|
|
4
|
+
* registered variants are implemented here from their normative parameters.
|
|
5
|
+
*
|
|
6
|
+
* All three are *reflected* CRCs (refin = refout = true), so the table-driven
|
|
7
|
+
* form processes the low byte and shifts right, using the reflected polynomial.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const MASK32 = 0xffff_ffff;
|
|
11
|
+
const MASK64 = (1n << 64n) - 1n;
|
|
12
|
+
|
|
13
|
+
function makeTable32(reflectedPoly: number): Uint32Array {
|
|
14
|
+
const table = new Uint32Array(256);
|
|
15
|
+
for (let n = 0; n < 256; n++) {
|
|
16
|
+
let c = n;
|
|
17
|
+
for (let k = 0; k < 8; k++) {
|
|
18
|
+
c = c & 1 ? reflectedPoly ^ (c >>> 1) : c >>> 1;
|
|
19
|
+
}
|
|
20
|
+
table[n] = c >>> 0;
|
|
21
|
+
}
|
|
22
|
+
return table;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeTable64(reflectedPoly: bigint): BigUint64Array {
|
|
26
|
+
const table = new BigUint64Array(256);
|
|
27
|
+
for (let n = 0n; n < 256n; n++) {
|
|
28
|
+
let c = n;
|
|
29
|
+
for (let k = 0; k < 8; k++) {
|
|
30
|
+
c = c & 1n ? reflectedPoly ^ (c >> 1n) : c >> 1n;
|
|
31
|
+
}
|
|
32
|
+
table[Number(n)] = c & MASK64;
|
|
33
|
+
}
|
|
34
|
+
return table;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Reflected polynomials for each registered CRC (spec section 8.1).
|
|
38
|
+
const CRC32_TABLE = makeTable32(0xedb88320); // CRC-32/ISO-HDLC
|
|
39
|
+
const CRC32C_TABLE = makeTable32(0x82f63b78); // CRC-32C (Castagnoli)
|
|
40
|
+
// CRC-64/XZ: normal poly 0x42F0E1EBA9EA3693, reflected 0xC96C5795D7870F42.
|
|
41
|
+
const CRC64_TABLE = makeTable64(0xc96c5795d7870f42n);
|
|
42
|
+
|
|
43
|
+
function crc32With(table: Uint32Array, data: Uint8Array): number {
|
|
44
|
+
let crc = MASK32;
|
|
45
|
+
for (let i = 0; i < data.length; i++) {
|
|
46
|
+
crc = (crc >>> 8) ^ table[(crc ^ data[i]!) & 0xff]!;
|
|
47
|
+
}
|
|
48
|
+
return (crc ^ MASK32) >>> 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** CRC-32/ISO-HDLC (the CRC used by zlib, gzip, and PNG). */
|
|
52
|
+
export function crc32(data: Uint8Array): number {
|
|
53
|
+
return crc32With(CRC32_TABLE, data);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** CRC-32C (Castagnoli). */
|
|
57
|
+
export function crc32c(data: Uint8Array): number {
|
|
58
|
+
return crc32With(CRC32C_TABLE, data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** CRC-64/XZ. */
|
|
62
|
+
export function crc64(data: Uint8Array): bigint {
|
|
63
|
+
let crc = MASK64;
|
|
64
|
+
for (let i = 0; i < data.length; i++) {
|
|
65
|
+
const idx = Number((crc ^ BigInt(data[i]!)) & 0xffn);
|
|
66
|
+
crc = (crc >> 8n) ^ CRC64_TABLE[idx]!;
|
|
67
|
+
}
|
|
68
|
+
return (crc ^ MASK64) & MASK64;
|
|
69
|
+
}
|