Skip to main content

ktrs_cli/ktlint/
jpath.rs

1//! `java.nio.file.Path` semantics that ktlint's file handling depends on, over `/`-separated strings (on
2//! Windows, `\` is read as `/`): parsing drops redundant separators, `resolve` keeps `.`/`..`, `normalize`
3//! is lexical, `startsWith` compares whole names.
4
5use std::path::PathBuf;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct JPath {
9    /// `""` (relative), `"/"` or, on Windows, `"C:/"`.
10    root: String,
11    names: Vec<String>,
12}
13
14pub const ON_WINDOWS: bool = cfg!(windows);
15
16impl JPath {
17    /// `Paths.get(s)`; `None` where Windows throws `InvalidPathException` (wildcards and the like).
18    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    /// `resolve(other)`: `other` when absolute, else appended (no normalization).
40    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    /// Kotlin's `relativeToOrSelf(base)` (`PathRelativizer`): `None` where it would throw.
83    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    /// `pathString` with `/` separators (ktlint replaces `File.separatorChar` with `/` before showing one).
108    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}