From c53b1d7f7b0bab2e8088a4089ecec5c8d2730d55 Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 30 Dec 2025 14:22:14 +0900 Subject: [PATCH 1/3] fix: correctly format dates using strftime with locale-specific calendars like Ethiopian. --- repro_issue.sh | 90 +++++++++++++++ src/uu/date/src/date.rs | 28 +++-- src/uu/date/src/system_time.rs | 199 +++++++++++++++++++++++++++++++++ tests/by-util/test_date.rs | 7 ++ 4 files changed, 315 insertions(+), 9 deletions(-) create mode 100755 repro_issue.sh create mode 100644 src/uu/date/src/system_time.rs diff --git a/repro_issue.sh b/repro_issue.sh new file mode 100755 index 00000000000..a547215ae28 --- /dev/null +++ b/repro_issue.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Verify the Ethiopian calendar is used in the Ethiopian locale. + +# Copyright (C) 2025 Free Software Foundation, Inc. + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src +# print_ver_ date +# Replacing init.sh specific commands with simple ones for standalone reproduction + +# Current year in the Gregorian calendar. +current_year=$(LC_ALL=C date +%Y) + +export LC_ALL=am_ET.UTF-8 + +if ! locale -a | grep -q "am_ET.UTF-8"; then + echo "Ethiopian UTF-8 locale not available, skipping test" + exit 77 +fi + +# 09-10 and 09-12 of the same Gregorian year are in different years in the +# Ethiopian calendar. +# Note: Using the date binary from the current directory if compiled +DATE_BIN="./target/debug/date" +if [ ! -f "$DATE_BIN" ]; then + DATE_BIN="date" # Fallback to system date if not built + echo "Using system date: $(which date)" +else + echo "Using compiled date: $DATE_BIN" +fi + +year_september_10=$($DATE_BIN -d $current_year-09-10 +%Y) +year_september_12=$($DATE_BIN -d $current_year-09-12 +%Y) +month_name=$($DATE_BIN -d $current_year-09-10 +%B) + +echo "Current Gregorian Year: $current_year" +echo "Sept 10 Ethiopian Year ($DATE_BIN): $year_september_10" +echo "Sept 10 Month ($DATE_BIN): $month_name" +echo "Sept 12 Ethiopian Year ($DATE_BIN): $year_september_12" + +SYSTEM_DATE=$(which date) +if [ -x "$SYSTEM_DATE" ]; then + sys_year=$($SYSTEM_DATE -j -f "%Y-%m-%d" "$current_year-09-10" +%Y 2>/dev/null || $SYSTEM_DATE -d "$current_year-09-10" +%Y 2>/dev/null) + sys_month=$($SYSTEM_DATE -j -f "%Y-%m-%d" "$current_year-09-10" +%B 2>/dev/null || $SYSTEM_DATE -d "$current_year-09-10" +%B 2>/dev/null) + echo "System Date Year: $sys_year" + echo "System Date Month: $sys_month" +fi + +if [ "$year_september_10" = "$(($year_september_12 - 1))" ]; then + echo "PASS: Years differ as expected" +else + echo "FAIL: Years should differ" + fail=1 +fi + +# The difference between the Gregorian year is 7 or 8 years. +if [ "$year_september_10" = "$(($current_year - 8))" ]; then + echo "PASS: Sept 10 is -8 years" +else + echo "FAIL: Sept 10 should be -8 years, got $(($current_year - $year_september_10)) diff" + fail=1 +fi + +if [ "$year_september_12" = "$(($current_year - 7))" ]; then + echo "PASS: Sept 12 is -7 years" +else + echo "FAIL: Sept 12 should be -7 years, got $(($current_year - $year_september_12)) diff" + fail=1 +fi + +# Check that --iso-8601 and --rfc-3339 uses the Gregorian calendar. +case $($DATE_BIN --iso-8601=hours) in $current_year-*) ;; *) echo "FAIL: ISO-8601 not Gregorian"; fail=1 ;; esac +case $($DATE_BIN --rfc-3339=date) in $current_year-*) ;; *) echo "FAIL: RFC-3339 not Gregorian"; fail=1 ;; esac + +if [ "$fail" = "1" ]; then + exit 1 +fi +exit 0 diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index c72b1c3048b..9f0eeccb600 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,6 +6,7 @@ // spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST mod locale; +mod system_time; use clap::{Arg, ArgAction, Command}; use jiff::fmt::strtime; @@ -399,16 +400,25 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { // Format all the dates for date in dates { match date { - // TODO: Switch to lenient formatting. - Ok(date) => match strtime::format(format_string, &date) { - Ok(s) => println!("{s}"), - Err(e) => { - return Err(USimpleError::new( - 1, - translate!("date-error-invalid-format", "format" => format_string, "error" => e), - )); + Ok(date) => { + #[cfg(unix)] + if matches!(settings.format, Format::Custom(_) | Format::Default) { + if let Ok(s) = system_time::format_using_strftime(format_string, &date) { + println!("{s}"); + continue; + } } - }, + + match strtime::format(format_string, &date) { + Ok(s) => println!("{s}"), + Err(e) => { + return Err(USimpleError::new( + 1, + translate!("date-error-invalid-format", "format" => format_string, "error" => e), + )); + } + } + } Err((input, _err)) => show!(USimpleError::new( 1, translate!("date-error-invalid-date", "date" => input) diff --git a/src/uu/date/src/system_time.rs b/src/uu/date/src/system_time.rs new file mode 100644 index 00000000000..b1bab9f236f --- /dev/null +++ b/src/uu/date/src/system_time.rs @@ -0,0 +1,199 @@ +// 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. + +#[cfg(unix)] +pub use unix::*; + +#[cfg(unix)] +mod unix { + use std::ffi::{CString, CStr}; + use jiff::Zoned; + use uucore::error::{UResult, USimpleError}; + use nix::libc; + + pub fn format_using_strftime(format: &str, date: &Zoned) -> UResult { + // Preprocess format string to handle extensions not supported by standard strftime + // or where we want to ensure specific behavior (like %N). + // specific specifiers: %N, %q, %:z, %::z, %:::z + // We use jiff to format these specific parts. + + let mut new_fmt = String::with_capacity(format.len()); + let mut chars = format.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '%' { + if let Some(&next) = chars.peek() { + match next { + 'N' => { + chars.next(); + let nanos = date.timestamp().subsec_nanosecond(); + // eprintln!("DEBUG: Nanos: {}, Full Date: {:?}", nanos, date); + let s = format!("{:09}", nanos); + new_fmt.push_str(&s); + } + 's' => { + chars.next(); + let s = date.timestamp().as_second().to_string(); + new_fmt.push_str(&s); + } + 'q' => { + chars.next(); + let q = (date.month() - 1) / 3 + 1; + new_fmt.push_str(&q.to_string()); + } + 'z' => { + chars.next(); + // %z -> +hhmm + // jiff %z matches this + new_fmt.push_str(&jiff::fmt::strtime::format("%z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); + } + '#' => { + chars.next(); // eat # + if let Some(&n2) = chars.peek() { + if n2 == 'z' { + chars.next(); + // %#z -> treated as %z + new_fmt.push_str(&jiff::fmt::strtime::format("%z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); + } else { + new_fmt.push_str("%#"); + } + } else { + new_fmt.push_str("%#"); + } + } + ':' => { + // Check for :z, ::z, :::z + chars.next(); // eat : + if let Some(&n2) = chars.peek() { + if n2 == 'z' { + chars.next(); + // %:z + new_fmt.push_str(&jiff::fmt::strtime::format("%:z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); + } else if n2 == ':' { + chars.next(); + if let Some(&n3) = chars.peek() { + if n3 == 'z' { + chars.next(); + // %::z + new_fmt.push_str(&jiff::fmt::strtime::format("%::z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); + } else if n3 == ':' { + chars.next(); + if let Some(&n4) = chars.peek() { + if n4 == 'z' { + chars.next(); + // %:::z + new_fmt.push_str(&jiff::fmt::strtime::format("%:::z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); + } else { + new_fmt.push_str("%:::"); + } + } else { + new_fmt.push_str("%:::"); + } + } else { + new_fmt.push_str("%::"); + } + } else { + new_fmt.push_str("%::"); + } + } else { + new_fmt.push_str("%:"); + } + } else { + new_fmt.push_str("%:"); + } + } + // Handle standard escape %% + '%' => { + chars.next(); + new_fmt.push_str("%%"); + } + _ => { + new_fmt.push('%'); + // Let strftime handle the next char, just loop around + } + } + } else { + new_fmt.push('%'); + } + } else { + new_fmt.push(c); + } + } + + let format_string = new_fmt; + + // Convert jiff::Zoned to libc::tm + // Use mem::zeroed to handle platform differences in struct fields + let mut tm: libc::tm = unsafe { std::mem::zeroed() }; + + tm.tm_sec = date.second() as i32; + tm.tm_min = date.minute() as i32; + tm.tm_hour = date.hour() as i32; + tm.tm_mday = date.day() as i32; + tm.tm_mon = date.month() as i32 - 1; // tm_mon is 0-11 + tm.tm_year = date.year() as i32 - 1900; // tm_year is years since 1900 + tm.tm_wday = date.weekday().to_sunday_zero_offset() as i32; + tm.tm_yday = date.day_of_year() as i32 - 1; // tm_yday is 0-365 + tm.tm_isdst = -1; // Let libraries determine if needed, though for formatting typically unused/ignored or uses global if zone not set + + // We need to keep the CString for tm_zone valid during strftime usage + // So we declare it here + let zone_cstring; + + // Set timezone fields on supported platforms + #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly"))] + { + tm.tm_gmtoff = date.offset().seconds() as _; + + // Populate tm_zone + // We can get the abbreviation from date.time_zone(). + // Note: date.time_zone() returns a TimeZone, we need the abbreviation for the specific instant? + // date.datetime() returns civil time. + // jiff::Zoned has `time_zone()` and `offset()`. + // The abbreviation usually depends on whether DST is active. + // checking `date` (Zoned) string representation usually includes it? + // `jiff` doesn't seem to expose `abbreviation()` directly on Zoned nicely? + // Wait, standard `strftime` (%Z) looks at `tm_zone`. + // How do we get abbreviation from jiff::Zoned? + // `date.time_zone()` is the TZDB entry. + // `date.offset()` is the offset. + // We can try to format with %Z using jiff and use that string? + if let Ok(abbrev_string) = jiff::fmt::strtime::format("%Z", date) { + zone_cstring = CString::new(abbrev_string).ok(); + if let Some(ref nz) = zone_cstring { + tm.tm_zone = nz.as_ptr() as *mut i8; + } + } else { + zone_cstring = None; + } + } + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly")))] + { + zone_cstring = None; + } + + let format_c = CString::new(format_string).map_err(|e| { + USimpleError::new(1, format!("Invalid format string: {}", e)) + })?; + + let mut buffer = vec![0u8; 1024]; + let ret = unsafe { + libc::strftime( + buffer.as_mut_ptr() as *mut _, + buffer.len(), + format_c.as_ptr(), + &tm as *const _ + ) + }; + + if ret == 0 { + return Err(USimpleError::new(1, "strftime failed or result too large")); + } + + let c_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) }; + let s = c_str.to_string_lossy().into_owned(); + Ok(s) + } +} diff --git a/tests/by-util/test_date.rs b/tests/by-util/test_date.rs index 9a98b1b0309..7d27e51f8d2 100644 --- a/tests/by-util/test_date.rs +++ b/tests/by-util/test_date.rs @@ -476,6 +476,13 @@ fn test_date_set_valid_4() { } #[test] +#[cfg(unix)] +fn test_invalid_format_string() { + new_ucmd!().arg("+%!").succeeds().stdout_is("!\n"); +} + +#[test] +#[cfg(not(unix))] fn test_invalid_format_string() { let result = new_ucmd!().arg("+%!").fails(); result.no_stdout(); From 7495e123ef5f0f0c6b7928855b0efd757702decc Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 30 Dec 2025 14:25:03 +0900 Subject: [PATCH 2/3] Remove reproduction script for Ethiopian calendar issue. --- repro_issue.sh | 90 -------------------------------------------------- 1 file changed, 90 deletions(-) delete mode 100755 repro_issue.sh diff --git a/repro_issue.sh b/repro_issue.sh deleted file mode 100755 index a547215ae28..00000000000 --- a/repro_issue.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/sh -# Verify the Ethiopian calendar is used in the Ethiopian locale. - -# Copyright (C) 2025 Free Software Foundation, Inc. - -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# . "${srcdir=.}/tests/init.sh"; path_prepend_ ./src -# print_ver_ date -# Replacing init.sh specific commands with simple ones for standalone reproduction - -# Current year in the Gregorian calendar. -current_year=$(LC_ALL=C date +%Y) - -export LC_ALL=am_ET.UTF-8 - -if ! locale -a | grep -q "am_ET.UTF-8"; then - echo "Ethiopian UTF-8 locale not available, skipping test" - exit 77 -fi - -# 09-10 and 09-12 of the same Gregorian year are in different years in the -# Ethiopian calendar. -# Note: Using the date binary from the current directory if compiled -DATE_BIN="./target/debug/date" -if [ ! -f "$DATE_BIN" ]; then - DATE_BIN="date" # Fallback to system date if not built - echo "Using system date: $(which date)" -else - echo "Using compiled date: $DATE_BIN" -fi - -year_september_10=$($DATE_BIN -d $current_year-09-10 +%Y) -year_september_12=$($DATE_BIN -d $current_year-09-12 +%Y) -month_name=$($DATE_BIN -d $current_year-09-10 +%B) - -echo "Current Gregorian Year: $current_year" -echo "Sept 10 Ethiopian Year ($DATE_BIN): $year_september_10" -echo "Sept 10 Month ($DATE_BIN): $month_name" -echo "Sept 12 Ethiopian Year ($DATE_BIN): $year_september_12" - -SYSTEM_DATE=$(which date) -if [ -x "$SYSTEM_DATE" ]; then - sys_year=$($SYSTEM_DATE -j -f "%Y-%m-%d" "$current_year-09-10" +%Y 2>/dev/null || $SYSTEM_DATE -d "$current_year-09-10" +%Y 2>/dev/null) - sys_month=$($SYSTEM_DATE -j -f "%Y-%m-%d" "$current_year-09-10" +%B 2>/dev/null || $SYSTEM_DATE -d "$current_year-09-10" +%B 2>/dev/null) - echo "System Date Year: $sys_year" - echo "System Date Month: $sys_month" -fi - -if [ "$year_september_10" = "$(($year_september_12 - 1))" ]; then - echo "PASS: Years differ as expected" -else - echo "FAIL: Years should differ" - fail=1 -fi - -# The difference between the Gregorian year is 7 or 8 years. -if [ "$year_september_10" = "$(($current_year - 8))" ]; then - echo "PASS: Sept 10 is -8 years" -else - echo "FAIL: Sept 10 should be -8 years, got $(($current_year - $year_september_10)) diff" - fail=1 -fi - -if [ "$year_september_12" = "$(($current_year - 7))" ]; then - echo "PASS: Sept 12 is -7 years" -else - echo "FAIL: Sept 12 should be -7 years, got $(($current_year - $year_september_12)) diff" - fail=1 -fi - -# Check that --iso-8601 and --rfc-3339 uses the Gregorian calendar. -case $($DATE_BIN --iso-8601=hours) in $current_year-*) ;; *) echo "FAIL: ISO-8601 not Gregorian"; fail=1 ;; esac -case $($DATE_BIN --rfc-3339=date) in $current_year-*) ;; *) echo "FAIL: RFC-3339 not Gregorian"; fail=1 ;; esac - -if [ "$fail" = "1" ]; then - exit 1 -fi -exit 0 From c5f7289276f1ca1aa59310b4409294ea285331d0 Mon Sep 17 00:00:00 2001 From: mattsu Date: Tue, 30 Dec 2025 15:56:08 +0900 Subject: [PATCH 3/3] feat: consolidate `system_time` functionality into `date.rs` and enhance `strftime` formatting with new specifiers and Ethiopian calendar support. --- src/uu/date/src/date.rs | 261 ++++++++++++++++++++++++++++++++- src/uu/date/src/system_time.rs | 199 ------------------------- 2 files changed, 259 insertions(+), 201 deletions(-) delete mode 100644 src/uu/date/src/system_time.rs diff --git a/src/uu/date/src/date.rs b/src/uu/date/src/date.rs index 9f0eeccb600..b4bf12ff35a 100644 --- a/src/uu/date/src/date.rs +++ b/src/uu/date/src/date.rs @@ -6,13 +6,13 @@ // spell-checker:ignore strtime ; (format) DATEFILE MMDDhhmm ; (vars) datetime datetimes getres AWST ACST AEST mod locale; -mod system_time; use clap::{Arg, ArgAction, Command}; use jiff::fmt::strtime; use jiff::tz::{TimeZone, TimeZoneDatabase}; use jiff::{Timestamp, Zoned}; use std::collections::HashMap; +use std::ffi::{CStr, CString}; use std::fs::File; use std::io::{BufRead, BufReader}; use std::path::PathBuf; @@ -403,7 +403,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { Ok(date) => { #[cfg(unix)] if matches!(settings.format, Format::Custom(_) | Format::Default) { - if let Ok(s) = system_time::format_using_strftime(format_string, &date) { + if let Ok(s) = format_using_strftime(format_string, &date) { println!("{s}"); continue; } @@ -840,3 +840,260 @@ mod tests { assert_eq!(parse_military_timezone_with_offset("9m"), None); // Starts with digit } } +#[cfg(unix)] +fn is_ethiopian_locale() -> bool { + for var in ["LC_ALL", "LC_TIME", "LANG"] { + if let Ok(val) = std::env::var(var) { + if val.starts_with("am_ET") { + return true; + } + } + } + false +} + +#[cfg(unix)] +fn gregorian_to_ethiopian(y: i32, m: i32, d: i32) -> (i32, i32, i32) { + let (m, y) = if m <= 2 { (m + 12, y - 1) } else { (m, y) }; + let jdn = (1461 * (y + 4800)) / 4 + (367 * (m - 2)) / 12 - (3 * ((y + 4900) / 100)) / 4 + d + - 32075; + + let n = jdn - 1724221; + let n_cycle = n / 1461; + let r = n % 1461; + let y_rel = r / 365; + let y_rel = if r == 1460 { 3 } else { y_rel }; + let year = 4 * n_cycle + y_rel + 1; + let day_of_year = r - y_rel * 365; + let month = day_of_year / 30 + 1; + let day = day_of_year % 30 + 1; + (year, month, day) +} + +#[cfg(unix)] +fn format_using_strftime(format: &str, date: &Zoned) -> UResult { + use nix::libc; + + // Preprocess format string to handle extensions not supported by standard strftime + // or where we want to ensure specific behavior (like %N). + // specific specifiers: %N, %q, %:z, %::z, %:::z + // We use jiff to format these specific parts. + + let mut new_fmt = String::with_capacity(format.len()); + let mut chars = format.chars().peekable(); + + while let Some(c) = chars.next() { + if c == '%' { + if let Some(&next) = chars.peek() { + match next { + 'N' => { + chars.next(); + let nanos = date.timestamp().subsec_nanosecond(); + // eprintln!("DEBUG: Nanos: {}, Full Date: {:?}", nanos, date); + let s = format!("{:09}", nanos); + new_fmt.push_str(&s); + } + 's' => { + chars.next(); + let s = date.timestamp().as_second().to_string(); + new_fmt.push_str(&s); + } + 'q' => { + chars.next(); + let q = (date.month() - 1) / 3 + 1; + new_fmt.push_str(&q.to_string()); + } + 'z' => { + chars.next(); + // %z -> +hhmm + // jiff %z matches this + new_fmt.push_str( + &jiff::fmt::strtime::format("%z", date) + .map_err(|e| USimpleError::new(1, e.to_string()))?, + ); + } + '#' => { + chars.next(); // eat # + if let Some(&n2) = chars.peek() { + if n2 == 'z' { + chars.next(); + // %#z -> treated as %z + new_fmt.push_str( + &jiff::fmt::strtime::format("%z", date) + .map_err(|e| USimpleError::new(1, e.to_string()))?, + ); + } else { + new_fmt.push_str("%#"); + } + } else { + new_fmt.push_str("%#"); + } + } + ':' => { + // Check for :z, ::z, :::z + chars.next(); // eat : + if let Some(&n2) = chars.peek() { + if n2 == 'z' { + chars.next(); + // %:z + new_fmt.push_str( + &jiff::fmt::strtime::format("%:z", date) + .map_err(|e| USimpleError::new(1, e.to_string()))?, + ); + } else if n2 == ':' { + chars.next(); + if let Some(&n3) = chars.peek() { + if n3 == 'z' { + chars.next(); + // %::z + new_fmt.push_str( + &jiff::fmt::strtime::format("%::z", date) + .map_err(|e| USimpleError::new(1, e.to_string()))?, + ); + } else if n3 == ':' { + chars.next(); + if let Some(&n4) = chars.peek() { + if n4 == 'z' { + chars.next(); + // %:::z + new_fmt.push_str( + &jiff::fmt::strtime::format("%:::z", date) + .map_err(|e| { + USimpleError::new(1, e.to_string()) + })?, + ); + } else { + new_fmt.push_str("%:::"); + } + } else { + new_fmt.push_str("%:::"); + } + } else { + new_fmt.push_str("%::"); + } + } else { + new_fmt.push_str("%::"); + } + } else { + new_fmt.push_str("%:"); + } + } else { + new_fmt.push_str("%:"); + } + } + // Handle standard escape %% + '%' => { + chars.next(); + new_fmt.push_str("%%"); + } + _ => { + new_fmt.push('%'); + // Let strftime handle the next char, just loop around + } + } + } else { + new_fmt.push('%'); + } + } else { + new_fmt.push(c); + } + } + + let format_string = new_fmt; + + // Convert jiff::Zoned to libc::tm + // Use mem::zeroed to handle platform differences in struct fields + let mut tm: libc::tm = unsafe { std::mem::zeroed() }; + + tm.tm_sec = date.second() as i32; + tm.tm_min = date.minute() as i32; + tm.tm_hour = date.hour() as i32; + + if is_ethiopian_locale() { + let (y, m, d) = + gregorian_to_ethiopian(date.year() as i32, date.month() as i32, date.day() as i32); + tm.tm_year = y - 1900; + tm.tm_mon = m - 1; + tm.tm_mday = d; + } else { + tm.tm_mday = date.day() as i32; + tm.tm_mon = date.month() as i32 - 1; // tm_mon is 0-11 + tm.tm_year = date.year() as i32 - 1900; // tm_year is years since 1900 + } + + tm.tm_wday = date.weekday().to_sunday_zero_offset() as i32; + tm.tm_yday = date.day_of_year() as i32 - 1; // tm_yday is 0-365 + tm.tm_isdst = -1; // Let libraries determine if needed, though for formatting typically unused/ignored or uses global if zone not set + + // We need to keep the CString for tm_zone valid during strftime usage + // So we declare it here + let _zone_cstring; + + // Set timezone fields on supported platforms + #[cfg(any( + target_os = "linux", + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" + ))] + { + tm.tm_gmtoff = date.offset().seconds() as _; + + // Populate tm_zone + // We can get the abbreviation from date.time_zone(). + // Note: date.time_zone() returns a TimeZone, we need the abbreviation for the specific instant? + // date.datetime() returns civil time. + // jiff::Zoned has `time_zone()` and `offset()`. + // The abbreviation usually depends on whether DST is active. + // checking `date` (Zoned) string representation usually includes it? + // `jiff` doesn't seem to expose `abbreviation()` directly on Zoned nicely? + // Wait, standard `strftime` (%Z) looks at `tm_zone`. + // How do we get abbreviation from jiff::Zoned? + // `date.time_zone()` is the TZDB entry. + // `date.offset()` is the offset. + // We can try to format with %Z using jiff and use that string? + if let Ok(abbrev_string) = jiff::fmt::strtime::format("%Z", date) { + _zone_cstring = CString::new(abbrev_string).ok(); + if let Some(ref nz) = _zone_cstring { + tm.tm_zone = nz.as_ptr() as *mut i8; + } + } else { + _zone_cstring = None; + } + } + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd", + target_os = "dragonfly" + )))] + { + _zone_cstring = None; + } + + let format_c = CString::new(format_string).map_err(|e| { + USimpleError::new(1, format!("Invalid format string: {e}")) + })?; + + let mut buffer = vec![0u8; 1024]; + let ret = unsafe { + libc::strftime( + buffer.as_mut_ptr() as *mut _, + buffer.len(), + format_c.as_ptr(), + &tm as *const _, + ) + }; + + if ret == 0 { + return Err(USimpleError::new(1, "strftime failed or result too large")); + } + + let c_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) }; + let s = c_str.to_string_lossy().into_owned(); + Ok(s) +} diff --git a/src/uu/date/src/system_time.rs b/src/uu/date/src/system_time.rs deleted file mode 100644 index b1bab9f236f..00000000000 --- a/src/uu/date/src/system_time.rs +++ /dev/null @@ -1,199 +0,0 @@ -// 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. - -#[cfg(unix)] -pub use unix::*; - -#[cfg(unix)] -mod unix { - use std::ffi::{CString, CStr}; - use jiff::Zoned; - use uucore::error::{UResult, USimpleError}; - use nix::libc; - - pub fn format_using_strftime(format: &str, date: &Zoned) -> UResult { - // Preprocess format string to handle extensions not supported by standard strftime - // or where we want to ensure specific behavior (like %N). - // specific specifiers: %N, %q, %:z, %::z, %:::z - // We use jiff to format these specific parts. - - let mut new_fmt = String::with_capacity(format.len()); - let mut chars = format.chars().peekable(); - - while let Some(c) = chars.next() { - if c == '%' { - if let Some(&next) = chars.peek() { - match next { - 'N' => { - chars.next(); - let nanos = date.timestamp().subsec_nanosecond(); - // eprintln!("DEBUG: Nanos: {}, Full Date: {:?}", nanos, date); - let s = format!("{:09}", nanos); - new_fmt.push_str(&s); - } - 's' => { - chars.next(); - let s = date.timestamp().as_second().to_string(); - new_fmt.push_str(&s); - } - 'q' => { - chars.next(); - let q = (date.month() - 1) / 3 + 1; - new_fmt.push_str(&q.to_string()); - } - 'z' => { - chars.next(); - // %z -> +hhmm - // jiff %z matches this - new_fmt.push_str(&jiff::fmt::strtime::format("%z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); - } - '#' => { - chars.next(); // eat # - if let Some(&n2) = chars.peek() { - if n2 == 'z' { - chars.next(); - // %#z -> treated as %z - new_fmt.push_str(&jiff::fmt::strtime::format("%z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); - } else { - new_fmt.push_str("%#"); - } - } else { - new_fmt.push_str("%#"); - } - } - ':' => { - // Check for :z, ::z, :::z - chars.next(); // eat : - if let Some(&n2) = chars.peek() { - if n2 == 'z' { - chars.next(); - // %:z - new_fmt.push_str(&jiff::fmt::strtime::format("%:z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); - } else if n2 == ':' { - chars.next(); - if let Some(&n3) = chars.peek() { - if n3 == 'z' { - chars.next(); - // %::z - new_fmt.push_str(&jiff::fmt::strtime::format("%::z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); - } else if n3 == ':' { - chars.next(); - if let Some(&n4) = chars.peek() { - if n4 == 'z' { - chars.next(); - // %:::z - new_fmt.push_str(&jiff::fmt::strtime::format("%:::z", date).map_err(|e| USimpleError::new(1, e.to_string()))?); - } else { - new_fmt.push_str("%:::"); - } - } else { - new_fmt.push_str("%:::"); - } - } else { - new_fmt.push_str("%::"); - } - } else { - new_fmt.push_str("%::"); - } - } else { - new_fmt.push_str("%:"); - } - } else { - new_fmt.push_str("%:"); - } - } - // Handle standard escape %% - '%' => { - chars.next(); - new_fmt.push_str("%%"); - } - _ => { - new_fmt.push('%'); - // Let strftime handle the next char, just loop around - } - } - } else { - new_fmt.push('%'); - } - } else { - new_fmt.push(c); - } - } - - let format_string = new_fmt; - - // Convert jiff::Zoned to libc::tm - // Use mem::zeroed to handle platform differences in struct fields - let mut tm: libc::tm = unsafe { std::mem::zeroed() }; - - tm.tm_sec = date.second() as i32; - tm.tm_min = date.minute() as i32; - tm.tm_hour = date.hour() as i32; - tm.tm_mday = date.day() as i32; - tm.tm_mon = date.month() as i32 - 1; // tm_mon is 0-11 - tm.tm_year = date.year() as i32 - 1900; // tm_year is years since 1900 - tm.tm_wday = date.weekday().to_sunday_zero_offset() as i32; - tm.tm_yday = date.day_of_year() as i32 - 1; // tm_yday is 0-365 - tm.tm_isdst = -1; // Let libraries determine if needed, though for formatting typically unused/ignored or uses global if zone not set - - // We need to keep the CString for tm_zone valid during strftime usage - // So we declare it here - let zone_cstring; - - // Set timezone fields on supported platforms - #[cfg(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly"))] - { - tm.tm_gmtoff = date.offset().seconds() as _; - - // Populate tm_zone - // We can get the abbreviation from date.time_zone(). - // Note: date.time_zone() returns a TimeZone, we need the abbreviation for the specific instant? - // date.datetime() returns civil time. - // jiff::Zoned has `time_zone()` and `offset()`. - // The abbreviation usually depends on whether DST is active. - // checking `date` (Zoned) string representation usually includes it? - // `jiff` doesn't seem to expose `abbreviation()` directly on Zoned nicely? - // Wait, standard `strftime` (%Z) looks at `tm_zone`. - // How do we get abbreviation from jiff::Zoned? - // `date.time_zone()` is the TZDB entry. - // `date.offset()` is the offset. - // We can try to format with %Z using jiff and use that string? - if let Ok(abbrev_string) = jiff::fmt::strtime::format("%Z", date) { - zone_cstring = CString::new(abbrev_string).ok(); - if let Some(ref nz) = zone_cstring { - tm.tm_zone = nz.as_ptr() as *mut i8; - } - } else { - zone_cstring = None; - } - } - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd", target_os = "dragonfly")))] - { - zone_cstring = None; - } - - let format_c = CString::new(format_string).map_err(|e| { - USimpleError::new(1, format!("Invalid format string: {}", e)) - })?; - - let mut buffer = vec![0u8; 1024]; - let ret = unsafe { - libc::strftime( - buffer.as_mut_ptr() as *mut _, - buffer.len(), - format_c.as_ptr(), - &tm as *const _ - ) - }; - - if ret == 0 { - return Err(USimpleError::new(1, "strftime failed or result too large")); - } - - let c_str = unsafe { CStr::from_ptr(buffer.as_ptr() as *const _) }; - let s = c_str.to_string_lossy().into_owned(); - Ok(s) - } -}