Skip to content

Commit 02dd81c

Browse files
committed
Add pcf-compact tool for compacting PCF files
A small CLI on top of the reference pcf crate's Container::compacted_image (spec section 11.5). Removes dead space between table blocks and partition data, and trims each partition's max_length to its used_bytes. Default operation is atomic in-place: write to sibling temp, fsync, rename. --output writes to a separate path; --no-verify skips integrity checks; --force allows overwriting an existing --output.
1 parent 58161bf commit 02dd81c

7 files changed

Lines changed: 741 additions & 1 deletion

File tree

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
11
[workspace]
22
resolver = "2"
3-
members = ["reference/PCF-v1.0", "reference/PFS-MS-v1.0", "tools/pcf-debug"]
3+
members = [
4+
"reference/PCF-v1.0",
5+
"reference/PFS-MS-v1.0",
6+
"tools/pcf-debug",
7+
"tools/pcf-compact",
8+
]

tools/pcf-compact/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "pcf-compact"
3+
version = "0.0.1"
4+
edition = "2021"
5+
rust-version = "1.75"
6+
license = "MIT OR Apache-2.0"
7+
description = "Compactor for Partitioned Container Format (PCF) files: reclaims dead space and trims reservations"
8+
repository = "https://github.com/kduma-OSS/Partitioned-Container-Format"
9+
homepage = "https://github.com/kduma-OSS/Partitioned-Container-Format"
10+
readme = "README.md"
11+
keywords = ["pcf", "container", "compact", "binary"]
12+
categories = ["command-line-utilities", "filesystem"]
13+
14+
[lib]
15+
name = "pcf_compact"
16+
path = "src/lib.rs"
17+
18+
[[bin]]
19+
name = "pcf-compact"
20+
path = "src/main.rs"
21+
22+
[dependencies]
23+
pcf = { path = "../../reference/PCF-v1.0", version = "0.0.1" }

tools/pcf-compact/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# `pcf-compact`
2+
3+
A compactor for **Partitioned Container Format (PCF)** files. It rebuilds a
4+
container with all dead space removed and every partition's reservation
5+
trimmed to its `used_bytes`, exactly as defined by spec section 11.5.
6+
7+
The tool is a thin wrapper around the reference [`pcf`](../../reference/PCF-v1.0)
8+
crate's `Container::compacted_image` — the algorithm lives there. What this
9+
crate adds is a CLI, safe atomic file replacement, and a savings report.
10+
11+
## Build & run
12+
13+
From the repository root:
14+
15+
```sh
16+
# build
17+
cargo build -p pcf-compact
18+
19+
# produce a sample file
20+
cargo run -p pcf --example gen_testvector -- /tmp/tv.pcf
21+
22+
# compact it in place
23+
cargo run -p pcf-compact -- /tmp/tv.pcf
24+
```
25+
26+
## Usage
27+
28+
```text
29+
pcf-compact <FILE> [FLAGS]
30+
31+
FLAGS:
32+
-o, --output <PATH> write the compacted file to PATH instead of
33+
overwriting <FILE> in place
34+
--no-verify skip integrity verification before and after
35+
compaction (default: verify both)
36+
--force overwrite an existing --output path
37+
-q, --quiet suppress the savings report on stderr
38+
-h, --help show help
39+
```
40+
41+
### Examples
42+
43+
```sh
44+
# In-place, with verification before and after (default).
45+
pcf-compact container.pcf
46+
47+
# Write the compacted copy to a new path, leave the original alone.
48+
pcf-compact container.pcf --output container.compact.pcf
49+
50+
# Overwrite an existing output file.
51+
pcf-compact container.pcf -o out.pcf --force
52+
53+
# Skip verification (fast path for known-good inputs).
54+
pcf-compact container.pcf --no-verify
55+
```
56+
57+
## Atomic write guarantees
58+
59+
In-place mode (and `--output` to a fresh path) writes via a sibling temp file
60+
named `<target>.pcf-compact.tmp.<pid>.<nanos>`, fsyncs the data, then issues an
61+
atomic `rename(2)`. On crash either the original file is intact, or the new
62+
file is fully durable. Stray `*.pcf-compact.tmp.*` files left after a crash
63+
are safe to delete.
64+
65+
Cross-filesystem `--output` (where the temp file and target would land on
66+
different mount points) is rejected rather than silently falling back to a
67+
non-atomic copy — pick an `--output` path on the same filesystem.
68+
69+
## What it does *not* do
70+
71+
- Edit individual partitions (`pcf-debug` is for inspection).
72+
- Change the table-hash algorithm beyond normalising every table block to
73+
the algorithm used by the first block in the source chain.
74+
- Stream: the whole file is loaded into memory.
75+
76+
## Tests
77+
78+
```sh
79+
cargo test -p pcf-compact
80+
```

tools/pcf-compact/src/cli.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Command-line parsing for `pcf-compact`.
2+
//!
3+
//! Hand-written on top of [`std::env::args`] to keep the tool free of any
4+
//! argument-parsing dependency (matching the reference crate's minimal-deps
5+
//! posture). Grammar:
6+
//!
7+
//! ```text
8+
//! pcf-compact <FILE> [FLAGS]
9+
//! ```
10+
11+
use std::path::PathBuf;
12+
13+
#[derive(Debug, Clone)]
14+
pub struct Args {
15+
pub file: PathBuf,
16+
pub output: Option<PathBuf>,
17+
pub verify: bool,
18+
pub quiet: bool,
19+
pub force: bool,
20+
}
21+
22+
#[derive(Debug)]
23+
pub enum Parsed {
24+
Run(Args),
25+
Help,
26+
}
27+
28+
const HELP: &str = "\
29+
pcf-compact — rebuild a Partitioned Container Format (PCF) file with all dead
30+
space and per-partition reservations removed (spec section 11.5).
31+
32+
USAGE:
33+
pcf-compact <FILE> [FLAGS]
34+
35+
FLAGS:
36+
-o, --output <PATH> write the compacted file to PATH instead of
37+
overwriting <FILE> in place
38+
--no-verify skip integrity verification before and after
39+
compaction (default: verify both)
40+
--force overwrite an existing --output path
41+
-q, --quiet suppress the savings report on stderr
42+
-h, --help show this help
43+
44+
By default the tool overwrites <FILE> atomically: it writes the compacted
45+
image to a sibling temp file, fsyncs it, and then renames it into place.
46+
";
47+
48+
pub fn help() -> &'static str {
49+
HELP
50+
}
51+
52+
/// Parse arguments (excluding `argv[0]`).
53+
pub fn parse(argv: &[String]) -> Result<Parsed, String> {
54+
let mut positionals: Vec<String> = Vec::new();
55+
let mut output: Option<PathBuf> = None;
56+
let mut verify = true;
57+
let mut quiet = false;
58+
let mut force = false;
59+
60+
fn value(argv: &[String], i: &mut usize, flag: &str) -> Result<String, String> {
61+
*i += 1;
62+
argv.get(*i)
63+
.cloned()
64+
.ok_or_else(|| format!("flag {flag} needs a value"))
65+
}
66+
67+
let mut i = 0;
68+
while i < argv.len() {
69+
let a = argv[i].clone();
70+
match a.as_str() {
71+
"-h" | "--help" => return Ok(Parsed::Help),
72+
"-o" | "--output" => output = Some(PathBuf::from(value(argv, &mut i, &a)?)),
73+
"--no-verify" => verify = false,
74+
"--force" => force = true,
75+
"-q" | "--quiet" => quiet = true,
76+
other if other.starts_with('-') => {
77+
return Err(format!("unknown flag: {other}"));
78+
}
79+
_ => positionals.push(a),
80+
}
81+
i += 1;
82+
}
83+
84+
if positionals.is_empty() {
85+
return Err("missing FILE argument".into());
86+
}
87+
if positionals.len() > 1 {
88+
return Err(format!(
89+
"unexpected positional argument: {:?}",
90+
positionals[1]
91+
));
92+
}
93+
let file = PathBuf::from(&positionals[0]);
94+
95+
Ok(Parsed::Run(Args {
96+
file,
97+
output,
98+
verify,
99+
quiet,
100+
force,
101+
}))
102+
}

0 commit comments

Comments
 (0)