diff --git a/impl/rust-cli/benches/operation_benchmarks.rs b/impl/rust-cli/benches/operation_benchmarks.rs index 789d3dc2..b2f22b39 100644 --- a/impl/rust-cli/benches/operation_benchmarks.rs +++ b/impl/rust-cli/benches/operation_benchmarks.rs @@ -10,7 +10,7 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; use tempfile::tempdir; -use vsh::commands::{mkdir, rmdir, rm, touch}; +use vsh::commands::{mkdir, rm, rmdir, touch}; use vsh::state::ShellState; /// Benchmark: mkdir operation diff --git a/impl/rust-cli/benches/performance_benchmarks.rs b/impl/rust-cli/benches/performance_benchmarks.rs index 9c0d301b..f4c096d0 100644 --- a/impl/rust-cli/benches/performance_benchmarks.rs +++ b/impl/rust-cli/benches/performance_benchmarks.rs @@ -18,7 +18,7 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criteri use std::fs; use std::io::Write; use tempfile::TempDir; -use vsh::commands::{mkdir, rm, touch, undo, redo}; +use vsh::commands::{mkdir, redo, rm, touch, undo}; use vsh::glob::expand_glob; use vsh::state::ShellState; @@ -50,24 +50,28 @@ fn bench_undo_scaling(c: &mut Criterion) { for num_ops in [10, 50, 100].iter() { group.throughput(Throughput::Elements(*num_ops as u64)); - group.bench_with_input(BenchmarkId::from_parameter(num_ops), num_ops, |b, &num_ops| { - b.iter(|| { - let temp = TempDir::new().unwrap(); - let root = temp.path().to_str().unwrap(); - let mut state = ShellState::new(root).unwrap(); + group.bench_with_input( + BenchmarkId::from_parameter(num_ops), + num_ops, + |b, &num_ops| { + b.iter(|| { + let temp = TempDir::new().unwrap(); + let root = temp.path().to_str().unwrap(); + let mut state = ShellState::new(root).unwrap(); - // Create operations - for i in 0..num_ops { - mkdir(&mut state, &format!("dir_{}", i), false).unwrap(); - } + // Create operations + for i in 0..num_ops { + mkdir(&mut state, &format!("dir_{}", i), false).unwrap(); + } - // Benchmark undoing all - for _ in 0..num_ops { - undo(&mut state, 1, false).unwrap(); - } - black_box(&state); - }); - }); + // Benchmark undoing all + for _ in 0..num_ops { + undo(&mut state, 1, false).unwrap(); + } + black_box(&state); + }); + }, + ); } group.finish(); @@ -216,9 +220,10 @@ fn bench_checkpoint_creation(c: &mut Criterion) { state }, |mut state| { - state - .checkpoints - .insert("test".to_string(), (state.history.len(), chrono::Utc::now())); + state.checkpoints.insert( + "test".to_string(), + (state.history.len(), chrono::Utc::now()), + ); black_box(&state.checkpoints); }, criterion::BatchSize::SmallInput, diff --git a/impl/rust-cli/src/arith.rs b/impl/rust-cli/src/arith.rs index accbdc07..de81f9bf 100644 --- a/impl/rust-cli/src/arith.rs +++ b/impl/rust-cli/src/arith.rs @@ -26,8 +26,8 @@ //! assert_eq!(result, 15); //! ``` -use anyhow::{anyhow, Result}; use crate::state::ShellState; +use anyhow::{anyhow, Result}; /// Arithmetic operators #[derive(Debug, Clone, PartialEq)] @@ -144,7 +144,8 @@ impl Tokenizer { } } - num_str.parse::() + num_str + .parse::() .map_err(|_| anyhow!("Invalid number: {}", num_str)) } @@ -295,7 +296,10 @@ impl Tokenizer { } } - Some(ch) => Err(anyhow!("Unexpected character in arithmetic expression: {}", ch)), + Some(ch) => Err(anyhow!( + "Unexpected character in arithmetic expression: {}", + ch + )), } } } @@ -508,7 +512,11 @@ impl Parser { if matches!(self.peek(), Token::Op(ArithOp::Pow)) { self.advance(); let right = self.parse_power()?; // Right-associative recursion - Ok(ArithExpr::BinaryOp(ArithOp::Pow, Box::new(left), Box::new(right))) + Ok(ArithExpr::BinaryOp( + ArithOp::Pow, + Box::new(left), + Box::new(right), + )) } else { Ok(left) } @@ -554,7 +562,10 @@ impl Parser { Ok(expr) } - token => Err(anyhow!("Unexpected token in arithmetic expression: {:?}", token)), + token => Err(anyhow!( + "Unexpected token in arithmetic expression: {:?}", + token + )), } } } @@ -704,21 +715,48 @@ mod tests { fn test_arith_basic_ops() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("5 + 3").unwrap(), &state).unwrap(), 8); - assert_eq!(eval_arith(&parse_arithmetic("10 - 4").unwrap(), &state).unwrap(), 6); - assert_eq!(eval_arith(&parse_arithmetic("6 * 7").unwrap(), &state).unwrap(), 42); - assert_eq!(eval_arith(&parse_arithmetic("20 / 4").unwrap(), &state).unwrap(), 5); - assert_eq!(eval_arith(&parse_arithmetic("17 % 5").unwrap(), &state).unwrap(), 2); + assert_eq!( + eval_arith(&parse_arithmetic("5 + 3").unwrap(), &state).unwrap(), + 8 + ); + assert_eq!( + eval_arith(&parse_arithmetic("10 - 4").unwrap(), &state).unwrap(), + 6 + ); + assert_eq!( + eval_arith(&parse_arithmetic("6 * 7").unwrap(), &state).unwrap(), + 42 + ); + assert_eq!( + eval_arith(&parse_arithmetic("20 / 4").unwrap(), &state).unwrap(), + 5 + ); + assert_eq!( + eval_arith(&parse_arithmetic("17 % 5").unwrap(), &state).unwrap(), + 2 + ); } #[test] fn test_arith_precedence() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("2 + 3 * 4").unwrap(), &state).unwrap(), 14); - assert_eq!(eval_arith(&parse_arithmetic("(2 + 3) * 4").unwrap(), &state).unwrap(), 20); - assert_eq!(eval_arith(&parse_arithmetic("2 ** 3 + 1").unwrap(), &state).unwrap(), 9); - assert_eq!(eval_arith(&parse_arithmetic("2 ** (3 + 1)").unwrap(), &state).unwrap(), 16); + assert_eq!( + eval_arith(&parse_arithmetic("2 + 3 * 4").unwrap(), &state).unwrap(), + 14 + ); + assert_eq!( + eval_arith(&parse_arithmetic("(2 + 3) * 4").unwrap(), &state).unwrap(), + 20 + ); + assert_eq!( + eval_arith(&parse_arithmetic("2 ** 3 + 1").unwrap(), &state).unwrap(), + 9 + ); + assert_eq!( + eval_arith(&parse_arithmetic("2 ** (3 + 1)").unwrap(), &state).unwrap(), + 16 + ); } #[test] @@ -726,43 +764,100 @@ mod tests { let state = ShellState::new("/tmp").unwrap(); // 2 ** 3 ** 2 = 2 ** (3 ** 2) = 2 ** 9 = 512 - assert_eq!(eval_arith(&parse_arithmetic("2 ** 3 ** 2").unwrap(), &state).unwrap(), 512); + assert_eq!( + eval_arith(&parse_arithmetic("2 ** 3 ** 2").unwrap(), &state).unwrap(), + 512 + ); } #[test] fn test_arith_comparison() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("5 > 3").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("5 < 3").unwrap(), &state).unwrap(), 0); - assert_eq!(eval_arith(&parse_arithmetic("5 == 5").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("5 != 3").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("5 >= 5").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("5 <= 4").unwrap(), &state).unwrap(), 0); + assert_eq!( + eval_arith(&parse_arithmetic("5 > 3").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 < 3").unwrap(), &state).unwrap(), + 0 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 == 5").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 != 3").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 >= 5").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 <= 4").unwrap(), &state).unwrap(), + 0 + ); } #[test] fn test_arith_logical() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("1 && 1").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("1 && 0").unwrap(), &state).unwrap(), 0); - assert_eq!(eval_arith(&parse_arithmetic("1 || 0").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("0 || 0").unwrap(), &state).unwrap(), 0); - assert_eq!(eval_arith(&parse_arithmetic("!0").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("!5").unwrap(), &state).unwrap(), 0); + assert_eq!( + eval_arith(&parse_arithmetic("1 && 1").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("1 && 0").unwrap(), &state).unwrap(), + 0 + ); + assert_eq!( + eval_arith(&parse_arithmetic("1 || 0").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("0 || 0").unwrap(), &state).unwrap(), + 0 + ); + assert_eq!( + eval_arith(&parse_arithmetic("!0").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("!5").unwrap(), &state).unwrap(), + 0 + ); } #[test] fn test_arith_bitwise() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("5 & 3").unwrap(), &state).unwrap(), 1); - assert_eq!(eval_arith(&parse_arithmetic("5 | 3").unwrap(), &state).unwrap(), 7); - assert_eq!(eval_arith(&parse_arithmetic("5 ^ 3").unwrap(), &state).unwrap(), 6); - assert_eq!(eval_arith(&parse_arithmetic("~5").unwrap(), &state).unwrap(), -6); - assert_eq!(eval_arith(&parse_arithmetic("8 << 2").unwrap(), &state).unwrap(), 32); - assert_eq!(eval_arith(&parse_arithmetic("8 >> 2").unwrap(), &state).unwrap(), 2); + assert_eq!( + eval_arith(&parse_arithmetic("5 & 3").unwrap(), &state).unwrap(), + 1 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 | 3").unwrap(), &state).unwrap(), + 7 + ); + assert_eq!( + eval_arith(&parse_arithmetic("5 ^ 3").unwrap(), &state).unwrap(), + 6 + ); + assert_eq!( + eval_arith(&parse_arithmetic("~5").unwrap(), &state).unwrap(), + -6 + ); + assert_eq!( + eval_arith(&parse_arithmetic("8 << 2").unwrap(), &state).unwrap(), + 32 + ); + assert_eq!( + eval_arith(&parse_arithmetic("8 >> 2").unwrap(), &state).unwrap(), + 2 + ); } #[test] @@ -771,8 +866,14 @@ mod tests { state.set_variable("x", "5"); state.set_variable("y", "10"); - assert_eq!(eval_arith(&parse_arithmetic("x + y").unwrap(), &state).unwrap(), 15); - assert_eq!(eval_arith(&parse_arithmetic("x * 2 + y").unwrap(), &state).unwrap(), 20); + assert_eq!( + eval_arith(&parse_arithmetic("x + y").unwrap(), &state).unwrap(), + 15 + ); + assert_eq!( + eval_arith(&parse_arithmetic("x * 2 + y").unwrap(), &state).unwrap(), + 20 + ); } #[test] @@ -780,7 +881,10 @@ mod tests { let state = ShellState::new("/tmp").unwrap(); // Undefined variables should be treated as 0 - assert_eq!(eval_arith(&parse_arithmetic("undefined_var + 5").unwrap(), &state).unwrap(), 5); + assert_eq!( + eval_arith(&parse_arithmetic("undefined_var + 5").unwrap(), &state).unwrap(), + 5 + ); } #[test] @@ -795,24 +899,45 @@ mod tests { fn test_arith_nested() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("((5 + 3) * 2) + 1").unwrap(), &state).unwrap(), 17); - assert_eq!(eval_arith(&parse_arithmetic("2 ** (3 ** 2)").unwrap(), &state).unwrap(), 512); + assert_eq!( + eval_arith(&parse_arithmetic("((5 + 3) * 2) + 1").unwrap(), &state).unwrap(), + 17 + ); + assert_eq!( + eval_arith(&parse_arithmetic("2 ** (3 ** 2)").unwrap(), &state).unwrap(), + 512 + ); } #[test] fn test_arith_unary_minus() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic("-5").unwrap(), &state).unwrap(), -5); - assert_eq!(eval_arith(&parse_arithmetic("-(5 + 3)").unwrap(), &state).unwrap(), -8); - assert_eq!(eval_arith(&parse_arithmetic("10 + -5").unwrap(), &state).unwrap(), 5); + assert_eq!( + eval_arith(&parse_arithmetic("-5").unwrap(), &state).unwrap(), + -5 + ); + assert_eq!( + eval_arith(&parse_arithmetic("-(5 + 3)").unwrap(), &state).unwrap(), + -8 + ); + assert_eq!( + eval_arith(&parse_arithmetic("10 + -5").unwrap(), &state).unwrap(), + 5 + ); } #[test] fn test_arith_whitespace() { let state = ShellState::new("/tmp").unwrap(); - assert_eq!(eval_arith(&parse_arithmetic(" 5 + 3 ").unwrap(), &state).unwrap(), 8); - assert_eq!(eval_arith(&parse_arithmetic("( ( 5 + 3 ) * 2 )").unwrap(), &state).unwrap(), 16); + assert_eq!( + eval_arith(&parse_arithmetic(" 5 + 3 ").unwrap(), &state).unwrap(), + 8 + ); + assert_eq!( + eval_arith(&parse_arithmetic("( ( 5 + 3 ) * 2 )").unwrap(), &state).unwrap(), + 16 + ); } } diff --git a/impl/rust-cli/src/audit_log.rs b/impl/rust-cli/src/audit_log.rs index 95870e7b..1d2f7e58 100644 --- a/impl/rust-cli/src/audit_log.rs +++ b/impl/rust-cli/src/audit_log.rs @@ -74,7 +74,7 @@ impl AuditEntry { .ok() .and_then(|p| p.to_str().map(|s| s.to_string())) .unwrap_or_else(|| "/".to_string()), - signature: None, // TODO: Add HMAC signing + signature: None, // TODO: Add HMAC signing } } @@ -129,10 +129,7 @@ impl AuditLog { .open(&log_path)?; } - Ok(Self { - log_path, - hmac_key, - }) + Ok(Self { log_path, hmac_key }) } /// Resolve the default audit-log path following the XDG Base Directory spec. @@ -221,8 +218,8 @@ impl AuditLog { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn read_all(&self) -> Result> { - let content = std::fs::read_to_string(&self.log_path) - .context("Failed to read audit log")?; + let content = + std::fs::read_to_string(&self.log_path).context("Failed to read audit log")?; let mut entries = Vec::new(); for (line_num, line) in content.lines().enumerate() { @@ -233,7 +230,11 @@ impl AuditLog { match AuditEntry::from_json_line(line) { Ok(entry) => entries.push(entry), Err(e) => { - eprintln!("Warning: Failed to parse audit entry at line {}: {}", line_num + 1, e); + eprintln!( + "Warning: Failed to parse audit entry at line {}: {}", + line_num + 1, + e + ); } } } @@ -389,13 +390,16 @@ mod tests { let log = AuditLog::new(log_path, None).unwrap(); let op1 = Operation::new(OperationType::Mkdir, "dir1".to_string(), None); - log.append(&AuditEntry::from_operation(&op1, "success", None)).unwrap(); + log.append(&AuditEntry::from_operation(&op1, "success", None)) + .unwrap(); let op2 = Operation::new(OperationType::CreateFile, "file1".to_string(), None); - log.append(&AuditEntry::from_operation(&op2, "success", None)).unwrap(); + log.append(&AuditEntry::from_operation(&op2, "success", None)) + .unwrap(); let op3 = Operation::new(OperationType::Mkdir, "dir2".to_string(), None); - log.append(&AuditEntry::from_operation(&op3, "success", None)).unwrap(); + log.append(&AuditEntry::from_operation(&op3, "success", None)) + .unwrap(); let mkdirs = log.read_by_type("Mkdir").unwrap(); assert_eq!(mkdirs.len(), 2); diff --git a/impl/rust-cli/src/commands.rs b/impl/rust-cli/src/commands.rs index 1547c66b..9ec3d3a7 100644 --- a/impl/rust-cli/src/commands.rs +++ b/impl/rust-cli/src/commands.rs @@ -110,11 +110,7 @@ pub fn mkdir(state: &mut ShellState, path: &str, verbose: bool) -> Result<()> { if verbose { let proof = OperationType::Mkdir.proof_reference(); println!(" {} {}", "Proof:".bright_black(), proof.format_short()); - println!( - " {} rmdir {}", - "Undo:".bright_black(), - path - ); + println!(" {} rmdir {}", "Undo:".bright_black(), path); } Ok(()) @@ -291,8 +287,8 @@ pub fn rm(state: &mut ShellState, path: &str, verbose: bool) -> Result<()> { fs::remove_file(&full_path).context("rm failed")?; - let op = Operation::new(OperationType::DeleteFile, path.to_string(), None) - .with_undo_data(content); + let op = + Operation::new(OperationType::DeleteFile, path.to_string(), None).with_undo_data(content); let op_id = op.id; state.record_operation(op); @@ -534,11 +530,7 @@ pub fn symlink(state: &mut ShellState, target: &str, link: &str, verbose: bool) if verbose { let proof = OperationType::Symlink.proof_reference(); println!(" {} {}", "Proof:".bright_black(), proof.format_short()); - println!( - " {} unlink {}", - "Undo:".bright_black(), - link - ); + println!(" {} unlink {}", "Undo:".bright_black(), link); } Ok(()) @@ -562,8 +554,8 @@ pub fn chmod(state: &mut ShellState, mode_str: &str, path: &str, verbose: bool) #[cfg(unix)] let old_mode = { use std::os::unix::fs::PermissionsExt; - let metadata = fs::symlink_metadata(&file_path) - .context("chmod: failed to read metadata")?; + let metadata = + fs::symlink_metadata(&file_path).context("chmod: failed to read metadata")?; metadata.permissions().mode() }; #[cfg(not(unix))] @@ -615,8 +607,7 @@ pub fn chmod(state: &mut ShellState, mode_str: &str, path: &str, verbose: bool) fn parse_chmod_mode(mode_str: &str, current_mode: u32) -> Result { // Try octal first if mode_str.chars().all(|c| c.is_ascii_digit()) { - let mode = u32::from_str_radix(mode_str, 8) - .context("Invalid octal mode")?; + let mode = u32::from_str_radix(mode_str, 8).context("Invalid octal mode")?; if mode > 0o7777 { anyhow::bail!("Mode out of range: {:o}", mode); } @@ -639,10 +630,22 @@ fn parse_chmod_mode(mode_str: &str, current_mode: u32) -> Result { while let Some(&c) = chars.peek() { match c { - 'u' => { who_mask |= 0o700; chars.next(); } - 'g' => { who_mask |= 0o070; chars.next(); } - 'o' => { who_mask |= 0o007; chars.next(); } - 'a' => { who_mask |= 0o777; chars.next(); } + 'u' => { + who_mask |= 0o700; + chars.next(); + } + 'g' => { + who_mask |= 0o070; + chars.next(); + } + 'o' => { + who_mask |= 0o007; + chars.next(); + } + 'a' => { + who_mask |= 0o777; + chars.next(); + } '+' | '-' | '=' => break, _ => anyhow::bail!("Invalid who character: '{}'", c), } @@ -705,8 +708,7 @@ pub fn chown(state: &mut ShellState, owner_str: &str, path: &str, verbose: bool) // Capture current ownership for undo use std::os::unix::fs::MetadataExt; - let metadata = fs::symlink_metadata(&file_path) - .context("chown: failed to read metadata")?; + let metadata = fs::symlink_metadata(&file_path).context("chown: failed to read metadata")?; let old_uid = metadata.uid(); let old_gid = metadata.gid(); @@ -769,7 +771,11 @@ fn parse_chown_spec(spec: &str, current_uid: u32, current_gid: u32) -> Result<(u // SAFETY: c_name is a valid NUL-terminated string; getpwnam // returns a pointer to a static passwd struct or null. let pw = unsafe { libc::getpwnam(c_name.as_ptr()) }; - if pw.is_null() { u32::MAX } else { unsafe { (*pw).pw_uid } } + if pw.is_null() { + u32::MAX + } else { + unsafe { (*pw).pw_uid } + } } Err(_) => u32::MAX, // Name contains null bytes — invalid } @@ -784,7 +790,11 @@ fn parse_chown_spec(spec: &str, current_uid: u32, current_gid: u32) -> Result<(u // SAFETY: c_name is a valid NUL-terminated string; getgrnam // returns a pointer to a static group struct or null. let gr = unsafe { libc::getgrnam(c_name.as_ptr()) }; - if gr.is_null() { u32::MAX } else { unsafe { (*gr).gr_gid } } + if gr.is_null() { + u32::MAX + } else { + unsafe { (*gr).gr_gid } + } } Err(_) => u32::MAX, // Name contains null bytes — invalid } @@ -804,7 +814,11 @@ fn parse_chown_spec(spec: &str, current_uid: u32, current_gid: u32) -> Result<(u Ok(c_name) => { // SAFETY: c_name is a valid NUL-terminated string; getpwnam is POSIX-safe. let pw = unsafe { libc::getpwnam(c_name.as_ptr()) }; - if pw.is_null() { u32::MAX } else { unsafe { (*pw).pw_uid } } + if pw.is_null() { + u32::MAX + } else { + unsafe { (*pw).pw_uid } + } } Err(_) => u32::MAX, } @@ -885,12 +899,12 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::FileAppended => { // Undo append: truncate file to original size - let size_bytes = op.undo_data.as_ref().context("Missing original size for undo")?; - let original_size = u64::from_le_bytes( - size_bytes[..8] - .try_into() - .context("Invalid size data")?, - ); + let size_bytes = op + .undo_data + .as_ref() + .context("Missing original size for undo")?; + let original_size = + u64::from_le_bytes(size_bytes[..8].try_into().context("Invalid size data")?); use std::fs::OpenOptions; let file = OpenOptions::new() @@ -916,7 +930,9 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::Symlink => { // Undo unlink = re-create the symlink - let target = op.undo_data.as_ref() + let target = op + .undo_data + .as_ref() .map(|d| String::from_utf8_lossy(d).to_string()) .context("Missing symlink target for undo")?; #[cfg(unix)] @@ -927,7 +943,9 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::SetVariable => { // Undo variable set = restore previous value (or unset if was unset) - let previous: Option = op.undo_data.as_ref() + let previous: Option = op + .undo_data + .as_ref() .and_then(|d| serde_json::from_slice(d).ok()) .unwrap_or(None); match previous { @@ -942,7 +960,9 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::UnsetVariable => { // Undo unset = restore the variable to its previous value - let previous: Option = op.undo_data.as_ref() + let previous: Option = op + .undo_data + .as_ref() .and_then(|d| serde_json::from_slice(d).ok()) .unwrap_or(None); if let Some(val) = previous { @@ -951,10 +971,12 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::Chmod => { // Undo chmod = restore previous mode from undo_data - let mode_bytes = op.undo_data.as_ref().context("Missing mode data for undo chmod")?; - let old_mode = u32::from_le_bytes( - mode_bytes[..4].try_into().context("Invalid mode data")? - ); + let mode_bytes = op + .undo_data + .as_ref() + .context("Missing mode data for undo chmod")?; + let old_mode = + u32::from_le_bytes(mode_bytes[..4].try_into().context("Invalid mode data")?); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -964,7 +986,9 @@ pub fn undo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::Chown => { // Undo chown = restore previous uid:gid from undo_data - let uid_gid_str = op.undo_data.as_ref() + let uid_gid_str = op + .undo_data + .as_ref() .map(|d| String::from_utf8_lossy(d).to_string()) .context("Missing uid:gid data for undo chown")?; #[cfg(unix)] @@ -1084,11 +1108,15 @@ pub fn redo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::FileAppended => { // Cannot redo append without knowing what was appended - anyhow::bail!("FileAppended redo not yet implemented (would need appended content)"); + anyhow::bail!( + "FileAppended redo not yet implemented (would need appended content)" + ); } OperationType::CopyFile => { // Redo copy: copy again from src (stored in undo_data) to dst (path) - let src = op.undo_data.as_ref() + let src = op + .undo_data + .as_ref() .map(|d| String::from_utf8_lossy(d).to_string()) .context("Missing source path for redo cp")?; let src_path = state.resolve_path(&src); @@ -1106,7 +1134,9 @@ pub fn redo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::Symlink => { // Redo symlink: create symlink again - let target = op.undo_data.as_ref() + let target = op + .undo_data + .as_ref() .map(|d| String::from_utf8_lossy(d).to_string()) .context("Missing symlink target for redo")?; #[cfg(unix)] @@ -1124,7 +1154,10 @@ pub fn redo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { // For redo, we need the value that was set, but we don't have it. // Redo of SetVariable is a no-op — variable state has moved on. // This is correct: variable redo is best-effort; filesystem redo is exact. - println!("{}", "Variable set cannot be exactly redone (state-dependent)".bright_yellow()); + println!( + "{}", + "Variable set cannot be exactly redone (state-dependent)".bright_yellow() + ); } OperationType::UnsetVariable => { // Redo unset: unset the variable again @@ -1133,15 +1166,24 @@ pub fn redo(state: &mut ShellState, count: usize, verbose: bool) -> Result<()> { } OperationType::Chmod => { // Redo chmod: cannot exactly redo without storing new mode - println!("{}", "chmod cannot be exactly redone (state-dependent)".bright_yellow()); + println!( + "{}", + "chmod cannot be exactly redone (state-dependent)".bright_yellow() + ); } OperationType::Chown => { // Redo chown: cannot exactly redo without storing new uid:gid - println!("{}", "chown cannot be exactly redone (state-dependent)".bright_yellow()); + println!( + "{}", + "chown cannot be exactly redone (state-dependent)".bright_yellow() + ); } OperationType::HardwareErase | OperationType::Obliterate => { // Cannot redo - irreversible operations - anyhow::bail!("{:?} cannot be redone - operation is irreversible", op.op_type); + anyhow::bail!( + "{:?} cannot be redone - operation is irreversible", + op.op_type + ); } } @@ -1197,8 +1239,13 @@ pub fn history(state: &ShellState, count: usize, show_proofs: bool) -> Result<() for op in history.iter().rev() { let status = if op.undone { - format!("[undone by {}]", op.undone_by.map(|u| u.to_string()[..8].to_string()).unwrap_or_default()) - .bright_red() + format!( + "[undone by {}]", + op.undone_by + .map(|u| u.to_string()[..8].to_string()) + .unwrap_or_default() + ) + .bright_red() } else { "".normal() }; @@ -1326,7 +1373,11 @@ pub fn commit_transaction(state: &mut ShellState) -> Result<()> { /// # Ok::<(), anyhow::Error>(()) /// ``` pub fn rollback_transaction(state: &mut ShellState) -> Result<()> { - let ops: Vec<_> = state.current_transaction_ops().iter().map(|o| (*o).clone()).collect(); + let ops: Vec<_> = state + .current_transaction_ops() + .iter() + .map(|o| (*o).clone()) + .collect(); if ops.is_empty() { println!("{}", "Nothing to rollback".bright_yellow()); @@ -1342,12 +1393,8 @@ pub fn rollback_transaction(state: &mut ShellState) -> Result<()> { let path = state.resolve_path(&op.path); let result = match inverse_type { - OperationType::Rmdir => { - fs::remove_dir(&path).context("Failed to remove directory") - } - OperationType::Mkdir => { - fs::create_dir(&path).context("Failed to create directory") - } + OperationType::Rmdir => fs::remove_dir(&path).context("Failed to remove directory"), + OperationType::Mkdir => fs::create_dir(&path).context("Failed to create directory"), OperationType::DeleteFile => { fs::remove_file(&path).context("Failed to remove file") } @@ -1397,27 +1444,43 @@ pub fn rollback_transaction(state: &mut ShellState) -> Result<()> { if let Some(target_bytes) = op.undo_data.as_ref() { let target = String::from_utf8_lossy(target_bytes).to_string(); #[cfg(unix)] - { std::os::unix::fs::symlink(&target, &path).context("Failed to re-create symlink") } + { + std::os::unix::fs::symlink(&target, &path) + .context("Failed to re-create symlink") + } #[cfg(not(unix))] - { Err(anyhow::anyhow!("Symbolic links not supported on this platform")) } + { + Err(anyhow::anyhow!( + "Symbolic links not supported on this platform" + )) + } } else { Err(anyhow::anyhow!("Missing symlink target for rollback")) } } OperationType::SetVariable => { // Rollback variable set: restore previous value - let previous: Option = op.undo_data.as_ref() + let previous: Option = op + .undo_data + .as_ref() .and_then(|d| serde_json::from_slice(d).ok()) .unwrap_or(None); match previous { - Some(val) => { state.variables.insert(op.path.clone(), val); } - None => { state.variables.remove(&op.path); state.exported_vars.remove(&op.path); } + Some(val) => { + state.variables.insert(op.path.clone(), val); + } + None => { + state.variables.remove(&op.path); + state.exported_vars.remove(&op.path); + } } Ok(()) } OperationType::UnsetVariable => { // Rollback unset: restore the variable - let previous: Option = op.undo_data.as_ref() + let previous: Option = op + .undo_data + .as_ref() .and_then(|d| serde_json::from_slice(d).ok()) .unwrap_or(None); if let Some(val) = previous { @@ -1434,7 +1497,8 @@ pub fn rollback_transaction(state: &mut ShellState) -> Result<()> { { use std::os::unix::fs::PermissionsExt; let perms = fs::Permissions::from_mode(old_mode); - fs::set_permissions(&path, perms).context("Failed to restore permissions") + fs::set_permissions(&path, perms) + .context("Failed to restore permissions") } #[cfg(not(unix))] Ok(()) @@ -1459,7 +1523,10 @@ pub fn rollback_transaction(state: &mut ShellState) -> Result<()> { .context("Path contains null bytes")?; let ret = unsafe { libc::chown(c_path.as_ptr(), uid, gid) }; if ret != 0 { - Err(anyhow::anyhow!("Failed to restore ownership: {}", std::io::Error::last_os_error())) + Err(anyhow::anyhow!( + "Failed to restore ownership: {}", + std::io::Error::last_os_error() + )) } else { Ok(()) } @@ -1588,10 +1655,7 @@ pub fn show_graph(state: &ShellState) -> Result<()> { println!(" │ (no operations)"); println!(" ▼"); println!("┌─────────────────────────────────────┐"); - println!( - "│ {} │", - "[current state] ◄── YOU ARE HERE".bright_yellow() - ); + println!("│ {} │", "[current state] ◄── YOU ARE HERE".bright_yellow()); println!("└─────────────────────────────────────┘"); } @@ -1606,7 +1670,9 @@ pub fn show_graph(state: &ShellState) -> Result<()> { print!(" → "); } // Defensive: handle operations without inverses (shouldn't happen currently) - let inverse_str = op.op_type.inverse() + let inverse_str = op + .op_type + .inverse() .map(|inv| format!("{} {}", inv, op.path)) .unwrap_or_else(|| format!("[non-reversible: {}]", op.path)); print!("{}", inverse_str.bright_yellow()); @@ -1681,7 +1747,9 @@ pub fn fg(state: &mut ShellState, job_spec: Option<&str>) -> Result<()> { let spec = job_spec.unwrap_or("%+"); // Find the job - let job = state.jobs.get_job(spec) + let job = state + .jobs + .get_job(spec) .ok_or_else(|| anyhow::anyhow!("fg: no such job: {}", spec))?; let pgid = job.pgid; @@ -1731,7 +1799,11 @@ pub fn fg(state: &mut ShellState, job_spec: Option<&str>) -> Result<()> { // Update job state based on wait result if unsafe { libc::WIFSTOPPED(status) } { state.jobs.update_job_state(pgid, JobState::Stopped); - println!("\n[{}]+ Stopped {}", job_id, job_cmd.trim_end_matches(" &")); + println!( + "\n[{}]+ Stopped {}", + job_id, + job_cmd.trim_end_matches(" &") + ); } else { // Job completed - remove from table state.jobs.remove_job(job_id); @@ -1761,7 +1833,9 @@ pub fn bg(state: &mut ShellState, job_spec: Option<&str>) -> Result<()> { let spec = job_spec.unwrap_or("%+"); // Find the job - let job = state.jobs.get_job(spec) + let job = state + .jobs + .get_job(spec) .ok_or_else(|| anyhow::anyhow!("bg: no such job: {}", spec))?; let pgid = job.pgid; @@ -1815,7 +1889,9 @@ pub fn bg(state: &mut ShellState, job_spec: Option<&str>) -> Result<()> { /// Returns error if job does not exist or signal sending fails. pub fn kill_job(state: &mut ShellState, signal: Option<&str>, job_spec: &str) -> Result<()> { // Find the job - let job = state.jobs.get_job(job_spec) + let job = state + .jobs + .get_job(job_spec) .ok_or_else(|| anyhow::anyhow!("kill: no such job: {}", job_spec))?; let pgid = job.pgid; @@ -1914,11 +1990,7 @@ fn parse_signal(sig_str: &str) -> Result { /// commands::hardware_erase(&mut state, "/dev/nvme0n1", Some("nvme-format"))?; /// # Ok::<(), anyhow::Error>(()) /// ``` -pub fn hardware_erase( - state: &mut ShellState, - device: &str, - method: Option<&str>, -) -> Result<()> { +pub fn hardware_erase(state: &mut ShellState, device: &str, method: Option<&str>) -> Result<()> { use crate::confirmation::{confirm_destructive_operation, ConfirmationLevel}; use crate::secure_erase::{ ata_secure_erase, check_ata_secure_erase_support, check_nvme_sanitize_support, @@ -1926,8 +1998,7 @@ pub fn hardware_erase( }; // Detect drive type - let drive_type = detect_drive_type(device) - .context("Failed to detect drive type")?; + let drive_type = detect_drive_type(device).context("Failed to detect drive type")?; // Determine method let erase_method = match (drive_type, method) { @@ -1950,11 +2021,7 @@ pub fn hardware_erase( }; // CRITICAL: Get user confirmation with device-level warnings - let confirmed = confirm_destructive_operation( - ConfirmationLevel::Device, - device, - erase_method, - )?; + let confirmed = confirm_destructive_operation(ConfirmationLevel::Device, device, erase_method)?; if !confirmed { println!(); @@ -1979,7 +2046,10 @@ pub fn hardware_erase( "nvme-format" => { println!(); - println!("{}", "🔥 Executing NVMe Format (Crypto Erase)...".bright_cyan()); + println!( + "{}", + "🔥 Executing NVMe Format (Crypto Erase)...".bright_cyan() + ); nvme_format_crypto(device)?; println!(); println!("{}", "✓ Device securely erased".bright_green().bold()); @@ -1992,7 +2062,10 @@ pub fn hardware_erase( } println!(); - println!("{}", "🔥 Executing NVMe Sanitize (this may take hours)...".bright_cyan()); + println!( + "{}", + "🔥 Executing NVMe Sanitize (this may take hours)...".bright_cyan() + ); nvme_sanitize(device, true)?; println!(); println!("{}", "✓ Device securely erased".bright_green().bold()); @@ -2051,10 +2124,7 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu use crate::proof_refs::ProofReference; use crate::state::OperationType; - println!( - "{}", - "═══ Proof-Annotated Dry Run ═══".bright_blue().bold() - ); + println!("{}", "═══ Proof-Annotated Dry Run ═══".bright_blue().bold()); println!(); let desc = cmd.description(); @@ -2062,38 +2132,51 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu // Get operation type and proof reference let (op_type, proof_ref) = match cmd { - crate::parser::Command::Mkdir { .. } => { - (Some(OperationType::Mkdir), Some(ProofReference::for_operation(OperationType::Mkdir))) - } - crate::parser::Command::Rmdir { .. } => { - (Some(OperationType::Rmdir), Some(ProofReference::for_operation(OperationType::Rmdir))) - } - crate::parser::Command::Touch { .. } => { - (Some(OperationType::CreateFile), Some(ProofReference::for_operation(OperationType::CreateFile))) - } - crate::parser::Command::Rm { .. } => { - (Some(OperationType::DeleteFile), Some(ProofReference::for_operation(OperationType::DeleteFile))) - } - crate::parser::Command::Cp { .. } => { - (Some(OperationType::CopyFile), Some(ProofReference::for_operation(OperationType::CopyFile))) - } - crate::parser::Command::Mv { .. } => { - (Some(OperationType::Move), Some(ProofReference::for_operation(OperationType::Move))) - } - crate::parser::Command::Ln { .. } => { - (Some(OperationType::Symlink), Some(ProofReference::for_operation(OperationType::Symlink))) - } - crate::parser::Command::Chmod { .. } => { - (Some(OperationType::Chmod), Some(ProofReference::for_operation(OperationType::Chmod))) - } - crate::parser::Command::Chown { .. } => { - (Some(OperationType::Chown), Some(ProofReference::for_operation(OperationType::Chown))) - } + crate::parser::Command::Mkdir { .. } => ( + Some(OperationType::Mkdir), + Some(ProofReference::for_operation(OperationType::Mkdir)), + ), + crate::parser::Command::Rmdir { .. } => ( + Some(OperationType::Rmdir), + Some(ProofReference::for_operation(OperationType::Rmdir)), + ), + crate::parser::Command::Touch { .. } => ( + Some(OperationType::CreateFile), + Some(ProofReference::for_operation(OperationType::CreateFile)), + ), + crate::parser::Command::Rm { .. } => ( + Some(OperationType::DeleteFile), + Some(ProofReference::for_operation(OperationType::DeleteFile)), + ), + crate::parser::Command::Cp { .. } => ( + Some(OperationType::CopyFile), + Some(ProofReference::for_operation(OperationType::CopyFile)), + ), + crate::parser::Command::Mv { .. } => ( + Some(OperationType::Move), + Some(ProofReference::for_operation(OperationType::Move)), + ), + crate::parser::Command::Ln { .. } => ( + Some(OperationType::Symlink), + Some(ProofReference::for_operation(OperationType::Symlink)), + ), + crate::parser::Command::Chmod { .. } => ( + Some(OperationType::Chmod), + Some(ProofReference::for_operation(OperationType::Chmod)), + ), + crate::parser::Command::Chown { .. } => ( + Some(OperationType::Chown), + Some(ProofReference::for_operation(OperationType::Chown)), + ), _ => (None, None), }; if let Some(ref proof) = proof_ref { - println!(" {} {}", "Theorem:".bright_white().bold(), proof.theorem.bright_cyan()); + println!( + " {} {}", + "Theorem:".bright_white().bold(), + proof.theorem.bright_cyan() + ); } println!(); @@ -2108,12 +2191,20 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu let path_free = !resolved.exists(); println!( " {} Parent exists: {}", - if parent_exists { "✓".bright_green() } else { "✗".bright_red() }, + if parent_exists { + "✓".bright_green() + } else { + "✗".bright_red() + }, parent.map_or("(root)".to_string(), |p| p.display().to_string()) ); println!( " {} Path doesn't exist: {}", - if path_free { "✓".bright_green() } else { "✗".bright_red() }, + if path_free { + "✓".bright_green() + } else { + "✗".bright_red() + }, resolved.display() ); } @@ -2123,7 +2214,11 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu let exists = resolved.exists(); println!( " {} Path exists: {}", - if exists { "✓".bright_green() } else { "✗".bright_red() }, + if exists { + "✓".bright_green() + } else { + "✗".bright_red() + }, resolved.display() ); } @@ -2141,28 +2236,44 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu let exists = resolved.exists(); println!( " {} Path exists: {}", - if exists { "✓".bright_green() } else { "✗".bright_red() }, + if exists { + "✓".bright_green() + } else { + "✗".bright_red() + }, resolved.display() ); } - crate::parser::Command::Cp { src, dst, .. } | crate::parser::Command::Mv { src, dst, .. } => { + crate::parser::Command::Cp { src, dst, .. } + | crate::parser::Command::Mv { src, dst, .. } => { let src_exp = crate::parser::expand_variables(src, state); let dst_exp = crate::parser::expand_variables(dst, state); let src_resolved = state.resolve_path(&src_exp); let dst_resolved = state.resolve_path(&dst_exp); println!( " {} Source exists: {}", - if src_resolved.exists() { "✓".bright_green() } else { "✗".bright_red() }, + if src_resolved.exists() { + "✓".bright_green() + } else { + "✗".bright_red() + }, src_resolved.display() ); println!( " {} Dest doesn't exist: {}", - if !dst_resolved.exists() { "✓".bright_green() } else { "✗".bright_red() }, + if !dst_resolved.exists() { + "✓".bright_green() + } else { + "✗".bright_red() + }, dst_resolved.display() ); } _ => { - println!(" {} No preconditions (non-filesystem command)", "○".bright_yellow()); + println!( + " {} No preconditions (non-filesystem command)", + "○".bright_yellow() + ); } } println!(); @@ -2174,22 +2285,28 @@ pub fn explain_command(cmd: &crate::parser::Command, state: &ShellState) -> Resu println!( " fs ─── {}({}) ───▶ fs'", format!("{}", ot).bright_green(), - desc.split_whitespace().skip(1).collect::>().join(", ") + desc.split_whitespace() + .skip(1) + .collect::>() + .join(", ") ); if let Some(inv) = inverse { println!(); println!(" {}", "Inverse operation:".bright_yellow().bold()); - println!( - " fs' ─── {} ───▶ fs", - format!("{}", inv).bright_red(), - ); + println!(" fs' ─── {} ───▶ fs", format!("{}", inv).bright_red(),); println!( " Proof: {} {}", - proof_ref.as_ref().map_or("(none)", |p| p.description).bright_white(), + proof_ref + .as_ref() + .map_or("(none)", |p| p.description) + .bright_white(), "[QED in 6 systems]".bright_green().bold() ); } else { - println!(" {} This operation is self-inverse (restores previous value)", "↺".bright_cyan()); + println!( + " {} This operation is self-inverse (restores previous value)", + "↺".bright_cyan() + ); } } else { println!(" (not a tracked filesystem operation)"); @@ -2286,14 +2403,14 @@ pub fn restore(state: &mut ShellState, name: &str) -> Result<()> { /// List all named checkpoints. pub fn list_checkpoints(state: &ShellState) -> Result<()> { if state.checkpoints.is_empty() { - println!("{}", "No checkpoints saved. Use 'checkpoint ' to create one.".bright_black()); + println!( + "{}", + "No checkpoints saved. Use 'checkpoint ' to create one.".bright_black() + ); return Ok(()); } - println!( - "{}", - "═══ Checkpoints ═══".bright_blue().bold() - ); + println!("{}", "═══ Checkpoints ═══".bright_blue().bold()); let current = state.history.len(); let mut sorted: Vec<_> = state.checkpoints.iter().collect(); @@ -2346,7 +2463,10 @@ pub fn diff_state(state: &ShellState, target_op: usize) -> Result<()> { let action; match op.op_type { - OperationType::Mkdir | OperationType::CreateFile | OperationType::CopyFile | OperationType::Symlink => { + OperationType::Mkdir + | OperationType::CreateFile + | OperationType::CopyFile + | OperationType::Symlink => { symbol = "-"; color_fn = |s: &str| s.bright_red(); action = format!("would be removed — {} at op:{}", op.op_type, op_idx); @@ -2358,7 +2478,9 @@ pub fn diff_state(state: &ShellState, target_op: usize) -> Result<()> { action = format!("would be restored — {} at op:{}", op.op_type, op_idx); deletions += 1; // counts as a change } - OperationType::WriteFile | OperationType::FileTruncated | OperationType::FileAppended => { + OperationType::WriteFile + | OperationType::FileTruncated + | OperationType::FileAppended => { symbol = "~"; color_fn = |s: &str| s.bright_yellow(); action = format!("content would revert — {} at op:{}", op.op_type, op_idx); @@ -2457,7 +2579,11 @@ pub fn replay(state: &ShellState, start: usize, end: usize) -> Result<()> { ); println!( " State: fs{} ──▶ fs{}", - if idx == 0 { "₀".to_string() } else { format!("_{}", idx) }, + if idx == 0 { + "₀".to_string() + } else { + format!("_{}", idx) + }, format!("_{}", idx + 1) ); println!( @@ -2465,11 +2591,7 @@ pub fn replay(state: &ShellState, start: usize, end: usize) -> Result<()> { proof.theorem.bright_cyan(), "✓".bright_green() ); - println!( - " [{}] {}%", - bar.bright_green(), - (step * 100) / total - ); + println!(" [{}] {}%", bar.bright_green(), (step * 100) / total); println!(); // Brief pause for animation effect (50ms per step) @@ -2495,12 +2617,11 @@ pub fn replay(state: &ShellState, start: usize, end: usize) -> Result<()> { println!("{}", "Full undo path:".bright_yellow()); for idx in (start..end).rev() { let op = &state.history[idx]; - let inv = op.op_type.inverse().map_or("(none)".to_string(), |t| format!("{}", t)); - print!( - " {} {}", - inv.bright_red(), - op.path - ); + let inv = op + .op_type + .inverse() + .map_or("(none)".to_string(), |t| format!("{}", t)); + print!(" {} {}", inv.bright_red(), op.path); if idx > start { print!(" →"); } diff --git a/impl/rust-cli/src/commands/secure_deletion.rs b/impl/rust-cli/src/commands/secure_deletion.rs index 8d1131c8..4475186b 100644 --- a/impl/rust-cli/src/commands/secure_deletion.rs +++ b/impl/rust-cli/src/commands/secure_deletion.rs @@ -32,10 +32,10 @@ //! GDPR Article 17 ("right to erasure") on cooperating filesystems. For //! hardware-level guarantees use [`crate::secure_erase`]. -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use colored::Colorize; use std::fs::{self, OpenOptions}; -use std::io::{Read, Write, Seek, SeekFrom, BufRead}; +use std::io::{BufRead, Read, Seek, SeekFrom, Write}; use std::path::Path; use crate::state::{Operation, OperationType, ShellState}; @@ -239,7 +239,10 @@ pub fn obliterate(state: &mut ShellState, path: &str, verbose: bool, force: bool // CRITICAL: Require confirmation for irreversible operation if !force { eprintln!(); - eprintln!("{}", "⚠️ WARNING: IRREVERSIBLE DELETION".bright_red().bold()); + eprintln!( + "{}", + "⚠️ WARNING: IRREVERSIBLE DELETION".bright_red().bold() + ); eprintln!(); eprintln!(" File: {}", full_path.display().to_string().bright_cyan()); eprintln!(" Size: {} bytes", file_size.to_string().bright_yellow()); @@ -247,7 +250,10 @@ pub fn obliterate(state: &mut ShellState, path: &str, verbose: bool, force: bool eprintln!("This will:"); eprintln!(" 1. Overwrite with 3-pass NIST SP 800-88 Purge pattern (random, 0x00, 0xFF)"); eprintln!(" 2. Delete the file"); - eprintln!(" 3. Make recovery {} impossible on in-place filesystems", "cryptographically".bright_red()); + eprintln!( + " 3. Make recovery {} impossible on in-place filesystems", + "cryptographically".bright_red() + ); eprintln!(" (CoW filesystems like btrfs/ZFS/APFS and SSDs with FTL"); eprintln!(" remapping require hardware-level erase — see `secure_erase`)"); eprintln!(); @@ -281,11 +287,20 @@ pub fn obliterate(state: &mut ShellState, path: &str, verbose: bool, force: bool "obliterate".bright_red().bold(), path ); - println!("{}", "✓ File securely obliterated (CANNOT be undone)".bright_red()); + println!( + "{}", + "✓ File securely obliterated (CANNOT be undone)".bright_red() + ); if verbose { - println!(" {} DoD 5220.22-M 3-pass overwrite", "Method:".bright_black()); - println!(" {} GDPR Article 17 compliant", "Compliance:".bright_black()); + println!( + " {} DoD 5220.22-M 3-pass overwrite", + "Method:".bright_black() + ); + println!( + " {} GDPR Article 17 compliant", + "Compliance:".bright_black() + ); println!(" {} IRREVERSIBLE", "Undo:".bright_black()); } @@ -311,7 +326,12 @@ pub fn obliterate(state: &mut ShellState, path: &str, verbose: bool, force: bool /// secure_deletion::obliterate_dir(&mut state, "sensitive_data/", false, false)?; /// # Ok::<(), anyhow::Error>(()) /// ``` -pub fn obliterate_dir(state: &mut ShellState, path: &str, verbose: bool, force: bool) -> Result<()> { +pub fn obliterate_dir( + state: &mut ShellState, + path: &str, + verbose: bool, + force: bool, +) -> Result<()> { let full_path = state.resolve_path(path); if !full_path.exists() { @@ -337,11 +357,22 @@ pub fn obliterate_dir(state: &mut ShellState, path: &str, verbose: bool, force: // CRITICAL: Require confirmation if !force { eprintln!(); - eprintln!("{}", "⚠️ WARNING: IRREVERSIBLE DIRECTORY DELETION".bright_red().bold()); + eprintln!( + "{}", + "⚠️ WARNING: IRREVERSIBLE DIRECTORY DELETION" + .bright_red() + .bold() + ); eprintln!(); - eprintln!(" Directory: {}", full_path.display().to_string().bright_cyan()); + eprintln!( + " Directory: {}", + full_path.display().to_string().bright_cyan() + ); eprintln!(" Files: {}", file_count.to_string().bright_yellow()); - eprintln!(" Total size: {} bytes", total_size.to_string().bright_yellow()); + eprintln!( + " Total size: {} bytes", + total_size.to_string().bright_yellow() + ); eprintln!(); eprintln!("{}", "This operation CANNOT be undone.".bright_red().bold()); eprintln!(); @@ -354,8 +385,8 @@ pub fn obliterate_dir(state: &mut ShellState, path: &str, verbose: bool, force: // Obliterate all files in directory tree let mut obliterated = 0usize; - for entry in walkdir::WalkDir::new(&full_path) - .contents_first(true) // Process files before directories + for entry in walkdir::WalkDir::new(&full_path).contents_first(true) + // Process files before directories { let entry = entry?; let entry_path = entry.path(); @@ -390,7 +421,11 @@ pub fn obliterate_dir(state: &mut ShellState, path: &str, verbose: bool, force: ); println!( "{}", - format!("✓ {} files securely obliterated (CANNOT be undone)", obliterated).bright_red() + format!( + "✓ {} files securely obliterated (CANNOT be undone)", + obliterated + ) + .bright_red() ); Ok(()) @@ -399,8 +434,8 @@ pub fn obliterate_dir(state: &mut ShellState, path: &str, verbose: bool, force: #[cfg(test)] mod tests { use super::*; - use tempfile::TempDir; use std::path::PathBuf; + use tempfile::TempDir; #[test] fn test_secure_overwrite_3pass() { diff --git a/impl/rust-cli/src/confirmation.rs b/impl/rust-cli/src/confirmation.rs index ccd49ac6..fc17ed0b 100644 --- a/impl/rust-cli/src/confirmation.rs +++ b/impl/rust-cli/src/confirmation.rs @@ -3,9 +3,9 @@ //! //! SAFETY CRITICAL: Prevents accidental data destruction -use anyhow::{Result, bail}; -use std::io::{self, Write}; +use anyhow::{bail, Result}; use colored::Colorize; +use std::io::{self, Write}; /// Confirmation level for operations #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -38,7 +38,10 @@ fn confirm_file_deletion(path: &str, method: &str) -> Result { println!(" File: {}", path.bright_white()); println!(" Method: {} (NIST SP 800-88)", method.bright_cyan()); println!(); - println!("{}", " This operation is IRREVERSIBLE!".bright_red().bold()); + println!( + "{}", + " This operation is IRREVERSIBLE!".bright_red().bold() + ); println!(" The file cannot be recovered after deletion."); println!(); @@ -56,10 +59,16 @@ fn confirm_tree_deletion(path: &str, method: &str) -> Result { println!("{}", "⚠️ SECURE TREE DELETION".yellow().bold()); println!(); println!(" Directory: {}", path.bright_white()); - println!(" Files: {} files will be destroyed", file_count.to_string().bright_red().bold()); + println!( + " Files: {} files will be destroyed", + file_count.to_string().bright_red().bold() + ); println!(" Method: {} (NIST SP 800-88)", method.bright_cyan()); println!(); - println!("{}", " THIS OPERATION IS IRREVERSIBLE!".bright_red().bold()); + println!( + "{}", + " THIS OPERATION IS IRREVERSIBLE!".bright_red().bold() + ); println!(" All {} files will be permanently destroyed.", file_count); println!(); @@ -74,38 +83,80 @@ fn confirm_device_erase(device: &str, method: &str) -> Result { let device_info = get_device_info(device)?; println!(); - println!("{}", "🚨 CRITICAL WARNING - DEVICE-LEVEL SECURE ERASE 🚨".bright_red().bold()); + println!( + "{}", + "🚨 CRITICAL WARNING - DEVICE-LEVEL SECURE ERASE 🚨" + .bright_red() + .bold() + ); println!(); println!("{}", "═".repeat(60).bright_red()); println!(); - println!(" {}", "THIS WILL ERASE THE ENTIRE DEVICE!".bright_red().bold()); - println!(" {}", "ALL DATA ON THE DEVICE WILL BE PERMANENTLY DESTROYED!".bright_red().bold()); + println!( + " {}", + "THIS WILL ERASE THE ENTIRE DEVICE!".bright_red().bold() + ); + println!( + " {}", + "ALL DATA ON THE DEVICE WILL BE PERMANENTLY DESTROYED!" + .bright_red() + .bold() + ); println!(); println!("{}", "═".repeat(60).bright_red()); println!(); println!(" Device: {}", device.bright_white().bold()); println!(" Type: {}", device_info.drive_type.bright_yellow()); println!(" Size: {}", device_info.size.bright_yellow()); - println!(" Method: {} (NIST SP 800-88 Purge)", method.bright_cyan()); + println!( + " Method: {} (NIST SP 800-88 Purge)", + method.bright_cyan() + ); println!(); println!(" Mounted partitions:"); for mount in &device_info.mounts { - println!(" {} → {}", mount.partition.bright_red(), mount.mount_point.bright_white()); + println!( + " {} → {}", + mount.partition.bright_red(), + mount.mount_point.bright_white() + ); } println!(); println!("{}", " SAFETY CHECKS:".bright_yellow().bold()); - println!(" {} System drive check", if device_info.is_system_drive { "❌ SYSTEM DRIVE DETECTED!".bright_red().bold() } else { "✓ Not system drive".green() }); - println!(" {} Mount check", if device_info.mounts.is_empty() { "✓ Device unmounted".green() } else { "⚠️ Device has mounted partitions!".bright_red().bold() }); + println!( + " {} System drive check", + if device_info.is_system_drive { + "❌ SYSTEM DRIVE DETECTED!".bright_red().bold() + } else { + "✓ Not system drive".green() + } + ); + println!( + " {} Mount check", + if device_info.mounts.is_empty() { + "✓ Device unmounted".green() + } else { + "⚠️ Device has mounted partitions!".bright_red().bold() + } + ); println!(); if device_info.is_system_drive { - println!("{}", "❌ ABORTED: Cannot erase system drive!".bright_red().bold()); + println!( + "{}", + "❌ ABORTED: Cannot erase system drive!".bright_red().bold() + ); println!(); return Ok(false); } if !device_info.mounts.is_empty() { - println!("{}", "⚠️ WARNING: Device has mounted partitions!".bright_yellow().bold()); + println!( + "{}", + "⚠️ WARNING: Device has mounted partitions!" + .bright_yellow() + .bold() + ); println!(" You must unmount all partitions before secure erase."); println!(); @@ -128,7 +179,12 @@ fn confirm_device_erase(device: &str, method: &str) -> Result { if input != device { println!(); - println!("{}", "❌ Device name mismatch - operation CANCELLED".bright_red().bold()); + println!( + "{}", + "❌ Device name mismatch - operation CANCELLED" + .bright_red() + .bold() + ); println!(); return Ok(false); } @@ -257,9 +313,7 @@ fn get_mounted_partitions(device: &str) -> Result> { fn is_system_device(device: &str) -> Result { // Check if root filesystem is on this device - let output = std::process::Command::new("df") - .arg("/") - .output()?; + let output = std::process::Command::new("df").arg("/").output()?; let stdout = String::from_utf8_lossy(&output.stdout); diff --git a/impl/rust-cli/src/correction.rs b/impl/rust-cli/src/correction.rs index 856542e1..369370ec 100644 --- a/impl/rust-cli/src/correction.rs +++ b/impl/rust-cli/src/correction.rs @@ -57,10 +57,10 @@ pub fn levenshtein_distance(a: &str, b: &str) -> usize { matrix[i][j] = std::cmp::min( std::cmp::min( - matrix[i - 1][j] + 1, // Deletion - matrix[i][j - 1] + 1, // Insertion + matrix[i - 1][j] + 1, // Deletion + matrix[i][j - 1] + 1, // Insertion ), - matrix[i - 1][j - 1] + cost, // Substitution + matrix[i - 1][j - 1] + cost, // Substitution ); } } @@ -74,12 +74,12 @@ fn get_all_commands() -> Vec { // Built-in commands let builtins: Vec = vec![ - "cd", "exit", "undo", "redo", "history", "help", - "mkdir", "rmdir", "touch", "rm", - "test", "[", - "pwd", "echo", "export", "unset", "set", - "jobs", "fg", "bg", "kill", - ].iter().map(|s| s.to_string()).collect(); + "cd", "exit", "undo", "redo", "history", "help", "mkdir", "rmdir", "touch", "rm", "test", + "[", "pwd", "echo", "export", "unset", "set", "jobs", "fg", "bg", "kill", + ] + .iter() + .map(|s| s.to_string()) + .collect(); commands.extend(builtins); @@ -188,7 +188,8 @@ pub fn suggest_corrections(cmd: &str, limit: usize) -> Vec { matches.sort_by_key(|(dist, _)| *dist); // Take top N, return just the commands - matches.into_iter() + matches + .into_iter() .take(limit) .map(|(_, cmd)| cmd) .collect() diff --git a/impl/rust-cli/src/echidna_integration.rs b/impl/rust-cli/src/echidna_integration.rs index 30013e42..b8660364 100644 --- a/impl/rust-cli/src/echidna_integration.rs +++ b/impl/rust-cli/src/echidna_integration.rs @@ -393,7 +393,10 @@ mod tests { #[test] fn test_property_category_display() { - assert_eq!(format!("{}", PropertyCategory::Reversibility), "reversibility"); + assert_eq!( + format!("{}", PropertyCategory::Reversibility), + "reversibility" + ); assert_eq!(format!("{}", PropertyCategory::Security), "security"); } } diff --git a/impl/rust-cli/src/enhanced_repl.rs b/impl/rust-cli/src/enhanced_repl.rs index 9bcff073..514320e1 100644 --- a/impl/rust-cli/src/enhanced_repl.rs +++ b/impl/rust-cli/src/enhanced_repl.rs @@ -11,10 +11,10 @@ use anyhow::Result; use nu_ansi_term::{Color, Style}; use reedline::{ - default_emacs_keybindings, ColumnarMenu, Completer, DefaultHinter, DefaultValidator, - Emacs, FileBackedHistory, HistoryItem, KeyCode, KeyModifiers, MenuBuilder, Prompt, - PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, - ReedlineMenu, Signal, Span, StyledText, Suggestion, + default_emacs_keybindings, ColumnarMenu, Completer, DefaultHinter, DefaultValidator, Emacs, + FileBackedHistory, HistoryItem, KeyCode, KeyModifiers, MenuBuilder, Prompt, PromptEditMode, + PromptHistorySearch, PromptHistorySearchStatus, Reedline, ReedlineEvent, ReedlineMenu, Signal, + Span, StyledText, Suggestion, }; use std::borrow::Cow; use std::path::PathBuf; @@ -27,12 +27,60 @@ use crate::state::ShellState; /// Shell commands for completion const BUILTIN_COMMANDS: &[&str] = &[ - "cd", "pwd", "exit", "mkdir", "rmdir", "touch", "rm", "cp", "mv", "ln", "cat", "echo", "ls", - "undo", "redo", "history", "begin", "commit", "rollback", "help", - "true", "false", "read", "source", "set", "unset", "eval", "export", - "if", "then", "elif", "else", "fi", "while", "until", "for", "do", "done", "case", "esac", "in", - "test", "jobs", "fg", "bg", "kill", - "chmod", "chown", "explain", "checkpoint", "restore", "checkpoints", "diff", "replay", + "cd", + "pwd", + "exit", + "mkdir", + "rmdir", + "touch", + "rm", + "cp", + "mv", + "ln", + "cat", + "echo", + "ls", + "undo", + "redo", + "history", + "begin", + "commit", + "rollback", + "help", + "true", + "false", + "read", + "source", + "set", + "unset", + "eval", + "export", + "if", + "then", + "elif", + "else", + "fi", + "while", + "until", + "for", + "do", + "done", + "case", + "esac", + "in", + "test", + "jobs", + "fg", + "bg", + "kill", + "chmod", + "chown", + "explain", + "checkpoint", + "restore", + "checkpoints", + "diff", + "replay", ]; /// Custom completer for VSH @@ -71,7 +119,10 @@ fn complete_path(prefix: &str, pos: usize) -> Vec { } else if prefix.ends_with('/') { PathBuf::from(prefix) } else { - PathBuf::from(prefix).parent().unwrap_or_else(|| std::path::Path::new(".")).to_path_buf() + PathBuf::from(prefix) + .parent() + .unwrap_or_else(|| std::path::Path::new(".")) + .to_path_buf() }; let prefix_path = PathBuf::from(prefix); @@ -194,8 +245,7 @@ pub fn run(state: &mut ShellState) -> Result<()> { .join(".vsh_history"); let history = Box::new( - FileBackedHistory::with_file(1000, history_file) - .expect("Failed to initialize history"), + FileBackedHistory::with_file(1000, history_file).expect("Failed to initialize history"), ); // Set up completion menu @@ -236,10 +286,7 @@ pub fn run(state: &mut ShellState) -> Result<()> { loop { // Build prompt - let txn_name = state - .active_transaction - .as_ref() - .map(|t| t.name.clone()); + let txn_name = state.active_transaction.as_ref().map(|t| t.name.clone()); let undo_count = state.history.iter().filter(|o| !o.undone).count(); let cwd = std::env::current_dir() .ok() @@ -316,7 +363,11 @@ pub fn run(state: &mut ShellState) -> Result<()> { // Reedline handles Ctrl-C itself (returns Signal::CtrlC) so // we fire the trap synchronously here rather than relying on // the SIGINT flag. - if let Some(handler) = state.traps.get(crate::posix_builtins::TrapSignal::Int).map(|s| s.to_string()) { + if let Some(handler) = state + .traps + .get(crate::posix_builtins::TrapSignal::Int) + .map(|s| s.to_string()) + { if let Ok(cmd) = parser::parse_command(&handler) { if let Ok(ExecutionResult::Exit) = cmd.execute(state) { break; @@ -378,8 +429,7 @@ fn execute_line(state: &mut ShellState, input: &str) -> Result { // Handle execution result match result { ExecutionResult::Exit => Ok(true), - ExecutionResult::ExternalCommand { exit_code } - | ExecutionResult::Return { exit_code } => { + ExecutionResult::ExternalCommand { exit_code } | ExecutionResult::Return { exit_code } => { state.last_exit_code = exit_code; Ok(false) } diff --git a/impl/rust-cli/src/executable.rs b/impl/rust-cli/src/executable.rs index fea78ef1..c655cbb6 100644 --- a/impl/rust-cli/src/executable.rs +++ b/impl/rust-cli/src/executable.rs @@ -121,7 +121,11 @@ impl ExecutableCommand for Command { Ok(ExecutionResult::Success) } - Command::Cp { src, dst, redirects } => { + Command::Cp { + src, + dst, + redirects, + } => { let expanded_src = crate::parser::expand_variables(src, state); let expanded_dst = crate::parser::expand_variables(dst, state); if redirects.is_empty() { @@ -134,7 +138,11 @@ impl ExecutableCommand for Command { Ok(ExecutionResult::Success) } - Command::Mv { src, dst, redirects } => { + Command::Mv { + src, + dst, + redirects, + } => { let expanded_src = crate::parser::expand_variables(src, state); let expanded_dst = crate::parser::expand_variables(dst, state); if redirects.is_empty() { @@ -147,7 +155,11 @@ impl ExecutableCommand for Command { Ok(ExecutionResult::Success) } - Command::Ln { target, link, redirects } => { + Command::Ln { + target, + link, + redirects, + } => { let expanded_target = crate::parser::expand_variables(target, state); let expanded_link = crate::parser::expand_variables(link, state); if redirects.is_empty() { @@ -160,7 +172,11 @@ impl ExecutableCommand for Command { Ok(ExecutionResult::Success) } - Command::Chmod { mode, path, redirects } => { + Command::Chmod { + mode, + path, + redirects, + } => { let expanded_mode = crate::parser::expand_variables(mode, state); let expanded_path = crate::parser::expand_variables(path, state); if redirects.is_empty() { @@ -174,7 +190,11 @@ impl ExecutableCommand for Command { } #[cfg(unix)] - Command::Chown { owner, path, redirects } => { + Command::Chown { + owner, + path, + redirects, + } => { let expanded_owner = crate::parser::expand_variables(owner, state); let expanded_path = crate::parser::expand_variables(path, state); if redirects.is_empty() { @@ -242,7 +262,9 @@ impl ExecutableCommand for Command { // Navigation (built-ins but not reversible) Command::Ls { path, redirects } => { // Expand variables in path - let expanded_path = path.as_ref().map(|p| crate::parser::expand_variables(p, state)); + let expanded_path = path + .as_ref() + .map(|p| crate::parser::expand_variables(p, state)); if redirects.is_empty() { // Direct output to terminal @@ -317,7 +339,9 @@ impl ExecutableCommand for Command { use std::path::PathBuf; // Expand variables in path first - let expanded_path = path.as_ref().map(|p| crate::parser::expand_variables(p, state)); + let expanded_path = path + .as_ref() + .map(|p| crate::parser::expand_variables(p, state)); let target = if let Some(ref p) = expanded_path { if p == "-" { @@ -518,12 +542,17 @@ impl ExecutableCommand for Command { } // Pipeline commands (not reversible by default, but redirections are) - Command::Pipeline { stages, redirects, background } => { + Command::Pipeline { + stages, + redirects, + background, + } => { // Expand variables and command substitutions in all pipeline stages let expanded_stages: Result)>> = stages .iter() .map(|(program, args)| { - let expanded_program = crate::parser::expand_with_command_sub(program, state)?; + let expanded_program = + crate::parser::expand_with_command_sub(program, state)?; let expanded_args: Result> = args .iter() .map(|arg| crate::parser::expand_with_command_sub(arg, state)) @@ -537,7 +566,9 @@ impl ExecutableCommand for Command { // Background pipeline: launch first stage in background, pipe rest // For now, warn and run in foreground — full background pipeline // requires SIGCHLD handler and process group management for all stages - eprintln!("vsh: background pipelines not yet fully supported, running in foreground"); + eprintln!( + "vsh: background pipelines not yet fully supported, running in foreground" + ); } let exit_code = external::execute_pipeline(&expanded_stages, redirects, state) @@ -628,15 +659,20 @@ impl ExecutableCommand for Command { } // Shell builtins - - Command::Echo { args, no_newline, interpret_escapes, redirects } => { + Command::Echo { + args, + no_newline, + interpret_escapes, + redirects, + } => { let expanded_args: Vec = args .iter() .map(|a| crate::parser::expand_variables(a, state)) .collect(); let output = if *interpret_escapes { - expanded_args.join(" ") + expanded_args + .join(" ") .replace("\\n", "\n") .replace("\\t", "\t") .replace("\\\\", "\\") @@ -667,15 +703,15 @@ impl ExecutableCommand for Command { Ok(ExecutionResult::Success) } - Command::True => { - Ok(ExecutionResult::ExternalCommand { exit_code: 0 }) - } + Command::True => Ok(ExecutionResult::ExternalCommand { exit_code: 0 }), - Command::False => { - Ok(ExecutionResult::ExternalCommand { exit_code: 1 }) - } + Command::False => Ok(ExecutionResult::ExternalCommand { exit_code: 1 }), - Command::Read { var_names, prompt, redirects: _ } => { + Command::Read { + var_names, + prompt, + redirects: _, + } => { if let Some(p) = prompt { let expanded_prompt = crate::parser::expand_variables(p, state); eprint!("{}", expanded_prompt); @@ -697,15 +733,12 @@ impl ExecutableCommand for Command { let ifs = state .get_variable("IFS") .map(|s| s.to_string()) - .unwrap_or_else(|| { - crate::posix_builtins::DEFAULT_IFS.to_string() - }); + .unwrap_or_else(|| crate::posix_builtins::DEFAULT_IFS.to_string()); let fields = crate::posix_builtins::ifs_split(value, &ifs); for (i, var_name) in var_names.iter().enumerate() { - let expanded_var = - crate::parser::expand_variables(var_name, state); + let expanded_var = crate::parser::expand_variables(var_name, state); if i == var_names.len() - 1 { // Last variable gets the remainder. // Rejoin un-consumed fields with a single @@ -717,10 +750,7 @@ impl ExecutableCommand for Command { }; state.set_variable(expanded_var, remainder); } else { - let field = fields - .get(i) - .cloned() - .unwrap_or_default(); + let field = fields.get(i).cloned().unwrap_or_default(); state.set_variable(expanded_var, field); } } @@ -798,9 +828,18 @@ impl ExecutableCommand for Command { let enable = arg.starts_with('-'); for ch in arg[1..].chars() { match ch { - 'e' => state.set_variable("SHOPT_E".to_string(), if enable { "1" } else { "0" }.to_string()), - 'x' => state.set_variable("SHOPT_X".to_string(), if enable { "1" } else { "0" }.to_string()), - 'u' => state.set_variable("SHOPT_U".to_string(), if enable { "1" } else { "0" }.to_string()), + 'e' => state.set_variable( + "SHOPT_E".to_string(), + if enable { "1" } else { "0" }.to_string(), + ), + 'x' => state.set_variable( + "SHOPT_X".to_string(), + if enable { "1" } else { "0" }.to_string(), + ), + 'u' => state.set_variable( + "SHOPT_U".to_string(), + if enable { "1" } else { "0" }.to_string(), + ), _ => {} } } @@ -840,8 +879,12 @@ impl ExecutableCommand for Command { } // Control structures - - Command::If { condition, then_body, elif_parts, else_body } => { + Command::If { + condition, + then_body, + elif_parts, + else_body, + } => { // Execute condition and get exit code let cond_result = condition.execute(state)?; let cond_exit = match cond_result { @@ -888,7 +931,10 @@ impl ExecutableCommand for Command { loop { if iterations >= max_iterations { - return Err(anyhow::anyhow!("while: exceeded {} iterations (safety limit)", max_iterations)); + return Err(anyhow::anyhow!( + "while: exceeded {} iterations (safety limit)", + max_iterations + )); } iterations += 1; @@ -907,7 +953,10 @@ impl ExecutableCommand for Command { // Execute body last_result = execute_block(body, state)?; - if matches!(last_result, ExecutionResult::Exit | ExecutionResult::Return { .. }) { + if matches!( + last_result, + ExecutionResult::Exit | ExecutionResult::Return { .. } + ) { return Ok(last_result); } @@ -945,7 +994,10 @@ impl ExecutableCommand for Command { // Execute body last_result = execute_block(body, state)?; - if matches!(last_result, ExecutionResult::Exit | ExecutionResult::Return { .. }) { + if matches!( + last_result, + ExecutionResult::Exit | ExecutionResult::Return { .. } + ) { return Ok(last_result); } @@ -976,7 +1028,11 @@ impl ExecutableCommand for Command { } // Logical operators (short-circuit evaluation) - Command::LogicalOp { operator, left, right } => { + Command::LogicalOp { + operator, + left, + right, + } => { use crate::parser::LogicalOperator; // Execute left command @@ -997,7 +1053,9 @@ impl ExecutableCommand for Command { if left_exit_code == 0 { right.execute(state) } else { - Ok(ExecutionResult::ExternalCommand { exit_code: left_exit_code }) + Ok(ExecutionResult::ExternalCommand { + exit_code: left_exit_code, + }) } } LogicalOperator::Or => { @@ -1005,7 +1063,9 @@ impl ExecutableCommand for Command { if left_exit_code != 0 { right.execute(state) } else { - Ok(ExecutionResult::ExternalCommand { exit_code: left_exit_code }) + Ok(ExecutionResult::ExternalCommand { + exit_code: left_exit_code, + }) } } } @@ -1038,7 +1098,11 @@ impl ExecutableCommand for Command { } // Shell function definition - Command::FunctionDef { name, body, raw_body } => { + Command::FunctionDef { + name, + body, + raw_body, + } => { use crate::functions::{FunctionDef as FuncDef, SourceLocation}; let def = FuncDef { name: name.clone(), @@ -1155,18 +1219,13 @@ impl ExecutableCommand for Command { fn proof_reference(&self) -> Option { use crate::proof_refs::{ - MKDIR_RMDIR_REVERSIBLE, CREATE_DELETE_REVERSIBLE, - COPY_FILE_REVERSIBLE, MOVE_REVERSIBLE, SYMLINK_UNLINK_REVERSIBLE, - CHMOD_REVERSIBLE, CHOWN_REVERSIBLE, + CHMOD_REVERSIBLE, CHOWN_REVERSIBLE, COPY_FILE_REVERSIBLE, CREATE_DELETE_REVERSIBLE, + MKDIR_RMDIR_REVERSIBLE, MOVE_REVERSIBLE, SYMLINK_UNLINK_REVERSIBLE, }; match self { - Command::Mkdir { .. } | Command::Rmdir { .. } => { - Some(MKDIR_RMDIR_REVERSIBLE) - } - Command::Touch { .. } | Command::Rm { .. } => { - Some(CREATE_DELETE_REVERSIBLE) - } + Command::Mkdir { .. } | Command::Rmdir { .. } => Some(MKDIR_RMDIR_REVERSIBLE), + Command::Touch { .. } | Command::Rm { .. } => Some(CREATE_DELETE_REVERSIBLE), Command::Cp { .. } => Some(COPY_FILE_REVERSIBLE), Command::Mv { .. } => Some(MOVE_REVERSIBLE), Command::Ln { .. } => Some(SYMLINK_UNLINK_REVERSIBLE), @@ -1316,7 +1375,11 @@ impl ExecutableCommand for Command { } } - Command::LogicalOp { operator, left, right } => { + Command::LogicalOp { + operator, + left, + right, + } => { use crate::parser::LogicalOperator; let op_str = match operator { LogicalOperator::And => "&&", @@ -1365,12 +1428,10 @@ impl ExecutableCommand for Command { .collect(); format!("local {}", vars.join(" ")) } - Command::Trap { action, signals } => { - match action { - Some(a) => format!("trap '{}' {}", a, signals.join(" ")), - None => "trap".to_string(), - } - } + Command::Trap { action, signals } => match action { + Some(a) => format!("trap '{}' {}", a, signals.join(" ")), + None => "trap".to_string(), + }, Command::Alias { definitions } => { if definitions.is_empty() { "alias".to_string() @@ -1490,7 +1551,9 @@ fn execute_function_call( for (var_name, prev_value) in &frame.saved_vars { match prev_value { Some(val) => state.set_variable(var_name.clone(), val.clone()), - None => { state.unset_variable(var_name); } + None => { + state.unset_variable(var_name); + } } } } @@ -1539,7 +1602,12 @@ fn glob_match(pattern: &str, text: &str) -> bool { let mut p = pattern.chars().peekable(); let mut t = text.chars().peekable(); - glob_match_inner(&mut pattern.chars().collect::>(), &mut text.chars().collect::>(), 0, 0) + glob_match_inner( + &mut pattern.chars().collect::>(), + &mut text.chars().collect::>(), + 0, + 0, + ) } fn glob_match_inner(pattern: &[char], text: &[char], pi: usize, ti: usize) -> bool { diff --git a/impl/rust-cli/src/external.rs b/impl/rust-cli/src/external.rs index 8f322097..75257cb8 100644 --- a/impl/rust-cli/src/external.rs +++ b/impl/rust-cli/src/external.rs @@ -11,7 +11,7 @@ use std::process::{Command, Stdio}; use std::time::Duration; use crate::glob; -use crate::redirection::{Redirection, RedirectSetup}; +use crate::redirection::{RedirectSetup, Redirection}; use crate::signals; use crate::state::ShellState; @@ -156,19 +156,20 @@ pub fn execute_pipeline( let is_last = idx == stages.len() - 1; // Configure stdin - let stdin_cfg = if is_first { - // First stage: inherit from shell - Stdio::inherit() - } else { - // Middle/last stages: read from previous stdout. - // Invariant: when idx > 0, the previous iteration spawned a - // non-last child with Stdio::piped() stdout (line below) and - // stored child.stdout into prev_stdout after spawning. So the - // expect documents an unreachable branch rather than a TODO. - Stdio::from(prev_stdout.take().expect( - "prev_stdout invariant: previous non-last stage stored its piped stdout", - )) - }; + let stdin_cfg = + if is_first { + // First stage: inherit from shell + Stdio::inherit() + } else { + // Middle/last stages: read from previous stdout. + // Invariant: when idx > 0, the previous iteration spawned a + // non-last child with Stdio::piped() stdout (line below) and + // stored child.stdout into prev_stdout after spawning. So the + // expect documents an unreachable branch rather than a TODO. + Stdio::from(prev_stdout.take().expect( + "prev_stdout invariant: previous non-last stage stored its piped stdout", + )) + }; // Configure stdout. Pivot on redirect_setup directly: it is Some // iff redirects is non-empty (constructed above), so matching the @@ -345,8 +346,11 @@ fn stdio_config_from_redirects( let file_handle = File::create(&target) .with_context(|| format!("Failed to open output file: {}", target.display()))?; // Keep a clone for potential 2>&1 duplication - stdout_file_dup = Some(file_handle.try_clone() - .context("Failed to duplicate stdout file handle for 2>&1 tracking")?); + stdout_file_dup = Some( + file_handle + .try_clone() + .context("Failed to duplicate stdout file handle for 2>&1 tracking")?, + ); stdout_cfg = Stdio::from(file_handle); } @@ -356,9 +360,17 @@ fn stdio_config_from_redirects( .create(true) .append(true) .open(&target) - .with_context(|| format!("Failed to open output file for append: {}", target.display()))?; - stdout_file_dup = Some(file_handle.try_clone() - .context("Failed to duplicate stdout file handle for 2>&1 tracking")?); + .with_context(|| { + format!( + "Failed to open output file for append: {}", + target.display() + ) + })?; + stdout_file_dup = Some( + file_handle + .try_clone() + .context("Failed to duplicate stdout file handle for 2>&1 tracking")?, + ); stdout_cfg = Stdio::from(file_handle); } @@ -372,10 +384,7 @@ fn stdio_config_from_redirects( Redirection::ErrorOutput { file } | Redirection::ErrorAppend { file } => { let target = state.resolve_path(file); let file_handle = if matches!(redirect, Redirection::ErrorAppend { .. }) { - OpenOptions::new() - .create(true) - .append(true) - .open(&target) + OpenOptions::new().create(true).append(true).open(&target) } else { File::create(&target) } @@ -394,8 +403,11 @@ fn stdio_config_from_redirects( .try_clone() .context("Failed to duplicate file handle")?; - stdout_file_dup = Some(file_handle.try_clone() - .context("Failed to duplicate stdout file handle for 2>&1 tracking")?); + stdout_file_dup = Some( + file_handle + .try_clone() + .context("Failed to duplicate stdout file handle for 2>&1 tracking")?, + ); stdout_cfg = Stdio::from(file_handle); stderr_cfg = Stdio::from(file_handle2); } @@ -405,22 +417,23 @@ fn stdio_config_from_redirects( // POSIX processes redirections left-to-right, so at this point // stdout_file_dup reflects the current stdout target (if redirected) if let Some(ref f) = stdout_file_dup { - stderr_cfg = Stdio::from(f.try_clone() - .context("Failed to duplicate fd for 2>&1")?); + stderr_cfg = + Stdio::from(f.try_clone().context("Failed to duplicate fd for 2>&1")?); } else { // stdout is still inherited (terminal), so stderr inherits too stderr_cfg = Stdio::inherit(); } } - Redirection::HereDoc { content, expand, strip_tabs, .. } => { + Redirection::HereDoc { + content, + expand, + strip_tabs, + .. + } => { // Process here document content - let processed = crate::parser::process_heredoc_content( - content, - *strip_tabs, - *expand, - state, - )?; + let processed = + crate::parser::process_heredoc_content(content, *strip_tabs, *expand, state)?; // Write heredoc content to a pipe instead of a temp file (avoids // predictable temp path + symlink attacks entirely). @@ -428,13 +441,16 @@ fn stdio_config_from_redirects( let mut pipe_fds = [0i32; 2]; // SAFETY: pipe() is a POSIX syscall; pipe_fds is a valid 2-element array. if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } != 0 { - anyhow::bail!("Failed to create pipe for here document: {}", std::io::Error::last_os_error()); + anyhow::bail!( + "Failed to create pipe for here document: {}", + std::io::Error::last_os_error() + ); } // SAFETY: pipe_fds[1] is a valid file descriptor from pipe(). let mut write_end = unsafe { File::from_raw_fd(pipe_fds[1]) }; std::io::Write::write_all(&mut write_end, processed.as_bytes())?; drop(write_end); // Close write end so reader gets EOF - // SAFETY: pipe_fds[0] is a valid file descriptor from pipe(). + // SAFETY: pipe_fds[0] is a valid file descriptor from pipe(). let read_end = unsafe { File::from_raw_fd(pipe_fds[0]) }; stdin_cfg = Stdio::from(read_end); } @@ -453,13 +469,16 @@ fn stdio_config_from_redirects( let mut pipe_fds = [0i32; 2]; // SAFETY: pipe() is a POSIX syscall; pipe_fds is a valid 2-element array. if unsafe { libc::pipe(pipe_fds.as_mut_ptr()) } != 0 { - anyhow::bail!("Failed to create pipe for here string: {}", std::io::Error::last_os_error()); + anyhow::bail!( + "Failed to create pipe for here string: {}", + std::io::Error::last_os_error() + ); } // SAFETY: pipe_fds[1] is a valid file descriptor from pipe(). let mut write_end = unsafe { File::from_raw_fd(pipe_fds[1]) }; std::io::Write::write_all(&mut write_end, processed.as_bytes())?; drop(write_end); // Close write end so reader gets EOF - // SAFETY: pipe_fds[0] is a valid file descriptor from pipe(). + // SAFETY: pipe_fds[0] is a valid file descriptor from pipe(). let read_end = unsafe { File::from_raw_fd(pipe_fds[0]) }; stdin_cfg = Stdio::from(read_end); } @@ -492,8 +511,7 @@ fn expand_glob_args(args: &[String], _state: &ShellState) -> Result> let mut expanded: Vec = Vec::new(); // Get current working directory for glob expansion - let cwd = std::env::current_dir() - .context("Failed to get current working directory")?; + let cwd = std::env::current_dir().context("Failed to get current working directory")?; for arg in args { // Check if argument contains glob metacharacters @@ -725,10 +743,7 @@ mod tests { let temp = TempDir::new().unwrap(); let mut state = crate::state::ShellState::new(temp.path().to_str().unwrap()).unwrap(); - let stages = vec![ - ("true".to_string(), vec![]), - ("true".to_string(), vec![]), - ]; + let stages = vec![("true".to_string(), vec![]), ("true".to_string(), vec![])]; let exit_code = execute_pipeline(&stages, &[], &mut state).unwrap(); assert_eq!(exit_code, 0, "true | true should return 0"); } @@ -740,10 +755,7 @@ mod tests { let temp = TempDir::new().unwrap(); let mut state = crate::state::ShellState::new(temp.path().to_str().unwrap()).unwrap(); - let stages = vec![ - ("true".to_string(), vec![]), - ("false".to_string(), vec![]), - ]; + let stages = vec![("true".to_string(), vec![]), ("false".to_string(), vec![])]; let exit_code = execute_pipeline(&stages, &[], &mut state).unwrap(); assert_eq!(exit_code, 1, "true | false should return 1 (from false)"); } @@ -767,8 +779,8 @@ mod tests { #[test] fn test_pipeline_with_redirect() { // Test pipeline with output redirection - use tempfile::TempDir; use std::fs; + use tempfile::TempDir; let temp = TempDir::new().unwrap(); let mut state = crate::state::ShellState::new(temp.path().to_str().unwrap()).unwrap(); diff --git a/impl/rust-cli/src/friendly_errors.rs b/impl/rust-cli/src/friendly_errors.rs index b6ae2130..a9eda075 100644 --- a/impl/rust-cli/src/friendly_errors.rs +++ b/impl/rust-cli/src/friendly_errors.rs @@ -56,7 +56,10 @@ pub fn display_friendly_error(error: &anyhow::Error) { } fn handle_not_found_error(error_msg: &str) { - eprintln!("{}: The file or directory does not exist.", "vsh".bright_red().bold()); + eprintln!( + "{}: The file or directory does not exist.", + "vsh".bright_red().bold() + ); eprintln!(); // Try to extract path from error message @@ -98,11 +101,20 @@ fn handle_permission_error(error_msg: &str) { eprintln!(" {} Run as superuser", "sudo vsh".bright_yellow()); if let Some(path) = extract_path(error_msg) { if error_msg.contains("read") || error_msg.contains("open") { - eprintln!(" {} Make file readable", format!("chmod +r {}", path).bright_yellow()); + eprintln!( + " {} Make file readable", + format!("chmod +r {}", path).bright_yellow() + ); } else if error_msg.contains("write") { - eprintln!(" {} Make file writable", format!("chmod +w {}", path).bright_yellow()); + eprintln!( + " {} Make file writable", + format!("chmod +w {}", path).bright_yellow() + ); } else if error_msg.contains("execute") { - eprintln!(" {} Make file executable", format!("chmod +x {}", path).bright_yellow()); + eprintln!( + " {} Make file executable", + format!("chmod +x {}", path).bright_yellow() + ); } } } @@ -125,14 +137,23 @@ fn handle_command_not_found(error_msg: &str) { // Suggest package search eprintln!("Search for this command:"); - eprintln!(" {} (Fedora)", format!("dnf search {}", cmd).bright_yellow()); - eprintln!(" {} (Ubuntu/Debian)", format!("apt search {}", cmd).bright_yellow()); + eprintln!( + " {} (Fedora)", + format!("dnf search {}", cmd).bright_yellow() + ); + eprintln!( + " {} (Ubuntu/Debian)", + format!("apt search {}", cmd).bright_yellow() + ); eprintln!(" {} (Arch)", format!("pacman -Ss {}", cmd).bright_yellow()); } } fn handle_already_exists_error(error_msg: &str) { - eprintln!("{}: File or directory already exists.", "vsh".bright_red().bold()); + eprintln!( + "{}: File or directory already exists.", + "vsh".bright_red().bold() + ); eprintln!(); if let Some(path) = extract_path(error_msg) { @@ -140,7 +161,10 @@ fn handle_already_exists_error(error_msg: &str) { eprintln!(); eprintln!("To replace it:"); - eprintln!(" {} Remove first, then create", format!("rm {} && touch {}", path, path).bright_yellow()); + eprintln!( + " {} Remove first, then create", + format!("rm {} && touch {}", path, path).bright_yellow() + ); eprintln!(); eprintln!("Or use a different name:"); eprintln!(" {} Add suffix", format!("{}.new", path).bright_yellow()); @@ -148,7 +172,10 @@ fn handle_already_exists_error(error_msg: &str) { } fn handle_is_directory_error(error_msg: &str) { - eprintln!("{}: Expected a file but found a directory.", "vsh".bright_red().bold()); + eprintln!( + "{}: Expected a file but found a directory.", + "vsh".bright_red().bold() + ); eprintln!(); if let Some(path) = extract_path(error_msg) { @@ -156,13 +183,22 @@ fn handle_is_directory_error(error_msg: &str) { eprintln!(); eprintln!("To remove a directory:"); - eprintln!(" {} Remove empty directory", format!("rmdir {}", path).bright_yellow()); - eprintln!(" {} Remove directory and contents", format!("rm -r {}", path).bright_yellow()); + eprintln!( + " {} Remove empty directory", + format!("rmdir {}", path).bright_yellow() + ); + eprintln!( + " {} Remove directory and contents", + format!("rm -r {}", path).bright_yellow() + ); } } fn handle_not_directory_error(error_msg: &str) { - eprintln!("{}: Expected a directory but found a file.", "vsh".bright_red().bold()); + eprintln!( + "{}: Expected a directory but found a file.", + "vsh".bright_red().bold() + ); eprintln!(); if let Some(path) = extract_path(error_msg) { @@ -170,8 +206,14 @@ fn handle_not_directory_error(error_msg: &str) { eprintln!(); eprintln!("To create a directory:"); - eprintln!(" {} Remove file first", format!("rm {}", path).bright_yellow()); - eprintln!(" {} Then create directory", format!("mkdir {}", path).bright_yellow()); + eprintln!( + " {} Remove file first", + format!("rm {}", path).bright_yellow() + ); + eprintln!( + " {} Then create directory", + format!("mkdir {}", path).bright_yellow() + ); } } @@ -202,7 +244,9 @@ fn extract_path(error_msg: &str) -> Option { // Try space-separated (find first thing that looks like a path) for word in error_msg.split_whitespace() { - let cleaned = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '/' && c != '.' && c != '_' && c != '-'); + let cleaned = word.trim_matches(|c: char| { + !c.is_alphanumeric() && c != '/' && c != '.' && c != '_' && c != '-' + }); if cleaned.starts_with('/') || cleaned.starts_with("./") || cleaned.starts_with("../") { return Some(cleaned.to_string()); } @@ -228,8 +272,7 @@ fn extract_command(error_msg: &str) -> Option { if let Some(start) = error_msg.find("CommandNotFound(") { let after = &error_msg[start + 16..]; if let Some(end) = after.find(')') { - let cmd = &after[..end] - .trim_matches(|c| c == '"' || c == '\'' || c == '(' || c == ')'); + let cmd = &after[..end].trim_matches(|c| c == '"' || c == '\'' || c == '(' || c == ')'); return Some(cmd.to_string()); } } diff --git a/impl/rust-cli/src/glob.rs b/impl/rust-cli/src/glob.rs index 7369b0d0..91679fe6 100644 --- a/impl/rust-cli/src/glob.rs +++ b/impl/rust-cli/src/glob.rs @@ -129,11 +129,7 @@ pub fn expand_glob(pattern: &str, base_dir: &Path) -> Result> { if !pattern.starts_with('/') { matches = matches .into_iter() - .map(|p| { - p.strip_prefix(base_dir) - .unwrap_or(&p) - .to_path_buf() - }) + .map(|p| p.strip_prefix(base_dir).unwrap_or(&p).to_path_buf()) .collect(); } @@ -203,8 +199,8 @@ fn expand_braces_limited(pattern: &str, remaining: usize) -> Vec { // arm above) sets brace_start = Some(i) when brace_depth was // zero, and only the matched '}' decrements it back. So this // expect documents an unreachable case rather than a TODO. - let start = brace_start - .expect("brace_start invariant: Some whenever brace_depth >= 1"); + let start = + brace_start.expect("brace_start invariant: Some whenever brace_depth >= 1"); let prefix = &pattern[..start]; let suffix = &pattern[i + 1..]; let content = &pattern[start + 1..i]; @@ -351,20 +347,11 @@ mod tests { #[test] fn test_split_brace_content() { - assert_eq!( - split_brace_content("a,b,c"), - vec!["a", "b", "c"] - ); + assert_eq!(split_brace_content("a,b,c"), vec!["a", "b", "c"]); - assert_eq!( - split_brace_content("main,lib"), - vec!["main", "lib"] - ); + assert_eq!(split_brace_content("main,lib"), vec!["main", "lib"]); // Nested braces - assert_eq!( - split_brace_content("a,{b,c}"), - vec!["a", "{b,c}"] - ); + assert_eq!(split_brace_content("a,{b,c}"), vec!["a", "{b,c}"]); } } diff --git a/impl/rust-cli/src/help.rs b/impl/rust-cli/src/help.rs index e5e1b426..d5b84ca4 100644 --- a/impl/rust-cli/src/help.rs +++ b/impl/rust-cli/src/help.rs @@ -73,13 +73,12 @@ impl HelpEntry { /// Tier 1: Quick one-liner fn display_quick(&self) -> Result<()> { + println!("{} - {}", self.name.bright_green().bold(), self.summary); + println!("Usage: {}", self.usage.bright_cyan()); println!( - "{} - {}", - self.name.bright_green().bold(), - self.summary + "Run {} for details", + format!("help -v {}", self.name).bright_yellow() ); - println!("Usage: {}", self.usage.bright_cyan()); - println!("Run {} for details", format!("help -v {}", self.name).bright_yellow()); Ok(()) } @@ -88,7 +87,12 @@ impl HelpEntry { let mut output = String::new(); // Header - output.push_str(&format!("{}\n", format!("=== {} ===", self.name.to_uppercase()).bright_blue().bold())); + output.push_str(&format!( + "{}\n", + format!("=== {} ===", self.name.to_uppercase()) + .bright_blue() + .bold() + )); output.push('\n'); // Summary @@ -112,7 +116,10 @@ impl HelpEntry { output.push_str(&format!("{}\n", "EXAMPLES:".bright_yellow().bold())); for (i, example) in self.examples.iter().enumerate() { output.push_str(&format!(" {}. {}\n", i + 1, example.description)); - output.push_str(&format!(" {}\n", format!("$ {}", example.command).bright_cyan())); + output.push_str(&format!( + " {}\n", + format!("$ {}", example.command).bright_cyan() + )); output.push('\n'); } } @@ -126,16 +133,24 @@ impl HelpEntry { // Proof reference if let Some(op_type) = self.proof_operation { - output.push_str(&format!("{}\n", "FORMAL VERIFICATION:".bright_yellow().bold())); + output.push_str(&format!( + "{}\n", + "FORMAL VERIFICATION:".bright_yellow().bold() + )); let proof_ref = proof_refs::ProofReference::for_operation(op_type); output.push_str(&format!(" {}\n", proof_ref.format_short())); - output.push_str(&format!(" Run {} for proof details\n", "proof".bright_cyan())); + output.push_str(&format!( + " Run {} for proof details\n", + "proof".bright_cyan() + )); output.push('\n'); } // Footer - output.push_str(&format!("Run {} for comprehensive documentation\n", - format!("man vsh-{}", self.name).bright_yellow())); + output.push_str(&format!( + "Run {} for comprehensive documentation\n", + format!("man vsh-{}", self.name).bright_yellow() + )); // Page if output is long pager::page(&output)?; @@ -147,7 +162,11 @@ impl HelpEntry { let mut output = String::new(); // Man page header - output.push_str(&format!("VSH-{}(1) Valence Shell Manual VSH-{}(1)\n\n", self.name.to_uppercase(), self.name.to_uppercase())); + output.push_str(&format!( + "VSH-{}(1) Valence Shell Manual VSH-{}(1)\n\n", + self.name.to_uppercase(), + self.name.to_uppercase() + )); // NAME output.push_str("NAME\n"); @@ -177,27 +196,43 @@ impl HelpEntry { if let Some(op_type) = self.proof_operation { output.push_str("FORMAL VERIFICATION\n"); let proof_ref = proof_refs::ProofReference::for_operation(op_type); - output.push_str(&format!(" This operation is formally verified for correctness.\n\n")); + output.push_str(&format!( + " This operation is formally verified for correctness.\n\n" + )); output.push_str(&format!(" Theorem: {}\n", proof_ref.theorem)); - output.push_str(&format!(" Description: {}\n\n", proof_ref.description)); + output.push_str(&format!( + " Description: {}\n\n", + proof_ref.description + )); output.push_str(&format!(" Proof locations:\n")); output.push_str(&format!(" Coq: {}\n", proof_ref.coq_location)); output.push_str(&format!(" Lean 4: {}\n", proof_ref.lean_location)); output.push_str(&format!(" Agda: {}\n", proof_ref.agda_location)); - output.push_str(&format!(" Isabelle: {}\n\n", proof_ref.isabelle_location)); + output.push_str(&format!( + " Isabelle: {}\n\n", + proof_ref.isabelle_location + )); } // SEE ALSO if !self.related.is_empty() { output.push_str("SEE ALSO\n"); - output.push_str(&format!(" {}\n\n", - self.related.iter().map(|cmd| format!("vsh-{}(1)", cmd)).collect::>().join(", "))); + output.push_str(&format!( + " {}\n\n", + self.related + .iter() + .map(|cmd| format!("vsh-{}(1)", cmd)) + .collect::>() + .join(", ") + )); } // Footer - output.push_str(&format!("Valence Shell 0.14.0 {} VSH-{}(1)\n", + output.push_str(&format!( + "Valence Shell 0.14.0 {} VSH-{}(1)\n", chrono::Utc::now().format("%Y-%m-%d"), - self.name.to_uppercase())); + self.name.to_uppercase() + )); // Page the output pager::page(&output)?; @@ -221,7 +256,10 @@ pub fn display_help(command: Option<&str>, tier: HelpTier) -> Result<()> { if let Some(entry) = get_help(cmd) { entry.display(tier)?; } else { - bail!("No help available for '{}'. Run 'help' to see all commands.", cmd); + bail!( + "No help available for '{}'. Run 'help' to see all commands.", + cmd + ); } } None => { @@ -257,8 +295,14 @@ fn display_command_list() -> Result<()> { } println!("Run {} for command details", "help ".bright_cyan()); - println!("Run {} for detailed help with examples", "help -v ".bright_cyan()); - println!("Run {} for full manual page", "man vsh-".bright_cyan()); + println!( + "Run {} for detailed help with examples", + "help -v ".bright_cyan() + ); + println!( + "Run {} for full manual page", + "man vsh-".bright_cyan() + ); Ok(()) } @@ -285,7 +329,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["rmdir", "undo", "touch"], proof_operation: Some(OperationType::Mkdir), }, - // rmdir HelpEntry { name: "rmdir", @@ -307,7 +350,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["mkdir", "undo", "rm"], proof_operation: Some(OperationType::Rmdir), }, - // touch HelpEntry { name: "touch", @@ -328,7 +370,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["rm", "mkdir", "undo"], proof_operation: Some(OperationType::CreateFile), }, - // rm HelpEntry { name: "rm", @@ -351,7 +392,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["touch", "rmdir", "undo", "obliterate"], proof_operation: Some(OperationType::DeleteFile), }, - // undo HelpEntry { name: "undo", @@ -373,7 +413,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["redo", "history"], proof_operation: None, }, - // redo HelpEntry { name: "redo", @@ -381,16 +420,13 @@ static HELP_DATABASE: &[HelpEntry] = &[ usage: "redo [count]", description: "Re-applies the last N undone operations (default: 1).\n\ Only works if operations were undone and not yet overwritten.", - examples: &[ - Example { - description: "Undo and redo", - command: "mkdir test && undo && redo", - }, - ], + examples: &[Example { + description: "Undo and redo", + command: "mkdir test && undo && redo", + }], related: &["undo", "history"], proof_operation: None, }, - // cd HelpEntry { name: "cd", @@ -411,7 +447,6 @@ static HELP_DATABASE: &[HelpEntry] = &[ related: &["pwd", "pushd", "popd"], proof_operation: None, }, - // help HelpEntry { name: "help", diff --git a/impl/rust-cli/src/highlighter.rs b/impl/rust-cli/src/highlighter.rs index 29435ddb..35cac87a 100644 --- a/impl/rust-cli/src/highlighter.rs +++ b/impl/rust-cli/src/highlighter.rs @@ -32,25 +32,17 @@ impl VshHighlighter { pub fn new() -> Self { let builtins = vec![ // Navigation - "cd", "pwd", "pushd", "popd", "dirs", - // File operations + "cd", "pwd", "pushd", "popd", "dirs", // File operations "mkdir", "rmdir", "touch", "rm", "cp", "mv", "ln", "cat", "ls", // VSH-specific - "undo", "redo", "history", - // Transactions - "begin", "commit", "rollback", - // Utilities - "echo", "help", "exit", "eval", "source", "read", "true", "false", - // Test - "test", "[", - // Variables - "export", "unset", "set", - // Jobs - "jobs", "fg", "bg", "kill", - // Control structures - "if", "then", "elif", "else", "fi", - "while", "until", "for", "do", "done", - "case", "esac", "in", + "undo", "redo", "history", // Transactions + "begin", "commit", "rollback", // Utilities + "echo", "help", "exit", "eval", "source", "read", "true", "false", // Test + "test", "[", // Variables + "export", "unset", "set", // Jobs + "jobs", "fg", "bg", "kill", // Control structures + "if", "then", "elif", "else", "fi", "while", "until", "for", "do", "done", "case", + "esac", "in", ] .iter() .map(|s| s.to_string()) @@ -275,13 +267,9 @@ impl VshHighlighter { (token.to_string(), style) } - TokenType::Operator => { - (token.to_string(), Style::new().fg(Color::White).bold()) - } + TokenType::Operator => (token.to_string(), Style::new().fg(Color::White).bold()), - TokenType::Comment => { - (token.to_string(), Style::new().fg(Color::DarkGray)) - } + TokenType::Comment => (token.to_string(), Style::new().fg(Color::DarkGray)), } } } @@ -319,7 +307,10 @@ impl Highlighter for VshHighlighter { if let Some(token_pos) = line[position..].find(&token) { // Add any whitespace before token as unstyled if token_pos > 0 { - styled.push((Style::new(), line[position..position + token_pos].to_string())); + styled.push(( + Style::new(), + line[position..position + token_pos].to_string(), + )); position += token_pos; } } diff --git a/impl/rust-cli/src/job.rs b/impl/rust-cli/src/job.rs index 6a785f67..4b945bcb 100644 --- a/impl/rust-cli/src/job.rs +++ b/impl/rust-cli/src/job.rs @@ -53,8 +53,8 @@ pub struct Job { pub struct JobTable { jobs: Vec, next_id: usize, - current_job_id: Option, // %+ marker - previous_job_id: Option, // %- marker + current_job_id: Option, // %+ marker + previous_job_id: Option, // %- marker } impl JobTable { @@ -186,7 +186,6 @@ impl JobTable { pub fn jobs(&self) -> &[Job] { &self.jobs } - } impl Default for JobTable { @@ -289,6 +288,6 @@ mod tests { assert_eq!(lines.len(), 2); assert!(lines[0].contains("[1]")); assert!(lines[1].contains("[2]")); - assert!(lines[1].contains("+")); // Current job marker + assert!(lines[1].contains("+")); // Current job marker } } diff --git a/impl/rust-cli/src/lib.rs b/impl/rust-cli/src/lib.rs index 1ecb233b..cbc806f4 100644 --- a/impl/rust-cli/src/lib.rs +++ b/impl/rust-cli/src/lib.rs @@ -22,13 +22,13 @@ pub mod glob; pub mod help; pub mod highlighter; pub mod history; -pub mod quotes; pub mod job; pub mod pager; pub mod parser; pub mod posix_builtins; pub mod process_sub; pub mod proof_refs; +pub mod quotes; pub mod redirection; pub mod repl; pub mod secure_erase; @@ -43,8 +43,8 @@ pub mod verification; pub mod echidna_integration; // Re-export commonly used types -pub use state::ShellState; pub use executable::ExecutableCommand; +pub use state::ShellState; // Re-export signal handling pub use signals::INTERRUPT_REQUESTED; diff --git a/impl/rust-cli/src/main.rs b/impl/rust-cli/src/main.rs index 5accb8f8..3b439234 100644 --- a/impl/rust-cli/src/main.rs +++ b/impl/rust-cli/src/main.rs @@ -20,8 +20,8 @@ use clap::{Parser, Subcommand}; use colored::Colorize; // Use library modules -use vsh::{commands, state}; use vsh::executable::{ExecutableCommand, ExecutionResult}; +use vsh::{commands, state}; // REPL modules (choose one based on feature flags) #[cfg(feature = "enhanced-repl")] @@ -141,8 +141,7 @@ fn main() -> Result<()> { let root = match cli.root { Some(r) => r, None => { - let current = std::env::current_dir() - .context("Failed to get current directory")?; + let current = std::env::current_dir().context("Failed to get current directory")?; current.to_string_lossy().to_string() } }; @@ -230,7 +229,11 @@ fn main() -> Result<()> { } // Run EXIT trap if registered - if let Some(exit_cmd) = state.traps.get(vsh::posix_builtins::TrapSignal::Exit).map(|s| s.to_string()) { + if let Some(exit_cmd) = state + .traps + .get(vsh::posix_builtins::TrapSignal::Exit) + .map(|s| s.to_string()) + { if let Ok(cmd) = vsh::parser::parse_command(&exit_cmd) { let _ = cmd.execute(&mut state); } @@ -290,7 +293,11 @@ fn execute_script_content(content: &str, state: &mut state::ShellState) -> Resul } // Run EXIT trap if registered - if let Some(exit_cmd) = state.traps.get(vsh::posix_builtins::TrapSignal::Exit).map(|s| s.to_string()) { + if let Some(exit_cmd) = state + .traps + .get(vsh::posix_builtins::TrapSignal::Exit) + .map(|s| s.to_string()) + { if let Ok(cmd) = vsh::parser::parse_command(&exit_cmd) { let _ = cmd.execute(state); } diff --git a/impl/rust-cli/src/parser.rs b/impl/rust-cli/src/parser.rs index 173a305c..67ee5630 100644 --- a/impl/rust-cli/src/parser.rs +++ b/impl/rust-cli/src/parser.rs @@ -52,18 +52,12 @@ enum ExpansionOp { /// Use default value: ${VAR:-default} or ${VAR-default} Default { value: String, - check_null: bool, // true for :-, false for - + check_null: bool, // true for :-, false for - }, /// Assign default value: ${VAR:=default} or ${VAR=default} - AssignDefault { - value: String, - check_null: bool, - }, + AssignDefault { value: String, check_null: bool }, /// Use alternative value: ${VAR:+value} or ${VAR+value} - UseAlternative { - value: String, - check_null: bool, - }, + UseAlternative { value: String, check_null: bool }, /// Error if unset: ${VAR:?message} or ${VAR?message} ErrorIfUnset { message: Option, @@ -72,10 +66,7 @@ enum ExpansionOp { /// String length: ${#VAR} Length, /// Substring extraction: ${VAR:offset} or ${VAR:offset:length} - Substring { - offset: i32, - length: Option, - }, + Substring { offset: i32, length: Option }, } /// Parsed parameter expansion from ${VAR...} syntax @@ -420,7 +411,6 @@ pub enum Command { }, // Control structures - /// If/then/elif/else/fi conditional If { condition: Box, @@ -449,7 +439,6 @@ pub enum Command { }, // Wow-factor features (unique to verified reversible shell) - /// Explain: proof-annotated dry run showing preconditions, state transition, /// inverse operation, and proof references across all 6 verification systems Explain { @@ -481,7 +470,6 @@ pub enum Command { }, // Shell functions and related builtins - /// Function definition: `fname() { commands; }` or `function fname { commands; }` FunctionDef { name: String, @@ -502,7 +490,6 @@ pub enum Command { }, // POSIX builtins (trap, alias, unalias) - /// trap builtin: register signal handlers /// `trap 'command' SIGNAL...` or `trap - SIGNAL...` or `trap` (list) Trap { @@ -544,9 +531,9 @@ pub enum LogicalOperator { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum QuoteState { None, - SingleQuote, // Inside '...' - DoubleQuote, // Inside "..." - Backslash, // After \ (escape next char) + SingleQuote, // Inside '...' + DoubleQuote, // Inside "..." + Backslash, // After \ (escape next char) } /// Tokenize input string into words and redirection operators @@ -742,7 +729,8 @@ fn tokenize(input: &str) -> Result> { // Check if this is start of 2> or 2>&1 // Only treat as redirect if '2' is the start of a new token // (not part of a word like "file2>out") - if current_literal.is_empty() && current_word.is_empty() + if current_literal.is_empty() + && current_word.is_empty() && chars.peek() == Some(&'>') { chars.next(); // consume > @@ -929,7 +917,7 @@ fn parse_variable( /// Parse command substitution in $(cmd) form fn parse_command_sub_dollar(chars: &mut std::iter::Peekable) -> Result { let mut cmd = String::new(); - let mut depth = 1; // Track nesting depth for nested $() + let mut depth = 1; // Track nesting depth for nested $() while let Some(ch) = chars.next() { match ch { @@ -994,7 +982,7 @@ fn parse_process_sub_input(chars: &mut std::iter::Peekable) -> chars.next(); // consume '(' let mut cmd = String::new(); - let mut depth = 1; // Track nesting depth + let mut depth = 1; // Track nesting depth while let Some(ch) = chars.next() { match ch { @@ -1022,7 +1010,7 @@ fn parse_process_sub_output(chars: &mut std::iter::Peekable) -> chars.next(); // consume '(' let mut cmd = String::new(); - let mut depth = 1; // Track nesting depth + let mut depth = 1; // Track nesting depth while let Some(ch) = chars.next() { match ch { @@ -1069,7 +1057,9 @@ fn parse_extended_test(tokens: &[Token]) -> Result { } // Find closing ]] - let close_pos = tokens.iter().position(|t| matches!(t, Token::ExtendedTestClose)) + let close_pos = tokens + .iter() + .position(|t| matches!(t, Token::ExtendedTestClose)) .ok_or_else(|| anyhow!("Extended test missing closing ]]"))?; // Extract arguments between [[ and ]] @@ -1248,7 +1238,7 @@ fn parse_pipeline(tokens: &[Token]) -> Result { Ok(Command::Pipeline { stages: parsed_stages, redirects: final_redirects, - background: false, // TODO: detect & in pipeline + background: false, // TODO: detect & in pipeline }) } @@ -1314,11 +1304,17 @@ fn extract_redirections_from_tokens(tokens: &[Token]) -> Result<(Vec, Vec let delimiter = expect_word(&tokens, i + 1, "here document delimiter")?; // Check if delimiter is quoted (disables expansion) - let (delimiter_clean, expand) = if delimiter.starts_with('\'') || delimiter.starts_with('"') { - (delimiter.trim_matches(|c| c == '\'' || c == '"').to_string(), false) - } else { - (delimiter.clone(), true) - }; + let (delimiter_clean, expand) = + if delimiter.starts_with('\'') || delimiter.starts_with('"') { + ( + delimiter + .trim_matches(|c| c == '\'' || c == '"') + .to_string(), + false, + ) + } else { + (delimiter.clone(), true) + }; // Content will be provided by REPL after reading subsequent lines // For now, create placeholder - will be filled by execute_with_heredoc @@ -1344,10 +1340,7 @@ fn extract_redirections_from_tokens(tokens: &[Token]) -> Result<(Vec, Vec (content_word.clone(), true) }; - redirects.push(Redirection::HereString { - content, - expand, - }); + redirects.push(Redirection::HereString { content, expand }); i += 2; } @@ -1365,7 +1358,9 @@ fn extract_redirections_from_tokens(tokens: &[Token]) -> Result<(Vec, Vec } Token::ExtendedTestOpen | Token::ExtendedTestClose => { - return Err(anyhow!("Unexpected [[ or ]] token - should be handled at top level")); + return Err(anyhow!( + "Unexpected [[ or ]] token - should be handled at top level" + )); } } } @@ -1523,7 +1518,8 @@ fn split_on_top_level(input: &str, split_on_newline: bool) -> Vec<&str> { // Check for keyword at word boundary if ch.is_alphabetic() || ch == '_' { let word_start = i; - while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { i += 1; } let word = &input[word_start..i]; @@ -1566,13 +1562,20 @@ pub fn parse_command(input: &str) -> Result { // Check for function definitions before tokenization // Function definitions contain braces which interact with tokenization if let Some((name, body, raw_body)) = crate::functions::parse_function_def(trimmed) { - return Ok(Command::FunctionDef { name, body, raw_body }); + return Ok(Command::FunctionDef { + name, + body, + raw_body, + }); } // Check for control structures before tokenization // Control structures are parsed at the string level because they contain // semicolons as internal separators between keywords - let first_word = trimmed.split(|c: char| c.is_whitespace() || c == ';').next().unwrap_or(""); + let first_word = trimmed + .split(|c: char| c.is_whitespace() || c == ';') + .next() + .unwrap_or(""); match first_word { "if" => return parse_if_command(trimmed), "while" => return parse_while_command(trimmed), @@ -1661,7 +1664,10 @@ pub fn parse_command(input: &str) -> Result { // Check for scalar assignment: VAR=value if let Some(eq_pos) = first_str.find('=') { // Make sure it's not already handled above - if !first_str.contains("=(") && !first_str.contains("[") && !first_str.contains("+=") { + if !first_str.contains("=(") + && !first_str.contains("[") + && !first_str.contains("+=") + { let name = &first_str[..eq_pos]; let value = &first_str[eq_pos + 1..]; @@ -1685,7 +1691,10 @@ pub fn parse_command(input: &str) -> Result { // Check for logical operators (&&, ||) - lowest precedence // Use rposition to find RIGHTMOST operator for left-to-right associativity: // "a && b || c" splits at || giving "(a && b) || c" (POSIX correct) - if let Some(op_pos) = tokens.iter().rposition(|t| matches!(t, Token::And | Token::Or)) { + if let Some(op_pos) = tokens + .iter() + .rposition(|t| matches!(t, Token::And | Token::Or)) + { return parse_logical_op(&tokens, op_pos); } @@ -1754,11 +1763,17 @@ pub fn parse_command(input: &str) -> Result { let delimiter = expect_word(&tokens, i + 1, "here document delimiter")?; // Check if delimiter is quoted (disables expansion) - let (delimiter_clean, expand) = if delimiter.starts_with('\'') || delimiter.starts_with('"') { - (delimiter.trim_matches(|c| c == '\'' || c == '"').to_string(), false) - } else { - (delimiter.clone(), true) - }; + let (delimiter_clean, expand) = + if delimiter.starts_with('\'') || delimiter.starts_with('"') { + ( + delimiter + .trim_matches(|c| c == '\'' || c == '"') + .to_string(), + false, + ) + } else { + (delimiter.clone(), true) + }; // Content will be provided by REPL after reading subsequent lines // For now, create placeholder - will be filled by execute_with_heredoc @@ -1784,10 +1799,7 @@ pub fn parse_command(input: &str) -> Result { (content_word.clone(), true) }; - redirects.push(Redirection::HereString { - content, - expand, - }); + redirects.push(Redirection::HereString { content, expand }); i += 2; } @@ -1877,7 +1889,10 @@ fn is_valid_var_name(name: &str) -> bool { /// # Ok::<(), anyhow::Error>(()) /// ``` /// Expand variables and command substitutions in a string -pub fn expand_with_command_sub(input: &str, state: &mut crate::state::ShellState) -> Result { +pub fn expand_with_command_sub( + input: &str, + state: &mut crate::state::ShellState, +) -> Result { let mut result = String::new(); let mut chars = input.chars().peekable(); @@ -1976,7 +1991,12 @@ pub fn expand_with_command_sub(input: &str, state: &mut crate::state::ShellState // The peek above already bound next_ch by value; // chars.next() advances the iterator and the bound value // is used directly. Removes three previous panic sites. - if next_ch == '?' || next_ch == '$' || next_ch == '#' || next_ch == '@' || next_ch == '*' { + if next_ch == '?' + || next_ch == '$' + || next_ch == '#' + || next_ch == '@' + || next_ch == '*' + { // Single-character special variable chars.next(); result.push_str(&state.expand_variable(&next_ch.to_string())); @@ -2037,11 +2057,8 @@ pub fn expand_with_process_sub( let cmd = parse_process_sub_input(&mut chars)?; // Create and start process substitution - let mut proc_sub = crate::process_sub::ProcessSubstitution::create( - ProcessSubType::Input, - cmd, - state, - )?; + let mut proc_sub = + crate::process_sub::ProcessSubstitution::create(ProcessSubType::Input, cmd, state)?; proc_sub.start(state)?; // Add FIFO path to result @@ -2095,7 +2112,8 @@ fn parse_parameter_expansion(content: &str) -> Result Result= chars.len() { // ${VAR:} with nothing after - try substring with offset 0 return Ok(ParameterExpansion { var_name, - operation: ExpansionOp::Substring { offset: 0, length: None }, + operation: ExpansionOp::Substring { + offset: 0, + length: None, + }, }); } @@ -2171,17 +2192,19 @@ fn parse_parameter_expansion(content: &str) -> Result { // ${VAR:offset} or ${VAR: -offset} (substring with space before negative) let offset_str = content[pos..].to_string(); - parse_substring_params(&offset_str) - .map(|(offset, length)| ParameterExpansion { - var_name, - operation: ExpansionOp::Substring { offset, length }, - }) + parse_substring_params(&offset_str).map(|(offset, length)| ParameterExpansion { + var_name, + operation: ExpansionOp::Substring { offset, length }, + }) } _ => Err(format!("Unknown expansion operator: {}", op_char)), } @@ -2194,15 +2217,21 @@ fn parse_substring_params(params: &str) -> Result<(i32, Option), String> match parts.len() { 1 => { // Just offset (trim to handle ${VAR: -5} with space before negative) - let offset = parts[0].trim().parse::() + let offset = parts[0] + .trim() + .parse::() .map_err(|_| format!("Invalid offset: {}", parts[0]))?; Ok((offset, None)) } 2 => { // Offset and length (trim both parts) - let offset = parts[0].trim().parse::() + let offset = parts[0] + .trim() + .parse::() .map_err(|_| format!("Invalid offset: {}", parts[0]))?; - let length = parts[1].trim().parse::() + let length = parts[1] + .trim() + .parse::() .map_err(|_| format!("Invalid length: {}", parts[1]))?; Ok((offset, Some(length))) } @@ -2231,7 +2260,9 @@ fn apply_expansion(expansion: &ParameterExpansion, state: &crate::state::ShellSt // var_value is Some by construction here: the surrounding // `if is_unset || ...` branch is the unset/null path; this // else branch only fires when var_value is Some. - var_value.expect("var_value is Some when !is_unset (checked above)").to_string() + var_value + .expect("var_value is Some when !is_unset (checked above)") + .to_string() } } @@ -2248,7 +2279,9 @@ fn apply_expansion(expansion: &ParameterExpansion, state: &crate::state::ShellSt // var_value is Some by construction here: the surrounding // `if is_unset || ...` branch is the unset/null path; this // else branch only fires when var_value is Some. - var_value.expect("var_value is Some when !is_unset (checked above)").to_string() + var_value + .expect("var_value is Some when !is_unset (checked above)") + .to_string() } } @@ -2261,7 +2294,10 @@ fn apply_expansion(expansion: &ParameterExpansion, state: &crate::state::ShellSt } } - ExpansionOp::ErrorIfUnset { message, check_null } => { + ExpansionOp::ErrorIfUnset { + message, + check_null, + } => { // ${VAR:?message} or ${VAR?message} if is_unset || (*check_null && is_null) { let error_msg = message.as_deref().unwrap_or("parameter null or not set"); @@ -2272,7 +2308,9 @@ fn apply_expansion(expansion: &ParameterExpansion, state: &crate::state::ShellSt // var_value is Some by construction here: the surrounding // `if is_unset || ...` branch is the unset/null path; this // else branch only fires when var_value is Some. - var_value.expect("var_value is Some when !is_unset (checked above)").to_string() + var_value + .expect("var_value is Some when !is_unset (checked above)") + .to_string() } } @@ -2307,9 +2345,7 @@ fn apply_substring(value: &str, offset: i32, length: Option) -> String { let end = (start + n).min(chars.len()); chars[start..end].iter().collect() } - None => { - chars[start..].iter().collect() - } + None => chars[start..].iter().collect(), } } @@ -2418,8 +2454,11 @@ pub fn expand_variables(input: &str, state: &crate::state::ShellState) -> String // The peek above already bound next_ch; chars.next() advances // the iterator and the bound value is used directly. Removes // three previous panic sites. - if next_ch == '?' || next_ch == '$' || next_ch == '#' - || next_ch == '@' || next_ch == '*' + if next_ch == '?' + || next_ch == '$' + || next_ch == '#' + || next_ch == '@' + || next_ch == '*' { // Single-character special variable chars.next(); @@ -2464,7 +2503,10 @@ pub fn expand_variables(input: &str, state: &crate::state::ShellState) -> String fn expect_word(tokens: &[Token], index: usize, context: &str) -> Result { match tokens.get(index) { Some(Token::Word(w)) => Ok(quoted_word_to_string(w)), - Some(_) => Err(anyhow!("{}: expected filename, got redirection operator", context)), + Some(_) => Err(anyhow!( + "{}: expected filename, got redirection operator", + context + )), None => Err(anyhow!("{}: missing filename", context)), } } @@ -2559,7 +2601,10 @@ fn quoted_word_to_string(word: &QuotedWord) -> String { } /// Expand command substitution by executing the command and capturing output -pub fn expand_command_substitution(cmd: &str, state: &mut crate::state::ShellState) -> Result { +pub fn expand_command_substitution( + cmd: &str, + state: &mut crate::state::ShellState, +) -> Result { use std::process::{Command as ProcessCommand, Stdio}; // Parse the command @@ -2582,13 +2627,17 @@ pub fn expand_command_substitution(cmd: &str, state: &mut crate::state::ShellSta process_cmd.args(&expanded_args); - let output_result = process_cmd.output() + let output_result = process_cmd + .output() .with_context(|| format!("Failed to execute: {}", program))?; if output_result.status.success() { String::from_utf8_lossy(&output_result.stdout).to_string() } else { - return Err(anyhow!("Command failed with exit code: {:?}", output_result.status.code())); + return Err(anyhow!( + "Command failed with exit code: {:?}", + output_result.status.code() + )); } } @@ -2597,7 +2646,8 @@ pub fn expand_command_substitution(cmd: &str, state: &mut crate::state::ShellSta std::env::current_dir() .context("Failed to get current directory")? .to_string_lossy() - .to_string() + "\n" + .to_string() + + "\n" } Command::Ls { path, .. } => { @@ -2614,7 +2664,9 @@ pub fn expand_command_substitution(cmd: &str, state: &mut crate::state::ShellSta } _ => { - return Err(anyhow!("Command substitution not supported for this command type")); + return Err(anyhow!( + "Command substitution not supported for this command type" + )); } }; @@ -2625,7 +2677,10 @@ pub fn expand_command_substitution(cmd: &str, state: &mut crate::state::ShellSta } /// Expand a QuotedWord into a final string, respecting quote context -pub fn expand_quoted_word_with_state(word: &QuotedWord, state: &mut crate::state::ShellState) -> Result { +pub fn expand_quoted_word_with_state( + word: &QuotedWord, + state: &mut crate::state::ShellState, +) -> Result { let mut result = String::new(); for part in &word.parts { @@ -2743,13 +2798,23 @@ fn split_control_keywords<'a>(input: &'a str, keywords: &[&str]) -> Vec<(&'a str } match ch { - '\\' if !in_single_quote => { escaped = true; i += 1; } - '\'' if !in_double_quote => { in_single_quote = !in_single_quote; i += 1; } - '"' if !in_single_quote => { in_double_quote = !in_double_quote; i += 1; } + '\\' if !in_single_quote => { + escaped = true; + i += 1; + } + '\'' if !in_double_quote => { + in_single_quote = !in_single_quote; + i += 1; + } + '"' if !in_single_quote => { + in_double_quote = !in_double_quote; + i += 1; + } _ if !in_single_quote && !in_double_quote => { if ch.is_alphabetic() || ch == '_' { let word_start = i; - while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { i += 1; } let word = &input[word_start..i]; @@ -2770,7 +2835,8 @@ fn split_control_keywords<'a>(input: &'a str, keywords: &[&str]) -> Vec<(&'a str if nested_depth == 0 && keywords.contains(&word) { // Top-level keyword split point if !last_keyword.is_empty() || !result.is_empty() { - result.push((last_keyword, input[content_start..word_start].trim())); + result + .push((last_keyword, input[content_start..word_start].trim())); } last_keyword = word; content_start = i; @@ -2786,7 +2852,9 @@ fn split_control_keywords<'a>(input: &'a str, keywords: &[&str]) -> Vec<(&'a str i += 1; } } - _ => { i += 1; } + _ => { + i += 1; + } } } @@ -3008,7 +3076,10 @@ fn parse_for_command(input: &str) -> Result { "in" => { let words_str = content.trim().trim_end_matches(';').trim(); if !words_str.is_empty() { - words = words_str.split_whitespace().map(|s| s.to_string()).collect(); + words = words_str + .split_whitespace() + .map(|s| s.to_string()) + .collect(); } } "do" => { @@ -3144,16 +3215,30 @@ pub fn is_incomplete_control_structure(input: &str) -> bool { while i < bytes.len() { let ch = bytes[i] as char; - if escaped { escaped = false; i += 1; continue; } + if escaped { + escaped = false; + i += 1; + continue; + } match ch { - '\\' if !in_single_quote => { escaped = true; i += 1; } - '\'' if !in_double_quote => { in_single_quote = !in_single_quote; i += 1; } - '"' if !in_single_quote => { in_double_quote = !in_double_quote; i += 1; } + '\\' if !in_single_quote => { + escaped = true; + i += 1; + } + '\'' if !in_double_quote => { + in_single_quote = !in_single_quote; + i += 1; + } + '"' if !in_single_quote => { + in_double_quote = !in_double_quote; + i += 1; + } _ if !in_single_quote && !in_double_quote => { if ch.is_alphabetic() || ch == '_' { let word_start = i; - while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { + while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') + { i += 1; } let word = &trimmed[word_start..i]; @@ -3178,14 +3263,21 @@ pub fn is_incomplete_control_structure(input: &str) -> bool { i += 1; } } - _ => { i += 1; } + _ => { + i += 1; + } } } block_depth > 0 } -fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, background: bool) -> Result { +fn parse_base_command( + cmd: &str, + args: Vec, + redirects: Vec, + background: bool, +) -> Result { match cmd { "mkdir" => { if args.is_empty() { @@ -3297,19 +3389,25 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, let inner_cmd = &args[0]; let inner_args = args[1..].to_vec(); let inner = parse_base_command(inner_cmd, inner_args, vec![], false)?; - Ok(Command::Explain { inner: Box::new(inner) }) + Ok(Command::Explain { + inner: Box::new(inner), + }) } "checkpoint" => { if args.is_empty() { return Err(anyhow!("checkpoint: requires a name")); } - Ok(Command::Checkpoint { name: args[0].clone() }) + Ok(Command::Checkpoint { + name: args[0].clone(), + }) } "restore" => { if args.is_empty() { return Err(anyhow!("restore: requires a checkpoint name")); } - Ok(Command::Restore { name: args[0].clone() }) + Ok(Command::Restore { + name: args[0].clone(), + }) } "checkpoints" => Ok(Command::Checkpoints), "diff" => { @@ -3331,8 +3429,8 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, } "history" => { - let show_proofs = args.contains(&"--proofs".to_string()) - || args.contains(&"-p".to_string()); + let show_proofs = + args.contains(&"--proofs".to_string()) || args.contains(&"-p".to_string()); let count = args .iter() .filter(|s| !s.starts_with('-')) @@ -3499,19 +3597,23 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, if var_names.is_empty() { var_names.push("REPLY".to_string()); } - Ok(Command::Read { var_names, prompt, redirects }) + Ok(Command::Read { + var_names, + prompt, + redirects, + }) } "source" | "." => { if args.is_empty() { return Err(anyhow!("source: missing filename")); } - Ok(Command::Source { file: args[0].clone() }) + Ok(Command::Source { + file: args[0].clone(), + }) } - "set" => { - Ok(Command::Set { args }) - } + "set" => Ok(Command::Set { args }), "unset" => { if args.is_empty() { @@ -3523,15 +3625,17 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, return Err(anyhow!("unset -f: missing function name")); } // We reuse Unset but prefix with "-f " so the executor can distinguish - Ok(Command::Unset { name: format!("-f {}", args[1]) }) + Ok(Command::Unset { + name: format!("-f {}", args[1]), + }) } else { - Ok(Command::Unset { name: args[0].clone() }) + Ok(Command::Unset { + name: args[0].clone(), + }) } } - "eval" => { - Ok(Command::Eval { args }) - } + "eval" => Ok(Command::Eval { args }), "return" => { let code = args.get(0).and_then(|s| s.parse::().ok()); @@ -3559,22 +3663,33 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, "trap" => { if args.is_empty() { // trap with no args: list all traps - Ok(Command::Trap { action: None, signals: vec![] }) + Ok(Command::Trap { + action: None, + signals: vec![], + }) } else if args.len() == 1 { // trap SIGNAL (reset to default) - Ok(Command::Trap { action: Some("-".to_string()), signals: vec![args[0].clone()] }) + Ok(Command::Trap { + action: Some("-".to_string()), + signals: vec![args[0].clone()], + }) } else { // trap 'action' SIGNAL... let action = args[0].clone(); let signals = args[1..].to_vec(); - Ok(Command::Trap { action: Some(action), signals }) + Ok(Command::Trap { + action: Some(action), + signals, + }) } } "alias" => { if args.is_empty() { // alias with no args: list all aliases - Ok(Command::Alias { definitions: vec![] }) + Ok(Command::Alias { + definitions: vec![], + }) } else { let mut definitions = Vec::new(); for arg in &args { @@ -3599,12 +3714,7 @@ fn parse_base_command(cmd: &str, args: Vec, redirects: Vec, } // Conditionals - "test" => { - Ok(Command::Test { - args, - redirects, - }) - } + "test" => Ok(Command::Test { args, redirects }), "[" => { // For bracket command, verify closing ] @@ -3683,18 +3793,24 @@ mod tests { #[test] fn test_parse_history() { let cmd = parse_command("history 20 --proofs").unwrap(); - assert_eq!(cmd, Command::History { - count: 20, - show_proofs: true - }); + assert_eq!( + cmd, + Command::History { + count: 20, + show_proofs: true + } + ); } #[test] fn test_parse_begin() { let cmd = parse_command("begin mytxn").unwrap(); - assert_eq!(cmd, Command::Begin { - name: "mytxn".to_string() - }); + assert_eq!( + cmd, + Command::Begin { + name: "mytxn".to_string() + } + ); } #[test] @@ -3901,7 +4017,11 @@ mod tests { fn test_parse_simple_pipeline() { let cmd = parse_command("ls | grep test").unwrap(); match cmd { - Command::Pipeline { stages, redirects, background } => { + Command::Pipeline { + stages, + redirects, + background, + } => { assert_eq!(stages.len(), 2); assert_eq!(stages[0].0, "ls"); assert_eq!(stages[0].1.len(), 0); @@ -3935,7 +4055,11 @@ mod tests { fn test_parse_pipeline_with_redirect() { let cmd = parse_command("ls | grep test > output.txt").unwrap(); match cmd { - Command::Pipeline { stages, redirects, background } => { + Command::Pipeline { + stages, + redirects, + background, + } => { assert_eq!(stages.len(), 2); assert_eq!(redirects.len(), 1); assert_eq!(background, false); @@ -3959,14 +4083,14 @@ mod tests { // Single command with no pipe should not create pipeline let cmd = parse_command("ls").unwrap(); match cmd { - Command::Ls { .. } => {}, // Built-in command + Command::Ls { .. } => {} // Built-in command _ => panic!("Single command should not create pipeline"), } // Builtin command without pipe should also not create pipeline let cmd2 = parse_command("echo hello").unwrap(); match cmd2 { - Command::Echo { .. } => {}, + Command::Echo { .. } => {} _ => panic!("echo should parse as Echo builtin, not pipeline"), } } @@ -4000,7 +4124,10 @@ mod tests { // Test braced variable expansion assert_eq!(expand_variables("${VAR}", &state), "test"); - assert_eq!(expand_variables("prefix_${VAR}_suffix", &state), "prefix_test_suffix"); + assert_eq!( + expand_variables("prefix_${VAR}_suffix", &state), + "prefix_test_suffix" + ); // Test concatenation assert_eq!(expand_variables("${VAR}file", &state), "testfile"); @@ -4032,16 +4159,10 @@ mod tests { state.set_variable("SECOND", "World"); // Test multiple variables in one string - assert_eq!( - expand_variables("$FIRST $SECOND!", &state), - "Hello World!" - ); + assert_eq!(expand_variables("$FIRST $SECOND!", &state), "Hello World!"); // Test mixed simple and braced - assert_eq!( - expand_variables("$FIRST ${SECOND}", &state), - "Hello World" - ); + assert_eq!(expand_variables("$FIRST ${SECOND}", &state), "Hello World"); } #[test] @@ -4589,7 +4710,12 @@ mod tests { fn test_parse_if_then_fi() { let cmd = parse_command("if [ -f test.txt ]; then echo found; fi").unwrap(); match cmd { - Command::If { condition, then_body, elif_parts, else_body } => { + Command::If { + condition, + then_body, + elif_parts, + else_body, + } => { // Condition should be a bracket test assert!(matches!(*condition, Command::Bracket { .. })); assert_eq!(then_body.len(), 1); @@ -4604,7 +4730,12 @@ mod tests { fn test_parse_if_then_else_fi() { let cmd = parse_command("if [ -d /tmp ]; then echo yes; else echo no; fi").unwrap(); match cmd { - Command::If { condition, then_body, elif_parts, else_body } => { + Command::If { + condition, + then_body, + elif_parts, + else_body, + } => { assert!(matches!(*condition, Command::Bracket { .. })); assert_eq!(then_body.len(), 1); assert!(elif_parts.is_empty()); @@ -4618,10 +4749,16 @@ mod tests { #[test] fn test_parse_if_elif_else_fi() { let cmd = parse_command( - "if [ $x -eq 1 ]; then echo one; elif [ $x -eq 2 ]; then echo two; else echo other; fi" - ).unwrap(); + "if [ $x -eq 1 ]; then echo one; elif [ $x -eq 2 ]; then echo two; else echo other; fi", + ) + .unwrap(); match cmd { - Command::If { then_body, elif_parts, else_body, .. } => { + Command::If { + then_body, + elif_parts, + else_body, + .. + } => { assert_eq!(then_body.len(), 1); assert_eq!(elif_parts.len(), 1); assert!(else_body.is_some()); @@ -4657,7 +4794,8 @@ mod tests { #[test] fn test_parse_case_esac() { - let cmd = parse_command("case $x in a) echo a;; b|c) echo bc;; *) echo default;; esac").unwrap(); + let cmd = + parse_command("case $x in a) echo a;; b|c) echo bc;; *) echo default;; esac").unwrap(); match cmd { Command::CaseStatement { word, arms } => { assert_eq!(word, "$x"); @@ -4700,8 +4838,12 @@ mod tests { assert!(is_incomplete_control_structure("if true; then")); assert!(is_incomplete_control_structure("while true; do")); assert!(is_incomplete_control_structure("for x in a b c; do")); - assert!(!is_incomplete_control_structure("if true; then echo yes; fi")); - assert!(!is_incomplete_control_structure("while true; do echo yes; done")); + assert!(!is_incomplete_control_structure( + "if true; then echo yes; fi" + )); + assert!(!is_incomplete_control_structure( + "while true; do echo yes; done" + )); assert!(!is_incomplete_control_structure("echo hello")); } } @@ -4767,7 +4909,10 @@ pub fn fill_heredoc_content( let mut heredoc_index = 0; for redirect in redirects.iter_mut() { - if let Redirection::HereDoc { ref mut content, .. } = redirect { + if let Redirection::HereDoc { + ref mut content, .. + } = redirect + { if heredoc_index < heredoc_contents.len() { *content = heredoc_contents[heredoc_index].1.clone(); heredoc_index += 1; @@ -4883,7 +5028,12 @@ mod job_control_tests { fn test_parse_background_job() { let cmd = parse_command("sleep 10 &").unwrap(); match cmd { - Command::External { program, args, background, .. } => { + Command::External { + program, + args, + background, + .. + } => { assert_eq!(program, "sleep"); assert_eq!(args, vec!["10"]); assert!(background); @@ -4975,7 +5125,11 @@ mod job_control_tests { fn test_parse_logical_and() { let cmd = parse_command("mkdir foo && touch bar").unwrap(); match cmd { - Command::LogicalOp { operator, left, right } => { + Command::LogicalOp { + operator, + left, + right, + } => { assert_eq!(operator, LogicalOperator::And); assert!(matches!(*left, Command::Mkdir { .. })); assert!(matches!(*right, Command::Touch { .. })); @@ -4988,7 +5142,11 @@ mod job_control_tests { fn test_parse_logical_or() { let cmd = parse_command("test -f file.txt || touch file.txt").unwrap(); match cmd { - Command::LogicalOp { operator, left, right } => { + Command::LogicalOp { + operator, + left, + right, + } => { assert_eq!(operator, LogicalOperator::Or); assert!(matches!(*left, Command::Test { .. })); assert!(matches!(*right, Command::Touch { .. })); diff --git a/impl/rust-cli/src/process_sub.rs b/impl/rust-cli/src/process_sub.rs index 235f6af0..eaf33403 100644 --- a/impl/rust-cli/src/process_sub.rs +++ b/impl/rust-cli/src/process_sub.rs @@ -35,11 +35,7 @@ pub struct ProcessSubstitution { impl ProcessSubstitution { /// Create new process substitution with FIFO - pub fn create( - sub_type: ProcessSubType, - cmd: String, - state: &mut ShellState, - ) -> Result { + pub fn create(sub_type: ProcessSubType, cmd: String, state: &mut ShellState) -> Result { // Generate globally-unique FIFO path: // /tmp/vsh-fifo--- // The pid + counter make collisions improbable in single-process use; @@ -55,8 +51,9 @@ impl ProcessSubstitution { #[cfg(unix)] { use std::ffi::CString; - let path_str = fifo_path.to_str() - .ok_or_else(|| anyhow!("FIFO path contains invalid UTF-8: {}", fifo_path.display()))?; + let path_str = fifo_path.to_str().ok_or_else(|| { + anyhow!("FIFO path contains invalid UTF-8: {}", fifo_path.display()) + })?; let path_cstr = CString::new(path_str) .map_err(|_| anyhow!("FIFO path contains null bytes: {}", fifo_path.display()))?; // SAFETY: path_cstr is a valid NUL-terminated C string; mkfifo is a POSIX syscall @@ -71,17 +68,27 @@ impl ProcessSubstitution { let result = unsafe { libc::mkfifo(path_cstr.as_ptr(), 0o600) }; if result != 0 { let err = std::io::Error::last_os_error(); - return Err(anyhow!("Failed to create FIFO {}: {}", fifo_path.display(), err)); + return Err(anyhow!( + "Failed to create FIFO {}: {}", + fifo_path.display(), + err + )); } } else { - return Err(anyhow!("Failed to create FIFO {}: {}", fifo_path.display(), err)); + return Err(anyhow!( + "Failed to create FIFO {}: {}", + fifo_path.display(), + err + )); } } } #[cfg(not(unix))] { - return Err(anyhow!("Process substitution requires Unix (FIFOs not supported on Windows)")); + return Err(anyhow!( + "Process substitution requires Unix (FIFOs not supported on Windows)" + )); } Ok(Self { @@ -187,7 +194,11 @@ fn start_command_with_output_redirect( } cmd } - _ => return Err(anyhow!("Command type not supported in process substitution")), + _ => { + return Err(anyhow!( + "Command type not supported in process substitution" + )) + } }; // Use sh -c to run: cmd > fifo_path @@ -226,7 +237,11 @@ fn start_command_with_input_redirect( } cmd } - _ => return Err(anyhow!("Command type not supported for output process substitution")), + _ => { + return Err(anyhow!( + "Command type not supported for output process substitution" + )) + } }; // Use sh -c to run: cmd < fifo_path @@ -250,11 +265,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let mut state = ShellState::new(temp_dir.path().to_str().unwrap()).unwrap(); - let proc_sub = ProcessSubstitution::create( - ProcessSubType::Input, - "echo test".to_string(), - &mut state, - ).unwrap(); + let proc_sub = + ProcessSubstitution::create(ProcessSubType::Input, "echo test".to_string(), &mut state) + .unwrap(); // FIFO should exist assert!(proc_sub.fifo_path.exists()); @@ -278,17 +291,13 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let mut state = ShellState::new(temp_dir.path().to_str().unwrap()).unwrap(); - let proc_sub1 = ProcessSubstitution::create( - ProcessSubType::Input, - "echo a".to_string(), - &mut state, - ).unwrap(); - - let proc_sub2 = ProcessSubstitution::create( - ProcessSubType::Input, - "echo b".to_string(), - &mut state, - ).unwrap(); + let proc_sub1 = + ProcessSubstitution::create(ProcessSubType::Input, "echo a".to_string(), &mut state) + .unwrap(); + + let proc_sub2 = + ProcessSubstitution::create(ProcessSubType::Input, "echo b".to_string(), &mut state) + .unwrap(); // FIFOs should have different paths assert_ne!(proc_sub1.fifo_path, proc_sub2.fifo_path); diff --git a/impl/rust-cli/src/proof_refs.rs b/impl/rust-cli/src/proof_refs.rs index f069b826..7a4c5020 100644 --- a/impl/rust-cli/src/proof_refs.rs +++ b/impl/rust-cli/src/proof_refs.rs @@ -61,7 +61,9 @@ impl ProofReference { OperationType::Symlink | OperationType::Unlink => SYMLINK_UNLINK_REVERSIBLE, OperationType::Chmod => CHMOD_REVERSIBLE, OperationType::Chown => CHOWN_REVERSIBLE, - OperationType::SetVariable | OperationType::UnsetVariable => VARIABLE_ASSIGNMENT_REVERSIBLE, + OperationType::SetVariable | OperationType::UnsetVariable => { + VARIABLE_ASSIGNMENT_REVERSIBLE + } OperationType::HardwareErase => HARDWARE_ERASE_IRREVERSIBLE, OperationType::Obliterate => OBLITERATE_IRREVERSIBLE, } @@ -275,18 +277,27 @@ pub fn all_proofs() -> Vec { /// Displays proof count, verification systems, and coverage information /// with colored terminal output. pub fn print_verification_summary() { - println!("{}", "═══ Formal Verification Status ═══".bright_blue().bold()); + println!( + "{}", + "═══ Formal Verification Status ═══".bright_blue().bold() + ); println!(); println!("{}: ~250+ theorems proven", "Total Proofs".bright_green()); println!("{}: 6", "Proof Systems".bright_green()); println!(); println!("{}", "Verification Systems:".bright_yellow()); - println!(" 1. {} - Calculus of Inductive Constructions", "Coq".bright_cyan()); + println!( + " 1. {} - Calculus of Inductive Constructions", + "Coq".bright_cyan() + ); println!(" 2. {} - Dependent Type Theory", "Lean 4".bright_cyan()); println!(" 3. {} - Intensional Type Theory", "Agda".bright_cyan()); println!(" 4. {} - Higher-Order Logic", "Isabelle/HOL".bright_cyan()); - println!(" 5. {} - Tarski-Grothendieck Set Theory", "Mizar".bright_cyan()); + println!( + " 5. {} - Tarski-Grothendieck Set Theory", + "Mizar".bright_cyan() + ); println!(" 6. {} - SMT Automated Verification", "Z3".bright_cyan()); println!(); @@ -298,10 +309,19 @@ pub fn print_verification_summary() { println!(); println!("{}", "Trust Model:".bright_yellow()); - println!(" {} Formal proofs verified by proof assistant kernels", "✓".bright_green()); - println!(" {} Zig FFI layer checks preconditions", "✓".bright_green()); + println!( + " {} Formal proofs verified by proof assistant kernels", + "✓".bright_green() + ); + println!( + " {} Zig FFI layer checks preconditions", + "✓".bright_green() + ); println!(" {} Rust CLI maintains undo stack", "✓".bright_green()); - println!(" {} POSIX semantics assumed correct (OS trust)", "○".bright_yellow()); + println!( + " {} POSIX semantics assumed correct (OS trust)", + "○".bright_yellow() + ); println!(); println!("{}", "Verification Gap:".bright_red()); diff --git a/impl/rust-cli/src/quotes.rs b/impl/rust-cli/src/quotes.rs index 7211f0e6..17374299 100644 --- a/impl/rust-cli/src/quotes.rs +++ b/impl/rust-cli/src/quotes.rs @@ -76,10 +76,7 @@ impl QuotedSegment { /// Check if this segment allows variable expansion pub fn allows_variable_expansion(&self) -> bool { - matches!( - self.state, - QuoteState::Unquoted | QuoteState::DoubleQuoted - ) + matches!(self.state, QuoteState::Unquoted | QuoteState::DoubleQuoted) } /// Check if this segment allows glob expansion @@ -228,7 +225,8 @@ pub fn parse_quotes(input: &str) -> Result> { // Check for unclosed quotes if state != QuoteState::Unquoted { - anyhow::bail!("Unclosed quote (expected closing {})", + anyhow::bail!( + "Unclosed quote (expected closing {})", match state { QuoteState::SingleQuoted => "'", QuoteState::DoubleQuoted => "\"", @@ -268,9 +266,9 @@ pub fn parse_quotes(input: &str) -> Result> { /// assert!(!should_expand_glob(&segments)); /// ``` pub fn should_expand_glob(segments: &[QuotedSegment]) -> bool { - segments.iter().any(|seg| { - seg.allows_glob_expansion() && crate::glob::contains_glob_pattern(&seg.content) - }) + segments + .iter() + .any(|seg| seg.allows_glob_expansion() && crate::glob::contains_glob_pattern(&seg.content)) } /// Reconstruct the expanded string from quoted segments diff --git a/impl/rust-cli/src/redirection.rs b/impl/rust-cli/src/redirection.rs index 3a282d55..e3d112c7 100644 --- a/impl/rust-cli/src/redirection.rs +++ b/impl/rust-cli/src/redirection.rs @@ -139,10 +139,7 @@ pub enum Redirection { }, /// Here string: `<<< string` - HereString { - content: String, - expand: bool, - }, + HereString { content: String, expand: bool }, } /// Type of file modification for undo tracking. @@ -171,10 +168,7 @@ pub enum FileModification { }, /// File was appended to - Appended { - path: PathBuf, - original_size: u64, - }, + Appended { path: PathBuf, original_size: u64 }, } impl FileModification { @@ -191,16 +185,18 @@ impl FileModification { pub fn reverse(&self) -> Result<()> { match self { FileModification::Created { path } => { - fs::remove_file(path) - .with_context(|| format!("Failed to remove created file: {}", path.display()))?; + fs::remove_file(path).with_context(|| { + format!("Failed to remove created file: {}", path.display()) + })?; } FileModification::Truncated { path, original_content, } => { - fs::write(path, original_content) - .with_context(|| format!("Failed to restore truncated file: {}", path.display()))?; + fs::write(path, original_content).with_context(|| { + format!("Failed to restore truncated file: {}", path.display()) + })?; } FileModification::Appended { @@ -212,10 +208,9 @@ impl FileModification { .open(path) .with_context(|| format!("Failed to open appended file: {}", path.display()))?; - file.set_len(*original_size) - .with_context(|| { - format!("Failed to truncate appended file: {}", path.display()) - })?; + file.set_len(*original_size).with_context(|| { + format!("Failed to truncate appended file: {}", path.display()) + })?; } } Ok(()) @@ -320,15 +315,15 @@ impl RedirectSetup { let metadata = fs::metadata(&path) .with_context(|| format!("Failed to stat file: {}", path.display()))?; - self.modifications - .push(FileModification::Appended { - path: path.clone(), - original_size: metadata.len(), - }); + self.modifications.push(FileModification::Appended { + path: path.clone(), + original_size: metadata.len(), + }); } else { // Truncate - save original content - let original_content = fs::read(&path) - .with_context(|| format!("Failed to read file for backup: {}", path.display()))?; + let original_content = fs::read(&path).with_context(|| { + format!("Failed to read file for backup: {}", path.display()) + })?; // Warn if file is large if original_content.len() > 10 * 1024 * 1024 { @@ -338,17 +333,15 @@ impl RedirectSetup { ); } - self.modifications - .push(FileModification::Truncated { - path: path.clone(), - original_content, - }); + self.modifications.push(FileModification::Truncated { + path: path.clone(), + original_content, + }); } } else { // File doesn't exist - will be created - self.modifications.push(FileModification::Created { - path: path.clone(), - }); + self.modifications + .push(FileModification::Created { path: path.clone() }); } // Open file for writing @@ -430,7 +423,11 @@ impl RedirectSetup { } }; - let mut op = Operation::new(op_type, path, state.active_transaction.as_ref().map(|t| t.id)); + let mut op = Operation::new( + op_type, + path, + state.active_transaction.as_ref().map(|t| t.id), + ); if let Some(data) = undo_data { op = op.with_undo_data(data); } @@ -529,10 +526,7 @@ fn validate_redirections(redirects: &[Redirection], state: &ShellState) -> Resul } /// Check for input/output conflict (same file) -fn validate_no_input_output_conflict( - redirects: &[Redirection], - state: &ShellState, -) -> Result<()> { +fn validate_no_input_output_conflict(redirects: &[Redirection], state: &ShellState) -> Result<()> { let input_files: HashSet = redirects .iter() .filter_map(|r| match r { @@ -622,8 +616,9 @@ fn validate_output_file(file: &str, state: &ShellState) -> Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - let metadata = fs::metadata(parent) - .with_context(|| format!("Failed to stat parent directory: {}", parent.display()))?; + let metadata = fs::metadata(parent).with_context(|| { + format!("Failed to stat parent directory: {}", parent.display()) + })?; let mode = metadata.permissions().mode(); if mode & 0o200 == 0 { @@ -635,7 +630,10 @@ fn validate_output_file(file: &str, state: &ShellState) -> Result<()> { // If file exists, check it's writable if path.exists() { if !path.is_file() { - anyhow::bail!("Output redirection target is not a file: {}", path.display()); + anyhow::bail!( + "Output redirection target is not a file: {}", + path.display() + ); } #[cfg(unix)] @@ -799,7 +797,10 @@ mod tests { FileModification::Truncated { .. } )); - if let FileModification::Truncated { original_content, .. } = &setup.modifications[0] { + if let FileModification::Truncated { + original_content, .. + } = &setup.modifications[0] + { assert_eq!(original_content, b"original content"); } } diff --git a/impl/rust-cli/src/repl.rs b/impl/rust-cli/src/repl.rs index 807780c0..f0d0411c 100644 --- a/impl/rust-cli/src/repl.rs +++ b/impl/rust-cli/src/repl.rs @@ -108,10 +108,7 @@ pub fn run(state: &mut ShellState) -> Result<()> { fn build_prompt(state: &ShellState) -> String { let txn_indicator = if let Some(ref txn) = state.active_transaction { - format!( - "{}/", - format!("txn:{}", txn.name).bright_cyan() - ) + format!("{}/", format!("txn:{}", txn.name).bright_cyan()) } else { String::new() }; @@ -153,18 +150,10 @@ fn execute_line(state: &mut ShellState, input: &str) -> Result { // Handle aliases before parsing let input_normalized = match trimmed { - s if s.starts_with("u ") || s == "u" => { - trimmed.replacen("u", "undo", 1) - } - s if s.starts_with("r ") || s == "r" => { - trimmed.replacen("r", "redo", 1) - } - s if s.starts_with("h ") || s == "h" => { - trimmed.replacen("h", "history", 1) - } - s if s.starts_with("g ") || s == "g" => { - trimmed.replacen("g", "graph", 1) - } + s if s.starts_with("u ") || s == "u" => trimmed.replacen("u", "undo", 1), + s if s.starts_with("r ") || s == "r" => trimmed.replacen("r", "redo", 1), + s if s.starts_with("h ") || s == "h" => trimmed.replacen("h", "history", 1), + s if s.starts_with("g ") || s == "g" => trimmed.replacen("g", "graph", 1), "q" => "quit".to_string(), _ => trimmed.to_string(), }; @@ -181,8 +170,7 @@ fn execute_line(state: &mut ShellState, input: &str) -> Result { // Handle execution result match result { ExecutionResult::Exit => Ok(true), - ExecutionResult::ExternalCommand { exit_code } - | ExecutionResult::Return { exit_code } => { + ExecutionResult::ExternalCommand { exit_code } | ExecutionResult::Return { exit_code } => { // Return leaking past a function boundary is defensive: the // `Command::Return` handler already errors if invoked outside // a function, so we treat it as a plain exit-code result here. @@ -198,41 +186,89 @@ fn print_help() { println!(); println!("{}", "Filesystem Operations:".bright_yellow()); - println!(" {} Create a directory", "mkdir ".bright_green()); - println!(" {} Remove an empty directory", "rmdir ".bright_green()); - println!(" {} Create an empty file", "touch ".bright_green()); + println!( + " {} Create a directory", + "mkdir ".bright_green() + ); + println!( + " {} Remove an empty directory", + "rmdir ".bright_green() + ); + println!( + " {} Create an empty file", + "touch ".bright_green() + ); println!(" {} Remove a file", "rm ".bright_green()); - println!(" {} List directory contents", "ls [path]".bright_green()); + println!( + " {} List directory contents", + "ls [path]".bright_green() + ); println!(" {} Change directory", "cd [path]".bright_green()); - println!(" {} Return to previous directory", "cd -".bright_green()); - println!(" {} Show current directory", "pwd".bright_green()); + println!( + " {} Return to previous directory", + "cd -".bright_green() + ); + println!( + " {} Show current directory", + "pwd".bright_green() + ); println!(); println!("{}", "Reversibility:".bright_yellow()); - println!(" {} Undo last operation(s)", "undo [N]".bright_cyan()); - println!(" {} Redo last undone operation(s)", "redo [N]".bright_cyan()); + println!( + " {} Undo last operation(s)", + "undo [N]".bright_cyan() + ); + println!( + " {} Redo last undone operation(s)", + "redo [N]".bright_cyan() + ); println!(" {} Show operation history", "history [N]".bright_cyan()); - println!(" {} Add --proofs to see theorems", "history -p".bright_cyan()); + println!( + " {} Add --proofs to see theorems", + "history -p".bright_cyan() + ); println!(" {} Show operation DAG", "graph".bright_cyan()); println!(); println!("{}", "Transactions:".bright_yellow()); - println!(" {} Start a transaction group", "begin ".bright_magenta()); - println!(" {} Commit current transaction", "commit".bright_magenta()); - println!(" {} Rollback current transaction", "rollback".bright_magenta()); + println!( + " {} Start a transaction group", + "begin ".bright_magenta() + ); + println!( + " {} Commit current transaction", + "commit".bright_magenta() + ); + println!( + " {} Rollback current transaction", + "rollback".bright_magenta() + ); println!(); println!("{}", "Job Control:".bright_yellow()); println!(" {} Run command in background", "cmd &".bright_green()); println!(" {} List all jobs", "jobs".bright_green()); println!(" {} List jobs with PIDs", "jobs -l".bright_green()); - println!(" {} Bring job to foreground", "fg [%N]".bright_green()); - println!(" {} Continue job in background", "bg [%N]".bright_green()); - println!(" {} Send signal to job", "kill [-%N] %N".bright_green()); + println!( + " {} Bring job to foreground", + "fg [%N]".bright_green() + ); + println!( + " {} Continue job in background", + "bg [%N]".bright_green() + ); + println!( + " {} Send signal to job", + "kill [-%N] %N".bright_green() + ); println!(); println!("{}", "Information:".bright_yellow()); - println!(" {} Show verification info", "proofs".bright_white()); + println!( + " {} Show verification info", + "proofs".bright_white() + ); println!(" {} Show shell status", "status".bright_white()); println!(" {} Show this help", "help".bright_white()); println!(); @@ -244,7 +280,11 @@ fn print_help() { println!("{}", "External Commands:".bright_yellow()); println!(" Any other command will be executed as an external program"); - println!(" Example: {} or {}", "ls -la".bright_green(), "cat file.txt".bright_green()); + println!( + " Example: {} or {}", + "ls -la".bright_green(), + "cat file.txt".bright_green() + ); println!(); println!( @@ -260,7 +300,11 @@ fn print_help() { fn print_status(state: &ShellState) { println!("{}", "═══ Shell Status ═══".bright_blue().bold()); println!(); - println!(" {}: {}", "Sandbox root".bright_black(), state.root.display()); + println!( + " {}: {}", + "Sandbox root".bright_black(), + state.root.display() + ); println!( " {}: {}", "Total operations".bright_black(), diff --git a/impl/rust-cli/src/secure_erase.rs b/impl/rust-cli/src/secure_erase.rs index 33c2f113..dd4d5e49 100644 --- a/impl/rust-cli/src/secure_erase.rs +++ b/impl/rust-cli/src/secure_erase.rs @@ -6,7 +6,7 @@ //! - NVMe Format/Sanitize (NVMe SSDs) //! - Cryptographic erase (when supported) -use anyhow::{Context, Result, bail}; +use anyhow::{bail, Context, Result}; use std::fs; use std::path::Path; use std::process::Command; @@ -59,11 +59,7 @@ pub fn detect_drive_type(device: &str) -> Result { } // Fallback: Check with smartctl if available - if let Ok(output) = Command::new("smartctl") - .arg("-i") - .arg(device) - .output() - { + if let Ok(output) = Command::new("smartctl").arg("-i").arg(device).output() { let info = String::from_utf8_lossy(&output.stdout); if info.contains("Solid State Device") || info.contains("NVMe") { return Ok(DriveType::SataSSD); @@ -115,8 +111,7 @@ pub fn check_ata_secure_erase_support(device: &str) -> Result { let info = String::from_utf8_lossy(&output.stdout); // Check for "supported: enhanced erase" - Ok(info.contains("supported: enhanced erase") || - info.contains("SECURITY ERASE UNIT")) + Ok(info.contains("supported: enhanced erase") || info.contains("SECURITY ERASE UNIT")) } /// Perform ATA Secure Erase @@ -187,7 +182,7 @@ pub fn nvme_format_crypto(device: &str) -> Result<()> { let status = Command::new("nvme") .arg("format") .arg(device) - .arg("--ses=1") // Secure Erase Settings: Cryptographic Erase + .arg("--ses=1") // Secure Erase Settings: Cryptographic Erase .status() .context("Failed to execute NVMe format")?; @@ -204,9 +199,9 @@ pub fn nvme_sanitize(device: &str, block_erase: bool) -> Result<()> { println!("🔥 Performing NVMe Sanitize (this may take a long time)..."); let action = if block_erase { - "--sanact=2" // Block Erase + "--sanact=2" // Block Erase } else { - "--sanact=1" // Exit Failure Mode + "--sanact=1" // Exit Failure Mode }; let status = Command::new("nvme") diff --git a/impl/rust-cli/src/state.rs b/impl/rust-cli/src/state.rs index 053ad96f..d9b40d71 100644 --- a/impl/rust-cli/src/state.rs +++ b/impl/rust-cli/src/state.rs @@ -114,15 +114,15 @@ impl OperationType { OperationType::FileTruncated => Some(OperationType::WriteFile), // Restore original content OperationType::FileAppended => Some(OperationType::FileAppended), // Self-inverse (truncate to original size) OperationType::CopyFile => Some(OperationType::DeleteFile), // Undo copy = delete destination - OperationType::Move => Some(OperationType::Move), // Self-inverse (move back) + OperationType::Move => Some(OperationType::Move), // Self-inverse (move back) OperationType::Symlink => Some(OperationType::Unlink), OperationType::Unlink => Some(OperationType::Symlink), OperationType::SetVariable => Some(OperationType::SetVariable), // Self-inverse (restore previous) OperationType::UnsetVariable => Some(OperationType::SetVariable), // Restore = set previous value OperationType::Chmod => Some(OperationType::Chmod), // Self-inverse (restore previous mode) OperationType::Chown => Some(OperationType::Chown), // Self-inverse (restore previous uid:gid) - OperationType::HardwareErase => None, // NOT REVERSIBLE - OperationType::Obliterate => None, // NOT REVERSIBLE - GDPR deletion + OperationType::HardwareErase => None, // NOT REVERSIBLE + OperationType::Obliterate => None, // NOT REVERSIBLE - GDPR deletion } } @@ -381,11 +381,7 @@ impl ShellState { // Persist state - warn on failure but don't abort if let Err(e) = self.save() { - eprintln!( - "{} Failed to save state: {}", - "Warning:".bright_yellow(), - e - ); + eprintln!("{} Failed to save state: {}", "Warning:".bright_yellow(), e); eprintln!("Operation succeeded but may not persist across restarts"); } } @@ -405,11 +401,7 @@ impl ShellState { // Persist state - warn on failure but don't abort if let Err(e) = self.save() { - eprintln!( - "{} Failed to save state: {}", - "Warning:".bright_yellow(), - e - ); + eprintln!("{} Failed to save state: {}", "Warning:".bright_yellow(), e); eprintln!("Redo succeeded but may not persist across restarts"); } } @@ -425,7 +417,8 @@ impl ShellState { // Archive old operations if path is set if let Some(ref archive_path) = self.history_archive_path { - if let Err(e) = self.archive_operations(&self.history[0..excess], archive_path) { + if let Err(e) = self.archive_operations(&self.history[0..excess], archive_path) + { eprintln!( "{} Failed to archive old history: {}", "Warning:".bright_yellow(), @@ -657,7 +650,8 @@ impl ShellState { /// Set a shell variable (no undo tracking — use set_variable_tracked for reversibility) pub fn set_variable(&mut self, name: impl Into, value: impl Into) { - self.variables.insert(name.into(), VariableValue::Scalar(value.into())); + self.variables + .insert(name.into(), VariableValue::Scalar(value.into())); } /// Set a shell variable with undo tracking. @@ -672,11 +666,11 @@ impl ShellState { let undo_data = serde_json::to_vec(&previous).unwrap_or_default(); // Perform the set - self.variables.insert(name.clone(), VariableValue::Scalar(value)); + self.variables + .insert(name.clone(), VariableValue::Scalar(value)); // Record operation - let op = Operation::new(OperationType::SetVariable, name, None) - .with_undo_data(undo_data); + let op = Operation::new(OperationType::SetVariable, name, None).with_undo_data(undo_data); self.record_operation(op); } @@ -741,7 +735,8 @@ impl ShellState { for (index, value) in elements.into_iter().enumerate() { array.insert(index, value); } - self.variables.insert(name.into(), VariableValue::Array(array)); + self.variables + .insert(name.into(), VariableValue::Array(array)); } /// Get a single array element by index @@ -757,9 +752,7 @@ impl ShellState { /// Returns None if variable doesn't exist or is not an array pub fn get_array_all(&self, name: &str) -> Option> { self.variables.get(name).and_then(|v| match v { - VariableValue::Array(arr) => { - Some(arr.values().map(|s| s.as_str()).collect()) - } + VariableValue::Array(arr) => Some(arr.values().map(|s| s.as_str()).collect()), VariableValue::Scalar(_) => None, }) } @@ -823,7 +816,9 @@ impl ShellState { /// Check if a variable is an array pub fn is_array(&self, name: &str) -> bool { - self.variables.get(name).map_or(false, |v| matches!(v, VariableValue::Array(_))) + self.variables + .get(name) + .map_or(false, |v| matches!(v, VariableValue::Array(_))) } // ======================================================================== @@ -846,10 +841,7 @@ impl ShellState { VariableValue::Scalar(s) => s.clone(), VariableValue::Array(arr) => { // Join array elements with spaces (bash behavior) - arr.values() - .cloned() - .collect::>() - .join(" ") + arr.values().cloned().collect::>().join(" ") } }; (name.clone(), string_value) @@ -997,7 +989,10 @@ mod tests { #[test] fn test_get_array_all() { let mut state = ShellState::new("/tmp/vsh_array_test_3").unwrap(); - state.set_array("arr", vec!["a".to_string(), "b".to_string(), "c".to_string()]); + state.set_array( + "arr", + vec!["a".to_string(), "b".to_string(), "c".to_string()], + ); let all = state.get_array_all("arr").unwrap(); assert_eq!(all, vec!["a", "b", "c"]); @@ -1105,7 +1100,10 @@ mod tests { #[test] fn test_export_array() { let mut state = ShellState::new("/tmp/vsh_array_test_13").unwrap(); - state.set_array("arr", vec!["one".to_string(), "two".to_string(), "three".to_string()]); + state.set_array( + "arr", + vec!["one".to_string(), "two".to_string(), "three".to_string()], + ); state.export_variable("arr"); let exported = state.get_exported_env(); @@ -1161,7 +1159,10 @@ mod tests { #[test] fn test_array_with_empty_strings() { let mut state = ShellState::new("/tmp/vsh_array_test_19").unwrap(); - state.set_array("arr", vec!["".to_string(), "nonempty".to_string(), "".to_string()]); + state.set_array( + "arr", + vec!["".to_string(), "nonempty".to_string(), "".to_string()], + ); assert_eq!(state.get_array_length("arr"), 3); assert_eq!(state.get_array_element("arr", 0), Some("")); @@ -1235,7 +1236,10 @@ mod tests { let last_op = state.history.last().unwrap(); let previous: Option = serde_json::from_slice(last_op.undo_data.as_ref().unwrap()).unwrap(); - assert_eq!(previous, Some(VariableValue::Scalar("old_value".to_string()))); + assert_eq!( + previous, + Some(VariableValue::Scalar("old_value".to_string())) + ); } #[test] @@ -1289,18 +1293,12 @@ mod tests { #[test] fn test_chmod_inverse() { - assert_eq!( - OperationType::Chmod.inverse(), - Some(OperationType::Chmod) - ); + assert_eq!(OperationType::Chmod.inverse(), Some(OperationType::Chmod)); } #[test] fn test_chown_inverse() { - assert_eq!( - OperationType::Chown.inverse(), - Some(OperationType::Chown) - ); + assert_eq!(OperationType::Chown.inverse(), Some(OperationType::Chown)); } #[test] diff --git a/impl/rust-cli/src/test_command.rs b/impl/rust-cli/src/test_command.rs index cb0a5281..90c74184 100644 --- a/impl/rust-cli/src/test_command.rs +++ b/impl/rust-cli/src/test_command.rs @@ -45,17 +45,17 @@ pub enum TestExpr { FileNotEmpty(String), // -s /// String tests - StringEmpty(String), // -z + StringEmpty(String), // -z StringNotEmpty(String), // -n StringEqual(String, String), StringNotEqual(String, String), /// Integer comparisons - IntEqual(String, String), // -eq - IntNotEqual(String, String), // -ne - IntLessThan(String, String), // -lt - IntLessEqual(String, String), // -le - IntGreater(String, String), // -gt + IntEqual(String, String), // -eq + IntNotEqual(String, String), // -ne + IntLessThan(String, String), // -lt + IntLessEqual(String, String), // -le + IntGreater(String, String), // -gt IntGreaterEqual(String, String), // -ge /// Logical operators @@ -88,13 +88,9 @@ impl TestExpr { // File tests TestExpr::FileExists(path) => Ok(Path::new(path).exists()), - TestExpr::FileIsRegular(path) => { - Ok(Path::new(path).is_file()) - } + TestExpr::FileIsRegular(path) => Ok(Path::new(path).is_file()), - TestExpr::FileIsDirectory(path) => { - Ok(Path::new(path).is_dir()) - } + TestExpr::FileIsDirectory(path) => Ok(Path::new(path).is_dir()), TestExpr::FileIsReadable(path) => { let path = Path::new(path); @@ -146,9 +142,11 @@ impl TestExpr { // Integer comparisons TestExpr::IntEqual(a, b) => { - let a_val = a.parse::() + let a_val = a + .parse::() .context("Invalid integer for -eq comparison")?; - let b_val = b.parse::() + let b_val = b + .parse::() .context("Invalid integer for -eq comparison")?; Ok(a_val == b_val) } @@ -184,9 +182,7 @@ impl TestExpr { } // Logical operators - TestExpr::Not(expr) => { - Ok(!expr.evaluate()?) - } + TestExpr::Not(expr) => Ok(!expr.evaluate()?), TestExpr::And(left, right) => { // Short-circuit evaluation @@ -336,7 +332,10 @@ fn parse_primary_expr(args: &[String], start: usize) -> Result<(TestExpr, usize) if start + 1 >= args.len() { bail!("test: -d requires argument"); } - return Ok((TestExpr::FileIsDirectory(args[start + 1].clone()), start + 2)); + return Ok(( + TestExpr::FileIsDirectory(args[start + 1].clone()), + start + 2, + )); } "-r" => { if start + 1 >= args.len() { @@ -354,7 +353,10 @@ fn parse_primary_expr(args: &[String], start: usize) -> Result<(TestExpr, usize) if start + 1 >= args.len() { bail!("test: -x requires argument"); } - return Ok((TestExpr::FileIsExecutable(args[start + 1].clone()), start + 2)); + return Ok(( + TestExpr::FileIsExecutable(args[start + 1].clone()), + start + 2, + )); } "-s" => { if start + 1 >= args.len() { @@ -435,7 +437,11 @@ pub fn execute_extended_test(args: &[String]) -> Result { // Verify all args were consumed if pos < args.len() { - bail!("[[: unexpected argument at position {}: '{}'", pos, args[pos]); + bail!( + "[[: unexpected argument at position {}: '{}'", + pos, + args[pos] + ); } let result = expr.evaluate()?; @@ -521,7 +527,10 @@ fn parse_extended_primary_expr(args: &[String], start: usize) -> Result<(TestExp if start + 1 >= args.len() { bail!("[[: -d requires argument"); } - return Ok((TestExpr::FileIsDirectory(args[start + 1].clone()), start + 2)); + return Ok(( + TestExpr::FileIsDirectory(args[start + 1].clone()), + start + 2, + )); } "-r" => { if start + 1 >= args.len() { @@ -539,7 +548,10 @@ fn parse_extended_primary_expr(args: &[String], start: usize) -> Result<(TestExp if start + 1 >= args.len() { bail!("[[: -x requires argument"); } - return Ok((TestExpr::FileIsExecutable(args[start + 1].clone()), start + 2)); + return Ok(( + TestExpr::FileIsExecutable(args[start + 1].clone()), + start + 2, + )); } "-s" => { if start + 1 >= args.len() { @@ -646,7 +658,9 @@ mod tests { #[test] fn test_string_empty() { assert!(TestExpr::StringEmpty("".to_string()).evaluate().unwrap()); - assert!(!TestExpr::StringEmpty("text".to_string()).evaluate().unwrap()); + assert!(!TestExpr::StringEmpty("text".to_string()) + .evaluate() + .unwrap()); } #[test] @@ -660,14 +674,26 @@ mod tests { #[test] fn test_int_comparisons() { - assert!(TestExpr::IntEqual("42".to_string(), "42".to_string()).evaluate().unwrap()); - assert!(!TestExpr::IntEqual("42".to_string(), "43".to_string()).evaluate().unwrap()); - - assert!(TestExpr::IntLessThan("10".to_string(), "20".to_string()).evaluate().unwrap()); - assert!(!TestExpr::IntLessThan("20".to_string(), "10".to_string()).evaluate().unwrap()); - - assert!(TestExpr::IntGreater("20".to_string(), "10".to_string()).evaluate().unwrap()); - assert!(!TestExpr::IntGreater("10".to_string(), "20".to_string()).evaluate().unwrap()); + assert!(TestExpr::IntEqual("42".to_string(), "42".to_string()) + .evaluate() + .unwrap()); + assert!(!TestExpr::IntEqual("42".to_string(), "43".to_string()) + .evaluate() + .unwrap()); + + assert!(TestExpr::IntLessThan("10".to_string(), "20".to_string()) + .evaluate() + .unwrap()); + assert!(!TestExpr::IntLessThan("20".to_string(), "10".to_string()) + .evaluate() + .unwrap()); + + assert!(TestExpr::IntGreater("20".to_string(), "10".to_string()) + .evaluate() + .unwrap()); + assert!(!TestExpr::IntGreater("10".to_string(), "20".to_string()) + .evaluate() + .unwrap()); } #[test] @@ -720,7 +746,10 @@ mod tests { fn test_parse_string_comparison() { let args = vec!["hello".to_string(), "=".to_string(), "world".to_string()]; let expr = parse_test_expr(&args, false).unwrap(); - assert_eq!(expr, TestExpr::StringEqual("hello".to_string(), "world".to_string())); + assert_eq!( + expr, + TestExpr::StringEqual("hello".to_string(), "world".to_string()) + ); } #[test] diff --git a/impl/rust-cli/tests/correspondence_tests.rs b/impl/rust-cli/tests/correspondence_tests.rs index 94d5170f..7b8b8f4a 100644 --- a/impl/rust-cli/tests/correspondence_tests.rs +++ b/impl/rust-cli/tests/correspondence_tests.rs @@ -28,7 +28,7 @@ use anyhow::Result; use std::fs; use tempfile::tempdir; -use vsh::commands::{mkdir, rmdir, rm, touch}; +use vsh::commands::{mkdir, rm, rmdir, touch}; use vsh::state::ShellState; /// Test: mkdir followed by rmdir restores original state @@ -160,7 +160,10 @@ fn test_mkdir_precondition_parent_exists() { // Attempting to create nested dir without parent should fail let result = mkdir(&mut state, "nonexistent/nested", false); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Parent directory does not exist")); + assert!(result + .unwrap_err() + .to_string() + .contains("Parent directory does not exist")); } /// Test: rmdir precondition - path must be a directory diff --git a/impl/rust-cli/tests/e2e_script_execution.rs b/impl/rust-cli/tests/e2e_script_execution.rs index 2bcf818c..f82023f5 100644 --- a/impl/rust-cli/tests/e2e_script_execution.rs +++ b/impl/rust-cli/tests/e2e_script_execution.rs @@ -12,9 +12,9 @@ mod e2e_script_execution { use std::fs; use tempfile::TempDir; + use vsh::commands::mkdir; use vsh::parser::parse_command; use vsh::state::ShellState; - use vsh::commands::mkdir; // ================================================================ // Basic Script Constructs @@ -25,7 +25,10 @@ mod e2e_script_execution { // Simulate: VAR=hello let result = parse_command("VAR=hello"); // Just verify it parses without error - assert!(result.is_ok(), "Variable assignment should parse successfully"); + assert!( + result.is_ok(), + "Variable assignment should parse successfully" + ); } #[test] @@ -49,7 +52,10 @@ mod e2e_script_execution { #[test] fn test_function_definition() { let result = parse_command("setup() { mkdir src; touch src/main.rs; }"); - assert!(result.is_ok(), "function definition should parse successfully"); + assert!( + result.is_ok(), + "function definition should parse successfully" + ); } // ================================================================ @@ -59,11 +65,7 @@ mod e2e_script_execution { #[test] fn test_multicommand_sequence_parsing() { // Simulate a multi-command script - let commands = vec![ - "mkdir project", - "cd project", - "touch file.txt", - ]; + let commands = vec!["mkdir project", "cd project", "touch file.txt"]; for cmd_str in commands { let result = parse_command(cmd_str); @@ -101,10 +103,7 @@ mod e2e_script_execution { fn test_unclosed_quote_detection() { // Unclosed quotes should be detected let result = parse_command("echo \"unclosed"); - assert!( - result.is_err(), - "Unclosed quote should fail" - ); + assert!(result.is_err(), "Unclosed quote should fail"); } #[test] @@ -227,7 +226,10 @@ mkdir test_dir // Test the complete if statement as it would be parsed let script = "if [ -d project ]; then echo yes; else echo no; fi"; let result = parse_command(script); - assert!(result.is_ok(), "Complete if/then/else/fi should parse successfully"); + assert!( + result.is_ok(), + "Complete if/then/else/fi should parse successfully" + ); } #[test] diff --git a/impl/rust-cli/tests/extended_test_tests.rs b/impl/rust-cli/tests/extended_test_tests.rs index 5c1cd372..532f6a6a 100644 --- a/impl/rust-cli/tests/extended_test_tests.rs +++ b/impl/rust-cli/tests/extended_test_tests.rs @@ -17,49 +17,77 @@ use vsh::test_command::execute_extended_test; #[test] fn test_pattern_match_simple() { - let args = vec!["test.txt".to_string(), "==".to_string(), "*.txt".to_string()]; + let args = vec![ + "test.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches pattern } #[test] fn test_pattern_match_no_match() { - let args = vec!["test.log".to_string(), "==".to_string(), "*.txt".to_string()]; + let args = vec![ + "test.log".to_string(), + "==".to_string(), + "*.txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - doesn't match pattern } #[test] fn test_pattern_nomatch_operator() { - let args = vec!["test.log".to_string(), "!=".to_string(), "*.txt".to_string()]; + let args = vec![ + "test.log".to_string(), + "!=".to_string(), + "*.txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - doesn't match pattern } #[test] fn test_pattern_question_mark() { - let args = vec!["file1.txt".to_string(), "==".to_string(), "file?.txt".to_string()]; + let args = vec![ + "file1.txt".to_string(), + "==".to_string(), + "file?.txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - single char wildcard } #[test] fn test_pattern_question_mark_no_match() { - let args = vec!["file10.txt".to_string(), "==".to_string(), "file?.txt".to_string()]; + let args = vec![ + "file10.txt".to_string(), + "==".to_string(), + "file?.txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - two chars don't match single wildcard } #[test] fn test_pattern_character_class() { - let args = vec!["file1.txt".to_string(), "==".to_string(), "file[0-9].txt".to_string()]; + let args = vec![ + "file1.txt".to_string(), + "==".to_string(), + "file[0-9].txt".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches character class } #[test] fn test_pattern_multiple_wildcards() { - let args = vec!["prefix-test-suffix".to_string(), "==".to_string(), "*-test-*".to_string()]; + let args = vec![ + "prefix-test-suffix".to_string(), + "==".to_string(), + "*-test-*".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches both wildcards } @@ -91,7 +119,11 @@ fn test_pattern_wildcard_matches_empty() { #[test] fn test_regex_match_digits() { - let args = vec!["12345".to_string(), "=~".to_string(), "^[0-9]+$".to_string()]; + let args = vec![ + "12345".to_string(), + "=~".to_string(), + "^[0-9]+$".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches digits } @@ -105,21 +137,33 @@ fn test_regex_match_no_match() { #[test] fn test_regex_email_pattern() { - let args = vec!["test@example.com".to_string(), "=~".to_string(), "^[a-z]+@[a-z]+\\.[a-z]+$".to_string()]; + let args = vec![ + "test@example.com".to_string(), + "=~".to_string(), + "^[a-z]+@[a-z]+\\.[a-z]+$".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches email pattern } #[test] fn test_regex_partial_match() { - let args = vec!["test123more".to_string(), "=~".to_string(), "[0-9]+".to_string()]; + let args = vec![ + "test123more".to_string(), + "=~".to_string(), + "[0-9]+".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - contains digits (partial match) } #[test] fn test_regex_anchored() { - let args = vec!["test123".to_string(), "=~".to_string(), "^[a-z]+$".to_string()]; + let args = vec![ + "test123".to_string(), + "=~".to_string(), + "^[a-z]+$".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - has digits, doesn't match letters-only } @@ -133,7 +177,11 @@ fn test_regex_empty_string() { #[test] fn test_regex_invalid_pattern() { - let args = vec!["test".to_string(), "=~".to_string(), "[invalid(".to_string()]; + let args = vec![ + "test".to_string(), + "=~".to_string(), + "[invalid(".to_string(), + ]; let result = execute_extended_test(&args); assert!(result.is_err()); // Error - invalid regex } @@ -192,9 +240,13 @@ fn test_lexical_case_sensitive() { #[test] fn test_and_operator_both_true() { let args = vec![ - "test".to_string(), "==".to_string(), "test".to_string(), + "test".to_string(), + "==".to_string(), + "test".to_string(), "&&".to_string(), - "file.txt".to_string(), "==".to_string(), "*.txt".to_string(), + "file.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - both conditions true @@ -203,9 +255,13 @@ fn test_and_operator_both_true() { #[test] fn test_and_operator_first_false() { let args = vec![ - "test".to_string(), "==".to_string(), "other".to_string(), + "test".to_string(), + "==".to_string(), + "other".to_string(), "&&".to_string(), - "file.txt".to_string(), "==".to_string(), "*.txt".to_string(), + "file.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - first condition false @@ -214,9 +270,13 @@ fn test_and_operator_first_false() { #[test] fn test_or_operator_first_true() { let args = vec![ - "test".to_string(), "==".to_string(), "test".to_string(), + "test".to_string(), + "==".to_string(), + "test".to_string(), "||".to_string(), - "file".to_string(), "==".to_string(), "*.txt".to_string(), + "file".to_string(), + "==".to_string(), + "*.txt".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - first condition true (short-circuit) @@ -225,9 +285,13 @@ fn test_or_operator_first_true() { #[test] fn test_or_operator_both_false() { let args = vec![ - "test".to_string(), "==".to_string(), "other".to_string(), + "test".to_string(), + "==".to_string(), + "other".to_string(), "||".to_string(), - "file.log".to_string(), "==".to_string(), "*.txt".to_string(), + "file.log".to_string(), + "==".to_string(), + "*.txt".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - both conditions false @@ -236,11 +300,17 @@ fn test_or_operator_both_false() { #[test] fn test_multiple_and_operators() { let args = vec![ - "a".to_string(), "==".to_string(), "a".to_string(), + "a".to_string(), + "==".to_string(), + "a".to_string(), "&&".to_string(), - "b".to_string(), "==".to_string(), "b".to_string(), + "b".to_string(), + "==".to_string(), + "b".to_string(), "&&".to_string(), - "c".to_string(), "==".to_string(), "c".to_string(), + "c".to_string(), + "==".to_string(), + "c".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - all three conditions true @@ -250,11 +320,17 @@ fn test_multiple_and_operators() { fn test_or_and_precedence() { // (false || true) && false = false let args = vec![ - "a".to_string(), "==".to_string(), "b".to_string(), // false + "a".to_string(), + "==".to_string(), + "b".to_string(), // false "||".to_string(), - "c".to_string(), "==".to_string(), "c".to_string(), // true + "c".to_string(), + "==".to_string(), + "c".to_string(), // true "&&".to_string(), - "d".to_string(), "==".to_string(), "e".to_string(), // false + "d".to_string(), + "==".to_string(), + "e".to_string(), // false ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - because && binds tighter than || @@ -266,14 +342,24 @@ fn test_or_and_precedence() { #[test] fn test_negation_simple() { - let args = vec!["!".to_string(), "test".to_string(), "==".to_string(), "other".to_string()]; + let args = vec![ + "!".to_string(), + "test".to_string(), + "==".to_string(), + "other".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - !(false) = true } #[test] fn test_negation_true_expr() { - let args = vec!["!".to_string(), "test".to_string(), "==".to_string(), "test".to_string()]; + let args = vec![ + "!".to_string(), + "test".to_string(), + "==".to_string(), + "test".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 1); // false - !(true) = false } @@ -283,7 +369,9 @@ fn test_double_negation() { let args = vec![ "!".to_string(), "!".to_string(), - "test".to_string(), "==".to_string(), "test".to_string(), + "test".to_string(), + "==".to_string(), + "test".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - !!(true) = true @@ -297,7 +385,9 @@ fn test_double_negation() { fn test_parentheses_grouping() { let args = vec![ "(".to_string(), - "a".to_string(), "==".to_string(), "a".to_string(), + "a".to_string(), + "==".to_string(), + "a".to_string(), ")".to_string(), ]; let result = execute_extended_test(&args).unwrap(); @@ -309,9 +399,13 @@ fn test_parentheses_with_or() { // (false || true) = true let args = vec![ "(".to_string(), - "a".to_string(), "==".to_string(), "b".to_string(), // false + "a".to_string(), + "==".to_string(), + "b".to_string(), // false "||".to_string(), - "c".to_string(), "==".to_string(), "c".to_string(), // true + "c".to_string(), + "==".to_string(), + "c".to_string(), // true ")".to_string(), ]; let result = execute_extended_test(&args).unwrap(); @@ -322,12 +416,18 @@ fn test_parentheses_with_or() { fn test_parentheses_change_precedence() { // false || (true && false) = false let args = vec![ - "a".to_string(), "==".to_string(), "b".to_string(), // false + "a".to_string(), + "==".to_string(), + "b".to_string(), // false "||".to_string(), "(".to_string(), - "c".to_string(), "==".to_string(), "c".to_string(), // true + "c".to_string(), + "==".to_string(), + "c".to_string(), // true "&&".to_string(), - "d".to_string(), "==".to_string(), "e".to_string(), // false + "d".to_string(), + "==".to_string(), + "e".to_string(), // false ")".to_string(), ]; let result = execute_extended_test(&args).unwrap(); @@ -339,7 +439,9 @@ fn test_negation_with_parentheses() { let args = vec![ "!".to_string(), "(".to_string(), - "a".to_string(), "==".to_string(), "a".to_string(), + "a".to_string(), + "==".to_string(), + "a".to_string(), ")".to_string(), ]; let result = execute_extended_test(&args).unwrap(); @@ -423,9 +525,13 @@ fn test_int_less_than() { #[test] fn test_pattern_and_regex_combined() { let args = vec![ - "test.txt".to_string(), "==".to_string(), "*.txt".to_string(), + "test.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), "&&".to_string(), - "test".to_string(), "=~".to_string(), "^[a-z]+$".to_string(), + "test".to_string(), + "=~".to_string(), + "^[a-z]+$".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - both match @@ -435,7 +541,9 @@ fn test_pattern_and_regex_combined() { fn test_negated_pattern() { let args = vec![ "!".to_string(), - "test.log".to_string(), "==".to_string(), "*.txt".to_string(), + "test.log".to_string(), + "==".to_string(), + "*.txt".to_string(), ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - !( false) = true @@ -446,12 +554,18 @@ fn test_complex_with_parentheses() { // (file.txt == *.txt && test =~ ^[a-z]+$) || false = true let args = vec![ "(".to_string(), - "file.txt".to_string(), "==".to_string(), "*.txt".to_string(), + "file.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), "&&".to_string(), - "test".to_string(), "=~".to_string(), "^[a-z]+$".to_string(), + "test".to_string(), + "=~".to_string(), + "^[a-z]+$".to_string(), ")".to_string(), "||".to_string(), - "a".to_string(), "==".to_string(), "b".to_string(), // false + "a".to_string(), + "==".to_string(), + "b".to_string(), // false ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - first part is true @@ -484,14 +598,22 @@ fn test_single_empty_string_arg() { #[test] fn test_pattern_with_spaces() { - let args = vec!["hello world".to_string(), "==".to_string(), "hello*".to_string()]; + let args = vec![ + "hello world".to_string(), + "==".to_string(), + "hello*".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - "hello world" matches "hello*" } #[test] fn test_regex_with_whitespace() { - let args = vec!["hello world".to_string(), "=~".to_string(), "^hello\\s+world$".to_string()]; + let args = vec![ + "hello world".to_string(), + "=~".to_string(), + "^hello\\s+world$".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - matches pattern with whitespace } @@ -500,7 +622,9 @@ fn test_regex_with_whitespace() { fn test_unclosed_parenthesis() { let args = vec![ "(".to_string(), - "a".to_string(), "==".to_string(), "a".to_string(), + "a".to_string(), + "==".to_string(), + "a".to_string(), // Missing ) ]; let result = execute_extended_test(&args); @@ -509,7 +633,12 @@ fn test_unclosed_parenthesis() { #[test] fn test_unexpected_operator() { - let args = vec!["a".to_string(), "==".to_string(), "b".to_string(), "extra".to_string()]; + let args = vec![ + "a".to_string(), + "==".to_string(), + "b".to_string(), + "extra".to_string(), + ]; let result = execute_extended_test(&args); assert!(result.is_err()); // Error - unexpected argument } @@ -521,7 +650,11 @@ fn test_unexpected_operator() { #[test] fn test_pattern_vs_literal_comparison() { // In [[ ]], == does pattern matching - let args = vec!["test.txt".to_string(), "==".to_string(), "*.txt".to_string()]; + let args = vec![ + "test.txt".to_string(), + "==".to_string(), + "*.txt".to_string(), + ]; let extended_result = execute_extended_test(&args).unwrap(); assert_eq!(extended_result, 0); // true - pattern matches @@ -536,7 +669,11 @@ fn test_pattern_vs_literal_comparison() { fn test_no_word_splitting_simulation() { // In [[]], variables don't undergo word splitting // Simulated by passing strings with spaces - let args = vec!["hello world".to_string(), "==".to_string(), "hello*".to_string()]; + let args = vec![ + "hello world".to_string(), + "==".to_string(), + "hello*".to_string(), + ]; let result = execute_extended_test(&args).unwrap(); assert_eq!(result, 0); // true - pattern matches full string } diff --git a/impl/rust-cli/tests/function_and_script_tests.rs b/impl/rust-cli/tests/function_and_script_tests.rs index 791af200..87697872 100644 --- a/impl/rust-cli/tests/function_and_script_tests.rs +++ b/impl/rust-cli/tests/function_and_script_tests.rs @@ -109,10 +109,7 @@ fn test_function_restores_positional_params() -> Result<()> { invoke_cmd.execute(&mut state)?; // Positional params should be restored - assert_eq!( - state.get_positional_param(1), - Some("outer_arg") - ); + assert_eq!(state.get_positional_param(1), Some("outer_arg")); Ok(()) } diff --git a/impl/rust-cli/tests/function_control_flow_tests.rs b/impl/rust-cli/tests/function_control_flow_tests.rs index 744e8b8e..77a70e26 100644 --- a/impl/rust-cli/tests/function_control_flow_tests.rs +++ b/impl/rust-cli/tests/function_control_flow_tests.rs @@ -43,8 +43,7 @@ fn function_body_contains_for_loop() -> Result<()> { let temp = tempdir()?; let mut state = ShellState::new(temp.path().to_str().unwrap())?; - parse_command("ffunc() { for d in a b c; do mkdir $d; done; }")? - .execute(&mut state)?; + parse_command("ffunc() { for d in a b c; do mkdir $d; done; }")?.execute(&mut state)?; parse_command("ffunc")?.execute(&mut state)?; assert!(state.resolve_path("a").exists()); @@ -58,10 +57,8 @@ fn function_body_contains_case_statement() -> Result<()> { let temp = tempdir()?; let mut state = ShellState::new(temp.path().to_str().unwrap())?; - parse_command( - "cfunc() { case $1 in a) mkdir chose_a ;; b) mkdir chose_b ;; esac; }", - )? - .execute(&mut state)?; + parse_command("cfunc() { case $1 in a) mkdir chose_a ;; b) mkdir chose_b ;; esac; }")? + .execute(&mut state)?; parse_command("cfunc b")?.execute(&mut state)?; @@ -79,8 +76,7 @@ fn return_from_inside_if_sets_exit_code() -> Result<()> { let temp = tempdir()?; let mut state = ShellState::new(temp.path().to_str().unwrap())?; - parse_command("rf() { if true; then return 7; fi; mkdir after; }")? - .execute(&mut state)?; + parse_command("rf() { if true; then return 7; fi; mkdir after; }")?.execute(&mut state)?; parse_command("rf")?.execute(&mut state)?; assert_eq!(state.last_exit_code, 7); diff --git a/impl/rust-cli/tests/ifs_splitting_tests.rs b/impl/rust-cli/tests/ifs_splitting_tests.rs index be231066..f20de76d 100644 --- a/impl/rust-cli/tests/ifs_splitting_tests.rs +++ b/impl/rust-cli/tests/ifs_splitting_tests.rs @@ -23,8 +23,7 @@ fn for_loop_splits_variable_by_default_ifs() -> Result<()> { state.set_variable("ITEMS", "alpha beta gamma"); - parse_command("for x in $ITEMS; do mkdir $x; done")? - .execute(&mut state)?; + parse_command("for x in $ITEMS; do mkdir $x; done")?.execute(&mut state)?; assert!(state.resolve_path("alpha").exists()); assert!(state.resolve_path("beta").exists()); @@ -40,8 +39,7 @@ fn for_loop_splits_variable_by_custom_ifs() -> Result<()> { state.set_variable("CSV", "one,two,three"); state.set_variable("IFS", ","); - parse_command("for x in $CSV; do mkdir $x; done")? - .execute(&mut state)?; + parse_command("for x in $CSV; do mkdir $x; done")?.execute(&mut state)?; assert!(state.resolve_path("one").exists()); assert!(state.resolve_path("two").exists()); @@ -60,8 +58,7 @@ fn for_loop_with_empty_ifs_does_not_split() -> Result<()> { // With empty IFS, the entire value is one word. // The for-loop body runs once and creates one dir whose name // contains a space. - parse_command("for x in $WORDS; do mkdir $x; done")? - .execute(&mut state)?; + parse_command("for x in $WORDS; do mkdir $x; done")?.execute(&mut state)?; // "hello world" stays as one token — but mkdir may receive it as // two words due to the shell's argument tokenization. The key test @@ -82,8 +79,7 @@ fn for_loop_splits_newlines_in_variable() -> Result<()> { // Newline is part of default IFS state.set_variable("LINES", "line1\nline2\nline3"); - parse_command("for x in $LINES; do mkdir $x; done")? - .execute(&mut state)?; + parse_command("for x in $LINES; do mkdir $x; done")?.execute(&mut state)?; assert!(state.resolve_path("line1").exists()); assert!(state.resolve_path("line2").exists()); @@ -99,8 +95,7 @@ fn for_loop_literal_words_not_affected_by_ifs() -> Result<()> { // Literal words in the for-list are already split by the parser // at whitespace boundaries. IFS affects EXPANSION splitting, not // literal splitting. - parse_command("for x in one two three; do mkdir $x; done")? - .execute(&mut state)?; + parse_command("for x in one two three; do mkdir $x; done")?.execute(&mut state)?; assert!(state.resolve_path("one").exists()); assert!(state.resolve_path("two").exists()); diff --git a/impl/rust-cli/tests/integration_test.rs b/impl/rust-cli/tests/integration_test.rs index e61c9f53..702c10fb 100644 --- a/impl/rust-cli/tests/integration_test.rs +++ b/impl/rust-cli/tests/integration_test.rs @@ -41,7 +41,10 @@ fn test_mkdir_rmdir_reversible() { fs::remove_dir(&target).unwrap(); // Verify return to initial state - assert!(!target.exists(), "rmdir(mkdir(p)) should return to initial state"); + assert!( + !target.exists(), + "rmdir(mkdir(p)) should return to initial state" + ); } /// Test: touch followed by rm returns to initial state @@ -64,7 +67,10 @@ fn test_create_delete_file_reversible() { fs::remove_file(&target).unwrap(); // Verify return to initial state - assert!(!target.exists(), "rm(touch(p)) should return to initial state"); + assert!( + !target.exists(), + "rm(touch(p)) should return to initial state" + ); } /// Test: Nested operations can be fully reversed @@ -98,7 +104,10 @@ fn test_operation_sequence_reversible() { fs::remove_dir(&dir1).unwrap(); // Verify return to initial state - assert!(!dir1.exists(), "Sequence reversal should return to initial state"); + assert!( + !dir1.exists(), + "Sequence reversal should return to initial state" + ); } // ============================================================ @@ -135,7 +144,10 @@ fn test_mkdir_enoent() { let target = temp.path().join("nonexistent/child"); let result = fs::create_dir(&target); - assert!(result.is_err(), "mkdir should fail when parent doesn't exist"); + assert!( + result.is_err(), + "mkdir should fail when parent doesn't exist" + ); let err = result.unwrap_err(); assert_eq!( @@ -162,12 +174,14 @@ fn test_rmdir_enotempty() { // Some systems return PermissionDenied or Other let err = result.unwrap_err(); assert!( - matches!(err.kind(), - std::io::ErrorKind::DirectoryNotEmpty | - std::io::ErrorKind::PermissionDenied | - std::io::ErrorKind::Other + matches!( + err.kind(), + std::io::ErrorKind::DirectoryNotEmpty + | std::io::ErrorKind::PermissionDenied + | std::io::ErrorKind::Other ), - "Error should indicate directory not empty, got {:?}", err.kind() + "Error should indicate directory not empty, got {:?}", + err.kind() ); } @@ -296,14 +310,21 @@ fn test_transaction_rollback_simulation() { // Rollback: reverse all operations for (op, path) in operations.iter().rev() { match *op { - "mkdir" => { fs::remove_dir(path).unwrap(); } - "touch" => { fs::remove_file(path).unwrap(); } + "mkdir" => { + fs::remove_dir(path).unwrap(); + } + "touch" => { + fs::remove_file(path).unwrap(); + } _ => {} } } // Verify rollback - assert!(!target1.exists(), "Rollback should remove all created items"); + assert!( + !target1.exists(), + "Rollback should remove all created items" + ); assert!(!target2.exists()); assert!(!target3.exists()); } @@ -354,7 +375,10 @@ fn test_deep_nesting() { current = current.parent().unwrap().to_path_buf(); } - assert!(!temp.path().join("a").exists(), "Deep cleanup should remove all"); + assert!( + !temp.path().join("a").exists(), + "Deep cleanup should remove all" + ); } /// Test: Special characters in paths @@ -680,13 +704,20 @@ fn test_redirect_undo_file_truncated() { .unwrap(); let truncated = fs::read_to_string(&target).unwrap(); - assert_ne!(truncated.trim(), original_content, "File should be truncated"); + assert_ne!( + truncated.trim(), + original_content, + "File should be truncated" + ); // Undo: restore original content fs::write(&target, &saved_content).unwrap(); let restored = fs::read_to_string(&target).unwrap(); - assert_eq!(restored, original_content, "Undo should restore original content"); + assert_eq!( + restored, original_content, + "Undo should restore original content" + ); } /// Test: Undo file append from redirection @@ -719,8 +750,14 @@ fn test_redirect_undo_file_appended() { // Verify append happened let appended = fs::read_to_string(&target).unwrap(); - assert!(appended.len() > original_content.len(), "File should be larger"); - assert!(appended.contains("line2"), "Appended content should be present"); + assert!( + appended.len() > original_content.len(), + "File should be larger" + ); + assert!( + appended.contains("line2"), + "Appended content should be present" + ); // Undo: truncate to original size let file = OpenOptions::new().write(true).open(&target).unwrap(); @@ -729,7 +766,10 @@ fn test_redirect_undo_file_appended() { // Verify undo let restored = fs::read_to_string(&target).unwrap(); - assert_eq!(restored, original_content, "Undo should restore original size"); + assert_eq!( + restored, original_content, + "Undo should restore original size" + ); } /// Test: Redo file truncation @@ -742,15 +782,27 @@ fn test_redirect_redo_truncate() { // Truncate fs::write(&target, "").unwrap(); - assert_eq!(fs::read_to_string(&target).unwrap(), "", "Should be truncated"); + assert_eq!( + fs::read_to_string(&target).unwrap(), + "", + "Should be truncated" + ); // Undo (restore) fs::write(&target, "original").unwrap(); - assert_eq!(fs::read_to_string(&target).unwrap(), "original", "Should be restored"); + assert_eq!( + fs::read_to_string(&target).unwrap(), + "original", + "Should be restored" + ); // Redo (truncate again) fs::write(&target, "").unwrap(); - assert_eq!(fs::read_to_string(&target).unwrap(), "", "Should be truncated again"); + assert_eq!( + fs::read_to_string(&target).unwrap(), + "", + "Should be truncated again" + ); } // ============================================================ // Glob Expansion Tests (Phase 6 M12) @@ -770,9 +822,18 @@ fn test_glob_wildcard_expansion() { let matches = vsh::glob::expand_glob("*.txt", temp.path()).unwrap(); let names: Vec = matches.iter().map(|p| p.display().to_string()).collect(); - assert!(names.contains(&"file1.txt".to_string()), "Should match file1.txt"); - assert!(names.contains(&"file2.txt".to_string()), "Should match file2.txt"); - assert!(!names.iter().any(|n| n.contains("file3.log")), "Should not match .log files"); + assert!( + names.contains(&"file1.txt".to_string()), + "Should match file1.txt" + ); + assert!( + names.contains(&"file2.txt".to_string()), + "Should match file2.txt" + ); + assert!( + !names.iter().any(|n| n.contains("file3.log")), + "Should not match .log files" + ); } /// Test: Question mark glob (file?.txt) @@ -789,9 +850,18 @@ fn test_glob_question_mark() { let matches = vsh::glob::expand_glob("file?.txt", temp.path()).unwrap(); let names: Vec = matches.iter().map(|p| p.display().to_string()).collect(); - assert!(names.contains(&"file1.txt".to_string()), "Should match single character"); - assert!(names.contains(&"file2.txt".to_string()), "Should match single character"); - assert!(!names.contains(&"file10.txt".to_string()), "Should not match two characters"); + assert!( + names.contains(&"file1.txt".to_string()), + "Should match single character" + ); + assert!( + names.contains(&"file2.txt".to_string()), + "Should match single character" + ); + assert!( + !names.contains(&"file10.txt".to_string()), + "Should not match two characters" + ); } /// Test: Brace expansion (file{1,2,3}.txt) @@ -812,11 +882,19 @@ fn test_glob_brace_expansion() { // Each expanded pattern should match existing files when globbed for pattern in &expanded { let matches = vsh::glob::expand_glob(pattern, temp.path()).unwrap(); - assert_eq!(matches.len(), 1, "Pattern {} should match one file", pattern); + assert_eq!( + matches.len(), + 1, + "Pattern {} should match one file", + pattern + ); } // file4.txt should not be in the expansion - assert!(!expanded.contains(&"file4.txt".to_string()), "Should not include file4.txt"); + assert!( + !expanded.contains(&"file4.txt".to_string()), + "Should not include file4.txt" + ); } /// Test: Empty glob matches return literal (POSIX behavior) @@ -834,7 +912,10 @@ fn test_glob_no_matches_literal() { .unwrap(); let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("*.xyz"), "Should pass literal pattern when no matches"); + assert!( + stdout.contains("*.xyz"), + "Should pass literal pattern when no matches" + ); } /// Test: Glob expansion in multiple arguments @@ -852,13 +933,31 @@ fn test_glob_multiple_args() { let txt_matches = vsh::glob::expand_glob("*.txt", temp.path()).unwrap(); let log_matches = vsh::glob::expand_glob("*.log", temp.path()).unwrap(); - let txt_names: Vec = txt_matches.iter().map(|p| p.display().to_string()).collect(); - let log_names: Vec = log_matches.iter().map(|p| p.display().to_string()).collect(); + let txt_names: Vec = txt_matches + .iter() + .map(|p| p.display().to_string()) + .collect(); + let log_names: Vec = log_matches + .iter() + .map(|p| p.display().to_string()) + .collect(); - assert!(txt_names.contains(&"a1.txt".to_string()), "Should expand *.txt"); - assert!(txt_names.contains(&"a2.txt".to_string()), "Should expand *.txt"); - assert!(log_names.contains(&"b1.log".to_string()), "Should expand *.log"); - assert!(log_names.contains(&"b2.log".to_string()), "Should expand *.log"); + assert!( + txt_names.contains(&"a1.txt".to_string()), + "Should expand *.txt" + ); + assert!( + txt_names.contains(&"a2.txt".to_string()), + "Should expand *.txt" + ); + assert!( + log_names.contains(&"b1.log".to_string()), + "Should expand *.log" + ); + assert!( + log_names.contains(&"b2.log".to_string()), + "Should expand *.log" + ); } /// Test: Glob patterns do not expand in quoted strings @@ -893,8 +992,14 @@ fn test_glob_hidden_files() { let matches = vsh::glob::expand_glob("*.txt", temp.path()).unwrap(); let names: Vec = matches.iter().map(|p| p.display().to_string()).collect(); - assert!(names.contains(&"visible.txt".to_string()), "Should match visible files"); - assert!(!names.iter().any(|n| n.contains(".hidden")), "Should not match hidden files without explicit ."); + assert!( + names.contains(&"visible.txt".to_string()), + "Should match visible files" + ); + assert!( + !names.iter().any(|n| n.contains(".hidden")), + "Should not match hidden files without explicit ." + ); } /// Test: Glob character class [0-9] @@ -911,7 +1016,16 @@ fn test_glob_character_class() { let matches = vsh::glob::expand_glob("file[0-9].txt", temp.path()).unwrap(); let names: Vec = matches.iter().map(|p| p.display().to_string()).collect(); - assert!(names.contains(&"file1.txt".to_string()), "Should match digit 1"); - assert!(names.contains(&"file2.txt".to_string()), "Should match digit 2"); - assert!(!names.contains(&"fileA.txt".to_string()), "Should not match letter A"); + assert!( + names.contains(&"file1.txt".to_string()), + "Should match digit 1" + ); + assert!( + names.contains(&"file2.txt".to_string()), + "Should match digit 2" + ); + assert!( + !names.contains(&"fileA.txt".to_string()), + "Should not match letter A" + ); } diff --git a/impl/rust-cli/tests/integration_tests.rs b/impl/rust-cli/tests/integration_tests.rs index 3303404e..d643c895 100644 --- a/impl/rust-cli/tests/integration_tests.rs +++ b/impl/rust-cli/tests/integration_tests.rs @@ -12,9 +12,9 @@ //! - History limits use anyhow::Result; +use reedline::Highlighter; use std::path::PathBuf; use tempfile::TempDir; -use reedline::Highlighter; use vsh::{ audit_log::{AuditEntry, AuditLog}, commands, @@ -159,7 +159,10 @@ fn test_history_limits() -> Result<()> { } // Verify history is limited - assert!(state.history.len() <= 5, "History should be limited to 5 operations"); + assert!( + state.history.len() <= 5, + "History should be limited to 5 operations" + ); Ok(()) } @@ -242,7 +245,10 @@ fn test_all_features_integration() -> Result<()> { // Verify audit log let entries = audit_log.read_all()?; - assert!(entries.len() >= 4, "Audit log should contain at least 4 entries"); + assert!( + entries.len() >= 4, + "Audit log should contain at least 4 entries" + ); // Test undo commands::undo(&mut state, 2, false)?; diff --git a/impl/rust-cli/tests/multiline_script_tests.rs b/impl/rust-cli/tests/multiline_script_tests.rs index 643159a9..2f115f3d 100644 --- a/impl/rust-cli/tests/multiline_script_tests.rs +++ b/impl/rust-cli/tests/multiline_script_tests.rs @@ -37,9 +37,12 @@ fn statement_splitter_splits_on_newline() { #[test] fn statement_splitter_keeps_multiline_if_together() { - let parts = - split_on_statement_separators("if true\nthen\n mkdir d\nfi\necho after"); - let segs: Vec<&str> = parts.iter().map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); + let parts = split_on_statement_separators("if true\nthen\n mkdir d\nfi\necho after"); + let segs: Vec<&str> = parts + .iter() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); // The whole if/fi block is one segment, echo is a second. assert_eq!(segs.len(), 2); assert!(segs[0].starts_with("if") && segs[0].ends_with("fi")); @@ -49,7 +52,11 @@ fn statement_splitter_keeps_multiline_if_together() { #[test] fn statement_splitter_keeps_function_def_together() { let parts = split_on_statement_separators("foo() { mkdir a; mkdir b; }\necho x"); - let segs: Vec<&str> = parts.iter().map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); + let segs: Vec<&str> = parts + .iter() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); assert_eq!(segs.len(), 2); assert_eq!(segs[0], "foo() { mkdir a; mkdir b; }"); assert_eq!(segs[1], "echo x"); @@ -58,7 +65,11 @@ fn statement_splitter_keeps_function_def_together() { #[test] fn statement_splitter_ignores_newlines_inside_quotes() { let parts = split_on_statement_separators("echo 'a\nb'\necho c"); - let segs: Vec<&str> = parts.iter().map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); + let segs: Vec<&str> = parts + .iter() + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(); assert_eq!(segs.len(), 2); assert_eq!(segs[0], "echo 'a\nb'"); assert_eq!(segs[1], "echo c"); @@ -104,10 +115,7 @@ fn sourced_script_defines_and_invokes_multiline_function() -> Result<()> { let mut state = ShellState::new(temp.path().to_str().unwrap())?; let script = temp.path().join("fns.sh"); - fs::write( - &script, - "greet() { mkdir hi; }\n# a comment\ngreet\n", - )?; + fs::write(&script, "greet() { mkdir hi; }\n# a comment\ngreet\n")?; parse_command(&format!("source {}", script.display()))?.execute(&mut state)?; diff --git a/impl/rust-cli/tests/parameter_expansion_tests.rs b/impl/rust-cli/tests/parameter_expansion_tests.rs index 9eeca9dd..5c166263 100644 --- a/impl/rust-cli/tests/parameter_expansion_tests.rs +++ b/impl/rust-cli/tests/parameter_expansion_tests.rs @@ -9,9 +9,9 @@ //! - String length: ${#VAR} //! - Substring: ${VAR:offset}, ${VAR:offset:length} -use vsh::state::ShellState; -use vsh::parser::expand_variables; use tempfile::TempDir; +use vsh::parser::expand_variables; +use vsh::state::ShellState; /// Helper to create test state fn test_state() -> ShellState { @@ -40,23 +40,26 @@ fn test_default_value_set() { #[test] fn test_default_value_null_with_colon() { let mut state = test_state(); - state.set_variable("VAR", ""); // Set to empty - // ${VAR:-default} checks for null, so should use default + state.set_variable("VAR", ""); // Set to empty + // ${VAR:-default} checks for null, so should use default assert_eq!(expand_variables("${VAR:-default}", &state), "default"); } #[test] fn test_default_value_null_without_colon() { let mut state = test_state(); - state.set_variable("VAR", ""); // Set to empty - // ${VAR-default} doesn't check for null, only unset + state.set_variable("VAR", ""); // Set to empty + // ${VAR-default} doesn't check for null, only unset assert_eq!(expand_variables("${VAR-default}", &state), ""); } #[test] fn test_default_value_with_spaces() { let state = test_state(); - assert_eq!(expand_variables("${VAR:-default value}", &state), "default value"); + assert_eq!( + expand_variables("${VAR:-default value}", &state), + "default value" + ); } #[test] @@ -64,10 +67,7 @@ fn test_default_value_multiple_in_string() { let mut state = test_state(); state.set_variable("A", "foo"); // B is unset - assert_eq!( - expand_variables("${A:-x} and ${B:-y}", &state), - "foo and y" - ); + assert_eq!(expand_variables("${A:-x} and ${B:-y}", &state), "foo and y"); } // ============================================================================ @@ -386,7 +386,10 @@ fn test_length_special_chars() { fn test_default_with_dollar_signs() { let state = test_state(); // Default value contains literal dollar (not variable) - assert_eq!(expand_variables("${VAR:-$$}", &state), format!("{}", std::process::id())); + assert_eq!( + expand_variables("${VAR:-$$}", &state), + format!("{}", std::process::id()) + ); } // ============================================================================ @@ -410,10 +413,7 @@ fn test_deeply_nested_with_mixed_operators() { state.set_variable("X", "set"); state.set_variable("Y", "value"); // Nested with different operators - assert_eq!( - expand_variables("${A:-${X:+${Y}}}", &state), - "value" - ); + assert_eq!(expand_variables("${A:-${X:+${Y}}}", &state), "value"); } #[test] @@ -516,7 +516,10 @@ fn test_unicode_emoji_substring() { fn test_default_value_with_newlines() { let state = test_state(); // Default contains newlines - assert_eq!(expand_variables("${VAR:-line1\nline2}", &state), "line1\nline2"); + assert_eq!( + expand_variables("${VAR:-line1\nline2}", &state), + "line1\nline2" + ); } #[test] @@ -577,7 +580,10 @@ fn test_default_with_special_chars() { let mut state = test_state(); state.set_variable("MYPATH", "/usr/bin"); // Default with variable expansion (MYPATH will be expanded) - assert_eq!(expand_variables("${VAR:-$MYPATH:/usr/local}", &state), "/usr/bin:/usr/local"); + assert_eq!( + expand_variables("${VAR:-$MYPATH:/usr/local}", &state), + "/usr/bin:/usr/local" + ); } #[test] @@ -592,7 +598,10 @@ fn test_nested_in_double_quotes() { fn test_whitespace_in_default() { let state = test_state(); // Default with various whitespace - assert_eq!(expand_variables("${VAR:- tabs\t\tand spaces }", &state), " tabs\t\tand spaces "); + assert_eq!( + expand_variables("${VAR:- tabs\t\tand spaces }", &state), + " tabs\t\tand spaces " + ); } #[test] diff --git a/impl/rust-cli/tests/property_correspondence_tests.rs b/impl/rust-cli/tests/property_correspondence_tests.rs index 4245e997..048924d1 100644 --- a/impl/rust-cli/tests/property_correspondence_tests.rs +++ b/impl/rust-cli/tests/property_correspondence_tests.rs @@ -10,7 +10,7 @@ use anyhow::Result; use proptest::prelude::*; use std::fs; use tempfile::tempdir; -use vsh::commands::{mkdir, rmdir, rm, touch}; +use vsh::commands::{mkdir, rm, rmdir, touch}; use vsh::state::ShellState; // ============================================================================ @@ -24,20 +24,18 @@ fn dir_name() -> impl Strategy { /// Generate valid file names with extension fn file_name() -> impl Strategy { - ("[a-z][a-z0-9_]{0,10}", "[a-z]{2,4}") - .prop_map(|(name, ext)| format!("{}.{}", name, ext)) + ("[a-z][a-z0-9_]{0,10}", "[a-z]{2,4}").prop_map(|(name, ext)| format!("{}.{}", name, ext)) } /// Generate lists of directory names (for path components) fn dir_path() -> impl Strategy { - prop::collection::vec(dir_name(), 1..=3) - .prop_map(|parts| parts.join("/")) + prop::collection::vec(dir_name(), 1..=3).prop_map(|parts| parts.join("/")) } /// Generate either a file or directory name fn any_name() -> impl Strategy { prop_oneof![ - dir_name().prop_map(|n| (n, true)), // (name, is_dir) + dir_name().prop_map(|n| (n, true)), // (name, is_dir) file_name().prop_map(|n| (n, false)), // (name, is_file) ] } diff --git a/impl/rust-cli/tests/property_tests.rs b/impl/rust-cli/tests/property_tests.rs index 8130d00a..b9ffc599 100644 --- a/impl/rust-cli/tests/property_tests.rs +++ b/impl/rust-cli/tests/property_tests.rs @@ -43,7 +43,11 @@ impl FsSnapshot { for entry in fs::read_dir(current)? { let entry = entry?; let path = entry.path(); - let rel_path = path.strip_prefix(base).unwrap().to_string_lossy().to_string(); + let rel_path = path + .strip_prefix(base) + .unwrap() + .to_string_lossy() + .to_string(); if path.is_dir() { dirs.push(rel_path.clone()); @@ -74,8 +78,7 @@ fn valid_path_strategy() -> impl Strategy { /// Valid nested path strategy fn valid_nested_path_strategy() -> impl Strategy { - prop::collection::vec(valid_path_strategy(), 1..=3) - .prop_map(|parts| parts.join("/")) + prop::collection::vec(valid_path_strategy(), 1..=3).prop_map(|parts| parts.join("/")) } // ============================================================ @@ -1038,10 +1041,10 @@ fn prop_quote_prevents_glob() { // Test various quoted glob patterns let test_cases = vec![ - ("'*.txt'", true), // Single quotes - no expansion - ("\"*.txt\"", true), // Double quotes - no expansion - ("*.txt", false), // Unquoted - should expand - ("'[abc]'", true), // Bracket glob in quotes + ("'*.txt'", true), // Single quotes - no expansion + ("\"*.txt\"", true), // Double quotes - no expansion + ("*.txt", false), // Unquoted - should expand + ("'[abc]'", true), // Bracket glob in quotes ("'{1,2,3}'", true), // Brace expansion in quotes ]; @@ -1050,9 +1053,9 @@ fn prop_quote_prevents_glob() { if should_be_quoted { // At least one segment should be quoted - let has_quoted = segments.iter().any(|seg| { - !matches!(seg.state, QuoteState::Unquoted) - }); + let has_quoted = segments + .iter() + .any(|seg| !matches!(seg.state, QuoteState::Unquoted)); assert!(has_quoted, "Pattern {} should have quoted segments", input); } } diff --git a/impl/rust-cli/tests/secure_audit_prop_tests.rs b/impl/rust-cli/tests/secure_audit_prop_tests.rs index f7f20a80..b8b45ee5 100644 --- a/impl/rust-cli/tests/secure_audit_prop_tests.rs +++ b/impl/rust-cli/tests/secure_audit_prop_tests.rs @@ -34,8 +34,7 @@ fn dir_name() -> impl Strategy { } fn file_name() -> impl Strategy { - ("[a-z][a-z0-9_]{0,10}", "[a-z]{1,4}") - .prop_map(|(name, ext)| format!("{}.{}", name, ext)) + ("[a-z][a-z0-9_]{0,10}", "[a-z]{1,4}").prop_map(|(name, ext)| format!("{}.{}", name, ext)) } fn file_content() -> impl Strategy> { diff --git a/impl/rust-cli/tests/security_tests.rs b/impl/rust-cli/tests/security_tests.rs index 9d891ca4..af1d2720 100644 --- a/impl/rust-cli/tests/security_tests.rs +++ b/impl/rust-cli/tests/security_tests.rs @@ -42,7 +42,10 @@ fn security_no_command_injection_via_path() { if result.is_ok() { // If it succeeded, verify it created a literal directory with that name let created_path = state.root.join(malicious_path); - assert!(created_path.exists(), "Should create literal path, not execute command"); + assert!( + created_path.exists(), + "Should create literal path, not execute command" + ); } } } @@ -55,7 +58,10 @@ fn security_no_shell_metacharacter_execution() { let test_cases = vec![ ("mkdir foo; rm -rf /", "Should not execute rm"), ("touch file && cat /etc/passwd", "Should not execute cat"), - ("mkdir test | grep test", "Should parse as pipeline, not execute"), + ( + "mkdir test | grep test", + "Should parse as pipeline, not execute", + ), ]; for (input, expected_behavior) in test_cases { @@ -139,11 +145,7 @@ fn security_null_byte_injection() { let mut state = ShellState::new(temp.path().to_str().unwrap()).unwrap(); // Null byte injection attempts - let null_byte_paths = vec![ - "file\0.txt", - "foo\0bar", - "test\0/etc/passwd", - ]; + let null_byte_paths = vec!["file\0.txt", "foo\0bar", "test\0/etc/passwd"]; for path in null_byte_paths { let result = vsh::commands::touch(&mut state, path, true); @@ -184,12 +186,7 @@ fn security_unicode_handling() { let mut state = ShellState::new(temp.path().to_str().unwrap()).unwrap(); // Unicode in paths (valid on modern filesystems) - let unicode_paths = vec![ - "测试文件.txt", - "файл.txt", - "αρχείο.txt", - "🚀.txt", - ]; + let unicode_paths = vec!["测试文件.txt", "файл.txt", "αρχείο.txt", "🚀.txt"]; for path in unicode_paths { let result = vsh::commands::touch(&mut state, path, true); @@ -197,7 +194,11 @@ fn security_unicode_handling() { // Should handle Unicode gracefully if result.is_ok() { let created_path = state.root.join(path); - assert!(created_path.exists(), "Unicode path should be supported: {}", path); + assert!( + created_path.exists(), + "Unicode path should be supported: {}", + path + ); } } } @@ -229,8 +230,8 @@ fn security_no_infinite_loops_in_parser() { // Parser should handle malformed input without hanging let malicious_inputs = vec![ - "(((((((((((((((((((((((((((((((((((((((((", // Deep nesting - "\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"", // Many quotes + "(((((((((((((((((((((((((((((((((((((((((", // Deep nesting + "\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"", // Many quotes "||||||||||||||||||||||||||||||||||||||||", // Many pipes "&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&", // Many operators ]; diff --git a/impl/rust-cli/tests/stress_tests.rs b/impl/rust-cli/tests/stress_tests.rs index 441d9365..464c3b26 100644 --- a/impl/rust-cli/tests/stress_tests.rs +++ b/impl/rust-cli/tests/stress_tests.rs @@ -41,7 +41,10 @@ fn stress_deep_nesting_1000_levels() { fs::create_dir_all(¤t_path).unwrap(); let create_time = start.elapsed(); - println!("✓ Created 1000-level nested directories in {:?}", create_time); + println!( + "✓ Created 1000-level nested directories in {:?}", + create_time + ); // Verify it exists assert!(current_path.exists()); @@ -83,7 +86,10 @@ fn stress_deep_nesting_no_stack_overflow() { // Should not cause stack overflow let result = fs::create_dir_all(¤t_path); - assert!(result.is_ok(), "Should handle 5000 levels without stack overflow"); + assert!( + result.is_ok(), + "Should handle 5000 levels without stack overflow" + ); // Cleanup should also not overflow let cleanup_result = fs::remove_dir_all(temp.path().join("d0")); @@ -119,7 +125,11 @@ fn stress_large_file_1gb() { // Verify size let metadata = fs::metadata(&large_file).unwrap(); - assert_eq!(metadata.len(), 1024 * 1024 * 1024, "File should be exactly 1GB"); + assert_eq!( + metadata.len(), + 1024 * 1024 * 1024, + "File should be exactly 1GB" + ); // Test operations on large file let mut state = ShellState::new(temp.path().to_str().unwrap()).unwrap(); @@ -130,7 +140,10 @@ fn stress_large_file_1gb() { let touch_time = touch_start.elapsed(); println!("✓ Touch on 1GB file in {:?}", touch_time); - assert!(touch_time.as_millis() < 100, "Touch should be <100ms even for large files"); + assert!( + touch_time.as_millis() < 100, + "Touch should be <100ms even for large files" + ); // Memory usage check: should not load entire file into memory // This test would fail if implementation naively reads entire file @@ -274,8 +287,16 @@ fn stress_undo_redo_efficiency() { let avg_undo_us = undo_time.as_micros() / 100; let avg_redo_us = redo_time.as_micros() / 100; - assert!(avg_undo_us < 1000, "Undo should be <1ms per op (was {}μs)", avg_undo_us); - assert!(avg_redo_us < 1000, "Redo should be <1ms per op (was {}μs)", avg_redo_us); + assert!( + avg_undo_us < 1000, + "Undo should be <1ms per op (was {}μs)", + avg_undo_us + ); + assert!( + avg_redo_us < 1000, + "Redo should be <1ms per op (was {}μs)", + avg_redo_us + ); } // ============================================================ @@ -332,7 +353,9 @@ fn stress_concurrent_multiple_instances() { #[ignore] fn stress_concurrent_no_corruption() { let temp = TempDir::new().unwrap(); - let state = Arc::new(Mutex::new(ShellState::new(temp.path().to_str().unwrap()).unwrap())); + let state = Arc::new(Mutex::new( + ShellState::new(temp.path().to_str().unwrap()).unwrap(), + )); // Multiple threads sharing same ShellState (via Mutex) let mut handles = vec![]; diff --git a/impl/rust-cli/tests/tilde_expansion_tests.rs b/impl/rust-cli/tests/tilde_expansion_tests.rs index f40ebace..0d1076ef 100644 --- a/impl/rust-cli/tests/tilde_expansion_tests.rs +++ b/impl/rust-cli/tests/tilde_expansion_tests.rs @@ -43,10 +43,7 @@ fn tilde_plus_expands_to_pwd() { std::env::set_var("PWD", "/some/where"); assert_eq!(expand_variables("~+", &state), "/some/where"); - assert_eq!( - expand_variables("~+/subdir", &state), - "/some/where/subdir" - ); + assert_eq!(expand_variables("~+/subdir", &state), "/some/where/subdir"); } #[test]