use std::{env, fs};
use std::os::unix::fs::MetadataExt;
use crate::params::getsparam;
use crate::ported::zsh_h::dirsav;
use crate::utils::{unmeta, zwarn};
use crate::zsh_system_h::{timespec, OPEN_MAX, ZSH_INITIAL_OPEN_MAX};
pub fn zgettime(ts: &mut timespec) -> i32 {
let mut ret: i32 = -1; unsafe {
let mut dts: timespec = std::mem::zeroed();
if libc::clock_gettime(libc::CLOCK_REALTIME, &mut dts) < 0 {
zwarn(&format!(
"unable to retrieve time: {}",
std::io::Error::last_os_error()
));
ret -= 1; } else {
ret += 1; ts.tv_sec = dts.tv_sec; ts.tv_nsec = dts.tv_nsec; }
if ret != 0 {
let mut dtv: libc::timeval = std::mem::zeroed(); libc::gettimeofday(&mut dtv, std::ptr::null_mut()); ret += 1; ts.tv_sec = dtv.tv_sec; ts.tv_nsec = (dtv.tv_usec as libc::c_long) * 1000; }
}
ret }
pub fn zgettime_monotonic_if_available(ts: &mut timespec) -> i32 {
let mut ret: i32 = -1; unsafe {
let mut dts: timespec = std::mem::zeroed(); #[cfg(target_os = "macos")]
let clk = libc::CLOCK_MONOTONIC_RAW;
#[cfg(not(target_os = "macos"))]
let clk = libc::CLOCK_MONOTONIC;
if libc::clock_gettime(clk, &mut dts) < 0 {
zwarn(&format!(
"unable to retrieve CLOCK_MONOTONIC time: {}",
std::io::Error::last_os_error()
));
ret -= 1; } else {
ret += 1; ts.tv_sec = dts.tv_sec; ts.tv_nsec = dts.tv_nsec; }
}
if ret != 0 {
ret = zgettime(ts); }
ret }
pub fn difftime(t2: i64, t1: i64) -> f64 {
(t2 - t1) as f64
}
pub fn strerror(errnum: i32) -> String {
std::io::Error::from_raw_os_error(errnum).to_string()
}
pub fn zopenmax() -> i64 {
#[cfg(unix)]
{
unsafe {
let mut openmax = libc::sysconf(libc::_SC_OPEN_MAX);
if openmax < 1 {
openmax = OPEN_MAX as i64;
} else if openmax > OPEN_MAX as i64 {
if openmax > ZSH_INITIAL_OPEN_MAX as i64 {
openmax = ZSH_INITIAL_OPEN_MAX as i64;
}
let mut j = OPEN_MAX as i64;
let mut i = j;
while i < openmax {
let r = libc::fcntl(i as i32, libc::F_GETFL, 0);
if r < 0 {
let e = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
if e == libc::EBADF || e == libc::EINTR {
if e != libc::EINTR {
i += 1;
}
continue;
}
}
j = i;
i += 1;
}
openmax = j;
}
openmax
}
}
#[cfg(not(unix))]
{
OPEN_MAX
}
}
pub fn zgetdir(d: Option<&mut dirsav>) -> Option<String> {
let cwd = env::current_dir().ok()?;
let cwd_str = cwd.to_str()?.to_string();
#[cfg(unix)]
if let Some(dirsav) = d {
if let Ok(meta) = fs::metadata(&cwd) {
dirsav.ino = meta.ino();
dirsav.dev = meta.dev();
}
dirsav.dirname = Some(cwd_str.clone());
}
#[cfg(not(unix))]
if let Some(dirsav) = d {
dirsav.dirname = Some(cwd_str.clone());
}
Some(cwd_str)
}
pub fn zgetcwd() -> String {
if let Some(ret) = zgetdir(None) {
if !ret.is_empty() {
return ret;
}
}
if let Some(pwd) = getsparam("PWD") {
let unmeta_pwd = unmeta(&pwd); if !unmeta_pwd.is_empty() {
return unmeta_pwd;
}
}
".".to_string() }
pub fn zchdir(dir: &str) -> i32 {
#[cfg(unix)]
{
let path_max: usize = libc::PATH_MAX as usize;
let mut remaining: Vec<u8> = dir.as_bytes().to_vec();
let mut saved_currdir: i32 = -2; loop {
if remaining.is_empty() {
if saved_currdir >= 0 {
unsafe {
libc::close(saved_currdir);
}
}
return 0;
}
let c_dir = match std::ffi::CString::new(remaining.clone()) {
Ok(c) => c,
Err(_) => return -1, };
let rc = unsafe { libc::chdir(c_dir.as_ptr()) };
if rc == 0 {
if saved_currdir >= 0 {
unsafe {
libc::close(saved_currdir);
} }
return 0; }
let err = std::io::Error::last_os_error().raw_os_error().unwrap_or(0);
let ok_errno = err == libc::ENAMETOOLONG || err == libc::ENOMEM;
if !ok_errno || remaining.len() < path_max {
break;
}
let mut s_idx: isize = (path_max - 1) as isize;
while s_idx > 0 && remaining.get(s_idx as usize) != Some(&b'/') {
s_idx -= 1;
}
if s_idx == 0 {
break;
}
if saved_currdir == -2 {
let dot = std::ffi::CString::new(".").unwrap();
saved_currdir =
unsafe { libc::open(dot.as_ptr(), libc::O_RDONLY | libc::O_NOCTTY) };
}
let prefix: Vec<u8> = remaining[..s_idx as usize].to_vec();
let c_prefix = match std::ffi::CString::new(prefix) {
Ok(c) => c,
Err(_) => break,
};
if unsafe { libc::chdir(c_prefix.as_ptr()) } < 0 {
break;
}
let mut tail_start = s_idx as usize + 1;
while tail_start < remaining.len() && remaining[tail_start] == b'/' {
tail_start += 1;
}
remaining = remaining[tail_start..].to_vec();
}
if saved_currdir >= 0 {
let rc = unsafe { libc::fchdir(saved_currdir) };
unsafe {
libc::close(saved_currdir);
} if rc < 0 {
return -2; }
return -1; }
if saved_currdir == -2 {
-1
} else {
-2
} }
#[cfg(not(unix))]
{
let _ = (dir, env::set_current_dir);
if dir.is_empty() {
return 0;
}
match env::set_current_dir(dir) {
Ok(_) => 0,
Err(_) => -1,
}
}
}
pub fn output64(val: i64) -> String {
val.to_string()
}
pub fn u9_wcwidth(ucs: char) -> i32 {
unicode_width::UnicodeWidthChar::width(ucs)
.map(|w| w as i32)
.unwrap_or(if ucs.is_control() { -1 } else { 1 })
}
pub fn u9_iswprint(ucs: char) -> bool {
!ucs.is_control() && u9_wcwidth(ucs) >= 0
}
pub fn isprint_ascii(c: char) -> bool {
let b = c as u32;
(0x20..=0x7e).contains(&b)
}
pub fn strstr(s: &str, t: &str) -> Option<usize> {
s.find(t) }
pub fn gettimeofday() -> (i64, i64) {
#[cfg(unix)]
{
let mut tv: libc::timeval = unsafe { std::mem::zeroed() };
unsafe {
libc::gettimeofday(&mut tv, std::ptr::null_mut());
} (tv.tv_sec as i64, tv.tv_usec as i64)
}
#[cfg(not(unix))]
{
(0, 0)
}
}
pub fn strtoul(nptr: &str, base: u32) -> (u64, usize) {
let bytes = nptr.as_bytes();
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_whitespace() {
i += 1;
} let neg = i < bytes.len() && bytes[i] == b'-'; if neg || (i < bytes.len() && bytes[i] == b'+') {
i += 1;
} let (radix, start) = if (base == 0 || base == 16)
&& bytes.get(i).copied() == Some(b'0')
&& bytes
.get(i + 1)
.map(|b| b.eq_ignore_ascii_case(&b'x'))
.unwrap_or(false)
{
(16u32, i + 2) } else if base == 0 {
(
if bytes.get(i).copied() == Some(b'0') {
8
} else {
10
},
i,
) } else {
(base, i)
};
let mut acc: u64 = 0;
let mut consumed = start;
for &b in &bytes[start..] {
let digit = if b.is_ascii_digit() {
(b - b'0') as u32
} else if b.is_ascii_uppercase() {
(b - b'A' + 10) as u32
} else if b.is_ascii_lowercase() {
(b - b'a' + 10) as u32
} else {
break;
};
if digit >= radix {
break;
}
acc = acc
.saturating_mul(radix as u64)
.saturating_add(digit as u64);
consumed += 1;
}
(if neg { acc.wrapping_neg() } else { acc }, consumed)
}
pub fn zpathmax(dir: &str) -> i64 {
#[cfg(unix)]
unsafe {
let mut buf: Vec<u8> = dir.as_bytes().to_vec(); #[cfg(target_os = "macos")]
let errno_loc: *mut libc::c_int = libc::__error();
#[cfg(target_os = "linux")]
let errno_loc: *mut libc::c_int = libc::__errno_location();
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
let errno_loc: *mut libc::c_int = std::ptr::null_mut();
if errno_loc.is_null() {
let dirlen = buf.len() as i64;
let path_max = 4096i64;
return if dirlen >= path_max {
-1
} else {
path_max - dirlen
};
}
let mut accumulated_taillen: libc::c_long = 0; loop {
let cs = match std::ffi::CString::new(buf.clone()) {
Ok(c) => c,
Err(_) => return -1,
};
*errno_loc = 0; let pathmax = libc::pathconf(cs.as_ptr(), libc::_PC_PATH_MAX); if pathmax >= 0 {
if accumulated_taillen == 0 {
return pathmax as i64; }
if accumulated_taillen < pathmax {
return (pathmax - accumulated_taillen) as i64; } else {
*errno_loc = libc::ENAMETOOLONG; return -1;
}
}
let err = *errno_loc;
if err != libc::EINVAL && err != libc::ENOENT && err != libc::ENOTDIR {
return if *errno_loc != 0 { -1 } else { 0 }; }
let tail_pos: Option<usize> = buf.iter().rposition(|&b| b == b'/');
let mut tail = match tail_pos {
Some(t) => t,
None => {
*errno_loc = 0;
let dot = std::ffi::CString::new(".").unwrap();
let pm = libc::pathconf(dot.as_ptr(), libc::_PC_PATH_MAX);
let taillen = (buf.len() + 1) as libc::c_long;
if pm > 0 && taillen < pm {
return (pm - taillen) as i64; }
if pm > 0 {
*errno_loc = libc::ENAMETOOLONG;
} return if *errno_loc != 0 { -1 } else { 0 }; }
};
while tail > 0 && buf[tail - 1] == b'/' {
tail -= 1;
} let taillen_now = (buf.len() - tail) as libc::c_long; accumulated_taillen += taillen_now;
if tail > 0 {
buf.truncate(tail); continue;
} else {
*errno_loc = 0;
let root = std::ffi::CString::new("/").unwrap();
let pm = libc::pathconf(root.as_ptr(), libc::_PC_PATH_MAX);
if pm > 0 && accumulated_taillen < pm {
return (pm - accumulated_taillen) as i64; }
if pm > 0 {
*errno_loc = libc::ENAMETOOLONG;
} return if *errno_loc != 0 { -1 } else { 0 }; }
}
}
#[cfg(not(unix))]
{
let dirlen = dir.len() as i64;
let path_max = 4096i64;
if dirlen >= path_max {
-1
} else {
path_max - dirlen
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zgettime() {
let _g = crate::test_util::global_state_lock();
let mut ts: timespec = unsafe { std::mem::zeroed() };
let r = zgettime(&mut ts);
assert!(r >= 0);
assert!(ts.tv_sec > 0);
}
#[test]
fn test_zgettime_monotonic() {
let _g = crate::test_util::global_state_lock();
let mut t1: timespec = unsafe { std::mem::zeroed() };
let mut t2: timespec = unsafe { std::mem::zeroed() };
let r1 = zgettime_monotonic_if_available(&mut t1);
std::thread::sleep(std::time::Duration::from_millis(10));
let r2 = zgettime_monotonic_if_available(&mut t2);
assert!(r1 >= 0 && r2 >= 0);
let elapsed_ns = (t2.tv_sec - t1.tv_sec) * 1_000_000_000 + (t2.tv_nsec - t1.tv_nsec) as i64;
assert!(elapsed_ns > 0);
}
#[test]
fn test_zgetcwd() {
let _g = crate::test_util::global_state_lock();
let cwd = zgetcwd();
assert!(!cwd.is_empty(), "c:564-565 — zgetcwd never returns empty");
}
#[test]
fn test_zopenmax() {
let _g = crate::test_util::global_state_lock();
let max = zopenmax();
assert!(max > 0);
}
#[test]
fn test_isprint_safe() {
let _g = crate::test_util::global_state_lock();
assert!(isprint_ascii('a'));
assert!(isprint_ascii('Z'));
assert!(isprint_ascii(' '));
assert!(!isprint_ascii('\x00'));
assert!(!isprint_ascii('\x1f'));
}
#[test]
fn test_wcwidth() {
let _g = crate::test_util::global_state_lock();
assert_eq!(u9_wcwidth('a'), 1);
assert_eq!(u9_wcwidth('中'), 2);
assert!(u9_wcwidth('\x00') <= 0);
}
#[test]
fn strstr_substring_hit_returns_byte_offset() {
let _g = crate::test_util::global_state_lock();
assert_eq!(strstr("hello world", "world"), Some(6));
assert_eq!(strstr("hello world", "hello"), Some(0));
assert_eq!(strstr("hello world", "xyz"), None);
assert_eq!(strstr("", "x"), None);
assert_eq!(strstr("anything", ""), Some(0));
}
#[cfg(unix)]
#[test]
fn gettimeofday_returns_positive_secs() {
let _g = crate::test_util::global_state_lock();
let (sec, _usec) = gettimeofday();
assert!(sec > 1_000_000_000, "epoch seconds should be past 2001");
}
#[test]
fn strtoul_parses_decimal() {
let _g = crate::test_util::global_state_lock();
let (v, n) = strtoul("12345", 10);
assert_eq!(v, 12345);
assert_eq!(n, 5);
}
#[test]
fn strtoul_parses_hex_with_0x_prefix_when_base_zero() {
let _g = crate::test_util::global_state_lock();
let (v, n) = strtoul("0xff", 0);
assert_eq!(v, 255);
assert_eq!(n, 4);
}
#[test]
fn strtoul_parses_octal_when_base_zero_with_leading_zero() {
let _g = crate::test_util::global_state_lock();
let (v, _n) = strtoul("0777", 0);
assert_eq!(v, 511);
}
#[test]
fn strtoul_skips_leading_whitespace() {
let _g = crate::test_util::global_state_lock();
let (v, _) = strtoul(" 42", 10);
assert_eq!(v, 42);
}
#[test]
fn strtoul_stops_at_first_non_digit() {
let _g = crate::test_util::global_state_lock();
let (v, n) = strtoul("100abc", 10);
assert_eq!(v, 100);
assert_eq!(n, 3);
}
#[test]
fn difftime_returns_signed_double_difference() {
let _g = crate::test_util::global_state_lock();
assert_eq!(difftime(1_700_000_010, 1_700_000_000), 10.0);
assert_eq!(
difftime(1_700_000_000, 1_700_000_010),
-10.0,
"c:178 — signed cast; t1 > t2 must be negative"
);
assert_eq!(difftime(42, 42), 0.0);
}
#[test]
fn isprint_ascii_matches_strict_ascii_printable_range() {
let _g = crate::test_util::global_state_lock();
assert!(isprint_ascii(' '), "c:786 — 0x20 is printable");
assert!(isprint_ascii('~'), "c:786 — 0x7e is printable");
assert!(!isprint_ascii('\x1f'), "c:786 — 0x1f is NOT printable");
assert!(!isprint_ascii('\x7f'), "c:786 — DEL is NOT printable");
assert!(isprint_ascii('A'));
assert!(isprint_ascii('0'));
assert!(isprint_ascii('!'));
assert!(!isprint_ascii('\t'));
assert!(!isprint_ascii('\n'));
assert!(!isprint_ascii('\0'));
assert!(!isprint_ascii('é'), "c:786 — non-ASCII outside range");
assert!(!isprint_ascii('字'), "c:786 — wide char outside range");
}
#[test]
fn output64_formats_i64_boundaries_and_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(output64(0), "0");
assert_eq!(output64(42), "42");
assert_eq!(output64(-1), "-1");
assert_eq!(output64(i64::MAX), "9223372036854775807");
assert_eq!(output64(i64::MIN), "-9223372036854775808");
}
#[test]
fn u9_iswprint_accepts_printable_rejects_controls() {
let _g = crate::test_util::global_state_lock();
assert!(u9_iswprint('a'));
assert!(u9_iswprint(' '));
assert!(u9_iswprint('é'), "Latin-1 letter is printable");
assert!(u9_iswprint('字'), "CJK ideograph is printable");
assert!(!u9_iswprint('\0'));
assert!(!u9_iswprint('\t'));
assert!(!u9_iswprint('\n'));
assert!(!u9_iswprint('\x07'));
assert!(!u9_iswprint('\x1b'));
assert!(!u9_iswprint('\x7f'), "DEL is a C0 control");
}
#[test]
fn u9_wcwidth_returns_canonical_widths() {
let _g = crate::test_util::global_state_lock();
assert_eq!(u9_wcwidth('\x07'), -1);
assert_eq!(u9_wcwidth('a'), 1);
assert_eq!(u9_wcwidth(' '), 1);
assert_eq!(u9_wcwidth('字'), 2);
assert_eq!(u9_wcwidth('\u{0301}'), 0);
}
#[test]
fn strerror_returns_non_empty_string_for_known_errno() {
let _g = crate::test_util::global_state_lock();
let s = strerror(2 );
assert!(
!s.is_empty(),
"c:194 — strerror must return non-empty for ENOENT"
);
}
#[test]
fn zopenmax_caps_within_canonical_ladder() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ZSH_INITIAL_OPEN_MAX,
64,
"Src/zsh_system.h:307 — ZSH_INITIAL_OPEN_MAX must be 64"
);
let m = zopenmax();
assert!(m > 0, "c:307 — zopenmax must report a positive ceiling");
}
#[test]
fn zgetcwd_always_returns_non_empty() {
let _g = crate::test_util::global_state_lock();
let cwd = zgetcwd();
assert!(
!cwd.is_empty(),
"c:564-565 — zgetcwd must NEVER return empty (falls through to dupstring(\".\"))"
);
#[cfg(unix)]
{
assert!(
cwd.starts_with('/') || cwd == ".",
"c:561 — zgetdir(NULL) returns absolute path, or c:565 fallback `.`"
);
}
}
#[test]
fn zchdir_empty_path_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(zchdir(""), 0, "c:585 — empty dir short-circuits to success");
}
#[test]
fn zchdir_existing_path_succeeds_without_fallback() {
let _g = crate::test_util::global_state_lock();
let saved = env::current_dir().unwrap();
let rc = zchdir("/");
assert_eq!(rc, 0, "c:585 — zchdir(\"/\") direct success");
env::set_current_dir(&saved).unwrap();
}
#[test]
fn zchdir_nonexistent_path_returns_minus_one_without_fallback() {
let _g = crate::test_util::global_state_lock();
let saved = env::current_dir().unwrap();
let rc = zchdir("/tmp/this_zshrs_test_path_does_not_exist_xyz_abc");
assert_eq!(
rc, -1,
"c:592-594 — non-ENAMETOOLONG failure breaks loop, returns -1"
);
assert_eq!(
env::current_dir().unwrap(),
saved,
"no chdir side-effect on non-recoverable failure"
);
}
#[test]
fn compat_corpus_output64_zero() {
assert_eq!(output64(0), "0");
}
#[test]
fn compat_corpus_output64_positive() {
assert_eq!(output64(42), "42");
assert_eq!(output64(1234567890), "1234567890");
}
#[test]
fn compat_corpus_output64_int_max() {
assert_eq!(output64(i64::MAX), i64::MAX.to_string());
}
#[test]
fn compat_corpus_output64_negative() {
assert_eq!(output64(-42), "-42");
assert_eq!(output64(i64::MIN), i64::MIN.to_string());
}
#[test]
fn compat_corpus_strstr_finds_substring() {
assert_eq!(strstr("hello world", "world"), Some(6));
}
#[test]
fn compat_corpus_strstr_empty_needle() {
let r = strstr("hello", "");
assert_eq!(r, Some(0), "empty needle matches at position 0");
}
#[test]
fn compat_corpus_strstr_missing_returns_none() {
assert_eq!(strstr("hello world", "zzz"), None);
}
#[test]
fn compat_corpus_strtoul_decimal() {
let (val, consumed) = strtoul("42", 10);
assert_eq!(val, 42);
assert_eq!(consumed, 2, "consumed 2 chars");
}
#[test]
fn compat_corpus_strtoul_hex() {
let (val, _) = strtoul("ff", 16);
assert_eq!(val, 255);
}
#[test]
fn compat_corpus_strtoul_stops_at_nondigit() {
let (val, consumed) = strtoul("123abc", 10);
assert_eq!(val, 123);
assert_eq!(consumed, 3, "stopped at non-digit");
}
#[test]
fn compat_corpus_difftime_positive() {
let d = difftime(100, 60);
assert!((d - 40.0).abs() < 1e-9, "100 - 60 = 40, got {d}");
}
#[test]
fn compat_corpus_difftime_negative() {
let d = difftime(60, 100);
assert!((d + 40.0).abs() < 1e-9, "60 - 100 = -40, got {d}");
}
#[test]
fn compat_corpus_isprint_ascii_visible() {
for c in ['a', 'Z', '0', '9', ' ', '~'] {
assert!(isprint_ascii(c), "{c:?} should be printable");
}
}
#[test]
fn compat_corpus_isprint_ascii_rejects_controls() {
for c in ['\0', '\n', '\r', '\t', '\x1b'] {
assert!(!isprint_ascii(c), "{c:?} should NOT be printable");
}
}
}