Skip to content

Commit

Permalink
Add wrappers for futimens(2) and utimesat(2)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmmv committed Sep 28, 2018
1 parent d302e8d commit 313f857
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 9 deletions.
79 changes: 73 additions & 6 deletions src/sys/stat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ use errno::Errno;
use fcntl::AtFlags;
use libc::{self, mode_t};
use std::mem;
use std::os::raw;
use std::os::unix::io::RawFd;
use sys::time::TimeSpec;

libc_bitflags!(
pub struct SFlag: mode_t {
Expand Down Expand Up @@ -133,6 +135,15 @@ pub fn fchmod(fd: RawFd, mode: Mode) -> Result<()> {
Errno::result(res).map(|_| ())
}

/// Computes the raw fd consumed by a function of the form `*at`.
#[inline]
fn actual_atfd(fd: Option<RawFd>) -> raw::c_int {
match fd {
None => libc::AT_FDCWD,
Some(fd) => fd,
}
}

/// Flags for `fchmodat` function.
#[derive(Clone, Copy, Debug)]
pub enum FchmodatFlags {
Expand Down Expand Up @@ -162,19 +173,14 @@ pub fn fchmodat<P: ?Sized + NixPath>(
mode: Mode,
flag: FchmodatFlags,
) -> Result<()> {
let actual_dirfd =
match dirfd {
None => libc::AT_FDCWD,
Some(fd) => fd,
};
let atflag =
match flag {
FchmodatFlags::FollowSymlink => AtFlags::empty(),
FchmodatFlags::NoFollowSymlink => AtFlags::AT_SYMLINK_NOFOLLOW,
};
let res = path.with_nix_path(|cstr| unsafe {
libc::fchmodat(
actual_dirfd,
actual_atfd(dirfd),
cstr.as_ptr(),
mode.bits() as mode_t,
atflag.bits() as libc::c_int,
Expand All @@ -183,3 +189,64 @@ pub fn fchmodat<P: ?Sized + NixPath>(

Errno::result(res).map(|_| ())
}

/// Change the access and modification times of the file specified by a file descriptor.
///
/// # References
///
/// [futimens(2)](http://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()];
let res = unsafe { libc::futimens(fd, &times[0]) };

Errno::result(res).map(|_| ())
}

/// Flags for `utimensat` function.
#[derive(Clone, Copy, Debug)]
pub enum UtimensatFlags {
FollowSymlink,
NoFollowSymlink,
}

/// Change the access and modification times of a file.
///
/// The file to be changed is determined relative to the directory associated
/// with the file descriptor `dirfd` or the current working directory
/// if `dirfd` is `None`.
///
/// If `flag` is `UtimensatFlags::NoFollowSymlink` and `path` names a symbolic link,
/// then the mode of the symbolic link is changed.
///
/// `utimensat(None, path, times, UtimensatFlags::FollowSymlink)` is identical to
/// a call `libc::utimes(path, times)`. That's why `utimes` is unimplemented
/// in the `nix` crate.
///
/// # References
///
/// [utimensat(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/utimens.html).
pub fn utimensat<P: ?Sized + NixPath>(
dirfd: Option<RawFd>,
path: &P,
atime: &TimeSpec,
mtime: &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 res = path.with_nix_path(|cstr| unsafe {
libc::utimensat(
actual_atfd(dirfd),
cstr.as_ptr(),
&times[0],
atflag.bits() as libc::c_int,
)
})?;

Errno::result(res).map(|_| ())
}
53 changes: 50 additions & 3 deletions test/test_stat.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
use std::fs::File;
use std::fs::{self, File};
use std::os::unix::fs::symlink;
use std::os::unix::prelude::AsRawFd;
use std::time::{Duration, UNIX_EPOCH};

use libc::{S_IFMT, S_IFLNK};

use nix::fcntl;
use nix::sys::stat::{self, fchmod, fchmodat, fstat, lstat, stat};
use nix::sys::stat::{FileStat, Mode, FchmodatFlags};
use nix::sys::stat::{self, fchmod, fchmodat, fstat, futimens, lstat, stat, utimensat};
use nix::sys::stat::{FileStat, Mode, FchmodatFlags, UtimensatFlags};
use nix::sys::time::{TimeSpec, TimeValLike};
use nix::unistd::chdir;
use nix::Result;
use tempfile;
Expand Down Expand Up @@ -152,3 +154,48 @@ fn test_fchmodat() {
let file_stat2 = stat(&fullpath).unwrap();
assert_eq!(file_stat2.st_mode & 0o7777, mode2.bits());
}

/// Asserts that the atime and mtime in a file's metadata match expected values.
///
/// The atime and mtime are expressed with a resolution of seconds because some file systems
/// (like macOS's HFS+) do not have higher granularity.
fn assert_times_eq(exp_atime_sec: u64, exp_mtime_sec: u64, attr: &fs::Metadata) {
assert_eq!(
Duration::new(exp_atime_sec, 0),
attr.accessed().unwrap().duration_since(UNIX_EPOCH).unwrap());
assert_eq!(
Duration::new(exp_mtime_sec, 0),
attr.modified().unwrap().duration_since(UNIX_EPOCH).unwrap());
}

#[test]
fn test_futimens() {
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();

futimens(fd, &TimeSpec::seconds(10), &TimeSpec::seconds(20)).unwrap();
assert_times_eq(10, 20, &fs::metadata(&fullpath).unwrap());
}

#[test]
fn test_utimensat() {
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();

utimensat(Some(dirfd), filename, &TimeSpec::seconds(12345), &TimeSpec::seconds(678),
UtimensatFlags::FollowSymlink).unwrap();
assert_times_eq(12345, 678, &fs::metadata(&fullpath).unwrap());

chdir(tempdir.path()).unwrap();

utimensat(None, filename, &TimeSpec::seconds(500), &TimeSpec::seconds(800),
UtimensatFlags::FollowSymlink).unwrap();
assert_times_eq(500, 800, &fs::metadata(&fullpath).unwrap());
}

0 comments on commit 313f857

Please sign in to comment.