Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
5bbf3f4
test: require successful incomplete-download help
seonghobae Aug 14, 2026
d66667a
test: cover incomplete-download execution help
seonghobae Aug 14, 2026
ae78c36
test: consolidate incomplete-download CLI contracts
seonghobae Aug 14, 2026
25c4628
test: avoid duplicate feature builds
seonghobae Aug 14, 2026
a7a86fc
fix: make materialization help terminal and bounded
seonghobae Aug 14, 2026
4aac7f3
fix: make incomplete recovery help terminal and bounded
seonghobae Aug 14, 2026
0b794a0
fix: make materialize help terminal and bounded
seonghobae Aug 14, 2026
8b80d2d
fix: keep materialize help change narrow
seonghobae Aug 14, 2026
0ccce55
test: reject non-UTF8 incomplete-download CLI arguments
seonghobae Aug 14, 2026
e20f931
test: pin incomplete-download CLI error contracts
seonghobae Aug 14, 2026
ac4c235
fix: bound materialization CLI non-UTF8 args
seonghobae Aug 14, 2026
b52328b
fix: bound incomplete recovery argv decoding
seonghobae Aug 14, 2026
558dc5d
fix: bound materialization argv decoding
seonghobae Aug 14, 2026
75b8d36
fix: preserve destination-plan diagnostic
seonghobae Aug 14, 2026
2d231f9
fix: keep mixed help fail-closed
seonghobae Aug 14, 2026
6eeb1a4
fix: keep recovery mixed help fail-closed
seonghobae Aug 14, 2026
c134b79
fix: keep mixed help invocation fail-closed
seonghobae Aug 15, 2026
4c12440
merge: converge incomplete-download CLI help onto current main
seonghobae Aug 24, 2026
a578374
test: preserve native incomplete-download paths
seonghobae Aug 24, 2026
168d8a4
fix: preserve native materialization paths
seonghobae Aug 24, 2026
b372342
fix: preserve native recovery paths
seonghobae Aug 24, 2026
d8a2400
fix: preserve native materialize paths
seonghobae Aug 24, 2026
7812527
test: reject duplicate incomplete-download limits
seonghobae Aug 24, 2026
e3da2f2
fix: reject duplicate materialization limits
seonghobae Aug 24, 2026
fef8544
fix: reject duplicate recovery limits
seonghobae Aug 24, 2026
6a4b186
fix: reject duplicate materialize limits
seonghobae Aug 24, 2026
8593746
test: accept bounded empty materialization roots
seonghobae Aug 25, 2026
40a363b
merge: converge incomplete-download help onto current main
seonghobae Aug 25, 2026
308480b
merge: preserve incomplete-download help owner across current depende…
seonghobae Aug 26, 2026
fa231da
merge: preserve incomplete-download help owner across current depende…
seonghobae Aug 26, 2026
5855226
merge: converge incomplete-download help owner onto current main
seonghobae Aug 26, 2026
d9e3a64
test: bind release verifier to matrix artifact namespaces
seonghobae Aug 26, 2026
8511b52
fix: verify Windows release artifact namespace
seonghobae Aug 26, 2026
ca071cd
chore: converge release verifier onto current main
seonghobae Aug 26, 2026
dd0ffa7
chore: converge incomplete-download help CLIs onto current main
seonghobae Aug 26, 2026
7af3711
test: reject cross-platform release artifact placement
seonghobae Aug 26, 2026
ccd5e8d
fix: bind release artifacts to platform directories
seonghobae Aug 26, 2026
5637622
test: bind operational CLIs to release platform namespace
seonghobae Aug 26, 2026
db672bb
test: require exact tag release artifact verification
seonghobae Aug 26, 2026
5e7b33d
fix: verify tag artifacts before sbom
seonghobae Aug 26, 2026
33f886c
Merge remote-tracking branch 'origin/fix/release-artifact-windows-nam…
seonghobae Aug 26, 2026
6f50a08
chore: remove release verifier drift from incomplete help owner
seonghobae Aug 26, 2026
d8b8a12
Merge remote-tracking branch 'origin/main' into pr-218
seonghobae Aug 29, 2026
9654d3d
docs(cli): add next-action help guidance
seonghobae Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 59 additions & 27 deletions src-tauri/src/bin/disksage-incomplete-download-materialization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use disksage_lib::incomplete_download_recovery::{
validate_incomplete_download_recovery, RecoveryValidationLimits,
};
use disksage_lib::private_evidence::write_private_json_create_new;
use std::ffi::{OsStr, OsString};
use std::path::{Component, Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -26,28 +27,54 @@ fn absolute_without_parent(path: &Path) -> bool {
.any(|component| matches!(component, Component::ParentDir))
}

fn parse_args(raw: &[String]) -> Result<Args, String> {
fn usage() -> String {
format!(
"usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH \
[--max-entries 1..={DEFAULT_MAX_ENTRIES}] \
[--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \
[--private-output ABSOLUTE_NEW_FILE.json]\n\
다음 단계: 생성된 계획을 검토하세요. 이 명령은 파일을 이동하거나 삭제하지 않습니다."
)
}

fn next_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result<OsString, String> {
*index += 1;
raw.get(*index)
.cloned()
.ok_or_else(|| format!("{flag} 값이 필요함"))
}

fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result<String, String> {
next_value(raw, index, flag)?
.into_string()
.map_err(|_| format!("{flag} 값은 UTF-8 텍스트여야 함"))
}

fn parse_args(raw: &[OsString]) -> Result<Args, String> {
let mut root = None;
let mut max_entries = DEFAULT_MAX_ENTRIES;
let mut max_entries_seen = false;
let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS;
let mut stale_after_days_seen = false;
let mut private_output = None;
let mut index = 0usize;
while index < raw.len() {
let value = |index: &mut usize, flag: &str| -> Result<String, String> {
*index += 1;
raw.get(*index)
.cloned()
.ok_or_else(|| format!("{flag} 값이 필요함"))
};
match raw[index].as_str() {
let option = raw[index]
.to_str()
.ok_or_else(|| "incomplete-download-materialization-unknown-argument".to_string())?;
match option {
"--root" => {
if root.is_some() {
return Err("--root는 한 번만 지정할 수 있음".into());
}
root = Some(PathBuf::from(value(&mut index, "--root")?));
root = Some(PathBuf::from(next_value(raw, &mut index, "--root")?));
}
"--max-entries" => {
let parsed = value(&mut index, "--max-entries")?
if max_entries_seen {
return Err("--max-entries는 한 번만 지정할 수 있음".into());
}
max_entries_seen = true;
let parsed = next_text_value(raw, &mut index, "--max-entries")?
.parse::<usize>()
.map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?;
if parsed == 0 || parsed > DEFAULT_MAX_ENTRIES {
Expand All @@ -58,7 +85,11 @@ fn parse_args(raw: &[String]) -> Result<Args, String> {
max_entries = parsed;
}
"--stale-after-days" => {
let parsed = value(&mut index, "--stale-after-days")?
if stale_after_days_seen {
return Err("--stale-after-days는 한 번만 지정할 수 있음".into());
}
stale_after_days_seen = true;
let parsed = next_text_value(raw, &mut index, "--stale-after-days")?
.parse::<u64>()
.map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?;
if !(1..=MAX_STALE_AFTER_DAYS).contains(&parsed) {
Expand All @@ -72,17 +103,13 @@ fn parse_args(raw: &[String]) -> Result<Args, String> {
if private_output.is_some() {
return Err("--private-output은 한 번만 지정할 수 있음".into());
}
private_output = Some(PathBuf::from(value(&mut index, "--private-output")?));
}
"--help" | "-h" => {
return Err(format!(
"usage: disksage-incomplete-download-materialization --root ABSOLUTE_PATH \
[--max-entries 1..={DEFAULT_MAX_ENTRIES}] \
[--stale-after-days 1..={MAX_STALE_AFTER_DAYS}] \
[--private-output ABSOLUTE_NEW_FILE.json]"
));
private_output = Some(PathBuf::from(next_value(
raw,
&mut index,
"--private-output",
)?));
}
flag => return Err(format!("알 수 없는 인자: {flag}")),
_unknown => return Err("incomplete-download-materialization-unknown-argument".into()),
}
index += 1;
}
Expand Down Expand Up @@ -110,9 +137,18 @@ fn system_now_ms() -> u64 {
.unwrap_or(0)
}

#[cfg(not(coverage))]
fn run() -> Result<(), String> {
let args = parse_args(&std::env::args().skip(1).collect::<Vec<_>>())?;
let raw = std::env::args_os().skip(1).collect::<Vec<_>>();
if raw.len() == 1
&& matches!(
raw.first().map(OsString::as_os_str),
Some(argument) if argument == OsStr::new("--help") || argument == OsStr::new("-h")
)
{
println!("{}", usage());
return Ok(());
Comment thread
seonghobae marked this conversation as resolved.
}
let args = parse_args(&raw)?;
let audit = collect_incomplete_download_audit(
&args.root,
system_now_ms(),
Expand Down Expand Up @@ -148,17 +184,13 @@ fn run() -> Result<(), String> {
Ok(())
}

#[cfg(not(coverage))]
fn main() {
if let Err(error) = run() {
eprintln!("DiskSage incomplete download materialization plan: {error}");
std::process::exit(2);
}
}

#[cfg(coverage)]
fn main() {}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
98 changes: 68 additions & 30 deletions src-tauri/src/bin/disksage-incomplete-download-materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use disksage_lib::incomplete_download_recovery::{
validate_incomplete_download_recovery, RecoveryValidationLimits,
};
use disksage_lib::provider_capacity::{collect_icloud_native_capacity, CloudCapacitySnapshot};
use std::ffi::{OsStr, OsString};
use std::io::Read;
use std::path::{Component, Path, PathBuf};

Expand Down Expand Up @@ -61,69 +62,94 @@ fn usage() -> String {
--approved-by human:ID --rationale TEXT --execute \
(--live-icloud-capacity | --capacity-snapshot ABSOLUTE.json) \
[--max-entries 1..={DEFAULT_MAX_ENTRIES}] \
[--stale-after-days 1..={MAX_STALE_AFTER_DAYS}]"
[--stale-after-days 1..={MAX_STALE_AFTER_DAYS}]\n\
다음 단계: 계획 지문과 용량 증거를 검토한 뒤 승인 정보와 --execute를 제공하세요."
)
}

fn parse_args(raw: &[String]) -> Result<Args, String> {
fn next_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result<OsString, String> {
*index += 1;
raw.get(*index)
.cloned()
.ok_or_else(|| format!("{flag} 값이 필요함"))
}

fn next_text_value(raw: &[OsString], index: &mut usize, flag: &str) -> Result<String, String> {
next_value(raw, index, flag)?
.into_string()
.map_err(|_| format!("{flag} 값은 UTF-8 텍스트여야 함"))
}

fn parse_args(raw: &[OsString]) -> Result<Args, String> {
let mut source_root = None;
let mut destination_plan = None;
let mut confirmed_plan_fingerprint = None;
let mut receipt_dir = None;
let mut approved_by = None;
let mut rationale = None;
let mut max_entries = DEFAULT_MAX_ENTRIES;
let mut max_entries_seen = false;
let mut stale_after_days = DEFAULT_STALE_AFTER_DAYS;
let mut stale_after_days_seen = false;
let mut live_icloud_capacity = false;
let mut capacity_snapshot = None;
let mut execute = false;
let mut index = 0usize;
while index < raw.len() {
let value = |index: &mut usize, flag: &str| -> Result<String, String> {
*index += 1;
raw.get(*index)
.cloned()
.ok_or_else(|| format!("{flag} 값이 필요함"))
};
match raw[index].as_str() {
let option = raw[index]
.to_str()
.ok_or_else(|| "incomplete-download-materialize-unknown-argument".to_string())?;
Comment thread
seonghobae marked this conversation as resolved.
match option {
"--source-root" => {
if source_root.is_some() {
return Err("--source-root는 한 번만 지정할 수 있음".into());
}
source_root = Some(PathBuf::from(value(&mut index, "--source-root")?));
source_root = Some(PathBuf::from(next_value(raw, &mut index, "--source-root")?));
}
"--destination-plan" => {
if destination_plan.is_some() {
return Err("--destination-plan은 한 번만 지정할 수 있음".into());
}
destination_plan = Some(PathBuf::from(value(&mut index, "--destination-plan")?));
destination_plan = Some(PathBuf::from(next_value(
raw,
&mut index,
"--destination-plan",
)?));
}
"--confirm-plan-fingerprint" => {
if confirmed_plan_fingerprint.is_some() {
return Err("--confirm-plan-fingerprint는 한 번만 지정할 수 있음".into());
}
confirmed_plan_fingerprint = Some(value(&mut index, "--confirm-plan-fingerprint")?);
confirmed_plan_fingerprint = Some(next_text_value(
raw,
&mut index,
"--confirm-plan-fingerprint",
)?);
}
"--receipt-dir" => {
if receipt_dir.is_some() {
return Err("--receipt-dir은 한 번만 지정할 수 있음".into());
}
receipt_dir = Some(PathBuf::from(value(&mut index, "--receipt-dir")?));
receipt_dir = Some(PathBuf::from(next_value(raw, &mut index, "--receipt-dir")?));
}
"--approved-by" => {
if approved_by.is_some() {
return Err("--approved-by는 한 번만 지정할 수 있음".into());
}
approved_by = Some(value(&mut index, "--approved-by")?);
approved_by = Some(next_text_value(raw, &mut index, "--approved-by")?);
}
"--rationale" => {
if rationale.is_some() {
return Err("--rationale은 한 번만 지정할 수 있음".into());
}
rationale = Some(value(&mut index, "--rationale")?);
rationale = Some(next_text_value(raw, &mut index, "--rationale")?);
}
"--max-entries" => {
let parsed = value(&mut index, "--max-entries")?
if max_entries_seen {
return Err("--max-entries는 한 번만 지정할 수 있음".into());
}
max_entries_seen = true;
let parsed = next_text_value(raw, &mut index, "--max-entries")?
.parse::<usize>()
.map_err(|_| "--max-entries는 양의 정수여야 함".to_string())?;
if parsed == 0 || parsed > DEFAULT_MAX_ENTRIES {
Expand All @@ -134,7 +160,11 @@ fn parse_args(raw: &[String]) -> Result<Args, String> {
max_entries = parsed;
}
"--stale-after-days" => {
let parsed = value(&mut index, "--stale-after-days")?
if stale_after_days_seen {
return Err("--stale-after-days는 한 번만 지정할 수 있음".into());
}
stale_after_days_seen = true;
let parsed = next_text_value(raw, &mut index, "--stale-after-days")?
.parse::<u64>()
.map_err(|_| "--stale-after-days는 양의 정수여야 함".to_string())?;
if !(1..=MAX_STALE_AFTER_DAYS).contains(&parsed) {
Expand All @@ -154,16 +184,19 @@ fn parse_args(raw: &[String]) -> Result<Args, String> {
if capacity_snapshot.is_some() {
return Err("--capacity-snapshot은 한 번만 지정할 수 있음".into());
}
capacity_snapshot = Some(PathBuf::from(value(&mut index, "--capacity-snapshot")?));
capacity_snapshot = Some(PathBuf::from(next_value(
raw,
&mut index,
"--capacity-snapshot",
)?));
}
"--execute" => {
if execute {
return Err("--execute는 한 번만 지정할 수 있음".into());
}
execute = true;
}
"--help" | "-h" => return Err(usage()),
flag => return Err(format!("알 수 없는 인자: {flag}")),
_unknown => return Err("incomplete-download-materialize-unknown-argument".into()),
}
index += 1;
}
Expand Down Expand Up @@ -296,9 +329,18 @@ fn verify_discovered_cloud_root(
Ok(())
}

#[cfg(not(coverage))]
fn run() -> Result<(), String> {
let args = parse_args(&std::env::args().skip(1).collect::<Vec<_>>())?;
let raw = std::env::args_os().skip(1).collect::<Vec<_>>();
if raw.len() == 1
&& matches!(
raw.first().map(OsString::as_os_str),
Some(argument) if argument == OsStr::new("--help") || argument == OsStr::new("-h")
)
{
println!("{}", usage());
return Ok(());
}
let args = parse_args(&raw)?;
let plan: IncompleteDownloadDestinationPlan = read_bounded_json(
&args.destination_plan,
MAX_PRIVATE_PLAN_BYTES,
Expand Down Expand Up @@ -370,29 +412,25 @@ fn run() -> Result<(), String> {
Ok(())
}

#[cfg(not(coverage))]
fn main() {
if let Err(error) = run() {
eprintln!("DiskSage incomplete download materialization execution: {error}");
std::process::exit(2);
}
}

#[cfg(coverage)]
fn main() {}

#[cfg(test)]
mod tests {
use super::*;

fn required() -> Vec<String> {
fn required() -> Vec<OsString> {
vec![
"--source-root".into(),
"/source".into(),
"--destination-plan".into(),
"/private/plan.json".into(),
"--confirm-plan-fingerprint".into(),
"a".repeat(64),
"a".repeat(64).into(),
"--receipt-dir".into(),
"/private/receipts".into(),
"--approved-by".into(),
Expand All @@ -416,7 +454,7 @@ mod tests {
#[test]
fn rejects_missing_execute_bad_attribution_and_ambiguous_capacity() {
let mut missing_execute = required();
missing_execute.retain(|value| value != "--execute");
missing_execute.retain(|value| value != OsStr::new("--execute"));
missing_execute.push("--live-icloud-capacity".into());
assert!(parse_args(&missing_execute).is_err());

Expand All @@ -431,7 +469,7 @@ mod tests {
let mut bad_attribution = required();
let position = bad_attribution
.iter()
.position(|value| value == "human:test")
.position(|value| value == OsStr::new("human:test"))
.unwrap();
bad_attribution[position] = "agent:test".into();
bad_attribution.push("--live-icloud-capacity".into());
Expand Down
Loading
Loading