1use nanvm_lib::vm::unstable::number_text;
8use std::{
9 fs::{self, File, OpenOptions},
10 io::{self, ErrorKind, Read, Seek, SeekFrom, Write},
11 path::Path,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct IoError {
18 pub code: Option<String>,
19 pub message: String,
20}
21
22impl IoError {
23 pub fn refusal(code: &str, message: impl Into<String>) -> IoError {
26 IoError {
27 code: Some(code.to_string()),
28 message: message.into(),
29 }
30 }
31
32 pub fn message(message: impl Into<String>) -> IoError {
34 IoError {
35 code: None,
36 message: message.into(),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Dirent {
44 pub name: String,
45 pub parent_path: String,
46 pub is_file: bool,
47 pub is_directory: bool,
48}
49
50pub const MAX_FILE_SIZE_BYTES: u64 = 1 << 17;
54
55const MAX_OFFSET: u64 = (1 << 53) - 1;
57
58fn code(kind: ErrorKind) -> Option<&'static str> {
61 match kind {
62 ErrorKind::NotFound => Some("ENOENT"),
63 ErrorKind::PermissionDenied => Some("EACCES"),
64 ErrorKind::AlreadyExists => Some("EEXIST"),
65 ErrorKind::NotADirectory => Some("ENOTDIR"),
66 ErrorKind::IsADirectory => Some("EISDIR"),
67 ErrorKind::DirectoryNotEmpty => Some("ENOTEMPTY"),
68 ErrorKind::ReadOnlyFilesystem => Some("EROFS"),
69 ErrorKind::StorageFull => Some("ENOSPC"),
70 _ => None,
71 }
72}
73
74fn raw_code(raw: i32) -> Option<&'static str> {
79 #[cfg(all(
81 target_os = "linux",
82 any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
83 ))]
84 let codes: &[(i32, &str)] = &[
85 (1, "EPERM"),
86 (5, "EIO"),
87 (23, "ENFILE"),
88 (24, "EMFILE"),
89 (28, "ENOSPC"),
90 (30, "EROFS"),
91 (40, "ELOOP"),
92 ];
93 #[cfg(target_os = "macos")]
95 let codes: &[(i32, &str)] = &[
96 (1, "EPERM"),
97 (5, "EIO"),
98 (23, "ENFILE"),
99 (24, "EMFILE"),
100 (28, "ENOSPC"),
101 (30, "EROFS"),
102 (62, "ELOOP"),
103 ];
104 #[cfg(target_os = "windows")]
106 let codes: &[(i32, &str)] = &[
107 (4, "EMFILE"),
108 (5, "EPERM"),
109 (19, "EROFS"),
110 (39, "ENOSPC"),
111 (112, "ENOSPC"),
112 (1_117, "EIO"),
113 (1_314, "EPERM"),
114 (1_921, "ELOOP"),
115 ];
116 #[cfg(not(any(
117 all(
118 target_os = "linux",
119 any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
120 ),
121 target_os = "macos",
122 target_os = "windows"
123 )))]
124 let codes: &[(i32, &str)] = &[];
125 codes
126 .iter()
127 .find_map(|&(number, name)| (raw == number).then_some(name))
128}
129
130pub(crate) fn failure(error: &io::Error, call: &str, path: &str) -> IoError {
133 IoError {
134 code: error
135 .raw_os_error()
136 .and_then(raw_code)
137 .or_else(|| code(error.kind()))
138 .map(str::to_string),
139 message: format!("{error}, {call} '{path}'"),
140 }
141}
142
143pub fn normalize(path: &str) -> String {
149 let path = path.replace('\\', "/");
150 let bytes = path.as_bytes();
151 let (root, rest) = if bytes.len() >= 3
152 && bytes[0].is_ascii_alphabetic()
153 && bytes[1] == b':'
154 && bytes[2] == b'/'
155 {
156 (&path[..3], &path[3..])
157 } else if path.starts_with("//") && !path.starts_with("///") {
158 ("//", &path[2..])
159 } else if let Some(rest) = path.strip_prefix('/') {
160 ("/", rest)
161 } else {
162 ("", &path[..])
163 };
164 let mut segments: Vec<&str> = Vec::new();
165 for segment in rest.split('/') {
166 match segment {
167 "" | "." => {}
168 ".." => match segments.last() {
169 Some(&last) if last != ".." => {
170 segments.pop();
171 }
172 _ if root.is_empty() => segments.push(".."),
173 _ => {}
174 },
175 _ => segments.push(segment),
176 }
177 }
178 format!("{root}{}", segments.join("/"))
179}
180
181fn entries(directory: &str, parent: &str) -> io::Result<Vec<Dirent>> {
184 let mut found = Vec::new();
185 for entry in fs::read_dir(directory)? {
186 let entry = entry?;
187 let kind = entry.file_type()?;
188 found.push(Dirent {
189 name: entry.file_name().to_string_lossy().into_owned(),
190 parent_path: parent.to_string(),
191 is_file: kind.is_file(),
192 is_directory: kind.is_dir(),
193 });
194 }
195 found.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
196 Ok(found)
197}
198
199fn tree(path: &str) -> io::Result<Vec<Dirent>> {
203 let mut found = Vec::new();
204 let mut queue = [path.to_string()].to_vec();
205 let mut next = 0;
206 while next < queue.len() {
207 let directory = queue[next].clone();
208 next += 1;
209 for entry in entries(&directory, &normalize(&directory))? {
210 if entry.is_directory {
213 queue.push(
214 Path::new(&directory)
215 .join(&entry.name)
216 .to_string_lossy()
217 .into_owned(),
218 );
219 }
220 found.push(entry);
221 }
222 }
223 Ok(found)
224}
225
226fn offset(value: f64) -> Option<u64> {
228 ((0.0..=MAX_OFFSET as f64).contains(&value) && value.fract() == 0.0).then_some(value as u64)
229}
230
231pub fn mkdir(path: &str, recursive: bool) -> Result<(), IoError> {
235 if path.is_empty() {
238 return Err(failure(&ErrorKind::NotFound.into(), "mkdir", path));
239 }
240 if recursive {
241 fs::create_dir_all(path)
242 } else {
243 fs::create_dir(path)
244 }
245 .map_err(|e| failure(&e, "mkdir", path))
246}
247
248pub fn read_file(path: &str) -> Result<Vec<u8>, IoError> {
252 let size = fs::metadata(path)
253 .map_err(|e| failure(&e, "stat", path))?
254 .len();
255 if size > MAX_FILE_SIZE_BYTES {
256 return Err(IoError::message(format!(
257 "File size {size} exceeds maximum allowed size of {MAX_FILE_SIZE_BYTES} bytes: '{path}'"
258 )));
259 }
260 let file = File::open(path).map_err(|e| failure(&e, "open", path))?;
261 read_bounded(path, file)
262}
263
264fn read_bounded(path: &str, reader: impl Read) -> Result<Vec<u8>, IoError> {
267 let mut bytes = Vec::new();
268 reader
269 .take(MAX_FILE_SIZE_BYTES + 1)
270 .read_to_end(&mut bytes)
271 .map_err(|e| failure(&e, "read", path))?;
272 if bytes.len() as u64 > MAX_FILE_SIZE_BYTES {
273 return Err(IoError::message(format!(
274 "File exceeds maximum allowed size of {MAX_FILE_SIZE_BYTES} bytes: '{path}'"
275 )));
276 }
277 Ok(bytes)
278}
279
280pub fn read_whole(path: &str) -> Result<Vec<Vec<u8>>, IoError> {
285 let metadata = fs::metadata(path).map_err(|e| failure(&e, "stat", path))?;
286 if !metadata.is_file() {
287 return Err(IoError::refusal(
288 "ERR_NOT_A_FILE",
289 format!("{path} is not a regular file"),
290 ));
291 }
292 let file = File::open(path).map_err(|e| failure(&e, "open", path))?;
293 read_windows(path, file)
294}
295
296fn read_windows(path: &str, mut reader: impl Read) -> Result<Vec<Vec<u8>>, IoError> {
299 let mut bytes = Vec::new();
300 reader
301 .read_to_end(&mut bytes)
302 .map_err(|e| failure(&e, "read", path))?;
303 Ok(bytes
304 .chunks(MAX_FILE_SIZE_BYTES as usize)
305 .map(<[u8]>::to_vec)
306 .collect())
307}
308
309pub fn readdir(path: &str, recursive: bool) -> Result<Vec<Dirent>, IoError> {
313 if recursive {
314 tree(path)
315 } else {
316 entries(path, &normalize(path))
317 }
318 .map_err(|e| failure(&e, "scandir", path))
319}
320
321fn sync(file: &File) -> io::Result<()> {
332 match file.sync_all() {
333 Err(e) if nothing_to_flush(&e) => Ok(()),
334 result => result,
335 }
336}
337
338const ERROR_INVALID_HANDLE: i32 = 6;
340
341fn nothing_to_flush(e: &io::Error) -> bool {
342 matches!(e.kind(), ErrorKind::InvalidInput | ErrorKind::Unsupported)
343 || (cfg!(windows) && e.raw_os_error() == Some(ERROR_INVALID_HANDLE))
344}
345
346pub fn write_file(path: &str, data: &[u8]) -> Result<(), IoError> {
348 let mut file = File::create(path).map_err(|e| failure(&e, "open", path))?;
349 file.write_all(data)
350 .map_err(|e| failure(&e, "write", path))?;
351 sync(&file).map_err(|e| failure(&e, "close", path))
352}
353
354pub fn write_bytes(path: &str, at: f64, data: &[u8]) -> Result<(), IoError> {
359 let Some(at) = offset(at) else {
360 return Err(IoError::refusal(
361 "ERR_OUT_OF_RANGE",
362 format!("The value of \"position\" is out of range: {at}"),
363 ));
364 };
365 let mut file: File = OpenOptions::new()
366 .read(true)
367 .write(true)
368 .open(path)
369 .map_err(|e| failure(&e, "open", path))?;
370 if !data.is_empty() {
371 file.seek(SeekFrom::Start(at))
372 .and_then(|_| file.write_all(data))
373 .map_err(|e| failure(&e, "write", path))?;
374 }
375 sync(&file).map_err(|e| failure(&e, "close", path))
376}
377
378fn window_refusal(at: f64, size: f64) -> Option<String> {
381 if at.fract() != 0.0 || !at.is_finite() {
382 Some(format!("Offset {} is not an integer", number_text(at)))
383 } else if at < 0.0 {
384 Some(format!("Offset {} is negative", number_text(at)))
385 } else if at > MAX_OFFSET as f64 {
386 Some(format!(
387 "Offset {} exceeds maximum allowed offset of {MAX_OFFSET}",
388 number_text(at)
389 ))
390 } else if size.fract() != 0.0 || !size.is_finite() {
391 Some(format!(
392 "Chunk size {} is not an integer",
393 number_text(size)
394 ))
395 } else if size < 0.0 {
396 Some(format!("Chunk size {} is negative", number_text(size)))
397 } else if size > MAX_FILE_SIZE_BYTES as f64 {
398 Some(format!(
399 "Chunk size {} exceeds maximum allowed size of {MAX_FILE_SIZE_BYTES} bytes",
400 number_text(size)
401 ))
402 } else {
403 None
404 }
405}
406
407pub fn read_bytes(path: &str, at: f64, size: f64) -> Result<Vec<u8>, IoError> {
411 check_window(at, size)?;
412 let file = File::open(path).map_err(|e| failure(&e, "open", path))?;
413 read_window(&file, at, size, path)
414}
415
416fn read_window(mut file: &File, at: f64, size: f64, what: &str) -> Result<Vec<u8>, IoError> {
420 let mut bytes = Vec::new();
421 if size == 0.0 {
422 return Ok(bytes);
423 }
424 file.seek(SeekFrom::Start(at as u64))
425 .and_then(|_| file.take(size as u64).read_to_end(&mut bytes))
426 .map_err(|e| failure(&e, "read", what))?;
427 Ok(bytes)
428}
429
430pub fn bad_descriptor() -> IoError {
432 IoError::refusal("EBADF", "bad file descriptor")
433}
434
435pub fn open(path: &str) -> Result<File, IoError> {
440 #[cfg(unix)]
441 {
442 use std::os::unix::fs::FileTypeExt;
443 if fs::metadata(path).is_ok_and(|m| m.file_type().is_fifo()) {
444 return Err(IoError::refusal(
445 "ERR_NOT_A_FILE",
446 format!("{path} is a FIFO, which cannot be opened without blocking"),
447 ));
448 }
449 }
450 File::open(path).map_err(|e| failure(&e, "open", path))
451}
452
453pub fn fstat(file: &File) -> Result<Stat, IoError> {
455 let metadata = file
456 .metadata()
457 .map_err(|e| failure(&e, "fstat", "handle"))?;
458 Ok(metadata.into())
459}
460
461pub fn pread(file: &File, at: f64, size: f64) -> Result<Vec<u8>, IoError> {
465 read_window(file, at, size, "handle")
466}
467
468pub fn check_window(at: f64, size: f64) -> Result<(), IoError> {
470 match window_refusal(at, size) {
471 Some(message) => Err(IoError::message(message)),
472 None => Ok(()),
473 }
474}
475
476pub fn create_exclusive(path: &str) -> Result<(), IoError> {
481 let file = exclusive(path).map_err(|e| failure(&e, "open", path))?;
482 file.sync_all().map_err(|e| failure(&e, "close", path))
483}
484
485fn exclusive(path: &str) -> io::Result<File> {
486 OpenOptions::new().write(true).create_new(true).open(path)
487}
488
489pub fn write_exclusive(path: &str, data: &[Vec<u8>]) -> Result<(), IoError> {
498 write_exclusive_with(
499 path,
500 |file| data.iter().try_for_each(|d| file.write_all(d)),
501 File::sync_all,
502 )
503}
504
505fn write_exclusive_with(
506 path: &str,
507 write: impl FnOnce(&mut File) -> io::Result<()>,
508 finish: impl FnOnce(&File) -> io::Result<()>,
509) -> Result<(), IoError> {
510 let failed = {
513 let mut file = exclusive(path).map_err(|e| failure(&e, "open", path))?;
514 match write(&mut file) {
515 Ok(()) => return finish(&file).map_err(|e| failure(&e, "close", path)),
516 Err(e) => e,
517 }
518 };
519 match fs::remove_file(path) {
523 Err(e) if e.kind() != ErrorKind::NotFound => Err(failure(&e, "unlink", path)),
524 _ => Err(failure(&failed, "write", path)),
525 }
526}
527
528pub fn rm(path: &str) -> Result<(), IoError> {
533 let metadata = fs::symlink_metadata(path);
534 if metadata.as_ref().is_ok_and(|m| m.is_dir()) {
535 return Err(IoError::refusal(
536 "ERR_FS_EISDIR",
537 format!("Path is a directory: rm returned EISDIR (is a directory) {path}"),
538 ));
539 }
540 #[cfg(windows)]
541 {
542 use std::os::windows::fs::FileTypeExt;
543
544 if metadata.is_ok_and(|m| m.file_type().is_symlink_dir()) {
545 return fs::remove_dir(path).map_err(|e| failure(&e, "rm", path));
546 }
547 }
548 fs::remove_file(path).map_err(|e| failure(&e, "rm", path))
549}
550
551#[derive(Debug, Clone, PartialEq, Eq)]
555pub struct Stat {
556 pub size: u64,
557 pub is_file: bool,
558 pub is_directory: bool,
559}
560
561impl From<fs::Metadata> for Stat {
562 fn from(metadata: fs::Metadata) -> Stat {
563 Stat {
564 size: metadata.len(),
565 is_file: metadata.is_file(),
566 is_directory: metadata.is_dir(),
567 }
568 }
569}
570
571pub fn stat(path: &str) -> Result<Stat, IoError> {
572 let metadata = fs::metadata(path).map_err(|e| failure(&e, "stat", path))?;
573 Ok(metadata.into())
574}
575
576pub fn access(path: &str) -> Result<(), IoError> {
578 fs::metadata(path)
579 .map(|_| ())
580 .map_err(|e| failure(&e, "access", path))
581}
582
583pub fn rename(src: &str, dst: &str) -> Result<(), IoError> {
585 fs::rename(src, dst).map_err(|e| failure(&e, "rename", &format!("{src}' -> '{dst}")))
586}
587
588pub fn rmdir(path: &str) -> Result<(), IoError> {
595 if fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink()) {
596 return Err(failure(&ErrorKind::NotADirectory.into(), "rmdir", path));
597 }
598 fs::remove_dir(path).map_err(|e| failure(&e, "rmdir", path))
599}
600
601#[cfg(test)]
602mod test {
603 use super::*;
604
605 #[test]
607 fn normalizing() {
608 for (path, expected) in [
609 (".", ""),
610 ("", ""),
611 ("a/", "a"),
612 ("./a", "a"),
613 ("a//b", "a/b"),
614 ("/a/./b", "/a/b"),
615 ("/", "/"),
616 ("a/../b", "b"),
617 ("..", ".."),
618 ("../a", "../a"),
619 ("a/b/..", "a"),
620 ("/..", "/"),
621 ("/a/..", "/"),
622 ("//a", "//a"),
623 ("///a", "/a"),
624 ("//", "//"),
625 ("//a/../b", "//b"),
626 ("a\\b", "a/b"),
627 ("/a/../../b", "/b"),
628 ("a/../..", ".."),
629 ("../..", "../.."),
630 ("C:/", "C:/"),
631 ("C:", "C:"),
632 ("C:a", "C:a"),
633 ("C:a/b", "C:a/b"),
634 ("c:/x", "c:/x"),
635 ("C://a", "C:/a"),
636 ("C:/..", "C:/"),
637 ("C:/a/../..", "C:/"),
638 ("C:\\a\\..", "C:/"),
639 ("C:/a/./b/", "C:/a/b"),
640 ("1:/a", "1:/a"),
641 ("CC:/a", "CC:/a"),
642 ("//C:/a", "//C:/a"),
643 ("/C:/a", "/C:/a"),
644 ] {
645 assert_eq!(normalize(path), expected, "{path:?}");
646 }
647 }
648
649 #[test]
651 fn window_refusals() {
652 let said = |at, size| window_refusal(at, size);
653 assert_eq!(said(0.0, 0.0), None);
654 assert_eq!(said(9007199254740991.0, 131072.0), None);
655 assert_eq!(said(1.5, 1.0), Some("Offset 1.5 is not an integer".into()));
656 assert_eq!(
657 said(f64::NAN, 1.0),
658 Some("Offset NaN is not an integer".into())
659 );
660 assert_eq!(
661 said(f64::INFINITY, 1.0),
662 Some("Offset Infinity is not an integer".into())
663 );
664 assert_eq!(said(-1.0, 1.0), Some("Offset -1 is negative".into()));
665 assert_eq!(
666 said(9007199254740992.0, 1.0),
667 Some(
668 "Offset 9007199254740992 exceeds maximum allowed offset of 9007199254740991".into()
669 )
670 );
671 assert_eq!(
672 said(0.0, 0.5),
673 Some("Chunk size 0.5 is not an integer".into())
674 );
675 assert_eq!(said(0.0, -2.0), Some("Chunk size -2 is negative".into()));
676 assert_eq!(
677 said(0.0, 131073.0),
678 Some("Chunk size 131073 exceeds maximum allowed size of 131072 bytes".into())
679 );
680 }
681
682 #[test]
684 fn mkdir_of_an_empty_path() {
685 for recursive in [false, true] {
686 assert_eq!(code_of(mkdir("", recursive)), Some("ENOENT".into()));
687 }
688 }
689
690 #[test]
691 fn offsets() {
692 assert_eq!(offset(0.0), Some(0));
693 assert_eq!(offset(5.0), Some(5));
694 assert_eq!(offset(-0.0), Some(0));
695 assert_eq!(offset(MAX_OFFSET as f64 - 1.0), Some(MAX_OFFSET - 1));
696 assert_eq!(offset(MAX_OFFSET as f64), Some(MAX_OFFSET));
697 assert_eq!(offset(MAX_OFFSET as f64 + 1.0), None);
698 assert_eq!(offset(u64::MAX as f64), None);
699 assert_eq!(offset(-1.0), None);
700 assert_eq!(offset(0.5), None);
701 assert_eq!(offset(f64::NAN), None);
702 assert_eq!(offset(f64::INFINITY), None);
703 assert_eq!(offset(f64::NEG_INFINITY), None);
704 assert_eq!(offset(1e30), None);
705 }
706
707 #[test]
708 fn codes() {
709 assert_eq!(code(ErrorKind::NotFound), Some("ENOENT"));
710 assert_eq!(code(ErrorKind::PermissionDenied), Some("EACCES"));
711 assert_eq!(code(ErrorKind::AlreadyExists), Some("EEXIST"));
712 assert_eq!(code(ErrorKind::NotADirectory), Some("ENOTDIR"));
713 assert_eq!(code(ErrorKind::IsADirectory), Some("EISDIR"));
714 assert_eq!(code(ErrorKind::DirectoryNotEmpty), Some("ENOTEMPTY"));
715 assert_eq!(code(ErrorKind::ReadOnlyFilesystem), Some("EROFS"));
716 assert_eq!(code(ErrorKind::StorageFull), Some("ENOSPC"));
717 assert_eq!(code(ErrorKind::Other), None);
718 }
719
720 #[test]
721 fn failures_preserve_codes_and_context() {
722 for (kind, expected) in [
723 (ErrorKind::ReadOnlyFilesystem, Some("EROFS")),
724 (ErrorKind::StorageFull, Some("ENOSPC")),
725 (ErrorKind::Other, None),
726 ] {
727 let error = failure(&kind.into(), "write", "p");
728 assert_eq!(error.code.as_deref(), expected);
729 assert!(error.message.ends_with(", write 'p'"));
730 }
731 let unknown = io::Error::from_raw_os_error(i32::MAX);
732 assert_eq!(raw_code(i32::MAX), None);
733 assert_eq!(failure(&unknown, "open", "p").code, None);
734 }
735
736 #[cfg(any(
737 all(
738 target_os = "linux",
739 any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
740 ),
741 target_os = "macos"
742 ))]
743 #[test]
744 fn unix_failures_preserve_raw_codes() {
745 for (raw, expected) in [
746 (1, "EPERM"),
747 (5, "EIO"),
748 (23, "ENFILE"),
749 (24, "EMFILE"),
750 (28, "ENOSPC"),
751 (30, "EROFS"),
752 ] {
753 let error = io::Error::from_raw_os_error(raw);
754 assert_eq!(failure(&error, "open", "p").code.as_deref(), Some(expected));
755 }
756 let denied = io::Error::from_raw_os_error(13);
758 assert_eq!(
759 failure(&denied, "open", "p").code.as_deref(),
760 Some("EACCES")
761 );
762 }
763
764 #[cfg(target_os = "windows")]
765 #[test]
766 fn windows_failures_preserve_raw_codes() {
767 for (raw, expected) in [
768 (4, "EMFILE"),
769 (5, "EPERM"),
770 (19, "EROFS"),
771 (39, "ENOSPC"),
772 (112, "ENOSPC"),
773 (1_117, "EIO"),
774 (1_314, "EPERM"),
775 (1_921, "ELOOP"),
776 ] {
777 let error = io::Error::from_raw_os_error(raw);
778 assert_eq!(failure(&error, "open", "p").code.as_deref(), Some(expected));
779 }
780 }
781
782 fn code_of<T: std::fmt::Debug>(r: Result<T, IoError>) -> Option<String> {
783 r.unwrap_err().code
784 }
785
786 #[test]
787 fn whole_read_answers_windows() {
788 let max = MAX_FILE_SIZE_BYTES;
789 for (size, expected) in [
790 (0, vec![]),
791 (1, vec![1]),
792 (max, vec![max]),
793 (max + 1, vec![max, 1]),
794 (2 * max + 5, vec![max, max, 5]),
795 ] {
796 let windows = read_windows("stream", io::repeat(0xa5).take(size)).unwrap();
797 let lengths: Vec<u64> = windows.iter().map(|w| w.len() as u64).collect();
798 assert_eq!(lengths, expected, "{size}");
799 }
800 }
801
802 #[cfg(not(target_family = "wasm"))]
804 #[test]
805 fn whole_read_refuses_a_directory() {
806 let dir = std::env::temp_dir();
807 let path = dir.to_string_lossy();
808 let error = read_whole(&path).unwrap_err();
809 assert_eq!(error.code.as_deref(), Some("ERR_NOT_A_FILE"));
810 assert_eq!(error.message, format!("{path} is not a regular file"));
811 let missing = dir.join("nanvm-effects-node-no-such-file");
812 assert_eq!(
813 code_of(read_whole(&missing.to_string_lossy())),
814 Some("ENOENT".into())
815 );
816 }
817
818 #[test]
819 fn bounded_read_accepts_up_to_the_limit() {
820 for size in [0, 1, MAX_FILE_SIZE_BYTES - 1, MAX_FILE_SIZE_BYTES] {
821 let bytes = read_bounded("stream", io::repeat(0xa5).take(size)).unwrap();
822 assert_eq!(bytes.len() as u64, size);
823 assert!(bytes.iter().all(|&byte| byte == 0xa5));
824 }
825 }
826
827 #[test]
829 fn bounded_read_refuses_overflow() {
830 let mut reader = io::repeat(0).take(MAX_FILE_SIZE_BYTES + 8);
831 let error = read_bounded("stream", &mut reader).unwrap_err();
832 assert_eq!(reader.limit(), 7);
833 assert_eq!(error.code, None);
834 assert_eq!(
835 error.message,
836 "File exceeds maximum allowed size of 131072 bytes: 'stream'"
837 );
838 }
839
840 #[test]
841 fn bounded_read_reports_io_errors() {
842 struct Broken;
843 impl Read for Broken {
844 fn read(&mut self, _: &mut [u8]) -> io::Result<usize> {
845 Err(ErrorKind::PermissionDenied.into())
846 }
847 }
848 let error = read_bounded("unreadable", Broken).unwrap_err();
849 assert_eq!(error.code, Some("EACCES".into()));
850 assert!(error.message.ends_with(", read 'unreadable'"));
851 }
852
853 #[cfg(not(target_family = "wasm"))]
856 mod directory {
857 use super::*;
858 use crate::scratch::Scratch;
859
860 #[test]
861 fn stat_and_access() {
862 let dir = Scratch::new("files");
863 let file = dir.at("f");
864 write_file(&file, &[1, 2, 3]).unwrap();
865 assert_eq!(
866 stat(&file),
867 Ok(Stat {
868 size: 3,
869 is_file: true,
870 is_directory: false
871 })
872 );
873 assert_eq!(
874 stat(&dir.at("")),
875 Ok(Stat {
876 size: stat(&dir.at("")).unwrap().size,
877 is_file: false,
878 is_directory: true
879 })
880 );
881 assert_eq!(code_of(stat(&dir.at("none"))), Some("ENOENT".into()));
882 assert_eq!(access(&file), Ok(()));
883 assert_eq!(access(&dir.at("")), Ok(()));
884 assert_eq!(code_of(access(&dir.at("none"))), Some("ENOENT".into()));
885 }
886
887 #[test]
888 fn rename_replaces_a_file() {
889 let dir = Scratch::new("files");
890 write_file(&dir.at("a"), &[1]).unwrap();
891 write_file(&dir.at("b"), &[2]).unwrap();
892 assert_eq!(rename(&dir.at("a"), &dir.at("b")), Ok(()));
893 assert_eq!(read_file(&dir.at("b")), Ok([1].to_vec()));
894 assert_eq!(code_of(read_file(&dir.at("a"))), Some("ENOENT".into()));
895 let error = rename(&dir.at("a"), &dir.at("c")).unwrap_err();
896 assert_eq!(error.code, Some("ENOENT".into()));
897 assert!(error.message.contains(" -> "), "{}", error.message);
898 }
899
900 #[test]
901 fn rmdir_removes_an_empty_directory_only() {
902 let dir = Scratch::new("files");
903 mkdir(&dir.at("d"), false).unwrap();
904 write_file(&dir.at("d/f"), &[]).unwrap();
905 assert_eq!(code_of(rmdir(&dir.at("d"))), Some("ENOTEMPTY".into()));
906 rm(&dir.at("d/f")).unwrap();
907 assert_eq!(rmdir(&dir.at("d")), Ok(()));
908 assert_eq!(code_of(rmdir(&dir.at("d"))), Some("ENOENT".into()));
909 write_file(&dir.at("f"), &[]).unwrap();
910 assert_eq!(code_of(rmdir(&dir.at("f"))), Some("ENOTDIR".into()));
911 }
912
913 #[cfg(unix)]
915 #[test]
916 fn rmdir_refuses_a_link() {
917 let dir = Scratch::new("files");
918 mkdir(&dir.at("target"), false).unwrap();
919 std::os::unix::fs::symlink(dir.at("target"), dir.at("link")).unwrap();
920 assert_eq!(code_of(rmdir(&dir.at("link"))), Some("ENOTDIR".into()));
921 assert_eq!(access(&dir.at("link")), Ok(()));
922 assert_eq!(rmdir(&dir.at("target")), Ok(()));
923 }
924
925 #[test]
926 fn read_bytes_windows() {
927 let dir = Scratch::new("files");
928 let file = dir.at("f");
929 write_file(&file, &[1, 2, 3, 4, 5]).unwrap();
930 assert_eq!(read_bytes(&file, 1.0, 3.0), Ok([2, 3, 4].to_vec()));
931 assert_eq!(read_bytes(&file, 3.0, 10.0), Ok([4, 5].to_vec()));
932 assert_eq!(read_bytes(&file, 9.0, 4.0), Ok(Vec::new()));
933 assert_eq!(read_bytes(&file, 0.0, 0.0), Ok(Vec::new()));
934 assert_eq!(
935 code_of(read_bytes(&dir.at("none"), 0.0, 1.0)),
936 Some("ENOENT".into())
937 );
938 let error = read_bytes(&dir.at("none"), -1.0, 1.0).unwrap_err();
940 assert_eq!(error.code, None);
941 assert_eq!(error.message, "Offset -1 is negative");
942 }
943
944 #[cfg(target_os = "linux")]
946 #[test]
947 fn empty_read_does_not_seek_a_pipe() {
948 use std::os::fd::AsRawFd;
949
950 let (reader, _writer) = io::pipe().unwrap();
951 let path = format!("/proc/self/fd/{}", reader.as_raw_fd());
952 for at in [0.0, MAX_OFFSET as f64] {
953 assert_eq!(read_bytes(&path, at, 0.0), Ok(Vec::new()));
954 }
955 assert!(read_bytes(&path, 0.0, 1.0).is_err());
957 }
958
959 #[cfg(target_os = "linux")]
962 #[test]
963 fn a_fifo_is_not_opened() {
964 use std::os::fd::AsRawFd;
965
966 let (reader, _writer) = io::pipe().unwrap();
967 let path = format!("/proc/self/fd/{}", reader.as_raw_fd());
968 assert_eq!(code_of(open(&path)), Some("ERR_NOT_A_FILE".into()));
969 }
970
971 #[test]
972 fn exclusive_creation() {
973 let dir = Scratch::new("files");
974 let file = dir.at("f");
975 assert_eq!(create_exclusive(&file), Ok(()));
976 assert_eq!(read_file(&file), Ok(Vec::new()));
977 assert_eq!(code_of(create_exclusive(&file)), Some("EEXIST".into()));
978 assert_eq!(code_of(write_exclusive(&file, &[])), Some("EEXIST".into()));
979 assert_eq!(read_file(&file), Ok(Vec::new()));
980 }
981
982 #[test]
983 fn write_exclusive_writes_every_chunk() {
984 let dir = Scratch::new("files");
985 let file = dir.at("f");
986 assert_eq!(
987 write_exclusive(&file, &[[1, 2].to_vec(), Vec::new(), [3].to_vec()]),
988 Ok(())
989 );
990 assert_eq!(read_file(&file), Ok([1, 2, 3].to_vec()));
991 assert_eq!(
992 code_of(write_exclusive(&dir.at("a/b"), &[])),
993 Some("ENOENT".into())
994 );
995 }
996
997 #[test]
1000 fn a_failed_exclusive_write_is_rolled_back() {
1001 let dir = Scratch::new("files");
1002 let file = dir.at("f");
1003 let result = write_exclusive_with(
1004 &file,
1005 |f| {
1006 f.write_all(&[1])?;
1007 Err(ErrorKind::StorageFull.into())
1008 },
1009 |_| unreachable!("a failed write is not finished"),
1010 );
1011 let error = result.unwrap_err();
1012 assert_eq!(error.code, Some("ENOSPC".into()));
1013 assert!(error.message.contains("write"), "{}", error.message);
1014 assert_eq!(code_of(access(&file)), Some("ENOENT".into()));
1015 }
1016
1017 #[test]
1021 fn a_failed_rollback_is_reported() {
1022 let dir = Scratch::new("files");
1023 let file = dir.at("f");
1024 let error = write_exclusive_with(
1025 &file,
1026 |_| {
1027 fs::remove_file(&file)?;
1028 fs::create_dir_all(format!("{file}/inside"))?;
1029 Err(ErrorKind::StorageFull.into())
1030 },
1031 |_| unreachable!("a failed write is not finished"),
1032 )
1033 .unwrap_err();
1034 assert!(error.message.contains("unlink"), "{}", error.message);
1035 let error = write_exclusive_with(
1036 &dir.at("g"),
1037 |_| {
1038 fs::remove_file(dir.at("g"))?;
1039 Err(ErrorKind::StorageFull.into())
1040 },
1041 |_| unreachable!("a failed write is not finished"),
1042 )
1043 .unwrap_err();
1044 assert_eq!(error.code, Some("ENOSPC".into()));
1045 assert!(error.message.contains("write"), "{}", error.message);
1046 }
1047
1048 #[test]
1050 fn a_console_that_cannot_be_flushed_has_nothing_to_flush() {
1051 let invalid_handle = io::Error::from_raw_os_error(ERROR_INVALID_HANDLE);
1052 assert_eq!(nothing_to_flush(&invalid_handle), cfg!(windows));
1053 assert!(nothing_to_flush(&ErrorKind::InvalidInput.into()));
1054 assert!(!nothing_to_flush(&ErrorKind::StorageFull.into()));
1055 }
1056
1057 #[cfg(target_os = "linux")]
1060 #[test]
1061 fn a_file_that_cannot_be_synced_is_not_a_failure() {
1062 use std::os::fd::AsRawFd;
1063
1064 let (_reader, writer) = io::pipe().unwrap();
1065 let path = format!("/proc/self/fd/{}", writer.as_raw_fd());
1066 let opened = OpenOptions::new().write(true).open(&path).unwrap();
1067 assert_eq!(
1068 opened.sync_all().unwrap_err().kind(),
1069 ErrorKind::InvalidInput
1070 );
1071 assert_eq!(write_file(&path, &[1]), Ok(()));
1072 assert_eq!(write_bytes(&path, 0.0, &[]), Ok(()));
1073 }
1074
1075 #[test]
1076 fn write_read_and_rm() {
1077 let dir = Scratch::new("files");
1078 let file = dir.at("a.bin");
1079 assert_eq!(write_file(&file, &[1, 2, 3]), Ok(()));
1080 assert_eq!(read_file(&file), Ok([1, 2, 3].to_vec()));
1081 assert_eq!(write_file(&file, &[9]), Ok(()));
1082 assert_eq!(read_file(&file), Ok([9].to_vec()));
1083 assert_eq!(rm(&file), Ok(()));
1084 assert_eq!(code_of(read_file(&file)), Some("ENOENT".into()));
1085 assert_eq!(code_of(rm(&file)), Some("ENOENT".into()));
1086 }
1087
1088 #[test]
1089 fn read_refuses_a_file_over_a_vec() {
1090 let dir = Scratch::new("files");
1091 let file = dir.at("big");
1092 assert_eq!(
1093 write_file(&file, &[0; MAX_FILE_SIZE_BYTES as usize]),
1094 Ok(())
1095 );
1096 assert_eq!(read_file(&file).map(|v| v.len()), Ok(1 << 17));
1097 assert_eq!(write_bytes(&file, (1 << 17) as f64, &[0]), Ok(()));
1098 let error = read_file(&file).unwrap_err();
1099 assert_eq!(error.code, None);
1100 assert!(
1101 error.message.starts_with("File size 131073 exceeds"),
1102 "{}",
1103 error.message
1104 );
1105 }
1106
1107 #[cfg(target_os = "linux")]
1110 #[test]
1111 fn read_refuses_bytes_hidden_by_metadata() {
1112 assert_eq!(fs::metadata("/dev/zero").unwrap().len(), 0);
1113 let error = read_file("/dev/zero").unwrap_err();
1114 assert_eq!(error.code, None);
1115 assert_eq!(
1116 error.message,
1117 "File exceeds maximum allowed size of 131072 bytes: '/dev/zero'"
1118 );
1119 }
1120
1121 #[cfg(any(
1122 all(
1123 target_os = "linux",
1124 any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")
1125 ),
1126 target_os = "macos"
1127 ))]
1128 #[test]
1129 fn read_reports_a_symlink_loop() {
1130 let dir = Scratch::new("files");
1131 let link = dir.at("loop");
1132 std::os::unix::fs::symlink("loop", &link).unwrap();
1133 assert_eq!(code_of(read_file(&link)), Some("ELOOP".into()));
1134 }
1135
1136 #[test]
1137 fn unsafe_offsets_are_refused_before_open() {
1138 let dir = Scratch::new("files");
1139 let missing = dir.at("missing");
1140 for at in [MAX_OFFSET as f64 + 1.0, u64::MAX as f64] {
1141 assert_eq!(
1144 code_of(write_bytes(&missing, at, &[1])),
1145 Some("ERR_OUT_OF_RANGE".into())
1146 );
1147 }
1148 assert!(!Path::new(&missing).exists());
1149 }
1150
1151 #[test]
1152 fn rm_refuses_a_directory() {
1153 let dir = Scratch::new("files");
1154 assert_eq!(code_of(rm(&dir.at(""))), Some("ERR_FS_EISDIR".into()));
1155 assert_eq!(code_of(rm(&dir.at("none"))), Some("ENOENT".into()));
1156 }
1157
1158 #[cfg(any(unix, windows))]
1161 #[test]
1162 fn rm_removes_directory_links_not_targets() {
1163 #[cfg(unix)]
1164 use std::os::unix::fs::symlink as symlink_dir;
1165 #[cfg(windows)]
1166 use std::os::windows::fs::symlink_dir;
1167
1168 let dir = Scratch::new("files");
1169 for name in ["empty", "nonempty"] {
1170 mkdir(&dir.at(name), false).unwrap();
1171 }
1172 let child = dir.at("nonempty/child");
1173 write_file(&child, &[1, 2, 3]).unwrap();
1174 for name in ["empty", "nonempty", "missing"] {
1175 let link = dir.at(&format!("{name}-link"));
1176 symlink_dir(dir.at(name), &link).unwrap();
1177 assert!(fs::symlink_metadata(&link).unwrap().is_symlink());
1178 assert_eq!(rm(&link), Ok(()));
1179 let error = fs::symlink_metadata(&link).unwrap_err();
1181 assert_eq!(error.kind(), ErrorKind::NotFound);
1182 }
1183 for name in ["empty", "nonempty"] {
1184 let target = dir.at(name);
1185 assert!(Path::new(&target).is_dir());
1186 assert_eq!(code_of(rm(&target)), Some("ERR_FS_EISDIR".into()));
1187 assert!(Path::new(&target).is_dir());
1188 }
1189 assert_eq!(read_file(&child), Ok([1, 2, 3].to_vec()));
1190 assert!(!Path::new(&dir.at("missing")).exists());
1191 }
1192
1193 #[cfg(any(unix, windows))]
1195 #[test]
1196 fn rm_removes_file_links_not_targets() {
1197 #[cfg(unix)]
1198 use std::os::unix::fs::symlink as symlink_file;
1199 #[cfg(windows)]
1200 use std::os::windows::fs::symlink_file;
1201
1202 let dir = Scratch::new("files");
1203 let target = dir.at("file");
1204 write_file(&target, &[4, 5]).unwrap();
1205 for name in ["file", "missing"] {
1206 let link = dir.at(&format!("{name}-link"));
1207 symlink_file(dir.at(name), &link).unwrap();
1208 assert!(fs::symlink_metadata(&link).unwrap().is_symlink());
1209 assert_eq!(rm(&link), Ok(()));
1210 let error = fs::symlink_metadata(&link).unwrap_err();
1211 assert_eq!(error.kind(), ErrorKind::NotFound);
1212 }
1213 assert_eq!(read_file(&target), Ok([4, 5].to_vec()));
1214 assert!(!Path::new(&dir.at("missing")).exists());
1215 }
1216
1217 #[test]
1218 fn mkdir_nested() {
1219 let dir = Scratch::new("files");
1220 let nested = dir.at("a/b/c");
1221 assert_eq!(code_of(mkdir(&nested, false)), Some("ENOENT".into()));
1222 assert_eq!(mkdir(&nested, true), Ok(()));
1223 assert_eq!(mkdir(&nested, true), Ok(()));
1224 assert_eq!(code_of(mkdir(&nested, false)), Some("EEXIST".into()));
1225 assert_eq!(mkdir(&dir.at("d"), false), Ok(()));
1226 }
1227
1228 #[test]
1229 fn write_bytes_in_place() {
1230 let dir = Scratch::new("files");
1231 let file = dir.at("a");
1232 assert_eq!(
1233 code_of(write_bytes(&file, 0.0, &[1])),
1234 Some("ENOENT".into())
1235 );
1236 write_file(&file, &[1, 2, 3, 4]).unwrap();
1237 assert_eq!(write_bytes(&file, 1.0, &[9, 8]), Ok(()));
1238 assert_eq!(read_file(&file), Ok([1, 9, 8, 4].to_vec()));
1239 assert_eq!(write_bytes(&file, 6.0, &[5]), Ok(()));
1240 assert_eq!(read_file(&file), Ok([1, 9, 8, 4, 0, 0, 5].to_vec()));
1241 assert_eq!(
1242 code_of(write_bytes(&file, -1.0, &[1])),
1243 Some("ERR_OUT_OF_RANGE".into())
1244 );
1245 assert_eq!(
1246 code_of(write_bytes(&file, 0.5, &[1])),
1247 Some("ERR_OUT_OF_RANGE".into())
1248 );
1249 }
1250
1251 #[test]
1252 fn empty_write_preserves_open_and_offset_checks() {
1253 let dir = Scratch::new("files");
1254 let file = dir.at("a");
1255 assert_eq!(code_of(write_bytes(&file, 0.0, &[])), Some("ENOENT".into()));
1256 assert!(!Path::new(&file).exists());
1257 write_file(&file, &[1, 2, 3]).unwrap();
1258 for at in [0.0, 1.0, MAX_OFFSET as f64] {
1259 assert_eq!(write_bytes(&file, at, &[]), Ok(()));
1260 assert_eq!(read_file(&file), Ok([1, 2, 3].to_vec()));
1261 }
1262 for at in [-1.0, 0.5, f64::NAN, f64::INFINITY, MAX_OFFSET as f64 + 1.0] {
1263 assert_eq!(
1264 code_of(write_bytes(&file, at, &[])),
1265 Some("ERR_OUT_OF_RANGE".into())
1266 );
1267 }
1268 }
1269
1270 #[cfg(target_os = "linux")]
1273 #[test]
1274 fn empty_write_does_not_seek_a_pipe() {
1275 use std::os::fd::AsRawFd;
1276
1277 let (_reader, writer) = io::pipe().unwrap();
1278 let path = format!("/proc/self/fd/{}", writer.as_raw_fd());
1279 for at in [0.0, MAX_OFFSET as f64] {
1280 assert_eq!(write_bytes(&path, at, &[]), Ok(()));
1281 }
1282 assert!(write_bytes(&path, 0.0, &[1]).is_err());
1283 }
1284
1285 #[cfg(unix)]
1289 #[test]
1290 fn readdir_lists_symlinks_without_following_them() {
1291 use std::os::unix::fs::symlink;
1292
1293 let dir = Scratch::new("files");
1294 mkdir(&dir.at("root"), false).unwrap();
1295 mkdir(&dir.at("target"), false).unwrap();
1296 write_file(&dir.at("target/child"), &[]).unwrap();
1297 for (name, target) in [
1298 ("dir-link", "../target"),
1299 ("file-link", "../target/child"),
1300 ("missing-link", "../missing"),
1301 ] {
1302 symlink(target, dir.at(&format!("root/{name}"))).unwrap();
1303 }
1304 let root = dir.at("root");
1305 let expected = ["dir-link", "file-link", "missing-link"].map(|name| Dirent {
1306 name: name.into(),
1307 parent_path: normalize(&root),
1308 is_file: false,
1309 is_directory: false,
1310 });
1311 for recursive in [false, true] {
1312 assert_eq!(readdir(&root, recursive).unwrap(), expected);
1313 }
1314 }
1315
1316 #[test]
1318 fn readdir_order() {
1319 let dir = Scratch::new("files");
1320 for d in ["b", "a", "a/x", "b/y", "a/x/deep"] {
1321 mkdir(&dir.at(d), false).unwrap();
1322 }
1323 for f in ["z", "a/f", "b/g", "a/x/deep/h"] {
1324 write_file(&dir.at(f), &[]).unwrap();
1325 }
1326 let root = normalize(&dir.at(""));
1327 let dirent = |parent: &str, name: &str, is_file: bool| Dirent {
1328 name: name.into(),
1329 parent_path: normalize(&format!("{root}/{parent}")),
1330 is_file,
1331 is_directory: !is_file,
1332 };
1333 assert_eq!(
1334 readdir(&dir.at(""), false).unwrap(),
1335 [
1336 dirent("", "a", false),
1337 dirent("", "b", false),
1338 dirent("", "z", true)
1339 ]
1340 );
1341 assert_eq!(
1342 readdir(&dir.at(""), true).unwrap(),
1343 [
1344 dirent("", "a", false),
1345 dirent("", "b", false),
1346 dirent("", "z", true),
1347 dirent("a", "f", true),
1348 dirent("a", "x", false),
1349 dirent("b", "g", true),
1350 dirent("b", "y", false),
1351 dirent("a/x", "deep", false),
1352 dirent("a/x/deep", "h", true),
1353 ]
1354 );
1355 assert_eq!(
1356 code_of(readdir(&dir.at("none"), false)),
1357 Some("ENOENT".into())
1358 );
1359 assert!(readdir(&dir.at("z"), false).is_err());
1360 }
1361 }
1362}