mbx_cache_core/
path_mapping.rs1use std::fmt;
2use std::path::{Component, Path, PathBuf};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct PathMapping {
7 pub root: PathBuf,
9 pub placeholder: String,
11}
12
13impl PathMapping {
14 pub fn new(root: impl Into<PathBuf>, placeholder: impl Into<String>) -> Self {
16 Self {
17 root: root.into(),
18 placeholder: placeholder.into(),
19 }
20 }
21
22 pub fn ordered(mappings: &[PathMapping]) -> Vec<PathMapping> {
24 let mut ordered = mappings.to_vec();
25 ordered.sort_by_key(|mapping| std::cmp::Reverse(mapping.root.components().count()));
26 ordered
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PathNormalizationError {
33 UnmappedAbsolutePath(PathBuf),
35 NonUtf8Path(PathBuf),
37}
38
39impl fmt::Display for PathNormalizationError {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::UnmappedAbsolutePath(path) => {
43 write!(
44 formatter,
45 "absolute path has no stable cache mapping: {}",
46 path.display()
47 )
48 }
49 Self::NonUtf8Path(path) => {
50 write!(
51 formatter,
52 "cache key paths must be valid UTF-8: {}",
53 path.display()
54 )
55 }
56 }
57 }
58}
59
60impl std::error::Error for PathNormalizationError {}
61
62pub fn normalize_mapped_path(
67 path: &Path,
68 working_dir: &Path,
69 mappings: &[PathMapping],
70) -> Result<String, PathNormalizationError> {
71 let mappings = resolve_path_mappings(mappings);
72 normalize_resolved_mapped_path(path, working_dir, &mappings)
73}
74
75pub fn resolve_path_mappings(mappings: &[PathMapping]) -> Vec<PathMapping> {
80 mappings
81 .iter()
82 .map(|mapping| PathMapping {
83 root: resolve_mapping_root(&mapping.root),
84 placeholder: mapping.placeholder.clone(),
85 })
86 .collect()
87}
88
89pub fn normalize_resolved_mapped_path(
92 path: &Path,
93 working_dir: &Path,
94 mappings: &[PathMapping],
95) -> Result<String, PathNormalizationError> {
96 let absolute = if path.is_absolute() {
97 normalize_components(path)
98 } else {
99 normalize_components(&working_dir.join(path))
100 };
101 let resolved = if absolute.is_absolute() {
102 resolve_path_aliases(&absolute)
103 } else {
104 absolute.clone()
105 };
106 for mapping in mappings {
107 if let Ok(relative) = resolved.strip_prefix(&mapping.root) {
108 let suffix = slash_path(relative)?;
109 return Ok(if suffix.is_empty() {
110 format!("${{{}}}", mapping.placeholder)
111 } else {
112 format!("${{{}}}/{suffix}", mapping.placeholder)
113 });
114 }
115 }
116 Err(PathNormalizationError::UnmappedAbsolutePath(absolute))
117}
118
119#[cfg(unix)]
120fn resolve_path_aliases(path: &Path) -> PathBuf {
121 let mut existing = path;
122 let mut missing = Vec::new();
123 loop {
124 match std::fs::canonicalize(existing) {
125 Ok(mut resolved) => {
126 for component in missing.iter().rev() {
127 resolved.push(component);
128 }
129 return normalize_components(&resolved);
130 }
131 Err(_) => {
132 let Some(name) = existing.file_name() else {
133 return path.to_path_buf();
134 };
135 missing.push(name.to_os_string());
136 let Some(parent) = existing.parent() else {
137 return path.to_path_buf();
138 };
139 existing = parent;
140 }
141 }
142 }
143}
144
145#[cfg(not(unix))]
146fn resolve_path_aliases(path: &Path) -> PathBuf {
147 path.to_path_buf()
148}
149
150fn resolve_mapping_root(root: &Path) -> PathBuf {
151 let root = normalize_components(root);
152 if root.is_absolute() {
153 resolve_path_aliases(&root)
154 } else {
155 root
156 }
157}
158
159fn normalize_components(path: &Path) -> PathBuf {
160 let mut normalized = PathBuf::new();
161 for component in path.components() {
162 match component {
163 Component::CurDir => {}
164 Component::ParentDir => {
165 normalized.pop();
166 }
167 component => normalized.push(component.as_os_str()),
168 }
169 }
170 normalized
171}
172
173fn slash_path(path: &Path) -> Result<String, PathNormalizationError> {
174 path.components()
175 .filter_map(|component| match component {
176 Component::Normal(value) => Some(
177 value
178 .to_str()
179 .map(ToOwned::to_owned)
180 .ok_or_else(|| PathNormalizationError::NonUtf8Path(path.to_path_buf())),
181 ),
182 _ => None,
183 })
184 .collect::<Result<Vec<_>, _>>()
185 .map(|components| components.join("/"))
186}