Skip to main content

zsh/
system.rs

1//! System I/O builtins - port of Modules/system.c
2//!
3//! Provides sysread, syswrite, sysopen, sysseek, syserror, zsystem builtins.
4
5use std::collections::HashMap;
6use std::io::{self, Read, Write};
7use std::time::{Duration, Instant};
8
9const SYSREAD_BUFSIZE: usize = 8192;
10
11/// Return values for sysread
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum SysreadResult {
14    Success = 0,
15    ParamError = 1,
16    ReadError = 2,
17    WriteError = 3,
18    Timeout = 4,
19    Eof = 5,
20}
21
22/// Options for sysread
23#[derive(Debug, Default)]
24pub struct SysreadOptions {
25    pub input_fd: Option<i32>,
26    pub output_fd: Option<i32>,
27    pub bufsize: Option<usize>,
28    pub timeout: Option<f64>,
29    pub count_var: Option<String>,
30    pub output_var: Option<String>,
31}
32
33/// Perform a system read
34pub fn sysread(options: &SysreadOptions) -> (SysreadResult, Option<Vec<u8>>, usize) {
35    let input_fd = options.input_fd.unwrap_or(0);
36    let bufsize = options.bufsize.unwrap_or(SYSREAD_BUFSIZE);
37
38    let mut buffer = vec![0u8; bufsize];
39
40    #[cfg(unix)]
41    {
42        if let Some(timeout_secs) = options.timeout {
43            if !wait_for_read(input_fd, timeout_secs) {
44                return (SysreadResult::Timeout, None, 0);
45            }
46        }
47
48        let count =
49            unsafe { libc::read(input_fd, buffer.as_mut_ptr() as *mut libc::c_void, bufsize) };
50
51        if count < 0 {
52            return (SysreadResult::ReadError, None, 0);
53        }
54
55        let count = count as usize;
56        buffer.truncate(count);
57
58        if let Some(output_fd) = options.output_fd {
59            if count == 0 {
60                return (SysreadResult::Eof, None, 0);
61            }
62
63            let mut written = 0;
64            while written < count {
65                let ret = unsafe {
66                    libc::write(
67                        output_fd,
68                        buffer[written..].as_ptr() as *const libc::c_void,
69                        count - written,
70                    )
71                };
72                if ret < 0 {
73                    return (
74                        SysreadResult::WriteError,
75                        Some(buffer[written..].to_vec()),
76                        written,
77                    );
78                }
79                written += ret as usize;
80            }
81            return (SysreadResult::Success, None, count);
82        }
83
84        if count == 0 {
85            (SysreadResult::Eof, Some(buffer), 0)
86        } else {
87            (SysreadResult::Success, Some(buffer), count)
88        }
89    }
90
91    #[cfg(not(unix))]
92    {
93        (SysreadResult::ParamError, None, 0)
94    }
95}
96
97#[cfg(unix)]
98fn wait_for_read(fd: i32, timeout_secs: f64) -> bool {
99    let timeout_ms = (timeout_secs * 1000.0) as i32;
100
101    unsafe {
102        let mut pfd = libc::pollfd {
103            fd,
104            events: libc::POLLIN,
105            revents: 0,
106        };
107
108        let ret = libc::poll(&mut pfd, 1, timeout_ms);
109        ret > 0
110    }
111}
112
113/// Options for syswrite
114#[derive(Debug, Default)]
115pub struct SyswriteOptions {
116    pub output_fd: Option<i32>,
117    pub count_var: Option<String>,
118}
119
120/// Perform a system write
121pub fn syswrite(data: &[u8], options: &SyswriteOptions) -> (i32, usize) {
122    let output_fd = options.output_fd.unwrap_or(1);
123
124    #[cfg(unix)]
125    {
126        let mut written = 0;
127        let mut remaining = data;
128
129        while !remaining.is_empty() {
130            let ret = unsafe {
131                libc::write(
132                    output_fd,
133                    remaining.as_ptr() as *const libc::c_void,
134                    remaining.len(),
135                )
136            };
137
138            if ret < 0 {
139                return (2, written);
140            }
141
142            let count = ret as usize;
143            written += count;
144            remaining = &remaining[count..];
145        }
146
147        (0, written)
148    }
149
150    #[cfg(not(unix))]
151    {
152        (1, 0)
153    }
154}
155
156/// Open options for sysopen
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum OpenOpt {
159    Cloexec,
160    Nofollow,
161    Sync,
162    Noatime,
163    Nonblock,
164    Excl,
165    Creat,
166    Truncate,
167}
168
169impl OpenOpt {
170    pub fn from_name(name: &str) -> Option<Self> {
171        let name = name.strip_prefix("O_").unwrap_or(name);
172        let name_lower = name.to_lowercase();
173        match name_lower.as_str() {
174            "cloexec" => Some(Self::Cloexec),
175            "nofollow" => Some(Self::Nofollow),
176            "sync" => Some(Self::Sync),
177            "noatime" => Some(Self::Noatime),
178            "nonblock" => Some(Self::Nonblock),
179            "excl" => Some(Self::Excl),
180            "creat" | "create" => Some(Self::Creat),
181            "truncate" | "trunc" => Some(Self::Truncate),
182            _ => None,
183        }
184    }
185
186    #[cfg(unix)]
187    pub fn to_flags(&self) -> i32 {
188        match self {
189            Self::Cloexec => libc::O_CLOEXEC,
190            Self::Nofollow => libc::O_NOFOLLOW,
191            Self::Sync => libc::O_SYNC,
192            Self::Noatime => 0, // Not all systems support O_NOATIME
193            Self::Nonblock => libc::O_NONBLOCK,
194            Self::Excl => libc::O_EXCL | libc::O_CREAT,
195            Self::Creat => libc::O_CREAT,
196            Self::Truncate => libc::O_TRUNC,
197        }
198    }
199}
200
201/// Options for sysopen
202#[derive(Debug, Default)]
203pub struct SysopenOptions {
204    pub read: bool,
205    pub write: bool,
206    pub append: bool,
207    pub options: Vec<OpenOpt>,
208    pub mode: Option<u32>,
209    pub fd_var: Option<String>,
210    pub explicit_fd: Option<i32>,
211}
212
213/// Open a file with system call
214pub fn sysopen(path: &str, options: &SysopenOptions) -> Result<i32, String> {
215    #[cfg(unix)]
216    {
217        use std::ffi::CString;
218
219        let mut flags = libc::O_NOCTTY;
220
221        if options.append {
222            flags |= libc::O_APPEND;
223        }
224
225        if options.append || options.write {
226            if options.read {
227                flags |= libc::O_RDWR;
228            } else {
229                flags |= libc::O_WRONLY;
230            }
231        } else {
232            flags |= libc::O_RDONLY;
233        }
234
235        for opt in &options.options {
236            flags |= opt.to_flags();
237        }
238
239        let mode = options.mode.unwrap_or(0o666);
240        let path_c = CString::new(path).map_err(|e| e.to_string())?;
241
242        let fd = unsafe {
243            if flags & libc::O_CREAT != 0 {
244                libc::open(path_c.as_ptr(), flags, mode)
245            } else {
246                libc::open(path_c.as_ptr(), flags)
247            }
248        };
249
250        if fd < 0 {
251            return Err(format!(
252                "can't open file {}: {}",
253                path,
254                io::Error::last_os_error()
255            ));
256        }
257
258        if let Some(explicit) = options.explicit_fd {
259            let new_fd = unsafe { libc::dup2(fd, explicit) };
260            unsafe {
261                libc::close(fd);
262            }
263            if new_fd < 0 {
264                return Err(format!("can't dup fd to {}", explicit));
265            }
266            Ok(new_fd)
267        } else {
268            Ok(fd)
269        }
270    }
271
272    #[cfg(not(unix))]
273    {
274        Err("sysopen not supported on this platform".to_string())
275    }
276}
277
278/// Seek whence options
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
280pub enum SeekWhence {
281    #[default]
282    Start,
283    Current,
284    End,
285}
286
287impl SeekWhence {
288    pub fn from_str(s: &str) -> Option<Self> {
289        match s.to_lowercase().as_str() {
290            "start" | "0" => Some(Self::Start),
291            "current" | "1" => Some(Self::Current),
292            "end" | "2" => Some(Self::End),
293            _ => None,
294        }
295    }
296
297    #[cfg(unix)]
298    pub fn to_libc(&self) -> i32 {
299        match self {
300            Self::Start => libc::SEEK_SET,
301            Self::Current => libc::SEEK_CUR,
302            Self::End => libc::SEEK_END,
303        }
304    }
305}
306
307/// Options for sysseek
308#[derive(Debug, Default)]
309pub struct SysseekOptions {
310    pub fd: Option<i32>,
311    pub whence: SeekWhence,
312}
313
314/// Seek on a file descriptor
315pub fn sysseek(offset: i64, options: &SysseekOptions) -> Result<i64, String> {
316    let fd = options.fd.unwrap_or(0);
317
318    #[cfg(unix)]
319    {
320        let result = unsafe { libc::lseek(fd, offset, options.whence.to_libc()) };
321        if result < 0 {
322            Err(io::Error::last_os_error().to_string())
323        } else {
324            Ok(result)
325        }
326    }
327
328    #[cfg(not(unix))]
329    {
330        Err("sysseek not supported on this platform".to_string())
331    }
332}
333
334/// Get current position in file descriptor
335pub fn systell(fd: i32) -> Result<i64, String> {
336    #[cfg(unix)]
337    {
338        let result = unsafe { libc::lseek(fd, 0, libc::SEEK_CUR) };
339        if result < 0 {
340            Err(io::Error::last_os_error().to_string())
341        } else {
342            Ok(result)
343        }
344    }
345
346    #[cfg(not(unix))]
347    {
348        Err("systell not supported on this platform".to_string())
349    }
350}
351
352/// Well-known errno names
353pub const ERRNO_NAMES: &[(&str, i32)] = &[
354    ("EPERM", 1),
355    ("ENOENT", 2),
356    ("ESRCH", 3),
357    ("EINTR", 4),
358    ("EIO", 5),
359    ("ENXIO", 6),
360    ("E2BIG", 7),
361    ("ENOEXEC", 8),
362    ("EBADF", 9),
363    ("ECHILD", 10),
364    ("EAGAIN", 11),
365    ("ENOMEM", 12),
366    ("EACCES", 13),
367    ("EFAULT", 14),
368    ("ENOTBLK", 15),
369    ("EBUSY", 16),
370    ("EEXIST", 17),
371    ("EXDEV", 18),
372    ("ENODEV", 19),
373    ("ENOTDIR", 20),
374    ("EISDIR", 21),
375    ("EINVAL", 22),
376    ("ENFILE", 23),
377    ("EMFILE", 24),
378    ("ENOTTY", 25),
379    ("ETXTBSY", 26),
380    ("EFBIG", 27),
381    ("ENOSPC", 28),
382    ("ESPIPE", 29),
383    ("EROFS", 30),
384    ("EMLINK", 31),
385    ("EPIPE", 32),
386    ("EDOM", 33),
387    ("ERANGE", 34),
388];
389
390/// Get error number from name
391pub fn errno_from_name(name: &str) -> Option<i32> {
392    ERRNO_NAMES
393        .iter()
394        .find(|(n, _)| *n == name)
395        .map(|(_, e)| *e)
396}
397
398/// Get error name from number
399pub fn errno_to_name(errno: i32) -> Option<&'static str> {
400    ERRNO_NAMES
401        .iter()
402        .find(|(_, e)| *e == errno)
403        .map(|(n, _)| *n)
404}
405
406/// Get error message for errno
407pub fn syserror(errno: i32, prefix: &str) -> String {
408    let msg = io::Error::from_raw_os_error(errno).to_string();
409    format!("{}{}", prefix, msg)
410}
411
412/// Options for zsystem flock
413#[derive(Debug, Default)]
414pub struct FlockOptions {
415    pub cloexec: bool,
416    pub read_lock: bool,
417    pub timeout: Option<f64>,
418    pub interval: Option<f64>,
419    pub fd_var: Option<String>,
420}
421
422/// Lock a file
423#[cfg(unix)]
424pub fn flock(path: &str, options: &FlockOptions) -> Result<i32, String> {
425    use std::ffi::CString;
426
427    let flags = if options.read_lock {
428        libc::O_RDONLY | libc::O_NOCTTY
429    } else {
430        libc::O_RDWR | libc::O_NOCTTY
431    };
432
433    let path_c = CString::new(path).map_err(|e| e.to_string())?;
434    let fd = unsafe { libc::open(path_c.as_ptr(), flags) };
435
436    if fd < 0 {
437        return Err(format!(
438            "failed to open {}: {}",
439            path,
440            io::Error::last_os_error()
441        ));
442    }
443
444    if options.cloexec {
445        unsafe {
446            let flags = libc::fcntl(fd, libc::F_GETFD);
447            if flags >= 0 {
448                libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
449            }
450        }
451    }
452
453    let lock_type = if options.read_lock {
454        libc::F_RDLCK
455    } else {
456        libc::F_WRLCK
457    };
458
459    let lck = libc::flock {
460        l_type: lock_type as i16,
461        l_whence: libc::SEEK_SET as i16,
462        l_start: 0,
463        l_len: 0,
464        l_pid: 0,
465    };
466
467    if let Some(timeout) = options.timeout {
468        if timeout > 0.0 {
469            let start = Instant::now();
470            let timeout_duration = Duration::from_secs_f64(timeout);
471            let interval = Duration::from_secs_f64(options.interval.unwrap_or(1.0));
472
473            loop {
474                let result = unsafe { libc::fcntl(fd, libc::F_SETLK, &lck) };
475                if result >= 0 {
476                    return Ok(fd);
477                }
478
479                let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
480                if errno != libc::EINTR && errno != libc::EACCES && errno != libc::EAGAIN {
481                    unsafe {
482                        libc::close(fd);
483                    }
484                    return Err(format!(
485                        "failed to lock {}: {}",
486                        path,
487                        io::Error::last_os_error()
488                    ));
489                }
490
491                if start.elapsed() >= timeout_duration {
492                    unsafe {
493                        libc::close(fd);
494                    }
495                    return Err("timeout waiting for lock".to_string());
496                }
497
498                std::thread::sleep(interval.min(timeout_duration - start.elapsed()));
499            }
500        }
501    }
502
503    let cmd = if options.timeout.map_or(true, |t| t != 0.0) {
504        libc::F_SETLKW
505    } else {
506        libc::F_SETLK
507    };
508
509    loop {
510        let result = unsafe { libc::fcntl(fd, cmd, &lck) };
511        if result >= 0 {
512            return Ok(fd);
513        }
514
515        let errno = io::Error::last_os_error().raw_os_error().unwrap_or(0);
516        if errno == libc::EINTR {
517            continue;
518        }
519
520        unsafe {
521            libc::close(fd);
522        }
523        return Err(format!(
524            "failed to lock {}: {}",
525            path,
526            io::Error::last_os_error()
527        ));
528    }
529}
530
531/// Unlock a file descriptor
532#[cfg(unix)]
533pub fn funlock(fd: i32) -> Result<(), String> {
534    let lck = libc::flock {
535        l_type: libc::F_UNLCK as i16,
536        l_whence: libc::SEEK_SET as i16,
537        l_start: 0,
538        l_len: 0,
539        l_pid: 0,
540    };
541
542    let result = unsafe { libc::fcntl(fd, libc::F_SETLK, &lck) };
543    if result < 0 {
544        Err(io::Error::last_os_error().to_string())
545    } else {
546        unsafe {
547            libc::close(fd);
548        }
549        Ok(())
550    }
551}
552
553/// Check if a zsystem feature is supported
554pub fn zsystem_supports(feature: &str) -> bool {
555    match feature {
556        "supports" => true,
557        "flock" => cfg!(unix),
558        _ => false,
559    }
560}
561
562/// System parameters
563pub fn get_sysparams() -> HashMap<String, String> {
564    let mut params = HashMap::new();
565
566    #[cfg(unix)]
567    {
568        params.insert("pid".to_string(), unsafe { libc::getpid() }.to_string());
569        params.insert("ppid".to_string(), unsafe { libc::getppid() }.to_string());
570    }
571
572    params
573}
574
575/// Get list of errno names
576pub fn get_errnos() -> Vec<&'static str> {
577    ERRNO_NAMES.iter().map(|(n, _)| *n).collect()
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583    use std::fs::File;
584    use std::io::Write;
585    use tempfile::TempDir;
586
587    #[test]
588    fn test_open_opt_from_name() {
589        assert_eq!(OpenOpt::from_name("cloexec"), Some(OpenOpt::Cloexec));
590        assert_eq!(OpenOpt::from_name("O_CREAT"), Some(OpenOpt::Creat));
591        assert_eq!(OpenOpt::from_name("truncate"), Some(OpenOpt::Truncate));
592        assert_eq!(OpenOpt::from_name("trunc"), Some(OpenOpt::Truncate));
593        assert_eq!(OpenOpt::from_name("invalid"), None);
594    }
595
596    #[test]
597    fn test_seek_whence_from_str() {
598        assert_eq!(SeekWhence::from_str("start"), Some(SeekWhence::Start));
599        assert_eq!(SeekWhence::from_str("0"), Some(SeekWhence::Start));
600        assert_eq!(SeekWhence::from_str("current"), Some(SeekWhence::Current));
601        assert_eq!(SeekWhence::from_str("1"), Some(SeekWhence::Current));
602        assert_eq!(SeekWhence::from_str("end"), Some(SeekWhence::End));
603        assert_eq!(SeekWhence::from_str("2"), Some(SeekWhence::End));
604        assert_eq!(SeekWhence::from_str("invalid"), None);
605    }
606
607    #[test]
608    fn test_errno_from_name() {
609        assert_eq!(errno_from_name("EPERM"), Some(1));
610        assert_eq!(errno_from_name("ENOENT"), Some(2));
611        assert_eq!(errno_from_name("EINVAL"), Some(22));
612        assert_eq!(errno_from_name("INVALID"), None);
613    }
614
615    #[test]
616    fn test_errno_to_name() {
617        assert_eq!(errno_to_name(1), Some("EPERM"));
618        assert_eq!(errno_to_name(2), Some("ENOENT"));
619        assert_eq!(errno_to_name(22), Some("EINVAL"));
620        assert_eq!(errno_to_name(999), None);
621    }
622
623    #[test]
624    fn test_syserror() {
625        let msg = syserror(2, "prefix: ");
626        assert!(msg.starts_with("prefix: "));
627    }
628
629    #[test]
630    fn test_zsystem_supports() {
631        assert!(zsystem_supports("supports"));
632        assert!(!zsystem_supports("unknown"));
633        #[cfg(unix)]
634        assert!(zsystem_supports("flock"));
635    }
636
637    #[test]
638    fn test_get_sysparams() {
639        let params = get_sysparams();
640        assert!(params.contains_key("pid"));
641        assert!(params.contains_key("ppid"));
642    }
643
644    #[test]
645    fn test_get_errnos() {
646        let errnos = get_errnos();
647        assert!(errnos.contains(&"EPERM"));
648        assert!(errnos.contains(&"ENOENT"));
649        assert!(errnos.contains(&"EINVAL"));
650    }
651
652    #[test]
653    #[cfg(unix)]
654    fn test_sysopen_and_close() {
655        let dir = TempDir::new().unwrap();
656        let file_path = dir.path().join("test.txt");
657
658        let options = SysopenOptions {
659            write: true,
660            options: vec![OpenOpt::Creat],
661            mode: Some(0o644),
662            ..Default::default()
663        };
664
665        let fd = sysopen(file_path.to_str().unwrap(), &options).unwrap();
666        assert!(fd >= 0);
667
668        unsafe {
669            libc::close(fd);
670        }
671    }
672
673    #[test]
674    #[cfg(unix)]
675    fn test_syswrite_sysread() {
676        let dir = TempDir::new().unwrap();
677        let file_path = dir.path().join("test.txt");
678
679        {
680            let mut f = File::create(&file_path).unwrap();
681            f.write_all(b"hello world").unwrap();
682        }
683
684        let fd = {
685            use std::ffi::CString;
686            let path_c = CString::new(file_path.to_str().unwrap()).unwrap();
687            unsafe { libc::open(path_c.as_ptr(), libc::O_RDONLY) }
688        };
689
690        let options = SysreadOptions {
691            input_fd: Some(fd),
692            bufsize: Some(100),
693            ..Default::default()
694        };
695
696        let (result, data, count) = sysread(&options);
697        unsafe {
698            libc::close(fd);
699        }
700
701        assert_eq!(result, SysreadResult::Success);
702        assert_eq!(count, 11);
703        assert_eq!(data.unwrap(), b"hello world");
704    }
705
706    #[test]
707    #[cfg(unix)]
708    fn test_sysseek_systell() {
709        let dir = TempDir::new().unwrap();
710        let file_path = dir.path().join("test.txt");
711
712        {
713            let mut f = File::create(&file_path).unwrap();
714            f.write_all(b"hello world").unwrap();
715        }
716
717        let fd = {
718            use std::ffi::CString;
719            let path_c = CString::new(file_path.to_str().unwrap()).unwrap();
720            unsafe { libc::open(path_c.as_ptr(), libc::O_RDONLY) }
721        };
722
723        let options = SysseekOptions {
724            fd: Some(fd),
725            whence: SeekWhence::Start,
726        };
727
728        let pos = sysseek(5, &options).unwrap();
729        assert_eq!(pos, 5);
730
731        let current = systell(fd).unwrap();
732        assert_eq!(current, 5);
733
734        unsafe {
735            libc::close(fd);
736        }
737    }
738}