use clap::Args;
use ms_codec::consts::{RESERVED_NOT_EMITTED_V01, TAG_ENTR, VALID_ENTR_LENGTHS, VALID_STR_LENGTHS};
use ms_codec::InspectReport;
use serde_json::to_string;
use crate::error::Result;
use crate::format::{InspectJson, InspectReportJson};
use crate::parse::read_input;
#[derive(Args, Debug)]
pub struct InspectArgs {
pub ms1: Option<String>,
#[arg(long)]
pub json: bool,
}
pub fn run(args: InspectArgs) -> Result<u8> {
let ms1 = read_input(args.ms1.as_deref())?;
let report = ms_codec::inspect(&ms1)?;
let (would_decode, reasons) = analyze(&report, ms1.len());
if args.json {
emit_json(&report, would_decode, &reasons)?;
} else {
emit_text(&report, would_decode, &reasons);
}
Ok(0)
}
fn analyze(report: &InspectReport, str_len: usize) -> (bool, Vec<&'static str>) {
let mut reasons: Vec<&'static str> = Vec::new();
let tag_bytes = *report.tag.as_bytes();
if report.hrp != "ms" {
reasons.push("wrong-hrp");
}
if report.threshold != 0 {
reasons.push("threshold-not-zero");
}
if report.share_index != 's' {
reasons.push("share-index-not-secret");
}
if tag_bytes != TAG_ENTR {
if RESERVED_NOT_EMITTED_V01.contains(&tag_bytes) {
reasons.push("reserved-tag-not-emitted");
} else {
reasons.push("unknown-tag");
}
}
if report.prefix_byte != 0x00 {
reasons.push("non-zero-prefix");
}
if !VALID_STR_LENGTHS.contains(&str_len) {
reasons.push("unexpected-string-length");
}
if tag_bytes == TAG_ENTR && !VALID_ENTR_LENGTHS.contains(&report.payload_bytes.len()) {
reasons.push("payload-length-mismatch");
}
(reasons.is_empty(), reasons)
}
fn reason_text(tag: &'static str) -> &'static str {
match tag {
"unexpected-string-length" => "string length not in v0.1 set [50, 56, 62, 69, 75]",
"wrong-hrp" => "HRP is not \"ms\"",
"threshold-not-zero" => "threshold not 0 (v0.1 is single-string only)",
"share-index-not-secret" => "share-index not 's' (BIP-93 requires 's' for threshold=0)",
"reserved-tag-not-emitted" => "tag is reserved-not-emitted in v0.1; deferred to v0.2+",
"unknown-tag" => "tag not in v0.1 RESERVED_TAG_TABLE",
"non-zero-prefix" => "reserved-prefix byte is not 0x00 (v0.1 reserves it)",
"payload-length-mismatch" => "entr payload length not in [16, 20, 24, 28, 32] bytes",
_ => "<unknown reason>",
}
}
fn emit_text(report: &InspectReport, would_decode: bool, reasons: &[&'static str]) {
if would_decode {
println!("OK: would decode v0.1");
} else {
println!("FAIL: would NOT decode v0.1");
for r in reasons {
println!(" reason: {} ({})", r, reason_text(r));
}
}
println!();
println!("hrp: {}", report.hrp);
println!("threshold: {}", report.threshold);
println!(
"tag: {}",
std::str::from_utf8(report.tag.as_bytes()).unwrap_or("<non-utf8>")
);
println!("share_index: {}", report.share_index);
println!("prefix_byte: 0x{:02x}", report.prefix_byte);
println!("payload_bytes: {}", hex::encode(&report.payload_bytes));
println!("checksum_valid: {}", report.checksum_valid);
}
fn emit_json(report: &InspectReport, would_decode: bool, reasons: &[&'static str]) -> Result<()> {
let json = InspectJson {
schema_version: "1",
report: InspectReportJson {
hrp: report.hrp.clone(),
threshold: report.threshold,
tag: std::str::from_utf8(report.tag.as_bytes())
.unwrap_or("<non-utf8>")
.to_string(),
share_index: report.share_index,
prefix_byte: report.prefix_byte,
payload_bytes_hex: hex::encode(&report.payload_bytes),
checksum_valid: report.checksum_valid,
},
would_decode,
failure_reasons: reasons.to_vec(),
};
let s = to_string(&json).expect("inspect json always serializes");
println!("{}", s);
Ok(())
}