add partial_canonicalize helper function

This is useful for checking for potential overlap between paths that
have not been fully created yet.
This commit is contained in:
Olivia Lee 2025-04-06 17:43:12 -07:00
parent c03103a142
commit abb1b5681e
No known key found for this signature in database
GPG key ID: 54D568A15B9CD1F9
3 changed files with 125 additions and 1 deletions

14
Cargo.lock generated
View file

@ -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"

View file

@ -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

View file

@ -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<PathBuf> {
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();
}
}