use std::path::Path;
pub fn normalize_for_digest(path: &Path) -> String {
let forward = path.to_string_lossy().replace('\\', "/");
let (prefix, rest) = root_prefix(&forward);
let joined = rest
.split('/')
.filter(|segment| !segment.is_empty())
.collect::<Vec<_>>()
.join("/");
format!("{prefix}{joined}")
}
fn root_prefix(forward: &str) -> (String, &str) {
if let Some(rest) = forward.strip_prefix("//") {
return ("//".to_owned(), rest);
}
if let Some(rest) = forward.strip_prefix('/') {
return ("/".to_owned(), rest);
}
let bytes = forward.as_bytes();
let has_drive_letter =
bytes.first().is_some_and(u8::is_ascii_alphabetic) && bytes.get(1) == Some(&b':');
if has_drive_letter {
let drive = &forward[..2];
let rest = forward[2..].strip_prefix('/').unwrap_or(&forward[2..]);
return (format!("{drive}/"), rest);
}
(String::new(), forward)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn windows_and_unix_paths_with_the_same_components_render_identically() {
let windows = normalize_for_digest(Path::new("C:\\repo\\sub"));
let unix = normalize_for_digest(Path::new("C:/repo/sub"));
assert_eq!(windows, unix);
}
#[test]
fn relative_windows_style_path_matches_relative_unix_style_path() {
let windows = normalize_for_digest(Path::new("sub\\dir\\leaf"));
let unix = normalize_for_digest(Path::new("sub/dir/leaf"));
assert_eq!(windows, unix);
}
#[test]
fn current_dir_renders_unchanged() {
assert_eq!(normalize_for_digest(Path::new(".")), ".");
}
#[test]
fn absolute_paths_render_with_a_single_leading_slash() {
assert_eq!(normalize_for_digest(Path::new("/repo/sub")), "/repo/sub");
assert_eq!(
normalize_for_digest(Path::new("C:\\repo\\sub")),
"C:/repo/sub"
);
}
#[test]
fn windows_root_and_unc_paths_do_not_collide_with_posix_absolute_paths() {
assert_eq!(normalize_for_digest(Path::new("C:\\")), "C:/");
assert_eq!(normalize_for_digest(Path::new("C:")), "C:/");
assert_eq!(
normalize_for_digest(Path::new("\\\\server\\share")),
"//server/share"
);
assert_ne!(
normalize_for_digest(Path::new("\\\\server\\share")),
normalize_for_digest(Path::new("/server/share"))
);
}
#[test]
fn examples_basic_digest_is_unchanged_by_cwd_normalization() {
let compiled = crate::dsl::compile(Path::new("examples/basic/Drovefile"))
.expect("examples/basic compiles");
let profile = compiled
.config
.profiles
.get("default")
.expect("examples/basic declares a `default` profile");
let ir = profile.to_ir();
let digests: Vec<(&str, &str)> = ir
.resources
.iter()
.map(|resource| (resource.name.as_str(), resource.digest.as_str()))
.collect();
assert_eq!(
digests,
vec![
(
"development",
"195d182808ab3bcbfe0f098ab4159b2dff2635a8433070c7e03f0967155ce01a"
),
(
"editor",
"999810892ca9ffcf59d6e38630d7291fc4a7968d6e844da5f2f3f2b1a33e423a"
),
(
"tests",
"999810892ca9ffcf59d6e38630d7291fc4a7968d6e844da5f2f3f2b1a33e423a"
),
]
);
}
}