Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions library/std/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,87 @@ pub fn write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, contents: C) -> io::Result
inner(path.as_ref(), contents.as_ref())
}

/// Changes the timestamps of the file or directory at the specified path.
///
/// This function will attempt to set the access and modification times
/// to the times specified. If the path refers to a symbolic link, this function
/// will follow the link and change the timestamps of the target file.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `utimensat` function on Unix platforms, the
/// `setattrlist` function on Apple platforms, and the `SetFileTime` function on Windows.
///
/// # Errors
///
/// This function will return an error if the user lacks permission to change timestamps on the
/// target file or symlink. It may also return an error if the OS does not support it.
///
/// # Examples
///
/// ```no_run
/// #![feature(fs_set_times)]
/// use std::fs::{self, FileTimes};
/// use std::time::SystemTime;
///
/// fn main() -> std::io::Result<()> {
/// let now = SystemTime::now();
/// let times = FileTimes::new()
/// .set_accessed(now)
/// .set_modified(now);
/// fs::set_times("foo.txt", times)?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "fs_set_times", issue = "147455")]
#[doc(alias = "utimens")]
#[doc(alias = "utimes")]
#[doc(alias = "utime")]
pub fn set_times<P: AsRef<Path>>(path: P, times: FileTimes) -> io::Result<()> {
fs_imp::set_times(path.as_ref(), times.0)
}

/// Changes the timestamps of the file or symlink at the specified path.
///
/// This function will attempt to set the access and modification times
/// to the times specified. Differ from `set_times`, if the path refers to a symbolic link,
/// this function will change the timestamps of the symlink itself, not the target file.
///
/// # Platform-specific behavior
///
/// This function currently corresponds to the `utimensat` function with `AT_SYMLINK_NOFOLLOW` on
/// Unix platforms, the `setattrlist` function with `FSOPT_NOFOLLOW` on Apple platforms, and the
/// `SetFileTime` function on Windows.
///
/// # Errors
///
/// This function will return an error if the user lacks permission to change timestamps on the
/// target file or symlink. It may also return an error if the OS does not support it.
///
/// # Examples
///
/// ```no_run
/// #![feature(fs_set_times)]
/// use std::fs::{self, FileTimes};
/// use std::time::SystemTime;
///
/// fn main() -> std::io::Result<()> {
/// let now = SystemTime::now();
/// let times = FileTimes::new()
/// .set_accessed(now)
/// .set_modified(now);
/// fs::set_times_nofollow("symlink.txt", times)?;
/// Ok(())
/// }
/// ```
#[unstable(feature = "fs_set_times", issue = "147455")]
#[doc(alias = "utimensat")]
#[doc(alias = "lutimens")]
#[doc(alias = "lutimes")]
pub fn set_times_nofollow<P: AsRef<Path>>(path: P, times: FileTimes) -> io::Result<()> {
fs_imp::set_times_nofollow(path.as_ref(), times.0)
}

#[stable(feature = "file_lock", since = "1.89.0")]
impl error::Error for TryLockError {}

Expand Down
219 changes: 219 additions & 0 deletions library/std/src/fs/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2226,3 +2226,222 @@ fn test_open_options_invalid_combinations() {
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(err.to_string(), "must specify at least one of read, write, or append access");
}

#[test]
fn test_fs_set_times() {
#[cfg(target_vendor = "apple")]
use crate::os::darwin::fs::FileTimesExt;
#[cfg(windows)]
use crate::os::windows::fs::FileTimesExt;

let tmp = tmpdir();
let path = tmp.join("foo");
File::create(&path).unwrap();

let mut times = FileTimes::new();
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345);
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321);
times = times.set_accessed(accessed).set_modified(modified);

#[cfg(any(windows, target_vendor = "apple"))]
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123);
#[cfg(any(windows, target_vendor = "apple"))]
{
times = times.set_created(created);
}

match fs::set_times(&path, times) {
// Allow unsupported errors on platforms which don't support setting times.
#[cfg(not(any(
windows,
all(
unix,
not(any(
target_os = "android",
target_os = "redox",
target_os = "espidf",
target_os = "horizon"
))
)
)))]
Err(e) if e.kind() == ErrorKind::Unsupported => return,
Err(e) => panic!("error setting file times: {e:?}"),
Ok(_) => {}
}

let metadata = fs::metadata(&path).unwrap();
assert_eq!(metadata.accessed().unwrap(), accessed);
assert_eq!(metadata.modified().unwrap(), modified);
#[cfg(any(windows, target_vendor = "apple"))]
{
assert_eq!(metadata.created().unwrap(), created);
}
}

#[test]
fn test_fs_set_times_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");
#[cfg(unix)]
crate::os::unix::fs::symlink(&target, &link).unwrap();

#[cfg(windows)]
let link = tmp.join("link.txt");
#[cfg(windows)]
crate::os::windows::fs::symlink_file(&target, &link).unwrap();

// Get the symlink's own modified time BEFORE calling set_times (to compare later)
// We don't check accessed time because reading metadata may update atime on some platforms.
let link_metadata_before = fs::symlink_metadata(&link).unwrap();
let link_modified_before = link_metadata_before.modified().unwrap();

let mut times = FileTimes::new();
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(12345);
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(54321);
times = times.set_accessed(accessed).set_modified(modified);

#[cfg(any(windows, target_vendor = "apple"))]
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(32123);
#[cfg(any(windows, target_vendor = "apple"))]
{
times = times.set_created(created);
}

// Call fs::set_times on the symlink - it should follow the link and modify the target
match fs::set_times(&link, times) {
// Allow unsupported errors on platforms which don't support setting times.
#[cfg(not(any(
windows,
all(
unix,
not(any(
target_os = "android",
target_os = "redox",
target_os = "espidf",
target_os = "horizon"
))
)
)))]
Err(e) if e.kind() == ErrorKind::Unsupported => return,
Err(e) => panic!("error setting file times through symlink: {e:?}"),
Ok(_) => {}
}

// Verify that the TARGET file's times were changed (following the symlink)
let target_metadata = fs::metadata(&target).unwrap();
assert_eq!(
target_metadata.accessed().unwrap(),
accessed,
"target file accessed time should match"
);
assert_eq!(
target_metadata.modified().unwrap(),
modified,
"target file modified time should match"
);
#[cfg(any(windows, target_vendor = "apple"))]
{
assert_eq!(
target_metadata.created().unwrap(),
created,
"target file created time should match"
);
}

// Also verify through the symlink (fs::metadata follows symlinks)
let link_followed_metadata = fs::metadata(&link).unwrap();
assert_eq!(link_followed_metadata.accessed().unwrap(), accessed);
assert_eq!(link_followed_metadata.modified().unwrap(), modified);

// Verify that the SYMLINK ITSELF was NOT modified
// Note: We only check modified time, not accessed time, because reading the symlink
// metadata may update its atime on some platforms (e.g., Linux).
let link_metadata_after = fs::symlink_metadata(&link).unwrap();
assert_eq!(
link_metadata_after.modified().unwrap(),
link_modified_before,
"symlink's own modified time should not change"
);
}

#[test]
fn test_fs_set_times_nofollow() {
#[cfg(target_vendor = "apple")]
use crate::os::darwin::fs::FileTimesExt;
#[cfg(windows)]
use crate::os::windows::fs::FileTimesExt;

let tmp = tmpdir();

// Create a target file and a symlink to it
let target = tmp.join("target");
File::create(&target).unwrap();

#[cfg(unix)]
let link = tmp.join("link");
#[cfg(unix)]
crate::os::unix::fs::symlink(&target, &link).unwrap();

#[cfg(windows)]
let link = tmp.join("link.txt");
#[cfg(windows)]
crate::os::windows::fs::symlink_file(&target, &link).unwrap();

let mut times = FileTimes::new();
let accessed = SystemTime::UNIX_EPOCH + Duration::from_secs(11111);
let modified = SystemTime::UNIX_EPOCH + Duration::from_secs(22222);
times = times.set_accessed(accessed).set_modified(modified);

#[cfg(any(windows, target_vendor = "apple"))]
let created = SystemTime::UNIX_EPOCH + Duration::from_secs(33333);
#[cfg(any(windows, target_vendor = "apple"))]
{
times = times.set_created(created);
}

// Set times on the symlink itself (not following it)
match fs::set_times_nofollow(&link, times) {
// Allow unsupported errors on platforms which don't support setting times.
#[cfg(not(any(
windows,
all(
unix,
not(any(
target_os = "android",
target_os = "redox",
target_os = "espidf",
target_os = "horizon"
))
)
)))]
Err(e) if e.kind() == ErrorKind::Unsupported => return,
Err(e) => panic!("error setting symlink times: {e:?}"),
Ok(_) => {}
}

// Read symlink metadata (without following)
let metadata = fs::symlink_metadata(&link).unwrap();
assert_eq!(metadata.accessed().unwrap(), accessed);
assert_eq!(metadata.modified().unwrap(), modified);
#[cfg(any(windows, target_vendor = "apple"))]
{
assert_eq!(metadata.created().unwrap(), created);
}

// Verify that the target file's times were NOT changed
let target_metadata = fs::metadata(&target).unwrap();
assert_ne!(target_metadata.accessed().unwrap(), accessed);
assert_ne!(target_metadata.modified().unwrap(), modified);
}
8 changes: 8 additions & 0 deletions library/std/src/sys/fs/hermit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,14 @@ pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> {
Err(Error::from_raw_os_error(22))
}

pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
Err(Error::from_raw_os_error(22))
}

pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
Err(Error::from_raw_os_error(22))
}

pub fn rmdir(path: &Path) -> io::Result<()> {
run_path_with_cstr(path, &|path| cvt(unsafe { hermit_abi::rmdir(path.as_ptr()) }).map(|_| ()))
}
Expand Down
8 changes: 8 additions & 0 deletions library/std/src/sys/fs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,11 @@ pub fn exists(path: &Path) -> io::Result<bool> {
#[cfg(windows)]
with_native_path(path, &imp::exists)
}

pub fn set_times(path: &Path, times: FileTimes) -> io::Result<()> {
with_native_path(path, &|path| imp::set_times(path, times.clone()))
}

pub fn set_times_nofollow(path: &Path, times: FileTimes) -> io::Result<()> {
with_native_path(path, &|path| imp::set_times_nofollow(path, times.clone()))
}
8 changes: 8 additions & 0 deletions library/std/src/sys/fs/solid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,14 @@ pub fn set_perm(p: &Path, perm: FilePermissions) -> io::Result<()> {
Ok(())
}

pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
unsupported()
}

pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
unsupported()
}

pub fn rmdir(p: &Path) -> io::Result<()> {
if stat(p)?.file_type().is_dir() {
error::SolidError::err_if_negative(unsafe { abi::SOLID_FS_Unlink(cstr(p)?.as_ptr()) })
Expand Down
8 changes: 8 additions & 0 deletions library/std/src/sys/fs/uefi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,14 @@ pub fn set_perm(_p: &Path, _perm: FilePermissions) -> io::Result<()> {
unsupported()
}

pub fn set_times(_p: &Path, _times: FileTimes) -> io::Result<()> {
unsupported()
}

pub fn set_times_nofollow(_p: &Path, _times: FileTimes) -> io::Result<()> {
unsupported()
}

pub fn rmdir(_p: &Path) -> io::Result<()> {
unsupported()
}
Expand Down
Loading
Loading