From 68d45e25309f5b66b751a9f79c9cb18fb2f16588 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 12:10:19 +0900 Subject: [PATCH 01/11] test: require successful archive-tree help --- src-tauri/tests/archive_tree_help_exit.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src-tauri/tests/archive_tree_help_exit.rs diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs new file mode 100644 index 000000000..b62838a4d --- /dev/null +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -0,0 +1,22 @@ +use std::process::Command; + +#[test] +fn archive_tree_help_exits_successfully_without_error_output() { + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .arg("--help") + .output() + .expect("archive-tree CLI must launch for its help contract"); + + assert!( + output.status.success(), + "--help must be a successful terminal action, got status {:?} and stderr {:?}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + output.stderr.is_empty(), + "successful help must not be projected through stderr" + ); + let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); + assert!(stdout.contains("disksage-archive-tree --zip PATH")); +} From 2a7f92f610cda25d36dd56a06e1800e07bdbff76 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 12:29:39 +0900 Subject: [PATCH 02/11] fix: make archive-tree help successful --- src-tauri/src/bin/disksage-archive-tree.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src-tauri/src/bin/disksage-archive-tree.rs b/src-tauri/src/bin/disksage-archive-tree.rs index f08956f5c..8b7906524 100644 --- a/src-tauri/src/bin/disksage-archive-tree.rs +++ b/src-tauri/src/bin/disksage-archive-tree.rs @@ -57,6 +57,13 @@ fn parse_args(args: &[String]) -> Result { fn run() -> Result<(), String> { let raw: Vec = std::env::args().skip(1).collect(); + if raw + .iter() + .any(|argument| matches!(argument.as_str(), "--help" | "-h")) + { + println!("{}", usage()); + return Ok(()); + } let args = parse_args(&raw)?; let root_mode = if args.keep_top_level { ArchiveTreeRootMode::KeepTopLevel From f9f2973d662be911b63c73baeabbdcaaffef1f14 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 12:31:51 +0900 Subject: [PATCH 03/11] test: bind archive help process test to archive feature --- src-tauri/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 445985b92..d905ed2f4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -116,6 +116,11 @@ name = "disksage-naruon-copy-readiness-verify" path = "src/bin/disksage-naruon-copy-readiness-verify.rs" required-features = ["cloud-cli"] +[[test]] +name = "archive_tree_help_exit" +path = "tests/archive_tree_help_exit.rs" +required-features = ["archive-cli"] + [build-dependencies] tauri-build = { version = "2", features = [] } From 91c0ea7f82d9c70b07a04c513001c8ed4823485d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 15:13:23 +0900 Subject: [PATCH 04/11] test: require archive help argument validation --- src-tauri/tests/archive_tree_help_exit.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs index b62838a4d..c5d9f7a19 100644 --- a/src-tauri/tests/archive_tree_help_exit.rs +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -20,3 +20,24 @@ fn archive_tree_help_exits_successfully_without_error_output() { let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); assert!(stdout.contains("disksage-archive-tree --zip PATH")); } + +#[test] +fn archive_tree_help_does_not_hide_an_unknown_argument() { + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .args(["--help", "--unknown"]) + .output() + .expect("archive-tree CLI must launch for invalid-argument validation"); + + assert!( + !output.status.success(), + "help must not turn an otherwise invalid invocation into success" + ); + assert!( + output.stdout.is_empty(), + "invalid invocation must not emit help on stdout" + ); + assert!( + !output.stderr.is_empty(), + "invalid invocation must remain visible through stderr" + ); +} From 4809a5437cf3ee0ba54ecf85fd1d25d1310a9a97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 16:04:04 +0900 Subject: [PATCH 05/11] ci: execute archive help process contract --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ad646064..3dfec92e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: run: | cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli archive_git_tree cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli --bin disksage-archive-tree + cargo test --manifest-path src-tauri/Cargo.toml --features archive-cli --test archive_tree_help_exit - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 20.19.0 From 6a47df3736cb771fb7967765537dbe1d2daf68ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 17:08:46 +0900 Subject: [PATCH 06/11] test: bound archive-tree argument diagnostics --- src-tauri/tests/archive_tree_help_exit.rs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs index c5d9f7a19..0a9e1c715 100644 --- a/src-tauri/tests/archive_tree_help_exit.rs +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -41,3 +41,35 @@ fn archive_tree_help_does_not_hide_an_unknown_argument() { "invalid invocation must remain visible through stderr" ); } + +#[test] +fn archive_tree_unknown_argument_uses_bounded_diagnostic() { + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .arg("--opaque-option=not-shown") + .output() + .expect("archive-tree CLI must launch for invalid-argument validation"); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); + assert_eq!(stderr.trim_end(), "archive-tree-unknown-argument"); + assert!(!stderr.contains("not-shown")); +} + +#[cfg(unix)] +#[test] +fn archive_tree_non_utf8_argument_fails_without_panic() { + use std::ffi::OsString; + use std::os::unix::ffi::OsStringExt; + + let opaque = OsString::from_vec(vec![b'-', b'-', b'o', b'p', b'a', b'q', b'u', b'e', 0xff]); + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .arg(opaque) + .output() + .expect("archive-tree CLI must launch for non-UTF-8 argument validation"); + + assert_eq!(output.status.code(), Some(2)); + assert!(output.stdout.is_empty()); + let stderr = String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); + assert_eq!(stderr.trim_end(), "archive-tree-argument-invalid"); +} From e09b6539bc13c5a7ecb808c847d19e6a549f668c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 18:20:51 +0900 Subject: [PATCH 07/11] fix: bind archive help to sole UTF-8 request --- src-tauri/src/bin/disksage-archive-tree.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/bin/disksage-archive-tree.rs b/src-tauri/src/bin/disksage-archive-tree.rs index 8b7906524..a44552782 100644 --- a/src-tauri/src/bin/disksage-archive-tree.rs +++ b/src-tauri/src/bin/disksage-archive-tree.rs @@ -40,7 +40,7 @@ fn parse_args(args: &[String]) -> Result { } "--keep-top-level" => keep_top_level = true, "--help" | "-h" => return Err(usage().into()), - unknown => return Err(format!("알 수 없는 인자: {unknown}")), + _ => return Err("archive-tree-unknown-argument".into()), } index += 1; } @@ -56,11 +56,15 @@ fn parse_args(args: &[String]) -> Result { } fn run() -> Result<(), String> { - let raw: Vec = std::env::args().skip(1).collect(); - if raw - .iter() - .any(|argument| matches!(argument.as_str(), "--help" | "-h")) - { + let raw = std::env::args_os() + .skip(1) + .map(|argument| { + argument + .into_string() + .map_err(|_| "archive-tree-argument-invalid".to_string()) + }) + .collect::, _>>()?; + if raw.len() == 1 && matches!(raw[0].as_str(), "--help" | "-h") { println!("{}", usage()); return Ok(()); } From 446e8962fcff67ad42f7f85806a2cc804ac7cec4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 18:19:09 +0900 Subject: [PATCH 08/11] test: require exact archive help output --- src-tauri/tests/archive_tree_help_exit.rs | 40 ++++++++++++++--------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs index 0a9e1c715..753791327 100644 --- a/src-tauri/tests/archive_tree_help_exit.rs +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -1,24 +1,32 @@ use std::process::Command; +const EXPECTED_USAGE: &str = "DiskSage archive proof: usage: disksage-archive-tree --zip PATH [--expected-tree HEX40 | --prove-subset-of PATH] [--keep-top-level]"; + #[test] fn archive_tree_help_exits_successfully_without_error_output() { - let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) - .arg("--help") - .output() - .expect("archive-tree CLI must launch for its help contract"); + for flag in ["--help", "-h"] { + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .arg(flag) + .output() + .expect("archive-tree CLI must launch for its help contract"); - assert!( - output.status.success(), - "--help must be a successful terminal action, got status {:?} and stderr {:?}", - output.status.code(), - String::from_utf8_lossy(&output.stderr) - ); - assert!( - output.stderr.is_empty(), - "successful help must not be projected through stderr" - ); - let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); - assert!(stdout.contains("disksage-archive-tree --zip PATH")); + assert!( + output.status.success(), + "{flag} must be a successful terminal action, got status {:?} and stderr {:?}", + output.status.code(), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + output.stderr.is_empty(), + "successful help must not be projected through stderr" + ); + let stdout = String::from_utf8(output.stdout).expect("help output must be valid UTF-8"); + assert_eq!( + stdout, + format!("{EXPECTED_USAGE}\n"), + "help output must equal the stable usage synopsis" + ); + } } #[test] From 6c91fe17b98c693e6cef7a2946019727a0838b79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 18:51:45 +0900 Subject: [PATCH 09/11] docs: explain archive-tree process contracts --- src-tauri/tests/archive_tree_help_exit.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs index 753791327..d15377d27 100644 --- a/src-tauri/tests/archive_tree_help_exit.rs +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -2,6 +2,7 @@ use std::process::Command; const EXPECTED_USAGE: &str = "DiskSage archive proof: usage: disksage-archive-tree --zip PATH [--expected-tree HEX40 | --prove-subset-of PATH] [--keep-top-level]"; +/// Prove both help flags return the exact stable usage line and empty stderr. #[test] fn archive_tree_help_exits_successfully_without_error_output() { for flag in ["--help", "-h"] { @@ -29,6 +30,7 @@ fn archive_tree_help_exits_successfully_without_error_output() { } } +/// Prove help cannot hide a trailing unknown option or emit successful stdout. #[test] fn archive_tree_help_does_not_hide_an_unknown_argument() { let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) @@ -50,6 +52,7 @@ fn archive_tree_help_does_not_hide_an_unknown_argument() { ); } +/// Prove an unknown option uses the stable bounded diagnostic without reflection. #[test] fn archive_tree_unknown_argument_uses_bounded_diagnostic() { let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) @@ -64,6 +67,7 @@ fn archive_tree_unknown_argument_uses_bounded_diagnostic() { assert!(!stderr.contains("not-shown")); } +/// Prove hostile non-UTF-8 arguments fail through the stable diagnostic on Unix. #[cfg(unix)] #[test] fn archive_tree_non_utf8_argument_fails_without_panic() { From 91c3fd82d089ced64f4cc0b6e8ebfcf737e598d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 05:05:12 +0900 Subject: [PATCH 10/11] test: bound mixed archive-tree help diagnostics --- src-tauri/tests/archive_tree_help_exit.rs | 47 ++++++++++++++--------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src-tauri/tests/archive_tree_help_exit.rs b/src-tauri/tests/archive_tree_help_exit.rs index d15377d27..754b15744 100644 --- a/src-tauri/tests/archive_tree_help_exit.rs +++ b/src-tauri/tests/archive_tree_help_exit.rs @@ -30,26 +30,37 @@ fn archive_tree_help_exits_successfully_without_error_output() { } } -/// Prove help cannot hide a trailing unknown option or emit successful stdout. +/// Prove mixed help and opaque invalid input stays a bounded failure in either order. #[test] -fn archive_tree_help_does_not_hide_an_unknown_argument() { - let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) - .args(["--help", "--unknown"]) - .output() - .expect("archive-tree CLI must launch for invalid-argument validation"); +fn archive_tree_help_does_not_hide_or_reflect_an_unknown_argument() { + for arguments in [ + ["--help", "--opaque-option=not-shown"], + ["--opaque-option=not-shown", "--help"], + ] { + let output = Command::new(env!("CARGO_BIN_EXE_disksage-archive-tree")) + .args(arguments) + .output() + .expect("archive-tree CLI must launch for invalid-argument validation"); - assert!( - !output.status.success(), - "help must not turn an otherwise invalid invocation into success" - ); - assert!( - output.stdout.is_empty(), - "invalid invocation must not emit help on stdout" - ); - assert!( - !output.stderr.is_empty(), - "invalid invocation must remain visible through stderr" - ); + assert!( + !output.status.success(), + "help must not turn an otherwise invalid invocation into success" + ); + assert!( + output.stdout.is_empty(), + "invalid invocation must not emit help on stdout" + ); + let stderr = + String::from_utf8(output.stderr).expect("CLI diagnostics must be valid UTF-8"); + assert!( + !stderr.is_empty(), + "invalid invocation must remain visible through stderr" + ); + assert!( + !stderr.contains("not-shown"), + "mixed help diagnostics must not reflect opaque argument payloads" + ); + } } /// Prove an unknown option uses the stable bounded diagnostic without reflection. From b6b997c1e021f2b6f82dbc39c0f973c8265edb60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 19:34:01 +0900 Subject: [PATCH 11/11] docs: complete archive-tree CLI documentation --- src-tauri/src/bin/disksage-archive-tree.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/bin/disksage-archive-tree.rs b/src-tauri/src/bin/disksage-archive-tree.rs index a44552782..72b2e995d 100644 --- a/src-tauri/src/bin/disksage-archive-tree.rs +++ b/src-tauri/src/bin/disksage-archive-tree.rs @@ -1,4 +1,8 @@ -//! Read-only ZIP-to-Git-tree proof. Archive entries are streamed and never extracted. +//! Read-only ZIP-to-Git-tree proof. +//! +//! Archive entries are streamed and never extracted. The command computes deterministic Git-tree +//! evidence, optionally verifies one expected tree or a content-subset relation, and never mutates +//! either archive or the local filesystem. use std::path::PathBuf; @@ -6,18 +10,25 @@ use disksage_lib::archive_git_tree::{ compare_zip_content_inclusion, inspect_zip_git_tree_with_mode, ArchiveTreeRootMode, }; +/// Parsed arguments for one archive-tree inspection or subset proof. #[derive(Debug, PartialEq, Eq)] struct Args { + /// ZIP archive whose content tree will be inspected. zip: PathBuf, + /// Optional expected 40-character Git tree identifier. expected_tree: Option, + /// Optional archive that must contain every content item from `zip`. superset_zip: Option, + /// Whether to retain a shared archive root directory in the computed tree. keep_top_level: bool, } +/// Returns the stable command synopsis used by help and bounded validation failures. fn usage() -> &'static str { "DiskSage archive proof: usage: disksage-archive-tree --zip PATH [--expected-tree HEX40 | --prove-subset-of PATH] [--keep-top-level]" } +/// Returns the required value after one known option and advances the parser index. fn value(args: &[String], index: &mut usize, flag: &str) -> Result { *index += 1; args.get(*index) @@ -25,6 +36,7 @@ fn value(args: &[String], index: &mut usize, flag: &str) -> Result Result { let mut zip = None; let mut expected_tree = None; @@ -55,6 +67,7 @@ fn parse_args(args: &[String]) -> Result { }) } +/// Reads process arguments, performs the requested read-only proof, and prints JSON evidence. fn run() -> Result<(), String> { let raw = std::env::args_os() .skip(1) @@ -97,6 +110,7 @@ fn run() -> Result<(), String> { Ok(()) } +/// Runs the CLI and returns exit code 2 for bounded validation or proof failures. fn main() { if let Err(error) = run() { eprintln!("{error}");