Skip to content

Commit 1dfbd05

Browse files
authored
Make linkat, unlinkat, and renameat weak on macos. (#649)
* 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.
1 parent 7f4c3fe commit 1dfbd05

File tree

10 files changed

+289
-5
lines changed

10 files changed

+289
-5
lines changed

.github/workflows/main.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,8 @@ jobs:
536536
cargo test --verbose --features=all-impls,all-apis,cc --release --workspace -- --nocapture
537537
env:
538538
RUST_BACKTRACE: full
539+
MACOSX_DEPLOYMENT_TARGET: 10.7
540+
MACOSX_SDK_VERSION: 10.7
539541
if: matrix.rust != '1.48'
540542
541543
- run: |
@@ -545,13 +547,17 @@ jobs:
545547
cargo test --verbose --features=fs-err,all-apis,cc --release --workspace -- --nocapture
546548
env:
547549
RUST_BACKTRACE: full
550+
MACOSX_DEPLOYMENT_TARGET: 10.7
551+
MACOSX_SDK_VERSION: 10.7
548552
if: matrix.rust == '1.48'
549553
550554
- run: |
551555
# Check the prebuilt debug libraries too.
552556
cargo check --features=all-impls,all-apis,cc
553557
env:
554558
RUST_BACKTRACE: full
559+
MACOSX_DEPLOYMENT_TARGET: 10.7
560+
MACOSX_SDK_VERSION: 10.7
555561
if: matrix.rust != '1.48'
556562
557563
- run: |
@@ -561,6 +567,8 @@ jobs:
561567
cargo check --features=fs-err,all-apis,cc
562568
env:
563569
RUST_BACKTRACE: full
570+
MACOSX_DEPLOYMENT_TARGET: 10.7
571+
MACOSX_SDK_VERSION: 10.7
564572
if: matrix.rust == '1.48'
565573
566574
test_use_libc:

src/backend/libc/fs/syscalls.rs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,42 @@ pub(crate) fn linkat(
251251
new_path: &CStr,
252252
flags: AtFlags,
253253
) -> io::Result<()> {
254+
// macOS <= 10.9 lacks `linkat`.
255+
#[cfg(target_os = "macos")]
256+
unsafe {
257+
weak! {
258+
fn linkat(
259+
c::c_int,
260+
*const c::c_char,
261+
c::c_int,
262+
*const c::c_char,
263+
c::c_int
264+
) -> c::c_int
265+
}
266+
// If we have `linkat`, use it.
267+
if let Some(libc_linkat) = linkat.get() {
268+
return ret(libc_linkat(
269+
borrowed_fd(old_dirfd),
270+
c_str(old_path),
271+
borrowed_fd(new_dirfd),
272+
c_str(new_path),
273+
flags.bits(),
274+
));
275+
}
276+
// Otherwise, see if we can emulate the `AT_FDCWD` case.
277+
if borrowed_fd(old_dirfd) != c::AT_FDCWD || borrowed_fd(new_dirfd) != c::AT_FDCWD {
278+
return Err(io::Errno::NOSYS);
279+
}
280+
if flags.intersects(!AtFlags::SYMLINK_FOLLOW) {
281+
return Err(io::Errno::INVAL);
282+
}
283+
if !flags.is_empty() {
284+
return Err(io::Errno::OPNOTSUPP);
285+
}
286+
ret(c::link(c_str(old_path), c_str(new_path)))
287+
}
288+
289+
#[cfg(not(target_os = "macos"))]
254290
unsafe {
255291
ret(c::linkat(
256292
borrowed_fd(old_dirfd),
@@ -264,7 +300,38 @@ pub(crate) fn linkat(
264300

265301
#[cfg(not(target_os = "redox"))]
266302
pub(crate) fn unlinkat(dirfd: BorrowedFd<'_>, path: &CStr, flags: AtFlags) -> io::Result<()> {
267-
unsafe { ret(c::unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits())) }
303+
// macOS <= 10.9 lacks `unlinkat`.
304+
#[cfg(target_os = "macos")]
305+
unsafe {
306+
weak! {
307+
fn unlinkat(
308+
c::c_int,
309+
*const c::c_char,
310+
c::c_int
311+
) -> c::c_int
312+
}
313+
// If we have `unlinkat`, use it.
314+
if let Some(libc_unlinkat) = unlinkat.get() {
315+
return ret(libc_unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits()));
316+
}
317+
// Otherwise, see if we can emulate the `AT_FDCWD` case.
318+
if borrowed_fd(dirfd) != c::AT_FDCWD {
319+
return Err(io::Errno::NOSYS);
320+
}
321+
if flags.intersects(!AtFlags::REMOVEDIR) {
322+
return Err(io::Errno::INVAL);
323+
}
324+
if flags.contains(AtFlags::REMOVEDIR) {
325+
ret(c::rmdir(c_str(path)))
326+
} else {
327+
ret(c::unlink(c_str(path)))
328+
}
329+
}
330+
331+
#[cfg(not(target_os = "macos"))]
332+
unsafe {
333+
ret(c::unlinkat(borrowed_fd(dirfd), c_str(path), flags.bits()))
334+
}
268335
}
269336

270337
#[cfg(not(target_os = "redox"))]
@@ -274,6 +341,34 @@ pub(crate) fn renameat(
274341
new_dirfd: BorrowedFd<'_>,
275342
new_path: &CStr,
276343
) -> io::Result<()> {
344+
// macOS <= 10.9 lacks `renameat`.
345+
#[cfg(target_os = "macos")]
346+
unsafe {
347+
weak! {
348+
fn renameat(
349+
c::c_int,
350+
*const c::c_char,
351+
c::c_int,
352+
*const c::c_char
353+
) -> c::c_int
354+
}
355+
// If we have `renameat`, use it.
356+
if let Some(libc_renameat) = renameat.get() {
357+
return ret(libc_renameat(
358+
borrowed_fd(old_dirfd),
359+
c_str(old_path),
360+
borrowed_fd(new_dirfd),
361+
c_str(new_path),
362+
));
363+
}
364+
// Otherwise, see if we can emulate the `AT_FDCWD` case.
365+
if borrowed_fd(old_dirfd) != c::AT_FDCWD || borrowed_fd(new_dirfd) != c::AT_FDCWD {
366+
return Err(io::Errno::NOSYS);
367+
}
368+
ret(c::rename(c_str(old_path), c_str(new_path)))
369+
}
370+
371+
#[cfg(not(target_os = "macos"))]
277372
unsafe {
278373
ret(c::renameat(
279374
borrowed_fd(old_dirfd),
@@ -405,6 +500,40 @@ pub(crate) fn accessat(
405500
access: Access,
406501
flags: AtFlags,
407502
) -> io::Result<()> {
503+
// macOS <= 10.9 lacks `faccessat`.
504+
#[cfg(target_os = "macos")]
505+
unsafe {
506+
weak! {
507+
fn faccessat(
508+
c::c_int,
509+
*const c::c_char,
510+
c::c_int,
511+
c::c_int
512+
) -> c::c_int
513+
}
514+
// If we have `faccessat`, use it.
515+
if let Some(libc_faccessat) = faccessat.get() {
516+
return ret(libc_faccessat(
517+
borrowed_fd(dirfd),
518+
c_str(path),
519+
access.bits(),
520+
flags.bits(),
521+
));
522+
}
523+
// Otherwise, see if we can emulate the `AT_FDCWD` case.
524+
if borrowed_fd(dirfd) != c::AT_FDCWD {
525+
return Err(io::Errno::NOSYS);
526+
}
527+
if flags.intersects(!(AtFlags::EACCESS | AtFlags::SYMLINK_NOFOLLOW)) {
528+
return Err(io::Errno::INVAL);
529+
}
530+
if !flags.is_empty() {
531+
return Err(io::Errno::OPNOTSUPP);
532+
}
533+
ret(c::access(c_str(path), access.bits()))
534+
}
535+
536+
#[cfg(not(target_os = "macos"))]
408537
unsafe {
409538
ret(c::faccessat(
410539
borrowed_fd(dirfd),

tests/fs/chmodat.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#[cfg(not(target_os = "wasi"))]
2+
#[test]
3+
fn test_chmodat() {
4+
use rustix::fs::{chmodat, cwd, openat, statat, AtFlags, Mode, OFlags};
5+
6+
let tmp = tempfile::tempdir().unwrap();
7+
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::RWXU).unwrap();
8+
9+
let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RWXU).unwrap();
10+
11+
let before = statat(&dir, "foo", AtFlags::empty()).unwrap();
12+
assert_ne!(before.st_mode as u64 & libc::S_IRWXU as u64, 0);
13+
14+
chmodat(&dir, "foo", Mode::empty()).unwrap();
15+
16+
let after = statat(&dir, "foo", AtFlags::empty()).unwrap();
17+
assert_eq!(after.st_mode as u64 & libc::S_IRWXU as u64, 0);
18+
19+
chmodat(&dir, "foo", Mode::RWXU).unwrap();
20+
21+
let reverted = statat(&dir, "foo", AtFlags::empty()).unwrap();
22+
assert_ne!(reverted.st_mode as u64 & libc::S_IRWXU as u64, 0);
23+
}
24+
25+
#[cfg(not(target_os = "wasi"))]
26+
#[test]
27+
fn test_chmodat_with() {
28+
use rustix::fs::{chmodat_with, cwd, openat, statat, symlinkat, AtFlags, Mode, OFlags};
29+
30+
let tmp = tempfile::tempdir().unwrap();
31+
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::RWXU).unwrap();
32+
33+
let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RWXU).unwrap();
34+
symlinkat("foo", &dir, "link").unwrap();
35+
36+
match chmodat_with(&dir, "link", Mode::empty(), AtFlags::SYMLINK_NOFOLLOW) {
37+
Ok(()) => (),
38+
Err(rustix::io::Errno::OPNOTSUPP) => return,
39+
Err(e) => Err(e).unwrap(),
40+
}
41+
42+
let before = statat(&dir, "foo", AtFlags::empty()).unwrap();
43+
assert_ne!(before.st_mode as u64 & libc::S_IRWXU as u64, 0);
44+
45+
chmodat_with(&dir, "foo", Mode::empty(), AtFlags::empty()).unwrap();
46+
47+
let after = statat(&dir, "foo", AtFlags::empty()).unwrap();
48+
assert_eq!(after.st_mode as u64 & libc::S_IRWXU as u64, 0);
49+
50+
chmodat_with(&dir, "foo", Mode::RWXU, AtFlags::empty()).unwrap();
51+
52+
let reverted = statat(&dir, "foo", AtFlags::empty()).unwrap();
53+
assert_ne!(reverted.st_mode as u64 & libc::S_IRWXU as u64, 0);
54+
}

tests/fs/linkat.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#[test]
2+
fn test_linkat() {
3+
use rustix::fs::{cwd, linkat, openat, readlinkat, statat, AtFlags, Mode, OFlags};
4+
5+
let tmp = tempfile::tempdir().unwrap();
6+
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();
7+
8+
let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RUSR).unwrap();
9+
10+
linkat(&dir, "foo", &dir, "link", AtFlags::empty()).unwrap();
11+
12+
readlinkat(&dir, "foo", Vec::new()).unwrap_err();
13+
readlinkat(&dir, "link", Vec::new()).unwrap_err();
14+
15+
assert_eq!(
16+
statat(&dir, "foo", AtFlags::empty()).unwrap().st_ino,
17+
statat(&dir, "link", AtFlags::empty()).unwrap().st_ino
18+
);
19+
20+
linkat(&dir, "link", &dir, "another", AtFlags::empty()).unwrap();
21+
22+
assert_eq!(
23+
statat(&dir, "foo", AtFlags::empty()).unwrap().st_ino,
24+
statat(&dir, "another", AtFlags::empty()).unwrap().st_ino
25+
);
26+
}

tests/fs/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#![cfg_attr(io_lifetimes_use_std, feature(io_safety))]
66
#![cfg_attr(core_c_str, feature(core_c_str))]
77

8+
mod chmodat;
89
mod cwd;
910
mod dir;
1011
mod fcntl;
@@ -20,6 +21,7 @@ mod file;
2021
mod flock;
2122
mod futimens;
2223
mod invalid_offset;
24+
mod linkat;
2325
mod long_paths;
2426
#[cfg(not(any(solarish, target_os = "haiku", target_os = "redox", target_os = "wasi")))]
2527
mod makedev;
@@ -31,11 +33,13 @@ mod openat;
3133
mod openat2;
3234
#[cfg(not(target_os = "redox"))]
3335
mod readdir;
36+
mod readlinkat;
3437
mod renameat;
3538
#[cfg(not(any(target_os = "haiku", target_os = "redox", target_os = "wasi")))]
3639
mod statfs;
3740
#[cfg(any(target_os = "android", target_os = "linux"))]
3841
mod statx;
42+
mod symlinkat;
3943
#[cfg(not(any(solarish, target_os = "redox", target_os = "wasi")))]
4044
mod sync;
4145
mod utimensat;

tests/fs/mkdirat.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
#[cfg(not(any(target_os = "redox", target_os = "wasi")))]
22
#[test]
33
fn test_mkdirat() {
4-
use rustix::fs::{cwd, mkdirat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
4+
use rustix::fs::{
5+
accessat, cwd, mkdirat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
6+
};
57

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

911
mkdirat(&dir, "foo", Mode::RWXU).unwrap();
1012
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
1113
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
14+
accessat(&dir, "foo", Access::READ_OK, AtFlags::empty()).unwrap();
15+
accessat(&dir, "foo", Access::WRITE_OK, AtFlags::empty()).unwrap();
16+
accessat(&dir, "foo", Access::EXEC_OK, AtFlags::empty()).unwrap();
17+
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
1218
unlinkat(&dir, "foo", AtFlags::REMOVEDIR).unwrap();
1319
}
1420

1521
#[cfg(any(target_os = "android", target_os = "linux"))]
1622
#[test]
1723
fn test_mkdirat_with_o_path() {
18-
use rustix::fs::{cwd, mkdirat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
24+
use rustix::fs::{
25+
accessat, cwd, mkdirat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
26+
};
1927

2028
let tmp = tempfile::tempdir().unwrap();
2129
let dir = openat(
@@ -29,5 +37,6 @@ fn test_mkdirat_with_o_path() {
2937
mkdirat(&dir, "foo", Mode::RWXU).unwrap();
3038
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
3139
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Directory);
40+
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
3241
unlinkat(&dir, "foo", AtFlags::REMOVEDIR).unwrap();
3342
}

tests/fs/mknodat.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#[cfg(not(any(apple, target_os = "redox", target_os = "wasi")))]
22
#[test]
33
fn test_mknodat() {
4-
use rustix::fs::{cwd, mknodat, openat, statat, unlinkat, AtFlags, FileType, Mode, OFlags};
4+
use rustix::fs::{
5+
accessat, cwd, mknodat, openat, statat, unlinkat, Access, AtFlags, FileType, Mode, OFlags,
6+
};
57

68
let tmp = tempfile::tempdir().unwrap();
79
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();
@@ -18,5 +20,6 @@ fn test_mknodat() {
1820
mknodat(&dir, "foo", FileType::Fifo, Mode::empty(), 0).unwrap();
1921
let stat = statat(&dir, "foo", AtFlags::empty()).unwrap();
2022
assert_eq!(FileType::from_raw_mode(stat.st_mode), FileType::Fifo);
23+
accessat(&dir, "foo", Access::EXISTS, AtFlags::empty()).unwrap();
2124
unlinkat(&dir, "foo", AtFlags::empty()).unwrap();
2225
}

tests/fs/readlinkat.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#[test]
2+
fn test_readlinkat() {
3+
use rustix::fs::{cwd, openat, readlinkat, symlinkat, Mode, OFlags};
4+
5+
let tmp = tempfile::tempdir().unwrap();
6+
let dir = openat(cwd(), tmp.path(), OFlags::RDONLY, Mode::empty()).unwrap();
7+
8+
let _ = openat(&dir, "foo", OFlags::CREATE | OFlags::WRONLY, Mode::RUSR).unwrap();
9+
symlinkat("foo", &dir, "link").unwrap();
10+
11+
readlinkat(&dir, "absent", Vec::new()).unwrap_err();
12+
readlinkat(&dir, "foo", Vec::new()).unwrap_err();
13+
14+
let target = readlinkat(&dir, "link", Vec::new()).unwrap();
15+
assert_eq!(target.to_string_lossy(), "foo");
16+
17+
symlinkat("link", &dir, "another").unwrap();
18+
19+
let target = readlinkat(&dir, "link", Vec::new()).unwrap();
20+
assert_eq!(target.to_string_lossy(), "foo");
21+
let target = readlinkat(&dir, "another", Vec::new()).unwrap();
22+
assert_eq!(target.to_string_lossy(), "link");
23+
}

0 commit comments

Comments
 (0)