diff --git a/Cargo.lock b/Cargo.lock index 74e936a916b..b8c738ac596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3773,7 +3773,6 @@ dependencies = [ name = "uu_stat" version = "0.1.0" dependencies = [ - "chrono", "clap", "fluent", "thiserror 2.0.12", diff --git a/src/uu/du/src/du.rs b/src/uu/du/src/du.rs index 0fa49cb1486..a6ddc8a6a0d 100644 --- a/src/uu/du/src/du.rs +++ b/src/uu/du/src/du.rs @@ -28,6 +28,7 @@ use uucore::translate; use uucore::parser::parse_glob; use uucore::parser::parse_size::{ParseSizeError, parse_size_u64}; use uucore::parser::shortcut_value_parser::ShortcutValueParser; +use uucore::time::{FormatSystemTimeFallback, format_system_time}; use uucore::{format_usage, show, show_error, show_warning}; #[cfg(windows)] use windows_sys::Win32::Foundation::HANDLE; @@ -508,7 +509,12 @@ impl StatPrinter { if let Some(md_time) = &self.time { if let Some(time) = metadata_get_time(&stat.metadata, *md_time) { - uucore::time::format_system_time(&mut stdout(), time, &self.time_format, true)?; + format_system_time( + &mut stdout(), + time, + &self.time_format, + FormatSystemTimeFallback::IntegerError, + )?; print!("\t"); } else { print!("???\t"); diff --git a/src/uu/ls/src/ls.rs b/src/uu/ls/src/ls.rs index 2f682e7a159..31108229bd1 100644 --- a/src/uu/ls/src/ls.rs +++ b/src/uu/ls/src/ls.rs @@ -59,6 +59,7 @@ use uucore::line_ending::LineEnding; use uucore::translate; use uucore::quoting_style::{QuotingStyle, locale_aware_escape_dir_name, locale_aware_escape_name}; +use uucore::time::{FormatSystemTimeFallback, format_system_time}; use uucore::{ display::Quotable, error::{UError, UResult, set_exit_code}, @@ -2946,7 +2947,7 @@ fn display_date( _ => &config.time_format_recent, }; - uucore::time::format_system_time(out, time, fmt, false) + format_system_time(out, time, fmt, FormatSystemTimeFallback::Integer) } #[allow(dead_code)] diff --git a/src/uu/stat/Cargo.toml b/src/uu/stat/Cargo.toml index 4b77d54ddee..21adecd90af 100644 --- a/src/uu/stat/Cargo.toml +++ b/src/uu/stat/Cargo.toml @@ -19,8 +19,13 @@ path = "src/stat.rs" [dependencies] clap = { workspace = true } -uucore = { workspace = true, features = ["entries", "libc", "fs", "fsext"] } -chrono = { workspace = true } +uucore = { workspace = true, features = [ + "entries", + "libc", + "fs", + "fsext", + "time", +] } thiserror = { workspace = true } fluent = { workspace = true } diff --git a/src/uu/stat/src/stat.rs b/src/uu/stat/src/stat.rs index 6a8aedd926a..ebc50ed0f5e 100644 --- a/src/uu/stat/src/stat.rs +++ b/src/uu/stat/src/stat.rs @@ -11,12 +11,12 @@ use clap::builder::ValueParser; use uucore::display::Quotable; use uucore::fs::display_permissions; use uucore::fsext::{ - BirthTime, FsMeta, StatFs, pretty_filetype, pretty_fstype, read_fs_list, statfs, + FsMeta, MetadataTimeField, StatFs, metadata_get_time, pretty_filetype, pretty_fstype, + read_fs_list, statfs, }; use uucore::libc::mode_t; use uucore::{entries, format_usage, show_error, show_warning}; -use chrono::{DateTime, Local}; use clap::{Arg, ArgAction, ArgMatches, Command}; use std::borrow::Cow; use std::ffi::{OsStr, OsString}; @@ -27,6 +27,7 @@ use std::path::Path; use std::{env, fs}; use thiserror::Error; +use uucore::time::{FormatSystemTimeFallback, format_system_time, system_time_to_sec}; #[derive(Debug, Error)] enum StatError { @@ -1022,41 +1023,28 @@ impl Stater { } // time of file birth, human-readable; - if unknown - 'w' => { - OutputType::Str(meta.birth().map_or(String::from("-"), |(sec, nsec)| { - pretty_time(sec as i64, nsec as i64) - })) - } + 'w' => OutputType::Str(pretty_time(meta, MetadataTimeField::Birth)), // time of file birth, seconds since Epoch; 0 if unknown - 'W' => OutputType::Unsigned(meta.birth().unwrap_or_default().0), + 'W' => OutputType::Integer( + metadata_get_time(meta, MetadataTimeField::Birth) + .map_or(0, |x| system_time_to_sec(x).0), + ), // time of last access, human-readable - 'x' => OutputType::Str(pretty_time(meta.atime(), meta.atime_nsec())), + 'x' => OutputType::Str(pretty_time(meta, MetadataTimeField::Access)), // time of last access, seconds since Epoch 'X' => OutputType::Integer(meta.atime()), // time of last data modification, human-readable - 'y' => OutputType::Str(pretty_time(meta.mtime(), meta.mtime_nsec())), + 'y' => OutputType::Str(pretty_time(meta, MetadataTimeField::Modification)), // time of last data modification, seconds since Epoch 'Y' => { - let sec = meta.mtime(); - let nsec = meta.mtime_nsec(); - let tm = DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default(); - let tm: DateTime = tm.into(); - match tm.timestamp_nanos_opt() { - None => { - let micros = tm.timestamp_micros(); - let secs = micros as f64 / 1_000_000.0; - OutputType::Float(secs) - } - Some(ns) => { - let secs = ns as f64 / 1_000_000_000.0; - OutputType::Float(secs) - } - } + let (sec, nsec) = metadata_get_time(meta, MetadataTimeField::Modification) + .map_or((0, 0), system_time_to_sec); + OutputType::Float(sec as f64 + nsec as f64 / 1_000_000_000.0) } // time of last status change, human-readable - 'z' => OutputType::Str(pretty_time(meta.ctime(), meta.ctime_nsec())), + 'z' => OutputType::Str(pretty_time(meta, MetadataTimeField::Change)), // time of last status change, seconds since Epoch 'Z' => OutputType::Integer(meta.ctime()), 'R' => { @@ -1291,14 +1279,23 @@ pub fn uu_app() -> Command { ) } -const PRETTY_DATETIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%f %z"; +const PRETTY_DATETIME_FORMAT: &str = "%Y-%m-%d %H:%M:%S.%N %z"; -fn pretty_time(sec: i64, nsec: i64) -> String { - // Return the date in UTC - let tm = DateTime::from_timestamp(sec, nsec as u32).unwrap_or_default(); - let tm: DateTime = tm.into(); - - tm.format(PRETTY_DATETIME_FORMAT).to_string() +fn pretty_time(meta: &Metadata, md_time_field: MetadataTimeField) -> String { + if let Some(time) = metadata_get_time(meta, md_time_field) { + let mut tmp = Vec::new(); + if format_system_time( + &mut tmp, + time, + PRETTY_DATETIME_FORMAT, + FormatSystemTimeFallback::Float, + ) + .is_ok() + { + return String::from_utf8(tmp).unwrap(); + } + } + "-".to_string() } #[cfg(test)] diff --git a/src/uucore/src/lib/features/fsext.rs b/src/uucore/src/lib/features/fsext.rs index 91e77216a76..4d58a807d84 100644 --- a/src/uucore/src/lib/features/fsext.rs +++ b/src/uucore/src/lib/features/fsext.rs @@ -69,7 +69,9 @@ use std::io::Error as IOError; use std::mem; #[cfg(windows)] use std::path::Path; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::SystemTime; +#[cfg(not(windows))] +use std::time::UNIX_EPOCH; use std::{borrow::Cow, ffi::OsString}; use std::fs::Metadata; @@ -114,19 +116,6 @@ pub use libc::statfs as statfs_fn; ))] pub use libc::statvfs as statfs_fn; -pub trait BirthTime { - fn birth(&self) -> Option<(u64, u32)>; -} - -impl BirthTime for Metadata { - fn birth(&self) -> Option<(u64, u32)> { - self.created() - .ok() - .and_then(|t| t.duration_since(UNIX_EPOCH).ok()) - .map(|e| (e.as_secs(), e.subsec_nanos())) - } -} - #[derive(Debug, Copy, Clone)] pub enum MetadataTimeField { Modification, @@ -153,8 +142,20 @@ impl From<&str> for MetadataTimeField { #[cfg(unix)] fn metadata_get_change_time(md: &Metadata) -> Option { - // TODO: This is incorrect for negative timestamps. - Some(UNIX_EPOCH + Duration::new(md.ctime() as u64, md.ctime_nsec() as u32)) + let mut st = UNIX_EPOCH; + let (secs, nsecs) = (md.ctime(), md.ctime_nsec()); + if secs >= 0 { + st += Duration::from_secs(secs as u64); + } else { + st -= Duration::from_secs(-secs as u64); + } + if nsecs >= 0 { + st += Duration::from_nanos(nsecs as u64); + } else { + // Probably never the case, but cover just in case. + st -= Duration::from_nanos(-nsecs as u64); + } + Some(st) } #[cfg(not(unix))] diff --git a/src/uucore/src/lib/features/time.rs b/src/uucore/src/lib/features/time.rs index 8fef2d63842..4f1505b7027 100644 --- a/src/uucore/src/lib/features/time.rs +++ b/src/uucore/src/lib/features/time.rs @@ -25,12 +25,30 @@ fn format_zoned(out: &mut W, zoned: Zoned, fmt: &str) -> UResult<()> { .map_err(|x| USimpleError::new(1, x.to_string())) } +/// Convert a SystemTime` to a number of seconds since UNIX_EPOCH +pub fn system_time_to_sec(time: SystemTime) -> (i64, u32) { + if time > UNIX_EPOCH { + let d = time.duration_since(UNIX_EPOCH).unwrap(); + (d.as_secs() as i64, d.subsec_nanos()) + } else { + let d = UNIX_EPOCH.duration_since(time).unwrap(); + (-(d.as_secs() as i64), d.subsec_nanos()) + } +} + +/// Sets how `format_system_time` behaves if the time cannot be converted. +pub enum FormatSystemTimeFallback { + Integer, // Just print seconds since epoch (`ls`) + IntegerError, // The above, and print an error (`du``) + Float, // Just print seconds+nanoseconds since epoch (`stat`) +} + /// Format a `SystemTime` according to given fmt, and append to vector out. pub fn format_system_time( out: &mut W, time: SystemTime, fmt: &str, - show_error: bool, + mode: FormatSystemTimeFallback, ) -> UResult<()> { let zoned: Result = time.try_into(); match zoned { @@ -42,16 +60,22 @@ pub fn format_system_time( // 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 (mut secs, mut nsecs) = system_time_to_sec(time); + match mode { + FormatSystemTimeFallback::Integer => out.write_all(secs.to_string().as_bytes())?, + FormatSystemTimeFallback::IntegerError => { + let str = secs.to_string(); + show_error!("time '{str}' is out of range"); + out.write_all(str.as_bytes())?; + } + FormatSystemTimeFallback::Float => { + if secs < 0 && nsecs != 0 { + secs -= 1; + nsecs = 1_000_000_000 - nsecs; + } + out.write_fmt(format_args!("{secs}.{nsecs:09}"))?; + } }; - let str = ts.to_string(); - if show_error { - show_error!("time '{str}' is out of range"); - } - out.write_all(str.as_bytes())?; Ok(()) } } @@ -59,7 +83,7 @@ pub fn format_system_time( #[cfg(test)] mod tests { - use crate::time::format_system_time; + use crate::time::{FormatSystemTimeFallback, format_system_time}; use std::time::{Duration, UNIX_EPOCH}; // Test epoch SystemTime get printed correctly at UTC0, with 2 simple formats. @@ -69,12 +93,23 @@ mod tests { let time = UNIX_EPOCH; let mut out = Vec::new(); - format_system_time(&mut out, time, "%Y-%m-%d %H:%M", false).expect("Formatting error."); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M", + FormatSystemTimeFallback::Integer, + ) + .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."); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M:%S.%N %z", + FormatSystemTimeFallback::Integer, + ) + .expect("Formatting error."); assert_eq!( String::from_utf8(out).unwrap(), "1970-01-01 00:00:00.000000000 +0000" @@ -86,12 +121,58 @@ mod tests { 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."); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M", + FormatSystemTimeFallback::Integer, + ) + .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."); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M", + FormatSystemTimeFallback::Integer, + ) + .expect("Formatting error."); assert_eq!(String::from_utf8(out).unwrap(), "-67768040922076800"); } + + // Test that very large (positive or negative) lead to just the timestamp being printed. + #[test] + fn test_large_system_time_float() { + let time = + UNIX_EPOCH + Duration::from_secs(67_768_036_191_763_000) + Duration::from_nanos(123); + let mut out = Vec::new(); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M", + FormatSystemTimeFallback::Float, + ) + .expect("Formatting error."); + assert_eq!( + String::from_utf8(out).unwrap(), + "67768036191763000.000000123" + ); + + let time = + UNIX_EPOCH - Duration::from_secs(67_768_040_922_076_000) + Duration::from_nanos(123); + let mut out = Vec::new(); + format_system_time( + &mut out, + time, + "%Y-%m-%d %H:%M", + FormatSystemTimeFallback::Float, + ) + .expect("Formatting error."); + assert_eq!( + String::from_utf8(out).unwrap(), + "-67768040922076000.000000123" + ); + } }