1pub fn canonicalize(path: impl AsRef<std::path::Path>) -> std::io::Result<std::path::PathBuf> {
13 std::fs::canonicalize(path).map(strip_verbatim)
14}
15
16#[cfg(not(windows))]
17fn strip_verbatim(p: std::path::PathBuf) -> std::path::PathBuf {
18 p
19}
20
21#[cfg(windows)]
30fn strip_verbatim(p: std::path::PathBuf) -> std::path::PathBuf {
31 use std::path::{Component, Prefix};
32 let mut comps = p.components();
33 let Some(Component::Prefix(prefix)) = comps.next() else {
34 return p;
35 };
36 let root = match prefix.kind() {
37 Prefix::VerbatimDisk(d) => format!("{}:\\", d as char),
38 Prefix::VerbatimUNC(server, share) => format!(
39 "\\\\{}\\{}",
40 server.to_string_lossy(),
41 share.to_string_lossy()
42 ),
43 _ => return p,
44 };
45 let mut out = std::path::PathBuf::from(root);
46 for c in comps {
47 if !matches!(c, Component::RootDir) {
48 out.push(c.as_os_str());
49 }
50 }
51 if out.symlink_metadata().is_ok() {
54 out
55 } else {
56 p
57 }
58}
59
60pub fn is_absolute_path(path: &str) -> bool {
64 if path.starts_with('/') || path.starts_with('\\') || path.contains("://") {
65 return true;
66 }
67 let b = path.as_bytes();
68 b.len() >= 3 && b[0].is_ascii_alphabetic() && b[1] == b':' && (b[2] == b'/' || b[2] == b'\\')
69}
70
71pub fn normalize_path(path: &str) -> String {
74 let path = path.replace('\\', "/");
75 let (absolute, rest) = if path.starts_with("//") && path.len() > 2 {
76 ("//", &path[2..])
77 } else if let Some(r) = path.strip_prefix('/') {
78 ("/", r)
79 } else {
80 ("", path.as_str())
81 };
82 let mut parts: Vec<&str> = Vec::new();
83 let mut up = false;
84 for chunk in rest.split('/') {
85 if chunk == ".." && (!absolute.is_empty() || up) {
86 parts.pop();
87 up = !(parts.is_empty() || parts.last() == Some(&".."));
88 } else if chunk != "." && !chunk.is_empty() {
89 parts.push(chunk);
90 up = chunk != "..";
91 }
92 }
93 format!("{absolute}{}", parts.join("/"))
94}
95
96pub fn php_dirname(p: &str) -> String {
99 match p.rfind('/') {
100 None => ".".to_owned(),
101 Some(0) => "/".to_owned(),
102 Some(i) => p[..i].to_owned(),
103 }
104}
105
106fn php_basename(p: &str) -> &str {
108 p.rsplit('/').next().unwrap_or(p)
109}
110
111fn common_path(from: &str, to: &str) -> String {
116 let mut common = to.to_owned();
117 while !format!("{from}/").starts_with(&format!("{common}/")) && common != "/" && common != "." {
118 common = php_dirname(&common);
119 }
120 common
121}
122
123pub fn find_shortest_path(from: &str, to: &str, directories: bool) -> String {
126 find_shortest_path_with(from, to, directories, false)
127}
128
129pub fn find_shortest_path_with(
133 from: &str,
134 to: &str,
135 directories: bool,
136 prefer_relative: bool,
137) -> String {
138 let mut from = normalize_path(from);
139 let to = normalize_path(to);
140 if directories {
141 from = format!("{}/dummy_file", from.trim_end_matches('/'));
142 }
143 if php_dirname(&from) == php_dirname(&to) {
144 return format!("./{}", php_basename(&to));
145 }
146 let common = common_path(&from, &to);
147 if !from.starts_with(&common) || common == "." {
148 return to;
149 }
150 let common = format!("{}/", common.trim_end_matches('/'));
151 let depth = from[common.len().min(from.len())..].matches('/').count();
152 if !prefer_relative && common == "/" && depth > 1 {
153 return to;
154 }
155 let result = format!(
156 "{}{}",
157 "../".repeat(depth),
158 &to[common.len().min(to.len())..]
159 );
160 if result.is_empty() {
161 "./".to_owned()
162 } else {
163 result
164 }
165}
166
167pub fn find_shortest_path_code(
170 from: &str,
171 to: &str,
172 directories: bool,
173 static_code: bool,
174) -> String {
175 let from = normalize_path(from);
176 let to = normalize_path(to);
177 if from == to {
178 return if directories { "__DIR__" } else { "__FILE__" }.to_owned();
179 }
180 let common = common_path(&from, &to);
181 if !from.starts_with(&common) || common == "." {
182 return php_str(&to);
183 }
184 let common = format!("{}/", common.trim_end_matches('/'));
185 if to.starts_with(&format!("{from}/")) {
186 return format!("__DIR__ . {}", php_str(&to[from.len()..]));
187 }
188 let depth =
189 from[common.len().min(from.len())..].matches('/').count() + usize::from(directories);
190 if common == "/" && depth > 1 {
191 return php_str(&to);
192 }
193 let code = if static_code {
194 format!("__DIR__ . '{}'", "/..".repeat(depth))
195 } else {
196 format!("{}__DIR__{}", "dirname(".repeat(depth), ")".repeat(depth))
197 };
198 let rel = &to[common.len().min(to.len())..];
199 if rel.is_empty() {
200 code
201 } else {
202 format!("{code}.{}", php_str(&format!("/{rel}")))
203 }
204}
205
206pub fn php_str(s: &str) -> String {
208 let mut out = String::with_capacity(s.len() + 2);
209 out.push('\'');
210 for c in s.chars() {
211 match c {
212 '\'' => out.push_str("\\'"),
213 '\\' => out.push_str("\\\\"),
214 c => out.push(c),
215 }
216 }
217 out.push('\'');
218 out
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn canonicalize_has_no_verbatim_prefix() {
227 let tmp = tempfile::tempdir().expect("tmp");
228 let real = canonicalize(tmp.path()).expect("canonicalize");
229 let s = real.to_string_lossy().into_owned();
230 assert!(!s.starts_with(r"\\?\"), "verbatim prefix not stripped: {s}");
231 assert!(real.is_dir());
233 assert!(!normalize_path(&s).starts_with("//?/"), "{s}");
234 }
235
236 #[cfg(windows)]
240 #[test]
241 fn canonicalize_strips_verbatim_beyond_max_path() {
242 let tmp = tempfile::tempdir().expect("tmp");
243 let mut deep = tmp.path().to_path_buf();
244 while deep.as_os_str().len() < 300 {
245 deep.push("abcdefghijklmnopqrstuvwxyz0123456789");
246 }
247 std::fs::create_dir_all(&deep).expect("mkdir deep");
248 std::fs::write(deep.join("f.txt"), b"x").expect("write");
249 let real = canonicalize(&deep).expect("canonicalize");
250 assert!(real.as_os_str().len() > 260, "{}", real.display());
251 assert!(
252 !real.to_string_lossy().starts_with(r"\\?\"),
253 "verbatim prefix beyond MAX_PATH: {}",
254 real.display()
255 );
256 assert!(std::fs::read(real.join("f.txt")).is_ok(), "unreadable form");
257 }
258
259 #[test]
260 fn normalize() {
261 assert_eq!(normalize_path("/a/b/../c/./d/"), "/a/c/d");
262 assert_eq!(normalize_path("app/"), "app");
263 assert_eq!(normalize_path("/a//b"), "/a/b");
264 assert_eq!(normalize_path("../x"), "../x");
265 assert_eq!(
266 normalize_path("/p/web/app/plugins/x/"),
267 "/p/web/app/plugins/x"
268 );
269 }
270
271 #[test]
272 fn shortest_paths_directories() {
273 assert_eq!(
274 find_shortest_path("/p/vendor/composer", "/p/vendor", true),
275 "../"
276 );
277 assert_eq!(
278 find_shortest_path("/p/vendor/composer", "/p", true),
279 "../../"
280 );
281 assert_eq!(find_shortest_path("/p/vendor", "/p/app", true), "../app");
282 assert_eq!(find_shortest_path("/p", "/p/app/x", true), "app/x");
283 assert_eq!(
284 find_shortest_path("/p/vendor/composer", "/p/vendor/a/b", true),
285 "../a/b"
286 );
287 assert_eq!(
288 find_shortest_path("/p/vendor/composer", "/p/vendor/composer/x", true),
289 "./x"
290 );
291 assert_eq!(
292 find_shortest_path("/p/vendor/composer", "/p/web/app/plugins/x", true),
293 "../../web/app/plugins/x"
294 );
295 assert_eq!(
296 find_shortest_path("/p/vendor/composer", "/q/x", true),
297 "/q/x"
298 );
299 assert_eq!(
300 find_shortest_path("/p/vendor/composer", "/p/vendor/composer", true),
301 "./"
302 );
303 }
304
305 #[test]
306 fn shortest_path_codes() {
307 assert_eq!(
308 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, true),
309 "__DIR__ . '/..'"
310 );
311 assert_eq!(
312 find_shortest_path_code("/p/vendor/composer", "/p", true, true),
313 "__DIR__ . '/../..'"
314 );
315 assert_eq!(
316 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, false),
317 "dirname(__DIR__)"
318 );
319 assert_eq!(
320 find_shortest_path_code("/p/vendor", "/p", true, false),
321 "dirname(__DIR__)"
322 );
323 assert_eq!(
324 find_shortest_path_code("/p/vendor", "/p/vendor/composer", true, false),
325 "__DIR__ . '/composer'"
326 );
327 assert_eq!(
328 find_shortest_path_code("/p/vendor/composer", "/p/vendor/composer", true, false),
329 "__DIR__"
330 );
331 assert_eq!(
332 find_shortest_path_code("/p/vendor/composer", "/p/web/x", true, true),
333 "__DIR__ . '/../..'.'/web/x'"
334 );
335 }
336}