From 6ea955db804d39083857a52bcc0862b2b2858b20 Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Mon, 7 Apr 2025 22:13:57 -0400 Subject: [PATCH 1/4] du: support arbitrary time formats --- src/uu/du/src/du.rs | 12 ++++++++++-- tests/by-util/test_du.rs | 12 ++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index af520543577..fef7810d9f3 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -708,7 +708,12 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { }; let time_format = if time.is_some() { - parse_time_style(matches.get_one::("time-style").map(|s| s.as_str()))?.to_string() + parse_time_style( + matches + .get_one::(options::TIME_STYLE) + .map(|s| s.as_str()), + )? + .to_string() } else { "%Y-%m-%d %H:%M".to_string() }; @@ -807,7 +812,10 @@ fn parse_time_style(s: Option<&str>) -> UResult<&str> { "full-iso" => Ok("%Y-%m-%d %H:%M:%S.%f %z"), "long-iso" => Ok("%Y-%m-%d %H:%M"), "iso" => Ok("%Y-%m-%d"), - _ => Err(DuError::InvalidTimeStyleArg(s.into()).into()), + _ => match s.strip_prefix('+') { + Some(s) => Ok(s), + _ => Err(DuError::InvalidTimeStyleArg(s.into()).into()), + }, }, None => Ok("%Y-%m-%d %H:%M"), } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 1edbeb63cff..5cb4abd246a 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -7,6 +7,7 @@ #[cfg(not(windows))] use regex::Regex; +use rstest::rstest; use uutests::at_and_ucmd; use uutests::new_ucmd; #[cfg(not(target_os = "windows"))] @@ -1173,6 +1174,17 @@ fn test_invalid_time_style() { .stdout_does_not_contain("du: invalid argument 'banana' for 'time style'"); } +#[rstest] +#[case::full_iso("+%Y-%m-%d %H:%M:%S.%f %z")] +#[case::long_iso("+%Y-%m-%d %H:%M")] +#[case::iso("+%Y-%m-%d")] +#[case::seconds("+%S")] +fn test_valid_time_style(#[case] input: &str) { + new_ucmd!() + .args(&["--time", "--time-style", input]) + .succeeds(); +} + #[test] fn test_human_size() { use std::fs::File; From df13534dd6b10637947af55fb358ed58df8db40b Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 8 Apr 2025 19:50:47 -0400 Subject: [PATCH 2/4] du: prevent panics from chrono --- Cargo.lock | 1 + src/uu/du/Cargo.toml | 5 +++-- src/uu/du/src/du.rs | 35 ++++++++++++++++++++++++++++++++--- tests/by-util/test_du.rs | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 72 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fdcd8f22b00..89a4fbf525b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2757,6 +2757,7 @@ dependencies = [ "chrono", "clap", "glob", + "regex", "thiserror 2.0.12", "uucore", "windows-sys 0.59.0", diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index 5bceda4b107..c6936fdd3be 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -21,13 +21,14 @@ chrono = { workspace = true } # For the --exclude & --exclude-from options glob = { workspace = true } clap = { workspace = true } +regex = { workspace = true } uucore = { workspace = true, features = ["format", "parser"] } thiserror = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ - "Win32_Storage_FileSystem", - "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_Foundation", ] } [[bin]] diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index fef7810d9f3..1ff17043ca7 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -6,6 +6,8 @@ use chrono::{DateTime, Local}; use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue}; use glob::Pattern; +use regex::Regex; +use std::borrow::Cow; use std::collections::HashSet; use std::env; #[cfg(not(windows))] @@ -20,7 +22,7 @@ use std::os::windows::fs::MetadataExt; use std::os::windows::io::AsRawHandle; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::mpsc; +use std::sync::{LazyLock, mpsc}; use std::thread; use std::time::{Duration, UNIX_EPOCH}; use thiserror::Error; @@ -74,6 +76,13 @@ const ABOUT: &str = help_about!("du.md"); const AFTER_HELP: &str = help_section!("after help", "du.md"); const USAGE: &str = help_usage!("du.md"); +static TIME_STYLE_REGEX: LazyLock = LazyLock::new(|| { + Regex::new( + r"(?(?:^|[^%])(?:%%)*)%(?[^YCyqmbBhdeaAwuUWGgVjDxFvHkIlPpMSfRTXrZzc+stn%.369:]|[-_0][^YCyqmdewuUWGgVjHkIlMSfs%]|\.(?:[^369f]|$)|\.[369](?:[^f]|$)|[369](?:[^f]|$)|:{1,2}(?:[^z:]|$)|:{3}[^z]|$)", // cspell:disable-line + ) + .unwrap() +}); + struct TraversalOptions { all: bool, separate_dirs: bool, @@ -806,8 +815,8 @@ fn get_time_secs(time: Time, stat: &Stat) -> Result { } } -fn parse_time_style(s: Option<&str>) -> UResult<&str> { - match s { +fn parse_time_style(s: Option<&str>) -> UResult> { + let s = match s { Some(s) => match s { "full-iso" => Ok("%Y-%m-%d %H:%M:%S.%f %z"), "long-iso" => Ok("%Y-%m-%d %H:%M"), @@ -818,6 +827,26 @@ fn parse_time_style(s: Option<&str>) -> UResult<&str> { }, }, None => Ok("%Y-%m-%d %H:%M"), + }; + match s { + Ok(s) => { + let mut s = s.to_owned(); + let mut start = 0; + while start < s.len() { + // FIXME: this should use let chains once they're stabilized + // See https://github.com/rust-lang/rust/issues/53667 + if let Some(cap) = TIME_STYLE_REGEX.captures(&s[start..]) { + let mat = cap.name("before").unwrap(); + let percent_idx = mat.end(); + s.replace_range(percent_idx + start..percent_idx + start + 1, "%%"); + start = percent_idx + 2; + } else { + break; + } + } + Ok(Cow::Owned(s)) + } + Err(e) => Err(e), } } diff --git a/tests/by-util/test_du.rs b/tests/by-util/test_du.rs index 5cb4abd246a..28c6526127c 100644 --- a/tests/by-util/test_du.rs +++ b/tests/by-util/test_du.rs @@ -1185,6 +1185,42 @@ fn test_valid_time_style(#[case] input: &str) { .succeeds(); } +#[test] +fn test_time_style_escaping() { + let ts = TestScenario::new(util_name!()); + let at = &ts.fixtures; + + at.touch("date_test"); + + // printable characters (a little overkill, but better than missing something) + for c in '!'..='~' { + let f = format!("+%{c}"); + ts.ucmd() + .arg("--time") + .arg("--time-style") + .arg(f) + .arg("date_test") + .succeeds(); + for prefix in ".:#0-_".chars() { + let f = format!("+%{prefix}{c}"); + ts.ucmd() + .arg("--time") + .arg("--time-style") + .arg(f) + .arg("date_test") + .succeeds(); + } + } + + let f = "+%%%"; + ts.ucmd() + .arg("--time") + .arg("--time-style") + .arg(f) + .arg("date_test") + .succeeds(); +} + #[test] fn test_human_size() { use std::fs::File; From 935ef6c75ef8f9317c76293c0e5c2d70e7ac0eef Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 8 Apr 2025 22:25:42 -0400 Subject: [PATCH 3/4] du: clippy --- src/uu/du/src/du.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 1ff17043ca7..4977baabe9f 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -838,7 +838,7 @@ fn parse_time_style(s: Option<&str>) -> UResult> { if let Some(cap) = TIME_STYLE_REGEX.captures(&s[start..]) { let mat = cap.name("before").unwrap(); let percent_idx = mat.end(); - s.replace_range(percent_idx + start..percent_idx + start + 1, "%%"); + s.replace_range(percent_idx + start..=percent_idx + start, "%%"); start = percent_idx + 2; } else { break; From 0ffb01a2a283e6eef21c6aa4ac417768dee183ef Mon Sep 17 00:00:00 2001 From: Jeremy Smart Date: Tue, 8 Apr 2025 22:27:20 -0400 Subject: [PATCH 4/4] du: taplo formatting --- src/uu/du/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uu/du/Cargo.toml b/src/uu/du/Cargo.toml index c6936fdd3be..d2dd9636879 100644 --- a/src/uu/du/Cargo.toml +++ b/src/uu/du/Cargo.toml @@ -27,8 +27,8 @@ thiserror = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] windows-sys = { workspace = true, features = [ - "Win32_Storage_FileSystem", - "Win32_Foundation", + "Win32_Storage_FileSystem", + "Win32_Foundation", ] } [[bin]]