use clap::Parser;
use epcis_hash::{canonicalize_json, canonicalize_xml, compute_hash_from_prehash};
use epcis_translate::{
Cpi, Gdti, Giai, Ginc, Grai, Gsin, Gsrn, Gsrnp, Itip, Lgtin, Pgln, Sgcn, Sgln, Sgtin, Sscc,
Upui,
};
use std::fs;
use std::io::{self, Read};
#[derive(Parser, Debug)]
#[command(
author = "Duma Mtungwa <duma@matangaresearch.com>",
version,
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:class:lgtin:") {
let lgtin = Lgtin::from_urn(key).map_err(|e| format!("Failed to parse LGTIN: {:?}", e))?;
return Ok(lgtin.to_digital_link(base_url));
}
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))
}
"pgln" => {
let pgln =
Pgln::from_urn(key).map_err(|e| format!("Failed to parse PGLN: {:?}", e))?;
Ok(pgln.to_digital_link(base_url))
}
"gdti" => {
let gdti =
Gdti::from_urn(key).map_err(|e| format!("Failed to parse GDTI: {:?}", e))?;
Ok(gdti.to_digital_link(base_url))
}
"gsrn" => {
let gsrn =
Gsrn::from_urn(key).map_err(|e| format!("Failed to parse GSRN: {:?}", e))?;
Ok(gsrn.to_digital_link(base_url))
}
"gsrnp" => {
let gsrnp =
Gsrnp::from_urn(key).map_err(|e| format!("Failed to parse GSRNP: {:?}", e))?;
Ok(gsrnp.to_digital_link(base_url))
}
"sgcn" => {
let sgcn =
Sgcn::from_urn(key).map_err(|e| format!("Failed to parse SGCN: {:?}", e))?;
Ok(sgcn.to_digital_link(base_url))
}
"ginc" => {
let ginc =
Ginc::from_urn(key).map_err(|e| format!("Failed to parse GINC: {:?}", e))?;
Ok(ginc.to_digital_link(base_url))
}
"gsin" => {
let gsin =
Gsin::from_urn(key).map_err(|e| format!("Failed to parse GSIN: {:?}", e))?;
Ok(gsin.to_digital_link(base_url))
}
"itip" => {
let itip =
Itip::from_urn(key).map_err(|e| format!("Failed to parse ITIP: {:?}", e))?;
Ok(itip.to_digital_link(base_url))
}
"upui" => {
let upui =
Upui::from_urn(key).map_err(|e| format!("Failed to parse UPUI: {:?}", e))?;
Ok(upui.to_digital_link(base_url))
}
"cpi" => {
let cpi =
Cpi::from_urn(key).map_err(|e| format!("Failed to parse CPI: {:?}", e))?;
Ok(cpi.to_digital_link(base_url))
}
other => Err(format!("Unsupported URN scheme: {}", other)),
}
} else if key.starts_with("http") || key.contains('/') {
if key.contains("/01/") && key.contains("/235/") {
let upui = Upui::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse UPUI DL: {:?}", e))?;
Ok(upui.to_urn())
} else if key.contains("/01/") && key.contains("/10/") {
let lgtin = Lgtin::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse LGTIN DL: {:?}", e))?;
Ok(lgtin.to_urn())
} else 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("/8006/") {
let itip = Itip::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse ITIP DL: {:?}", e))?;
Ok(itip.to_urn())
} else if key.contains("/8010/") {
let cpi = Cpi::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse CPI DL: {:?}", e))?;
Ok(cpi.to_urn())
} else if key.contains("/8017/") {
let gsrnp = Gsrnp::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse GSRNP DL: {:?}", e))?;
Ok(gsrnp.to_urn())
} else if key.contains("/8018/") {
let gsrn = Gsrn::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse GSRN DL: {:?}", e))?;
Ok(gsrn.to_urn())
} else if key.contains("/253/") {
let gdti = Gdti::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse GDTI DL: {:?}", e))?;
Ok(gdti.to_urn())
} else if key.contains("/255/") {
let sgcn = Sgcn::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse SGCN DL: {:?}", e))?;
Ok(sgcn.to_urn())
} else if key.contains("/401/") {
let ginc = Ginc::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse GINC DL: {:?}", e))?;
Ok(ginc.to_urn())
} else if key.contains("/402/") {
let gsin = Gsin::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse GSIN DL: {:?}", e))?;
Ok(gsin.to_urn())
} else if key.contains("/417/") {
let pgln = Pgln::from_digital_link(key, prefix_len)
.map_err(|e| format!("Failed to parse PGLN DL: {:?}", e))?;
Ok(pgln.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 (e.g. /01/, /00/, /414/, /417/, /253/, /255/, /401/, /402/, /8003/, /8004/, /8006/, /8010/, /8017/, /8018/)".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(())
}