use crate::error::ArchiveError;
use crate::error::Result;
pub const MAX_PATH_LENGTH: usize = 4096;
pub fn validate_raw_path_str(path: &str) -> Result<()> {
if path.len() > MAX_PATH_LENGTH {
return Err(ArchiveError::SecurityViolation {
reason: format!(
"path exceeds maximum length of {MAX_PATH_LENGTH} bytes (got {} bytes)",
path.len()
),
});
}
if path.contains('\0') {
return Err(ArchiveError::SecurityViolation {
reason: "path contains null bytes - potential security issue".to_string(),
});
}
Ok(())
}
pub const MAX_CONFIG_ENTRY_LENGTH: usize = 255;
pub fn validate_config_entry(value: &str, field: &str) -> Result<()> {
if value.len() > MAX_CONFIG_ENTRY_LENGTH {
return Err(ArchiveError::InvalidConfiguration {
reason: format!(
"{field} exceeds maximum length of {MAX_CONFIG_ENTRY_LENGTH} bytes (got {} bytes)",
value.len()
),
});
}
if value.contains('\0') {
return Err(ArchiveError::InvalidConfiguration {
reason: format!("{field} contains null bytes - potential security issue"),
});
}
if value.is_empty() {
return Err(ArchiveError::InvalidConfiguration {
reason: format!("{field} must not be empty"),
});
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_validate_raw_path_str_accepts_normal() {
assert!(validate_raw_path_str("/tmp/test.tar.gz").is_ok());
assert!(validate_raw_path_str("relative/path.tar").is_ok());
assert!(validate_raw_path_str("").is_ok());
}
#[test]
fn test_validate_raw_path_str_rejects_null_bytes() {
let err = validate_raw_path_str("/tmp/test\0malicious").expect_err("null byte");
match err {
ArchiveError::SecurityViolation { reason } => {
assert_eq!(
reason,
"path contains null bytes - potential security issue"
);
}
other => panic!("expected SecurityViolation, got: {other:?}"),
}
}
#[test]
fn test_validate_raw_path_str_rejects_too_long() {
let long_path = "x".repeat(MAX_PATH_LENGTH + 1);
let err = validate_raw_path_str(&long_path).expect_err("too long");
match err {
ArchiveError::SecurityViolation { reason } => {
assert_eq!(
reason,
format!(
"path exceeds maximum length of {MAX_PATH_LENGTH} bytes (got {} bytes)",
long_path.len()
)
);
}
other => panic!("expected SecurityViolation, got: {other:?}"),
}
}
#[test]
fn test_validate_raw_path_str_checks_length_before_null_byte() {
let long_path_with_null = format!("{}\0", "x".repeat(MAX_PATH_LENGTH));
let err = validate_raw_path_str(&long_path_with_null).expect_err("too long");
match err {
ArchiveError::SecurityViolation { reason } => {
assert!(
reason.contains("maximum length"),
"length check should run first: {reason}"
);
}
other => panic!("expected SecurityViolation, got: {other:?}"),
}
}
#[test]
fn test_validate_raw_path_str_accepts_max_length() {
let max_path = "x".repeat(MAX_PATH_LENGTH);
assert!(validate_raw_path_str(&max_path).is_ok());
}
#[test]
fn test_validate_config_entry_accepts_normal() {
assert!(validate_config_entry("txt", "extension").is_ok());
assert!(validate_config_entry(".git", "banned path component").is_ok());
}
#[test]
fn test_validate_config_entry_rejects_empty() {
let err = validate_config_entry("", "extension").expect_err("empty");
match err {
ArchiveError::InvalidConfiguration { reason } => {
assert_eq!(reason, "extension must not be empty");
}
other => panic!("expected InvalidConfiguration, got: {other:?}"),
}
}
#[test]
fn test_validate_config_entry_rejects_null_bytes() {
let err = validate_config_entry("bad\0ext", "extension").expect_err("null byte");
match err {
ArchiveError::InvalidConfiguration { reason } => {
assert_eq!(
reason,
"extension contains null bytes - potential security issue"
);
}
other => panic!("expected InvalidConfiguration, got: {other:?}"),
}
}
#[test]
fn test_validate_config_entry_accepts_max_length() {
let max_entry = "x".repeat(MAX_CONFIG_ENTRY_LENGTH);
assert!(validate_config_entry(&max_entry, "extension").is_ok());
}
#[test]
fn test_validate_config_entry_rejects_too_long() {
let long_entry = "x".repeat(MAX_CONFIG_ENTRY_LENGTH + 1);
let err = validate_config_entry(&long_entry, "extension").expect_err("too long");
match err {
ArchiveError::InvalidConfiguration { reason } => {
assert_eq!(
reason,
format!(
"extension exceeds maximum length of {MAX_CONFIG_ENTRY_LENGTH} bytes (got {} bytes)",
long_entry.len()
)
);
}
other => panic!("expected InvalidConfiguration, got: {other:?}"),
}
}
#[test]
fn test_validate_config_entry_rejects_multibyte_over_length() {
let multibyte_entry = "日".repeat(100);
assert!(multibyte_entry.chars().count() < MAX_CONFIG_ENTRY_LENGTH);
let err =
validate_config_entry(&multibyte_entry, "extension").expect_err("too long in bytes");
match err {
ArchiveError::InvalidConfiguration { reason } => {
assert!(
reason.contains("bytes"),
"message must use byte-length wording: {reason}"
);
}
other => panic!("expected InvalidConfiguration, got: {other:?}"),
}
}
#[test]
fn test_validate_config_entry_checks_length_before_null_byte() {
let long_entry_with_null = format!("{}\0", "x".repeat(MAX_CONFIG_ENTRY_LENGTH));
let err = validate_config_entry(&long_entry_with_null, "extension").expect_err("too long");
match err {
ArchiveError::InvalidConfiguration { reason } => {
assert!(
reason.contains("maximum length"),
"length check should run first: {reason}"
);
}
other => panic!("expected InvalidConfiguration, got: {other:?}"),
}
}
}