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
96fn php_dirname(p: &str) -> String {
98 match p.rfind('/') {
99 None => ".".to_owned(),
100 Some(0) => "/".to_owned(),
101 Some(i) => p[..i].to_owned(),
102 }
103}
104
105fn php_basename(p: &str) -> &str {
107 p.rsplit('/').next().unwrap_or(p)
108}
109
110fn common_path(from: &str, to: &str) -> String {
115 let mut common = to.to_owned();
116 while !format!("{from}/").starts_with(&format!("{common}/")) && common != "/" && common != "." {
117 common = php_dirname(&common);
118 }
119 common
120}
121
122pub fn find_shortest_path(from: &str, to: &str, directories: bool) -> String {
125 find_shortest_path_with(from, to, directories, false)
126}
127
128pub fn find_shortest_path_with(
132 from: &str,
133 to: &str,
134 directories: bool,
135 prefer_relative: bool,
136) -> String {
137 let mut from = normalize_path(from);
138 let to = normalize_path(to);
139 if directories {
140 from = format!("{}/dummy_file", from.trim_end_matches('/'));
141 }
142 if php_dirname(&from) == php_dirname(&to) {
143 return format!("./{}", php_basename(&to));
144 }
145 let common = common_path(&from, &to);
146 if !from.starts_with(&common) || common == "." {
147 return to;
148 }
149 let common = format!("{}/", common.trim_end_matches('/'));
150 let depth = from[common.len().min(from.len())..].matches('/').count();
151 if !prefer_relative && common == "/" && depth > 1 {
152 return to;
153 }
154 let result = format!(
155 "{}{}",
156 "../".repeat(depth),
157 &to[common.len().min(to.len())..]
158 );
159 if result.is_empty() {
160 "./".to_owned()
161 } else {
162 result
163 }
164}
165
166pub fn find_shortest_path_code(
169 from: &str,
170 to: &str,
171 directories: bool,
172 static_code: bool,
173) -> String {
174 let from = normalize_path(from);
175 let to = normalize_path(to);
176 if from == to {
177 return if directories { "__DIR__" } else { "__FILE__" }.to_owned();
178 }
179 let common = common_path(&from, &to);
180 if !from.starts_with(&common) || common == "." {
181 return php_str(&to);
182 }
183 let common = format!("{}/", common.trim_end_matches('/'));
184 if to.starts_with(&format!("{from}/")) {
185 return format!("__DIR__ . {}", php_str(&to[from.len()..]));
186 }
187 let depth =
188 from[common.len().min(from.len())..].matches('/').count() + usize::from(directories);
189 if common == "/" && depth > 1 {
190 return php_str(&to);
191 }
192 let code = if static_code {
193 format!("__DIR__ . '{}'", "/..".repeat(depth))
194 } else {
195 format!("{}__DIR__{}", "dirname(".repeat(depth), ")".repeat(depth))
196 };
197 let rel = &to[common.len().min(to.len())..];
198 if rel.is_empty() {
199 code
200 } else {
201 format!("{code}.{}", php_str(&format!("/{rel}")))
202 }
203}
204
205pub fn php_str(s: &str) -> String {
207 let mut out = String::with_capacity(s.len() + 2);
208 out.push('\'');
209 for c in s.chars() {
210 match c {
211 '\'' => out.push_str("\\'"),
212 '\\' => out.push_str("\\\\"),
213 c => out.push(c),
214 }
215 }
216 out.push('\'');
217 out
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn canonicalize_has_no_verbatim_prefix() {
226 let tmp = tempfile::tempdir().expect("tmp");
227 let real = canonicalize(tmp.path()).expect("canonicalize");
228 let s = real.to_string_lossy().into_owned();
229 assert!(!s.starts_with(r"\\?\"), "verbatim prefix not stripped: {s}");
230 assert!(real.is_dir());
232 assert!(!normalize_path(&s).starts_with("//?/"), "{s}");
233 }
234
235 #[cfg(windows)]
239 #[test]
240 fn canonicalize_strips_verbatim_beyond_max_path() {
241 let tmp = tempfile::tempdir().expect("tmp");
242 let mut deep = tmp.path().to_path_buf();
243 while deep.as_os_str().len() < 300 {
244 deep.push("abcdefghijklmnopqrstuvwxyz0123456789");
245 }
246 std::fs::create_dir_all(&deep).expect("mkdir deep");
247 std::fs::write(deep.join("f.txt"), b"x").expect("write");
248 let real = canonicalize(&deep).expect("canonicalize");
249 assert!(real.as_os_str().len() > 260, "{}", real.display());
250 assert!(
251 !real.to_string_lossy().starts_with(r"\\?\"),
252 "verbatim prefix beyond MAX_PATH: {}",
253 real.display()
254 );
255 assert!(std::fs::read(real.join("f.txt")).is_ok(), "unreadable form");
256 }
257
258 #[test]
259 fn normalize() {
260 assert_eq!(normalize_path("/a/b/../c/./d/"), "/a/c/d");
261 assert_eq!(normalize_path("app/"), "app");
262 assert_eq!(normalize_path("/a//b"), "/a/b");
263 assert_eq!(normalize_path("../x"), "../x");
264 assert_eq!(
265 normalize_path("/p/web/app/plugins/x/"),
266 "/p/web/app/plugins/x"
267 );
268 }
269
270 #[test]
271 fn shortest_paths_directories() {
272 assert_eq!(
273 find_shortest_path("/p/vendor/composer", "/p/vendor", true),
274 "../"
275 );
276 assert_eq!(
277 find_shortest_path("/p/vendor/composer", "/p", true),
278 "../../"
279 );
280 assert_eq!(find_shortest_path("/p/vendor", "/p/app", true), "../app");
281 assert_eq!(find_shortest_path("/p", "/p/app/x", true), "app/x");
282 assert_eq!(
283 find_shortest_path("/p/vendor/composer", "/p/vendor/a/b", true),
284 "../a/b"
285 );
286 assert_eq!(
287 find_shortest_path("/p/vendor/composer", "/p/vendor/composer/x", true),
288 "./x"
289 );
290 assert_eq!(
291 find_shortest_path("/p/vendor/composer", "/p/web/app/plugins/x", true),
292 "../../web/app/plugins/x"
293 );
294 assert_eq!(
295 find_shortest_path("/p/vendor/composer", "/q/x", true),
296 "/q/x"
297 );
298 assert_eq!(
299 find_shortest_path("/p/vendor/composer", "/p/vendor/composer", true),
300 "./"
301 );
302 }
303
304 #[test]
305 fn shortest_path_codes() {
306 assert_eq!(
307 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, true),
308 "__DIR__ . '/..'"
309 );
310 assert_eq!(
311 find_shortest_path_code("/p/vendor/composer", "/p", true, true),
312 "__DIR__ . '/../..'"
313 );
314 assert_eq!(
315 find_shortest_path_code("/p/vendor/composer", "/p/vendor", true, false),
316 "dirname(__DIR__)"
317 );
318 assert_eq!(
319 find_shortest_path_code("/p/vendor", "/p", true, false),
320 "dirname(__DIR__)"
321 );
322 assert_eq!(
323 find_shortest_path_code("/p/vendor", "/p/vendor/composer", true, false),
324 "__DIR__ . '/composer'"
325 );
326 assert_eq!(
327 find_shortest_path_code("/p/vendor/composer", "/p/vendor/composer", true, false),
328 "__DIR__"
329 );
330 assert_eq!(
331 find_shortest_path_code("/p/vendor/composer", "/p/web/x", true, true),
332 "__DIR__ . '/../..'.'/web/x'"
333 );
334 }
335}