use crate::ported::utils::{metafy, zwarnnam};
use std::sync::{Mutex, OnceLock};
use crate::ported::compat::zgettime;
use crate::ported::params::{getsparam, isident, setiparam, setsparam};
use crate::ported::zsh_system_h::timespec;
use crate::ported::zsh_h::{features, options, OPT_ARG, OPT_ISSET, MAX_OPS, module};
use chrono::{DateTime, Local, NaiveDateTime, TimeZone};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn reverse_strftime(
nam: &str,
argv: &[&str], scalar: Option<&str>,
quiet: i32,
) -> i32 {
if argv.len() < 2 {
zwarnnam(nam, "timestring expected");
return 1;
}
let format = argv[0];
let input = argv[1];
let dt = match NaiveDateTime::parse_from_str(input, format) {
Ok(d) => d,
Err(_) => {
if quiet == 0 {
zwarnnam(nam, &format!("format not matched: {}", input));
}
return 1;
}
};
let secs = match Local.from_local_datetime(&dt) {
chrono::LocalResult::Single(d) => d.timestamp(),
chrono::LocalResult::Ambiguous(d, _) => d.timestamp(),
chrono::LocalResult::None => {
if quiet == 0 {
zwarnnam(nam, "unable to convert to time");
}
return 1;
}
};
if let Some(name) = scalar {
setiparam(name, secs); } else {
println!("{}", secs); }
0 }
pub fn output_strftime(
nam: &str,
argv: &[&str], ops: &options,
_func: i32,
) -> i32 {
let scalar: Option<&str> = if OPT_ISSET(ops, b's') {
Some(OPT_ARG(ops, b's').unwrap_or(""))
} else {
None
};
if let Some(name) = scalar {
if !isident(name) {
zwarnnam(nam, &format!("not an identifier: {}", name)); return 1; }
}
if OPT_ISSET(ops, b'r') {
let quiet = if OPT_ISSET(ops, b'q') { 1 } else { 0 };
return reverse_strftime(nam, argv, scalar, quiet); }
if argv.is_empty() {
zwarnnam(nam, "format expected");
return 1;
}
let (secs, nsec) = if argv.len() < 2 {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO);
(now.as_secs() as i64, now.subsec_nanos() as i64) } else {
let secs = match argv[1].parse::<i64>() {
Ok(v) => v,
Err(_) => {
zwarnnam(nam, &format!("{}: invalid decimal number", argv[1]));
return 1; }
};
let nsec = if argv.len() > 2 {
match argv[2].parse::<i64>() {
Ok(v) if (0..=999_999_999).contains(&v) => v, Ok(_) => {
zwarnnam(nam, &format!("{}: invalid nanosecond value", argv[2]));
return 1; }
Err(_) => {
zwarnnam(nam, &format!("{}: invalid decimal number", argv[2]));
return 1;
}
}
} else {
0
};
(secs, nsec)
};
let format = argv[0];
let dt: DateTime<Local> = match Local.timestamp_opt(secs, nsec as u32) {
chrono::LocalResult::Single(d) => d,
chrono::LocalResult::Ambiguous(d, _) => d,
chrono::LocalResult::None => {
zwarnnam(nam, &format!("bad/unsupported format: '{}'", format));
return 1; }
};
let mut work = String::with_capacity(format.len() * 2);
let bytes = format.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 1 < bytes.len() {
match bytes[i + 1] {
b'N' => {
work.push_str(&format!("{:09}", nsec));
i += 2;
continue;
}
b'.' if i + 2 < bytes.len() && bytes[i + 2] == b'N' => {
work.push_str(&format!(".{:09}", nsec));
i += 3;
continue;
}
d if d.is_ascii_digit() && i + 2 < bytes.len() && bytes[i + 2] == b'N' => {
let digits = (d - b'0') as usize;
let scaled = if digits >= 9 {
nsec
} else {
nsec / 10i64.pow((9 - digits) as u32)
};
work.push_str(&format!("{:0width$}", scaled, width = digits));
i += 3;
continue;
}
b'%' => {
work.push_str("%%");
i += 2;
continue;
}
_ => {}
}
}
work.push(bytes[i] as char);
i += 1;
}
let formatted = dt.format(&work).to_string();
if let Some(name) = scalar {
setsparam(name, &metafy(&formatted));
} else {
print!("{}", formatted); if !OPT_ISSET(ops, b'n') {
println!(); }
}
0 }
pub fn bin_strftime(
nam: &str,
argv: &[String], ops: &options,
func: i32,
) -> i32 {
let tz_saved = getsparam("TZ"); if let Some(ref tz) = tz_saved {
std::env::set_var("TZ", tz); }
let argv_views: Vec<&str> = argv.iter().map(String::as_str).collect();
let result = output_strftime(nam, &argv_views, ops, func); if let Some(ref tz) = tz_saved {
std::env::set_var("TZ", tz);
}
result }
pub fn getcurrentsecs() -> i64 {
unsafe { libc::time(std::ptr::null_mut()) as i64 }
}
pub fn getcurrentrealtime() -> f64 {
let mut now: timespec = unsafe { std::mem::zeroed() }; zgettime(&mut now); (now.tv_sec as f64) + (now.tv_nsec as f64) * 1e-9 }
pub fn getcurrenttime() -> Vec<String> {
let mut arr: Vec<String> = Vec::with_capacity(2); let mut now: timespec = unsafe { std::mem::zeroed() }; zgettime(&mut now);
let buf = format!("{}", now.tv_sec as i64);
arr.push(buf); let buf = format!("{}", now.tv_nsec as i64);
arr.push(buf); arr }
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0
}
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0 }
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables) }
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
0
}
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None) }
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0
}
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec![
"b:strftime".to_string(),
"p:EPOCHSECONDS".to_string(),
"p:EPOCHREALTIME".to_string(),
"p:epochtime".to_string(),
]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 4]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 3,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_epoch_seconds() {
let _g = crate::test_util::global_state_lock();
let secs = getcurrentsecs();
assert!(secs > 1700000000);
}
#[test]
fn test_epoch_realtime() {
let _g = crate::test_util::global_state_lock();
let rt = getcurrentrealtime();
assert!(rt > 1700000000.0);
let arr = getcurrenttime();
let secs: i64 = arr[0].parse().unwrap();
assert!((rt - secs as f64).abs() < 1.0);
}
#[test]
fn test_epoch_time() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
let secs: i64 = arr[0].parse().unwrap();
let nanos: i64 = arr[1].parse().unwrap();
assert!(secs > 1700000000);
assert!((0..1_000_000_000).contains(&nanos));
}
fn ops_for(flags: &[u8], scalar: Option<&str>) -> options {
let mut ops = options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
for f in flags {
ops.ind[*f as usize] = 1;
}
if let Some(s) = scalar {
ops.ind[b's' as usize] = 4;
ops.args.push(s.to_string());
ops.argscount = 1;
ops.argsalloc = 1;
}
ops
}
fn pt_get(name: &str) -> Option<String> {
crate::ported::params::paramtab()
.read()
.ok()
.and_then(|t| t.get(name).and_then(|p| p.u_str.clone()))
}
#[test]
fn test_output_strftime_nanoseconds() {
let _g = crate::test_util::global_state_lock();
let ops = ops_for(&[b'n'], Some("OUT"));
let r = output_strftime("strftime", &["%9N", "1700000000", "123456789"], &ops, 0);
assert_eq!(r, 0);
assert_eq!(pt_get("OUT").as_deref(), Some("123456789"));
let r = output_strftime("strftime", &["%3N", "1700000000", "123456789"], &ops, 0);
assert_eq!(r, 0);
assert_eq!(pt_get("OUT").as_deref(), Some("123"));
}
#[test]
fn test_output_strftime_to_scalar() {
let _g = crate::test_util::global_state_lock();
let ops = ops_for(&[b'n'], Some("OUT2"));
let r = output_strftime("strftime", &["%s", "1700000000"], &ops, 0);
assert_eq!(r, 0);
assert_eq!(pt_get("OUT2").as_deref(), Some("1700000000"));
}
#[test]
fn test_output_strftime_format_required() {
let _g = crate::test_util::global_state_lock();
let ops = ops_for(&[], None);
let r = output_strftime("strftime", &[], &ops, 0);
assert_eq!(r, 1);
}
#[test]
fn getcurrentsecs_matches_libc_time() {
let _g = crate::test_util::global_state_lock();
let libc_now = unsafe { libc::time(std::ptr::null_mut()) } as i64;
let our_now = getcurrentsecs();
assert!(
(our_now - libc_now).abs() <= 1,
"getcurrentsecs {} drifted from libc::time {}",
our_now,
libc_now
);
}
#[test]
fn getcurrentrealtime_carries_subsecond_precision() {
let _g = crate::test_util::global_state_lock();
let mut saw_fractional = false;
for _ in 0..10 {
let rt = getcurrentrealtime();
if rt.fract().abs() > 1e-9 {
saw_fractional = true;
break;
}
}
assert!(
saw_fractional,
"getcurrentrealtime over 10 samples never produced a fractional part"
);
}
#[test]
fn getcurrenttime_nanos_under_one_billion() {
let _g = crate::test_util::global_state_lock();
for _ in 0..5 {
let arr = getcurrenttime();
let nanos: i64 = arr[1].parse().unwrap();
assert!(
nanos < 1_000_000_000,
"nanos {} >= 1e9 — unit confusion in c:220 port",
nanos
);
assert!(nanos >= 0, "nanos {} negative", nanos);
}
}
#[test]
fn getcurrentrealtime_advances_forward() {
let _g = crate::test_util::global_state_lock();
let a = getcurrentrealtime();
std::thread::sleep(Duration::from_millis(10));
let b = getcurrentrealtime();
assert!(b >= a, "realtime went backward: {} -> {}", a, b);
assert!(
b - a < 5.0,
"realtime jumped {} seconds in 10ms sleep",
b - a
);
}
#[test]
fn getcurrentsecs_advances_or_stays_equal() {
let _g = crate::test_util::global_state_lock();
let a = getcurrentsecs();
std::thread::sleep(Duration::from_millis(20));
let b = getcurrentsecs();
assert!(b >= a, "seconds went backward: {} -> {}", a, b);
}
#[test]
fn output_strftime_percent_s_round_trips() {
let _g = crate::test_util::global_state_lock();
let ops = ops_for(&[b'n'], Some("EPOCH_ROUND_TRIP"));
let r = output_strftime("strftime", &["%s", "1234567890"], &ops, 0);
assert_eq!(r, 0);
assert_eq!(pt_get("EPOCH_ROUND_TRIP").as_deref(), Some("1234567890"));
}
#[test]
fn output_strftime_invalid_epoch_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let ops = ops_for(&[b'n'], Some("BAD"));
let r = output_strftime("strftime", &["%s", "not-a-number"], &ops, 0);
assert_ne!(r, 0, "garbage epoch must be rejected");
}
#[test]
fn module_lifecycle_shims_all_return_zero() {
let _g = crate::test_util::global_state_lock();
let m: *const module = std::ptr::null();
assert_eq!(setup_(m), 0);
assert_eq!(boot_(m), 0);
assert_eq!(cleanup_(m), 0);
assert_eq!(finish_(m), 0);
}
#[test]
fn enables_populates_some_vec() {
let _g = crate::test_util::global_state_lock();
let m: *const module = std::ptr::null();
let mut enables: Option<Vec<i32>> = None;
assert_eq!(enables_(m, &mut enables), 0);
assert!(enables.is_some(), "enables must be Some after enables_");
}
#[test]
fn getcurrentsecs_returns_post_2020_epoch() {
let _g = crate::test_util::global_state_lock();
let s = getcurrentsecs();
assert!(s > 1_577_836_800, "epoch must be after 2020-01-01; got {s}");
assert!(s < 4_102_444_800, "epoch must be before 2100-01-01; got {s}");
}
#[test]
fn getcurrentsecs_is_monotonic_non_decreasing() {
let _g = crate::test_util::global_state_lock();
let a = getcurrentsecs();
let b = getcurrentsecs();
assert!(b >= a, "time went backwards: {a} → {b}");
}
#[test]
fn getcurrentrealtime_returns_positive_float() {
let _g = crate::test_util::global_state_lock();
let t = getcurrentrealtime();
assert!(t > 1_577_836_800.0, "realtime must be after 2020; got {t}");
}
#[test]
fn getcurrentrealtime_matches_getcurrentsecs_within_a_second() {
let _g = crate::test_util::global_state_lock();
let secs = getcurrentsecs() as f64;
let real = getcurrentrealtime();
let diff = (real - secs).abs();
assert!(diff < 2.0, "realtime/secs diverge by {diff} seconds");
}
#[test]
fn getcurrenttime_returns_two_element_array() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
assert_eq!(arr.len(), 2, "must be exactly 2 elements [secs, nanos]");
}
#[test]
fn getcurrenttime_first_element_is_parseable_epoch_seconds() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
let secs: i64 = arr[0].parse().expect("element 0 must be valid i64");
assert!(secs > 1_577_836_800, "secs must be post-2020");
}
#[test]
fn getcurrenttime_second_element_is_valid_nanoseconds() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
let nanos: i64 = arr[1].parse().expect("element 1 must be valid i64");
assert!(nanos >= 0, "nanos must be non-negative");
assert!(nanos < 1_000_000_000, "nanos must be < 1e9; got {nanos}");
}
#[test]
fn getcurrenttime_monotonic_seconds() {
let _g = crate::test_util::global_state_lock();
let a: i64 = getcurrenttime()[0].parse().unwrap();
let b: i64 = getcurrenttime()[0].parse().unwrap();
assert!(b >= a, "time went backwards: {a} → {b}");
}
#[test]
fn datetime_corpus_getcurrentsecs_positive() {
let _g = crate::test_util::global_state_lock();
let s = getcurrentsecs();
assert!(s > 1_577_836_800, "post-2020 epoch, got {s}");
}
#[test]
fn datetime_corpus_getcurrentsecs_monotonic() {
let _g = crate::test_util::global_state_lock();
let a = getcurrentsecs();
let b = getcurrentsecs();
assert!(b >= a, "time went backwards: {a} → {b}");
}
#[test]
fn datetime_corpus_getcurrentrealtime_positive() {
let _g = crate::test_util::global_state_lock();
let r = getcurrentrealtime();
assert!(r > 1_577_836_800.0, "post-2020 epoch, got {r}");
}
#[test]
fn datetime_corpus_getcurrentrealtime_returns_f64() {
let _g = crate::test_util::global_state_lock();
let r = getcurrentrealtime();
assert!(r.is_finite(), "must be finite, got {r}");
}
#[test]
fn datetime_corpus_getcurrenttime_two_elements() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
assert_eq!(arr.len(), 2, "exactly 2 elements (secs, nanos)");
}
#[test]
fn datetime_corpus_getcurrenttime_both_parse_as_i64() {
let _g = crate::test_util::global_state_lock();
let arr = getcurrenttime();
let _: i64 = arr[0].parse().expect("secs parses");
let _: i64 = arr[1].parse().expect("nanos parses");
}
#[test]
fn datetime_corpus_getcurrentsecs_matches_getcurrenttime_secs() {
let _g = crate::test_util::global_state_lock();
let a = getcurrentsecs();
let b: i64 = getcurrenttime()[0].parse().unwrap();
assert!(
(a - b).abs() <= 2,
"getcurrentsecs={a} should match getcurrenttime[0]={b} within ~2s"
);
}
}