@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.
Files changed (57) hide show
  1. package/README.md +116 -0
  2. package/dist/consts.d.ts +42 -0
  3. package/dist/consts.d.ts.map +1 -0
  4. package/dist/consts.js +44 -0
  5. package/dist/consts.js.map +1 -0
  6. package/dist/container.d.ts +124 -0
  7. package/dist/container.d.ts.map +1 -0
  8. package/dist/container.js +441 -0
  9. package/dist/container.js.map +1 -0
  10. package/dist/crc.d.ts +15 -0
  11. package/dist/crc.d.ts.map +1 -0
  12. package/dist/crc.js +62 -0
  13. package/dist/crc.js.map +1 -0
  14. package/dist/entry.d.ts +44 -0
  15. package/dist/entry.d.ts.map +1 -0
  16. package/dist/entry.js +118 -0
  17. package/dist/entry.js.map +1 -0
  18. package/dist/errors.d.ts +56 -0
  19. package/dist/errors.d.ts.map +1 -0
  20. package/dist/errors.js +88 -0
  21. package/dist/errors.js.map +1 -0
  22. package/dist/hash.d.ts +59 -0
  23. package/dist/hash.d.ts.map +1 -0
  24. package/dist/hash.js +151 -0
  25. package/dist/hash.js.map +1 -0
  26. package/dist/header.d.ts +20 -0
  27. package/dist/header.d.ts.map +1 -0
  28. package/dist/header.js +38 -0
  29. package/dist/header.js.map +1 -0
  30. package/dist/index.d.ts +42 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +42 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/node-storage.d.ts +24 -0
  35. package/dist/node-storage.d.ts.map +1 -0
  36. package/dist/node-storage.js +52 -0
  37. package/dist/node-storage.js.map +1 -0
  38. package/dist/storage.d.ts +35 -0
  39. package/dist/storage.d.ts.map +1 -0
  40. package/dist/storage.js +66 -0
  41. package/dist/storage.js.map +1 -0
  42. package/dist/table.d.ts +28 -0
  43. package/dist/table.d.ts.map +1 -0
  44. package/dist/table.js +48 -0
  45. package/dist/table.js.map +1 -0
  46. package/package.json +60 -0
  47. package/src/consts.ts +50 -0
  48. package/src/container.ts +575 -0
  49. package/src/crc.ts +69 -0
  50. package/src/entry.ts +152 -0
  51. package/src/errors.ts +124 -0
  52. package/src/hash.ts +165 -0
  53. package/src/header.ts +50 -0
  54. package/src/index.ts +68 -0
  55. package/src/node-storage.ts +75 -0
  56. package/src/storage.ts +85 -0
  57. package/src/table.ts +67 -0
package/src/entry.ts ADDED
@@ -0,0 +1,152 @@
1
+ /**
2
+ * The fixed 141-byte partition entry (spec section 5.2).
3
+ */
4
+
5
+ import {
6
+ ENTRY_SIZE,
7
+ HASH_FIELD_SIZE,
8
+ LABEL_SIZE,
9
+ NIL_UID,
10
+ TYPE_RESERVED,
11
+ } from "./consts.js";
12
+ import { PcfError } from "./errors.js";
13
+ import { HashAlgo, hashAlgoFromId, hashAlgoId } from "./hash.js";
14
+
15
+ /** One partition's metadata. */
16
+ export interface PartitionEntry {
17
+ /** Application-defined type (`0` and `0xFFFFFFFF` are reserved). */
18
+ partitionType: number;
19
+ /** 16-byte unique identifier. */
20
+ uid: Uint8Array;
21
+ /** 32-byte ASCII label, NUL-padded. */
22
+ label: Uint8Array;
23
+ /** Absolute offset of the partition's data region. */
24
+ startOffset: bigint;
25
+ /** Bytes reserved for the partition. */
26
+ maxLength: bigint;
27
+ /** Bytes currently used (a contiguous prefix of the reservation). */
28
+ usedBytes: bigint;
29
+ /** Algorithm used for `dataHash`. */
30
+ dataHashAlgo: HashAlgo;
31
+ /** 64-byte data hash field. */
32
+ dataHash: Uint8Array;
33
+ }
34
+
35
+ /** Serialise an entry to its on-disk 141-byte layout. */
36
+ export function entryToBytes(e: PartitionEntry): Uint8Array {
37
+ const b = new Uint8Array(ENTRY_SIZE);
38
+ const view = new DataView(b.buffer);
39
+ view.setUint32(0, e.partitionType >>> 0, true);
40
+ b.set(e.uid, 4);
41
+ b.set(e.label, 20);
42
+ view.setBigUint64(52, e.startOffset, true);
43
+ view.setBigUint64(60, e.maxLength, true);
44
+ view.setBigUint64(68, e.usedBytes, true);
45
+ b[76] = hashAlgoId(e.dataHashAlgo);
46
+ b.set(e.dataHash, 77);
47
+ return b;
48
+ }
49
+
50
+ /** Parse an entry from its on-disk 141-byte layout. */
51
+ export function entryFromBytes(b: Uint8Array): PartitionEntry {
52
+ const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
53
+ const partitionType = view.getUint32(0, true);
54
+ const uid = b.slice(4, 20);
55
+ const label = b.slice(20, 52);
56
+ const startOffset = view.getBigUint64(52, true);
57
+ const maxLength = view.getBigUint64(60, true);
58
+ const usedBytes = view.getBigUint64(68, true);
59
+ const dataHashAlgo = hashAlgoFromId(b[76]!);
60
+ const dataHash = b.slice(77, 77 + HASH_FIELD_SIZE);
61
+ return {
62
+ partitionType,
63
+ uid,
64
+ label,
65
+ startOffset,
66
+ maxLength,
67
+ usedBytes,
68
+ dataHashAlgo,
69
+ dataHash,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Apply the conformance checks a reader must run on a live entry
75
+ * (spec C5, C6, C7).
76
+ */
77
+ export function validateEntry(e: PartitionEntry): void {
78
+ if (e.partitionType === TYPE_RESERVED) {
79
+ throw PcfError.reservedType();
80
+ }
81
+ if (bytesEqual(e.uid, NIL_UID)) {
82
+ throw PcfError.nilUid();
83
+ }
84
+ if (e.usedBytes > e.maxLength) {
85
+ throw PcfError.usedExceedsMax();
86
+ }
87
+ decodeLabel(e.label); // validates label bytes
88
+ }
89
+
90
+ /** Free bytes remaining in a partition (`max_length - used_bytes`). */
91
+ export function freeBytes(e: PartitionEntry): bigint {
92
+ return e.usedBytes > e.maxLength ? 0n : e.maxLength - e.usedBytes;
93
+ }
94
+
95
+ /** Decode an entry's label as a string (reads up to the first NUL). */
96
+ export function entryLabelString(e: PartitionEntry): string {
97
+ return decodeLabel(e.label);
98
+ }
99
+
100
+ /** Build a 32-byte label field from a string (spec section 10). */
101
+ export function encodeLabel(s: string): Uint8Array {
102
+ // Labels are ASCII (0x01..0x7F); reject anything that is not.
103
+ const out = new Uint8Array(LABEL_SIZE);
104
+ let n = 0;
105
+ for (const ch of s) {
106
+ const code = ch.codePointAt(0)!;
107
+ if (code === 0 || code >= 0x80) {
108
+ throw PcfError.invalidLabel();
109
+ }
110
+ if (n >= LABEL_SIZE) {
111
+ throw PcfError.invalidLabel();
112
+ }
113
+ out[n] = code;
114
+ n++;
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Decode a 32-byte label field: read until the first NUL or 32 bytes,
121
+ * rejecting any byte >= 0x80 (spec section 10).
122
+ */
123
+ export function decodeLabel(label: Uint8Array): string {
124
+ let end = LABEL_SIZE;
125
+ for (let i = 0; i < LABEL_SIZE; i++) {
126
+ const c = label[i]!;
127
+ if (c === 0) {
128
+ end = i;
129
+ break;
130
+ }
131
+ if (c >= 0x80) {
132
+ throw PcfError.invalidLabel();
133
+ }
134
+ }
135
+ let s = "";
136
+ for (let i = 0; i < end; i++) {
137
+ s += String.fromCharCode(label[i]!);
138
+ }
139
+ return s;
140
+ }
141
+
142
+ function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
143
+ if (a.length !== b.length) {
144
+ return false;
145
+ }
146
+ for (let i = 0; i < a.length; i++) {
147
+ if (a[i] !== b[i]) {
148
+ return false;
149
+ }
150
+ }
151
+ return true;
152
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Error type shared across the library (mirrors the reference `Error` enum).
3
+ */
4
+
5
+ /** Discriminant identifying which kind of {@link PcfError} occurred. */
6
+ export enum PcfErrorKind {
7
+ /** Underlying I/O failure. */
8
+ Io = "Io",
9
+ /** The file does not begin with the PCF magic. */
10
+ BadMagic = "BadMagic",
11
+ /** The file's major version is not implemented by this library. */
12
+ UnsupportedMajor = "UnsupportedMajor",
13
+ /** A hash-algorithm identifier is not in the registry. */
14
+ UnknownHashAlgo = "UnknownHashAlgo",
15
+ /** A live entry used the reserved type `0x00000000`. */
16
+ ReservedType = "ReservedType",
17
+ /** A live entry used the NIL UID. */
18
+ NilUid = "NilUid",
19
+ /** `used_bytes` exceeded `max_length` for an entry. */
20
+ UsedExceedsMax = "UsedExceedsMax",
21
+ /** A label byte was outside the permitted range (>= 0x80), or too long. */
22
+ InvalidLabel = "InvalidLabel",
23
+ /** A table block failed hash verification. */
24
+ TableHashMismatch = "TableHashMismatch",
25
+ /** A partition's data failed hash verification. */
26
+ DataHashMismatch = "DataHashMismatch",
27
+ /** An in-place update supplied more data than the partition's reservation. */
28
+ DataTooLarge = "DataTooLarge",
29
+ /** No partition with the requested UID exists. */
30
+ NotFound = "NotFound",
31
+ /** An attempt was made to add a partition whose UID already exists. */
32
+ DuplicateUid = "DuplicateUid",
33
+ }
34
+
35
+ /** All ways a PCF operation can fail. */
36
+ export class PcfError extends Error {
37
+ /** The kind of failure. */
38
+ readonly kind: PcfErrorKind;
39
+ /**
40
+ * Optional numeric detail (e.g. the unsupported major version or the unknown
41
+ * hash-algorithm id), preserved for the variants that carry one.
42
+ */
43
+ readonly value?: number;
44
+
45
+ constructor(kind: PcfErrorKind, message: string, value?: number) {
46
+ super(message);
47
+ this.name = "PcfError";
48
+ this.kind = kind;
49
+ this.value = value;
50
+ // Restore the prototype chain when targeting older runtimes.
51
+ Object.setPrototypeOf(this, PcfError.prototype);
52
+ }
53
+
54
+ static badMagic(): PcfError {
55
+ return new PcfError(PcfErrorKind.BadMagic, "bad magic: not a PCF file");
56
+ }
57
+
58
+ static unsupportedMajor(v: number): PcfError {
59
+ return new PcfError(
60
+ PcfErrorKind.UnsupportedMajor,
61
+ `unsupported major version ${v}`,
62
+ v,
63
+ );
64
+ }
65
+
66
+ static unknownHashAlgo(id: number): PcfError {
67
+ return new PcfError(
68
+ PcfErrorKind.UnknownHashAlgo,
69
+ `unknown hash algorithm id ${id}`,
70
+ id,
71
+ );
72
+ }
73
+
74
+ static reservedType(): PcfError {
75
+ return new PcfError(
76
+ PcfErrorKind.ReservedType,
77
+ "reserved partition type used for a live entry",
78
+ );
79
+ }
80
+
81
+ static nilUid(): PcfError {
82
+ return new PcfError(PcfErrorKind.NilUid, "NIL UID used for a live entry");
83
+ }
84
+
85
+ static usedExceedsMax(): PcfError {
86
+ return new PcfError(
87
+ PcfErrorKind.UsedExceedsMax,
88
+ "used_bytes exceeds max_length",
89
+ );
90
+ }
91
+
92
+ static invalidLabel(): PcfError {
93
+ return new PcfError(PcfErrorKind.InvalidLabel, "invalid label");
94
+ }
95
+
96
+ static tableHashMismatch(): PcfError {
97
+ return new PcfError(
98
+ PcfErrorKind.TableHashMismatch,
99
+ "table block hash mismatch",
100
+ );
101
+ }
102
+
103
+ static dataHashMismatch(): PcfError {
104
+ return new PcfError(
105
+ PcfErrorKind.DataHashMismatch,
106
+ "partition data hash mismatch",
107
+ );
108
+ }
109
+
110
+ static dataTooLarge(): PcfError {
111
+ return new PcfError(
112
+ PcfErrorKind.DataTooLarge,
113
+ "data larger than partition reservation",
114
+ );
115
+ }
116
+
117
+ static notFound(): PcfError {
118
+ return new PcfError(PcfErrorKind.NotFound, "partition not found");
119
+ }
120
+
121
+ static duplicateUid(): PcfError {
122
+ return new PcfError(PcfErrorKind.DuplicateUid, "duplicate UID");
123
+ }
124
+ }
package/src/hash.ts ADDED
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Hash-algorithm registry (spec section 8).
3
+ *
4
+ * Each hash field in the format is a fixed 64-byte field accompanied by a
5
+ * `u8` algorithm identifier. Digests are stored left-aligned and zero-padded;
6
+ * CRC values are stored as little-endian integers, left-aligned and
7
+ * zero-padded (spec section 8.2).
8
+ *
9
+ * Digests are provided by the audited `@noble/hashes` package; the CRC variants
10
+ * are implemented locally in {@link "./crc"}.
11
+ */
12
+
13
+ import { sha256, sha512 } from "@noble/hashes/sha2";
14
+ import { md5, sha1 } from "@noble/hashes/legacy";
15
+ import { blake3 } from "@noble/hashes/blake3";
16
+
17
+ import { HASH_FIELD_SIZE } from "./consts.js";
18
+ import { crc32, crc32c, crc64 } from "./crc.js";
19
+ import { PcfError } from "./errors.js";
20
+
21
+ /**
22
+ * A hash algorithm from the PCF registry (spec section 8.1).
23
+ *
24
+ * The numeric value of each member is exactly its on-disk registry id, so
25
+ * `algo as number` yields the id byte.
26
+ */
27
+ export enum HashAlgo {
28
+ /** `0` — no verification. */
29
+ None = 0,
30
+ /** `1` — CRC-32/ISO-HDLC. */
31
+ Crc32 = 1,
32
+ /** `2` — CRC-32C (Castagnoli). */
33
+ Crc32c = 2,
34
+ /** `3` — CRC-64/XZ. */
35
+ Crc64 = 3,
36
+ /** `4` — MD5 (checksum use only). */
37
+ Md5 = 4,
38
+ /** `5` — SHA-1 (checksum use only). */
39
+ Sha1 = 5,
40
+ /** `16` — SHA-256 (default). */
41
+ Sha256 = 16,
42
+ /** `17` — SHA-512. */
43
+ Sha512 = 17,
44
+ /** `18` — BLAKE3. */
45
+ Blake3 = 18,
46
+ }
47
+
48
+ const KNOWN_IDS: ReadonlySet<number> = new Set([0, 1, 2, 3, 4, 5, 16, 17, 18]);
49
+
50
+ /** Map a registry id byte to an algorithm (spec section 8.1). */
51
+ export function hashAlgoFromId(id: number): HashAlgo {
52
+ if (!KNOWN_IDS.has(id)) {
53
+ throw PcfError.unknownHashAlgo(id);
54
+ }
55
+ return id as HashAlgo;
56
+ }
57
+
58
+ /** The registry id byte for an algorithm. */
59
+ export function hashAlgoId(algo: HashAlgo): number {
60
+ return algo;
61
+ }
62
+
63
+ /** Number of significant bytes an algorithm writes into a hash field. */
64
+ export function digestLen(algo: HashAlgo): number {
65
+ switch (algo) {
66
+ case HashAlgo.None:
67
+ return 0;
68
+ case HashAlgo.Crc32:
69
+ case HashAlgo.Crc32c:
70
+ return 4;
71
+ case HashAlgo.Crc64:
72
+ return 8;
73
+ case HashAlgo.Md5:
74
+ return 16;
75
+ case HashAlgo.Sha1:
76
+ return 20;
77
+ case HashAlgo.Sha256:
78
+ case HashAlgo.Blake3:
79
+ return 32;
80
+ case HashAlgo.Sha512:
81
+ return 64;
82
+ }
83
+ }
84
+
85
+ /** Whether an algorithm performs any verification (everything but `None`). */
86
+ export function verifies(algo: HashAlgo): boolean {
87
+ return algo !== HashAlgo.None;
88
+ }
89
+
90
+ function writeU32Le(field: Uint8Array, value: number): void {
91
+ field[0] = value & 0xff;
92
+ field[1] = (value >>> 8) & 0xff;
93
+ field[2] = (value >>> 16) & 0xff;
94
+ field[3] = (value >>> 24) & 0xff;
95
+ }
96
+
97
+ function writeU64Le(field: Uint8Array, value: bigint): void {
98
+ let v = value;
99
+ for (let i = 0; i < 8; i++) {
100
+ field[i] = Number(v & 0xffn);
101
+ v >>= 8n;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Compute the full 64-byte hash field for `data` per spec section 8.2.
107
+ *
108
+ * Digest-producing algorithms write their digest starting at byte 0; CRCs write
109
+ * a little-endian integer of their width; all remaining bytes are 0x00.
110
+ * Algorithm `None` yields an all-zero field.
111
+ */
112
+ export function computeHashField(algo: HashAlgo, data: Uint8Array): Uint8Array {
113
+ const field = new Uint8Array(HASH_FIELD_SIZE);
114
+ switch (algo) {
115
+ case HashAlgo.None:
116
+ break;
117
+ case HashAlgo.Crc32:
118
+ writeU32Le(field, crc32(data));
119
+ break;
120
+ case HashAlgo.Crc32c:
121
+ writeU32Le(field, crc32c(data));
122
+ break;
123
+ case HashAlgo.Crc64:
124
+ writeU64Le(field, crc64(data));
125
+ break;
126
+ case HashAlgo.Md5:
127
+ field.set(md5(data), 0);
128
+ break;
129
+ case HashAlgo.Sha1:
130
+ field.set(sha1(data), 0);
131
+ break;
132
+ case HashAlgo.Sha256:
133
+ field.set(sha256(data), 0);
134
+ break;
135
+ case HashAlgo.Sha512:
136
+ field.set(sha512(data), 0);
137
+ break;
138
+ case HashAlgo.Blake3:
139
+ field.set(blake3(data), 0);
140
+ break;
141
+ }
142
+ return field;
143
+ }
144
+
145
+ /**
146
+ * Verify `data` against a stored 64-byte hash field. `None` always succeeds
147
+ * (no verification). Only the significant prefix is compared, per spec 8.2.
148
+ */
149
+ export function verifyHashField(
150
+ algo: HashAlgo,
151
+ data: Uint8Array,
152
+ stored: Uint8Array,
153
+ ): boolean {
154
+ if (!verifies(algo)) {
155
+ return true;
156
+ }
157
+ const computed = computeHashField(algo, data);
158
+ const n = digestLen(algo);
159
+ for (let i = 0; i < n; i++) {
160
+ if (computed[i] !== stored[i]) {
161
+ return false;
162
+ }
163
+ }
164
+ return true;
165
+ }
package/src/header.ts ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The fixed 20-byte file header (spec section 4).
3
+ */
4
+
5
+ import { HEADER_SIZE, MAGIC, VERSION_MAJOR } from "./consts.js";
6
+ import { PcfError } from "./errors.js";
7
+
8
+ /** Parsed file header. */
9
+ export interface FileHeader {
10
+ /** Major format version. */
11
+ versionMajor: number;
12
+ /** Minor format version. */
13
+ versionMinor: number;
14
+ /** Absolute offset of the first table block. */
15
+ partitionTableOffset: bigint;
16
+ }
17
+
18
+ /** Serialise a header to its on-disk 20-byte layout. */
19
+ export function headerToBytes(h: FileHeader): Uint8Array {
20
+ const b = new Uint8Array(HEADER_SIZE);
21
+ b.set(MAGIC, 0);
22
+ const view = new DataView(b.buffer);
23
+ view.setUint16(8, h.versionMajor, true);
24
+ view.setUint16(10, h.versionMinor, true);
25
+ view.setBigUint64(12, h.partitionTableOffset, true);
26
+ return b;
27
+ }
28
+
29
+ /**
30
+ * Parse a header from its on-disk 20-byte layout, validating magic and major
31
+ * version (spec conformance checks C1, C2).
32
+ */
33
+ export function headerFromBytes(b: Uint8Array): FileHeader {
34
+ if (b.length < HEADER_SIZE) {
35
+ throw PcfError.badMagic();
36
+ }
37
+ for (let i = 0; i < MAGIC.length; i++) {
38
+ if (b[i] !== MAGIC[i]) {
39
+ throw PcfError.badMagic();
40
+ }
41
+ }
42
+ const view = new DataView(b.buffer, b.byteOffset, b.byteLength);
43
+ const versionMajor = view.getUint16(8, true);
44
+ if (versionMajor !== VERSION_MAJOR) {
45
+ throw PcfError.unsupportedMajor(versionMajor);
46
+ }
47
+ const versionMinor = view.getUint16(10, true);
48
+ const partitionTableOffset = view.getBigUint64(12, true);
49
+ return { versionMajor, versionMinor, partitionTableOffset };
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * # `pcf` — Partitioned Container Format (TypeScript implementation)
3
+ *
4
+ * A language-agnostic binary container that stores multiple independent regions
5
+ * of bytes ("partitions") in a single file. This package mirrors the written
6
+ * specification (`PCF-spec-v1.0.txt`) and the Rust reference implementation
7
+ * field-for-field and favours auditability over performance.
8
+ *
9
+ * ## Layout at a glance
10
+ *
11
+ * ```text
12
+ * [ 20-byte header ] [ table block(s) ] [ partition data regions ]
13
+ * ```
14
+ *
15
+ * All integers are little-endian. Free space is derived as
16
+ * `maxLength - usedBytes`.
17
+ *
18
+ * ## Example
19
+ *
20
+ * ```ts
21
+ * import { Container, HashAlgo } from "pcf";
22
+ *
23
+ * const c = Container.create();
24
+ * const uid = new Uint8Array(16).fill(1);
25
+ * c.addPartition(0x10, uid, "notes", new TextEncoder().encode("hello world"), 64, HashAlgo.Sha256);
26
+ *
27
+ * c.verify();
28
+ * const entries = c.entries();
29
+ * console.log(new TextDecoder().decode(c.readPartitionData(entries[0]))); // "hello world"
30
+ * ```
31
+ */
32
+
33
+ export * from "./consts.js";
34
+ export { PcfError, PcfErrorKind } from "./errors.js";
35
+ export {
36
+ HashAlgo,
37
+ hashAlgoFromId,
38
+ hashAlgoId,
39
+ digestLen,
40
+ verifies,
41
+ computeHashField,
42
+ verifyHashField,
43
+ } from "./hash.js";
44
+ export { crc32, crc32c, crc64 } from "./crc.js";
45
+ export {
46
+ type FileHeader,
47
+ headerToBytes,
48
+ headerFromBytes,
49
+ } from "./header.js";
50
+ export {
51
+ type PartitionEntry,
52
+ entryToBytes,
53
+ entryFromBytes,
54
+ validateEntry,
55
+ freeBytes,
56
+ entryLabelString,
57
+ encodeLabel,
58
+ decodeLabel,
59
+ } from "./entry.js";
60
+ export {
61
+ type TableBlockHeader,
62
+ tableHeaderToBytes,
63
+ tableHeaderFromBytes,
64
+ computeTableHash,
65
+ } from "./table.js";
66
+ export { type Storage, MemoryStorage } from "./storage.js";
67
+ export { NodeFileStorage } from "./node-storage.js";
68
+ export { Container, type BlockView } from "./container.js";
@@ -0,0 +1,75 @@
1
+ /**
2
+ * A {@link Storage} implementation backed by a Node file descriptor, the
3
+ * analogue of using `std::fs::File` with the reference implementation.
4
+ *
5
+ * This module imports `node:fs` and is therefore Node-only; the rest of the
6
+ * library is runtime-agnostic.
7
+ */
8
+
9
+ import {
10
+ closeSync,
11
+ fstatSync,
12
+ openSync,
13
+ readSync,
14
+ writeSync,
15
+ } from "node:fs";
16
+
17
+ import type { Storage } from "./storage.js";
18
+
19
+ /** File-descriptor backed random-access storage. */
20
+ export class NodeFileStorage implements Storage {
21
+ private fd: number;
22
+
23
+ private constructor(fd: number) {
24
+ this.fd = fd;
25
+ }
26
+
27
+ /**
28
+ * Open `path` for reading and writing, creating it if necessary (mode
29
+ * `"r+"`-like with creation). Use `truncate` to start from an empty file.
30
+ */
31
+ static open(path: string, truncate = false): NodeFileStorage {
32
+ // "w+" truncates+creates for read/write; "a+"/"r+" otherwise.
33
+ const flags = truncate ? "w+" : "a+";
34
+ const fd = openSync(path, flags);
35
+ return new NodeFileStorage(fd);
36
+ }
37
+
38
+ readAt(offset: number, length: number): Uint8Array {
39
+ const buf = new Uint8Array(length);
40
+ let read = 0;
41
+ while (read < length) {
42
+ const n = readSync(this.fd, buf, read, length - read, offset + read);
43
+ if (n === 0) {
44
+ throw new Error(
45
+ `unexpected EOF: wanted ${length} bytes at offset ${offset}`,
46
+ );
47
+ }
48
+ read += n;
49
+ }
50
+ return buf;
51
+ }
52
+
53
+ writeAt(offset: number, data: Uint8Array): void {
54
+ let written = 0;
55
+ while (written < data.length) {
56
+ const n = writeSync(
57
+ this.fd,
58
+ data,
59
+ written,
60
+ data.length - written,
61
+ offset + written,
62
+ );
63
+ written += n;
64
+ }
65
+ }
66
+
67
+ size(): number {
68
+ return fstatSync(this.fd).size;
69
+ }
70
+
71
+ /** Close the underlying file descriptor. */
72
+ close(): void {
73
+ closeSync(this.fd);
74
+ }
75
+ }