use std::path::{Path, PathBuf};
use unicode_normalization::UnicodeNormalization;
#[derive(Debug, Clone)]
struct Rule {
prefix: String,
key_sentinel: String,
source_sentinel: String,
flag_target: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ConfiguredSourceRoot {
pub root: PathBuf,
pub key_sentinel: String,
pub depinfo_sentinel: String,
pub priority: u8,
}
#[derive(Debug, Clone)]
struct ConfiguredSourceRootGroup {
restore_root: PathBuf,
aliases: Vec<PathBuf>,
key_sentinel: String,
depinfo_sentinel: String,
}
#[derive(Debug, Clone)]
pub struct PathNormalizer {
rules: Vec<Rule>,
configured_base_dir_count: usize,
configured_source_root_groups: Vec<ConfiguredSourceRootGroup>,
source_restore_roots: Vec<(String, PathBuf)>,
path_only_env_vars: Vec<String>,
}
impl PathNormalizer {
pub fn from_env(workspace_root: Option<&Path>) -> Self {
let mut rules = Vec::new();
let mut source_restore_roots = Vec::new();
let legacy_base_dir = std::env::var_os("KACHE_BASE_DIR");
push_legacy_base_dir_rules(
&mut rules,
&mut source_restore_roots,
legacy_base_dir.as_deref(),
);
if let Ok(current_dir) = std::env::current_dir() {
push_path_rule_aliases(
&mut rules,
&mut source_restore_roots,
¤t_dir,
"<WORKSPACE>",
"<CWD>",
);
}
if let Some(workspace_root) = workspace_root {
push_path_rule_aliases(
&mut rules,
&mut source_restore_roots,
workspace_root,
"<WORKSPACE>",
"<WORKSPACE>",
);
}
if let Some(target_dir) = std::env::var_os("CARGO_TARGET_DIR") {
push_path_rule_aliases(
&mut rules,
&mut source_restore_roots,
Path::new(&target_dir),
"<TARGET>",
"<TARGET>",
);
}
let cargo_home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".cargo")));
push_rule_with_variants(
&mut rules,
cargo_home.and_then(|p| canonical_string(&p)),
"<CARGO_HOME>",
);
let rustup_home = std::env::var_os("RUSTUP_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|h| h.join(".rustup")));
push_rule_with_variants(
&mut rules,
rustup_home.and_then(|p| canonical_string(&p)),
"<RUSTUP_HOME>",
);
push_rule_with_variants(
&mut rules,
dirs::home_dir().and_then(|p| canonical_string(&p)),
"<HOME>",
);
for (env_key, sentinel) in [
("APPDATA", "<APPDATA>"),
("LOCALAPPDATA", "<LOCALAPPDATA>"),
("PROGRAMFILES", "<PROGRAMFILES>"),
] {
push_rule_with_variants(
&mut rules,
std::env::var_os(env_key).and_then(|v| canonical_string(Path::new(&v))),
sentinel,
);
}
push_rule_with_variants(
&mut rules,
canonical_string(&std::env::temp_dir()),
"<TMPDIR>",
);
let mut seen = std::collections::HashSet::new();
rules.retain(|r| seen.insert(r.prefix.clone()));
Self {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots,
path_only_env_vars: Vec::new(),
}
}
pub fn with_path_only_env_vars(mut self, vars: Vec<String>) -> Self {
self.path_only_env_vars = vars;
self
}
pub(crate) fn with_target_dir(mut self, target_dir: Option<&Path>) -> Self {
let Some(target_dir) = target_dir else {
return self;
};
let mut target_rules = Vec::new();
push_path_rule_aliases(
&mut target_rules,
&mut self.source_restore_roots,
target_dir,
"<TARGET>",
"<TARGET>",
);
let target_rank = flag_emit_rank("<TARGET>");
let insert_at = self
.rules
.iter()
.position(|rule| flag_emit_rank(&rule.key_sentinel) < target_rank)
.unwrap_or(self.rules.len());
self.rules.splice(insert_at..insert_at, target_rules);
let mut seen = std::collections::HashSet::new();
self.rules.retain(|rule| seen.insert(rule.prefix.clone()));
self
}
pub fn with_base_dirs(mut self, base_dirs: &[String]) -> Self {
let (configured, mut extra_rules) = configured_base_dir_rules(base_dirs);
self.configured_base_dir_count = configured.len();
let legacy_prefixes: std::collections::HashSet<String> = self
.rules
.iter()
.filter(|rule| rule.key_sentinel == "<BASE_DIR>")
.map(|rule| rule.prefix.clone())
.collect();
extra_rules.retain(|rule| !legacy_prefixes.contains(&rule.prefix));
extra_rules.sort_by(|left, right| {
right
.prefix
.len()
.cmp(&left.prefix.len())
.then_with(|| left.prefix.cmp(&right.prefix))
.then_with(|| left.key_sentinel.cmp(&right.key_sentinel))
});
extra_rules.append(&mut self.rules);
let mut seen = std::collections::HashSet::new();
extra_rules.retain(|rule| seen.insert(rule.prefix.clone()));
self.rules = extra_rules;
self.configured_source_root_groups = configured
.iter()
.enumerate()
.filter_map(|(index, lexical)| {
let restore_root = PathBuf::from(lexical);
if !restore_root.exists() {
return None;
}
let key_sentinel = configured_base_dir_sentinel(index);
if self
.source_rule_for_path(&restore_root)
.is_none_or(|winner| winner.key_sentinel != key_sentinel)
{
return None;
}
let depinfo_sentinel = format!("__kache_base_dir_{index}__/");
let aliases = self
.rules
.iter()
.filter(|rule| rule.key_sentinel == key_sentinel)
.filter(|rule| is_native_configured_path(&rule.prefix))
.filter(|rule| rule.prefix != *lexical)
.map(|rule| PathBuf::from(&rule.prefix))
.filter(|alias| alias.exists())
.collect();
Some(ConfiguredSourceRootGroup {
restore_root,
aliases,
key_sentinel,
depinfo_sentinel,
})
})
.collect();
self
}
pub fn configured_base_dir_count(&self) -> usize {
self.configured_base_dir_count
}
pub fn path_only_env_vars(&self) -> &[String] {
&self.path_only_env_vars
}
pub(crate) fn raw_prefixes(&self) -> impl Iterator<Item = &str> {
self.rules
.iter()
.map(|r| r.prefix.as_str())
.filter(|p| !p.is_empty())
}
pub(crate) fn depinfo_source_roots(&self) -> Vec<ConfiguredSourceRoot> {
let mut groups = self.configured_source_root_groups.clone();
for source_sentinel in [
"<CWD>",
"<WORKSPACE>",
"<TARGET>",
"<CARGO_HOME>",
"<BASE_DIR>",
"<RUSTUP_HOME>",
"<HOME>",
"<APPDATA>",
"<LOCALAPPDATA>",
"<PROGRAMFILES>",
"<TMPDIR>",
] {
let owned_rules = self
.rules
.iter()
.filter(|rule| rule.source_sentinel == source_sentinel)
.filter(|rule| is_native_configured_path(&rule.prefix))
.filter(|rule| Path::new(&rule.prefix).exists())
.collect::<Vec<_>>();
let designated = self
.source_restore_roots
.iter()
.find(|(sentinel, _)| sentinel == source_sentinel)
.map(|(_, root)| root.clone());
let restore_root = designated
.and_then(|root| {
self.source_rule_for_path(&root)
.is_some_and(|rule| rule.source_sentinel == source_sentinel)
.then_some(root)
})
.or_else(|| {
owned_rules.iter().find_map(|rule| {
let root = PathBuf::from(&rule.prefix);
self.source_rule_for_path(&root)
.is_some_and(|winner| winner.source_sentinel == source_sentinel)
.then_some(root)
})
});
if let Some(restore_root) = restore_root {
let restore_spelling = restore_root.as_os_str();
let aliases = owned_rules
.iter()
.filter(|rule| {
self.source_rule_for_path(Path::new(&rule.prefix))
.is_some_and(|winner| winner.source_sentinel == source_sentinel)
})
.filter(|rule| std::ffi::OsStr::new(&rule.prefix) != restore_spelling)
.map(|rule| PathBuf::from(&rule.prefix))
.collect();
let Some(key_sentinel) = self
.source_rule_for_path(&restore_root)
.map(|rule| rule.key_sentinel.clone())
else {
continue;
};
groups.push(ConfiguredSourceRootGroup {
restore_root,
aliases,
key_sentinel,
depinfo_sentinel: depinfo_sentinel_for_source(source_sentinel)
.expect("listed source roots have dep-info sentinels"),
});
}
}
groups.sort_by_key(|group| {
let longest = std::iter::once(&group.restore_root)
.chain(group.aliases.iter())
.map(|root| root.as_os_str().len())
.max()
.unwrap_or_default();
std::cmp::Reverse((flag_emit_rank(&group.key_sentinel), longest))
});
groups
.into_iter()
.flat_map(|group| {
let key_sentinel = group.key_sentinel;
let depinfo_sentinel = group.depinfo_sentinel;
std::iter::once(group.restore_root)
.chain(group.aliases)
.map(move |root| ConfiguredSourceRoot {
root,
key_sentinel: key_sentinel.clone(),
depinfo_sentinel: depinfo_sentinel.clone(),
priority: flag_emit_rank(&key_sentinel),
})
})
.collect()
}
pub(crate) fn source_path_identity(&self, path: &Path) -> Option<Vec<u8>> {
let input = path.as_os_str().to_str()?;
let winner = self.source_rule_for_path(path)?;
depinfo_sentinel_for_source(&winner.source_sentinel)?;
let remainder = input.strip_prefix(&winner.prefix)?;
let remainder = remainder
.strip_prefix('/')
.or_else(|| remainder.strip_prefix('\\'))
.unwrap_or(remainder);
let mut identity = winner.source_sentinel.as_bytes().to_vec();
if !remainder.is_empty() {
identity.push(b'/');
identity.extend_from_slice(remainder.as_bytes());
}
Some(identity)
}
fn source_rule_for_path<'a>(&'a self, path: &Path) -> Option<&'a Rule> {
let input = path.as_os_str().to_str()?;
self.rules
.iter()
.filter(|rule| !rule.prefix.is_empty())
.filter_map(|rule| {
input.strip_prefix(&rule.prefix)?;
if !configured_prefix_boundaries_match(input, 0, rule.prefix.len(), &rule.prefix) {
return None;
}
Some((flag_emit_rank(&rule.key_sentinel), rule.prefix.len(), rule))
})
.max_by_key(|(rank, prefix_len, _)| (*rank, *prefix_len))
.map(|(_, _, rule)| rule)
}
pub fn with_rust_src_rule(mut self, sysroot: Option<&Path>, commit_hash: Option<&str>) -> Self {
let (Some(sysroot), Some(hash)) = (sysroot, commit_hash) else {
return self;
};
let rust_src = sysroot.join("lib").join("rustlib").join("src").join("rust");
let Some(prefix) = canonical_string(&rust_src) else {
return self;
};
let flag_target = format!("/rustc/{hash}");
let mut rust_src_rules = Vec::new();
push_rule_with_variants_target(
&mut rust_src_rules,
Some(prefix),
"<RUST_SRC>",
&flag_target,
);
let insert_at = self
.rules
.iter()
.position(|rule| configured_base_dir_index(&rule.key_sentinel).is_none())
.unwrap_or(self.rules.len());
self.rules.splice(insert_at..insert_at, rust_src_rules);
let mut seen = std::collections::HashSet::new();
self.rules.retain(|r| seen.insert(r.prefix.clone()));
self
}
#[allow(dead_code)]
pub fn empty() -> Self {
Self {
rules: Vec::new(),
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
}
}
pub fn normalize<S: AsRef<str>>(&self, s: S) -> String {
let mut out: String = s.as_ref().nfc().collect();
for rule in &self.rules {
if rule.prefix.is_empty() {
continue;
}
out = if configured_base_dir_index(&rule.key_sentinel).is_some() {
replace_configured_path_prefix(&out, &rule.prefix, &rule.key_sentinel)
} else {
out.replace(&rule.prefix, &rule.key_sentinel)
};
}
warn_if_path_leaked(&out);
out
}
pub fn remap_args(&self) -> Vec<String> {
let mut ordered: Vec<&Rule> = self.rules.iter().filter(|r| !r.prefix.is_empty()).collect();
ordered.sort_by_key(|r| (flag_emit_rank(&r.key_sentinel), r.prefix.len()));
ordered
.into_iter()
.map(|r| format!("--remap-path-prefix={}={}", r.prefix, r.flag_target))
.collect()
}
}
fn replace_configured_path_prefix(input: &str, prefix: &str, replacement: &str) -> String {
let mut out = String::with_capacity(input.len());
let mut copied_until = 0;
let mut search_from = 0;
while let Some(relative) = input[search_from..].find(prefix) {
let start = search_from + relative;
let end = start + prefix.len();
if configured_prefix_boundaries_match(input, start, end, prefix) {
out.push_str(&input[copied_until..start]);
out.push_str(replacement);
copied_until = end;
search_from = end;
} else {
search_from = start + 1;
}
}
out.push_str(&input[copied_until..]);
out
}
fn configured_prefix_boundaries_match(input: &str, start: usize, end: usize, prefix: &str) -> bool {
if !is_windows_absolute_path(prefix) && follows_windows_drive_prefix(input, start) {
return false;
}
let left = if start == 0 {
true
} else {
let before = &input[..start];
before.ends_with("-I")
|| before.ends_with("-L")
|| before.ends_with("-F")
|| before.ends_with("-B")
|| before.chars().next_back().is_some_and(|ch| {
ch.is_whitespace()
|| ch == '\u{1f}'
|| matches!(
ch,
'=' | ':' | ';' | ',' | '"' | '\'' | '(' | '[' | '{' | '@'
)
})
};
let windows = is_windows_absolute_path(prefix);
let prefix_has_trailing_separator =
prefix.ends_with('/') || (windows && prefix.ends_with('\\'));
let right = end == input.len()
|| prefix_has_trailing_separator
|| input[end..]
.chars()
.next()
.is_some_and(|ch| ch == '/' || (windows && ch == '\\'));
left && right
}
fn follows_windows_drive_prefix(input: &str, start: usize) -> bool {
if start < 2 {
return false;
}
let bytes = input.as_bytes();
if bytes[start - 1] != b':' || !bytes[start - 2].is_ascii_alphabetic() {
return false;
}
let lead = &input[..start - 2];
lead.is_empty()
|| lead.ends_with("-I")
|| lead.ends_with("-L")
|| lead.ends_with("-F")
|| lead.ends_with("-B")
|| lead.chars().next_back().is_some_and(|ch| {
ch.is_whitespace()
|| ch == '\u{1f}'
|| matches!(
ch,
'=' | ':' | ';' | ',' | '"' | '\'' | '(' | '[' | '{' | '@'
)
})
}
fn flag_emit_rank(key_sentinel: &str) -> u8 {
if configured_base_dir_index(key_sentinel).is_some() {
return 8;
}
match key_sentinel {
"<TMPDIR>" => 0,
"<HOME>" | "<APPDATA>" | "<LOCALAPPDATA>" | "<PROGRAMFILES>" => 1,
"<RUSTUP_HOME>" => 2,
"<TARGET>" => 3,
"<WORKSPACE>" => 4,
"<BASE_DIR>" => 5,
"<CARGO_HOME>" => 6,
"<RUST_SRC>" => 7,
_ => 4,
}
}
fn configured_base_dir_rules(base_dirs: &[String]) -> (Vec<String>, Vec<Rule>) {
let mut configured = base_dirs.to_vec();
configured.sort();
configured.dedup();
configured.retain(|path| !is_filesystem_root_prefix(path));
let mut lexical_rules = Vec::new();
for (index, path) in configured.iter().enumerate() {
push_configured_base_dir_rule(&mut lexical_rules, path.nfc().collect(), index);
}
let reserved_lexical: std::collections::HashSet<String> = lexical_rules
.iter()
.map(|rule| rule.prefix.clone())
.collect();
let mut alias_rules = Vec::new();
for (index, path) in configured.iter().enumerate() {
let lexical: String = path.nfc().collect();
if !is_native_configured_path(path) {
continue;
}
let Ok(canonical) = Path::new(path).canonicalize() else {
continue;
};
for canonical in os_path_spellings(&canonical) {
if canonical == lexical {
continue;
}
let mut candidates = Vec::new();
push_configured_base_dir_rule(&mut candidates, canonical, index);
candidates.retain(|rule| !reserved_lexical.contains(&rule.prefix));
alias_rules.extend(candidates);
}
}
lexical_rules.extend(alias_rules);
lexical_rules.sort_by(|left, right| {
right
.prefix
.len()
.cmp(&left.prefix.len())
.then_with(|| left.prefix.cmp(&right.prefix))
.then_with(|| left.key_sentinel.cmp(&right.key_sentinel))
});
let mut seen = std::collections::HashSet::new();
lexical_rules.retain(|rule| seen.insert(rule.prefix.clone()));
(configured, lexical_rules)
}
fn push_configured_base_dir_rule(rules: &mut Vec<Rule>, prefix: String, index: usize) {
let sentinel = configured_base_dir_sentinel(index);
let target = configured_base_dir_target(index);
if prefix.is_empty() {
return;
}
rules.push(Rule {
prefix: prefix.clone(),
key_sentinel: sentinel.clone(),
source_sentinel: sentinel.clone(),
flag_target: target.clone(),
});
if is_windows_absolute_path(&prefix) {
push_slash_and_case_variants(rules, &prefix, &sentinel, &sentinel, &target);
if let Some(short) = short_path_name(&prefix)
&& short != prefix
{
rules.push(Rule {
prefix: short.clone(),
key_sentinel: sentinel.clone(),
source_sentinel: sentinel.clone(),
flag_target: target.clone(),
});
push_slash_and_case_variants(rules, &short, &sentinel, &sentinel, &target);
}
}
}
fn is_windows_absolute_path(path: &str) -> bool {
let bytes = path.as_bytes();
(bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\'))
|| path.starts_with("//")
|| path.starts_with(r"\\")
}
fn is_native_configured_path(path: &str) -> bool {
is_native_configured_path_for(path, cfg!(windows))
}
fn is_native_configured_path_for(path: &str, windows_host: bool) -> bool {
if windows_host {
is_windows_absolute_path(path)
} else {
path.starts_with('/') && !is_windows_absolute_path(path)
}
}
pub(crate) fn configured_base_dir_prefix_maps(base_dirs: &[String]) -> Vec<(String, String)> {
configured_base_dir_rules(base_dirs)
.1
.into_iter()
.map(|rule| (rule.prefix, rule.flag_target))
.collect()
}
pub(crate) fn configured_base_dir_sentinel(index: usize) -> String {
format!("<BASE_DIR_{index}>")
}
pub(crate) fn configured_base_dir_target(index: usize) -> String {
format!("/kache/base-dir-{index}")
}
fn configured_base_dir_index(sentinel: &str) -> Option<usize> {
sentinel
.strip_prefix("<BASE_DIR_")?
.strip_suffix('>')?
.parse()
.ok()
}
fn depinfo_sentinel_for_source(source_sentinel: &str) -> Option<String> {
if let Some(index) = configured_base_dir_index(source_sentinel) {
return Some(format!("__kache_base_dir_{index}__/"));
}
Some(
match source_sentinel {
"<CWD>" => "__kache_cwd__/",
"<WORKSPACE>" => "__kache_workspace__/",
"<TARGET>" => "__kache_target_rule__/",
"<CARGO_HOME>" => "__kache_cargo_home__/",
"<BASE_DIR>" => "__kache_base_dir__/",
"<RUSTUP_HOME>" => "__kache_rustup_home__/",
"<HOME>" => "__kache_home__/",
"<APPDATA>" => "__kache_appdata__/",
"<LOCALAPPDATA>" => "__kache_localappdata__/",
"<PROGRAMFILES>" => "__kache_programfiles__/",
"<TMPDIR>" => "__kache_tmpdir__/",
_ => return None,
}
.to_string(),
)
}
fn flag_target_for(key_sentinel: &str) -> String {
match key_sentinel {
"<WORKSPACE>" => workspace_flag_target().to_string(),
"<BASE_DIR>" => "/kache/base-dir".to_string(),
"<TARGET>" => "/kache/target".to_string(),
"<CARGO_HOME>" => "/kache/.cargo".to_string(),
"<RUSTUP_HOME>" => "/kache/rustup".to_string(),
"<HOME>" => "/kache/home".to_string(),
"<APPDATA>" => "/kache/appdata".to_string(),
"<LOCALAPPDATA>" => "/kache/localappdata".to_string(),
"<PROGRAMFILES>" => "/kache/programfiles".to_string(),
"<TMPDIR>" => "/kache/tmp".to_string(),
other => format!(
"/kache/{}",
other.trim_matches(['<', '>']).to_ascii_lowercase()
),
}
}
fn workspace_flag_target() -> &'static str {
if cfg!(target_os = "linux") {
"/proc/self/cwd"
} else {
"/kache/workspace"
}
}
pub fn rustc_path_normalize_enabled() -> bool {
parse_rustc_normalize_toggle(std::env::var("KACHE_RUSTC_PATH_NORMALIZE").ok().as_deref())
}
fn parse_rustc_normalize_toggle(value: Option<&str>) -> bool {
match value {
Some(v) => !matches!(
v.trim().to_ascii_lowercase().as_str(),
"0" | "false" | "off" | "no"
),
None => true,
}
}
pub(crate) fn check_for_path_leak(value: &str, context: &str) {
const SUSPICIOUS_PREFIXES: &[&str] = &[
"/Users/", "/home/", "/private/tmp/", "/private/var/", "/var/folders/", "C:\\Users\\", ];
for prefix in SUSPICIOUS_PREFIXES {
if let Some(idx) = value.find(prefix) {
let start = idx.saturating_sub(40);
let end = (idx + prefix.len() + 40).min(value.len());
tracing::warn!(
"residual absolute path detected in `{}` (prefix `{}`): ...{}...",
context,
prefix,
&value[start..end]
);
return;
}
}
}
fn warn_if_path_leaked(s: &str) {
check_for_path_leak(s, "PathNormalizer::normalize");
}
pub(crate) fn canonical_string(path: &Path) -> Option<String> {
let canon = path.canonicalize().ok()?;
let lossy = canon.to_string_lossy();
let s: String = strip_verbatim_prefix(&lossy).nfc().collect();
if s.is_empty() { None } else { Some(s) }
}
fn strip_verbatim_prefix(s: &str) -> std::borrow::Cow<'_, str> {
if let Some(unc) = s.strip_prefix(r"\\?\UNC\") {
std::borrow::Cow::Owned(format!(r"\\{unc}"))
} else if let Some(drive) = s.strip_prefix(r"\\?\") {
std::borrow::Cow::Borrowed(drive)
} else {
std::borrow::Cow::Borrowed(s)
}
}
pub(crate) fn is_filesystem_root_prefix(value: &str) -> bool {
let value = strip_verbatim_prefix(value);
if value == "/" {
return true;
}
let bytes = value.as_bytes();
if bytes.len() == 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& matches!(bytes[2], b'/' | b'\\')
{
return true;
}
let normalized = value.replace('\\', "/");
let Some(unc) = normalized.strip_prefix("//") else {
return false;
};
let mut components = unc.trim_end_matches('/').split('/');
components.next().is_some_and(|part| !part.is_empty())
&& components.next().is_some_and(|part| !part.is_empty())
&& components.next().is_none()
}
fn push_rule_with_variants(rules: &mut Vec<Rule>, prefix: Option<String>, sentinel: &str) {
let flag_target = flag_target_for(sentinel);
push_rule_with_variants_target(rules, prefix, sentinel, &flag_target);
}
fn push_rule_with_variants_source(
rules: &mut Vec<Rule>,
prefix: Option<String>,
sentinel: &str,
source_sentinel: &str,
) {
let flag_target = flag_target_for(sentinel);
push_rule_with_variants_target_and_source(
rules,
prefix,
sentinel,
source_sentinel,
&flag_target,
);
}
fn push_path_rule_aliases(
rules: &mut Vec<Rule>,
source_restore_roots: &mut Vec<(String, PathBuf)>,
path: &Path,
sentinel: &str,
source_sentinel: &str,
) {
let lexical = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
source_restore_roots.push((source_sentinel.to_string(), lexical.clone()));
let mut spellings = os_path_spellings(&lexical);
if let Ok(canonical) = path.canonicalize()
&& canonical != lexical
{
spellings.extend(os_path_spellings(&canonical));
}
spellings.sort_by_key(|prefix| std::cmp::Reverse(prefix.len()));
spellings.dedup();
for prefix in spellings {
push_rule_with_variants_source(rules, Some(prefix), sentinel, source_sentinel);
}
}
fn push_legacy_base_dir_rules(
rules: &mut Vec<Rule>,
source_restore_roots: &mut Vec<(String, PathBuf)>,
base_dir: Option<&std::ffi::OsStr>,
) {
let Some(base_dir) = base_dir.filter(|value| !value.is_empty()) else {
return;
};
let path = Path::new(base_dir);
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let Some(prefix) = absolute.as_os_str().to_str() else {
return;
};
if is_filesystem_root_prefix(prefix) {
return;
}
push_path_rule_aliases(
rules,
source_restore_roots,
path,
"<BASE_DIR>",
"<BASE_DIR>",
);
}
fn os_path_spellings(path: &Path) -> Vec<String> {
let Some(raw) = path.as_os_str().to_str().filter(|value| !value.is_empty()) else {
return Vec::new();
};
let normalized: String = raw.nfc().collect();
let stripped = strip_verbatim_prefix(&normalized).into_owned();
let mut spellings = vec![raw.to_string(), normalized, stripped];
spellings.retain(|spelling| !is_filesystem_root_prefix(spelling));
spellings.dedup();
spellings
}
fn push_rule_with_variants_target(
rules: &mut Vec<Rule>,
prefix: Option<String>,
sentinel: &str,
flag_target: &str,
) {
push_rule_with_variants_target_and_source(rules, prefix, sentinel, sentinel, flag_target);
}
fn push_rule_with_variants_target_and_source(
rules: &mut Vec<Rule>,
prefix: Option<String>,
sentinel: &str,
source_sentinel: &str,
flag_target: &str,
) {
let Some(prefix) = prefix else { return };
if prefix.is_empty() {
return;
}
rules.push(Rule {
prefix: prefix.clone(),
key_sentinel: sentinel.to_string(),
source_sentinel: source_sentinel.to_string(),
flag_target: flag_target.to_string(),
});
if !cfg!(windows) {
return;
}
push_slash_and_case_variants(rules, &prefix, sentinel, source_sentinel, flag_target);
if let Some(short) = short_path_name(&prefix)
&& short != prefix
{
rules.push(Rule {
prefix: short.clone(),
key_sentinel: sentinel.to_string(),
source_sentinel: source_sentinel.to_string(),
flag_target: flag_target.to_string(),
});
push_slash_and_case_variants(rules, &short, sentinel, source_sentinel, flag_target);
}
}
fn push_slash_and_case_variants(
rules: &mut Vec<Rule>,
prefix: &str,
sentinel: &str,
source_sentinel: &str,
flag_target: &str,
) {
let push = |rules: &mut Vec<Rule>, p: String| {
rules.push(Rule {
prefix: p,
key_sentinel: sentinel.to_string(),
source_sentinel: source_sentinel.to_string(),
flag_target: flag_target.to_string(),
});
};
let fs = prefix.replace('\\', "/");
if fs != prefix {
push(rules, fs.clone());
}
let bs = prefix.replace('/', "\\");
if bs != prefix {
push(rules, bs.clone());
}
let lc = lowercase_drive_letter(prefix);
if let Some(ref lc_str) = lc
&& lc_str != prefix
{
push(rules, lc_str.clone());
}
if let Some(fs_lc) = lowercase_drive_letter(&fs)
&& fs_lc != fs
&& Some(&fs_lc) != lc.as_ref()
{
push(rules, fs_lc);
}
if let Some(bs_lc) = lowercase_drive_letter(&bs)
&& bs_lc != bs
&& Some(&bs_lc) != lc.as_ref()
{
push(rules, bs_lc);
}
}
#[cfg(windows)]
fn short_path_name(path: &str) -> Option<String> {
use std::ffi::{OsStr, OsString};
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW;
let wide: Vec<u16> = OsStr::new(path)
.encode_wide()
.chain(std::iter::once(0))
.collect();
let needed = unsafe { GetShortPathNameW(wide.as_ptr(), std::ptr::null_mut(), 0) };
if needed == 0 {
return None;
}
let mut buf = vec![0u16; needed as usize];
let written = unsafe { GetShortPathNameW(wide.as_ptr(), buf.as_mut_ptr(), needed) };
if written == 0 || written >= needed {
return None;
}
Some(
OsString::from_wide(&buf[..written as usize])
.to_string_lossy()
.into_owned(),
)
}
#[cfg(not(windows))]
fn short_path_name(_path: &str) -> Option<String> {
None
}
fn lowercase_drive_letter(s: &str) -> Option<String> {
let bytes = s.as_bytes();
if bytes.len() < 2 || bytes[1] != b':' || !bytes[0].is_ascii_uppercase() {
return None;
}
let mut out = s.to_string();
unsafe {
out.as_bytes_mut()[0] = bytes[0].to_ascii_lowercase();
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn parse_rustc_normalize_toggle_defaults_on_opts_out_explicitly() {
for on in [
None,
Some("1"),
Some("yes"),
Some("on"),
Some(""),
Some("garbage"),
] {
assert!(parse_rustc_normalize_toggle(on), "{on:?} should keep it on");
}
for off in [
Some("0"),
Some("false"),
Some("off"),
Some("no"),
Some(" OFF "),
] {
assert!(
!parse_rustc_normalize_toggle(off),
"{off:?} should disable it"
);
}
}
#[test]
fn empty_normalizer_is_identity() {
let n = PathNormalizer::empty();
assert_eq!(n.normalize("/anything/at/all"), "/anything/at/all");
assert_eq!(n.normalize(""), "");
}
#[test]
fn canonicalizes_workspace_prefix_and_replaces_with_sentinel() {
let dir = TempDir::new().unwrap();
let n = PathNormalizer::from_env(Some(dir.path()));
let canonical = dir.path().canonicalize().unwrap();
let input = format!("{}/src/main.rs", canonical.display());
assert!(
n.normalize(&input).contains("<WORKSPACE>"),
"got {} for input {input}",
n.normalize(&input)
);
}
#[test]
fn workspace_rule_strips_macos_private_tmp_symlink() {
if !cfg!(target_os = "macos") {
return;
}
let symlink_root = Path::new("/tmp");
let unique = format!("kache-pn-test-{}", std::process::id());
let real_dir = symlink_root.join(&unique);
let _ = fs::create_dir_all(&real_dir);
let n = PathNormalizer::from_env(Some(&real_dir));
let private_form = format!("/private/tmp/{unique}/target/release/build/foo/out");
let normalized = n.normalize(&private_form);
let _ = fs::remove_dir_all(&real_dir);
assert!(
normalized.starts_with("<WORKSPACE>"),
"expected <WORKSPACE> sentinel, got {normalized:?}"
);
}
#[test]
fn home_rule_normalizes_paths_inside_home() {
let n = PathNormalizer::from_env(None);
if let Some(home) = dirs::home_dir().and_then(|p| p.canonicalize().ok()) {
let home = home.display().to_string();
let home = home.strip_prefix(r"\\?\").unwrap_or(&home);
let input = format!("{home}/some/thing");
let out = n.normalize(&input);
assert!(
out.starts_with('<'),
"expected a sentinel prefix, got {out:?}"
);
}
}
#[test]
fn empty_prefix_does_not_corrupt_input() {
let n = PathNormalizer {
rules: vec![Rule {
prefix: String::new(),
key_sentinel: "<NEVER>".to_string(),
source_sentinel: "<NEVER>".to_string(),
flag_target: "<NEVER>".to_string(),
}],
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
assert_eq!(n.normalize("hello world"), "hello world");
}
#[test]
fn unmatched_input_passes_through_unchanged() {
let n = PathNormalizer::from_env(None);
let input = "this/path/does/not/match/anything/local";
assert_eq!(n.normalize(input), input);
}
#[test]
fn from_env_includes_rustup_home_rule_when_dir_exists() {
let rustup_dir = dirs::home_dir().map(|h| h.join(".rustup"));
let Some(dir) = rustup_dir else {
return;
};
if !dir.exists() {
return;
}
let n = PathNormalizer::from_env(None);
let canonical = dir.canonicalize().unwrap();
let input = format!("{}/toolchains/stable/bin/rustc", canonical.display());
let out = n.normalize(&input);
assert!(
out.contains("<RUSTUP_HOME>") || out.contains("<HOME>"),
"expected rustup or home sentinel in {out:?}"
);
}
#[test]
fn from_env_includes_tempdir_rule() {
let n = PathNormalizer::from_env(None);
let temp = std::env::temp_dir().canonicalize().unwrap();
let input = format!("{}/some-build-artifact", temp.display());
let out = n.normalize(&input);
assert!(
out.contains("<TMPDIR>") || out.contains("<HOME>") || out.starts_with('<'),
"expected a sentinel for tempdir-based path, got {out:?}"
);
}
#[test]
fn from_env_includes_cwd_rule_for_comp_dir() {
let cwd = std::env::current_dir().unwrap();
let canonical = canonical_string(&cwd).expect("cwd must canonicalize");
let n = PathNormalizer::from_env(None);
let normalized = n.normalize(format!("{canonical}/src/main.rs"));
assert!(
normalized.starts_with('<'),
"CWD path should normalize to a sentinel, got {normalized:?}"
);
let remaps = n.remap_args();
let expected = format!(
"--remap-path-prefix={canonical}={}",
workspace_flag_target()
);
assert!(
remaps.iter().any(|a| a == &expected),
"from_env must emit a CWD remap arg mapping to the resolvable \
workspace target; wanted {expected:?}, got {remaps:?}"
);
}
#[test]
fn remap_args_targets_are_absolute_and_carry_no_sentinel() {
let dir = TempDir::new().unwrap();
let n = PathNormalizer::from_env(Some(dir.path()));
let args = n.remap_args();
assert!(!args.is_empty());
for arg in &args {
let body = arg
.strip_prefix("--remap-path-prefix=")
.expect("rustc-recognized shape");
let (_, target) = body.rsplit_once('=').expect("PREFIX=TARGET shape");
assert!(
target.starts_with('/'),
"target must be absolute, got {target:?}"
);
assert!(
!target.contains('<') && !target.contains('>'),
"target must not be an angle-bracket sentinel, got {target:?}"
);
}
}
#[test]
fn remap_args_orders_cargo_home_and_rustup_after_workspace() {
let dir = TempDir::new().unwrap();
let n = PathNormalizer::from_env(Some(dir.path()))
.with_rust_src_rule(Some(Path::new("/nonexistent/sysroot")), Some("deadbeef"));
let args = n.remap_args();
let pos = |needle: &str| args.iter().position(|a| a.ends_with(needle));
let ws = pos(&format!("={}", workspace_flag_target()));
let home = pos("=/kache/home");
let cargo = pos("=/kache/.cargo");
if let (Some(h), Some(w)) = (home, ws) {
assert!(h < w, "HOME must precede WORKSPACE; got {args:#?}");
}
if let (Some(w), Some(c)) = (ws, cargo) {
assert!(w < c, "WORKSPACE must precede CARGO_HOME; got {args:#?}");
}
}
#[test]
fn flag_emit_rank_encodes_the_load_bearing_precedence() {
assert!(flag_emit_rank("<RUST_SRC>") > flag_emit_rank("<CARGO_HOME>"));
assert!(flag_emit_rank("<CARGO_HOME>") > flag_emit_rank("<WORKSPACE>"));
assert!(flag_emit_rank("<WORKSPACE>") > flag_emit_rank("<TARGET>"));
assert!(flag_emit_rank("<RUSTUP_HOME>") > flag_emit_rank("<HOME>"));
assert!(flag_emit_rank("<RUST_SRC>") > flag_emit_rank("<RUSTUP_HOME>"));
assert!(flag_emit_rank("<HOME>") > flag_emit_rank("<TMPDIR>"));
}
#[test]
fn workspace_flag_target_is_pinned_per_os() {
let t = workspace_flag_target();
assert!(t.starts_with('/'));
#[cfg(target_os = "linux")]
assert_eq!(t, "/proc/self/cwd");
#[cfg(not(target_os = "linux"))]
assert_eq!(t, "/kache/workspace");
}
#[test]
fn flag_target_for_maps_known_sentinels_to_absolute_resolvable_paths() {
assert_eq!(flag_target_for("<CARGO_HOME>"), "/kache/.cargo");
assert!(
flag_target_for("<CARGO_HOME>").ends_with("/.cargo"),
"CARGO_HOME target must keep the /.cargo tail for samply's crates.io matcher"
);
assert_eq!(flag_target_for("<WORKSPACE>"), workspace_flag_target());
assert_eq!(flag_target_for("<FUTURE_THING>"), "/kache/future_thing");
}
#[test]
fn remap_args_skips_empty_prefixes() {
let n = PathNormalizer {
rules: vec![Rule {
prefix: String::new(),
key_sentinel: "<NEVER>".to_string(),
source_sentinel: "<NEVER>".to_string(),
flag_target: "<NEVER>".to_string(),
}],
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
assert!(n.remap_args().is_empty());
}
fn rules_for(n: &PathNormalizer) -> Vec<(String, String)> {
n.rules
.iter()
.map(|r| (r.prefix.clone(), r.key_sentinel.clone()))
.collect()
}
#[test]
fn lowercase_drive_letter_only_touches_first_byte() {
assert_eq!(
lowercase_drive_letter("C:\\Users\\Alice"),
Some("c:\\Users\\Alice".to_string())
);
assert_eq!(
lowercase_drive_letter("D:/Projects/Foo"),
Some("d:/Projects/Foo".to_string())
);
}
#[test]
fn lowercase_drive_letter_returns_none_for_non_drive_paths() {
assert_eq!(lowercase_drive_letter("/unix/path"), None);
assert_eq!(lowercase_drive_letter("c:\\already\\lower"), None);
assert_eq!(lowercase_drive_letter("C"), None);
assert_eq!(lowercase_drive_letter(""), None);
assert_eq!(lowercase_drive_letter("CD"), None); assert_eq!(lowercase_drive_letter("1:\\foo"), None); }
#[test]
fn push_rule_with_variants_adds_only_canonical_form_on_unix() {
if cfg!(windows) {
return;
}
let mut rules = Vec::new();
push_rule_with_variants(
&mut rules,
Some("/Users/alice/.cargo".to_string()),
"<CARGO_HOME>",
);
assert_eq!(rules.len(), 1);
assert_eq!(rules[0].prefix, "/Users/alice/.cargo");
assert_eq!(rules[0].key_sentinel, "<CARGO_HOME>");
}
#[test]
fn push_rule_with_variants_expands_on_windows() {
if !cfg!(windows) {
return;
}
let mut rules = Vec::new();
push_rule_with_variants(
&mut rules,
Some("C:\\Users\\Alice\\.cargo".to_string()),
"<CARGO_HOME>",
);
let prefixes: Vec<&str> = rules.iter().map(|r| r.prefix.as_str()).collect();
assert!(prefixes.contains(&"C:\\Users\\Alice\\.cargo"));
assert!(prefixes.contains(&"C:/Users/Alice/.cargo"));
assert!(prefixes.contains(&"c:\\Users\\Alice\\.cargo"));
assert!(prefixes.contains(&"c:/Users/Alice/.cargo"));
assert!(rules.iter().all(|r| r.key_sentinel == "<CARGO_HOME>"));
}
#[cfg(windows)]
#[test]
fn push_rule_with_variants_adds_8dot3_short_name() {
let tmp = TempDir::new().unwrap();
let long_dir = tmp.path().join("Long Program Dir");
fs::create_dir(&long_dir).unwrap();
let canonical = canonical_string(&long_dir).expect("canonicalize long dir");
let Some(short) = short_path_name(&canonical) else {
return;
};
if short == canonical {
return;
}
let mut rules = Vec::new();
push_rule_with_variants(&mut rules, Some(canonical), "<BASE_DIR>");
let n = PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
let input = format!("{short}\\src\\main.rs");
let out = n.normalize(&input);
assert!(
out.contains("<BASE_DIR>"),
"8.3 short-name input {input:?} should normalize via the short variant, got {out:?}"
);
assert_eq!(
n.source_path_identity(Path::new(&input)).unwrap(),
b"<BASE_DIR>/src\\main.rs"
);
assert!(
n.depinfo_source_roots()
.iter()
.any(|root| root.root.as_path() == Path::new(short.as_str())),
"the same short spelling must be accepted while relativizing dep-info"
);
}
#[test]
fn push_rule_with_variants_skips_empty_and_none() {
let mut rules = Vec::new();
push_rule_with_variants(&mut rules, None, "<NEVER>");
push_rule_with_variants(&mut rules, Some(String::new()), "<NEVER>");
assert!(rules.is_empty());
}
#[test]
fn normalize_matches_any_variant_form() {
let mut rules = Vec::new();
push_rule_with_variants(
&mut rules,
Some("C:\\Users\\Alice\\.cargo".to_string()),
"<CARGO_HOME>",
);
let n = PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
let inputs_and_expectations: &[(&str, bool)] = &[
("C:\\Users\\Alice\\.cargo\\registry\\foo", true), ("C:/Users/Alice/.cargo/registry/foo", cfg!(windows)),
("c:\\Users\\Alice\\.cargo\\registry\\foo", cfg!(windows)),
("c:/Users/Alice/.cargo/registry/foo", cfg!(windows)),
("/Users/alice/.cargo/registry/foo", false), ];
for (input, should_match) in inputs_and_expectations {
let out = n.normalize(input);
let matched = out.contains("<CARGO_HOME>");
assert_eq!(
matched, *should_match,
"input {input:?}: expected match={should_match}, got out={out:?}"
);
}
}
#[test]
fn normalize_does_not_transform_unmatched_input() {
let n = PathNormalizer::empty();
let weird_inputs = &[
"C:\\Users\\foo",
"/unix/with\\backslash/mixed",
r"\\?\C:\extended\length",
"\\//\\/", "no path here at all",
];
for input in weird_inputs {
assert_eq!(
n.normalize(input),
*input,
"unmatched input {input:?} must pass through unchanged"
);
}
}
#[test]
fn rules_dedup_keeps_first_for_identical_canonicalization() {
let mut rules = Vec::new();
push_rule_with_variants(&mut rules, Some("/same/path".to_string()), "<FIRST>");
push_rule_with_variants(&mut rules, Some("/same/path".to_string()), "<SECOND>");
let mut seen = std::collections::HashSet::new();
rules.retain(|r| seen.insert(r.prefix.clone()));
let names: Vec<_> = rules_for(&PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
})
.into_iter()
.collect();
assert!(!names.is_empty());
assert!(names.iter().all(|(_, sentinel)| *sentinel == "<FIRST>"));
assert!(names.iter().any(|(prefix, _)| prefix == "/same/path"));
let unique: std::collections::HashSet<_> = names.iter().map(|(p, _)| p).collect();
assert_eq!(unique.len(), names.len());
}
#[test]
fn leak_detector_does_not_panic_on_unsentineled_paths() {
let n = PathNormalizer::empty();
let _ = n.normalize("/Users/alice/leaked/path");
let _ = n.normalize("/home/bob/leaked/path");
let _ = n.normalize("C:\\Users\\charlie\\leaked");
}
fn pn_with_rules(rules: Vec<(&str, &'static str)>) -> PathNormalizer {
PathNormalizer {
rules: rules
.into_iter()
.map(|(p, s)| Rule {
prefix: p.to_string(),
key_sentinel: s.to_string(),
source_sentinel: s.to_string(),
flag_target: flag_target_for(s),
})
.collect(),
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
}
}
fn apply_last_match(args: &[String], path: &str) -> String {
let mut winner: Option<(String, String)> = None;
for a in args {
let body = a.strip_prefix("--remap-path-prefix=").unwrap();
let (from, to) = body.rsplit_once('=').unwrap();
if path.starts_with(from) {
winner = Some((from.to_string(), to.to_string()));
}
}
match winner {
Some((from, to)) => format!("{to}{}", &path[from.len()..]),
None => path.to_string(),
}
}
#[test]
fn registry_dep_comp_dir_maps_under_cargo_home_via_last_match() {
let cargo_home = "/home/u/.cargo";
let dep_dir = "/home/u/.cargo/registry/src/index.crates.io-abc/itoa-1.0.18";
let n = pn_with_rules(vec![
(dep_dir, "<WORKSPACE>"), (cargo_home, "<CARGO_HOME>"),
]);
assert_eq!(
apply_last_match(&n.remap_args(), dep_dir),
"/kache/.cargo/registry/src/index.crates.io-abc/itoa-1.0.18",
"a registry dep's comp_dir must resolve under /kache/.cargo"
);
let member = "/repo/crates/foo";
let n2 = pn_with_rules(vec![(member, "<WORKSPACE>"), (cargo_home, "<CARGO_HOME>")]);
assert_eq!(
apply_last_match(&n2.remap_args(), member),
workspace_flag_target(),
"a workspace member must keep the workspace target"
);
}
#[test]
fn workspace_member_maps_via_cwd_not_repo_root() {
let member = "/repo/crates/foo";
let repo = "/repo";
let n = pn_with_rules(vec![(member, "<WORKSPACE>"), (repo, "<WORKSPACE>")]);
assert_eq!(
apply_last_match(&n.remap_args(), &format!("{member}/src/lib.rs")),
format!("{}/src/lib.rs", workspace_flag_target()),
"a workspace member's own source must map via the cwd prefix, not the repo root"
);
}
#[test]
fn with_rust_src_rule_revirtualizes_std_when_installed() {
let tmp = TempDir::new().unwrap();
let sysroot = tmp.path();
let rust_src = sysroot.join("lib").join("rustlib").join("src").join("rust");
std::fs::create_dir_all(&rust_src).unwrap();
let n = PathNormalizer::from_env(None).with_rust_src_rule(Some(sysroot), Some("abc123"));
let args = n.remap_args();
let prefix = canonical_string(&rust_src).expect("rust-src canonicalizes");
assert_eq!(
apply_last_match(&args, &format!("{prefix}/library/core/src/ptr/mod.rs")),
"/rustc/abc123/library/core/src/ptr/mod.rs"
);
}
#[test]
fn configured_root_wins_over_rust_src_on_key_and_remap_sides() {
let tmp = TempDir::new().unwrap();
let sysroot = tmp.path().join("sysroot");
let rust_src = sysroot.join("lib").join("rustlib").join("src").join("rust");
std::fs::create_dir_all(&rust_src).unwrap();
let base = canonical_string(tmp.path()).unwrap();
let source = format!("{base}/sysroot/lib/rustlib/src/rust/library/core/src/lib.rs");
let normalizer = PathNormalizer::empty()
.with_base_dirs(std::slice::from_ref(&base))
.with_rust_src_rule(Some(&sysroot), Some("abc123"));
assert_eq!(
normalizer.normalize(&source),
"<BASE_DIR_0>/sysroot/lib/rustlib/src/rust/library/core/src/lib.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), &source),
"/kache/base-dir-0/sysroot/lib/rustlib/src/rust/library/core/src/lib.rs"
);
}
#[test]
fn with_rust_src_rule_is_noop_without_sysroot_hash_or_installed_src() {
let base = PathNormalizer::from_env(None).remap_args().len();
assert_eq!(
PathNormalizer::from_env(None)
.with_rust_src_rule(None, Some("abc"))
.remap_args()
.len(),
base
);
assert_eq!(
PathNormalizer::from_env(None)
.with_rust_src_rule(Some(Path::new("/x")), None)
.remap_args()
.len(),
base
);
assert_eq!(
PathNormalizer::from_env(None)
.with_rust_src_rule(
Some(Path::new("/definitely/nonexistent/sysroot")),
Some("abc")
)
.remap_args()
.len(),
base
);
}
#[test]
fn normalize_replaces_all_occurrences_of_same_prefix() {
let n = pn_with_rules(vec![("/ws", "<W>")]);
let input = "-L /ws/lib -L /ws/build/deps -L /ws/extra";
let out = n.normalize(input);
assert_eq!(out, "-L <W>/lib -L <W>/build/deps -L <W>/extra");
}
#[test]
fn normalize_handles_input_equal_to_prefix() {
let n = pn_with_rules(vec![("/ws", "<W>")]);
assert_eq!(n.normalize("/ws"), "<W>");
}
#[test]
fn normalize_chains_multiple_distinct_prefixes_in_one_input() {
let n = pn_with_rules(vec![("/ws", "<W>"), ("/home/u/.cargo", "<C>")]);
let input = "-L /ws/lib -L /home/u/.cargo/registry/src/foo";
let out = n.normalize(input);
assert_eq!(out, "-L <W>/lib -L <C>/registry/src/foo");
}
#[test]
fn most_specific_prefix_wins_when_nested() {
let n = pn_with_rules(vec![("/home/u/projects/ws", "<W>"), ("/home/u", "<H>")]);
let input = "/home/u/projects/ws/src/lib.rs";
let out = n.normalize(input);
assert_eq!(out, "<W>/src/lib.rs");
let sibling = "/home/u/other/foo.rs";
assert_eq!(n.normalize(sibling), "<H>/other/foo.rs");
}
#[test]
fn normalize_is_idempotent_on_already_sentinelized_input() {
let n = pn_with_rules(vec![("/home/u", "<HOME>"), ("/workspace", "<WORKSPACE>")]);
let input = "/home/u/projects/foo /workspace/src/main.rs";
let once = n.normalize(input);
let twice = n.normalize(&once);
assert_eq!(once, twice, "normalize is not idempotent");
assert!(once.contains("<HOME>"));
assert!(once.contains("<WORKSPACE>"));
}
#[test]
fn normalize_substring_match_is_documented_limitation() {
let n = pn_with_rules(vec![("/home/u", "<H>")]);
assert_eq!(n.normalize("/home/usr/foo"), "<H>sr/foo");
assert_eq!(n.normalize("/home/u/foo"), "<H>/foo");
}
#[test]
fn normalize_handles_realistic_out_dir_value() {
let n = pn_with_rules(vec![("/Users/alice/projects/myrepo", "<WORKSPACE>")]);
let out_dir =
"/Users/alice/projects/myrepo/target/release/build/serde-65d43fa14511931c/out";
assert_eq!(
n.normalize(out_dir),
"<WORKSPACE>/target/release/build/serde-65d43fa14511931c/out"
);
}
#[test]
fn normalize_handles_realistic_rustflags_value() {
let n = pn_with_rules(vec![
("/Users/alice/.cargo", "<CARGO_HOME>"),
("/Users/alice/projects/myrepo", "<WORKSPACE>"),
]);
let flags = "-L /Users/alice/.cargo/registry/cache/foo \
-L /Users/alice/projects/myrepo/target/release/deps \
-C link-arg=-Wl,-rpath,/system/lib";
let out = n.normalize(flags);
assert!(out.contains("<CARGO_HOME>/registry/cache/foo"));
assert!(out.contains("<WORKSPACE>/target/release/deps"));
assert!(out.contains("/system/lib"));
}
#[test]
fn lowercase_drive_letter_handles_drive_root_alone() {
assert_eq!(lowercase_drive_letter("C:\\"), Some("c:\\".to_string()));
assert_eq!(lowercase_drive_letter("C:"), Some("c:".to_string()));
assert_eq!(lowercase_drive_letter("D:/"), Some("d:/".to_string()));
}
#[test]
fn windows_variants_are_distinct_for_distinct_canonical_forms() {
if !cfg!(windows) {
return;
}
let mut rules = Vec::new();
push_rule_with_variants(
&mut rules,
Some("c:/users/alice/.cargo".to_string()),
"<CARGO_HOME>",
);
let prefixes: Vec<&str> = rules.iter().map(|r| r.prefix.as_str()).collect();
assert!(prefixes.contains(&"c:/users/alice/.cargo"));
assert!(prefixes.contains(&"c:\\users\\alice\\.cargo"));
assert!(!prefixes.iter().any(|p| p.starts_with("C:")));
}
#[test]
fn remap_args_emits_all_windows_variants_with_same_target() {
if !cfg!(windows) {
return;
}
let mut rules = Vec::new();
push_rule_with_variants(
&mut rules,
Some("C:\\Users\\Alice\\.cargo".to_string()),
"<CARGO_HOME>",
);
let n = PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
let args = n.remap_args();
assert_eq!(args.len(), 4);
let target = flag_target_for("<CARGO_HOME>");
for arg in &args {
assert!(
arg.ends_with(&format!("={target}")),
"every variant maps to the same target {target:?}; got {arg:?}"
);
}
}
#[test]
fn lowercase_drive_letter_does_not_panic_on_multibyte_first_char() {
assert_eq!(lowercase_drive_letter("É:foo"), None);
assert_eq!(lowercase_drive_letter("日本:"), None);
assert_eq!(lowercase_drive_letter("é"), None); assert_eq!(lowercase_drive_letter("C:foo"), Some("c:foo".to_string()));
}
#[test]
fn normalize_handles_unicode_paths_correctly() {
let n = pn_with_rules(vec![("/Users/José/.cargo", "<CARGO_HOME>")]);
let input = "/Users/José/.cargo/registry/src/foo";
assert_eq!(n.normalize(input), "<CARGO_HOME>/registry/src/foo");
}
#[test]
fn normalize_matches_across_nfc_nfd_unicode_normalization_forms() {
let nfc_prefix = "/Users/Jos\u{00E9}/.cargo"; let nfd_input = "/Users/Jos\u{0065}\u{0301}/.cargo/registry/foo";
assert_ne!(
nfc_prefix.as_bytes(),
&nfd_input.as_bytes()[..nfc_prefix.len()]
);
let n = pn_with_rules(vec![(nfc_prefix, "<CARGO_HOME>")]);
let out = n.normalize(nfd_input);
assert_eq!(out, "<CARGO_HOME>/registry/foo");
let nfc_input = "/Users/Jos\u{00E9}/.cargo/registry/bar";
assert_eq!(n.normalize(nfc_input), "<CARGO_HOME>/registry/bar");
}
#[test]
fn strip_verbatim_prefix_removes_extended_length_marker() {
assert_eq!(strip_verbatim_prefix(r"\\?\C:\proj\out"), r"C:\proj\out");
assert_eq!(
strip_verbatim_prefix(r"\\?\UNC\server\share\x"),
r"\\server\share\x"
);
assert_eq!(strip_verbatim_prefix(r"C:\proj\out"), r"C:\proj\out");
assert_eq!(strip_verbatim_prefix("/home/u/proj"), "/home/u/proj");
}
#[test]
fn filesystem_root_prefix_distinguishes_unc_share_roots() {
for root in [
"//server/share",
"//server/share/",
r"\\server\share",
r"\\server\share\",
r"\\?\UNC\server\share",
] {
assert!(is_filesystem_root_prefix(root), "{root:?}");
}
for narrower_or_incomplete in [
"//server",
"//server/",
"///share",
"//server/share/app",
r"\\server\share\app",
] {
assert!(
!is_filesystem_root_prefix(narrower_or_incomplete),
"{narrower_or_incomplete:?}"
);
}
}
#[test]
fn os_path_spellings_keep_raw_and_plain_verbatim_windows_forms() {
assert_eq!(
os_path_spellings(Path::new(r"\\?\C:\Work\Root")),
vec![r"\\?\C:\Work\Root".to_string(), r"C:\Work\Root".to_string()]
);
assert_eq!(
os_path_spellings(Path::new(r"\\?\UNC\server\share\Root")),
vec![
r"\\?\UNC\server\share\Root".to_string(),
r"\\server\share\Root".to_string()
]
);
}
#[test]
fn canonical_string_normalizes_to_nfc() {
let dir = TempDir::new().unwrap();
let nfc_name = "Jos\u{00E9}";
let subdir = dir.path().join(nfc_name);
std::fs::create_dir(&subdir).unwrap();
let result = canonical_string(&subdir).expect("canonicalize should succeed");
let renormalized: String = result.nfc().collect();
assert_eq!(
result, renormalized,
"canonical_string output must be in NFC form, got {result:?}"
);
}
#[test]
fn normalize_preserves_unicode_outside_matched_prefix() {
let n = pn_with_rules(vec![("/ws", "<W>")]);
let input = "/ws/José/䏿–‡/файл.rs"; let out = n.normalize(input);
assert_eq!(out, "<W>/José/䏿–‡/файл.rs");
}
#[test]
fn from_env_construction_is_deterministic() {
let dir = TempDir::new().unwrap();
let n1 = PathNormalizer::from_env(Some(dir.path()));
let n2 = PathNormalizer::from_env(Some(dir.path()));
let p1: Vec<(String, String)> = n1
.rules
.iter()
.map(|r| (r.prefix.clone(), r.key_sentinel.clone()))
.collect();
let p2: Vec<(String, String)> = n2
.rules
.iter()
.map(|r| (r.prefix.clone(), r.key_sentinel.clone()))
.collect();
assert_eq!(p1, p2);
}
#[test]
fn configured_base_dirs_are_order_independent_and_longest_match_wins() {
let dir = TempDir::new().unwrap();
let parent = dir.path().join("container");
let child = parent.join("work");
std::fs::create_dir_all(&child).unwrap();
let parent_cfg = parent.to_string_lossy().into_owned();
let child_cfg = child.to_string_lossy().into_owned();
let forward =
PathNormalizer::empty().with_base_dirs(&[parent_cfg.clone(), child_cfg.clone()]);
let reverse = PathNormalizer::empty().with_base_dirs(&[child_cfg, parent_cfg]);
let input = format!(
"{}/src/lib.rs",
canonical_string(&child).expect("child canonicalizes")
);
assert_eq!(forward.normalize(&input), reverse.normalize(&input));
assert_eq!(forward.normalize(&input), "<BASE_DIR_1>/src/lib.rs");
assert_eq!(forward.configured_base_dir_count(), 2);
}
#[test]
fn configured_base_dir_requires_path_component_boundaries() {
let dir = TempDir::new().unwrap();
let root = dir.path().join("work");
std::fs::create_dir_all(&root).unwrap();
let root = root.to_string_lossy().into_owned();
let normalizer = PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&root));
assert_eq!(
normalizer.normalize(format!("{root}/src {root}space/src /opt{root}/src")),
format!("<BASE_DIR_0>/src {root}space/src /opt{root}/src")
);
let backslash_path = format!(r"{root}\src");
let expected_backslash_path = if cfg!(windows) {
r"<BASE_DIR_0>\src".to_string()
} else {
backslash_path.clone()
};
assert_eq!(
normalizer.normalize(&backslash_path),
expected_backslash_path
);
assert_eq!(
normalizer.normalize(format!("-L{root}/lib key={root}/src")),
"-L<BASE_DIR_0>/lib key=<BASE_DIR_0>/src"
);
assert_eq!(
normalizer.normalize(format!("-L\u{1f}{root}/lib")),
"-L\u{1f}<BASE_DIR_0>/lib"
);
}
#[test]
fn configured_windows_root_has_slash_and_drive_case_variants() {
let normalizer = PathNormalizer::empty().with_base_dirs(&["C:/Build/Root".to_string()]);
assert_eq!(
normalizer.normalize(r"c:\Build\Root\src\main.c"),
r"<BASE_DIR_0>\src\main.c"
);
}
#[test]
fn configured_posix_root_does_not_gain_windows_aliases() {
let normalizer = PathNormalizer::empty().with_base_dirs(&["/snap".to_string()]);
assert!(normalizer.rules.iter().all(|rule| rule.prefix != r"\snap"));
assert_eq!(normalizer.normalize("/snap/pkg"), "<BASE_DIR_0>/pkg");
assert_eq!(normalizer.normalize("C:/snap/pkg"), "C:/snap/pkg");
assert_eq!(normalizer.normalize(r"C:\snap\pkg"), r"C:\snap\pkg");
}
#[test]
fn configured_path_canonicalization_is_native_syntax_only() {
assert!(is_native_configured_path_for("/work", false));
assert!(!is_native_configured_path_for("C:/work", false));
assert!(!is_native_configured_path_for("//server/share", false));
assert!(!is_native_configured_path_for("/work", true));
assert!(is_native_configured_path_for("C:/work", true));
assert!(is_native_configured_path_for(r"\\server\share", true));
}
#[cfg(windows)]
#[test]
fn configured_plain_root_owns_verbatim_canonical_sources_and_depinfo() {
let producer = TempDir::new().unwrap();
let consumer = TempDir::new().unwrap();
let producer_root = producer.path().join("Configured Root");
let consumer_root = consumer.path().join("Configured Root");
std::fs::create_dir_all(producer_root.join("src")).unwrap();
std::fs::create_dir_all(consumer_root.join("src")).unwrap();
std::fs::write(producer_root.join("src/value.rs"), "pub const V: u8 = 1;").unwrap();
let producer_cfg = producer_root.to_string_lossy().into_owned();
let consumer_cfg = consumer_root.to_string_lossy().into_owned();
let producer_normalizer =
PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&producer_cfg));
let consumer_normalizer =
PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&consumer_cfg));
let canonical_source = producer_root.join("src/value.rs").canonicalize().unwrap();
assert_eq!(
producer_normalizer
.source_path_identity(&canonical_source)
.unwrap(),
b"<BASE_DIR_0>/src\\value.rs"
);
assert_eq!(
apply_last_match(
&producer_normalizer.remap_args(),
canonical_source.to_str().unwrap()
),
"/kache/base-dir-0\\src\\value.rs"
);
let producer_roots = producer_normalizer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let consumer_roots = consumer_normalizer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let raw = format!(r"C:\target\demo.d: {}", canonical_source.display());
let stored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&raw,
Path::new(r"C:\target"),
Path::new(r"C:\work"),
None,
&producer_roots,
crate::link::DepInfoMode::Relativize,
);
assert!(
stored.contains("__kache_base_dir_0__/src\\value.rs"),
"{stored}"
);
let restored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&stored,
Path::new(r"C:\target"),
Path::new(r"C:\work"),
None,
&consumer_roots,
crate::link::DepInfoMode::Expand,
);
let expected = format!(r"{}\src\value.rs", consumer_root.display()).replace(' ', "\\ ");
assert!(restored.contains(&expected), "{restored}");
assert!(!restored.contains(&producer_root.to_string_lossy().into_owned()));
}
#[test]
fn configured_root_wins_consistently_in_key_and_rustc_remap() {
let cargo_home = "/sandbox/.cargo";
let path = "/sandbox/.cargo/registry/src/pkg/lib.rs";
let normalizer = pn_with_rules(vec![(cargo_home, "<CARGO_HOME>")])
.with_base_dirs(&["/sandbox".to_string()]);
assert_eq!(
normalizer.normalize(path),
"<BASE_DIR_0>/.cargo/registry/src/pkg/lib.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), path),
"/kache/base-dir-0/.cargo/registry/src/pkg/lib.rs"
);
assert_eq!(
normalizer.source_path_identity(Path::new(path)).unwrap(),
b"<BASE_DIR_0>/.cargo/registry/src/pkg/lib.rs"
);
}
#[test]
fn legacy_base_dir_is_a_complete_owner_and_rejects_empty_or_root() {
let dir = TempDir::new().unwrap();
let base = dir.path().join("legacy");
let source = base.join("src/lib.rs");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, "pub const VALUE: u8 = 1;").unwrap();
let mut rules = Vec::new();
let mut source_restore_roots = Vec::new();
push_legacy_base_dir_rules(
&mut rules,
&mut source_restore_roots,
Some(base.as_os_str()),
);
let normalizer = PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots,
path_only_env_vars: Vec::new(),
};
let relative = source.strip_prefix(&base).unwrap().to_string_lossy();
assert_eq!(
normalizer.source_path_identity(&source).unwrap(),
format!("<BASE_DIR>/{relative}").as_bytes()
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), source.to_str().unwrap()),
format!(
"{}{}",
flag_target_for("<BASE_DIR>"),
source
.to_str()
.unwrap()
.strip_prefix(base.to_str().unwrap())
.unwrap()
)
);
assert!(normalizer.depinfo_source_roots().iter().any(|root| {
root.root == base
&& root.key_sentinel == "<BASE_DIR>"
&& root.depinfo_sentinel == "__kache_base_dir__/"
}));
let filesystem_root = if cfg!(windows) {
Path::new(r"C:\")
} else {
Path::new("/")
};
for invalid in [std::ffi::OsStr::new(""), filesystem_root.as_os_str()] {
let mut rules = Vec::new();
let mut restore_roots = Vec::new();
push_legacy_base_dir_rules(&mut rules, &mut restore_roots, Some(invalid));
assert!(rules.is_empty());
assert!(restore_roots.is_empty());
}
}
#[cfg(windows)]
#[test]
fn legacy_base_dir_owns_verbatim_canonical_sources() {
let dir = TempDir::new().unwrap();
let base = dir.path().join("Legacy Root");
let source = base.join("src/value.rs");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, "pub const VALUE: u8 = 1;").unwrap();
let mut rules = Vec::new();
let mut source_restore_roots = Vec::new();
push_legacy_base_dir_rules(
&mut rules,
&mut source_restore_roots,
Some(base.as_os_str()),
);
let normalizer = PathNormalizer {
rules,
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots,
path_only_env_vars: Vec::new(),
};
let canonical_source = source.canonicalize().unwrap();
assert_eq!(
normalizer.source_path_identity(&canonical_source).unwrap(),
b"<BASE_DIR>/src\\value.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), canonical_source.to_str().unwrap()),
"/kache/base-dir\\src\\value.rs"
);
let canonical_base = base.canonicalize().unwrap();
assert!(normalizer.depinfo_source_roots().iter().any(|root| {
root.root == canonical_base && root.depinfo_sentinel == "__kache_base_dir__/"
}));
}
#[test]
fn legacy_base_dir_exposes_a_depinfo_restore_descriptor() {
let dir = TempDir::new().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let normalizer = pn_with_rules(vec![(&root, "<BASE_DIR>")]);
let roots = normalizer.depinfo_source_roots();
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].root, dir.path());
assert_eq!(roots[0].key_sentinel, "<BASE_DIR>");
assert_eq!(roots[0].depinfo_sentinel, "__kache_base_dir__/");
assert_eq!(roots[0].priority, flag_emit_rank("<BASE_DIR>"));
}
#[test]
fn external_target_exposes_a_distinct_source_restore_descriptor() {
let dir = TempDir::new().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let normalizer = pn_with_rules(vec![(&root, "<TARGET>")]);
let roots = normalizer.depinfo_source_roots();
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].root, dir.path());
assert_eq!(roots[0].key_sentinel, "<TARGET>");
assert_eq!(roots[0].depinfo_sentinel, "__kache_target_rule__/");
assert_eq!(roots[0].priority, flag_emit_rank("<TARGET>"));
}
#[test]
fn every_source_sentinel_has_a_distinct_depinfo_sentinel() {
let mapped = [
("<CWD>", "__kache_cwd__/"),
("<WORKSPACE>", "__kache_workspace__/"),
("<TARGET>", "__kache_target_rule__/"),
("<CARGO_HOME>", "__kache_cargo_home__/"),
("<BASE_DIR>", "__kache_base_dir__/"),
("<RUSTUP_HOME>", "__kache_rustup_home__/"),
("<HOME>", "__kache_home__/"),
("<APPDATA>", "__kache_appdata__/"),
("<LOCALAPPDATA>", "__kache_localappdata__/"),
("<PROGRAMFILES>", "__kache_programfiles__/"),
("<TMPDIR>", "__kache_tmpdir__/"),
];
for (source_sentinel, expected) in mapped {
assert_eq!(
depinfo_sentinel_for_source(source_sentinel).as_deref(),
Some(expected),
"{source_sentinel} must map to a portable dep-info sentinel"
);
}
let distinct: std::collections::HashSet<&str> =
mapped.iter().map(|(_, sentinel)| *sentinel).collect();
assert_eq!(
distinct.len(),
mapped.len(),
"two source roots sharing a dep-info sentinel would expand to the wrong tree"
);
assert_eq!(depinfo_sentinel_for_source("<RUST_SRC>"), None);
assert_eq!(depinfo_sentinel_for_source("<NOT_A_SENTINEL>"), None);
assert_eq!(
depinfo_sentinel_for_source("<BASE_DIR_2>").as_deref(),
Some("__kache_base_dir_2__/")
);
}
#[test]
fn a_second_target_root_lands_between_its_higher_and_lower_ranked_peers() {
let dir = TempDir::new().unwrap();
let base = dir.path().canonicalize().unwrap();
let outer_target = base.join("target");
let nested = outer_target.join("wasm32-unknown-unknown");
fs::create_dir_all(&nested).unwrap();
let workspace_prefix = base.join("workspace").to_string_lossy().into_owned();
let outer_prefix = outer_target.to_string_lossy().into_owned();
let tmpdir_prefix = base.join("tmp").to_string_lossy().into_owned();
let normalizer = pn_with_rules(vec![
(&workspace_prefix, "<WORKSPACE>"),
(&outer_prefix, "<TARGET>"),
(&tmpdir_prefix, "<TMPDIR>"),
])
.with_target_dir(Some(&nested));
let spellings = || {
normalizer
.rules
.iter()
.map(|rule| rule.prefix.clone())
.collect::<Vec<_>>()
};
let at = |prefix: &str| {
normalizer
.rules
.iter()
.position(|rule| rule.prefix == prefix)
.unwrap_or_else(|| panic!("{prefix} survives, got {:?}", spellings()))
};
let nested_at = normalizer
.rules
.iter()
.position(|rule| {
rule.prefix.starts_with(&outer_prefix) && rule.prefix.len() > outer_prefix.len()
})
.expect("the nested target rule is added");
assert!(
at(&workspace_prefix) < nested_at,
"a higher-ranked root must stay ahead of a new <TARGET>, got {:?}",
spellings()
);
assert!(
at(&outer_prefix) < nested_at,
"the established <TARGET> peer must keep normalizing first, got {:?}",
spellings()
);
assert!(
nested_at < at(&tmpdir_prefix),
"a lower-ranked root must not consume the target first, got {:?}",
spellings()
);
let generated = nested.join("generated.rs").to_string_lossy().into_owned();
let suffix = generated
.strip_prefix(&outer_prefix)
.expect("the nested target is under the outer target");
assert_eq!(
normalizer.normalize(&generated),
format!("<TARGET>{suffix}"),
"the outer target owns the prefix, so the nested segment stays in the key"
);
}
#[test]
fn depinfo_restore_root_prefers_the_designated_spelling_over_the_first_alias() {
let dir = TempDir::new().unwrap();
let base = dir.path().canonicalize().unwrap();
let alias = base.join("alias-target");
let designated = base.join("designated-target");
let workspace = base.join("workspace");
for path in [&alias, &designated, &workspace] {
fs::create_dir(path).unwrap();
}
let rule = |path: &Path, sentinel: &str| Rule {
prefix: path.to_string_lossy().into_owned(),
key_sentinel: sentinel.to_string(),
source_sentinel: sentinel.to_string(),
flag_target: flag_target_for(sentinel),
};
let normalizer = PathNormalizer {
rules: vec![
rule(&alias, "<TARGET>"),
rule(&designated, "<TARGET>"),
rule(&workspace, "<WORKSPACE>"),
],
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: vec![
("<WORKSPACE>".to_string(), workspace.clone()),
("<TARGET>".to_string(), designated.clone()),
],
path_only_env_vars: Vec::new(),
};
let target_root = normalizer
.depinfo_source_roots()
.into_iter()
.find(|root| root.key_sentinel == "<TARGET>")
.expect("the target sentinel has an existing native root");
assert_eq!(
target_root.root, designated,
"the designated <TARGET> spelling must win over the earlier alias rule"
);
}
#[test]
fn configured_slot_does_not_duplicate_an_identical_legacy_owner() {
let dir = TempDir::new().unwrap();
let root = dir.path().to_string_lossy().into_owned();
let normalizer =
pn_with_rules(vec![(&root, "<BASE_DIR>")]).with_base_dirs(std::slice::from_ref(&root));
let roots = normalizer.depinfo_source_roots();
assert_eq!(normalizer.configured_base_dir_count(), 1);
assert_eq!(roots.len(), 1);
assert_eq!(roots[0].key_sentinel, "<BASE_DIR>");
assert_eq!(roots[0].depinfo_sentinel, "__kache_base_dir__/");
assert!(
!roots
.iter()
.any(|descriptor| descriptor.key_sentinel == "<BASE_DIR_0>")
);
}
#[test]
fn source_identity_matches_rustc_semantic_root_precedence() {
let normalizer = pn_with_rules(vec![
("/workspace/target-a", "<TARGET>"),
("/workspace", "<WORKSPACE>"),
]);
let source = Path::new("/workspace/target-a/generated.rs");
assert_eq!(
normalizer.source_path_identity(source).unwrap(),
b"<WORKSPACE>/target-a/generated.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), source.to_str().unwrap()),
format!("{}/target-a/generated.rs", workspace_flag_target())
);
}
#[test]
fn source_identity_distinguishes_member_cwd_from_workspace_root() {
let normalizer = PathNormalizer {
rules: vec![
Rule {
prefix: "/repo/member".to_string(),
key_sentinel: "<WORKSPACE>".to_string(),
source_sentinel: "<CWD>".to_string(),
flag_target: workspace_flag_target().to_string(),
},
Rule {
prefix: "/repo".to_string(),
key_sentinel: "<WORKSPACE>".to_string(),
source_sentinel: "<WORKSPACE>".to_string(),
flag_target: workspace_flag_target().to_string(),
},
],
configured_base_dir_count: 0,
configured_source_root_groups: Vec::new(),
source_restore_roots: Vec::new(),
path_only_env_vars: Vec::new(),
};
assert_eq!(
normalizer
.source_path_identity(Path::new("/repo/member/value.rs"))
.unwrap(),
b"<CWD>/value.rs"
);
assert_eq!(
normalizer
.source_path_identity(Path::new("/repo/value.rs"))
.unwrap(),
b"<WORKSPACE>/value.rs"
);
}
#[cfg(unix)]
#[test]
fn automatic_workspace_root_round_trips_lexical_and_canonical_spellings() {
let dir = TempDir::new().unwrap();
let real = dir.path().join("real/workspace");
let link = dir.path().join("workspace-link");
std::fs::create_dir_all(real.join("src")).unwrap();
std::os::unix::fs::symlink(&real, &link).unwrap();
let normalizer = PathNormalizer::from_env(Some(&link));
let lexical = link.join("src/lib.rs");
let canonical = real.canonicalize().unwrap().join("src/lib.rs");
for source in [&lexical, &canonical] {
assert_eq!(
normalizer.source_path_identity(source).unwrap(),
b"<WORKSPACE>/src/lib.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), source.to_str().unwrap()),
format!("{}/src/lib.rs", workspace_flag_target())
);
}
let roots = normalizer.depinfo_source_roots();
let workspace_roots = roots
.iter()
.filter(|root| root.depinfo_sentinel == "__kache_workspace__/")
.map(|root| root.root.as_path())
.collect::<Vec<_>>();
assert_eq!(workspace_roots.first().copied(), Some(link.as_path()));
assert!(workspace_roots.contains(&real.canonicalize().unwrap().as_path()));
let depinfo_roots = roots
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let target = dir.path().join("target");
let raw = format!("{}/demo.d: {}\n", target.display(), canonical.display());
let stored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&raw,
&target,
&link,
Some(&link),
&depinfo_roots,
crate::link::DepInfoMode::Relativize,
);
assert!(
stored.contains("__kache_workspace__/src/lib.rs"),
"{stored}"
);
let restored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&stored,
&target,
&link,
Some(&link),
&depinfo_roots,
crate::link::DepInfoMode::Expand,
);
assert!(restored.contains(&format!("{}/src/lib.rs", link.display())));
assert!(!restored.contains(&format!("{}/src/lib.rs", real.display())));
}
#[cfg(unix)]
#[test]
fn configured_ancestor_keeps_a_winning_workspace_canonical_alias() {
let dir = TempDir::new().unwrap();
let producer_configured = dir.path().join("producer/configured");
let consumer_configured = dir.path().join("consumer/configured");
let producer_link = producer_configured.join("workspace");
let consumer_link = consumer_configured.join("workspace");
let producer_real = dir.path().join("producer-real/workspace");
let consumer_real = dir.path().join("consumer-real/workspace");
std::fs::create_dir_all(producer_real.join("src")).unwrap();
std::fs::create_dir_all(consumer_real.join("src")).unwrap();
std::fs::create_dir_all(&producer_configured).unwrap();
std::fs::create_dir_all(&consumer_configured).unwrap();
std::os::unix::fs::symlink(&producer_real, &producer_link).unwrap();
std::os::unix::fs::symlink(&consumer_real, &consumer_link).unwrap();
let producer = PathNormalizer::from_env(Some(&producer_link))
.with_base_dirs(&[producer_configured.to_string_lossy().into_owned()]);
let consumer = PathNormalizer::from_env(Some(&consumer_link))
.with_base_dirs(&[consumer_configured.to_string_lossy().into_owned()]);
let producer_canonical = producer_real.canonicalize().unwrap();
let consumer_canonical = consumer_real.canonicalize().unwrap();
let producer_source = producer_canonical.join("src/lib.rs");
assert_eq!(
producer.source_path_identity(&producer_source).unwrap(),
b"<WORKSPACE>/src/lib.rs"
);
let producer_roots = producer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
assert!(producer_roots.iter().any(|(root, sentinel, _)| {
root == &producer_canonical && sentinel == "__kache_workspace__/"
}));
let target = producer_link.join("target");
let raw = format!(
"{}/demo.d: {}\n",
target.display(),
producer_source.display()
);
let stored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&raw,
&target,
&producer_link,
Some(&producer_link),
&producer_roots,
crate::link::DepInfoMode::Relativize,
);
assert!(
stored.contains("__kache_workspace__/src/lib.rs"),
"{stored}"
);
let consumer_roots = consumer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let restored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&stored,
&consumer_link.join("target"),
&consumer_link,
Some(&consumer_link),
&consumer_roots,
crate::link::DepInfoMode::Expand,
);
assert!(restored.contains(&format!("{}/src/lib.rs", consumer_canonical.display())));
assert!(!restored.contains(&producer_canonical.to_string_lossy().into_owned()));
}
#[test]
fn argv_target_dir_owns_generated_sources_without_a_workspace_root() {
let dir = TempDir::new().unwrap();
let target = dir.path().join("target");
let generated = target.join("release/build/serde-core/out/private.rs");
std::fs::create_dir_all(generated.parent().unwrap()).unwrap();
std::fs::write(&generated, "pub const PRIVATE: bool = true;\n").unwrap();
let normalizer = PathNormalizer::empty().with_target_dir(Some(&target));
assert_eq!(
normalizer.source_path_identity(&generated).unwrap(),
b"<TARGET>/release/build/serde-core/out/private.rs"
);
assert!(normalizer.depinfo_source_roots().iter().any(|root| {
root.root == target && root.depinfo_sentinel == "__kache_target_rule__/"
}));
assert!(
normalizer
.remap_args()
.iter()
.any(|arg| arg.contains("=/kache/target"))
);
}
#[cfg(unix)]
#[test]
fn canonical_filesystem_root_is_never_added_as_an_alias() {
let dir = TempDir::new().unwrap();
let link = dir.path().join("root-link");
std::os::unix::fs::symlink("/", &link).unwrap();
let mut legacy_rules = Vec::new();
let mut restore_roots = Vec::new();
push_legacy_base_dir_rules(
&mut legacy_rules,
&mut restore_roots,
Some(link.as_os_str()),
);
assert!(legacy_rules.iter().all(|rule| rule.prefix != "/"));
let configured =
PathNormalizer::empty().with_base_dirs(&[link.to_string_lossy().into_owned()]);
assert!(configured.rules.iter().all(|rule| rule.prefix != "/"));
assert!(
configured
.source_path_identity(Path::new("/etc/passwd"))
.is_none()
);
}
#[test]
fn source_identity_stays_local_for_roots_without_depinfo_restore_ownership() {
let normalizer = pn_with_rules(vec![("/toolchain/rust", "<RUST_SRC>")]);
assert!(
normalizer
.source_path_identity(Path::new("/toolchain/rust/library/core/src/lib.rs"))
.is_none()
);
}
#[test]
fn configured_ancestor_wins_over_nested_workspace_for_source_identity() {
let normalizer = pn_with_rules(vec![("/sandbox/checkout", "<WORKSPACE>")])
.with_base_dirs(&["/sandbox".to_string()]);
let source = Path::new("/sandbox/checkout/src/lib.rs");
assert_eq!(
normalizer.source_path_identity(source).unwrap(),
b"<BASE_DIR_0>/checkout/src/lib.rs"
);
assert_eq!(
apply_last_match(&normalizer.remap_args(), source.to_str().unwrap()),
"/kache/base-dir-0/checkout/src/lib.rs"
);
}
#[test]
fn configured_ancestor_also_wins_depinfo_ownership() {
let dir = TempDir::new().unwrap();
let configured = dir.path().join("configured");
let workspace = configured.join("checkout");
let source = workspace.join("src/lib.rs");
std::fs::create_dir_all(source.parent().unwrap()).unwrap();
std::fs::write(&source, "pub const VALUE: u8 = 1;").unwrap();
let normalizer = PathNormalizer::from_env(Some(&workspace))
.with_base_dirs(&[configured.to_string_lossy().into_owned()]);
let relative = source.strip_prefix(&configured).unwrap().to_string_lossy();
assert_eq!(
normalizer.source_path_identity(&source).unwrap(),
format!("<BASE_DIR_0>/{relative}").as_bytes()
);
let roots = normalizer.depinfo_source_roots();
assert!(!roots.iter().any(|root| {
root.root == workspace
&& matches!(
root.depinfo_sentinel.as_str(),
"__kache_cwd__/" | "__kache_workspace__/"
)
}));
let roots = roots
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let target = workspace.join("target");
let raw = format!("{}/demo.d: {}\n", target.display(), source.display());
let stored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&raw,
&target,
&workspace,
Some(&workspace),
&roots,
crate::link::DepInfoMode::Relativize,
);
assert!(
stored.contains(&format!("__kache_base_dir_0__/{relative}")),
"{stored}"
);
assert!(!stored.contains("__kache_cwd__/"), "{stored}");
assert!(!stored.contains("__kache_workspace__/"), "{stored}");
}
#[cfg(unix)]
#[test]
fn configured_lexical_root_is_not_stolen_by_another_roots_alias() {
let dir = TempDir::new().unwrap();
let real_path = dir.path().join("z-real");
let link_path = dir.path().join("a-link");
std::fs::create_dir_all(real_path.join("src")).unwrap();
std::os::unix::fs::symlink(&real_path, &link_path).unwrap();
let link = link_path.to_string_lossy().into_owned();
let real = real_path.to_string_lossy().into_owned();
let normalizer = PathNormalizer::empty().with_base_dirs(&[link.clone(), real.clone()]);
assert_eq!(normalizer.configured_base_dir_count(), 2);
assert_eq!(
normalizer.normalize(format!("{link}/src")),
"<BASE_DIR_0>/src"
);
assert_eq!(
normalizer.normalize(format!("{real}/src")),
"<BASE_DIR_1>/src"
);
assert_eq!(
normalizer
.source_path_identity(&link_path.join("src/lib.rs"))
.unwrap(),
b"<BASE_DIR_0>/src/lib.rs"
);
assert_eq!(
normalizer
.source_path_identity(&real_path.join("src/lib.rs"))
.unwrap(),
b"<BASE_DIR_1>/src/lib.rs"
);
let roots = normalizer.depinfo_source_roots();
assert!(roots.iter().any(|root| {
root.root == link_path
&& root.key_sentinel == "<BASE_DIR_0>"
&& root.depinfo_sentinel == "__kache_base_dir_0__/"
&& root.priority == flag_emit_rank("<BASE_DIR_0>")
}));
assert!(roots.iter().any(|root| {
root.root == real_path
&& root.key_sentinel == "<BASE_DIR_1>"
&& root.depinfo_sentinel == "__kache_base_dir_1__/"
&& root.priority == flag_emit_rank("<BASE_DIR_1>")
}));
assert!(!roots.iter().any(|root| {
root.root == real_path
&& (root.key_sentinel != "<BASE_DIR_1>"
|| root.depinfo_sentinel != "__kache_base_dir_1__/")
}));
}
#[cfg(unix)]
#[test]
fn configured_symlink_alias_restores_through_lexical_root_after_retarget() {
let dir = TempDir::new().unwrap();
let old_real = dir.path().join("a-very-long-old-real-target");
let new_real = dir.path().join("new-real-target");
let link = dir.path().join("x");
std::fs::create_dir_all(old_real.join("src")).unwrap();
std::fs::create_dir_all(new_real.join("src")).unwrap();
std::os::unix::fs::symlink(&old_real, &link).unwrap();
let configured = link.to_string_lossy().into_owned();
let producer = PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&configured));
let producer_roots = producer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let old_canonical = old_real.canonicalize().unwrap();
let input = format!("out: {}/src/value.rs\n", old_canonical.display());
let stored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&input,
Path::new("/unrelated-target"),
Path::new("/unrelated-cwd"),
None,
&producer_roots,
crate::link::DepInfoMode::Relativize,
);
assert!(
stored.contains("__kache_base_dir_0__/src/value.rs"),
"{stored}"
);
std::fs::remove_file(&link).unwrap();
std::os::unix::fs::symlink(&new_real, &link).unwrap();
let consumer = PathNormalizer::empty().with_base_dirs(&[configured]);
let consumer_roots = consumer
.depinfo_source_roots()
.into_iter()
.map(|root| (root.root, root.depinfo_sentinel, root.priority))
.collect::<Vec<_>>();
let restored = crate::link::rewrite_rustc_depinfo_content_with_configured_roots(
&stored,
Path::new("/other-target"),
Path::new("/other-cwd"),
None,
&consumer_roots,
crate::link::DepInfoMode::Expand,
);
assert!(restored.contains(&format!("{}/src/value.rs", link.display())));
assert!(!restored.contains(old_canonical.to_str().unwrap()));
}
#[test]
fn configured_base_dirs_relocate_equivalently_but_stay_distinct() {
let host_a = TempDir::new().unwrap();
let host_b = TempDir::new().unwrap();
let root_a = host_a.path().join("mount");
let root_b = host_b.path().join("mount");
std::fs::create_dir_all(&root_a).unwrap();
std::fs::create_dir_all(&root_b).unwrap();
let root_a_cfg = root_a.to_string_lossy().into_owned();
let root_b_cfg = root_b.to_string_lossy().into_owned();
let a = PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&root_a_cfg));
let b = PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&root_b_cfg));
let a_input = format!("{}/pkg/file.rs", canonical_string(&root_a).unwrap());
let b_input = format!("{}/pkg/file.rs", canonical_string(&root_b).unwrap());
assert_eq!(a.normalize(&a_input), b.normalize(&b_input));
assert_eq!(a.normalize(&a_input), "<BASE_DIR_0>/pkg/file.rs");
let distinct = PathNormalizer::empty().with_base_dirs(&[root_a_cfg, root_b_cfg]);
assert_ne!(distinct.normalize(&a_input), distinct.normalize(&b_input));
}
#[test]
fn configured_missing_root_still_normalizes_and_emits_auditable_remap() {
let dir = TempDir::new().unwrap();
let missing = dir.path().join("not-mounted-here");
let input = format!("{}/target/out.o", missing.display());
let missing_cfg = missing.to_string_lossy().into_owned();
let normalizer = PathNormalizer::empty().with_base_dirs(std::slice::from_ref(&missing_cfg));
assert_eq!(normalizer.normalize(&input), "<BASE_DIR_0>/target/out.o");
let expected = format!(
"--remap-path-prefix={}={}",
missing.display(),
configured_base_dir_target(0)
);
assert!(normalizer.remap_args().iter().any(|arg| arg == &expected));
}
}