use std::path::PathBuf;
use prikk_object::ObjectId;
use prikk_store::{
AcceptOptions, ClaimSignatureVerification, DEFAULT_EXCHANGE_ARTIFACT_MAX_OBJECT_COUNT,
DEFAULT_EXCHANGE_ARTIFACT_MAX_TOTAL_BYTES, DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT,
DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES, ObjectReadSnapshot, ReceivedTagResolution,
SealFromAcceptedOutcome, SyncArtifactOutcome, TagSignatureVerification,
accept_exchange_artifact, accepted_but_unsealed_patch_ids, adopt_tag, build_have_list,
build_sync_artifact, build_sync_summary, compare_sync_summary, decode_sync_summary,
list_received_tags, order_claims_for_sealing, seal_from_accepted_claim,
};
use crate::maintainer_signer_from_env;
pub fn run_sync(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut iter = args.into_iter();
match iter.next().as_deref() {
Some("summary") => run_summary(root, iter.collect()),
Some("compare") => run_compare(root, iter.collect()),
Some("have") => run_have(root, iter.collect()),
Some("build") => run_build(root, iter.collect()),
Some("accept") => run_accept(root, iter.collect()),
Some("pending") => run_pending(root, iter.collect()),
Some("seal") => run_seal(root, iter.collect()),
Some("tags") => run_tags(root, iter.collect()),
Some("adopt-tag") => run_adopt_tag(root, iter.collect()),
Some(other) => Err(format!(
"unknown sync subcommand: {other} (expected summary, compare, have, build, accept, \
pending, seal, tags, or adopt-tag)"
)),
None => Err(
"sync requires a subcommand: summary, compare, have, build, accept, pending, seal, \
tags, or adopt-tag"
.to_string(),
),
}
}
fn run_summary(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_output_args(args, "sync summary")?;
let layout = crate::open_repository(root)?;
let bytes = build_sync_summary(&layout).map_err(|err| err.to_string())?;
let entries = decode_sync_summary(
&bytes,
DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES,
DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT,
)
.map_err(|err| err.to_string())?;
std::fs::write(&parsed.output, &bytes).map_err(|err| {
format!(
"failed to write sync summary to {}: {err}",
parsed.output.display()
)
})?;
println!("refs: {}", entries.len());
println!("wrote {}", parsed.output.display());
Ok(())
}
fn run_compare(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_summary_input_args(args, "sync compare")?;
let layout = crate::open_repository(root)?;
let bytes = std::fs::read(&parsed.summary).map_err(|err| {
format!(
"failed to read sync summary from {}: {err}",
parsed.summary.display()
)
})?;
let (max_total_bytes, max_ref_count) = sync_summary_limits_from_env()?;
let remote = decode_sync_summary(&bytes, max_total_bytes, max_ref_count)
.map_err(|err| err.to_string())?;
let comparisons = compare_sync_summary(&layout, &remote).map_err(|err| err.to_string())?;
for comparison in &comparisons {
println!("{} {}", comparison.ref_name, comparison.state.as_str());
}
println!("refs compared: {}", comparisons.len());
Ok(())
}
fn run_have(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_ref_and_output_args(args, "sync have")?;
let layout = crate::open_repository(root)?;
let bytes = build_have_list(&layout, &parsed.ref_name).map_err(|err| err.to_string())?;
std::fs::write(&parsed.output, &bytes).map_err(|err| {
format!(
"failed to write have-list to {}: {err}",
parsed.output.display()
)
})?;
println!("have-list for {}", parsed.ref_name);
println!("wrote {}", parsed.output.display());
Ok(())
}
fn run_build(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_build_args(args)?;
let layout = crate::open_repository(root)?;
let have_list_bytes = std::fs::read(&parsed.have).map_err(|err| {
format!(
"failed to read have-list from {}: {err}",
parsed.have.display()
)
})?;
let signer = maintainer_signer_from_env()?;
let outcome = build_sync_artifact(&layout, &parsed.ref_name, &have_list_bytes, &signer)
.map_err(|err| err.to_string())?;
match outcome {
SyncArtifactOutcome::AlreadyInSync { ref_name } => {
println!("{ref_name} is already in sync -- no artifact written");
}
SyncArtifactOutcome::Artifact { report, bytes } => {
std::fs::write(&parsed.output, &bytes).map_err(|err| {
format!(
"failed to write sync artifact to {}: {err}",
parsed.output.display()
)
})?;
println!("built sync artifact for {}", report.ref_name);
println!("delta patches: {}", report.delta_patch_count);
println!("claims: {}", report.claim_count);
println!("tags: {}", report.tag_count);
println!(
"blobs: {} | author key material: {}",
report.export_report.blob_count, report.export_report.author_key_count
);
println!("wrote {}", parsed.output.display());
println!(
"note: this artifact contains repository content in the clear -- prikk does not \
encrypt it; move it only over a channel you trust"
);
}
}
Ok(())
}
fn run_accept(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_accept_args(args)?;
let layout = crate::open_repository(root)?;
let bytes = std::fs::read(&parsed.input).map_err(|err| {
format!(
"failed to read sync artifact from {}: {err}",
parsed.input.display()
)
})?;
let options = accept_options_from_env()?;
let report =
accept_exchange_artifact(&layout, &bytes, &options).map_err(|err| err.to_string())?;
println!("accepted sync artifact");
println!("patches: {}", report.patch_count);
println!("blobs: {}", report.blob_count);
println!("claims: {}", report.claim_count);
println!("tags: {}", report.tag_count);
println!("new objects: {}", report.written_object_count);
println!(
"author key material: {} recorded (continuity only, not a trust decision)",
report.recorded_author_key_count
);
for (claim_id, outcome) in &report.claim_signature_outcomes {
match outcome {
ClaimSignatureVerification::Sound { key_id } => {
println!(" claim {claim_id}: Sound ({key_id})");
}
ClaimSignatureVerification::Unverifiable { key_id } => {
println!(
" claim {claim_id}: Unverifiable ({key_id} not adopted here -- its order is \
unattributed)"
);
}
}
}
for (tag_id, outcome) in &report.tag_signature_outcomes {
match outcome {
TagSignatureVerification::Sound { key_id } => {
println!(" tag {tag_id}: Sound ({key_id})");
}
TagSignatureVerification::Unverifiable { key_id } => {
println!(" tag {tag_id}: Unverifiable ({key_id} not adopted here)");
}
}
}
if let Some(claims_out) = &parsed.claims_out {
let mut content = String::new();
for (claim_id, _) in &report.claim_signature_outcomes {
content.push_str(&claim_id.to_string());
content.push('\n');
}
std::fs::write(claims_out, content).map_err(|err| {
format!(
"failed to write claim ids to {}: {err}",
claims_out.display()
)
})?;
println!("wrote claim ids to {}", claims_out.display());
}
Ok(())
}
fn run_pending(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
require_no_args(args, "sync pending")?;
let layout = crate::open_repository(root)?;
let patch_ids = accepted_but_unsealed_patch_ids(&layout).map_err(|err| err.to_string())?;
println!("pending (accepted, unsealed) patches: {}", patch_ids.len());
for patch_id in &patch_ids {
println!(" {patch_id}");
}
Ok(())
}
fn run_tags(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
require_no_args(args, "sync tags")?;
let layout = crate::open_repository(root)?;
let summaries = list_received_tags(&layout).map_err(|err| err.to_string())?;
if summaries.is_empty() {
println!("no received tags");
return Ok(());
}
println!("received tags: {}", summaries.len());
for summary in &summaries {
println!("{} ({})", summary.name, summary.tag_id);
match &summary.signature_outcome {
TagSignatureVerification::Sound { key_id } => {
println!(" signature: Sound ({key_id})");
}
TagSignatureVerification::Unverifiable { key_id } => {
println!(" signature: Unverifiable ({key_id} not adopted here)");
}
}
match &summary.resolution {
ReceivedTagResolution::Resolved(block_id) => {
println!(" resolution: Resolved {block_id}");
}
ReceivedTagResolution::NotHeld => {
println!(
" resolution: NotHeld (not enough of this repository's history has been \
synced yet)"
);
}
ReceivedTagResolution::Ambiguous { detail } => {
println!(" resolution: Ambiguous -- {detail}");
}
}
}
Ok(())
}
fn run_adopt_tag(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let mut iter = args.into_iter();
let Some(name) = iter.next() else {
return Err("sync adopt-tag requires <name>".to_string());
};
if name.starts_with("--") {
return Err("sync adopt-tag requires <name> before any flags".to_string());
}
require_no_args(iter.collect(), "sync adopt-tag")?;
let layout = crate::open_repository(root)?;
let signer = maintainer_signer_from_env()?;
let created = adopt_tag(&layout, &name, &signer).map_err(|err| err.to_string())?;
println!("adopted tag {name}");
println!("tag object: {}", created.tag_object_id);
println!("RefState: {}", created.ref_state_id);
Ok(())
}
fn run_seal(root: PathBuf, args: Vec<String>) -> std::result::Result<(), String> {
let parsed = parse_seal_args(args)?;
let layout = crate::open_repository(root)?;
let signer = maintainer_signer_from_env()?;
match parsed.claims {
SealClaims::Single(claim_id) => {
let outcome = seal_from_accepted_claim(&layout, &parsed.ref_name, claim_id, &signer)
.map_err(|err| err.to_string())?;
print_seal_outcome(&outcome, claim_id);
}
SealClaims::Batch(claims_file) => {
let content = std::fs::read_to_string(&claims_file).map_err(|err| {
format!(
"failed to read claim ids from {}: {err}",
claims_file.display()
)
})?;
let claim_ids: Vec<ObjectId> = content
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| {
line.parse::<ObjectId>().map_err(|err| {
format!(
"{line:?} in {} is not a valid object id: {err}",
claims_file.display()
)
})
})
.collect::<std::result::Result<_, _>>()?;
let object_store = ObjectReadSnapshot::open(&layout).map_err(|err| err.to_string())?;
let ordered = order_claims_for_sealing(&object_store, &claim_ids)
.map_err(|err| err.to_string())?;
for claim_id in ordered {
let outcome =
seal_from_accepted_claim(&layout, &parsed.ref_name, claim_id, &signer)
.map_err(|err| {
println!("claim {claim_id}: FAILED: {err}");
let _ = std::io::Write::flush(&mut std::io::stdout());
err.to_string()
})?;
print_seal_outcome(&outcome, claim_id);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
}
}
Ok(())
}
fn print_seal_outcome(outcome: &SealFromAcceptedOutcome, claim_id: ObjectId) {
match outcome {
SealFromAcceptedOutcome::Sealed {
ref_name,
block_id,
ref_state_id,
patch_count,
claim_signature_outcome,
} => {
println!("claim {claim_id}: sealed {patch_count} patch(es) into {ref_name}");
println!(" block id: {block_id}");
println!(" {ref_name} RefState: {ref_state_id}");
match claim_signature_outcome {
ClaimSignatureVerification::Sound { key_id } => {
println!(" claim signature: Sound ({key_id})");
}
ClaimSignatureVerification::Unverifiable { key_id } => {
println!(" claim signature: Unverifiable ({key_id} not adopted here)");
}
}
}
SealFromAcceptedOutcome::AlreadySealed { ref_name, .. } => {
println!("claim {claim_id}: {ref_name} was already fully sealed -- no-op");
}
}
}
fn accept_options_from_env() -> std::result::Result<AcceptOptions, String> {
let max_object_count = parse_limit_env(
"PRIKK_EXCHANGE_MAX_OBJECTS",
DEFAULT_EXCHANGE_ARTIFACT_MAX_OBJECT_COUNT,
)?;
let max_total_bytes = parse_limit_env(
"PRIKK_EXCHANGE_MAX_BYTES",
DEFAULT_EXCHANGE_ARTIFACT_MAX_TOTAL_BYTES,
)?;
Ok(AcceptOptions::default_limits()
.with_max_object_count(max_object_count)
.with_max_total_bytes(max_total_bytes))
}
fn sync_summary_limits_from_env() -> std::result::Result<(usize, usize), String> {
let max_total_bytes = parse_limit_env(
"PRIKK_SYNC_SUMMARY_MAX_BYTES",
DEFAULT_SYNC_SUMMARY_MAX_TOTAL_BYTES,
)?;
let max_ref_count = parse_limit_env(
"PRIKK_SYNC_SUMMARY_MAX_REFS",
DEFAULT_SYNC_SUMMARY_MAX_REF_COUNT,
)?;
Ok((max_total_bytes, max_ref_count))
}
fn parse_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 OutputArgs {
output: PathBuf,
}
fn parse_output_args(args: Vec<String>, command: &str) -> std::result::Result<OutputArgs, String> {
let mut output = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--output" => {
let Some(value) = iter.next() else {
return Err(format!("{command} --output requires a value"));
};
output = Some(PathBuf::from(value));
}
other => return Err(format!("unknown {command} argument: {other}")),
}
}
let output = output.ok_or_else(|| format!("{command} requires --output"))?;
Ok(OutputArgs { output })
}
struct SummaryInputArgs {
summary: PathBuf,
}
fn parse_summary_input_args(
args: Vec<String>,
command: &str,
) -> std::result::Result<SummaryInputArgs, String> {
let mut summary = None;
let mut iter = args.into_iter();
while let Some(arg) = iter.next() {
match arg.as_str() {
"--summary" => {
let Some(value) = iter.next() else {
return Err(format!("{command} --summary requires a value"));
};
summary = Some(PathBuf::from(value));
}
other => return Err(format!("unknown {command} argument: {other}")),
}
}
let summary = summary.ok_or_else(|| format!("{command} requires --summary"))?;
Ok(SummaryInputArgs { summary })
}
struct AcceptArgs {
input: PathBuf,
claims_out: Option<PathBuf>,
}
fn parse_accept_args(args: Vec<String>) -> std::result::Result<AcceptArgs, String> {
let mut iter = args.into_iter();
let Some(input) = iter.next() else {
return Err("sync accept requires a file path".to_string());
};
if input.starts_with("--") {
return Err("sync accept requires a file path before any flags".to_string());
}
let mut claims_out = None;
while let Some(arg) = iter.next() {
match arg.as_str() {
"--claims-out" => {
let Some(value) = iter.next() else {
return Err("sync accept --claims-out requires a value".to_string());
};
claims_out = Some(PathBuf::from(value));
}
other => return Err(format!("unknown sync accept argument: {other}")),
}
}
Ok(AcceptArgs {
input: PathBuf::from(input),
claims_out,
})
}
fn require_no_args(args: Vec<String>, command: &str) -> std::result::Result<(), String> {
if let Some(extra) = args.into_iter().next() {
return Err(format!("unknown {command} argument: {extra}"));
}
Ok(())
}
struct RefAndOutputArgs {
ref_name: String,
output: PathBuf,
}
fn parse_ref_and_output_args(
args: Vec<String>,
command: &str,
) -> std::result::Result<RefAndOutputArgs, String> {
let mut iter = args.into_iter();
let Some(ref_name) = iter.next() else {
return Err(format!("{command} requires a ref name"));
};
if ref_name.starts_with("--") {
return Err(format!("{command} requires a ref name before any flags"));
}
let mut output = None;
while let Some(arg) = iter.next() {
match arg.as_str() {
"--output" => {
let Some(value) = iter.next() else {
return Err(format!("{command} --output requires a value"));
};
output = Some(PathBuf::from(value));
}
other => return Err(format!("unknown {command} argument: {other}")),
}
}
let output = output.ok_or_else(|| format!("{command} requires --output"))?;
Ok(RefAndOutputArgs { ref_name, output })
}
struct BuildArgs {
ref_name: String,
have: PathBuf,
output: PathBuf,
}
fn parse_build_args(args: Vec<String>) -> std::result::Result<BuildArgs, String> {
let mut iter = args.into_iter();
let Some(ref_name) = iter.next() else {
return Err("sync build requires a ref name".to_string());
};
if ref_name.starts_with("--") {
return Err("sync build requires a ref name before any flags".to_string());
}
let mut have = None;
let mut output = None;
while let Some(arg) = iter.next() {
match arg.as_str() {
"--have" => {
let Some(value) = iter.next() else {
return Err("sync build --have requires a value".to_string());
};
have = Some(PathBuf::from(value));
}
"--output" => {
let Some(value) = iter.next() else {
return Err("sync build --output requires a value".to_string());
};
output = Some(PathBuf::from(value));
}
other => return Err(format!("unknown sync build argument: {other}")),
}
}
let have = have.ok_or_else(|| "sync build requires --have".to_string())?;
let output = output.ok_or_else(|| "sync build requires --output".to_string())?;
Ok(BuildArgs {
ref_name,
have,
output,
})
}
enum SealClaims {
Single(ObjectId),
Batch(PathBuf),
}
struct SealArgs {
ref_name: String,
claims: SealClaims,
}
fn parse_seal_args(args: Vec<String>) -> std::result::Result<SealArgs, String> {
let mut iter = args.into_iter();
let Some(ref_name) = iter.next() else {
return Err("sync seal requires a ref name".to_string());
};
if ref_name.starts_with("--") {
return Err("sync seal requires a ref name before any flags".to_string());
}
let mut claim_id = None;
let mut claims_file = None;
while let Some(arg) = iter.next() {
match arg.as_str() {
"--claim" => {
let Some(value) = iter.next() else {
return Err("sync seal --claim requires a value".to_string());
};
claim_id =
Some(value.parse::<ObjectId>().map_err(|err| {
format!("--claim {value:?} is not a valid object id: {err}")
})?);
}
"--claims" => {
let Some(value) = iter.next() else {
return Err("sync seal --claims requires a value".to_string());
};
claims_file = Some(PathBuf::from(value));
}
other => return Err(format!("unknown sync seal argument: {other}")),
}
}
let claims = match (claim_id, claims_file) {
(Some(_), Some(_)) => {
return Err("sync seal accepts either --claim or --claims, not both".to_string());
}
(Some(claim_id), None) => SealClaims::Single(claim_id),
(None, Some(claims_file)) => SealClaims::Batch(claims_file),
(None, None) => {
return Err("sync seal requires --claim <id> or --claims <file>".to_string());
}
};
Ok(SealArgs { ref_name, claims })
}