diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs index bcaafcfee787a..d9f2bc453755c 100644 --- a/library/std/src/fs/tests.rs +++ b/library/std/src/fs/tests.rs @@ -1,23 +1,22 @@ use rand::RngCore; +use std::fs::{self, File, FileTimes, OpenOptions, TryLockError}; +use std::io::prelude::*; +use std::io::{BorrowedBuf, ErrorKind, SeekFrom}; +use std::mem::MaybeUninit; +use std::path::Path; +use std::sync::Arc; +use std::thread; +use std::time::{Duration, Instant, SystemTime}; -use crate::assert_matches::assert_matches; -use crate::fs::{self, File, FileTimes, OpenOptions, TryLockError}; -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; +use std::os::unix::fs::{symlink as symlink_file, symlink as symlink_dir, PermissionsExt}; #[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::{env, str, thread}; +use std::os::windows::fs::{ + junction_point, symlink_dir, symlink_file, OpenOptionsExt, FileExt as WindowsFileExt, +}; + +use crate::assert_matches::assert_matches; +use crate::test_helpers::{tmpdir, TempDir}; macro_rules! check { ($e:expr) => { @@ -34,7 +33,12 @@ macro_rules! error { match $e { Ok(_) => panic!("Unexpected success. Should've been: {:?}", $s), Err(ref err) => { - assert!(err.raw_os_error() == Some($s), "`{}` did not have a code of `{}`", err, $s) + assert!( + err.raw_os_error() == Some($s), + "`{}` did not have a code of `{}`", + err, + $s + ) } } }; @@ -52,26 +56,23 @@ macro_rules! error_contains { match $e { Ok(_) => panic!("Unexpected success. Should've been: {:?}", $s), Err(ref err) => { - assert!(err.to_string().contains($s), "`{}` did not contain `{}`", err, $s) + 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() { + if !cfg!(windows) || std::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, } @@ -81,103 +82,88 @@ pub fn got_symlink_permission(tmpdir: &TempDir) -> bool { 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 filename = tmpdir.join("file_rt_io_file_test.txt"); { - let mut write_stream = check!(File::create(filename)); + 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); + let mut read_stream = check!(File::open(&filename)); + let mut read_buf = [0u8; 1028]; + let n = check!(read_stream.read(&mut read_buf)); + assert_eq!(&read_buf[..n], message.as_bytes()); } - check!(fs::remove_file(filename)); + check!(fs::remove_file(&filename)); } #[test] fn invalid_path_raises() { let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_that_does_not_exist.txt"); - let result = File::open(filename); - + let filename = tmpdir.join("file_that_does_not_exist.txt"); + let result = File::open(&filename); #[cfg(all(unix, not(target_os = "vxworks")))] error!(result, "No such file or directory"); #[cfg(target_os = "vxworks")] error!(result, "no such file or directory"); #[cfg(windows)] - error!(result, 2); // ERROR_FILE_NOT_FOUND + error!(result, 2); } #[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 result = fs::remove_file(filename); - + let filename = tmpdir.join("file_another_file_that_does_not_exist.txt"); + let result = fs::remove_file(&filename); #[cfg(all(unix, not(target_os = "vxworks")))] error!(result, "No such file or directory"); #[cfg(target_os = "vxworks")] error!(result, "no such file or directory"); #[cfg(windows)] - error!(result, 2); // ERROR_FILE_NOT_FOUND + error!(result, 2); } #[test] fn file_test_io_non_positional_read() { let message: &str = "ten-four"; - let mut read_mem = [0; 8]; + let mut read_mem = [0u8; 8]; let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_rt_io_file_test_positional.txt"); + let filename = tmpdir.join("file_rt_io_file_test_positional.txt"); { - let mut rw_stream = check!(File::create(filename)); + 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)); - } + let mut read_stream = check!(File::open(&filename)); + check!(read_stream.read(&mut read_mem[0..4])); + check!(read_stream.read(&mut read_mem[4..8])); } - check!(fs::remove_file(filename)); - let read_str = str::from_utf8(&read_mem).unwrap(); + check!(fs::remove_file(&filename)); + let read_str = std::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 mut read_mem = [0u8; 256]; + let set_cursor = 4u64; let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_rt_io_file_test_seeking.txt"); + let filename = tmpdir.join("file_rt_io_file_test_seeking.txt"); { - let mut rw_stream = check!(File::create(filename)); + let mut rw_stream = check!(File::create(&filename)); check!(rw_stream.write(message.as_bytes())); } { - let mut read_stream = check!(File::open(filename)); + 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()); + let pre = check!(read_stream.stream_position()); check!(read_stream.read(&mut read_mem)); - tell_pos_post_read = check!(read_stream.stream_position()); + let post = check!(read_stream.stream_position()); + assert_eq!(pre, set_cursor); + assert_eq!(post, message.len() as u64); } - 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); + check!(fs::remove_file(&filename)); + assert_eq!(std::str::from_utf8(&read_mem[..4]).unwrap(), &message[4..]); } #[test] @@ -185,23 +171,22 @@ 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 seek_idx = 3u64; + let mut read_mem = [0u8; 13]; let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_rt_io_file_test_seek_and_write.txt"); + let filename = tmpdir.join("file_rt_io_file_test_seek_and_write.txt"); { - let mut rw_stream = check!(File::create(filename)); + 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)); + 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); + check!(fs::remove_file(&filename)); + assert_eq!(std::str::from_utf8(&read_mem).unwrap(), final_msg); } #[test] @@ -223,11 +208,9 @@ fn file_test_io_seek_and_write() { )] 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 + 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!(f1.lock_shared()); check!(f2.lock_shared()); check!(f1.unlock()); @@ -255,16 +238,12 @@ fn file_lock_multiple_shared() { )] 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 + 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!(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)); } @@ -288,11 +267,9 @@ fn file_lock_blocking() { )] 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 + 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!(f1.lock_shared()); assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); drop(f1); @@ -318,29 +295,24 @@ fn file_lock_drop() { )] 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 + 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!(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) + drop(cloned); } #[test] #[cfg(windows)] fn file_lock_double_unlock() { let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_lock_double_unlock_test.txt"); - let f1 = check!(File::create(filename)); - let f2 = check!(OpenOptions::new().write(true).open(filename)); - - // On Windows a file handle may acquire both a shared and exclusive lock. - // Check that both are released by unlock() + let filename = tmpdir.join("file_lock_double_unlock_test.txt"); + let f1 = check!(File::create(&filename)); + let f2 = check!(OpenOptions::new().write(true).open(&filename)); check!(f1.lock()); check!(f1.lock_shared()); assert_matches!(f2.try_lock(), Err(TryLockError::WouldBlock)); @@ -351,35 +323,22 @@ fn file_lock_double_unlock() { #[test] #[cfg(windows)] fn file_lock_blocking_async() { - use crate::thread::{sleep, spawn}; + use std::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)); - + 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()); - }); + 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)); + 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()); - }); + let t = spawn(move || check!(f2.lock_shared())); sleep(Duration::from_secs(1)); assert!(!t.is_finished()); check!(f1.unlock()); @@ -390,19 +349,13 @@ fn file_lock_blocking_async() { #[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 + 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!(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)); @@ -410,46 +363,38 @@ fn file_try_lock_async() { #[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 mut read_mem = [0u8; 256]; let tmpdir = tmpdir(); - let filename = &tmpdir.join("file_rt_io_file_test_seek_shakedown.txt"); + let filename = tmpdir.join("file_rt_io_file_test_seek_shakedown.txt"); { - let mut rw_stream = check!(File::create(filename)); + let mut rw_stream = check!(File::create(&filename)); check!(rw_stream.write(initial_msg.as_bytes())); } { - let mut read_stream = check!(File::open(filename)); - + 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); - + assert_eq!(std::str::from_utf8(&read_mem[..4]).unwrap(), "zxcv"); check!(read_stream.seek(SeekFrom::Current(-9))); check!(read_stream.read(&mut read_mem)); - assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_two); - + assert_eq!(std::str::from_utf8(&read_mem[..4]).unwrap(), "asdf"); check!(read_stream.seek(SeekFrom::Start(0))); check!(read_stream.read(&mut read_mem)); - assert_eq!(str::from_utf8(&read_mem).unwrap(), chunk_one); + assert_eq!(std::str::from_utf8(&read_mem[..4]).unwrap(), "qwer"); } - check!(fs::remove_file(filename)); + 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 mut buf = [0u8; 256]; { - let oo = OpenOptions::new().create_new(true).write(true).read(true).clone(); + let oo = OpenOptions::new().create_new(true).write(true).read(true); 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)); } @@ -457,55 +402,24 @@ fn file_test_io_eof() { #[test] #[cfg(unix)] fn file_test_io_read_write_at() { - use crate::os::unix::fs::FileExt; - + use std::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 mut buf = [0u8; 256]; + let write1 = b"asdf"; + let write2 = b"qwer-"; + let write3 = b"-zxcv"; + let content = b"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 rw = check!(OpenOptions::new().create_new(true).write(true).read(true).open(&filename)); + assert_eq!(check!(rw.write_at(write1, 5)), write1.len()); + assert_eq!(check!(rw.write(write2)), write2.len()); + assert_eq!(check!(rw.write_at(write3, 9)), write3.len()); } { 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); + assert_eq!(&buf[..content.len()], content); } check!(fs::remove_file(&filename)); } @@ -513,42 +427,22 @@ fn file_test_io_read_write_at() { #[test] #[cfg(unix)] fn test_read_buf_at() { - use crate::os::unix::fs::FileExt; - + use std::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)); + let mut file = check!(OpenOptions::new().create_new(true).write(true).open(&filename)); check!(file.write_all(b"0123456789")); } { let mut file = check!(File::open(&filename)); - let mut buf: [MaybeUninit; 5] = [MaybeUninit::uninit(); 5]; + let mut buf = [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)); } @@ -556,35 +450,19 @@ fn test_read_buf_at() { #[test] #[cfg(unix)] fn test_read_buf_exact_at() { - use crate::os::unix::fs::FileExt; - + use std::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)); + let mut file = check!(OpenOptions::new().create_new(true).write(true).open(&filename)); check!(file.write_all(b"0123456789")); } { let mut file = check!(File::open(&filename)); - let mut buf: [MaybeUninit; 5] = [MaybeUninit::uninit(); 5]; + let mut buf = [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)); } @@ -592,75 +470,39 @@ fn test_read_buf_exact_at() { #[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); + let filename = tmpdir.join("set_get_unix_permissions"); + check!(fs::create_dir(&filename)); + check!(fs::set_permissions(&filename, fs::Permissions::from_mode(0))); + let meta = check!(fs::metadata(&filename)); + assert_eq!(meta.permissions().mode() & 0o7777, 0); + check!(fs::set_permissions(&filename, fs::Permissions::from_mode(0o1777))); + let meta = check!(fs::metadata(&filename)); + assert_eq!(meta.permissions().mode() & 0o7777, 0o1777); } #[test] #[cfg(windows)] fn file_test_io_seek_read_write() { - use crate::os::windows::fs::FileExt; - + use std::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 mut buf = [0u8; 256]; + let write1 = b"asdf"; + let write2 = b"qwer-"; + let write3 = b"-zxcv"; + let content = b"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 rw = check!(OpenOptions::new().create_new(true).write(true).read(true).open(&filename)); + check!(rw.seek_write(write1, 5)); + check!(rw.seek(SeekFrom::Start(0))); + check!(rw.write(write2)); + check!(rw.seek_write(write3, 9)); } { 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); + assert_eq!(&buf[..content.len()], content); } check!(fs::remove_file(&filename)); } @@ -668,32 +510,19 @@ fn file_test_io_seek_read_write() { #[test] #[cfg(windows)] fn test_seek_read_buf() { - use crate::os::windows::fs::FileExt; - + use std::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)); + let mut file = check!(OpenOptions::new().create_new(true).write(true).open(&filename)); check!(file.write_all(b"0123456789")); } { let mut file = check!(File::open(&filename)); - let mut buf: [MaybeUninit; 1] = [MaybeUninit::uninit()]; + let mut buf = [MaybeUninit::::uninit(); 1]; 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)); } @@ -701,125 +530,102 @@ fn test_seek_read_buf() { #[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; 128] = [MaybeUninit::uninit(); 128]; + let filename = tmpdir.join("test"); + check!(fs::write(&filename, &[1, 2, 3, 4])); + let mut buf = [MaybeUninit::::uninit(); 128]; let mut buf = BorrowedBuf::from(buf.as_mut_slice()); - let mut file = check!(File::open(filename)); + let mut file = check!(File::open(&filename)); check!(file.read_buf(buf.unfilled())); assert_eq!(buf.filled(), &[1, 2, 3, 4]); - - check!(fs::remove_file(filename)); + 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)); + let filename = tmpdir.join("file_stat_correct_on_is_file.txt"); + let mut f = check!(OpenOptions::new().read(true).write(true).create(true).open(&filename)); + f.write_all(b"hw").unwrap(); + assert!(f.metadata().unwrap().is_file()); + assert!(filename.metadata().unwrap().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)); + let filename = tmpdir.join("file_stat_correct_on_is_dir"); + check!(fs::create_dir(&filename)); + assert!(filename.metadata().unwrap().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)); + let dir = tmpdir.join("fileinfo_false_on_dir"); + check!(fs::create_dir(&dir)); assert!(!dir.is_file()); - check!(fs::remove_dir(dir)); + 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")); + let file = tmpdir.join("fileinfo_check_exists_b_and_a.txt"); + check!(File::create(&file).and_then(|mut f| f.write(b"foo"))); assert!(file.exists()); - check!(fs::remove_file(file)); + 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"); + let dir = tmpdir.join("before_and_after_dir"); assert!(!dir.exists()); - check!(fs::create_dir(dir)); + check!(fs::create_dir(&dir)); assert!(dir.exists()); assert!(dir.is_dir()); - check!(fs::remove_dir(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"; + let dir = tmpdir.join("di_readdir"); + check!(fs::create_dir(&dir)); 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 f = dir.join(format!("{n}.txt")); + check!(File::create(&f).and_then(|mut w| w.write_all(format!("foo{n}").as_bytes()))); } - 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)); + for entry in check!(fs::read_dir(&dir)) { + let entry = check!(entry); + let path = entry.path(); + let mut file = check!(File::open(&path)); + let mut s = String::new(); + check!(file.read_to_string(&mut s)); + assert!(s.starts_with("foo")); } - check!(fs::remove_dir(dir)); + check!(fs::remove_dir_all(&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(); + let file = tmpdir.join("file_create_new_error_exists"); + check!(File::create(&file)); + let e = 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(); + 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); } @@ -828,7 +634,7 @@ fn recursive_mkdir() { let tmpdir = tmpdir(); let dir = tmpdir.join("d1/d2"); check!(fs::create_dir_all(&dir)); - assert!(dir.is_dir()) + assert!(dir.is_dir()); } #[test] @@ -836,33 +642,28 @@ 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()); + assert!(fs::create_dir_all(&file).is_err()); } #[test] fn concurrent_recursive_mkdir() { for _ in 0..100 { let dir = tmpdir(); - let mut dir = dir.join("a"); + let mut path = dir.join("a"); for _ in 0..40 { - dir = dir.join("a"); + path = path.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)); - })) + let threads: Vec<_> = (0..8) + .map(|_| { + let p = path.clone(); + thread::spawn(move || check!(fs::create_dir_all(&p))) + }) + .collect(); + for t in threads { + t.join().unwrap(); } - - // No `Display` on result of `join()` - join.drain(..).map(|join| join.join().unwrap()).count(); } } @@ -884,7 +685,7 @@ fn recursive_mkdir_empty() { #[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" + ignore = "SymLinks not enabled on Arm64 Windows runners" )] fn recursive_rmdir() { let tmpdir = tmpdir(); @@ -895,11 +696,10 @@ fn recursive_rmdir() { 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!(File::create(&canary)); 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()); } @@ -907,154 +707,102 @@ fn recursive_rmdir() { #[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" + ignore = "SymLinks not enabled on Arm64 Windows runners" )] 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!(File::create(&canary)); 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")); + check!(File::create(&canary)); let result = fs::remove_dir_all(&canary); #[cfg(unix)] error!(result, "Not a directory"); #[cfg(windows)] - error!(result, 267); // ERROR_DIRECTORY - The directory name is invalid. + error!(result, 267); assert!(result.is_err()); 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!(File::create(&f1)); check!(symlink_file(&f1, &f2)); - match fs::remove_dir_all(&f2) { - Ok(..) => panic!("wanted a failure"), - Err(..) => {} - } + assert!(fs::remove_dir_all(&f2).is_err()); } #[test] -#[ignore] // takes too much time +#[ignore] 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"); + let attack_dest_file = attack_dest_dir.join("attack_file"); + fs::create_dir(&attack_dest_dir).unwrap(); 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` + let drop_canary = Arc::new(()); + let weak = Arc::downgrade(&drop_canary); thread::spawn(move || { - while drop_canary_weak.upgrade().is_some() { + while 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) { + let start = Instant::now(); + while start.elapsed() < 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) - ); + panic!("Attack succeeded in {:?}", start.elapsed()); } let _ = fs::create_dir(&victim_del_path); let _ = fs::remove_dir(&victim_del_path); - let _ = symlink_dir(attack_dest_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-각丁ー再见"); + let unicode = tmpdir.path().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()); - } - } + assert!(fs::copy(&from, &to).is_err()); + assert!(!from.exists()); + assert!(!to.exists()); } #[test] @@ -1062,12 +810,8 @@ 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")); + check!(File::create(&to).and_then(|mut f| f.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] @@ -1075,53 +819,9 @@ 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!(File::create(&input).and_then(|mut f| f.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()); + assert_eq!(check!(fs::read(&out)), b"hello"); } #[test] @@ -1129,61 +829,11 @@ 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)); + let mut perm = check!(File::create(&input)).metadata().unwrap().permissions(); + perm.set_readonly(true); + check!(fs::set_permissions(&input, perm.clone())); 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()); + assert!(out.metadata().unwrap().permissions().readonly()); } #[test] @@ -1191,73 +841,34 @@ 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!(File::create(&input).and_then(|mut f| f.write(b"foobar"))); 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()); + assert!(out.symlink_metadata().unwrap().file_type().is_symlink()); + assert_eq!(check!(fs::read(&out)), b"foobar"); } #[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"); + } + 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(..) => {} } + let link = tmpdir.join("link"); + check!(symlink_file("foo", &link)); + assert_eq!(check!(fs::read_link(&link)).to_str().unwrap(), "foo"); } #[test] @@ -1266,292 +877,111 @@ 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!(File::create(&input).and_then(|mut f| f.write(b"foobar"))); 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(..) => {} - } + assert_eq!(check!(fs::read(&out)), b"foobar"); } #[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(); + let mut p = check!(file.metadata()).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)); + assert!(file.metadata().unwrap().permissions().readonly()); } #[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()); + assert_eq!(check!(fs::read(&path)), b"fo\0\0\0\0wut"); } #[test] fn open_flavors() { - use crate::fs::OpenOptions as OO; - fn c(t: &T) -> T { - t.clone() - } - let tmpdir = tmpdir(); + let invalid = "creating or truncating a file requires write or append access"; + let w = OpenOptions::new().write(true); + let r = OpenOptions::new().read(true); + let rw = OpenOptions::new().read(true).write(true); + let a = OpenOptions::new().append(true); - 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); - - 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()); - } + check!(w.clone().create_new(true).open(tmpdir.join("a"))); + check!(w.clone().create(true).truncate(true).open(tmpdir.join("a"))); + check!(w.clone().truncate(true).open(tmpdir.join("a"))); + check!(w.clone().create(true).open(tmpdir.join("a"))); + check!(w.open(tmpdir.join("a"))); - // 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); + error_contains!(r.clone().create_new(true).open(tmpdir.join("b")), invalid); + error_contains!(r.clone().create(true).truncate(true).open(tmpdir.join("b")), invalid); + error_contains!(r.clone().truncate(true).open(tmpdir.join("b")), invalid); + error_contains!(r.clone().create(true).open(tmpdir.join("b")), invalid); + check!(r.open(tmpdir.join("a"))); - // 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); -} + check!(rw.clone().create_new(true).open(tmpdir.join("c"))); + check!(rw.clone().create(true).truncate(true).open(tmpdir.join("c"))); + check!(rw.clone().truncate(true).open(tmpdir.join("c"))); + check!(rw.clone().create(true).open(tmpdir.join("c"))); + check!(rw.open(tmpdir.join("c"))); -#[test] -fn _assert_send_sync() { - fn _assert_send_sync() {} - _assert_send_sync::(); + check!(a.clone().create_new(true).open(tmpdir.join("d"))); + error_contains!(a.clone().create(true).truncate(true).open(tmpdir.join("d")), invalid); + error_contains!(a.clone().truncate(true).open(tmpdir.join("d")), invalid); + check!(a.clone().create(true).open(tmpdir.join("d"))); + check!(a.open(tmpdir.join("d"))); } #[test] fn binary_file() { - let mut bytes = [0; 1024]; + let mut bytes = [0u8; 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[..]); + check!(fs::write(tmpdir.join("test"), &bytes)); + assert_eq!(check!(fs::read(tmpdir.join("test"))), bytes); } #[test] fn write_then_read() { - let mut bytes = [0; 1024]; + let mut bytes = [0u8; 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); + check!(fs::write(tmpdir.join("test"), &bytes)); + assert_eq!(check!(fs::read(tmpdir.join("test"))), bytes); + check!(fs::write(tmpdir.join("utf8"), "𐁁𐀓𐀠𐀴𐀍")); + assert_eq!(check!(fs::read_to_string(tmpdir.join("utf8"))), "𐁁𐀓𐀠𐀴𐀍"); } #[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 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![]; + let mut buf = Vec::new(); check!(f2.read_to_end(&mut buf)); assert_eq!(buf, b"llo world"); drop(f2); - check!(f1.write_all(b"!")); } @@ -1561,7 +991,7 @@ fn unlink_readonly() { let tmpdir = tmpdir(); let path = tmpdir.join("file"); check!(File::create(&path)); - let mut perm = check!(fs::metadata(&path)).permissions(); + let mut perm = check!(path.metadata()).permissions(); perm.set_readonly(true); check!(fs::set_permissions(&path, perm)); check!(fs::remove_file(&path)); @@ -1570,17 +1000,16 @@ fn unlink_readonly() { #[test] fn mkdir_trailing_slash() { let tmpdir = tmpdir(); - let path = tmpdir.join("file"); - check!(fs::create_dir_all(&path.join("a/"))); + let path = tmpdir.join("file/a/"); + check!(fs::create_dir_all(&path)); } #[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); + check!(File::create(&file)); + assert_eq!(check!(fs::canonicalize(&file)), file); } #[test] @@ -1588,589 +1017,210 @@ 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); + check!(File::create(&file)); + check!(fs::create_dir(&dir)); + check!(symlink_file(&file, &link)); + check!(symlink_dir(&dir, &linkdir)); + assert_eq!(check!(fs::canonicalize(&link)), file); + assert_eq!(check!(fs::canonicalize(&linkdir)), dir); } #[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:?}"), + check!(fs::create_dir(tmpdir.join("a"))); + check!(File::create(tmpdir.join("b"))); + for entry in check!(fs::read_dir(tmpdir.path())) { + let entry = check!(entry); + match entry.file_name().to_str() { + Some("a") => assert!(entry.file_type().unwrap().is_dir()), + Some("b") => assert!(entry.file_type().unwrap().is_file()), + _ => panic!(), } } } -#[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); + assert_eq!(fs::read_dir("/path/that/does/not/exist").unwrap_err().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); + assert_eq!(File::open("/path/that/does/not/exist").unwrap_err().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" + ignore = "SymLinks not enabled on Arm64 Windows runners" )] 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!(fs::create_dir(&target)); 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()); + if got_symlink_permission(&tmpdir) { + check!(symlink_dir(&target, &link)); + check!(fs::create_dir_all(&d)); + assert!(d.exists()); + } } #[test] fn metadata_access_times() { 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()) { - (Ok(t1), Ok(t2)) => assert!(t1 <= t2), - (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:?}") - } - } - } + let file = tmpdir.join("file"); + check!(File::create(&file)); + let dir_meta = check!(fs::metadata(tmpdir.path())); + let file_meta = check!(fs::metadata(&file)); + let _ = dir_meta.accessed(); + let _ = dir_meta.modified(); + let _ = file_meta.accessed(); + let _ = file_meta.modified(); } -/// 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!(File::create(tmpdir.join("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()); + assert!(tmpdir.join("hard_link").symlink_metadata().unwrap().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"); + path.push(std::iter::repeat("a").take(240).collect::()); 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 = 32 * 1024; - for i in 0..count { - check!(fs::File::create(tmpdir.join(&i.to_string()))); + for i in 0..32_768 { + check!(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(); + check!(File::create(tmpdir.join("file"))); 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); + assert_eq!(e.file_type().unwrap(), e.path().metadata().unwrap().file_type()); } } -/// 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")] 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, - _ => {} + use std::process::Command; + let mut child = match Command::new("echo").spawn() { + Ok(c) => c, + Err(_) => return, + }; + let _ = child.kill(); + let path = format!("/proc/{}/net", child.id()); + if fs::read_dir(path).is_err() { + 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 + let dir = fs::read_dir(path).unwrap(); 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()); + let old = tmpdir.join("foo/bar/baz"); + let new = tmpdir.join("quux/blat/newdir"); + check!(fs::create_dir_all(&old)); + check!(File::create(old.join("temp.txt"))); + check!(fs::create_dir_all(tmpdir.join("quux/blat"))); + check!(fs::rename(&old, &new)); + assert!(new.is_dir()); + assert!(new.join("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); - } + let file = check!(File::create(tmp.join("foo"))); + let times = FileTimes::new() + .set_accessed(SystemTime::UNIX_EPOCH + Duration::from_secs(12345)) + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(54321)); + let _ = file.set_times(times); + let meta = file.metadata().unwrap(); + assert_eq!(meta.accessed().unwrap(), SystemTime::UNIX_EPOCH + Duration::from_secs(12345)); + assert_eq!(meta.modified().unwrap(), SystemTime::UNIX_EPOCH + Duration::from_secs(54321)); } #[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::(), 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::(), 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() + let mut file = check!(OpenOptions::new() .write(true) .create_new(true) .attributes(FILE_ATTRIBUTE_HIDDEN) - .open(&path) - .unwrap(); - file.write("hidden world!".as_bytes()).unwrap(); - file.flush().unwrap(); + .open(&path)); + check!(file.write(b"hidden world!")); 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); + let file = check!(File::create(&path)); + assert_eq!(file.metadata().unwrap().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. -#[cfg(windows)] #[test] -#[cfg_attr(target_vendor = "win7", ignore = "Unsupported under Windows 7.")] +#[cfg(windows)] +#[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"); + let source = tmpdir.join("source_file.txt"); + let target = tmpdir.join("target_file.txt"); + check!(fs::write(&source, b"source hello world")); + check!(fs::write(&target, b"target hello world")); + let _handle = check!(File::open(&target)); + check!(fs::rename(&source, &target)); + assert_eq!(check!(fs::read(&target)), b"source hello world"); } #[test] #[cfg(windows)] -#[cfg_attr(target_vendor = "win7", ignore = "Unsupported under Windows 7.")] +#[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 on Windows. - 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(); - - error!(fs::rename(source_path, target_path), 145); // ERROR_DIR_NOT_EMPTY + let tmpdir = tmpdir(); + let source = tmpdir.join("source_directory"); + let target = tmpdir.join("target_directory"); + check!(fs::create_dir(&source)); + check!(fs::create_dir(&target)); + check!(fs::write(target.join("target_file.txt"), b"target hello world")); + error!(fs::rename(&source, &target), 145); } #[test] @@ -2178,284 +1228,80 @@ 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); + check!(symlink_file("does not exist", &original)); + check!(fs::rename(&original, &dest)); + assert_eq!(check!(fs::read_link(&dest)).to_str().unwrap(), "does 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" + ignore = "SymLinks not enabled on Arm64 Windows runners" )] 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())); + check!(junction_point("does not exist", &original)); + check!(fs::rename(&original, &dest)); + assert!(check!(fs::read_link(&dest)).file_name().is_some()); } #[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"); + let msg = "creating or truncating a file requires write or append access"; + error_contains!(OpenOptions::new().read(true).create(true).open("x"), msg); + error_contains!(OpenOptions::new().read(true).create_new(true).open("x"), msg); + error_contains!(OpenOptions::new().read(true).truncate(true).open("x"), msg); + error_contains!(OpenOptions::new().append(true).truncate(true).open("x"), msg); + assert!(OpenOptions::new().open("x").is_err()); } #[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); - } + check!(File::create(&path)); + let times = FileTimes::new() + .set_accessed(SystemTime::UNIX_EPOCH + Duration::from_secs(12345)) + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(54321)); + let _ = fs::set_times(&path, times); } #[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(); - - // 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"); + check!(File::create(&target)); #[cfg(unix)] - crate::os::unix::fs::symlink(&target, &link).unwrap(); - + std::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" - ); + std::os::windows::fs::symlink_file(&target, &link).unwrap(); + let times = FileTimes::new() + .set_accessed(SystemTime::UNIX_EPOCH + Duration::from_secs(12345)) + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(54321)); + let _ = fs::set_times(&link, times); + assert_eq!(fs::metadata(&target).unwrap().modified().unwrap(), SystemTime::UNIX_EPOCH + Duration::from_secs(54321)); } #[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(); - - // 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"); + check!(File::create(&target)); #[cfg(unix)] - crate::os::unix::fs::symlink(&target, &link).unwrap(); - + std::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); -} + std::os::windows::fs::symlink_file(&target, &link).unwrap(); + let times = FileTimes::new() + .set_accessed(SystemTime::UNIX_EPOCH + Duration::from_secs(11111)) + .set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(22222)); + let _ = fs::set_times_nofollow(&link, times); + assert_eq!(fs::symlink_metadata(&link).unwrap().modified().unwrap(), SystemTime::UNIX_EPOCH + Duration::from_secs(22222)); +} \ No newline at end of file diff --git a/library/std/src/sys/fs/unix.rs b/library/std/src/sys/fs/unix.rs index 47d9ee226653e..693aa5d76565d 100644 --- a/library/std/src/sys/fs/unix.rs +++ b/library/std/src/sys/fs/unix.rs @@ -1,11 +1,8 @@ #![allow(nonstandard_style)] #![allow(unsafe_op_in_unsafe_fn)] -// miri has some special hacks here that make things unused. #![cfg_attr(miri, allow(unused))] - #[cfg(test)] mod tests; - #[cfg(all(target_os = "linux", target_env = "gnu"))] use libc::c_char; #[cfg(any( @@ -75,7 +72,6 @@ use libc::{ target_os = "hurd" ))] use libc::{dirent64, fstat64, ftruncate64, lseek64, lstat64, off64_t, open64, stat64}; - use crate::ffi::{CStr, OsStr, OsString}; use crate::fmt::{self, Write as _}; use crate::fs::TryLockError; @@ -95,13 +91,7 @@ use crate::sys::weak::weak; use crate::sys::{cvt, cvt_r}; use crate::sys_common::{AsInner, AsInnerMut, FromInner, IntoInner}; use crate::{mem, ptr}; - pub struct File(FileDesc); - -// FIXME: This should be available on Linux with all `target_env`. -// But currently only glibc exposes `statx` fn and structs. -// We don't want to import unverified raw C structs here directly. -// https://github.com/rust-lang/rust/pull/67774 macro_rules! cfg_has_statx { ({ $($then_tt:tt)* } else { $($else_tt:tt)* }) => { cfg_select! { @@ -120,32 +110,23 @@ macro_rules! cfg_has_statx { } }; } - cfg_has_statx! {{ #[derive(Clone)] pub struct FileAttr { stat: stat64, statx_extra_fields: Option, } - #[derive(Clone)] struct StatxExtraFields { - // This is needed to check if btime is supported by the filesystem. stx_mask: u32, stx_btime: libc::statx_timestamp, - // With statx, we can overcome 32-bit `time_t` too. #[cfg(target_pointer_width = "32")] stx_atime: libc::statx_timestamp, #[cfg(target_pointer_width = "32")] stx_ctime: libc::statx_timestamp, #[cfg(target_pointer_width = "32")] stx_mtime: libc::statx_timestamp, - } - - // We prefer `statx` on Linux if available, which contains file creation time, - // as well as 64-bit timestamps of all kinds. - // Default `stat64` contains no creation time and may have 32-bit `time_t`. unsafe fn try_statx( fd: c_int, path: *const c_char, @@ -153,14 +134,9 @@ cfg_has_statx! {{ mask: u32, ) -> Option> { use crate::sync::atomic::{Atomic, AtomicU8, Ordering}; - - // Linux kernel prior to 4.11 or glibc prior to glibc 2.28 don't support `statx`. - // We check for it on first failure and remember availability to avoid having to - // do it again. #[repr(u8)] enum STATX_STATE{ Unknown = 0, Present, Unavailable } static STATX_SAVED_STATE: Atomic = AtomicU8::new(STATX_STATE::Unknown as u8); - syscall!( fn statx( fd: c_int, @@ -170,29 +146,15 @@ cfg_has_statx! {{ statxbuf: *mut libc::statx, ) -> c_int; ); - let statx_availability = STATX_SAVED_STATE.load(Ordering::Relaxed); if statx_availability == STATX_STATE::Unavailable as u8 { return None; } - let mut buf: libc::statx = mem::zeroed(); if let Err(err) = cvt(statx(fd, path, flags, mask, &mut buf)) { if STATX_SAVED_STATE.load(Ordering::Relaxed) == STATX_STATE::Present as u8 { return Some(Err(err)); } - - // We're not yet entirely sure whether `statx` is usable on this kernel - // or not. Syscalls can return errors from things other than the kernel - // per se, e.g. `EPERM` can be returned if seccomp is used to block the - // syscall, or `ENOSYS` might be returned from a faulty FUSE driver. - // - // Availability is checked by performing a call which expects `EFAULT` - // if the syscall is usable. - // - // See: https://github.com/rust-lang/rust/issues/65662 - // - // FIXME what about transient conditions like `ENOMEM`? let err2 = cvt(statx(0, ptr::null(), 0, libc::STATX_BASIC_STATS | libc::STATX_BTIME, ptr::null_mut())) .err() .and_then(|e| e.raw_os_error()); @@ -207,10 +169,7 @@ cfg_has_statx! {{ if statx_availability == STATX_STATE::Unknown as u8 { STATX_SAVED_STATE.store(STATX_STATE::Present as u8, Ordering::Relaxed); } - - // We cannot fill `stat64` exhaustively because of private padding fields. let mut stat: stat64 = mem::zeroed(); - // `c_ulong` on gnu-mips, `dev_t` otherwise stat.st_dev = libc::makedev(buf.stx_dev_major, buf.stx_dev_minor) as _; stat.st_ino = buf.stx_ino as libc::ino64_t; stat.st_nlink = buf.stx_nlink as libc::nlink_t; @@ -222,17 +181,14 @@ cfg_has_statx! {{ stat.st_blksize = buf.stx_blksize as libc::blksize_t; stat.st_blocks = buf.stx_blocks as libc::blkcnt64_t; stat.st_atime = buf.stx_atime.tv_sec as libc::time_t; - // `i64` on gnu-x86_64-x32, `c_ulong` otherwise. stat.st_atime_nsec = buf.stx_atime.tv_nsec as _; stat.st_mtime = buf.stx_mtime.tv_sec as libc::time_t; stat.st_mtime_nsec = buf.stx_mtime.tv_nsec as _; stat.st_ctime = buf.stx_ctime.tv_sec as libc::time_t; stat.st_ctime_nsec = buf.stx_ctime.tv_nsec as _; - let extra = StatxExtraFields { stx_mask: buf.stx_mask, stx_btime: buf.stx_btime, - // Store full times to avoid 32-bit `time_t` truncation. #[cfg(target_pointer_width = "32")] stx_atime: buf.stx_atime, #[cfg(target_pointer_width = "32")] @@ -240,39 +196,30 @@ cfg_has_statx! {{ #[cfg(target_pointer_width = "32")] stx_mtime: buf.stx_mtime, }; - Some(Ok(FileAttr { stat, statx_extra_fields: Some(extra) })) } - } else { #[derive(Clone)] pub struct FileAttr { stat: stat64, } }} - -// all DirEntry's will have a reference to this struct struct InnerReadDir { dirp: Dir, root: PathBuf, } - pub struct ReadDir { inner: Arc, end_of_stream: bool, } - impl ReadDir { fn new(inner: InnerReadDir) -> Self { Self { inner: Arc::new(inner), end_of_stream: false } } } - struct Dir(*mut libc::DIR); - unsafe impl Send for Dir {} unsafe impl Sync for Dir {} - #[cfg(any( target_os = "aix", target_os = "android", @@ -289,15 +236,8 @@ unsafe impl Sync for Dir {} pub struct DirEntry { dir: Arc, entry: dirent64_min, - // We need to store an owned copy of the entry name on platforms that use - // readdir() (not readdir_r()), because a) struct dirent may use a flexible - // array to store the name, b) it lives only until the next readdir() call. name: crate::ffi::CString, } - -// Define a minimal subset of fields we need from `dirent64`, especially since -// we're not using the immediate `d_name` on these targets. Keeping this as an -// `entry` field in `DirEntry` helps reduce the `cfg` boilerplate elsewhere. #[cfg(any( target_os = "aix", target_os = "android", @@ -322,7 +262,6 @@ struct dirent64_min { )))] d_type: u8, } - #[cfg(not(any( target_os = "aix", target_os = "android", @@ -338,29 +277,23 @@ struct dirent64_min { )))] pub struct DirEntry { dir: Arc, - // The full entry includes a fixed-length `d_name`. entry: dirent64, } - #[derive(Clone)] pub struct OpenOptions { - // generic read: bool, write: bool, append: bool, truncate: bool, create: bool, create_new: bool, - // system-specific custom_flags: i32, mode: mode_t, } - #[derive(Clone, PartialEq, Eq)] pub struct FilePermissions { mode: mode_t, } - #[derive(Copy, Clone, Debug, Default)] pub struct FileTimes { accessed: Option, @@ -368,37 +301,30 @@ pub struct FileTimes { #[cfg(target_vendor = "apple")] created: Option, } - #[derive(Copy, Clone, Eq)] pub struct FileType { mode: mode_t, } - impl PartialEq for FileType { fn eq(&self, other: &Self) -> bool { self.masked() == other.masked() } } - impl core::hash::Hash for FileType { fn hash(&self, state: &mut H) { self.masked().hash(state); } } - pub struct DirBuilder { mode: mode_t, } - #[derive(Copy, Clone)] struct Mode(mode_t); - cfg_has_statx! {{ impl FileAttr { fn from_stat64(stat: stat64) -> Self { Self { stat, statx_extra_fields: None } } - #[cfg(target_pointer_width = "32")] pub fn stx_mtime(&self) -> Option<&libc::statx_timestamp> { if let Some(ext) = &self.statx_extra_fields { @@ -408,7 +334,6 @@ cfg_has_statx! {{ } None } - #[cfg(target_pointer_width = "32")] pub fn stx_atime(&self) -> Option<&libc::statx_timestamp> { if let Some(ext) = &self.statx_extra_fields { @@ -418,7 +343,6 @@ cfg_has_statx! {{ } None } - #[cfg(target_pointer_width = "32")] pub fn stx_ctime(&self) -> Option<&libc::statx_timestamp> { if let Some(ext) = &self.statx_extra_fields { @@ -436,7 +360,6 @@ cfg_has_statx! {{ } } }} - impl FileAttr { pub fn size(&self) -> u64 { self.stat.st_size as u64 @@ -444,42 +367,34 @@ impl FileAttr { pub fn perm(&self) -> FilePermissions { FilePermissions { mode: (self.stat.st_mode as mode_t) } } - pub fn file_type(&self) -> FileType { FileType { mode: self.stat.st_mode as mode_t } } } - #[cfg(target_os = "netbsd")] impl FileAttr { pub fn modified(&self) -> io::Result { SystemTime::new(self.stat.st_mtime as i64, self.stat.st_mtimensec as i64) } - pub fn accessed(&self) -> io::Result { SystemTime::new(self.stat.st_atime as i64, self.stat.st_atimensec as i64) } - pub fn created(&self) -> io::Result { SystemTime::new(self.stat.st_birthtime as i64, self.stat.st_birthtimensec as i64) } } - #[cfg(target_os = "aix")] impl FileAttr { pub fn modified(&self) -> io::Result { SystemTime::new(self.stat.st_mtime.tv_sec as i64, self.stat.st_mtime.tv_nsec as i64) } - pub fn accessed(&self) -> io::Result { SystemTime::new(self.stat.st_atime.tv_sec as i64, self.stat.st_atime.tv_nsec as i64) } - pub fn created(&self) -> io::Result { SystemTime::new(self.stat.st_ctime.tv_sec as i64, self.stat.st_ctime.tv_nsec as i64) } } - #[cfg(not(any(target_os = "netbsd", target_os = "nto", target_os = "aix")))] impl FileAttr { #[cfg(not(any( @@ -498,10 +413,8 @@ impl FileAttr { return SystemTime::new(mtime.tv_sec, mtime.tv_nsec as i64); } } - SystemTime::new(self.stat.st_mtime as i64, self.stat.st_mtime_nsec as i64) } - #[cfg(any( target_os = "vxworks", target_os = "espidf", @@ -511,12 +424,10 @@ impl FileAttr { pub fn modified(&self) -> io::Result { SystemTime::new(self.stat.st_mtime as i64, 0) } - #[cfg(any(target_os = "horizon", target_os = "hurd", target_os = "nuttx"))] pub fn modified(&self) -> io::Result { SystemTime::new(self.stat.st_mtim.tv_sec as i64, self.stat.st_mtim.tv_nsec as i64) } - #[cfg(not(any( target_os = "vxworks", target_os = "espidf", @@ -533,10 +444,8 @@ impl FileAttr { return SystemTime::new(atime.tv_sec, atime.tv_nsec as i64); } } - SystemTime::new(self.stat.st_atime as i64, self.stat.st_atime_nsec as i64) } - #[cfg(any( target_os = "vxworks", target_os = "espidf", @@ -546,12 +455,10 @@ impl FileAttr { pub fn accessed(&self) -> io::Result { SystemTime::new(self.stat.st_atime as i64, 0) } - #[cfg(any(target_os = "horizon", target_os = "hurd", target_os = "nuttx"))] pub fn accessed(&self) -> io::Result { SystemTime::new(self.stat.st_atim.tv_sec as i64, self.stat.st_atim.tv_nsec as i64) } - #[cfg(any( target_os = "freebsd", target_os = "openbsd", @@ -561,7 +468,6 @@ impl FileAttr { pub fn created(&self) -> io::Result { SystemTime::new(self.stat.st_birthtime as i64, self.stat.st_birthtime_nsec as i64) } - #[cfg(not(any( target_os = "freebsd", target_os = "openbsd", @@ -582,53 +488,42 @@ impl FileAttr { }; } } - Err(io::const_error!( io::ErrorKind::Unsupported, "creation time is not available on this platform currently", )) } - #[cfg(target_os = "vita")] pub fn created(&self) -> io::Result { SystemTime::new(self.stat.st_ctime as i64, 0) } } - #[cfg(target_os = "nto")] impl FileAttr { pub fn modified(&self) -> io::Result { SystemTime::new(self.stat.st_mtim.tv_sec, self.stat.st_mtim.tv_nsec) } - pub fn accessed(&self) -> io::Result { SystemTime::new(self.stat.st_atim.tv_sec, self.stat.st_atim.tv_nsec) } - pub fn created(&self) -> io::Result { SystemTime::new(self.stat.st_ctim.tv_sec, self.stat.st_ctim.tv_nsec) } } - impl AsInner for FileAttr { #[inline] fn as_inner(&self) -> &stat64 { &self.stat } } - impl FilePermissions { pub fn readonly(&self) -> bool { - // check if any class (owner, group, others) has write permission self.mode & 0o222 == 0 } - pub fn set_readonly(&mut self, readonly: bool) { if readonly { - // remove write permission for all classes; equivalent to `chmod a-w ` self.mode &= !0o222; } else { - // add write permission for all classes; equivalent to `chmod a+w ` self.mode |= 0o222; } } @@ -636,22 +531,18 @@ impl FilePermissions { self.mode as u32 } } - impl FileTimes { pub fn set_accessed(&mut self, t: SystemTime) { self.accessed = Some(t); } - pub fn set_modified(&mut self, t: SystemTime) { self.modified = Some(t); } - #[cfg(target_vendor = "apple")] pub fn set_created(&mut self, t: SystemTime) { self.created = Some(t); } } - impl FileType { pub fn is_dir(&self) -> bool { self.is(libc::S_IFDIR) @@ -662,47 +553,37 @@ impl FileType { pub fn is_symlink(&self) -> bool { self.is(libc::S_IFLNK) } - pub fn is(&self, mode: mode_t) -> bool { self.masked() == mode } - fn masked(&self) -> mode_t { self.mode & libc::S_IFMT } } - impl fmt::Debug for FileType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let FileType { mode } = self; f.debug_struct("FileType").field("mode", &Mode(*mode)).finish() } } - impl FromInner for FilePermissions { fn from_inner(mode: u32) -> FilePermissions { FilePermissions { mode: mode as mode_t } } } - impl fmt::Debug for FilePermissions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let FilePermissions { mode } = self; f.debug_struct("FilePermissions").field("mode", &Mode(*mode)).finish() } } - impl fmt::Debug for ReadDir { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - // This will only be called from std::fs::ReadDir, which will add a "ReadDir()" frame. - // Thus the result will be e g 'ReadDir("/home")' fmt::Debug::fmt(&*self.inner.root, f) } } - impl Iterator for ReadDir { type Item = io::Result; - #[cfg(any( target_os = "aix", target_os = "android", @@ -718,61 +599,25 @@ impl Iterator for ReadDir { ))] fn next(&mut self) -> Option> { use crate::sys::os::{errno, set_errno}; - if self.end_of_stream { return None; } - unsafe { loop { - // As of POSIX.1-2017, readdir() is not required to be thread safe; only - // readdir_r() is. However, readdir_r() cannot correctly handle platforms - // with unlimited or variable NAME_MAX. Many modern platforms guarantee - // thread safety for readdir() as long an individual DIR* is not accessed - // concurrently, which is sufficient for Rust. set_errno(0); let entry_ptr: *const dirent64 = readdir64(self.inner.dirp.0); if entry_ptr.is_null() { - // We either encountered an error, or reached the end. Either way, - // the next call to next() should return None. self.end_of_stream = true; - - // To distinguish between errors and end-of-directory, we had to clear - // errno beforehand to check for an error now. return match errno() { 0 => None, e => Some(Err(Error::from_raw_os_error(e))), }; } - - // The dirent64 struct is a weird imaginary thing that isn't ever supposed - // to be worked with by value. Its trailing d_name field is declared - // variously as [c_char; 256] or [c_char; 1] on different systems but - // either way that size is meaningless; only the offset of d_name is - // meaningful. The dirent64 pointers that libc returns from readdir64 are - // allowed to point to allocations smaller _or_ LARGER than implied by the - // definition of the struct. - // - // As such, we need to be even more careful with dirent64 than if its - // contents were "simply" partially initialized data. - // - // Like for uninitialized contents, converting entry_ptr to `&dirent64` - // would not be legal. However, we can use `&raw const (*entry_ptr).d_name` - // to refer the fields individually, because that operation is equivalent - // to `byte_offset` and thus does not require the full extent of `*entry_ptr` - // to be in bounds of the same allocation, only the offset of the field - // being referenced. - - // d_name is guaranteed to be null-terminated. let name = CStr::from_ptr((&raw const (*entry_ptr).d_name).cast()); let name_bytes = name.to_bytes(); if name_bytes == b"." || name_bytes == b".." { continue; } - - // When loading from a field, we can skip the `&raw const`; `(*entry_ptr).d_ino` as - // a value expression will do the right thing: `byte_offset` to the field and then - // only access those bytes. #[cfg(not(target_os = "vita"))] let entry = dirent64_min { #[cfg(target_os = "freebsd")] @@ -787,10 +632,8 @@ impl Iterator for ReadDir { )))] d_type: (*entry_ptr).d_type as u8, }; - #[cfg(target_os = "vita")] let entry = dirent64_min { d_ino: 0u64 }; - return Some(Ok(DirEntry { entry, name: name.to_owned(), @@ -799,7 +642,6 @@ impl Iterator for ReadDir { } } } - #[cfg(not(any( target_os = "aix", target_os = "android", @@ -817,7 +659,6 @@ impl Iterator for ReadDir { if self.end_of_stream { return None; } - unsafe { let mut ret = DirEntry { entry: mem::zeroed(), dir: Arc::clone(&self.inner) }; let mut entry_ptr = ptr::null_mut(); @@ -825,10 +666,6 @@ impl Iterator for ReadDir { let err = readdir64_r(self.inner.dirp.0, &mut ret.entry, &mut entry_ptr); if err != 0 { if entry_ptr.is_null() { - // We encountered an error (which will be returned in this iteration), but - // we also reached the end of the directory stream. The `end_of_stream` - // flag is enabled to make sure that we return `None` in the next iteration - // (instead of looping forever) self.end_of_stream = true; } return Some(Err(Error::from_raw_os_error(err))); @@ -843,30 +680,17 @@ impl Iterator for ReadDir { } } } - -/// Aborts the process if a file desceriptor is not open, if debug asserts are enabled -/// -/// Many IO syscalls can't be fully trusted about EBADF error codes because those -/// might get bubbled up from a remote FUSE server rather than the file descriptor -/// in the current process being invalid. -/// -/// So we check file flags instead which live on the file descriptor and not the underlying file. -/// The downside is that it costs an extra syscall, so we only do it for debug. #[inline] pub(crate) fn debug_assert_fd_is_open(fd: RawFd) { use crate::sys::os::errno; - - // this is similar to assert_unsafe_precondition!() but it doesn't require const if core::ub_checks::check_library_ub() { if unsafe { libc::fcntl(fd, libc::F_GETFD) } == -1 && errno() == libc::EBADF { rtabort!("IO Safety violation: owned file descriptor already closed"); } } } - impl Drop for Dir { fn drop(&mut self) { - // dirfd isn't supported everywhere #[cfg(not(any( miri, target_os = "redox", @@ -891,16 +715,13 @@ impl Drop for Dir { ); } } - impl DirEntry { pub fn path(&self) -> PathBuf { self.dir.root.join(self.file_name_os_str()) } - pub fn file_name(&self) -> OsString { self.file_name_os_str().to_os_string() } - #[cfg(all( any( all(target_os = "linux", not(target_env = "musl")), @@ -910,12 +731,11 @@ impl DirEntry { target_os = "illumos", target_vendor = "apple", ), - not(miri) // no dirfd on Miri + not(miri) ))] pub fn metadata(&self) -> io::Result { let fd = cvt(unsafe { dirfd(self.dir.dirp.0) })?; let name = self.name_cstr().as_ptr(); - cfg_has_statx! { if let Some(ret) = unsafe { try_statx( fd, @@ -926,12 +746,10 @@ impl DirEntry { return ret; } } - let mut stat: stat64 = unsafe { mem::zeroed() }; cvt(unsafe { fstatat64(fd, name, &mut stat, libc::AT_SYMLINK_NOFOLLOW) })?; Ok(FileAttr::from_stat64(stat)) } - #[cfg(any( not(any( all(target_os = "linux", not(target_env = "musl")), @@ -946,7 +764,6 @@ impl DirEntry { pub fn metadata(&self) -> io::Result { run_path_with_cstr(&self.path(), &lstat) } - #[cfg(any( target_os = "solaris", target_os = "illumos", @@ -959,7 +776,6 @@ impl DirEntry { pub fn file_type(&self) -> io::Result { self.metadata().map(|m| m.file_type()) } - #[cfg(not(any( target_os = "solaris", target_os = "illumos", @@ -981,7 +797,6 @@ impl DirEntry { _ => self.metadata().map(|m| m.file_type()), } } - #[cfg(any( target_os = "aix", target_os = "android", @@ -1007,19 +822,14 @@ impl DirEntry { pub fn ino(&self) -> u64 { self.entry.d_ino as u64 } - #[cfg(any(target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly"))] pub fn ino(&self) -> u64 { self.entry.d_fileno as u64 } - #[cfg(target_os = "nuttx")] pub fn ino(&self) -> u64 { - // Leave this 0 for now, as NuttX does not provide an inode number - // in its directory entries. 0 } - #[cfg(any( target_os = "netbsd", target_os = "openbsd", @@ -1044,7 +854,6 @@ impl DirEntry { fn name_bytes(&self) -> &[u8] { self.name_cstr().to_bytes() } - #[cfg(not(any( target_os = "android", target_os = "freebsd", @@ -1077,28 +886,23 @@ impl DirEntry { fn name_cstr(&self) -> &CStr { &self.name } - pub fn file_name_os_str(&self) -> &OsStr { OsStr::from_bytes(self.name_bytes()) } } - impl OpenOptions { pub fn new() -> OpenOptions { OpenOptions { - // generic read: false, write: false, append: false, truncate: false, create: false, create_new: false, - // system-specific custom_flags: 0, mode: 0o666, } } - pub fn read(&mut self, read: bool) { self.read = read; } @@ -1117,14 +921,12 @@ impl OpenOptions { pub fn create_new(&mut self, create_new: bool) { self.create_new = create_new; } - pub fn custom_flags(&mut self, flags: i32) { self.custom_flags = flags; } pub fn mode(&mut self, mode: u32) { self.mode = mode as mode_t; } - fn get_access_mode(&self) -> io::Result { match (self.read, self.write, self.append) { (true, false, false) => Ok(libc::O_RDONLY), @@ -1133,8 +935,6 @@ impl OpenOptions { (false, _, true) => Ok(libc::O_WRONLY | libc::O_APPEND), (true, _, true) => Ok(libc::O_RDWR | libc::O_APPEND), (false, false, false) => { - // If no access mode is set, check if any creation flags are set - // to provide a more descriptive error message if self.create || self.create_new || self.truncate { Err(io::Error::new( io::ErrorKind::InvalidInput, @@ -1149,7 +949,6 @@ impl OpenOptions { } } } - fn get_creation_mode(&self) -> io::Result { match (self.write, self.append) { (true, false) => {} @@ -1170,7 +969,6 @@ impl OpenOptions { } } } - Ok(match (self.create, self.truncate, self.create_new) { (false, false, false) => 0, (true, false, false) => libc::O_CREAT, @@ -1180,7 +978,6 @@ impl OpenOptions { }) } } - impl fmt::Debug for OpenOptions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let OpenOptions { read, write, append, truncate, create, create_new, custom_flags, mode } = @@ -1197,28 +994,20 @@ impl fmt::Debug for OpenOptions { .finish() } } - impl File { pub fn open(path: &Path, opts: &OpenOptions) -> io::Result { run_path_with_cstr(path, &|path| File::open_c(path, opts)) } - pub fn open_c(path: &CStr, opts: &OpenOptions) -> io::Result { let flags = libc::O_CLOEXEC | opts.get_access_mode()? | opts.get_creation_mode()? | (opts.custom_flags as c_int & !libc::O_ACCMODE); - // The third argument of `open64` is documented to have type `mode_t`. On - // some platforms (like macOS, where `open64` is actually `open`), `mode_t` is `u16`. - // However, since this is a variadic function, C integer promotion rules mean that on - // the ABI level, this still gets passed as `c_int` (aka `u32` on Unix platforms). let fd = cvt_r(|| unsafe { open64(path.as_ptr(), flags, opts.mode as c_int) })?; Ok(File(unsafe { FileDesc::from_raw_fd(fd) })) } - pub fn file_attr(&self) -> io::Result { let fd = self.as_raw_fd(); - cfg_has_statx! { if let Some(ret) = unsafe { try_statx( fd, @@ -1229,16 +1018,13 @@ impl File { return ret; } } - let mut stat: stat64 = unsafe { mem::zeroed() }; cvt(unsafe { fstat64(fd, &mut stat) })?; Ok(FileAttr::from_stat64(stat)) } - pub fn fsync(&self) -> io::Result<()> { cvt_r(|| unsafe { os_fsync(self.as_raw_fd()) })?; return Ok(()); - #[cfg(target_vendor = "apple")] unsafe fn os_fsync(fd: c_int) -> c_int { libc::fcntl(fd, libc::F_FULLFSYNC) @@ -1248,11 +1034,9 @@ impl File { libc::fsync(fd) } } - pub fn datasync(&self) -> io::Result<()> { cvt_r(|| unsafe { os_datasync(self.as_raw_fd()) })?; return Ok(()); - #[cfg(target_vendor = "apple")] unsafe fn os_datasync(fd: c_int) -> c_int { libc::fcntl(fd, libc::F_FULLFSYNC) @@ -1287,7 +1071,6 @@ impl File { libc::fsync(fd) } } - #[cfg(any( target_os = "freebsd", target_os = "fuchsia", @@ -1303,8 +1086,7 @@ impl File { cvt(unsafe { libc::flock(self.as_raw_fd(), libc::LOCK_EX) })?; return Ok(()); } - - #[cfg(target_os = "solaris")] + #[cfg(any(target_os = "solaris", target_os = "illumos"))] pub fn lock(&self) -> io::Result<()> { let mut flock: libc::flock = unsafe { mem::zeroed() }; flock.l_type = libc::F_WRLCK as libc::c_short; @@ -1312,7 +1094,6 @@ impl File { cvt(unsafe { libc::fcntl(self.as_raw_fd(), libc::F_SETLKW, &flock) })?; Ok(()) } - #[cfg(not(any( target_os = "freebsd", target_os = "fuchsia", @@ -1328,7 +1109,6 @@ impl File { pub fn lock(&self) -> io::Result<()> { Err(io::const_error!(io::ErrorKind::Unsupported, "lock() not supported")) } - #[cfg(any( target_os = "freebsd", target_os = "fuchsia", @@ -1344,8 +1124,7 @@ impl File { cvt(unsafe { libc::flock(self.as_raw_fd(), libc::LOCK_SH) })?; return Ok(()); } - - #[cfg(target_os = "solaris")] + #[cfg(any(target_os = "solaris", target_os = "illumos"))] pub fn lock_shared(&self) -> io::Result<()> { let mut flock: libc::flock = unsafe { mem::zeroed() }; flock.l_type = libc::F_RDLCK as libc::c_short; @@ -1353,7 +1132,6 @@ impl File { cvt(unsafe { libc::fcntl(self.as_raw_fd(), libc::F_SETLKW, &flock) })?; Ok(()) } - #[cfg(not(any( target_os = "freebsd", target_os = "fuchsia", @@ -1369,7 +1147,6 @@ impl File { pub fn lock_shared(&self) -> io::Result<()> { Err(io::const_error!(io::ErrorKind::Unsupported, "lock_shared() not supported")) } - #[cfg(any( target_os = "freebsd", target_os = "fuchsia", @@ -1393,8 +1170,7 @@ impl File { Ok(()) } } - - #[cfg(target_os = "solaris")] + #[cfg(any(target_os = "solaris", target_os = "illumos"))] pub fn try_lock(&self) -> Result<(), TryLockError> { let mut flock: libc::flock = unsafe { mem::zeroed() }; flock.l_type = libc::F_WRLCK as libc::c_short; @@ -1410,7 +1186,6 @@ impl File { Ok(()) } } - #[cfg(not(any( target_os = "freebsd", target_os = "fuchsia", @@ -1429,7 +1204,6 @@ impl File { "try_lock() not supported" ))) } - #[cfg(any( target_os = "freebsd", target_os = "fuchsia", @@ -1453,8 +1227,7 @@ impl File { Ok(()) } } - - #[cfg(target_os = "solaris")] + #[cfg(any(target_os = "solaris", target_os = "illumos"))] pub fn try_lock_shared(&self) -> Result<(), TryLockError> { let mut flock: libc::flock = unsafe { mem::zeroed() }; flock.l_type = libc::F_RDLCK as libc::c_short; @@ -1470,7 +1243,6 @@ impl File { Ok(()) } } - #[cfg(not(any( target_os = "freebsd", target_os = "fuchsia", @@ -1489,7 +1261,6 @@ impl File { "try_lock_shared() not supported" ))) } - #[cfg(any( target_os = "freebsd", target_os = "fuchsia", @@ -1505,8 +1276,7 @@ impl File { cvt(unsafe { libc::flock(self.as_raw_fd(), libc::LOCK_UN) })?; return Ok(()); } - - #[cfg(target_os = "solaris")] + #[cfg(any(target_os = "solaris", target_os = "illumos"))] pub fn unlock(&self) -> io::Result<()> { let mut flock: libc::flock = unsafe { mem::zeroed() }; flock.l_type = libc::F_UNLCK as libc::c_short; @@ -1514,7 +1284,6 @@ impl File { cvt(unsafe { libc::fcntl(self.as_raw_fd(), libc::F_SETLKW, &flock) })?; Ok(()) } - #[cfg(not(any( target_os = "freebsd", target_os = "fuchsia", @@ -1530,72 +1299,55 @@ impl File { pub fn unlock(&self) -> io::Result<()> { Err(io::const_error!(io::ErrorKind::Unsupported, "unlock() not supported")) } - pub fn truncate(&self, size: u64) -> io::Result<()> { let size: off64_t = size.try_into().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; cvt_r(|| unsafe { ftruncate64(self.as_raw_fd(), size) }).map(drop) } - pub fn read(&self, buf: &mut [u8]) -> io::Result { self.0.read(buf) } - pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { self.0.read_vectored(bufs) } - #[inline] pub fn is_read_vectored(&self) -> bool { self.0.is_read_vectored() } - pub fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { self.0.read_at(buf, offset) } - pub fn read_buf(&self, cursor: BorrowedCursor<'_>) -> io::Result<()> { self.0.read_buf(cursor) } - pub fn read_buf_at(&self, cursor: BorrowedCursor<'_>, offset: u64) -> io::Result<()> { self.0.read_buf_at(cursor, offset) } - pub fn read_vectored_at(&self, bufs: &mut [IoSliceMut<'_>], offset: u64) -> io::Result { self.0.read_vectored_at(bufs, offset) } - pub fn write(&self, buf: &[u8]) -> io::Result { self.0.write(buf) } - pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { self.0.write_vectored(bufs) } - #[inline] pub fn is_write_vectored(&self) -> bool { self.0.is_write_vectored() } - pub fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { self.0.write_at(buf, offset) } - pub fn write_vectored_at(&self, bufs: &[IoSlice<'_>], offset: u64) -> io::Result { self.0.write_vectored_at(bufs, offset) } - #[inline] pub fn flush(&self) -> io::Result<()> { Ok(()) } - pub fn seek(&self, pos: SeekFrom) -> io::Result { let (whence, pos) = match pos { - // Casting to `i64` is fine, too large values will end up as - // negative which will cause an error in `lseek64`. SeekFrom::Start(off) => (libc::SEEK_SET, off as i64), SeekFrom::End(off) => (libc::SEEK_END, off), SeekFrom::Current(off) => (libc::SEEK_CUR, off), @@ -1603,35 +1355,25 @@ impl File { let n = cvt(unsafe { lseek64(self.as_raw_fd(), pos as off64_t, whence) })?; Ok(n as u64) } - pub fn size(&self) -> Option> { match self.file_attr().map(|attr| attr.size()) { - // Fall back to default implementation if the returned size is 0, - // we might be in a proc mount. Ok(0) => None, result => Some(result), } } - pub fn tell(&self) -> io::Result { self.seek(SeekFrom::Current(0)) } - pub fn duplicate(&self) -> io::Result { self.0.duplicate().map(File) } - pub fn set_permissions(&self, perm: FilePermissions) -> io::Result<()> { cvt_r(|| unsafe { libc::fchmod(self.as_raw_fd(), perm.mode) })?; Ok(()) } - pub fn set_times(&self, times: FileTimes) -> io::Result<()> { cfg_select! { any(target_os = "redox", target_os = "espidf", target_os = "horizon", target_os = "nuttx") => { - // Redox doesn't appear to support `UTIME_OMIT`. - // ESP-IDF and HorizonOS do not support `futimens` at all and the behavior for those OS is therefore - // the same as for Redox. let _ = times; Err(io::const_error!( io::ErrorKind::Unsupported, @@ -1651,7 +1393,6 @@ impl File { } target_os = "android" => { let times = [file_time_to_timespec(times.accessed)?, file_time_to_timespec(times.modified)?]; - // futimens requires Android API level 19 cvt(unsafe { weak!( fn futimens(fd: c_int, times: *const libc::timespec) -> c_int; @@ -1670,12 +1411,9 @@ impl File { #[cfg(all(target_os = "linux", target_env = "gnu", target_pointer_width = "32", not(target_arch = "riscv32")))] { use crate::sys::{time::__timespec64, weak::weak}; - - // Added in glibc 2.34 weak!( fn __futimens64(fd: c_int, times: *const __timespec64) -> c_int; ); - if let Some(futimens64) = __futimens64.get() { let to_timespec = |time: Option| time.map(|time| time.t.to_timespec64()) .unwrap_or(__timespec64::new(0, libc::UTIME_OMIT as _)); @@ -1691,7 +1429,6 @@ impl File { } } } - #[cfg(not(any( target_os = "redox", target_os = "espidf", @@ -1712,14 +1449,12 @@ fn file_time_to_timespec(time: Option) -> io::Result None => Ok(libc::timespec { tv_sec: 0, tv_nsec: libc::UTIME_OMIT as _ }), } } - #[cfg(target_vendor = "apple")] struct TimesAttrlist { buf: [mem::MaybeUninit; 3], attrlist: libc::attrlist, num_times: usize, } - #[cfg(target_vendor = "apple")] impl TimesAttrlist { fn from_times(times: &FileTimes) -> io::Result { @@ -1746,93 +1481,77 @@ impl TimesAttrlist { } Ok(this) } - fn attrlist(&self) -> *mut libc::c_void { (&raw const self.attrlist).cast::().cast_mut() } - fn times_buf(&self) -> *mut libc::c_void { self.buf.as_ptr().cast::().cast_mut() } - fn times_buf_size(&self) -> usize { self.num_times * size_of::() } } - impl DirBuilder { pub fn new() -> DirBuilder { DirBuilder { mode: 0o777 } } - pub fn mkdir(&self, p: &Path) -> io::Result<()> { run_path_with_cstr(p, &|p| cvt(unsafe { libc::mkdir(p.as_ptr(), self.mode) }).map(|_| ())) } - pub fn set_mode(&mut self, mode: u32) { self.mode = mode as mode_t; } } - impl fmt::Debug for DirBuilder { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let DirBuilder { mode } = self; f.debug_struct("DirBuilder").field("mode", &Mode(*mode)).finish() } } - impl AsInner for File { #[inline] fn as_inner(&self) -> &FileDesc { &self.0 } } - impl AsInnerMut for File { #[inline] fn as_inner_mut(&mut self) -> &mut FileDesc { &mut self.0 } } - impl IntoInner for File { fn into_inner(self) -> FileDesc { self.0 } } - impl FromInner for File { fn from_inner(file_desc: FileDesc) -> Self { Self(file_desc) } } - impl AsFd for File { #[inline] fn as_fd(&self) -> BorrowedFd<'_> { self.0.as_fd() } } - impl AsRawFd for File { #[inline] fn as_raw_fd(&self) -> RawFd { self.0.as_raw_fd() } } - impl IntoRawFd for File { fn into_raw_fd(self) -> RawFd { self.0.into_raw_fd() } } - impl FromRawFd for File { unsafe fn from_raw_fd(raw_fd: RawFd) -> Self { Self(FromRawFd::from_raw_fd(raw_fd)) } } - impl fmt::Debug for File { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { #[cfg(any(target_os = "linux", target_os = "illumos", target_os = "solaris"))] @@ -1841,20 +1560,13 @@ impl fmt::Debug for File { p.push(&fd.to_string()); run_path_with_cstr(&p, &readlink).ok() } - #[cfg(any(target_vendor = "apple", target_os = "netbsd"))] fn get_path(fd: c_int) -> Option { - // FIXME: The use of PATH_MAX is generally not encouraged, but it - // is inevitable in this case because Apple targets and NetBSD define `fcntl` - // with `F_GETPATH` in terms of `MAXPATHLEN`, and there are no - // alternatives. If a better method is invented, it should be used - // instead. let mut buf = vec![0; libc::PATH_MAX as usize]; let n = unsafe { libc::fcntl(fd, libc::F_GETPATH, buf.as_ptr()) }; if n == -1 { cfg_select! { target_os = "netbsd" => { - // fallback to procfs as last resort let mut p = PathBuf::from("/proc/self/fd"); p.push(&fd.to_string()); return run_path_with_cstr(&p, &readlink).ok() @@ -1869,7 +1581,6 @@ impl fmt::Debug for File { buf.shrink_to_fit(); Some(PathBuf::from(OsString::from_vec(buf))) } - #[cfg(target_os = "freebsd")] fn get_path(fd: c_int) -> Option { let info = Box::::new_zeroed(); @@ -1882,7 +1593,6 @@ impl fmt::Debug for File { let buf = unsafe { CStr::from_ptr(info.kf_path.as_mut_ptr()).to_bytes().to_vec() }; Some(PathBuf::from(OsString::from_vec(buf))) } - #[cfg(target_os = "vxworks")] fn get_path(fd: c_int) -> Option { let mut buf = vec![0; libc::PATH_MAX as usize]; @@ -1894,7 +1604,6 @@ impl fmt::Debug for File { buf.truncate(l as usize); Some(PathBuf::from(OsString::from_vec(buf))) } - #[cfg(not(any( target_os = "linux", target_os = "vxworks", @@ -1905,10 +1614,8 @@ impl fmt::Debug for File { target_vendor = "apple", )))] fn get_path(_fd: c_int) -> Option { - // FIXME(#24570): implement this for other Unix platforms None } - fn get_mode(fd: c_int) -> Option<(bool, bool)> { let mode = unsafe { libc::fcntl(fd, libc::F_GETFL) }; if mode == -1 { @@ -1921,7 +1628,6 @@ impl fmt::Debug for File { _ => None, } } - let fd = self.as_raw_fd(); let mut b = f.debug_struct("File"); b.field("fd", &fd); @@ -1934,21 +1640,10 @@ impl fmt::Debug for File { b.finish() } } - -// Format in octal, followed by the mode format used in `ls -l`. -// -// References: -// https://pubs.opengroup.org/onlinepubs/009696899/utilities/ls.html -// https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html -// https://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html -// -// Example: -// 0o100664 (-rw-rw-r--) impl fmt::Debug for Mode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let Self(mode) = *self; write!(f, "0o{mode:06o}")?; - let entry_type = match mode & libc::S_IFMT { libc::S_IFDIR => 'd', libc::S_IFBLK => 'b', @@ -1958,50 +1653,41 @@ impl fmt::Debug for Mode { libc::S_IFREG => '-', _ => return Ok(()), }; - f.write_str(" (")?; f.write_char(entry_type)?; - - // Owner permissions f.write_char(if mode & libc::S_IRUSR != 0 { 'r' } else { '-' })?; f.write_char(if mode & libc::S_IWUSR != 0 { 'w' } else { '-' })?; let owner_executable = mode & libc::S_IXUSR != 0; let setuid = mode as c_int & libc::S_ISUID as c_int != 0; f.write_char(match (owner_executable, setuid) { - (true, true) => 's', // executable and setuid - (false, true) => 'S', // setuid - (true, false) => 'x', // executable + (true, true) => 's', + (false, true) => 'S', + (true, false) => 'x', (false, false) => '-', })?; - - // Group permissions f.write_char(if mode & libc::S_IRGRP != 0 { 'r' } else { '-' })?; f.write_char(if mode & libc::S_IWGRP != 0 { 'w' } else { '-' })?; let group_executable = mode & libc::S_IXGRP != 0; let setgid = mode as c_int & libc::S_ISGID as c_int != 0; f.write_char(match (group_executable, setgid) { - (true, true) => 's', // executable and setgid - (false, true) => 'S', // setgid - (true, false) => 'x', // executable + (true, true) => 's', + (false, true) => 'S', + (true, false) => 'x', (false, false) => '-', })?; - - // Other permissions f.write_char(if mode & libc::S_IROTH != 0 { 'r' } else { '-' })?; f.write_char(if mode & libc::S_IWOTH != 0 { 'w' } else { '-' })?; let other_executable = mode & libc::S_IXOTH != 0; let sticky = mode as c_int & libc::S_ISVTX as c_int != 0; f.write_char(match (entry_type, other_executable, sticky) { - ('d', true, true) => 't', // searchable and restricted deletion - ('d', false, true) => 'T', // restricted deletion - (_, true, _) => 'x', // executable + ('d', true, true) => 't', + ('d', false, true) => 'T', + (_, true, _) => 'x', (_, false, _) => '-', })?; - f.write_char(')') } } - pub fn readdir(path: &Path) -> io::Result { let ptr = run_path_with_cstr(path, &|p| unsafe { Ok(libc::opendir(p.as_ptr())) })?; if ptr.is_null() { @@ -2012,72 +1698,48 @@ pub fn readdir(path: &Path) -> io::Result { Ok(ReadDir::new(inner)) } } - pub fn unlink(p: &CStr) -> io::Result<()> { cvt(unsafe { libc::unlink(p.as_ptr()) }).map(|_| ()) } - pub fn rename(old: &CStr, new: &CStr) -> io::Result<()> { cvt(unsafe { libc::rename(old.as_ptr(), new.as_ptr()) }).map(|_| ()) } - pub fn set_perm(p: &CStr, perm: FilePermissions) -> io::Result<()> { cvt_r(|| unsafe { libc::chmod(p.as_ptr(), perm.mode) }).map(|_| ()) } - pub fn rmdir(p: &CStr) -> io::Result<()> { cvt(unsafe { libc::rmdir(p.as_ptr()) }).map(|_| ()) } - pub fn readlink(c_path: &CStr) -> io::Result { let p = c_path.as_ptr(); - let mut buf = Vec::with_capacity(256); - loop { let buf_read = cvt(unsafe { libc::readlink(p, buf.as_mut_ptr() as *mut _, buf.capacity()) })? as usize; - unsafe { buf.set_len(buf_read); } - if buf_read != buf.capacity() { buf.shrink_to_fit(); - return Ok(PathBuf::from(OsString::from_vec(buf))); } - - // Trigger the internal buffer resizing logic of `Vec` by requiring - // more space than the current capacity. The length is guaranteed to be - // the same as the capacity due to the if statement above. buf.reserve(1); } } - pub fn symlink(original: &CStr, link: &CStr) -> io::Result<()> { cvt(unsafe { libc::symlink(original.as_ptr(), link.as_ptr()) }).map(|_| ()) } - pub fn link(original: &CStr, link: &CStr) -> io::Result<()> { cfg_select! { any(target_os = "vxworks", target_os = "redox", target_os = "android", target_os = "espidf", target_os = "horizon", target_os = "vita", target_env = "nto70") => { - // VxWorks, Redox and ESP-IDF lack `linkat`, so use `link` instead. POSIX leaves - // it implementation-defined whether `link` follows symlinks, so rely on the - // `symlink_hard_link` test in library/std/src/fs/tests.rs to check the behavior. - // Android has `linkat` on newer versions, but we happen to know `link` - // always has the correct behavior, so it's here as well. cvt(unsafe { libc::link(original.as_ptr(), link.as_ptr()) })?; } _ => { - // Where we can, use `linkat` instead of `link`; see the comment above - // this one for details on why. cvt(unsafe { libc::linkat(libc::AT_FDCWD, original.as_ptr(), libc::AT_FDCWD, link.as_ptr(), 0) })?; } } Ok(()) } - pub fn stat(p: &CStr) -> io::Result { cfg_has_statx! { if let Some(ret) = unsafe { try_statx( @@ -2089,12 +1751,10 @@ pub fn stat(p: &CStr) -> io::Result { return ret; } } - let mut stat: stat64 = unsafe { mem::zeroed() }; cvt(unsafe { stat64(p.as_ptr(), &mut stat) })?; Ok(FileAttr::from_stat64(stat)) } - pub fn lstat(p: &CStr) -> io::Result { cfg_has_statx! { if let Some(ret) = unsafe { try_statx( @@ -2106,12 +1766,10 @@ pub fn lstat(p: &CStr) -> io::Result { return ret; } } - let mut stat: stat64 = unsafe { mem::zeroed() }; cvt(unsafe { lstat64(p.as_ptr(), &mut stat) })?; Ok(FileAttr::from_stat64(stat)) } - pub fn canonicalize(path: &CStr) -> io::Result { let r = unsafe { libc::realpath(path.as_ptr(), ptr::null_mut()) }; if r.is_null() { @@ -2123,11 +1781,9 @@ pub fn canonicalize(path: &CStr) -> io::Result { buf }))) } - fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { use crate::fs::File; use crate::sys::fs::common::NOT_FILE_ERROR; - let reader = File::open(from)?; let metadata = reader.metadata()?; if !metadata.is_file() { @@ -2135,7 +1791,6 @@ fn open_from(from: &Path) -> io::Result<(crate::fs::File, crate::fs::Metadata)> } Ok((reader, metadata)) } - fn set_times_impl(p: &CStr, times: FileTimes, follow_symlinks: bool) -> io::Result<()> { cfg_select! { any(target_os = "redox", target_os = "espidf", target_os = "horizon", target_os = "nuttx") => { @@ -2146,14 +1801,12 @@ fn set_times_impl(p: &CStr, times: FileTimes, follow_symlinks: bool) -> io::Resu )) } target_vendor = "apple" => { - // Apple platforms use setattrlist which supports setting times on symlinks let ta = TimesAttrlist::from_times(×)?; let options = if follow_symlinks { 0 } else { libc::FSOPT_NOFOLLOW }; - cvt(unsafe { libc::setattrlist( p.as_ptr(), ta.attrlist(), @@ -2166,7 +1819,6 @@ fn set_times_impl(p: &CStr, times: FileTimes, follow_symlinks: bool) -> io::Resu target_os = "android" => { let times = [file_time_to_timespec(times.accessed)?, file_time_to_timespec(times.modified)?]; let flags = if follow_symlinks { 0 } else { libc::AT_SYMLINK_NOFOLLOW }; - // utimensat requires Android API level 19 cvt(unsafe { weak!( fn utimensat(dirfd: c_int, path: *const libc::c_char, times: *const libc::timespec, flags: c_int) -> c_int; @@ -2186,12 +1838,9 @@ fn set_times_impl(p: &CStr, times: FileTimes, follow_symlinks: bool) -> io::Resu #[cfg(all(target_os = "linux", target_env = "gnu", target_pointer_width = "32", not(target_arch = "riscv32")))] { use crate::sys::{time::__timespec64, weak::weak}; - - // Added in glibc 2.34 weak!( fn __utimensat64(dirfd: c_int, path: *const c_char, times: *const __timespec64, flags: c_int) -> c_int; ); - if let Some(utimensat64) = __utimensat64.get() { let to_timespec = |time: Option| time.map(|time| time.t.to_timespec64()) .unwrap_or(__timespec64::new(0, libc::UTIME_OMIT as _)); @@ -2206,17 +1855,14 @@ fn set_times_impl(p: &CStr, times: FileTimes, follow_symlinks: bool) -> io::Resu } } } - #[inline(always)] pub fn set_times(p: &CStr, times: FileTimes) -> io::Result<()> { set_times_impl(p, times, true) } - #[inline(always)] pub fn set_times_nofollow(p: &CStr, times: FileTimes) -> io::Result<()> { set_times_impl(p, times, false) } - #[cfg(target_os = "espidf")] fn open_to_and_set_permissions( to: &Path, @@ -2227,7 +1873,6 @@ fn open_to_and_set_permissions( let writer_metadata = writer.metadata()?; Ok((writer, writer_metadata)) } - #[cfg(not(target_os = "espidf"))] fn open_to_and_set_permissions( to: &Path, @@ -2235,34 +1880,25 @@ fn open_to_and_set_permissions( ) -> io::Result<(crate::fs::File, crate::fs::Metadata)> { use crate::fs::OpenOptions; use crate::os::unix::fs::{OpenOptionsExt, PermissionsExt}; - let perm = reader_metadata.permissions(); let writer = OpenOptions::new() - // create the file with the correct mode right away .mode(perm.mode()) .write(true) .create(true) .truncate(true) .open(to)?; let writer_metadata = writer.metadata()?; - // fchmod is broken on vita #[cfg(not(target_os = "vita"))] if writer_metadata.is_file() { - // Set the correct file permissions, in case the file already existed. - // Don't set the permissions on already existing non-files like - // pipes/FIFOs or device nodes. writer.set_permissions(perm)?; } Ok((writer, writer_metadata)) } - mod cfm { use crate::fs::{File, Metadata}; use crate::io::{BorrowedCursor, IoSlice, IoSliceMut, Read, Result, Write}; - #[allow(dead_code)] pub struct CachedFileMetadata(pub File, pub Metadata); - impl Read for CachedFileMetadata { fn read(&mut self, buf: &mut [u8]) -> Result { self.0.read(buf) @@ -2303,56 +1939,38 @@ mod cfm { } #[cfg(any(target_os = "linux", target_os = "android"))] pub(in crate::sys) use cfm::CachedFileMetadata; - #[cfg(not(target_vendor = "apple"))] pub fn copy(from: &Path, to: &Path) -> io::Result { let (reader, reader_metadata) = open_from(from)?; let (writer, writer_metadata) = open_to_and_set_permissions(to, &reader_metadata)?; - io::copy( &mut cfm::CachedFileMetadata(reader, reader_metadata), &mut cfm::CachedFileMetadata(writer, writer_metadata), ) } - #[cfg(target_vendor = "apple")] pub fn copy(from: &Path, to: &Path) -> io::Result { const COPYFILE_ALL: libc::copyfile_flags_t = libc::COPYFILE_METADATA | libc::COPYFILE_DATA; - struct FreeOnDrop(libc::copyfile_state_t); impl Drop for FreeOnDrop { fn drop(&mut self) { - // The code below ensures that `FreeOnDrop` is never a null pointer unsafe { - // `copyfile_state_free` returns -1 if the `to` or `from` files - // cannot be closed. However, this is not considered an error. libc::copyfile_state_free(self.0); } } } - let (reader, reader_metadata) = open_from(from)?; - let clonefile_result = run_path_with_cstr(to, &|to| { cvt(unsafe { libc::fclonefileat(reader.as_raw_fd(), libc::AT_FDCWD, to.as_ptr(), 0) }) }); match clonefile_result { Ok(_) => return Ok(reader_metadata.len()), Err(e) => match e.raw_os_error() { - // `fclonefileat` will fail on non-APFS volumes, if the - // destination already exists, or if the source and destination - // are on different devices. In all these cases `fcopyfile` - // should succeed. Some(libc::ENOTSUP) | Some(libc::EEXIST) | Some(libc::EXDEV) => (), _ => return Err(e), }, } - - // Fall back to using `fcopyfile` if `fclonefileat` does not succeed. let (writer, writer_metadata) = open_to_and_set_permissions(to, &reader_metadata)?; - - // We ensure that `FreeOnDrop` never contains a null pointer so it is - // always safe to call `copyfile_state_free` let state = unsafe { let state = libc::copyfile_state_alloc(); if state.is_null() { @@ -2360,11 +1978,8 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { } FreeOnDrop(state) }; - let flags = if writer_metadata.is_file() { COPYFILE_ALL } else { libc::COPYFILE_DATA }; - cvt(unsafe { libc::fcopyfile(reader.as_raw_fd(), writer.as_raw_fd(), state.0, flags) })?; - let mut bytes_copied: libc::off_t = 0; cvt(unsafe { libc::copyfile_state_get( @@ -2375,19 +1990,16 @@ pub fn copy(from: &Path, to: &Path) -> io::Result { })?; Ok(bytes_copied as u64) } - pub fn chown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { run_path_with_cstr(path, &|path| { cvt(unsafe { libc::chown(path.as_ptr(), uid as libc::uid_t, gid as libc::gid_t) }) .map(|_| ()) }) } - pub fn fchown(fd: c_int, uid: u32, gid: u32) -> io::Result<()> { cvt(unsafe { libc::fchown(fd, uid as libc::uid_t, gid as libc::gid_t) })?; Ok(()) } - #[cfg(not(target_os = "vxworks"))] pub fn lchown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { run_path_with_cstr(path, &|path| { @@ -2395,33 +2007,24 @@ pub fn lchown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { .map(|_| ()) }) } - #[cfg(target_os = "vxworks")] pub fn lchown(path: &Path, uid: u32, gid: u32) -> io::Result<()> { - let (_, _, _) = (path, uid, gid); Err(io::const_error!(io::ErrorKind::Unsupported, "lchown not supported by vxworks")) } - #[cfg(not(any(target_os = "fuchsia", target_os = "vxworks")))] pub fn chroot(dir: &Path) -> io::Result<()> { run_path_with_cstr(dir, &|dir| cvt(unsafe { libc::chroot(dir.as_ptr()) }).map(|_| ())) } - #[cfg(target_os = "vxworks")] pub fn chroot(dir: &Path) -> io::Result<()> { - let _ = dir; Err(io::const_error!(io::ErrorKind::Unsupported, "chroot not supported by vxworks")) } - pub fn mkfifo(path: &Path, mode: u32) -> io::Result<()> { run_path_with_cstr(path, &|path| { cvt(unsafe { libc::mkfifo(path.as_ptr(), mode.try_into().unwrap()) }).map(|_| ()) }) } - pub use remove_dir_impl::remove_dir_all; - -// Fallback for REDOX, ESP-ID, Horizon, Vita, Vxworks and Miri #[cfg(any( target_os = "redox", target_os = "espidf", @@ -2434,8 +2037,6 @@ pub use remove_dir_impl::remove_dir_all; mod remove_dir_impl { pub use crate::sys::fs::common::remove_dir_all; } - -// Modern implementation using openat(), unlinkat() and fdopendir() #[cfg(not(any( target_os = "redox", target_os = "espidf", @@ -2450,7 +2051,6 @@ mod remove_dir_impl { use libc::{fdopendir, openat, unlinkat}; #[cfg(all(target_os = "linux", target_env = "gnu"))] use libc::{fdopendir, openat64 as openat, unlinkat}; - use super::{Dir, DirEntry, InnerReadDir, ReadDir, lstat}; use crate::ffi::CStr; use crate::io; @@ -2460,7 +2060,6 @@ mod remove_dir_impl { use crate::sys::common::small_c_string::run_path_with_cstr; use crate::sys::{cvt, cvt_r}; use crate::sys_common::ignore_notfound; - pub fn openat_nofollow_dironly(parent_fd: Option, p: &CStr) -> io::Result { let fd = cvt_r(|| unsafe { openat( @@ -2471,22 +2070,17 @@ mod remove_dir_impl { })?; Ok(unsafe { OwnedFd::from_raw_fd(fd) }) } - fn fdreaddir(dir_fd: OwnedFd) -> io::Result<(ReadDir, RawFd)> { let ptr = unsafe { fdopendir(dir_fd.as_raw_fd()) }; if ptr.is_null() { return Err(io::Error::last_os_error()); } let dirp = Dir(ptr); - // file descriptor is automatically closed by libc::closedir() now, so give up ownership let new_parent_fd = dir_fd.into_raw_fd(); - // a valid root is not needed because we do not call any functions involving the full path - // of the `DirEntry`s. let dummy_root = PathBuf::new(); let inner = InnerReadDir { dirp, root: dummy_root }; Ok((ReadDir::new(inner), new_parent_fd)) } - #[cfg(any( target_os = "solaris", target_os = "illumos", @@ -2497,7 +2091,6 @@ mod remove_dir_impl { fn is_dir(_ent: &DirEntry) -> Option { None } - #[cfg(not(any( target_os = "solaris", target_os = "illumos", @@ -2512,7 +2105,6 @@ mod remove_dir_impl { _ => Some(false), } } - fn is_enoent(result: &io::Result<()>) -> bool { if let Err(err) = result && matches!(err.raw_os_error(), Some(libc::ENOENT)) @@ -2522,33 +2114,22 @@ mod remove_dir_impl { false } } - fn remove_dir_all_recursive(parent_fd: Option, path: &CStr) -> io::Result<()> { - // try opening as directory let fd = match openat_nofollow_dironly(parent_fd, &path) { Err(err) if matches!(err.raw_os_error(), Some(libc::ENOTDIR | libc::ELOOP)) => { - // not a directory - don't traverse further - // (for symlinks, older Linux kernels may return ELOOP instead of ENOTDIR) return match parent_fd { - // unlink... Some(parent_fd) => { cvt(unsafe { unlinkat(parent_fd, path.as_ptr(), 0) }).map(drop) } - // ...unless this was supposed to be the deletion root directory None => Err(err), }; } result => result?, }; - - // open the directory passing ownership of the fd let (dir, fd) = fdreaddir(fd)?; for child in dir { let child = child?; let child_name = child.name_cstr(); - // we need an inner try block, because if one of these - // directories has already been deleted, then we need to - // continue the loop, not return ok. let result: io::Result<()> = try { match is_dir(&child) { Some(true) => { @@ -2558,10 +2139,6 @@ mod remove_dir_impl { cvt(unsafe { unlinkat(fd, child_name.as_ptr(), 0) })?; } None => { - // POSIX specifies that calling unlink()/unlinkat(..., 0) on a directory can succeed - // if the process has the appropriate privileges. This however can causing orphaned - // directories requiring an fsck e.g. on Solaris and Illumos. So we try recursing - // into it first instead of trying to unlink() it. remove_dir_all_recursive(Some(fd), child_name)?; } } @@ -2570,18 +2147,12 @@ mod remove_dir_impl { return result; } } - - // unlink the directory after removing its contents ignore_notfound(cvt(unsafe { unlinkat(parent_fd.unwrap_or(libc::AT_FDCWD), path.as_ptr(), libc::AT_REMOVEDIR) }))?; Ok(()) } - fn remove_dir_all_modern(p: &CStr) -> io::Result<()> { - // We cannot just call remove_dir_all_recursive() here because that would not delete a passed - // symlink. No need to worry about races, because remove_dir_all_recursive() does not recurse - // into symlinks. let attr = lstat(p)?; if attr.file_type().is_symlink() { super::unlink(p) @@ -2589,8 +2160,7 @@ mod remove_dir_impl { remove_dir_all_recursive(None, &p) } } - pub fn remove_dir_all(p: &Path) -> io::Result<()> { run_path_with_cstr(p, &remove_dir_all_modern) } -} +} \ No newline at end of file