From 48429d3d241aa03c60eb3cc2d8d705a51b196c9e Mon Sep 17 00:00:00 2001 From: Joseph Rafael Ferrer Date: Fri, 15 Aug 2025 07:28:07 +0800 Subject: [PATCH] Basic functionality --- Cargo.lock | 35 ++++++---- tree/du.rs | 115 +++++++++++++++++++------------- tree/tests/du/mod.rs | 140 +++++++++++++++++++++++++++++++++++++++ tree/tests/tree-tests.rs | 1 + 4 files changed, 232 insertions(+), 59 deletions(-) create mode 100644 tree/tests/du/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 2a2e2a48..cdcdc9ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -296,7 +296,6 @@ dependencies = [ "num-traits", "pure-rust-locales", "wasm-bindgen", - "windows-targets 0.52.6", "windows-link", ] @@ -709,7 +708,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.89", + "syn 2.0.100", ] [[package]] @@ -1488,18 +1487,10 @@ dependencies = [ name = "posixutils-display" version = "0.2.2" dependencies = [ - "aho-corasick", - "chrono", "clap", "gettext-rs", "libc", - "once_cell", - "pest", - "pest_derive", "plib", - "regex", - "rstest", - "terminfo 0.9.0", "termion", "thiserror 1.0.69", ] @@ -1569,6 +1560,24 @@ dependencies = [ "rowan", ] +[[package]] +name = "posixutils-man" +version = "0.2.2" +dependencies = [ + "chrono", + "clap", + "gettext-rs", + "lazy_static", + "libc", + "pest", + "pest_derive", + "plib", + "regex", + "rstest", + "terminfo 0.9.0", + "thiserror 1.0.69", +] + [[package]] name = "posixutils-misc" version = "0.2.2" @@ -1975,7 +1984,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.89", + "syn 2.0.100", "unicode-ident", ] @@ -2911,7 +2920,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" dependencies = [ - "memchr" + "memchr", ] [[package]] @@ -2920,7 +2929,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0" + "bitflags 2.9.0", ] [[package]] diff --git a/tree/du.rs b/tree/du.rs index 5fe487b6..51a5b0da 100644 --- a/tree/du.rs +++ b/tree/du.rs @@ -6,15 +6,10 @@ // file in the root directory of this project. // SPDX-License-Identifier: MIT // -// TODO: -// - implement -H, -L, -x -// use clap::Parser; use gettextrs::{bind_textdomain_codeset, setlocale, textdomain, LocaleCategory}; -use std::os::unix::fs::MetadataExt; -use std::path::Path; -use std::{fs, io}; +use std::{cell::RefCell, collections::LinkedList, os::unix::fs::MetadataExt}; /// du - estimate file space usage #[derive(Parser)] @@ -56,50 +51,80 @@ fn calc_size(kilo: bool, size: u64) -> u64 { } } -fn print_pathinfo(args: &Args, filename: &str, size: u64, toplevel: bool) { - if args.sum && !toplevel { - return; - } - - // print the file size - println!("{}\t{}", size, filename); +struct Node { + total_blocks: u64, } -fn du_cli_arg( - args: &Args, - filename: &str, - total: &mut u64, - toplevel: bool, -) -> Result<(), io::Error> { - let path = Path::new(filename); - let metadata = fs::metadata(path)?; - - // recursively process directories - if metadata.is_dir() { - let mut sub_total = 0; - for entry in fs::read_dir(path)? { - let entry = entry?; - let path = entry.path(); - let filename = path.to_str().unwrap(); - if let Err(e) = du_cli_arg(args, filename, &mut sub_total, false) { - eprintln!("{}: {}", filename, e); +fn du_impl(args: &Args, filename: &str) -> bool { + let terminate = RefCell::new(false); + + let stack: RefCell> = RefCell::new(LinkedList::new()); + + let path = std::path::Path::new(filename); + + // TODO: Comment this + ftw::traverse_directory( + path, + |entry| { + if *terminate.borrow() { + return Ok(false); } - } - print_pathinfo(args, filename, sub_total, toplevel); - *total += sub_total; - return Ok(()); - } + let md = entry.metadata().unwrap(); + let size = calc_size(args.kilo, md.blocks()); + let recurse = md.is_dir(); - // print the file size - let size = calc_size(args.kilo, metadata.blocks()); - *total += size; + let mut stack = stack.borrow_mut(); + if let Some(back) = stack.back_mut() { + back.total_blocks += size; + } - if args.all { - print_pathinfo(args, filename, size, toplevel); - } + // -a + if !recurse && !args.sum { + println!("{}\t{}", size, entry.path()); + } - Ok(()) + if recurse { + stack.push_back(Node { total_blocks: size }); + } + + Ok(recurse) + }, + |entry| { + let mut stack = stack.borrow_mut(); + if let Some(node) = stack.pop_back() { + let size = node.total_blocks; + + // Recursively sum the block size + if let Some(back) = stack.back_mut() { + back.total_blocks += size; + } + + if args.sum { + // -s, report only the total sum for the args + let entry_path = entry.path(); + if entry_path.as_inner() == path { + println!("{}\t{}", size, entry_path); + } + } else { + println!("{}\t{}", size, entry.path()); + } + } + Ok(()) + }, + |_entry, error| { + *terminate.borrow_mut() = true; + eprintln!("du: {}", error.inner()); + }, + ftw::TraverseDirectoryOpts { + follow_symlinks_on_args: args.follow_cli, + follow_symlinks: args.dereference, + ..Default::default() + }, + ); + + let failed = *terminate.borrow(); + !failed } fn main() -> Result<(), Box> { @@ -114,13 +139,11 @@ fn main() -> Result<(), Box> { args.files.push(".".to_string()); } let mut exit_code = 0; - let mut total = 0; // apply the group to each file for filename in &args.files { - if let Err(e) = du_cli_arg(&args, filename, &mut total, true) { + if !du_impl(&args, filename) { exit_code = 1; - eprintln!("{}: {}", filename, e); } } diff --git a/tree/tests/du/mod.rs b/tree/tests/du/mod.rs new file mode 100644 index 00000000..0b9385bb --- /dev/null +++ b/tree/tests/du/mod.rs @@ -0,0 +1,140 @@ +// +// Copyright (c) 2024 Hemi Labs, Inc. +// +// This file is part of the posixutils-rs project covered under +// the MIT License. For the full license text, please see the LICENSE +// file in the root directory of this project. +// SPDX-License-Identifier: MIT +// + +use plib::testing::{run_test, run_test_with_checker, TestPlan}; +use std::{fs, io::Write, os::unix::fs::MetadataExt, process::Output}; + +fn du_test(args: &[&str], expected_output: &str, expected_error: &str, expected_exit_code: i32) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test(TestPlan { + cmd: String::from("du"), + args: str_args, + stdin_data: String::new(), + expected_out: String::from(expected_output), + expected_err: String::from(expected_error), + expected_exit_code, + }); +} + +fn du_test2(args: &[&str], checker: impl FnMut(&TestPlan, &Output)) { + let str_args: Vec = args.iter().map(|s| String::from(*s)).collect(); + + run_test_with_checker( + TestPlan { + cmd: String::from("du"), + args: str_args, + stdin_data: String::new(), + expected_out: String::new(), + expected_err: String::new(), + expected_exit_code: 0, + }, + checker, + ); +} + +// Partial port of coreutils/tests/du/basic.sh +// Omits the nonstandard --block-size and -S options +#[test] +fn test_du_basic() { + let test_dir = &format!("{}/test_du_basic", env!("CARGO_TARGET_TMPDIR")); + + // DEBUG + let _ = fs::remove_dir_all(test_dir); + + let a = &format!("{test_dir}/a"); + + let a_b = &format!("{test_dir}/a/b"); + let d = &format!("{test_dir}/d"); + let d_sub = &format!("{test_dir}/d/sub"); + + let a_b_f = &format!("{a_b}/F"); + let d_1 = &format!("{d}/1"); + let d_sub_2 = &format!("{d_sub}/2"); + + fs::create_dir(test_dir).unwrap(); + for dir in [a_b, d, d_sub] { + fs::create_dir_all(dir).unwrap(); + } + + { + // Create a > 257 bytes file + let mut file1 = fs::File::create(a_b_f).unwrap(); + write!(file1, "{:>257}", "x").unwrap(); + + // Create a 4 KiB file + let mut file2 = fs::File::create(d_1).unwrap(); + write!(file2, "{:>4096}", "x").unwrap(); + + fs::copy(d_1, d_sub_2).unwrap(); + } + + // Manually calculate the block sizes for directory a + let [size_abf, mut size_ab, mut size_a] = + [a_b_f, a_b, a].map(|filename| fs::metadata(filename).unwrap().blocks()); + size_ab += size_abf; + size_a += size_ab; + + du_test( + &["-a", a], + &format!( + "{size_abf}\t{a_b_f}\ + \n{size_ab}\t{a_b}\ + \n{size_a}\t{a}\n" + ), + "", + 0, + ); + + du_test(&["-s", a], &format!("{size_a}\t{a}\n"), "", 0); + + // Manually calculate the block sizes for directory d + let [size_dsub2, mut size_dsub, size_d1, mut size_d] = + [d_sub_2, d_sub, d_1, d].map(|filename| fs::metadata(filename).unwrap().blocks()); + size_dsub += size_dsub2; + size_d += size_d1 + size_dsub; + + du_test2(&["-a", d], |_, output| { + // d/1 is not guaranteed to be listed after d/sub + // + // Case #1: + // 8 d/sub/2 + // 8 d/sub + // 8 d/1 + // 16 d + // + // Case #2: + // 8 d/1 + // 8 d/sub/2 + // 8 d/sub + // 16 d + // + // So instead, we compare the sorted lines to check for correctness + + // Split the output wrt to new lines and then sort them. + let split_then_sort = |s: String| -> Vec { + let mut lines: Vec = s.lines().map(|s| s.to_string()).collect(); + lines.sort(); + lines + }; + + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + + let expected = format!( + "{size_dsub2}\t{d_sub_2}\ + \n{size_dsub}\t{d_sub}\ + \n{size_d1}\t{d_1}\ + \n{size_d}\t{d}\n" + ); + + assert_eq!(split_then_sort(stdout), split_then_sort(expected)); + }); + + fs::remove_dir_all(test_dir).unwrap(); +} diff --git a/tree/tests/tree-tests.rs b/tree/tests/tree-tests.rs index 8c8491ec..4f628285 100644 --- a/tree/tests/tree-tests.rs +++ b/tree/tests/tree-tests.rs @@ -11,6 +11,7 @@ mod chgrp; mod chmod; mod chown; mod cp; +mod du; mod link; mod ls; mod mkdir;