diff --git a/README.md b/README.md index 7597ba0afc..528a33d691 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ usage. As an example of what Nix provides, examine the differences between what is exposed by libc and nix for the -[gethostname](https://man7.org/linux/man-pages/man2/gethostname.2.html) system +[gethostname](https://pubs.opengroup.org/onlinepubs/9699919799/functions/gethostname.html) system call: ```rust,ignore diff --git a/src/sys/stat.rs b/src/sys/stat.rs index 78203bfbe3..7bfc3576be 100644 --- a/src/sys/stat.rs +++ b/src/sys/stat.rs @@ -7,6 +7,7 @@ pub use libc::c_uint; ))] pub use libc::c_ulong; pub use libc::stat as FileStat; +pub use libc::UTIME_NOW; pub use libc::{dev_t, mode_t}; #[cfg(not(target_os = "redox"))] @@ -402,14 +403,51 @@ pub fn lutimes( Errno::result(res).map(drop) } -/// Change the access and modification times of the file specified by a file descriptor. +/// A helper function to convert `atime: Option<&TimeSpec>, mtime: Option<&TimeSpec>` +/// to `[libc::timespec; 2], used in the implementation of [`utimnsat(3)`] +/// and [`futimens(3)`] +fn time_convert( + atime: Option<&TimeSpec>, + mtime: Option<&TimeSpec>, +) -> [libc::timespec; 2] { + let mut times = [ + libc::timespec { + tv_sec: 0, + tv_nsec: libc::UTIME_OMIT, + }, + libc::timespec { + tv_sec: 0, + tv_nsec: libc::UTIME_OMIT, + }, + ]; + if let Some(atime) = atime { + times[0].tv_sec = atime.tv_sec(); + times[0].tv_nsec = atime.tv_nsec(); + } + if let Some(mtime) = mtime { + times[1].tv_sec = mtime.tv_sec(); + times[1].tv_nsec = mtime.tv_nsec(); + } + times +} + +/// Change the access and modification times of the file specified by a file +/// descriptor. +/// +/// When a timestamp argument is set to `None`, it remains unchanged. If you +/// would like to change a timestamp to the special value `Now`, set the `st_nsec` +/// field of that timestamp to [`UTIME_NOW`]. /// /// # References /// /// [futimens(2)](https://pubs.opengroup.org/onlinepubs/9699919799/functions/futimens.html). #[inline] -pub fn futimens(fd: RawFd, atime: &TimeSpec, mtime: &TimeSpec) -> Result<()> { - let times: [libc::timespec; 2] = [*atime.as_ref(), *mtime.as_ref()]; +pub fn futimens( + fd: RawFd, + atime: Option<&TimeSpec>, + mtime: Option<&TimeSpec>, +) -> Result<()> { + let times = time_convert(atime, mtime); let res = unsafe { libc::futimens(fd, ×[0]) }; Errno::result(res).map(drop) @@ -429,6 +467,10 @@ pub enum UtimensatFlags { /// with the file descriptor `dirfd` or the current working directory /// if `dirfd` is `None`. /// +/// When a timestamp argument is set to `None`, it remains unchanged. If you +/// would like to change a timestamp to the special value `Now`, set the `st_nsec` +/// field of that timestamp to [`UTIME_NOW`]. +/// /// If `flag` is `UtimensatFlags::NoFollowSymlink` and `path` names a symbolic link, /// then the mode of the symbolic link is changed. /// @@ -444,15 +486,15 @@ pub enum UtimensatFlags { pub fn utimensat( dirfd: Option, path: &P, - atime: &TimeSpec, - mtime: &TimeSpec, + atime: Option<&TimeSpec>, + mtime: Option<&TimeSpec>, flag: UtimensatFlags, ) -> Result<()> { let atflag = match flag { UtimensatFlags::FollowSymlink => AtFlags::empty(), UtimensatFlags::NoFollowSymlink => AtFlags::AT_SYMLINK_NOFOLLOW, }; - let times: [libc::timespec; 2] = [*atime.as_ref(), *mtime.as_ref()]; + let times = time_convert(atime, mtime); let res = path.with_nix_path(|cstr| unsafe { libc::utimensat( at_rawfd(dirfd), diff --git a/test/test_stat.rs b/test/test_stat.rs index 55f15c0771..8d09e41e83 100644 --- a/test/test_stat.rs +++ b/test/test_stat.rs @@ -275,10 +275,47 @@ fn test_futimens() { let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty()) .unwrap(); - futimens(fd, &TimeSpec::seconds(10), &TimeSpec::seconds(20)).unwrap(); + futimens( + fd, + Some(&TimeSpec::seconds(10)), + Some(&TimeSpec::seconds(20)), + ) + .unwrap(); assert_times_eq(10, 20, &fs::metadata(&fullpath).unwrap()); } +#[test] +#[cfg(not(any(target_os = "redox", target_os = "haiku")))] +fn test_futimens_unchanged() { + let tempdir = tempfile::tempdir().unwrap(); + let fullpath = tempdir.path().join("file"); + drop(File::create(&fullpath).unwrap()); + let fd = fcntl::open(&fullpath, fcntl::OFlag::empty(), stat::Mode::empty()) + .unwrap(); + + let old_atime = fs::metadata(fullpath.as_path()) + .unwrap() + .accessed() + .unwrap(); + let old_mtime = fs::metadata(fullpath.as_path()) + .unwrap() + .modified() + .unwrap(); + + futimens(fd, None, None).unwrap(); + + let new_atime = fs::metadata(fullpath.as_path()) + .unwrap() + .accessed() + .unwrap(); + let new_mtime = fs::metadata(fullpath.as_path()) + .unwrap() + .modified() + .unwrap(); + assert_eq!(old_atime, new_atime); + assert_eq!(old_mtime, new_mtime); +} + #[test] #[cfg(not(any(target_os = "redox", target_os = "haiku")))] fn test_utimensat() { @@ -295,8 +332,8 @@ fn test_utimensat() { utimensat( Some(dirfd), filename, - &TimeSpec::seconds(12345), - &TimeSpec::seconds(678), + Some(&TimeSpec::seconds(12345)), + Some(&TimeSpec::seconds(678)), UtimensatFlags::FollowSymlink, ) .unwrap(); @@ -307,14 +344,54 @@ fn test_utimensat() { utimensat( None, filename, - &TimeSpec::seconds(500), - &TimeSpec::seconds(800), + Some(&TimeSpec::seconds(500)), + Some(&TimeSpec::seconds(800)), UtimensatFlags::FollowSymlink, ) .unwrap(); assert_times_eq(500, 800, &fs::metadata(&fullpath).unwrap()); } +#[test] +#[cfg(not(any(target_os = "redox", target_os = "haiku")))] +fn test_utimensat_unchanged() { + let _dr = crate::DirRestore::new(); + let tempdir = tempfile::tempdir().unwrap(); + let filename = "foo.txt"; + let fullpath = tempdir.path().join(filename); + drop(File::create(&fullpath).unwrap()); + let dirfd = + fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()) + .unwrap(); + + let old_atime = fs::metadata(fullpath.as_path()) + .unwrap() + .accessed() + .unwrap(); + let old_mtime = fs::metadata(fullpath.as_path()) + .unwrap() + .modified() + .unwrap(); + utimensat( + Some(dirfd), + filename, + None, + None, + UtimensatFlags::NoFollowSymlink, + ) + .unwrap(); + let new_atime = fs::metadata(fullpath.as_path()) + .unwrap() + .accessed() + .unwrap(); + let new_mtime = fs::metadata(fullpath.as_path()) + .unwrap() + .modified() + .unwrap(); + assert_eq!(old_atime, new_atime); + assert_eq!(old_mtime, new_mtime); +} + #[test] #[cfg(not(target_os = "redox"))] fn test_mkdirat_success_path() {