#![allow(missing_docs)]
use assert_cmd::Command;
use std::process::Command as StdCommand;
fn encode_then_decode_exit(template: &str, extra_args: &[&str]) -> i32 {
let mut enc_args = vec!["encode", "--group-size", "0"];
enc_args.extend_from_slice(extra_args);
enc_args.push(template);
let enc = StdCommand::new(assert_cmd::cargo::cargo_bin("md"))
.args(&enc_args)
.output()
.expect("invoke md encode");
assert!(enc.status.success(), "encode must succeed (exit 0)");
let phrase = String::from_utf8(enc.stdout)
.unwrap()
.lines()
.next()
.unwrap()
.to_string();
let dec = StdCommand::new(assert_cmd::cargo::cargo_bin("md"))
.args(["decode", &phrase])
.output()
.expect("invoke md decode");
dec.status.code().expect("decode exited normally")
}
const PATHLESS_ADVISORY_SUBSTR: &str = "no canonical default derivation path";
fn run(args: &[&str]) -> (String, String, bool) {
let out = Command::cargo_bin("md")
.unwrap()
.args(args)
.output()
.unwrap();
(
String::from_utf8(out.stdout).unwrap(),
String::from_utf8(out.stderr).unwrap(),
out.status.success(),
)
}
const DEAD_SHAPES: &[&str] = &[
"tr(@0/<0;1>/*,pk(@1/<0;1>/*))",
"sh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*))",
"wsh(pk(@0/<0;1>/*))",
"wsh(or_d(pk(@0/<0;1>/*),and_v(v:pk(@1/<0;1>/*),older(144))))",
];
const CANONICAL_SHAPES: &[&str] = &[
"tr(@0/<0;1>/*)",
"wpkh(@0/<0;1>/*)",
"sh(wpkh(@0/<0;1>/*))",
"wsh(multi(2,@0/<0;1>/*,@1/<0;1>/*))",
];
#[test]
fn pathless_shape_without_path_emits_advisory() {
for template in DEAD_SHAPES {
let (stdout, stderr, ok) = run(&["encode", template]);
assert!(ok, "{template} should still encode (exit 0)");
assert!(
stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"{template}: expected pathless advisory on stderr; got: {stderr}"
);
assert!(stdout.trim().starts_with("md1"), "stdout must be the card");
assert!(
!stdout.contains(PATHLESS_ADVISORY_SUBSTR),
"advisory must never land on stdout: {stdout}"
);
}
}
#[cfg(feature = "json")]
#[test]
fn pathless_shape_without_path_json_branch_also_emits_advisory() {
for template in DEAD_SHAPES {
let (stdout, stderr, ok) = run(&["encode", "--json", template]);
assert!(ok);
assert!(
stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"{template}: json branch missed the pathless advisory; got: {stderr}"
);
assert!(!stdout.contains(PATHLESS_ADVISORY_SUBSTR));
}
}
#[test]
fn pathless_shape_with_path_suppresses_advisory() {
for template in DEAD_SHAPES {
let (_stdout, stderr, ok) = run(&["encode", template, "--path", "bip48"]);
assert!(ok, "{template} + --path should encode");
assert!(
!stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"{template} + --path must NOT emit the pathless advisory; got: {stderr}"
);
}
}
#[test]
fn canonical_shapes_never_emit_pathless_advisory_with_or_without_path() {
for template in CANONICAL_SHAPES {
let (_stdout, stderr, ok) = run(&["encode", template]);
assert!(ok, "{template} should encode");
assert!(
!stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"{template} (canonical, no --path) must NOT emit the pathless advisory; got: {stderr}"
);
let (_stdout2, stderr2, ok2) = run(&["encode", template, "--path", "bip44"]);
assert!(ok2);
assert!(
!stderr2.contains(PATHLESS_ADVISORY_SUBSTR),
"{template} + --path (canonical) must NOT emit the pathless advisory; got: {stderr2}"
);
}
}
#[test]
fn advisory_does_not_perturb_card_bytes() {
let template = "sh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*))";
let (stdout_no_path, _e1, _s1) = run(&["encode", template, "--group-size", "0"]);
let (stdout_no_path_2, _e2, _s2) = run(&["encode", template, "--group-size", "0"]);
assert_eq!(
stdout_no_path, stdout_no_path_2,
"stdout must be deterministic"
);
assert!(stdout_no_path.trim().starts_with("md1"));
}
#[test]
fn inline_per_at_n_origins_no_path_full_decodes_and_is_not_warned() {
let template = "sh(sortedmulti(2,@0/48'/0'/0'/1'/<0;1>/*,@1/48'/0'/0'/1'/<0;1>/*))";
assert_eq!(
encode_then_decode_exit(template, &[]),
0,
"inline-origin card must FULL-decode (exit 0)"
);
let (_stdout, stderr, ok) = run(&["encode", template]);
assert!(ok, "encode must still succeed (exit 0)");
assert!(
!stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"a full-decodable inline-origin card must NOT be warned as pathless; got: {stderr}"
);
}
#[test]
fn path_m_zero_components_on_dead_shape_still_warns() {
let template = "sh(sortedmulti(2,@0/<0;1>/*,@1/<0;1>/*))";
assert_eq!(
encode_then_decode_exit(template, &["--path", "m"]),
4,
"--path m on a dead shape must still partial-decode (exit 4)"
);
let (_stdout, stderr, ok) = run(&["encode", template, "--path", "m"]);
assert!(ok, "encode must still succeed (exit 0)");
assert!(
stderr.contains(PATHLESS_ADVISORY_SUBSTR),
"--path m must NOT suppress the pathless advisory (the footgun is not bypassed); got: {stderr}"
);
}