Merge pull request #8390 from drinkcat/ls-bigtime
Some checks are pending
CICD / Style/cargo-deny (push) Waiting to run
CICD / Test all features separately (push) Blocked by required conditions
CICD / Style/deps (push) Waiting to run
CICD / Documentation/warnings (push) Waiting to run
CICD / MinRustV (push) Waiting to run
CICD / Build (push) Blocked by required conditions
CICD / Dependencies (push) Waiting to run
CICD / Build/Makefile (push) Blocked by required conditions
CICD / Build/stable (push) Blocked by required conditions
CICD / Build/nightly (push) Blocked by required conditions
CICD / Binary sizes (push) Blocked by required conditions
CICD / Tests/BusyBox test suite (push) Blocked by required conditions
CICD / Tests/Toybox test suite (push) Blocked by required conditions
CICD / Code Coverage (push) Waiting to run
CICD / Separate Builds (push) Waiting to run
CICD / Build/SELinux (push) Blocked by required conditions
GnuTests / Run GNU tests (push) Waiting to run
Android / Test builds (push) Waiting to run
Code Quality / Style/format (push) Waiting to run
Code Quality / Style/lint (push) Waiting to run
Code Quality / Style/spelling (push) Waiting to run
Code Quality / Style/toml (push) Waiting to run
Code Quality / Style/Python (push) Waiting to run
Code Quality / Pre-commit hooks (push) Waiting to run
FreeBSD / Style and Lint (push) Waiting to run
FreeBSD / Tests (push) Waiting to run

du/ls: Merge date formatting code, and handle timestamps far in the future/past by just printing them
This commit is contained in:
Sylvestre Ledru 2025-07-27 08:01:29 +09:00 committed by GitHub
commit ded761d334
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 170 additions and 103 deletions

3
Cargo.lock generated
View file

@ -3164,7 +3164,6 @@ dependencies = [
name = "uu_du"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"glob",
"thiserror 2.0.12",
@ -3381,7 +3380,6 @@ dependencies = [
"clap",
"glob",
"hostname",
"jiff",
"lscolors",
"selinux",
"terminal_size",
@ -3995,6 +3993,7 @@ dependencies = [
"icu_locale",
"icu_provider",
"itertools 0.14.0",
"jiff",
"libc",
"md-5",
"memchr",

View file

@ -18,11 +18,10 @@ workspace = true
path = "src/du.rs"
[dependencies]
chrono = { workspace = true }
# For the --exclude & --exclude-from options
glob = { workspace = true }
clap = { workspace = true }
uucore = { workspace = true, features = ["format", "parser"] }
uucore = { workspace = true, features = ["format", "parser", "time"] }
thiserror = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]

View file

@ -3,7 +3,6 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use chrono::{DateTime, Local};
use clap::{Arg, ArgAction, ArgMatches, Command, builder::PossibleValue};
use glob::Pattern;
use std::collections::{HashMap, HashSet};
@ -11,7 +10,7 @@ use std::env;
#[cfg(not(windows))]
use std::fs::Metadata;
use std::fs::{self, DirEntry, File};
use std::io::{BufRead, BufReader};
use std::io::{BufRead, BufReader, stdout};
#[cfg(not(windows))]
use std::os::unix::fs::MetadataExt;
#[cfg(windows)]
@ -576,13 +575,13 @@ impl StatPrinter {
}
fn print_stat(&self, stat: &Stat, size: u64) -> UResult<()> {
print!("{}\t", self.convert_size(size));
if let Some(time) = self.time {
let secs = get_time_secs(time, stat)?;
let tm = DateTime::<Local>::from(UNIX_EPOCH + Duration::from_secs(secs));
let time_str = tm.format(&self.time_format).to_string();
print!("{}\t{time_str}\t", self.convert_size(size));
} else {
print!("{}\t", self.convert_size(size));
let time = UNIX_EPOCH + Duration::from_secs(secs);
uucore::time::format_system_time(&mut stdout(), time, &self.time_format, true)?;
print!("\t");
}
print_verbatim(&stat.path).unwrap();

View file

@ -24,11 +24,6 @@ ansi-width = { workspace = true }
clap = { workspace = true, features = ["env"] }
glob = { workspace = true }
hostname = { workspace = true }
jiff = { workspace = true, features = [
"tzdb-bundle-platform",
"tzdb-zoneinfo",
"tzdb-concatenated",
] }
lscolors = { workspace = true }
selinux = { workspace = true, optional = true }
terminal_size = { workspace = true }
@ -41,6 +36,7 @@ uucore = { workspace = true, features = [
"fsxattr",
"parser",
"quoting-style",
"time",
"version-cmp",
] }
uutils_term_grid = { workspace = true }

View file

@ -31,9 +31,6 @@ use clap::{
builder::{NonEmptyStringValueParser, PossibleValue, ValueParser},
};
use glob::{MatchOptions, Pattern};
use jiff::fmt::StdIoWrite;
use jiff::fmt::strtime::BrokenDownTime;
use jiff::{Timestamp, Zoned};
use lscolors::LsColors;
use term_grid::{DEFAULT_SEPARATOR_SIZE, Direction, Filling, Grid, GridOptions, SPACES_IN_TAB};
use thiserror::Error;
@ -258,56 +255,30 @@ enum Time {
Birth,
}
#[derive(Debug)]
enum TimeStyle {
FullIso,
LongIso,
Iso,
Locale,
Format(String),
}
/// Whether the given date is considered recent (i.e., in the last 6 months).
fn is_recent(time: Timestamp, state: &mut ListState) -> bool {
time > state.recent_time_threshold
}
impl TimeStyle {
/// Format the given time according to this time format style.
fn format(
&self,
date: Zoned,
out: &mut Vec<u8>,
state: &mut ListState,
) -> Result<(), jiff::Error> {
let recent = is_recent(date.timestamp(), state);
let tm = BrokenDownTime::from(&date);
let mut out = StdIoWrite(out);
let config = jiff::fmt::strtime::Config::new().lenient(true);
let fmt = match (self, recent) {
(Self::FullIso, _) => "%Y-%m-%d %H:%M:%S.%f %z",
(Self::LongIso, _) => "%Y-%m-%d %H:%M",
(Self::Iso, true) => "%m-%d %H:%M",
(Self::Iso, false) => "%Y-%m-%d ",
// TODO: Using correct locale string is not implemented.
(Self::Locale, true) => "%b %e %H:%M",
(Self::Locale, false) => "%b %e %Y",
(Self::Format(fmt), _) => fmt,
};
tm.format_with_config(&config, fmt, &mut out)
}
}
fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> {
let possible_time_styles = vec![
"full-iso".to_string(),
"long-iso".to_string(),
"iso".to_string(),
"locale".to_string(),
"+FORMAT (e.g., +%H:%M) for a 'date'-style format".to_string(),
fn parse_time_style(options: &clap::ArgMatches) -> Result<(String, Option<String>), LsError> {
const TIME_STYLES: [(&str, (&str, Option<&str>)); 4] = [
("full-iso", ("%Y-%m-%d %H:%M:%S.%f %z", None)),
("long-iso", ("%Y-%m-%d %H:%M", None)),
("iso", ("%m-%d %H:%M", Some("%Y-%m-%d "))),
// TODO: Using correct locale string is not implemented.
("locale", ("%b %e %H:%M", Some("%b %e %Y"))),
];
// A map from a time-style parameter to a length-2 tuple of formats:
// the first one is used for recent dates, the second one for older ones (optional).
let time_styles = HashMap::from(TIME_STYLES);
let possible_time_styles = TIME_STYLES
.iter()
.map(|(x, _)| *x)
.chain(iter::once(
"+FORMAT (e.g., +%H:%M) for a 'date'-style format",
))
.map(|s| s.to_string());
// Convert time_styles references to owned String/option.
fn ok((recent, older): (&str, Option<&str>)) -> Result<(String, Option<String>), LsError> {
Ok((recent.to_string(), older.map(String::from)))
}
if let Some(field) = options.get_one::<String>(options::TIME_STYLE) {
//If both FULL_TIME and TIME_STYLE are present
//The one added last is dominant
@ -315,26 +286,23 @@ fn parse_time_style(options: &clap::ArgMatches) -> Result<TimeStyle, LsError> {
&& options.indices_of(options::FULL_TIME).unwrap().next_back()
> options.indices_of(options::TIME_STYLE).unwrap().next_back()
{
Ok(TimeStyle::FullIso)
ok(time_styles["full-iso"])
} else {
match field.as_str() {
"full-iso" => Ok(TimeStyle::FullIso),
"long-iso" => Ok(TimeStyle::LongIso),
"iso" => Ok(TimeStyle::Iso),
"locale" => Ok(TimeStyle::Locale),
_ => match field.chars().next().unwrap() {
'+' => Ok(TimeStyle::Format(String::from(&field[1..]))),
match time_styles.get(field.as_str()) {
Some(formats) => ok(*formats),
None => match field.chars().next().unwrap() {
'+' => Ok((field[1..].to_string(), None)),
_ => Err(LsError::TimeStyleParseError(
String::from(field),
possible_time_styles,
possible_time_styles.collect(),
)),
},
}
}
} else if options.get_flag(options::FULL_TIME) {
Ok(TimeStyle::FullIso)
ok(time_styles["full-iso"])
} else {
Ok(TimeStyle::Locale)
ok(time_styles["locale"])
}
}
@ -377,7 +345,8 @@ pub struct Config {
// Dir and vdir needs access to this field
pub quoting_style: QuotingStyle,
indicator_style: IndicatorStyle,
time_style: TimeStyle,
time_format_recent: String, // Time format for recent dates
time_format_older: Option<String>, // Time format for older dates (optional, if not present, time_format_recent is used)
context: bool,
selinux_supported: bool,
group_directories_first: bool,
@ -925,10 +894,10 @@ impl Config {
let indicator_style = extract_indicator_style(options);
// Only parse the value to "--time-style" if it will become relevant.
let dired = options.get_flag(options::DIRED);
let time_style = if format == Format::Long || dired {
let (time_format_recent, time_format_older) = if format == Format::Long || dired {
parse_time_style(options)?
} else {
TimeStyle::Iso
Default::default()
};
let mut ignore_patterns: Vec<Pattern> = Vec::new();
@ -1114,7 +1083,8 @@ impl Config {
width,
quoting_style,
indicator_style,
time_style,
time_format_recent,
time_format_older,
context,
selinux_supported: {
#[cfg(feature = "selinux")]
@ -2002,7 +1972,7 @@ struct ListState<'a> {
uid_cache: HashMap<u32, String>,
#[cfg(unix)]
gid_cache: HashMap<u32, String>,
recent_time_threshold: Timestamp,
recent_time_threshold: SystemTime,
}
#[allow(clippy::cognitive_complexity)]
@ -2020,7 +1990,7 @@ pub fn list(locs: Vec<&Path>, config: &Config) -> UResult<()> {
#[cfg(unix)]
gid_cache: HashMap::new(),
// According to GNU a Gregorian year has 365.2425 * 24 * 60 * 60 == 31556952 seconds on the average.
recent_time_threshold: Timestamp::now() - Duration::new(31_556_952 / 2, 0),
recent_time_threshold: SystemTime::now() - Duration::new(31_556_952 / 2, 0),
};
for loc in locs {
@ -2993,7 +2963,7 @@ fn display_group(_metadata: &Metadata, _config: &Config, _state: &mut ListState)
"somegroup"
}
// The implementations for get_time are separated because some options, such
// The implementations for get_system_time are separated because some options, such
// as ctime will not be available
#[cfg(unix)]
fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
@ -3015,28 +2985,25 @@ fn get_system_time(md: &Metadata, config: &Config) -> Option<SystemTime> {
}
}
fn get_time(md: &Metadata, config: &Config) -> Option<Zoned> {
let time = get_system_time(md, config)?;
time.try_into().ok()
}
fn display_date(
metadata: &Metadata,
config: &Config,
state: &mut ListState,
out: &mut Vec<u8>,
) -> UResult<()> {
match get_time(metadata, config) {
// TODO: Some fancier error conversion might be nice.
Some(time) => config
.time_style
.format(time, out, state)
.map_err(|x| USimpleError::new(1, x.to_string())),
None => {
out.extend(b"???");
Ok(())
}
}
let Some(time) = get_system_time(metadata, config) else {
out.extend(b"???");
return Ok(());
};
// Use "recent" format if the given date is considered recent (i.e., in the last 6 months),
// or if no "older" format is available.
let fmt = match &config.time_format_older {
Some(time_format_older) if time <= state.recent_time_threshold => time_format_older,
_ => &config.time_format_recent,
};
uucore::time::format_system_time(out, time, fmt, false)
}
#[allow(dead_code)]

View file

@ -1,4 +1,4 @@
# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal
# spell-checker:ignore (features) bigdecimal zerocopy extendedbigdecimal tzdb zoneinfo
[package]
name = "uucore"
@ -29,6 +29,11 @@ dunce = { version = "1.0.4", optional = true }
wild = "2.2.1"
glob = { workspace = true, optional = true }
itertools = { workspace = true, optional = true }
jiff = { workspace = true, optional = true, features = [
"tzdb-bundle-platform",
"tzdb-zoneinfo",
"tzdb-concatenated",
] }
time = { workspace = true, optional = true, features = [
"formatting",
"local-offset",
@ -150,4 +155,5 @@ utmpx = ["time", "time/macros", "libc", "dns-lookup"]
version-cmp = []
wide = []
tty = []
time = ["jiff"]
uptime = ["chrono", "libc", "windows-sys", "utmpx", "utmp-classic"]

View file

@ -40,6 +40,8 @@ pub mod ranges;
pub mod ringbuffer;
#[cfg(feature = "sum")]
pub mod sum;
#[cfg(feature = "time")]
pub mod time;
#[cfg(feature = "update-control")]
pub mod update_control;
#[cfg(feature = "uptime")]

View file

@ -0,0 +1,97 @@
// This file is part of the uutils coreutils package.
//
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore (ToDO) strtime
//! Set of functions related to time handling
use jiff::Zoned;
use jiff::fmt::StdIoWrite;
use jiff::fmt::strtime::{BrokenDownTime, Config};
use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::error::{UResult, USimpleError};
use crate::show_error;
/// Format the given date according to this time format style.
fn format_zoned<W: Write>(out: &mut W, zoned: Zoned, fmt: &str) -> UResult<()> {
let tm = BrokenDownTime::from(&zoned);
let mut out = StdIoWrite(out);
let config = Config::new().lenient(true);
tm.format_with_config(&config, fmt, &mut out)
.map_err(|x| USimpleError::new(1, x.to_string()))
}
/// Format a `SystemTime` according to given fmt, and append to vector out.
pub fn format_system_time<W: Write>(
out: &mut W,
time: SystemTime,
fmt: &str,
show_error: bool,
) -> UResult<()> {
let zoned: Result<Zoned, _> = time.try_into();
match zoned {
Ok(zoned) => format_zoned(out, zoned, fmt),
Err(_) => {
// Assume that if we cannot build a Zoned element, the timestamp is
// out of reasonable range, just print it then.
// TODO: The range allowed by jiff is different from what GNU accepts,
// but it still far enough in the future/past to be unlikely to matter:
// jiff: Year between -9999 to 9999 (UTC) [-377705023201..=253402207200]
// GNU: Year fits in signed 32 bits (timezone dependent)
let ts: i64 = if time > UNIX_EPOCH {
time.duration_since(UNIX_EPOCH).unwrap().as_secs() as i64
} else {
-(UNIX_EPOCH.duration_since(time).unwrap().as_secs() as i64)
};
let str = ts.to_string();
if show_error {
show_error!("time '{str}' is out of range");
}
out.write_all(str.as_bytes())?;
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use crate::time::format_system_time;
use std::time::{Duration, UNIX_EPOCH};
// Test epoch SystemTime get printed correctly at UTC0, with 2 simple formats.
#[test]
fn test_simple_system_time() {
unsafe { std::env::set_var("TZ", "UTC0") };
let time = UNIX_EPOCH;
let mut out = Vec::new();
format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error.");
assert_eq!(String::from_utf8(out).unwrap(), "1970-01-01 00:00");
let mut out = Vec::new();
format_system_time(&mut out, time, "%Y-%m-%d %H:%M:%S.%N %z", false)
.expect("Formatting error.");
assert_eq!(
String::from_utf8(out).unwrap(),
"1970-01-01 00:00:00.000000000 +0000"
);
}
// Test that very large (positive or negative) lead to just the timestamp being printed.
#[test]
fn test_large_system_time() {
let time = UNIX_EPOCH + Duration::from_secs(67_768_036_191_763_200);
let mut out = Vec::new();
format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error.");
assert_eq!(String::from_utf8(out).unwrap(), "67768036191763200");
let time = UNIX_EPOCH - Duration::from_secs(67_768_040_922_076_800);
let mut out = Vec::new();
format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error.");
assert_eq!(String::from_utf8(out).unwrap(), "-67768040922076800");
}
}

View file

@ -65,6 +65,8 @@ pub use crate::features::ranges;
pub use crate::features::ringbuffer;
#[cfg(feature = "sum")]
pub use crate::features::sum;
#[cfg(feature = "time")]
pub use crate::features::time;
#[cfg(feature = "update-control")]
pub use crate::features::update_control;
#[cfg(feature = "uptime")]