use std::io::{self, Read};
use std::fs;
use clap::Parser;
use epcis_hash::{canonicalize_json, canonicalize_xml, compute_hash_from_prehash};
use epcis_translate::{Sgtin, Sscc, Sgln, Grai, Giai};
#[derive(Parser, Debug)]
#[command(
author = "Duma Mtungwa <duma@matangaresearch.com>",
version = "0.1.2",
about = "EPCIS SDK Command Line Interface",
long_about = "A command line tool to generate canonical pre-hashes, final SHA-256 hashes, and translate GS1 identifiers between URN and Digital Link formats."
)]
struct Args {
file: Option<String>,
#[arg(short, long)]
prehash: bool,
#[arg(short, long)]
enforce_format: Option<String>,
#[arg(short, long)]
translate: Option<String>,
#[arg(short = 'l', long, default_value_t = 7)]
prefix_len: usize,
#[arg(long)]
cbv_1_2: bool,
#[arg(long, default_value = "https://id.gs1.org")]
base_url: String,
}
fn handle_translation(key: &str, prefix_len: usize, base_url: &str) -> Result<String, String> {
if key.starts_with("urn:epc:id:") {
let parts: Vec<&str> = key.split(':').collect();
if parts.len() < 5 {
return Err("Invalid URN format. Expected e.g. urn:epc:id:sgtin:...".to_string());
}
let scheme = parts[3];
match scheme {
"sgtin" => {
let sgtin = Sgtin::from_urn(key).map_err(|e| format!("Failed to parse SGTIN: {:?}", e))?;
Ok(sgtin.to_digital_link(base_url))
}
"sscc" => {
let sscc = Sscc::from_urn(key).map_err(|e| format!("Failed to parse SSCC: {:?}", e))?;
Ok(sscc.to_digital_link(base_url))
}
"sgln" => {
let sgln = Sgln::from_urn(key).map_err(|e| format!("Failed to parse SGLN: {:?}", e))?;
Ok(sgln.to_digital_link(base_url))
}
"grai" => {
let grai = Grai::from_urn(key).map_err(|e| format!("Failed to parse GRAI: {:?}", e))?;
Ok(grai.to_digital_link(base_url))
}
"giai" => {
let giai = Giai::from_urn(key).map_err(|e| format!("Failed to parse GIAI: {:?}", e))?;
Ok(giai.to_digital_link(base_url))
}
other => Err(format!("Unsupported URN scheme: {}", other)),
}
} else if key.starts_with("http") || key.contains('/') {
if key.contains("/01/") {
let sgtin = Sgtin::from_digital_link(key, prefix_len).map_err(|e| format!("Failed to parse SGTIN DL: {:?}", e))?;
Ok(sgtin.to_urn())
} else if key.contains("/00/") {
let sscc = Sscc::from_digital_link(key, prefix_len).map_err(|e| format!("Failed to parse SSCC DL: {:?}", e))?;
Ok(sscc.to_urn())
} else if key.contains("/414/") {
let sgln = Sgln::from_digital_link(key, prefix_len).map_err(|e| format!("Failed to parse SGLN DL: {:?}", e))?;
Ok(sgln.to_urn())
} else if key.contains("/8003/") {
let grai = Grai::from_digital_link(key, prefix_len).map_err(|e| format!("Failed to parse GRAI DL: {:?}", e))?;
Ok(grai.to_urn())
} else if key.contains("/8004/") {
let giai = Giai::from_digital_link(key, prefix_len).map_err(|e| format!("Failed to parse GIAI DL: {:?}", e))?;
Ok(giai.to_urn())
} else {
Err("Could not detect GS1 Application Identifier (AI) in Digital Link path (expected e.g. /01/ SGTIN, /00/ SSCC, /414/ SGLN, /8003/ GRAI, /8004/ GIAI)".to_string())
}
} else {
Err("Invalid key format. Key must be an EPC URN (starts with urn:epc:id:) or GS1 Digital Link URL/path".to_string())
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if let Some(ref key) = args.translate {
match handle_translation(key, args.prefix_len, &args.base_url) {
Ok(translated) => {
println!("{}", translated);
return Ok(());
}
Err(e) => {
eprintln!("Translation Error: {}", e);
std::process::exit(1);
}
}
}
let mut content = String::new();
if let Some(ref filepath) = args.file {
if filepath == "-" {
io::stdin().read_to_string(&mut content)?;
} else {
content = fs::read_to_string(filepath)?;
}
} else {
io::stdin().read_to_string(&mut content)?;
}
let trimmed = content.trim();
if trimmed.is_empty() {
eprintln!("Error: Input is empty.");
std::process::exit(1);
}
let is_xml = if let Some(ref format) = args.enforce_format {
format.to_lowercase() == "xml"
} else {
trimmed.starts_with('<')
};
let is_cbv_2_0 = !args.cbv_1_2;
let prehashes_str = if is_xml {
canonicalize_xml(&content, is_cbv_2_0)?
} else {
let json_val = serde_json::from_str(&content)?;
canonicalize_json(&json_val, is_cbv_2_0)?
};
if args.prehash {
print!("{}", prehashes_str);
} else {
for line in prehashes_str.lines() {
if !line.trim().is_empty() {
let hash = compute_hash_from_prehash(line);
println!("{}", hash);
}
}
}
Ok(())
}