b5f0be2
//! validate: common input-validation helpers shared across the crate.
/// RFC 3986 hex-digit check for a single byte.
fn is_hex(b: u8) -> bool {
b.is_ascii_hexdigit()
}
/// Validate a git object id string (hex) of the given byte length.
///
/// Returns `Ok(())` if `s` is exactly `len` hexadecimal characters.
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(())
}
/// Validate a short name (branch/tag) before it is interpolated into a ref path.
///
/// Rejects empty names, path separators, `..`, leading `/`, whitespace and
/// control characters. An already-qualified `refs/...` name is rejected here;
/// callers that accept fully-qualified names must validate separately.
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(())
}
/// Validate a fully-qualified ref name (start with `refs/`).
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> {
// Reject local/absolute paths and file:// — only explicit http(s) or ssh
// 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}"
))),
}
}
/// Maximum size (bytes) for a single object written to the object database.
pub const MAX_OBJECT_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB
/// Maximum size (bytes) for a blob read into memory.
pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024; // 100 MiB
/// Enforce a maximum object size.
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());
}
}