Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 22 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 69 additions & 46 deletions tree/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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<LinkedList<Node>> = 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<dyn std::error::Error>> {
Expand All @@ -114,13 +139,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
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);
}
}

Expand Down
140 changes: 140 additions & 0 deletions tree/tests/du/mod.rs
Original file line number Diff line number Diff line change
@@ -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<String> = 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<String> = 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<String> {
let mut lines: Vec<String> = 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();
}
1 change: 1 addition & 0 deletions tree/tests/tree-tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod chgrp;
mod chmod;
mod chown;
mod cp;
mod du;
mod link;
mod ls;
mod mkdir;
Expand Down
Loading