use std::fs;
#[cfg(feature = "rans")]
use std::fs::File;
use std::io::Write;
#[cfg(feature = "rans")]
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::{SystemTime, UNIX_EPOCH};
use vole_document::adapter::pdf;
#[cfg(feature = "store")]
use vole_document::container::Descriptor;
use vole_document::container::UNIVERSE;
#[cfg(feature = "rans")]
use vole_document::container::header::{FEATURE_SEEK_DIRECTORY, HEADER_LEN, Header};
use vole_document::dra::Op;
use vole_document::encode::candidates::CandidateKind;
use vole_document::error::{Error, Result};
use vole_document::limits::Limits;
#[cfg(feature = "rans")]
use vole_document::materialize::observation::{ObservationReport, ObservationSelector};
#[cfg(feature = "store")]
use vole_document::store::{EmbeddedStore, account, externalize, gc};
use vole_document::{encode, integrity, materialize};
const USAGE_HEAD: &str = "\
vole-document — byte-exact procedural document storage
USAGE:
vole-document encode [--force KIND] INPUT OUTPUT.voldoc
vole-document decode INPUT.voldoc OUTPUT
vole-document materialize INPUT.voldoc OUTPUT
vole-document verify INPUT.voldoc
vole-document inspect INPUT.voldoc
vole-document pdf-inspect INPUT
vole-document pdf-make-samples DIR
vole-document pdf-make-large DIR [OBJECTS]
";
#[cfg(feature = "deflate-replay")]
const USAGE_DEFLATE_STATS: &str = " vole-document deflate-stats INPUT...\n";
#[cfg(not(feature = "deflate-replay"))]
const USAGE_DEFLATE_STATS: &str = "";
#[cfg(feature = "rans")]
const USAGE_VIEW: &str = "\
vole-document view INPUT.voldoc [OUTPUT] --byte-range A:L | --pdf-object N:G |\n\
--pdf-stream N:G | --pdf-revision I [--stats]\n";
#[cfg(not(feature = "rans"))]
const USAGE_VIEW: &str = "";
#[cfg(feature = "store")]
const USAGE_STORE: &str = "\
vole-document decode --store STORE_DIR INPUT.voldoc OUTPUT\n\
vole-document store put INPUT.voldoc STORE_DIR\n\
vole-document store account STORE_DIR ROOT...\n\
vole-document store gc STORE_DIR ROOT...\n";
#[cfg(not(feature = "store"))]
const USAGE_STORE: &str = "";
const USAGE_TAIL: &str = "\
vole-document capabilities
KIND (for encode --force): raw | rle | byte-rans | pdf-physical | pdf-channels |
pdf-layout | pdf-layout-rans | pdf-deflate-replay | pdf-deflate-replay-rans |
pdf-deflate-replay-rans-indexed
Forces the complete-cost court to consider only that candidate family, for
honest per-mechanism ablation. Fails when the input does not propose it.
EXIT CODES:
0 ok 2 usage 3 io 4 invalid-container 5 unsupported-version
6 unsupported-feature 7 integrity-mismatch 8 resource-limit
9 invalid-graph 10 invalid-model 15 coverage-violation
16 reconstruction-mismatch 70 internal-invariant
";
fn usage() -> String {
format!("{USAGE_HEAD}{USAGE_VIEW}{USAGE_DEFLATE_STATS}{USAGE_STORE}{USAGE_TAIL}")
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
eprintln!("error: {e}");
let code = e.exit_code();
ExitCode::from(u8::try_from(code).unwrap_or(70))
}
}
}
fn run(args: &[String]) -> Result<()> {
#[cfg(feature = "deflate-replay")]
{
use vole_document::codec::deflate::{
REPLAY_WORKER_SUBCOMMAND, install_default_replay_worker, run_worker_stdio,
};
if args.get(1).map(String::as_str) == Some(REPLAY_WORKER_SUBCOMMAND) {
run_worker_stdio();
}
if std::env::var_os("VOLE_REPLAY_WORKER").is_none()
&& let Ok(exe) = std::env::current_exe()
{
install_default_replay_worker(exe);
}
}
let cmd = match args.get(1).map(String::as_str) {
Some(c) => c,
None => {
print!("{}", usage());
return Err(Error::usage("no subcommand given"));
}
};
let limits = Limits::DEFAULT;
match cmd {
"-h" | "--help" | "help" => {
print!("{}", usage());
Ok(())
}
"capabilities" => cmd_capabilities(),
"encode" => cmd_encode_args(args, limits),
"decode" | "materialize" => cmd_decode_args(args, limits),
"verify" => {
let input = arg(args, 2, "INPUT.voldoc")?;
cmd_verify(&input, limits)
}
"inspect" => {
let input = arg(args, 2, "INPUT.voldoc")?;
cmd_inspect(&input, limits)
}
#[cfg(feature = "rans")]
"view" => cmd_view_args(args, limits),
#[cfg(feature = "store")]
"store" => cmd_store_args(args, limits),
"pdf-inspect" => {
let input = arg(args, 2, "INPUT")?;
cmd_pdf_inspect(&input, limits)
}
"pdf-make-samples" => {
let dir = arg(args, 2, "DIR")?;
cmd_pdf_make_samples(&dir)
}
"pdf-make-large" => {
let dir = arg(args, 2, "DIR")?;
let objects = args.get(3).map(String::as_str);
cmd_pdf_make_large(&dir, objects)
}
#[cfg(feature = "deflate-replay")]
"deflate-stats" => {
let inputs: Vec<PathBuf> = args
.get(2..)
.unwrap_or(&[])
.iter()
.map(PathBuf::from)
.collect();
if inputs.is_empty() {
return Err(Error::usage(
"deflate-stats requires at least one INPUT (a PDF)",
));
}
cmd_deflate_stats(&inputs, limits)
}
other => Err(Error::usage(format!(
"unknown subcommand {other:?}\n\n{}",
usage()
))),
}
}
fn arg(args: &[String], idx: usize, name: &str) -> Result<PathBuf> {
args.get(idx)
.map(PathBuf::from)
.ok_or_else(|| Error::usage(format!("missing argument {name}")))
}
fn cmd_encode(
input: &Path,
output: &Path,
limits: Limits,
force: Option<CandidateKind>,
) -> Result<()> {
let source = fs::read(input)?;
if source.len() as u64 > limits.max_input_bytes {
return Err(Error::resource_limit(
"input exceeds configured input limit",
));
}
let (bytes, report) = encode::encode_with(&source, limits, force)?;
write_atomic(output, &bytes)?;
println!("{}", report_json(&report));
Ok(())
}
fn cmd_encode_args(args: &[String], limits: Limits) -> Result<()> {
let mut force: Option<CandidateKind> = None;
let mut positional: Vec<&str> = Vec::new();
let mut i = 2;
while i < args.len() {
let a = args[i].as_str();
if a == "--force" {
let kind = args
.get(i + 1)
.ok_or_else(|| Error::usage("--force requires a KIND argument"))?;
force = Some(parse_force_kind(kind)?);
i += 2;
} else if let Some(kind) = a.strip_prefix("--force=") {
force = Some(parse_force_kind(kind)?);
i += 1;
} else {
positional.push(a);
i += 1;
}
}
let input = positional
.first()
.map(PathBuf::from)
.ok_or_else(|| Error::usage("missing argument INPUT"))?;
let output = positional
.get(1)
.map(PathBuf::from)
.ok_or_else(|| Error::usage("missing argument OUTPUT.voldoc"))?;
if positional.len() > 2 {
return Err(Error::usage(format!(
"unexpected extra argument {:?}",
positional[2]
)));
}
cmd_encode(&input, &output, limits, force)
}
fn parse_force_kind(s: &str) -> Result<CandidateKind> {
match s {
"raw" => Ok(CandidateKind::Raw),
"rle" => Ok(CandidateKind::Rle),
"byte-rans" => Ok(CandidateKind::ByteRans),
"pdf-physical" => Ok(CandidateKind::PdfPhysical),
"pdf-channels" => Ok(CandidateKind::PdfChannels),
"pdf-layout" => Ok(CandidateKind::PdfLayout),
"pdf-layout-rans" => Ok(CandidateKind::PdfLayoutRans),
"pdf-deflate-replay" => Ok(CandidateKind::PdfDeflateReplay),
"pdf-deflate-replay-rans" => Ok(CandidateKind::PdfDeflateReplayRans),
"pdf-deflate-replay-rans-indexed" => Ok(CandidateKind::PdfDeflateReplayRansIndexed),
other => Err(Error::usage(format!(
"unknown --force kind {other:?}; expected one of raw, rle, byte-rans, pdf-physical, pdf-channels, pdf-layout, pdf-layout-rans, pdf-deflate-replay, pdf-deflate-replay-rans, pdf-deflate-replay-rans-indexed"
))),
}
}
#[cfg(feature = "rans")]
fn cmd_view_args(args: &[String], limits: Limits) -> Result<()> {
let mut selector: Option<ObservationSelector> = None;
let mut stats = false;
let mut positional: Vec<&str> = Vec::new();
let mut i = 2;
while i < args.len() {
let a = args[i].as_str();
if a == "--stats" {
stats = true;
i += 1;
} else if let Some(v) = a.strip_prefix("--byte-range=") {
set_selector(&mut selector, parse_byte_range(v)?)?;
i += 1;
} else if let Some(v) = a.strip_prefix("--pdf-object=") {
set_selector(&mut selector, parse_pdf_object(v)?)?;
i += 1;
} else if let Some(v) = a.strip_prefix("--pdf-stream=") {
set_selector(&mut selector, parse_pdf_stream(v)?)?;
i += 1;
} else if let Some(v) = a.strip_prefix("--pdf-revision=") {
set_selector(&mut selector, parse_pdf_revision(v)?)?;
i += 1;
} else if a == "--byte-range" {
let v = args
.get(i + 1)
.ok_or_else(|| Error::usage("--byte-range requires A:L"))?;
set_selector(&mut selector, parse_byte_range(v)?)?;
i += 2;
} else if a == "--pdf-object" {
let v = args
.get(i + 1)
.ok_or_else(|| Error::usage("--pdf-object requires N:G"))?;
set_selector(&mut selector, parse_pdf_object(v)?)?;
i += 2;
} else if a == "--pdf-stream" {
let v = args
.get(i + 1)
.ok_or_else(|| Error::usage("--pdf-stream requires N:G"))?;
set_selector(&mut selector, parse_pdf_stream(v)?)?;
i += 2;
} else if a == "--pdf-revision" {
let v = args
.get(i + 1)
.ok_or_else(|| Error::usage("--pdf-revision requires I"))?;
set_selector(&mut selector, parse_pdf_revision(v)?)?;
i += 2;
} else {
positional.push(a);
i += 1;
}
}
let selector = selector.ok_or_else(|| {
Error::usage(
"view requires exactly one of --byte-range, --pdf-object, --pdf-stream, --pdf-revision",
)
})?;
let input = positional
.first()
.map(PathBuf::from)
.ok_or_else(|| Error::usage("missing argument INPUT.voldoc"))?;
if positional.len() > 2 {
return Err(Error::usage(format!(
"unexpected extra argument {:?}",
positional[2]
)));
}
let output = positional.get(1).map(PathBuf::from);
cmd_view(&input, output.as_deref(), selector, stats, limits)
}
#[cfg(feature = "rans")]
fn set_selector(
slot: &mut Option<ObservationSelector>,
selector: ObservationSelector,
) -> Result<()> {
if slot.is_some() {
return Err(Error::usage(
"view accepts exactly one selector; more than one was given",
));
}
*slot = Some(selector);
Ok(())
}
#[cfg(feature = "rans")]
fn parse_byte_range(value: &str) -> Result<ObservationSelector> {
let (offset, len) = value
.split_once(':')
.ok_or_else(|| Error::usage("--byte-range must be A:L (e.g. 1024:4096)"))?;
let offset: u64 = offset
.parse()
.map_err(|_| Error::usage(format!("--byte-range offset {offset:?} is not a u64")))?;
let len: u64 = len
.parse()
.map_err(|_| Error::usage(format!("--byte-range length {len:?} is not a u64")))?;
Ok(ObservationSelector::ByteRange { offset, len })
}
#[cfg(feature = "rans")]
fn parse_pdf_object(value: &str) -> Result<ObservationSelector> {
let (object, generation) = parse_n_g(value, "--pdf-object")?;
Ok(ObservationSelector::PdfIndirectObject { object, generation })
}
#[cfg(feature = "rans")]
fn parse_pdf_stream(value: &str) -> Result<ObservationSelector> {
let (object, generation) = parse_n_g(value, "--pdf-stream")?;
Ok(ObservationSelector::PdfEncodedStream { object, generation })
}
#[cfg(feature = "rans")]
fn parse_pdf_revision(value: &str) -> Result<ObservationSelector> {
let index: u32 = value
.parse()
.map_err(|_| Error::usage(format!("--pdf-revision {value:?} is not a u32")))?;
Ok(ObservationSelector::PdfRevision { index })
}
#[cfg(feature = "rans")]
fn parse_n_g(value: &str, flag: &str) -> Result<(u32, u16)> {
let (n, g) = value
.split_once(':')
.ok_or_else(|| Error::usage(format!("{flag} must be N:G (e.g. 4:0)")))?;
let object: u32 = n
.parse()
.map_err(|_| Error::usage(format!("{flag} object {n:?} is not a u32")))?;
let generation: u16 = g
.parse()
.map_err(|_| Error::usage(format!("{flag} generation {g:?} is not a u16")))?;
Ok((object, generation))
}
#[cfg(feature = "rans")]
fn cmd_view(
input: &Path,
output: Option<&Path>,
selector: ObservationSelector,
stats: bool,
limits: Limits,
) -> Result<()> {
let mut file = File::open(input)?;
let mut hdr = [0u8; HEADER_LEN];
file.read_exact(&mut hdr)
.map_err(|_| Error::invalid_container("truncated header"))?;
let header = Header::decode(&hdr)?;
if header.optional_features & FEATURE_SEEK_DIRECTORY != 0 {
file.seek(SeekFrom::Start(0))
.map_err(|e| Error::io(format!("seek to start failed: {e}")))?;
let report = vole_document::materialize::seek::materialize_observation_seeked(
file, selector, limits,
)?;
let json = observation_json(&selector, header.declared_source_len, &report);
return emit_view(output, stats, &report, &json);
}
drop(file);
let encoded = fs::read(input)?;
let parsed = vole_document::container::Descriptor::parse(&encoded, limits)?;
let report = vole_document::materialize::observation::materialize_observation(
&parsed, selector, limits,
)?;
let json = observation_json(&selector, parsed.descriptor.source_len, &report);
emit_view(output, stats, &report, &json)
}
#[cfg(feature = "rans")]
fn emit_view(
output: Option<&Path>,
stats: bool,
report: &ObservationReport,
json: &str,
) -> Result<()> {
match output {
Some(path) => {
write_atomic(path, &report.bytes)?;
println!("{json}");
}
None if stats => println!("{json}"),
None => {
std::io::stdout()
.write_all(&report.bytes)
.map_err(Error::from)?;
eprintln!("{json}");
}
}
Ok(())
}
#[cfg(feature = "rans")]
fn observation_json(
selector: &ObservationSelector,
source_len: u64,
report: &ObservationReport,
) -> String {
let s = &report.stats;
format!(
concat!(
"{{",
"\"ok\":true,",
"\"selector\":\"{}\",",
"\"source_len\":{},",
"\"range_start\":{},",
"\"range_len\":{},",
"\"bytes\":{},",
"\"ops_evaluated\":{},",
"\"ops_total\":{},",
"\"objects_fetched\":{},",
"\"objects_total\":{},",
"\"channels_decoded\":{},",
"\"channels_total\":{},",
"\"entropy_bytes_decoded\":{},",
"\"descriptor_bytes_traversed\":{},",
"\"bytes_read\":{},",
"\"integrity_verified\":{},",
"\"output_bytes\":{},",
"\"work_amplification\":{:.6}",
"}}"
),
selector_label(selector),
source_len,
report.range.0,
report.range.1.saturating_sub(report.range.0),
report.bytes.len(),
s.ops_evaluated,
s.ops_total,
s.objects_fetched,
s.objects_total,
s.channels_decoded,
s.channels_total,
s.entropy_bytes_decoded,
s.descriptor_bytes_traversed,
s.bytes_read,
s.integrity_verified,
s.output_bytes,
s.work_amplification(),
)
}
#[cfg(feature = "rans")]
fn selector_label(selector: &ObservationSelector) -> String {
match selector {
ObservationSelector::ByteRange { offset, len } => format!("byte-range:{offset}:{len}"),
ObservationSelector::PdfIndirectObject { object, generation } => {
format!("pdf-object:{object}:{generation}")
}
ObservationSelector::PdfEncodedStream { object, generation } => {
format!("pdf-stream:{object}:{generation}")
}
ObservationSelector::PdfRevision { index } => format!("pdf-revision:{index}"),
}
}
fn cmd_decode(input: &Path, output: &Path, limits: Limits) -> Result<()> {
let encoded = fs::read(input)?;
let (bytes, parsed) = materialize::decode_to_bytes(&encoded, limits)?;
write_atomic(output, &bytes)?;
println!(
"{{\"ok\":true,\"source_len\":{},\"sha256\":\"{}\",\"graph_ops\":{},\"objects\":{}}}",
bytes.len(),
integrity::to_hex(&parsed.descriptor.source_sha256),
parsed.descriptor.program.ops.len(),
parsed.descriptor.objects.len()
);
Ok(())
}
fn cmd_decode_args(args: &[String], limits: Limits) -> Result<()> {
let mut store_dir: Option<PathBuf> = None;
let mut positional: Vec<&str> = Vec::new();
let mut i = 2;
while i < args.len() {
let a = args[i].as_str();
if a == "--store" {
let dir = args
.get(i + 1)
.ok_or_else(|| Error::usage("--store requires a STORE_DIR argument"))?;
store_dir = Some(PathBuf::from(dir));
i += 2;
} else if let Some(dir) = a.strip_prefix("--store=") {
store_dir = Some(PathBuf::from(dir));
i += 1;
} else {
positional.push(a);
i += 1;
}
}
let input = positional
.first()
.map(PathBuf::from)
.ok_or_else(|| Error::usage("missing argument INPUT.voldoc"))?;
let output = positional
.get(1)
.map(PathBuf::from)
.ok_or_else(|| Error::usage("missing argument OUTPUT"))?;
if positional.len() > 2 {
return Err(Error::usage(format!(
"unexpected extra argument {:?}",
positional[2]
)));
}
if let Some(dir) = store_dir {
#[cfg(feature = "store")]
{
return cmd_decode_with_store(&dir, &input, &output, limits);
}
#[cfg(not(feature = "store"))]
{
let _ = dir;
return Err(Error::unsupported_feature(
"this build was compiled without the `store` feature",
));
}
}
cmd_decode(&input, &output, limits)
}
#[cfg(feature = "store")]
fn cmd_decode_with_store(
store_dir: &Path,
input: &Path,
output: &Path,
limits: Limits,
) -> Result<()> {
let encoded = fs::read(input)?;
let store = EmbeddedStore::open(store_dir)?;
let (bytes, parsed) = materialize::decode_to_bytes_with(&encoded, &store, limits)?;
write_atomic(output, &bytes)?;
println!(
concat!(
"{{",
"\"ok\":true,",
"\"source_len\":{},",
"\"sha256\":\"{}\",",
"\"graph_ops\":{},",
"\"objects\":{},",
"\"store\":\"{}\"",
"}}"
),
bytes.len(),
integrity::to_hex(&parsed.descriptor.source_sha256),
parsed.descriptor.program.ops.len(),
parsed.descriptor.objects.len(),
store_dir.display()
);
Ok(())
}
fn cmd_verify(input: &Path, limits: Limits) -> Result<()> {
let encoded = fs::read(input)?;
let report = materialize::verify(&encoded, limits)?;
println!(
"{{\"ok\":true,\"source_len\":{},\"sha256\":\"{}\",\"objects\":{},\"graph_ops\":{}}}",
report.source_len, report.sha256_hex, report.object_count, report.graph_ops
);
Ok(())
}
#[cfg(feature = "store")]
fn cmd_store_args(args: &[String], limits: Limits) -> Result<()> {
match args.get(2).map(String::as_str) {
Some("put") => {
let input = arg(args, 3, "INPUT.voldoc")?;
let store_dir = arg(args, 4, "STORE_DIR")?;
if let Some(extra) = args.get(5) {
return Err(Error::usage(format!("unexpected extra argument {extra:?}")));
}
cmd_store_put(&input, &store_dir, limits)
}
Some("account") => {
let store_dir = arg(args, 3, "STORE_DIR")?;
let roots = root_args(args, 4)?;
cmd_store_account(&store_dir, &roots, limits)
}
Some("gc") => {
let store_dir = arg(args, 3, "STORE_DIR")?;
let roots = root_args(args, 4)?;
cmd_store_gc(&store_dir, &roots, limits)
}
Some(other) => Err(Error::usage(format!(
"unknown store subcommand {other:?}; expected put | account | gc"
))),
None => Err(Error::usage(
"store requires a subcommand: put | account | gc",
)),
}
}
#[cfg(feature = "store")]
fn root_args(args: &[String], start: usize) -> Result<Vec<PathBuf>> {
let roots: Vec<PathBuf> = args
.get(start..)
.unwrap_or(&[])
.iter()
.map(PathBuf::from)
.collect();
if roots.is_empty() {
return Err(Error::usage("expected at least one ROOT.voldoc"));
}
Ok(roots)
}
#[cfg(feature = "store")]
fn cmd_store_put(input: &Path, store_dir: &Path, limits: Limits) -> Result<()> {
let encoded = fs::read(input)?;
let mut descriptor = Descriptor::parse(&encoded, limits)?.descriptor;
let stem = input
.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| Error::usage(format!("input {input:?} has no usable file stem")))?;
let output = store_dir.join(format!("{stem}.voldoc"));
let mut store = EmbeddedStore::open(store_dir)?;
let resolver = store.clone();
externalize(&mut descriptor, &resolver, &mut store)?;
let (bytes, _cost) = descriptor.serialize()?;
write_atomic(&output, &bytes)?;
let stats = store.stats()?;
println!(
concat!(
"{{",
"\"ok\":true,",
"\"output\":\"{}\",",
"\"objects\":{},",
"\"stored_objects\":{},",
"\"stored_bytes\":{},",
"\"root_bytes\":{}",
"}}"
),
output.display(),
descriptor.objects.len(),
stats.object_count,
stats.stored_bytes,
bytes.len()
);
Ok(())
}
#[cfg(feature = "store")]
fn cmd_store_account(store_dir: &Path, roots: &[PathBuf], limits: Limits) -> Result<()> {
let store = EmbeddedStore::open(store_dir)?;
let mut descriptors = Vec::with_capacity(roots.len());
for path in roots {
let encoded = fs::read(path)?;
descriptors.push(Descriptor::parse(&encoded, limits)?.descriptor);
}
let report = account(&descriptors, &store)?;
let stats = store.stats()?;
let per_root: Vec<String> = report
.roots
.iter()
.zip(roots)
.map(|(r, path)| {
format!(
concat!(
"{{",
"\"root\":\"{}\",",
"\"root_bytes\":{},",
"\"standalone_bytes\":{},",
"\"reachable_objects\":{},",
"\"amortized_bytes\":{}",
"}}"
),
path.display(),
r.root_bytes,
r.standalone_bytes,
r.reachable_objects,
r.amortized_bytes
)
})
.collect();
println!(
concat!(
"{{",
"\"ok\":true,",
"\"roots\":{},",
"\"standalone_bytes\":{},",
"\"unique_reachable_bytes\":{},",
"\"amortized_bytes\":{},",
"\"unique_objects\":{},",
"\"unique_object_bytes\":{},",
"\"stored_bytes\":{},",
"\"dangling\":{},",
"\"per_root\":[{}]",
"}}"
),
report.roots.len(),
report.standalone_bytes,
report.unique_reachable_bytes,
report.amortized_bytes,
report.unique_objects,
report.unique_object_bytes,
stats.stored_bytes,
report.dangling.len(),
per_root.join(","),
);
Ok(())
}
#[cfg(feature = "store")]
fn cmd_store_gc(store_dir: &Path, roots: &[PathBuf], limits: Limits) -> Result<()> {
let store = EmbeddedStore::open(store_dir)?;
let mut descriptors = Vec::with_capacity(roots.len());
for path in roots {
let encoded = fs::read(path)?;
descriptors.push(Descriptor::parse(&encoded, limits)?.descriptor);
}
for (path, d) in roots.iter().zip(&descriptors) {
if !d
.objects
.iter()
.any(|o| matches!(o, vole_document::container::ObjectSource::External { .. }))
{
return Err(Error::usage(format!(
"ROOT {} is not store-backed (no EXTERNAL_REF); refusing to GC",
path.display()
)));
}
}
let report = gc(&descriptors, &store)?;
let dangling: Vec<String> = report
.dangling
.iter()
.map(|id| format!("\"{}\"", id.to_hex()))
.collect();
println!(
concat!(
"{{",
"\"ok\":true,",
"\"reachable\":{},",
"\"swept\":{},",
"\"bytes_reclaimed\":{},",
"\"dangling\":{},",
"\"dangling_ids\":[{}]",
"}}"
),
report.reachable,
report.swept,
report.bytes_reclaimed,
report.dangling.len(),
dangling.join(",")
);
Ok(())
}
fn cmd_inspect(input: &Path, limits: Limits) -> Result<()> {
let encoded = fs::read(input)?;
let parsed = vole_document::container::Descriptor::parse(&encoded, limits)?;
let d = &parsed.descriptor;
let ops: Vec<String> = d.program.ops.iter().map(describe_op).collect();
let ops_json = ops
.iter()
.map(|s| format!("\"{s}\""))
.collect::<Vec<_>>()
.join(",");
let object_lens: Vec<String> = d.objects.iter().map(|o| o.len().to_string()).collect();
println!(
concat!(
"{{",
"\"universe\":\"{}\",",
"\"universe_id\":\"{}\",",
"\"format_version\":\"{}.{}\",",
"\"exactness_profile\":\"EXACT_BYTES\",",
"\"source_format\":{},",
"\"format_basis\":\"{}\",",
"\"source_len\":{},",
"\"source_sha256\":\"{}\",",
"\"object_count\":{},",
"\"object_lens\":[{}],",
"\"graph_ops\":{},",
"\"program\":[{}],",
"\"encoded_len\":{},",
"\"cost\":{}",
"}}"
),
d.universe,
integrity::to_hex(&parsed.universe_id),
vole_document::container::header::FORMAT_MAJOR,
vole_document::container::header::FORMAT_MINOR,
d.source_format,
d.format_basis,
d.source_len,
integrity::to_hex(&d.source_sha256),
d.objects.len(),
object_lens.join(","),
d.program.ops.len(),
ops_json,
encoded.len(),
parsed.cost.to_json(),
);
Ok(())
}
fn cmd_pdf_inspect(input: &Path, limits: Limits) -> Result<()> {
let bytes = fs::read(input)?;
let is_pdf = pdf::detect(&bytes, limits);
let physical = pdf::scan(&bytes, limits)?;
let objects: Vec<String> = physical
.objects
.iter()
.map(|o| {
format!(
"{{\"number\":{},\"generation\":{},\"role\":\"{}\"}}",
o.number,
o.generation,
role_name(o.role)
)
})
.collect();
let startxref: Vec<String> = physical.startxref.iter().map(u64::to_string).collect();
println!(
concat!(
"{{",
"\"file\":\"{}\",",
"\"is_pdf\":{},",
"\"span_count\":{},",
"\"object_count\":{},",
"\"objects\":[{}],",
"\"revision_count\":{},",
"\"startxref\":[{}],",
"\"eof_count\":{}",
"}}"
),
json_escape(&input.display().to_string()),
is_pdf,
physical.spans.len(),
physical.objects.len(),
objects.join(","),
physical.revisions.len(),
startxref.join(","),
physical.eofs.len(),
);
Ok(())
}
fn cmd_pdf_make_samples(dir: &Path) -> Result<()> {
fs::create_dir_all(dir)?;
let samples = pdf::samples::sample_pdfs();
let mut names: Vec<String> = Vec::with_capacity(samples.len());
for (name, bytes) in &samples {
write_atomic(&dir.join(name), bytes)?;
names.push(format!("\"{}\"", json_escape(name)));
}
println!(
"{{\"ok\":true,\"dir\":\"{}\",\"count\":{},\"samples\":[{}]}}",
json_escape(&dir.display().to_string()),
names.len(),
names.join(",")
);
Ok(())
}
const LARGE_DEFAULT_OBJECTS: u64 = 800;
const LARGE_TARGET_BYTES: u64 = 32 * 1024 * 1024;
const LARGE_MAX_OBJECTS: u64 = 100_000;
fn cmd_pdf_make_large(dir: &Path, objects_arg: Option<&str>) -> Result<()> {
let objects: u64 = match objects_arg {
Some(s) => s
.parse()
.map_err(|_| Error::usage(format!("OBJECTS {s:?} is not a non-negative integer")))?,
None => LARGE_DEFAULT_OBJECTS,
};
if objects == 0 || objects > LARGE_MAX_OBJECTS {
return Err(Error::usage(format!(
"OBJECTS must be between 1 and {LARGE_MAX_OBJECTS}"
)));
}
let bytes = pdf::large_pdf(objects, LARGE_TARGET_BYTES);
fs::create_dir_all(dir)?;
let name = "large.pdf";
write_atomic(&dir.join(name), &bytes)?;
let indirect_objects = 2 * objects + 3;
println!(
"{{\"ok\":true,\"dir\":\"{}\",\"file\":\"{}\",\"objects\":{},\"streams\":{},\"indirect_objects\":{},\"source_len\":{},\"sha256\":\"{}\"}}",
json_escape(&dir.display().to_string()),
name,
objects,
objects,
indirect_objects,
bytes.len(),
integrity::to_hex(&integrity::sha256(&bytes)),
);
Ok(())
}
#[cfg(feature = "deflate-replay")]
fn cmd_deflate_stats(inputs: &[PathBuf], limits: Limits) -> Result<()> {
for input in inputs {
let bytes = fs::read(input)?;
let stats = pdf::deflate_stats(&bytes, limits)?;
println!("{}", deflate_stats_json(input, &stats));
}
Ok(())
}
#[cfg(feature = "deflate-replay")]
fn deflate_stats_json(input: &Path, stats: &pdf::DeflateStats) -> String {
let streams: Vec<String> = stats.streams.iter().map(stream_stats_json).collect();
let s = &stats.summary;
let replayed_full = s.plaintext_bytes.saturating_add(s.correction_bytes);
let replayed_rans_full = s.rans_plaintext_bytes.saturating_add(s.correction_bytes);
let replayed_rans_dedup = s.replayed_rans_dedup_bytes;
format!(
concat!(
"{{",
"\"file\":\"{}\",",
"\"is_pdf\":{},",
"\"streams\":[{}],",
"\"summary\":{{",
"\"flate_streams\":{},",
"\"replayed\":{},",
"\"declined\":{},",
"\"compressed_bytes\":{},",
"\"plaintext_bytes\":{},",
"\"correction_bytes\":{},",
"\"rans_plaintext_bytes\":{},",
"\"correction_over_compressed\":{},",
"\"replayed_full_bytes\":{},",
"\"replayed_rans_full_bytes\":{},",
"\"replayed_rans_dedup_bytes\":{}",
"}}",
"}}"
),
json_escape(&input.display().to_string()),
stats.is_pdf,
streams.join(","),
s.flate_streams,
s.replayed,
s.declined,
s.compressed_bytes,
s.plaintext_bytes,
s.correction_bytes,
s.rans_plaintext_bytes,
ratio6(s.correction_bytes, s.compressed_bytes),
replayed_full,
replayed_rans_full,
replayed_rans_dedup,
)
}
#[cfg(feature = "deflate-replay")]
fn stream_stats_json(s: &pdf::StreamStats) -> String {
let reason = match s.decline_reason {
Some(r) => format!("\"{r}\""),
None => "null".to_string(),
};
let raw_ratio = match s.correction_bytes {
Some(c) => ratio6(c, s.compressed_bytes),
None => "null".to_string(),
};
let plain_ratio = match (s.plaintext_bytes, s.correction_bytes) {
(Some(p), Some(c)) => ratio6(p.saturating_add(c), s.compressed_bytes),
_ => "null".to_string(),
};
let rans_ratio = match (s.rans_plaintext_bytes, s.correction_bytes) {
(Some(r), Some(c)) => ratio6(r.saturating_add(c), s.compressed_bytes),
_ => "null".to_string(),
};
format!(
concat!(
"{{",
"\"object\":{},",
"\"generation\":{},",
"\"compressed_bytes\":{},",
"\"replayed\":{},",
"\"decline_reason\":{},",
"\"plaintext_bytes\":{},",
"\"correction_bytes\":{},",
"\"rans_plaintext_bytes\":{},",
"\"raw_ratio\":{},",
"\"plain_ratio\":{},",
"\"rans_ratio\":{}",
"}}"
),
s.object,
s.generation,
s.compressed_bytes,
s.replayed,
reason,
opt_u64(s.plaintext_bytes),
opt_u64(s.correction_bytes),
opt_u64(s.rans_plaintext_bytes),
raw_ratio,
plain_ratio,
rans_ratio,
)
}
#[cfg(feature = "deflate-replay")]
fn opt_u64(v: Option<u64>) -> String {
match v {
Some(n) => n.to_string(),
None => "null".to_string(),
}
}
#[cfg(feature = "deflate-replay")]
fn ratio6(num: u64, den: u64) -> String {
if den == 0 {
return "0.000000".to_string();
}
let scaled = u128::from(num) * 1_000_000;
let q = scaled / u128::from(den);
format!("{}.{:06}", q / 1_000_000, q % 1_000_000)
}
fn role_name(role: pdf::ObjRole) -> &'static str {
match role {
pdf::ObjRole::Generic => "Generic",
pdf::ObjRole::XRefStream => "XRefStream",
pdf::ObjRole::ObjectStream => "ObjectStream",
}
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
fn cmd_capabilities() -> Result<()> {
println!(
concat!(
"{{",
"\"crate\":\"vole-document\",",
"\"format_major\":{},",
"\"format_minor\":{},",
"\"dra_version\":{},",
"\"exactness_profiles\":[\"EXACT_BYTES\"],",
"\"source_formats\":[\"OPAQUE\",\"PDF\"],",
"\"universe\":\"{}\",",
"\"dra_ops\":[\"EMIT_OBJECT\",\"INLINE\",\"REPEAT_LAST\",\"DECODE_CHANNEL\",\"INTERLEAVE_CHANNELS\",\"MARK_OFFSET\",\"EMIT_OFFSET\",\"PACK_SEGMENTS\",\"PACKED_CHANNELS\",\"DEFLATE_REPLAY\"],",
"\"entropy_channels\":[\"ORDER0_BYTE_RANS\"],",
"\"features\":{{\"rans\":{},\"deflate_replay\":{}}}",
"}}"
),
vole_document::container::header::FORMAT_MAJOR,
vole_document::container::header::FORMAT_MINOR,
vole_document::dra::program::DRA_VERSION,
UNIVERSE,
cfg!(feature = "rans"),
cfg!(feature = "deflate-replay"),
);
Ok(())
}
fn describe_op(op: &Op) -> String {
match op {
Op::EmitObject { object_id } => format!("EMIT_OBJECT({object_id})"),
Op::Inline { bytes } => format!("INLINE({})", bytes.len()),
Op::RepeatLast { count } => format!("REPEAT_LAST({count})"),
Op::DecodeChannel { channel_id } => format!("DECODE_CHANNEL({channel_id})"),
Op::InterleaveChannels {
first_payload_channel,
payload_channel_count,
..
} => format!("INTERLEAVE_CHANNELS({first_payload_channel},{payload_channel_count})"),
Op::MarkOffset { slot } => format!("MARK_OFFSET({slot})"),
Op::EmitOffset { slot, width } => format!("EMIT_OFFSET({slot},{width})"),
Op::PackSegments { data_object, items } => {
format!("PACK_SEGMENTS({data_object},{})", items.len())
}
Op::PackedChannels {
data_channel,
plan_channel,
declared_output_len,
} => format!("PACKED_CHANNELS({data_channel},{plan_channel},{declared_output_len})"),
Op::DeflateReplay {
replay_codec,
source_kind,
source_id,
corrections_object,
declared_output_len,
} => format!(
"DEFLATE_REPLAY({replay_codec},{source_kind},{source_id},{corrections_object},{declared_output_len})"
),
}
}
fn report_json(r: &encode::EncodeReport) -> String {
format!(
concat!(
"{{",
"\"ok\":true,",
"\"candidate\":\"{}\",",
"\"candidates_evaluated\":{},",
"\"source_len\":{},",
"\"encoded_len\":{},",
"\"ratio\":{:.6},",
"\"sha256\":\"{}\",",
"\"graph_ops\":{},",
"\"cost\":{}",
"}}"
),
r.kind.name(),
r.candidates_evaluated,
r.source_len,
r.encoded_len,
r.compression_ratio(),
r.sha256_hex,
r.graph_ops,
r.cost.to_json(),
)
}
fn write_atomic(path: &Path, bytes: &[u8]) -> Result<()> {
let dir = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
};
let name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "out".to_string());
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let tmp = dir.join(format!(".{name}.voldoc-tmp-{}-{nanos}", std::process::id()));
{
let mut f = fs::File::create(&tmp)?;
f.write_all(bytes)?;
f.sync_all()?;
}
if let Err(e) = fs::rename(&tmp, path) {
let _ = fs::remove_file(&tmp);
return Err(Error::from(e));
}
if let Ok(d) = fs::File::open(&dir) {
let _ = d.sync_all();
}
Ok(())
}