Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable setting file times for any writable files #105

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
91 changes: 89 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,23 @@ where
imp::set_file_times(p.as_ref(), atime, mtime)
}

/// Set the last access and modification times for a file on the filesystem to "now".
/// If `follow_symlinks` is true, then this function follows symlinks; otherwise,
/// it operates on the file itself, even if it is a symlink.
///
/// This function will set the `atime` and `mtime` metadata fields for a file
/// on the local filesystem, returning any error encountered.
///
/// This is not just a convenience function; on unix, a special form of the
/// syscall is required to convince the kernel to update atime/mtime of files
/// owned by other users.
pub fn set_file_times_now<P>(p: P, follow_symlink: bool) -> io::Result<()>
where
P: AsRef<Path>,
{
imp::set_file_times_now(p.as_ref(), follow_symlink)
}

/// Set the last access and modification times for a file handle.
///
/// This function will either or both of the `atime` and `mtime` metadata
Expand All @@ -253,6 +270,20 @@ pub fn set_file_handle_times(
imp::set_file_handle_times(f, atime, mtime)
}

/// Set the last access and modification times for a file handle to "now".
///
/// This function will either or both of the `atime` and `mtime` metadata
/// fields for a file handle , returning any error encountered. If `None` is
/// specified then the time won't be updated. If `None` is specified for both
/// options then no action is taken.
///
/// This is not just a convenience function; on unix, a special form of the
/// syscall is required to convinve the kernel to update atime/mtime of files
/// owned by other users.
pub fn set_file_handle_times_now(f: &fs::File) -> io::Result<()> {
imp::set_file_handle_times_now(f)
}

/// Set the last access and modification times for a file on the filesystem.
/// This function does not follow symlink.
///
Expand Down Expand Up @@ -302,8 +333,8 @@ where
#[cfg(test)]
mod tests {
use super::{
set_file_atime, set_file_handle_times, set_file_mtime, set_file_times,
set_symlink_file_times, FileTime,
set_file_atime, set_file_handle_times, set_file_handle_times_now, set_file_mtime,
set_file_times, set_file_times_now, set_symlink_file_times, FileTime,
};
use std::fs::{self, File};
use std::io;
Expand Down Expand Up @@ -486,6 +517,62 @@ mod tests {
Ok(())
}

#[test]
#[cfg(unix)]
// TODO: Do Android and MacOS implement this feature of utimensat?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I guess it's safest to implement it just the way that this PR already does: Defer to the non-NULL variant, and improve it in a later PR if/when more is known about what Darwin accepts.

#[cfg(not(target_os = "android"))]
#[cfg(not(target_os = "macos"))]
fn set_root_file_path_times_test() -> io::Result<()> {
// Need a root-owned all-writable file while we execute as non-root.
// Thankfully, /dev/null is such a file, and is universally present!
let path = Path::new("/dev/null");

let old_metadata = fs::metadata(&path)?;
let old_mtime = FileTime::from_last_modification_time(&old_metadata);
let old_atime = FileTime::from_last_access_time(&old_metadata);
std::thread::sleep(std::time::Duration::from_millis(10));
set_file_times_now(&path, true)?;
let new_metadata = fs::metadata(&path)?;
let new_mtime = FileTime::from_last_modification_time(&new_metadata);
let new_atime = FileTime::from_last_access_time(&new_metadata);
// It is possible that something else updated the atime/mtime, so this
// test has false negatives (i.e. might succeed when it's supposed to
// fail). Note that this should not mean that the test is flaky.
assert!(old_mtime < new_mtime, "modification should be updated");
assert!(old_atime < new_atime, "access should be updated");
Ok(())
}

#[test]
#[cfg(unix)]
// TODO: Do Android and MacOS implement this feature of futimens?
#[cfg(not(target_os = "android"))]
#[cfg(not(target_os = "macos"))]
fn set_root_file_handle_times_test() -> io::Result<()> {
// Need a root-owned all-writable file while we execute as non-root.
// Thankfully, /dev/null is such a file, and is universally present!
let path = Path::new("/dev/null");
let file = std::fs::OpenOptions::new()
.write(true)
.truncate(false)
.open(path)?;

let old_metadata = fs::metadata(&path)?;
let old_mtime = FileTime::from_last_modification_time(&old_metadata);
let old_atime = FileTime::from_last_access_time(&old_metadata);
std::thread::sleep(std::time::Duration::from_millis(10));
set_file_handle_times_now(&file)?;
let new_metadata = fs::metadata(&path)?;
let new_mtime = FileTime::from_last_modification_time(&new_metadata);
let new_atime = FileTime::from_last_access_time(&new_metadata);
// It is possible that something else updated the atime/mtime, so this
// test has false negatives (i.e. might succeed when it's supposed to
// fail). Note that this should not mean that the test is flaky.
assert!(old_mtime < new_mtime, "modification should be updated");
assert!(old_atime < new_atime, "access should be updated");
Ok(())
}

#[test]
fn set_dir_times_test() -> io::Result<()> {
let td = Builder::new().prefix("filetime").tempdir()?;
Expand Down
14 changes: 14 additions & 0 deletions src/redox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ pub fn set_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<
set_file_times_redox(fd.raw(), atime, mtime)
}

pub fn set_file_times_now(p: &Path, follow_symlink: bool) -> io::Result<()> {
let time = FileTime::now();
if follow_symlink {
set_file_times(p, time, time)
} else {
set_symlink_file_times(p, time, time)
}
}

pub fn set_file_mtime(p: &Path, mtime: FileTime) -> io::Result<()> {
let fd = open_redox(p, 0)?;
let st = fd.stat()?;
Expand Down Expand Up @@ -71,6 +80,11 @@ pub fn set_file_handle_times(
set_file_times_redox(f.as_raw_fd() as usize, atime1, mtime1)
}

pub fn set_file_handle_times_now(f: &File) -> io::Result<()> {
let time = FileTime::now();
set_file_handle_times(f, Some(time), Some(time))
}

fn open_redox(path: &Path, flags: i32) -> Result<Fd> {
match path.to_str() {
Some(string) => Fd::open(string, flags, 0),
Expand Down
15 changes: 15 additions & 0 deletions src/unix/android.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ pub fn set_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<
set_times(p, Some(atime), Some(mtime), false)
}

pub fn set_file_times_now(p: &Path, follow_symlink: bool) -> io::Result<()> {
let time = FileTime::now();
// TODO: Do the same trick as on Linux?
if follow_symlink {
set_file_times(p, time, time)
} else {
set_symlink_file_times(p, time, time)
}
}

pub fn set_file_mtime(p: &Path, mtime: FileTime) -> io::Result<()> {
set_times(p, None, Some(mtime), false)
}
Expand Down Expand Up @@ -36,6 +46,11 @@ pub fn set_file_handle_times(
}
}

pub fn set_file_handle_times_now(f: &File) -> io::Result<()> {
let time = FileTime::now();
set_file_handle_times(f, Some(time), Some(time)) // TODO: Do the same trick as on Linux?
}

pub fn set_symlink_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<()> {
set_times(p, Some(atime), Some(mtime), true)
}
Expand Down
88 changes: 87 additions & 1 deletion src/unix/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,39 @@ pub fn set_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<
set_times(p, Some(atime), Some(mtime), false)
}

pub fn set_file_times_now(p: &Path, follow_symlink: bool) -> io::Result<()> {
let flags = if !follow_symlink {
libc::AT_SYMLINK_NOFOLLOW
} else {
0
};

// Same as `set_file_handle_times` below.
static INVALID: AtomicBool = AtomicBool::new(false);
if !INVALID.load(SeqCst) {
let p = CString::new(p.as_os_str().as_bytes())?;
let rc = unsafe {
libc::utimensat(
libc::AT_FDCWD,
p.as_ptr(),
ptr::null::<libc::timespec>(),
flags,
)
};
if rc == 0 {
return Ok(());
}
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOSYS) {
INVALID.store(true, SeqCst);
} else {
return Err(err);
}
}

super::utimes::set_file_times_now(p, follow_symlink)
}

pub fn set_file_mtime(p: &Path, mtime: FileTime) -> io::Result<()> {
set_times(p, None, Some(mtime), false)
}
Expand Down Expand Up @@ -80,6 +113,59 @@ pub fn set_file_handle_times(
super::utimes::set_file_handle_times(f, atime, mtime)
}

pub fn set_file_handle_times_now(f: &fs::File) -> io::Result<()> {
eprintln!("Begin linux::set_file_handle_times_now");
// Same as `set_file_handle_times` above.
static INVALID: AtomicBool = AtomicBool::new(false);
if !INVALID.load(SeqCst) {
// We normally use a syscall because the `utimensat` function is documented
// as not accepting a file descriptor in the first argument (even though, on
// Linux, the syscall itself can accept a file descriptor there).
#[cfg(not(target_env = "musl"))]
let rc = unsafe {
eprintln!("linux::set_file_handle_times_now calling non-musl thing");
libc::syscall(
libc::SYS_utimensat,
f.as_raw_fd(),
ptr::null::<libc::c_char>(),
ptr::null::<libc::timespec>(),
0,
)
};
// However, on musl, we call the musl libc function instead. This is because
// on newer musl versions starting with musl 1.2, `timespec` is always a 64-bit
// value even on 32-bit targets. As a result, musl internally converts their
// `timespec` values to the correct ABI before invoking the syscall. Since we
// use `timespec` from the libc crate, it matches musl's definition and not
// the Linux kernel's version (for some platforms) so we must use musl's
// `utimensat` function to properly convert the value. musl's `utimensat`
// function allows file descriptors in the path argument so this is fine.
#[cfg(target_env = "musl")]
let rc = unsafe {
libc::utimensat(
f.as_raw_fd(),
ptr::null::<libc::c_char>(),
ptr::null::<libc::timespec>(),
0,
)
};

eprintln!("linux::set_file_handle_times_now inner done");
if rc == 0 {
return Ok(());
}
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOSYS) {
INVALID.store(true, SeqCst);
} else {
return Err(err);
}
}

eprintln!("linux::set_file_handle_times_now FALLING BACK!!!");
super::utimes::set_file_handle_times_now(f)
}

pub fn set_symlink_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<()> {
set_times(p, Some(atime), Some(mtime), true)
}
Expand All @@ -96,7 +182,7 @@ fn set_times(
0
};

// Same as the `if` statement above.
// Same as `set_file_handle_times` above.
static INVALID: AtomicBool = AtomicBool::new(false);
if !INVALID.load(SeqCst) {
let p = CString::new(p.as_os_str().as_bytes())?;
Expand Down
15 changes: 15 additions & 0 deletions src/unix/macos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ pub fn set_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<
set_times(p, Some(atime), Some(mtime), false)
}

pub fn set_file_times_now(p: &Path, follow_symlink: bool) -> io::Result<()> {
let time = FileTime::now();
// TODO: Do the same trick as on Linux?
if follow_symlink {
set_file_times(p, time, time)
} else {
set_symlink_file_times(p, time, time)
}
}

pub fn set_file_mtime(p: &Path, mtime: FileTime) -> io::Result<()> {
set_times(p, None, Some(mtime), false)
}
Expand Down Expand Up @@ -42,6 +52,11 @@ pub fn set_file_handle_times(
super::utimes::set_file_handle_times(f, atime, mtime)
}

pub fn set_file_handle_times_now(f: &File) -> io::Result<()> {
let time = FileTime::now();
set_file_handle_times(f, Some(time), Some(time)) // TODO: Do the same trick as on Linux?
}

pub fn set_symlink_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<()> {
set_times(p, Some(atime), Some(mtime), true)
}
Expand Down
39 changes: 39 additions & 0 deletions src/unix/utimensat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,41 @@ use std::fs::File;
use std::io;
use std::os::unix::prelude::*;
use std::path::Path;
use std::ptr;

pub fn set_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<()> {
set_times(p, Some(atime), Some(mtime), false)
}

pub fn set_file_times_now(p: &Path, follow_symlink: bool) -> io::Result<()> {
let flags = if !follow_symlink {
if cfg!(target_os = "emscripten") {
return Err(io::Error::new(
io::ErrorKind::Other,
"emscripten does not support utimensat for symlinks",
));
}
libc::AT_SYMLINK_NOFOLLOW
} else {
0
};

let p = CString::new(p.as_os_str().as_bytes())?;
let rc = unsafe {
libc::utimensat(
libc::AT_FDCWD,
p.as_ptr(),
ptr::null::<libc::timespec>(),
flags,
)
};
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

pub fn set_file_mtime(p: &Path, mtime: FileTime) -> io::Result<()> {
set_times(p, None, Some(mtime), false)
}
Expand All @@ -31,6 +61,15 @@ pub fn set_file_handle_times(
}
}

pub fn set_file_handle_times_now(f: &File) -> io::Result<()> {
let rc = unsafe { libc::futimens(f.as_raw_fd(), ptr::null::<libc::timespec>()) };
if rc == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}

pub fn set_symlink_file_times(p: &Path, atime: FileTime, mtime: FileTime) -> io::Result<()> {
set_times(p, Some(atime), Some(mtime), true)
}
Expand Down
Loading