use std::fs::File;
use std::io::Read;
use std::path::{Component, Path};
pub const MAX_SOURCE_FILE_BYTES: u64 = 8 * 1024 * 1024;
pub const MAX_ADVISORY_FILE_BYTES: u64 = 1024 * 1024;
pub const MAX_DIR_DEPTH: usize = 32;
pub const MAX_DIR_ENTRIES: usize = 200_000;
pub const MAX_NAME_LEN: usize = 64;
pub const MAX_VERSION_LEN: usize = 64;
pub fn is_safe_crate_name(name: &str) -> bool {
!name.is_empty()
&& name.len() <= MAX_NAME_LEN
&& name
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
}
pub fn is_safe_version(version: &str) -> bool {
!version.is_empty()
&& version.len() <= MAX_VERSION_LEN
&& version
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'+' | b'-' | b'_'))
&& version != ".."
&& !version.contains("..")
}
pub fn is_safe_path_segment(segment: &str) -> bool {
if segment.is_empty() || segment.len() > 255 {
return false;
}
if segment.contains('/') || segment.contains('\\') || segment.contains('\0') {
return false;
}
!matches!(segment, "." | "..")
}
pub fn is_contained_within(base: &Path, child: &Path) -> bool {
match (base.canonicalize(), child.canonicalize()) {
(Ok(b), Ok(c)) => c.starts_with(&b),
_ => false,
}
}
pub fn has_no_parent_components(path: &Path) -> bool {
!path.components().any(|c| matches!(c, Component::ParentDir))
}
pub fn read_file_capped(path: &Path, max_bytes: u64) -> Option<String> {
let file = File::open(path).ok()?;
let meta = file.metadata().ok()?;
if !meta.is_file() {
return None;
}
if meta.len() > max_bytes {
return None;
}
let mut bytes = Vec::new();
file.take(max_bytes).read_to_end(&mut bytes).ok()?;
Some(String::from_utf8_lossy(&bytes).into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crate_name_allows_normal() {
assert!(is_safe_crate_name("serde"));
assert!(is_safe_crate_name("openssl-sys"));
assert!(is_safe_crate_name("wasm_bindgen"));
assert!(is_safe_crate_name("a1"));
}
#[test]
fn crate_name_rejects_traversal_and_injection() {
assert!(!is_safe_crate_name(""));
assert!(!is_safe_crate_name(".."));
assert!(!is_safe_crate_name("../etc"));
assert!(!is_safe_crate_name("foo/bar"));
assert!(!is_safe_crate_name("foo\\bar"));
assert!(!is_safe_crate_name("foo bar"));
assert!(!is_safe_crate_name("foo\0"));
assert!(!is_safe_crate_name("a/../../b"));
assert!(!is_safe_crate_name("café")); assert!(!is_safe_crate_name(&"a".repeat(65)));
assert!(!is_safe_crate_name("evil.com"));
assert!(!is_safe_crate_name("crate@host"));
}
#[test]
fn version_validation() {
assert!(is_safe_version("1.0.0"));
assert!(is_safe_version("0.9.99"));
assert!(is_safe_version("1.0.0+spec-1.1.0"));
assert!(!is_safe_version("../1.0.0"));
assert!(!is_safe_version("1.0.0/.."));
assert!(!is_safe_version(".."));
assert!(!is_safe_version("1 0"));
assert!(!is_safe_version(""));
}
#[test]
fn path_segment_validation() {
assert!(is_safe_path_segment("serde-1.0.0"));
assert!(!is_safe_path_segment(".."));
assert!(!is_safe_path_segment("a/b"));
assert!(!is_safe_path_segment(""));
}
#[test]
fn containment_blocks_escape() {
let base = std::env::temp_dir();
assert!(is_contained_within(&base, &base));
assert!(!is_contained_within(&base, std::path::Path::new("/")));
}
#[test]
fn no_parent_components_detects_dotdot() {
assert!(has_no_parent_components(Path::new("a/b/c")));
assert!(!has_no_parent_components(Path::new("a/../b")));
}
#[test]
fn read_cap_rejects_oversize() {
let dir = std::env::temp_dir();
let path = dir.join("rustinel_safety_big.txt");
std::fs::write(&path, vec![b'a'; 1024]).unwrap();
assert!(read_file_capped(&path, 4096).is_some());
assert!(read_file_capped(&path, 512).is_none());
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_cap_decodes_non_utf8_lossily() {
let dir = std::env::temp_dir();
let path = dir.join("rustinel_safety_nonutf8.rs");
let mut bytes = b"fn x(){ reqwest::get(\"https://x.workers.dev\");".to_vec();
bytes.push(0xFF);
bytes.extend_from_slice(b" }");
std::fs::write(&path, &bytes).unwrap();
let got = read_file_capped(&path, 4096).expect("file must not be dropped");
assert!(got.contains(".workers.dev"));
let _ = std::fs::remove_file(&path);
}
}