Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: PHP CI

on:
push:
branches: [master]
pull_request:
branches: [master]

defaults:
run:
working-directory: implementations/php

jobs:
test:
name: test (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: hash, mbstring
coverage: none
tools: composer:v2

- name: Validate composer.json
run: composer validate --strict

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Run PHPUnit
run: vendor/bin/phpunit

test-vector:
name: regenerate spec test vector
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: hash, mbstring
coverage: none
tools: composer:v2

- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-interaction

- name: Build the test-vector example
run: php examples/gen_testvector.php pcf_testvector.bin

- name: Inspect generated test vector
run: |
ls -l pcf_testvector.bin
test "$(wc -c < pcf_testvector.bin)" = "395"

- uses: actions/upload-artifact@v4
with:
name: pcf-testvector-php
path: implementations/php/pcf_testvector.bin
19 changes: 19 additions & 0 deletions implementations/php/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# --- Composer ---
/vendor/
composer.lock

# --- PHPUnit ---
/.phpunit.cache/
.phpunit.result.cache

# --- Generated artifacts ---
*.bin

# --- Editors ---
.idea/
.vscode/
*.swp
*~

# --- macOS ---
.DS_Store
127 changes: 127 additions & 0 deletions implementations/php/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# pcf — Partitioned Container Format (PHP implementation)

PHP reader/writer for **PCF v1.0**, a language-agnostic binary container that
stores multiple independent byte regions ("partitions") in one file.

This is the first PHP port of the format. It mirrors the written specification
(`specs/PCF-spec-v1.0.txt`) and the Rust reference (`reference/PCF-v1.0/`)
field-for-field, and it reproduces the canonical 395-byte test vector from spec
section 15 **byte-for-byte**. It favours auditability over performance.

## Layout

```
[ 20-byte header ] [ table block(s) ] [ partition data regions ]
```

* **Header** (20 B): magic `0x89 K P R T 0x0D 0x0A 0x1A`, major/minor version,
absolute offset of the first table block.
* **Table block**: 74-byte header (`partition_count`, `next_table_offset`,
hash algo + 64-byte block hash) followed by `partition_count` entries.
Blocks form a singly linked chain to hold more than 255 partitions.
* **Entry** (141 B): `type`, 16-byte UID, 32-byte ASCII label, `start_offset`,
`max_length`, `used_bytes`, 1-byte data-hash algorithm, 64-byte data hash.

All integers are little-endian. Free space is `max_length - used_bytes`.

## Hash registry

| id | algorithm | id | algorithm |
|----|------------------|----|-----------|
| 0 | none | 5 | SHA-1 |
| 1 | CRC-32/ISO-HDLC | 16 | SHA-256 (default) |
| 2 | CRC-32C | 17 | SHA-512 |
| 3 | CRC-64/XZ | 18 | BLAKE3 |
| 4 | MD5 | | |

Most algorithms come from PHP's bundled `ext-hash`. The two it does not provide:

* **CRC-64/XZ** — `Kduma\PCF\Crc64` (check value `0x995DC9BBDF1939FA`), shipped
pure-PHP and validated against the canonical check vector.
* **BLAKE3** — delegated to the `tourze/blake3-php` Composer package, wrapped
behind `HashAlgo::blake3()` so the dependency is isolated to one method.

## Requirements

* PHP >= 8.1 with `ext-hash` (bundled by default).
* Composer (for the BLAKE3 dependency).

## Installation

```bash
composer require kduma-oss/pcf
```

## Usage

```php
use Kduma\PCF\Container;
use Kduma\PCF\HashAlgo;
use Kduma\PCF\Storage\MemoryStorage;
use Kduma\PCF\Storage\StreamStorage;

// In-memory container.
$c = Container::create(new MemoryStorage());
$uid = str_repeat("\x01", 16);
$c->addPartition(0x10, $uid, 'notes', 'hello world', 64, HashAlgo::Sha256);

$c->verify();
$entries = $c->entries();
echo $c->readPartitionData($entries[0]); // "hello world"

// File-backed container.
$f = Container::create(StreamStorage::fromFile('container.pcf', 'c+'));
$f->addPartition(0xFFFFFFFF, str_repeat("\x02", 16), 'blob', "\x00\x01\x02", 0, HashAlgo::Crc32c);
$f->verify();

// Reclaim dead space into the canonical compacted layout.
$image = $c->compactedImage();
file_put_contents('compacted.pcf', $image);
```

`Container` works over any `Kduma\PCF\Storage\StorageInterface`:

* `MemoryStorage` — an in-memory string buffer (analogue of the reference's
`Cursor<Vec<u8>>`).
* `StreamStorage` — any seekable PHP stream / file (analogue of `std::fs::File`).

### Operations

| Method | Purpose |
|------------------------------|---------|
| `Container::create()` / `createWith()` | Start an empty container. |
| `Container::open()` | Open an existing one (validates magic + major version). |
| `addPartition()` | Append a partition (unique non-NIL UID, non-reserved type). |
| `updatePartitionData()` | Replace data in place; maintains the hash cascade. |
| `removePartition()` | Remove a partition (data region becomes dead space). |
| `entries()` / `readPartitionData()` | Read metadata and data. |
| `verify()` | Verify every table-block and partition hash + conformance checks. |
| `compactedImage()` / `compactInto()` | Produce the tightly packed canonical form. |

Errors are reported as `Kduma\PCF\PcfException`; the precise cause is available
as `$e->kind` (a `Kduma\PCF\ErrorKind`).

## Tests

```bash
composer install
composer test # or: vendor/bin/phpunit
php examples/gen_testvector.php out.bin # writes the canonical 395-byte file
```

The suite mirrors the Rust reference:

```
implementations/php/
├── composer.json
├── src/ # library sources
├── examples/
│ └── gen_testvector.php # produces the canonical spec test vector
└── tests/
├── HashTest.php # hash registry + CRC/BLAKE3 vectors
├── HeaderTest.php # 20-byte file header
├── EntryTest.php # 141-byte entry + labels
├── TableTest.php # 74-byte table block + table hash
├── RoundtripTest.php # end-to-end create/open/update/remove/compact
└── SpecComplianceTest.php# one test per normative MUST/SHALL + byte-exact vector
```
32 changes: 32 additions & 0 deletions implementations/php/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "kduma-oss/pcf",
"description": "PHP implementation of the Partitioned Container Format (PCF) v1.0",
"type": "library",
"license": "MIT",
"keywords": ["pcf", "partitioned-container-format", "binary", "container", "file-format"],
"require": {
"php": ">=8.1",
"ext-hash": "*",
"tourze/blake3-php": "^0.0.1"
},
"require-dev": {
"phpunit/phpunit": "^10.5 || ^11.0"
},
"autoload": {
"psr-4": {
"Kduma\\PCF\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Kduma\\PCF\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"gen-testvector": "php examples/gen_testvector.php"
},
"config": {
"sort-packages": true
}
}
50 changes: 50 additions & 0 deletions implementations/php/examples/gen_testvector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

/**
* Generates the canonical PCF v1.0 test-vector file used in spec section 15.
*
* Run with: php examples/gen_testvector.php [output-path]
* (defaults to ./pcf_testvector.bin). Everything is fixed and deterministic so
* that ports can reproduce the file byte-for-byte.
*/

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Kduma\PCF\Container;
use Kduma\PCF\HashAlgo;
use Kduma\PCF\Storage\MemoryStorage;

$path = $argv[1] ?? 'pcf_testvector.bin';

$c = Container::createWith(new MemoryStorage(), 8, HashAlgo::Sha256);

// Partition 0: a SHA-256-protected text region.
$c->addPartition(0x0000_0010, str_repeat("\x11", 16), 'alpha', 'Hello, PCF!', 0, HashAlgo::Sha256);

// Partition 1: a RAW region protected by CRC-32C.
$c->addPartition(0xFFFF_FFFF, str_repeat("\x22", 16), 'raw', "\x00\x01\x02\x03\x04\x05\x06\x07", 0, HashAlgo::Crc32c);

// Compact to the canonical, tightly-packed layout.
$image = $c->compactedImage();
file_put_contents($path, $image);

// Re-open the produced bytes and verify, then print a short report.
$v = Container::open(new MemoryStorage($image));
$v->verify();

fwrite(STDERR, sprintf("wrote %s (%d bytes)\n", $path, \strlen($image)));
foreach ($v->entries() as $e) {
$n = $e->dataHashAlgo->digestLen();
$hex = bin2hex(substr($e->dataHash, 0, $n));
fwrite(STDERR, sprintf(
" %-6s type=0x%08X algo=%s start=%d used=%d data_hash=%s\n",
$e->labelString(),
$e->partitionType,
$e->dataHashAlgo->name,
$e->startOffset,
$e->usedBytes,
$hex
));
}
20 changes: 20 additions & 0 deletions implementations/php/phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory=".phpunit.cache"
failOnWarning="true"
failOnRisky="true"
beStrictAboutOutputDuringTests="true">
<testsuites>
<testsuite name="PCF">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">src</directory>
</include>
</source>
</phpunit>
60 changes: 60 additions & 0 deletions implementations/php/src/Consts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Kduma\PCF;

/**
* On-disk constants defined by PCF v1.0.
*
* Every value here is normative and corresponds directly to a figure in the
* specification (see Appendix A, "Field Layout Summary").
*/
final class Consts
{
/** File signature, 8 bytes: 0x89 'K' 'P' 'R' 'T' 0x0D 0x0A 0x1A. */
public const MAGIC = "\x89KPRT\x0D\x0A\x1A";

/** Major format version implemented by this library. */
public const VERSION_MAJOR = 1;
/** Minor format version implemented by this library. */
public const VERSION_MINOR = 0;

/** Fixed size of the file header, in bytes. */
public const HEADER_SIZE = 20;
/** Fixed size of a table-block header, in bytes. */
public const TABLE_HEADER_SIZE = 74;
/** Fixed size of a single partition entry, in bytes. */
public const ENTRY_SIZE = 141;

/** Size of every hash field, in bytes (large enough for the widest digest). */
public const HASH_FIELD_SIZE = 64;
/** Size of the partition label field, in bytes. */
public const LABEL_SIZE = 32;
/** Size of the partition UID field, in bytes. */
public const UID_SIZE = 16;

/**
* Reserved partition type: invalid / uninitialised. MUST NOT label a live
* partition.
*/
public const TYPE_RESERVED = 0x0000_0000;
/**
* Reserved partition type: raw / blob, interpreted entirely by the
* application.
*/
public const TYPE_RAW = 0xFFFF_FFFF;

/** The NIL UID (all zero). MUST NOT label a live partition. */
public const NIL_UID = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00";

/**
* Maximum number of entries a single table block can hold (partition_count
* is a u8).
*/
public const MAX_ENTRIES_PER_BLOCK = 255;

private function __construct()
{
}
}
Loading
Loading