use std::path::PathBuf;
use clap::{Args as ClapArgs, ValueEnum};
use mf2_catalog::{Catalog, Manifest, MsgId};
use crate::error::{Error, Result, read_bytes};
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, ValueEnum)]
pub(crate) enum Shape {
#[default]
Mf2,
Json,
}
#[derive(Debug, ClapArgs)]
pub(crate) struct Args {
#[arg(value_name = "FILE.mf2b")]
pub(crate) catalog: PathBuf,
#[arg(long, value_enum, default_value_t = Shape::Mf2)]
format: Shape,
#[arg(long, value_name = "FILE.mf2m")]
pub(crate) manifest: Option<PathBuf>,
#[arg(long, value_name = "ID")]
id: Option<String>,
}
pub(crate) fn run(args: &Args) -> Result<()> {
let bytes = read_bytes(&args.catalog)?;
let manifest = match &args.manifest {
Some(path) => Some(Manifest::read(&read_bytes(path)?).map_err(mf2_build::Error::from)?),
None => None,
};
let hash = header_hash(&bytes).ok_or_else(|| {
Error::Usage(format!(
"{}: not a catalog (no MF2B header)",
args.catalog.display()
))
})?;
let catalog = Catalog::new(bytes, hash).map_err(|source| Error::Catalog {
path: args.catalog.clone(),
source,
})?;
let count = catalog.message_count();
let mut out = String::new();
for index in 0..count {
let id = name_of(index, &catalog, manifest.as_ref());
if let Some(wanted) = &args.id
&& id.as_deref() != Some(wanted.as_str())
{
continue;
}
let message = mf2_catalog::decode(&catalog, MsgId::from_raw(index)).map_err(|source| {
Error::Decode {
path: args.catalog.clone(),
id: index,
source,
}
})?;
let shown = id.unwrap_or_else(|| format!("#{index}"));
match args.format {
Shape::Mf2 => {
let source = mf2_syntax::serialize(&message).unwrap_or_default();
out.push_str(&shown);
out.push_str(" = ");
out.push_str(&source.replace('\n', "\\n"));
out.push('\n');
}
Shape::Json => {
let value = serde_json::json!({
"id": shown,
"message": message,
});
out.push_str(&serde_json::to_string(&value).unwrap_or_default());
out.push('\n');
}
}
}
if out.is_empty() {
return Err(Error::Usage(format!(
"{}: no message matched",
args.catalog.display()
)));
}
print!("{out}");
Ok(())
}
fn name_of(index: u32, catalog: &Catalog, manifest: Option<&Manifest>) -> Option<String> {
if let Some(manifest) = manifest {
return manifest.ids.get(index as usize).cloned();
}
catalog.id_of(MsgId::from_raw(index))
}
fn header_hash(bytes: &[u8]) -> Option<u64> {
if bytes.get(..4)? != b"MF2B" {
return None;
}
let field = bytes.get(8..16)?;
Some(u64::from_le_bytes(field.try_into().ok()?))
}