1use std::path::PathBuf;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct JPath {
9 root: String,
11 names: Vec<String>,
12}
13
14pub const ON_WINDOWS: bool = cfg!(windows);
15
16impl JPath {
17 pub fn parse(s: &str) -> Option<JPath> {
19 if s.contains('\0') {
20 return None;
21 }
22 let s = if ON_WINDOWS { s.replace('\\', "/") } else { s.to_owned() };
23 let (root, rest) = split_root(&s);
24 if ON_WINDOWS && rest.chars().any(|c| matches!(c, '<' | '>' | ':' | '"' | '|' | '?' | '*')) {
25 return None;
26 }
27 let names = rest.split('/').filter(|n| !n.is_empty()).map(str::to_owned).collect();
28 Some(JPath { root, names })
29 }
30
31 pub fn from_path(path: &std::path::Path) -> JPath {
32 JPath::parse(&path.to_string_lossy()).unwrap_or(JPath { root: String::new(), names: Vec::new() })
33 }
34
35 pub fn is_absolute(&self) -> bool {
36 !self.root.is_empty()
37 }
38
39 pub fn resolve(&self, other: &str) -> Option<JPath> {
41 let other = JPath::parse(other)?;
42 if other.is_absolute() {
43 return Some(other);
44 }
45 let mut names = self.names.clone();
46 names.extend(other.names);
47 Some(JPath { root: self.root.clone(), names })
48 }
49
50 pub fn normalize(&self) -> JPath {
51 let mut names: Vec<String> = Vec::new();
52 for name in &self.names {
53 match name.as_str() {
54 "." => {}
55 ".." if names.last().is_some_and(|n| n != "..") => {
56 names.pop();
57 }
58 ".." if self.is_absolute() => {}
59 _ => names.push(name.clone()),
60 }
61 }
62 JPath { root: self.root.clone(), names }
63 }
64
65 pub fn starts_with(&self, other: &JPath) -> bool {
66 eq_root(&self.root, &other.root)
67 && self.names.len() >= other.names.len()
68 && self.names.iter().zip(&other.names).all(|(a, b)| eq_name(a, b))
69 }
70
71 pub fn parent(&self) -> Option<JPath> {
72 if self.names.is_empty() || (self.root.is_empty() && self.names.len() == 1) {
73 return None;
74 }
75 Some(JPath { root: self.root.clone(), names: self.names[..self.names.len() - 1].to_vec() })
76 }
77
78 pub fn file_name(&self) -> Option<&str> {
79 self.names.last().map(String::as_str)
80 }
81
82 pub fn relative_to(&self, base: &JPath) -> Option<JPath> {
84 let (bn, pn) = (base.normalize(), self.normalize());
85 if !eq_root(&bn.root, &pn.root) {
86 return None;
87 }
88 let common = bn.names.iter().zip(&pn.names).take_while(|(a, b)| eq_name(a, b)).count();
89 if bn.names[common..].iter().any(|n| n == "..") {
90 return None;
91 }
92 let mut names: Vec<String> = bn.names[common..].iter().map(|_| "..".to_owned()).collect();
93 names.extend(pn.names[common..].iter().cloned());
94 Some(JPath { root: String::new(), names })
95 }
96
97 pub fn relative_to_or_self(&self, base: &JPath) -> JPath {
98 self.relative_to(base).unwrap_or_else(|| self.clone())
99 }
100
101 pub fn to_path_buf(&self) -> PathBuf {
102 PathBuf::from(self.to_string())
103 }
104}
105
106impl std::fmt::Display for JPath {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 write!(f, "{}{}", self.root, self.names.join("/"))
110 }
111}
112
113fn split_root(s: &str) -> (String, &str) {
114 let bytes = s.as_bytes();
115 if ON_WINDOWS && bytes.len() >= 2 && bytes[1] == b':' && bytes[0].is_ascii_alphabetic() {
116 let rest = &s[2..];
117 return (format!("{}/", &s[..2].to_ascii_uppercase()), rest.trim_start_matches('/'));
118 }
119 if s.starts_with('/') { ("/".to_owned(), s) } else { (String::new(), s) }
120}
121
122fn eq_root(a: &str, b: &str) -> bool {
123 if ON_WINDOWS { a.eq_ignore_ascii_case(b) } else { a == b }
124}
125
126fn eq_name(a: &str, b: &str) -> bool {
127 if ON_WINDOWS { a.eq_ignore_ascii_case(b) } else { a == b }
128}