#![allow(missing_docs)]
mod bip39_friendly;
mod cmd;
mod codex32_friendly;
mod error;
mod format;
mod language;
#[allow(dead_code)]
mod mlock;
mod parse;
use std::io::Write;
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use error::{CliError, Result};
use format::{ErrorBodyJson, ErrorEnvelopeJson};
#[derive(Parser, Debug)]
#[command(
name = "ms",
version,
about = "ms — engrave-friendly BIP-39 entropy backups (the ms1 format)"
)]
pub(crate) struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
#[command(
after_long_help = "EXAMPLES:\n ms encode --phrase \"abandon abandon … about\"\n ms encode --phrase - < phrase.txt\n ms encode --hex 00000000000000000000000000000000 --no-engraving-card\n ms encode --phrase \"...\" --json | jq .ms1"
)]
Encode(cmd::encode::EncodeArgs),
#[command(
after_long_help = "EXAMPLES:\n ms decode ms10entrs…\n ms decode - < engraved.txt\n ms decode <ms1> --language french\n ms decode <ms1> --json | jq .phrase"
)]
Decode(cmd::decode::DecodeArgs),
#[command(
after_long_help = "EXAMPLES:\n ms inspect <ms1> # verdict + fields\n ms inspect <ms1> --json # structured output for tooling\n printf \"ms10e ntrsq…\" | ms inspect - # back-typed chunked form"
)]
Inspect(cmd::inspect::InspectArgs),
#[command(
after_long_help = "EXAMPLES:\n ms verify <ms1> # exit 0 = valid v0.1\n ms verify <ms1> --phrase \"abandon … about\" # round-trip; exit 4 on mismatch\n ms verify <ms1> --phrase \"...\" --json # structured outcome"
)]
Verify(cmd::verify::VerifyArgs),
#[command(
after_long_help = "EXAMPLES:\n ms vectors # compact JSON\n ms vectors --pretty # indented JSON\n ms vectors | jq '.[0]' # filter via jq"
)]
Vectors(cmd::vectors::VectorsArgs),
#[command(
name = "gui-schema",
after_long_help = "EXAMPLES:\n ms gui-schema | jq .version # always 1\n ms gui-schema | jq '.subcommands[].name' # list subcommands\n ms gui-schema | jq '.subcommands[] | select(.name == \"encode\").flags'"
)]
GuiSchema,
#[command(
after_long_help = "EXAMPLES:\n ms repair --ms1 ms10entrsqq... # text-form report on stdout\n ms repair --ms1 - < broken.txt # read ms1 from stdin\n ms repair --ms1 ms10entrsqq... --json # JSON envelope on stdout"
)]
Repair(cmd::repair::RepairArgs),
}
fn main() -> ExitCode {
let cli = match Cli::try_parse() {
Ok(cli) => cli,
Err(e) => {
e.print().ok();
return match e.kind() {
clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
ExitCode::SUCCESS
}
_ => ExitCode::from(64),
};
}
};
let json_mode = is_json_mode(&cli.command);
let result: Result<u8> = match cli.command {
Command::Encode(args) => cmd::encode::run(args),
Command::Decode(args) => cmd::decode::run(args),
Command::Inspect(args) => cmd::inspect::run(args),
Command::Verify(args) => cmd::verify::run(args),
Command::Vectors(args) => cmd::vectors::run(args),
Command::GuiSchema => cmd::gui_schema::run(),
Command::Repair(args) => cmd::repair::run(args),
};
let exit = match result {
Ok(code) => ExitCode::from(code),
Err(e) => {
emit_error(&e, json_mode);
ExitCode::from(e.exit_code())
}
};
mlock::report_at_exit();
exit
}
fn is_json_mode(cmd: &Command) -> bool {
match cmd {
Command::Encode(a) => a.json,
Command::Decode(a) => a.json,
Command::Inspect(a) => a.json,
Command::Verify(a) => a.json,
Command::Vectors(_) => false, Command::GuiSchema => false, Command::Repair(a) => a.json,
}
}
fn emit_error(e: &CliError, json_mode: bool) {
if matches!(e, CliError::FutureFormat { .. }) && !json_mode {
return;
}
if json_mode {
let envelope = ErrorEnvelopeJson {
schema_version: "1",
error: ErrorBodyJson {
kind: e.kind(),
message: e.message(),
exit_code: e.exit_code(),
details: e.details(),
},
};
let s = serde_json::to_string(&envelope).expect("error envelope serializes");
println!("{}", s);
} else {
let mut stderr = std::io::stderr().lock();
writeln!(stderr, "{}", e).ok();
}
}