Skip to content

Commit e7df035

Browse files
authored
Merge pull request #8 from kduma-OSS/claude/cool-dijkstra-1cU6x
Add PFS-MS v1.0 reference implementation with compression support
2 parents 863a1db + ef43e3e commit e7df035

40 files changed

Lines changed: 5273 additions & 57 deletions

.github/workflows/ci-pfs.yml

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
name: CI (PFS-MS)
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: "-D warnings"
12+
13+
defaults:
14+
run:
15+
working-directory: reference/PFS-MS-v1.0
16+
17+
jobs:
18+
fmt:
19+
name: rustfmt
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- uses: dtolnay/rust-toolchain@stable
24+
with:
25+
components: rustfmt
26+
- run: cargo fmt --all -- --check
27+
28+
clippy:
29+
name: clippy
30+
runs-on: ubuntu-latest
31+
steps:
32+
- uses: actions/checkout@v4
33+
- uses: dtolnay/rust-toolchain@stable
34+
with:
35+
components: clippy
36+
- uses: Swatinem/rust-cache@v2
37+
with:
38+
workspaces: reference/PFS-MS-v1.0
39+
- run: cargo clippy --all-targets --all-features -- -D warnings
40+
41+
test:
42+
name: test (${{ matrix.os }})
43+
runs-on: ${{ matrix.os }}
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
os: [ubuntu-latest, macos-latest, windows-latest]
48+
steps:
49+
- uses: actions/checkout@v4
50+
- uses: dtolnay/rust-toolchain@stable
51+
- uses: Swatinem/rust-cache@v2
52+
with:
53+
workspaces: reference/PFS-MS-v1.0
54+
- run: cargo build --verbose
55+
- run: cargo test --all-targets --verbose
56+
- run: cargo test --doc --verbose
57+
58+
test-vector:
59+
name: regenerate spec test vector
60+
runs-on: ubuntu-latest
61+
steps:
62+
- uses: actions/checkout@v4
63+
- uses: dtolnay/rust-toolchain@stable
64+
- uses: Swatinem/rust-cache@v2
65+
with:
66+
workspaces: reference/PFS-MS-v1.0
67+
- name: Build and run the test-vector example
68+
run: cargo run --example gen_testvector
69+
- name: Inspect generated test vector
70+
run: |
71+
ls -l pfs_ms_testvector.bin
72+
test "$(wc -c < pfs_ms_testvector.bin)" = "2986"
73+
- uses: actions/upload-artifact@v4
74+
with:
75+
name: pfs-ms-testvector
76+
path: reference/PFS-MS-v1.0/pfs_ms_testvector.bin
77+
78+
coverage:
79+
name: code coverage
80+
runs-on: ubuntu-latest
81+
steps:
82+
- uses: actions/checkout@v4
83+
- uses: dtolnay/rust-toolchain@stable
84+
with:
85+
components: llvm-tools-preview
86+
- uses: Swatinem/rust-cache@v2
87+
with:
88+
workspaces: reference/PFS-MS-v1.0
89+
- name: Install cargo-llvm-cov
90+
uses: taiki-e/install-action@cargo-llvm-cov
91+
- name: Generate coverage report (lcov)
92+
run: cargo llvm-cov --all-features --lcov --output-path lcov.info
93+
- name: Print coverage summary
94+
run: cargo llvm-cov report
95+
# The `pfs` CLI binary is exercised manually, not by `cargo test`; the
96+
# library floor is enforced over everything except the binary and examples.
97+
- name: Enforce minimum library coverage
98+
run: |
99+
cargo llvm-cov report --summary-only \
100+
--ignore-filename-regex 'bin/|examples/' \
101+
--fail-under-lines 90 \
102+
--fail-under-functions 90
103+
- uses: actions/upload-artifact@v4
104+
with:
105+
name: pfs-ms-coverage-lcov
106+
path: reference/PFS-MS-v1.0/lcov.info

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[workspace]
22
resolver = "2"
3-
members = ["reference/PCF-v1.0", "tools/pcf-debug"]
3+
members = ["reference/PCF-v1.0", "reference/PFS-MS-v1.0", "tools/pcf-debug"]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Collections.Generic;
2+
3+
namespace Pcf;
4+
5+
/// <summary>
6+
/// One table block read from disk: its absolute <see cref="Offset"/>, its parsed
7+
/// <see cref="TableBlockHeader"/> (including <c>table_hash</c> and
8+
/// <c>next_table_offset</c>), and its <see cref="PartitionEntry"/> list.
9+
/// </summary>
10+
/// <remarks>
11+
/// This is a read-only view returned by <see cref="Container.ReadBlockAt"/>. It
12+
/// exists so that profiles layered on PCF (which must group blocks, inspect each
13+
/// block's <c>table_hash</c>, and follow non-default <c>next_table_offset</c>
14+
/// chains) can reuse PCF's block parsing rather than re-decoding raw bytes. It
15+
/// plays no part in the writer's in-memory bookkeeping.
16+
/// </remarks>
17+
public sealed class BlockView
18+
{
19+
/// <summary>Absolute file offset of the table block.</summary>
20+
public ulong Offset { get; }
21+
22+
/// <summary>Parsed 74-byte block header.</summary>
23+
public TableBlockHeader Header { get; }
24+
25+
/// <summary>The block's entries, in stored order.</summary>
26+
public IReadOnlyList<PartitionEntry> Entries { get; }
27+
28+
/// <summary>Create a block view.</summary>
29+
public BlockView(ulong offset, TableBlockHeader header, IReadOnlyList<PartitionEntry> entries)
30+
{
31+
Offset = offset;
32+
Header = header;
33+
Entries = entries;
34+
}
35+
}

implementations/dotnet/src/Pcf/Container.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,20 @@ public List<PartitionEntry> Entries()
227227
return outp;
228228
}
229229

230+
/// <summary>
231+
/// Read a single table block at an absolute <paramref name="offset"/>,
232+
/// returning its parsed header (including <c>table_hash</c>) and entries.
233+
/// Unlike <see cref="Entries"/>, which flattens the whole chain, this exposes
234+
/// one block at a time so a caller can follow an arbitrary
235+
/// <c>next_table_offset</c> chain and inspect each block's <c>table_hash</c>.
236+
/// It is a read-only operation and does not alter the container.
237+
/// </summary>
238+
public BlockView ReadBlockAt(ulong offset)
239+
{
240+
(TableBlockHeader h, List<PartitionEntry> entries) = ReadBlock(offset);
241+
return new BlockView(offset, h, entries);
242+
}
243+
230244
/// <summary>Read a partition's used data.</summary>
231245
public byte[] ReadPartitionData(PartitionEntry entry)
232246
{

implementations/dotnet/tests/Pcf.Tests/RoundtripTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,35 @@ public void Overflow_chain_roundtrips()
132132
reopened.Verify();
133133
Assert.Equal(5, reopened.Entries().Count);
134134
}
135+
136+
[Fact]
137+
public void ReadBlockAt_exposes_block_view()
138+
{
139+
// A first-block capacity of 2 forces a second (overflow) block for 3
140+
// partitions, so we can walk the chain block-by-block via ReadBlockAt.
141+
var c = Container.CreateWith(new MemoryStream(), 2, HashAlgo.Sha256);
142+
for (byte i = 1; i <= 3; i++)
143+
{
144+
c.AddPartition(i, TestSupport.Uid(i), $"p{i}",
145+
new byte[] { i, i, i, i }, 0, HashAlgo.Sha256);
146+
}
147+
148+
ulong off = c.Header.PartitionTableOffset;
149+
int total = 0, blocks = 0;
150+
while (off != 0)
151+
{
152+
BlockView view = c.ReadBlockAt(off);
153+
Assert.Equal(off, view.Offset);
154+
Assert.Equal((int)view.Header.PartitionCount, view.Entries.Count);
155+
// The exposed table_hash must match a recomputation over the block.
156+
byte[] recomputed = TableBlockHeader.ComputeTableHash(
157+
view.Header.TableHashAlgo, view.Header.NextTableOffset, view.Entries);
158+
Assert.Equal(recomputed, view.Header.TableHash);
159+
total += view.Entries.Count;
160+
blocks++;
161+
off = view.Header.NextTableOffset;
162+
}
163+
Assert.Equal(3, total);
164+
Assert.Equal(2, blocks);
165+
}
135166
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kduma\PCF;
6+
7+
/**
8+
* One table block read from disk: its absolute offset, its parsed
9+
* {@see TableBlockHeader} (including table_hash and next_table_offset), and its
10+
* {@see PartitionEntry} list.
11+
*
12+
* This is a read-only view returned by {@see Container::readBlockAt()}. It lets
13+
* code layered on PCF group blocks, inspect each block's table_hash, and follow
14+
* non-default next_table_offset chains, instead of {@see Container::entries()}
15+
* which flattens the whole chain.
16+
*/
17+
final class BlockView
18+
{
19+
/**
20+
* @param PartitionEntry[] $entries the block's entries, in stored order
21+
*/
22+
public function __construct(
23+
public int $offset,
24+
public TableBlockHeader $header,
25+
public array $entries,
26+
) {
27+
}
28+
}

implementations/php/src/Container.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,21 @@ public function entries(): array
187187
return $out;
188188
}
189189

190+
/**
191+
* Read a single table block at an absolute offset, returning its parsed
192+
* header (including table_hash) and entries.
193+
*
194+
* Unlike {@see entries()}, which flattens the whole chain, this exposes one
195+
* block at a time so a caller can follow an arbitrary next_table_offset
196+
* chain and inspect each block's table_hash. Read-only.
197+
*/
198+
public function readBlockAt(int $offset): BlockView
199+
{
200+
[$h, $entries] = $this->readBlock($offset);
201+
202+
return new BlockView($offset, $h, $entries);
203+
}
204+
190205
/** Read a partition's used data. */
191206
public function readPartitionData(PartitionEntry $entry): string
192207
{

implementations/php/tests/RoundtripTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Kduma\PCF\HashAlgo;
1111
use Kduma\PCF\Storage\MemoryStorage;
1212
use Kduma\PCF\Storage\StreamStorage;
13+
use Kduma\PCF\TableBlockHeader;
1314

1415
/**
1516
* End-to-end container tests, porting `roundtrip.rs` and `coverage.rs`.
@@ -265,4 +266,35 @@ public function testCompactIntoWritesImage(): void
265266
$c->compactInto($out);
266267
self::assertSame($c->compactedImage(), $out->getContents());
267268
}
269+
270+
public function testReadBlockAtExposesBlockView(): void
271+
{
272+
// First block capacity of 2 forces a second (overflow) block for 3
273+
// partitions, so we can walk the chain block-by-block via readBlockAt.
274+
$c = Container::createWith(new MemoryStorage(), 2, HashAlgo::Sha256);
275+
for ($i = 1; $i <= 3; ++$i) {
276+
$c->addPartition($i, self::uid($i), "p{$i}", str_repeat(\chr($i), 4), 0, HashAlgo::Sha256);
277+
}
278+
279+
$off = $c->header()->partitionTableOffset;
280+
$total = 0;
281+
$blocks = 0;
282+
while ($off !== 0) {
283+
$view = $c->readBlockAt($off);
284+
self::assertSame($off, $view->offset);
285+
self::assertCount($view->header->partitionCount, $view->entries);
286+
// The exposed table_hash must match a recomputation over the block.
287+
$recomputed = TableBlockHeader::computeTableHash(
288+
$view->header->tableHashAlgo,
289+
$view->header->nextTableOffset,
290+
$view->entries,
291+
);
292+
self::assertSame($recomputed, $view->header->tableHash);
293+
$total += \count($view->entries);
294+
++$blocks;
295+
$off = $view->header->nextTableOffset;
296+
}
297+
self::assertSame(3, $total);
298+
self::assertSame(2, $blocks);
299+
}
268300
}

implementations/ts/src/container.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,25 @@ interface BlockInfo {
6868
next: number;
6969
}
7070

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+
7190
function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {
7291
if (a.length !== b.length) {
7392
return false;
@@ -242,6 +261,18 @@ export class Container {
242261
return out;
243262
}
244263

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+
245276
/** Read a partition's used data. */
246277
readPartitionData(entry: PartitionEntry): Uint8Array {
247278
const used = Number(entry.usedBytes);

implementations/ts/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ export {
6565
} from "./table.js";
6666
export { type Storage, MemoryStorage } from "./storage.js";
6767
export { NodeFileStorage } from "./node-storage.js";
68-
export { Container } from "./container.js";
68+
export { Container, type BlockView } from "./container.js";

0 commit comments

Comments
 (0)