use core::panic::AssertUnwindSafe; use redoxfs::{unmount_path, DirEntry, DiskMemory, DiskSparse, FileSystem, Node, TreePtr}; use std::io::{Read, Seek, SeekFrom, Write}; use std::panic::catch_unwind; use std::path::Path; use std::process::Command; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering::Relaxed; use std::thread::sleep; use std::time::Duration; use std::{env, fs, time}; static IMAGE_SEQ: AtomicUsize = AtomicUsize::new(0); fn with_redoxfs(callback: F) -> T where T: Send + Sync + 'static, F: FnOnce(&str) -> T + Send + Sync + 'static, { let disk_path = format!("image{}.bin", IMAGE_SEQ.fetch_add(1, Relaxed)); { let disk = DiskSparse::create(dbg!(&disk_path), 1024 * 1024 * 1024).unwrap(); let ctime = dbg!(time::SystemTime::now().duration_since(time::UNIX_EPOCH)).unwrap(); FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); } let res = callback(&disk_path); dbg!(fs::remove_file(dbg!(disk_path))).unwrap(); res } fn with_mounted(callback: F) -> T where T: Send + Sync + 'static, F: FnOnce(&Path) -> T + Send + Sync + 'static, { let mount_path_o = format!("image{}", IMAGE_SEQ.fetch_add(1, Relaxed)); let mount_path = mount_path_o.clone(); let res = with_redoxfs(move |fs| { // At redox, we mount on /scheme/ path, no need an empty dir if cfg!(not(target_os = "redox")) { if !Path::new(&mount_path).exists() { dbg!(fs::create_dir(dbg!(&mount_path))).unwrap(); } } else { //FIXME: cargo_bin is broken when cross compiling. This is redoxer specific workaround env::set_var( "CARGO_BIN_EXE_redoxfs", "/root/target/x86_64-unknown-redox/debug/redoxfs", ); } let mut mount_cmd = Command::new(assert_cmd::cargo_bin!("redoxfs")); mount_cmd.arg("-d").arg(dbg!(&fs)).arg(dbg!(&mount_path)); let mut child = mount_cmd.spawn().expect("mount failed to run"); let real_path = if cfg!(target_os = "redox") { let real_path = dbg!(Path::new("/scheme").join(&mount_path)); let mut tries = 0; loop { if real_path.exists() { break; } tries += 1; if tries == 10 { panic!("Fail to wait for mount") } println!("{tries}"); sleep(Duration::from_millis(500)); } real_path } else { sleep(Duration::from_millis(200)); let r = Path::new(".").join(&mount_path); r }; let res = catch_unwind(AssertUnwindSafe(|| callback(&real_path))); sleep(Duration::from_millis(200)); child.kill().expect("Can't kill"); let _ = child.wait(); if cfg!(target_os = "redox") { unmount_path(&mount_path).unwrap(); } else { if !dbg!(Command::new("sync").status()).unwrap().success() { panic!("sync failed"); } if unmount_path(&mount_path).is_err() { // There seems to be a race condition where the device can be busy when trying to unmount. // So, we pause for a moment and retry. There will still be an error output to the logs // for the first failed attempt. sleep(Duration::from_millis(200)); if unmount_path(&mount_path).is_err() { panic!("umount failed"); } } } res.expect("Test failed") }); if cfg!(not(target_os = "redox")) { dbg!(fs::remove_dir(dbg!(mount_path_o))).unwrap(); } res } #[test] fn simple() { with_mounted(|path| { dbg!(fs::create_dir(path.join("test"))).unwrap(); }) } #[test] fn create_and_remove_file() { with_mounted(|path| { let file_name = "test_file.txt"; let file_path = path.join(file_name); // Create the file fs::write(&file_path, "Hello, world!").unwrap(); assert!(fs::exists(&file_path).unwrap()); // Read the file let contents = fs::read_to_string(&file_path).unwrap(); assert_eq!(contents, "Hello, world!"); // Remove the file fs::remove_file(&file_path).unwrap(); assert!(!fs::exists(&file_path).unwrap()); }); } #[test] fn create_and_remove_directory() { with_mounted(|path| { let dir_name = "test_dir"; let dir_path = path.join(dir_name); // Create the directory fs::create_dir(&dir_path) .unwrap_or_else(|_| panic!("cannot create dir {}", &dir_path.display())); assert!(fs::exists(&dir_path).unwrap()); // Check that the directory is empty let entries: Vec<_> = fs::read_dir(&dir_path) .unwrap() .map(|e| e.unwrap().file_name()) .collect(); assert!(entries.is_empty()); // Add a file to the directory let file_name = "test_file.txt"; let file_path = dir_path.join(file_name); fs::write(&file_path, "Hello, world!").unwrap(); // Check that the dir cannot be removed when not empty let error = fs::remove_dir(&dir_path); assert!(error.is_err()); assert_eq!( error.unwrap_err().kind(), std::io::ErrorKind::DirectoryNotEmpty ); // Remove the file fs::remove_file(&file_path).unwrap(); // Remove the directory fs::remove_dir(&dir_path).unwrap(); assert!(!fs::exists(&dir_path).unwrap()); }); } #[test] fn create_and_remove_symlink() { with_mounted(|path| { let real_file = "real_file.txt"; let real_path = path.join(real_file); let symlink_file = "symlink_to_real_file.txt"; let symlink_path = path.join(symlink_file); // Create the real file fs::write(&real_path, "Hello, world!").unwrap(); // Create the symmlink according to the platform #[cfg(unix)] std::os::unix::fs::symlink(real_file, &symlink_path).unwrap(); #[cfg(windows)] std::os::windows::fs::symlink_file(&real_file, &symlink_path).unwrap(); // Check that the symlink exists and points to the correct target let exists = fs::exists(&symlink_path); assert!( exists.is_ok() && exists.unwrap(), "Symlink should exist but was: {:?}", fs::exists(&symlink_path) ); let symlink_metadata = fs::symlink_metadata(&symlink_path).unwrap(); assert!(symlink_metadata.file_type().is_symlink()); let target = fs::read_link(&symlink_path).unwrap(); assert_eq!(target.to_str().unwrap(), real_file); assert_eq!(fs::read(&symlink_path).unwrap(), b"Hello, world!"); // Confirm the symlink cannot be removed as a directory let error = fs::remove_dir(&symlink_path); assert!(error.is_err()); assert_eq!(error.unwrap_err().kind(), std::io::ErrorKind::NotADirectory); // Remove the symlink fs::remove_file(&symlink_path).unwrap(); assert!(!fs::exists(&symlink_path).unwrap()); }); } #[cfg(target_os = "redox")] #[test] fn mmap() { //TODO with_mounted(|path| { use std::slice; let path = dbg!(path.join("test")); let mmap_inner = |write: bool| { let fd = dbg!(libredox::call::open( path.to_str().unwrap(), libredox::flag::O_CREAT | libredox::flag::O_RDWR | libredox::flag::O_CLOEXEC, 0, )) .unwrap(); let map = unsafe { slice::from_raw_parts_mut( dbg!(libredox::call::mmap(libredox::call::MmapArgs { fd, offset: 0, length: 128, prot: libredox::flag::PROT_READ | libredox::flag::PROT_WRITE, flags: libredox::flag::MAP_SHARED, addr: core::ptr::null_mut(), })) .unwrap() as *mut u8, 128, ) }; // Maps should be available after closing assert_eq!(dbg!(libredox::call::close(fd)), Ok(())); for i in 0..128 { if write { map[i as usize] = i; } assert_eq!(map[i as usize], i); } //TODO: add msync unsafe { assert_eq!( dbg!(libredox::call::munmap(map.as_mut_ptr().cast(), map.len())), Ok(()) ); } }; mmap_inner(true); mmap_inner(false); }) } // TODO: When increasing the total_count to 8000, the Allocator's deallocate() function surfaces as "slow" according to flamegraph. This // appears to be the result of bulk deleting in this test, but I would bet that any filesystem that has lived for a long time would // start to see degraded performance due to this. #[test] fn many_create_write_list_find_read_delete() { let disk = DiskMemory::new(1024 * 1024 * 1024); let ctime = time::SystemTime::now() .duration_since(time::UNIX_EPOCH) .unwrap(); let mut fs = FileSystem::create(disk, None, ctime.as_secs(), ctime.subsec_nanos()).unwrap(); let tree_ptr = TreePtr::::root(); let total_count = 3000; // Create a bunch of files for i in 0..total_count { let result = fs.tx(|tx| { tx.create_node( tree_ptr, &format!("file{i:05}"), Node::MODE_FILE | 0o644, 1, 0, ) }); if result.is_err() { println!("Failure on create iteration {i}"); } let file_node = result.unwrap(); let result = fs.tx(|tx| { tx.write_node( file_node.ptr(), 0, format!("Hello World! #{i}").as_bytes(), ctime.as_secs(), ctime.subsec_nanos(), ) }); if result.is_err() { println!("Failure on write iteration {i}"); } assert!(result.unwrap() > 0) } // Confirm that they can be listed { let mut children = Vec::::with_capacity(total_count); fs.tx(|tx| tx.child_nodes(tree_ptr, &mut children)).unwrap(); assert_eq!( children.len(), total_count, "The list of children should match the number of files created." ); let mut children: Vec = children .iter() .map(|entry| entry.name().unwrap_or_default().to_string()) .collect(); children.sort(); for i in 0..total_count { let expected = format!("file{i:05}"); let idx = children.binary_search(&expected); assert!(idx.is_ok(), "Children did not contain '{}'", expected); } } // Find and read the files for i in 0..total_count { let result = fs.tx(|tx| tx.find_node(tree_ptr, &format!("file{i:05}"))); if result.is_err() { println!("Failure on find node iteration {i}"); } let file_node = result.unwrap(); let offset = 0; let mut buf = [0_u8; 32]; let result = fs.tx(|tx| { tx.read_node( file_node.ptr(), offset, &mut buf, ctime.as_secs(), ctime.subsec_nanos(), ) }); if result.is_err() { println!("Failure on read iteration {i}"); } let size = result.unwrap(); let body = std::str::from_utf8(&buf[..size]).unwrap(); assert_eq!(body, format!("Hello World! #{i}")); } // Delete all the files for i in 0..total_count { let file_name = format!("file{i:05}"); if let Err(e) = fs.tx(|tx| tx.remove_node(tree_ptr, &file_name, Node::MODE_FILE)) { println!("Failure on delete iteration {i}"); panic!("{e}"); } let result = fs.tx(|tx| tx.find_node(tree_ptr, &file_name)); if result.is_ok() || result.unwrap_err().errno != syscall::error::ENOENT { println!("Failure on delete verification iteration {i}"); panic!("Deletion appears to have failed"); } } } #[test] fn many_write_read_delete_mounted() { with_mounted(|path| { let total_count = 500; for i in 0..total_count { fs::write( path.join(format!("file{}", i)), format!("Hello, number {i}!"), ) .unwrap(); } // Confirm each of the created files can be found and read for i in 0..total_count { let contents = fs::read_to_string(path.join(format!("file{}", i))).unwrap(); assert_eq!(contents, format!("Hello, number {i}!")); } // Remove all the files for i in 0..total_count { let file_path = path.join(format!("file{i}")); assert!(fs::exists(&file_path).unwrap()); fs::remove_file(&file_path).unwrap(); assert!(!fs::exists(&file_path).unwrap()); } }); } #[test] fn rename_no_replace() { let disk = DiskMemory::new(1024 * 1024 * 1024); let mut fs = FileSystem::create(disk, None, 0, 0) .expect("Creating in memory file system should succeed"); let root = TreePtr::root(); let dir = fs .tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0)) .expect("Creating a directory should succeed"); let source_file = fs .tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0)) .expect("Creating source file to copy should succeed"); let no_clobber_file = fs .tx(|tx| tx.create_node(root, "no_clobber", Node::MODE_FILE, 0, 0)) .expect("Creating second file to not clobber should succeed"); // Rename /source to /target fs.tx(|tx| tx.rename_node_no_replace(root, "source", root, "target")) .expect("Renaming existing 'source' to non-existing 'target' should succeed"); let target_file = fs .tx(|tx| tx.find_node(root, "target")) .expect("'target' should exist because we just renamed 'source' to 'target'"); assert_eq!( source_file.id(), target_file.id(), "source and target are most definitely the same file" ); // Don't rename /target to /no_clobber let err = fs .tx(|tx| tx.rename_node_no_replace(root, "target", root, "no_clobber")) .expect_err("Renaming 'target' to existing 'no_clobber' should fail"); assert_eq!( syscall::EEXIST, err.errno, "Renaming to existing file should fail with EEXIST" ); assert_ne!( no_clobber_file.id(), target_file.id(), "'target' and 'no_clobber' should be distinct files" ); // Don't rename /target to /dir let err = fs .tx(|tx| tx.rename_node_no_replace(root, "target", root, "dir")) .expect_err("Renaming 'target' to existing directory 'dir' should fail"); assert_eq!( syscall::EEXIST, err.errno, "Renaming to existing file should fail with EEXIST" ); assert_ne!( dir.id(), target_file.id(), "'target' and 'dir' should be distinct nodes" ); // Don't rename /dir to /target let err = fs .tx(|tx| tx.rename_node_no_replace(root, "dir", root, "target")) .expect_err("Renaming 'dir' to existing file 'target' should fail"); assert_eq!( syscall::EEXIST, err.errno, "Renaming to existing file should fail with EEXIST" ); assert_ne!( target_file.id(), dir.id(), "'dir' and 'target' should be distinct nodes" ); // Don't rename /target to /target let err = fs .tx(|tx| tx.rename_node_no_replace(root, "target", root, "target")) .expect_err("Renaming 'target' to itself should fail"); assert_eq!( syscall::EEXIST, err.errno, "Renaming file to itself should fail with EEXIST" ); // Rename /target to /dir/target fs.tx(|tx| tx.rename_node_no_replace(root, "target", dir.ptr(), "target")) .expect("Renaming /target to /dir/target should succeed"); let moved_target = fs .tx(|tx| tx.find_node(dir.ptr(), "target")) .expect("'target' should have moved to /dir/target"); assert_eq!(target_file.id(), moved_target.id()); // Rename /dir to /newdir fs.tx(|tx| tx.rename_node_no_replace(root, "dir", root, "newdir")) .expect("Renaming 'dir' to 'newdir' should succeed"); } #[test] fn rename_works() { let disk = DiskMemory::new(1024 * 1024 * 1024); let mut fs = FileSystem::create(disk, None, 0, 0) .expect("Creating in memory file system should succeed"); let root = TreePtr::root(); let dir = fs .tx(|tx| tx.create_node(root, "dir", Node::MODE_DIR, 0, 0)) .expect("Creating a directory should succeed"); let source_file = fs .tx(|tx| tx.create_node(root, "source", Node::MODE_FILE, 0, 0)) .expect("Creating source file should succeed"); let target_file_orig = fs .tx(|tx| tx.create_node(root, "target", Node::MODE_FILE, 0, 0)) .expect("Creating target file should succeed"); // Rename /source to /source2 fs.tx(|tx| tx.rename_node(root, "source", root, "source2")) .expect("Renaming existing 'source' to non-existing 'source2' should succeed"); let source2_file = fs .tx(|tx| tx.find_node(root, "source2")) .expect("'source2' should exist because we just renamed 'source' to 'source2'"); assert_eq!(source_file.id(), source2_file.id()); let err = fs .tx(|tx| tx.find_node(root, "source")) .expect_err("'source' should not exist because it was moved"); assert_eq!(syscall::ENOENT, err.errno); // Rename /source2 to /target fs.tx(|tx| tx.rename_node(root, "source2", root, "target")) .expect("Renaming existing 'source2' to existing 'target' should succeed"); let target_file_mv = fs .tx(|tx| tx.find_node(root, "target")) .expect("'target' should exist because the rename succeeded"); assert_ne!( target_file_orig.id(), target_file_mv.id(), "Move failed because 'target' is still the same" ); assert_eq!( source2_file.id(), target_file_mv.id(), "Move failed because 'source2' != 'target'" ); // Don't rename /target to /dir // XXX: A similar test fails on Linux using rename(). Not sure if the discrepancy matters. // let err = fs // .tx(|tx| tx.rename_node(root, "target", root, "dir")) // .expect_err("Renaming 'target' to existing directory 'dir' should fail"); // assert_eq!( // syscall::EEXIST, // err.errno, // "Renaming to existing file should fail with EEXIST" // ); // assert_ne!( // dir.id(), // target_file_mv.id(), // "'target' and 'dir' should be distinct nodes" // ); // Don't rename /dir to /target // XXX: A similar test fails on Linux using rename(). // let err = fs // .tx(|tx| tx.rename_node(root, "dir", root, "target")) // .expect_err("Renaming 'dir' to existing file 'target' should fail"); // assert_eq!( // syscall::EEXIST, // err.errno, // "Renaming to existing file should fail with EEXIST" // ); // assert_ne!( // target_file_mv.id(), // dir.id(), // "'dir' and 'target' should be distinct nodes" // ); // Rename /target to /target fs.tx(|tx| tx.rename_node(root, "target", root, "target")) .expect("Renaming 'target' to itself should succeed"); let target_self_mv = fs .tx(|tx| tx.find_node(root, "target")) .expect("'target' should exist because rename succeeded"); assert_eq!( target_file_mv.id(), target_self_mv.id(), "'target' shouldn't have changed during a move to self" ); // Rename /target to /dir/target fs.tx(|tx| tx.rename_node(root, "target", dir.ptr(), "target")) .expect("Renaming /target to /dir/target should succeed"); let moved_target = fs .tx(|tx| tx.find_node(dir.ptr(), "target")) .expect("'target' should have moved to /dir/target"); assert_eq!(target_file_mv.id(), moved_target.id()); // Rename /dir to /newdir fs.tx(|tx| tx.rename_node(root, "dir", root, "newdir")) .expect("Renaming 'dir' to 'newdir' should succeed"); } #[test] fn temporary_file() { with_mounted(|path| { let file_path = path.join("temp"); let mut file = fs::File::create_new(&file_path).expect("failed to create temp file"); fs::remove_file(&file_path).expect("failed to unlink temp file"); let write_data = "Test\n"; file.write_all(write_data.as_bytes()) .expect("failed to write temp file"); let mut read_data = String::new(); file.seek(SeekFrom::Start(0)) .expect("failed to seek temp file"); file.read_to_string(&mut read_data) .expect("failed to read temp file"); assert_eq!(read_data, write_data); }); }