about summary refs log tree commit diff
diff options
context:
space:
mode:
authorDan Gohman <dev@sunfishcode.online>2020-10-16 09:09:20 -0700
committerDan Gohman <dev@sunfishcode.online>2020-10-16 12:05:49 -0700
commit91a9f83dd1d73cfd451f81306361df3fafad84a5 (patch)
treeb3eff22b5672b6fbd8099552216dbc58c65ee78f
parenta78a62fc996ba16f7a111c99520b23f77029f4eb (diff)
downloadrust-91a9f83dd1d73cfd451f81306361df3fafad84a5.tar.gz
rust-91a9f83dd1d73cfd451f81306361df3fafad84a5.zip
Define `fs::hard_link` to not follow symlinks.
POSIX leaves it implementation-defined whether `link` follows symlinks.
In practice, for example, on Linux it does not and on FreeBSD it does.
So, switch to `linkat`, so that we can pick a behavior rather than
depending on OS defaults.

Pick the option to not follow symlinks. This is somewhat arbitrary, but
seems the less surprising choice because hard linking is a very
low-level feature which requires the source and destination to be on
the same mounted filesystem, and following a symbolic link could end
up in a different mounted filesystem.
-rw-r--r--library/std/src/fs.rs7
-rw-r--r--library/std/src/fs/tests.rs51
-rw-r--r--library/std/src/sys/unix/fs.rs5
3 files changed, 60 insertions, 3 deletions
diff --git a/library/std/src/fs.rs b/library/std/src/fs.rs
index 161bfe3795c..c611bf4d74a 100644
--- a/library/std/src/fs.rs
+++ b/library/std/src/fs.rs
@@ -1701,10 +1701,13 @@ pub fn copy<P: AsRef<Path>, Q: AsRef<Path>>(from: P, to: Q) -> io::Result<u64> {
 /// The `dst` path will be a link pointing to the `src` path. Note that systems
 /// often require these two paths to both be located on the same filesystem.
 ///
+/// If `src` names a symbolic link, it is not followed. The created hard link
+/// points to the symbolic link itself.
+///
 /// # Platform-specific behavior
 ///
-/// This function currently corresponds to the `link` function on Unix
-/// and the `CreateHardLink` function on Windows.
+/// This function currently corresponds to the `linkat` function with no flags
+/// on Unix and the `CreateHardLink` function on Windows.
 /// Note that, this [may change in the future][changes].
 ///
 /// [changes]: io#platform-specific-behavior
diff --git a/library/std/src/fs/tests.rs b/library/std/src/fs/tests.rs
index 65a29076fef..8a723d3b4ae 100644
--- a/library/std/src/fs/tests.rs
+++ b/library/std/src/fs/tests.rs
@@ -1337,3 +1337,54 @@ fn metadata_access_times() {
         }
     }
 }
+
+/// Test creating hard links to symlinks.
+#[test]
+fn symlink_hard_link() {
+    let tmpdir = tmpdir();
+
+    // Create "file", a file.
+    check!(fs::File::create(tmpdir.join("file")));
+
+    // Create "symlink", a symlink to "file".
+    check!(symlink_file("file", tmpdir.join("symlink")));
+
+    // Create "hard_link", a hard link to "symlink".
+    check!(fs::hard_link(tmpdir.join("symlink"), tmpdir.join("hard_link")));
+
+    // "hard_link" should appear as a symlink.
+    assert!(check!(fs::symlink_metadata(tmpdir.join("hard_link"))).file_type().is_symlink());
+
+    // We sould be able to open "file" via any of the above names.
+    let _ = check!(fs::File::open(tmpdir.join("file")));
+    assert!(fs::File::open(tmpdir.join("file.renamed")).is_err());
+    let _ = check!(fs::File::open(tmpdir.join("symlink")));
+    let _ = check!(fs::File::open(tmpdir.join("hard_link")));
+
+    // Rename "file" to "file.renamed".
+    check!(fs::rename(tmpdir.join("file"), tmpdir.join("file.renamed")));
+
+    // Now, the symlink and the hard link should be dangling.
+    assert!(fs::File::open(tmpdir.join("file")).is_err());
+    let _ = check!(fs::File::open(tmpdir.join("file.renamed")));
+    assert!(fs::File::open(tmpdir.join("symlink")).is_err());
+    assert!(fs::File::open(tmpdir.join("hard_link")).is_err());
+
+    // The symlink and the hard link should both still point to "file".
+    assert!(fs::read_link(tmpdir.join("file")).is_err());
+    assert!(fs::read_link(tmpdir.join("file.renamed")).is_err());
+    assert_eq!(check!(fs::read_link(tmpdir.join("symlink"))), Path::new("file"));
+    assert_eq!(check!(fs::read_link(tmpdir.join("hard_link"))), Path::new("file"));
+
+    // Remove "file.renamed".
+    check!(fs::remove_file(tmpdir.join("file.renamed")));
+
+    // Now, we can't open the file by any name.
+    assert!(fs::File::open(tmpdir.join("file")).is_err());
+    assert!(fs::File::open(tmpdir.join("file.renamed")).is_err());
+    assert!(fs::File::open(tmpdir.join("symlink")).is_err());
+    assert!(fs::File::open(tmpdir.join("hard_link")).is_err());
+
+    // "hard_link" should still appear as a symlink.
+    assert!(check!(fs::symlink_metadata(tmpdir.join("hard_link"))).file_type().is_symlink());
+}
diff --git a/library/std/src/sys/unix/fs.rs b/library/std/src/sys/unix/fs.rs
index 819e8ef1841..88693e4786c 100644
--- a/library/std/src/sys/unix/fs.rs
+++ b/library/std/src/sys/unix/fs.rs
@@ -1067,7 +1067,10 @@ pub fn symlink(src: &Path, dst: &Path) -> io::Result<()> {
 pub fn link(src: &Path, dst: &Path) -> io::Result<()> {
     let src = cstr(src)?;
     let dst = cstr(dst)?;
-    cvt(unsafe { libc::link(src.as_ptr(), dst.as_ptr()) })?;
+    // Use `linkat` with `AT_FDCWD` instead of `link` as `link` leaves it
+    // implmentation-defined whether it follows symlinks. Pass 0 as the
+    // `linkat` flags argument so that we don't follow symlinks.
+    cvt(unsafe { libc::linkat(libc::AT_FDCWD, src.as_ptr(), libc::AT_FDCWD, dst.as_ptr(), 0) })?;
     Ok(())
 }