Skip to main content

zsh/
compat.rs

1//! Compatibility and utility routines for zshrs
2//!
3//! Direct port from zsh/Src/compat.c
4//!
5//! Provides:
6//! - High-resolution time functions
7//! - Directory navigation utilities
8//! - Path handling for long pathnames
9//! - 64-bit integer formatting
10
11use std::env;
12use std::fs;
13use std::path::{Path, PathBuf};
14use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
15
16/// Time with nanosecond precision
17#[derive(Debug, Clone, Copy, Default)]
18pub struct TimeSpec {
19    pub tv_sec: i64,
20    pub tv_nsec: i64,
21}
22
23impl TimeSpec {
24    pub fn new(sec: i64, nsec: i64) -> Self {
25        TimeSpec {
26            tv_sec: sec,
27            tv_nsec: nsec,
28        }
29    }
30
31    pub fn now() -> Self {
32        match SystemTime::now().duration_since(UNIX_EPOCH) {
33            Ok(d) => TimeSpec {
34                tv_sec: d.as_secs() as i64,
35                tv_nsec: d.subsec_nanos() as i64,
36            },
37            Err(_) => TimeSpec::default(),
38        }
39    }
40
41    pub fn as_duration(&self) -> Duration {
42        Duration::new(self.tv_sec as u64, self.tv_nsec as u32)
43    }
44
45    pub fn as_secs_f64(&self) -> f64 {
46        self.tv_sec as f64 + (self.tv_nsec as f64 / 1_000_000_000.0)
47    }
48}
49
50impl std::ops::Sub for TimeSpec {
51    type Output = TimeSpec;
52
53    fn sub(self, other: TimeSpec) -> TimeSpec {
54        let mut sec = self.tv_sec - other.tv_sec;
55        let mut nsec = self.tv_nsec - other.tv_nsec;
56        if nsec < 0 {
57            sec -= 1;
58            nsec += 1_000_000_000;
59        }
60        TimeSpec {
61            tv_sec: sec,
62            tv_nsec: nsec,
63        }
64    }
65}
66
67/// Get current time with nanosecond precision (real time)
68pub fn zgettime() -> TimeSpec {
69    TimeSpec::now()
70}
71
72/// Monotonic time tracking
73static MONOTONIC_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
74
75/// Get monotonic time (for timing, doesn't go backwards)
76pub fn zgettime_monotonic() -> TimeSpec {
77    let start = MONOTONIC_START.get_or_init(Instant::now);
78    let elapsed = start.elapsed();
79    TimeSpec {
80        tv_sec: elapsed.as_secs() as i64,
81        tv_nsec: elapsed.subsec_nanos() as i64,
82    }
83}
84
85/// Compute difference between two times
86pub fn difftime(t2: i64, t1: i64) -> f64 {
87    (t2 - t1) as f64
88}
89
90/// Get system's maximum open file descriptors
91pub fn zopenmax() -> i64 {
92    #[cfg(unix)]
93    {
94        // Try to get from system
95        unsafe {
96            let max = libc::sysconf(libc::_SC_OPEN_MAX);
97            if max > 0 {
98                // Cap at a reasonable value
99                return max.min(1024 * 1024);
100            }
101        }
102    }
103
104    // Fallback
105    1024
106}
107
108/// Get the current working directory
109pub fn zgetcwd() -> Option<String> {
110    env::current_dir()
111        .ok()
112        .and_then(|p| p.to_str().map(|s| s.to_string()))
113}
114
115/// Get directory with additional metadata
116pub struct DirSav {
117    pub dirname: Option<String>,
118    #[cfg(unix)]
119    pub ino: u64,
120    #[cfg(unix)]
121    pub dev: u64,
122}
123
124impl Default for DirSav {
125    fn default() -> Self {
126        DirSav {
127            dirname: None,
128            #[cfg(unix)]
129            ino: 0,
130            #[cfg(unix)]
131            dev: 0,
132        }
133    }
134}
135
136/// Get current directory with optional metadata storage
137pub fn zgetdir(d: Option<&mut DirSav>) -> Option<String> {
138    let cwd = env::current_dir().ok()?;
139    let cwd_str = cwd.to_str()?.to_string();
140
141    #[cfg(unix)]
142    if let Some(dirsav) = d {
143        use std::os::unix::fs::MetadataExt;
144        if let Ok(meta) = fs::metadata(&cwd) {
145            dirsav.ino = meta.ino();
146            dirsav.dev = meta.dev();
147        }
148        dirsav.dirname = Some(cwd_str.clone());
149    }
150
151    #[cfg(not(unix))]
152    if let Some(dirsav) = d {
153        dirsav.dirname = Some(cwd_str.clone());
154    }
155
156    Some(cwd_str)
157}
158
159/// Change directory with support for long pathnames
160/// Returns 0 on success, -1 on normal failure, -2 if current directory is lost
161pub fn zchdir(dir: &str) -> i32 {
162    if dir.is_empty() {
163        return 0;
164    }
165
166    // Try direct chdir first
167    if env::set_current_dir(dir).is_ok() {
168        return 0;
169    }
170
171    // For long paths, try changing incrementally
172    let path = Path::new(dir);
173    if !path.is_absolute() {
174        return -1;
175    }
176
177    // Save current directory
178    let saved_dir = env::current_dir().ok();
179
180    // Try to change directory component by component
181    let mut current = PathBuf::from("/");
182    for component in path.components().skip(1) {
183        current.push(component);
184        if env::set_current_dir(&current).is_err() {
185            // Try to restore
186            if let Some(ref saved) = saved_dir {
187                if env::set_current_dir(saved).is_err() {
188                    return -2; // Lost current directory
189                }
190            }
191            return -1;
192        }
193    }
194
195    0
196}
197
198/// Format a 64-bit integer for output
199pub fn output64(val: i64) -> String {
200    val.to_string()
201}
202
203/// Format an unsigned 64-bit integer for output
204pub fn output64u(val: u64) -> String {
205    val.to_string()
206}
207
208/// Convert number to string with given base
209pub fn convbase(val: i64, base: u32) -> String {
210    if base == 0 || base == 10 {
211        return val.to_string();
212    }
213
214    let is_negative = val < 0;
215    let mut n = val.unsigned_abs();
216    let mut result = String::new();
217
218    if n == 0 {
219        return "0".to_string();
220    }
221
222    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
223    while n > 0 {
224        let digit = (n % base as u64) as usize;
225        result.push(digits[digit] as char);
226        n /= base as u64;
227    }
228
229    if is_negative {
230        result.push('-');
231    }
232
233    result.chars().rev().collect()
234}
235
236/// Convert unsigned number to string with given base
237pub fn convbaseu(val: u64, base: u32) -> String {
238    if base == 0 || base == 10 {
239        return val.to_string();
240    }
241
242    let mut n = val;
243    let mut result = String::new();
244
245    if n == 0 {
246        return "0".to_string();
247    }
248
249    let digits = b"0123456789abcdefghijklmnopqrstuvwxyz";
250    while n > 0 {
251        let digit = (n % base as u64) as usize;
252        result.push(digits[digit] as char);
253        n /= base as u64;
254    }
255
256    result.chars().rev().collect()
257}
258
259/// Get hostname
260pub fn gethostname() -> Option<String> {
261    hostname::get().ok().and_then(|h| h.into_string().ok())
262}
263
264/// Check if a character is printable (ASCII safe version)
265pub fn isprint_safe(c: char) -> bool {
266    let b = c as u32;
267    b >= 0x20 && b <= 0x7e
268}
269
270/// Unicode-aware character width
271pub fn wcwidth(c: char) -> i32 {
272    unicode_width::UnicodeWidthChar::width(c)
273        .map(|w| w as i32)
274        .unwrap_or(if c.is_control() { -1 } else { 1 })
275}
276
277/// Check if a wide character is printable
278pub fn iswprint(c: char) -> bool {
279    !c.is_control() && wcwidth(c) >= 0
280}
281
282/// String width accounting for unicode
283pub fn strwidth(s: &str) -> usize {
284    unicode_width::UnicodeWidthStr::width(s)
285}
286
287/// Metafy a string (encode special characters)
288pub fn metafy(s: &str) -> String {
289    let mut result = String::with_capacity(s.len() * 2);
290    for c in s.chars() {
291        let b = c as u32;
292        if b < 32 || (b >= 0x83 && b <= 0x9b) {
293            result.push('\u{83}'); // Meta marker
294            result.push(char::from_u32(b ^ 32).unwrap_or(c));
295        } else {
296            result.push(c);
297        }
298    }
299    result
300}
301
302/// Unmetafy a string (decode special characters)
303pub fn unmetafy(s: &str) -> String {
304    let mut result = String::with_capacity(s.len());
305    let mut chars = s.chars().peekable();
306
307    while let Some(c) = chars.next() {
308        if c == '\u{83}' {
309            if let Some(&next) = chars.peek() {
310                chars.next();
311                let b = next as u32;
312                result.push(char::from_u32(b ^ 32).unwrap_or(next));
313            } else {
314                result.push(c);
315            }
316        } else {
317            result.push(c);
318        }
319    }
320    result
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_timespec() {
329        let t1 = TimeSpec::new(10, 500_000_000);
330        let t2 = TimeSpec::new(12, 200_000_000);
331        let diff = t2 - t1;
332        assert_eq!(diff.tv_sec, 1);
333        assert_eq!(diff.tv_nsec, 700_000_000);
334    }
335
336    #[test]
337    fn test_timespec_negative() {
338        let t1 = TimeSpec::new(10, 800_000_000);
339        let t2 = TimeSpec::new(12, 200_000_000);
340        let diff = t2 - t1;
341        assert_eq!(diff.tv_sec, 1);
342        assert_eq!(diff.tv_nsec, 400_000_000);
343    }
344
345    #[test]
346    fn test_zgettime() {
347        let t = zgettime();
348        assert!(t.tv_sec > 0);
349    }
350
351    #[test]
352    fn test_zgettime_monotonic() {
353        let t1 = zgettime_monotonic();
354        std::thread::sleep(std::time::Duration::from_millis(10));
355        let t2 = zgettime_monotonic();
356        let diff = t2 - t1;
357        assert!(diff.tv_sec > 0 || diff.tv_nsec > 0);
358    }
359
360    #[test]
361    fn test_convbase() {
362        assert_eq!(convbase(255, 16), "ff");
363        assert_eq!(convbase(8, 2), "1000");
364        assert_eq!(convbase(-10, 10), "-10");
365        assert_eq!(convbase(0, 16), "0");
366    }
367
368    #[test]
369    fn test_convbaseu() {
370        assert_eq!(convbaseu(255, 16), "ff");
371        assert_eq!(convbaseu(8, 8), "10");
372    }
373
374    #[test]
375    fn test_zgetcwd() {
376        let cwd = zgetcwd();
377        assert!(cwd.is_some());
378        assert!(!cwd.unwrap().is_empty());
379    }
380
381    #[test]
382    fn test_zopenmax() {
383        let max = zopenmax();
384        assert!(max > 0);
385    }
386
387    #[test]
388    fn test_gethostname() {
389        let host = gethostname();
390        assert!(host.is_some());
391    }
392
393    #[test]
394    fn test_isprint_safe() {
395        assert!(isprint_safe('a'));
396        assert!(isprint_safe('Z'));
397        assert!(isprint_safe(' '));
398        assert!(!isprint_safe('\x00'));
399        assert!(!isprint_safe('\x1f'));
400    }
401
402    #[test]
403    fn test_wcwidth() {
404        assert_eq!(wcwidth('a'), 1);
405        assert_eq!(wcwidth('中'), 2);
406        assert!(wcwidth('\x00') <= 0);
407    }
408
409    #[test]
410    fn test_strwidth() {
411        assert_eq!(strwidth("hello"), 5);
412        assert_eq!(strwidth("中文"), 4);
413    }
414
415    #[test]
416    fn test_metafy_unmetafy() {
417        let original = "hello\x00world";
418        let meta = metafy(original);
419        let unmeta = unmetafy(&meta);
420        assert_eq!(unmeta, original);
421    }
422}