fn is_hex(b: u8) -> bool { b.is_ascii_hexdigit() } pub fn validate_oid(s: &str, len: u64) -> Result<(), crate::Error> { if s.len() as u64 != len || !s.bytes().all(is_hex) { return Err(crate::Error::InvalidRevSpec(format!( "invalid object id: expected {len} hex chars" ))); } Ok(()) } pub fn validate_short_name(kind: &str, name: &str) -> Result<(), crate::Error> { if name.is_empty() { return Err(crate::Error::InvalidRefName(format!("empty {kind} name"))); } if name.contains('/') || name.contains("..") || name.starts_with('/') { return Err(crate::Error::InvalidRefName(format!( "invalid {kind} name: {name}" ))); } if name.starts_with("refs/") { return Err(crate::Error::InvalidRefName(format!( "expected short {kind} name, got qualified: {name}" ))); } if name .bytes() .any(|b| b <= 0x20 || b == 0x7f || b == b'~' || b == b'^' || b == b':') { return Err(crate::Error::InvalidRefName(format!( "invalid {kind} name: {name}" ))); } Ok(()) } pub fn validate_qualified_ref(name: &str) -> Result<(), crate::Error> { if !name.starts_with("refs/") { return Err(crate::Error::InvalidRefName(format!( "expected qualified ref, got: {name}" ))); } if name.contains("..") || name.contains("@{") || name.contains("//") { return Err(crate::Error::InvalidRefName(format!( "invalid ref name: {name}" ))); } Ok(()) } /// Validate a remote URL scheme; returns the scheme if allowed. pub fn allowed_remote_scheme(url: &str) -> Result<(), crate::Error> { // are allowed for remote operations. let scheme = url .split_once("://") .map(|(s, _)| s.to_ascii_lowercase()) .unwrap_or_default(); match scheme.as_str() { "http" | "https" | "ssh" => Ok(()), _ => Err(crate::Error::InvalidRevSpec(format!( "remote url scheme not allowed: {url}" ))), } } pub const MAX_OBJECT_SIZE: u64 = 100 * 1024 * 1024; pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024; pub fn enforce_max_size(data: &[u8], max: u64, what: &str) -> Result<(), crate::Error> { if data.len() as u64 > max { return Err(crate::Error::Other(format!( "{what} exceeds maximum size of {max} bytes" ))); } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn oid_validation() { assert!(validate_oid(&"a".repeat(64), 64).is_ok()); assert!(validate_oid(&"g".repeat(64), 64).is_err()); assert!(validate_oid(&"a".repeat(63), 64).is_err()); assert!(validate_oid(&"A".repeat(64), 64).is_ok()); } #[test] fn short_name_validation() { assert!(validate_short_name("branch", "feature/x").is_err()); assert!(validate_short_name("branch", "..").is_err()); assert!(validate_short_name("branch", "refs/heads/main").is_err()); assert!(validate_short_name("branch", "").is_err()); assert!(validate_short_name("branch", "feature_x").is_ok()); assert!(validate_short_name("branch", "main").is_ok()); } #[test] fn qualified_ref_validation() { assert!(validate_qualified_ref("refs/heads/main").is_ok()); assert!(validate_qualified_ref("main").is_err()); assert!(validate_qualified_ref("refs/heads/ma..in").is_err()); } #[test] fn remote_scheme_validation() { assert!(allowed_remote_scheme("https://example.com/x").is_ok()); assert!(allowed_remote_scheme("ssh://git@example.com/x").is_ok()); assert!(allowed_remote_scheme("/etc/passwd").is_err()); assert!(allowed_remote_scheme("file:///etc/passwd").is_err()); assert!(allowed_remote_scheme("tmp/repo").is_err()); } #[test] fn size_limit() { assert!(enforce_max_size(&[0u8; 10], 100, "obj").is_ok()); assert!(enforce_max_size(&[0u8; 200], 100, "obj").is_err()); } }