mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-13 23:17:44 +00:00
Follow symbolic links without canonicalize
The target is found by reading links one hop at a time, resolving relative links against the link's folder. A dangling link writes the file it names and stays a link, and a loop of links is an error.
This commit is contained in:
1 parent
fb26a9f90f
commit
658ca37c8e
2 files changed
+50
-7
No files matched your search
@@ -154,7 +154,7 @@ pub fn atomic_write_custom(fs: &dyn AtomicFs, retry: RenameRetry, path: &Path, b
|
||||
/// The replace path behind every `atomic_write*` function: stage the new content beside the
|
||||
/// target, then rename it over the target.
|
||||
fn replace(fs: &dyn AtomicFs, retry: RenameRetry, path: &Path, fill: impl FnOnce(&mut File) -> io::Result<()>) -> io::Result<()> {
|
||||
let target = resolve_symlink(path);
|
||||
let target = resolve_links(path)?;
|
||||
let (dir, name) = split(&target, path)?;
|
||||
let existed = target.exists();
|
||||
|
||||
@@ -236,7 +236,7 @@ pub fn write_new_custom(fs: &dyn AtomicFs, path: &Path, bytes: &[u8]) -> io::Res
|
||||
/// then give it the target's name with a hard link, which fails rather than replace a file that
|
||||
/// appeared in the meantime. Volumes without hard links get a copy into a newly created file.
|
||||
fn create(fs: &dyn AtomicFs, path: &Path, fill: impl FnOnce(&mut File) -> io::Result<()>) -> io::Result<()> {
|
||||
let target = resolve_symlink(path);
|
||||
let target = resolve_links(path)?;
|
||||
let (dir, name) = split(&target, path)?;
|
||||
if target.exists() {
|
||||
return Err(write_error(io::Error::from(io::ErrorKind::AlreadyExists), path, &dir, true));
|
||||
@@ -296,11 +296,27 @@ fn sleep_ms(ms: u64) {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn sleep_ms(_ms: u64) {}
|
||||
|
||||
fn resolve_symlink(path: &Path) -> PathBuf {
|
||||
match std::fs::symlink_metadata(path) {
|
||||
Ok(m) if m.file_type().is_symlink() => std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()),
|
||||
_ => path.to_path_buf(),
|
||||
/// Symbolic links followed before giving up (a loop, or an absurd chain).
|
||||
const MAX_LINKS: usize = 40;
|
||||
|
||||
/// The file a chain of symbolic links ends at (`path` itself when it is not a link), so the link
|
||||
/// is kept and the file it names is written. A dangling link resolves to the missing file it
|
||||
/// names, which is then created.
|
||||
fn resolve_links(path: &Path) -> io::Result<PathBuf> {
|
||||
let mut p = path.to_path_buf();
|
||||
for _ in 0..MAX_LINKS {
|
||||
match std::fs::symlink_metadata(&p) {
|
||||
Ok(m) if m.file_type().is_symlink() => {
|
||||
let to = std::fs::read_link(&p).map_err(|e| io::Error::new(e.kind(), format!("cannot save {}: {e}", path.display())))?;
|
||||
p = match p.parent() {
|
||||
Some(dir) if to.is_relative() => dir.join(to),
|
||||
_ => to,
|
||||
};
|
||||
}
|
||||
_ => return Ok(p),
|
||||
}
|
||||
}
|
||||
Err(io::Error::new(io::ErrorKind::InvalidInput, format!("cannot save {}: too many levels of symbolic links", path.display())))
|
||||
}
|
||||
|
||||
/// Generates a unique, hidden temporary file name beside `file`.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use craft_atomic::{atomic_write, atomic_write_with, or_plain_fsync, write_new_with};
|
||||
use craft_atomic::{atomic_write, atomic_write_with, or_plain_fsync, write_new, write_new_with};
|
||||
use std::cell::Cell;
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -125,3 +125,30 @@ fn mode_and_symlink_preservation() {
|
||||
assert!(std::fs::symlink_metadata(&link).map(|m| m.file_type().is_symlink()).unwrap_or(false));
|
||||
assert_eq!(std::fs::read(&real).ok(), Some(b"through symlink".to_vec()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn dangling_relative_symlink_stays_a_link() {
|
||||
let d = TempDir::new("dangling");
|
||||
std::fs::create_dir(d.0.join("real")).ok();
|
||||
let link = d.0.join("link.txt");
|
||||
std::os::unix::fs::symlink("real/doc.txt", &link).ok();
|
||||
|
||||
assert!(atomic_write(&link, b"created through the link").is_ok());
|
||||
assert!(std::fs::symlink_metadata(&link).map(|m| m.file_type().is_symlink()).unwrap_or(false));
|
||||
assert_eq!(std::fs::read(d.0.join("real/doc.txt")).ok(), Some(b"created through the link".to_vec()));
|
||||
assert_no_temp(&d.0);
|
||||
assert_no_temp(&d.0.join("real"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn symlink_loop_is_an_error() {
|
||||
let d = TempDir::new("loop");
|
||||
std::os::unix::fs::symlink("b", d.0.join("a")).ok();
|
||||
std::os::unix::fs::symlink("a", d.0.join("b")).ok();
|
||||
assert!(atomic_write(&d.0.join("a"), b"x").is_err_and(|e| e.to_string().starts_with("cannot save") && e.to_string().contains("symbolic links")));
|
||||
assert!(write_new(&d.0.join("a"), b"x").is_err());
|
||||
assert!(std::fs::symlink_metadata(d.0.join("a")).map(|m| m.file_type().is_symlink()).unwrap_or(false));
|
||||
assert_no_temp(&d.0);
|
||||
}
|
||||
Reference in new issue
Block a user