use anyhow::{Result, bail};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::{Path, PathBuf};
use std::str::FromStr;
fn validate_crate_name(name: &str) -> Result<()> {
if name.contains("..") || name.contains("/") || name.contains("\\") {
bail!(
"Invalid crate name '{}': contains path separators or traversal sequences",
name
);
}
if name.starts_with('/')
|| name.starts_with('\\')
|| (name.len() > 2 && name.chars().nth(1) == Some(':'))
{
bail!(
"Invalid crate name '{}': appears to be an absolute path",
name
);
}
if !name
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-')
{
bail!(
"Invalid crate name '{}': contains invalid characters. Only alphanumeric, underscore, and dash are allowed",
name
);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CrateIdentifier {
name: String,
version: String,
}
impl CrateIdentifier {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Result<Self> {
let name = name.into();
let version = version.into();
if name.is_empty() {
bail!("Crate name cannot be empty");
}
validate_crate_name(&name)?;
if version.is_empty() {
bail!("Crate version cannot be empty");
}
Ok(Self { name, version })
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
}
impl fmt::Display for CrateIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}-{}", self.name, self.version)
}
}
impl FromStr for CrateIdentifier {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
let parts: Vec<&str> = s.rsplitn(2, '-').collect();
if parts.len() != 2 {
bail!("Invalid crate identifier format. Expected 'name-version'");
}
let version = parts[0];
let name = parts[1];
Self::new(name, version)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MemberPath {
path: PathBuf,
member_name: String,
}
impl MemberPath {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
if path.as_os_str().is_empty() {
bail!("Member path cannot be empty");
}
let member_name = path
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| anyhow::anyhow!("Invalid member path: no file name component"))?
.to_string();
Ok(Self {
path: path.to_path_buf(),
member_name,
})
}
}
impl fmt::Display for MemberPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.path.display())
}
}
impl FromStr for MemberPath {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
Self::new(s)
}
}
impl AsRef<Path> for MemberPath {
fn as_ref(&self) -> &Path {
&self.path
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crate_identifier() -> Result<()> {
let id = CrateIdentifier::new("serde", "1.0.0")?;
assert_eq!(id.name(), "serde");
assert_eq!(id.version(), "1.0.0");
assert_eq!(id.to_string(), "serde-1.0.0");
assert!(CrateIdentifier::new("", "1.0.0").is_err());
assert!(CrateIdentifier::new("serde", "").is_err());
Ok(())
}
#[test]
fn test_crate_identifier_security_validation() -> Result<()> {
assert!(CrateIdentifier::new("../../../etc/passwd", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate/../../../etc", "1.0.0").is_err());
assert!(CrateIdentifier::new("..", "1.0.0").is_err());
assert!(CrateIdentifier::new(".", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate/subcrate", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate\\subcrate", "1.0.0").is_err());
assert!(CrateIdentifier::new("/absolute/path", "1.0.0").is_err());
assert!(CrateIdentifier::new("\\absolute\\path", "1.0.0").is_err());
assert!(CrateIdentifier::new("C:\\windows", "1.0.0").is_err());
assert!(CrateIdentifier::new("C:/windows", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate$name", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate@name", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate name", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate\nname", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate\0name", "1.0.0").is_err());
assert!(CrateIdentifier::new("valid_crate", "1.0.0").is_ok());
assert!(CrateIdentifier::new("valid-crate", "1.0.0").is_ok());
assert!(CrateIdentifier::new("Valid123", "1.0.0").is_ok());
assert!(CrateIdentifier::new("a", "1.0.0").is_ok());
Ok(())
}
#[test]
fn test_crate_identifier_from_str() -> Result<()> {
let id: CrateIdentifier = "serde-1.0.0".parse()?;
assert_eq!(id.name(), "serde");
assert_eq!(id.version(), "1.0.0");
let id: CrateIdentifier = "rust-docs-mcp-0.1.0".parse()?;
assert_eq!(id.name(), "rust-docs-mcp");
assert_eq!(id.version(), "0.1.0");
assert!("invalid".parse::<CrateIdentifier>().is_err());
Ok(())
}
#[test]
fn test_member_path() -> Result<()> {
let member = MemberPath::new("crates/rmcp")?;
assert_eq!(member.path, Path::new("crates/rmcp"));
assert_eq!(member.member_name, "rmcp");
assert!(MemberPath::new("").is_err());
Ok(())
}
#[test]
fn test_validate_crate_name() {
assert!(validate_crate_name("serde").is_ok());
assert!(validate_crate_name("tokio-util").is_ok());
assert!(validate_crate_name("async_trait").is_ok());
assert!(validate_crate_name("log2").is_ok());
assert!(validate_crate_name("h3").is_ok());
assert!(validate_crate_name("../etc/passwd").is_err());
assert!(validate_crate_name("crate/../../../etc").is_err());
assert!(validate_crate_name("..").is_err());
assert!(validate_crate_name("./config").is_err());
assert!(validate_crate_name("crate/..").is_err());
assert!(validate_crate_name("some/path").is_err());
assert!(validate_crate_name("some\\path").is_err());
assert!(validate_crate_name("path/to/crate").is_err());
assert!(validate_crate_name("/etc/passwd").is_err());
assert!(validate_crate_name("\\Windows\\System32").is_err());
assert!(validate_crate_name("C:\\Windows").is_err());
assert!(validate_crate_name("C:").is_err());
assert!(validate_crate_name("crate@2.0").is_err());
assert!(validate_crate_name("my crate").is_err());
assert!(validate_crate_name("crate!name").is_err());
assert!(validate_crate_name("crate#name").is_err());
assert!(validate_crate_name("crate$name").is_err());
}
#[test]
fn test_crate_identifier_validation() {
assert!(CrateIdentifier::new("serde", "1.0.0").is_ok());
assert!(CrateIdentifier::new("tokio-util", "0.7.0").is_ok());
assert!(CrateIdentifier::new("../malicious", "1.0.0").is_err());
assert!(CrateIdentifier::new("/etc/passwd", "1.0.0").is_err());
assert!(CrateIdentifier::new("crate@2.0", "1.0.0").is_err());
assert!(CrateIdentifier::new("", "1.0.0").is_err());
assert!(CrateIdentifier::new("serde", "").is_err());
}
}