Skip to main content

kcode_rust_libs/
path.rs

1use std::fmt;
2use std::str::FromStr;
3
4use crate::{Error, Result};
5
6/// A canonical, UTF-8 path to a file relative to a managed Rust library root.
7///
8/// The serialized/displayed form always uses `/` separators.
9#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct RustLibPath(String);
11
12impl RustLibPath {
13    /// Validate and construct a library-relative path.
14    pub fn new(path: impl Into<String>) -> Result<Self> {
15        let path = path.into();
16        validate(&path)?;
17        Ok(Self(path))
18    }
19
20    /// Return the canonical string representation.
21    pub fn as_str(&self) -> &str {
22        &self.0
23    }
24
25    /// Consume the path and return its string representation.
26    pub fn into_string(self) -> String {
27        self.0
28    }
29}
30
31impl AsRef<str> for RustLibPath {
32    fn as_ref(&self) -> &str {
33        self.as_str()
34    }
35}
36
37impl fmt::Display for RustLibPath {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.write_str(self.as_str())
40    }
41}
42
43impl FromStr for RustLibPath {
44    type Err = Error;
45
46    fn from_str(value: &str) -> Result<Self> {
47        Self::new(value)
48    }
49}
50
51impl TryFrom<String> for RustLibPath {
52    type Error = Error;
53
54    fn try_from(value: String) -> Result<Self> {
55        Self::new(value)
56    }
57}
58
59impl TryFrom<&str> for RustLibPath {
60    type Error = Error;
61
62    fn try_from(value: &str) -> Result<Self> {
63        Self::new(value)
64    }
65}
66
67fn validate(path: &str) -> Result<()> {
68    let invalid = |reason| Error::InvalidRustLibPath {
69        path: path.to_owned(),
70        reason,
71    };
72
73    if path.is_empty() {
74        return Err(invalid("path is empty"));
75    }
76    if path.starts_with('/') {
77        return Err(invalid("absolute paths are not allowed"));
78    }
79    if path.ends_with('/') {
80        return Err(invalid("trailing '/' is not allowed"));
81    }
82    if path.contains('\\') {
83        return Err(invalid("use '/' rather than backslash as the separator"));
84    }
85    if path.contains(':') {
86        return Err(invalid("':' is not allowed in Rust library paths"));
87    }
88    if path.contains('\0') {
89        return Err(invalid("NUL is not allowed"));
90    }
91
92    for component in path.split('/') {
93        if component.is_empty() {
94            return Err(invalid("empty path components are not allowed"));
95        }
96        if component == "." || component == ".." {
97            return Err(invalid("'.' and '..' components are not allowed"));
98        }
99    }
100
101    Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106    use super::RustLibPath;
107
108    #[test]
109    fn accepts_canonical_relative_paths() {
110        for path in ["Cargo.toml", "src/lib.rs", ".gitignore", "资料/代码.rs"] {
111            assert!(RustLibPath::new(path).is_ok(), "{path}");
112        }
113    }
114
115    #[test]
116    fn rejects_unsafe_or_noncanonical_paths() {
117        for path in [
118            "",
119            "/etc/passwd",
120            "../outside",
121            "src/../outside",
122            "./src/lib.rs",
123            "src//lib.rs",
124            "src/lib.rs/",
125            "src\\lib.rs",
126            "C:drive-relative.rs",
127            "nul\0name",
128        ] {
129            assert!(RustLibPath::new(path).is_err(), "{path:?}");
130        }
131    }
132}