Skip to content

Commit a16441d

Browse files
authored
Merge pull request #3 from kduma-OSS/claude/serene-brahmagupta-ukyGv
Add TypeScript implementation of PCF v1.0
2 parents bdd17a7 + d7b20db commit a16441d

23 files changed

Lines changed: 5741 additions & 0 deletions

.github/workflows/ts-ci.yml

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: TS CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
defaults:
10+
run:
11+
working-directory: implementations/ts
12+
13+
jobs:
14+
build:
15+
name: typecheck & build
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: 22
22+
cache: npm
23+
cache-dependency-path: implementations/ts/package-lock.json
24+
- run: npm ci
25+
- run: npm run build
26+
27+
test:
28+
name: test (${{ matrix.os }})
29+
runs-on: ${{ matrix.os }}
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
os: [ubuntu-latest, macos-latest, windows-latest]
34+
steps:
35+
- uses: actions/checkout@v4
36+
- uses: actions/setup-node@v4
37+
with:
38+
node-version: 22
39+
cache: npm
40+
cache-dependency-path: implementations/ts/package-lock.json
41+
- run: npm ci
42+
- run: npm test
43+
44+
test-vector:
45+
name: regenerate spec test vector
46+
runs-on: ubuntu-latest
47+
steps:
48+
- uses: actions/checkout@v4
49+
- uses: actions/setup-node@v4
50+
with:
51+
node-version: 22
52+
cache: npm
53+
cache-dependency-path: implementations/ts/package-lock.json
54+
- run: npm ci
55+
- name: Build and run the test-vector example
56+
run: npm run gen-testvector -- pcf_testvector.bin
57+
- name: Inspect generated test vector
58+
run: |
59+
ls -l pcf_testvector.bin
60+
test "$(wc -c < pcf_testvector.bin)" = "395"
61+
- uses: actions/upload-artifact@v4
62+
with:
63+
name: pcf-testvector-ts
64+
path: implementations/ts/pcf_testvector.bin
65+
66+
coverage:
67+
name: code coverage
68+
runs-on: ubuntu-latest
69+
steps:
70+
- uses: actions/checkout@v4
71+
- uses: actions/setup-node@v4
72+
with:
73+
node-version: 22
74+
cache: npm
75+
cache-dependency-path: implementations/ts/package-lock.json
76+
- run: npm ci
77+
- name: Generate coverage report (enforces >=95% line / 100% function)
78+
run: npm run coverage
79+
- uses: actions/upload-artifact@v4
80+
with:
81+
name: coverage-lcov-ts
82+
path: implementations/ts/coverage/lcov.info

implementations/ts/.gitignore

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# --- Node / TypeScript ---
2+
node_modules/
3+
dist/
4+
coverage/
5+
*.tsbuildinfo
6+
7+
# --- generated artefacts ---
8+
pcf_testvector.bin
9+
*.bin
10+
11+
# --- editors ---
12+
.idea/
13+
.vscode/
14+
*.swp
15+
*.swo
16+
*~
17+
18+
# --- macOS ---
19+
.DS_Store

implementations/ts/README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# pcf — Partitioned Container Format (TypeScript implementation)
2+
3+
A TypeScript reader/writer for **PCF v1.0**, a language-agnostic binary
4+
container that stores multiple independent byte regions ("partitions") in one
5+
file.
6+
7+
This package is a faithful port of the Rust reference implementation
8+
(`reference/PCF-v1.0/`) and mirrors the written specification
9+
(`specs/PCF-spec-v1.0.txt`) field-for-field. It favours auditability over
10+
performance and produces the **byte-exact** canonical test vector from spec
11+
section 15.
12+
13+
## Layout
14+
15+
```
16+
[ 20-byte header ] [ table block(s) ] [ partition data regions ]
17+
```
18+
19+
- **Header** (20 B): magic `0x89 K P R T 0x0D 0x0A 0x1A`, major/minor version,
20+
absolute offset of the first table block.
21+
- **Table block**: 74-byte header (`partitionCount`, `nextTableOffset`,
22+
hash algo + 64-byte block hash) followed by `partitionCount` entries. Blocks
23+
form a singly linked chain to hold more than 255 partitions.
24+
- **Entry** (141 B): `type`, 16-byte UID, 32-byte ASCII label, `startOffset`,
25+
`maxLength`, `usedBytes`, 1-byte data-hash algorithm, 64-byte data hash.
26+
27+
All integers are little-endian. `u64` fields are modelled as `bigint` to
28+
preserve full 64-bit fidelity; free space is `maxLength - usedBytes`.
29+
30+
## Hash registry
31+
32+
| id | algorithm | id | algorithm |
33+
|----|------------------|----|-----------|
34+
| 0 | none | 5 | SHA-1 |
35+
| 1 | CRC-32/ISO-HDLC | 16 | SHA-256 (default) |
36+
| 2 | CRC-32C | 17 | SHA-512 |
37+
| 3 | CRC-64/XZ | 18 | BLAKE3 |
38+
| 4 | MD5 | | |
39+
40+
Digests are provided by the audited [`@noble/hashes`](https://github.com/paulmillr/noble-hashes)
41+
package; the three CRC variants are implemented in pure TypeScript
42+
(`src/crc.ts`).
43+
44+
## Usage
45+
46+
```ts
47+
import { Container, HashAlgo } from "pcf";
48+
49+
const c = Container.create();
50+
const uid = new Uint8Array(16).fill(1);
51+
c.addPartition(
52+
0x10,
53+
uid,
54+
"notes",
55+
new TextEncoder().encode("hello world"),
56+
64,
57+
HashAlgo.Sha256,
58+
);
59+
60+
c.verify();
61+
const entries = c.entries();
62+
const data = c.readPartitionData(entries[0]);
63+
console.log(new TextDecoder().decode(data)); // "hello world"
64+
```
65+
66+
A `Container` is backed by any `Storage`. Two implementations ship with the
67+
package:
68+
69+
- `MemoryStorage` — an in-memory growable buffer (the default for
70+
`Container.create()`).
71+
- `NodeFileStorage` — backed by a Node file descriptor:
72+
73+
```ts
74+
import { Container, NodeFileStorage, HashAlgo } from "pcf";
75+
76+
const store = NodeFileStorage.open("container.pcf", /* truncate */ true);
77+
const c = Container.create(store);
78+
// … add partitions …
79+
store.close();
80+
```
81+
82+
## Project layout
83+
84+
```
85+
implementations/ts/
86+
├── package.json
87+
├── tsconfig.json
88+
├── vitest.config.ts
89+
├── src/ # library sources
90+
│ ├── consts.ts errors.ts crc.ts hash.ts
91+
│ ├── header.ts entry.ts table.ts
92+
│ ├── storage.ts node-storage.ts container.ts
93+
│ └── index.ts # public re-exports
94+
├── test/
95+
│ ├── roundtrip.test.ts # end-to-end black-box tests
96+
│ ├── coverage.test.ts # targeted error-path / edge-case tests
97+
│ └── spec-compliance.test.ts # one test per normative MUST/SHALL
98+
└── examples/
99+
└── gen-testvector.ts # produces the canonical 395-byte spec vector
100+
```
101+
102+
## Scripts
103+
104+
Run from this directory:
105+
106+
```
107+
npm install # install dependencies
108+
npm run build # type-check and emit dist/ (tsc, strict)
109+
npm test # run the full vitest suite
110+
npm run coverage # vitest + v8 coverage (95% line / 100% function floor)
111+
npm run gen-testvector # writes pcf_testvector.bin (the 395-byte spec vector)
112+
```
113+
114+
CI (`.github/workflows/ts-ci.yml`) runs the type-check/build, the test suite on
115+
Linux/macOS/Windows, regenerates and size-checks the spec test vector, and
116+
enforces the coverage floor.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* Generates the canonical PCF v1.0 test-vector file used in spec section 15.
3+
*
4+
* Run with: `npx tsx examples/gen-testvector.ts <output-path>`
5+
* (defaults to ./pcf_testvector.bin). Everything is fixed and deterministic so
6+
* that ports can reproduce the file byte-for-byte.
7+
*/
8+
9+
import { writeFileSync } from "node:fs";
10+
11+
import {
12+
Container,
13+
digestLen,
14+
entryLabelString,
15+
HashAlgo,
16+
MemoryStorage,
17+
TYPE_RAW,
18+
} from "../src/index.js";
19+
20+
function main(): void {
21+
const path = process.argv[2] ?? "pcf_testvector.bin";
22+
23+
const c = Container.createWith(new MemoryStorage(), 8, HashAlgo.Sha256);
24+
25+
// Partition 0: a SHA-256-protected text region.
26+
c.addPartition(
27+
0x0000_0010,
28+
new Uint8Array(16).fill(0x11),
29+
"alpha",
30+
new TextEncoder().encode("Hello, PCF!"),
31+
0,
32+
HashAlgo.Sha256,
33+
);
34+
35+
// Partition 1: a RAW region protected by CRC-32C.
36+
c.addPartition(
37+
TYPE_RAW,
38+
new Uint8Array(16).fill(0x22),
39+
"raw",
40+
new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]),
41+
0,
42+
HashAlgo.Crc32c,
43+
);
44+
45+
// Compact to the canonical, tightly-packed layout.
46+
const image = c.compactedImage();
47+
writeFileSync(path, image);
48+
49+
// Re-open the produced bytes and verify, then print a short report.
50+
const v = Container.open(new MemoryStorage(image));
51+
v.verify();
52+
53+
process.stderr.write(`wrote ${path} (${image.length} bytes)\n`);
54+
for (const e of v.entries()) {
55+
const n = digestLen(e.dataHashAlgo);
56+
const hex = Array.from(e.dataHash.slice(0, n))
57+
.map((b) => b.toString(16).padStart(2, "0"))
58+
.join("");
59+
const typeHex = (e.partitionType >>> 0)
60+
.toString(16)
61+
.padStart(8, "0")
62+
.toUpperCase();
63+
process.stderr.write(
64+
` ${entryLabelString(e).padEnd(6)} type=0x${typeHex} ` +
65+
`algo=${HashAlgo[e.dataHashAlgo]} start=${e.startOffset} ` +
66+
`used=${e.usedBytes} data_hash=${hex}\n`,
67+
);
68+
}
69+
}
70+
71+
main();

0 commit comments

Comments
 (0)