1use std::ffi::OsStr;
12use std::fs;
13use std::path::{Component, Path, PathBuf};
14
15use crate::{GitError, Result};
16
17#[allow(clippy::suspicious_operation_groupings)]
30pub fn relative_path_bytes(input: &[u8], prefix: &[u8]) -> Vec<u8> {
31 let in_len = input.len();
32 let prefix_len = prefix.len();
33 if in_len == 0 {
34 return b"./".to_vec();
35 }
36 if prefix_len == 0 {
37 return input.to_vec();
38 }
39 let is_sep = |byte: u8| byte == b'/';
40 let mut i = 0usize;
41 let mut j = 0usize;
42 let mut prefix_off = 0usize;
43 let mut in_off = 0usize;
44 while i < prefix_len && j < in_len && prefix.get(i) == input.get(j) {
45 if is_sep(prefix[i]) {
46 while i < prefix_len && is_sep(prefix[i]) {
47 i += 1;
48 }
49 while j < in_len && is_sep(input[j]) {
50 j += 1;
51 }
52 prefix_off = i;
53 in_off = j;
54 } else {
55 i += 1;
56 j += 1;
57 }
58 }
59
60 if i >= prefix_len && prefix_off < prefix_len {
61 if j >= in_len {
62 in_off = in_len;
63 } else if is_sep(input[j]) {
64 while j < in_len && is_sep(input[j]) {
65 j += 1;
66 }
67 in_off = j;
68 } else {
69 i = prefix_off;
70 }
71 } else if j >= in_len && in_off < in_len && i < prefix_len && is_sep(prefix[i]) {
72 while i < prefix_len && is_sep(prefix[i]) {
73 i += 1;
74 }
75 in_off = in_len;
76 }
77
78 let input = &input[in_off..];
79 if i >= prefix_len {
80 if input.is_empty() {
81 return b"./".to_vec();
82 }
83 return input.to_vec();
84 }
85
86 let mut out = Vec::new();
87 while i < prefix_len {
88 if is_sep(prefix[i]) {
89 out.extend_from_slice(b"../");
90 while i < prefix_len && is_sep(prefix[i]) {
91 i += 1;
92 }
93 continue;
94 }
95 i += 1;
96 }
97 if !is_sep(prefix[prefix_len - 1]) {
98 out.extend_from_slice(b"../");
99 }
100 out.extend_from_slice(input);
101 out
102}
103
104pub fn normalize_lexical(path: &Path) -> PathBuf {
118 let mut out = PathBuf::new();
119 for component in path.components() {
120 match component {
121 Component::ParentDir => {
122 if !out.pop() {
123 out.push("..");
124 }
125 }
126 Component::CurDir => {}
127 other => out.push(other.as_os_str()),
128 }
129 }
130 out
131}
132
133pub fn relative_path_lexical(target: &Path, base: &Path) -> String {
137 let target = normalize_lexical(target);
138 let base = normalize_lexical(base);
139 let target_components: Vec<_> = target.components().collect();
140 let base_components: Vec<_> = base.components().collect();
141 let common = target_components
142 .iter()
143 .zip(base_components.iter())
144 .take_while(|(a, b)| a == b)
145 .count();
146 let mut result = PathBuf::new();
147 for _ in common..base_components.len() {
148 result.push("..");
149 }
150 for component in &target_components[common..] {
151 result.push(component.as_os_str());
152 }
153 if result.as_os_str().is_empty() {
154 ".".to_string()
155 } else {
156 result.display().to_string()
157 }
158}
159
160pub fn relative_path_from_absolute(cwd: &Path, target: &Path) -> Result<String> {
169 let cwd = fs::canonicalize(cwd).map_err(|err| GitError::Io(err.to_string()))?;
170 relative_path_from_absolute_components(&cwd, target)
171}
172
173pub fn relative_path_from_absolute_components(cwd: &Path, target: &Path) -> Result<String> {
176 let cwd_components = cwd.components().collect::<Vec<_>>();
177 let target_components = target.components().collect::<Vec<_>>();
178 let common = cwd_components
179 .iter()
180 .zip(target_components.iter())
181 .take_while(|(left, right)| left == right)
182 .count();
183 if common == 0 {
184 return Ok(target.display().to_string());
185 }
186
187 let up_count = cwd_components.len().saturating_sub(common);
188 let mut parts = Vec::new();
189 parts.extend((0..up_count).map(|_| "..".to_string()));
190 parts.extend(
191 target_components[common..]
192 .iter()
193 .map(|component| component.as_os_str().to_string_lossy().into_owned()),
194 );
195 if parts.is_empty() {
196 return Ok("./".into());
197 }
198 let mut relative = parts.join("/");
199 if common == target_components.len() {
200 relative.push('/');
201 }
202 Ok(relative)
203}
204
205pub fn relative_path_between(from_dir: &Path, to_path: &Path) -> PathBuf {
212 let from = normalize_lexical(from_dir);
213 let to = normalize_lexical(to_path);
214 let from_components = from.components().collect::<Vec<_>>();
215 let to_components = to.components().collect::<Vec<_>>();
216 let mut common = 0usize;
217 while common < from_components.len()
218 && common < to_components.len()
219 && from_components[common] == to_components[common]
220 {
221 common += 1;
222 }
223 if common == 0 {
224 return to;
225 }
226 let mut relative = PathBuf::new();
227 for component in &from_components[common..] {
228 if matches!(component, Component::Normal(_)) {
229 relative.push("..");
230 }
231 }
232 for component in &to_components[common..] {
233 match component {
234 Component::Normal(value) => relative.push(value),
235 Component::ParentDir => relative.push(".."),
236 Component::CurDir | Component::RootDir | Component::Prefix(_) => {}
237 }
238 }
239 if relative.as_os_str().is_empty() {
240 relative.push(".");
241 }
242 relative
243}
244
245pub fn os_str_to_bytes(value: &OsStr) -> Vec<u8> {
253 #[cfg(unix)]
254 {
255 use std::os::unix::ffi::OsStrExt;
256 value.as_bytes().to_vec()
257 }
258 #[cfg(not(unix))]
259 {
260 value.to_string_lossy().replace('\\', "/").into_bytes()
261 }
262}
263
264pub fn path_to_bytes(path: &Path) -> Vec<u8> {
266 os_str_to_bytes(path.as_os_str())
267}
268
269pub fn bytes_to_os_path(bytes: &[u8]) -> PathBuf {
272 #[cfg(unix)]
273 {
274 use std::os::unix::ffi::OsStrExt;
275 PathBuf::from(OsStr::from_bytes(bytes))
276 }
277 #[cfg(not(unix))]
278 {
279 PathBuf::from(String::from_utf8_lossy(bytes).into_owned())
280 }
281}
282
283pub fn bytes_to_path_string(bytes: &[u8]) -> Result<String> {
287 std::str::from_utf8(bytes)
288 .map(str::to_string)
289 .map_err(|_| GitError::InvalidFormat("non-utf8 worktree path".into()))
290}
291
292pub fn path_to_slash(path: &Path) -> String {
295 path.components()
296 .filter_map(|component| match component {
297 Component::Normal(part) => Some(part.to_string_lossy().into_owned()),
298 _ => None,
299 })
300 .collect::<Vec<_>>()
301 .join("/")
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 #[test]
312 fn relative_path_bytes_matches_git_relative_path() {
313 assert_eq!(relative_path_bytes(b"a/b/c.txt", b""), b"a/b/c.txt");
315 assert_eq!(relative_path_bytes(b"", b"a/b/"), b"./");
317 assert_eq!(relative_path_bytes(b"a/b/c.txt", b"a/b/"), b"c.txt");
319 assert_eq!(relative_path_bytes(b"a/b/", b"a/b/"), b"./");
321 assert_eq!(relative_path_bytes(b"a/x.txt", b"a/b/"), b"../x.txt");
323 assert_eq!(
325 relative_path_bytes(b"sib/out.txt", b"a/b/c/"),
326 b"../../../sib/out.txt"
327 );
328 assert_eq!(relative_path_bytes(b"root.txt", b"a/b/c/"), b"../../../root.txt");
330 assert_eq!(relative_path_bytes(b"a/b/c/d/e.txt", b"a/b/c/"), b"d/e.txt");
332 assert_eq!(relative_path_bytes(b"a/top.txt", b"a/b"), b"../top.txt");
335 assert_eq!(
337 relative_path_bytes(b"t/c.txt", b"same/"),
338 b"../t/c.txt"
339 );
340 }
341
342 #[test]
343 fn normalize_lexical_retains_leading_dotdot_and_drops_curdir() {
344 assert_eq!(normalize_lexical(Path::new("a/b/../c")), PathBuf::from("a/c"));
345 assert_eq!(normalize_lexical(Path::new("./a/./b")), PathBuf::from("a/b"));
346 assert_eq!(normalize_lexical(Path::new("../b")), PathBuf::from("../b"));
348 assert_eq!(
349 normalize_lexical(Path::new("a/../../b")),
350 PathBuf::from("../b")
351 );
352 assert_eq!(normalize_lexical(Path::new("/..")), PathBuf::from("/.."));
355 assert_eq!(
356 normalize_lexical(Path::new("/a/../../c")),
357 PathBuf::from("/../c")
358 );
359 assert_eq!(normalize_lexical(Path::new("")), PathBuf::from(""));
360 }
361
362 #[test]
363 fn relative_path_lexical_handles_sibling_worktree_layouts() {
364 let admin = Path::new("/repo/.git");
366 let wt = Path::new("/repo/wt/.git");
367 assert_eq!(relative_path_lexical(wt, admin), "../wt/.git");
368 assert_eq!(relative_path_lexical(admin, wt), "../../.git");
369 assert_eq!(relative_path_lexical(admin, admin), ".");
370 }
371
372 #[test]
373 fn relative_path_from_absolute_components_pins_edges() {
374 assert_eq!(
376 relative_path_from_absolute_components(Path::new("/r/wt"), Path::new("/r/wt"))
377 .unwrap_or_default(),
378 "./"
379 );
380 assert_eq!(
382 relative_path_from_absolute_components(Path::new("/r/wt"), Path::new("/r/wt/a/b"))
383 .unwrap_or_default(),
384 "a/b"
385 );
386 assert_eq!(
388 relative_path_from_absolute_components(Path::new("/r/wt/sub"), Path::new("/r/.git"))
389 .unwrap_or_default(),
390 "../../.git"
391 );
392 assert_eq!(
394 relative_path_from_absolute_components(Path::new("/a"), Path::new("/b/c"))
395 .unwrap_or_default(),
396 "../b/c"
397 );
398 assert_eq!(
401 relative_path_from_absolute_components(Path::new(""), Path::new("/b/c"))
402 .unwrap_or_default(),
403 "/b/c"
404 );
405 }
406
407 #[test]
408 fn relative_path_between_keeps_move_remove_edge_semantics() {
409 assert_eq!(
411 relative_path_between(Path::new("/r/.git"), Path::new("/r/.git")),
412 PathBuf::from(".")
413 );
414 assert_eq!(
415 relative_path_between(Path::new("/r/.git"), Path::new("/r/wt/.git")),
416 PathBuf::from("../wt/.git")
417 );
418 assert_eq!(
419 relative_path_between(Path::new("/r/wt/.git"), Path::new("/r/.git")),
420 PathBuf::from("../../.git")
421 );
422 assert_eq!(
424 relative_path_between(
425 Path::new("/r/wt/../.git"),
426 Path::new("/r/linked/../wt2/.git")
427 ),
428 PathBuf::from("../wt2/.git")
429 );
430 }
431
432 #[test]
433 fn byte_conversions_round_trip_and_slash_normalize() {
434 let weird = bytes_to_os_path(b"\xff\xfe/weird.txt");
435 assert_eq!(path_to_bytes(&weird), b"\xff\xfe/weird.txt");
436 assert_eq!(os_str_to_bytes(OsStr::new("plain/path")), b"plain/path");
437 assert_eq!(path_to_slash(Path::new("/a/b/c")), "a/b/c");
438 assert_eq!(path_to_slash(Path::new("a/b")), "a/b");
439 assert_eq!(bytes_to_path_string(b"ok.txt").ok(), Some("ok.txt".to_string()));
440 assert!(bytes_to_path_string(b"\xff").is_err());
441 }
442}