refactor(bare): enhance security and performance optimizations
- Remove unnecessary sorting in advertise_refs for deterministic output - Add path traversal detection and validation in bare_dir construction - Implement symlink resolution checks to prevent security vulnerabilities - Refactor cache system with CRC validation and improved metrics - Integrate repo-specific cache invalidation using indexed keys - Add comprehensive unit tests for commit operations and diff functionality - Move configuration constants to centralized config module - Optimize string operations in disk cache random value generation - Enhance license detection algorithm with cleaner matching logic - Streamline argument processing in various git operations - Update dependencies including crc32fast and flate2 for performance - Add signal handling capability to tokio runtime configuration
This commit is contained in:
@@ -519,3 +519,221 @@ async fn test_oid_binary_encoding() {
|
||||
let hex_from_bytes: String = oid.value.iter().map(|b| format!("{b:02x}")).collect();
|
||||
assert_eq!(hex_from_bytes, oid.hex);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_count_commits_head() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.count_commits(CountCommitsRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: String::new(),
|
||||
path: String::new(),
|
||||
since: String::new(),
|
||||
until: String::new(),
|
||||
}).unwrap();
|
||||
assert_eq!(resp.count, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_commits_with_revision() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.count_commits(CountCommitsRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: "feature".into(),
|
||||
path: String::new(),
|
||||
since: String::new(),
|
||||
until: String::new(),
|
||||
}).unwrap();
|
||||
assert_eq!(resp.count, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_commits_with_path() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.count_commits(CountCommitsRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: "main".into(),
|
||||
path: "README.md".into(),
|
||||
since: String::new(),
|
||||
until: String::new(),
|
||||
}).unwrap();
|
||||
assert!(resp.count >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_count_diverging_commits() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.count_diverging_commits(CountDivergingCommitsRequest {
|
||||
repository: Some(hdr()),
|
||||
left: "feature".into(),
|
||||
right: "main".into(),
|
||||
}).unwrap();
|
||||
assert_eq!(resp.left_count, 0);
|
||||
assert_eq!(resp.right_count, 3);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_find_commit_by_oid() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let commit = gb.find_commit(FindCommitRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: common::oid_selector(&oid),
|
||||
include_stats: false,
|
||||
}).unwrap();
|
||||
assert!(!commit.oid.as_ref().unwrap().hex.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_commit_by_revision() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let commit = gb.find_commit(FindCommitRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: common::rev_selector("main"),
|
||||
include_stats: false,
|
||||
}).unwrap();
|
||||
assert!(!commit.oid.as_ref().unwrap().hex.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_commit_default_head() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let commit = gb.find_commit(FindCommitRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: None,
|
||||
include_stats: false,
|
||||
}).unwrap();
|
||||
assert!(!commit.oid.as_ref().unwrap().hex.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_list_commits_by_oid() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let oid_bytes = gitks::oid::hex_to_bytes(&oid).unwrap();
|
||||
let resp = gb.list_commits_by_oid(ListCommitsByOidRequest {
|
||||
repository: Some(hdr()),
|
||||
oids: vec![oid_bytes],
|
||||
include_stats: false,
|
||||
}).unwrap();
|
||||
assert_eq!(resp.commits.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_list_commits_by_oid_empty() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.list_commits_by_oid(ListCommitsByOidRequest {
|
||||
repository: Some(hdr()),
|
||||
oids: vec![],
|
||||
include_stats: false,
|
||||
}).unwrap();
|
||||
assert!(resp.commits.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_commits_by_message_basic() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.commits_by_message(CommitsByMessageRequest {
|
||||
repository: Some(hdr()),
|
||||
query: "initial".into(),
|
||||
revision: String::new(),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
case_insensitive: false,
|
||||
}).unwrap();
|
||||
assert_eq!(resp.commits.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commits_by_message_case_insensitive() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.commits_by_message(CommitsByMessageRequest {
|
||||
repository: Some(hdr()),
|
||||
query: "INITIAL".into(),
|
||||
revision: String::new(),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
case_insensitive: true,
|
||||
}).unwrap();
|
||||
assert_eq!(resp.commits.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commits_by_message_no_match() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.commits_by_message(CommitsByMessageRequest {
|
||||
repository: Some(hdr()),
|
||||
query: "zzzznonexistent".into(),
|
||||
revision: String::new(),
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
case_insensitive: false,
|
||||
}).unwrap();
|
||||
assert!(resp.commits.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_check_objects_exist() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.check_objects_exist(CheckObjectsExistRequest {
|
||||
repository: Some(hdr()),
|
||||
revisions: vec![oid.clone(), "HEAD".into(), "nonexistent-branch".into()],
|
||||
}).unwrap();
|
||||
assert_eq!(resp.revisions.len(), 3);
|
||||
assert!(resp.revisions[0].exists);
|
||||
assert!(resp.revisions[1].exists);
|
||||
assert!(!resp.revisions[2].exists);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_get_commit_stats() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let stats = gb.get_commit_stats(GetCommitStatsRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: common::oid_selector(&oid),
|
||||
}).unwrap();
|
||||
assert!(stats.changed_files >= 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_commit_stats_default() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let stats = gb.get_commit_stats(GetCommitStatsRequest {
|
||||
repository: Some(hdr()),
|
||||
revision: None,
|
||||
}).unwrap();
|
||||
assert!(stats.changed_files >= 1);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_last_commit_for_path() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.last_commit_for_path(LastCommitForPathRequest {
|
||||
repository: Some(hdr()),
|
||||
path: "README.md".into(),
|
||||
revision: "main".into(),
|
||||
literal_pathspec: false,
|
||||
}).unwrap();
|
||||
assert!(resp.commit.is_some());
|
||||
assert_eq!(resp.path, "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_commit_for_path_nonexistent() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.last_commit_for_path(LastCommitForPathRequest {
|
||||
repository: Some(hdr()),
|
||||
path: "nonexistent.txt".into(),
|
||||
revision: "main".into(),
|
||||
literal_pathspec: false,
|
||||
}).unwrap();
|
||||
assert!(resp.commit.is_none());
|
||||
}
|
||||
|
||||
@@ -172,3 +172,52 @@ pub fn setup_bare_repo_with_conflict() -> (tempfile::TempDir, GitBare) {
|
||||
|
||||
(dir, GitBare::new(bare_dir))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_oid(gb: &GitBare, rev: &str) -> String {
|
||||
let output = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir",
|
||||
gb.bare_dir.to_string_lossy().as_ref(),
|
||||
"rev-parse",
|
||||
rev,
|
||||
])
|
||||
.output()
|
||||
.expect("git rev-parse");
|
||||
assert!(output.status.success(), "git rev-parse {rev} failed");
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_main_oid(gb: &GitBare) -> String {
|
||||
get_oid(gb, "refs/heads/main")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn get_feature_oid(gb: &GitBare) -> String {
|
||||
get_oid(gb, "refs/heads/feature")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn oid_selector(hex: &str) -> Option<gitks::pb::ObjectSelector> {
|
||||
Some(gitks::pb::ObjectSelector {
|
||||
selector: Some(gitks::pb::object_selector::Selector::Oid(
|
||||
gitks::pb::Oid {
|
||||
hex: hex.to_string(),
|
||||
value: vec![],
|
||||
format: 0,
|
||||
},
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn rev_selector(rev: &str) -> Option<gitks::pb::ObjectSelector> {
|
||||
Some(gitks::pb::ObjectSelector {
|
||||
selector: Some(gitks::pb::object_selector::Selector::Revision(
|
||||
gitks::pb::ObjectName {
|
||||
revision: rev.to_string(),
|
||||
},
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -266,3 +266,79 @@ async fn test_get_patch() {
|
||||
.collect();
|
||||
assert!(combined.contains("diff --git") || combined.contains("@@"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_find_changed_paths() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let resp = gb.find_changed_paths(FindChangedPathsRequest {
|
||||
repository: Some(hdr()),
|
||||
base: feature_oid,
|
||||
head: main_oid,
|
||||
paths: vec![],
|
||||
}).unwrap();
|
||||
assert!(!resp.paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_changed_paths_same_ref() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.find_changed_paths(FindChangedPathsRequest {
|
||||
repository: Some(hdr()),
|
||||
base: oid.clone(),
|
||||
head: oid,
|
||||
paths: vec![],
|
||||
}).unwrap();
|
||||
assert!(resp.paths.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_raw_diff() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let chunks = gb.raw_diff(RawDiffRequest {
|
||||
repository: Some(hdr()),
|
||||
base: feature_oid,
|
||||
head: main_oid,
|
||||
options: None,
|
||||
}).unwrap();
|
||||
assert!(!chunks.is_empty());
|
||||
let combined: Vec<u8> = chunks.iter().flat_map(|c| c.data.clone()).collect();
|
||||
let text = String::from_utf8_lossy(&combined);
|
||||
assert!(text.contains("diff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_raw_patch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let chunks = gb.raw_patch(RawPatchRequest {
|
||||
repository: Some(hdr()),
|
||||
base: feature_oid,
|
||||
head: main_oid,
|
||||
}).unwrap();
|
||||
assert!(!chunks.is_empty());
|
||||
let combined: Vec<u8> = chunks.iter().flat_map(|c| c.data.clone()).collect();
|
||||
let text = String::from_utf8_lossy(&combined);
|
||||
assert!(text.contains("From"));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_get_raw_changes() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let resp = gb.get_raw_changes(GetRawChangesRequest {
|
||||
repository: Some(hdr()),
|
||||
base: feature_oid,
|
||||
head: main_oid,
|
||||
}).unwrap();
|
||||
assert!(!resp.changes.is_empty());
|
||||
}
|
||||
|
||||
@@ -63,3 +63,102 @@ fn test_list_refs_direct() {
|
||||
assert_eq!(oid.hex.len(), 40, "SHA-1 hex should be 40 chars");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_write_ref_and_ref_exists() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.write_ref(gitks::pb::WriteRefRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/test-write".into(),
|
||||
new_oid: oid,
|
||||
old_oid: String::new(),
|
||||
force: false,
|
||||
}).unwrap();
|
||||
assert!(resp.ok, "write_ref failed: {}", resp.error);
|
||||
|
||||
let exists = gb.ref_exists(gitks::pb::RefExistsRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/test-write".into(),
|
||||
}).unwrap();
|
||||
assert!(exists.exists);
|
||||
|
||||
let not_exists = gb.ref_exists(gitks::pb::RefExistsRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/nonexistent".into(),
|
||||
}).unwrap();
|
||||
assert!(!not_exists.exists);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_update_references_batch() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.update_references(gitks::pb::UpdateReferencesRequest {
|
||||
repository: Some(hdr()),
|
||||
updates: vec![
|
||||
gitks::pb::RefUpdateEntry {
|
||||
ref_name: "refs/heads/batch-a".into(),
|
||||
new_oid: oid.clone(),
|
||||
old_oid: String::new(),
|
||||
},
|
||||
gitks::pb::RefUpdateEntry {
|
||||
ref_name: "refs/heads/batch-b".into(),
|
||||
new_oid: oid,
|
||||
old_oid: String::new(),
|
||||
},
|
||||
],
|
||||
}).unwrap();
|
||||
assert!(resp.failed_refs.is_empty(), "error: {}", resp.error);
|
||||
|
||||
let a = gb.ref_exists(gitks::pb::RefExistsRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/batch-a".into(),
|
||||
}).unwrap();
|
||||
assert!(a.exists);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_references_empty() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.update_references(gitks::pb::UpdateReferencesRequest {
|
||||
repository: Some(hdr()),
|
||||
updates: vec![],
|
||||
}).unwrap();
|
||||
assert!(resp.failed_refs.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_refs() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
gb.write_ref(gitks::pb::WriteRefRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/to-delete".into(),
|
||||
new_oid: oid,
|
||||
old_oid: String::new(),
|
||||
force: false,
|
||||
}).unwrap();
|
||||
|
||||
let resp = gb.delete_refs(gitks::pb::DeleteRefsRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_names: vec!["refs/heads/to-delete".into()],
|
||||
}).unwrap();
|
||||
assert!(resp.failed_refs.is_empty());
|
||||
|
||||
let exists = gb.ref_exists(gitks::pb::RefExistsRequest {
|
||||
repository: Some(hdr()),
|
||||
ref_name: "refs/heads/to-delete".into(),
|
||||
}).unwrap();
|
||||
assert!(!exists.exists);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_find_default_branch_name() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.find_default_branch_name().unwrap();
|
||||
assert_eq!(resp.name, "main");
|
||||
}
|
||||
|
||||
@@ -326,3 +326,193 @@ async fn test_exists_nonexistent_repo() {
|
||||
.into_inner();
|
||||
assert!(!result.exists);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_find_merge_base() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let resp = gb.find_merge_base(FindMergeBaseRequest {
|
||||
repository: Some(header(&gb)),
|
||||
revisions: vec![
|
||||
main_oid.as_bytes().to_vec(),
|
||||
feature_oid.as_bytes().to_vec(),
|
||||
],
|
||||
}).unwrap();
|
||||
assert!(!resp.base_oid.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_merge_base_empty() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.find_merge_base(FindMergeBaseRequest {
|
||||
repository: Some(header(&gb)),
|
||||
revisions: vec![],
|
||||
}).unwrap();
|
||||
assert!(resp.base_oid.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_merge_base_single() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.find_merge_base(FindMergeBaseRequest {
|
||||
repository: Some(header(&gb)),
|
||||
revisions: vec![oid.as_bytes().to_vec()],
|
||||
}).unwrap();
|
||||
assert!(!resp.base_oid.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_is_ancestor() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let resp = gb.commit_is_ancestor(CommitIsAncestorRequest {
|
||||
repository: Some(header(&gb)),
|
||||
ancestor_oid: feature_oid,
|
||||
descendant_oid: main_oid,
|
||||
}).unwrap();
|
||||
assert!(resp.is_ancestor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_commit_is_ancestor_false() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let main_oid = common::get_main_oid(&gb);
|
||||
let feature_oid = common::get_feature_oid(&gb);
|
||||
let resp = gb.commit_is_ancestor(CommitIsAncestorRequest {
|
||||
repository: Some(header(&gb)),
|
||||
ancestor_oid: main_oid,
|
||||
descendant_oid: feature_oid,
|
||||
}).unwrap();
|
||||
assert!(!resp.is_ancestor);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_objects_size() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let oid = common::get_main_oid(&gb);
|
||||
let resp = gb.objects_size(ObjectsSizeRequest {
|
||||
repository: Some(header(&gb)),
|
||||
oids: vec![oid.clone(), "0000000000000000000000000000000000000000".into()],
|
||||
}).unwrap();
|
||||
assert_eq!(resp.sizes.len(), 2);
|
||||
assert!(resp.sizes[0].found);
|
||||
assert!(resp.sizes[0].size > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_objects_size_empty() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.objects_size(ObjectsSizeRequest {
|
||||
repository: Some(header(&gb)),
|
||||
oids: vec![],
|
||||
}).unwrap();
|
||||
assert!(resp.sizes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_repository_size() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.repository_size().unwrap();
|
||||
assert!(resp.size_bytes > 0);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_find_license_no_license() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.find_license().unwrap();
|
||||
assert!(resp.license_spdx.is_empty());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_optimize_repository_heuristic() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.optimize_repository(OptimizeRepositoryRequest {
|
||||
repository: Some(header(&gb)),
|
||||
strategy: OptimizeStrategy::Heuristic as i32,
|
||||
}).unwrap();
|
||||
assert!(resp.ok);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optimize_repository_incremental() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.optimize_repository(OptimizeRepositoryRequest {
|
||||
repository: Some(header(&gb)),
|
||||
strategy: OptimizeStrategy::Incremental as i32,
|
||||
}).unwrap();
|
||||
assert!(resp.ok);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_search_files_by_content() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.search_files_by_content(SearchFilesByContentRequest {
|
||||
repository: Some(header(&gb)),
|
||||
query: "Test".into(),
|
||||
revision: "main".into(),
|
||||
max_results: 10,
|
||||
case_sensitive: true,
|
||||
}).unwrap();
|
||||
assert!(!resp.results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_files_by_content_no_match() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.search_files_by_content(SearchFilesByContentRequest {
|
||||
repository: Some(header(&gb)),
|
||||
query: "zzzznonexistentzzzz".into(),
|
||||
revision: "main".into(),
|
||||
max_results: 10,
|
||||
case_sensitive: true,
|
||||
}).unwrap();
|
||||
assert!(resp.results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_files_by_content_empty_query() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.search_files_by_content(SearchFilesByContentRequest {
|
||||
repository: Some(header(&gb)),
|
||||
query: String::new(),
|
||||
revision: "main".into(),
|
||||
max_results: 10,
|
||||
case_sensitive: true,
|
||||
});
|
||||
assert!(resp.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_files_by_name() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.search_files_by_name(SearchFilesByNameRequest {
|
||||
repository: Some(header(&gb)),
|
||||
query: "README".into(),
|
||||
revision: "main".into(),
|
||||
max_results: 10,
|
||||
recursive: true,
|
||||
}).unwrap();
|
||||
assert!(!resp.results.is_empty());
|
||||
assert!(resp.results.iter().any(|r| r.path.contains("README")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_files_by_name_no_match() {
|
||||
let (_dir, gb) = common::setup_bare_repo();
|
||||
let resp = gb.search_files_by_name(SearchFilesByNameRequest {
|
||||
repository: Some(header(&gb)),
|
||||
query: "zzzznonexistentzzzz".into(),
|
||||
revision: "main".into(),
|
||||
max_results: 10,
|
||||
recursive: true,
|
||||
}).unwrap();
|
||||
assert!(resp.results.is_empty());
|
||||
}
|
||||
|
||||
+182
-5
@@ -1,6 +1,5 @@
|
||||
use gitks::sanitize::*;
|
||||
|
||||
// ==================== validate_ref_name tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_ref_name_accepts_valid_names() {
|
||||
@@ -69,7 +68,6 @@ fn test_validate_ref_name_rejects_too_long() {
|
||||
assert!(validate_ref_name(&max_valid_name).is_ok());
|
||||
}
|
||||
|
||||
// ==================== validate_revision tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_revision_accepts_empty() {
|
||||
@@ -149,7 +147,6 @@ fn test_validate_revision_accepts_valid_branch_names() {
|
||||
assert!(validate_revision("v1.0.0").is_ok());
|
||||
}
|
||||
|
||||
// ==================== validate_file_path tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_file_path_accepts_valid_paths() {
|
||||
@@ -216,7 +213,6 @@ fn test_validate_file_path_rejects_windows_reserved_names() {
|
||||
assert!(validate_file_path("CON.txt").is_err());
|
||||
}
|
||||
|
||||
// ==================== validate_relative_path tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_relative_path_accepts_valid_paths() {
|
||||
@@ -244,7 +240,6 @@ fn test_validate_relative_path_rejects_traversal() {
|
||||
assert!(validate_relative_path("path/..").is_err());
|
||||
}
|
||||
|
||||
// ==================== validate_config_key tests ====================
|
||||
|
||||
#[test]
|
||||
fn test_validate_config_key_accepts_safe_keys() {
|
||||
@@ -281,3 +276,185 @@ fn test_validate_config_key_rejects_invalid_chars() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user