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 is_normalized_absolute(path: &str) -> bool {
76 path.starts_with('/')
77 && !path.starts_with("//")
78 && !path.ends_with('/')
79 && !path.contains('\\')
80 && !path[1..]
81 .split('/')
82 .any(|seg| seg.is_empty() || seg == "." || seg == "..")
83}
84
85pub fn normalize_path_cow(path: &str) -> std::borrow::Cow<'_, str> {
88 if is_normalized_absolute(path) {
89 std::borrow::Cow::Borrowed(path)
90 } else {
91 std::borrow::Cow::Owned(normalize_path(path))
92 }
93}
94
95pub fn normalize_path(path: &str) -> String {
96 let path = path.replace('\\', "/");
97 let (absolute, rest) = if path.starts_with("//") && path.len() > 2 {
98 ("//", &path[2..])
99 } else if let Some(r) = path.strip_prefix('/') {
100 ("/", r)
101 } else {
102 ("", path.as_str())
103 };
104 let mut parts: Vec<&str> = Vec::new();
105 let mut up = false;
106 for chunk in rest.split('/') {
107 if chunk == ".." && (!absolute.is_empty() || up) {
108 parts.pop();
109 up = !(parts.is_empty() || parts.last() == Some(&".."));
110 } else if chunk != "." && !chunk.is_empty() {
111 parts.push(chunk);
112 up = chunk != "..";
113 }
114 }
115 format!("{absolute}{}", parts.join("/"))
116}
117
118pub fn php_dirname(p: &str) -> String {
121 match p.rfind('/') {
122 None => ".".to_owned(),
123 Some(0) => "/".to_owned(),
124 Some(i) => p[..i].to_owned(),
125 }
126}
127
128fn php_basename(p: &str) -> &str {
130 p.rsplit('/').next().unwrap_or(p)
131}
132
133fn common_path(from: &str, to: &str) -> String {
138 let mut common = to.to_owned();
139 while !format!("{from}/").starts_with(&format!("{common}/")) && common != "/" && common != "." {
140 common = php_dirname(&common);
141 }
142 common
143}
144
145pub fn find_shortest_path(from: &str, to: &str, directories: bool) -> String {
148 find_shortest_path_with(from, to, directories, false)
149}
150
151pub fn find_shortest_path_with(
155 from: &str,
156 to: &str,
157 directories: bool,
158 prefer_relative: bool,
159) -> String {
160 let mut from = normalize_path(from);
161 let to = normalize_path(to);
162 if directories {
163 from = format!("{}/dummy_file", from.trim_end_matches('/'));
164 }
165 if php_dirname(&from) == php_dirname(&to) {
166 return format!("./{}", php_basename(&to));
167 }
168 let common = common_path(&from, &to);
169 if !from.starts_with(&common) || common == "." {
170 return to;
171 }
172 let common = format!("{}/", common.trim_end_matches('/'));
173 let depth = from[common.len().min(from.len())..].matches('/').count();
174 if !prefer_relative && common == "/" && depth > 1 {
175 return to;
176 }
177 let result = format!(
178 "{}{}",
179 "../".repeat(depth),
180 &to[common.len().min(to.len())..]
181 );
182 if result.is_empty() {
183 "./".to_owned()
184 } else {
185 result
186 }
187}
188
189pub fn find_shortest_path_code(
192 from: &str,
193 to: &str,
194 directories: bool,
195 static_code: bool,
196) -> String {
197 let from = normalize_path(from);
198 let to = normalize_path(to);
199 if from == to {
200 return if directories { "__DIR__" } else { "__FILE__" }.to_owned();
201 }
202 let common = common_path(&from, &to);
203 if !from.starts_with(&common) || common == "." {
204 return php_str(&to);
205 }
206 let common = format!("{}/", common.trim_end_matches('/'));
207 if to.starts_with(&format!("{from}/")) {
208 return format!("__DIR__ . {}", php_str(&to[from.len()..]));
209 }
210 let depth =
211 from[common.len().min(from.len())..].matches('/').count() + usize::from(directories);
212 if common == "/" && depth > 1 {
213 return php_str(&to);
214 }
215 let code = if static_code {
216 format!("__DIR__ . '{}'", "/..".repeat(depth))
217 } else {
218 format!("{}__DIR__{}", "dirname(".repeat(depth), ")".repeat(depth))
219 };
220 let rel = &to[common.len().min(to.len())..];
221 if rel.is_empty() {
222 code
223 } else {
224 format!("{code}.{}", php_str(&format!("/{rel}")))
225 }
226}
227
228pub fn php_str(s: &str) -> String {
230 let mut out = String::with_capacity(s.len() + 2);
231 out.push('\'');
232 for c in s.chars() {
233 match c {
234 '\'' => out.push_str("\\'"),
235 '\\' => out.push_str("\\\\"),
236 c => out.push(c),
237 }
238 }
239 out.push('\'');
240 out
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn canonicalize_has_no_verbatim_prefix() {
249 let tmp = tempfile::tempdir().expect("tmp");
250 let real = canonicalize(tmp.path()).expect("canonicalize");
251 let s = real.to_string_lossy().into_owned();
252 assert!(!s.starts_with(r"\\?\"), "verbatim prefix not stripped: {s}");
253 assert!(real.is_dir());
255 assert!(!normalize_path(&s).starts_with("//?/"), "{s}");
256 }
257
258 #[cfg(windows)]
262 #[test]
263 fn canonicalize_strips_verbatim_beyond_max_path() {
264 let tmp = tempfile::tempdir().expect("tmp");
265 let mut deep = tmp.path().to_path_buf();
266 while deep.as_os_str().len() < 300 {
267 deep.push("abcdefghijklmnopqrstuvwxyz0123456789");
268 }
269 std::fs::create_dir_all(&deep).expect("mkdir deep");
270 std::fs::write(deep.join("f.txt"), b"x").expect("write");
271 let real = canonicalize(&deep).expect("canonicalize");
272 assert!(real.as_os_str().len() > 260, "{}", real.display());
273 assert!(
274 !real.to_string_lossy().starts_with(r"\\?\"),
275 "verbatim prefix beyond MAX_PATH: {}",
276 real.display()
277 );
278 assert!(std::fs::read(real.join("f.txt")).is_ok(), "unreadable form");
279 }
280
281 #[test]
282 fn normalize() {
283 assert_eq!(normalize_path("/a/b/../c/./d/"), "/a/c/d");
284 assert_eq!(normalize_path("app/"), "app");
285 assert_eq!(normalize_path("/a//b"), "/a/b");
286 assert_eq!(normalize_path("../x"), "../x");
287 assert_eq!(
288 normalize_path("/p/web/app/plugins/x/"),
289 "/p/web/app/plugins/x"
290 );
291 }
292
293 #[test]
294 fn shortest_paths_directories() {
295 assert_eq!(
296 find_shortest_path("/p/vendor/composer", "/p/vendor", true),
297 "../"
298 );
299 assert_eq!(
300 find_shortest_path("/p/vendor/composer", "/p", true),
301 "../../"
302 );
303 assert_eq!(find_shortest_path("/p/vendor", "/p/app", true), "../app");
304 assert_eq!(find_shortest_path("/p", "/p/app/x", true), "app/x");
305 assert_eq!(
306 find_shortest_path("/p/vendor/composer", "/p/vendor/a/b", true),
307 "../a/b"
308 );
309 assert_eq!(
310 find_shortest_path("/p/vendor/composer", "/p/vendor/composer/x", true),
311 "./x"
312 );
313 assert_eq!(
314 find_shortest_path("/p/vendor/composer", "/p/web/app/plugins/x", true),
315 "../../web/app/plugins/x"
316 );
317 assert_eq!(
318 find_shortest_path("/p/vendor/composer", "/q/x", true),
319 "/q/x"
320 );
321 assert_eq!(
322 find_shortest_path("/p/vendor/composer", "/p/vendor/composer", true),
323 "./"
324 );
325 }
326
327 #[test]
328 fn shortest_path_codes() {
329 assert_eq!(
330 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, true),
331 "__DIR__ . '/..'"
332 );
333 assert_eq!(
334 find_shortest_path_code("/p/vendor/composer", "/p", true, true),
335 "__DIR__ . '/../..'"
336 );
337 assert_eq!(
338 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, false),
339 "dirname(__DIR__)"
340 );
341 assert_eq!(
342 find_shortest_path_code("/p/vendor", "/p", true, false),
343 "dirname(__DIR__)"
344 );
345 assert_eq!(
346 find_shortest_path_code("/p/vendor", "/p/vendor/composer", true, false),
347 "__DIR__ . '/composer'"
348 );
349 assert_eq!(
350 find_shortest_path_code("/p/vendor/composer", "/p/vendor/composer", true, false),
351 "__DIR__"
352 );
353 assert_eq!(
354 find_shortest_path_code("/p/vendor/composer", "/p/web/x", true, true),
355 "__DIR__ . '/../..'.'/web/x'"
356 );
357 }
358}