| use rand::RngCore; | |
| use super::Dir; | |
| use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError, exists}; | |
| use crate::io::prelude::*; | |
| use crate::io::{BorrowedBuf, ErrorKind, SeekFrom}; | |
| use crate::mem::MaybeUninit; | |
| #[cfg(unix)] | |
| use crate::os::unix::fs::symlink as symlink_dir; | |
| #[cfg(unix)] | |
| use crate::os::unix::fs::symlink as symlink_file; | |
| #[cfg(unix)] | |
| use crate::os::unix::fs::symlink as junction_point; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::{OpenOptionsExt, junction_point, symlink_dir, symlink_file}; | |
| use crate::path::Path; | |
| use crate::sync::Arc; | |
| use crate::test_helpers::{TempDir, tmpdir}; | |
| use crate::time::{Duration, Instant, SystemTime}; | |
| use crate::{assert_matches, env, io, str, thread}; | |
| macro_rules! check { | |
| ($e:expr) => { | |
| match $e { | |
| Ok(t) => t, | |
| Err(e) => panic!("{} failed with: {e}", stringify!($e)), | |
| } | |
| }; | |
| } | |
| macro_rules! error_contains { | |
| ($e:expr, $s:expr) => { | |
| match $e { | |
| Ok(_) => panic!("Unexpected success. Should've been: {:?}", $s), | |
| Err(ref err) => { | |
| assert!(err.to_string().contains($s), "`{}` did not contain `{}`", err, $s) | |
| } | |
| } | |
| }; | |
| } | |
| // Several test fail on windows if the user does not have permission to | |
| // create symlinks (the `SeCreateSymbolicLinkPrivilege`). Instead of | |
| // disabling these test on Windows, use this function to test whether we | |
| // have permission, and return otherwise. This way, we still don't run these | |
| // tests most of the time, but at least we do if the user has the right | |
| // permissions. | |
| pub fn got_symlink_permission(tmpdir: &TempDir) -> bool { | |
| if cfg!(not(windows)) || env::var_os("CI").is_some() { | |
| return true; | |
| } | |
| let link = tmpdir.join("some_hopefully_unique_link_name"); | |
| match symlink_file(r"nonexisting_target", link) { | |
| // ERROR_PRIVILEGE_NOT_HELD = 1314 | |
| Err(ref err) if err.raw_os_error() == Some(1314) => false, | |
| Ok(_) | Err(_) => true, | |
| } | |
| } | |
| #[test] | |
| fn file_test_io_smoke_test() { | |
| let message = "it's alright. have a good time"; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_rt_io_file_test.txt"); | |
| { | |
| let mut write_stream = check!(File::create(filename)); | |
| check!(write_stream.write(message.as_bytes())); | |
| } | |
| { | |
| let mut read_stream = check!(File::open(filename)); | |
| let mut read_buf = [0; 1028]; | |
| let read_str = match check!(read_stream.read(&mut read_buf)) { | |
| 0 => panic!("shouldn't happen"), | |
| n => str::from_utf8(&read_buf[..n]).unwrap().to_string(), | |
| }; | |
| assert_eq!(read_str, message); | |
| } | |
| check!(fs::remove_file(filename)); | |
| } | |
| #[test] | |
| fn invalid_path_raises() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_that_does_not_exist.txt"); | |
| let err = File::open(filename).unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::NotFound); | |
| } | |
| #[test] | |
| fn file_test_iounlinking_invalid_path_should_raise_condition() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_another_file_that_does_not_exist.txt"); | |
| let err = fs::remove_file(filename).unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::NotFound); | |
| } | |
| #[test] | |
| fn file_test_io_non_positional_read() { | |
| let message: &str = "ten-four"; | |
| let mut read_mem = [0; 8]; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_rt_io_file_test_positional.txt"); | |
| { | |
| let mut rw_stream = check!(File::create(filename)); | |
| check!(rw_stream.write(message.as_bytes())); | |
| } | |
| { | |
| let mut read_stream = check!(File::open(filename)); | |
| { | |
| let read_buf = &mut read_mem[0..4]; | |
| check!(read_stream.read(read_buf)); | |
| } | |
| { | |
| let read_buf = &mut read_mem[4..8]; | |
| check!(read_stream.read(read_buf)); | |
| } | |
| } | |
| check!(fs::remove_file(filename)); | |
| let read_str = str::from_utf8(&read_mem).unwrap(); | |
| assert_eq!(read_str, message); | |
| } | |
| #[test] | |
| fn file_test_io_seek_and_tell_smoke_test() { | |
| let message = "ten-four"; | |
| let mut read_mem = [0; char::MAX_LEN_UTF8]; | |
| let set_cursor = 4 as u64; | |
| let tell_pos_pre_read; | |
| let tell_pos_post_read; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_rt_io_file_test_seeking.txt"); | |
| { | |
| let mut rw_stream = check!(File::create(filename)); | |
| check!(rw_stream.write(message.as_bytes())); | |
| } | |
| { | |
| let mut read_stream = check!(File::open(filename)); | |
| check!(read_stream.seek(SeekFrom::Start(set_cursor))); | |
| tell_pos_pre_read = check!(read_stream.stream_position()); | |
| check!(read_stream.read(&mut read_mem)); | |
| tell_pos_post_read = check!(read_stream.stream_position()); | |
| } | |
| check!(fs::remove_file(filename)); | |
| let read_str = str::from_utf8(&read_mem).unwrap(); | |
| assert_eq!(read_str, &message[4..8]); | |
| assert_eq!(tell_pos_pre_read, set_cursor); | |
| assert_eq!(tell_pos_post_read, message.len() as u64); | |
| } | |
| #[test] | |
| fn file_test_io_seek_and_write() { | |
| let initial_msg = "food-is-yummy"; | |
| let overwrite_msg = "-the-bar!!"; | |
| let final_msg = "foo-the-bar!!"; | |
| let seek_idx = 3; | |
| let mut read_mem = [0; 13]; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_rt_io_file_test_seek_and_write.txt"); | |
| { | |
| let mut rw_stream = check!(File::create(filename)); | |
| check!(rw_stream.write(initial_msg.as_bytes())); | |
| check!(rw_stream.seek(SeekFrom::Start(seek_idx))); | |
| check!(rw_stream.write(overwrite_msg.as_bytes())); | |
| } | |
| { | |
| let mut read_stream = check!(File::open(filename)); | |
| check!(read_stream.read(&mut read_mem)); | |
| } | |
| check!(fs::remove_file(filename)); | |
| let read_str = str::from_utf8(&read_mem).unwrap(); | |
| assert!(read_str == final_msg); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| not(any( | |
| windows, | |
| target_os = "aix", | |
| target_os = "cygwin", | |
| target_os = "freebsd", | |
| target_os = "fuchsia", | |
| target_os = "hurd", | |
| target_os = "illumos", | |
| target_os = "linux", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| target_os = "solaris", | |
| target_os = "android", | |
| target_vendor = "apple", | |
| )), | |
| should_panic | |
| )] | |
| fn file_lock_multiple_shared() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_multiple_shared_test.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = check!(OpenOptions::new().write(true).open(filename)); | |
| // Check that we can acquire concurrent shared locks | |
| check!(f1.lock_shared()); | |
| check!(f2.lock_shared()); | |
| check!(f1.unlock()); | |
| check!(f2.unlock()); | |
| check!(f1.try_lock_shared()); | |
| check!(f2.try_lock_shared()); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| not(any( | |
| windows, | |
| target_os = "aix", | |
| target_os = "cygwin", | |
| target_os = "freebsd", | |
| target_os = "fuchsia", | |
| target_os = "hurd", | |
| target_os = "illumos", | |
| target_os = "linux", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| target_os = "solaris", | |
| target_os = "android", | |
| target_vendor = "apple", | |
| )), | |
| should_panic | |
| )] | |
| fn file_lock_blocking() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_blocking_test.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = check!(OpenOptions::new().write(true).open(filename)); | |
| // Check that shared locks block exclusive locks | |
| check!(f1.lock_shared()); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| check!(f1.unlock()); | |
| // Check that exclusive locks block shared locks | |
| check!(f1.lock()); | |
| assert_matches!(f2.try_lock_shared(), Err(TryLockError::WouldBlock)); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| not(any( | |
| windows, | |
| target_os = "aix", | |
| target_os = "cygwin", | |
| target_os = "freebsd", | |
| target_os = "fuchsia", | |
| target_os = "hurd", | |
| target_os = "illumos", | |
| target_os = "linux", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| target_os = "solaris", | |
| target_os = "android", | |
| target_vendor = "apple", | |
| )), | |
| should_panic | |
| )] | |
| fn file_lock_drop() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_dup_test.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = check!(OpenOptions::new().write(true).open(filename)); | |
| // Check that locks are released when the File is dropped | |
| check!(f1.lock_shared()); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| drop(f1); | |
| check!(f2.try_lock()); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| not(any( | |
| windows, | |
| target_os = "aix", | |
| target_os = "cygwin", | |
| target_os = "freebsd", | |
| target_os = "fuchsia", | |
| target_os = "hurd", | |
| target_os = "illumos", | |
| target_os = "linux", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| target_os = "solaris", | |
| target_os = "android", | |
| target_vendor = "apple", | |
| )), | |
| should_panic | |
| )] | |
| fn file_lock_dup() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_dup_test.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = check!(OpenOptions::new().write(true).open(filename)); | |
| // Check that locks are not dropped if the File has been cloned | |
| check!(f1.lock_shared()); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| let cloned = check!(f1.try_clone()); | |
| drop(f1); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| drop(cloned) | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| not(any( | |
| windows, | |
| target_os = "aix", | |
| target_os = "cygwin", | |
| target_os = "freebsd", | |
| target_os = "fuchsia", | |
| target_os = "hurd", | |
| target_os = "illumos", | |
| target_os = "linux", | |
| target_os = "netbsd", | |
| target_os = "openbsd", | |
| target_os = "solaris", | |
| target_os = "android", | |
| target_vendor = "apple", | |
| )), | |
| should_panic | |
| )] | |
| fn file_lock_double() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_double_test.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = check!(OpenOptions::new().write(true).open(filename)); | |
| // A file handle may acquire both a shared and exclusive lock. | |
| check!(f1.lock()); | |
| check!(f1.lock_shared()); | |
| // The behavior here differs between Windows and Unix: on Windows, f1 holds both locks; | |
| // on Unix, the lock got downgraded so f1 only holds the shared lock. | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| if cfg!(windows) { | |
| assert_matches!(f2.try_lock_shared(), Err(TryLockError::WouldBlock)); | |
| } else { | |
| check!(f2.try_lock_shared()); | |
| check!(f2.unlock()); | |
| } | |
| // Check that both are released by unlock(). | |
| check!(f1.unlock()); | |
| check!(f2.try_lock()); | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn file_lock_blocking_async() { | |
| use crate::thread::{sleep, spawn}; | |
| const FILE_FLAG_OVERLAPPED: u32 = 0x40000000; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_lock_blocking_async.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = | |
| check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename)); | |
| check!(f1.lock()); | |
| // Ensure that lock() is synchronous when the file is opened for asynchronous IO | |
| let t = spawn(move || { | |
| check!(f2.lock()); | |
| }); | |
| sleep(Duration::from_secs(1)); | |
| assert!(!t.is_finished()); | |
| check!(f1.unlock()); | |
| t.join().unwrap(); | |
| // Ensure that lock_shared() is synchronous when the file is opened for asynchronous IO | |
| let f2 = | |
| check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename)); | |
| check!(f1.lock()); | |
| // Ensure that lock() is synchronous when the file is opened for asynchronous IO | |
| let t = spawn(move || { | |
| check!(f2.lock_shared()); | |
| }); | |
| sleep(Duration::from_secs(1)); | |
| assert!(!t.is_finished()); | |
| check!(f1.unlock()); | |
| t.join().unwrap(); | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn file_try_lock_async() { | |
| const FILE_FLAG_OVERLAPPED: u32 = 0x40000000; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_try_lock_async.txt"); | |
| let f1 = check!(File::create(filename)); | |
| let f2 = | |
| check!(OpenOptions::new().custom_flags(FILE_FLAG_OVERLAPPED).write(true).open(filename)); | |
| // Check that shared locks block exclusive locks | |
| check!(f1.lock_shared()); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| check!(f1.unlock()); | |
| // Check that exclusive locks block all locks | |
| check!(f1.lock()); | |
| assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); | |
| assert_matches!(f2.try_lock_shared(), Err(TryLockError::WouldBlock)); | |
| } | |
| #[test] | |
| fn file_test_io_seek_shakedown() { | |
| // 01234567890123 | |
| let initial_msg = "qwer-asdf-zxcv"; | |
| let chunk_one: &str = "qwer"; | |
| let chunk_two: &str = "asdf"; | |
| let chunk_three: &str = "zxcv"; | |
| let mut read_mem = [0; char::MAX_LEN_UTF8]; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_rt_io_file_test_seek_shakedown.txt"); | |
| { | |
| let mut rw_stream = check!(File::create(filename)); | |
| check!(rw_stream.write(initial_msg.as_bytes())); | |
| } | |
| { | |
| let mut read_stream = check!(File::open(filename)); | |
| check!(read_stream.seek(SeekFrom::End(-4))); | |
| check!(read_stream.read(&mut read_mem)); | |
| assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_three); | |
| check!(read_stream.seek(SeekFrom::Current(-9))); | |
| check!(read_stream.read(&mut read_mem)); | |
| assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_two); | |
| check!(read_stream.seek(SeekFrom::Start(0))); | |
| check!(read_stream.read(&mut read_mem)); | |
| assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_one); | |
| } | |
| check!(fs::remove_file(filename)); | |
| } | |
| #[test] | |
| fn file_test_io_eof() { | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_eof.txt"); | |
| let mut buf = [0; 256]; | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut rw = check!(oo.open(&filename)); | |
| assert_eq!(check!(rw.read(&mut buf)), 0); | |
| assert_eq!(check!(rw.read(&mut buf)), 0); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| #[cfg(unix)] | |
| fn file_test_io_read_write_at() { | |
| use crate::os::unix::fs::FileExt; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_read_write_at.txt"); | |
| let mut buf = [0; 256]; | |
| let write1 = "asdf"; | |
| let write2 = "qwer-"; | |
| let write3 = "-zxcv"; | |
| let content = "qwer-asdf-zxcv"; | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut rw = check!(oo.open(&filename)); | |
| assert_eq!(check!(rw.write_at(write1.as_bytes(), 5)), write1.len()); | |
| assert_eq!(check!(rw.stream_position()), 0); | |
| assert_eq!(check!(rw.read_at(&mut buf, 5)), write1.len()); | |
| assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); | |
| assert_eq!(check!(rw.stream_position()), 0); | |
| assert_eq!(check!(rw.read_at(&mut buf[..write2.len()], 0)), write2.len()); | |
| assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok("\0\0\0\0\0")); | |
| assert_eq!(check!(rw.stream_position()), 0); | |
| assert_eq!(check!(rw.write(write2.as_bytes())), write2.len()); | |
| assert_eq!(check!(rw.stream_position()), 5); | |
| assert_eq!(check!(rw.read(&mut buf)), write1.len()); | |
| assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| assert_eq!(check!(rw.read_at(&mut buf[..write2.len()], 0)), write2.len()); | |
| assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok(write2)); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| assert_eq!(check!(rw.write_at(write3.as_bytes(), 9)), write3.len()); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| } | |
| { | |
| let mut read = check!(File::open(&filename)); | |
| assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 0); | |
| assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); | |
| assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 9); | |
| assert_eq!(check!(read.read(&mut buf)), write3.len()); | |
| assert_eq!(str::from_utf8(&buf[..write3.len()]), Ok(write3)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.read_at(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.read_at(&mut buf, 14)), 0); | |
| assert_eq!(check!(read.read_at(&mut buf, 15)), 0); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| #[cfg(unix)] | |
| fn test_read_buf_at() { | |
| use crate::os::unix::fs::FileExt; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_read_buf_at.txt"); | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut file = check!(oo.open(&filename)); | |
| check!(file.write_all(b"0123456789")); | |
| } | |
| { | |
| let mut file = check!(File::open(&filename)); | |
| let mut buf: [MaybeUninit<u8>; 5] = [MaybeUninit::uninit(); 5]; | |
| let mut buf = BorrowedBuf::from(buf.as_mut_slice()); | |
| // Fill entire buffer with potentially short reads | |
| while buf.unfilled().capacity() > 0 { | |
| let len = buf.len(); | |
| check!(file.read_buf_at(buf.unfilled(), 2 + len as u64)); | |
| assert!(!buf.filled().is_empty()); | |
| assert!(b"23456".starts_with(buf.filled())); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| } | |
| assert_eq!(buf.filled(), b"23456"); | |
| // Already full | |
| check!(file.read_buf_at(buf.unfilled(), 3)); | |
| check!(file.read_buf_at(buf.unfilled(), 10)); | |
| assert_eq!(buf.filled(), b"23456"); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| // Read past eof is noop | |
| check!(file.read_buf_at(buf.clear().unfilled(), 10)); | |
| assert_eq!(buf.filled(), b""); | |
| check!(file.read_buf_at(buf.clear().unfilled(), 11)); | |
| assert_eq!(buf.filled(), b""); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| #[cfg(unix)] | |
| fn test_read_buf_exact_at() { | |
| use crate::os::unix::fs::FileExt; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_read_buf_exact_at.txt"); | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut file = check!(oo.open(&filename)); | |
| check!(file.write_all(b"0123456789")); | |
| } | |
| { | |
| let mut file = check!(File::open(&filename)); | |
| let mut buf: [MaybeUninit<u8>; 5] = [MaybeUninit::uninit(); 5]; | |
| let mut buf = BorrowedBuf::from(buf.as_mut_slice()); | |
| // Exact read | |
| check!(file.read_buf_exact_at(buf.unfilled(), 2)); | |
| assert_eq!(buf.filled(), b"23456"); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| // Already full | |
| check!(file.read_buf_exact_at(buf.unfilled(), 3)); | |
| check!(file.read_buf_exact_at(buf.unfilled(), 10)); | |
| assert_eq!(buf.filled(), b"23456"); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| // Non-empty exact read past eof fails | |
| let err = file.read_buf_exact_at(buf.clear().unfilled(), 6).unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::UnexpectedEof); | |
| assert_eq!(check!(file.stream_position()), 0); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| #[cfg(unix)] | |
| fn set_get_unix_permissions() { | |
| use crate::os::unix::fs::PermissionsExt; | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("set_get_unix_permissions"); | |
| check!(fs::create_dir(filename)); | |
| let mask = 0o7777; | |
| check!(fs::set_permissions(filename, fs::Permissions::from_mode(0))); | |
| let metadata0 = check!(fs::metadata(filename)); | |
| assert_eq!(mask & metadata0.permissions().mode(), 0); | |
| check!(fs::set_permissions(filename, fs::Permissions::from_mode(0o1777))); | |
| let metadata1 = check!(fs::metadata(filename)); | |
| #[cfg(all(unix, not(target_os = "vxworks")))] | |
| assert_eq!(mask & metadata1.permissions().mode(), 0o1777); | |
| #[cfg(target_os = "vxworks")] | |
| assert_eq!(mask & metadata1.permissions().mode(), 0o0777); | |
| } | |
| #[test] | |
| fn set_get_permissions_nofollows() { | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("set_get_unix_permissions_file"); | |
| check!(File::create(&filename)); | |
| let file_metadata = check!(fs::metadata(&filename)); | |
| assert!(!file_metadata.permissions().readonly()); | |
| let mut permission_bits = file_metadata.permissions(); | |
| permission_bits.set_readonly(true); | |
| let result = fs::set_permissions_nofollow(&filename, permission_bits); | |
| cfg_select! { | |
| any(windows, unix, target_os = "uefi", target_os = "solid_asp3", target_os = "motor") => { | |
| assert_eq!(result.unwrap(), ()); | |
| let metadata0 = check!(fs::metadata(&filename)); | |
| assert!(metadata0.permissions().readonly()); | |
| }, | |
| _ => { | |
| let error_kind = result.unwrap_err().kind(); | |
| assert_eq!(error_kind, crate::io::ErrorKind::Unsupported); | |
| } | |
| } | |
| } | |
| // Only Windows and Unix support `fs::set_permissions_nofollow` | |
| #[test] | |
| #[cfg(all(any(windows, unix), not(any(target_os = "espidf", target_os = "horizon"))))] | |
| fn set_get_permissions_nofollows_symlink() { | |
| #[cfg(not(windows))] | |
| use crate::os::unix::fs::symlink as symlink_dir; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::symlink_dir; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("set_get_unix_permissions_file"); | |
| let symlink_name = tmpdir.join("set_get_unix_permissions"); | |
| check!(File::create(&filename)); | |
| check!(symlink_dir(&filename, &symlink_name)); | |
| let sym_metadata = check!(fs::symlink_metadata(&symlink_name)); | |
| let mut permission_bits = sym_metadata.permissions(); | |
| permission_bits.set_readonly(true); | |
| let result = fs::set_permissions_nofollow(&symlink_name, permission_bits); | |
| cfg_select! { | |
| any(windows, target_os = "android", target_os = "macos", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly") => { | |
| assert_eq!(result.unwrap(), ()); | |
| let metadata0 = check!(fs::symlink_metadata(&symlink_name)); | |
| // So seems like BSD-based systems trying to set permissions | |
| // on symlinks could lead to no effect, so we should expect | |
| // there being no change to BSD-based systems. | |
| // https://superuser.com/questions/1099634/change-permissions-symbolic-link-mac-os | |
| #[cfg(windows)] | |
| assert!(metadata0.permissions().readonly()); | |
| #[cfg(not(windows))] | |
| assert!(!metadata0.permissions().readonly()); | |
| }, | |
| _ => { | |
| let error_kind = result.unwrap_err().kind(); | |
| assert_eq!(error_kind, crate::io::ErrorKind::Unsupported); | |
| } | |
| } | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn file_test_io_seek_read_write() { | |
| use crate::os::windows::fs::FileExt; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_seek_read_write.txt"); | |
| let mut buf = [0; 256]; | |
| let write1 = "asdf"; | |
| let write2 = "qwer-"; | |
| let write3 = "-zxcv"; | |
| let content = "qwer-asdf-zxcv"; | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut rw = check!(oo.open(&filename)); | |
| assert_eq!(check!(rw.seek_write(write1.as_bytes(), 5)), write1.len()); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| assert_eq!(check!(rw.seek_read(&mut buf, 5)), write1.len()); | |
| assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| assert_eq!(check!(rw.seek(SeekFrom::Start(0))), 0); | |
| assert_eq!(check!(rw.write(write2.as_bytes())), write2.len()); | |
| assert_eq!(check!(rw.stream_position()), 5); | |
| assert_eq!(check!(rw.read(&mut buf)), write1.len()); | |
| assert_eq!(str::from_utf8(&buf[..write1.len()]), Ok(write1)); | |
| assert_eq!(check!(rw.stream_position()), 9); | |
| assert_eq!(check!(rw.seek_read(&mut buf[..write2.len()], 0)), write2.len()); | |
| assert_eq!(str::from_utf8(&buf[..write2.len()]), Ok(write2)); | |
| assert_eq!(check!(rw.stream_position()), 5); | |
| assert_eq!(check!(rw.seek_write(write3.as_bytes(), 9)), write3.len()); | |
| assert_eq!(check!(rw.stream_position()), 14); | |
| } | |
| { | |
| let mut read = check!(File::open(&filename)); | |
| assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); | |
| assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.seek(SeekFrom::End(-5))), 9); | |
| assert_eq!(check!(read.read(&mut buf)), write3.len()); | |
| assert_eq!(str::from_utf8(&buf[..write3.len()]), Ok(write3)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.seek_read(&mut buf, 0)), content.len()); | |
| assert_eq!(str::from_utf8(&buf[..content.len()]), Ok(content)); | |
| assert_eq!(check!(read.stream_position()), 14); | |
| assert_eq!(check!(read.seek_read(&mut buf, 14)), 0); | |
| assert_eq!(check!(read.seek_read(&mut buf, 15)), 0); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn test_seek_read_buf() { | |
| use crate::os::windows::fs::FileExt; | |
| let tmpdir = tmpdir(); | |
| let filename = tmpdir.join("file_rt_io_file_test_seek_read_buf.txt"); | |
| { | |
| let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); | |
| let mut file = check!(oo.open(&filename)); | |
| check!(file.write_all(b"0123456789")); | |
| } | |
| { | |
| let mut file = check!(File::open(&filename)); | |
| let mut buf: [MaybeUninit<u8>; 1] = [MaybeUninit::uninit()]; | |
| let mut buf = BorrowedBuf::from(buf.as_mut_slice()); | |
| // Seek read | |
| check!(file.seek_read_buf(buf.unfilled(), 8)); | |
| assert_eq!(buf.filled(), b"8"); | |
| assert_eq!(check!(file.stream_position()), 9); | |
| // Empty seek read | |
| check!(file.seek_read_buf(buf.unfilled(), 0)); | |
| assert_eq!(buf.filled(), b"8"); | |
| // Seek read past eof | |
| check!(file.seek_read_buf(buf.clear().unfilled(), 10)); | |
| assert_eq!(buf.filled(), b""); | |
| } | |
| check!(fs::remove_file(&filename)); | |
| } | |
| #[test] | |
| fn file_test_read_buf() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("test"); | |
| check!(fs::write(filename, &[1, 2, 3, 4])); | |
| let mut buf: [MaybeUninit<u8>; 128] = [MaybeUninit::uninit(); 128]; | |
| let mut buf = BorrowedBuf::from(buf.as_mut_slice()); | |
| let mut file = check!(File::open(filename)); | |
| check!(file.read_buf(buf.unfilled())); | |
| assert_eq!(buf.filled(), &[1, 2, 3, 4]); | |
| // File::read_buf should omit buffer initialization. | |
| assert!(!buf.is_init()); | |
| check!(fs::remove_file(filename)); | |
| } | |
| #[test] | |
| fn file_test_stat_is_correct_on_is_file() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_stat_correct_on_is_file.txt"); | |
| { | |
| let mut opts = OpenOptions::new(); | |
| let mut fs = check!(opts.read(true).write(true).create(true).open(filename)); | |
| let msg = "hw"; | |
| fs.write(msg.as_bytes()).unwrap(); | |
| let fstat_res = check!(fs.metadata()); | |
| assert!(fstat_res.is_file()); | |
| } | |
| let stat_res_fn = check!(fs::metadata(filename)); | |
| assert!(stat_res_fn.is_file()); | |
| let stat_res_meth = check!(filename.metadata()); | |
| assert!(stat_res_meth.is_file()); | |
| check!(fs::remove_file(filename)); | |
| } | |
| #[test] | |
| fn file_test_stat_is_correct_on_is_dir() { | |
| let tmpdir = tmpdir(); | |
| let filename = &tmpdir.join("file_stat_correct_on_is_dir"); | |
| check!(fs::create_dir(filename)); | |
| let stat_res_fn = check!(fs::metadata(filename)); | |
| assert!(stat_res_fn.is_dir()); | |
| let stat_res_meth = check!(filename.metadata()); | |
| assert!(stat_res_meth.is_dir()); | |
| check!(fs::remove_dir(filename)); | |
| } | |
| #[test] | |
| fn file_test_fileinfo_false_when_checking_is_file_on_a_directory() { | |
| let tmpdir = tmpdir(); | |
| let dir = &tmpdir.join("fileinfo_false_on_dir"); | |
| check!(fs::create_dir(dir)); | |
| assert!(!dir.is_file()); | |
| check!(fs::remove_dir(dir)); | |
| } | |
| #[test] | |
| fn file_test_fileinfo_check_exists_before_and_after_file_creation() { | |
| let tmpdir = tmpdir(); | |
| let file = &tmpdir.join("fileinfo_check_exists_b_and_a.txt"); | |
| check!(check!(File::create(file)).write(b"foo")); | |
| assert!(file.exists()); | |
| check!(fs::remove_file(file)); | |
| assert!(!file.exists()); | |
| } | |
| #[test] | |
| fn file_test_directoryinfo_check_exists_before_and_after_mkdir() { | |
| let tmpdir = tmpdir(); | |
| let dir = &tmpdir.join("before_and_after_dir"); | |
| assert!(!dir.exists()); | |
| check!(fs::create_dir(dir)); | |
| assert!(dir.exists()); | |
| assert!(dir.is_dir()); | |
| check!(fs::remove_dir(dir)); | |
| assert!(!dir.exists()); | |
| } | |
| #[test] | |
| fn file_test_directoryinfo_readdir() { | |
| let tmpdir = tmpdir(); | |
| let dir = &tmpdir.join("di_readdir"); | |
| check!(fs::create_dir(dir)); | |
| let prefix = "foo"; | |
| for n in 0..3 { | |
| let f = dir.join(&format!("{n}.txt")); | |
| let mut w = check!(File::create(&f)); | |
| let msg_str = format!("{}{}", prefix, n.to_string()); | |
| let msg = msg_str.as_bytes(); | |
| check!(w.write(msg)); | |
| } | |
| let files = check!(fs::read_dir(dir)); | |
| let mut mem = [0; char::MAX_LEN_UTF8]; | |
| for f in files { | |
| let f = f.unwrap().path(); | |
| { | |
| let n = f.file_stem().unwrap(); | |
| check!(check!(File::open(&f)).read(&mut mem)); | |
| let read_str = str::from_utf8(&mem).unwrap(); | |
| let expected = format!("{}{}", prefix, n.to_str().unwrap()); | |
| assert_eq!(expected, read_str); | |
| } | |
| check!(fs::remove_file(&f)); | |
| } | |
| check!(fs::remove_dir(dir)); | |
| } | |
| #[test] | |
| fn file_create_new_already_exists_error() { | |
| let tmpdir = tmpdir(); | |
| let file = &tmpdir.join("file_create_new_error_exists"); | |
| check!(fs::File::create(file)); | |
| let e = fs::OpenOptions::new().write(true).create_new(true).open(file).unwrap_err(); | |
| assert_eq!(e.kind(), ErrorKind::AlreadyExists); | |
| } | |
| #[test] | |
| fn mkdir_path_already_exists_error() { | |
| let tmpdir = tmpdir(); | |
| let dir = &tmpdir.join("mkdir_error_twice"); | |
| check!(fs::create_dir(dir)); | |
| let e = fs::create_dir(dir).unwrap_err(); | |
| assert_eq!(e.kind(), ErrorKind::AlreadyExists); | |
| } | |
| #[test] | |
| fn recursive_mkdir() { | |
| let tmpdir = tmpdir(); | |
| let dir = tmpdir.join("d1/d2"); | |
| check!(fs::create_dir_all(&dir)); | |
| assert!(dir.is_dir()) | |
| } | |
| #[test] | |
| fn recursive_mkdir_failure() { | |
| let tmpdir = tmpdir(); | |
| let dir = tmpdir.join("d1"); | |
| let file = dir.join("f1"); | |
| check!(fs::create_dir_all(&dir)); | |
| check!(File::create(&file)); | |
| let result = fs::create_dir_all(&file); | |
| assert!(result.is_err()); | |
| } | |
| #[test] | |
| fn concurrent_recursive_mkdir() { | |
| let count = if cfg!(miri) { 10 } else { 100 }; | |
| let nest = if cfg!(miri) { 10 } else { 40 }; | |
| for _ in 0..count { | |
| let dir = tmpdir(); | |
| let mut dir = dir.join("a"); | |
| for _ in 0..nest { | |
| dir = dir.join("a"); | |
| } | |
| let mut join = vec![]; | |
| for _ in 0..8 { | |
| let dir = dir.clone(); | |
| join.push(thread::spawn(move || { | |
| check!(fs::create_dir_all(&dir)); | |
| })) | |
| } | |
| // No `Display` on result of `join()` | |
| join.drain(..).map(|join| join.join().unwrap()).count(); | |
| } | |
| } | |
| #[test] | |
| fn recursive_mkdir_slash() { | |
| check!(fs::create_dir_all(Path::new("/"))); | |
| } | |
| #[test] | |
| fn recursive_mkdir_dot() { | |
| check!(fs::create_dir_all(Path::new("."))); | |
| } | |
| #[test] | |
| fn recursive_mkdir_empty() { | |
| check!(fs::create_dir_all(Path::new(""))); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| all(windows, target_arch = "aarch64"), | |
| ignore = "SymLinks not enabled on Arm64 Windows runners https://github.com/actions/partner-runner-images/issues/94" | |
| )] | |
| fn recursive_rmdir() { | |
| let tmpdir = tmpdir(); | |
| let d1 = tmpdir.join("d1"); | |
| let dt = d1.join("t"); | |
| let dtt = dt.join("t"); | |
| let d2 = tmpdir.join("d2"); | |
| let canary = d2.join("do_not_delete"); | |
| check!(fs::create_dir_all(&dtt)); | |
| check!(fs::create_dir_all(&d2)); | |
| check!(check!(File::create(&canary)).write(b"foo")); | |
| check!(junction_point(&d2, &dt.join("d2"))); | |
| let _ = symlink_file(&canary, &d1.join("canary")); | |
| check!(fs::remove_dir_all(&d1)); | |
| assert!(!d1.is_dir()); | |
| assert!(canary.exists()); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| all(windows, target_arch = "aarch64"), | |
| ignore = "SymLinks not enabled on Arm64 Windows runners https://github.com/actions/partner-runner-images/issues/94" | |
| )] | |
| fn recursive_rmdir_of_symlink() { | |
| // test we do not recursively delete a symlink but only dirs. | |
| let tmpdir = tmpdir(); | |
| let link = tmpdir.join("d1"); | |
| let dir = tmpdir.join("d2"); | |
| let canary = dir.join("do_not_delete"); | |
| check!(fs::create_dir_all(&dir)); | |
| check!(check!(File::create(&canary)).write(b"foo")); | |
| check!(junction_point(&dir, &link)); | |
| check!(fs::remove_dir_all(&link)); | |
| assert!(!link.is_dir()); | |
| assert!(canary.exists()); | |
| } | |
| #[test] | |
| fn recursive_rmdir_of_file_fails() { | |
| // test we do not delete a directly specified file. | |
| let tmpdir = tmpdir(); | |
| let canary = tmpdir.join("do_not_delete"); | |
| check!(check!(File::create(&canary)).write(b"foo")); | |
| let err = fs::remove_dir_all(&canary).unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::NotADirectory); | |
| assert!(canary.exists()); | |
| } | |
| #[test] | |
| // only Windows makes a distinction between file and directory symlinks. | |
| #[cfg(windows)] | |
| fn recursive_rmdir_of_file_symlink() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| let f1 = tmpdir.join("f1"); | |
| let f2 = tmpdir.join("f2"); | |
| check!(check!(File::create(&f1)).write(b"foo")); | |
| check!(symlink_file(&f1, &f2)); | |
| match fs::remove_dir_all(&f2) { | |
| Ok(..) => panic!("wanted a failure"), | |
| Err(..) => {} | |
| } | |
| } | |
| #[test] | |
| #[ignore] // takes too much time | |
| fn recursive_rmdir_toctou() { | |
| // Test for time-of-check to time-of-use issues. | |
| // | |
| // Scenario: | |
| // The attacker wants to get directory contents deleted, to which they do not have access. | |
| // They have a way to get a privileged Rust binary call `std::fs::remove_dir_all()` on a | |
| // directory they control, e.g. in their home directory. | |
| // | |
| // The POC sets up the `attack_dest/attack_file` which the attacker wants to have deleted. | |
| // The attacker repeatedly creates a directory and replaces it with a symlink from | |
| // `victim_del` to `attack_dest` while the victim code calls `std::fs::remove_dir_all()` | |
| // on `victim_del`. After a few seconds the attack has succeeded and | |
| // `attack_dest/attack_file` is deleted. | |
| let tmpdir = tmpdir(); | |
| let victim_del_path = tmpdir.join("victim_del"); | |
| let victim_del_path_clone = victim_del_path.clone(); | |
| // setup dest | |
| let attack_dest_dir = tmpdir.join("attack_dest"); | |
| let attack_dest_dir = attack_dest_dir.as_path(); | |
| fs::create_dir(attack_dest_dir).unwrap(); | |
| let attack_dest_file = tmpdir.join("attack_dest/attack_file"); | |
| File::create(&attack_dest_file).unwrap(); | |
| let drop_canary_arc = Arc::new(()); | |
| let drop_canary_weak = Arc::downgrade(&drop_canary_arc); | |
| eprintln!("x: {victim_del_path:?}"); | |
| // victim just continuously removes `victim_del` | |
| thread::spawn(move || { | |
| while drop_canary_weak.upgrade().is_some() { | |
| let _ = fs::remove_dir_all(&victim_del_path_clone); | |
| } | |
| }); | |
| // attacker (could of course be in a separate process) | |
| let start_time = Instant::now(); | |
| while Instant::now().duration_since(start_time) < Duration::from_secs(1000) { | |
| if !attack_dest_file.exists() { | |
| panic!( | |
| "Victim deleted symlinked file outside of victim_del. Attack succeeded in {:?}.", | |
| Instant::now().duration_since(start_time) | |
| ); | |
| } | |
| let _ = fs::create_dir(&victim_del_path); | |
| let _ = fs::remove_dir(&victim_del_path); | |
| let _ = symlink_dir(attack_dest_dir, &victim_del_path); | |
| } | |
| } | |
| #[test] | |
| fn unicode_path_is_dir() { | |
| assert!(Path::new(".").is_dir()); | |
| assert!(!Path::new("test/stdtest/fs.rs").is_dir()); | |
| let tmpdir = tmpdir(); | |
| let mut dirpath = tmpdir.path().to_path_buf(); | |
| dirpath.push("test-가一ー你好"); | |
| check!(fs::create_dir(&dirpath)); | |
| assert!(dirpath.is_dir()); | |
| let mut filepath = dirpath; | |
| filepath.push("unicode-file-\u{ac00}\u{4e00}\u{30fc}\u{4f60}\u{597d}.rs"); | |
| check!(File::create(&filepath)); // ignore return; touch only | |
| assert!(!filepath.is_dir()); | |
| assert!(filepath.exists()); | |
| } | |
| #[test] | |
| fn unicode_path_exists() { | |
| assert!(Path::new(".").exists()); | |
| assert!(!Path::new("test/nonexistent-bogus-path").exists()); | |
| let tmpdir = tmpdir(); | |
| let unicode = tmpdir.path(); | |
| let unicode = unicode.join("test-각丁ー再见"); | |
| check!(fs::create_dir(&unicode)); | |
| assert!(unicode.exists()); | |
| assert!(!Path::new("test/unicode-bogus-path-각丁ー再见").exists()); | |
| } | |
| #[test] | |
| fn copy_file_does_not_exist() { | |
| let from = Path::new("test/nonexistent-bogus-path"); | |
| let to = Path::new("test/other-bogus-path"); | |
| match fs::copy(&from, &to) { | |
| Ok(..) => panic!(), | |
| Err(..) => { | |
| assert!(!from.exists()); | |
| assert!(!to.exists()); | |
| } | |
| } | |
| } | |
| #[test] | |
| fn copy_src_does_not_exist() { | |
| let tmpdir = tmpdir(); | |
| let from = Path::new("test/nonexistent-bogus-path"); | |
| let to = tmpdir.join("out.txt"); | |
| check!(check!(File::create(&to)).write(b"hello")); | |
| assert!(fs::copy(&from, &to).is_err()); | |
| assert!(!from.exists()); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&to)).read_to_end(&mut v)); | |
| assert_eq!(v, b"hello"); | |
| } | |
| #[test] | |
| fn copy_file_ok() { | |
| let tmpdir = tmpdir(); | |
| let input = tmpdir.join("in.txt"); | |
| let out = tmpdir.join("out.txt"); | |
| check!(check!(File::create(&input)).write(b"hello")); | |
| check!(fs::copy(&input, &out)); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&out)).read_to_end(&mut v)); | |
| assert_eq!(v, b"hello"); | |
| assert_eq!(check!(input.metadata()).permissions(), check!(out.metadata()).permissions()); | |
| } | |
| #[test] | |
| fn copy_file_dst_dir() { | |
| let tmpdir = tmpdir(); | |
| let out = tmpdir.join("out"); | |
| check!(File::create(&out)); | |
| match fs::copy(&*out, tmpdir.path()) { | |
| Ok(..) => panic!(), | |
| Err(..) => {} | |
| } | |
| } | |
| #[test] | |
| fn copy_file_dst_exists() { | |
| let tmpdir = tmpdir(); | |
| let input = tmpdir.join("in"); | |
| let output = tmpdir.join("out"); | |
| check!(check!(File::create(&input)).write("foo".as_bytes())); | |
| check!(check!(File::create(&output)).write("bar".as_bytes())); | |
| check!(fs::copy(&input, &output)); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&output)).read_to_end(&mut v)); | |
| assert_eq!(v, b"foo".to_vec()); | |
| } | |
| #[test] | |
| fn copy_file_src_dir() { | |
| let tmpdir = tmpdir(); | |
| let out = tmpdir.join("out"); | |
| match fs::copy(tmpdir.path(), &out) { | |
| Ok(..) => panic!(), | |
| Err(..) => {} | |
| } | |
| assert!(!out.exists()); | |
| } | |
| #[test] | |
| fn copy_file_preserves_perm_bits() { | |
| let tmpdir = tmpdir(); | |
| let input = tmpdir.join("in.txt"); | |
| let out = tmpdir.join("out.txt"); | |
| let attr = check!(check!(File::create(&input)).metadata()); | |
| let mut p = attr.permissions(); | |
| p.set_readonly(true); | |
| check!(fs::set_permissions(&input, p)); | |
| check!(fs::copy(&input, &out)); | |
| assert!(check!(out.metadata()).permissions().readonly()); | |
| check!(fs::set_permissions(&input, attr.permissions())); | |
| check!(fs::set_permissions(&out, attr.permissions())); | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn copy_file_preserves_streams() { | |
| let tmp = tmpdir(); | |
| check!(check!(File::create(tmp.join("in.txt:bunny"))).write("carrot".as_bytes())); | |
| assert_eq!(check!(fs::copy(tmp.join("in.txt"), tmp.join("out.txt"))), 0); | |
| assert_eq!(check!(tmp.join("out.txt").metadata()).len(), 0); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(tmp.join("out.txt:bunny"))).read_to_end(&mut v)); | |
| assert_eq!(v, b"carrot".to_vec()); | |
| } | |
| #[test] | |
| fn copy_file_returns_metadata_len() { | |
| let tmp = tmpdir(); | |
| let in_path = tmp.join("in.txt"); | |
| let out_path = tmp.join("out.txt"); | |
| check!(check!(File::create(&in_path)).write(b"lettuce")); | |
| #[cfg(windows)] | |
| check!(check!(File::create(tmp.join("in.txt:bunny"))).write(b"carrot")); | |
| let copied_len = check!(fs::copy(&in_path, &out_path)); | |
| assert_eq!(check!(out_path.metadata()).len(), copied_len); | |
| } | |
| #[test] | |
| fn copy_file_follows_dst_symlink() { | |
| let tmp = tmpdir(); | |
| if !got_symlink_permission(&tmp) { | |
| return; | |
| }; | |
| let in_path = tmp.join("in.txt"); | |
| let out_path = tmp.join("out.txt"); | |
| let out_path_symlink = tmp.join("out_symlink.txt"); | |
| check!(fs::write(&in_path, "foo")); | |
| check!(fs::write(&out_path, "bar")); | |
| check!(symlink_file(&out_path, &out_path_symlink)); | |
| check!(fs::copy(&in_path, &out_path_symlink)); | |
| assert!(check!(out_path_symlink.symlink_metadata()).file_type().is_symlink()); | |
| assert_eq!(check!(fs::read(&out_path_symlink)), b"foo".to_vec()); | |
| assert_eq!(check!(fs::read(&out_path)), b"foo".to_vec()); | |
| } | |
| #[test] | |
| fn symlinks_work() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| let input = tmpdir.join("in.txt"); | |
| let out = tmpdir.join("out.txt"); | |
| check!(check!(File::create(&input)).write("foobar".as_bytes())); | |
| check!(symlink_file(&input, &out)); | |
| assert!(check!(out.symlink_metadata()).file_type().is_symlink()); | |
| assert_eq!(check!(fs::metadata(&out)).len(), check!(fs::metadata(&input)).len()); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&out)).read_to_end(&mut v)); | |
| assert_eq!(v, b"foobar".to_vec()); | |
| } | |
| #[test] | |
| fn symlink_noexist() { | |
| // Symlinks can point to things that don't exist | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| // Use a relative path for testing. Symlinks get normalized by Windows, | |
| // so we might not get the same path back for absolute paths | |
| check!(symlink_file(&"foo", &tmpdir.join("bar"))); | |
| assert_eq!(check!(fs::read_link(&tmpdir.join("bar"))).to_str().unwrap(), "foo"); | |
| } | |
| #[test] | |
| fn read_link() { | |
| let tmpdir = tmpdir(); | |
| if cfg!(windows) { | |
| // directory symlink | |
| assert_eq!(check!(fs::read_link(r"C:\Users\All Users")), Path::new(r"C:\ProgramData")); | |
| // junction | |
| assert_eq!(check!(fs::read_link(r"C:\Users\Default User")), Path::new(r"C:\Users\Default")); | |
| // junction with special permissions | |
| // Since not all localized windows versions contain the folder "Documents and Settings" in english, | |
| // we will briefly check, if it exists and otherwise skip the test. Except during CI we will always execute the test. | |
| if Path::new(r"C:\Documents and Settings\").exists() || env::var_os("CI").is_some() { | |
| assert_eq!( | |
| check!(fs::read_link(r"C:\Documents and Settings\")), | |
| Path::new(r"C:\Users") | |
| ); | |
| } | |
| // Check that readlink works with non-drive paths on Windows. | |
| let link = tmpdir.join("link_unc"); | |
| if got_symlink_permission(&tmpdir) { | |
| check!(symlink_dir(r"\\localhost\c$\", &link)); | |
| assert_eq!(check!(fs::read_link(&link)), Path::new(r"\\localhost\c$\")); | |
| }; | |
| } | |
| let link = tmpdir.join("link"); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| check!(symlink_file(&"foo", &link)); | |
| assert_eq!(check!(fs::read_link(&link)).to_str().unwrap(), "foo"); | |
| } | |
| #[test] | |
| fn readlink_not_symlink() { | |
| let tmpdir = tmpdir(); | |
| match fs::read_link(tmpdir.path()) { | |
| Ok(..) => panic!("wanted a failure"), | |
| Err(..) => {} | |
| } | |
| } | |
| #[test] | |
| #[cfg_attr(target_os = "android", ignore = "Android SELinux rules prevent creating hardlinks")] | |
| fn links_work() { | |
| let tmpdir = tmpdir(); | |
| let input = tmpdir.join("in.txt"); | |
| let out = tmpdir.join("out.txt"); | |
| check!(check!(File::create(&input)).write("foobar".as_bytes())); | |
| check!(fs::hard_link(&input, &out)); | |
| assert_eq!(check!(fs::metadata(&out)).len(), check!(fs::metadata(&input)).len()); | |
| assert_eq!(check!(fs::metadata(&out)).len(), check!(input.metadata()).len()); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&out)).read_to_end(&mut v)); | |
| assert_eq!(v, b"foobar".to_vec()); | |
| // can't link to yourself | |
| match fs::hard_link(&input, &input) { | |
| Ok(..) => panic!("wanted a failure"), | |
| Err(..) => {} | |
| } | |
| // can't link to something that doesn't exist | |
| match fs::hard_link(&tmpdir.join("foo"), &tmpdir.join("bar")) { | |
| Ok(..) => panic!("wanted a failure"), | |
| Err(..) => {} | |
| } | |
| } | |
| #[test] | |
| fn chmod_works() { | |
| let tmpdir = tmpdir(); | |
| let file = tmpdir.join("in.txt"); | |
| check!(File::create(&file)); | |
| let attr = check!(fs::metadata(&file)); | |
| assert!(!attr.permissions().readonly()); | |
| let mut p = attr.permissions(); | |
| p.set_readonly(true); | |
| check!(fs::set_permissions(&file, p.clone())); | |
| let attr = check!(fs::metadata(&file)); | |
| assert!(attr.permissions().readonly()); | |
| match fs::set_permissions(&tmpdir.join("foo"), p.clone()) { | |
| Ok(..) => panic!("wanted an error"), | |
| Err(..) => {} | |
| } | |
| p.set_readonly(false); | |
| check!(fs::set_permissions(&file, p)); | |
| } | |
| #[test] | |
| fn fchmod_works() { | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("in.txt"); | |
| let file = check!(File::create(&path)); | |
| let attr = check!(fs::metadata(&path)); | |
| assert!(!attr.permissions().readonly()); | |
| let mut p = attr.permissions(); | |
| p.set_readonly(true); | |
| check!(file.set_permissions(p.clone())); | |
| let attr = check!(fs::metadata(&path)); | |
| assert!(attr.permissions().readonly()); | |
| p.set_readonly(false); | |
| check!(file.set_permissions(p)); | |
| } | |
| #[test] | |
| fn fchmodat_works() { | |
| let tmpdir = tmpdir(); | |
| let file = tmpdir.join("in.txt"); | |
| check!(File::create(&file)); | |
| let attr = check!(fs::metadata(&file)); | |
| assert!(!attr.permissions().readonly()); | |
| let mut p = attr.permissions(); | |
| p.set_readonly(true); | |
| check!(fs::set_permissions_nofollow(&file, p.clone())); | |
| let attr = check!(fs::metadata(&file)); | |
| assert!(attr.permissions().readonly()); | |
| match fs::set_permissions_nofollow(&tmpdir.join("foo"), p.clone()) { | |
| Ok(..) => panic!("wanted an error"), | |
| Err(..) => {} | |
| } | |
| p.set_readonly(false); | |
| check!(fs::set_permissions_nofollow(&file, p)); | |
| } | |
| #[test] | |
| fn sync_doesnt_kill_anything() { | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("in.txt"); | |
| let mut file = check!(File::create(&path)); | |
| check!(file.sync_all()); | |
| check!(file.sync_data()); | |
| check!(file.write(b"foo")); | |
| check!(file.sync_all()); | |
| check!(file.sync_data()); | |
| } | |
| #[test] | |
| fn truncate_works() { | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("in.txt"); | |
| let mut file = check!(File::create(&path)); | |
| check!(file.write(b"foo")); | |
| check!(file.sync_all()); | |
| // Do some simple things with truncation | |
| assert_eq!(check!(file.metadata()).len(), 3); | |
| check!(file.set_len(10)); | |
| assert_eq!(check!(file.metadata()).len(), 10); | |
| check!(file.write(b"bar")); | |
| check!(file.sync_all()); | |
| assert_eq!(check!(file.metadata()).len(), 10); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&path)).read_to_end(&mut v)); | |
| assert_eq!(v, b"foobar\0\0\0\0".to_vec()); | |
| // Truncate to a smaller length, don't seek, and then write something. | |
| // Ensure that the intermediate zeroes are all filled in (we have `seek`ed | |
| // past the end of the file). | |
| check!(file.set_len(2)); | |
| assert_eq!(check!(file.metadata()).len(), 2); | |
| check!(file.write(b"wut")); | |
| check!(file.sync_all()); | |
| assert_eq!(check!(file.metadata()).len(), 9); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&path)).read_to_end(&mut v)); | |
| assert_eq!(v, b"fo\0\0\0\0wut".to_vec()); | |
| } | |
| #[test] | |
| fn open_flavors() { | |
| use crate::fs::OpenOptions as OO; | |
| fn c<T: Clone>(t: &T) -> T { | |
| t.clone() | |
| } | |
| let tmpdir = tmpdir(); | |
| let mut r = OO::new(); | |
| r.read(true); | |
| let mut w = OO::new(); | |
| w.write(true); | |
| let mut rw = OO::new(); | |
| rw.read(true).write(true); | |
| let mut a = OO::new(); | |
| a.append(true); | |
| let mut ra = OO::new(); | |
| ra.read(true).append(true); | |
| // This error string is set by std itself so we are not at the whim of the OS here. | |
| let invalid_options = "creating or truncating a file requires write or append access"; | |
| // Test various combinations of creation modes and access modes. | |
| // | |
| // Allowed: | |
| // creation mode | read | write | read-write | append | read-append | | |
| // :-----------------------|:-----:|:-----:|:----------:|:------:|:-----------:| | |
| // not set (open existing) | X | X | X | X | X | | |
| // create | | X | X | X | X | | |
| // truncate | | X | X | | | | |
| // create and truncate | | X | X | | | | |
| // create_new | | X | X | X | X | | |
| // | |
| // tested in reverse order, so 'create_new' creates the file, and 'open existing' opens it. | |
| // write-only | |
| check!(c(&w).create_new(true).open(&tmpdir.join("a"))); | |
| check!(c(&w).create(true).truncate(true).open(&tmpdir.join("a"))); | |
| check!(c(&w).truncate(true).open(&tmpdir.join("a"))); | |
| check!(c(&w).create(true).open(&tmpdir.join("a"))); | |
| check!(c(&w).open(&tmpdir.join("a"))); | |
| // read-only | |
| error_contains!(c(&r).create_new(true).open(&tmpdir.join("b")), invalid_options); | |
| error_contains!(c(&r).create(true).truncate(true).open(&tmpdir.join("b")), invalid_options); | |
| error_contains!(c(&r).truncate(true).open(&tmpdir.join("b")), invalid_options); | |
| error_contains!(c(&r).create(true).open(&tmpdir.join("b")), invalid_options); | |
| check!(c(&r).open(&tmpdir.join("a"))); // try opening the file created with write_only | |
| // read-write | |
| check!(c(&rw).create_new(true).open(&tmpdir.join("c"))); | |
| check!(c(&rw).create(true).truncate(true).open(&tmpdir.join("c"))); | |
| check!(c(&rw).truncate(true).open(&tmpdir.join("c"))); | |
| check!(c(&rw).create(true).open(&tmpdir.join("c"))); | |
| check!(c(&rw).open(&tmpdir.join("c"))); | |
| // append | |
| check!(c(&a).create_new(true).open(&tmpdir.join("d"))); | |
| error_contains!(c(&a).create(true).truncate(true).open(&tmpdir.join("d")), invalid_options); | |
| error_contains!(c(&a).truncate(true).open(&tmpdir.join("d")), invalid_options); | |
| check!(c(&a).create(true).open(&tmpdir.join("d"))); | |
| check!(c(&a).open(&tmpdir.join("d"))); | |
| // read-append | |
| check!(c(&ra).create_new(true).open(&tmpdir.join("e"))); | |
| error_contains!(c(&ra).create(true).truncate(true).open(&tmpdir.join("e")), invalid_options); | |
| error_contains!(c(&ra).truncate(true).open(&tmpdir.join("e")), invalid_options); | |
| check!(c(&ra).create(true).open(&tmpdir.join("e"))); | |
| check!(c(&ra).open(&tmpdir.join("e"))); | |
| // Test opening a file without setting an access mode | |
| let mut blank = OO::new(); | |
| error_contains!(blank.create(true).open(&tmpdir.join("f")), invalid_options); | |
| // Test write works | |
| check!(check!(File::create(&tmpdir.join("h"))).write("foobar".as_bytes())); | |
| // Test write fails for read-only | |
| check!(r.open(&tmpdir.join("h"))); | |
| { | |
| let mut f = check!(r.open(&tmpdir.join("h"))); | |
| assert!(f.write("wut".as_bytes()).is_err()); | |
| } | |
| // Test write overwrites | |
| { | |
| let mut f = check!(c(&w).open(&tmpdir.join("h"))); | |
| check!(f.write("baz".as_bytes())); | |
| } | |
| { | |
| let mut f = check!(c(&r).open(&tmpdir.join("h"))); | |
| let mut b = vec![0; 6]; | |
| check!(f.read(&mut b)); | |
| assert_eq!(b, "bazbar".as_bytes()); | |
| } | |
| // Test truncate works | |
| { | |
| let mut f = check!(c(&w).truncate(true).open(&tmpdir.join("h"))); | |
| check!(f.write("foo".as_bytes())); | |
| } | |
| assert_eq!(check!(fs::metadata(&tmpdir.join("h"))).len(), 3); | |
| // Test append works | |
| assert_eq!(check!(fs::metadata(&tmpdir.join("h"))).len(), 3); | |
| { | |
| let mut f = check!(c(&a).open(&tmpdir.join("h"))); | |
| check!(f.write("bar".as_bytes())); | |
| } | |
| assert_eq!(check!(fs::metadata(&tmpdir.join("h"))).len(), 6); | |
| // Test .append(true) equals .write(true).append(true) | |
| { | |
| let mut f = check!(c(&w).append(true).open(&tmpdir.join("h"))); | |
| check!(f.write("baz".as_bytes())); | |
| } | |
| assert_eq!(check!(fs::metadata(&tmpdir.join("h"))).len(), 9); | |
| } | |
| #[test] | |
| fn _assert_send_sync() { | |
| fn _assert_send_sync<T: Send + Sync>() {} | |
| _assert_send_sync::<OpenOptions>(); | |
| } | |
| #[test] | |
| fn binary_file() { | |
| let mut bytes = [0; 1024]; | |
| crate::test_helpers::test_rng().fill_bytes(&mut bytes); | |
| let tmpdir = tmpdir(); | |
| check!(check!(File::create(&tmpdir.join("test"))).write(&bytes)); | |
| let mut v = Vec::new(); | |
| check!(check!(File::open(&tmpdir.join("test"))).read_to_end(&mut v)); | |
| assert!(v == &bytes[..]); | |
| } | |
| #[test] | |
| fn write_then_read() { | |
| let mut bytes = [0; 1024]; | |
| crate::test_helpers::test_rng().fill_bytes(&mut bytes); | |
| let tmpdir = tmpdir(); | |
| check!(fs::write(&tmpdir.join("test"), &bytes[..])); | |
| let v = check!(fs::read(&tmpdir.join("test"))); | |
| assert!(v == &bytes[..]); | |
| check!(fs::write(&tmpdir.join("not-utf8"), &[0xFF])); | |
| error_contains!( | |
| fs::read_to_string(&tmpdir.join("not-utf8")), | |
| "stream did not contain valid UTF-8" | |
| ); | |
| let s = "𐁁𐀓𐀠𐀴𐀍"; | |
| check!(fs::write(&tmpdir.join("utf8"), s.as_bytes())); | |
| let string = check!(fs::read_to_string(&tmpdir.join("utf8"))); | |
| assert_eq!(string, s); | |
| } | |
| #[test] | |
| fn file_try_clone() { | |
| let tmpdir = tmpdir(); | |
| let mut f1 = | |
| check!(OpenOptions::new().read(true).write(true).create(true).open(&tmpdir.join("test"))); | |
| let mut f2 = check!(f1.try_clone()); | |
| check!(f1.write_all(b"hello world")); | |
| check!(f1.seek(SeekFrom::Start(2))); | |
| let mut buf = vec![]; | |
| check!(f2.read_to_end(&mut buf)); | |
| assert_eq!(buf, b"llo world"); | |
| drop(f2); | |
| check!(f1.write_all(b"!")); | |
| } | |
| #[test] | |
| #[cfg(not(target_vendor = "win7"))] | |
| fn unlink_readonly() { | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("file"); | |
| check!(File::create(&path)); | |
| let mut perm = check!(fs::metadata(&path)).permissions(); | |
| perm.set_readonly(true); | |
| check!(fs::set_permissions(&path, perm)); | |
| check!(fs::remove_file(&path)); | |
| } | |
| #[test] | |
| fn mkdir_trailing_slash() { | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("file"); | |
| check!(fs::create_dir_all(&path.join("a/"))); | |
| } | |
| #[test] | |
| fn canonicalize_works_simple() { | |
| let tmpdir = tmpdir(); | |
| let tmpdir = fs::canonicalize(tmpdir.path()).unwrap(); | |
| let file = tmpdir.join("test"); | |
| File::create(&file).unwrap(); | |
| assert_eq!(fs::canonicalize(&file).unwrap(), file); | |
| } | |
| #[test] | |
| fn realpath_works() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| let tmpdir = fs::canonicalize(tmpdir.path()).unwrap(); | |
| let file = tmpdir.join("test"); | |
| let dir = tmpdir.join("test2"); | |
| let link = dir.join("link"); | |
| let linkdir = tmpdir.join("test3"); | |
| File::create(&file).unwrap(); | |
| fs::create_dir(&dir).unwrap(); | |
| symlink_file(&file, &link).unwrap(); | |
| symlink_dir(&dir, &linkdir).unwrap(); | |
| assert!(link.symlink_metadata().unwrap().file_type().is_symlink()); | |
| assert_eq!(fs::canonicalize(&tmpdir).unwrap(), tmpdir); | |
| assert_eq!(fs::canonicalize(&file).unwrap(), file); | |
| assert_eq!(fs::canonicalize(&link).unwrap(), file); | |
| assert_eq!(fs::canonicalize(&linkdir).unwrap(), dir); | |
| assert_eq!(fs::canonicalize(&linkdir.join("link")).unwrap(), file); | |
| } | |
| #[test] | |
| fn realpath_works_tricky() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| let tmpdir = fs::canonicalize(tmpdir.path()).unwrap(); | |
| let a = tmpdir.join("a"); | |
| let b = a.join("b"); | |
| let c = b.join("c"); | |
| let d = a.join("d"); | |
| let e = d.join("e"); | |
| let f = a.join("f"); | |
| fs::create_dir_all(&b).unwrap(); | |
| fs::create_dir_all(&d).unwrap(); | |
| File::create(&f).unwrap(); | |
| if cfg!(not(windows)) { | |
| symlink_file("../d/e", &c).unwrap(); | |
| symlink_file("../f", &e).unwrap(); | |
| } | |
| if cfg!(windows) { | |
| symlink_file(r"..\d\e", &c).unwrap(); | |
| symlink_file(r"..\f", &e).unwrap(); | |
| } | |
| assert_eq!(fs::canonicalize(&c).unwrap(), f); | |
| assert_eq!(fs::canonicalize(&e).unwrap(), f); | |
| } | |
| #[test] | |
| fn dir_entry_methods() { | |
| let tmpdir = tmpdir(); | |
| fs::create_dir_all(&tmpdir.join("a")).unwrap(); | |
| File::create(&tmpdir.join("b")).unwrap(); | |
| for file in tmpdir.path().read_dir().unwrap().map(|f| f.unwrap()) { | |
| let fname = file.file_name(); | |
| match fname.to_str() { | |
| Some("a") => { | |
| assert!(file.file_type().unwrap().is_dir()); | |
| assert!(file.metadata().unwrap().is_dir()); | |
| } | |
| Some("b") => { | |
| assert!(file.file_type().unwrap().is_file()); | |
| assert!(file.metadata().unwrap().is_file()); | |
| } | |
| f => panic!("unknown file name: {f:?}"), | |
| } | |
| } | |
| } | |
| #[test] | |
| fn dir_entry_debug() { | |
| let tmpdir = tmpdir(); | |
| File::create(&tmpdir.join("b")).unwrap(); | |
| let mut read_dir = tmpdir.path().read_dir().unwrap(); | |
| let dir_entry = read_dir.next().unwrap().unwrap(); | |
| let actual = format!("{dir_entry:?}"); | |
| let expected = format!("DirEntry({:?})", dir_entry.0.path()); | |
| assert_eq!(actual, expected); | |
| } | |
| #[test] | |
| fn read_dir_not_found() { | |
| let res = fs::read_dir("/path/that/does/not/exist"); | |
| assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); | |
| } | |
| #[test] | |
| fn file_open_not_found() { | |
| let res = File::open("/path/that/does/not/exist"); | |
| assert_eq!(res.err().unwrap().kind(), ErrorKind::NotFound); | |
| } | |
| #[test] | |
| #[cfg_attr( | |
| all(windows, target_arch = "aarch64"), | |
| ignore = "SymLinks not enabled on Arm64 Windows runners https://github.com/actions/partner-runner-images/issues/94" | |
| )] | |
| fn create_dir_all_with_junctions() { | |
| let tmpdir = tmpdir(); | |
| let target = tmpdir.join("target"); | |
| let junction = tmpdir.join("junction"); | |
| let b = junction.join("a/b"); | |
| let link = tmpdir.join("link"); | |
| let d = link.join("c/d"); | |
| fs::create_dir(&target).unwrap(); | |
| check!(junction_point(&target, &junction)); | |
| check!(fs::create_dir_all(&b)); | |
| // the junction itself is not a directory, but `is_dir()` on a Path | |
| // follows links | |
| assert!(junction.is_dir()); | |
| assert!(b.exists()); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| check!(symlink_dir(&target, &link)); | |
| check!(fs::create_dir_all(&d)); | |
| assert!(link.is_dir()); | |
| assert!(d.exists()); | |
| } | |
| #[test] | |
| fn metadata_access_times() { | |
| let start_time = SystemTime::now(); | |
| let tmpdir = tmpdir(); | |
| let b = tmpdir.join("b"); | |
| File::create(&b).unwrap(); | |
| let a = check!(fs::metadata(&tmpdir.path())); | |
| let b = check!(fs::metadata(&b)); | |
| assert_eq!(check!(a.accessed()), check!(a.accessed())); | |
| assert_eq!(check!(a.modified()), check!(a.modified())); | |
| assert_eq!(check!(b.accessed()), check!(b.modified())); | |
| if cfg!(target_vendor = "apple") || cfg!(target_os = "windows") { | |
| check!(a.created()); | |
| check!(b.created()); | |
| } | |
| if cfg!(target_os = "linux") { | |
| // Not always available | |
| match (a.created(), b.created()) { | |
| // It could be that, when the system clock goes backwards (e.g., due time change) | |
| // b, that gets created after a, has a greater creation date than a. | |
| // When such rare case occurs we skip the test, since the test to check that b | |
| // should be created after a would fail. | |
| (Ok(t1), Ok(t2)) => match start_time.elapsed() { | |
| Ok(_) => assert!(t1 <= t2), | |
| Err(_) => {} | |
| }, | |
| (Err(e1), Err(e2)) | |
| if e1.kind() == ErrorKind::Uncategorized | |
| && e2.kind() == ErrorKind::Uncategorized | |
| || e1.kind() == ErrorKind::Unsupported | |
| && e2.kind() == ErrorKind::Unsupported => {} | |
| (a, b) => { | |
| panic!("creation time must be always supported or not supported: {a:?} {b:?}") | |
| } | |
| } | |
| } | |
| } | |
| /// Test creating hard links to symlinks. | |
| #[test] | |
| #[cfg_attr(target_os = "android", ignore = "Android SELinux rules prevent creating hardlinks")] | |
| fn symlink_hard_link() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| // Create "file", a file. | |
| check!(fs::File::create(tmpdir.join("file"))); | |
| // Create "symlink", a symlink to "file". | |
| check!(symlink_file("file", tmpdir.join("symlink"))); | |
| // Create "hard_link", a hard link to "symlink". | |
| check!(fs::hard_link(tmpdir.join("symlink"), tmpdir.join("hard_link"))); | |
| // "hard_link" should appear as a symlink. | |
| assert!(check!(fs::symlink_metadata(tmpdir.join("hard_link"))).file_type().is_symlink()); | |
| // We should be able to open "file" via any of the above names. | |
| let _ = check!(fs::File::open(tmpdir.join("file"))); | |
| assert!(fs::File::open(tmpdir.join("file.renamed")).is_err()); | |
| let _ = check!(fs::File::open(tmpdir.join("symlink"))); | |
| let _ = check!(fs::File::open(tmpdir.join("hard_link"))); | |
| // Rename "file" to "file.renamed". | |
| check!(fs::rename(tmpdir.join("file"), tmpdir.join("file.renamed"))); | |
| // Now, the symlink and the hard link should be dangling. | |
| assert!(fs::File::open(tmpdir.join("file")).is_err()); | |
| let _ = check!(fs::File::open(tmpdir.join("file.renamed"))); | |
| assert!(fs::File::open(tmpdir.join("symlink")).is_err()); | |
| assert!(fs::File::open(tmpdir.join("hard_link")).is_err()); | |
| // The symlink and the hard link should both still point to "file". | |
| assert!(fs::read_link(tmpdir.join("file")).is_err()); | |
| assert!(fs::read_link(tmpdir.join("file.renamed")).is_err()); | |
| assert_eq!(check!(fs::read_link(tmpdir.join("symlink"))), Path::new("file")); | |
| assert_eq!(check!(fs::read_link(tmpdir.join("hard_link"))), Path::new("file")); | |
| // Remove "file.renamed". | |
| check!(fs::remove_file(tmpdir.join("file.renamed"))); | |
| // Now, we can't open the file by any name. | |
| assert!(fs::File::open(tmpdir.join("file")).is_err()); | |
| assert!(fs::File::open(tmpdir.join("file.renamed")).is_err()); | |
| assert!(fs::File::open(tmpdir.join("symlink")).is_err()); | |
| assert!(fs::File::open(tmpdir.join("hard_link")).is_err()); | |
| // "hard_link" should still appear as a symlink. | |
| assert!(check!(fs::symlink_metadata(tmpdir.join("hard_link"))).file_type().is_symlink()); | |
| } | |
| /// Ensure `fs::create_dir` works on Windows with longer paths. | |
| #[test] | |
| #[cfg(windows)] | |
| fn create_dir_long_paths() { | |
| use crate::ffi::OsStr; | |
| use crate::iter; | |
| use crate::os::windows::ffi::OsStrExt; | |
| const PATH_LEN: usize = 247; | |
| let tmpdir = tmpdir(); | |
| let mut path = tmpdir.path().to_path_buf(); | |
| path.push("a"); | |
| let mut path = path.into_os_string(); | |
| let utf16_len = path.encode_wide().count(); | |
| if utf16_len >= PATH_LEN { | |
| // Skip the test in the unlikely event the local user has a long temp directory path. | |
| // This should not affect CI. | |
| return; | |
| } | |
| // Increase the length of the path. | |
| path.extend(iter::repeat(OsStr::new("a")).take(PATH_LEN - utf16_len)); | |
| // This should succeed. | |
| fs::create_dir(&path).unwrap(); | |
| // This will fail if the path isn't converted to verbatim. | |
| path.push("a"); | |
| fs::create_dir(&path).unwrap(); | |
| // #90940: Ensure an empty path returns the "Not Found" error. | |
| let path = Path::new(""); | |
| assert_eq!(path.canonicalize().unwrap_err().kind(), crate::io::ErrorKind::NotFound); | |
| } | |
| /// Ensure ReadDir works on large directories. | |
| /// Regression test for https://github.com/rust-lang/rust/issues/93384. | |
| #[test] | |
| fn read_large_dir() { | |
| let tmpdir = tmpdir(); | |
| let count = if cfg!(miri) { 1024 } else { 32 * 1024 }; | |
| for i in 0..count { | |
| check!(fs::File::create(tmpdir.join(&i.to_string()))); | |
| } | |
| for entry in fs::read_dir(tmpdir.path()).unwrap() { | |
| entry.unwrap(); | |
| } | |
| } | |
| /// Test the fallback for getting the metadata of files like hiberfil.sys that | |
| /// Windows holds a special lock on, preventing normal means of querying | |
| /// metadata. See #96980. | |
| /// | |
| /// Note this fails in CI because `hiberfil.sys` does not actually exist there. | |
| /// Therefore it's marked as ignored. | |
| #[test] | |
| #[ignore] | |
| #[cfg(windows)] | |
| fn hiberfil_sys() { | |
| let hiberfil = Path::new(r"C:\hiberfil.sys"); | |
| assert_eq!(true, hiberfil.try_exists().unwrap()); | |
| fs::symlink_metadata(hiberfil).unwrap(); | |
| fs::metadata(hiberfil).unwrap(); | |
| assert_eq!(true, hiberfil.exists()); | |
| } | |
| /// Test that two different ways of obtaining the FileType give the same result. | |
| /// Cf. https://github.com/rust-lang/rust/issues/104900 | |
| #[test] | |
| fn test_eq_direntry_metadata() { | |
| let tmpdir = tmpdir(); | |
| let file_path = tmpdir.join("file"); | |
| File::create(file_path).unwrap(); | |
| for e in fs::read_dir(tmpdir.path()).unwrap() { | |
| let e = e.unwrap(); | |
| let p = e.path(); | |
| let ft1 = e.file_type().unwrap(); | |
| let ft2 = p.metadata().unwrap().file_type(); | |
| assert_eq!(ft1, ft2); | |
| } | |
| } | |
| /// Test that windows file type equality is not affected by attributes unrelated | |
| /// to the file type. | |
| #[test] | |
| #[cfg(target_os = "windows")] | |
| fn test_eq_windows_file_type() { | |
| let tmpdir = tmpdir(); | |
| let file1 = File::create(tmpdir.join("file1")).unwrap(); | |
| let file2 = File::create(tmpdir.join("file2")).unwrap(); | |
| assert_eq!(file1.metadata().unwrap().file_type(), file2.metadata().unwrap().file_type()); | |
| // Change the readonly attribute of one file. | |
| let mut perms = file1.metadata().unwrap().permissions(); | |
| perms.set_readonly(true); | |
| file1.set_permissions(perms.clone()).unwrap(); | |
| #[cfg(target_vendor = "win7")] | |
| let _g = ReadonlyGuard { file: &file1, perms }; | |
| assert_eq!(file1.metadata().unwrap().file_type(), file2.metadata().unwrap().file_type()); | |
| // Reset the attribute before the `TmpDir`'s drop that removes the | |
| // associated directory, which fails with a `PermissionDenied` error when | |
| // running under Windows 7. | |
| #[cfg(target_vendor = "win7")] | |
| struct ReadonlyGuard<'f> { | |
| file: &'f File, | |
| perms: fs::Permissions, | |
| } | |
| #[cfg(target_vendor = "win7")] | |
| impl<'f> Drop for ReadonlyGuard<'f> { | |
| fn drop(&mut self) { | |
| self.perms.set_readonly(false); | |
| let res = self.file.set_permissions(self.perms.clone()); | |
| if !thread::panicking() { | |
| res.unwrap(); | |
| } | |
| } | |
| } | |
| } | |
| /// Regression test for https://github.com/rust-lang/rust/issues/50619. | |
| #[test] | |
| #[cfg(target_os = "linux")] | |
| #[cfg_attr(miri, ignore)] // Cannot spawn processes on Miri | |
| fn test_read_dir_infinite_loop() { | |
| use crate::io::ErrorKind; | |
| use crate::process::Command; | |
| // Create a zombie child process | |
| let Ok(mut child) = Command::new("echo").spawn() else { return }; | |
| // Make sure the process is (un)dead | |
| match child.kill() { | |
| // InvalidInput means the child already exited | |
| Err(e) if e.kind() != ErrorKind::InvalidInput => return, | |
| _ => {} | |
| } | |
| // open() on this path will succeed, but readdir() will fail | |
| let id = child.id(); | |
| let path = format!("/proc/{id}/net"); | |
| // Skip the test if we can't open the directory in the first place | |
| let Ok(dir) = fs::read_dir(path) else { return }; | |
| // Check for duplicate errors | |
| assert!(dir.filter(|e| e.is_err()).take(2).count() < 2); | |
| } | |
| #[test] | |
| fn rename_directory() { | |
| let tmpdir = tmpdir(); | |
| let old_path = tmpdir.join("foo/bar/baz"); | |
| fs::create_dir_all(&old_path).unwrap(); | |
| let test_file = &old_path.join("temp.txt"); | |
| File::create(test_file).unwrap(); | |
| let new_path = tmpdir.join("quux/blat"); | |
| fs::create_dir_all(&new_path).unwrap(); | |
| fs::rename(&old_path, &new_path.join("newdir")).unwrap(); | |
| assert!(new_path.join("newdir").is_dir()); | |
| assert!(new_path.join("newdir/temp.txt").exists()); | |
| } | |
| #[test] | |
| fn test_file_times() { | |
| #[cfg(target_vendor = "apple")] | |
| use crate::os::darwin::fs::FileTimesExt; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| let file = File::create(tmp.join("foo")).unwrap(); | |
| let mut times = FileTimes::new(); | |
| let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345); | |
| let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321); | |
| times = times.set_accessed(accessed).set_modified(modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| times = times.set_created(created); | |
| } | |
| match file.set_times(times) { | |
| // Allow unsupported errors on platforms which don't support setting times. | |
| #[cfg(not(any( | |
| windows, | |
| all( | |
| unix, | |
| not(any( | |
| target_os = "android", | |
| target_os = "redox", | |
| target_os = "espidf", | |
| target_os = "horizon" | |
| )) | |
| ) | |
| )))] | |
| Err(e) if e.kind() == ErrorKind::Unsupported => return, | |
| Err(e) => panic!("error setting file times: {e:?}"), | |
| Ok(_) => {} | |
| } | |
| let metadata = file.metadata().unwrap(); | |
| assert_eq!(metadata.accessed().unwrap(), accessed); | |
| assert_eq!(metadata.modified().unwrap(), modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| assert_eq!(metadata.created().unwrap(), created); | |
| } | |
| } | |
| #[test] | |
| #[cfg(target_vendor = "apple")] | |
| fn test_file_times_pre_epoch_with_nanos() { | |
| use crate::os::darwin::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| let file = File::create(tmp.join("foo")).unwrap(); | |
| for (accessed, modified, created) in [ | |
| // The first round is to set filetimes to something we know works, but this time | |
| // it's validated with nanoseconds as well which probe the numeric boundary. | |
| ( | |
| SystemTime::UNIX_EPOCH + Duration::new(12345, 1), | |
| SystemTime::UNIX_EPOCH + Duration::new(54321, 100_000_000), | |
| SystemTime::UNIX_EPOCH + Duration::new(32123, 999_999_999), | |
| ), | |
| // The second rounds uses pre-epoch dates along with nanoseconds that probe | |
| // the numeric boundary. | |
| ( | |
| SystemTime::UNIX_EPOCH - Duration::new(1, 1), | |
| SystemTime::UNIX_EPOCH - Duration::new(60, 100_000_000), | |
| SystemTime::UNIX_EPOCH - Duration::new(3600, 999_999_999), | |
| ), | |
| ] { | |
| let mut times = FileTimes::new(); | |
| times = times.set_accessed(accessed).set_modified(modified).set_created(created); | |
| file.set_times(times).unwrap(); | |
| let metadata = file.metadata().unwrap(); | |
| assert_eq!(metadata.accessed().unwrap(), accessed); | |
| assert_eq!(metadata.modified().unwrap(), modified); | |
| assert_eq!(metadata.created().unwrap(), created); | |
| } | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| fn windows_unix_socket_exists() { | |
| use crate::sys::{c, net}; | |
| use crate::{mem, ptr}; | |
| let tmp = tmpdir(); | |
| let socket_path = tmp.join("socket"); | |
| // std doesn't currently support Unix sockets on Windows so manually create one here. | |
| net::init(); | |
| unsafe { | |
| let socket = c::WSASocketW( | |
| c::AF_UNIX as i32, | |
| c::SOCK_STREAM, | |
| 0, | |
| ptr::null_mut(), | |
| 0, | |
| c::WSA_FLAG_OVERLAPPED | c::WSA_FLAG_NO_HANDLE_INHERIT, | |
| ); | |
| // AF_UNIX is not supported on earlier versions of Windows, | |
| // so skip this test if it's unsupported and we're not in CI. | |
| if socket == c::INVALID_SOCKET { | |
| let error = c::WSAGetLastError(); | |
| if env::var_os("CI").is_none() && error == c::WSAEAFNOSUPPORT { | |
| return; | |
| } else { | |
| panic!("Creating AF_UNIX socket failed (OS error {error})"); | |
| } | |
| } | |
| let mut addr = c::SOCKADDR_UN { sun_family: c::AF_UNIX, sun_path: mem::zeroed() }; | |
| let bytes = socket_path.as_os_str().as_encoded_bytes(); | |
| let bytes = core::slice::from_raw_parts(bytes.as_ptr().cast::<i8>(), bytes.len()); | |
| addr.sun_path[..bytes.len()].copy_from_slice(bytes); | |
| let len = size_of_val(&addr) as i32; | |
| let result = c::bind(socket, (&raw const addr).cast::<c::SOCKADDR>(), len); | |
| c::closesocket(socket); | |
| assert_eq!(result, 0); | |
| } | |
| // Make sure all ways of testing a file exist work for a Unix socket. | |
| assert_eq!(socket_path.exists(), true); | |
| assert_eq!(socket_path.try_exists().unwrap(), true); | |
| assert_eq!(socket_path.metadata().is_ok(), true); | |
| } | |
| #[cfg(windows)] | |
| #[test] | |
| fn test_hidden_file_truncation() { | |
| // Make sure that File::create works on an existing hidden file. See #115745. | |
| let tmpdir = tmpdir(); | |
| let path = tmpdir.join("hidden_file.txt"); | |
| // Create a hidden file. | |
| const FILE_ATTRIBUTE_HIDDEN: u32 = 2; | |
| let mut file = OpenOptions::new() | |
| .write(true) | |
| .create_new(true) | |
| .attributes(FILE_ATTRIBUTE_HIDDEN) | |
| .open(&path) | |
| .unwrap(); | |
| file.write("hidden world!".as_bytes()).unwrap(); | |
| file.flush().unwrap(); | |
| drop(file); | |
| // Create a new file by truncating the existing one. | |
| let file = File::create(&path).unwrap(); | |
| let metadata = file.metadata().unwrap(); | |
| assert_eq!(metadata.len(), 0); | |
| } | |
| // See https://github.com/rust-lang/rust/pull/131072 for more details about why | |
| // these two tests are disabled under Windows 7 here. | |
| #[test] | |
| #[cfg_attr(target_vendor = "win7", ignore = "Unsupported under Windows 7.")] | |
| fn test_rename_file_over_open_file() { | |
| // Make sure that std::fs::rename works if the target file is already opened with FILE_SHARE_DELETE. See #123985. | |
| let tmpdir = tmpdir(); | |
| // Create source with test data to read. | |
| let source_path = tmpdir.join("source_file.txt"); | |
| fs::write(&source_path, b"source hello world").unwrap(); | |
| // Create target file with test data to read; | |
| let target_path = tmpdir.join("target_file.txt"); | |
| fs::write(&target_path, b"target hello world").unwrap(); | |
| // Open target file | |
| let target_file = fs::File::open(&target_path).unwrap(); | |
| // Rename source | |
| fs::rename(source_path, &target_path).unwrap(); | |
| core::mem::drop(target_file); | |
| assert_eq!(fs::read(target_path).unwrap(), b"source hello world"); | |
| } | |
| #[test] | |
| #[cfg_attr(target_vendor = "win7", ignore = "Unsupported under Windows 7.")] | |
| fn test_rename_directory_to_non_empty_directory() { | |
| // Renaming a directory over a non-empty existing directory should fail. | |
| let tmpdir: TempDir = tmpdir(); | |
| let source_path = tmpdir.join("source_directory"); | |
| let target_path = tmpdir.join("target_directory"); | |
| fs::create_dir(&source_path).unwrap(); | |
| fs::create_dir(&target_path).unwrap(); | |
| fs::write(target_path.join("target_file.txt"), b"target hello world").unwrap(); | |
| let err = fs::rename(source_path, target_path).unwrap_err(); | |
| assert_matches!( | |
| err.kind(), | |
| // On ext4, ntfs, apfs, tmpfs, and btrfs `DirectoryNotEmpty` is returned. | |
| // On xfs `AlreadyExists` is returned. | |
| ErrorKind::DirectoryNotEmpty | ErrorKind::AlreadyExists, | |
| "Expected DirectoryNotEmpty or AlreadyExists error, got {err}" | |
| ); | |
| } | |
| #[test] | |
| fn test_rename_symlink() { | |
| let tmpdir = tmpdir(); | |
| if !got_symlink_permission(&tmpdir) { | |
| return; | |
| }; | |
| let original = tmpdir.join("original"); | |
| let dest = tmpdir.join("dest"); | |
| let not_exist = Path::new("does not exist"); | |
| symlink_file(not_exist, &original).unwrap(); | |
| fs::rename(&original, &dest).unwrap(); | |
| // Make sure that renaming `original` to `dest` preserves the symlink. | |
| assert_eq!(fs::read_link(&dest).unwrap().as_path(), not_exist); | |
| } | |
| #[test] | |
| #[cfg(windows)] | |
| #[cfg_attr( | |
| all(windows, target_arch = "aarch64"), | |
| ignore = "SymLinks not enabled on Arm64 Windows runners https://github.com/actions/partner-runner-images/issues/94" | |
| )] | |
| fn test_rename_junction() { | |
| let tmpdir = tmpdir(); | |
| let original = tmpdir.join("original"); | |
| let dest = tmpdir.join("dest"); | |
| let not_exist = Path::new("does not exist"); | |
| junction_point(¬_exist, &original).unwrap(); | |
| fs::rename(&original, &dest).unwrap(); | |
| // Make sure that renaming `original` to `dest` preserves the junction point. | |
| // Junction links are always absolute so we just check the file name is correct. | |
| assert_eq!(fs::read_link(&dest).unwrap().file_name(), Some(not_exist.as_os_str())); | |
| } | |
| #[test] | |
| fn test_open_options_invalid_combinations() { | |
| use crate::fs::OpenOptions as OO; | |
| let test_cases: &[(fn() -> OO, &str)] = &[ | |
| (|| OO::new().create(true).read(true).clone(), "create without write"), | |
| (|| OO::new().create_new(true).read(true).clone(), "create_new without write"), | |
| (|| OO::new().truncate(true).read(true).clone(), "truncate without write"), | |
| (|| OO::new().truncate(true).append(true).clone(), "truncate with append"), | |
| ]; | |
| for (make_opts, desc) in test_cases { | |
| let opts = make_opts(); | |
| let result = opts.open("nonexistent.txt"); | |
| assert!(result.is_err(), "{desc} should fail"); | |
| let err = result.unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::InvalidInput, "{desc} - wrong error kind"); | |
| assert_eq!( | |
| err.to_string(), | |
| "creating or truncating a file requires write or append access", | |
| "{desc} - wrong error message" | |
| ); | |
| } | |
| let result = OO::new().open("nonexistent.txt"); | |
| assert!(result.is_err(), "no access mode should fail"); | |
| let err = result.unwrap_err(); | |
| assert_eq!(err.kind(), ErrorKind::InvalidInput); | |
| assert_eq!(err.to_string(), "must specify at least one of read, write, or append access"); | |
| } | |
| #[test] | |
| fn test_fs_set_times() { | |
| #[cfg(target_vendor = "apple")] | |
| use crate::os::darwin::fs::FileTimesExt; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| let path = tmp.join("foo"); | |
| File::create(&path).unwrap(); | |
| let mut times = FileTimes::new(); | |
| let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345); | |
| let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321); | |
| times = times.set_accessed(accessed).set_modified(modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| times = times.set_created(created); | |
| } | |
| match fs::set_times(&path, times) { | |
| // Allow unsupported errors on platforms which don't support setting times. | |
| #[cfg(not(any( | |
| windows, | |
| all( | |
| unix, | |
| not(any( | |
| target_os = "android", | |
| target_os = "redox", | |
| target_os = "espidf", | |
| target_os = "horizon" | |
| )) | |
| ) | |
| )))] | |
| Err(e) if e.kind() == ErrorKind::Unsupported => return, | |
| Err(e) => panic!("error setting file times: {e:?}"), | |
| Ok(_) => {} | |
| } | |
| let metadata = fs::metadata(&path).unwrap(); | |
| assert_eq!(metadata.accessed().unwrap(), accessed); | |
| assert_eq!(metadata.modified().unwrap(), modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| assert_eq!(metadata.created().unwrap(), created); | |
| } | |
| } | |
| #[test] | |
| fn test_fs_set_times_on_dir() { | |
| #[cfg(target_vendor = "apple")] | |
| use crate::os::darwin::fs::FileTimesExt; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| let dir_path = tmp.join("testdir"); | |
| fs::create_dir(&dir_path).unwrap(); | |
| let mut times = FileTimes::new(); | |
| let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345); | |
| let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321); | |
| times = times.set_accessed(accessed).set_modified(modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| times = times.set_created(created); | |
| } | |
| match fs::set_times(&dir_path, times) { | |
| // Allow unsupported errors on platforms which don't support setting times. | |
| #[cfg(not(any( | |
| windows, | |
| all( | |
| unix, | |
| not(any( | |
| target_os = "android", | |
| target_os = "redox", | |
| target_os = "espidf", | |
| target_os = "horizon" | |
| )) | |
| ) | |
| )))] | |
| Err(e) if e.kind() == ErrorKind::Unsupported => return, | |
| Err(e) => panic!("error setting directory times: {e:?}"), | |
| Ok(_) => {} | |
| } | |
| let metadata = fs::metadata(&dir_path).unwrap(); | |
| assert_eq!(metadata.accessed().unwrap(), accessed); | |
| assert_eq!(metadata.modified().unwrap(), modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| assert_eq!(metadata.created().unwrap(), created); | |
| } | |
| } | |
| #[test] | |
| fn test_fs_set_times_follows_symlink() { | |
| #[cfg(target_vendor = "apple")] | |
| use crate::os::darwin::fs::FileTimesExt; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| if !got_symlink_permission(&tmp) { | |
| return; | |
| } | |
| // Create a target file | |
| let target = tmp.join("target"); | |
| File::create(&target).unwrap(); | |
| // Create a symlink to the target | |
| #[cfg(unix)] | |
| let link = tmp.join("link"); | |
| #[cfg(unix)] | |
| crate::os::unix::fs::symlink(&target, &link).unwrap(); | |
| #[cfg(windows)] | |
| let link = tmp.join("link.txt"); | |
| #[cfg(windows)] | |
| crate::os::windows::fs::symlink_file(&target, &link).unwrap(); | |
| // Get the symlink's own modified time BEFORE calling set_times (to compare later) | |
| // We don't check accessed time because reading metadata may update atime on some platforms. | |
| let link_metadata_before = fs::symlink_metadata(&link).unwrap(); | |
| let link_modified_before = link_metadata_before.modified().unwrap(); | |
| let mut times = FileTimes::new(); | |
| let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345); | |
| let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321); | |
| times = times.set_accessed(accessed).set_modified(modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| times = times.set_created(created); | |
| } | |
| // Call fs::set_times on the symlink - it should follow the link and modify the target | |
| match fs::set_times(&link, times) { | |
| // Allow unsupported errors on platforms which don't support setting times. | |
| #[cfg(not(any( | |
| windows, | |
| all( | |
| unix, | |
| not(any( | |
| target_os = "android", | |
| target_os = "redox", | |
| target_os = "espidf", | |
| target_os = "horizon" | |
| )) | |
| ) | |
| )))] | |
| Err(e) if e.kind() == ErrorKind::Unsupported => return, | |
| Err(e) => panic!("error setting file times through symlink: {e:?}"), | |
| Ok(_) => {} | |
| } | |
| // Verify that the TARGET file's times were changed (following the symlink) | |
| let target_metadata = fs::metadata(&target).unwrap(); | |
| assert_eq!( | |
| target_metadata.accessed().unwrap(), | |
| accessed, | |
| "target file accessed time should match" | |
| ); | |
| assert_eq!( | |
| target_metadata.modified().unwrap(), | |
| modified, | |
| "target file modified time should match" | |
| ); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| assert_eq!( | |
| target_metadata.created().unwrap(), | |
| created, | |
| "target file created time should match" | |
| ); | |
| } | |
| // Also verify through the symlink (fs::metadata follows symlinks) | |
| let link_followed_metadata = fs::metadata(&link).unwrap(); | |
| assert_eq!(link_followed_metadata.accessed().unwrap(), accessed); | |
| assert_eq!(link_followed_metadata.modified().unwrap(), modified); | |
| // Verify that the SYMLINK ITSELF was NOT modified | |
| // Note: We only check modified time, not accessed time, because reading the symlink | |
| // metadata may update its atime on some platforms (e.g., Linux). | |
| let link_metadata_after = fs::symlink_metadata(&link).unwrap(); | |
| assert_eq!( | |
| link_metadata_after.modified().unwrap(), | |
| link_modified_before, | |
| "symlink's own modified time should not change" | |
| ); | |
| } | |
| #[test] | |
| fn test_fs_set_times_nofollow() { | |
| #[cfg(target_vendor = "apple")] | |
| use crate::os::darwin::fs::FileTimesExt; | |
| #[cfg(windows)] | |
| use crate::os::windows::fs::FileTimesExt; | |
| let tmp = tmpdir(); | |
| if !got_symlink_permission(&tmp) { | |
| return; | |
| } | |
| // Create a target file and a symlink to it | |
| let target = tmp.join("target"); | |
| File::create(&target).unwrap(); | |
| #[cfg(unix)] | |
| let link = tmp.join("link"); | |
| #[cfg(unix)] | |
| crate::os::unix::fs::symlink(&target, &link).unwrap(); | |
| #[cfg(windows)] | |
| let link = tmp.join("link.txt"); | |
| #[cfg(windows)] | |
| crate::os::windows::fs::symlink_file(&target, &link).unwrap(); | |
| let mut times = FileTimes::new(); | |
| let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(11111); | |
| let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(22222); | |
| times = times.set_accessed(accessed).set_modified(modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| let created = SystemTime::UNIX_EPOCH + Duration::from_secs(33333); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| times = times.set_created(created); | |
| } | |
| // Set times on the symlink itself (not following it) | |
| match fs::set_times_nofollow(&link, times) { | |
| // Allow unsupported errors on platforms which don't support setting times. | |
| #[cfg(not(any( | |
| windows, | |
| all( | |
| unix, | |
| not(any( | |
| target_os = "android", | |
| target_os = "redox", | |
| target_os = "espidf", | |
| target_os = "horizon" | |
| )) | |
| ) | |
| )))] | |
| Err(e) if e.kind() == ErrorKind::Unsupported => return, | |
| Err(e) => panic!("error setting symlink times: {e:?}"), | |
| Ok(_) => {} | |
| } | |
| // Read symlink metadata (without following) | |
| let metadata = fs::symlink_metadata(&link).unwrap(); | |
| assert_eq!(metadata.accessed().unwrap(), accessed); | |
| assert_eq!(metadata.modified().unwrap(), modified); | |
| #[cfg(any(windows, target_vendor = "apple"))] | |
| { | |
| assert_eq!(metadata.created().unwrap(), created); | |
| } | |
| // Verify that the target file's times were NOT changed | |
| let target_metadata = fs::metadata(&target).unwrap(); | |
| assert_ne!(target_metadata.accessed().unwrap(), accessed); | |
| assert_ne!(target_metadata.modified().unwrap(), modified); | |
| } | |
| #[test] | |
| fn test_dir_smoke_test() { | |
| let tmpdir = tmpdir(); | |
| let dir = Dir::open(tmpdir.path()); | |
| check!(dir); | |
| } | |
| #[test] | |
| fn test_dir_read_file() { | |
| let tmpdir = tmpdir(); | |
| let mut f = check!(File::create(tmpdir.join("foo.txt"))); | |
| check!(f.write_all(b"bar")); | |
| drop(f); | |
| let dir = check!(Dir::open(tmpdir.path())); | |
| let f = check!(dir.open_file("foo.txt")); | |
| let buf = check!(io::read_to_string(f)); | |
| assert_eq!("bar", &buf); | |
| let f = check!(dir.open_file(tmpdir.join("foo.txt"))); | |
| let buf = check!(io::read_to_string(f)); | |
| assert_eq!("bar", &buf); | |
| } | |
| #[test] | |
| fn test_dir_metadata() { | |
| let tmpdir = tmpdir(); | |
| let dir = check!(Dir::open(tmpdir.path())); | |
| let metadata = check!(dir.metadata()); | |
| assert!(metadata.is_dir()); | |
| } | |
| #[test] | |
| fn test_dir_write_file() { | |
| let tmpdir = tmpdir(); | |
| let dir = check!(Dir::open(tmpdir.path())); | |
| let mut f = check!(dir.open_file_with("foo.txt", &OpenOptions::new().write(true).create(true))); | |
| check!(f.write(b"bar")); | |
| check!(f.flush()); | |
| drop(f); | |
| let mut f = check!(File::open(tmpdir.join("foo.txt"))); | |
| let mut buf = [0u8; 3]; | |
| check!(f.read_exact(&mut buf)); | |
| assert_eq!(b"bar", &buf); | |
| } | |
| #[test] | |
| fn test_dir_remove_file() { | |
| let tmpdir = tmpdir(); | |
| let mut f = check!(File::create(tmpdir.join("foo.txt"))); | |
| check!(f.write(b"bar")); | |
| check!(f.flush()); | |
| drop(f); | |
| let dir = check!(Dir::open(tmpdir.path())); | |
| check!(dir.remove_file("foo.txt")); | |
| assert!(!matches!(exists(tmpdir.join("foo.txt")), Ok(true))); | |
| } | |
| #[test] | |
| fn test_dir_rename_file() { | |
| let tmpdir = tmpdir(); | |
| let mut f = check!(File::create(tmpdir.join("foo.txt"))); | |
| check!(f.write_all(b"bar")); | |
| drop(f); | |
| let dir = check!(Dir::open(tmpdir.path())); | |
| check!(dir.rename("foo.txt", &dir, "baz.txt")); | |
| let mut f = check!(File::open(tmpdir.join("baz.txt"))); | |
| let mut buf = [0u8; 3]; | |
| check!(f.read_exact(&mut buf)); | |
| assert_eq!(b"bar", &buf); | |
| } |