Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ead4c57
test: require successful bounded help for eviction planning CLIs
seonghobae Aug 14, 2026
b5a07a2
fix: make iCloud eviction help terminal and bounded
seonghobae Aug 14, 2026
a20e8f6
fix: make destination planning help terminal and bounded
seonghobae Aug 14, 2026
a869c0e
test: require strict batch eviction help
seonghobae Aug 14, 2026
1ab4124
fix: keep batch eviction help strictly terminal
seonghobae Aug 14, 2026
89de01a
test: reject non-UTF8 eviction CLI arguments
seonghobae Aug 14, 2026
64286ff
fix: bound non-UTF8 eviction CLI arguments
seonghobae Aug 14, 2026
d0e768b
fix: bound non-UTF8 destination-plan arguments
seonghobae Aug 14, 2026
3001e7b
fix: bound non-UTF8 eviction-batch arguments
seonghobae Aug 14, 2026
a3694be
test: pin complete eviction CLI help output
seonghobae Aug 15, 2026
88ba748
test: pin bounded CLI diagnostics
seonghobae Aug 15, 2026
f208c87
chore: converge eviction help owner on current main
seonghobae Aug 24, 2026
cce2f15
test: require eviction CLI runtime under coverage
seonghobae Aug 24, 2026
dfc8bb3
fix: preserve local eviction runtime under coverage
seonghobae Aug 24, 2026
b8cfc56
fix: preserve destination planner runtime under coverage
seonghobae Aug 24, 2026
587f713
test: preserve native eviction CLI paths
seonghobae Aug 24, 2026
594ea14
fix: preserve native local-eviction paths
seonghobae Aug 24, 2026
07ce119
fix: preserve native destination-plan paths
seonghobae Aug 24, 2026
3212686
fix: preserve native batch-eviction paths
seonghobae Aug 24, 2026
4ea432f
test: reject duplicate eviction authority options
seonghobae Aug 24, 2026
0f8cf2f
fix: reject duplicate eviction authority options
seonghobae Aug 24, 2026
804e323
test: reject duplicate eviction family singletons
seonghobae Aug 24, 2026
1b69519
fix: reject duplicate destination plan limits
seonghobae Aug 24, 2026
b1ef4c2
fix: reject duplicate batch eviction authority options
seonghobae Aug 24, 2026
03e8954
test: align eviction runtime coverage contract
seonghobae Aug 24, 2026
34ced3f
test: reuse eviction CLI build across singleton probes
seonghobae Aug 25, 2026
687c014
test: match destination-plan shipped diagnostics
seonghobae Aug 25, 2026
6baa367
merge: converge eviction CLI owner onto current main
seonghobae Aug 26, 2026
986286f
merge: preserve eviction CLI owner across current dependency base
seonghobae Aug 26, 2026
0b6b2f5
merge: preserve eviction CLI owner across current dependency base
seonghobae Aug 26, 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
88 changes: 69 additions & 19 deletions src-tauri/src/bin/disksage-icloud-local-eviction-batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use disksage_lib::cloud_local_eviction_batch::{
};
use serde::Deserialize;
use std::collections::BTreeMap;
use std::ffi::OsString;
use std::io::Read;
use std::path::{Path, PathBuf};

Expand All @@ -36,14 +37,24 @@ fn usage() -> &'static str {
--rationale TEXT --record-dir ABSOLUTE_LOCAL_DIRECTORY]"
}

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

fn parse_args(args: &[String]) -> Result<Args, String> {
fn text_value(args: &[OsString], index: &mut usize, flag: &str) -> Result<String, String> {
native_value(args, index, flag)?
.into_string()
.map_err(|_| "icloud-local-eviction-batch-invalid-utf8-argument".to_string())
}

fn parse_args_os(args: &[OsString]) -> Result<Args, String> {
if args.len() == 1 && matches!(args[0].to_str(), Some("--help" | "-h")) {
return Err(HELP_REQUESTED.into());
Comment thread
seonghobae marked this conversation as resolved.
}

let mut cloud_root = None;
let mut manifest = None;
let mut execute = false;
Expand All @@ -54,27 +65,60 @@ fn parse_args(args: &[String]) -> Result<Args, String> {
let mut record_dir = None;
let mut index = 0usize;
while index < args.len() {
match args[index].as_str() {
"--cloud-root" => {
cloud_root = Some(PathBuf::from(value(args, &mut index, "--cloud-root")?))
match args[index].to_str() {
Some("--cloud-root") => {
if cloud_root.is_some() {
return Err("--cloud-root는 한 번만 지정할 수 있음".into());
}
cloud_root = Some(PathBuf::from(native_value(args, &mut index, "--cloud-root")?));
}
Some("--manifest") => {
if manifest.is_some() {
return Err("--manifest는 한 번만 지정할 수 있음".into());
}
manifest = Some(PathBuf::from(native_value(args, &mut index, "--manifest")?));
}
Some("--execute") => {
if execute {
return Err("--execute는 한 번만 지정할 수 있음".into());
}
execute = true;
}
"--manifest" => manifest = Some(PathBuf::from(value(args, &mut index, "--manifest")?)),
"--execute" => execute = true,
"--approved-batch-fingerprint" => {
Some("--approved-batch-fingerprint") => {
if approved_batch_fingerprint.is_some() {
return Err("--approved-batch-fingerprint는 한 번만 지정할 수 있음".into());
}
approved_batch_fingerprint =
Some(value(args, &mut index, "--approved-batch-fingerprint")?)
Some(text_value(args, &mut index, "--approved-batch-fingerprint")?)
}
"--confirm-batch-fingerprint" => {
Some("--confirm-batch-fingerprint") => {
if confirm_batch_fingerprint.is_some() {
return Err("--confirm-batch-fingerprint는 한 번만 지정할 수 있음".into());
}
confirm_batch_fingerprint =
Some(value(args, &mut index, "--confirm-batch-fingerprint")?)
Some(text_value(args, &mut index, "--confirm-batch-fingerprint")?)
}
"--approved-by" => approved_by = Some(value(args, &mut index, "--approved-by")?),
"--rationale" => rationale = Some(value(args, &mut index, "--rationale")?),
"--record-dir" => {
record_dir = Some(PathBuf::from(value(args, &mut index, "--record-dir")?))
Some("--approved-by") => {
if approved_by.is_some() {
return Err("--approved-by는 한 번만 지정할 수 있음".into());
}
approved_by = Some(text_value(args, &mut index, "--approved-by")?)
}
"--help" | "-h" => return Err(HELP_REQUESTED.into()),
_unknown => return Err("알 수 없는 인자".into()),
Some("--rationale") => {
if rationale.is_some() {
return Err("--rationale은 한 번만 지정할 수 있음".into());
}
rationale = Some(text_value(args, &mut index, "--rationale")?)
}
Some("--record-dir") => {
if record_dir.is_some() {
return Err("--record-dir는 한 번만 지정할 수 있음".into());
}
record_dir = Some(PathBuf::from(native_value(args, &mut index, "--record-dir")?))
}
Some("--help" | "-h") => return Err("알 수 없는 인자".into()),
Some(_) => return Err("알 수 없는 인자".into()),
None => return Err("icloud-local-eviction-batch-invalid-utf8-argument".into()),
}
index += 1;
}
Expand Down Expand Up @@ -117,6 +161,12 @@ fn parse_args(args: &[String]) -> Result<Args, String> {
})
}

#[cfg(test)]
fn parse_args(args: &[String]) -> Result<Args, String> {
let native = args.iter().map(OsString::from).collect::<Vec<_>>();
parse_args_os(&native)
}

fn home_dir() -> Result<PathBuf, String> {
std::env::var_os("HOME")
.map(PathBuf::from)
Expand Down Expand Up @@ -336,8 +386,8 @@ fn print_json<T: serde::Serialize>(value: &T) -> Result<(), String> {
}

fn run() -> Result<(), String> {
let raw: Vec<String> = std::env::args().skip(1).collect();
let args = parse_args(&raw)?;
let raw = std::env::args_os().skip(1).collect::<Vec<_>>();
let args = parse_args_os(&raw)?;
let roots = cloud::discover_cloud_roots(&home_dir()?);
let root = select_root(&roots, &args.cloud_root)?.clone();
validate_control_locations(
Expand Down
105 changes: 69 additions & 36 deletions src-tauri/src/bin/disksage-icloud-local-eviction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
//! fingerprint, an attributed human approval, a second matching confirmation, and a local immutable
//! record directory.

#[cfg(not(coverage))]
use disksage_lib::cloud::{self, CloudRoot};
#[cfg(not(coverage))]
use disksage_lib::cloud_local_eviction::{
approve_icloud_local_eviction, execute_icloud_local_eviction, plan_icloud_local_eviction,
write_immutable_record, IcloudLocalEvictionApproval, IcloudLocalEvictionPlan,
IcloudLocalEvictionResult,
};
#[cfg(not(coverage))]
use std::ffi::OsString;
use std::path::{Path, PathBuf};

#[cfg(not(coverage))]
#[derive(Debug, Clone, PartialEq, Eq)]
struct Args {
cloud_root: PathBuf,
Expand All @@ -28,21 +25,24 @@ struct Args {
record_dir: Option<PathBuf>,
}

#[cfg(not(coverage))]
fn usage() -> &'static str {
"usage: disksage-icloud-local-eviction --cloud-root ABSOLUTE_PATH --path ABSOLUTE_FILE [--execute --approved-plan-fingerprint HEX64 --confirm-plan-fingerprint HEX64 --approved-by human:IDENTITY --rationale TEXT --record-dir ABSOLUTE_LOCAL_DIRECTORY]"
}

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

#[cfg(not(coverage))]
fn parse_args(args: &[String]) -> Result<Args, String> {
fn text_value(args: &[OsString], index: &mut usize, flag: &str) -> Result<String, String> {
native_value(args, index, flag)?
.into_string()
.map_err(|_| "icloud-local-eviction-invalid-utf8-argument".to_string())
}

fn parse_args_os(args: &[OsString]) -> Result<Args, String> {
let mut cloud_root = None;
let mut path = None;
let mut execute = false;
Expand All @@ -53,27 +53,60 @@ fn parse_args(args: &[String]) -> Result<Args, String> {
let mut record_dir = None;
let mut index = 0usize;
while index < args.len() {
match args[index].as_str() {
"--cloud-root" => {
cloud_root = Some(PathBuf::from(value(args, &mut index, "--cloud-root")?))
match args[index].to_str() {
Some("--cloud-root") => {
if cloud_root.is_some() {
return Err("--cloud-root는 한 번만 지정할 수 있음".into());
}
cloud_root = Some(PathBuf::from(native_value(args, &mut index, "--cloud-root")?));
}
Some("--path") => {
if path.is_some() {
return Err("--path는 한 번만 지정할 수 있음".into());
}
path = Some(PathBuf::from(native_value(args, &mut index, "--path")?));
}
"--path" => path = Some(PathBuf::from(value(args, &mut index, "--path")?)),
"--execute" => execute = true,
"--approved-plan-fingerprint" => {
Some("--execute") => {
if execute {
return Err("--execute는 한 번만 지정할 수 있음".into());
}
execute = true;
}
Some("--approved-plan-fingerprint") => {
if approved_plan_fingerprint.is_some() {
Comment thread
seonghobae marked this conversation as resolved.
return Err("--approved-plan-fingerprint는 한 번만 지정할 수 있음".into());
}
approved_plan_fingerprint =
Some(value(args, &mut index, "--approved-plan-fingerprint")?)
Some(text_value(args, &mut index, "--approved-plan-fingerprint")?)
}
"--confirm-plan-fingerprint" => {
Some("--confirm-plan-fingerprint") => {
if confirm_plan_fingerprint.is_some() {
return Err("--confirm-plan-fingerprint는 한 번만 지정할 수 있음".into());
}
confirm_plan_fingerprint =
Some(value(args, &mut index, "--confirm-plan-fingerprint")?)
Some(text_value(args, &mut index, "--confirm-plan-fingerprint")?)
}
Some("--approved-by") => {
if approved_by.is_some() {
return Err("--approved-by는 한 번만 지정할 수 있음".into());
}
approved_by = Some(text_value(args, &mut index, "--approved-by")?)
}
Some("--rationale") => {
if rationale.is_some() {
return Err("--rationale은 한 번만 지정할 수 있음".into());
}
rationale = Some(text_value(args, &mut index, "--rationale")?)
}
"--approved-by" => approved_by = Some(value(args, &mut index, "--approved-by")?),
"--rationale" => rationale = Some(value(args, &mut index, "--rationale")?),
"--record-dir" => {
record_dir = Some(PathBuf::from(value(args, &mut index, "--record-dir")?))
Some("--record-dir") => {
if record_dir.is_some() {
return Err("--record-dir는 한 번만 지정할 수 있음".into());
}
record_dir = Some(PathBuf::from(native_value(args, &mut index, "--record-dir")?))
}
"--help" | "-h" => return Err(usage().into()),
unknown => return Err(format!("알 수 없는 인자: {unknown}")),
Some("--help" | "-h") => return Err(usage().into()),
Some(_) => return Err("icloud-local-eviction-unknown-argument".into()),
None => return Err("icloud-local-eviction-invalid-utf8-argument".into()),
}
index += 1;
}
Expand Down Expand Up @@ -113,15 +146,19 @@ fn parse_args(args: &[String]) -> Result<Args, String> {
})
}

#[cfg(not(coverage))]
#[cfg(test)]
fn parse_args(args: &[String]) -> Result<Args, String> {
let native = args.iter().map(OsString::from).collect::<Vec<_>>();
parse_args_os(&native)
}

fn home_dir() -> Result<PathBuf, String> {
std::env::var("HOME")
.or_else(|_| std::env::var("USERPROFILE"))
.map(PathBuf::from)
.map_err(|_| "HOME/USERPROFILE을 찾을 수 없음".into())
}

#[cfg(not(coverage))]
fn select_root<'a>(roots: &'a [CloudRoot], requested: &Path) -> Result<&'a CloudRoot, String> {
let matches: Vec<_> = roots
.iter()
Expand All @@ -134,15 +171,13 @@ fn select_root<'a>(roots: &'a [CloudRoot], requested: &Path) -> Result<&'a Cloud
}
}

#[cfg(not(coverage))]
#[derive(Debug, serde::Serialize)]
struct PlanOutput {
action: &'static str,
mutation_executed: bool,
plan: IcloudLocalEvictionPlan,
}

#[cfg(not(coverage))]
#[derive(Debug, serde::Serialize)]
struct ExecuteOutput {
action: &'static str,
Expand All @@ -154,7 +189,6 @@ struct ExecuteOutput {
result_record: String,
}

#[cfg(not(coverage))]
fn print_json<T: serde::Serialize>(value: &T) -> Result<(), String> {
println!(
"{}",
Expand All @@ -163,10 +197,13 @@ fn print_json<T: serde::Serialize>(value: &T) -> Result<(), String> {
Ok(())
}

#[cfg(not(coverage))]
fn run() -> Result<(), String> {
let raw: Vec<String> = std::env::args().skip(1).collect();
let args = parse_args(&raw)?;
let raw = std::env::args_os().skip(1).collect::<Vec<_>>();
if raw.len() == 1 && matches!(raw[0].to_str(), Some("--help" | "-h")) {
println!("{}", usage());
return Ok(());
}
let args = parse_args_os(&raw)?;
let roots = cloud::discover_cloud_roots(&home_dir()?);
let root = select_root(&roots, &args.cloud_root)?.clone();
let now_ms = cloud::system_now_ms();
Expand Down Expand Up @@ -236,17 +273,13 @@ fn run() -> Result<(), String> {
})
}

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

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

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