Implement utimensat and futimens on macOS 10.12 and earlier.

macOS 10.12 lacks `utimensat` and `futimens`, so emulate them using
`setattrlist` and `fsetattrlist`.

Fixes #157.
This commit is contained in:
Dan Gohman
2022-04-10 19:07:27 -07:00
parent 3bf8c71ac1
commit 930e1cc00e
4 changed files with 403 additions and 2 deletions
+251 -2
View File
@@ -99,12 +99,22 @@ use crate::io::{self, OwnedFd, SeekFrom};
#[cfg(not(target_os = "wasi"))]
use crate::process::{Gid, Uid};
use core::convert::TryInto;
#[cfg(any(target_os = "android", target_os = "linux"))]
#[cfg(any(
target_os = "android",
target_os = "ios",
target_os = "linux",
target_os = "macos"
))]
use core::mem::size_of;
#[cfg(target_os = "linux")]
use core::mem::transmute;
use core::mem::MaybeUninit;
#[cfg(any(target_os = "android", target_os = "linux"))]
#[cfg(any(
target_os = "android",
target_os = "ios",
target_os = "linux",
target_os = "macos"
))]
use core::ptr::null_mut;
#[cfg(any(target_os = "ios", target_os = "macos"))]
use {
@@ -350,6 +360,7 @@ pub(crate) fn utimensat(
times: &Timestamps,
flags: AtFlags,
) -> io::Result<()> {
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
unsafe {
// Assert that `Timestamps` has the expected layout.
let _ = core::mem::transmute::<Timestamps, [c::timespec; 2]>(times.clone());
@@ -361,6 +372,120 @@ pub(crate) fn utimensat(
flags.bits(),
))
}
// `utimensat` was introduced in macOS 10.13.
#[cfg(any(target_os = "ios", target_os = "macos"))]
unsafe {
// ABI details
weak! {
fn utimensat(
c::c_int,
*const c::c_char,
*const c::timespec,
c::c_int
) -> c::c_int
}
extern "C" {
fn setattrlist(
path: *const c::c_char,
attr_list: *const Attrlist,
attr_buf: *const c::c_void,
attr_buf_size: c::size_t,
options: c::c_ulong,
) -> c::c_int;
}
const FSOPT_NOFOLLOW: c::c_ulong = 0x00000001;
// If we have `utimensat`, use it.
if let Some(have_utimensat) = utimensat.get() {
// Assert that `Timestamps` has the expected layout.
let _ = core::mem::transmute::<Timestamps, [c::timespec; 2]>(times.clone());
return ret(have_utimensat(
borrowed_fd(dirfd),
c_str(path),
as_ptr(times).cast(),
flags.bits(),
));
}
// `setattrlistat` was introduced in 10.13 along with `utimensat`, so if
// we don't have `utimensat`, we don't have `setattrlistat` either.
// Emulate it using `fork`, and `fchdir` and [`setattrlist`].
//
// [`setattrlist`]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/setattrlist.2.html
match c::fork() {
-1 => Err(io::Error::IO),
0 => {
if c::fchdir(borrowed_fd(dirfd)) != 0 {
let code = match errno::errno().0 {
libc::EACCES => 2,
libc::ENOTDIR => 3,
_ => 1,
};
c::_exit(code);
}
let mut flags_arg = 0;
if flags.contains(AtFlags::SYMLINK_NOFOLLOW) {
flags_arg |= FSOPT_NOFOLLOW;
}
let (attrbuf_size, times, attrs) = times_to_attrlist(times);
if setattrlist(
c_str(path),
&attrs,
as_ptr(&times).cast(),
attrbuf_size,
flags_arg,
) != 0
{
// Translate expected errno codes into ad-hoc integer
// values suitable for exit statuses.
let code = match errno::errno().0 {
libc::EACCES => 2,
libc::ENOTDIR => 3,
libc::EPERM => 4,
libc::EROFS => 5,
libc::ELOOP => 6,
libc::ENOENT => 7,
libc::ENAMETOOLONG => 8,
libc::EINVAL => 9,
libc::ESRCH => 10,
libc::ENOTSUP => 11,
_ => 1,
};
c::_exit(code);
}
c::_exit(0);
}
child_pid => {
let mut wstatus = 0;
let _ = ret_c_int(c::waitpid(child_pid, &mut wstatus, 0))?;
if c::WIFEXITED(wstatus) {
// Translate our ad-hoc exit statuses back to errno codes.
match c::WEXITSTATUS(wstatus) {
0 => Ok(()),
2 => Err(io::Error::ACCESS),
3 => Err(io::Error::NOTDIR),
4 => Err(io::Error::PERM),
5 => Err(io::Error::ROFS),
6 => Err(io::Error::LOOP),
7 => Err(io::Error::NOENT),
8 => Err(io::Error::NAMETOOLONG),
9 => Err(io::Error::INVAL),
10 => Err(io::Error::SRCH),
11 => Err(io::Error::NOTSUP),
_ => Err(io::Error::IO),
}
} else {
Err(io::Error::IO)
}
}
}
}
}
#[cfg(not(any(
@@ -663,12 +788,50 @@ pub(crate) fn fstatfs(fd: BorrowedFd<'_>) -> io::Result<StatFs> {
}
pub(crate) fn futimens(fd: BorrowedFd<'_>, times: &Timestamps) -> io::Result<()> {
#[cfg(not(any(target_os = "ios", target_os = "macos")))]
unsafe {
// Assert that `Timestamps` has the expected layout.
let _ = core::mem::transmute::<Timestamps, [c::timespec; 2]>(times.clone());
ret(c::futimens(borrowed_fd(fd), as_ptr(times).cast()))
}
// `futimens` was introduced in macOS 10.13.
#[cfg(any(target_os = "ios", target_os = "macos"))]
unsafe {
// ABI details.
weak! {
fn futimens(c::c_int, *const c::timespec) -> c::c_int
}
extern "C" {
fn fsetattrlist(
fd: c::c_int,
attr_list: *const Attrlist,
attr_buf: *const c::c_void,
attr_buf_size: c::size_t,
options: c::c_ulong,
) -> c::c_int;
}
// If we have `futimens`, use it.
if let Some(have_futimens) = futimens.get() {
// Assert that `Timestamps` has the expected layout.
let _ = core::mem::transmute::<Timestamps, [c::timespec; 2]>(times.clone());
return ret(have_futimens(borrowed_fd(fd), as_ptr(times).cast()));
}
// Otherwise use `fsetattrlist`.
let (attrbuf_size, times, attrs) = times_to_attrlist(times);
ret(fsetattrlist(
borrowed_fd(fd),
&attrs,
as_ptr(&times).cast(),
attrbuf_size,
0,
))
}
}
#[cfg(not(any(
@@ -1023,3 +1186,89 @@ pub(crate) fn fcntl_rdadvise(fd: BorrowedFd<'_>, offset: u64, len: u64) -> io::R
pub(crate) fn fcntl_fullfsync(fd: BorrowedFd<'_>) -> io::Result<()> {
unsafe { ret(c::fcntl(borrowed_fd(fd), c::F_FULLFSYNC)) }
}
/// Convert `times` from a `futimens`/`utimensat` argument into `setattrlist`
/// arguments.
#[cfg(any(target_os = "ios", target_os = "macos"))]
fn times_to_attrlist(times: &Timestamps) -> (c::size_t, [c::timespec; 2], Attrlist) {
// ABI details.
const ATTR_CMN_MODTIME: u32 = 0x00000400;
const ATTR_CMN_ACCTIME: u32 = 0x00001000;
const ATTR_BIT_MAP_COUNT: u16 = 5;
let mut times = times.clone();
// If we have any `UTIME_NOW` elements, replace them with the current time.
if times.last_access.tv_nsec == c::UTIME_NOW || times.last_modification.tv_nsec == c::UTIME_NOW
{
let now = {
let mut tv = c::timeval {
tv_sec: 0,
tv_usec: 0,
};
unsafe {
let r = c::gettimeofday(&mut tv, null_mut());
assert_eq!(r, 0);
}
c::timespec {
tv_sec: tv.tv_sec,
tv_nsec: (tv.tv_usec * 1000) as _,
}
};
if times.last_access.tv_nsec == c::UTIME_NOW {
times.last_access = now;
}
if times.last_modification.tv_nsec == c::UTIME_NOW {
times.last_modification = now;
}
}
// Pack the return values following the rules for [`getattrlist`].
//
// [`getattrlist`]: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getattrlist.2.html
let mut times_size = 0;
let mut attrs = Attrlist {
bitmapcount: ATTR_BIT_MAP_COUNT,
reserved: 0,
commonattr: 0,
volattr: 0,
dirattr: 0,
fileattr: 0,
forkattr: 0,
};
let mut return_times = [c::timespec {
tv_sec: 0,
tv_nsec: 0,
}; 2];
let mut times_index = 0;
if times.last_modification.tv_nsec != c::UTIME_OMIT {
attrs.commonattr |= ATTR_CMN_MODTIME;
return_times[times_index] = times.last_modification;
times_index += 1;
times_size += size_of::<c::timespec>();
}
if times.last_access.tv_nsec != c::UTIME_OMIT {
attrs.commonattr |= ATTR_CMN_ACCTIME;
return_times[times_index] = times.last_access;
times_size += size_of::<c::timespec>();
}
(times_size, return_times, attrs)
}
/// Support type for `Attrlist`.
#[cfg(any(target_os = "ios", target_os = "macos"))]
type Attrgroup = u32;
/// Attribute list for use with `setattrlist`.
#[cfg(any(target_os = "ios", target_os = "macos"))]
#[repr(C)]
struct Attrlist {
bitmapcount: u16,
reserved: u16,
commonattr: Attrgroup,
volattr: Attrgroup,
dirattr: Attrgroup,
fileattr: Attrgroup,
forkattr: Attrgroup,
}
+37
View File
@@ -0,0 +1,37 @@
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
#[test]
fn test_futimens() {
use rustix::fs::{cwd, fstat, futimens, openat, Mode, OFlags, Timestamps};
use rustix::time::Timespec;
let tmp = tempfile::tempdir().unwrap();
let dir = openat(&cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();
let foo = openat(
&dir,
"foo",
OFlags::CREATE | OFlags::WRONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let times = Timestamps {
last_access: Timespec {
tv_sec: 44000,
tv_nsec: 45000,
},
last_modification: Timespec {
tv_sec: 46000,
tv_nsec: 47000,
},
};
futimens(&foo, &times).unwrap();
let after = fstat(&foo).unwrap();
assert_eq!(times.last_modification.tv_sec as u64, after.st_mtime as u64);
assert_eq!(
times.last_modification.tv_nsec as u64,
after.st_mtime_nsec as u64
);
}
+2
View File
@@ -8,6 +8,7 @@ mod fcntl;
mod file;
#[cfg(not(target_os = "wasi"))]
mod flock;
mod futimens;
mod invalid_offset;
mod long_paths;
#[cfg(not(any(
@@ -31,3 +32,4 @@ mod renameat;
#[cfg(not(any(target_os = "netbsd", target_os = "redox", target_os = "wasi")))]
// not implemented in libc for netbsd yet
mod statfs;
mod utimensat;
+113
View File
@@ -0,0 +1,113 @@
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
#[test]
fn test_utimensat() {
use rustix::fs::{cwd, openat, statat, utimensat, AtFlags, Mode, OFlags, Timestamps};
use rustix::time::Timespec;
let tmp = tempfile::tempdir().unwrap();
let dir = openat(
&cwd(),
tmp.path(),
OFlags::RDONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let _ = openat(
&dir,
"foo",
OFlags::CREATE | OFlags::WRONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let times = Timestamps {
last_access: Timespec {
tv_sec: 44000,
tv_nsec: 45000,
},
last_modification: Timespec {
tv_sec: 46000,
tv_nsec: 47000,
},
};
utimensat(&dir, "foo", &times, AtFlags::empty()).unwrap();
let after = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(times.last_modification.tv_sec as u64, after.st_mtime as u64);
assert_eq!(
times.last_modification.tv_nsec as u64,
after.st_mtime_nsec as u64
);
}
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
#[test]
fn test_utimensat_noent() {
use rustix::fs::{cwd, openat, utimensat, AtFlags, Mode, OFlags, Timestamps};
use rustix::time::Timespec;
let tmp = tempfile::tempdir().unwrap();
let dir = openat(
&cwd(),
tmp.path(),
OFlags::RDONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let times = Timestamps {
last_access: Timespec {
tv_sec: 44000,
tv_nsec: 45000,
},
last_modification: Timespec {
tv_sec: 46000,
tv_nsec: 47000,
},
};
assert_eq!(
utimensat(&dir, "foo", &times, AtFlags::empty()).unwrap_err(),
rustix::io::Error::NOENT
);
}
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
#[test]
fn test_utimensat_notdir() {
use rustix::fs::{cwd, openat, utimensat, AtFlags, Mode, OFlags, Timestamps};
use rustix::time::Timespec;
let tmp = tempfile::tempdir().unwrap();
let dir = openat(
&cwd(),
tmp.path(),
OFlags::RDONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let foo = openat(
&dir,
"foo",
OFlags::CREATE | OFlags::WRONLY | OFlags::CLOEXEC,
Mode::empty(),
)
.unwrap();
let times = Timestamps {
last_access: Timespec {
tv_sec: 44000,
tv_nsec: 45000,
},
last_modification: Timespec {
tv_sec: 46000,
tv_nsec: 47000,
},
};
assert_eq!(
utimensat(&foo, "bar", &times, AtFlags::empty()).unwrap_err(),
rustix::io::Error::NOTDIR
);
}