use std::io::Write;
use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::Args;
use serde::Deserialize;
use super::{detect, print_json, read_input, Artifact};
#[derive(Debug, Args)]
pub struct FidelityArgs {
pub file: PathBuf,
#[arg(long, value_name = "NAME")]
pub stream: Option<String>,
#[arg(short = 'o', long, value_name = "FILE", requires = "stream")]
pub output: Option<PathBuf>,
#[arg(long, requires = "output")]
pub force: bool,
#[arg(long, requires = "stream")]
pub binary_stdout: bool,
#[arg(long, conflicts_with = "stream")]
pub json: bool,
}
#[derive(Deserialize)]
struct SidecarFidelityProbe {
#[serde(default)]
fidelity: Option<FidelityPayload>,
}
#[derive(Deserialize, Default)]
struct FidelityPayload {
#[serde(default)]
annotations: AnnotationsProbe,
#[serde(default)]
retained_records: Vec<RecordProbe>,
}
#[derive(Deserialize, Default)]
struct AnnotationsProbe {
#[serde(default)]
streams: Vec<String>,
#[serde(default)]
provenance: serde_json::Map<String, serde_json::Value>,
#[serde(default)]
exactness: serde_json::Map<String, serde_json::Value>,
}
#[derive(Deserialize)]
struct RecordProbe {
#[serde(default)]
id: String,
#[serde(default)]
stream: String,
#[serde(default)]
offset: u64,
#[serde(default)]
byte_len: u64,
#[serde(default, with = "cadmpeg_ir::bytes::option")]
data: Option<Vec<u8>>,
}
pub fn run(args: &FidelityArgs) -> Result<()> {
let bytes = read_input(&args.file)?;
match detect(&bytes, &args.file)? {
Artifact::Sidecar(_) => {}
Artifact::Cadir(_) => bail!(
"{} is a CADIR document; the fidelity payload lives in the \
`<stem>.fidelity.json` decode sidecar `cadmpeg dump` writes \
next to it",
args.file.display()
),
Artifact::Report(_) => bail!(
"{} is a command report; the fidelity payload lives in the \
`<stem>.fidelity.json` decode sidecar `cadmpeg dump` writes",
args.file.display()
),
}
let probe: SidecarFidelityProbe = serde_json::from_slice(&bytes)
.with_context(|| format!("parsing the fidelity payload of {}", args.file.display()))?;
let payload = probe.fidelity.unwrap_or_default();
match &args.stream {
Some(stream) => extract(args, &payload, stream),
None if args.json => {
let records: Vec<serde_json::Value> = payload
.retained_records
.iter()
.map(|record| {
serde_json::json!({
"id": record.id,
"stream": record.stream,
"offset": record.offset,
"byte_len": record.byte_len,
"data_retained": record.data.is_some(),
})
})
.collect();
let value = serde_json::json!({
"annotations": {
"streams": payload.annotations.streams.len(),
"provenance": payload.annotations.provenance.len(),
"exactness": payload.annotations.exactness.len(),
},
"retained_records": records,
});
print_json("fidelity", &value);
Ok(())
}
None => {
println!("stream\toffset\tbytes\tdata\tid");
for record in &payload.retained_records {
println!(
"{}\t{}\t{}\t{}\t{}",
super::cell(&record.stream),
record.offset,
record.byte_len,
if record.data.is_some() { "yes" } else { "no" },
super::cell(&record.id),
);
}
if payload.retained_records.is_empty() {
eprintln!("(this sidecar retains no source records)");
}
eprintln!(
"annotations: {} streams, {} provenance entries, {} exactness \
notes; extract retained bytes with `cadmpeg query fidelity \
FILE --stream NAME -o OUT`",
payload.annotations.streams.len(),
payload.annotations.provenance.len(),
payload.annotations.exactness.len(),
);
Ok(())
}
}
}
fn extract(args: &FidelityArgs, payload: &FidelityPayload, stream: &str) -> Result<()> {
const SHOWN: usize = 20;
let mut matched: Vec<&RecordProbe> = payload
.retained_records
.iter()
.filter(|record| record.stream == stream)
.collect();
if matched.is_empty() {
if payload.retained_records.is_empty() {
bail!("this sidecar retains no source records");
}
let mut streams: Vec<&str> = payload
.retained_records
.iter()
.map(|record| record.stream.as_str())
.collect();
streams.sort_unstable();
streams.dedup();
let shown: Vec<&str> = streams.iter().take(SHOWN).copied().collect();
bail!(
"no retained record has stream {stream:?}; retained streams: {}{}",
shown.join(", "),
if streams.len() > SHOWN { ", …" } else { "" }
);
}
let missing: Vec<&str> = matched
.iter()
.filter(|record| record.data.is_none())
.map(|record| record.id.as_str())
.collect();
if !missing.is_empty() {
bail!(
"stream {stream:?} is retained without bytes (extent and digest \
only) for: {}",
missing.join(", ")
);
}
matched.sort_by_key(|record| record.offset);
let mut assembled: Vec<u8> = Vec::new();
let mut expected_offset: Option<u64> = None;
for record in &matched {
let data = record.data.as_ref().expect("missing data handled above");
if data.len() as u64 != record.byte_len {
bail!(
"retained source record {} declares {} bytes but contains {}",
record.id,
record.byte_len,
data.len()
);
}
if let Some(expected) = expected_offset {
if record.offset != expected {
let extents: Vec<String> = matched
.iter()
.map(|r| format!("{}+{} ({})", r.offset, r.byte_len, r.id))
.collect();
bail!(
"the retained extents of stream {stream:?} are not \
contiguous: {}; extract records individually (their \
`data` fields are base64)",
extents.join(", ")
);
}
}
expected_offset = Some(record.offset + record.byte_len);
assembled.extend_from_slice(data);
}
if let Some(path) = &args.output {
if path.exists() && !args.force {
bail!("{} exists; pass --force to replace it", path.display());
}
std::fs::write(path, &assembled)
.with_context(|| format!("writing {} bytes to {}", assembled.len(), path.display()))?;
eprintln!(
"wrote {} bytes from {} record(s) of stream {stream:?} to {}",
assembled.len(),
matched.len(),
path.display()
);
return Ok(());
}
if args.binary_stdout {
std::io::stdout()
.write_all(&assembled)
.context("writing extracted bytes to stdout")?;
return Ok(());
}
bail!(
"retained stream bytes are binary output that a terminal cannot read; \
pass `-o FILE` or `--binary-stdout` to stream the bytes anyway"
)
}