From b8ffb635439678024906906ebfb9b552645cf855 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 | 5 +- src/fcntl.rs | 1 + src/unistd.rs | 52 ++++++++++++++++++ test/test_unistd.rs | 131 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 188 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa18a42960..501bf0dcd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). - Added `mkfifoat` ([#1133](https://github.com/nix-rust/nix/pull/1133)) +- Added `linkat` + ([#1101](https://github.com/nix-rust/nix/pull/1101)) + ### Changed - `Signal::from_c_int` has been replaced by `Signal::try_from` ([#1113](https://github.com/nix-rust/nix/pull/1113)) @@ -34,7 +37,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). // old code `readlink(&path, &mut buf)` can be replaced with the following let _: OsString = readlink(&path); - + // old code `readlinkat(dirfd, &path, &mut buf)` can be replaced with the following let _: OsString = readlinkat(dirfd, &path); ``` 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 886084e8c5..fb6de3b424 100644 --- a/src/unistd.rs +++ b/src/unistd.rs @@ -1169,6 +1169,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 `oldpath` +/// and/or `newpath` is then interpreted relative to the current working directory of the calling +/// process. If either `oldpath` or `newpath` 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 94975049da..e0b3ae7e51 100644 --- a/test/test_unistd.rs +++ b/test/test_unistd.rs @@ -660,6 +660,137 @@ 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()); +} + +#[test] +fn test_linkat_olddirfd_none() { + let tempdir_oldfile = tempfile::tempdir().unwrap(); + let oldfilename = "foo.txt"; + let oldfilepath = tempdir_oldfile.path().join(oldfilename); + + let tempdir_newfile = tempfile::tempdir().unwrap(); + let newfilename = "bar.txt"; + let newfilepath = tempdir_newfile.path().join(newfilename); + + // Create file + File::create(&oldfilepath).unwrap(); + + // Get file descriptor for base directory of new file + let dirfd = fcntl::open(tempdir_newfile.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap(); + + // Attempt hard link file using curent working directory as relative path for old file path + chdir(tempdir_oldfile.path()).unwrap(); + linkat(None, oldfilename, Some(dirfd), newfilename, LinkatFlags::SymlinkFollow).unwrap(); + assert!(newfilepath.exists()); +} + +#[test] +fn test_linkat_newdirfd_none() { + let tempdir_oldfile = tempfile::tempdir().unwrap(); + let oldfilename = "foo.txt"; + let oldfilepath = tempdir_oldfile.path().join(oldfilename); + + let tempdir_newfile = tempfile::tempdir().unwrap(); + let newfilename = "bar.txt"; + let newfilepath = tempdir_newfile.path().join(newfilename); + + // Create file + File::create(&oldfilepath).unwrap(); + + // Get file descriptor for base directory of old file + let dirfd = fcntl::open(tempdir_oldfile.path(), fcntl::OFlag::empty(), stat::Mode::empty()).unwrap(); + + // Attempt hard link file using current working directory as relative path for new file path + chdir(tempdir_newfile.path()).unwrap(); + linkat(Some(dirfd), oldfilename, None, newfilename, LinkatFlags::SymlinkFollow).unwrap(); + assert!(newfilepath.exists()); +} + +#[test] +#[cfg(not(any(target_os = "ios", target_os = "macos")))] +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() {