463 lines
13 KiB
Rust
463 lines
13 KiB
Rust
//! Copyright (c) 2022-2026 GitDataAi All rights reserved.
|
|
|
|
use gitks::sanitize::*;
|
|
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_accepts_valid_names() {
|
|
assert!(validate_ref_name("main").is_ok());
|
|
assert!(validate_ref_name("master").is_ok());
|
|
assert!(validate_ref_name("feature/new-api").is_ok());
|
|
assert!(validate_ref_name("hotfix/bug-fix-123").is_ok());
|
|
assert!(validate_ref_name("v1.0.0").is_ok());
|
|
assert!(validate_ref_name("release-2024").is_ok());
|
|
assert!(validate_ref_name("user/feature/branch").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_empty() {
|
|
assert!(validate_ref_name("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_dot_prefix_suffix() {
|
|
assert!(validate_ref_name(".branch").is_err());
|
|
assert!(validate_ref_name("branch.").is_err());
|
|
assert!(validate_ref_name(".branch.").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_slash_suffix() {
|
|
assert!(validate_ref_name("branch/").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_double_dot() {
|
|
assert!(validate_ref_name("feature..branch").is_err());
|
|
assert!(validate_ref_name("..branch").is_err());
|
|
assert!(validate_ref_name("branch..").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_at_brace() {
|
|
assert!(validate_ref_name("branch@{1}").is_err());
|
|
assert!(validate_ref_name("@{upstream}").is_err());
|
|
assert!(validate_ref_name("feature/@{branch}").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_forbidden_chars() {
|
|
assert!(validate_ref_name("branch~1").is_err());
|
|
assert!(validate_ref_name("branch^1").is_err());
|
|
assert!(validate_ref_name("branch:feature").is_err());
|
|
assert!(validate_ref_name("branch?query").is_err());
|
|
assert!(validate_ref_name("branch*glob").is_err());
|
|
assert!(validate_ref_name("branch[0]").is_err());
|
|
assert!(validate_ref_name("branch\\escape").is_err());
|
|
assert!(validate_ref_name("branch name").is_err());
|
|
assert!(validate_ref_name("branch\ttab").is_err());
|
|
assert!(validate_ref_name("branch\nnewline").is_err());
|
|
assert!(validate_ref_name("branch\rreturn").is_err());
|
|
assert!(validate_ref_name("branch\0null").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_ref_name_rejects_too_long() {
|
|
let long_name = "a".repeat(256);
|
|
assert!(validate_ref_name(&long_name).is_err());
|
|
|
|
let max_valid_name = "a".repeat(255);
|
|
assert!(validate_ref_name(&max_valid_name).is_ok());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_empty() {
|
|
assert!(validate_revision("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_head() {
|
|
assert!(validate_revision("HEAD").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_valid_hex() {
|
|
assert!(validate_revision("abc1234").is_ok());
|
|
assert!(validate_revision("abc1234567890123456789012345678901234567890").is_ok());
|
|
assert!(validate_revision("deadbeef").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_rejects_invalid_hex_length() {
|
|
// "abc" is 3 chars - too short to be hex OID (requires 4-64), but valid as branch name
|
|
assert!(validate_revision("abc").is_ok());
|
|
|
|
let too_long = "a".repeat(65);
|
|
// 65 hex chars - too long to be hex OID, but might be valid as branch name
|
|
// However, it will fail ref name length check (> 255 chars would fail, but 65 is fine)
|
|
// Actually 65 chars of 'a' is a valid branch name, so it should pass
|
|
assert!(validate_revision(&too_long).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_ref_prefix() {
|
|
assert!(validate_revision("ref:refs/heads/main").is_ok());
|
|
assert!(validate_revision("ref:refs/tags/v1.0.0").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_ancestry_operators() {
|
|
assert!(validate_revision("main~1").is_ok());
|
|
assert!(validate_revision("main~10").is_ok());
|
|
assert!(validate_revision("HEAD~10000").is_ok());
|
|
assert!(validate_revision("main^1").is_ok());
|
|
assert!(validate_revision("main^2").is_ok());
|
|
assert!(validate_revision("HEAD^10000").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_rejects_excessive_depth() {
|
|
assert!(validate_revision("main~10001").is_err());
|
|
assert!(validate_revision("main~999999999999").is_err());
|
|
assert!(validate_revision("main^10001").is_err());
|
|
assert!(validate_revision("main^999999999999").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_tree_suffix() {
|
|
assert!(validate_revision("main^{tree}").is_ok());
|
|
assert!(validate_revision("HEAD^{tree}").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_rejects_too_long() {
|
|
// Test length limit (256 chars) - use a simple branch name to avoid depth checks
|
|
let long_rev = "a".repeat(257);
|
|
assert!(validate_revision(&long_rev).is_err());
|
|
|
|
// For non-hex revisions, the effective limit is 255 chars (ref name limit)
|
|
let max_valid = "a".repeat(255);
|
|
assert!(validate_revision(&max_valid).is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_revision_accepts_valid_branch_names() {
|
|
assert!(validate_revision("main").is_ok());
|
|
assert!(validate_revision("feature/new-api").is_ok());
|
|
// v1.0.0 contains dots but they're not at start/end, so it's valid
|
|
assert!(validate_revision("v1.0.0").is_ok());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn test_validate_file_path_accepts_valid_paths() {
|
|
assert!(validate_file_path("file.txt").is_ok());
|
|
assert!(validate_file_path("src/main.rs").is_ok());
|
|
assert!(validate_file_path("deep/nested/path/file.js").is_ok());
|
|
assert!(validate_file_path("README.md").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_empty() {
|
|
assert!(validate_file_path("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_absolute_paths() {
|
|
assert!(validate_file_path("/etc/passwd").is_err());
|
|
assert!(validate_file_path("/absolute/path").is_err());
|
|
assert!(validate_file_path("/file.txt").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_path_traversal() {
|
|
assert!(validate_file_path("../escape").is_err());
|
|
assert!(validate_file_path("path/../escape").is_err());
|
|
assert!(validate_file_path("path/../../escape").is_err());
|
|
assert!(validate_file_path("..").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_null_bytes() {
|
|
assert!(validate_file_path("file\0.txt").is_err());
|
|
assert!(validate_file_path("path/\0escape").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_git_directory() {
|
|
assert!(validate_file_path(".git").is_err());
|
|
assert!(validate_file_path(".git/config").is_err());
|
|
assert!(validate_file_path(".git/hooks/pre-commit").is_err());
|
|
assert!(validate_file_path("path/.git/config").is_err());
|
|
assert!(validate_file_path(".git/").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_file_path_rejects_too_long() {
|
|
let long_path = "a".repeat(4097);
|
|
assert!(validate_file_path(&long_path).is_err());
|
|
|
|
let max_valid_path = "a".repeat(4096);
|
|
assert!(validate_file_path(&max_valid_path).is_ok());
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[test]
|
|
fn test_validate_file_path_rejects_windows_reserved_names() {
|
|
assert!(validate_file_path("CON").is_err());
|
|
assert!(validate_file_path("PRN").is_err());
|
|
assert!(validate_file_path("AUX").is_err());
|
|
assert!(validate_file_path("NUL").is_err());
|
|
assert!(validate_file_path("COM1").is_err());
|
|
assert!(validate_file_path("LPT1").is_err());
|
|
assert!(validate_file_path("path/CON").is_err());
|
|
assert!(validate_file_path("CON.txt").is_err());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn test_validate_relative_path_accepts_valid_paths() {
|
|
assert!(validate_relative_path("repo").is_ok());
|
|
assert!(validate_relative_path("path/to/repo").is_ok());
|
|
assert!(validate_relative_path("user/project").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_relative_path_rejects_empty() {
|
|
assert!(validate_relative_path("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_relative_path_rejects_absolute() {
|
|
assert!(validate_relative_path("/absolute/path").is_err());
|
|
assert!(validate_relative_path("/etc").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_relative_path_rejects_traversal() {
|
|
assert!(validate_relative_path("../escape").is_err());
|
|
assert!(validate_relative_path("path/../escape").is_err());
|
|
assert!(validate_relative_path("..").is_err());
|
|
assert!(validate_relative_path("path/..").is_err());
|
|
}
|
|
|
|
|
|
#[test]
|
|
fn test_validate_config_key_accepts_safe_keys() {
|
|
assert!(validate_config_key("user.name").is_ok());
|
|
assert!(validate_config_key("user.email").is_ok());
|
|
assert!(validate_config_key("core.editor").is_ok());
|
|
assert!(validate_config_key("alias.co").is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_config_key_rejects_empty() {
|
|
assert!(validate_config_key("").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_config_key_rejects_dangerous_keys() {
|
|
assert!(validate_config_key("core.sshCommand").is_err());
|
|
assert!(validate_config_key("core.hooksPath").is_err());
|
|
assert!(validate_config_key("safe.directory").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_config_key_rejects_wildcard_dangerous_keys() {
|
|
assert!(validate_config_key("remote.origin.url").is_err());
|
|
assert!(validate_config_key("remote.upstream.url").is_err());
|
|
assert!(validate_config_key("http.proxy").is_err());
|
|
assert!(validate_config_key("https.proxy").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_validate_config_key_rejects_invalid_chars() {
|
|
assert!(validate_config_key("key with space").is_err());
|
|
assert!(validate_config_key("key;rm -rf").is_err());
|
|
assert!(validate_config_key("key$(command)").is_err());
|
|
assert!(validate_config_key("key`command`").is_err());
|
|
}
|
|
|
|
|
|
/// Ensure no input causes panic in validate_ref_name.
|
|
#[test]
|
|
fn fuzz_validate_ref_name_no_panic() {
|
|
let long_name = "x".repeat(300);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"\0",
|
|
"\0\0\0",
|
|
"\x7f",
|
|
"\x01\x02\x03",
|
|
"~^:?*[]\\ ",
|
|
"../../../etc/passwd",
|
|
"a/b/c/d/e/f/g/h",
|
|
&long_name,
|
|
"branch@{upstream}",
|
|
"HEAD~99999999999",
|
|
"HEAD^99999999999",
|
|
"ref:HEAD",
|
|
"ref:refs/heads/main",
|
|
"; rm -rf /",
|
|
"$(echo pwned)",
|
|
"`echo pwned`",
|
|
"\n\r\t",
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_ref_name(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_revision.
|
|
#[test]
|
|
fn fuzz_validate_revision_no_panic() {
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"HEAD",
|
|
"HEAD~0",
|
|
"HEAD~99999999",
|
|
"HEAD^0",
|
|
"HEAD^99999999",
|
|
"HEAD^{tree}",
|
|
"HEAD^{commit}",
|
|
"HEAD^{object}",
|
|
"abcdef01",
|
|
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
|
|
"0000",
|
|
"zzzz",
|
|
"ref:HEAD",
|
|
"ref:refs/heads/main",
|
|
"\0",
|
|
"branch~abc",
|
|
"branch^abc",
|
|
"branch~",
|
|
"branch^",
|
|
"a~10001",
|
|
"a^10001",
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_revision(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_file_path.
|
|
#[test]
|
|
fn fuzz_validate_file_path_no_panic() {
|
|
let long_path = "x".repeat(5000);
|
|
let medium_path = "a".repeat(100);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"/etc/passwd",
|
|
"../escape",
|
|
"a/../b",
|
|
".git",
|
|
".git/config",
|
|
"src/.git/HEAD",
|
|
"a/b/.git",
|
|
"\0",
|
|
"\0\0\0",
|
|
&long_path,
|
|
"path/with\x00null",
|
|
"path/with\nnewline",
|
|
"normal/path.txt",
|
|
&medium_path,
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_file_path(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_remote_url.
|
|
#[test]
|
|
fn fuzz_validate_remote_url_no_panic() {
|
|
let long_url = "x".repeat(5000);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"https://github.com/user/repo",
|
|
"http://localhost:3000/repo",
|
|
"ssh://git@host/repo",
|
|
"git://host/repo",
|
|
"git+ssh://git@host/repo",
|
|
"file:///etc/passwd",
|
|
"ext::sh -c 'rm -rf /'",
|
|
"ftp://host/repo",
|
|
"https://user:pass@host/repo",
|
|
"\0",
|
|
"https://host\0injection",
|
|
&long_url,
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_remote_url(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_oid_hex.
|
|
#[test]
|
|
fn fuzz_validate_oid_hex_no_panic() {
|
|
let long_hex = "x".repeat(65);
|
|
let exact_hex = "x".repeat(64);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"abc",
|
|
"abcd",
|
|
"0123456789abcdef",
|
|
"ZZZZ",
|
|
"g000",
|
|
"0000000000000000000000000000000000000000",
|
|
&long_hex,
|
|
&exact_hex,
|
|
"\0",
|
|
" ",
|
|
"\n",
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_oid_hex(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_relative_path.
|
|
#[test]
|
|
fn fuzz_validate_relative_path_no_panic() {
|
|
let long_path = "x".repeat(5000);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"/absolute",
|
|
"relative/path",
|
|
"../escape",
|
|
"path/../escape",
|
|
"\0",
|
|
&long_path,
|
|
".",
|
|
"..",
|
|
"...",
|
|
"a/b/c",
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_relative_path(input);
|
|
}
|
|
}
|
|
|
|
/// Ensure no input causes panic in validate_refspec.
|
|
#[test]
|
|
fn fuzz_validate_refspec_no_panic() {
|
|
let long_refspec = "x".repeat(2000);
|
|
let test_inputs: Vec<&str> = vec![
|
|
"",
|
|
"+refs/heads/*:refs/heads/*",
|
|
"refs/heads/main",
|
|
"; rm -rf /",
|
|
"$(evil)",
|
|
"`evil`",
|
|
"| pipe",
|
|
"& bg",
|
|
"< redirect",
|
|
"> redirect",
|
|
"\0",
|
|
&long_refspec,
|
|
];
|
|
for input in test_inputs {
|
|
let _ = validate_refspec(input);
|
|
}
|
|
}
|