From ecfd322420c67b651a8cbf041e17d13edbe1e867 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:51 +0700 Subject: [PATCH 01/41] chore(workspace): update Cargo.lock for handler benchmarks --- Cargo.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4aed18a..950d5c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -283,6 +283,9 @@ dependencies = [ [[package]] name = "libvctrl_handler" version = "5.0.1" +dependencies = [ + "criterion", +] [[package]] name = "libvctrl_plumbing" From 29efd32bf2c7f7a7dae96f4aa7e99d5ab56f8cc2 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 02/41] chore(handler): add criterion dev-dependency and benchmark target --- libvctrl_handler/Cargo.toml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/libvctrl_handler/Cargo.toml b/libvctrl_handler/Cargo.toml index dcde7cb..e34ad00 100644 --- a/libvctrl_handler/Cargo.toml +++ b/libvctrl_handler/Cargo.toml @@ -13,4 +13,11 @@ keywords = ["version-control", "vcs", "library", "traits"] categories = ["development-tools", "data-structures"] [lints] -workspace = true \ No newline at end of file +workspace = true + +[dev-dependencies] +criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] } + +[[bench]] +name = "handler_bench" +harness = false \ No newline at end of file From 8550155b4bea9eb55d847222a639b7d1bbc7c984 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 03/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/constants.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/libvctrl_handler/src/constants.rs b/libvctrl_handler/src/constants.rs index 3ec33f1..1369874 100644 --- a/libvctrl_handler/src/constants.rs +++ b/libvctrl_handler/src/constants.rs @@ -1,24 +1,14 @@ pub mod entry_mode { - pub const BLOB: u32 = 0o100_644; - pub const EXECUTABLE: u32 = 0o100_755; - pub const SYMLINK: u32 = 0o120_000; - pub const TREE: u32 = 0o40_000; - pub const SUBMODULE: u32 = 0o160_000; } pub const HASH_LENGTH: usize = 64; - pub const MAX_NAME_LENGTH: u64 = 255; - pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024; - pub const MAX_TREE_ENTRIES: u64 = 100_000; - pub const MAX_MESSAGE_LENGTH: u64 = 1024 * 1024; - pub const MAX_PARENT_COUNT: u64 = 0xFFFF; From 2f5946ba4273e274438a5932fe48f287ec3ea085 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 04/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/enums/core/entry_kind.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libvctrl_handler/src/enums/core/entry_kind.rs b/libvctrl_handler/src/enums/core/entry_kind.rs index 6805719..3f2a9a5 100644 --- a/libvctrl_handler/src/enums/core/entry_kind.rs +++ b/libvctrl_handler/src/enums/core/entry_kind.rs @@ -4,13 +4,9 @@ use crate::constants::entry_mode; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum EntryKind { Blob, - Executable, - Symlink, - Tree, - Submodule, } From 720336c745cfbca14fcbda10d8451fc493bdad3d Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 05/41] style(handler): remove unnecessary blank line --- libvctrl_handler/src/enums/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libvctrl_handler/src/enums/mod.rs b/libvctrl_handler/src/enums/mod.rs index e91a6bc..f47b173 100644 --- a/libvctrl_handler/src/enums/mod.rs +++ b/libvctrl_handler/src/enums/mod.rs @@ -1,3 +1,2 @@ pub mod core; - pub use core::entry_kind::EntryKind; From a448c7fc7b0697b226c63e2c012c6108fc65a281 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 06/41] refactor(handler): use alloc and reorder imports --- libvctrl_handler/src/errors.rs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/libvctrl_handler/src/errors.rs b/libvctrl_handler/src/errors.rs index 019f2a5..a5a24d8 100644 --- a/libvctrl_handler/src/errors.rs +++ b/libvctrl_handler/src/errors.rs @@ -1,39 +1,27 @@ +use alloc::sync::Arc; +use core::error::Error; +use core::fmt; +use std::io; + use crate::constants::HASH_LENGTH; use crate::types::Hash; -use std::error::Error; -use std::fmt; -use std::io; -use std::sync::Arc; #[non_exhaustive] #[derive(Clone, Debug)] pub enum VctrlError { CorruptedData(String), - DuplicateParent, - ExceededMaxSize(String), - InvalidBlameRange, - InvalidEmail(String), - InvalidHashLength(usize), - InvalidName(String), - InvalidTimezoneOffset(i16), - InvalidTreeStructure(String), - IoError(Arc), - ObjectNotFound(Hash), - Other(String), - RefNotFound(String), - SerializationError(String), } From 5d4345e8949de383ad3a7c9ffde2f95eab9a8c60 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 07/41] refactor(handler): add extern crate and test imports --- libvctrl_handler/src/lib.rs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/libvctrl_handler/src/lib.rs b/libvctrl_handler/src/lib.rs index 4fc5d8f..9f8fd82 100644 --- a/libvctrl_handler/src/lib.rs +++ b/libvctrl_handler/src/lib.rs @@ -1,26 +1,22 @@ -pub mod constants; +extern crate alloc; -pub mod enums; +#[cfg(test)] +use criterion as _; +pub mod constants; +pub mod enums; pub mod errors; - pub mod macros; - pub mod traits; - pub mod types; - pub mod validation; pub use constants::{ HASH_LENGTH, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, MAX_NAME_LENGTH, MAX_PARENT_COUNT, MAX_TREE_ENTRIES, }; - pub use enums::EntryKind; - pub use errors::VctrlError; - pub use traits::core::{ blame::{Blame, BlameEntry}, config::ConfigStore, @@ -39,12 +35,10 @@ pub use traits::core::{ transport::Transport, verifier::Verifier, }; - pub use types::{ Blob, ChangeKind, Commit, CommitMeta, Conflict, FileDelta, Hash, MergeResult, ReflogEntry, Tag, Tree, TreeDelta, TreeEntry, UserID, }; - pub use validation::{ validate_hash_bytes, validate_name, validate_ref_name, validate_tree_entry_name, }; From febecd5205d00fc7bd1e8d8581f71a89351a576a Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 08/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/traits/core/config.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/libvctrl_handler/src/traits/core/config.rs b/libvctrl_handler/src/traits/core/config.rs index 94e87d2..2860cca 100644 --- a/libvctrl_handler/src/traits/core/config.rs +++ b/libvctrl_handler/src/traits/core/config.rs @@ -2,14 +2,9 @@ use crate::errors::VctrlError; pub trait ConfigStore: Send + Sync { fn get_string(&self, section: &str, key: &str) -> Result, VctrlError>; - fn set_string(&mut self, section: &str, key: &str, value: &str) -> Result<(), VctrlError>; - fn get_bool(&self, section: &str, key: &str) -> Result, VctrlError>; - fn set_bool(&mut self, section: &str, key: &str, value: bool) -> Result<(), VctrlError>; - fn remove(&mut self, section: &str, key: &str) -> Result<(), VctrlError>; - fn exists(&self, section: &str, key: &str) -> Result; } From 927be07d899f386dd5da9f52bbd271201c85e310 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:52 +0700 Subject: [PATCH 09/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/decoder.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libvctrl_handler/src/traits/core/decoder.rs b/libvctrl_handler/src/traits/core/decoder.rs index b05633c..45af17d 100644 --- a/libvctrl_handler/src/traits/core/decoder.rs +++ b/libvctrl_handler/src/traits/core/decoder.rs @@ -1,13 +1,11 @@ +use std::io::Read; + use crate::errors::VctrlError; use crate::types::{Blob, Commit, Tag, Tree}; -use std::io::Read; pub trait Decoder: Send + Sync { fn decode_blob(&self, reader: R) -> Result; - fn decode_tree(&self, reader: R) -> Result; - fn decode_commit(&self, reader: R) -> Result; - fn decode_tag(&self, reader: R) -> Result; } From f5bf9bebd580e4a34c67a328d551f0948a372f37 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 10/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/encoder.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libvctrl_handler/src/traits/core/encoder.rs b/libvctrl_handler/src/traits/core/encoder.rs index 3c129b3..aa5641f 100644 --- a/libvctrl_handler/src/traits/core/encoder.rs +++ b/libvctrl_handler/src/traits/core/encoder.rs @@ -1,17 +1,15 @@ +use std::io::Write; + use crate::errors::VctrlError; use crate::types::{Blob, Commit, Tag, Tree}; -use std::io::Write; pub trait Encoder: Send + Sync { fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError>; - fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError>; - fn encode_commit( &self, commit: &Commit, writer: &mut W, ) -> Result<(), VctrlError>; - fn encode_tag(&self, tag: &Tag, writer: &mut W) -> Result<(), VctrlError>; } From 2a7c8ce93e6a439c69a482b9763c845cde3669fe Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 11/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/hasher.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libvctrl_handler/src/traits/core/hasher.rs b/libvctrl_handler/src/traits/core/hasher.rs index a62b5cc..69ea767 100644 --- a/libvctrl_handler/src/traits/core/hasher.rs +++ b/libvctrl_handler/src/traits/core/hasher.rs @@ -1,6 +1,7 @@ +use std::io::Read; + use crate::errors::VctrlError; use crate::types::Hash; -use std::io::Read; pub trait Hasher: Send + Sync { fn hash(&self, reader: R) -> Result; From a04bf14e026b640a60ebe35cb457d0725527a470 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 12/41] refactor(handler): add Clone bound to Entry and clean up --- libvctrl_handler/src/traits/core/index.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/libvctrl_handler/src/traits/core/index.rs b/libvctrl_handler/src/traits/core/index.rs index 12f7247..de484a2 100644 --- a/libvctrl_handler/src/traits/core/index.rs +++ b/libvctrl_handler/src/traits/core/index.rs @@ -1,31 +1,20 @@ use crate::errors::VctrlError; pub trait Index: Send + Sync { - type Entry: Send + Sync; - + type Entry: Clone + Send + Sync; type Path: Send + Sync; - type TreeId: Send + Sync; fn add(&mut self, entry: Self::Entry) -> Result<(), VctrlError>; - fn remove(&mut self, path: &Self::Path) -> Result<(), VctrlError>; - fn clear(&mut self) -> Result<(), VctrlError>; - fn get(&self, path: &Self::Path) -> Result, VctrlError>; - fn contains(&self, path: &Self::Path) -> Result; - fn len(&self) -> Result; - fn is_empty(&self) -> Result { Ok(self.len()? == 0) } - fn entries(&self) -> Result, VctrlError>; - fn write_tree(&self) -> Result; - fn read_tree(&mut self, tree: &Self::TreeId) -> Result<(), VctrlError>; } From c1e66e3243955473df8d84a81697f8e3a616c088 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 13/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/traits/core/mod.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/libvctrl_handler/src/traits/core/mod.rs b/libvctrl_handler/src/traits/core/mod.rs index 0ec6ced..4dad8b4 100644 --- a/libvctrl_handler/src/traits/core/mod.rs +++ b/libvctrl_handler/src/traits/core/mod.rs @@ -1,31 +1,16 @@ pub mod blame; - pub mod config; - pub mod decoder; - pub mod diff; - pub mod encoder; - pub mod hasher; - pub mod index; - pub mod object_store; - pub mod pack; - pub mod ref_store; - pub mod reflog; - pub mod remote; - pub mod revwalk; - pub mod signer; - pub mod transport; - pub mod verifier; From 7f1c5979f6853c80581f5e5866b8c59599eb12d8 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 14/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/object_store.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/libvctrl_handler/src/traits/core/object_store.rs b/libvctrl_handler/src/traits/core/object_store.rs index 45670ad..166c3fc 100644 --- a/libvctrl_handler/src/traits/core/object_store.rs +++ b/libvctrl_handler/src/traits/core/object_store.rs @@ -1,13 +1,11 @@ +use std::io::Read; + use crate::errors::VctrlError; use crate::types::Hash; -use std::io::Read; pub trait ObjectStore: Send + Sync { fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; - fn get(&self, hash: &Hash) -> Result, VctrlError>; - fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError>; - fn exists(&self, hash: &Hash) -> Result; } From 10ecc1c2ddad7f4922b1d16cb33e7a4c200bcad3 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 15/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/pack.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libvctrl_handler/src/traits/core/pack.rs b/libvctrl_handler/src/traits/core/pack.rs index afe4501..c94d7c0 100644 --- a/libvctrl_handler/src/traits/core/pack.rs +++ b/libvctrl_handler/src/traits/core/pack.rs @@ -1,11 +1,11 @@ -use crate::errors::VctrlError; use std::io::Read; +use crate::errors::VctrlError; + pub trait PackWriter: Send + Sync { type ObjectId: Send + Sync; fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError>; - fn finish(&mut self) -> Result<(), VctrlError>; } From 30fca02be1109b83fecf1bd72036dd5165bf297a Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 16/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/traits/core/ref_store.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/libvctrl_handler/src/traits/core/ref_store.rs b/libvctrl_handler/src/traits/core/ref_store.rs index f789a99..c77c603 100644 --- a/libvctrl_handler/src/traits/core/ref_store.rs +++ b/libvctrl_handler/src/traits/core/ref_store.rs @@ -5,10 +5,7 @@ pub trait RefStore: Send + Sync { type RefsIterator: Iterator> + Send; fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError>; - fn get_ref(&self, name: &str) -> Result; - fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError>; - fn list_refs(&self) -> Result; } From 39ebab7f1829311a8a3de67567f7b14acab90b42 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 17/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/traits/core/remote.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/libvctrl_handler/src/traits/core/remote.rs b/libvctrl_handler/src/traits/core/remote.rs index 05b9746..10772c3 100644 --- a/libvctrl_handler/src/traits/core/remote.rs +++ b/libvctrl_handler/src/traits/core/remote.rs @@ -2,12 +2,9 @@ use crate::errors::VctrlError; pub trait Remote: Send + Sync { type RefSpec: Send + Sync; - type RemoteRef: Send + Sync; fn list_refs(&self) -> Result, VctrlError>; - fn fetch(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; - fn push(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; } From d965e5d93e60ce945e1319f7adae784363932dcf Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 18/41] style(handler): reorder imports --- libvctrl_handler/src/traits/core/transport.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libvctrl_handler/src/traits/core/transport.rs b/libvctrl_handler/src/traits/core/transport.rs index c728181..09ed5a1 100644 --- a/libvctrl_handler/src/traits/core/transport.rs +++ b/libvctrl_handler/src/traits/core/transport.rs @@ -1,9 +1,9 @@ +use std::io::Read; + use crate::errors::VctrlError; use crate::types::Hash; -use std::io::Read; pub trait Transport: Send + Sync { fn fetch_object(&self, hash: &Hash) -> Result, VctrlError>; - fn push_object(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; } From 292e769ae1ee80ea6595a1cb5fc53cd912b48b57 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:53 +0700 Subject: [PATCH 19/41] refactor(handler): use HashSet for parent deduplication --- libvctrl_handler/src/types/core/commit.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libvctrl_handler/src/types/core/commit.rs b/libvctrl_handler/src/types/core/commit.rs index e36ff89..874fa7f 100644 --- a/libvctrl_handler/src/types/core/commit.rs +++ b/libvctrl_handler/src/types/core/commit.rs @@ -1,8 +1,9 @@ +use std::collections::HashSet; + use super::hash::Hash; use super::user_id::UserID; use crate::constants::{MAX_MESSAGE_LENGTH, MAX_PARENT_COUNT}; use crate::errors::VctrlError; -use std::collections::HashSet; #[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct CommitMeta { @@ -95,8 +96,8 @@ impl Commit { } let mut seen = HashSet::new(); - for p in &parents { - if !seen.insert(*p) { + for parent in &parents { + if !seen.insert(*parent) { return Err(VctrlError::DuplicateParent); } } From fa727cacbdccfe6800198c1245b353968bb96f28 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 20/41] refactor(handler): use specific imports and iter types --- libvctrl_handler/src/types/core/delta.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/libvctrl_handler/src/types/core/delta.rs b/libvctrl_handler/src/types/core/delta.rs index 01c819b..b40b437 100644 --- a/libvctrl_handler/src/types/core/delta.rs +++ b/libvctrl_handler/src/types/core/delta.rs @@ -1,3 +1,5 @@ +use alloc::vec::IntoIter as VecIntoIter; +use core::slice::Iter as SliceIter; use std::path::{Path, PathBuf}; use crate::Hash; @@ -5,15 +7,10 @@ use crate::Hash; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ChangeKind { Added, - Deleted, - Modified, - TypeChange, - Renamed, - Copied, } @@ -187,7 +184,7 @@ impl TreeDelta { self.changes.is_empty() } - pub fn iter(&self) -> std::slice::Iter<'_, FileDelta> { + pub fn iter(&self) -> SliceIter<'_, FileDelta> { self.changes.iter() } @@ -199,7 +196,7 @@ impl TreeDelta { impl IntoIterator for TreeDelta { type Item = FileDelta; - type IntoIter = std::vec::IntoIter; + type IntoIter = VecIntoIter; fn into_iter(self) -> Self::IntoIter { self.changes.into_iter() @@ -208,7 +205,7 @@ impl IntoIterator for TreeDelta { impl<'a> IntoIterator for &'a TreeDelta { type Item = &'a FileDelta; - type IntoIter = std::slice::Iter<'a, FileDelta>; + type IntoIter = SliceIter<'a, FileDelta>; fn into_iter(self) -> Self::IntoIter { self.iter() From 137b4c123b77a0d8bb283cd1c44201d882482181 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 21/41] refactor(handler): reorder imports and use wrapping_add --- libvctrl_handler/src/types/core/hash.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/libvctrl_handler/src/types/core/hash.rs b/libvctrl_handler/src/types/core/hash.rs index da291ca..e5f8162 100644 --- a/libvctrl_handler/src/types/core/hash.rs +++ b/libvctrl_handler/src/types/core/hash.rs @@ -1,8 +1,9 @@ -use crate::constants::HASH_LENGTH; -use crate::errors::VctrlError; use core::fmt; use core::str::FromStr; +use crate::constants::HASH_LENGTH; +use crate::errors::VctrlError; + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Hash([u8; HASH_LENGTH]); @@ -16,7 +17,7 @@ impl Hash { let mut i = 0; while i < HASH_LENGTH { arr[i] = bytes[i]; - i += 1; + i = i.wrapping_add(1); } Ok(Self(arr)) } From 8bbd536289bee649356d5d7c856ffa2bd365a3e7 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 22/41] style(handler): remove unnecessary blank line --- libvctrl_handler/src/types/core/merge.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/libvctrl_handler/src/types/core/merge.rs b/libvctrl_handler/src/types/core/merge.rs index 8ee8f67..ac2d38a 100644 --- a/libvctrl_handler/src/types/core/merge.rs +++ b/libvctrl_handler/src/types/core/merge.rs @@ -45,7 +45,6 @@ impl Conflict { #[derive(Debug, Clone, PartialEq, Eq)] pub enum MergeResult { Success(Hash), - Conflicts(Vec), } From 1389f45c86d76615f832303fb1b5eea223cb03ad Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 23/41] fix(handler): improve duplicate detection in Tree --- libvctrl_handler/src/types/core/tree.rs | 34 +++++++++++++++---------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/libvctrl_handler/src/types/core/tree.rs b/libvctrl_handler/src/types/core/tree.rs index 72e8274..79a92f6 100644 --- a/libvctrl_handler/src/types/core/tree.rs +++ b/libvctrl_handler/src/types/core/tree.rs @@ -1,9 +1,11 @@ +use core::cmp::Ordering; +use std::collections::HashSet; + use super::hash::Hash; use crate::constants::MAX_TREE_ENTRIES; use crate::enums::EntryKind; use crate::errors::VctrlError; use crate::validation::validate_tree_entry_name; -use std::cmp::Ordering; #[derive(Clone, Debug, PartialEq, Eq)] pub struct TreeEntry { @@ -49,20 +51,19 @@ impl Tree { ))); } - let mut sorted = entries; - sorted.sort_by(compare_tree_entries); - - for window in sorted.windows(2) { - if let (Some(first), Some(second)) = (window.first(), window.get(1)) - && first.name == second.name - { + let mut seen = HashSet::with_capacity(entries.len()); + for entry in &entries { + if !seen.insert(entry.name.clone()) { return Err(VctrlError::InvalidTreeStructure(format!( "duplicate entry name: '{}'", - first.name + entry.name ))); } } + let mut sorted = entries; + sorted.sort_by(compare_tree_entries); + Ok(Self { entries: sorted }) } @@ -83,7 +84,7 @@ impl Tree { #[must_use] pub fn get(&self, name: &str) -> Option<&TreeEntry> { - self.entries.iter().find(|e| e.name == name) + self.entries.iter().find(|entry| entry.name == name) } } @@ -94,8 +95,8 @@ fn compare_tree_entries(a: &TreeEntry, b: &TreeEntry) -> Ordering { let a_is_tree = a.kind == EntryKind::Tree; let b_is_tree = b.kind == EntryKind::Tree; - let a_len = a_bytes.len() + usize::from(a_is_tree); - let b_len = b_bytes.len() + usize::from(b_is_tree); + let a_len = a_bytes.len().wrapping_add(usize::from(a_is_tree)); + let b_len = b_bytes.len().wrapping_add(usize::from(b_is_tree)); let min_len = a_len.min(b_len); for i in 0..min_len { @@ -140,13 +141,18 @@ mod tests { let e2 = TreeEntry::new("a".into(), EntryKind::Blob, h)?; let tree = Tree::new(vec![e1, e2])?; - assert_eq!(tree.entries().first().map(|e| e.name()), Some("a")); - assert_eq!(tree.entries().get(1).map(|e| e.name()), Some("b")); + assert_eq!(tree.entries().first().map(TreeEntry::name), Some("a")); + assert_eq!(tree.entries().get(1).map(TreeEntry::name), Some("b")); let dup1 = TreeEntry::new("x".into(), EntryKind::Blob, h)?; let dup2 = TreeEntry::new("x".into(), EntryKind::Tree, h)?; assert!(Tree::new(vec![dup1, dup2]).is_err()); + let tricky_dup1 = TreeEntry::new("x".into(), EntryKind::Blob, h)?; + let tricky_mid = TreeEntry::new("x.".into(), EntryKind::Blob, h)?; + let tricky_dup2 = TreeEntry::new("x".into(), EntryKind::Tree, h)?; + assert!(Tree::new(vec![tricky_dup1, tricky_mid, tricky_dup2]).is_err()); + Ok(()) } } From ed51e370d62cc1699ab0a39265e9da65d31257b0 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 24/41] style(handler): remove unnecessary blank lines --- libvctrl_handler/src/validation/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/libvctrl_handler/src/validation/mod.rs b/libvctrl_handler/src/validation/mod.rs index 939e2b9..7f580f2 100644 --- a/libvctrl_handler/src/validation/mod.rs +++ b/libvctrl_handler/src/validation/mod.rs @@ -1,7 +1,5 @@ pub mod hash; - pub mod name; pub use hash::validate_hash_bytes; - pub use name::{validate_name, validate_ref_name, validate_tree_entry_name}; From bcf304d3eff0528c7bd05eb88cb91c05c716efa7 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 25/41] fix(handler): validate ref name components more strictly --- libvctrl_handler/src/validation/name.rs | 60 +++++++++++++++---------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/libvctrl_handler/src/validation/name.rs b/libvctrl_handler/src/validation/name.rs index b0b1c4d..4a89992 100644 --- a/libvctrl_handler/src/validation/name.rs +++ b/libvctrl_handler/src/validation/name.rs @@ -1,6 +1,7 @@ +use std::path::Path; + use crate::constants::MAX_NAME_LENGTH; use crate::errors::VctrlError; -use std::path::Path; pub fn validate_name(name: &str) -> Result<(), VctrlError> { if name.is_empty() { @@ -22,33 +23,44 @@ pub fn validate_name(name: &str) -> Result<(), VctrlError> { pub fn validate_ref_name(name: &str) -> Result<(), VctrlError> { validate_name(name)?; - if name.contains("..") - || name.contains('~') - || name.contains('^') - || name.contains(':') - || name.contains('?') - || name.contains('*') - || name.contains('[') - || name.contains('\\') - || name.contains(' ') - || name.contains("@{") - || name.contains("//") - || name.starts_with('.') - || name.starts_with('/') - || name.ends_with('/') - || name.ends_with('.') - || name.contains('<') - || name.contains('>') - || name.contains('|') - || name.contains('"') - || Path::new(name) - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) - { + + if name == "@" { + return Err(VctrlError::InvalidName("ref name cannot be '@'".into())); + } + + if name.starts_with('/') || name.ends_with('/') || name.contains("//") { return Err(VctrlError::InvalidName(format!( "invalid ref name: '{name}'" ))); } + + for component in name.split('/') { + if component.is_empty() + || component.starts_with('.') + || Path::new(component) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) + || component.contains("..") + || component.contains('~') + || component.contains('^') + || component.contains(':') + || component.contains('?') + || component.contains('*') + || component.contains('[') + || component.contains('\\') + || component.contains(' ') + || component.contains("@{") + || component.contains('<') + || component.contains('>') + || component.contains('|') + || component.contains('"') + { + return Err(VctrlError::InvalidName(format!( + "invalid ref name: '{name}'" + ))); + } + } + Ok(()) } From 11c7f882b49fbc112a6bded43beb609164f4cc5c Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 26/41] bench(handler): add handler benchmarks --- libvctrl_handler/benches/handler_bench.rs | 129 ++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 libvctrl_handler/benches/handler_bench.rs diff --git a/libvctrl_handler/benches/handler_bench.rs b/libvctrl_handler/benches/handler_bench.rs new file mode 100644 index 0000000..ed0bc73 --- /dev/null +++ b/libvctrl_handler/benches/handler_bench.rs @@ -0,0 +1,129 @@ +#![allow(missing_docs)] + +use core::hint::black_box; +use core::str::FromStr; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use libvctrl_handler::{ + Blob, Commit, EntryKind, HASH_LENGTH, Hash, Tree, TreeEntry, UserID, validate_ref_name, +}; + +fn build_tree_entries(count: usize) -> Vec { + let hash = Hash::from([0_u8; HASH_LENGTH]); + let mut entries = Vec::with_capacity(count); + for i in 0..count { + let name = format!("file_{i:06}"); + if let Ok(entry) = TreeEntry::new(name, EntryKind::Blob, hash) { + entries.push(entry); + } + } + entries +} + +fn bench_tree_build(c: &mut Criterion) { + let entries = build_tree_entries(5_000); + let _ = c.bench_function("tree/build_5000_entries", |b| { + b.iter_batched( + || entries.clone(), + |entries| { + let _ = black_box(Tree::new(entries)); + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_validate_refs(c: &mut Criterion) { + let valid_refs = [ + "refs/heads/main", + "refs/tags/v1.0.0", + "refs/remotes/origin/feature/foo", + "refs/heads/bar", + "refs/heads/a-branch.name", + ]; + let invalid_refs = [ + "refs/heads/.hidden", + "refs/heads/foo.lock/bar", + "@", + "refs/heads//double", + ]; + + let _ = c.bench_function("validation/ref_name_valid", |b| { + b.iter(|| { + for name in &valid_refs { + let _ = black_box(validate_ref_name(name)); + } + }); + }); + + let _ = c.bench_function("validation/ref_name_invalid", |b| { + b.iter(|| { + for name in &invalid_refs { + let _ = black_box(validate_ref_name(name)); + } + }); + }); +} + +fn bench_hash_parse(c: &mut Criterion) { + let hex_str = "ab".repeat(HASH_LENGTH); // 64 byte hex = 128 char + let _ = c.bench_function("hash/from_hex_string", |b| { + b.iter(|| { + let _ = black_box(Hash::from_str(&hex_str)); + }); + }); +} + +fn bench_blob_new(c: &mut Criterion) { + let data = vec![0x42_u8; 1024 * 1024]; // 1 MiB + let _ = c.bench_function("blob/new_1MiB", |b| { + b.iter_batched( + || data.clone(), + |data| { + let _ = black_box(Blob::new(data)); + }, + BatchSize::LargeInput, + ); + }); +} + +fn build_user() -> Option { + UserID::new("Bench User".into(), "bench@example.com".into()).ok() +} + +fn bench_commit_build(c: &mut Criterion) { + let Some(user) = build_user() else { + return; + }; + let tree_hash = Hash::from([0_u8; HASH_LENGTH]); + let parents: Vec = (0..10).map(|_| Hash::from([1_u8; HASH_LENGTH])).collect(); + let message = "benchmark commit".to_string(); + + let _ = c.bench_function("commit/new_10_parents", |b| { + b.iter_batched( + || { + ( + tree_hash, + parents.clone(), + user.clone(), + user.clone(), + message.clone(), + ) + }, + |(tree, parents, author, committer, msg)| { + let _ = black_box(Commit::new(tree, parents, author, committer, msg)); + }, + BatchSize::SmallInput, + ); + }); +} + +criterion_group!( + benches, + bench_tree_build, + bench_validate_refs, + bench_hash_parse, + bench_blob_new, + bench_commit_build +); +criterion_main!(benches); From 6820a6f7d852f22716462dc739a9d197bd0613ed Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 27/41] test(handler): add blob tests --- libvctrl_handler/tests/blob.rs | 38 ++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 libvctrl_handler/tests/blob.rs diff --git a/libvctrl_handler/tests/blob.rs b/libvctrl_handler/tests/blob.rs new file mode 100644 index 0000000..c4bd4e4 --- /dev/null +++ b/libvctrl_handler/tests/blob.rs @@ -0,0 +1,38 @@ +use criterion as _; +use libvctrl_handler::{Blob, MAX_BLOB_SIZE, VctrlError}; +mod common; + +#[test] +fn test_blob_valid_empty() { + let blob = common::ok(Blob::new(Vec::new())); + assert!(blob.is_empty()); + assert_eq!(blob.size(), 0); + assert_eq!(blob.data(), &[] as &[u8]); +} + +#[test] +fn test_blob_valid_small() { + let data = vec![1, 2, 3, 4]; + let blob = common::ok(Blob::new(data.clone())); + assert!(!blob.is_empty()); + assert_eq!(blob.size(), 4); + assert_eq!(blob.data(), data.as_slice()); +} + +#[test] +fn test_blob_exceeds_max_size() { + let max_len = usize::try_from(MAX_BLOB_SIZE).unwrap_or(usize::MAX); + let data = vec![0_u8; max_len + 1]; + let result = Blob::new(data); + assert!(result.is_err()); + + let expected_msg = format!( + "blob size {} exceeds maximum allowed size {}", + max_len + 1, + MAX_BLOB_SIZE + ); + assert_eq!( + common::err(result), + VctrlError::ExceededMaxSize(expected_msg) + ); +} From 979cbac10b875ce78410f49b32fc81fbfebfb200 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 28/41] test(handler): add commit tests --- libvctrl_handler/tests/commit.rs | 117 +++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 libvctrl_handler/tests/commit.rs diff --git a/libvctrl_handler/tests/commit.rs b/libvctrl_handler/tests/commit.rs new file mode 100644 index 0000000..c678d4c --- /dev/null +++ b/libvctrl_handler/tests/commit.rs @@ -0,0 +1,117 @@ +use criterion as _; +use libvctrl_handler::{ + Commit, CommitMeta, HASH_LENGTH, Hash, MAX_MESSAGE_LENGTH, MAX_PARENT_COUNT, UserID, VctrlError, +}; +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +fn user() -> UserID { + common::ok(UserID::new( + "Alice".to_string(), + "alice@example.com".to_string(), + )) +} + +#[test] +fn test_commit_new_valid_empty_parents() { + let tree = h(1); + let author = user(); + let committer = user(); + + let commit = common::ok(Commit::new( + tree, + Vec::new(), + author.clone(), + committer.clone(), + "initial commit".to_string(), + )); + + assert_eq!(commit.tree(), &tree); + assert!(commit.parents().is_empty()); + assert_eq!(commit.author(), &author); + assert_eq!(commit.committer(), &committer); + assert_eq!(commit.message(), "initial commit"); + assert_eq!(commit.meta().timestamp(), 0); + assert_eq!(commit.meta().timezone_offset(), 0); +} + +#[test] +fn test_commit_new_duplicate_parent() { + let tree = h(1); + let parent = h(2); + let author = user(); + let committer = user(); + + let result = Commit::new( + tree, + vec![parent, parent], + author, + committer, + "duplicate".to_string(), + ); + + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::DuplicateParent); +} + +#[test] +fn test_commit_new_too_many_parents() { + let tree = h(1); + let parent = h(2); + let author = user(); + let committer = user(); + + let max_parents = usize::try_from(MAX_PARENT_COUNT).unwrap_or(usize::MAX); + let parents = vec![parent; max_parents + 1]; + + let result = Commit::new(tree, parents, author, committer, "many parents".to_string()); + + assert!(result.is_err()); + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_commit_new_message_too_long() { + let tree = h(1); + let author = user(); + let committer = user(); + let max_msg = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX); + let message = "a".repeat(max_msg + 1); + + let result = Commit::new(tree, Vec::new(), author, committer, message); + + assert!(result.is_err()); + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_commit_with_meta() { + let tree = h(1); + let author = user(); + let committer = user(); + let meta = common::ok(CommitMeta::new(1_700_000_000, 120, Some("utf-8".into()))); + + let commit = common::ok(Commit::with_meta( + tree, + Vec::new(), + author, + committer, + "meta commit".to_string(), + meta, + )); + + assert_eq!(commit.meta().timestamp(), 1_700_000_000); + assert_eq!(commit.meta().timezone_offset(), 120); + assert_eq!(commit.meta().encoding(), Some("utf-8")); +} From 411bd269fe4703dc4518fb20eaa67778753a5d0e Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 29/41] test(handler): add commit_meta tests --- libvctrl_handler/tests/commit_meta.rs | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 libvctrl_handler/tests/commit_meta.rs diff --git a/libvctrl_handler/tests/commit_meta.rs b/libvctrl_handler/tests/commit_meta.rs new file mode 100644 index 0000000..a550514 --- /dev/null +++ b/libvctrl_handler/tests/commit_meta.rs @@ -0,0 +1,35 @@ +use criterion as _; +use libvctrl_handler::{CommitMeta, VctrlError}; +mod common; + +#[test] +fn test_commit_meta_valid_boundaries() { + let meta_min = common::ok(CommitMeta::new(123, -1440, None)); + assert_eq!(meta_min.timestamp(), 123); + assert_eq!(meta_min.timezone_offset(), -1440); + assert_eq!(meta_min.encoding(), None); + + let meta_zero = common::ok(CommitMeta::new(0, 0, Some("utf-8".into()))); + assert_eq!(meta_zero.timestamp(), 0); + assert_eq!(meta_zero.timezone_offset(), 0); + assert_eq!(meta_zero.encoding(), Some("utf-8")); + + let meta_max = common::ok(CommitMeta::new(456, 1440, Some("iso-8859-1".into()))); + assert_eq!(meta_max.timestamp(), 456); + assert_eq!(meta_max.timezone_offset(), 1440); + assert_eq!(meta_max.encoding(), Some("iso-8859-1")); +} + +#[test] +fn test_commit_meta_invalid_timezone() { + let result = CommitMeta::new(0, -1441, None); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidTimezoneOffset(-1441) + ); + + let result = CommitMeta::new(0, 1441, None); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidTimezoneOffset(1441)); +} From bdc7660aab7efd378bd6bdb591aed94473ee0600 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:54 +0700 Subject: [PATCH 30/41] test(handler): add common test utilities --- libvctrl_handler/tests/common/mod.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 libvctrl_handler/tests/common/mod.rs diff --git a/libvctrl_handler/tests/common/mod.rs b/libvctrl_handler/tests/common/mod.rs new file mode 100644 index 0000000..bbd3878 --- /dev/null +++ b/libvctrl_handler/tests/common/mod.rs @@ -0,0 +1,15 @@ +#[allow(dead_code, clippy::panic)] +pub(crate) fn ok(result: Result) -> T { + match result { + Ok(value) => value, + Err(err) => panic!("expected Ok(..), got Err({err:?})"), + } +} + +#[allow(dead_code, clippy::panic)] +pub(crate) fn err(result: Result) -> E { + match result { + Ok(value) => panic!("expected Err(..), got Ok({value:?})"), + Err(err) => err, + } +} From 9cec122f427933acb0caa4b020a6e10258003f04 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 31/41] test(handler): add delta tests --- libvctrl_handler/tests/delta.rs | 157 ++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 libvctrl_handler/tests/delta.rs diff --git a/libvctrl_handler/tests/delta.rs b/libvctrl_handler/tests/delta.rs new file mode 100644 index 0000000..3243f3d --- /dev/null +++ b/libvctrl_handler/tests/delta.rs @@ -0,0 +1,157 @@ +use criterion as _; +use libvctrl_handler::{ + ChangeKind, Conflict, FileDelta, HASH_LENGTH, Hash, MergeResult, TreeDelta, +}; +use std::path::{Path, PathBuf}; + +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +#[test] +fn test_file_delta_added() { + let h1 = h(1); + let delta = FileDelta::added(PathBuf::from("a.txt"), h1); + + assert!(delta.is_added()); + assert!(!delta.is_deleted()); + assert!(!delta.is_modified()); + assert!(!delta.is_type_change()); + assert!(!delta.is_renamed()); + assert!(!delta.is_copied()); + + assert_eq!(delta.path(), Path::new("a.txt")); + assert_eq!(delta.old_path(), None); + assert_eq!(delta.old_hash(), None); + assert_eq!(delta.new_hash(), Some(h1)); + assert_eq!(delta.kind(), ChangeKind::Added); +} + +#[test] +fn test_file_delta_deleted() { + let h1 = h(1); + let delta = FileDelta::deleted(PathBuf::from("a.txt"), h1); + + assert!(delta.is_deleted()); + assert!(!delta.is_added()); + assert_eq!(delta.path(), Path::new("a.txt")); + assert_eq!(delta.old_hash(), Some(h1)); + assert_eq!(delta.new_hash(), None); + assert_eq!(delta.kind(), ChangeKind::Deleted); +} + +#[test] +fn test_file_delta_modified_and_type_change() { + let h1 = h(1); + let h2 = h(2); + + let modified = FileDelta::modified(PathBuf::from("a.txt"), h1, h2); + assert!(modified.is_modified()); + assert_eq!(modified.old_hash(), Some(h1)); + assert_eq!(modified.new_hash(), Some(h2)); + assert_eq!(modified.kind(), ChangeKind::Modified); + + let type_change = FileDelta::type_change(PathBuf::from("a.txt"), h1, h2); + assert!(type_change.is_type_change()); + assert_eq!(type_change.old_hash(), Some(h1)); + assert_eq!(type_change.new_hash(), Some(h2)); + assert_eq!(type_change.kind(), ChangeKind::TypeChange); +} + +#[test] +fn test_file_delta_renamed_and_copied() { + let h1 = h(1); + let h2 = h(2); + + let renamed = FileDelta::renamed(PathBuf::from("old.txt"), PathBuf::from("new.txt"), h1, h2); + assert!(renamed.is_renamed()); + assert_eq!(renamed.path(), Path::new("new.txt")); + assert_eq!(renamed.old_path(), Some(Path::new("old.txt"))); + assert_eq!(renamed.old_hash(), Some(h1)); + assert_eq!(renamed.new_hash(), Some(h2)); + assert_eq!(renamed.kind(), ChangeKind::Renamed); + + let copied = FileDelta::copied(PathBuf::from("old.txt"), PathBuf::from("copy.txt"), h1, h2); + assert!(copied.is_copied()); + assert_eq!(copied.path(), Path::new("copy.txt")); + assert_eq!(copied.old_path(), Some(Path::new("old.txt"))); + assert_eq!(copied.kind(), ChangeKind::Copied); +} + +#[test] +fn test_tree_delta_basic() { + let delta = TreeDelta::new(); + assert!(delta.is_empty()); + assert_eq!(delta.len(), 0); + assert_eq!(delta.changes().len(), 0); + assert_eq!(delta.iter().count(), 0); +} + +#[test] +fn test_tree_delta_from_changes() { + let h1 = h(1); + let changes = vec![ + FileDelta::added(PathBuf::from("a.txt"), h1), + FileDelta::added(PathBuf::from("b.txt"), h1), + ]; + + let delta = TreeDelta::from_changes(changes); + assert!(!delta.is_empty()); + assert_eq!(delta.len(), 2); + assert_eq!(delta.changes().len(), 2); + assert_eq!(delta.iter().count(), 2); + assert_eq!(delta.into_iter().count(), 2); +} + +#[test] +fn test_tree_delta_iter_by_ref() { + let h1 = h(1); + let delta = TreeDelta::from_changes(vec![FileDelta::added(PathBuf::from("a.txt"), h1)]); + + let refs: Vec<&FileDelta> = (&delta).into_iter().collect(); + assert_eq!(refs.len(), 1); + assert_eq!( + refs.first().map(|delta| delta.path()), + Some(Path::new("a.txt")) + ); +} + +#[test] +fn test_conflict_accessors() { + let ancestor = h(1); + let ours = h(2); + let theirs = h(3); + + let conflict = Conflict::new(PathBuf::from("file.txt"), ancestor, ours, theirs); + + assert_eq!(conflict.path(), Path::new("file.txt")); + assert_eq!(conflict.ancestor_blob(), ancestor); + assert_eq!(conflict.our_blob(), ours); + assert_eq!(conflict.their_blob(), theirs); +} + +#[test] +fn test_merge_result_variants() { + let h1 = h(1); + let success = MergeResult::Success(h1); + assert!(success.is_success()); + assert!(!success.is_conflicts()); + assert!(success.conflicts().is_none()); + + let conflict = Conflict::new(PathBuf::from("file.txt"), h1, h(2), h(3)); + let conflicts = MergeResult::Conflicts(vec![conflict]); + assert!(!conflicts.is_success()); + assert!(conflicts.is_conflicts()); + + let conflict_list = conflicts.conflicts(); + assert!(conflict_list.is_some(), "expected conflicts"); + if let Some(c) = conflict_list { + assert_eq!(c.len(), 1); + } else { + loop { + core::hint::spin_loop(); + } + } +} From 694fc69ad4e56352b5b16c819f5442b0c9a5ea8b Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 32/41] test(handler): add entry_kind tests --- libvctrl_handler/tests/entry_kind.rs | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 libvctrl_handler/tests/entry_kind.rs diff --git a/libvctrl_handler/tests/entry_kind.rs b/libvctrl_handler/tests/entry_kind.rs new file mode 100644 index 0000000..b9d16bb --- /dev/null +++ b/libvctrl_handler/tests/entry_kind.rs @@ -0,0 +1,34 @@ +use criterion as _; +use libvctrl_handler::EntryKind; +use libvctrl_handler::constants::entry_mode; +mod common; + +#[test] +fn test_entry_kind_mode_matches_constants() { + assert_eq!(EntryKind::Blob.mode(), entry_mode::BLOB); + assert_eq!(EntryKind::Executable.mode(), entry_mode::EXECUTABLE); + assert_eq!(EntryKind::Symlink.mode(), entry_mode::SYMLINK); + assert_eq!(EntryKind::Tree.mode(), entry_mode::TREE); + assert_eq!(EntryKind::Submodule.mode(), entry_mode::SUBMODULE); +} + +#[test] +fn test_entry_kind_from_mode_roundtrip() { + let kinds = [ + EntryKind::Blob, + EntryKind::Executable, + EntryKind::Symlink, + EntryKind::Tree, + EntryKind::Submodule, + ]; + + for kind in kinds { + assert_eq!(EntryKind::from_mode(kind.mode()), Some(kind)); + } +} + +#[test] +fn test_entry_kind_from_mode_invalid() { + assert_eq!(EntryKind::from_mode(0), None); + assert_eq!(EntryKind::from_mode(u32::MAX), None); +} From ccdd3eadcd7dca937ce1341ec12facfe1638008b Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 33/41] test(handler): add errors tests --- libvctrl_handler/tests/errors.rs | 123 +++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 libvctrl_handler/tests/errors.rs diff --git a/libvctrl_handler/tests/errors.rs b/libvctrl_handler/tests/errors.rs new file mode 100644 index 0000000..3404907 --- /dev/null +++ b/libvctrl_handler/tests/errors.rs @@ -0,0 +1,123 @@ +use core::error::Error as _; +use criterion as _; +use libvctrl_handler::{HASH_LENGTH, Hash, VctrlError}; +use std::io; + +mod common; + +#[test] +fn test_vctrl_error_display_variants() { + assert_eq!( + VctrlError::CorruptedData("x".to_string()).to_string(), + "Corrupted data: x" + ); + assert_eq!( + VctrlError::DuplicateParent.to_string(), + "Duplicate parent in commit" + ); + assert_eq!( + VctrlError::ExceededMaxSize("x".to_string()).to_string(), + "Exceeded max size: x" + ); + assert_eq!( + VctrlError::InvalidBlameRange.to_string(), + "Invalid blame range" + ); + assert_eq!( + VctrlError::InvalidEmail("a".to_string()).to_string(), + "Invalid email: 'a'" + ); + assert_eq!( + VctrlError::InvalidHashLength(10).to_string(), + "Invalid hash length: expected 64 bytes, got 10" + ); + assert_eq!( + VctrlError::InvalidName("n".to_string()).to_string(), + "Invalid name: 'n'" + ); + assert_eq!( + VctrlError::InvalidTimezoneOffset(-1441).to_string(), + "Invalid timezone offset: -1441" + ); + assert_eq!( + VctrlError::InvalidTreeStructure("t".to_string()).to_string(), + "Invalid tree structure: t" + ); + assert_eq!(VctrlError::Other("o".to_string()).to_string(), "o"); + assert_eq!( + VctrlError::RefNotFound("r".to_string()).to_string(), + "Reference not found: 'r'" + ); + assert_eq!( + VctrlError::SerializationError("s".to_string()).to_string(), + "Serialization error: s" + ); +} + +#[test] +fn test_vctrl_error_io_display_and_source() { + let io_err = io::Error::new(io::ErrorKind::NotFound, "missing"); + let err = VctrlError::from(io_err); + + assert!(err.to_string().contains("I/O error:")); + assert!(err.source().is_some()); + + assert!( + matches!(&err, VctrlError::IoError(_)), + "unexpected variant: {err:?}" + ); + + if let VctrlError::IoError(arc_err) = err { + assert_eq!(arc_err.as_ref().kind(), io::ErrorKind::NotFound); + assert_eq!(arc_err.as_ref().to_string(), "missing"); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_vctrl_error_from_io() { + let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied"); + let err = VctrlError::from_io(io_err); + + assert!( + matches!(&err, VctrlError::IoError(_)), + "unexpected variant: {err:?}" + ); + + if let VctrlError::IoError(arc_err) = err { + assert_eq!(arc_err.as_ref().kind(), io::ErrorKind::PermissionDenied); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_vctrl_error_partial_eq() { + assert_eq!( + VctrlError::InvalidName("x".to_string()), + VctrlError::InvalidName("x".to_string()) + ); + assert_ne!( + VctrlError::InvalidName("x".to_string()), + VctrlError::InvalidName("y".to_string()) + ); + + assert_eq!(VctrlError::DuplicateParent, VctrlError::DuplicateParent); + assert_ne!(VctrlError::DuplicateParent, VctrlError::InvalidBlameRange); + + let hash = Hash::from([0_u8; HASH_LENGTH]); + let hash2 = Hash::from([1_u8; HASH_LENGTH]); + assert_eq!( + VctrlError::ObjectNotFound(hash), + VctrlError::ObjectNotFound(hash) + ); + assert_ne!( + VctrlError::ObjectNotFound(hash), + VctrlError::ObjectNotFound(hash2) + ); +} From 2a75c120abfae2f4f7189178112c3cb52dcb2a28 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 34/41] test(handler): add hash tests --- libvctrl_handler/tests/hash.rs | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 libvctrl_handler/tests/hash.rs diff --git a/libvctrl_handler/tests/hash.rs b/libvctrl_handler/tests/hash.rs new file mode 100644 index 0000000..9b0528e --- /dev/null +++ b/libvctrl_handler/tests/hash.rs @@ -0,0 +1,110 @@ +use criterion as _; +use libvctrl_handler::constants::HASH_LENGTH; +use libvctrl_handler::{Hash, VctrlError}; +mod common; + +fn valid_hex() -> String { + use core::fmt::Write; + + let mut s = String::with_capacity(HASH_LENGTH * 2); + for b in 0..HASH_LENGTH { + let _ = write!(s, "{b:02x}"); + } + s +} + +#[test] +fn test_hash_from_bytes_valid() { + let bytes = [7_u8; HASH_LENGTH]; + let hash = common::ok(Hash::from_bytes(&bytes)); + assert_eq!(&hash.as_bytes()[..], &bytes[..]); +} + +#[test] +fn test_hash_from_bytes_invalid_length() { + let result = Hash::from_bytes(&[0_u8; 10]); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(10)); +} + +#[test] +fn test_hash_from_array() { + let arr = [1_u8; HASH_LENGTH]; + let hash = Hash::from(arr); + assert_eq!(&hash.as_bytes()[..], &arr[..]); +} + +#[test] +fn test_hash_try_from_slice_valid() { + let arr = [2_u8; HASH_LENGTH]; + let hash: Hash = common::ok(Hash::try_from(&arr[..])); + assert_eq!(&hash.as_bytes()[..], &arr[..]); +} + +#[test] +fn test_hash_try_from_slice_invalid() { + let result: Result = Hash::try_from(&[0_u8; 3][..]); + assert!(result.is_err()); +} + +#[test] +fn test_hash_as_ref() { + let arr = [3_u8; HASH_LENGTH]; + let hash = Hash::from(arr); + assert_eq!(hash.as_ref(), &arr[..]); +} + +#[test] +fn test_hash_from_str_valid() { + let s = valid_hex(); + let expected: Vec = (0..HASH_LENGTH) + .map(|i| u8::try_from(i).unwrap_or(0)) + .collect(); + let hash = common::ok(s.parse::()); + assert_eq!(&hash.as_bytes()[..], expected.as_slice()); +} + +#[test] +fn test_hash_from_str_invalid_length() { + let result = "abc".parse::(); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(3)); +} + +#[test] +fn test_hash_from_str_invalid_hex() { + let s = "zz".repeat(HASH_LENGTH); + let result = s.parse::(); + assert!(result.is_err()); + + let err = common::err(result); + assert!( + matches!(&err, VctrlError::CorruptedData(_)), + "unexpected error: {err:?}" + ); + + if let VctrlError::CorruptedData(msg) = err { + assert!(msg.contains("invalid hex char in hash")); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_hash_display() { + let s = valid_hex(); + let hash = common::ok(s.parse::()); + assert_eq!(hash.to_string(), s); +} + +#[test] +fn test_hash_debug() { + let s = valid_hex(); + let hash = common::ok(s.parse::()); + let dbg = format!("{hash:?}"); + assert!(dbg.starts_with("Hash(")); + assert!(dbg.contains("...")); + assert!(dbg.ends_with(')')); +} From 531fa3e87b5994e8a07d8da637582faee1e00a40 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 35/41] test(handler): add criterion import to hash_validation --- libvctrl_handler/tests/hash_validation.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libvctrl_handler/tests/hash_validation.rs b/libvctrl_handler/tests/hash_validation.rs index cbe10de..5662ef5 100644 --- a/libvctrl_handler/tests/hash_validation.rs +++ b/libvctrl_handler/tests/hash_validation.rs @@ -1,8 +1,9 @@ #![allow(missing_docs)] #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] +use criterion as _; -use core::error::Error as StdError; +use core::error::Error as _; use libvctrl_handler::*; fn make_hash(byte: u8) -> Hash { From bb06f8c3a6c3abe5f0c36514ab1763e644b94987 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 36/41] test(handler): add criterion import to type_validation --- libvctrl_handler/tests/type_validation.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/libvctrl_handler/tests/type_validation.rs b/libvctrl_handler/tests/type_validation.rs index c8458c2..05e814e 100644 --- a/libvctrl_handler/tests/type_validation.rs +++ b/libvctrl_handler/tests/type_validation.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] +use criterion as _; use libvctrl_handler::*; From de206aeaa552fbe171fd4bb5d709661f4dacce71 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 37/41] test(handler): add tag_reflog tests --- libvctrl_handler/tests/tag_reflog.rs | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 libvctrl_handler/tests/tag_reflog.rs diff --git a/libvctrl_handler/tests/tag_reflog.rs b/libvctrl_handler/tests/tag_reflog.rs new file mode 100644 index 0000000..da5eef4 --- /dev/null +++ b/libvctrl_handler/tests/tag_reflog.rs @@ -0,0 +1,90 @@ +use criterion as _; +use libvctrl_handler::{ + CommitMeta, HASH_LENGTH, Hash, MAX_MESSAGE_LENGTH, ReflogEntry, Tag, UserID, VctrlError, +}; +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +fn tagger() -> UserID { + common::ok(UserID::new( + "Tagger".to_string(), + "tagger@example.com".to_string(), + )) +} + +#[test] +fn test_tag_valid_with_meta() { + let target = h(1); + let tagger = tagger(); + let meta = common::ok(CommitMeta::new(1_700_000_000, 300, None)); + + let tag = common::ok(Tag::with_meta( + "v1.0.0".to_string(), + target, + Some(tagger.clone()), + "release 1.0.0".to_string(), + meta, + )); + + assert_eq!(tag.name(), "v1.0.0"); + assert_eq!(tag.target(), &target); + assert_eq!(tag.tagger(), Some(&tagger)); + assert_eq!(tag.message(), "release 1.0.0"); + assert_eq!(tag.meta().timestamp(), 1_700_000_000); + assert_eq!(tag.meta().timezone_offset(), 300); +} + +#[test] +fn test_tag_invalid_ref_name() { + let target = h(1); + let result = Tag::new("bad name".to_string(), target, None, "message".to_string()); + assert!(result.is_err()); +} + +#[test] +fn test_tag_message_too_long() { + let target = h(1); + let max_msg = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX); + let message = "a".repeat(max_msg + 1); + + let result = Tag::new("v1.0.0".to_string(), target, None, message); + assert!(result.is_err()); + + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_reflog_entry_valid() { + let old = Some(h(1)); + let new = Some(h(2)); + let entry = common::ok(ReflogEntry::new( + old, + new, + "update".to_string(), + 1_700_000_000, + 120, + )); + + assert_eq!(entry.old_id(), old); + assert_eq!(entry.new_id(), new); + assert_eq!(entry.reason(), "update"); + assert_eq!(entry.timestamp(), 1_700_000_000); + assert_eq!(entry.timezone_offset(), 120); +} + +#[test] +fn test_reflog_entry_invalid_timezone() { + let result = ReflogEntry::new(None, None, "update".to_string(), 1_700_000_000, -2000); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidTimezoneOffset(-2000) + ); +} From e8561895445b22c67aa472863353aa66c2ef1852 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 38/41] test(handler): add traits_index tests --- libvctrl_handler/tests/traits_index.rs | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 libvctrl_handler/tests/traits_index.rs diff --git a/libvctrl_handler/tests/traits_index.rs b/libvctrl_handler/tests/traits_index.rs new file mode 100644 index 0000000..3024496 --- /dev/null +++ b/libvctrl_handler/tests/traits_index.rs @@ -0,0 +1,61 @@ +use criterion as _; +use libvctrl_handler::{Index, VctrlError}; +mod common; + +#[derive(Debug)] +struct MockIndex { + len: usize, +} + +impl Index for MockIndex { + type Entry = i32; + type Path = String; + type TreeId = (); + + fn add(&mut self, _entry: Self::Entry) -> Result<(), VctrlError> { + Ok(()) + } + + fn remove(&mut self, _path: &Self::Path) -> Result<(), VctrlError> { + Ok(()) + } + + fn clear(&mut self) -> Result<(), VctrlError> { + Ok(()) + } + + fn get(&self, _path: &Self::Path) -> Result, VctrlError> { + Ok(None) + } + + fn contains(&self, _path: &Self::Path) -> Result { + Ok(false) + } + + fn len(&self) -> Result { + Ok(self.len) + } + + fn entries(&self) -> Result, VctrlError> { + Ok(Vec::new()) + } + + fn write_tree(&self) -> Result { + Ok(()) + } + + fn read_tree(&mut self, _tree: &Self::TreeId) -> Result<(), VctrlError> { + Ok(()) + } +} + +#[test] +fn test_index_is_empty_default_implementation() { + let empty = MockIndex { len: 0 }; + let empty_result = empty.is_empty(); + assert_eq!(empty_result, Ok(true)); + + let non_empty = MockIndex { len: 2 }; + let non_empty_result = non_empty.is_empty(); + assert_eq!(non_empty_result, Ok(false)); +} From 9c345be5e1d8299dcbe42c27a96547fd3922de23 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 39/41] test(handler): add tree tests --- libvctrl_handler/tests/tree.rs | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 libvctrl_handler/tests/tree.rs diff --git a/libvctrl_handler/tests/tree.rs b/libvctrl_handler/tests/tree.rs new file mode 100644 index 0000000..a17a90e --- /dev/null +++ b/libvctrl_handler/tests/tree.rs @@ -0,0 +1,88 @@ +use criterion as _; +use libvctrl_handler::{ + EntryKind, HASH_LENGTH, Hash, MAX_TREE_ENTRIES, Tree, TreeEntry, VctrlError, +}; +mod common; + +fn h() -> Hash { + Hash::from([0_u8; HASH_LENGTH]) +} + +#[test] +fn test_tree_entry_valid() { + let hash = h(); + let entry = common::ok(TreeEntry::new( + "file.txt".to_string(), + EntryKind::Blob, + hash, + )); + + assert_eq!(entry.name(), "file.txt"); + assert_eq!(entry.kind(), EntryKind::Blob); + assert_eq!(entry.hash(), &hash); +} + +#[test] +fn test_tree_entry_invalid_name() { + let result = TreeEntry::new("a/b".to_string(), EntryKind::Blob, h()); + assert!(result.is_err()); +} + +#[test] +fn test_tree_new_empty() { + let tree = common::ok(Tree::new(Vec::new())); + assert!(tree.is_empty()); + assert_eq!(tree.len(), 0); + assert_eq!(tree.entries().len(), 0); +} + +#[test] +fn test_tree_new_sorts_entries() { + let e1 = common::ok(TreeEntry::new("b".to_string(), EntryKind::Blob, h())); + let e2 = common::ok(TreeEntry::new("a".to_string(), EntryKind::Blob, h())); + + let tree = common::ok(Tree::new(vec![e1, e2])); + + assert_eq!(tree.len(), 2); + assert_eq!(tree.entries().get(0).map(TreeEntry::name), Some("a")); + assert_eq!(tree.entries().get(1).map(TreeEntry::name), Some("b")); +} + +#[test] +fn test_tree_new_duplicate_name() { + let dup1 = common::ok(TreeEntry::new("x".to_string(), EntryKind::Blob, h())); + let dup2 = common::ok(TreeEntry::new("x".to_string(), EntryKind::Tree, h())); + + let result = Tree::new(vec![dup1, dup2]); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidTreeStructure("duplicate entry name: 'x'".to_string()) + ); +} + +#[test] +fn test_tree_new_exceeds_max_entries() { + let max_entries = usize::try_from(MAX_TREE_ENTRIES).unwrap_or(usize::MAX); + let entries = (0..=max_entries) + .map(|i| common::ok(TreeEntry::new(format!("entry{i}"), EntryKind::Blob, h()))) + .collect::>(); + + let result = Tree::new(entries); + assert!(result.is_err()); + + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_tree_get() { + let e = common::ok(TreeEntry::new("a".to_string(), EntryKind::Blob, h())); + let tree = common::ok(Tree::new(vec![e])); + + assert_eq!(tree.get("a").map(TreeEntry::name), Some("a")); + assert!(tree.get("missing").is_none()); +} From 2aad95a1a0fc72e072c958a5543b73bce83487f7 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 40/41] test(handler): add user_id tests --- libvctrl_handler/tests/user_id.rs | 92 +++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 libvctrl_handler/tests/user_id.rs diff --git a/libvctrl_handler/tests/user_id.rs b/libvctrl_handler/tests/user_id.rs new file mode 100644 index 0000000..48af122 --- /dev/null +++ b/libvctrl_handler/tests/user_id.rs @@ -0,0 +1,92 @@ +use criterion as _; +use libvctrl_handler::{MAX_NAME_LENGTH, UserID, VctrlError}; +mod common; + +#[test] +fn test_user_id_valid() { + let user = common::ok(UserID::new( + "Alice".to_string(), + "alice@example.com".to_string(), + )); + assert_eq!(user.name(), "Alice"); + assert_eq!(user.email(), "alice@example.com"); +} + +#[test] +fn test_user_id_invalid_empty_name() { + let result = UserID::new(String::new(), "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName("user name is empty".to_string()) + ); +} + +#[test] +fn test_user_id_invalid_name_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let name = "a".repeat(max_len + 1); + let result = UserID::new(name, "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!( + "user name exceeds maximum length {MAX_NAME_LENGTH}" + )) + ); +} + +#[test] +fn test_user_id_invalid_name_control_chars() { + let name = "Alice\nBob".to_string(); + let result = UserID::new(name.clone(), "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!("user name contains control characters: '{name}'")) + ); +} + +#[test] +fn test_user_id_invalid_empty_email() { + let result = UserID::new("Alice".to_string(), String::new()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail("email is empty".to_string()) + ); +} + +#[test] +fn test_user_id_invalid_email_no_at() { + let email = "alice.example.com".to_string(); + let result = UserID::new("Alice".to_string(), email.clone()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email must contain '@': '{email}'")) + ); +} + +#[test] +fn test_user_id_invalid_email_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let email = format!("{}@example.com", "a".repeat(max_len + 1)); + let result = UserID::new("Alice".to_string(), email); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email exceeds maximum length {MAX_NAME_LENGTH}")) + ); +} + +#[test] +fn test_user_id_invalid_email_control_chars() { + let email = "alice@example.com\n".to_string(); + let result = UserID::new("Alice".to_string(), email.clone()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email contains control characters: '{email}'")) + ); +} From 20cfd644df45404990d8072cf522cd5ac42117e3 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 20:26:55 +0700 Subject: [PATCH 41/41] test(handler): add validation tests --- libvctrl_handler/tests/validation.rs | 116 +++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 libvctrl_handler/tests/validation.rs diff --git a/libvctrl_handler/tests/validation.rs b/libvctrl_handler/tests/validation.rs new file mode 100644 index 0000000..e35e294 --- /dev/null +++ b/libvctrl_handler/tests/validation.rs @@ -0,0 +1,116 @@ +use criterion as _; +use libvctrl_handler::{ + HASH_LENGTH, MAX_NAME_LENGTH, VctrlError, validate_hash_bytes, validate_name, + validate_ref_name, validate_tree_entry_name, +}; +mod common; + +#[test] +fn test_validate_hash_bytes_valid() { + let bytes = [0_u8; HASH_LENGTH]; + assert!(validate_hash_bytes(&bytes).is_ok()); +} + +#[test] +fn test_validate_hash_bytes_invalid() { + let result = validate_hash_bytes(&[0_u8; 10]); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(10)); +} + +#[test] +fn test_validate_name_valid() { + assert!(validate_name("file.txt").is_ok()); + assert!(validate_name("a").is_ok()); +} + +#[test] +fn test_validate_name_invalid_empty() { + let result = validate_name(""); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName("name is empty".to_string()) + ); +} + +#[test] +fn test_validate_name_invalid_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let name = "a".repeat(max_len + 1); + let result = validate_name(&name); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!( + "name exceeds maximum length {MAX_NAME_LENGTH}: '{name}'" + )) + ); +} + +#[test] +fn test_validate_name_invalid_control_chars() { + let name = "a\nb"; + let result = validate_name(name); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!("name contains control characters: '{name}'")) + ); +} + +#[test] +fn test_validate_ref_name_valid() { + assert!(validate_ref_name("refs/heads/main").is_ok()); + assert!(validate_ref_name("v1.0.0").is_ok()); +} + +#[test] +fn test_validate_ref_name_invalid_cases() { + let invalid_names = [ + "@", + "/leading", + "trailing/", + "double//slash", + "refs/.hidden", + "refs/heads/main.lock", + "refs/heads/main..", + "refs/heads/main~1", + "refs/heads/main^", + "refs/heads/main:", + "refs/heads/main?", + "refs/heads/main*", + "refs/heads/main[", + "refs/heads/main\\", + "refs/heads/main ", + "refs/heads/main@{", + "refs/heads/main<", + "refs/heads/main>", + "refs/heads/main|", + "refs/heads/main\"", + ]; + + for name in invalid_names { + assert!( + validate_ref_name(name).is_err(), + "expected invalid: '{name}'" + ); + } +} + +#[test] +fn test_validate_tree_entry_name_valid() { + assert!(validate_tree_entry_name("file.txt").is_ok()); +} + +#[test] +fn test_validate_tree_entry_name_invalid() { + let invalid_names = ["a/b", "a\\b", ".", ".."]; + + for name in invalid_names { + assert!( + validate_tree_entry_name(name).is_err(), + "expected invalid: '{name}'" + ); + } +}