Skip to content

Commit

Permalink
Make linkat, unlinkat, and renameat weak on macos. (#649)
Browse files Browse the repository at this point in the history
* Make `linkat`, `unlinkat`, and `renameat` weak on macos.

macOS <= 10.9 lacks these functions, so use weak symbols for them.

Fixes #648.

* Handle the `AT_FDCWD` cases with fallbacks.

* Test with macOS 10.7.

Set MACOSX_DEPLOYMENT_TARGET and MACOSX_SDK_VERSION to 10.7, currently
the oldest version supported by Rust itself.

* Add support for `faccessat` too.

* Add some tests for `faccessat`.

* Add more tests.
  • Loading branch information
sunfishcode committed May 3, 2023
1 parent 7f4c3fe commit 1dfbd05
Show file tree
Hide file tree
Showing 10 changed files with 289 additions and 5 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,8 @@ jobs:
cargo test --verbose --features=all-impls,all-apis,cc --release --workspace -- --nocapture
env:
RUST_BACKTRACE: full
MACOSX_DEPLOYMENT_TARGET: 10.7
MACOSX_SDK_VERSION: 10.7
if: matrix.rust != '1.48'
- run: |
Expand All @@ -545,13 +547,17 @@ jobs:
cargo test --verbose --features=fs-err,all-apis,cc --release --workspace -- --nocapture
env:
RUST_BACKTRACE: full
MACOSX_DEPLOYMENT_TARGET: 10.7
MACOSX_SDK_VERSION: 10.7
if: matrix.rust == '1.48'
- run: |
# Check the prebuilt debug libraries too.
cargo check --features=all-impls,all-apis,cc
env:
RUST_BACKTRACE: full
MACOSX_DEPLOYMENT_TARGET: 10.7
MACOSX_SDK_VERSION: 10.7
if: matrix.rust != '1.48'
- run: |
Expand All @@ -561,6 +567,8 @@ jobs:
cargo check --features=fs-err,all-apis,cc
env:
RUST_BACKTRACE: full
MACOSX_DEPLOYMENT_TARGET: 10.7
MACOSX_SDK_VERSION: 10.7
if: matrix.rust == '1.48'
test_use_libc:
Expand Down
131 changes: 130 additions & 1 deletion src/backend/libc/fs/syscalls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,42 @@ pub(crate) fn linkat(
new_path: &CStr,
flags: AtFlags,
) -> io::Result<()> {
// macOS <= 10.9 lacks `linkat`.
#[cfg(target_os = "macos")]
unsafe {
weak! {
fn linkat(
c::c_int,
*const c::c_char,
c::c_int,
*const c::c_char,
c::c_int
) -> c::c_int
}
// If we have `linkat`, use it.
if let Some(libc_linkat) = linkat.get() {
return ret(libc_linkat(
borrowed_fd(old_dirfd),
c_str(old_path),
borrowed_fd(new_dirfd),
c_str(new_path),
flags.bits(),
));
}
// Otherwise, see if we can emulate the `AT_FDCWD` case.
if borrowed_fd(old_dirfd) != c::AT_FDCWD || borrowed_fd(new_dirfd) != c::AT_FDCWD {
return Err(io::Errno::NOSYS);
}
if flags.intersects(!AtFlags::SYMLINK_FOLLOW) {
return Err(io::Errno::INVAL);
}
if !flags.is_empty() {
return Err(io::Errno::OPNOTSUPP);
}
ret(c::link(c_str(old_path), c_str(new_path)))
}

#[cfg(not(target_os = "macos"))]
unsafe {
ret(c::linkat(
borrowed_fd(old_dirfd),
Expand All @@ -264,7 +300,38 @@ pub(crate) fn linkat(

#[cfg(not(target_os = "redox"))]
pub(crate) fn unlinkat(dirfd: BorrowedFd<'_>, path: &CStr, flags: AtFlags) -> io::Result<()> {
unsafe { ret(c::unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits())) }
// macOS <= 10.9 lacks `unlinkat`.
#[cfg(target_os = "macos")]
unsafe {
weak! {
fn unlinkat(
c::c_int,
*const c::c_char,
c::c_int
) -> c::c_int
}
// If we have `unlinkat`, use it.
if let Some(libc_unlinkat) = unlinkat.get() {
return ret(libc_unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits()));
}
// Otherwise, see if we can emulate the `AT_FDCWD` case.
if borrowed_fd(dirfd) != c::AT_FDCWD {
return Err(io::Errno::NOSYS);
}
if flags.intersects(!AtFlags::REMOVEDIR) {
return Err(io::Errno::INVAL);
}
if flags.contains(AtFlags::REMOVEDIR) {
ret(c::rmdir(c_str(path)))
} else {
ret(c::unlink(c_str(path)))
}
}

#[cfg(not(target_os = "macos"))]
unsafe {
ret(c::unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits()))
}
}

#[cfg(not(target_os = "redox"))]
Expand All @@ -274,6 +341,34 @@ pub(crate) fn renameat(
new_dirfd: BorrowedFd<'_>,
new_path: &CStr,
) -> io::Result<()> {
// macOS <= 10.9 lacks `renameat`.
#[cfg(target_os = "macos")]
unsafe {
weak! {
fn renameat(
c::c_int,
*const c::c_char,
c::c_int,
*const c::c_char
) -> c::c_int
}
// If we have `renameat`, use it.
if let Some(libc_renameat) = renameat.get() {
return ret(libc_renameat(
borrowed_fd(old_dirfd),
c_str(old_path),
borrowed_fd(new_dirfd),
c_str(new_path),
));
}
// Otherwise, see if we can emulate the `AT_FDCWD` case.
if borrowed_fd(old_dirfd) != c::AT_FDCWD || borrowed_fd(new_dirfd) != c::AT_FDCWD {
return Err(io::Errno::NOSYS);
}
ret(c::rename(c_str(old_path), c_str(new_path)))
}

#[cfg(not(target_os = "macos"))]
unsafe {
ret(c::renameat(
borrowed_fd(old_dirfd),
Expand Down Expand Up @@ -405,6 +500,40 @@ pub(crate) fn accessat(
access: Access,
flags: AtFlags,
) -> io::Result<()> {
// macOS <= 10.9 lacks `faccessat`.
#[cfg(target_os = "macos")]
unsafe {
weak! {
fn faccessat(
c::c_int,
*const c::c_char,
c::c_int,
c::c_int
) -> c::c_int
}
// If we have `faccessat`, use it.
if let Some(libc_faccessat) = faccessat.get() {
return ret(libc_faccessat(
borrowed_fd(dirfd),
c_str(path),
access.bits(),
flags.bits(),
));
}
// Otherwise, see if we can emulate the `AT_FDCWD` case.
if borrowed_fd(dirfd) != c::AT_FDCWD {
return Err(io::Errno::NOSYS);
}
if flags.intersects(!(AtFlags::EACCESS | AtFlags::SYMLINK_NOFOLLOW)) {
return Err(io::Errno::INVAL);
}
if !flags.is_empty() {
return Err(io::Errno::OPNOTSUPP);
}
ret(c::access(c_str(path), access.bits()))
}

#[cfg(not(target_os = "macos"))]
unsafe {
ret(c::faccessat(
borrowed_fd(dirfd),
Expand Down
54 changes: 54 additions & 0 deletions tests/fs/chmodat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#[cfg(not(target_os = "wasi"))]
#[test]
fn test_chmodat() {
use rustix::fs::{chmodat, cwd, openat, statat, AtFlags, Mode, OFlags};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::RWXU).unwrap();

let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RWXU).unwrap();

let before = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_ne!(before.st_mode as u64 & libc::S_IRWXU as u64, 0);

chmodat(&dir, "foo", Mode::empty()).unwrap();

let after = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(after.st_mode as u64 & libc::S_IRWXU as u64, 0);

chmodat(&dir, "foo", Mode::RWXU).unwrap();

let reverted = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_ne!(reverted.st_mode as u64 & libc::S_IRWXU as u64, 0);
}

#[cfg(not(target_os = "wasi"))]
#[test]
fn test_chmodat_with() {
use rustix::fs::{chmodat_with, cwd, openat, statat, symlinkat, AtFlags, Mode, OFlags};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::RWXU).unwrap();

let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RWXU).unwrap();
symlinkat("foo", &dir, "link").unwrap();

match chmodat_with(&dir, "link", Mode::empty(), AtFlags::SYMLINK_NOFOLLOW) {
Ok(()) => (),
Err(rustix::io::Errno::OPNOTSUPP) => return,
Err(e) => Err(e).unwrap(),
}

let before = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_ne!(before.st_mode as u64 & libc::S_IRWXU as u64, 0);

chmodat_with(&dir, "foo", Mode::empty(), AtFlags::empty()).unwrap();

let after = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(after.st_mode as u64 & libc::S_IRWXU as u64, 0);

chmodat_with(&dir, "foo", Mode::RWXU, AtFlags::empty()).unwrap();

let reverted = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_ne!(reverted.st_mode as u64 & libc::S_IRWXU as u64, 0);
}
26 changes: 26 additions & 0 deletions tests/fs/linkat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#[test]
fn test_linkat() {
use rustix::fs::{cwd, linkat, openat, readlinkat, statat, AtFlags, Mode, OFlags};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();

let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RUSR).unwrap();

linkat(&dir, "foo", &dir, "link", AtFlags::empty()).unwrap();

readlinkat(&dir, "foo", Vec::new()).unwrap_err();
readlinkat(&dir, "link", Vec::new()).unwrap_err();

assert_eq!(
statat(&dir, "foo", AtFlags::empty()).unwrap().st_ino,
statat(&dir, "link", AtFlags::empty()).unwrap().st_ino
);

linkat(&dir, "link", &dir, "another", AtFlags::empty()).unwrap();

assert_eq!(
statat(&dir, "foo", AtFlags::empty()).unwrap().st_ino,
statat(&dir, "another", AtFlags::empty()).unwrap().st_ino
);
}
4 changes: 4 additions & 0 deletions tests/fs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#![cfg_attr(io_lifetimes_use_std, feature(io_safety))]
#![cfg_attr(core_c_str, feature(core_c_str))]

mod chmodat;
mod cwd;
mod dir;
mod fcntl;
Expand All @@ -20,6 +21,7 @@ mod file;
mod flock;
mod futimens;
mod invalid_offset;
mod linkat;
mod long_paths;
#[cfg(not(any(solarish, target_os = "haiku", target_os = "redox", target_os = "wasi")))]
mod makedev;
Expand All @@ -31,11 +33,13 @@ mod openat;
mod openat2;
#[cfg(not(target_os = "redox"))]
mod readdir;
mod readlinkat;
mod renameat;
#[cfg(not(any(target_os = "haiku", target_os = "redox", target_os = "wasi")))]
mod statfs;
#[cfg(any(target_os = "android", target_os = "linux"))]
mod statx;
mod symlinkat;
#[cfg(not(any(solarish, target_os = "redox", target_os = "wasi")))]
mod sync;
mod utimensat;
Expand Down
13 changes: 11 additions & 2 deletions tests/fs/mkdirat.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
#[test]
fn test_mkdirat() {
use rustix::fs::{cwd, mkdirat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
use rustix::fs::{
accessat, cwd, mkdirat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();

mkdirat(&dir, "foo", Mode::RWXU).unwrap();
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
accessat(&dir, "foo", Access::READ_OK, AtFlags::empty()).unwrap();
accessat(&dir, "foo", Access::WRITE_OK, AtFlags::empty()).unwrap();
accessat(&dir, "foo", Access::EXEC_OK, AtFlags::empty()).unwrap();
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
unlinkat(&dir, "foo", AtFlags::REMOVEDIR).unwrap();
}

#[cfg(any(target_os = "android", target_os = "linux"))]
#[test]
fn test_mkdirat_with_o_path() {
use rustix::fs::{cwd, mkdirat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
use rustix::fs::{
accessat, cwd, mkdirat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(
Expand All @@ -29,5 +37,6 @@ fn test_mkdirat_with_o_path() {
mkdirat(&dir, "foo", Mode::RWXU).unwrap();
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
unlinkat(&dir, "foo", AtFlags::REMOVEDIR).unwrap();
}
5 changes: 4 additions & 1 deletion tests/fs/mknodat.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#[cfg(not(any(apple, target_os = "redox", target_os = "wasi")))]
#[test]
fn test_mknodat() {
use rustix::fs::{cwd, mknodat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
use rustix::fs::{
accessat, cwd, mknodat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();
Expand All @@ -18,5 +20,6 @@ fn test_mknodat() {
mknodat(&dir, "foo", FileType::Fifo, Mode::empty(), 0).unwrap();
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Fifo);
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
unlinkat(&dir, "foo", AtFlags::empty()).unwrap();
}
23 changes: 23 additions & 0 deletions tests/fs/readlinkat.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#[test]
fn test_readlinkat() {
use rustix::fs::{cwd, openat, readlinkat, symlinkat, Mode, OFlags};

let tmp = tempfile::tempdir().unwrap();
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();

let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RUSR).unwrap();
symlinkat("foo", &dir, "link").unwrap();

readlinkat(&dir, "absent", Vec::new()).unwrap_err();
readlinkat(&dir, "foo", Vec::new()).unwrap_err();

let target = readlinkat(&dir, "link", Vec::new()).unwrap();
assert_eq!(target.to_string_lossy(), "foo");

symlinkat("link", &dir, "another").unwrap();

let target = readlinkat(&dir, "link", Vec::new()).unwrap();
assert_eq!(target.to_string_lossy(), "foo");
let target = readlinkat(&dir, "another", Vec::new()).unwrap();
assert_eq!(target.to_string_lossy(), "link");
}

0 comments on commit 1dfbd05

Please sign in to comment.