1use std::fmt;
2use std::str::FromStr;
3
4use crate::{Error, Result};
5
6#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct RustLibPath(String);
11
12impl RustLibPath {
13 pub fn new(path: impl Into<String>) -> Result<Self> {
15 let path = path.into();
16 validate(&path)?;
17 Ok(Self(path))
18 }
19
20 pub fn as_str(&self) -> &str {
22 &self.0
23 }
24
25 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}