use std::path::PathBuf;
use crate::arg_scan::{SetOnce, flag_value, mark_seen, unknown_argument};
use crate::commands::CliError;
use crate::stdout::println;
use prikk_store::{
BundleImportOptions, BundleManifest, BundleScope, DEFAULT_BUNDLE_MAX_OBJECT_COUNT,
DEFAULT_BUNDLE_MAX_TOTAL_BYTES, export_bundle, import_bundle, verify_bundle,
};
pub fn run_bundle(root: PathBuf, args: Vec<String>) -> std::result::Result<(), CliError> {
let mut iter = args.into_iter();
match iter.next().as_deref() {
Some("export") => run_export(root, iter.collect()),
Some("import") => run_import(root, iter.collect()),
Some("verify") => run_verify(iter.collect()),
Some(other) => Err(CliError::Usage(format!(
"unknown bundle subcommand: {other} (expected export, import, or verify)"
))),
None => Err(CliError::Usage(
"bundle requires a subcommand: export, import, or verify".to_string(),
)),
}
}
fn run_export(root: PathBuf, args: Vec<String>) -> std::result::Result<(), CliError> {
let parsed = parse_export_args(args)?;
if !parsed.force && crate::durable_output::destination_exists(&parsed.output) {
return Err(format!(
"refusing to overwrite existing file at {} (pass --force to overwrite it \
intentionally)",
parsed.output.display()
)
.into());
}
let layout = crate::open_repository(root)?;
let (report, bytes) =
export_bundle(&layout, &parsed.ref_name).map_err(|err| err.to_string())?;
crate::durable_output::write_new_file_durably(&parsed.output, &bytes)?;
println!("exported {}", report.ref_name);
println!("tip block: {}", report.tip_block_id);
println!("objects: {}", report.object_count);
println!(
"author key material: {} included (continuity only, not a trust decision)",
report.author_key_count
);
print_manifest(&report.manifest);
println!("wrote {}", parsed.output.display());
Ok(())
}
fn run_import(root: PathBuf, args: Vec<String>) -> std::result::Result<(), CliError> {
let parsed = parse_import_args(args)?;
let layout = crate::open_repository(root)?;
let bytes = std::fs::read(&parsed.input).map_err(|err| {
format!(
"failed to read bundle from {}: {err}",
parsed.input.display()
)
})?;
let options = bundle_import_options_from_env()?;
let report = import_bundle(&layout, &bytes, &options).map_err(|err| err.to_string())?;
println!("received {}", report.ref_name);
println!("RefState: {}", report.ref_state_id);
println!("objects: {}", report.object_count);
println!("new objects: {}", report.written_object_count);
println!(
"author key material: {} recorded (continuity only, not a trust decision)",
report.recorded_author_key_count
);
println!(
"note: no local ref was created or advanced, and no MAINTAINER key was trusted; run \
`trust maintainer add` to trust the sealing key, then `merge` to incorporate this history"
);
Ok(())
}
fn run_verify(args: Vec<String>) -> std::result::Result<(), CliError> {
let parsed = parse_verify_args(args)?;
let bytes = std::fs::read(&parsed.input).map_err(|err| {
format!(
"failed to read bundle from {}: {err}",
parsed.input.display()
)
})?;
let options = bundle_import_options_from_env()?;
let report = verify_bundle(&bytes, &options).map_err(|err| err.to_string())?;
println!("bundle verifies: {}", report.ref_name);
println!("RefState: {}", report.ref_state_id);
println!("tip block: {}", report.tip_block_id);
println!("objects: {}", report.object_count);
println!(
"author key material: {} present (continuity only, not a trust decision)",
report.author_key_count
);
match &report.manifest {
Some(manifest) => print_manifest(manifest),
None => println!(
"manifest: not present (this bundle predates the PBNDL003 manifest section -- \
repository format, tool version, and scope are unknown)"
),
}
println!(
"note: this checks structural and internal consistency only -- no signature is \
cryptographically verified (a standalone bundle carries no trust material to check one \
against), and this bundle's own author-key section is recorded here, never \
independently verified, the same as at import. A verified bundle is not yet a trusted \
one -- import it and run `prikk verify` for that."
);
Ok(())
}
fn print_manifest(manifest: &BundleManifest) {
println!("repository format: {}", manifest.repository_format);
println!("tool version: {}", manifest.tool_version);
match manifest.scope {
BundleScope::SingleRef => println!(
"note: this bundle contains one ref's closure only -- other refs in the source \
repository, if any, are not included, and this bundle makes no claim about them"
),
}
}
struct VerifyArgs {
input: PathBuf,
}
fn parse_verify_args(args: Vec<String>) -> std::result::Result<VerifyArgs, CliError> {
let mut input = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--input" => {
let value = flag_value(&mut iter, "bundle verify --input")?;
input.set_once("--input", PathBuf::from(value))?;
}
other => return Err(unknown_argument("bundle verify", other)),
}
}
let input =
input.ok_or_else(|| CliError::Usage("bundle verify requires --input".to_string()))?;
Ok(VerifyArgs { input })
}
fn bundle_import_options_from_env() -> std::result::Result<BundleImportOptions, String> {
let max_object_count =
parse_bundle_limit_env("PRIKK_BUNDLE_MAX_OBJECTS", DEFAULT_BUNDLE_MAX_OBJECT_COUNT)?;
let max_total_bytes =
parse_bundle_limit_env("PRIKK_BUNDLE_MAX_BYTES", DEFAULT_BUNDLE_MAX_TOTAL_BYTES)?;
Ok(BundleImportOptions::default_limits()
.with_max_object_count(max_object_count)
.with_max_total_bytes(max_total_bytes))
}
fn parse_bundle_limit_env(name: &str, default: usize) -> std::result::Result<usize, String> {
let Ok(raw) = std::env::var(name) else {
return Ok(default);
};
let trimmed = raw.trim();
let value: usize = trimmed
.parse()
.map_err(|_| format!("{name} must be a positive integer, got {raw:?}"))?;
if value == 0 {
return Err(format!("{name} must be greater than zero, got 0"));
}
Ok(value)
}
struct ExportArgs {
ref_name: String,
output: PathBuf,
force: bool,
}
fn parse_export_args(args: Vec<String>) -> std::result::Result<ExportArgs, CliError> {
let mut ref_name = None;
let mut output = None;
let mut force = false;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--ref" => {
let value = flag_value(&mut iter, "bundle export --ref")?;
if value.trim().is_empty() {
return Err(CliError::Usage(
"bundle export --ref must not be empty".to_string(),
));
}
ref_name.set_once("--ref", value)?;
}
"--output" => {
let value = flag_value(&mut iter, "bundle export --output")?;
output.set_once("--output", PathBuf::from(value))?;
}
"--force" => mark_seen(&mut force, "--force")?,
other => return Err(unknown_argument("bundle export", other)),
}
}
let ref_name =
ref_name.ok_or_else(|| CliError::Usage("bundle export requires --ref".to_string()))?;
let output =
output.ok_or_else(|| CliError::Usage("bundle export requires --output".to_string()))?;
Ok(ExportArgs {
ref_name,
output,
force,
})
}
struct ImportArgs {
input: PathBuf,
}
fn parse_import_args(args: Vec<String>) -> std::result::Result<ImportArgs, CliError> {
let mut input = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--input" => {
let value = flag_value(&mut iter, "bundle import --input")?;
input.set_once("--input", PathBuf::from(value))?;
}
other => return Err(unknown_argument("bundle import", other)),
}
}
let input =
input.ok_or_else(|| CliError::Usage("bundle import requires --input".to_string()))?;
Ok(ImportArgs { input })
}