Skip to content
Merged
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
1 change: 0 additions & 1 deletion Cargo.lock

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

8 changes: 7 additions & 1 deletion src/uu/du/src/du.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
Expand Down
3 changes: 2 additions & 1 deletion src/uu/ls/src/ls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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)]
Expand Down
9 changes: 7 additions & 2 deletions src/uu/stat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand Down
63 changes: 30 additions & 33 deletions src/uu/stat/src/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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 {
Expand Down Expand Up @@ -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<Local> = 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' => {
Expand Down Expand Up @@ -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<Local> = 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)]
Expand Down
33 changes: 17 additions & 16 deletions src/uucore/src/lib/features/fsext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -153,8 +142,20 @@ impl From<&str> for MetadataTimeField {

#[cfg(unix)]
fn metadata_get_change_time(md: &Metadata) -> Option<SystemTime> {
// 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))]
Expand Down
113 changes: 97 additions & 16 deletions src/uucore/src/lib/features/time.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,30 @@ fn format_zoned<W: Write>(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<W: Write>(
out: &mut W,
time: SystemTime,
fmt: &str,
show_error: bool,
mode: FormatSystemTimeFallback,
) -> UResult<()> {
let zoned: Result<Zoned, _> = time.try_into();
match zoned {
Expand All @@ -42,24 +60,30 @@ pub fn format_system_time<W: Write>(
// 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(())
}
}
}

#[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.
Expand All @@ -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"
Expand All @@ -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"
);
}
}
Loading