rust_docs_mcp/cache/
types.rs1use anyhow::{Result, bail};
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11
12fn validate_crate_name(name: &str) -> Result<()> {
14 if name.contains("..") || name.contains("/") || name.contains("\\") {
16 bail!(
17 "Invalid crate name '{}': contains path separators or traversal sequences",
18 name
19 );
20 }
21
22 if name.starts_with('/')
24 || name.starts_with('\\')
25 || (name.len() > 2 && name.chars().nth(1) == Some(':'))
26 {
27 bail!(
28 "Invalid crate name '{}': appears to be an absolute path",
29 name
30 );
31 }
32
33 if !name
35 .chars()
36 .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
37 {
38 bail!(
39 "Invalid crate name '{}': contains invalid characters. Only alphanumeric, underscore, and dash are allowed",
40 name
41 );
42 }
43
44 Ok(())
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
49pub struct CrateIdentifier {
50 name: String,
51 version: String,
52}
53
54impl CrateIdentifier {
55 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self> {
57 let name = name.into();
58 let version = version.into();
59
60 if name.is_empty() {
62 bail!("Crate name cannot be empty");
63 }
64
65 validate_crate_name(&name)?;
67
68 if version.is_empty() {
70 bail!("Crate version cannot be empty");
71 }
72
73 Ok(Self { name, version })
74 }
75
76 pub fn name(&self) -> &str {
78 &self.name
79 }
80
81 pub fn version(&self) -> &str {
83 &self.version
84 }
85}
86
87impl fmt::Display for CrateIdentifier {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "{}-{}", self.name, self.version)
90 }
91}
92
93impl FromStr for CrateIdentifier {
94 type Err = anyhow::Error;
95
96 fn from_str(s: &str) -> Result<Self> {
97 let parts: Vec<&str> = s.rsplitn(2, '-').collect();
98 if parts.len() != 2 {
99 bail!("Invalid crate identifier format. Expected 'name-version'");
100 }
101
102 let version = parts[0];
104 let name = parts[1];
105
106 Self::new(name, version)
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
112pub struct MemberPath {
113 path: PathBuf,
114 member_name: String,
115}
116
117impl MemberPath {
118 pub fn new(path: impl AsRef<Path>) -> Result<Self> {
120 let path = path.as_ref();
121
122 if path.as_os_str().is_empty() {
124 bail!("Member path cannot be empty");
125 }
126
127 let member_name = path
129 .file_name()
130 .and_then(|n| n.to_str())
131 .ok_or_else(|| anyhow::anyhow!("Invalid member path: no file name component"))?
132 .to_string();
133
134 Ok(Self {
135 path: path.to_path_buf(),
136 member_name,
137 })
138 }
139}
140
141impl fmt::Display for MemberPath {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{}", self.path.display())
144 }
145}
146
147impl FromStr for MemberPath {
148 type Err = anyhow::Error;
149
150 fn from_str(s: &str) -> Result<Self> {
151 Self::new(s)
152 }
153}
154
155impl AsRef<Path> for MemberPath {
156 fn as_ref(&self) -> &Path {
157 &self.path
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_crate_identifier() -> Result<()> {
167 let id = CrateIdentifier::new("serde", "1.0.0")?;
168 assert_eq!(id.name(), "serde");
169 assert_eq!(id.version(), "1.0.0");
170 assert_eq!(id.to_string(), "serde-1.0.0");
171
172 assert!(CrateIdentifier::new("", "1.0.0").is_err());
174 assert!(CrateIdentifier::new("serde", "").is_err());
175
176 Ok(())
177 }
178
179 #[test]
180 fn test_crate_identifier_security_validation() -> Result<()> {
181 assert!(CrateIdentifier::new("../../../etc/passwd", "1.0.0").is_err());
183 assert!(CrateIdentifier::new("crate/../../../etc", "1.0.0").is_err());
184 assert!(CrateIdentifier::new("..", "1.0.0").is_err());
185 assert!(CrateIdentifier::new(".", "1.0.0").is_err());
186
187 assert!(CrateIdentifier::new("crate/subcrate", "1.0.0").is_err());
189 assert!(CrateIdentifier::new("crate\\subcrate", "1.0.0").is_err());
190 assert!(CrateIdentifier::new("/absolute/path", "1.0.0").is_err());
191 assert!(CrateIdentifier::new("\\absolute\\path", "1.0.0").is_err());
192 assert!(CrateIdentifier::new("C:\\windows", "1.0.0").is_err());
193 assert!(CrateIdentifier::new("C:/windows", "1.0.0").is_err());
194
195 assert!(CrateIdentifier::new("crate$name", "1.0.0").is_err());
197 assert!(CrateIdentifier::new("crate@name", "1.0.0").is_err());
198 assert!(CrateIdentifier::new("crate name", "1.0.0").is_err());
199 assert!(CrateIdentifier::new("crate\nname", "1.0.0").is_err());
200 assert!(CrateIdentifier::new("crate\0name", "1.0.0").is_err());
201
202 assert!(CrateIdentifier::new("valid_crate", "1.0.0").is_ok());
204 assert!(CrateIdentifier::new("valid-crate", "1.0.0").is_ok());
205 assert!(CrateIdentifier::new("Valid123", "1.0.0").is_ok());
206 assert!(CrateIdentifier::new("a", "1.0.0").is_ok());
207
208 Ok(())
209 }
210
211 #[test]
212 fn test_crate_identifier_from_str() -> Result<()> {
213 let id: CrateIdentifier = "serde-1.0.0".parse()?;
214 assert_eq!(id.name(), "serde");
215 assert_eq!(id.version(), "1.0.0");
216
217 let id: CrateIdentifier = "rust-docs-mcp-0.1.0".parse()?;
219 assert_eq!(id.name(), "rust-docs-mcp");
220 assert_eq!(id.version(), "0.1.0");
221
222 assert!("invalid".parse::<CrateIdentifier>().is_err());
224
225 Ok(())
226 }
227
228 #[test]
229 fn test_member_path() -> Result<()> {
230 let member = MemberPath::new("crates/rmcp")?;
231 assert_eq!(member.path, Path::new("crates/rmcp"));
232 assert_eq!(member.member_name, "rmcp");
233
234 assert!(MemberPath::new("").is_err());
236
237 Ok(())
238 }
239
240 #[test]
241 fn test_validate_crate_name() {
242 assert!(validate_crate_name("serde").is_ok());
244 assert!(validate_crate_name("tokio-util").is_ok());
245 assert!(validate_crate_name("async_trait").is_ok());
246 assert!(validate_crate_name("log2").is_ok());
247 assert!(validate_crate_name("h3").is_ok());
248
249 assert!(validate_crate_name("../etc/passwd").is_err());
251 assert!(validate_crate_name("crate/../../../etc").is_err());
252 assert!(validate_crate_name("..").is_err());
253 assert!(validate_crate_name("./config").is_err());
254 assert!(validate_crate_name("crate/..").is_err());
255
256 assert!(validate_crate_name("some/path").is_err());
258 assert!(validate_crate_name("some\\path").is_err());
259 assert!(validate_crate_name("path/to/crate").is_err());
260
261 assert!(validate_crate_name("/etc/passwd").is_err());
263 assert!(validate_crate_name("\\Windows\\System32").is_err());
264 assert!(validate_crate_name("C:\\Windows").is_err());
265 assert!(validate_crate_name("C:").is_err());
266
267 assert!(validate_crate_name("crate@2.0").is_err());
269 assert!(validate_crate_name("my crate").is_err());
270 assert!(validate_crate_name("crate!name").is_err());
271 assert!(validate_crate_name("crate#name").is_err());
272 assert!(validate_crate_name("crate$name").is_err());
273 }
274
275 #[test]
276 fn test_crate_identifier_validation() {
277 assert!(CrateIdentifier::new("serde", "1.0.0").is_ok());
279 assert!(CrateIdentifier::new("tokio-util", "0.7.0").is_ok());
280
281 assert!(CrateIdentifier::new("../malicious", "1.0.0").is_err());
283 assert!(CrateIdentifier::new("/etc/passwd", "1.0.0").is_err());
284 assert!(CrateIdentifier::new("crate@2.0", "1.0.0").is_err());
285
286 assert!(CrateIdentifier::new("", "1.0.0").is_err());
288 assert!(CrateIdentifier::new("serde", "").is_err());
289 }
290}