1use std::collections::HashMap;
6use std::io::{self, Read, Write};
7use std::time::{Duration, Instant};
8
9const SYSREAD_BUFSIZE: usize = 8192;
10
11#[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#[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
33pub 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#[derive(Debug, Default)]
115pub struct SyswriteOptions {
116 pub output_fd: Option<i32>,
117 pub count_var: Option<String>,
118}
119
120pub 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#[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, 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#[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
213pub 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#[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#[derive(Debug, Default)]
309pub struct SysseekOptions {
310 pub fd: Option<i32>,
311 pub whence: SeekWhence,
312}
313
314pub 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
334pub 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
352pub 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
390pub fn errno_from_name(name: &str) -> Option<i32> {
392 ERRNO_NAMES
393 .iter()
394 .find(|(n, _)| *n == name)
395 .map(|(_, e)| *e)
396}
397
398pub 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
406pub 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#[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#[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#[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
553pub fn zsystem_supports(feature: &str) -> bool {
555 match feature {
556 "supports" => true,
557 "flock" => cfg!(unix),
558 _ => false,
559 }
560}
561
562pub 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
575pub 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}