From a27248b0c7a15011c8bb615418617d3455c0349a Mon Sep 17 00:00:00 2001 From: Johnnie Birch <45402135+jlb6740@users.noreply.github.com> Date: Wed, 24 Jul 2019 18:50:04 -0700 Subject: [PATCH] Implment linkat This adds the linkat function which is part of POSIX: http://pubs.opengroup.org/onlinepubs/9699919799/functions/linkat.html and widely implmented on Unix-Family platforms. --- CHANGELOG.md | 2 ++ src/fcntl.rs | 1 + src/unistd.rs | 52 +++++++++++++++++++++++++++ test/test_unistd.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a0691d134..0c2afaf497 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ This project adheres to [Semantic Versioning](http://semver.org/). ([#1058](https://github.com/nix-rust/nix/pull/1058)) - Add `renameat`. ([#1097](https://github.com/nix-rust/nix/pull/1097)) +- Add `linkat` + ([#1101](https://github.com/nix-rust/nix/pull/1101)) ### Changed - Support for `ifaddrs` now present when building for Android. diff --git a/src/fcntl.rs b/src/fcntl.rs index 7d745b068b..1d66eb75d4 100644 --- a/src/fcntl.rs +++ b/src/fcntl.rs @@ -24,6 +24,7 @@ pub use self::posix_fadvise::*; libc_bitflags!{ pub struct AtFlags: c_int { AT_REMOVEDIR; + AT_SYMLINK_FOLLOW; AT_SYMLINK_NOFOLLOW; #[cfg(any(target_os = "android", target_os = "linux"))] AT_NO_AUTOMOUNT; diff --git a/src/unistd.rs b/src/unistd.rs index a269a5c252..6bdc432f2e 100644 --- a/src/unistd.rs +++ b/src/unistd.rs @@ -1149,6 +1149,58 @@ pub fn isatty(fd: RawFd) -> Result { } } +/// Flags for `linkat` function. +#[derive(Clone, Copy, Debug)] +pub enum LinkatFlags { + SymlinkFollow, + NoSymlinkFollow, +} + +/// Link one file to another file +/// +/// Creates a new link (directory entry) at `newpath` for the existing file at `oldpath`. In the +/// case of a relative `oldpath`, the path is interpreted relative to the directory associated +/// with file descriptor `olddirfd` instead of the current working directory and similiarly for +/// `newpath` and file descriptor `newdirfd`. In case `flag` is LinkatFlags::SymlinkFollow and +/// `oldpath` names a symoblic link, a new link for the target of the symbolic link is created. +/// If either olddirfd or newdirfd is None, AT_FDCWD is used respectively where pathname is then +/// interpreted relative to the current working directory of the calling process. If pathname is +/// absolute, then dirfd is ignored. +/// +/// # References +/// See also [linkat(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/linkat.html) +pub fn linkat( + olddirfd: Option, + oldpath: &P, + newdirfd: Option, + newpath: &P, + flag: LinkatFlags, +) -> Result<()> { + + let atflag = + match flag { + LinkatFlags::SymlinkFollow => AtFlags::AT_SYMLINK_FOLLOW, + LinkatFlags::NoSymlinkFollow => AtFlags::empty(), + }; + + let res = + oldpath.with_nix_path(|oldcstr| { + newpath.with_nix_path(|newcstr| { + unsafe { + libc::linkat( + at_rawfd(olddirfd), + oldcstr.as_ptr(), + at_rawfd(newdirfd), + newcstr.as_ptr(), + atflag.bits() as libc::c_int + ) + } + }) + })??; + Errno::result(res).map(drop) +} + + /// Remove a directory entry /// /// See also [unlink(2)](http://pubs.opengroup.org/onlinepubs/9699919799/functions/unlink.html) diff --git a/test/test_unistd.rs b/test/test_unistd.rs index 9e45ceeb89..cf67e97bd2 100644 --- a/test/test_unistd.rs +++ b/test/test_unistd.rs @@ -609,6 +609,94 @@ fn test_symlinkat() { } +#[test] +fn test_linkat_file() { + let tempdir = tempfile::tempdir().unwrap(); + let oldfilename = "foo.txt"; + let oldfilepath = tempdir.path().join(oldfilename); + + let newfilename = "bar.txt"; + let newfilepath = tempdir.path().join(newfilename); + + // Create file + File::create(&oldfilepath).unwrap(); + + // Get file descriptor for base directory + let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap(); + + // Attempt hard link file at relative path + linkat(Some(dirfd), oldfilename, Some(dirfd), newfilename, LinkatFlags::SymlinkFollow).unwrap(); + assert!(newfilepath.exists()); +} + +#[cfg(target_os = "freebsd")] +#[test] +fn test_linkat_no_follow_symlink() { + let tempdir = tempfile::tempdir().unwrap(); + let oldfilename = "foo.txt"; + let oldfilepath = tempdir.path().join(oldfilename); + + let symoldfilename = "symfoo.txt"; + let symoldfilepath = tempdir.path().join(symoldfilename); + + let newfilename = "nofollowsymbar.txt"; + let newfilepath = tempdir.path().join(newfilename); + + // Create file + File::create(&oldfilepath).unwrap(); + + // Create symlink to file + symlinkat(&oldfilepath, None, &symoldfilepath).unwrap(); + + // Get file descriptor for base directory + let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap(); + + // Attempt link symlink of file at relative path + linkat(Some(dirfd), symoldfilename, Some(dirfd), newfilename, LinkatFlags::NoSymlinkFollow).unwrap(); + + // Assert newfile is actually a symlink to oldfile. + assert_eq!( + readlink(&newfilepath) + .unwrap() + .to_str() + .unwrap(), + oldfilepath.to_str().unwrap() + ); +} + +#[test] +fn test_linkat_follow_symlink() { + let tempdir = tempfile::tempdir().unwrap(); + let oldfilename = "foo.txt"; + let oldfilepath = tempdir.path().join(oldfilename); + + let symoldfilename = "symfoo.txt"; + let symoldfilepath = tempdir.path().join(symoldfilename); + + let newfilename = "nofollowsymbar.txt"; + let newfilepath = tempdir.path().join(newfilename); + + // Create file + File::create(&oldfilepath).unwrap(); + + // Create symlink to file + symlinkat(&oldfilepath, None, &symoldfilepath).unwrap(); + + // Get file descriptor for base directory + let dirfd = fcntl::open(tempdir.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap(); + + // Attempt link target of symlink of file at relative path + linkat(Some(dirfd), symoldfilename, Some(dirfd), newfilename, LinkatFlags::SymlinkFollow).unwrap(); + + let newfilestat = stat::stat(&newfilepath).unwrap(); + + // Check the file type of the new link + assert!((stat::SFlag::from_bits_truncate(newfilestat.st_mode) & SFlag::S_IFMT) == SFlag::S_IFREG); + + // Check the number of hard links to the original file + assert_eq!(newfilestat.st_nlink, 2); +} + #[test] fn test_unlinkat_dir_noremovedir() { let tempdir = tempfile::tempdir().unwrap();