From abb1b5681ebd08f78d62420ce175ea852dcafa45 Mon Sep 17 00:00:00 2001 From: Olivia Lee Date: Sun, 6 Apr 2025 17:43:12 -0700 Subject: [PATCH] add partial_canonicalize helper function This is useful for checking for potential overlap between paths that have not been fully created yet. --- Cargo.lock | 14 +++++++ Cargo.toml | 1 + src/utils.rs | 111 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 2641dca3..59f2ff41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -962,6 +962,7 @@ dependencies = [ "serde_yaml", "sha-1", "strum", + "tempfile", "thiserror 2.0.12", "thread_local", "tikv-jemallocator", @@ -3195,6 +3196,19 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "terminal_size" version = "0.4.2" diff --git a/Cargo.toml b/Cargo.toml index f0913909..00971197 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,6 +153,7 @@ nix = { version = "0.29", features = ["resource", "time"] } assert_cmd = "2.0.16" insta = { version = "1.42.2", features = ["filters", "json", "redactions"] } predicates = "3.1.3" +tempfile = "3.19.1" [profile.dev.package.insta] opt-level = 3 diff --git a/src/utils.rs b/src/utils.rs index 45211399..568bc5be 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,6 +2,8 @@ use std::{ borrow::Cow, cmp, fmt, fmt::Write, + io, + path::{Component, Path, PathBuf}, str::FromStr, time::{SystemTime, UNIX_EPOCH}, }; @@ -14,6 +16,7 @@ use ruma::{ api::client::error::ErrorKind, canonical_json::try_from_json_map, CanonicalJsonError, CanonicalJsonObject, MxcUri, MxcUriError, OwnedMxcUri, }; +use tokio::fs; use crate::{Error, Result}; @@ -380,9 +383,67 @@ pub(crate) fn u8_slice_to_hex(slice: &[u8]) -> String { }) } +/// Canonicalize a path where some components may not exist yet. +/// +/// It's assumed that non-existent components will be created as +/// directories. This should match the result of [`fs::canonicalize`] +/// _after_ calling [`fs::create_dir_all`] on `path`. +#[allow(dead_code)] +pub(crate) async fn partial_canonicalize(path: &Path) -> io::Result { + let mut ret = std::env::current_dir()?; + + let mut base_path = Cow::Borrowed(path); + let mut components = base_path.components(); + + while let Some(component) = components.next() { + match component { + Component::Prefix(_) | Component::RootDir => { + let component_path: &Path = component.as_ref(); + component_path.clone_into(&mut ret); + } + Component::CurDir => (), + Component::ParentDir => { + ret.pop(); + } + Component::Normal(p) => { + let component_path = ret.join(p); + match fs::symlink_metadata(&component_path).await { + // path is a symlink + Ok(metadata) if metadata.is_symlink() => { + let destination = + fs::read_link(&component_path).await?; + // iterate over the symlink destination components + // before continuing with the original path + base_path = + Cow::Owned(destination.join(components.as_path())); + components = base_path.components(); + } + // path exists, not a symlink + Ok(_) => { + ret.push(p); + } + // path does not exist + Err(error) if error.kind() == io::ErrorKind::NotFound => { + // assume a directory will be created here + ret.push(p); + } + Err(error) => return Err(error), + } + } + } + } + + Ok(ret) +} + #[cfg(test)] mod tests { - use crate::utils::{dbg_truncate_str, u8_slice_to_hex}; + use tempfile::TempDir; + use tokio::fs; + + use crate::utils::{ + dbg_truncate_str, partial_canonicalize, u8_slice_to_hex, + }; #[test] fn test_truncate_str() { @@ -412,4 +473,52 @@ mod tests { 4242424242424242424242424242424242424242" ); } + + #[tokio::test] + async fn test_partial_canonicalize() { + let tmp_dir = + TempDir::with_prefix("test_partial_canonicalize").unwrap(); + let path = tmp_dir.path(); + + fs::create_dir(&path.join("dir")).await.unwrap(); + fs::symlink(path.join("dir"), path.join("absolute-link-to-dir")) + .await + .unwrap(); + fs::symlink("./dir", path.join("relative-link-to-dir")).await.unwrap(); + + assert_eq!(partial_canonicalize(path).await.unwrap(), path); + assert_eq!(partial_canonicalize(&path.join("./")).await.unwrap(), path); + assert_eq!( + partial_canonicalize(&path.join("dir/..")).await.unwrap(), + path + ); + assert_eq!( + partial_canonicalize(&path.join("absolute-link-to-dir")) + .await + .unwrap(), + path.join("dir") + ); + assert_eq!( + partial_canonicalize(&path.join("relative-link-to-dir")) + .await + .unwrap(), + path.join("dir") + ); + assert_eq!( + partial_canonicalize(&path.join("absolute-link-to-dir/new-dir")) + .await + .unwrap(), + path.join("dir/new-dir") + ); + assert_eq!( + partial_canonicalize( + &path.join("absolute-link-to-dir/new-dir/../..") + ) + .await + .unwrap(), + path, + ); + + tmp_dir.close().unwrap(); + } }