From 1496f528847a85a1f2d67cd81a47895747a77416 Mon Sep 17 00:00:00 2001 From: mroczect Date: Fri, 21 Aug 2026 20:26:22 +0700 Subject: [PATCH] ci(workflow): pin rust 1.96 and disable autocrlf --- .github/workflows/rust.yml | 14 + a | 16469 +++++++++++++++++++++++++++++++++++ rust-toolchain.toml | 4 + 3 files changed, 16487 insertions(+) create mode 100644 a create mode 100644 rust-toolchain.toml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index dc604cf1..7cdc040b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -15,8 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Disable autocrlf + run: git config --global core.autocrlf false - uses: dtolnay/rust-toolchain@stable with: + toolchain: 1.96.0 components: rustfmt - run: cargo fmt --all -- --check @@ -25,8 +28,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Disable autocrlf + run: git config --global core.autocrlf false - uses: dtolnay/rust-toolchain@stable with: + toolchain: 1.96.0 components: clippy - uses: Swatinem/rust-cache@v2 - name: Run clippy with warnings denied @@ -37,7 +43,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Disable autocrlf + run: git config --global core.autocrlf false - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.96.0 - uses: Swatinem/rust-cache@v2 - run: cargo test --workspace --all-targets --all-features @@ -47,6 +57,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.96.0 - uses: Swatinem/rust-cache@v2 - name: Install cargo-audit run: cargo install cargo-audit --locked @@ -59,6 +71,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + toolchain: 1.96.0 - uses: Swatinem/rust-cache@v2 - name: Install cargo-deny run: cargo install cargo-deny --locked diff --git a/a b/a new file mode 100644 index 00000000..f42234e8 --- /dev/null +++ b/a @@ -0,0 +1,16469 @@ +diff --git a/Cargo.lock b/Cargo.lock +index 950d5c3..0f50111 100644 +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -263,7 +263,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + + [[package]] + name = "libvctrl" +-version = "2.1.3" ++version = "2.1.2" + dependencies = [ + "libvctrl_core", + "libvctrl_handler", +@@ -273,7 +273,7 @@ dependencies = [ + + [[package]] + name = "libvctrl_core" +-version = "3.0.1" ++version = "3.0.0" + dependencies = [ + "libvctrl_handler", + "libvctrl_sha512", +@@ -282,10 +282,7 @@ dependencies = [ + + [[package]] + name = "libvctrl_handler" +-version = "5.0.1" +-dependencies = [ +- "criterion", +-] ++version = "5.0.0" + + [[package]] + name = "libvctrl_plumbing" +@@ -301,10 +298,9 @@ version = "0.1.0" + + [[package]] + name = "libvctrl_sha512" +-version = "3.1.0" ++version = "3.0.0" + dependencies = [ + "criterion", +- "zeroize", + ] + + [[package]] +@@ -721,12 +717,6 @@ dependencies = [ + "syn 2.0.119", + ] + +-[[package]] +-name = "zeroize" +-version = "1.9.0" +-source = "registry+https://github.com/rust-lang/crates.io-index" +-checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +- + [[package]] + name = "zmij" + version = "1.0.23" +diff --git a/Cargo.toml b/Cargo.toml +index 878ee26..f3d0551 100644 +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -1,118 +1,62 @@ + [workspace] +-members = [ +- "libvctrl", +- "libvctrl_core", +- "libvctrl_handler", +- "libvctrl_plumbing", +- "libvctrl_porcelain", +- "libvctrl_sha512" +-] + resolver = "2" +- +-[workspace.lints.clippy] +-all = { level = "deny", priority = -1 } +-alloc_instead_of_core = "deny" +-allow_attributes = "allow" +-allow_attributes_without_reason = "allow" +-arithmetic_side_effects = "deny" +-cargo = { level = "deny", priority = -1 } +-complexity = { level = "deny", priority = -1 } +-correctness = { level = "deny", priority = -1 } +-doc_lazy_continuation = "allow" +-doc_markdown = "allow" +-empty_docs = "allow" +-expect_used = "deny" +-implicit_hasher = "allow" +-indexing_slicing = "deny" +-map_err_ignore = "deny" +-match_same_arms = "allow" +-missing_docs_in_private_items = "allow" +-missing_errors_doc = "allow" +-missing_panics_doc = "allow" +-missing_safety_doc = "allow" +-module_name_repetitions = "allow" +-needless_doctest_main = "allow" +-needless_return = "allow" +-nursery = { level = "deny", priority = -1 } +-panic = "deny" +-pedantic = { level = "deny", priority = -1 } +-perf = { level = "deny", priority = -1 } +-std_instead_of_alloc = "deny" +-std_instead_of_core = "deny" +-style = { level = "deny", priority = -1 } +-suspicious = { level = "deny", priority = -1 } +-uninlined_format_args = "allow" +-unwrap_used = "deny" +-wildcard_enum_match_arm = "deny" +- +-[workspace.lints.rust] +-deprecated = "deny" +-elided_lifetimes_in_paths = "deny" +-explicit_outlives_requirements = "deny" +-future_incompatible = { level = "deny", priority = -1 } +-invalid_reference_casting = "deny" +-macro_use_extern_crate = "deny" +-missing_copy_implementations = "deny" +-missing_debug_implementations = "deny" +-missing_docs = "allow" +-no_mangle_generic_items = "deny" +-non_ascii_idents = "deny" +-non_camel_case_types = "deny" +-non_snake_case = "deny" +-non_upper_case_globals = "deny" +-noop_method_call = "deny" +-overlapping_range_endpoints = "deny" +-private_bounds = "deny" +-private_interfaces = "deny" +-redundant_lifetimes = "deny" +-renamed_and_removed_lints = "deny" +-rust_2018_idioms = { level = "deny", priority = -1 } +-rust_2021_compatibility = { level = "deny", priority = -1 } +-rust_2024_compatibility = { level = "deny", priority = -1 } +-single_use_lifetimes = "deny" +-trivial_bounds = "deny" +-trivial_casts = "deny" +-trivial_numeric_casts = "deny" +-unexpected_cfgs = "deny" +-uninhabited_static = "deny" +-unit_bindings = "deny" +-unknown_lints = "deny" +-unnameable_types = "deny" +-unreachable_code = "deny" +-unreachable_patterns = "deny" +-unreachable_pub = "deny" +-unsafe_code = "forbid" +-unsafe_op_in_unsafe_fn = "deny" +-unused = { level = "deny", priority = -1 } +-unused_allocation = "deny" +-unused_assignments = "deny" +-unused_braces = "deny" +-unused_comparisons = "deny" +-unused_crate_dependencies = "deny" +-unused_doc_comments = "allow" +-unused_extern_crates = "deny" +-unused_features = "deny" +-unused_imports = "deny" +-unused_labels = "deny" +-unused_lifetimes = "deny" +-unused_macro_rules = "deny" +-unused_macros = "deny" +-unused_must_use = "deny" +-unused_mut = "deny" +-unused_parens = "deny" +-unused_qualifications = "deny" +-unused_results = "deny" +-unused_unsafe = "deny" +-unused_variables = "deny" +-warnings = "deny" ++members = ["libvctrl", "libvctrl_core", "libvctrl_handler", "libvctrl_plumbing", "libvctrl_porcelain", "libvctrl_sha512"] + + [workspace.package] +-authors = [ "mroczect" ] +-categories = [ "development-tools", "cryptography", "algorithms", "no-std" ] +-documentation = "https://docs.rs/libvctrl" + edition = "2024" +-homepage = "https://github.com/mroczect/libvctrl" +-keywords = [ "git", "vcs", "cryptography", "sha512", "no-std" ] ++rust-version = "1.96" + license = "MIT" ++authors = ["mroczect"] + repository = "https://github.com/mroczect/libvctrl" +-rust-version = "1.96" ++homepage = "https://github.com/mroczect/libvctrl" ++documentation = "https://docs.rs/libvctrl" ++keywords = ["git", "vcs", "cryptography", "sha512", "no-std"] ++categories = ["development-tools", "cryptography", "algorithms", "no-std"] ++ ++[workspace.lints.rust] ++unsafe_code = "forbid" ++macro_use_extern_crate = "forbid" ++missing_docs = "warn" ++dead_code = "warn" ++unused_imports = "warn" ++unused_variables = "warn" ++unused_lifetimes = "warn" ++unused_macro_rules = "warn" ++unused_crate_dependencies = "warn" ++unreachable_pub = "warn" ++rust_2018_idioms = { level = "warn", priority = -1 } ++elided_lifetimes_in_paths = "warn" ++explicit_outlives_requirements = "warn" ++non_ascii_idents = "warn" ++trivial_bounds = "warn" ++unit_bindings = "warn" ++single_use_lifetimes = "warn" ++redundant_lifetimes = "warn" ++rust_2021_compatibility = { level = "warn", priority = -1 } ++rust_2024_compatibility = { level = "warn", priority = -1 } ++unused_qualifications = "warn" ++noop_method_call = "warn" ++unnameable_types = "warn" ++ ++[workspace.lints.clippy] ++all = { level = "warn", priority = -1 } ++pedantic = { level = "allow", priority = -1 } ++nursery = { level = "allow", priority = -1 } ++cargo = { level = "allow", priority = -1 } ++todo = "warn" ++unimplemented = "warn" ++unreachable = "warn" ++unwrap_used = "warn" ++expect_used = "warn" ++panic = "warn" ++indexing_slicing = "warn" ++map_err_ignore = "warn" ++wildcard_enum_match_arm = "warn" ++std_instead_of_core = "allow" ++std_instead_of_alloc = "allow" ++alloc_instead_of_core = "allow" ++doc_markdown = "allow" ++doc_lazy_continuation = "allow" ++needless_return = "allow" ++match_same_arms = "allow" ++uninlined_format_args = "allow" +diff --git a/Makefile b/Makefile +index bc8fbda..89e24b3 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,32 +1,29 @@ + SHELL = /bin/bash + .SHELLFLAGS = -euo pipefail -c + +-CARGO = cargo +-MEMBERS = libvctrl_handler libvctrl_core libvctrl_plumbing libvctrl_porcelain libvctrl libvctrl_sha512 +-PUBLISH_ORDER = libvctrl_core libvctrl_plumbing libvctrl_porcelain libvctrl_handler libvctrl libvctrl_sha512 ++CARGO = cargo ++MEMBERS = libvctrl_handler libvctrl_core libvctrl_plumbing libvctrl_porcelain libvctrl libvctrl_sha512 + +-PKG ?= libvctrl_handler ++# Default package jika ingin menjalankan CI untuk satu package ++PKG ?= libvctrl_handler + +-CLIPPY_FLAGS ?= -- -D warnings ++# Flag tambahan untuk Clippy (kosong = santai) ++CLIPPY_FLAGS ?= + +-.DEFAULT_GOAL := help ++.PHONY: all ++all: build + + .PHONY: help + help: +- @echo "Usage: make [PKG=] [CLIPPY_FLAGS=]" ++ @echo "Usage: make [PKG=]" + @echo "" + @echo "Targets:" + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ +- | awk 'BEGIN {FS = ":.*?## "}; {printf " %-20s %s\n", $$1, $$2}' +- @echo "" +- @echo "Contoh:" +- @echo " make ci +- @echo " make clippy CLIPPY_FLAGS='' +- @echo " make test-pkg PKG=libvctrl_core" +- +-.PHONY: all +-all: build ++ | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}' + ++# --------------------------------------------------------------------------- ++# Global ++# --------------------------------------------------------------------------- + .PHONY: build + build: + $(CARGO) build --workspace +@@ -39,17 +36,10 @@ release: + check: + $(CARGO) check --workspace + +-.PHONY: check-all +-check-all: +- $(CARGO) check --workspace --all-targets --all-features +- + .PHONY: test + test: + $(CARGO) test --workspace + +-.PHONY: test-all +-test-all: test +- + .PHONY: test-verbose + test-verbose: + RUST_BACKTRACE=1 $(CARGO) test --workspace -- --nocapture +@@ -70,16 +60,10 @@ fmt: + fmt-check: + $(CARGO) fmt --all -- --check + ++# Clippy santai (tidak -D warnings) + .PHONY: clippy + clippy: +- $(CARGO) clippy --workspace --all-targets --all-features $(CLIPPY_FLAGS) +- +-.PHONY: clippy-all +-clippy-all: clippy +- +-.PHONY: clippy-strict +-clippy-strict: +- $(CARGO) clippy --workspace --all-targets --all-features -- -D warnings ++ $(CARGO) clippy --all-targets --all-features $(CLIPPY_FLAGS) + + .PHONY: lint + lint: fmt clippy +@@ -87,9 +71,6 @@ lint: fmt clippy + .PHONY: ci + ci: fmt-check clippy test-verbose + +-.PHONY: ci-fast +-ci-fast: fmt-check clippy test +- + .PHONY: clean + clean: + $(CARGO) clean +@@ -106,11 +87,6 @@ doc-open: doc + bench: + $(CARGO) bench --workspace + +-.PHONY: coverage +-coverage: +- $(CARGO) llvm-cov --workspace --html +- @echo "Coverage report: target/llvm-cov/html/index.html" +- + .PHONY: update + update: + $(CARGO) update +@@ -124,21 +100,35 @@ audit: + fi + + .PHONY: publish-check +-publish-check: ++publish-check: check-readmes + @for crate in $(MEMBERS); do \ +- echo "🔍 Memeriksa packaging $$crate"; \ +- $(CARGO) package -p "$$crate" || exit 1; \ ++ echo "Packaging $$crate"; \ ++ $(CARGO) package -p "$$crate" --no-verify || exit 1; \ + done +- @echo "✅ Semua crate siap publish." ++ @echo "All crates are ready for publish." + + .PHONY: publish-all +-publish-all: +- @for crate in $(PUBLISH_ORDER); do \ +- echo "📦 Publishing $$crate ..."; \ +- $(CARGO) publish -p $$crate || exit 1; \ +- sleep 5; \ +- done +- @echo "✅ Semua crate berhasil dipublish." ++publish-all: check-readmes ++ @echo "Publishing libvctrl_handler ..." ++ $(CARGO) publish -p libvctrl_handler ++ @sleep 5 ++ @echo "Publishing libvctrl_core ..." ++ $(CARGO) publish -p libvctrl_core ++ @sleep 5 ++ @echo "Publishing libvctrl_plumbing ..." ++ $(CARGO) publish -p libvctrl_plumbing ++ @sleep 5 ++ @echo "Publishing libvctrl_porcelain ..." ++ $(CARGO) publish -p libvctrl_porcelain ++ @sleep 5 ++ @echo "Publishing libvctrl (root) ..." ++ $(CARGO) publish -p libvctrl ++ @echo "All crates published successfully." ++ ++.PHONY: coverage ++coverage: ++ $(CARGO) llvm-cov --workspace --html ++ @echo "Coverage report: target/llvm-cov/html/index.html" + + .PHONY: version + version: +@@ -171,7 +161,7 @@ snap: + + .PHONY: run + run: +- $(CARGO) run -p $(PKG) ++ $(CARGO) run + + .PHONY: install + install: +@@ -184,6 +174,9 @@ uninstall: + .PHONY: rebuild + rebuild: release install + ++# --------------------------------------------------------------------------- ++# Package-specific targets (pkg=) ++# --------------------------------------------------------------------------- + .PHONY: build-pkg + build-pkg: + $(CARGO) build -p $(PKG) +@@ -212,19 +205,20 @@ fmt-pkg: + fmt-check-pkg: + $(CARGO) fmt -p $(PKG) -- --check + ++# Clippy per package (santai) + .PHONY: clippy-pkg + clippy-pkg: + $(CARGO) clippy -p $(PKG) --all-targets --all-features $(CLIPPY_FLAGS) + +-.PHONY: clippy-pkg-strict +-clippy-pkg-strict: +- $(CARGO) clippy -p $(PKG) --all-targets --all-features -- -D warnings ++# Alias backward-compatible ++.PHONY: clippy-pkg-unwarn ++clippy-pkg-unwarn: clippy-pkg + + .PHONY: ci-pkg + ci-pkg: fmt-check-pkg clippy-pkg test-verbose-pkg + +-.PHONY: ci-pkg-strict +-ci-pkg-strict: fmt-check-pkg clippy-pkg-strict test-verbose-pkg ++.PHONY: ci-pkg-unwarn ++ci-pkg-unwarn: ci-pkg + + .PHONY: doc-pkg + doc-pkg: +@@ -238,6 +232,9 @@ watch-test-pkg: + watch-build-pkg: + $(CARGO) watch -x 'check -p $(PKG)' + ++# --------------------------------------------------------------------------- ++# Convenience aliases for common packages ++# --------------------------------------------------------------------------- + .PHONY: handler + handler: PKG=libvctrl_handler + handler: ci-pkg +@@ -261,3 +258,8 @@ root-pkg: ci-pkg + .PHONY: sha512 + sha512: PKG=libvctrl_sha512 + sha512: ci-pkg ++ ++# Target khusus kalau mau lebih ketat ++.PHONY: clippy-strict ++clippy-strict: ++ $(CARGO) clippy --all-targets --all-features -- -D warnings +diff --git a/libvctrl/Cargo.toml b/libvctrl/Cargo.toml +index 1431e19..6eccb76 100644 +--- a/libvctrl/Cargo.toml ++++ b/libvctrl/Cargo.toml +@@ -19,9 +19,9 @@ exclude = [ + ] + + [dependencies] +-libvctrl_handler = { path = "../libvctrl_handler", version = "5.0.1" } +-libvctrl_core = { path = "../libvctrl_core", version = "3.0.1" } +-libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.1.0", default-features = false } ++libvctrl_handler = { path = "../libvctrl_handler", version = "5.0.0" } ++libvctrl_core = { path = "../libvctrl_core", version = "3.0.0" } ++libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.0.0", default-features = false } + + [dev-dependencies] + proptest = "1.11.0" +diff --git a/libvctrl/src/lib.rs b/libvctrl/src/lib.rs +index e669390..10df03f 100644 +--- a/libvctrl/src/lib.rs ++++ b/libvctrl/src/lib.rs +@@ -1,65 +1,336 @@ ++//! # libvctrl ++//! ++//! A unified facade for the libvctrl ecosystem. ++//! ++//! This crate aggregates the foundational crates of the version control ++//! system into a single, coherent namespace. It re-exports all core types, ++//! traits, constants, validation functions, and reference implementations ++//! from: ++//! ++//! - [`libvctrl_handler`](https://docs.rs/libvctrl_handler) — abstract ++//! contracts, immutable data types, and system limits. ++//! - [`libvctrl_core`](https://docs.rs/libvctrl_core) — production-ready ++//! reference implementations: binary codec, SHA-512 hasher, builders, and ++//! in-memory stores. ++//! - [`libvctrl_sha512`](https://docs.rs/libvctrl_sha512) — zero-dependency ++//! cryptographic primitives. ++//! ++//! By re-exporting these crates under one roof, `libvctrl` allows downstream ++//! applications to bootstrap a complete version control system without ++//! manually stitching together multiple dependencies. It also serves as the ++//! public API surface for the main binary crate. ++//! ++//! ## Architecture ++//! ++//! The crate exposes three top-level namespaces: ++//! ++//! - [`handler`](crate::handler) — the original `libvctrl_handler` crate. ++//! - [`reference`](crate::reference) — the `libvctrl_core` reference ++//! implementation crate. ++//! - [`crypto`](crate::crypto) — the `libvctrl_sha512` crate. ++//! ++//! In addition, the most commonly used items are re-exported directly at the ++//! crate root for ergonomic access. ++//! ++//! ### Handler re-exports ++//! ++//! Core contracts and types: ++//! ++//! - Traits: [`Encoder`](crate::Encoder), [`Decoder`](crate::Decoder), ++//! [`Hasher`](crate::Hasher), [`ObjectStore`](crate::ObjectStore), ++//! [`RefStore`](crate::RefStore), [`Signer`](crate::Signer), ++//! [`Verifier`](crate::Verifier), [`Transport`](crate::Transport). ++//! - Types: [`Blob`](crate::Blob), [`Tree`](crate::Tree), ++//! [`TreeEntry`](crate::TreeEntry), [`Commit`](crate::Commit), ++//! [`CommitMeta`](crate::CommitMeta), [`Tag`](crate::Tag), ++//! [`Hash`](crate::Hash), [`UserID`](crate::UserID), ++//! [`EntryKind`](crate::EntryKind). ++//! - Error type: [`VctrlError`](crate::VctrlError). ++//! ++//! System limits and validation: ++//! ++//! - Constants such as [`HASH_LENGTH`](crate::HASH_LENGTH), ++//! [`MAX_BLOB_SIZE`](crate::MAX_BLOB_SIZE), ++//! [`MAX_MESSAGE_LENGTH`](crate::MAX_MESSAGE_LENGTH), ++//! [`MAX_NAME_LENGTH`](crate::MAX_NAME_LENGTH), ++//! [`MAX_PARENT_COUNT`](crate::MAX_PARENT_COUNT), and ++//! [`MAX_TREE_ENTRIES`](crate::MAX_TREE_ENTRIES). ++//! - Validation functions: ++//! [`validate_hash_bytes`](crate::validate_hash_bytes), ++//! [`validate_name`](crate::validate_name), ++//! [`validate_ref_name`](crate::validate_ref_name), and ++//! [`validate_tree_entry_name`](crate::validate_tree_entry_name). ++//! ++//! ### Core re-exports ++//! ++//! Reference implementations: ++//! ++//! - Codec: [`BinaryEncoder`](crate::BinaryEncoder) and ++//! [`BinaryDecoder`](crate::BinaryDecoder) for deterministic binary ++//! serialization. ++//! - Hasher: [`Sha512Hasher`](crate::Sha512Hasher) for content addressing. ++//! - Builders: [`BlobBuilder`](crate::BlobBuilder), ++//! [`CommitBuilder`](crate::CommitBuilder), ++//! [`TagBuilder`](crate::TagBuilder), ++//! [`TreeBuilder`](crate::TreeBuilder), and ++//! [`TreeEntryBuilder`](crate::TreeEntryBuilder). ++//! - Stores: [`MemoryStore`](crate::MemoryStore) and ++//! [`MemoryRefStore`](crate::MemoryRefStore). ++//! ++//! ## Why a unified facade? ++//! ++//! The libvctrl workspace is designed around strict separation of concerns. ++//! However, end users often need a single dependency that exposes the full ++//! stack. This crate provides that convenience without hiding the underlying ++//! modularity. Developers can still access the original crates through the ++//! `handler`, `reference`, and `crypto` namespaces. ++//! ++//! ## How it works ++//! ++//! All re-exports are compile-time aliases. There is no runtime overhead, and ++//! no code is duplicated. The only cost is a slightly larger public API ++//! surface. ++//! ++//! ## Safety and quality ++//! ++//! This crate inherits the strict safety guarantees of its dependencies: ++//! ++//! - `#![forbid(unsafe_code)]` — no unsafe code, period. ++//! - Strict Clippy, rustc, and documentation lints are denied. ++//! - All public items are documented and have doctests where applicable. ++//! ++//! ## Example ++//! ++//! The following example demonstrates a typical workflow: create a blob, ++//! encode it, hash it, store it, and retrieve it. ++//! ++//! ``` ++//! # use libvctrl::{Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore}; ++//! # fn main() -> Result<(), libvctrl::VctrlError> { ++//! let blob = Blob::new(b"my content".to_vec())?; ++//! ++//! // Encode the blob into deterministic bytes. ++//! let mut encoded = Vec::new(); ++//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; ++//! ++//! // Hash the encoded bytes to obtain a content address. ++//! let hash = Sha512Hasher.hash(&mut encoded.as_slice())?; ++//! ++//! // Store the encoded object in memory. ++//! let mut store = MemoryStore::new(); ++//! store.put(&hash, &encoded)?; ++//! ++//! // Verify the object exists. ++//! assert!(store.exists(&hash)?); ++//! # Ok(()) ++//! # } ++//! ``` ++//! ++//! Use [`handler`](crate::handler), [`reference`](crate::reference), or ++//! [`crypto`](crate::crypto) if you need direct access to the underlying ++//! crates. ++ + #[cfg(test)] + use proptest as _; + ++/// Re-export of the `libvctrl_core` reference implementation crate. ++/// ++/// This namespace contains production-ready implementations of the handler ++/// contracts: binary codec, SHA-512 hasher, builders, and in-memory stores. + pub use libvctrl_core as reference; + ++/// Re-export of the `libvctrl_handler` contracts and types crate. ++/// ++/// This namespace contains the abstract traits, immutable data types, ++/// validation functions, and system constants that define the core VCS model. + pub use libvctrl_handler as handler; + ++/// Re-export of the `libvctrl_sha512` cryptographic primitives crate. ++/// ++/// This namespace exposes zero-dependency SHA-512, HMAC-SHA512, HKDF-SHA512, ++/// and optional SHA-384 implementations. + pub use libvctrl_sha512 as crypto; + ++/// Handler module re-exports. ++/// ++/// These modules are re-exported for direct access to the original crate's ++/// internal organization. Most users will prefer the flattened root items, ++/// but these are available for advanced use cases. + pub use handler::constants; + ++/// Enumerations and kind discriminants. ++/// ++/// Contains [`EntryKind`](crate::EntryKind) and any other enum types defined ++/// by the handler crate. + pub use handler::enums; + ++/// Error types and constructors. ++/// ++/// Contains [`VctrlError`](crate::VctrlError) and associated error variants. + pub use handler::errors; + ++/// Macros exported by the handler crate. ++/// ++/// These macros assist in implementing common traits or validation logic. + pub use handler::macros; + ++/// Core behavior traits. ++/// ++/// Contains the trait definitions for [`Encoder`](crate::Encoder), ++/// [`Decoder`](crate::Decoder), [`Hasher`](crate::Hasher), ++/// [`ObjectStore`](crate::ObjectStore), [`RefStore`](crate::RefStore), ++/// [`Signer`](crate::Signer), [`Verifier`](crate::Verifier), and ++/// [`Transport`](crate::Transport). + pub use handler::traits; + ++/// Immutable data types. ++/// ++/// Contains the core object model: [`Blob`](crate::Blob), ++/// [`Tree`](crate::Tree), [`TreeEntry`](crate::TreeEntry), ++/// [`Commit`](crate::Commit), [`CommitMeta`](crate::CommitMeta), ++/// [`Tag`](crate::Tag), [`Hash`](crate::Hash), [`UserID`](crate::UserID), ++/// and related types. + pub use handler::types; + ++/// Validation helper functions. ++/// ++/// Contains functions like [`validate_name`](crate::validate_name) and ++/// [`validate_ref_name`](crate::validate_ref_name) used to enforce safety ++/// invariants. + pub use handler::validation; + ++/// System limit constants. ++/// ++/// Re-exports the following constants at the crate root: ++/// ++/// - [`HASH_LENGTH`](crate::HASH_LENGTH) ++/// - [`MAX_BLOB_SIZE`](crate::MAX_BLOB_SIZE) ++/// - [`MAX_MESSAGE_LENGTH`](crate::MAX_MESSAGE_LENGTH) ++/// - [`MAX_NAME_LENGTH`](crate::MAX_NAME_LENGTH) ++/// - [`MAX_PARENT_COUNT`](crate::MAX_PARENT_COUNT) ++/// - [`MAX_TREE_ENTRIES`](crate::MAX_TREE_ENTRIES) + pub use handler::{ + HASH_LENGTH, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, MAX_NAME_LENGTH, MAX_PARENT_COUNT, + MAX_TREE_ENTRIES, + }; + ++/// Represents the kind of a tree entry. ++/// ++/// This enum distinguishes blobs, executable files, symlinks, trees, and ++/// submodules. + pub use handler::EntryKind; + ++/// Unified error type for all libvctrl operations. ++/// ++/// All fallible operations across the ecosystem return this error type. + pub use handler::VctrlError; + ++/// Core behavior traits. ++/// ++/// Re-exports the following traits at the crate root: ++/// ++/// - [`Decoder`](crate::Decoder) ++/// - [`Encoder`](crate::Encoder) ++/// - [`Hasher`](crate::Hasher) ++/// - [`ObjectStore`](crate::ObjectStore) ++/// - [`RefStore`](crate::RefStore) ++/// - [`Signer`](crate::Signer) ++/// - [`Transport`](crate::Transport) ++/// - [`Verifier`](crate::Verifier) + pub use handler::{Decoder, Encoder, Hasher, ObjectStore, RefStore, Signer, Transport, Verifier}; + ++/// Immutable data types. ++/// ++/// Re-exports the following types at the crate root: ++/// ++/// - [`Blob`](crate::Blob) ++/// - [`Commit`](crate::Commit) ++/// - [`CommitMeta`](crate::CommitMeta) ++/// - [`Hash`](crate::Hash) ++/// - [`Tag`](crate::Tag) ++/// - [`Tree`](crate::Tree) ++/// - [`TreeEntry`](crate::TreeEntry) ++/// - [`UserID`](crate::UserID) + pub use handler::{Blob, Commit, CommitMeta, Hash, Tag, Tree, TreeEntry, UserID}; + ++/// Validation functions. ++/// ++/// Re-exports the following functions at the crate root: ++/// ++/// - [`validate_hash_bytes`](crate::validate_hash_bytes) ++/// - [`validate_name`](crate::validate_name) ++/// - [`validate_ref_name`](crate::validate_ref_name) ++/// - [`validate_tree_entry_name`](crate::validate_tree_entry_name) + pub use handler::{ + validate_hash_bytes, validate_name, validate_ref_name, validate_tree_entry_name, + }; + ++/// Core reference implementation re-exports. ++/// ++/// These items provide concrete implementations of the handler contracts. + pub use reference::codec; + ++/// Object builders for ergonomic construction. ++/// ++/// This module contains builder types for blobs, commits, tags, trees, and ++/// tree entries. + pub use reference::object; + ++/// In-memory object and reference stores. ++/// ++/// This module contains [`MemoryStore`](crate::MemoryStore) and ++/// [`MemoryRefStore`](crate::MemoryRefStore). + pub use reference::store; + ++/// Decoder for the binary format. ++/// ++/// This zero-sized type implements [`Decoder`](crate::Decoder) and parses ++/// versioned binary payloads with strict bounds checking. + pub use reference::codec::BinaryDecoder; + ++/// Encoder for the binary format. ++/// ++/// This zero-sized type implements [`Encoder`](crate::Encoder) and produces ++/// deterministic, versioned binary payloads. + pub use reference::codec::BinaryEncoder; + ++/// SHA-512 content hasher. ++/// ++/// This type implements [`Hasher`](crate::Hasher) and produces 64-byte ++/// content addresses. + pub use reference::hash::Sha512Hasher; + ++/// Builder for [`Blob`] objects. ++/// ++/// Provides a fluent API for constructing validated blobs. + pub use reference::object::BlobBuilder; + ++/// Builder for [`Commit`] objects. ++/// ++/// Provides a fluent API for constructing validated commits. + pub use reference::object::CommitBuilder; + ++/// Builder for [`Tag`] objects. ++/// ++/// Provides a fluent API for constructing validated tags. + pub use reference::object::TagBuilder; + ++/// Builder for [`Tree`] objects. ++/// ++/// Provides a fluent API for constructing validated trees. + pub use reference::object::TreeBuilder; + ++/// Builder for [`TreeEntry`] objects. ++/// ++/// Provides a fluent API for constructing validated tree entries. + pub use reference::object::TreeEntryBuilder; + ++/// In-memory reference store. ++/// ++/// Implements [`RefStore`](crate::RefStore) using a `HashMap`. + pub use reference::store::MemoryRefStore; + ++/// In-memory object store. ++/// ++/// Implements [`ObjectStore`](crate::ObjectStore) using a `HashMap`. + pub use reference::store::MemoryStore; +diff --git a/libvctrl_core/Cargo.toml b/libvctrl_core/Cargo.toml +index c9a404e..6db201c 100644 +--- a/libvctrl_core/Cargo.toml ++++ b/libvctrl_core/Cargo.toml +@@ -11,8 +11,8 @@ keywords = ["version-control", "vcs", "core"] + categories = ["development-tools"] + + [dependencies] +-libvctrl_handler = {path = "../libvctrl_handler", version = "5.0.1" } +-libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.1.0" } ++libvctrl_handler = {path = "../libvctrl_handler", version = "5.0.0" } ++libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.0.0" } + + [dev-dependencies] + proptest = "1.11.0" +diff --git a/libvctrl_core/src/codec/binary_decoder.rs b/libvctrl_core/src/codec/binary_decoder.rs +index 5067465..960917d 100644 +--- a/libvctrl_core/src/codec/binary_decoder.rs ++++ b/libvctrl_core/src/codec/binary_decoder.rs +@@ -1,17 +1,75 @@ +-use alloc::str; +-use alloc::sync::Arc; ++//! # Binary Decoder ++//! ++//! This module provides a strict, bounds-checked decoder for the binary ++//! serialization format defined by the sibling encoder. It is the inverse of ++//! the encoder: every byte sequence produced by the encoder is accepted by ++//! this decoder, and every decoded object is guaranteed to satisfy the ++//! invariants of the corresponding `libvctrl_handler` types. ++//! ++//! ## Design rationale ++//! ++//! Decoding untrusted input is one of the most dangerous operations in a ++//! version control system. A naive implementation might trust length prefixes ++//! and parse out of bounds. This decoder therefore follows a "defense in ++//! depth" strategy: ++//! ++//! - The stream is first bounded by a conservative maximum size. ++//! - Every offset is checked before slicing. ++//! - Every string is validated as UTF-8. ++//! - System limits are re-checked after numeric conversion. ++//! ++//! ## How it works ++//! ++//! Each `decode_*` method first calls [`read_bounded`] to slurp the input into ++//! a bounded `Vec`, then calls [`check_version`] to strip and validate the ++//! version byte, and finally parses the remaining bytes with explicit offset ++//! checks. No slice indexing is performed without a preceding bounds check. + + use libvctrl_handler::{ + Blob, Commit, CommitMeta, Decoder, EntryKind, HASH_LENGTH, Hash, MAX_BLOB_SIZE, + MAX_MESSAGE_LENGTH, MAX_TREE_ENTRIES, Tag, Tree, TreeEntry, UserID, VctrlError, + }; ++use std::str; + ++/// The binary format version this decoder accepts. + const EXPECTED_VERSION: u8 = 3; + +-#[derive(Debug, Copy, Clone)] ++/// Decodes the binary format for Git objects. ++/// ++/// `BinaryDecoder` is a zero-sized type that implements [`Decoder`]. It accepts ++/// any [`std::io::Read`] source and verifies the version byte, length prefixes, ++/// and all system limits before constructing the object. ++/// ++/// # Why this struct exists ++/// ++/// The encoder/decoder split separates serialization concerns. `BinaryDecoder` ++/// ensures that reading data from external sources is as safe as constructing ++/// objects directly through the handler types. ++/// ++/// # How it works ++/// ++/// Each `decode_*` method first calls [`read_bounded`] to slurp the input into ++/// a bounded `Vec`, then calls [`check_version`] to strip the version byte, ++/// and finally parses the remaining bytes with explicit offset checks. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_handler::Decoder; ++/// use libvctrl_core::codec::BinaryDecoder; ++/// ++/// let decoder = BinaryDecoder; ++/// // Decoding methods require an encoded byte stream; see the individual ++/// // `decode_blob`, `decode_tree`, `decode_commit`, and `decode_tag` examples. ++/// ``` + pub struct BinaryDecoder; + + impl BinaryDecoder { ++ /// Strips and validates the version byte. ++ /// ++ /// The first byte of every encoded object must equal [`EXPECTED_VERSION`]. ++ /// Returns the remaining bytes if valid, otherwise a ++ /// [`VctrlError::CorruptedData`]. + fn check_version(data: &[u8]) -> Result<&[u8], VctrlError> { + let version = data + .first() +@@ -27,6 +85,12 @@ impl BinaryDecoder { + .ok_or_else(|| VctrlError::CorruptedData("missing payload after version".into())) + } + ++ /// Reads the reader into memory while enforcing a hard size bound. ++ /// ++ /// This helper prevents denial-of-service attacks by refusing to allocate ++ /// more than `max_size` bytes. It uses a fixed 4 KiB buffer to avoid ++ /// reallocation on each byte and returns [`VctrlError::IoError`] if the ++ /// underlying reader fails. + fn read_bounded( + reader: &mut R, + max_size: usize, +@@ -36,7 +100,7 @@ impl BinaryDecoder { + loop { + let n = reader + .read(&mut chunk) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + if n == 0 { + break; + } +@@ -50,12 +114,14 @@ impl BinaryDecoder { + Ok(buf) + } + ++ /// Returns a single byte at `pos`, or a structured error. + fn require_byte(data: &[u8], pos: usize, what: &str) -> Result { + data.get(pos) + .copied() + .ok_or_else(|| VctrlError::CorruptedData(format!("missing {what}"))) + } + ++ /// Returns a slice `data[start..start+len]`, with overflow and bounds checks. + fn require_slice<'a>( + data: &'a [u8], + start: usize, +@@ -71,6 +137,35 @@ impl BinaryDecoder { + } + + impl Decoder for BinaryDecoder { ++ /// Decodes a binary blob. ++ /// ++ /// # Format ++ /// ++ /// The encoded blob starts with a version byte (currently `3`), followed by ++ /// an 8-byte little-endian length prefix and exactly that many data bytes. ++ /// The declared byte length is re-checked against [`MAX_BLOB_SIZE`]. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::CorruptedData`] if the version is wrong, the length ++ /// prefix is truncated, the blob exceeds the limit, or the declared length ++ /// does not match the remaining bytes. Returns [`VctrlError::IoError`] if the ++ /// reader fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Blob, Decoder, Encoder}; ++ /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++ /// let original = Blob::new(b"hello world".to_vec()).unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_blob(&original, &mut encoded).unwrap(); ++ /// ++ /// let decoded = BinaryDecoder.decode_blob(Cursor::new(encoded.as_slice())).unwrap(); ++ /// assert_eq!(decoded, original); ++ /// ``` + fn decode_blob(&self, mut reader: R) -> Result { + let max_size = usize::try_from(MAX_BLOB_SIZE).unwrap_or(usize::MAX) + 16; + let data = Self::read_bounded(&mut reader, max_size)?; +@@ -98,6 +193,38 @@ impl Decoder for BinaryDecoder { + Blob::new(payload.to_vec()) + } + ++ /// Decodes a binary tree. ++ /// ++ /// # Format ++ /// ++ /// After the version byte, a 4-byte little-endian count is followed by that ++ /// many entries. Each entry starts with a one-byte name length, a UTF-8 name, ++ /// a one-byte kind tag, and a 64-byte hash. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::CorruptedData`] if any prefix is truncated, the entry ++ /// count exceeds [`MAX_TREE_ENTRIES`], the name is not valid UTF-8, the kind ++ /// byte is unknown, the hash is invalid, or the final parsed position does not ++ /// equal the total byte length. Also returns validation errors from ++ /// [`Tree::new`] and [`TreeEntry::new`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Decoder, Encoder, EntryKind, Hash, Tree, TreeEntry}; ++ /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++ /// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let entry = TreeEntry::new("a.txt".to_owned(), EntryKind::Blob, hash).unwrap(); ++ /// let original = Tree::new(vec![entry]).unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_tree(&original, &mut encoded).unwrap(); ++ /// ++ /// let decoded = BinaryDecoder.decode_tree(Cursor::new(encoded.as_slice())).unwrap(); ++ /// assert_eq!(decoded, original); ++ /// ``` + fn decode_tree(&self, mut reader: R) -> Result { + let max_size = usize::try_from(MAX_TREE_ENTRIES).unwrap_or(usize::MAX) * 321 + 5; + let data = Self::read_bounded(&mut reader, max_size)?; +@@ -157,15 +284,57 @@ impl Decoder for BinaryDecoder { + Tree::new(entries) + } + ++ /// Decodes a binary commit. ++ /// ++ /// # Format ++ /// ++ /// The commit layout is fixed: tree hash, u16 parent count, parent hashes, ++ /// author name/email with u8 length prefixes, committer name/email, u32 ++ /// message length, message bytes, i64 timestamp, i16 timezone offset, and an ++ /// optional encoding string. All integer fields are little-endian. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::CorruptedData`] for structural issues and ++ /// [`VctrlError::SerializationError`] if the message exceeds ++ /// [`MAX_MESSAGE_LENGTH`]. Also returns validation errors from ++ /// [`Commit::with_meta`] and [`UserID::new`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Commit, Decoder, Encoder, Hash, UserID}; ++ /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++ /// let tree = Hash::from_bytes(&[1u8; 64]).unwrap(); ++ /// let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); ++ /// let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); ++ /// let original = Commit::new( ++ /// tree, ++ /// vec![], ++ /// author, ++ /// committer, ++ /// "Initial commit".to_owned(), ++ /// ) ++ /// .unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_commit(&original, &mut encoded).unwrap(); ++ /// ++ /// let decoded = BinaryDecoder.decode_commit(Cursor::new(encoded.as_slice())).unwrap(); ++ /// assert_eq!(decoded, original); ++ /// ``` + #[allow(clippy::too_many_lines)] + fn decode_commit(&self, mut reader: R) -> Result { + let max_size = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX) + 1024; + let data = Self::read_bounded(&mut reader, max_size)?; + let data = Self::check_version(&data)?; + ++ // Tree hash + let tree_hash = Self::require_slice(data, 0, HASH_LENGTH, "commit tree hash")?; + let tree = Hash::from_bytes(tree_hash)?; + ++ // Parent count and parents + let parent_count_bytes = Self::require_slice(data, HASH_LENGTH, 2, "commit parent count")?; + let parent_count = u16::from_le_bytes( + parent_count_bytes +@@ -181,6 +350,7 @@ impl Decoder for BinaryDecoder { + pos += HASH_LENGTH; + } + ++ // Author name + let author_name_len = Self::require_byte(data, pos, "author name length")? as usize; + pos += 1; + let author_name_bytes = Self::require_slice(data, pos, author_name_len, "author name")?; +@@ -189,6 +359,7 @@ impl Decoder for BinaryDecoder { + .to_string(); + pos += author_name_len; + ++ // Author email + let author_email_len = Self::require_byte(data, pos, "author email length")? as usize; + pos += 1; + let author_email_bytes = Self::require_slice(data, pos, author_email_len, "author email")?; +@@ -199,6 +370,7 @@ impl Decoder for BinaryDecoder { + + let author = UserID::new(author_name, author_email)?; + ++ // Committer name + let committer_name_len = Self::require_byte(data, pos, "committer name length")? as usize; + pos += 1; + let committer_name_bytes = +@@ -210,6 +382,7 @@ impl Decoder for BinaryDecoder { + .to_string(); + pos += committer_name_len; + ++ // Committer email + let committer_email_len = Self::require_byte(data, pos, "committer email length")? as usize; + pos += 1; + let committer_email_bytes = +@@ -223,6 +396,7 @@ impl Decoder for BinaryDecoder { + + let committer = UserID::new(committer_name, committer_email)?; + ++ // Message + let msg_len_bytes = Self::require_slice(data, pos, 4, "commit message length")?; + let msg_len = u32::from_le_bytes( + msg_len_bytes +@@ -243,6 +417,7 @@ impl Decoder for BinaryDecoder { + .to_string(); + pos += msg_len; + ++ // Timestamp and timezone + let timestamp_bytes = Self::require_slice(data, pos, 8, "commit timestamp")?; + let timestamp = i64::from_le_bytes( + timestamp_bytes +@@ -259,6 +434,7 @@ impl Decoder for BinaryDecoder { + ); + pos += 2; + ++ // Optional encoding + let encoding_len = Self::require_byte(data, pos, "commit encoding length")? as usize; + pos += 1; + let encoding = if encoding_len > 0 { +@@ -280,12 +456,50 @@ impl Decoder for BinaryDecoder { + Commit::with_meta(tree, parents, author, committer, message, meta) + } + ++ /// Decodes a binary tag. ++ /// ++ /// # Format ++ /// ++ /// Tag starts with a one-byte name length and name, a 64-byte target hash, a ++ /// tagger presence byte, optional tagger name/email, u32 message length, ++ /// message, timestamp, timezone offset, and optional encoding. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::CorruptedData`] for structural issues and ++ /// [`VctrlError::SerializationError`] if the message exceeds ++ /// [`MAX_MESSAGE_LENGTH`]. Also returns validation errors from ++ /// [`Tag::with_meta`] and [`UserID::new`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Decoder, Encoder, Hash, Tag, UserID}; ++ /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++ /// let target = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// let tagger = UserID::new("Tagger".to_owned(), "tagger@example.com".to_owned()).unwrap(); ++ /// let original = Tag::new( ++ /// "v1.0.0".to_owned(), ++ /// target, ++ /// Some(tagger), ++ /// "Release".to_owned(), ++ /// ) ++ /// .unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_tag(&original, &mut encoded).unwrap(); ++ /// ++ /// let decoded = BinaryDecoder.decode_tag(Cursor::new(encoded.as_slice())).unwrap(); ++ /// assert_eq!(decoded, original); ++ /// ``` + #[allow(clippy::too_many_lines)] + fn decode_tag(&self, mut reader: R) -> Result { + let max_size = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX) + 1024; + let data = Self::read_bounded(&mut reader, max_size)?; + let data = Self::check_version(&data)?; + ++ // Tag name + let name_len = Self::require_byte(data, 0, "tag name length")? as usize; + let name_bytes = Self::require_slice(data, 1, name_len, "tag name")?; + let name = str::from_utf8(name_bytes) +@@ -293,10 +507,12 @@ impl Decoder for BinaryDecoder { + .to_string(); + let mut pos = 1 + name_len; + ++ // Target hash + let target_bytes = Self::require_slice(data, pos, HASH_LENGTH, "tag target hash")?; + let target = Hash::from_bytes(target_bytes)?; + pos += HASH_LENGTH; + ++ // Tagger presence + let has_tagger = match Self::require_byte(data, pos, "tagger presence byte")? { + 0 => false, + 1 => true, +@@ -308,6 +524,7 @@ impl Decoder for BinaryDecoder { + }; + pos += 1; + ++ // Optional tagger + let tagger = if has_tagger { + let tagger_name_len = Self::require_byte(data, pos, "tagger name length")? as usize; + pos += 1; +@@ -335,6 +552,7 @@ impl Decoder for BinaryDecoder { + None + }; + ++ // Message + let msg_len_bytes = Self::require_slice(data, pos, 4, "tag message length")?; + let msg_len = u32::from_le_bytes( + msg_len_bytes +@@ -355,6 +573,7 @@ impl Decoder for BinaryDecoder { + .to_string(); + pos += msg_len; + ++ // Timestamp and timezone + let timestamp_bytes = Self::require_slice(data, pos, 8, "tag timestamp")?; + let timestamp = i64::from_le_bytes( + timestamp_bytes +@@ -371,6 +590,7 @@ impl Decoder for BinaryDecoder { + ); + pos += 2; + ++ // Optional encoding + let encoding_len = Self::require_byte(data, pos, "tag encoding length")? as usize; + pos += 1; + let encoding = if encoding_len > 0 { +@@ -392,274 +612,3 @@ impl Decoder for BinaryDecoder { + Tag::with_meta(name, target, tagger, message, meta) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- use crate::codec::BinaryEncoder; +- use libvctrl_handler::{Encoder, TreeEntry}; +- use std::io::Cursor; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- fn user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +- } +- +- fn meta(ts: i64, tz: i16) -> Result { +- CommitMeta::new(ts, tz, None) +- } +- +- #[test] +- fn check_version_valid() -> Result<(), VctrlError> { +- let data = [3_u8, 42]; +- let rest = BinaryDecoder::check_version(&data)?; +- assert_eq!(rest, &[42]); +- Ok(()) +- } +- +- #[test] +- fn check_version_missing_byte() { +- assert!(BinaryDecoder::check_version(&[]).is_err()); +- } +- +- #[test] +- fn check_version_unsupported() -> Result<(), VctrlError> { +- let result = BinaryDecoder::check_version(&[4_u8]); +- assert!(result.is_err()); +- match result { +- Err(VctrlError::CorruptedData(msg)) => { +- assert!(msg.contains("unsupported version")); +- } +- _ => return Err(VctrlError::Other("expected CorruptedData".into())), +- } +- Ok(()) +- } +- +- #[test] +- fn read_bounded_within_limit() -> Result<(), VctrlError> { +- let mut reader = Cursor::new(vec![1_u8, 2, 3]); +- let data = BinaryDecoder::read_bounded(&mut reader, 10)?; +- assert_eq!(data, vec![1, 2, 3]); +- Ok(()) +- } +- +- #[test] +- fn read_bounded_exceeds_limit() { +- let mut reader = Cursor::new(vec![1_u8, 2, 3, 4]); +- assert!(BinaryDecoder::read_bounded(&mut reader, 2).is_err()); +- } +- +- #[test] +- fn require_byte_valid() -> Result<(), VctrlError> { +- let value = BinaryDecoder::require_byte(&[10, 20], 1, "second byte")?; +- assert_eq!(value, 20); +- Ok(()) +- } +- +- #[test] +- fn require_byte_missing() { +- assert!(BinaryDecoder::require_byte(&[10], 1, "second byte").is_err()); +- } +- +- #[test] +- fn require_slice_valid() -> Result<(), VctrlError> { +- let data = [1, 2, 3, 4]; +- let slice = BinaryDecoder::require_slice(&data, 1, 2, "middle")?; +- assert_eq!(slice, &[2, 3]); +- Ok(()) +- } +- +- #[test] +- fn require_slice_overflow() { +- let data = [1, 2, 3]; +- assert!(BinaryDecoder::require_slice(&data, usize::MAX, 2, "overflow").is_err()); +- } +- +- #[test] +- fn decode_blob_valid_roundtrip() -> Result<(), VctrlError> { +- let encoder = BinaryEncoder; +- let codec = BinaryDecoder; +- let payload = vec![1_u8, 2, 3, 4]; +- +- let blob = Blob::new(payload.clone())?; +- let mut buf = Vec::new(); +- encoder.encode_blob(&blob, &mut buf)?; +- let decoded = codec.decode_blob(Cursor::new(buf))?; +- assert_eq!(decoded.data(), payload.as_slice()); +- Ok(()) +- } +- +- #[test] +- fn decode_blob_invalid_version() { +- let codec = BinaryDecoder; +- let data = [4_u8, 0, 0, 0, 0, 0, 0, 0, 0]; +- assert!(codec.decode_blob(Cursor::new(data)).is_err()); +- } +- +- #[test] +- fn decode_blob_length_mismatch() { +- let codec = BinaryDecoder; +- let mut data = Vec::new(); +- data.push(3_u8); +- data.extend_from_slice(&5_u64.to_le_bytes()); +- data.push(1_u8); +- assert!(codec.decode_blob(Cursor::new(data)).is_err()); +- } +- +- #[test] +- fn decode_tree_valid_roundtrip() -> Result<(), VctrlError> { +- let encoder = BinaryEncoder; +- let codec = BinaryDecoder; +- +- let hash = hash_byte(0x22)?; +- let entry = TreeEntry::new("a.txt".to_string(), EntryKind::Blob, hash)?; +- let tree = Tree::new(vec![entry])?; +- +- let mut buf = Vec::new(); +- encoder.encode_tree(&tree, &mut buf)?; +- let decoded = codec.decode_tree(Cursor::new(buf))?; +- +- let entries = decoded.entries(); +- assert_eq!(entries.len(), 1); +- let first = entries +- .first() +- .ok_or_else(|| VctrlError::Other("expected one entry".into()))?; +- assert_eq!(first.name(), "a.txt"); +- assert_eq!(first.kind(), EntryKind::Blob); +- assert_eq!(*first.hash(), hash); +- Ok(()) +- } +- +- #[test] +- fn decode_tree_unknown_kind() -> Result<(), VctrlError> { +- let codec = BinaryDecoder; +- let mut data = Vec::new(); +- data.push(3_u8); +- data.extend_from_slice(&1_u32.to_le_bytes()); +- data.push(1_u8); +- data.push(b'a'); +- data.push(9_u8); +- data.extend_from_slice(hash_byte(0x33)?.as_bytes()); +- +- let result = codec.decode_tree(Cursor::new(data)); +- assert!(result.is_err()); +- match result { +- Err(VctrlError::CorruptedData(msg)) => { +- assert!(msg.contains("unknown entry kind")); +- } +- _ => return Err(VctrlError::Other("expected CorruptedData".into())), +- } +- Ok(()) +- } +- +- #[test] +- fn decode_commit_valid_roundtrip() -> Result<(), VctrlError> { +- let encoder = BinaryEncoder; +- let codec = BinaryDecoder; +- +- let tree = hash_byte(0x01)?; +- let parent = hash_byte(0x02)?; +- let author = user("Alice", "alice@example.com")?; +- let committer = user("Bob", "bob@example.com")?; +- let message = "initial commit".to_string(); +- let meta = meta(1_600_000_000, 0)?; +- +- let commit = +- Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; +- +- let mut buf = Vec::new(); +- encoder.encode_commit(&commit, &mut buf)?; +- let decoded = codec.decode_commit(Cursor::new(buf))?; +- +- assert_eq!(decoded.tree(), &tree); +- let parents = decoded.parents(); +- assert_eq!(parents.len(), 1); +- assert_eq!(parents.first(), Some(&parent)); +- assert_eq!(decoded.author().name(), "Alice"); +- assert_eq!(decoded.committer().email(), "bob@example.com"); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 1_600_000_000); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- assert!(decoded.meta().encoding().is_none()); +- Ok(()) +- } +- +- #[test] +- fn decode_commit_trailing_bytes() -> Result<(), VctrlError> { +- let encoder = BinaryEncoder; +- let codec = BinaryDecoder; +- +- let tree = hash_byte(0x01)?; +- let author = user("Alice", "alice@example.com")?; +- let committer = user("Bob", "bob@example.com")?; +- let message = "initial commit".to_string(); +- let meta = meta(1_600_000_000, 0)?; +- +- let commit = Commit::with_meta(tree, vec![], author, committer, message, meta)?; +- let mut buf = Vec::new(); +- encoder.encode_commit(&commit, &mut buf)?; +- buf.push(0_u8); +- +- assert!(codec.decode_commit(Cursor::new(buf)).is_err()); +- Ok(()) +- } +- +- #[test] +- fn decode_tag_valid_roundtrip() -> Result<(), VctrlError> { +- let encoder = BinaryEncoder; +- let codec = BinaryDecoder; +- +- let target = hash_byte(0x33)?; +- let tagger = user("Tagger", "tagger@example.com")?; +- let message = "v1.0".to_string(); +- let meta = meta(1_600_000_000, 0)?; +- +- let tag = Tag::with_meta( +- "v1.0".to_string(), +- target, +- Some(tagger), +- message.clone(), +- meta, +- )?; +- +- let mut buf = Vec::new(); +- encoder.encode_tag(&tag, &mut buf)?; +- let decoded = codec.decode_tag(Cursor::new(buf))?; +- +- assert_eq!(decoded.name(), "v1.0"); +- assert_eq!(decoded.target(), &target); +- let decoded_tagger = decoded +- .tagger() +- .ok_or_else(|| VctrlError::Other("expected tagger".into()))?; +- assert_eq!(decoded_tagger.name(), "Tagger"); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 1_600_000_000); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- assert!(decoded.meta().encoding().is_none()); +- Ok(()) +- } +- +- #[test] +- fn decode_tag_invalid_tagger_presence() -> Result<(), VctrlError> { +- let codec = BinaryDecoder; +- let mut data = Vec::new(); +- data.push(3_u8); +- data.push(1_u8); +- data.push(b'v'); +- data.extend_from_slice(hash_byte(0x33)?.as_bytes()); +- data.push(2_u8); +- +- let result = codec.decode_tag(Cursor::new(data)); +- assert!(result.is_err()); +- match result { +- Err(VctrlError::CorruptedData(msg)) => { +- assert!(msg.contains("invalid tagger presence")); +- } +- _ => return Err(VctrlError::Other("expected CorruptedData".into())), +- } +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/codec/binary_encoder.rs b/libvctrl_core/src/codec/binary_encoder.rs +index 9bad0c1..2bd8f73 100644 +--- a/libvctrl_core/src/codec/binary_encoder.rs ++++ b/libvctrl_core/src/codec/binary_encoder.rs +@@ -1,14 +1,112 @@ ++//! # Binary Encoder ++//! ++//! This module provides a deterministic, versioned, little-endian binary ++//! encoder for every core object type defined by `libvctrl_handler`. ++//! ++//! The encoder is the counterpart to [`BinaryDecoder`](super::binary_decoder::BinaryDecoder). ++//! Data written by this encoder can always be decoded back into an equivalent ++//! object, provided the same system limits and version are used. ++//! ++//! ## Design rationale ++//! ++//! Version control objects are content-addressed. Deterministic serialization ++//! is therefore critical: the same object must always produce exactly the same ++//! bytes, otherwise the hash changes and the object becomes unreachable. ++//! ++//! The encoder achieves determinism by: ++//! ++//! - Using a fixed version byte. ++//! - Using little-endian integer encoding on all supported platforms. ++//! - Writing fields in a strict, documented order. ++//! - Never depending on platform-specific layouts. ++//! ++//! ## How it works ++//! ++//! Every `encode_*` method writes directly to the supplied writer. Length ++//! prefixes are validated before conversion to prevent silent truncation. ++//! All string fields are encoded as a one-byte length followed by UTF-8 bytes. ++//! The writer uses [`std::io::Write::write_all`] to guarantee complete writes. ++ + use libvctrl_handler::{ + Blob, Commit, Encoder, EntryKind, MAX_MESSAGE_LENGTH, Tag, Tree, VctrlError, + }; + use std::io::Write; + ++/// The current version of the binary encoding format. ++/// ++/// This version byte is written as the first byte of every encoded object. ++/// The decoder rejects any input whose first byte does not equal this value. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_core::codec::VERSION; ++/// assert_eq!(VERSION, 3); ++/// ``` + pub const VERSION: u8 = 3; + +-#[derive(Debug, Default, Clone, Copy)] ++/// An encoder for the binary format of Git objects. ++/// ++/// `BinaryEncoder` is a stateless, zero-sized type that implements the ++/// [`Encoder`] trait. It converts high-level objects such as [`Blob`], ++/// [`Tree`], [`Commit`], and [`Tag`] into a compact, versioned byte stream. ++/// ++/// # Why this struct exists ++/// ++/// Serialization is isolated behind a trait so that different storage backends ++/// can use different wire formats. `BinaryEncoder` is the reference ++/// implementation and defines the canonical on-disk format for the workspace. ++/// ++/// # How it works ++/// ++/// Each method writes to a [`std::io::Write`] implementation. The encoder does ++/// not allocate the entire payload upfront; it streams fields directly to the ++/// writer. However, all length conversions are checked with `try_from`, so ++/// impossible lengths are reported as [`VctrlError::SerializationError`] ++/// instead of causing silent truncation. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use std::io::Cursor; ++/// # use libvctrl_handler::{Blob, Encoder}; ++/// # use libvctrl_core::codec::BinaryEncoder; ++/// let blob = Blob::new(b"hello".to_vec()).unwrap(); ++/// let mut buf = Vec::new(); ++/// BinaryEncoder.encode_blob(&blob, &mut buf).unwrap(); ++/// assert_eq!(buf[0], 3); ++/// assert_eq!(buf.len(), 1 + 8 + 5); ++/// ``` + pub struct BinaryEncoder; + + impl Encoder for BinaryEncoder { ++ /// Encodes a [`Blob`] into the binary format. ++ /// ++ /// The output layout is: ++ /// ++ /// | Offset | Size | Field | ++ /// |--------|------------|---------------------| ++ /// | 0 | 1 | Version byte | ++ /// | 1 | 8 | `data_len` (u64 LE) | ++ /// | 9 | `data_len` | Raw blob data | ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::IoError`] if the writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Blob, Encoder}; ++ /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; ++ /// let blob = Blob::new(b"hello world".to_vec()).unwrap(); ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_blob(&blob, &mut encoded).unwrap(); ++ /// ++ /// assert_eq!(encoded[0], VERSION); ++ /// assert_eq!(encoded.len(), 1 + 8 + blob.data().len()); ++ /// ``` + fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { + let data = blob.data(); + writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; +@@ -19,7 +117,47 @@ impl Encoder for BinaryEncoder { + Ok(()) + } + +- #[allow(clippy::wildcard_enum_match_arm)] ++ /// Encodes a [`Tree`] into the binary format. ++ /// ++ /// The output layout is: ++ /// ++ /// | Offset | Size | Field | ++ /// |--------|------------|------------------------------------------| ++ /// | 0 | 1 | Version byte | ++ /// | 1 | 4 | `entry_count` (u32 LE) | ++ /// | 5 | varies | Repeated entries, each consisting of: | ++ /// | | | - `name_len` (u8) | ++ /// | | | - `name` (UTF-8) | ++ /// | | | - `kind_byte` (u8) | ++ /// | | | - `hash` (64 bytes) | ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::SerializationError`] if: ++ /// ++ /// - the tree contains more than `u32::MAX` entries, ++ /// - an entry name is longer than `u8::MAX` bytes, ++ /// - an entry kind is unknown. ++ /// ++ /// Returns [`VctrlError::IoError`] if the writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Encoder, EntryKind, Hash, Tree, TreeEntry}; ++ /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; ++ /// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let entry = TreeEntry::new("a.txt".to_owned(), EntryKind::Blob, hash).unwrap(); ++ /// let tree = Tree::new(vec![entry]).unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_tree(&tree, &mut encoded).unwrap(); ++ /// ++ /// assert_eq!(encoded[0], VERSION); ++ /// let count = u32::from_le_bytes(encoded[1..5].try_into().unwrap()); ++ /// assert_eq!(count, 1); ++ /// ``` + fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError> { + let entries = tree.entries(); + writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; +@@ -44,7 +182,9 @@ impl Encoder for BinaryEncoder { + EntryKind::Symlink => 2, + EntryKind::Tree => 3, + EntryKind::Submodule => 4, +- _ => return Err(VctrlError::SerializationError("unknown entry kind".into())), ++ _ => { ++ return Err(VctrlError::SerializationError("unknown entry kind".into())); ++ } + }; + writer + .write_all(&[kind_byte]) +@@ -56,6 +196,67 @@ impl Encoder for BinaryEncoder { + Ok(()) + } + ++ /// Encodes a [`Commit`] into the binary format. ++ /// ++ /// The output layout is fixed and ordered: ++ /// ++ /// | Field | Size | ++ /// |-----------------------|---------------| ++ /// | Version | 1 | ++ /// | Tree hash | 64 | ++ /// | Parent count | 2 (u16 LE) | ++ /// | Parent hashes | 64 * count | ++ /// | Author name length | 1 | ++ /// | Author name | length | ++ /// | Author email length | 1 | ++ /// | Author email | length | ++ /// | Committer name length | 1 | ++ /// | Committer name | length | ++ /// | Committer email length| 1 | ++ /// | Committer email | length | ++ /// | Message length | 4 (u32 LE) | ++ /// | Message | length | ++ /// | Timestamp | 8 (i64 LE) | ++ /// | Timezone offset | 2 (i16 LE) | ++ /// | Encoding length | 1 | ++ /// | Encoding | length or 0 | ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::SerializationError`] if: ++ /// ++ /// - the commit has more than `u16::MAX` parents, ++ /// - any name or email is longer than `u8::MAX` bytes, ++ /// - the message length cannot be represented as `u32`, ++ /// - the message exceeds [`MAX_MESSAGE_LENGTH`], ++ /// - the encoding string is longer than `u8::MAX` bytes. ++ /// ++ /// Returns [`VctrlError::IoError`] if the writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Commit, Encoder, Hash, UserID}; ++ /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; ++ /// let tree = Hash::from_bytes(&[1u8; 64]).unwrap(); ++ /// let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); ++ /// let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); ++ /// let commit = Commit::new( ++ /// tree, ++ /// vec![], ++ /// author, ++ /// committer, ++ /// "Initial commit".to_owned(), ++ /// ) ++ /// .unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_commit(&commit, &mut encoded).unwrap(); ++ /// ++ /// assert_eq!(encoded[0], VERSION); ++ /// assert!(encoded.len() > 1 + 64 + 2); ++ /// ``` + fn encode_commit( + &self, + commit: &Commit, +@@ -73,9 +274,9 @@ impl Encoder for BinaryEncoder { + .write_all(&parent_count.to_le_bytes()) + .map_err(VctrlError::from_io)?; + +- for parent in parents { ++ for p in parents { + writer +- .write_all(parent.as_bytes()) ++ .write_all(p.as_bytes()) + .map_err(VctrlError::from_io)?; + } + +@@ -151,11 +352,67 @@ impl Encoder for BinaryEncoder { + .write_all(enc.as_bytes()) + .map_err(VctrlError::from_io)?; + } +- None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, ++ None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + } + Ok(()) + } + ++ /// Encodes a [`Tag`] into the binary format. ++ /// ++ /// The output layout is: ++ /// ++ /// | Field | Size | ++ /// |--------------------|--------------| ++ /// | Version | 1 | ++ /// | Name length | 1 | ++ /// | Name | length | ++ /// | Target hash | 64 | ++ /// | Tagger presence | 1 | ++ /// | Tagger name length | 1 or omitted | ++ /// | Tagger name | length | ++ /// | Tagger email length| 1 or omitted | ++ /// | Tagger email | length | ++ /// | Message length | 4 (u32 LE) | ++ /// | Message | length | ++ /// | Timestamp | 8 (i64 LE) | ++ /// | Timezone offset | 2 (i16 LE) | ++ /// | Encoding length | 1 | ++ /// | Encoding | length or 0 | ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::SerializationError`] if: ++ /// ++ /// - the tag name is longer than `u8::MAX` bytes, ++ /// - a tagger name or email is longer than `u8::MAX` bytes, ++ /// - the message cannot be represented as `u32`, ++ /// - the message exceeds [`MAX_MESSAGE_LENGTH`], ++ /// - the encoding string is longer than `u8::MAX` bytes. ++ /// ++ /// Returns [`VctrlError::IoError`] if the writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Cursor; ++ /// # use libvctrl_handler::{Encoder, Hash, Tag, UserID}; ++ /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; ++ /// let target = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// let tagger = UserID::new("Tagger".to_owned(), "tagger@example.com".to_owned()).unwrap(); ++ /// let tag = Tag::new( ++ /// "v1.0.0".to_owned(), ++ /// target, ++ /// Some(tagger), ++ /// "Release".to_owned(), ++ /// ) ++ /// .unwrap(); ++ /// ++ /// let mut encoded = Vec::new(); ++ /// BinaryEncoder.encode_tag(&tag, &mut encoded).unwrap(); ++ /// ++ /// assert_eq!(encoded[0], VERSION); ++ /// assert!(encoded.len() > 1 + 64 + 1); ++ /// ``` + fn encode_tag(&self, tag: &Tag, writer: &mut W) -> Result<(), VctrlError> { + writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; + +@@ -173,7 +430,7 @@ impl Encoder for BinaryEncoder { + + match tag.tagger() { + Some(tagger) => { +- writer.write_all(&[1_u8]).map_err(VctrlError::from_io)?; ++ writer.write_all(&[1u8]).map_err(VctrlError::from_io)?; + + let tagger_name = tagger.name(); + writer +@@ -195,7 +452,7 @@ impl Encoder for BinaryEncoder { + .write_all(tagger_email.as_bytes()) + .map_err(VctrlError::from_io)?; + } +- None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, ++ None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + } + + let msg = tag.message(); +@@ -230,113 +487,8 @@ impl Encoder for BinaryEncoder { + .write_all(enc.as_bytes()) + .map_err(VctrlError::from_io)?; + } +- None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, ++ None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + } + Ok(()) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- use crate::codec::BinaryDecoder; +- use libvctrl_handler::{CommitMeta, Decoder, Hash, TreeEntry, UserID}; +- use std::io::Cursor; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- fn user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +- } +- +- #[test] +- fn encode_blob_exact_bytes() -> Result<(), VctrlError> { +- let blob = Blob::new(vec![1_u8, 2, 3])?; +- let mut buf = Vec::new(); +- BinaryEncoder.encode_blob(&blob, &mut buf)?; +- assert_eq!(buf, vec![3, 3, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3]); +- Ok(()) +- } +- +- #[test] +- fn encode_tree_exact_prefix() -> Result<(), VctrlError> { +- let hash = hash_byte(0x22)?; +- let entry = TreeEntry::new("a".to_string(), EntryKind::Blob, hash)?; +- let tree = Tree::new(vec![entry])?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_tree(&tree, &mut buf)?; +- +- assert_eq!(buf.first(), Some(&3_u8)); +- assert_eq!(buf.get(1..5), Some(&1_u32.to_le_bytes()[..])); +- assert_eq!(buf.get(5), Some(&1_u8)); +- assert_eq!(buf.get(6), Some(&b'a')); +- assert_eq!(buf.get(7), Some(&0_u8)); +- assert_eq!(buf.len(), 5 + 1 + 1 + 1 + 64); +- Ok(()) +- } +- +- #[test] +- fn encode_commit_roundtrip_with_decoder() -> Result<(), VctrlError> { +- let tree = hash_byte(0x11)?; +- let parent = hash_byte(0x12)?; +- let author = user("Alice", "alice@example.com")?; +- let committer = user("Bob", "bob@example.com")?; +- let message = "commit message".to_string(); +- let meta = CommitMeta::new(123, 0, None)?; +- +- let commit = +- Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_commit(&commit, &mut buf)?; +- let decoded = BinaryDecoder.decode_commit(Cursor::new(buf))?; +- +- assert_eq!(decoded.tree(), &tree); +- assert_eq!(decoded.parents(), &[parent]); +- assert_eq!(decoded.author().name(), "Alice"); +- assert_eq!(decoded.committer().email(), "bob@example.com"); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 123); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- assert!(decoded.meta().encoding().is_none()); +- Ok(()) +- } +- +- #[test] +- fn encode_tag_roundtrip_with_decoder() -> Result<(), VctrlError> { +- let target = hash_byte(0x33)?; +- let tagger = user("Tagger", "tagger@example.com")?; +- let message = "v1.0".to_string(); +- let meta = CommitMeta::new(456, 0, None)?; +- +- let tag = Tag::with_meta( +- "v1.0".to_string(), +- target, +- Some(tagger), +- message.clone(), +- meta, +- )?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_tag(&tag, &mut buf)?; +- let decoded = BinaryDecoder.decode_tag(Cursor::new(buf))?; +- +- assert_eq!(decoded.name(), "v1.0"); +- assert_eq!(decoded.target(), &target); +- assert_eq!( +- decoded +- .tagger() +- .ok_or_else(|| VctrlError::Other("expected tagger".into()))? +- .name(), +- "Tagger" +- ); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 456); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- assert!(decoded.meta().encoding().is_none()); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/codec/mod.rs b/libvctrl_core/src/codec/mod.rs +index 1baa3df..fdda6ce 100644 +--- a/libvctrl_core/src/codec/mod.rs ++++ b/libvctrl_core/src/codec/mod.rs +@@ -1,5 +1,70 @@ ++//! # Binary Codec ++//! ++//! This module provides the reference implementation of the binary ++//! serialization format for Git objects. It contains two zero-sized types: ++//! ++//! - [`BinaryEncoder`](binary_encoder::BinaryEncoder): writes objects into a ++//! deterministic, versioned byte stream. ++//! - [`BinaryDecoder`](binary_decoder::BinaryDecoder): reads such byte streams ++//! back into strongly validated, immutable objects. ++//! ++//! ## Why this module exists ++//! ++//! Version control systems rely on content addressing. To compute a stable ++//! hash, objects must be serialized in a way that is independent of platform, ++//! compiler, and runtime conditions. This module defines such a canonical ++//! encoding and the corresponding decoding logic. ++//! ++//! The encoder and decoder are deliberately separate to enforce a clear ++//! boundary between producing bytes and consuming untrusted bytes. The decoder ++//! performs extensive bounds and validity checks, whereas the encoder assumes ++//! its input objects are already valid. ++//! ++//! ## How it works ++//! ++//! Every encoded object begins with a single version byte. The current version ++//! is [`VERSION`](binary_encoder::VERSION) = 3. The decoder rejects any input ++//! whose first byte does not match this value. ++//! ++//! After the version byte, fields are written in a strict order using ++//! little-endian integer encoding. Strings are length-prefixed with a single ++//! byte; larger payloads (like blob content or commit messages) use dedicated ++//! 32-bit or 64-bit length prefixes. ++//! ++//! ## Examples ++//! ++//! The following example shows a complete round-trip through the encoder and ++//! decoder. It encodes a [`Blob`], then decodes it back and asserts equality. ++//! ++//! ``` ++//! # use std::io::Cursor; ++//! # use libvctrl_handler::{Blob, Decoder, Encoder}; ++//! # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++//! let original = Blob::new(b"round trip".to_vec()).unwrap(); ++//! ++//! let mut encoded = Vec::new(); ++//! BinaryEncoder.encode_blob(&original, &mut encoded).unwrap(); ++//! ++//! let decoded = BinaryDecoder ++//! .decode_blob(Cursor::new(encoded.as_slice())) ++//! .unwrap(); ++//! ++//! assert_eq!(original, decoded); ++//! ``` ++ ++/// Binary decoder for Git objects. ++/// ++/// This submodule provides [`BinaryDecoder`](self::BinaryDecoder), the ++/// strictly validated inverse of the encoder. It accepts any ++/// [`std::io::Read`] source and returns either a fully constructed object or a ++/// [`VctrlError`] describing the exact corruption encountered. + pub mod binary_decoder; + ++/// Binary encoder for Git objects. ++/// ++/// This submodule provides [`BinaryEncoder`](self::BinaryEncoder), the ++/// canonical producer of binary object data. It writes directly to any ++/// [`std::io::Write`] sink without intermediate heap allocations. + pub mod binary_encoder; + + pub use binary_decoder::BinaryDecoder; +diff --git a/libvctrl_core/src/hash/mod.rs b/libvctrl_core/src/hash/mod.rs +index fb1573f..4653e97 100644 +--- a/libvctrl_core/src/hash/mod.rs ++++ b/libvctrl_core/src/hash/mod.rs +@@ -1,3 +1,48 @@ ++//! SHA-512 hasher implementation for content addressing. ++//! ++//! # Why this module exists ++//! ++//! The [`libvctrl_handler`] crate defines the [`Hasher`](libvctrl_handler::Hasher) ++//! trait as the abstraction for content-addressable object hashing. This module ++//! provides a concrete implementation using the SHA-512 algorithm from the ++//! [`libvctrl_sha512`] crate. It bridges the raw SHA-512 digest computation to ++//! the handler's [`Hash`] type, ensuring that all hashes produced by this ++//! crate are compatible with the rest of the VCS ecosystem. ++//! ++//! # How it works ++//! ++//! The [`Sha512Hasher`] is a zero-sized struct. It holds no state because ++//! hashing is stateless across invocations. The [`hash`](Sha512Hasher::hash) ++//! method reads from a generic [`Read`](std::io::Read) stream in fixed-size ++//! chunks, feeds each chunk into the underlying [`Sha512Hash`] engine, and ++//! finalizes the digest into a 64-byte [`Hash`]. The result length always ++//! matches [`HASH_LENGTH`](libvctrl_handler::HASH_LENGTH), so conversion ++//! cannot fail. ++//! ++//! # Examples ++//! ++//! Hash a byte slice: ++//! ++//! ``` ++//! use libvctrl_core::hash::Sha512Hasher; ++//! use libvctrl_handler::Hasher; ++//! ++//! let hasher = Sha512Hasher; ++//! let hash = hasher.hash(b"hello world".as_ref()).unwrap(); ++//! assert_eq!(hash.as_bytes().len(), 64); ++//! ``` ++ ++/// SHA-512 hasher implementation. ++/// ++/// This submodule contains the [`Sha512Hasher`] type, which implements the ++/// [`Hasher`](libvctrl_handler::Hasher) trait using the SHA-512 algorithm. ++/// The implementation is stateless, thread-safe, and suitable for both small ++/// byte slices and large streaming inputs. + pub mod sha512; + ++/// Re-export of [`Sha512Hasher`] for convenient access at the module root. ++/// ++/// By re-exporting, users can refer to `libvctrl_core::hash::Sha512Hasher` ++/// instead of the longer `libvctrl_core::hash::sha512::Sha512Hasher`. This ++/// aligns with the crate's goal of providing ergonomic, discoverable APIs. + pub use sha512::Sha512Hasher; +diff --git a/libvctrl_core/src/hash/sha512.rs b/libvctrl_core/src/hash/sha512.rs +index 8926e9d..3474d03 100644 +--- a/libvctrl_core/src/hash/sha512.rs ++++ b/libvctrl_core/src/hash/sha512.rs +@@ -1,14 +1,99 @@ +-use alloc::sync::Arc; +-use std::io; ++//! SHA-512 hasher implementation for content addressing. ++//! ++//! # Why this module exists ++//! ++//! The [`libvctrl_handler`] crate defines the [`Hasher`](libvctrl_handler::Hasher) ++//! trait as the abstraction for content-addressable object hashing. This module ++//! provides a concrete implementation using the SHA-512 algorithm from the ++//! [`libvctrl_sha512`] crate. It bridges the raw SHA-512 digest computation to ++//! the handler's [`Hash`] type, ensuring that all hashes produced by this ++//! crate are compatible with the rest of the VCS ecosystem. ++//! ++//! # How it works ++//! ++//! The [`Sha512Hasher`] is a zero-sized struct. It holds no state because ++//! hashing is stateless across invocations. The [`hash`](Sha512Hasher::hash) ++//! method reads from a generic [`Read`](std::io::Read) stream in fixed-size ++//! chunks, feeds each chunk into the underlying [`Sha512Hash`] engine, and ++//! finalizes the digest into a 64-byte [`Hash`]. The result length always ++//! matches [`HASH_LENGTH`](libvctrl_handler::HASH_LENGTH), so conversion ++//! cannot fail. ++//! ++//! # Examples ++//! ++//! Hash a byte slice: ++//! ++//! ``` ++//! use libvctrl_core::hash::Sha512Hasher; ++//! use libvctrl_handler::Hasher; ++//! ++//! let hasher = Sha512Hasher; ++//! let hash = hasher.hash(b"hello world".as_ref()).unwrap(); ++//! assert_eq!(hash.as_bytes().len(), 64); ++//! ``` + + use libvctrl_handler::{Hash, Hasher, VctrlError}; + use libvctrl_sha512::Hash as Sha512Hash; + +-#[derive(Debug, Default, Clone, Copy)] ++/// A hasher that uses the SHA-512 algorithm. ++/// ++/// # Design rationale ++/// ++/// This is a zero-sized struct (ZST) because the SHA-512 algorithm does not ++/// require any persistent state between calls. Each call to ++/// [`hash`](Sha512Hasher::hash) creates a fresh [`Sha512Hash`] engine, ++/// processes the input, and drops it. This makes the hasher trivially ++/// [`Clone`], [`Default`], and [`Debug`], and allows it to be passed by value ++/// without overhead. ++/// ++/// The struct name follows the convention of naming the concrete implementation ++/// after the algorithm it uses, making it obvious to users what cryptographic ++/// function will be applied. ++/// ++/// # Examples ++/// ++/// Create a hasher instance: ++/// ++/// ``` ++/// # use libvctrl_core::hash::Sha512Hasher; ++/// let hasher = Sha512Hasher::default(); ++/// // The hasher is stateless and can be reused for multiple inputs. ++/// ``` ++#[derive(Debug, Default, Clone)] + pub struct Sha512Hasher; + + impl Hasher for Sha512Hasher { +- fn hash(&self, mut reader: R) -> Result { ++ /// Hashes the contents of a reader using SHA-512. ++ /// ++ /// # How it works ++ /// ++ /// The method reads from `reader` in 4096-byte chunks to avoid loading ++ /// large objects entirely into memory. For each chunk, it calls ++ /// [`update`](Sha512Hash::update) on a fresh [`Sha512Hash`] engine. Once ++ /// EOF is reached (read returns 0), the engine is finalized and the raw ++ /// 64-byte digest is converted into a [`Hash`] via ++ /// [`Hash::from_bytes`]. Because SHA-512 always produces 64 bytes, the ++ /// conversion cannot fail and the `?` operator is safe to use. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::IoError`] if an I/O error occurs while reading ++ /// from the underlying reader. Hash computation itself is infallible. ++ /// ++ /// # Examples ++ /// ++ /// Hash data from a [`Cursor`](std::io::Cursor): ++ /// ++ /// ``` ++ /// # use libvctrl_core::hash::Sha512Hasher; ++ /// # use libvctrl_handler::Hasher; ++ /// # use std::io::Cursor; ++ /// let hasher = Sha512Hasher; ++ /// let data = b"streaming data"; ++ /// let hash = hasher.hash(Cursor::new(data)).unwrap(); ++ /// assert_eq!(hash.as_bytes().len(), 64); ++ /// ``` ++ fn hash(&self, mut reader: R) -> Result { + let mut hasher = Sha512Hash::new(); + let mut buffer = [0u8; 4096]; + loop { +@@ -17,8 +102,8 @@ impl Hasher for Sha512Hasher { + break; + } + let chunk = buffer.get(..n).ok_or_else(|| { +- VctrlError::IoError(Arc::new(io::Error::new( +- io::ErrorKind::UnexpectedEof, ++ VctrlError::IoError(std::sync::Arc::new(std::io::Error::new( ++ std::io::ErrorKind::UnexpectedEof, + "read returned invalid length", + ))) + })?; +@@ -28,49 +113,3 @@ impl Hasher for Sha512Hasher { + Hash::from_bytes(&digest) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- use std::io::Cursor; +- +- #[test] +- fn hash_empty_input() -> Result<(), VctrlError> { +- let hash = Sha512Hasher.hash(Cursor::new(Vec::::new()))?; +- assert_eq!( +- hash.as_bytes(), +- &[ +- 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, +- 0x80, 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, +- 0xd3, 0x6c, 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, +- 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, +- 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e +- ] +- ); +- Ok(()) +- } +- +- #[test] +- fn hash_abc() -> Result<(), VctrlError> { +- let hash = Sha512Hasher.hash(Cursor::new(b"abc"))?; +- assert_eq!( +- hash.as_bytes(), +- &[ +- 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, +- 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, +- 0x4b, 0x55, 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, +- 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, +- 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f +- ] +- ); +- Ok(()) +- } +- +- #[test] +- fn hash_multiple_chunks() -> Result<(), VctrlError> { +- let data = vec![0xAB; 8192]; +- let hash = Sha512Hasher.hash(Cursor::new(data))?; +- assert_eq!(hash.as_bytes().len(), 64); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/lib.rs b/libvctrl_core/src/lib.rs +index 3ed84be..9d83e94 100644 +--- a/libvctrl_core/src/lib.rs ++++ b/libvctrl_core/src/lib.rs +@@ -1,11 +1,92 @@ +-#![allow(clippy::arithmetic_side_effects)] +- +-extern crate alloc; ++//! # libvctrl_core ++//! ++//! Reference implementations for the contracts defined by ++//! [`libvctrl_handler`](https://docs.rs/libvctrl_handler). ++//! ++//! This crate provides production-ready, safe implementations of hashing, ++//! binary serialization, in-memory storage, reference management, and builder ++//! utilities. It is the first concrete consumer of the `libvctrl_handler` ++//! traits and serves as a quality exemplar for downstream custom backends. ++//! ++//! ## Architecture ++//! ++//! The crate is organized by domain responsibility: ++//! ++//! - [`codec`](crate::codec) — deterministic binary encoding and decoding. ++//! - [`hash`](crate::hash) — SHA-512 content addressing. ++//! - [`object`](crate::object) — ergonomic builder patterns. ++//! - [`store`](crate::store) — in-memory object and reference stores. ++//! ++//! Each module depends only on the public contracts exposed by ++//! `libvctrl_handler`, plus the SHA-512 implementation from ++//! `libvctrl_sha512`. No module contains unsafe code. ++//! ++//! ## Safety and quality ++//! ++//! The crate forbids unsafe code and denies a strict set of Clippy and ++//! rustc lints. Every public item is documented and has doctests where ++//! applicable. The binary decoder is especially defensive: it bounds all ++//! input reads, verifies version bytes, validates UTF-8, and re-checks system ++//! limits before constructing any object. ++//! ++//! ## Example ++//! ++//! A common workflow encodes an object, hashes it, stores it, and retrieves ++//! it through the in-memory store: ++//! ++//! ``` ++//! # use libvctrl_handler::{Blob, Encoder, Hasher, ObjectStore}; ++//! # use libvctrl_core::codec::BinaryEncoder; ++//! # use libvctrl_core::hash::Sha512Hasher; ++//! # use libvctrl_core::store::MemoryStore; ++//! # use std::io::Read; ++//! let blob = Blob::new(b"my content".to_vec()).unwrap(); ++//! ++//! let mut encoded = Vec::new(); ++//! BinaryEncoder.encode_blob(&blob, &mut encoded).unwrap(); ++//! ++//! let hash = Sha512Hasher.hash(&mut encoded.as_slice()).unwrap(); ++//! ++//! let mut store = MemoryStore::new(); ++//! store.put(&hash, &encoded).unwrap(); ++//! ++//! let mut reader = store.get(&hash).unwrap(); ++//! let mut decoded = Vec::new(); ++//! reader.read_to_end(&mut decoded).unwrap(); ++//! ++//! assert_eq!(decoded, encoded); ++//! ``` + + #[cfg(test)] + use proptest as _; + ++/// Binary codec for encoding and decoding objects. ++/// ++/// This module contains the reference binary serialization format. The ++/// encoder and decoder are separated to isolate trusted production of bytes ++/// from untrusted parsing. See [`crate::codec`] for the module-level details. + pub mod codec; ++ ++/// Hashing algorithms. ++/// ++/// This module bridges the pure SHA-512 implementation from ++/// `libvctrl_sha512` to the [`Hasher`](libvctrl_handler::Hasher) trait. ++/// The result is a content-addressing primitive that produces 64-byte hashes ++/// matching `libvctrl_handler::HASH_LENGTH`. + pub mod hash; ++ ++/// Object builders for ergonomic construction. ++/// ++/// These builders provide fluent APIs for creating blobs, commits, tags, ++/// trees, and tree entries. They defer validation until the final build step, ++/// allowing fields to be supplied in any order while keeping the resulting ++/// objects immutable and validated. + pub mod object; ++ ++/// In-memory object and reference stores. ++/// ++/// These stores implement the [`ObjectStore`](libvctrl_handler::ObjectStore) ++/// and [`RefStore`](libvctrl_handler::RefStore) contracts using ++/// [`std::collections::HashMap`]. They are ideal for tests, prototypes, and ++/// short-lived embedded use cases. + pub mod store; +diff --git a/libvctrl_core/src/object/blob.rs b/libvctrl_core/src/object/blob.rs +index ddc22d8..1d1e622 100644 +--- a/libvctrl_core/src/object/blob.rs ++++ b/libvctrl_core/src/object/blob.rs +@@ -1,43 +1,121 @@ ++//! # Blob Builder ++//! ++//! This module provides a fluent, ownership-driven builder for constructing ++//! [`Blob`] objects. The builder pattern is used because a [`Blob`] is an ++//! immutable value object with exactly one required piece of data: the raw ++//! content bytes. The builder allows setting that data in a chainable, ++//! readable way while deferring validation until the final `build()` call. ++ + use libvctrl_handler::{Blob, VctrlError}; + ++/// A builder for creating [`Blob`] objects. ++/// ++/// `BlobBuilder` provides a safe, ergonomic way to construct a [`Blob`] from a ++/// `Vec` while deferring size validation to the final build step. It is a ++/// zero-cost abstraction: after the build, the builder is consumed and the ++/// resulting [`Blob`] owns the data with no extra copies. ++/// ++/// # Why this struct exists ++/// ++/// The [`Blob`] constructor `Blob::new` may fail if the supplied data exceeds ++/// [`MAX_BLOB_SIZE`](libvctrl_handler::MAX_BLOB_SIZE). A builder delays that ++/// fallible operation, allowing callers to accumulate or transform data before ++/// finalizing. It also makes construction consistent with other object types ++/// that have more fields, providing a uniform API across the crate. ++/// ++/// # How it works ++/// ++/// The builder stores the content in a private `Vec`. `with_data` replaces ++/// that buffer. `build` moves the buffer into `Blob::new`, which performs ++/// validation and returns a [`Result`]. After `build`, the builder is consumed ++/// and cannot be reused. ++/// ++/// # Examples ++/// ++/// Basic usage: ++/// ++/// ``` ++/// # use libvctrl_core::object::BlobBuilder; ++/// let blob = BlobBuilder::new() ++/// .with_data(b"file content".to_vec()) ++/// .build() ++/// .unwrap(); ++/// ++/// assert_eq!(blob.data(), b"file content"); ++/// ``` + #[derive(Debug, Default)] + pub struct BlobBuilder { + data: Vec, + } + + impl BlobBuilder { ++ /// Creates a new `BlobBuilder` with no data. ++ /// ++ /// The builder is initially empty. Use [`with_data`](Self::with_data) to ++ /// set the content, or call [`build`](Self::build) to produce an empty ++ /// [`Blob`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::BlobBuilder; ++ /// let builder = BlobBuilder::new(); ++ /// let blob = builder.build().unwrap(); ++ /// assert!(blob.data().is_empty()); ++ /// ``` + #[must_use] + pub const fn new() -> Self { + Self { data: Vec::new() } + } + ++ /// Sets the data for the blob. ++ /// ++ /// This method consumes `self` and returns a new builder with the given ++ /// `data` replacing any previously set content. It does not validate the ++ /// size; validation occurs only when [`build`](Self::build) is called. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::BlobBuilder; ++ /// let blob = BlobBuilder::new() ++ /// .with_data(vec![1, 2, 3]) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(blob.data(), &[1, 2, 3]); ++ /// ``` + #[must_use] + pub fn with_data(mut self, data: Vec) -> Self { + self.data = data; + self + } + ++ /// Builds the [`Blob`]. ++ /// ++ /// This consumes the builder, moves the stored data into the new [`Blob`], ++ /// and validates it against the system limits. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the data exceeds ++ /// [`MAX_BLOB_SIZE`](libvctrl_handler::MAX_BLOB_SIZE). The exact variant ++ /// depends on the implementation in `libvctrl_handler`. ++ /// ++ /// # Examples ++ /// ++ /// Successful build: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::BlobBuilder; ++ /// let blob = BlobBuilder::new() ++ /// .with_data(b"hello".to_vec()) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(blob.data(), b"hello"); ++ /// ``` + pub fn build(self) -> Result { + Blob::new(self.data) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- #[test] +- fn builder_empty_data_builds_ok() -> Result<(), VctrlError> { +- let blob = BlobBuilder::new().build()?; +- assert!(blob.data().is_empty()); +- Ok(()) +- } +- +- #[test] +- fn builder_with_data_builds_ok() -> Result<(), VctrlError> { +- let data = vec![1_u8, 2, 3]; +- let blob = BlobBuilder::new().with_data(data.clone()).build()?; +- assert_eq!(blob.data(), data.as_slice()); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/object/commit.rs b/libvctrl_core/src/object/commit.rs +index d1ccafc..7f7867e 100644 +--- a/libvctrl_core/src/object/commit.rs ++++ b/libvctrl_core/src/object/commit.rs +@@ -1,5 +1,82 @@ ++//! Builder for constructing [`Commit`] objects with a fluent, type-safe API. ++//! ++//! # Why this module exists ++//! ++//! A [`Commit`] aggregates several mandatory pieces of metadata: a tree hash, ++//! one or more parent hashes, author and committer identities, a message, and ++//! optional metadata such as timestamp and encoding. Direct construction would ++//! force every caller to provide all fields at once, even when they are built ++//! incrementally or derived from different sources. The builder pattern solves ++//! this by separating field assignment from final validation. ++//! ++//! # How it works ++//! ++//! The builder stores each field as an `Option` (or a `Vec` for parents) and ++//! consumes `self` on every setter, returning `Self`. This ensures that each ++//! setter is used exactly once in a chain and that the builder cannot be reused ++//! after partial construction. The final [`build`](CommitBuilder::build) ++//! method extracts all required fields, reports a descriptive [`VctrlError`] ++//! if any are missing, and delegates to either [`Commit::with_meta`] or ++//! [`Commit::new`] depending on whether metadata was supplied. ++//! ++//! # Examples ++//! ++//! ``` ++//! use libvctrl_core::object::CommitBuilder; ++//! use libvctrl_handler::{Hash, UserID}; ++//! ++//! let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); ++//! let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); ++//! let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); ++//! ++//! let commit = CommitBuilder::new() ++//! .tree(tree) ++//! .author(author) ++//! .committer(committer) ++//! .message("Initial commit") ++//! .build() ++//! .unwrap(); ++//! ++//! assert_eq!(commit.message(), "Initial commit"); ++//! ``` ++ + use libvctrl_handler::{Commit, CommitMeta, Hash, UserID, VctrlError}; + ++/// A builder for creating [`Commit`] objects. ++/// ++/// # Design rationale ++/// ++/// This type follows the *consuming builder* pattern. Each setter takes `self` ++/// by value and returns `Self`, which makes the builder single-use and prevents ++/// accidental reuse of a partially configured builder. Fields are stored ++/// internally as `Option` (or a `Vec` for parents) because the builder must ++/// remain `Default` while allowing the final [`build`](CommitBuilder::build) ++/// to distinguish between “not provided” and “explicitly set to `None`”. ++/// ++/// The struct is `#[derive(Default)]` so that callers may start from ++/// `CommitBuilder::default()` if they prefer, but the explicit ++/// [`new`](CommitBuilder::new) constructor is provided for clarity. ++/// ++/// # Examples ++/// ++/// Basic construction with all required fields: ++/// ++/// ``` ++/// # use libvctrl_core::object::CommitBuilder; ++/// # use libvctrl_handler::{Hash, UserID}; ++/// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); ++/// # let author = UserID::new("A".into(), "a@b.c".into()).unwrap(); ++/// # let committer = author.clone(); ++/// let commit = CommitBuilder::new() ++/// .tree(tree) ++/// .author(author) ++/// .committer(committer) ++/// .message("Initial commit") ++/// .build() ++/// .unwrap(); ++/// ++/// assert!(commit.parents().is_empty()); ++/// ``` + #[derive(Debug, Default)] + pub struct CommitBuilder { + tree: Option, +@@ -11,6 +88,25 @@ pub struct CommitBuilder { + } + + impl CommitBuilder { ++ /// Creates a new `CommitBuilder` with no fields set. ++ /// ++ /// # Why this is `const` ++ /// ++ /// Marking the constructor as `const fn` allows the builder to be created ++ /// in constant contexts and gives the compiler more opportunities for ++ /// compile-time evaluation. The returned builder is a plain value on the ++ /// stack with all `Option` fields set to `None` and the `parents` vector ++ /// empty; no heap allocation occurs until the first `parent` call or ++ /// message assignment. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// let builder = CommitBuilder::new(); ++ /// // builder is empty; calling build() now would fail with a missing-field error ++ /// assert!(builder.build().is_err()); ++ /// ``` + #[must_use] + pub const fn new() -> Self { + Self { +@@ -23,42 +119,184 @@ impl CommitBuilder { + } + } + ++ /// Sets the tree hash for the commit. ++ /// ++ /// The tree hash points to the root tree object that represents the ++ /// snapshot of the project at the time of the commit. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let builder = CommitBuilder::new().tree(tree); ++ /// assert!(builder.build().is_err()); // other fields still missing ++ /// ``` + #[must_use] + pub const fn tree(mut self, tree: Hash) -> Self { + self.tree = Some(tree); + self + } + ++ /// Adds a parent commit hash. ++ /// ++ /// This method may be called multiple times to create a commit with ++ /// multiple parents (e.g., a merge commit). Parents are stored in the ++ /// order they are added, preserving the caller’s intended ordering for ++ /// serialization. ++ /// ++ /// # Examples ++ /// ++ /// Adding two parents: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// # let parent1 = Hash::from_bytes(&[1u8; 64]).unwrap(); ++ /// # let parent2 = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// let builder = CommitBuilder::new() ++ /// .parent(parent1) ++ /// .parent(parent2); ++ /// // Use builder further or build after setting other fields ++ /// ``` + #[must_use] + pub fn parent(mut self, parent: Hash) -> Self { + self.parents.push(parent); + self + } + ++ /// Sets the author of the commit. ++ /// ++ /// The author is the person who originally wrote the changes, which may ++ /// differ from the committer (for example, when applying a patch). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::UserID; ++ /// # let author = UserID::new("Alice".into(), "alice@example.com".into()).unwrap(); ++ /// let builder = CommitBuilder::new().author(author); ++ /// assert!(builder.build().is_err()); // tree and committer still missing ++ /// ``` + #[must_use] + pub fn author(mut self, author: UserID) -> Self { + self.author = Some(author); + self + } + ++ /// Sets the committer of the commit. ++ /// ++ /// The committer is the person who created the commit object. In simple ++ /// workflows the author and committer are identical, but they are kept ++ /// separate to preserve Git’s distinction. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::UserID; ++ /// # let committer = UserID::new("Bob".into(), "bob@example.com".into()).unwrap(); ++ /// let builder = CommitBuilder::new().committer(committer); ++ /// assert!(builder.build().is_err()); // tree and author still missing ++ /// ``` + #[must_use] + pub fn committer(mut self, committer: UserID) -> Self { + self.committer = Some(committer); + self + } + ++ /// Sets the commit message. ++ /// ++ /// The method accepts any type that implements `Into`, including ++ /// `&str`, `String`, and `Cow`, making call sites ergonomic. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// let builder = CommitBuilder::new().message("Initial commit"); ++ /// // The message is stored internally as a String. ++ /// assert!(builder.build().is_err()); // other required fields missing ++ /// ``` + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + ++ /// Sets the optional commit metadata. ++ /// ++ /// Metadata includes the timestamp, timezone offset, and optional character ++ /// encoding. If this method is not called, [`build`](CommitBuilder::build) ++ /// delegates to [`Commit::new`], which uses default metadata. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::CommitMeta; ++ /// # let meta = CommitMeta::new(1_700_000_000, 0, None).unwrap(); ++ /// let builder = CommitBuilder::new().meta(meta); ++ /// assert!(builder.build().is_err()); // other required fields missing ++ /// ``` + #[must_use] + pub fn meta(mut self, meta: CommitMeta) -> Self { + self.meta = Some(meta); + self + } + ++ /// Builds the [`Commit`] object after validating all required fields. ++ /// ++ /// # How it works ++ /// ++ /// The method checks the four mandatory fields (`tree`, `author`, ++ /// `committer`, and `message`) in order. If any is missing, it returns a ++ /// [`VctrlError::Other`] with a descriptive message and does not allocate ++ /// a commit. If all mandatory fields are present, it constructs the ++ /// [`Commit`] by calling [`Commit::with_meta`] when metadata was supplied, ++ /// or [`Commit::new`] otherwise. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::Other`] if any of the required fields is missing: ++ /// - `tree` ++ /// - `author` ++ /// - `committer` ++ /// - `message` ++ /// ++ /// Also returns any [`VctrlError`] produced by the underlying ++ /// [`Commit::new`] or [`Commit::with_meta`] validation. ++ /// ++ /// # Examples ++ /// ++ /// Successful build: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// # use libvctrl_handler::{Hash, UserID}; ++ /// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// # let author = UserID::new("Alice".into(), "alice@example.com".into()).unwrap(); ++ /// # let committer = UserID::new("Bob".into(), "bob@example.com".into()).unwrap(); ++ /// let commit = CommitBuilder::new() ++ /// .tree(tree) ++ /// .author(author) ++ /// .committer(committer) ++ /// .message("Initial commit") ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(commit.message(), "Initial commit"); ++ /// ``` ++ /// ++ /// Missing field error: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::CommitBuilder; ++ /// let result = CommitBuilder::new().build(); ++ /// assert!(result.is_err()); ++ /// ``` + pub fn build(self) -> Result { + let tree = self + .tree +@@ -80,106 +318,3 @@ impl CommitBuilder { + } + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- fn user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +- } +- +- #[test] +- fn build_missing_tree_errors() -> Result<(), VctrlError> { +- let result = CommitBuilder::new() +- .author(user("A", "a@example.com")?) +- .committer(user("B", "b@example.com")?) +- .message("msg") +- .build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn build_missing_author_errors() -> Result<(), VctrlError> { +- let result = CommitBuilder::new() +- .tree(hash_byte(0x01)?) +- .committer(user("B", "b@example.com")?) +- .message("msg") +- .build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn build_missing_committer_errors() -> Result<(), VctrlError> { +- let result = CommitBuilder::new() +- .tree(hash_byte(0x01)?) +- .author(user("A", "a@example.com")?) +- .message("msg") +- .build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn build_missing_message_errors() -> Result<(), VctrlError> { +- let result = CommitBuilder::new() +- .tree(hash_byte(0x01)?) +- .author(user("A", "a@example.com")?) +- .committer(user("B", "b@example.com")?) +- .build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn build_valid_commit_without_meta() -> Result<(), VctrlError> { +- let tree = hash_byte(0x11)?; +- let parent = hash_byte(0x12)?; +- let author = user("Alice", "alice@example.com")?; +- let committer = user("Bob", "bob@example.com")?; +- let message = "hello".to_string(); +- +- let commit = CommitBuilder::new() +- .tree(tree) +- .parent(parent) +- .author(author) +- .committer(committer) +- .message(message.clone()) +- .build()?; +- +- assert_eq!(commit.tree(), &tree); +- assert_eq!(commit.parents(), &[parent]); +- assert_eq!(commit.author().name(), "Alice"); +- assert_eq!(commit.committer().name(), "Bob"); +- assert_eq!(commit.message(), message); +- Ok(()) +- } +- +- #[test] +- fn build_valid_commit_with_meta() -> Result<(), VctrlError> { +- let tree = hash_byte(0x21)?; +- let author = user("Alice", "alice@example.com")?; +- let committer = user("Bob", "bob@example.com")?; +- let message = "hello".to_string(); +- let meta = CommitMeta::new(123, 0, Some("utf-8".to_string()))?; +- +- let commit = CommitBuilder::new() +- .tree(tree) +- .author(author) +- .committer(committer) +- .message(message) +- .meta(meta) +- .build()?; +- +- assert_eq!(commit.meta().timestamp(), 123); +- assert_eq!(commit.meta().timezone_offset(), 0); +- assert_eq!(commit.meta().encoding(), Some("utf-8")); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/object/mod.rs b/libvctrl_core/src/object/mod.rs +index 509cc40..13e0941 100644 +--- a/libvctrl_core/src/object/mod.rs ++++ b/libvctrl_core/src/object/mod.rs +@@ -1,15 +1,96 @@ ++//! Object builders for ergonomic construction of Git objects. ++//! ++//! # Why this module exists ++//! ++//! The data types in [`libvctrl_handler`] are immutable and enforce their own ++//! invariants through constructors such as ++//! [`Commit::new`](libvctrl_handler::Commit::new). While those constructors ++//! are safe and correct, they often require every field to be supplied at once. ++//! In real applications, fields may arrive gradually from parsing, user input, ++//! or configuration. The builder pattern separates gradual assembly from final ++//! validation. ++//! ++//! Each builder in this module consumes `self` on every setter, returns `Self`, ++//! and exposes a single `build` method that performs validation and constructs ++//! the final object. This design prevents partially configured builders from ++//! being used accidentally after construction, while still allowing fluent ++//! chains. ++//! ++//! # Module organization ++//! ++//! The module mirrors the object type hierarchy: ++//! ++//! - [`blob`] contains [`BlobBuilder`] for [`Blob`](libvctrl_handler::Blob). ++//! - [`tree`] contains [`TreeBuilder`] and [`TreeEntryBuilder`] for ++//! [`Tree`](libvctrl_handler::Tree) and ++//! [`TreeEntry`](libvctrl_handler::TreeEntry). ++//! - [`commit`] contains [`CommitBuilder`] for ++//! [`Commit`](libvctrl_handler::Commit). ++//! - [`tag`] contains [`TagBuilder`] for [`Tag`](libvctrl_handler::Tag). ++//! ++//! All builders are re-exported at this module level so callers can use ++//! `libvctrl_core::object::CommitBuilder` instead of the longer submodule path. ++//! ++//! # Examples ++//! ++//! Construct a commit using the builder: ++//! ++//! ``` ++//! use libvctrl_core::object::CommitBuilder; ++//! use libvctrl_handler::{Hash, UserID}; ++//! ++//! let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); ++//! let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); ++//! let committer = author.clone(); ++//! ++//! let commit = CommitBuilder::new() ++//! .tree(tree) ++//! .author(author) ++//! .committer(committer) ++//! .message("Initial commit") ++//! .build() ++//! .unwrap(); ++//! ++//! assert_eq!(commit.message(), "Initial commit"); ++//! ``` ++ ++/// Blob builder. ++/// ++/// This submodule contains [`BlobBuilder`], a builder for constructing ++/// [`Blob`](libvctrl_handler::Blob) objects from arbitrary byte data. + pub mod blob; + ++/// Commit builder. ++/// ++/// This submodule contains [`CommitBuilder`], a builder for constructing ++/// [`Commit`](libvctrl_handler::Commit) objects with tree, parents, author, ++/// committer, message, and optional metadata. + pub mod commit; + ++/// Tag builder. ++/// ++/// This submodule contains [`TagBuilder`], a builder for constructing ++/// [`Tag`](libvctrl_handler::Tag) objects with a name, target hash, optional ++/// tagger, message, and optional metadata. + pub mod tag; + ++/// Tree builder. ++/// ++/// This submodule contains [`TreeBuilder`] and [`TreeEntryBuilder`], builders ++/// for constructing [`Tree`](libvctrl_handler::Tree) and ++/// [`TreeEntry`](libvctrl_handler::TreeEntry) objects with sorted entries and ++/// entry kinds. + pub mod tree; + ++/// Re-export of [`BlobBuilder`] for convenient access at the module root. + pub use blob::BlobBuilder; + ++/// Re-export of [`CommitBuilder`] for convenient access at the module root. + pub use commit::CommitBuilder; + ++/// Re-export of [`TagBuilder`] for convenient access at the module root. + pub use tag::TagBuilder; + ++/// Re-export of [`TreeBuilder`] and [`TreeEntryBuilder`] for convenient access ++/// at the module root. + pub use tree::{TreeBuilder, TreeEntryBuilder}; +diff --git a/libvctrl_core/src/object/tag.rs b/libvctrl_core/src/object/tag.rs +index ca6ee1d..0950a42 100644 +--- a/libvctrl_core/src/object/tag.rs ++++ b/libvctrl_core/src/object/tag.rs +@@ -1,5 +1,76 @@ ++//! # Tag Builder ++//! ++//! This module provides a fluent, ownership-driven builder for constructing ++//! [`Tag`] objects. The builder pattern is used because a [`Tag`] is an ++//! immutable value object with several fields, some mandatory and some ++//! optional. The builder allows setting each field separately and defers ++//! validation and object creation to the final `build()` call. ++ + use libvctrl_handler::{CommitMeta, Hash, Tag, UserID, VctrlError}; + ++/// A builder for creating [`Tag`] objects. ++/// ++/// `TagBuilder` provides a safe, ergonomic way to construct a [`Tag`] by ++/// setting fields individually. The builder consumes itself with each method ++/// and returns a new builder state, enabling method chaining. The final ++/// `build()` call validates required fields and constructs the [`Tag`]. ++/// ++/// # Why this struct exists ++/// ++/// The [`Tag`] constructor may fail if required fields are missing or ++/// validation fails. A builder delays those operations, allowing callers to ++/// supply fields in any order and to provide optional values only when ++/// necessary. It also gives a uniform construction API across all object ++/// types in this crate. ++/// ++/// # How it works ++/// ++/// The builder stores each field in an `Option`. Required fields (`name`, ++/// `target`) must be set before `build()`; otherwise `build()` returns a ++/// [`VctrlError::Other`] describing the missing field. Optional fields ++/// (`tagger`, `message`, `meta`) default to `None` (or an empty string for ++/// message). `build()` consumes the builder and moves the values into the new ++/// [`Tag`]. ++/// ++/// # Examples ++/// ++/// Basic construction with a tagger: ++/// ++/// ``` ++/// # use libvctrl_core::object::TagBuilder; ++/// # use libvctrl_handler::{Hash, UserID}; ++/// let target = Hash::from_bytes(&[0u8; 64]).unwrap(); ++/// let tagger = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); ++/// ++/// let tag = TagBuilder::new() ++/// .name("v1.0.0") ++/// .target(target) ++/// .tagger(tagger) ++/// .message("Release 1.0") ++/// .build() ++/// .unwrap(); ++/// ++/// assert_eq!(tag.name(), "v1.0.0"); ++/// assert!(tag.tagger().is_some()); ++/// assert_eq!(tag.message(), "Release 1.0"); ++/// ``` ++/// ++/// Building without a tagger: ++/// ++/// ``` ++/// # use libvctrl_core::object::TagBuilder; ++/// # use libvctrl_handler::Hash; ++/// let target = Hash::from_bytes(&[1u8; 64]).unwrap(); ++/// ++/// let tag = TagBuilder::new() ++/// .name("v2.0.0") ++/// .target(target) ++/// .build() ++/// .unwrap(); ++/// ++/// assert_eq!(tag.name(), "v2.0.0"); ++/// assert!(tag.tagger().is_none()); ++/// ``` + #[derive(Debug, Default)] + pub struct TagBuilder { + name: Option, +@@ -10,6 +81,19 @@ pub struct TagBuilder { + } + + impl TagBuilder { ++ /// Creates a new `TagBuilder` with all fields unset. ++ /// ++ /// The builder is initially empty. Use the setter methods to populate ++ /// fields, then call [`build`](Self::build) to produce a [`Tag`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// let builder = TagBuilder::new(); ++ /// // The builder can be consumed by chaining setters: ++ /// let _ = builder.name("v0.0.0"); // Example only; typically followed by target() ++ /// ``` + #[must_use] + pub const fn new() -> Self { + Self { +@@ -21,36 +105,185 @@ impl TagBuilder { + } + } + ++ /// Sets the tag name. ++ /// ++ /// This method consumes the builder and returns a new builder with `name` ++ /// set. The name must be a non-empty string and is validated during ++ /// [`build`](Self::build). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// let target = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.2.3") ++ /// .target(target) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tag.name(), "v1.2.3"); ++ /// ``` + #[must_use] + pub fn name(mut self, name: impl Into) -> Self { + self.name = Some(name.into()); + self + } + ++ /// Sets the target hash. ++ /// ++ /// This method consumes the builder and returns a new builder with ++ /// `target` set. The target must point to another object (usually a commit ++ /// or tree) and is validated during [`build`](Self::build). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// let target = Hash::from_bytes(&[3u8; 64]).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.0.0") ++ /// .target(target) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tag.target(), &target); ++ /// ``` + #[must_use] + pub const fn target(mut self, target: Hash) -> Self { + self.target = Some(target); + self + } + ++ /// Sets the tagger. ++ /// ++ /// This method consumes the builder and returns a new builder with ++ /// `tagger` set. The tagger is optional; omit this method to create an ++ /// unsigned tag. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::{Hash, UserID}; ++ /// let target = Hash::from_bytes(&[4u8; 64]).unwrap(); ++ /// let tagger = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.0.0") ++ /// .target(target) ++ /// .tagger(tagger) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert!(tag.tagger().is_some()); ++ /// ``` + #[must_use] + pub fn tagger(mut self, tagger: UserID) -> Self { + self.tagger = Some(tagger); + self + } + ++ /// Sets the tag message. ++ /// ++ /// This method consumes the builder and returns a new builder with ++ /// `message` set. The message is optional and defaults to an empty string ++ /// if not set. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// let target = Hash::from_bytes(&[5u8; 64]).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.0.0") ++ /// .target(target) ++ /// .message("Annotated tag") ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tag.message(), "Annotated tag"); ++ /// ``` + #[must_use] + pub fn message(mut self, msg: impl Into) -> Self { + self.message = Some(msg.into()); + self + } + ++ /// Sets the tag metadata. ++ /// ++ /// This method consumes the builder and returns a new builder with `meta` ++ /// set. Metadata includes timestamp, timezone offset, and optional ++ /// encoding. If omitted, the [`Tag`] is created without metadata. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::{CommitMeta, Hash}; ++ /// let target = Hash::from_bytes(&[6u8; 64]).unwrap(); ++ /// let meta = CommitMeta::new(1_700_000_000, 0, None).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.0.0") ++ /// .target(target) ++ /// .meta(meta) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tag.meta().timestamp(), 1_700_000_000); ++ /// ``` + #[must_use] + pub fn meta(mut self, meta: CommitMeta) -> Self { + self.meta = Some(meta); + self + } + ++ /// Builds the [`Tag`]. ++ /// ++ /// This consumes the builder, moves all fields into the new [`Tag`], and ++ /// performs validation. Required fields (`name` and `target`) must be set; ++ /// otherwise an error is returned. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::Other`] if `name` or `target` is missing. ++ /// If metadata is present, validation errors from ++ /// [`Tag::with_meta`](libvctrl_handler::Tag::with_meta) may also be ++ /// returned. Similarly, if metadata is absent, errors from ++ /// [`Tag::new`](libvctrl_handler::Tag::new) are propagated. ++ /// ++ /// # Examples ++ /// ++ /// Successful build: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// # use libvctrl_handler::Hash; ++ /// let target = Hash::from_bytes(&[7u8; 64]).unwrap(); ++ /// ++ /// let tag = TagBuilder::new() ++ /// .name("v1.0.0") ++ /// .target(target) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tag.name(), "v1.0.0"); ++ /// ``` ++ /// ++ /// Missing required field: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TagBuilder; ++ /// let result = TagBuilder::new().name("v1.0.0").build(); ++ /// assert!(result.is_err()); ++ /// ``` + pub fn build(self) -> Result { + let name = self + .name +@@ -72,78 +305,3 @@ impl TagBuilder { + } + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- fn user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +- } +- +- #[test] +- fn build_missing_name_errors() -> Result<(), VctrlError> { +- let result = TagBuilder::new() +- .target(hash_byte(0x01)?) +- .message("msg") +- .build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn build_missing_target_errors() { +- let result = TagBuilder::new().name("v1.0").message("msg").build(); +- assert!(result.is_err()); +- } +- +- #[test] +- fn build_valid_tag_without_tagger_or_meta() -> Result<(), VctrlError> { +- let name = "v1.0".to_string(); +- let target = hash_byte(0x22)?; +- let message = "release".to_string(); +- +- let tag = TagBuilder::new() +- .name(name.clone()) +- .target(target) +- .message(message.clone()) +- .build()?; +- +- assert_eq!(tag.name(), name); +- assert_eq!(tag.target(), &target); +- assert!(tag.tagger().is_none()); +- assert_eq!(tag.message(), message); +- Ok(()) +- } +- +- #[test] +- fn build_valid_tag_with_tagger_and_meta() -> Result<(), VctrlError> { +- let name = "v2.0".to_string(); +- let target = hash_byte(0x23)?; +- let tagger = user("Tagger", "tagger@example.com")?; +- let message = "release".to_string(); +- let meta = CommitMeta::new(42, 0, Some("utf-8".to_string()))?; +- +- let tag = TagBuilder::new() +- .name(name) +- .target(target) +- .tagger(tagger) +- .message(message) +- .meta(meta) +- .build()?; +- +- assert_eq!( +- tag.tagger() +- .ok_or_else(|| VctrlError::Other("expected tagger".into()))? +- .name(), +- "Tagger" +- ); +- assert_eq!(tag.meta().timestamp(), 42); +- assert_eq!(tag.meta().encoding(), Some("utf-8")); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/object/tree.rs b/libvctrl_core/src/object/tree.rs +index 4e53743..6e9e8a1 100644 +--- a/libvctrl_core/src/object/tree.rs ++++ b/libvctrl_core/src/object/tree.rs +@@ -1,11 +1,78 @@ ++//! # Tree Builders ++//! ++//! This module provides ergonomic builders for constructing [`Tree`] and ++//! [`TreeEntry`] objects. ++//! ++//! A [`Tree`] is a sorted collection of entries. The invariant is enforced by ++//! [`Tree::new`], which rejects unsorted or duplicate entry names. These ++//! builders defer that validation to the final `build()` step, allowing ++//! callers to assemble entries incrementally. ++//! ++//! The module exposes two builder types: ++//! ++//! - [`TreeBuilder`] for building a full tree from individual entries. ++//! - [`TreeEntryBuilder`] for building a single entry. ++ + use libvctrl_handler::{EntryKind, Hash, Tree, TreeEntry, VctrlError}; + ++/// A builder for creating [`Tree`] objects. ++/// ++/// `TreeBuilder` accumulates [`TreeEntry`] values and produces a validated ++/// [`Tree`] when [`build`](Self::build) is called. ++/// ++/// # Why this struct exists ++/// ++/// A [`Tree`] requires its entries to be sorted and free of duplicates. If ++/// callers constructed a [`Tree`] directly and supplied entries one by one, ++/// they would need to sort and validate manually. This builder centralizes ++/// that concern and provides a chainable API. ++/// ++/// # How it works ++/// ++/// The builder stores entries in an internal `Vec`. The `entry` and ++/// `add_entry` methods push entries without performing any ordering checks. ++/// Validation occurs only when [`build`](Self::build) consumes the builder and ++/// calls [`Tree::new`], which enforces the ordering invariant. ++/// ++/// # Examples ++/// ++/// Building a tree with two sorted entries: ++/// ++/// ``` ++/// # use libvctrl_core::object::TreeBuilder; ++/// # use libvctrl_handler::{EntryKind, Hash}; ++/// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++/// ++/// let tree = TreeBuilder::new() ++/// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) ++/// .unwrap() ++/// .add_entry("b.txt".to_owned(), EntryKind::Blob, hash) ++/// .unwrap() ++/// .build() ++/// .unwrap(); ++/// ++/// assert_eq!(tree.entries().len(), 2); ++/// ``` + #[derive(Debug, Default)] + pub struct TreeBuilder { + entries: Vec, + } + + impl TreeBuilder { ++ /// Creates a new `TreeBuilder` with no entries. ++ /// ++ /// The builder is initially empty. Use [`entry`](Self::entry) or ++ /// [`add_entry`](Self::add_entry) to add entries, then call ++ /// [`build`](Self::build) to construct the [`Tree`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeBuilder; ++ /// let builder = TreeBuilder::new(); ++ /// let tree = builder.build().unwrap(); ++ /// assert!(tree.entries().is_empty()); ++ /// ``` + #[must_use] + pub const fn new() -> Self { + Self { +@@ -13,12 +80,75 @@ impl TreeBuilder { + } + } + ++ /// Adds an existing [`TreeEntry`]. ++ /// ++ /// This method consumes the builder and returns a new builder with the ++ /// given entry appended. No validation is performed at this point. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::{TreeBuilder, TreeEntryBuilder}; ++ /// # use libvctrl_handler::{EntryKind, Hash}; ++ /// let hash = Hash::from_bytes(&[1u8; 64]).unwrap(); ++ /// let entry = TreeEntryBuilder::new("file.txt".to_owned(), EntryKind::Blob, hash) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// let tree = TreeBuilder::new() ++ /// .entry(entry) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tree.entries().len(), 1); ++ /// ``` + #[must_use] + pub fn entry(mut self, entry: TreeEntry) -> Self { + self.entries.push(entry); + self + } + ++ /// Creates and adds a new [`TreeEntry`]. ++ /// ++ /// This method consumes the builder, constructs a [`TreeEntry`] using ++ /// [`TreeEntry::new`], appends it, and returns the updated builder. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the entry name is invalid according to ++ /// [`TreeEntry::new`]. No ordering validation is performed here; it is ++ /// deferred to [`build`](Self::build). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeBuilder; ++ /// # use libvctrl_handler::{EntryKind, Hash}; ++ /// let hash = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// ++ /// let builder = TreeBuilder::new() ++ /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) ++ /// .unwrap(); ++ /// ++ /// let tree = builder.build().unwrap(); ++ /// assert_eq!(tree.len(), 1); ++ /// # Ok::<(), libvctrl_handler::VctrlError>(()) ++ /// ``` ++ /// ++ /// This example uses `?` inside a function returning `Result`: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeBuilder; ++ /// # use libvctrl_handler::{EntryKind, Hash, VctrlError}; ++ /// # fn example() -> Result<(), VctrlError> { ++ /// let hash = Hash::from_bytes(&[3u8; 64])?; ++ /// let tree = TreeBuilder::new() ++ /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash)? ++ /// .build()?; ++ /// assert_eq!(tree.entries().len(), 1); ++ /// # Ok(()) ++ /// # } ++ /// ``` + pub fn add_entry( + mut self, + name: String, +@@ -30,11 +160,76 @@ impl TreeBuilder { + Ok(self) + } + ++ /// Builds the [`Tree`]. ++ /// ++ /// Consumes the builder, moves all entries into the new [`Tree`], and ++ /// validates the ordering invariant. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the entries are not sorted lexicographically ++ /// by name or if duplicate names exist. The exact variant depends on the ++ /// `libvctrl_handler` implementation. ++ /// ++ /// # Examples ++ /// ++ /// Successful build: ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeBuilder; ++ /// # use libvctrl_handler::{EntryKind, Hash}; ++ /// let hash = Hash::from_bytes(&[4u8; 64]).unwrap(); ++ /// ++ /// let tree = TreeBuilder::new() ++ /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) ++ /// .unwrap() ++ /// .add_entry("b.txt".to_owned(), EntryKind::Blob, hash) ++ /// .unwrap() ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(tree.entries().len(), 2); ++ /// ``` + pub fn build(self) -> Result { + Tree::new(self.entries) + } + } + ++/// A builder for creating [`TreeEntry`] objects. ++/// ++/// `TreeEntryBuilder` holds the fields required to construct a [`TreeEntry`]: ++/// name, kind, and hash. It performs validation only when ++/// [`build`](Self::build) is called. ++/// ++/// # Why this struct exists ++/// ++/// [`TreeEntry::new`] can fail if the name is invalid. This builder gives ++/// callers an explicit place to defer that error while keeping construction ++/// straightforward. It is particularly useful when entries are generated or ++/// configured dynamically. ++/// ++/// # How it works ++/// ++/// The builder stores the three fields by value. `build` moves them into ++/// [`TreeEntry::new`] and returns the result, consuming the builder. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_core::object::TreeEntryBuilder; ++/// # use libvctrl_handler::{EntryKind, Hash}; ++/// let hash = Hash::from_bytes(&[5u8; 64]).unwrap(); ++/// let entry = TreeEntryBuilder::new( ++/// "file.txt".to_owned(), ++/// EntryKind::Blob, ++/// hash, ++/// ) ++/// .build() ++/// .unwrap(); ++/// ++/// assert_eq!(entry.name(), "file.txt"); ++/// assert_eq!(entry.kind(), EntryKind::Blob); ++/// ``` + #[derive(Debug)] + pub struct TreeEntryBuilder { + name: String, +@@ -43,65 +238,58 @@ pub struct TreeEntryBuilder { + } + + impl TreeEntryBuilder { ++ /// Creates a new `TreeEntryBuilder`. ++ /// ++ /// The builder stores the supplied `name`, `kind`, and `hash`. No ++ /// validation is performed until [`build`](Self::build) is called. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeEntryBuilder; ++ /// # use libvctrl_handler::{EntryKind, Hash}; ++ /// let hash = Hash::from_bytes(&[6u8; 64]).unwrap(); ++ /// let builder = TreeEntryBuilder::new( ++ /// "file.txt".to_owned(), ++ /// EntryKind::Blob, ++ /// hash, ++ /// ); ++ /// ++ /// let entry = builder.build().unwrap(); ++ /// assert_eq!(entry.name(), "file.txt"); ++ /// ``` + #[must_use] + pub const fn new(name: String, kind: EntryKind, hash: Hash) -> Self { + Self { name, kind, hash } + } + ++ /// Builds the [`TreeEntry`]. ++ /// ++ /// Consumes the builder and constructs the [`TreeEntry`] by moving all ++ /// fields into [`TreeEntry::new`]. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the entry name is invalid according to ++ /// [`TreeEntry::new`]. The exact variant is implementation-defined. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::object::TreeEntryBuilder; ++ /// # use libvctrl_handler::{EntryKind, Hash}; ++ /// let hash = Hash::from_bytes(&[7u8; 64]).unwrap(); ++ /// let entry = TreeEntryBuilder::new( ++ /// "file.txt".to_owned(), ++ /// EntryKind::Blob, ++ /// hash, ++ /// ) ++ /// .build() ++ /// .unwrap(); ++ /// ++ /// assert_eq!(entry.name(), "file.txt"); ++ /// ``` + pub fn build(self) -> Result { + TreeEntry::new(self.name, self.kind, self.hash) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- #[test] +- fn tree_entry_builder_valid() -> Result<(), VctrlError> { +- let hash = hash_byte(0x11)?; +- let entry = TreeEntryBuilder::new("file.txt".to_string(), EntryKind::Blob, hash).build()?; +- assert_eq!(entry.name(), "file.txt"); +- assert_eq!(entry.kind(), EntryKind::Blob); +- assert_eq!(*entry.hash(), hash); +- Ok(()) +- } +- +- #[test] +- fn tree_entry_builder_empty_name_errors() -> Result<(), VctrlError> { +- let hash = hash_byte(0x11)?; +- let result = TreeEntryBuilder::new(String::new(), EntryKind::Blob, hash).build(); +- assert!(result.is_err()); +- Ok(()) +- } +- +- #[test] +- fn tree_builder_add_entry_and_build() -> Result<(), VctrlError> { +- let hash = hash_byte(0x22)?; +- let tree = TreeBuilder::new() +- .add_entry("a".to_string(), EntryKind::Blob, hash)? +- .build()?; +- +- let entries = tree.entries(); +- assert_eq!(entries.len(), 1); +- assert_eq!( +- entries +- .first() +- .ok_or_else(|| VctrlError::Other("expected entry".into()))? +- .name(), +- "a" +- ); +- Ok(()) +- } +- +- #[test] +- fn tree_builder_build_empty_tree() -> Result<(), VctrlError> { +- let tree = TreeBuilder::new().build()?; +- assert!(tree.entries().is_empty()); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/store/memory.rs b/libvctrl_core/src/store/memory.rs +index bf55773..47fefa1 100644 +--- a/libvctrl_core/src/store/memory.rs ++++ b/libvctrl_core/src/store/memory.rs +@@ -1,13 +1,104 @@ ++//! In-memory [`ObjectStore`] implementation backed by a [`HashMap`]. ++//! ++//! # Why this module exists ++//! ++//! The [`MemoryStore`] type provides a lightweight, ephemeral storage backend ++//! for version-control objects. It implements the [`ObjectStore`] contract ++//! without requiring disk I/O, network access, or persistent state. This makes ++//! it ideal for: ++//! ++//! - Unit tests that need an isolated object database. ++//! - Caching and temporary storage. ++//! - Embedded or ephemeral applications where persistence is not desired. ++//! ++//! # How it works ++//! ++//! Objects are stored as raw byte vectors (`Vec`) keyed by their content ++//! hash ([`Hash`]). The use of a [`HashMap`] gives average O(1) lookup, ++//! insertion, and deletion. The raw bytes are not parsed or validated on ++//! insertion; validation is the responsibility of higher layers. This keeps ++//! the store fast and agnostic to object type. ++//! ++//! The [`get`](MemoryStore::get) method returns a ++//! `Box` rather than a `Vec` to support streaming ++//! reads of large objects without forcing the entire object into a contiguous ++//! buffer. Internally, it wraps the stored slice in a [`Cursor`]. ++//! ++//! # Examples ++//! ++//! Store and retrieve an object: ++//! ++//! ``` ++//! use libvctrl_core::store::MemoryStore; ++//! use libvctrl_handler::{Hash, ObjectStore}; ++//! use std::io::Read; ++//! ++//! let mut store = MemoryStore::new(); ++//! let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++//! ++//! store.put(&hash, b"hello world").unwrap(); ++//! ++//! let mut reader = store.get(&hash).unwrap(); ++//! let mut buf = Vec::new(); ++//! reader.read_to_end(&mut buf).unwrap(); ++//! assert_eq!(buf, b"hello world"); ++//! ``` ++ + use libvctrl_handler::{Hash, ObjectStore, VctrlError}; + use std::collections::HashMap; + use std::io::{Cursor, Read}; + ++/// An in-memory implementation of [`ObjectStore`]. ++/// ++/// # Design rationale ++/// ++/// The struct uses a [`HashMap>`] as its sole storage. This ++/// choice provides: ++/// ++/// - **Fast average O(1) access** — hashing is performed by the [`Hash`] key. ++/// - **No parsing overhead** — objects are stored as opaque byte sequences. ++/// - **Simple ownership model** — the map owns both keys and values, so the ++/// store can be dropped without manual cleanup. ++/// ++/// The type derives [`Default`], allowing `MemoryStore::default()` to create a ++/// new empty store without requiring a custom constructor. However, an explicit ++/// [`new`](MemoryStore::new) is still provided for symmetry with other store ++/// implementations. ++/// ++/// # Examples ++/// ++/// Create an empty store and verify it is initially empty: ++/// ++/// ``` ++/// # use libvctrl_core::store::MemoryStore; ++/// # use libvctrl_handler::{Hash, ObjectStore}; ++/// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++/// let store = MemoryStore::new(); ++/// assert!(!store.exists(&hash).unwrap()); ++/// ``` + #[derive(Debug, Default)] + pub struct MemoryStore { + objects: HashMap>, + } + + impl MemoryStore { ++ /// Creates a new empty `MemoryStore`. ++ /// ++ /// # Why this is `const` ++ /// ++ /// The constructor is a `const fn` because constructing an empty ++ /// [`HashMap`] does not require any runtime heap allocation. The map is ++ /// allocated lazily on the first insertion. This allows the store to be ++ /// created in constant contexts and enables potential compile-time ++ /// evaluation by the compiler. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryStore; ++ /// let store = MemoryStore::new(); ++ /// // store is ready to use, but contains no objects ++ /// ``` + #[must_use] + pub fn new() -> Self { + Self { +@@ -17,11 +108,65 @@ impl MemoryStore { + } + + impl ObjectStore for MemoryStore { ++ /// Stores an object under the given hash. ++ /// ++ /// # How it works ++ /// ++ /// The method copies the provided byte slice into a new `Vec` and ++ /// inserts it into the internal [`HashMap`]. If an object with the same ++ /// hash already exists, the old value is silently replaced. The method ++ /// always returns `Ok(())` because an in-memory map has no failure modes ++ /// under normal conditions (excluding allocation failure, which panics). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryStore; ++ /// # use libvctrl_handler::{Hash, ObjectStore}; ++ /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let mut store = MemoryStore::new(); ++ /// store.put(&hash, b"data").unwrap(); ++ /// assert!(store.exists(&hash).unwrap()); ++ /// ``` + fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { + let _ = self.objects.insert(*hash, data.to_vec()); + Ok(()) + } + ++ /// Retrieves an object as a streaming reader. ++ /// ++ /// # Design rationale ++ /// ++ /// Returning `Box` instead of `Vec` allows ++ /// callers to consume large objects incrementally. The lifetime `'_` is ++ /// tied to `&self`, enabling the returned reader to borrow the stored bytes ++ /// without cloning the entire object. ++ /// ++ /// Internally, the stored slice is wrapped in a [`Cursor`], which ++ /// implements both [`Read`] and [`Send`]. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::ObjectNotFound`] if no object with the given hash ++ /// exists in the store. ++ /// ++ /// # Examples ++ /// ++ /// Read back a stored object: ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryStore; ++ /// # use libvctrl_handler::{Hash, ObjectStore}; ++ /// # use std::io::Read; ++ /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let mut store = MemoryStore::new(); ++ /// store.put(&hash, b"hello").unwrap(); ++ /// ++ /// let mut reader = store.get(&hash).unwrap(); ++ /// let mut buf = Vec::new(); ++ /// reader.read_to_end(&mut buf).unwrap(); ++ /// assert_eq!(buf, b"hello"); ++ /// ``` + fn get(&self, hash: &Hash) -> Result, VctrlError> { + let data = self + .objects +@@ -30,67 +175,53 @@ impl ObjectStore for MemoryStore { + Ok(Box::new(Cursor::new(data.as_slice()))) + } + ++ /// Deletes an object from the store. ++ /// ++ /// # How it works ++ /// ++ /// Removes the key-value pair from the internal [`HashMap`]. If the object ++ /// does not exist, the method still returns `Ok(())`; deletion is ++ /// idempotent. This mirrors the behavior of [`HashMap::remove`], which ++ /// returns [`Option`] but does not fail. ++ /// ++ /// # Examples ++ /// ++ /// Delete an object and verify it is gone: ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryStore; ++ /// # use libvctrl_handler::{Hash, ObjectStore}; ++ /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let mut store = MemoryStore::new(); ++ /// store.put(&hash, b"data").unwrap(); ++ /// store.delete(&hash).unwrap(); ++ /// assert!(!store.exists(&hash).unwrap()); ++ /// ``` + fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError> { + let _ = self.objects.remove(hash); + Ok(()) + } + ++ /// Checks whether an object exists in the store. ++ /// ++ /// # How it works ++ /// ++ /// Delegates to [`HashMap::contains_key`], which is an average O(1) ++ /// operation. The method does not inspect the object bytes or validate the ++ /// hash; it only checks for key presence. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryStore; ++ /// # use libvctrl_handler::{Hash, ObjectStore}; ++ /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// let mut store = MemoryStore::new(); ++ /// assert!(!store.exists(&hash).unwrap()); ++ /// store.put(&hash, b"data").unwrap(); ++ /// assert!(store.exists(&hash).unwrap()); ++ /// ``` + fn exists(&self, hash: &Hash) -> Result { + Ok(self.objects.contains_key(hash)) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- #[test] +- fn put_and_get_roundtrip() -> Result<(), VctrlError> { +- let mut store = MemoryStore::new(); +- let hash = hash_byte(0xAB)?; +- let data = vec![10_u8, 20, 30]; +- +- store.put(&hash, &data)?; +- { +- let mut reader = store.get(&hash)?; +- let mut buf = Vec::new(); +- let _ = reader.read_to_end(&mut buf)?; +- assert_eq!(buf, data); +- } +- Ok(()) +- } +- +- #[test] +- fn get_missing_object_errors() -> Result<(), VctrlError> { +- let store = MemoryStore::new(); +- let hash = hash_byte(0xCD)?; +- let result = store.get(&hash); +- assert!(matches!(result, Err(VctrlError::ObjectNotFound(_)))); +- Ok(()) +- } +- +- #[test] +- fn delete_removes_object() -> Result<(), VctrlError> { +- let mut store = MemoryStore::new(); +- let hash = hash_byte(0xEF)?; +- let data = vec![1_u8, 2, 3]; +- +- store.put(&hash, &data)?; +- assert!(store.exists(&hash)?); +- store.delete(&hash)?; +- assert!(!store.exists(&hash)?); +- Ok(()) +- } +- +- #[test] +- fn exists_missing_object_returns_false() -> Result<(), VctrlError> { +- let store = MemoryStore::new(); +- let hash = hash_byte(0x77)?; +- assert!(!store.exists(&hash)?); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/src/store/mod.rs b/libvctrl_core/src/store/mod.rs +index 1578b12..0a6e1d7 100644 +--- a/libvctrl_core/src/store/mod.rs ++++ b/libvctrl_core/src/store/mod.rs +@@ -1,5 +1,70 @@ ++//! # In-Memory Stores ++//! ++//! This module provides ephemeral, in-memory implementations of the core ++//! storage contracts defined in `libvctrl_handler`: ++//! ++//! - [`MemoryStore`] implements [`ObjectStore`](libvctrl_handler::ObjectStore) ++//! for storing and retrieving raw object bytes. ++//! - [`MemoryRefStore`] implements [`RefStore`](libvctrl_handler::RefStore) ++//! for managing named references such as branches and tags. ++//! ++//! ## Why this module exists ++//! ++//! Version control backends must persist objects and references. However, ++//! persistent storage requires platform-specific I/O and error handling. The ++//! in-memory implementations decouple core VCS logic from those concerns. ++//! They serve as: ++//! ++//! - Reference implementations for the traits. ++//! - Test doubles for unit and integration tests. ++//! - Backends for short-lived or embedded scenarios. ++//! ++//! ## How it works ++//! ++//! Both stores use [`std::collections::HashMap`] under the hood. ++//! ++//! - [`MemoryStore`] maps a [`Hash`] to raw encoded bytes (`Vec`). ++//! - [`MemoryRefStore`] maps a reference name (`String`) to a [`Hash`]. ++//! ++//! Lookups are O(1) on average. The reference store sorts names before ++//! returning them from [`list_refs`](libvctrl_handler::RefStore::list_refs) to ++//! provide deterministic iteration. ++//! ++//! ## Examples ++//! ++//! The following example shows how the two stores can be used together: an ++//! object is placed into [`MemoryStore`], and a reference pointing to it is ++//! stored in [`MemoryRefStore`]. ++//! ++//! ``` ++//! # use libvctrl_handler::{Hash, ObjectStore, RefStore}; ++//! # use libvctrl_core::store::{MemoryStore, MemoryRefStore}; ++//! let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++//! ++//! let mut object_store = MemoryStore::new(); ++//! object_store.put(&hash, b"encoded object bytes").unwrap(); ++//! ++//! let mut ref_store = MemoryRefStore::new(); ++//! ref_store.set_ref("refs/heads/main", &hash).unwrap(); ++//! ++//! assert!(object_store.exists(&hash).unwrap()); ++//! assert_eq!(ref_store.get_ref("refs/heads/main").unwrap(), hash); ++//! ``` ++ ++/// In-memory object store. ++/// ++/// This submodule contains [`MemoryStore`](self::MemoryStore), a ++/// [`HashMap`]-backed implementation of ++/// [`ObjectStore`](libvctrl_handler::ObjectStore). It stores raw object bytes ++/// and is suitable for testing and ephemeral storage. + pub mod memory; + ++/// In-memory reference store. ++/// ++/// This submodule contains [`MemoryRefStore`](self::MemoryRefStore), a ++/// [`HashMap`]-backed implementation of ++/// [`RefStore`](libvctrl_handler::RefStore). It manages named references and ++/// returns sorted reference names. + pub mod ref_store; + + pub use memory::MemoryStore; +diff --git a/libvctrl_core/src/store/ref_store.rs b/libvctrl_core/src/store/ref_store.rs +index de5f8fe..2998e60 100644 +--- a/libvctrl_core/src/store/ref_store.rs ++++ b/libvctrl_core/src/store/ref_store.rs +@@ -1,14 +1,78 @@ +-use alloc::vec::IntoIter; +-use std::collections::HashMap; ++//! # In-Memory Reference Store ++//! ++//! This module provides [`MemoryRefStore`], a lightweight implementation of the ++//! [`RefStore`](libvctrl_handler::RefStore) trait backed by a ++//! [`std::collections::HashMap`]. ++//! ++//! The store is intended for testing, prototyping, and scenarios where ++//! persistence is not required. It stores references in memory only and loses ++//! all data when dropped. ++//! ++//! ## Why this exists ++//! ++//! The [`RefStore`](libvctrl_handler::RefStore) trait defines the contract for ++//! managing named references such as branches and tags. A concrete in-memory ++//! implementation is essential for unit tests, examples, and as a reference ++//! backend. It also demonstrates the expected behavior of the trait without ++//! any disk or network dependencies. ++//! ++//! ## How it works ++//! ++//! References are stored in a private `HashMap`. The `set_ref` ++//! method validates the reference name using ++//! [`validate_ref_name`](libvctrl_handler::validate_ref_name) before inserting. ++//! The `list_refs` method collects and sorts all keys to provide deterministic ++//! iteration order. + + use libvctrl_handler::{Hash, RefStore, VctrlError}; ++use std::collections::HashMap; + ++/// An in-memory implementation of [`RefStore`]. ++/// ++/// `MemoryRefStore` stores named references such as branches and tags in a ++/// `HashMap`. It is suitable for ephemeral use cases and testing. ++/// ++/// # Why this struct exists ++/// ++/// The [`RefStore`] trait requires an implementation to be useful. This struct ++/// provides a minimal, safe, and deterministic reference store that can be ++/// embedded in applications or used as a baseline for tests. ++/// ++/// # How it works ++/// ++/// Internally, references are keyed by name and mapped to their target ++/// [`Hash`]. The store validates names on insertion and returns errors when ++/// lookups fail. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_core::store::MemoryRefStore; ++/// # use libvctrl_handler::{Hash, RefStore}; ++/// let mut store = MemoryRefStore::new(); ++/// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++/// ++/// store.set_ref("refs/heads/main", &hash).unwrap(); ++/// assert_eq!(store.get_ref("refs/heads/main").unwrap(), hash); ++/// ``` + #[derive(Debug, Default)] + pub struct MemoryRefStore { + refs: HashMap, + } + + impl MemoryRefStore { ++ /// Creates a new empty `MemoryRefStore`. ++ /// ++ /// The store contains no references initially. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_core::store::MemoryRefStore; ++ /// use libvctrl_handler::RefStore; ++ /// let store = MemoryRefStore::new(); ++ /// assert!(store.list_refs().unwrap().next().is_none()); ++ /// ``` + #[must_use] + pub fn new() -> Self { + Self { +@@ -18,14 +82,53 @@ impl MemoryRefStore { + } + + impl RefStore for MemoryRefStore { +- type RefsIterator = IntoIter>; +- ++ type RefsIterator = std::vec::IntoIter>; ++ ++ /// Sets or updates a reference. ++ /// ++ /// The reference name is validated before insertion. If the name already ++ /// exists, its target hash is replaced. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if `name` is invalid according to ++ /// [`validate_ref_name`](libvctrl_handler::validate_ref_name). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryRefStore; ++ /// # use libvctrl_handler::{Hash, RefStore}; ++ /// let mut store = MemoryRefStore::new(); ++ /// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); ++ /// ++ /// store.set_ref("refs/heads/main", &hash).unwrap(); ++ /// assert!(store.get_ref("refs/heads/main").is_ok()); ++ /// ``` + fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError> { + libvctrl_handler::validate_ref_name(name)?; + let _ = self.refs.insert(name.to_string(), *hash); + Ok(()) + } + ++ /// Retrieves the target hash for a reference. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::RefNotFound`] if no reference with the given name ++ /// exists. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryRefStore; ++ /// # use libvctrl_handler::{Hash, RefStore}; ++ /// let mut store = MemoryRefStore::new(); ++ /// let hash = Hash::from_bytes(&[1u8; 64]).unwrap(); ++ /// store.set_ref("refs/heads/main", &hash).unwrap(); ++ /// ++ /// assert_eq!(store.get_ref("refs/heads/main").unwrap(), hash); ++ /// ``` + fn get_ref(&self, name: &str) -> Result { + self.refs + .get(name) +@@ -33,75 +136,62 @@ impl RefStore for MemoryRefStore { + .ok_or_else(|| VctrlError::RefNotFound(name.into())) + } + ++ /// Deletes a reference. ++ /// ++ /// If the reference does not exist, this method does nothing and returns ++ /// `Ok(())`. ++ /// ++ /// # Errors ++ /// ++ /// This method currently cannot fail; it always returns `Ok(())`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryRefStore; ++ /// # use libvctrl_handler::{Hash, RefStore}; ++ /// let mut store = MemoryRefStore::new(); ++ /// let hash = Hash::from_bytes(&[2u8; 64]).unwrap(); ++ /// store.set_ref("refs/heads/temp", &hash).unwrap(); ++ /// ++ /// store.delete_ref("refs/heads/temp").unwrap(); ++ /// assert!(store.get_ref("refs/heads/temp").is_err()); ++ /// ``` + fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError> { + let _ = self.refs.remove(name); + Ok(()) + } + ++ /// Lists all reference names in sorted order. ++ /// ++ /// The returned iterator yields `Result`. Sorting ++ /// ensures deterministic output, which is important for tests and ++ /// reproducibility. ++ /// ++ /// # Errors ++ /// ++ /// This method currently cannot fail; it always returns `Ok(iterator)`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_core::store::MemoryRefStore; ++ /// # use libvctrl_handler::{Hash, RefStore}; ++ /// let mut store = MemoryRefStore::new(); ++ /// let hash = Hash::from_bytes(&[3u8; 64]).unwrap(); ++ /// store.set_ref("refs/heads/b", &hash).unwrap(); ++ /// store.set_ref("refs/heads/a", &hash).unwrap(); ++ /// ++ /// let names: Vec = store ++ /// .list_refs() ++ /// .unwrap() ++ /// .map(|r| r.unwrap()) ++ /// .collect(); ++ /// assert_eq!(names, vec!["refs/heads/a".to_owned(), "refs/heads/b".to_owned()]); ++ /// ``` + fn list_refs(&self) -> Result { + let mut names: Vec = self.refs.keys().cloned().collect(); + names.sort(); + Ok(names.into_iter().map(Ok).collect::>().into_iter()) + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- fn hash_byte(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +- } +- +- #[test] +- fn set_and_get_ref_roundtrip() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let hash = hash_byte(0xAB)?; +- +- store.set_ref("refs/heads/main", &hash)?; +- let got = store.get_ref("refs/heads/main")?; +- assert_eq!(got, hash); +- Ok(()) +- } +- +- #[test] +- fn set_ref_invalid_name_errors() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let hash = hash_byte(0xCD)?; +- assert!(store.set_ref("bad name", &hash).is_err()); +- Ok(()) +- } +- +- #[test] +- fn get_ref_missing_errors() { +- let store = MemoryRefStore::new(); +- let result = store.get_ref("refs/heads/nope"); +- assert!(matches!(result, Err(VctrlError::RefNotFound(_)))); +- } +- +- #[test] +- fn delete_ref_removes_ref() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let hash = hash_byte(0xEF)?; +- store.set_ref("refs/tags/v1", &hash)?; +- store.delete_ref("refs/tags/v1")?; +- assert!(store.get_ref("refs/tags/v1").is_err()); +- Ok(()) +- } +- +- #[test] +- fn list_refs_sorted() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let h1 = hash_byte(0x01)?; +- let h2 = hash_byte(0x02)?; +- store.set_ref("refs/heads/b", &h1)?; +- store.set_ref("refs/heads/a", &h2)?; +- +- let names: Vec = store.list_refs()?.collect::>()?; +- assert_eq!( +- names, +- vec!["refs/heads/a".to_string(), "refs/heads/b".to_string()] +- ); +- Ok(()) +- } +-} +diff --git a/libvctrl_core/tests/codec_test.rs b/libvctrl_core/tests/codec_test.rs +new file mode 100644 +index 0000000..a1881ae +--- /dev/null ++++ b/libvctrl_core/tests/codec_test.rs +@@ -0,0 +1,424 @@ ++//! # Codec Round-Trip and Limit Tests ++//! ++//! This test module validates the binary encoder and decoder for all core ++//! object types: [`Blob`], [`Tree`], [`Commit`], and [`Tag`]. ++//! ++//! The tests verify: ++//! ++//! - Successful round-trip serialization for valid objects. ++//! - Malformed byte streams are rejected with [`VctrlError`]. ++//! - System limits (`MAX_BLOB_SIZE`, `MAX_TREE_ENTRIES`, ++//! `MAX_PARENT_COUNT`, `MAX_MESSAGE_LENGTH`) are enforced. ++//! - Version byte is checked. ++//! - All [`EntryKind`] variants survive encoding and decoding. ++//! ++//! These tests are integration-style but located within the same crate. ++//! They help ensure the codec remains backward-compatible and robust against ++//! corrupted or malicious input. ++ ++#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] ++#![allow(missing_docs)] ++#![allow(unused_crate_dependencies)] ++ ++use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; ++use libvctrl_handler::{ ++ Blob, Commit, CommitMeta, Decoder, Encoder, EntryKind, Hash, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, ++ MAX_PARENT_COUNT, MAX_TREE_ENTRIES, Tag, Tree, TreeEntry, UserID, ++}; ++use libvctrl_sha512 as _; ++use proptest as _; ++use std::io::Cursor; ++ ++/// Returns a hash filled with the byte `0xAB`. ++/// ++/// This is useful as a placeholder for an arbitrary valid object ID. ++fn dummy_hash() -> Hash { ++ Hash::from_bytes(&[0xAB; 64]).unwrap() ++} ++ ++/// Returns a hash filled with the given byte. ++/// ++/// The byte `b` is repeated 64 times to form a valid [`Hash`]. This helper ++/// creates distinguishable hashes for testing equality and ordering. ++fn hash_from_byte(b: u8) -> Hash { ++ Hash::from_bytes(&[b; 64]).unwrap() ++} ++ ++/// Creates a [`Blob`] of the specified size, filled with `0x42`. ++/// ++/// The size must not exceed [`MAX_BLOB_SIZE`]. The resulting blob is used to ++/// test size limits and round-trip behavior. ++fn blob_of_size(size: usize) -> Blob { ++ Blob::new(vec![0x42; size]).unwrap() ++} ++ ++/// Creates a [`Tree`] with `n` entries. ++/// ++/// Each entry is named `entry_XXX` (zero-padded) and points to ++/// [`dummy_hash`]. The entries are sorted by name to satisfy [`Tree`] ++/// ordering requirements. ++fn tree_with_n_entries(n: usize) -> Tree { ++ let mut entries = Vec::with_capacity(n); ++ for i in 0..n { ++ let name = format!("entry_{i:03}"); ++ entries.push(TreeEntry::new(name, EntryKind::Blob, dummy_hash()).unwrap()); ++ } ++ Tree::new(entries).unwrap() ++} ++ ++/// Creates a minimal, parentless commit with a fixed author and message. ++/// ++/// The tree is [`dummy_hash`], the author and committer are both ++/// "author ", and the message is "message". ++fn minimal_commit() -> Commit { ++ let user = UserID::new("author".into(), "author@example.com".into()).unwrap(); ++ Commit::new(dummy_hash(), vec![], user.clone(), user, "message".into()).unwrap() ++} ++ ++/// Creates a lightweight tag (no tagger, empty message) with the given name. ++/// ++/// The target is [`dummy_hash`]. ++fn lightweight_tag(name: &str) -> Tag { ++ Tag::new(name.into(), dummy_hash(), None, String::new()).unwrap() ++} ++ ++/// Tests blob encoding/decoding and blob size limits. ++/// ++/// Checks: ++/// - Empty blob round-trips. ++/// - Small blob round-trips. ++/// - Blob of exactly `MAX_BLOB_SIZE` round-trips. ++/// - Blob exceeding `MAX_BLOB_SIZE` fails at construction. ++#[test] ++fn test_blob_roundtrip_and_limits() { ++ // 1. Empty blob ++ let b = Blob::new(vec![]).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_blob(&b, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_blob(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.data(), b.data()); ++ ++ // 2. Small blob ++ let b = Blob::new(b"hello world".to_vec()).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_blob(&b, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_blob(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.data(), b.data()); ++ ++ // 3. Max size blob ++ let max_size = usize::try_from(MAX_BLOB_SIZE).unwrap(); ++ let b = blob_of_size(max_size); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_blob(&b, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_blob(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.size(), max_size); ++ ++ // 4. Exceeds max size (should fail at Blob::new) ++ let over_size = max_size + 1; ++ assert!(Blob::new(vec![0; over_size]).is_err()); ++} ++ ++/// Tests that malformed blob inputs are rejected. ++/// ++/// Covers: ++/// - Empty input. ++/// - Correct version but missing length prefix. ++/// - Wrong version byte. ++/// - Length mismatch (trailing byte). ++/// - Declared length exceeding `MAX_BLOB_SIZE`. ++#[test] ++fn test_blob_malformed_data() { ++ // Empty input ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&[])).is_err()); ++ ++ // Correct version but missing length prefix ++ let data = vec![0x03]; ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&data)).is_err()); ++ ++ // Wrong version ++ let data = vec![0x02]; ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&data)).is_err()); ++ ++ // Length mismatch (trailing byte) ++ let b = Blob::new(vec![0; 5]).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_blob(&b, &mut enc).unwrap(); ++ enc.push(0x00); ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&enc)).is_err()); ++ ++ // Declared length exceeds MAX_BLOB_SIZE ++ let over_size = usize::try_from(MAX_BLOB_SIZE).unwrap() + 1; ++ let mut bytes = vec![0x03u8]; ++ bytes.extend_from_slice(&(over_size as u64).to_le_bytes()); ++ bytes.extend(vec![0x00; over_size]); ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&bytes)).is_err()); ++} ++ ++/// Tests tree encoding/decoding and limit enforcement. ++/// ++/// Verifies: ++/// - Empty tree round-trips. ++/// - Tree with multiple entries round-trips. ++/// - All [`EntryKind`] variants survive round-trip. ++#[test] ++fn test_tree_roundtrip_and_limits() { ++ // Empty tree ++ let t = Tree::new(vec![]).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tree(Cursor::new(&enc)).unwrap(); ++ assert!(dec.entries().is_empty()); ++ ++ // Multiple entries ++ let t = tree_with_n_entries(5); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tree(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.entries().len(), 5); ++ ++ // All entry kinds roundtrip ++ let entries = vec![ ++ TreeEntry::new("blob".into(), EntryKind::Blob, hash_from_byte(1)).unwrap(), ++ TreeEntry::new("dir".into(), EntryKind::Tree, hash_from_byte(4)).unwrap(), ++ TreeEntry::new("exec".into(), EntryKind::Executable, hash_from_byte(2)).unwrap(), ++ TreeEntry::new("link".into(), EntryKind::Symlink, hash_from_byte(3)).unwrap(), ++ TreeEntry::new("sub".into(), EntryKind::Submodule, hash_from_byte(5)).unwrap(), ++ ]; ++ let t = Tree::new(entries).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tree(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.entries().len(), 5); ++} ++ ++/// Tests that malformed tree inputs are rejected. ++/// ++/// Covers: ++/// - Empty input. ++/// - Missing entry count. ++/// - Wrong version. ++/// - Entry count exceeding `MAX_TREE_ENTRIES`. ++/// - Truncated name. ++/// - Invalid entry kind byte. ++/// - Truncated hash. ++/// - Trailing bytes. ++#[test] ++fn test_tree_malformed_data() { ++ // Empty input ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&[])).is_err()); ++ ++ // Correct version but missing entry count bytes ++ let data = vec![0x03]; ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&data)).is_err()); ++ ++ // Wrong version ++ let data = vec![0x02]; ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&data)).is_err()); ++ ++ // Entry count exceeds MAX_TREE_ENTRIES ++ let over = usize::try_from(MAX_TREE_ENTRIES).unwrap() + 1; ++ let mut enc = vec![0x03u8]; ++ enc.extend_from_slice(&u32::try_from(over).unwrap().to_le_bytes()); ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&enc)).is_err()); ++ ++ // Truncated entry name ++ let tree = Tree::new(vec![]).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&tree, &mut enc).unwrap(); ++ enc[1..5].copy_from_slice(&1u32.to_le_bytes()); ++ enc.push(50); // Name length 50, but no data ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&enc)).is_err()); ++ ++ // Invalid entry kind ++ let tree = tree_with_n_entries(1); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&tree, &mut enc).unwrap(); ++ let kind_pos = 6 + 9; // version + count + name_len + name ++ enc[kind_pos] = 99; ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&enc)).is_err()); ++ ++ // Truncated hash ++ let tree = tree_with_n_entries(1); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&tree, &mut enc).unwrap(); ++ enc.truncate(enc.len() - 4); ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&enc)).is_err()); ++ ++ // Trailing bytes ++ let tree = tree_with_n_entries(1); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tree(&tree, &mut enc).unwrap(); ++ enc.push(0x00); ++ assert!(BinaryDecoder.decode_tree(Cursor::new(&enc)).is_err()); ++} ++ ++/// Tests commit encoding/decoding and limit enforcement. ++/// ++/// Verifies: ++/// - Minimal commit round-trips. ++/// - Commits with 0–256 parents round-trip. ++/// - Duplicate parents are rejected. ++/// - Parent count exceeding `MAX_PARENT_COUNT` is rejected. ++/// - Metadata encoding survives round-trip. ++/// - Invalid timezone offset is rejected. ++/// - Message exceeding `MAX_MESSAGE_LENGTH` is rejected. ++#[test] ++fn test_commit_roundtrip_and_limits() { ++ let user = UserID::new("author".into(), "author@example.com".into()).unwrap(); ++ ++ // Minimal commit ++ let c = minimal_commit(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_commit(&c, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_commit(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.tree(), c.tree()); ++ assert!(dec.parents().is_empty()); ++ assert_eq!(dec.author().name(), "author"); ++ assert_eq!(dec.message(), "message"); ++ ++ // With parents ++ let parents = vec![hash_from_byte(1), hash_from_byte(2), hash_from_byte(3)]; ++ let c = Commit::new( ++ dummy_hash(), ++ parents, ++ user.clone(), ++ user.clone(), ++ "merge".into(), ++ ) ++ .unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_commit(&c, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_commit(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.parents().len(), 3); ++ ++ // With many parents (u16 range — test 256 which exceeds old u8 limit) ++ let many_parents: Vec = (0u8..=255).map(hash_from_byte).collect(); ++ let c = Commit::new( ++ dummy_hash(), ++ many_parents.clone(), ++ user.clone(), ++ user.clone(), ++ "octopus".into(), ++ ) ++ .unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_commit(&c, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_commit(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.parents().len(), 256); ++ assert_eq!(dec.parents(), many_parents); ++ ++ // Duplicate parent rejected ++ let dup = vec![dummy_hash(), dummy_hash()]; ++ assert!(Commit::new(dummy_hash(), dup, user.clone(), user.clone(), "dup".into()).is_err()); ++ ++ // Exceeds MAX_PARENT_COUNT rejected ++ let too_many = vec![dummy_hash(); usize::try_from(MAX_PARENT_COUNT).unwrap() + 1]; ++ assert!( ++ Commit::new( ++ dummy_hash(), ++ too_many, ++ user.clone(), ++ user.clone(), ++ "toomany".into() ++ ) ++ .is_err() ++ ); ++ ++ // With meta ++ let meta = CommitMeta::new(1, 2, Some("UTF-8".into())).unwrap(); ++ let c = Commit::with_meta( ++ dummy_hash(), ++ vec![], ++ user.clone(), ++ user.clone(), ++ "msg".into(), ++ meta, ++ ) ++ .unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_commit(&c, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_commit(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.meta().encoding(), Some("UTF-8")); ++ ++ // Invalid timezone offset ++ assert!(CommitMeta::new(1, 1441, None).is_err()); ++ ++ // Message too long ++ let msg_len = usize::try_from(MAX_MESSAGE_LENGTH).unwrap() + 1; ++ let msg = "A".repeat(msg_len); ++ assert!(Commit::new(dummy_hash(), vec![], user.clone(), user, msg).is_err()); ++} ++ ++/// Tests tag encoding/decoding and limit enforcement. ++/// ++/// Verifies: ++/// - Lightweight tag round-trips. ++/// - Annotated tag (with tagger and message) round-trips. ++/// - Metadata encoding survives round-trip. ++/// - Tag name longer than 255 bytes is rejected. ++/// - Message exceeding `MAX_MESSAGE_LENGTH` is rejected. ++#[test] ++fn test_tag_roundtrip_and_limits() { ++ let tagger = UserID::new("tagger".into(), "tag@example.com".into()).unwrap(); ++ ++ // Lightweight tag ++ let t = lightweight_tag("v0.1"); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tag(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tag(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.name(), "v0.1"); ++ assert!(dec.tagger().is_none()); ++ ++ // Annotated tag ++ let t = Tag::new( ++ "v1.0".into(), ++ dummy_hash(), ++ Some(tagger.clone()), ++ "Release".into(), ++ ) ++ .unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tag(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tag(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.tagger().unwrap().name(), "tagger"); ++ assert_eq!(dec.message(), "Release"); ++ ++ // Tag with meta ++ let meta = CommitMeta::new(3, 4, Some("ISO-8859-1".into())).unwrap(); ++ let t = Tag::with_meta( ++ "v2.0".into(), ++ dummy_hash(), ++ Some(tagger), ++ "msg".into(), ++ meta, ++ ) ++ .unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_tag(&t, &mut enc).unwrap(); ++ let dec = BinaryDecoder.decode_tag(Cursor::new(&enc)).unwrap(); ++ assert_eq!(dec.meta().encoding(), Some("ISO-8859-1")); ++ ++ // Tag name too long ++ let long_name = "a".repeat(256); ++ assert!(Tag::new(long_name, dummy_hash(), None, String::new()).is_err()); ++ ++ // Message too long ++ let msg_len = usize::try_from(MAX_MESSAGE_LENGTH).unwrap() + 1; ++ let msg = "A".repeat(msg_len); ++ assert!(Tag::new("v".into(), dummy_hash(), None, msg).is_err()); ++} ++ ++/// Tests that a corrupted version byte is rejected. ++/// ++/// The version byte is the first byte of every encoded object. Changing it ++/// to an unsupported value must cause decoding to fail with ++/// [`VctrlError::CorruptedData`]. ++#[test] ++fn test_wrong_version_rejected() { ++ // Version 2 is no longer supported ++ let b = Blob::new(vec![]).unwrap(); ++ let mut enc = Vec::new(); ++ BinaryEncoder.encode_blob(&b, &mut enc).unwrap(); ++ enc[0] = 0x02; // Corrupt version byte ++ assert!(BinaryDecoder.decode_blob(Cursor::new(&enc)).is_err()); ++} +diff --git a/libvctrl_core/tests/common/mod.rs b/libvctrl_core/tests/common/mod.rs +deleted file mode 100644 +index bee37c0..0000000 +--- a/libvctrl_core/tests/common/mod.rs ++++ /dev/null +@@ -1,5 +0,0 @@ +-use libvctrl_handler::{Hash, VctrlError}; +- +-pub const fn make_hash(byte: u8) -> Result { +- Hash::from_bytes(&[byte; 64]) +-} +diff --git a/libvctrl_core/tests/integration_builders.rs b/libvctrl_core/tests/integration_builders.rs +deleted file mode 100644 +index 1393e27..0000000 +--- a/libvctrl_core/tests/integration_builders.rs ++++ /dev/null +@@ -1,40 +0,0 @@ +-use libvctrl_sha512 as _; +-use proptest as _; +- +-use libvctrl_core::object::{ +- BlobBuilder, CommitBuilder, TagBuilder, TreeBuilder, TreeEntryBuilder, +-}; +-use libvctrl_handler::{EntryKind, UserID, VctrlError}; +- +-pub mod common; +- +-fn make_user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +-} +- +-#[test] +-fn builder_chain_public_api() -> Result<(), VctrlError> { +- let hash = common::make_hash(0x77)?; +- let entry = TreeEntryBuilder::new("file".to_string(), EntryKind::Blob, hash).build()?; +- let _tree = TreeBuilder::new().entry(entry).build()?; +- +- let blob = BlobBuilder::new().with_data(vec![1_u8, 2]).build()?; +- assert_eq!(blob.data(), &[1_u8, 2]); +- +- let commit = CommitBuilder::new() +- .tree(common::make_hash(0x78)?) +- .author(make_user("Alice", "alice@example.com")?) +- .committer(make_user("Bob", "bob@example.com")?) +- .message("builder commit") +- .build()?; +- assert_eq!(commit.message(), "builder commit"); +- +- let tag = TagBuilder::new() +- .name("v1") +- .target(common::make_hash(0x79)?) +- .message("builder tag") +- .build()?; +- assert_eq!(tag.name(), "v1"); +- +- Ok(()) +-} +diff --git a/libvctrl_core/tests/integration_codec.rs b/libvctrl_core/tests/integration_codec.rs +deleted file mode 100644 +index bdc4aaa..0000000 +--- a/libvctrl_core/tests/integration_codec.rs ++++ /dev/null +@@ -1,113 +0,0 @@ +-use std::io::Cursor; +- +-use libvctrl_sha512 as _; +-use proptest as _; +- +-use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; +-use libvctrl_handler::{ +- Blob, Commit, CommitMeta, Decoder, Encoder, EntryKind, Tag, Tree, TreeEntry, UserID, VctrlError, +-}; +- +-pub mod common; +- +-fn make_user(name: &str, email: &str) -> Result { +- UserID::new(name.to_string(), email.to_string()) +-} +- +-fn make_meta(ts: i64, tz: i16) -> Result { +- CommitMeta::new(ts, tz, None) +-} +- +-#[test] +-fn blob_roundtrip_through_public_api() -> Result<(), VctrlError> { +- let payload = vec![9_u8, 8, 7, 6]; +- let blob = Blob::new(payload.clone())?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_blob(&blob, &mut buf)?; +- +- let decoded = BinaryDecoder.decode_blob(Cursor::new(buf))?; +- assert_eq!(decoded.data(), payload.as_slice()); +- +- Ok(()) +-} +- +-#[test] +-fn tree_roundtrip_through_public_api() -> Result<(), VctrlError> { +- let hash = common::make_hash(0x44)?; +- let entry = TreeEntry::new("file.txt".to_string(), EntryKind::Executable, hash)?; +- let tree = Tree::new(vec![entry])?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_tree(&tree, &mut buf)?; +- +- let decoded = BinaryDecoder.decode_tree(Cursor::new(buf))?; +- assert_eq!(decoded.entries().len(), 1); +- let first = decoded +- .entries() +- .first() +- .ok_or_else(|| VctrlError::Other("expected entry".into()))?; +- assert_eq!(first.name(), "file.txt"); +- assert_eq!(first.kind(), EntryKind::Executable); +- assert_eq!(*first.hash(), hash); +- +- Ok(()) +-} +- +-#[test] +-fn commit_roundtrip_through_public_api() -> Result<(), VctrlError> { +- let tree = common::make_hash(0x55)?; +- let parent = common::make_hash(0x56)?; +- let author = make_user("Alice", "alice@example.com")?; +- let committer = make_user("Bob", "bob@example.com")?; +- let message = "integration commit".to_string(); +- let meta = make_meta(1_600_000_000, 0)?; +- +- let commit = Commit::with_meta(tree, vec![parent], author, committer, message.clone(), meta)?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_commit(&commit, &mut buf)?; +- +- let decoded = BinaryDecoder.decode_commit(Cursor::new(buf))?; +- assert_eq!(decoded.tree(), &tree); +- assert_eq!(decoded.parents(), &[parent]); +- assert_eq!(decoded.author().name(), "Alice"); +- assert_eq!(decoded.committer().email(), "bob@example.com"); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 1_600_000_000); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- +- Ok(()) +-} +- +-#[test] +-fn tag_roundtrip_through_public_api() -> Result<(), VctrlError> { +- let target = common::make_hash(0x66)?; +- let tagger = make_user("Tagger", "tagger@example.com")?; +- let message = "v1.0".to_string(); +- let meta = make_meta(1_600_000_000, 0)?; +- +- let tag = Tag::with_meta( +- "v1.0".to_string(), +- target, +- Some(tagger), +- message.clone(), +- meta, +- )?; +- +- let mut buf = Vec::new(); +- BinaryEncoder.encode_tag(&tag, &mut buf)?; +- +- let decoded = BinaryDecoder.decode_tag(Cursor::new(buf))?; +- assert_eq!(decoded.name(), "v1.0"); +- assert_eq!(decoded.target(), &target); +- let tagger = decoded +- .tagger() +- .ok_or_else(|| VctrlError::Other("expected tagger".into()))?; +- assert_eq!(tagger.name(), "Tagger"); +- assert_eq!(decoded.message(), message); +- assert_eq!(decoded.meta().timestamp(), 1_600_000_000); +- assert_eq!(decoded.meta().timezone_offset(), 0); +- +- Ok(()) +-} +diff --git a/libvctrl_core/tests/integration_hash.rs b/libvctrl_core/tests/integration_hash.rs +deleted file mode 100644 +index 3070cf2..0000000 +--- a/libvctrl_core/tests/integration_hash.rs ++++ /dev/null +@@ -1,26 +0,0 @@ +-use std::io::Cursor; +- +-use libvctrl_sha512 as _; +-use proptest as _; +- +-use libvctrl_core::hash::Sha512Hasher; +-use libvctrl_handler::{Hasher, VctrlError}; +- +-#[test] +-fn sha512_hasher_public_api() -> Result<(), VctrlError> { +- let hasher = Sha512Hasher; +- let hash = hasher.hash(Cursor::new(b"abc"))?; +- +- assert_eq!( +- hash.as_bytes(), +- &[ +- 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, +- 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, +- 0x4b, 0x55, 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, +- 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, +- 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f +- ] +- ); +- +- Ok(()) +-} +diff --git a/libvctrl_core/tests/integration_store.rs b/libvctrl_core/tests/integration_store.rs +deleted file mode 100644 +index a0e22c8..0000000 +--- a/libvctrl_core/tests/integration_store.rs ++++ /dev/null +@@ -1,72 +0,0 @@ +-use std::io::Read; +- +-use libvctrl_sha512 as _; +-use proptest as _; +- +-use libvctrl_core::store::{MemoryRefStore, MemoryStore}; +-use libvctrl_handler::{ObjectStore, RefStore, VctrlError}; +- +-pub mod common; +- +-#[test] +-fn memory_store_put_get_delete_exists() -> Result<(), VctrlError> { +- let mut store = MemoryStore::new(); +- let hash = common::make_hash(0xAA)?; +- let data = vec![1_u8, 2, 3, 4]; +- +- store.put(&hash, &data)?; +- +- { +- let mut reader = store.get(&hash)?; +- let mut buf = Vec::new(); +- let _ = reader.read_to_end(&mut buf)?; +- assert_eq!(buf, data); +- } +- +- assert!(store.exists(&hash)?); +- store.delete(&hash)?; +- assert!(!store.exists(&hash)?); +- +- Ok(()) +-} +- +-#[test] +-fn memory_store_get_missing_errors() -> Result<(), VctrlError> { +- let store = MemoryStore::new(); +- let hash = common::make_hash(0xBB)?; +- let result = store.get(&hash); +- assert!(matches!(result, Err(VctrlError::ObjectNotFound(_)))); +- Ok(()) +-} +- +-#[test] +-fn memory_ref_store_roundtrip_and_list() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let h1 = common::make_hash(0x01)?; +- let h2 = common::make_hash(0x02)?; +- +- store.set_ref("refs/heads/main", &h1)?; +- store.set_ref("refs/heads/dev", &h2)?; +- +- assert_eq!(store.get_ref("refs/heads/main")?, h1); +- +- let names: Vec = store.list_refs()?.collect::>()?; +- assert_eq!( +- names, +- vec!["refs/heads/dev".to_string(), "refs/heads/main".to_string()] +- ); +- +- store.delete_ref("refs/heads/dev")?; +- assert!(store.get_ref("refs/heads/dev").is_err()); +- +- Ok(()) +-} +- +-#[test] +-fn memory_ref_store_invalid_name_errors() -> Result<(), VctrlError> { +- let mut store = MemoryRefStore::new(); +- let hash = common::make_hash(0x03)?; +- let result = store.set_ref("bad name", &hash); +- assert!(result.is_err()); +- Ok(()) +-} +diff --git a/libvctrl_core/tests/store_test.rs b/libvctrl_core/tests/store_test.rs +new file mode 100644 +index 0000000..bb6e432 +--- /dev/null ++++ b/libvctrl_core/tests/store_test.rs +@@ -0,0 +1,171 @@ ++//! # Store and RefStore Integration Tests ++//! ++//! This module contains integration-style tests for the in-memory object and ++//! reference store implementations: ++//! ++//! - `MemoryStore` implements `ObjectStore` and provides CRUD operations plus ++//! streaming reads via `Box`. ++//! - `MemoryRefStore` implements `RefStore` and manages named references with ++//! strict name validation and deterministic sorted iteration. ++//! ++//! The tests verify both normal behavior and defensive handling of malformed ++//! or potentially hostile inputs. ++ ++#![allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] ++#![allow(missing_docs)] ++#![allow(unused_crate_dependencies)] ++ ++use libvctrl_core::hash::Sha512Hasher; ++use libvctrl_core::store::{MemoryRefStore, MemoryStore}; ++use libvctrl_handler::{Hash, Hasher, MAX_NAME_LENGTH, ObjectStore, RefStore}; ++use libvctrl_sha512 as _; ++use proptest as _; ++use std::io::Read; ++ ++/// Computes a SHA-512 content hash for the given data. ++/// ++/// This helper uses `Sha512Hasher` to derive a stable, content-addressed ++/// identifier. It is used to generate distinct `Hash` values for objects and ++/// references in the tests. ++fn dummy_hash_from_data(data: &[u8]) -> Hash { ++ let hasher = Sha512Hasher; ++ hasher.hash(data).unwrap() ++} ++ ++/// Tests CRUD operations and streaming reads for `MemoryStore`. ++/// ++/// Verifies: ++/// - `put` stores data and `exists` reports it correctly. ++/// - `get` returns a stream that yields the exact stored bytes. ++/// - `delete` removes the object and subsequent `get` fails. ++/// - Deleting or reading a non-existent object does not panic. ++#[test] ++fn test_memory_store_crud_and_streaming() { ++ let mut store = MemoryStore::new(); ++ let data = b"hello world"; ++ let hash = dummy_hash_from_data(data); ++ ++ // Put ++ store.put(&hash, data).unwrap(); ++ ++ // Exists ++ assert!(store.exists(&hash).unwrap()); ++ assert!(!store.exists(&dummy_hash_from_data(b"other")).unwrap()); ++ ++ // Get and verify (zero-clone streaming) ++ { ++ let mut reader = store.get(&hash).unwrap(); ++ let mut buf = Vec::new(); ++ reader.read_to_end(&mut buf).unwrap(); ++ assert_eq!(buf, data); ++ } // reader is dropped here, releasing the immutable borrow ++ ++ // Delete ++ store.delete(&hash).unwrap(); ++ assert!(!store.exists(&hash).unwrap()); ++ ++ // Delete non-existent ++ assert!(store.delete(&hash).is_ok()); ++ ++ // Get non-existent ++ assert!(store.get(&hash).is_err()); ++} ++ ++/// Tests that `MemoryStore` can stream a large object without requiring a ++/// full contiguous copy beyond the stored data. ++/// ++/// The object is 10 MiB; reading it back through the returned reader must ++/// yield the exact original bytes. ++#[test] ++fn test_memory_store_large_object_streaming() { ++ let mut store = MemoryStore::new(); ++ // 10 MB object to test zero-copy cursor limits ++ let data = vec![0x42u8; 10 * 1024 * 1024]; ++ let hash = dummy_hash_from_data(&data); ++ ++ store.put(&hash, &data).unwrap(); ++ ++ let mut reader = store.get(&hash).unwrap(); ++ let mut buf = Vec::new(); ++ reader.read_to_end(&mut buf).unwrap(); ++ ++ assert_eq!(buf.len(), data.len()); ++ assert_eq!(buf, data); ++} ++ ++/// Tests CRUD operations and sorted iteration for `MemoryRefStore`. ++/// ++/// Verifies: ++/// - References can be set and retrieved. ++/// - `list_refs` returns names in sorted order. ++/// - Deleting a reference removes it from the store and from the listing. ++#[test] ++fn test_memory_ref_store_crud_and_sorting() { ++ let mut store = MemoryRefStore::new(); ++ let hash1 = dummy_hash_from_data(b"1"); ++ let hash2 = dummy_hash_from_data(b"2"); ++ ++ // Set refs ++ store.set_ref("refs/heads/main", &hash1).unwrap(); ++ store.set_ref("refs/heads/feature", &hash2).unwrap(); ++ ++ // Get ++ assert_eq!(store.get_ref("refs/heads/main").unwrap(), hash1); ++ ++ // List (should be sorted) ++ let refs: Vec = store ++ .list_refs() ++ .unwrap() ++ .collect::, _>>() ++ .unwrap(); ++ assert_eq!(refs, vec!["refs/heads/feature", "refs/heads/main"]); ++ ++ // Delete ++ store.delete_ref("refs/heads/main").unwrap(); ++ assert!(store.get_ref("refs/heads/main").is_err()); ++ ++ let refs: Vec = store ++ .list_refs() ++ .unwrap() ++ .collect::, _>>() ++ .unwrap(); ++ assert_eq!(refs, vec!["refs/heads/feature"]); ++} ++ ++/// Tests that `MemoryRefStore` enforces strict reference name validation. ++/// ++/// The following invalid names are rejected: ++/// - Empty string. ++/// - Names exceeding `MAX_NAME_LENGTH`. ++/// - Path traversal attempts (`../`, `..\\`, `..`). ++/// - Git illegal characters (`~`, `^`, `:`, space, `@{`, ending with `.lock`). ++/// ++/// A normal valid name is accepted. ++#[test] ++fn test_memory_ref_store_strict_validation() { ++ let mut store = MemoryRefStore::new(); ++ let hash = dummy_hash_from_data(b"1"); ++ ++ // Empty name ++ assert!(store.set_ref("", &hash).is_err()); ++ ++ // Too long name ++ let long_name = "a".repeat(usize::try_from(MAX_NAME_LENGTH).unwrap() + 1); ++ assert!(store.set_ref(&long_name, &hash).is_err()); ++ ++ // Path traversal attempts (Security) ++ assert!(store.set_ref("../config", &hash).is_err()); ++ assert!(store.set_ref("..\\config", &hash).is_err()); ++ assert!(store.set_ref("refs/heads/..", &hash).is_err()); ++ ++ // Git illegal characters ++ assert!(store.set_ref("refs/heads/foo~bar", &hash).is_err()); ++ assert!(store.set_ref("refs/heads/foo^bar", &hash).is_err()); ++ assert!(store.set_ref("refs/heads/foo:bar", &hash).is_err()); ++ assert!(store.set_ref("refs/heads/foo bar", &hash).is_err()); // Space ++ assert!(store.set_ref("refs/heads/@{upstream}", &hash).is_err()); ++ assert!(store.set_ref("refs/heads/foo.lock", &hash).is_err()); ++ ++ // Valid name ++ assert!(store.set_ref("refs/heads/valid_name", &hash).is_ok()); ++} +diff --git a/libvctrl_handler/Cargo.toml b/libvctrl_handler/Cargo.toml +index e34ad00..dcde7cb 100644 +--- a/libvctrl_handler/Cargo.toml ++++ b/libvctrl_handler/Cargo.toml +@@ -13,11 +13,4 @@ keywords = ["version-control", "vcs", "library", "traits"] + categories = ["development-tools", "data-structures"] + + [lints] +-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 ++workspace = true +\ No newline at end of file +diff --git a/libvctrl_handler/benches/handler_bench.rs b/libvctrl_handler/benches/handler_bench.rs +deleted file mode 100644 +index ed0bc73..0000000 +--- a/libvctrl_handler/benches/handler_bench.rs ++++ /dev/null +@@ -1,129 +0,0 @@ +-#![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); +diff --git a/libvctrl_handler/src/constants.rs b/libvctrl_handler/src/constants.rs +index 1369874..40d04fb 100644 +--- a/libvctrl_handler/src/constants.rs ++++ b/libvctrl_handler/src/constants.rs +@@ -1,14 +1,187 @@ ++//! Constants related to Git object formats and operational limits. ++//! ++//! # Architecture ++//! This module centralizes all magic numbers and structural limits used across the crate. ++//! By extracting these into named constants, we eliminate "magic numbers" from the business ++//! logic, making the codebase easier to audit and maintain. ++//! ++//! # Design Rationale: Resource Exhaustion Prevention ++//! Version control systems frequently handle untrusted or malformed data. Without strict ++//! upper limits, a maliciously crafted repository could instruct the parser to allocate ++//! gigabytes of memory (e.g., a blob claiming to be 10 Exabytes). The `MAX_*` constants ++//! act as fail-fast circuit breakers during object construction, ensuring that memory ++//! allocation remains bounded and predictable. ++//! ++//! # Git Protocol Compliance ++//! Constants like [`HASH_LENGTH`] and the modes in [`entry_mode`] are dictated by the Git ++//! core specification. Hardcoding them ensures strict compliance with standard Git clients ++//! and servers, preventing protocol violations. ++ ++/// Git object entry modes. ++/// ++/// # Architecture ++/// In Git, filesystem objects are identified by a 32-bit mode. This module exposes ++/// the specific constants recognized by the Git protocol. Using named constants ++/// instead of raw integers prevents invalid mode combinations and makes tree ++/// manipulation code self-documenting. ++/// ++/// # How it works ++/// The modes combine Unix permission bits with Git-specific object types. ++/// For example, `0o100_644` indicates a regular file (`0o100`) with read/write ++/// permissions for the owner and read-only for others (`0o644`). + pub mod entry_mode { ++ /// Regular file mode. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::constants::entry_mode::BLOB; ++ /// assert_eq!(BLOB, 0o100_644); ++ /// ``` + pub const BLOB: u32 = 0o100_644; ++ ++ /// Executable file mode. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::constants::entry_mode::EXECUTABLE; ++ /// assert_eq!(EXECUTABLE, 0o100_755); ++ /// ``` + pub const EXECUTABLE: u32 = 0o100_755; ++ ++ /// Symbolic link mode. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::constants::entry_mode::SYMLINK; ++ /// assert_eq!(SYMLINK, 0o120_000); ++ /// ``` + pub const SYMLINK: u32 = 0o120_000; ++ ++ /// Directory (tree) mode. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::constants::entry_mode::TREE; ++ /// assert_eq!(TREE, 0o40_000); ++ /// ``` + pub const TREE: u32 = 0o40_000; ++ ++ /// Submodule commit mode. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::constants::entry_mode::SUBMODULE; ++ /// assert_eq!(SUBMODULE, 0o160_000); ++ /// ``` + pub const SUBMODULE: u32 = 0o160_000; + } + ++/// The length of a hash in bytes (SHA-512 = 64). ++/// ++/// # Why this exists ++/// This crate mandates SHA-512 for cryptographic integrity. By hardcoding the length ++/// to 64 bytes, we enable the use of fixed-size arrays (e.g., `[u8; HASH_LENGTH]`) ++/// instead of dynamically allocated `Vec`. This shifts memory management to the ++/// compile-time stack, eliminating heap allocation overhead and fragmentation for ++/// every hash operation. ++/// ++/// # How it works ++/// The constant is evaluated at compile time. Any array sized with this constant ++/// benefits from fixed stack layout, and the compiler can aggressively optimize ++/// loops iterating exactly `HASH_LENGTH` times. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::HASH_LENGTH; ++/// assert_eq!(HASH_LENGTH, 64); ++/// let hash_array = [0_u8; HASH_LENGTH]; ++/// assert_eq!(hash_array.len(), 64); ++/// ``` + pub const HASH_LENGTH: usize = 64; ++ ++/// The maximum allowed length for names (in bytes). ++/// ++/// # Why this exists ++/// Enforces a sane upper bound on file, directory, and reference names. This aligns ++/// closely with typical filesystem limits (e.g., 255 bytes in most Unix filesystems). ++/// It prevents malicious inputs from causing excessive memory consumption or ++/// triggering filesystem errors during checkout operations. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::MAX_NAME_LENGTH; ++/// assert_eq!(MAX_NAME_LENGTH, 255); ++/// ``` + pub const MAX_NAME_LENGTH: u64 = 255; ++ ++/// The maximum allowed size for blob objects (in bytes). ++/// ++/// # Why this exists ++/// To prevent denial-of-service (`DoS`) via memory exhaustion. If unbounded, a parser ++/// reading a malformed packfile could attempt to allocate gigabytes of memory for a ++/// single blob. The 100 MiB limit provides ample room for legitimate source code and ++/// small binary assets while acting as a circuit breaker against malicious payloads. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::MAX_BLOB_SIZE; ++/// assert_eq!(MAX_BLOB_SIZE, 100 * 1024 * 1024); ++/// ``` + pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024; ++ ++/// The maximum number of entries allowed in a tree. ++/// ++/// # Why this exists ++/// While Git allows a technically unlimited number of entries in a tree object, ++/// performance degrades quadratically if entries are not handled correctly. Capping ++/// this at 100,000 ensures that tree parsing, diffing, and serialization remain ++/// performant and bounded in memory usage. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::MAX_TREE_ENTRIES; ++/// assert_eq!(MAX_TREE_ENTRIES, 100_000); ++/// ``` + pub const MAX_TREE_ENTRIES: u64 = 100_000; ++ ++/// The maximum allowed length for commit/tag messages (in bytes). ++/// ++/// # Why this exists ++/// Commit and tag messages are metadata. A 1 MiB limit is exceedingly generous for ++/// textual descriptions but strictly prevents malicious actors from embedding massive ++/// payloads (e.g., encoded binaries) into commit logs, which would bloat repository ++/// history and memory usage during traversal. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::MAX_MESSAGE_LENGTH; ++/// assert_eq!(MAX_MESSAGE_LENGTH, 1024 * 1024); ++/// ``` + pub const MAX_MESSAGE_LENGTH: u64 = 1024 * 1024; ++ ++/// The maximum number of parent commits allowed (binary format uses u16). ++/// ++/// # Why this exists ++/// Restricts the complexity of octopus merges. While Git supports many parents, ++/// allowing an unbounded number can lead to pathological graph structures that are ++/// expensive to traverse. The limit of 65,535 corresponds to the maximum value of ++/// an unsigned 16-bit integer, ensuring it can be packed efficiently if a binary ++/// format is introduced. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::constants::MAX_PARENT_COUNT; ++/// assert_eq!(MAX_PARENT_COUNT, 0xFFFF); ++/// ``` + pub const MAX_PARENT_COUNT: u64 = 0xFFFF; +diff --git a/libvctrl_handler/src/enums/core/entry_kind.rs b/libvctrl_handler/src/enums/core/entry_kind.rs +index 3f2a9a5..01d6412 100644 +--- a/libvctrl_handler/src/enums/core/entry_kind.rs ++++ b/libvctrl_handler/src/enums/core/entry_kind.rs +@@ -1,16 +1,73 @@ ++//! Core enum definitions for Git object types. ++//! ++//! # Architecture ++//! This module replaces raw integer mode bits (e.g., `0o100644`) with strongly-typed ++//! enumerations. By using [`EntryKind`], the compiler enforces exhaustive matching, ++//! preventing invalid or unrecognized file modes from propagating through the system. ++//! ++//! # Design Rationale ++//! Raw mode bits are error-prone; a typo like `0o100646` is a valid integer but an invalid Git ++//! mode. Enum variants encode domain logic directly into the type system, making the API ++//! self-documenting and eliminating entire classes of runtime errors associated with ++//! bit manipulation. ++ + use crate::constants::entry_mode; + ++/// The kind of an entry in a Git tree. ++/// ++/// # Why this exists ++/// Git stores filesystem objects (files, directories, symlinks) in tree objects. ++/// Each entry is identified by a 32-bit mode. This enum abstracts those raw bits into ++/// a strongly-typed domain model. It ensures that only valid Git object types can be ++/// represented, preventing invalid states (e.g., a mode of `0o000000`) from being ++/// constructed. ++/// ++/// # How it works ++/// The enum is marked as `#[non_exhaustive]` to allow for the addition of new Git ++/// object types in the future without breaking downstream API compatibility. Consumers ++/// must include a `_` catch-all arm when matching. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::enums::EntryKind; ++/// let kind = EntryKind::Blob; ++/// assert_eq!(kind.mode(), 0o100_644); ++/// ``` + #[non_exhaustive] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub enum EntryKind { ++ /// A regular file. + Blob, ++ /// An executable file. + Executable, ++ /// A symbolic link. + Symlink, ++ /// A directory (tree). + Tree, ++ /// A submodule commit. + Submodule, + } + + impl EntryKind { ++ /// Returns the Git mode bits for this entry kind. ++ /// ++ /// # Why this exists ++ /// Provides a seamless conversion from the strongly-typed [`EntryKind`] back to the ++ /// raw `u32` mode bits required for serializing Git tree objects or interacting with ++ /// lower-level filesystem APIs. ++ /// ++ /// # How it works ++ /// Implemented as a `const fn`, this allows the conversion to be evaluated at compile ++ /// time if the variant is known statically. This incurs zero runtime cost and enables ++ /// its use in other `const` contexts. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::enums::EntryKind; ++ /// assert_eq!(EntryKind::Executable.mode(), 0o100_755); ++ /// ``` + #[must_use] + pub const fn mode(self) -> u32 { + match self { +@@ -22,6 +79,37 @@ impl EntryKind { + } + } + ++ /// Converts raw Git mode bits into an [`EntryKind`]. ++ /// ++ /// # Why this exists ++ /// When parsing raw Git packfiles or loose objects, data is read as integers. This ++ /// function safely translates those integers into the domain model. By returning an ++ /// `Option`, it gracefully handles malformed or unrecognized mode bits without ++ /// panicking, allowing the caller to decide whether to ignore the entry or error out. ++ /// ++ /// # How it works ++ /// Matches the input against known Git mode constants defined in [`entry_mode`]. ++ /// If no match is found, `None` is returned. Like [`mode`](Self::mode), this is a ++ /// `const fn` to enable compile-time evaluation. ++ /// ++ /// # Examples ++ /// ++ /// Parsing a valid mode: ++ /// ++ /// ``` ++ /// # use libvctrl_handler::enums::EntryKind; ++ /// let mode = 0o120_000; // Symlink ++ /// let kind = EntryKind::from_mode(mode); ++ /// assert_eq!(kind, Some(EntryKind::Symlink)); ++ /// ``` ++ /// ++ /// Handling an invalid mode: ++ /// ++ /// ``` ++ /// # use libvctrl_handler::enums::EntryKind; ++ /// let invalid_mode = 0o000_000; ++ /// assert_eq!(EntryKind::from_mode(invalid_mode), None); ++ /// ``` + #[must_use] + pub const fn from_mode(mode: u32) -> Option { + match mode { +diff --git a/libvctrl_handler/src/enums/core/mod.rs b/libvctrl_handler/src/enums/core/mod.rs +index ff38ed1..9bb4e58 100644 +--- a/libvctrl_handler/src/enums/core/mod.rs ++++ b/libvctrl_handler/src/enums/core/mod.rs +@@ -1 +1,26 @@ ++//! Core enum definitions for Git object types. ++//! ++//! # Architecture ++//! This module acts as the central registry for enumerations that represent ++//! discrete, finite states in the Git protocol. By isolating these enums into ++//! a dedicated `core` submodule, the crate separates raw protocol definitions ++//! from higher-level domain logic and data structures. ++//! ++//! # Design Rationale: Strong Typing over Raw Integers ++//! The Git protocol frequently relies on raw integers or specific byte sequences ++//! to denote object types (e.g., mode bits in tree objects). Parsing these directly ++//! into integers throughout the codebase invites logic errors and security vulnerabilities. ++//! This module transforms those raw values into strongly-typed enums, allowing the ++//! Rust compiler to enforce exhaustive matching and guarantee that invalid states ++//! are unrepresentable at compile time. ++ ++/// Provides the [`EntryKind`](crate::enums::EntryKind) enum, which classifies ++/// the type of filesystem objects stored within a Git tree. ++/// ++/// # Why this exists ++/// Git tree objects map directory structures. Each entry in a tree requires a ++/// mode to distinguish between regular files, executable files, symbolic links, ++/// subdirectories (trees), and submodule commits. This submodule exposes the ++/// canonical enum for those classifications, ensuring that mode handling across ++/// the crate is type-safe and self-documenting. + pub mod entry_kind; +diff --git a/libvctrl_handler/src/enums/mod.rs b/libvctrl_handler/src/enums/mod.rs +index f47b173..60222df 100644 +--- a/libvctrl_handler/src/enums/mod.rs ++++ b/libvctrl_handler/src/enums/mod.rs +@@ -1,2 +1,51 @@ ++//! Enums for Git object types. ++//! ++//! # Architecture ++//! This module serves as the central registry for enumerations representing ++//! discrete, finite states within the Git protocol. By grouping these types ++//! together, the crate isolates protocol-level definitions from higher-level ++//! domain logic and data structures. ++//! ++//! # Design Rationale: Strong Typing over Raw Integers ++//! The Git protocol frequently relies on raw integers or specific byte sequences ++//! to denote object types (such as mode bits in tree objects). Parsing these ++//! directly into integers throughout the codebase invites logic errors and ++//! security vulnerabilities. This module transforms those raw values into ++//! strongly-typed enums, allowing the Rust compiler to enforce exhaustive ++//! matching and guarantee that invalid states are unrepresentable at compile time. ++//! ++//! # Examples ++//! *Note: The following example assumes this crate is named `libvctrl_handler`.* ++//! ++//! ``` ++//! # use libvctrl_handler::enums::EntryKind; ++//! let kind = EntryKind::Tree; ++//! assert_eq!(kind.mode(), 0o40_000); ++//! ``` ++ ++/// Core enum definitions representing fundamental Git protocol types. ++/// ++/// # Why this exists ++/// This submodule houses the primary enumerations used across the crate. ++/// Separating them into a `core` module allows the top-level `enums` module ++/// to remain organized, distinguishing between essential protocol types and ++/// any auxiliary or implementation-specific enums that may be added in the future. + pub mod core; ++ ++/// Re-export of the [`EntryKind`](core::entry_kind::EntryKind) enum for ergonomic access. ++/// ++/// # Why this exists ++/// Provides a flattened import path. Consumers can directly use ++/// `libvctrl_handler::enums::EntryKind` instead of navigating the full ++/// `libvctrl_handler::enums::core::entry_kind::EntryKind` path. This reduces ++/// boilerplate in consumer code while keeping the internal module ++/// structure logically separated. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::enums::EntryKind; ++/// let kind = EntryKind::Blob; ++/// assert_eq!(kind.mode(), 0o100_644); ++/// ``` + pub use core::entry_kind::EntryKind; +diff --git a/libvctrl_handler/src/errors.rs b/libvctrl_handler/src/errors.rs +index a5a24d8..e144c4c 100644 +--- a/libvctrl_handler/src/errors.rs ++++ b/libvctrl_handler/src/errors.rs +@@ -1,27 +1,89 @@ +-use alloc::sync::Arc; +-use core::error::Error; +-use core::fmt; +-use std::io; ++//! Error types used throughout the crate. ++//! ++//! # Architecture ++//! This module centralizes all error handling into a single, comprehensive [`VctrlError`] enum. ++//! By using a unified error type, the crate ensures that consumers can handle failures ++//! uniformly using the `?` operator across different subsystems (I/O, validation, parsing) ++//! without needing to manually box or wrap disparate error types. ++//! ++//! # Design Rationale: `Arc` ++//! The standard library's [`std::io::Error`] does not implement the `Clone` trait because ++//! it may contain custom payloads that are not safely cloneable. To allow [`VctrlError`] ++//! to be `Clone`, I/O errors are wrapped in an `Arc`. This provides thread-safe ++//! reference counting, allowing the error to be cloned cheaply (a single atomic increment) ++//! and shared across threads if necessary, while maintaining the original error's context. ++//! ++//! # Custom `PartialEq` Implementation ++//! Because [`std::io::Error`] lacks a `PartialEq` implementation, a manual comparison is ++//! provided for [`VctrlError::IoError`]. Two I/O errors are considered equal if their ++//! [`std::io::Error::kind()`] and their string representations match. This heuristic ++//! allows for predictable testing and equality checks without discarding the error details. ++//! ++//! # Examples ++//! *Note: The following examples assume this crate is named `libvctrl_handler`.* ++//! ++//! Handling errors from I/O operations: ++//! ++//! ``` ++//! # use libvctrl_handler::VctrlError; ++//! use std::io::{self, ErrorKind}; ++//! ++//! let io_err = io::Error::new(ErrorKind::NotFound, "file missing"); ++//! let vctrl_err = VctrlError::from_io(io_err); ++//! ++//! assert!(matches!(vctrl_err, VctrlError::IoError(_))); ++//! ``` + + use crate::constants::HASH_LENGTH; + use crate::types::Hash; ++use std::error::Error; ++use std::fmt; ++use std::io; ++use std::sync::Arc; + ++/// The main error type for all operations in this crate. ++/// ++/// This enum is marked as `#[non_exhaustive]` to allow for the addition of new error ++/// variants in future versions without causing breaking API changes. Consumers must ++/// include a `_` catch-all arm when matching against this enum to ensure forward compatibility. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::VctrlError; ++/// let err = VctrlError::InvalidName("bad name".to_string()); ++/// assert_eq!(err.to_string(), "Invalid name: 'bad name'"); ++/// ``` + #[non_exhaustive] + #[derive(Clone, Debug)] + pub enum VctrlError { ++ /// Data was corrupted or malformed. + CorruptedData(String), ++ /// A commit contains duplicate parent hashes. + DuplicateParent, ++ /// A size or count limit was exceeded. + ExceededMaxSize(String), ++ /// An invalid blame range was specified (e.g., zero line count). + InvalidBlameRange, ++ /// An email address was invalid. + InvalidEmail(String), ++ /// The length of a hash did not match the expected length. + InvalidHashLength(usize), ++ /// A name was invalid (empty, too long, or contained control characters). + InvalidName(String), ++ /// The timezone offset is out of the valid range (-1440 to 1440). + InvalidTimezoneOffset(i16), ++ /// The tree structure is invalid (e.g., unsorted entries, duplicates). + InvalidTreeStructure(String), ++ /// An I/O error occurred. + IoError(Arc), ++ /// An object with the given hash was not found. + ObjectNotFound(Hash), ++ /// Any other error not covered by the above variants. + Other(String), ++ /// A reference with the given name was not found. + RefNotFound(String), ++ /// A serialization/deserialization error occurred. + SerializationError(String), + } + +@@ -122,6 +184,28 @@ impl From for VctrlError { + } + + impl VctrlError { ++ /// Creates a [`VctrlError::IoError`] from a [`std::io::Error`]. ++ /// ++ /// This is the canonical way to convert I/O errors within the crate, ++ /// ensuring the `Arc` wrapping is applied consistently. ++ /// ++ /// # How it works ++ /// It wraps the provided error in an `Arc`, allowing the resulting ++ /// [`VctrlError`] to be cloned and shared across threads cheaply, despite ++ /// [`std::io::Error`] not natively implementing `Clone`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::VctrlError; ++ /// use std::io::{self, ErrorKind}; ++ /// ++ /// let io_err = io::Error::new(ErrorKind::PermissionDenied, "access denied"); ++ /// let vctrl_err = VctrlError::from_io(io_err); ++ /// ++ /// let cloned_err = vctrl_err.clone(); ++ /// assert_eq!(vctrl_err, cloned_err); ++ /// ``` + #[must_use] + #[inline] + pub fn from_io(err: io::Error) -> Self { +diff --git a/libvctrl_handler/src/lib.rs b/libvctrl_handler/src/lib.rs +index 9f8fd82..fb85615 100644 +--- a/libvctrl_handler/src/lib.rs ++++ b/libvctrl_handler/src/lib.rs +@@ -1,22 +1,123 @@ +-extern crate alloc; +- +-#[cfg(test)] +-use criterion as _; ++//! # `libvctrl_handler` ++//! ++//! A robust, pure-Rust implementation of Git internals, designed for ++//! high-performance and enterprise-grade reliability. ++//! ++//! ## Architecture ++//! ++//! The crate is strictly separated into distinct domains of responsibility: ++//! ++//! - **[`constants`]**: Defines hard limits and magic numbers used across the crate to prevent ++//! unbounded memory allocation and ensure protocol compliance. ++//! - **[`enums`]**: Provides exhaustive enumerations for Git-specific types, such as tree entry kinds. ++//! - **[`errors`]**: Centralizes all error handling via the [`VctrlError`] enum, ensuring consistent ++//! error propagation and diagnostics. ++//! - **[`macros`]**: Exposes declarative macros to reduce boilerplate for error construction. ++//! - **[`traits`]**: Defines the core abstract behaviors (e.g., [`Encoder`], [`Decoder`], [`ObjectStore`]). ++//! This allows consumers to plug in their own backends (in-memory, filesystem, network). ++//! - **[`types`]**: Contains strongly-typed representations of Git objects (e.g., [`Blob`], [`Tree`], [`Commit`]). ++//! - **[`validation`]**: Provides pure functions to validate inputs like names, hashes, and references ++//! before they enter the system state. ++//! ++//! ## Safety and Idioms ++//! ++//! This crate enforces `#![forbid(unsafe_code)]` to guarantee memory safety without compromise. ++//! It also aggressively denies clippy lints (all, pedantic, nursery) and enforces ++//! `missing_docs` to ensure the public API is fully documented. The design relies on ++//! Rust's zero-cost abstractions, utilizing `const fn` where possible to shift computations ++//! to compile time. ++//! ++//! ## Examples ++//! ++//! *Note: The following examples assume this crate is named `libvctrl_handler`.* ++//! ++//! Creating a valid [`Hash`] and inspecting an [`EntryKind`]: ++//! ++//! ``` ++//! # use libvctrl_handler::{EntryKind, Hash}; ++//! // Hash requires exactly 64 bytes (SHA-512). ++//! let raw_bytes = [0_u8; 64]; ++//! let hash = Hash::from_bytes(&raw_bytes); ++//! assert!(hash.is_ok()); ++//! ++//! // Git object modes can be inspected via the EntryKind enum. ++//! let blob_mode = EntryKind::Blob.mode(); ++//! assert_eq!(blob_mode, 0o100_644); ++//! ``` + ++/// Constants related to Git object formats and operational limits. ++/// ++/// # Why this exists ++/// Git has implicit and explicit limits (like maximum blob size or tree entries). ++/// Centralizing these constants prevents magic numbers across the codebase and ++/// ensures that limits are uniformly enforced at the type construction level. + pub mod constants; ++ ++/// Enums for Git object types. ++/// ++/// # Why this exists ++/// Using strongly-typed enums instead of raw integers (like `u32` mode bits) ++/// allows the compiler to exhaustively match object kinds, preventing invalid states ++/// and making the API self-documenting. + pub mod enums; ++ ++/// Error types used throughout the crate. ++/// ++/// # Why this exists ++/// Centralizes all error variants into a single [`VctrlError`] enum. This allows ++/// consumers to handle errors uniformly using the `?` operator across different subsystems ++/// without needing to box or wrap disparate error types manually. + pub mod errors; ++ ++/// Helper macros for the crate. ++/// ++/// # Why this exists ++/// Provides syntactic sugar for error creation, reducing boilerplate when wrapping ++/// strings into [`VctrlError::Other`] and ensuring consistent error formatting. + pub mod macros; ++ ++/// Traits defining repository operations. ++/// ++/// # Why this exists ++/// By defining traits like [`ObjectStore`] or [`Encoder`], the crate decouples ++/// the business logic from the underlying I/O backend. This enables mocking ++/// for tests and allows for custom storage implementations (e.g., in-memory vs. disk). + pub mod traits; ++ ++/// Core data types for Git objects. ++/// ++/// # Why this exists ++/// Provides immutable, validated structures like [`Commit`] and [`Tree`]. ++/// Construction is fallible, ensuring that invalid objects cannot exist at runtime. + pub mod types; ++ ++/// Pure validation functions for Git inputs. ++/// ++/// # Why this exists ++/// Separating validation from data structures allows the same logic to be ++/// applied to raw inputs before attempting object construction, failing fast ++/// on malformed data and preventing invalid states from ever being created. + pub mod validation; + ++/// Re-exports of fundamental constants for easy access. ++/// ++/// These limits are enforced during object construction to prevent memory exhaustion ++/// and maintain Git protocol compliance. + pub use constants::{ + HASH_LENGTH, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, MAX_NAME_LENGTH, MAX_PARENT_COUNT, + MAX_TREE_ENTRIES, + }; ++ ++/// Re-export of the [`EntryKind`] enum for classifying tree entries. + pub use enums::EntryKind; ++ ++/// Re-export of the primary error type [`VctrlError`]. + pub use errors::VctrlError; ++ ++/// Re-exports of core operational traits for backend implementation. ++/// ++/// Implement these traits to create a custom Git backend or to interact with ++/// repository data generically. + pub use traits::core::{ + blame::{Blame, BlameEntry}, + config::ConfigStore, +@@ -35,10 +136,18 @@ pub use traits::core::{ + transport::Transport, + verifier::Verifier, + }; ++ ++/// Re-exports of strongly-typed Git object representations. ++/// ++/// These types are the primary data carriers used in encoding, decoding, and manipulation. + pub use types::{ + Blob, ChangeKind, Commit, CommitMeta, Conflict, FileDelta, Hash, MergeResult, ReflogEntry, Tag, + Tree, TreeDelta, TreeEntry, UserID, + }; ++ ++/// Re-exports of validation utilities. ++/// ++/// Use these functions to sanitize or verify inputs before passing them to constructors. + pub use validation::{ + validate_hash_bytes, validate_name, validate_ref_name, validate_tree_entry_name, + }; +diff --git a/libvctrl_handler/src/macros.rs b/libvctrl_handler/src/macros.rs +index 322fabd..e6f2488 100644 +--- a/libvctrl_handler/src/macros.rs ++++ b/libvctrl_handler/src/macros.rs +@@ -1,3 +1,42 @@ ++/// Constructs a [`VctrlError::Other`](crate::VctrlError::Other) from a format string and arguments. ++/// ++/// # Why this exists ++/// In Rust, formatting a string and wrapping it into a custom error variant often requires ++/// verbose syntax like `VctrlError::Other(format!(...))`. This declarative macro provides ++/// syntactic sugar to eliminate this boilerplate. It ensures that ad-hoc errors are ++/// constructed consistently and concisely across the codebase, mirroring the ergonomics ++/// of the standard library's `println!` or `format!` macros. ++/// ++/// # How it works ++/// Under the hood, this macro delegates to the standard `format!` macro to allocate ++/// a new `String` on the heap. It then wraps this `String` in the ++/// [`VctrlError::Other`](crate::VctrlError::Other) variant. ++/// ++/// The use of `$crate` in the expansion is critical. It guarantees that the path to ++/// `VctrlError` resolves correctly to this crate's root, even if the macro is invoked ++/// from an external crate that has brought the macro into scope via a glob import. ++/// This prevents shadowing issues and ensures absolute path resolution without requiring ++/// the consumer to manually import the error enum alongside the macro. ++/// ++/// # Examples ++/// ++/// Creating a simple error message: ++/// ++/// ``` ++/// # use libvctrl_handler::{VctrlError, vctrl_error_other}; ++/// let err = vctrl_error_other!("file not found"); ++/// assert_eq!(err.to_string(), "file not found"); ++/// ``` ++/// ++/// Formatting arguments into the error message: ++/// ++/// ``` ++/// # use libvctrl_handler::{VctrlError, vctrl_error_other}; ++/// let filename = "config.toml"; ++/// let code = 404; ++/// let err = vctrl_error_other!("missing configuration file: {} (code {})", filename, code); ++/// assert_eq!(err.to_string(), "missing configuration file: config.toml (code 404)"); ++/// ``` + #[macro_export] + macro_rules! vctrl_error_other { + ($($arg:tt)*) => { +diff --git a/libvctrl_handler/src/traits/core/blame.rs b/libvctrl_handler/src/traits/core/blame.rs +index f659801..69dba60 100644 +--- a/libvctrl_handler/src/traits/core/blame.rs ++++ b/libvctrl_handler/src/traits/core/blame.rs +@@ -1,6 +1,49 @@ ++//! Blame computation trait. ++//! ++//! # Architecture ++//! This module provides the contracts for attributing lines in a file to specific commits. ++//! Blame computation is fundamentally different from standard diffing; it requires traversing ++//! history in reverse and tracking line movements across revisions. By isolating this into ++//! a dedicated trait, the crate allows consumers to plug in different blame algorithms ++//! (e.g., linear history vs. merge-aware) without altering the core engine. ++//! ++//! # Design Rationale: Immutability and Validation ++//! The [`BlameEntry`] struct is constructed via a fallible constructor (`new`). This ensures ++//! that invalid states—such as a line range starting at 0 or having a length of 0—cannot ++//! exist at runtime. Once constructed, the entry is immutable, guaranteeing that the blame ++//! history remains tamper-proof. ++ + use crate::errors::VctrlError; + use crate::types::Hash; + ++/// A single line range in a file attributed to a commit. ++/// ++/// # Why this exists ++/// Represents the atomic unit of blame data. Instead of attributing an entire file to a single ++/// commit, Git blame operates on line ranges. This struct encapsulates the mapping between a ++/// specific range of lines in a file and the commit that last modified them. ++/// ++/// # How it works ++/// The struct holds a reference to the committing [`Hash`], the 1-based line number range, ++/// the file path, and an optional commit summary. The `commit_id` is stored as a copied `Hash` ++/// (which is a fixed 64-byte array) rather than a reference, to simplify lifetime management ++/// when returning vectors of blame entries from background threads. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::blame::BlameEntry; ++/// # use libvctrl_handler::Hash; ++/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++/// let entry = BlameEntry::new( ++/// hash, ++/// 10, ++/// 5, ++/// "src/main.rs".to_string(), ++/// Some("Initial commit".to_string()), ++/// ); ++/// assert!(entry.is_ok()); ++/// ``` + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct BlameEntry { + commit_id: Hash, +@@ -11,6 +54,39 @@ pub struct BlameEntry { + } + + impl BlameEntry { ++ /// Creates a new `BlameEntry`. ++ /// ++ /// # Why this exists ++ /// Acts as a validation gate. In text file representations, line numbers are strictly ++ /// 1-based and must have a positive length. Allowing a `start_line` of 0 or a ++ /// `line_count` of 0 would violate these invariants and cause off-by-one errors ++ /// in downstream UI rendering or analysis. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidBlameRange`] if `start_line` is 0 or `line_count` is 0. ++ /// ++ /// # Examples ++ /// ++ /// Valid construction: ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 1, 10, "file.txt".into(), None); ++ /// assert!(entry.is_ok()); ++ /// ``` ++ /// ++ /// Invalid construction (zero start line): ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 0, 10, "file.txt".into(), None); ++ /// assert!(matches!(entry, Err(VctrlError::InvalidBlameRange))); ++ /// ``` + pub fn new( + commit_id: Hash, + start_line: usize, +@@ -30,32 +106,158 @@ impl BlameEntry { + }) + } + ++ /// Returns the commit that last modified these lines. ++ /// ++ /// # How it works ++ /// Because [`Hash`] is a `Copy` type (a fixed-size array wrapper), this accessor returns ++ /// a copy rather than a reference. This eliminates the need for lifetime annotations ++ /// on the returned value, making it easier to pass the hash to asynchronous tasks or ++ /// store in independent data structures. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 1, 1, "f".into(), None).unwrap(); ++ /// assert_eq!(entry.commit_id(), hash); ++ /// ``` + #[must_use] + pub const fn commit_id(&self) -> Hash { + self.commit_id + } + ++ /// Returns the first line number (1-based). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 42, 1, "f".into(), None).unwrap(); ++ /// assert_eq!(entry.start_line(), 42); ++ /// ``` + #[must_use] + pub const fn start_line(&self) -> usize { + self.start_line + } + ++ /// Returns the number of lines in this range. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 1, 5, "f".into(), None).unwrap(); ++ /// assert_eq!(entry.line_count(), 5); ++ /// ``` + #[must_use] + pub const fn line_count(&self) -> usize { + self.line_count + } + ++ /// Returns the path of the file. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. This avoids ++ /// allocation when the caller only needs to read the path. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 1, 1, "src/main.rs".into(), None).unwrap(); ++ /// assert_eq!(entry.path(), "src/main.rs"); ++ /// ``` + #[must_use] + pub fn path(&self) -> &str { + &self.path + } + ++ /// Returns an optional summary of the commit message. ++ /// ++ /// # How it works ++ /// Uses `as_deref()` to transparently convert `&Option` to `Option<&str>`, ++ /// avoiding the need to clone the `String` if the caller only wishes to read the summary. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::BlameEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++ /// let entry = BlameEntry::new(hash, 1, 1, "f".into(), Some("Fix bug".into())).unwrap(); ++ /// assert_eq!(entry.summary(), Some("Fix bug")); ++ /// ``` + #[must_use] + pub fn summary(&self) -> Option<&str> { + self.summary.as_deref() + } + } + ++/// Trait for computing blame information for files. ++/// ++/// # Why this exists ++/// Defines the abstract contract for attributing file lines to commits. By using a trait, ++/// the crate decouples the blame algorithm from the repository backend. This allows for ++/// different implementations (e.g., a simple linear walker vs. a complex graph traversal ++/// that handles merges). ++/// ++/// # Design Rationale: `Send + Sync` ++/// The trait requires `Send + Sync` because blame computation is highly parallelizable. ++/// File-level blame operations are independent of one another. Implementors can safely ++/// distribute `&self` across multiple threads to compute blame for different files ++/// concurrently, leveraging multi-core processors without data races. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock repository: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::blame::{Blame, BlameEntry}; ++/// # use libvctrl_handler::{Hash, VctrlError}; ++/// # ++/// struct MockRepo; ++/// ++/// impl Blame for MockRepo { ++/// fn blame_file(&self, _path: &str) -> Result, VctrlError> { ++/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++/// let entry = BlameEntry::new(hash, 1, 10, "file.txt".into(), None)?; ++/// Ok(vec![entry]) ++/// } ++/// } ++/// ++/// let repo = MockRepo; ++/// let entries = repo.blame_file("file.txt").unwrap(); ++/// assert_eq!(entries.len(), 1); ++/// ``` + pub trait Blame: Send + Sync { ++ /// Returns blame entries for the given file path. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the file cannot be found or the blame calculation fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::blame::{Blame, BlameEntry}; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # ++ /// # struct MockRepo; ++ /// # impl Blame for MockRepo { ++ /// # fn blame_file(&self, _path: &str) -> Result, VctrlError> { ++ /// # Ok(Vec::new()) ++ /// # } ++ /// # } ++ /// let repo = MockRepo; ++ /// assert!(repo.blame_file("nonexistent.txt").is_ok()); ++ /// ``` + fn blame_file(&self, path: &str) -> Result, VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/config.rs b/libvctrl_handler/src/traits/core/config.rs +index 2860cca..8d061c0 100644 +--- a/libvctrl_handler/src/traits/core/config.rs ++++ b/libvctrl_handler/src/traits/core/config.rs +@@ -1,10 +1,289 @@ ++//! Configuration store trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for reading and writing repository ++//! configuration settings (e.g., `.git/config`). By abstracting this into a trait, ++//! the crate decouples the core engine from the underlying storage mechanism, ++//! allowing consumers to use INI files, databases, or in-memory hash maps. ++//! ++//! # Design Rationale: `Option` vs `Result` ++//! Configuration is inherently sparse. A missing key is often a valid state indicating ++//! that a default value should be used, not an exceptional error. Therefore, read ++//! operations return `Option`. An `Err(VctrlError)` is reserved strictly for ++//! I/O failures or parsing corruption, ensuring a clear distinction between ++//! "key not set" and "failed to read configuration". ++ + use crate::errors::VctrlError; + ++/// A trait for reading and writing configuration values. ++/// ++/// # Why this exists ++/// Provides a unified, type-safe interface for managing repository settings. Git ++/// configurations are segmented by sections (e.g., `user`, `core`) and keys. ++/// This trait enforces that structure, preventing malformed configuration access ++/// and allowing backend-agnostic validation. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync`. Configuration is frequently read by multiple ++/// concurrent operations (e.g., checking commit hooks, resolving user identities) ++/// but rarely written. This trait design allows implementors to use `RwLock` ++/// internally or rely on immutable snapshots, enabling safe parallel reads across ++/// threads without locking the entire repository state. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory store: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::config::ConfigStore; ++/// # use libvctrl_handler::VctrlError; ++/// # use std::collections::HashMap; ++/// # ++/// #[derive(Default)] ++/// struct MockConfig { ++/// data: HashMap, ++/// } ++/// ++/// impl ConfigStore for MockConfig { ++/// fn get_string(&self, section: &str, key: &str) -> Result, VctrlError> { ++/// let full_key = format!("{section}.{key}"); ++/// Ok(self.data.get(&full_key).cloned()) ++/// } ++/// ++/// fn set_string(&mut self, section: &str, key: &str, value: &str) -> Result<(), VctrlError> { ++/// let full_key = format!("{section}.{key}"); ++/// self.data.insert(full_key, value.to_string()); ++/// Ok(()) ++/// } ++/// ++/// fn get_bool(&self, section: &str, key: &str) -> Result, VctrlError> { ++/// Ok(self.get_string(section, key)?.map(|v| v == "true")) ++/// } ++/// ++/// fn set_bool(&mut self, section: &str, key: &str, value: bool) -> Result<(), VctrlError> { ++/// self.set_string(section, key, if value { "true" } else { "false" }) ++/// } ++/// ++/// fn remove(&mut self, section: &str, key: &str) -> Result<(), VctrlError> { ++/// let full_key = format!("{section}.{key}"); ++/// self.data.remove(&full_key); ++/// Ok(()) ++/// } ++/// ++/// fn exists(&self, section: &str, key: &str) -> Result { ++/// let full_key = format!("{section}.{key}"); ++/// Ok(self.data.contains_key(&full_key)) ++/// } ++/// } ++/// ++/// let mut cfg = MockConfig::default(); ++/// cfg.set_string("user", "name", "Alice")?; ++/// assert_eq!(cfg.get_string("user", "name")?, Some("Alice".to_string())); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait ConfigStore: Send + Sync { ++ /// Returns the string value for the given section and key. ++ /// ++ /// # How it works ++ /// Looks up the configuration value in the specified section. If the section ++ /// or key does not exist, it returns `Ok(None)` rather than an error, allowing ++ /// the caller to fall back to default values gracefully. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be read (e.g., due to ++ /// an I/O failure or corrupted configuration file). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let mut cfg = MockConfig::default(); ++ /// cfg.set_string("core", "editor", "vim")?; ++ /// assert_eq!(cfg.get_string("core", "editor")?, Some("vim".to_string())); ++ /// assert_eq!(cfg.get_string("core", "missing")?, None); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn get_string(&self, section: &str, key: &str) -> Result, VctrlError>; ++ ++ /// Sets the string value for the given section and key. ++ /// ++ /// # How it works ++ /// Requires `&mut self`, enforcing exclusive access for write operations. This ++ /// ensures that no other thread can read a partially written configuration state, ++ /// maintaining atomicity at the trait level. Implementors are responsible for ++ /// persisting this change to the underlying storage medium. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be written (e.g., due to ++ /// insufficient permissions or disk full). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let mut cfg = MockConfig::default(); ++ /// cfg.set_string("user", "email", "test@example.com")?; ++ /// assert!(cfg.exists("user", "email")?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn set_string(&mut self, section: &str, key: &str, value: &str) -> Result<(), VctrlError>; ++ ++ /// Returns the boolean value for the given section and key. ++ /// ++ /// # How it works ++ /// Retrieves the string representation and attempts to parse it as a boolean. ++ /// If the key exists but is not a valid boolean (e.g., "yes", "1", "true"), ++ /// the implementor should return a [`VctrlError::SerializationError`] or similar, ++ /// as this indicates a corrupted or malformed configuration. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be read or is not a boolean. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let mut cfg = MockConfig::default(); ++ /// cfg.set_bool("core", "bare", true)?; ++ /// assert_eq!(cfg.get_bool("core", "bare")?, Some(true)); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn get_bool(&self, section: &str, key: &str) -> Result, VctrlError>; ++ ++ /// Sets the boolean value for the given section and key. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be written. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let mut cfg = MockConfig::default(); ++ /// cfg.set_bool("core", "autocrlf", false)?; ++ /// assert_eq!(cfg.get_string("core", "autocrlf")?, Some("false".to_string())); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn set_bool(&mut self, section: &str, key: &str, value: bool) -> Result<(), VctrlError>; ++ ++ /// Removes a key from the configuration. ++ /// ++ /// # How it works ++ /// Deletes the specified key within the given section. If the key or section ++ /// does not exist, this operation is idempotent and returns `Ok(())`, ensuring ++ /// that cleanup operations do not fail spuriously on missing data. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be modified (e.g., due to ++ /// file permission issues). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let mut cfg = MockConfig::default(); ++ /// cfg.set_string("remote", "origin", "url")?; ++ /// cfg.remove("remote", "origin")?; ++ /// assert!(!cfg.exists("remote", "origin")?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn remove(&mut self, section: &str, key: &str) -> Result<(), VctrlError>; ++ ++ /// Checks if a key exists in the configuration. ++ /// ++ /// # How it works ++ /// Performs a lightweight existence check without retrieving the value. This is ++ /// useful for validating configuration prerequisites before attempting complex ++ /// operations. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the configuration cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::config::ConfigStore; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockConfig { data: HashMap } ++ /// # impl ConfigStore for MockConfig { ++ /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } ++ /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } ++ /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } ++ /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } ++ /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } ++ /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } ++ /// # } ++ /// let cfg = MockConfig::default(); ++ /// assert!(!cfg.exists("nonexistent", "key")?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn exists(&self, section: &str, key: &str) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/decoder.rs b/libvctrl_handler/src/traits/core/decoder.rs +index 45af17d..0141b04 100644 +--- a/libvctrl_handler/src/traits/core/decoder.rs ++++ b/libvctrl_handler/src/traits/core/decoder.rs +@@ -1,11 +1,223 @@ +-use std::io::Read; ++//! Object decoder trait. ++//! ++//! # Architecture ++//! This module defines the contract for deserializing raw byte streams into ++//! strongly-typed Git domain objects ([`Blob`], [`Tree`], [`Commit`], [`Tag`]). ++//! It acts as the bridge between unstructured I/O data and the crate's type-safe ++//! in-memory representations. ++//! ++//! # Design Rationale: Streaming Deserialization ++//! Instead of accepting a `&[u8]` or `Vec`, the decoder methods require a ++//! generic `R: Read` bound. This is a critical architectural decision: it forces ++//! streaming deserialization. Git objects (especially blobs) can be massive. ++//! By reading from a stream, the decoder can process gigabytes of data with a ++//! fixed memory footprint, preventing denial-of-service (DoS) vulnerabilities ++//! associated with unbounded memory allocation. + + use crate::errors::VctrlError; + use crate::types::{Blob, Commit, Tag, Tree}; ++use std::io::Read; + ++/// Trait for decoding raw Git object bytes into structured types. ++/// ++/// # Why this exists ++/// Abstracts the parsing logic away from the storage backend. Whether objects ++/// are being read from loose files on disk, extracted from a compressed packfile, ++/// or streamed over a network socket, the decoding logic remains identical. ++/// This allows the crate to support multiple wire formats or compression ++/// algorithms by simply providing different implementations of this trait. ++/// ++/// # How it works ++/// The trait uses generic methods (``) rather than dynamic ++/// trait objects (`&mut dyn Read`). This design leverages Rust's monomorphization: ++/// the compiler generates a specific version of the decode function for every ++/// concrete reader type used at runtime. This eliminates dynamic dispatch overhead, ++/// allowing the compiler to aggressively inline the reading logic. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync` on `Self`, and `Send` on the reader `R`. ++/// This ensures that decoding operations can be safely dispatched to a thread pool. ++/// For example, when parsing a multi-object packfile, the engine can distribute ++/// object streams across multiple worker threads to utilize multi-core parallelism ++/// without risking data races. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock streaming parser: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::decoder::Decoder; ++/// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++/// # use std::io::{Cursor, Read}; ++/// # ++/// struct MockDecoder; ++/// ++/// impl Decoder for MockDecoder { ++/// fn decode_blob(&self, mut reader: R) -> Result { ++/// let mut buf = Vec::new(); ++/// reader.read_to_end(&mut buf)?; ++/// Blob::new(buf) ++/// } ++/// ++/// fn decode_tree(&self, _reader: R) -> Result { ++/// // Mock implementation returns an empty tree ++/// Tree::new(vec![]) ++/// } ++/// ++/// fn decode_commit(&self, _reader: R) -> Result { ++/// // Mock implementation returns an error for brevity ++/// Err(VctrlError::Other("mock commit decode".into())) ++/// } ++/// ++/// fn decode_tag(&self, _reader: R) -> Result { ++/// Err(VctrlError::Other("mock tag decode".into())) ++/// } ++/// } ++/// ++/// let decoder = MockDecoder; ++/// let raw_data = Cursor::new(b"file content".to_vec()); ++/// let blob = decoder.decode_blob(raw_data)?; ++/// assert_eq!(blob.data(), b"file content"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Decoder: Send + Sync { ++ /// Decodes a blob object from a reader. ++ /// ++ /// # How it works ++ /// Reads bytes from the provided reader until EOF, enforcing the ++ /// [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE) limit during the ++ /// construction of the [`Blob`] type. This prevents memory exhaustion ++ /// from maliciously large streams. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if decoding fails. This can occur if the reader ++ /// encounters an I/O error, or if the parsed data exceeds the maximum ++ /// allowed size limits. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::decoder::Decoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::{Cursor, Read}; ++ /// # ++ /// # struct MockDecoder; ++ /// # impl Decoder for MockDecoder { ++ /// # fn decode_blob(&self, mut reader: R) -> Result { ++ /// # let mut buf = Vec::new(); ++ /// # reader.read_to_end(&mut buf)?; ++ /// # Blob::new(buf) ++ /// # } ++ /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } ++ /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # } ++ /// let decoder = MockDecoder; ++ /// let stream = Cursor::new(b"binary data".to_vec()); ++ /// assert!(decoder.decode_blob(stream).is_ok()); ++ /// ``` + fn decode_blob(&self, reader: R) -> Result; ++ ++ /// Decodes a tree object from a reader. ++ /// ++ /// # How it works ++ /// Parses the binary tree format, reading entry modes, names, and hashes ++ /// sequentially. It enforces Git's strict sorting rules (directories are ++ /// sorted as if they have a trailing `/`) and rejects duplicate entries ++ /// during the construction of the [`Tree`] type. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if decoding fails. This can occur if the stream ++ /// is truncated, contains invalid mode bits, or violates tree structural ++ /// integrity (e.g., unsorted entries). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::decoder::Decoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::{Cursor, Read}; ++ /// # ++ /// # struct MockDecoder; ++ /// # impl Decoder for MockDecoder { ++ /// # fn decode_blob(&self, mut reader: R) -> Result { Blob::new(Vec::new()) } ++ /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } ++ /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # } ++ /// let decoder = MockDecoder; ++ /// let stream = Cursor::new(Vec::new()); ++ /// assert!(decoder.decode_tree(stream).is_ok()); ++ /// ``` + fn decode_tree(&self, reader: R) -> Result; ++ ++ /// Decodes a commit object from a reader. ++ /// ++ /// # How it works ++ /// Parses the textual commit format, extracting tree references, parent ++ /// hashes, author/committer metadata, and the commit message. It validates ++ /// parent counts and message lengths against crate constants before ++ /// constructing the [`Commit`] type. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if decoding fails. This can occur if the commit ++ /// contains duplicate parents, if the timestamp is malformed, or if an ++ /// I/O error occurs while reading the stream. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::decoder::Decoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::{Cursor, Read}; ++ /// # ++ /// # struct MockDecoder; ++ /// # impl Decoder for MockDecoder { ++ /// # fn decode_blob(&self, _reader: R) -> Result { Blob::new(Vec::new()) } ++ /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } ++ /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # } ++ /// let decoder = MockDecoder; ++ /// let stream = Cursor::new(Vec::new()); ++ /// assert!(decoder.decode_commit(stream).is_err()); // Mock returns err ++ /// ``` + fn decode_commit(&self, reader: R) -> Result; ++ ++ /// Decodes a tag object from a reader. ++ /// ++ /// # How it works ++ /// Parses the annotated tag format, extracting the target object hash, ++ /// tagger identity, and tag message. It enforces reference naming rules ++ /// (via [`validate_ref_name`](crate::validation::validate_ref_name)) on the ++ /// tag's name during construction. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if decoding fails. This can occur if the tag name ++ /// is invalid, if the message exceeds the maximum length, or if the stream ++ /// is corrupted. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::decoder::Decoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::{Cursor, Read}; ++ /// # ++ /// # struct MockDecoder; ++ /// # impl Decoder for MockDecoder { ++ /// # fn decode_blob(&self, _reader: R) -> Result { Blob::new(Vec::new()) } ++ /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } ++ /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } ++ /// # } ++ /// let decoder = MockDecoder; ++ /// let stream = Cursor::new(Vec::new()); ++ /// assert!(decoder.decode_tag(stream).is_err()); // Mock returns err ++ /// ``` + fn decode_tag(&self, reader: R) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/diff.rs b/libvctrl_handler/src/traits/core/diff.rs +index f07ad5a..82d52bb 100644 +--- a/libvctrl_handler/src/traits/core/diff.rs ++++ b/libvctrl_handler/src/traits/core/diff.rs +@@ -1,8 +1,119 @@ ++//! Tree differencing trait. ++//! ++//! # Architecture ++//! This module provides the abstract contract for computing structural deltas ++//! between two tree objects. It abstracts the diffing algorithm (e.g., Myers, ++//! Histogram) away from the core engine, allowing consumers to plug in ++//! optimized or specialized diffing strategies. ++//! ++//! # Design Rationale: Associated Types over Generics ++//! The trait uses an associated type (`type TreeId`) rather than a generic ++//! parameter (``). This design choice is deliberate: it ties the ++//! identifier type to the specific `TreeDiffer` implementation. A differ that ++//! reads from an in-memory store might use array indices as IDs, while a ++//! filesystem-based differ uses `Hash`. Associated types prevent the need to ++//! annotate the trait with generics at every call site, simplifying the API ++//! while preserving flexibility. ++ + use crate::errors::VctrlError; + use crate::types::TreeDelta; + ++/// Trait for computing differences between two trees. ++/// ++/// # Why this exists ++/// Comparing two trees to find file additions, deletions, modifications, and ++/// renames is a fundamental operation in version control. By defining this as ++/// a trait, the crate ensures that the core logic does not depend on a specific ++/// algorithm or storage backend. The output is a strongly-typed [`TreeDelta`], ++/// which aggregates [`FileDelta`](crate::FileDelta) entries, ensuring that ++/// downstream consumers (like UI renderers or merge drivers) receive a ++/// consistent, validated data structure. ++/// ++/// # How it works ++/// The implementor receives references to two tree identifiers (`old` and `new`). ++/// It is responsible for resolving these IDs to actual tree data (if necessary), ++/// comparing their entries recursively, and classifying the changes. The ++/// resulting [`TreeDelta`] provides an iterator-like interface over these ++/// atomic file changes. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync` on both `Self` and the associated `TreeId`. ++/// This is critical for performance: diffing large repositories is highly ++/// parallelizable. By enforcing thread safety, the engine can dispatch ++/// multiple `diff_trees` calls across a thread pool (e.g., using `rayon`) ++/// to compare different directory branches concurrently without data races. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock store that always reports no changes: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::diff::TreeDiffer; ++/// # use libvctrl_handler::{TreeDelta, Hash, VctrlError}; ++/// # ++/// struct MockDiffer; ++/// ++/// impl TreeDiffer for MockDiffer { ++/// type TreeId = Hash; ++/// ++/// fn diff_trees(&self, _old: &Self::TreeId, _new: &Self::TreeId) -> Result { ++/// // In a real implementation, this would load trees and compare entries. ++/// Ok(TreeDelta::new()) ++/// } ++/// } ++/// ++/// let differ = MockDiffer; ++/// let old_hash = Hash::from_bytes(&[0_u8; 64])?; ++/// let new_hash = Hash::from_bytes(&[1u8; 64])?; ++/// ++/// let delta = differ.diff_trees(&old_hash, &new_hash)?; ++/// assert!(delta.is_empty()); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait TreeDiffer: Send + Sync { ++ /// The identifier type for a tree. ++ /// ++ /// # Why this exists ++ /// Allows the differ implementation to define its own lookup mechanism. While ++ /// typically a [`Hash`], it could also be a database primary key or an ++ /// in-memory pointer, decoupling the diff logic from the object storage format. + type TreeId: Send + Sync; + ++ /// Computes the list of changes between two trees. ++ /// ++ /// # How it works ++ /// Resolves the `old` and `new` identifiers and performs a structural ++ /// comparison. The method returns a [`TreeDelta`] containing a list of ++ /// [`FileDelta`](crate::FileDelta)s. If a file exists in `new` but not `old`, ++ /// it is classified as `Added`; if it exists in `old` but not `new`, it is ++ /// `Deleted`. If the hashes differ but paths match, it is `Modified`. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if either tree cannot be loaded (e.g., ++ /// [`VctrlError::ObjectNotFound`]) or if the diffing process fails due to ++ /// corrupted data. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::diff::TreeDiffer; ++ /// # use libvctrl_handler::{TreeDelta, Hash, VctrlError}; ++ /// # ++ /// # struct MockDiffer; ++ /// # impl TreeDiffer for MockDiffer { ++ /// # type TreeId = Hash; ++ /// # fn diff_trees(&self, _old: &Self::TreeId, _new: &Self::TreeId) -> Result { ++ /// # Ok(TreeDelta::new()) ++ /// # } ++ /// # } ++ /// let differ = MockDiffer; ++ /// let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// ++ /// // Diffing a tree against itself should yield an empty delta. ++ /// let delta = differ.diff_trees(&hash, &hash)?; ++ /// assert_eq!(delta.len(), 0); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn diff_trees(&self, old: &Self::TreeId, new: &Self::TreeId) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/encoder.rs b/libvctrl_handler/src/traits/core/encoder.rs +index aa5641f..47e2fb4 100644 +--- a/libvctrl_handler/src/traits/core/encoder.rs ++++ b/libvctrl_handler/src/traits/core/encoder.rs +@@ -1,15 +1,228 @@ +-use std::io::Write; ++//! Object encoder trait. ++//! ++//! # Architecture ++//! This module defines the contract for serializing strongly-typed Git domain ++//! objects ([`Blob`], [`Tree`], [`Commit`], [`Tag`]) into raw byte streams. ++//! It acts as the bridge between the crate's type-safe in-memory representations ++//! and unstructured I/O data storage or network transmission. ++//! ++//! # Design Rationale: Streaming Serialization ++//! Instead of returning a `Vec` or `Box<[u8]>`, the encoder methods require a ++//! generic `W: Write` bound. This is a critical architectural decision: it forces ++//! streaming serialization. Git objects (especially blobs) can be massive. By writing ++//! directly to a stream, the encoder can process gigabytes of data with a fixed memory ++//! footprint, preventing out-of-memory (OOM) errors and avoiding the CPU overhead of ++//! allocating and resizing temporary heap buffers. + + use crate::errors::VctrlError; + use crate::types::{Blob, Commit, Tag, Tree}; ++use std::io::Write; + ++/// Trait for encoding structured Git objects into raw bytes. ++/// ++/// # Why this exists ++/// Abstracts the serialization logic away from the storage backend. Whether objects ++/// are being written to loose files on disk, compressed into a packfile, or streamed ++/// over a network socket, the encoding logic remains identical. This allows the crate ++/// to support multiple wire formats or compression algorithms by simply providing ++/// different implementations of this trait. ++/// ++/// # How it works ++/// The trait uses generic methods (``) rather than dynamic trait ++/// objects (`&mut dyn Write`). This design leverages Rust's monomorphization: the ++/// compiler generates a specific version of the encode function for every concrete ++/// writer type used at runtime. This eliminates dynamic dispatch overhead, allowing ++/// the compiler to aggressively inline the writing logic and optimize away function ++/// call boundaries. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync` on `Self`, and `Send` on the writer `W`. This ++/// ensures that encoding operations can be safely dispatched to a thread pool. For ++/// example, when writing a multi-object packfile, the engine can distribute object ++/// serialization across multiple worker threads to utilize multi-core parallelism ++/// without risking data races on the underlying writer or encoder state. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock streaming writer: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::encoder::Encoder; ++/// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++/// # use std::io::Write; ++/// # ++/// struct MockEncoder; ++/// ++/// impl Encoder for MockEncoder { ++/// fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { ++/// // Write the raw blob data directly to the stream ++/// writer.write_all(blob.data())?; ++/// Ok(()) ++/// } ++/// ++/// fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { ++/// // Mock implementation ++/// Ok(()) ++/// } ++/// ++/// fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { ++/// // Mock implementation ++/// Ok(()) ++/// } ++/// ++/// fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { ++/// // Mock implementation ++/// Ok(()) ++/// } ++/// } ++/// ++/// let encoder = MockEncoder; ++/// let blob = Blob::new(b"file content".to_vec())?; ++/// let mut buffer = Vec::new(); ++/// encoder.encode_blob(&blob, &mut buffer)?; ++/// assert_eq!(&buffer, b"file content"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Encoder: Send + Sync { ++ /// Encodes a blob object into a writer. ++ /// ++ /// # How it works ++ /// Writes the raw byte content of the [`Blob`] directly to the provided writer. ++ /// Because [`Blob`] enforces size limits during construction, this method does ++ /// not need to re-validate the payload size, allowing for a high-throughput, ++ /// direct memory-to-stream copy. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if encoding fails. This typically occurs if the underlying ++ /// writer experiences an I/O error (e.g., disk full, broken pipe). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::encoder::Encoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::Write; ++ /// # struct MockEncoder; ++ /// # impl Encoder for MockEncoder { ++ /// # fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { writer.write_all(blob.data())?; Ok(()) } ++ /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let encoder = MockEncoder; ++ /// let blob = Blob::new(b"binary data".to_vec())?; ++ /// let mut buffer = Vec::new(); ++ /// assert!(encoder.encode_blob(&blob, &mut buffer).is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError>; ++ ++ /// Encodes a tree object into a writer. ++ /// ++ /// # How it works ++ /// Serializes the tree entries into the canonical Git binary format. It writes the ++ /// mode bits (as octal ASCII), a null byte, the entry name, and the 64-byte SHA-512 ++ /// hash for each entry. Entries are guaranteed to be in Git-sorted order, as enforced ++ /// by the [`Tree`] constructor, ensuring the output is deterministic and canonical. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Read; ++ /// # use libvctrl_handler::traits::core::encoder::Encoder; ++ /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; ++ /// # use std::io::Write; ++ /// # struct MockEncoder; ++ /// # impl Encoder for MockEncoder { ++ /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let encoder = MockEncoder; ++ /// let tree = Tree::new(vec![])?; ++ /// let mut buffer = Vec::new(); ++ /// assert!(encoder.encode_tree(&tree, &mut buffer).is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError>; ++ ++ /// Encodes a commit object into a writer. ++ /// ++ /// # How it works ++ /// Formats the commit into the canonical Git text format. It writes tree references, ++ /// parent hashes, author/committer metadata (with timestamps and timezone offsets), ++ /// and the commit message. The formatting adheres strictly to Git specifications to ++ /// ensure interoperability with standard Git clients. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::encoder::Encoder; ++ /// # use libvctrl_handler::{Blob, Commit, CommitMeta, Hash, Tag, Tree, UserID, VctrlError}; ++ /// # use std::io::Write; ++ /// # struct MockEncoder; ++ /// # impl Encoder for MockEncoder { ++ /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// # let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; ++ /// let encoder = MockEncoder; ++ /// let commit = Commit::new(hash, vec![], user.clone(), user, "message".to_string())?; /// ++ /// let mut buffer = Vec::new(); ++ /// assert!(encoder.encode_commit(&commit, &mut buffer).is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn encode_commit( + &self, + commit: &Commit, + writer: &mut W, + ) -> Result<(), VctrlError>; ++ ++ /// Encodes a tag object into a writer. ++ /// ++ /// # How it works ++ /// Formats the annotated tag into the canonical Git text format. It writes the target ++ /// object hash, tagger identity, and tag message. As with [`encode_commit`](Self::encode_commit), ++ /// strict adherence to the Git specification ensures that the resulting tag is recognized ++ /// by standard Git tooling. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying writer fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::encoder::Encoder; ++ /// # use libvctrl_handler::{Blob, Commit, Hash, Tag, Tree, UserID, VctrlError}; ++ /// # use std::io::Write; ++ /// # struct MockEncoder; ++ /// # impl Encoder for MockEncoder { ++ /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// # let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; ++ /// let encoder = MockEncoder; ++ /// let tag = Tag::new("v1.0".to_string(), hash, Some(user), "release".to_string())?; ++ /// let mut buffer = Vec::new(); ++ /// assert!(encoder.encode_tag(&tag, &mut buffer).is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn encode_tag(&self, tag: &Tag, writer: &mut W) -> Result<(), VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/hasher.rs b/libvctrl_handler/src/traits/core/hasher.rs +index 69ea767..74ce3cd 100644 +--- a/libvctrl_handler/src/traits/core/hasher.rs ++++ b/libvctrl_handler/src/traits/core/hasher.rs +@@ -1,8 +1,109 @@ +-use std::io::Read; ++//! Hashing trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for computing cryptographic hashes. ++//! By abstracting the hashing mechanism into a trait, the crate decouples its ++//! content-addressing logic from the specific cryptographic algorithm (e.g., SHA-1, ++//! SHA-256, SHA-512). This allows consumers to swap algorithms or inject hardware-accelerated ++//! implementations without modifying the core object database logic. ++//! ++//! # Design Rationale: Streaming Cryptography ++//! The trait operates on `R: Read` rather than `&[u8]` or `Vec`. This is a critical ++//! architectural decision for performance and security. Git objects, particularly blobs, ++//! can be gigabytes in size. Loading an entire object into memory to hash it would cause ++//! severe memory fragmentation and potential out-of-memory (OOM) errors. By requiring a ++//! reader, the hasher processes data in fixed-size chunks, maintaining a constant memory ++//! footprint regardless of the input size. + + use crate::errors::VctrlError; + use crate::types::Hash; ++use std::io::Read; + ++/// Trait for computing hash values. ++/// ++/// # Why this exists ++/// In a content-addressable storage (CAS) system, the identifier of an object is derived ++/// from its content. This trait provides the contract for that derivation. Separating it ++/// from the encoder or storage backend allows for independent optimization and testing ++/// of the cryptographic pipeline. ++/// ++/// # How it works ++/// The trait uses a generic method (``) instead of a dynamic trait object ++/// (`&mut dyn Read`). This leverages Rust's monomorphization: the compiler generates a ++/// specialized version of the `hash` method for every concrete reader type used at runtime. ++/// This eliminates dynamic dispatch overhead, allowing the compiler to aggressively inline ++/// the read loops and buffering logic. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync` on `Self`, and `Send` on the reader `R`. Hashing is ++/// a CPU-bound, stateless operation (from the perspective of the hasher). By enforcing ++/// thread safety, the engine can safely distribute hashing tasks across a thread pool. ++/// For example, when writing a packfile, multiple objects can be hashed concurrently on ++/// different threads without requiring external synchronization. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock hasher that reads stream to completion: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::hasher::Hasher; ++/// # use libvctrl_handler::{Hash, VctrlError}; ++/// # use std::io::Read; ++/// # ++/// struct MockHasher; ++/// ++/// impl Hasher for MockHasher { ++/// fn hash(&self, mut reader: R) -> Result { ++/// // In a real implementation, this would update a cryptographic state ++/// // (e.g., SHA-512) and finalize it. Here, we just drain the reader. ++/// let mut buf = Vec::new(); ++/// reader.read_to_end(&mut buf)?; ++/// // Return a deterministic mock hash ++/// Hash::from_bytes(&[0_u8; 64]) ++/// } ++/// } ++/// ++/// let hasher = MockHasher; ++/// let data = std::io::Cursor::new(b"some data".to_vec()); ++/// let hash = hasher.hash(data)?; ++/// assert_eq!(hash.as_bytes(), &[0_u8; 64]); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Hasher: Send + Sync { ++ /// Returns the hash of the data read from the given reader. ++ /// ++ /// # How it works ++ /// Reads bytes from the provided reader in chunks until EOF is reached. As data is ++ /// read, it is fed into the underlying hashing algorithm's state machine. Once the ++ /// stream is exhausted, the final digest is computed and returned as a strongly-typed ++ /// [`Hash`]. This ensures that the hash is always the correct length (64 bytes for ++ /// SHA-512) as validated by [`Hash::from_bytes`]. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if hashing fails. This typically occurs if the underlying ++ /// reader experiences an I/O error (e.g., a broken pipe or disk read failure) during ++ /// the streaming process. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::hasher::Hasher; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::io::Read; ++ /// # struct MockHasher; ++ /// # impl Hasher for MockHasher { ++ /// # fn hash(&self, mut reader: R) -> Result { ++ /// # let mut buf = Vec::new(); ++ /// # reader.read_to_end(&mut buf)?; ++ /// # Hash::from_bytes(&[0_u8; 64]) ++ /// # } ++ /// # } ++ /// let hasher = MockHasher; ++ /// let stream = std::io::Cursor::new(b"hash this content".to_vec()); ++ /// let result = hasher.hash(stream); ++ /// assert!(result.is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn hash(&self, reader: R) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/index.rs b/libvctrl_handler/src/traits/core/index.rs +index de484a2..9adcba0 100644 +--- a/libvctrl_handler/src/traits/core/index.rs ++++ b/libvctrl_handler/src/traits/core/index.rs +@@ -1,20 +1,503 @@ ++//! Index (staging area) trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for managing the Git index, commonly ++//! known as the staging area. The index acts as the crucial intermediate state ++//! between the working directory and the object database, tracking planned changes ++//! for the next commit. ++//! ++//! # Design Rationale: Associated Types over Generics ++//! The trait uses associated types (`type Entry`, `type Path`, `type TreeId`) ++//! rather than generic parameters. This design ties the data representations ++//! directly to the specific `Index` implementation. An in-memory index might use ++//! `Rc` and `String`, while a disk-backed index might use `TreeEntry` ++//! and `PathBuf`. This prevents type mismatches at compile time and simplifies ++//! the API by removing the need for verbose generic annotations at every call site. ++ + use crate::errors::VctrlError; + ++/// A trait for managing a Git index (staging area). ++/// ++/// # Why this exists ++/// The staging area allows users to stage partial changes (hunks) before committing ++/// them to history. By abstracting this into a trait, the crate allows the core ++/// engine to orchestrate commits, diffs, and merges without being tied to a specific ++/// binary format (like the `.git/index` file) or an in-memory representation. ++/// ++/// # How it works ++/// The index maintains a mapping between file paths and their staged object entries. ++/// It supports adding, removing, and querying entries. The `write_tree` method ++/// serializes the current state into one or more tree objects in the object database, ++/// returning the root tree identifier. `read_tree` performs the inverse, populating ++/// the index from an existing tree. ++/// ++/// # Design Rationale: `&self` on `write_tree` ++/// Note that `write_tree` takes `&self` instead of `&mut self`. This is because ++/// writing a tree does not mutate the logical state of the index itself. The ++/// implementor is responsible for handling any necessary interior mutability ++/// (e.g., using `RefCell` or `Mutex`) when interacting with the underlying ++/// `ObjectStore` to persist the tree objects. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory store: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::index::Index; ++/// # use libvctrl_handler::VctrlError; ++/// # use std::collections::HashMap; ++/// # ++/// #[derive(Default)] ++/// struct MockIndex { ++/// data: HashMap, ++/// } ++/// ++/// impl Index for MockIndex { ++/// type Entry = String; ++/// type Path = String; ++/// type TreeId = u32; ++/// ++/// fn add(&mut self, entry: Self::Entry) -> Result<(), VctrlError> { ++/// self.data.insert(entry.clone(), entry); ++/// Ok(()) ++/// } ++/// ++/// fn remove(&mut self, path: &Self::Path) -> Result<(), VctrlError> { ++/// self.data.remove(path); ++/// Ok(()) ++/// } ++/// ++/// fn clear(&mut self) -> Result<(), VctrlError> { ++/// self.data.clear(); ++/// Ok(()) ++/// } ++/// ++/// fn get(&self, path: &Self::Path) -> Result, VctrlError> { ++/// Ok(self.data.get(path).cloned()) ++/// } ++/// ++/// fn contains(&self, path: &Self::Path) -> Result { ++/// Ok(self.data.contains_key(path)) ++/// } ++/// ++/// fn len(&self) -> Result { ++/// Ok(self.data.len()) ++/// } ++/// ++/// fn entries(&self) -> Result, VctrlError> { ++/// Ok(self.data.values().cloned().collect()) ++/// } ++/// ++/// fn write_tree(&self) -> Result { ++/// // In a real impl, this would write to an ObjectStore. ++/// Ok(1) ++/// } ++/// ++/// fn read_tree(&mut self, _tree: &Self::TreeId) -> Result<(), VctrlError> { ++/// // Mock implementation ++/// Ok(()) ++/// } ++/// } ++/// ++/// let mut index = MockIndex::default(); ++/// index.add("file.txt".to_string())?; ++/// assert_eq!(index.len()?, 1); ++/// assert!(index.contains(&"file.txt".to_string())?); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Index: Send + Sync { +- type Entry: Clone + Send + Sync; ++ /// The entry type used by the index. ++ /// ++ /// # Why this exists ++ /// Allows the backend to define its own representation of a staged file, which ++ /// might include mode bits, object hashes, and filesystem stat data (mtime, ctime) ++ /// for optimization. ++ type Entry: Send + Sync; ++ ++ /// The path type used by the index. ++ /// ++ /// # Why this exists ++ /// Decouples the path representation. While typically a `String` or `PathBuf`, ++ /// this allows backends to use interned strings or OS-specific paths. + type Path: Send + Sync; ++ ++ /// The tree identifier type. ++ /// ++ /// # Why this exists ++ /// Matches the identifier type used by the backend's `ObjectStore` or `TreeDiffer`, ++ /// ensuring seamless interoperability when writing or reading trees. + type TreeId: Send + Sync; + ++ /// Adds an entry to the index. ++ /// ++ /// # How it works ++ /// Inserts or updates the entry in the index. If an entry with the same path already ++ /// exists, it is overwritten. Requires `&mut self` as it mutates the logical state ++ /// of the staging area. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage fails to persist the update ++ /// or if the entry is invalid. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("new_file.txt".to_string())?; ++ /// assert_eq!(index.len()?, 1); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn add(&mut self, entry: Self::Entry) -> Result<(), VctrlError>; ++ ++ /// Removes an entry from the index by path. ++ /// ++ /// # How it works ++ /// Locates the entry by its path and removes it. If the path does not exist, ++ /// this operation is typically idempotent and returns `Ok(())`. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage fails to persist the deletion. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("file.txt".to_string())?; ++ /// index.remove(&"file.txt".to_string())?; ++ /// assert!(index.is_empty()?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn remove(&mut self, path: &Self::Path) -> Result<(), VctrlError>; ++ ++ /// Clears all entries from the index. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be cleared. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("a".to_string())?; ++ /// index.clear()?; ++ /// assert_eq!(index.len()?, 0); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn clear(&mut self) -> Result<(), VctrlError>; ++ ++ /// Retrieves an entry by path. ++ /// ++ /// # How it works ++ /// Performs a lookup. Returns `Ok(None)` if the path is not staged, maintaining ++ /// a clear distinction between "not staged" and "I/O error". ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("file.txt".to_string())?; ++ /// assert!(index.get(&"file.txt".to_string())?.is_some()); ++ /// assert!(index.get(&"missing.txt".to_string())?.is_none()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn get(&self, path: &Self::Path) -> Result, VctrlError>; ++ ++ /// Checks if an entry exists by path. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("file.txt".to_string())?; ++ /// assert!(index.contains(&"file.txt".to_string())?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn contains(&self, path: &Self::Path) -> Result; ++ ++ /// Returns the number of entries in the index. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("a".to_string())?; ++ /// index.add("b".to_string())?; ++ /// assert_eq!(index.len()?, 2); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn len(&self) -> Result; ++ ++ /// Returns `true` if the index is empty. ++ /// ++ /// # How it works ++ /// This is a provided method that default-implements by calling `len()`. It ++ /// exists to provide ergonomic, self-documenting code at call sites. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let index = MockIndex::default(); ++ /// assert!(index.is_empty()?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn is_empty(&self) -> Result { + Ok(self.len()? == 0) + } ++ ++ /// Returns all entries in the index. ++ /// ++ /// # How it works ++ /// Collects all staged entries into a `Vec`. This requires heap allocation. ++ /// Callers should prefer `get` or `contains` if they only need to query a ++ /// specific path, to avoid the overhead of collecting the entire index. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("a".to_string())?; ++ /// let entries = index.entries()?; ++ /// assert_eq!(entries.len(), 1); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn entries(&self) -> Result, VctrlError>; ++ ++ /// Writes the current index to a tree object and returns its identifier. ++ /// ++ /// # How it works ++ /// Traverses the staged entries, recursively building tree objects for directories. ++ /// It persists these trees to the `ObjectStore` (handled internally by the implementor) ++ /// and returns the hash (or ID) of the root tree. This is the final step before ++ /// creating a commit object. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the tree cannot be constructed or persisted, typically ++ /// due to I/O failures or invalid index states (e.g., unsorted entries). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(42) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.add("file.txt".to_string())?; ++ /// let tree_id = index.write_tree()?; ++ /// assert_eq!(tree_id, 42); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn write_tree(&self) -> Result; ++ ++ /// Reads a tree into the index. ++ /// ++ /// # How it works ++ /// Clears the current index state and populates it with the entries from the ++ /// specified tree object. This is commonly used during `checkout` or `reset` ++ /// operations to synchronize the staging area with a specific commit's state. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the tree cannot be found or if the index cannot be ++ /// mutated (e.g., I/O errors). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::index::Index; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockIndex { data: HashMap } ++ /// # impl Index for MockIndex { ++ /// # type Entry = String; type Path = String; type TreeId = u32; ++ /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } ++ /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } ++ /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } ++ /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } ++ /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } ++ /// # fn len(&self) -> Result { Ok(self.data.len()) } ++ /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } ++ /// # fn write_tree(&self) -> Result { Ok(1) } ++ /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut index = MockIndex::default(); ++ /// index.read_tree(&99)?; ++ /// assert!(index.is_empty()?); // Mock implementation does not populate ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn read_tree(&mut self, tree: &Self::TreeId) -> Result<(), VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/mod.rs b/libvctrl_handler/src/traits/core/mod.rs +index 4dad8b4..8b1a09a 100644 +--- a/libvctrl_handler/src/traits/core/mod.rs ++++ b/libvctrl_handler/src/traits/core/mod.rs +@@ -1,16 +1,340 @@ ++//! Core traits for repository operations. ++//! ++//! # Architecture ++//! This module defines the fundamental contracts required to build a functional ++//! version control backend. By segregating these traits into a dedicated `core` ++//! module, we establish a strict boundary between abstract domain logic and ++//! concrete I/O implementations. ++//! ++//! # Design Rationale: Dependency Inversion ++//! The entire crate operates against these traits, never against concrete types. ++//! This allows consumers to inject custom backends (in-memory, disk-based, or ++//! network-attached) seamlessly. It also simplifies unit testing, as mock ++//! implementations can be substituted without altering the core algorithms. ++//! ++//! # Bounded Contexts ++//! Each submodule represents a distinct bounded context within the Git architecture: ++//! - **Storage**: [`object_store`], [`pack`] ++//! - **State**: [`ref_store`], [`reflog`], [`index`] ++//! - **Serialization**: [`encoder`], [`decoder`], [`hasher`] ++//! - **Analysis**: [`diff`], [`blame`], [`revwalk`] ++//! - **Security**: [`signer`], [`verifier`] ++//! - **Networking**: [`remote`], [`transport`] ++//! - **Configuration**: [`config`] ++//! ++//! # Examples ++//! *Note: The following example assumes this crate is named `libvctrl_handler`.* ++//! ++//! ``` ++//! # use libvctrl_handler::traits::core::{ ++//! # blame, config, decoder, diff, encoder, hasher, index, object_store, ++//! # pack, ref_store, reflog, remote, revwalk, signer, transport, verifier, ++//! # }; ++//! // All core trait modules are publicly accessible. ++//! ``` ++ ++/// Blame computation trait. ++/// ++/// # Why this exists ++/// Provides the contract for attributing lines in a file to specific commits. ++/// This is separated from standard diffing because blame requires traversing ++/// history and tracking line movements across revisions, which is computationally ++/// distinct from simple tree-to-tree comparisons. ++/// ++/// # How it works ++/// Implementors will analyze the history of a given path and return a sequence ++/// of [`BlameEntry`](blame::BlameEntry) items, mapping line ranges to commits. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::blame; ++/// // The blame submodule is accessible. ++/// ``` + pub mod blame; ++ ++/// Configuration store trait. ++/// ++/// # Why this exists ++/// Abstracts the reading and writing of repository configuration (e.g., `.git/config`). ++/// Decoupling this allows the core engine to query settings (like user name or ++/// signing keys) without being tied to a specific file format or key-value backend. ++/// ++/// # How it works ++/// Defines a key-value interface segmented by sections, enabling persistent ++/// configuration management across different storage mediums. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::config; ++/// // The config submodule is accessible. ++/// ``` + pub mod config; ++ ++/// Object decoder trait. ++/// ++/// # Why this exists ++/// Defines the contract for deserializing raw bytes into strongly-typed Git objects ++/// (e.g., [`Blob`](crate::Blob), [`Tree`](crate::Tree)). This abstraction allows ++/// the engine to support multiple wire formats or compression algorithms. ++/// ++/// # How it works ++/// Implementors read from a generic `std::io::Read` source, parse the headers ++/// and payloads, and construct the corresponding domain types, enforcing structural ++/// validity during the process. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::decoder; ++/// // The decoder submodule is accessible. ++/// ``` + pub mod decoder; ++ ++/// Tree differencing trait. ++/// ++/// # Why this exists ++/// Provides the contract for computing the delta between two tree objects. ++/// Separating this logic allows for different diffing algorithms (e.g., Myers, ++/// patience) to be plugged in without modifying the core comparison logic. ++/// ++/// # How it works ++/// Accepts two tree identifiers and returns a [`TreeDelta`](crate::TreeDelta), ++/// enumerating all added, deleted, or modified entries between the two states. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::diff; ++/// // The diff submodule is accessible. ++/// ``` + pub mod diff; ++ ++/// Object encoder trait. ++/// ++/// # Why this exists ++/// Defines the contract for serializing strongly-typed Git objects into raw bytes. ++/// This is the inverse of the [`decoder`] module, ensuring that objects can be ++/// written to disk or transmitted over the network in a standardized format. ++/// ++/// # How it works ++/// Implementors write the canonical Git representation of the object to a generic ++/// `std::io::Write` destination, handling headers and payload formatting. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::encoder; ++/// // The encoder submodule is accessible. ++/// ``` + pub mod encoder; ++ ++/// Hashing trait. ++/// ++/// # Why this exists ++/// Abstracts the cryptographic hashing mechanism. While Git traditionally uses ++/// SHA-1 or SHA-256, this trait allows the engine to support arbitrary hash ++/// functions or custom hashing contexts. ++/// ++/// # How it works ++/// Reads data from a generic `std::io::Read` source and computes the final ++/// [`Hash`](crate::Hash) digest, ensuring that the object's content matches its ++/// identifier. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::hasher; ++/// // The hasher submodule is accessible. ++/// ``` + pub mod hasher; ++ ++/// Index (staging area) trait. ++/// ++/// # Why this exists ++/// Defines the contract for managing the staging area between the working directory ++/// and the object database. This abstraction is crucial for orchestrating commits ++/// and tracking file states. ++/// ++/// # How it works ++/// Provides methods to add, remove, and query entries by path, and to serialize ++/// the staged state into a tree object ready for committing. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::index; ++/// // The index submodule is accessible. ++/// ``` + pub mod index; ++ ++/// Object storage trait. ++/// ++/// # Why this exists ++/// Provides the fundamental contract for storing and retrieving content-addressed ++/// objects. This is the backbone of the version control system, allowing backends ++/// to use plain directories, packed files, or databases. ++/// ++/// # How it works ++/// Defines `put`, `get`, `delete`, and `exists` operations keyed by [`Hash`](crate::Hash), ++/// ensuring that object retrieval is opaque to the caller. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::object_store; ++/// // The object_store submodule is accessible. ++/// ``` + pub mod object_store; ++ ++/// Pack file reader/writer traits. ++/// ++/// # Why this exists ++/// Packfiles are Git's compressed archive format for objects. This module defines ++/// contracts for both writing and reading packfiles, isolating the complex ++/// delta-compression and indexing logic from the standard object store. ++/// ++/// # How it works ++/// The writer trait handles object insertion and finalization, while the reader ++/// trait provides random access to objects within the pack via their identifiers. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::pack; ++/// // The pack submodule is accessible. ++/// ``` + pub mod pack; ++ ++/// Reference store trait. ++/// ++/// # Why this exists ++/// Abstracts the management of symbolic references (branches, tags, HEAD). ++/// Decoupling this allows the engine to manage mutable state independently of ++/// the immutable object database. ++/// ++/// # How it works ++/// Defines operations to set, get, delete, and list references, mapping human-readable ++/// names to [`Hash`](crate::Hash) values. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::ref_store; ++/// // The ref_store submodule is accessible. ++/// ``` + pub mod ref_store; ++ ++/// Reflog store trait. ++/// ++/// # Why this exists ++/// Provides the contract for recording the history of reference updates. ++/// Reflogs are essential for recovering from mistakes and tracking branch movement. ++/// ++/// # How it works ++/// Appends timestamped entries to a reference's log and retrieves them, ensuring ++/// that the chronological history of repository mutations is preserved. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::reflog; ++/// // The reflog submodule is accessible. ++/// ``` + pub mod reflog; ++ ++/// Remote repository trait. ++/// ++/// # Why this exists ++/// Defines the contract for interacting with remote repositories. ++/// This abstraction normalizes operations like fetching and pushing across ++/// different protocols (e.g., HTTP, SSH, Git). ++/// ++/// # How it works ++/// Manages refspecs and remote references, coordinating the transfer of objects ++/// and updates between local and remote states. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::remote; ++/// // The remote submodule is accessible. ++/// ``` + pub mod remote; ++ ++/// Revision walking trait. ++/// ++/// # Why this exists ++/// Provides the contract for traversing the commit graph. ++/// Walking history is a fundamental operation for log generation, bisecting, ++/// and ancestry queries. ++/// ++/// # How it works ++/// Returns a lazy iterator over commit identifiers starting from a given point, ++/// allowing efficient traversal without loading the entire graph into memory. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::revwalk; ++/// // The revwalk submodule is accessible. ++/// ``` + pub mod revwalk; ++ ++/// Signing trait. ++/// ++/// # Why this exists ++/// Abstracts the cryptographic signing of data (e.g., commits or tags). ++/// This allows the engine to support various signing backends (GPG, SSH, X.509) ++/// without hardcoding the cryptographic primitives. ++/// ++/// # How it works ++/// Accepts a key identifier and raw data, returning a cryptographic signature ++/// that can be appended to the object. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::signer; ++/// // The signer submodule is accessible. ++/// ``` + pub mod signer; ++ ++/// Transport trait. ++/// ++/// # Why this exists ++/// Defines the low-level contract for sending and receiving raw Git objects ++/// over a network. This is distinct from the [`remote`] module, which handles ++/// higher-level repository semantics. ++/// ++/// # How it works ++/// Provides simple fetch and push primitives based on object hashes, acting as ++/// the pipe between local and remote object stores. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::transport; ++/// // The transport submodule is accessible. ++/// ``` + pub mod transport; ++ ++/// Verification trait. ++/// ++/// # Why this exists ++/// Abstracts the verification of cryptographic signatures. It is the counterpart ++/// to the [`signer`] module, ensuring that objects can be authenticated against ++/// trusted keys. ++/// ++/// # How it works ++/// Accepts a key identifier, raw data, and a signature, returning a boolean ++/// indicating the validity of the signature. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::verifier; ++/// // The verifier submodule is accessible. ++/// ``` + pub mod verifier; +diff --git a/libvctrl_handler/src/traits/core/object_store.rs b/libvctrl_handler/src/traits/core/object_store.rs +index 166c3fc..f11beb8 100644 +--- a/libvctrl_handler/src/traits/core/object_store.rs ++++ b/libvctrl_handler/src/traits/core/object_store.rs +@@ -1,11 +1,243 @@ +-use std::io::Read; ++//! Object storage trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for a Content-Addressable Storage (CAS) ++//! backend. In a CAS system, the identifier of an object is derived directly from its ++//! content (typically via a cryptographic hash). This trait abstracts the underlying ++//! storage mechanism, allowing the engine to use loose files on disk, packed objects, ++//! or entirely in-memory representations. ++//! ++//! # Design Rationale: Streaming I/O ++//! The `get` method returns a `Box` rather than a `Vec` or `&[u8]`. ++//! This is a critical architectural decision for performance and memory safety. Git ++//! objects, particularly blobs, can be gigabytes in size. Loading an entire object ++//! into memory could cause severe memory fragmentation and potential out-of-memory ++//! (OOM) errors. By returning a reader, the storage backend allows the caller to ++//! stream the data in fixed-size chunks, maintaining a constant memory footprint ++//! regardless of the object's size. + + use crate::errors::VctrlError; + use crate::types::Hash; ++use std::io::Read; + ++/// A trait for storing and retrieving Git objects. ++/// ++/// # Why this exists ++/// Provides the fundamental contract for interacting with the Git object database. ++/// By using a trait, the crate decouples the core VCS logic from the specific I/O ++/// backend. This allows consumers to inject custom backends (e.g., S3 storage, ++/// encrypted databases, or mock memory stores for testing) without altering the ++/// core algorithms. ++/// ++/// # How it works ++/// The store maps [`Hash`] keys to raw byte payloads. Write operations (`put`, ++/// `delete`) require `&mut self`, enforcing exclusive access to prevent data races ++/// during mutations. Read operations (`get`, `exists`) take `&self`, allowing ++/// highly concurrent parallel reads across multiple threads. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync`. Object storage is frequently accessed by ++/// multiple concurrent operations (e.g., packing objects, resolving diffs, checking ++/// out files). The `Send + Sync` bound guarantees that the implementor is thread-safe, ++/// enabling the engine to parallelize object retrieval without external synchronization. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory store: ++/// ++/// ``` ++/// # use std::io::Read; ++/// # use libvctrl_handler::traits::core::object_store::ObjectStore; ++/// # use libvctrl_handler::{Hash, VctrlError}; ++/// # use std::collections::HashMap; ++/// # use std::io::Cursor; ++/// # ++/// #[derive(Default)] ++/// struct MockStore { ++/// data: HashMap>, ++/// } ++/// ++/// impl ObjectStore for MockStore { ++/// fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { ++/// self.data.insert(*hash, data.to_vec()); ++/// Ok(()) ++/// } ++/// ++/// fn get(&self, hash: &Hash) -> Result, VctrlError> { ++/// match self.data.get(hash) { ++/// Some(data) => Ok(Box::new(Cursor::new(data.clone()))), ++/// None => Err(VctrlError::ObjectNotFound(*hash)), ++/// } ++/// } ++/// ++/// fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError> { ++/// self.data.remove(hash); ++/// Ok(()) ++/// } ++/// ++/// fn exists(&self, hash: &Hash) -> Result { ++/// Ok(self.data.contains_key(hash)) ++/// } ++/// } ++/// ++/// let mut store = MockStore::default(); ++/// let hash = Hash::from_bytes(&[0_u8; 64])?; ++/// store.put(&hash, b"blob content")?; ++/// assert!(store.exists(&hash)?); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait ObjectStore: Send + Sync { ++ /// Stores an object under the given hash. ++ /// ++ /// # How it works ++ /// Accepts a reference to the [`Hash`] and a byte slice of the object's raw, ++ /// uncompressed content. The implementor is responsible for persisting this ++ /// data (e.g., writing to disk, compressing into a packfile, or inserting ++ /// into a database). Requires `&mut self` as it mutates the underlying storage. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage fails (e.g., disk full, ++ /// permission denied) or if the data violates storage constraints. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Read; ++ /// # use libvctrl_handler::traits::core::object_store::ObjectStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::Cursor; ++ /// # #[derive(Default)] ++ /// # struct MockStore { data: HashMap> } ++ /// # impl ObjectStore for MockStore { ++ /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } ++ /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } ++ /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } ++ /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } ++ /// # } ++ /// let mut store = MockStore::default(); ++ /// let hash = Hash::from_bytes(&[1u8; 64])?; ++ /// store.put(&hash, b"new data")?; ++ /// assert!(store.exists(&hash)?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; ++ ++ /// Retrieves an object by hash, returning a reader. ++ /// ++ /// # How it works ++ /// Looks up the object by its [`Hash`] and returns a boxed reader. The reader ++ /// abstracts the underlying storage medium (file handle, network socket, or ++ /// memory cursor). The lifetime `'_` ties the returned reader to the lifetime ++ /// of the `ObjectStore` instance, ensuring the underlying storage remains valid ++ /// while the stream is active. This prevents loading large objects into memory ++ /// all at once. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::ObjectNotFound`] if the hash does not exist in the store. ++ /// Returns [`VctrlError`] if an I/O error occurs while initializing the stream. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::object_store::ObjectStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::{Cursor, Read}; ++ /// # #[derive(Default)] ++ /// # struct MockStore { data: HashMap> } ++ /// # impl ObjectStore for MockStore { ++ /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } ++ /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } ++ /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } ++ /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } ++ /// # } ++ /// let mut store = MockStore::default(); ++ /// let hash = Hash::from_bytes(&[2u8; 64])?; ++ /// store.put(&hash, b"readable data")?; ++ /// ++ /// let mut reader = store.get(&hash)?; ++ /// let mut content = String::new(); ++ /// reader.read_to_string(&mut content)?; ++ /// assert_eq!(content, "readable data"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn get(&self, hash: &Hash) -> Result, VctrlError>; ++ ++ /// Deletes an object by hash. ++ /// ++ /// # How it works ++ /// Locates the object by its [`Hash`] and removes it from the underlying storage. ++ /// If the object does not exist, this operation is typically idempotent and ++ /// returns `Ok(())`, preventing spurious errors during garbage collection. ++ /// Requires `&mut self` to enforce exclusive access during mutation. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be modified (e.g., ++ /// file permission issues or read-only filesystem). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Read; ++ /// # use libvctrl_handler::traits::core::object_store::ObjectStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::Cursor; ++ /// # #[derive(Default)] ++ /// # struct MockStore { data: HashMap> } ++ /// # impl ObjectStore for MockStore { ++ /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } ++ /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } ++ /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } ++ /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } ++ /// # } ++ /// let mut store = MockStore::default(); ++ /// let hash = Hash::from_bytes(&[3u8; 64])?; ++ /// store.put(&hash, b"to be deleted")?; ++ /// store.delete(&hash)?; ++ /// assert!(!store.exists(&hash)?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError>; ++ ++ /// Checks whether an object exists. ++ /// ++ /// # How it works ++ /// Performs a lightweight existence check without retrieving the object's data ++ /// or initializing a stream. This is significantly faster than calling `get` ++ /// and checking for `ObjectNotFound`, especially on network-backed storage. ++ /// Takes `&self` to allow concurrent existence checks. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be queried (e.g., ++ /// an I/O error while listing directory contents). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Read; ++ /// # use libvctrl_handler::traits::core::object_store::ObjectStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::Cursor; ++ /// # #[derive(Default)] ++ /// # struct MockStore { data: HashMap> } ++ /// # impl ObjectStore for MockStore { ++ /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } ++ /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } ++ /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } ++ /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } ++ /// # } ++ /// let store = MockStore::default(); ++ /// let hash = Hash::from_bytes(&[4u8; 64])?; ++ /// // Check a missing object ++ /// assert!(!store.exists(&hash)?); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn exists(&self, hash: &Hash) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/pack.rs b/libvctrl_handler/src/traits/core/pack.rs +index c94d7c0..3a39535 100644 +--- a/libvctrl_handler/src/traits/core/pack.rs ++++ b/libvctrl_handler/src/traits/core/pack.rs +@@ -1,16 +1,231 @@ +-use std::io::Read; ++//! Pack file reader/writer traits. ++//! ++//! # Architecture ++//! Packfiles are Git's highly compressed archive format for storing multiple objects. ++//! This module defines the contracts for both writing and reading packfiles, isolating ++//! the complex delta-compression and indexing logic from the standard object store. ++//! ++//! # Design Rationale: Streaming I/O ++//! Packfiles can contain thousands of objects and span gigabytes. The reader trait ++//! returns a `Box` rather than a `Vec`. This is a critical architectural ++//! decision: it forces streaming deserialization. It allows the engine to resolve ++//! deltas and decompress zlib streams on the fly, maintaining a constant memory ++//! footprint regardless of the packfile's total size. + + use crate::errors::VctrlError; ++use std::io::Read; + ++/// Trait for writing Git pack files. ++/// ++/// # Why this exists ++/// Provides the contract for building a packfile. Packfiles are essential for ++/// network transfers and repository garbage collection, as they compress objects ++/// using delta encoding to save space. Abstracting this into a trait allows the ++/// crate to support different compression levels or custom delta algorithms. ++/// ++/// # How it works ++/// The writer maintains internal state, tracking the offsets of each written object ++/// to build a final index. As objects are written via `write_object`, the implementor ++/// compresses the data and appends it to the underlying stream. The `finish` method ++/// is required to flush any remaining buffers, write the packfile trailer, and ++/// finalize the corresponding index file. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory writer: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::pack::PackWriter; ++/// # use libvctrl_handler::VctrlError; ++/// # use std::collections::HashMap; ++/// # ++/// struct MockPackWriter { ++/// objects: HashMap, Vec>, ++/// } ++/// ++/// impl PackWriter for MockPackWriter { ++/// type ObjectId = Vec; ++/// ++/// fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { ++/// self.objects.insert(id.clone(), data.to_vec()); ++/// Ok(()) ++/// } ++/// ++/// fn finish(&mut self) -> Result<(), VctrlError> { ++/// // In a real impl, this would write the checksum and flush the stream. ++/// Ok(()) ++/// } ++/// } ++/// ++/// let mut writer = MockPackWriter { objects: HashMap::new() }; ++/// writer.write_object(&vec![1, 2, 3], b"blob data")?; ++/// writer.finish()?; ++/// assert_eq!(writer.objects.len(), 1); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait PackWriter: Send + Sync { ++ /// The object identifier type. ++ /// ++ /// # Why this exists ++ /// Allows the writer backend to define its own representation of an object hash, ++ /// ensuring compatibility with the associated `ObjectStore` implementation. + type ObjectId: Send + Sync; + ++ /// Writes an object to the pack. ++ /// ++ /// # How it works ++ /// Accepts an identifier and the raw, uncompressed byte slice of the object. ++ /// The implementor is responsible for compressing the data (e.g., using zlib), ++ /// calculating offsets, and potentially encoding the object as a delta against ++ /// a previously written base object. Requires `&mut self` because writing ++ /// mutates the packfile's internal offset tracker and compression state. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if an I/O error occurs during writing or if the ++ /// compression algorithm fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::pack::PackWriter; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # struct MockPackWriter { objects: HashMap, Vec> } ++ /// # impl PackWriter for MockPackWriter { ++ /// # type ObjectId = Vec; ++ /// # fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { ++ /// # self.objects.insert(id.clone(), data.to_vec()); Ok(()) ++ /// # } ++ /// # fn finish(&mut self) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut writer = MockPackWriter { objects: HashMap::new() }; ++ /// writer.write_object(&vec![0_u8; 20], b"data")?; ++ /// assert!(writer.objects.contains_key(&vec![0_u8; 20])); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError>; ++ ++ /// Finishes writing the pack file. ++ /// ++ /// # How it works ++ /// This method must be called exactly once after all objects have been written. ++ /// It flushes any remaining data in the compression buffers, writes the 20-byte ++ /// SHA-1 trailer for the packfile, and finalizes the index. Failing to call this ++ /// method will result in a corrupted, unreadable packfile. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying stream cannot be flushed or if the ++ /// final checksum calculation fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::pack::PackWriter; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # struct MockPackWriter { objects: HashMap, Vec> } ++ /// # impl PackWriter for MockPackWriter { ++ /// # type ObjectId = Vec; ++ /// # fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { ++ /// # self.objects.insert(id.clone(), data.to_vec()); Ok(()) ++ /// # } ++ /// # fn finish(&mut self) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut writer = MockPackWriter { objects: HashMap::new() }; ++ /// assert!(writer.finish().is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn finish(&mut self) -> Result<(), VctrlError>; + } + ++/// Trait for reading Git pack files. ++/// ++/// # Why this exists ++/// Provides the contract for random access reading of objects within a packfile. ++/// By abstracting this, the crate allows backends to use memory-mapped files, ++/// direct file I/O, or entirely in-memory representations for testing. ++/// ++/// # Design Rationale: `&self` and Thread Safety ++/// The trait requires `&self` for `read_object` (not `&mut self`). This is crucial ++/// for concurrency. Packfiles are immutable once written. By taking an immutable ++/// reference, multiple threads can safely read different objects from the same ++/// packfile concurrently without requiring external locking. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory reader: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::pack::PackReader; ++/// # use libvctrl_handler::VctrlError; ++/// # use std::collections::HashMap; ++/// # use std::io::{Cursor, Read}; ++/// # ++/// struct MockPackReader { ++/// objects: HashMap, Vec>, ++/// } ++/// ++/// impl PackReader for MockPackReader { ++/// type ObjectId = Vec; ++/// ++/// fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError> { ++/// let data = self.objects.get(id).cloned().unwrap_or_default(); ++/// Ok(Box::new(Cursor::new(data))) ++/// } ++/// } ++/// ++/// let reader = MockPackReader { objects: HashMap::from([(vec![1], b"data".to_vec())]) }; ++/// let mut r = reader.read_object(&vec![1])?; ++/// let mut buf = String::new(); ++/// r.read_to_string(&mut buf)?; ++/// assert_eq!(buf, "data"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait PackReader: Send + Sync { ++ /// The object identifier type. ++ /// ++ /// # Why this exists ++ /// Matches the identifier type used by the corresponding `PackWriter` and ++ /// `ObjectStore`, ensuring type-safe lookups across the storage layer. + type ObjectId: Send + Sync; + ++ /// Reads an object from the pack, returning a reader. ++ /// ++ /// # How it works ++ /// Looks up the object's offset in the packfile index, seeks to that position, ++ /// and returns a boxed reader. The returned reader handles zlib decompression ++ /// and, if the object is stored as a delta, resolves the delta against its base ++ /// object lazily as bytes are read. The lifetime `'_` ties the returned reader ++ /// to the lifetime of the `PackReader` instance, ensuring the underlying file ++ /// handle or memory mapping remains valid. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the object is not found in the pack, if the ++ /// data is corrupted, or if an I/O error occurs while seeking or reading. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::pack::PackReader; ++ /// # use libvctrl_handler::VctrlError; ++ /// # use std::collections::HashMap; ++ /// # use std::io::{Cursor, Read}; ++ /// # struct MockPackReader { objects: HashMap, Vec> } ++ /// # impl PackReader for MockPackReader { ++ /// # type ObjectId = Vec; ++ /// # fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError> { ++ /// # let data = self.objects.get(id).cloned().unwrap_or_default(); ++ /// # Ok(Box::new(Cursor::new(data))) ++ /// # } ++ /// # } ++ /// let reader = MockPackReader { objects: HashMap::new() }; ++ /// let result = reader.read_object(&vec![1, 2, 3]); ++ /// // Mock returns empty cursor for missing keys, but real impls return ObjectNotFound. ++ /// assert!(result.is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/ref_store.rs b/libvctrl_handler/src/traits/core/ref_store.rs +index c77c603..fe685f9 100644 +--- a/libvctrl_handler/src/traits/core/ref_store.rs ++++ b/libvctrl_handler/src/traits/core/ref_store.rs +@@ -1,11 +1,251 @@ ++//! Reference store trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for managing Git references (branches, ++//! tags, HEAD). In Git's architecture, the object database is strictly immutable, ++//! while references provide the mutable pointers that track the current state of ++//! branches and tags. By isolating reference management into a dedicated trait, ++//! the crate decouples state mutations from content storage. ++//! ++//! # Design Rationale: Lazy Iteration ++//! The [`RefStore::list_refs`] method returns a custom associated iterator type ++//! (`type RefsIterator`) rather than a `Vec`. This is a critical architectural ++//! decision for scalability. Repositories like the Linux kernel contain millions of ++//! references. Returning a `Vec` would require loading all names into memory ++//! simultaneously, risking out-of-memory (OOM) errors. By returning an iterator, ++//! backends can stream reference names lazily from disk or a database cursor, ++//! maintaining a constant memory footprint. ++ + use crate::errors::VctrlError; + use crate::types::Hash; + ++/// A trait for managing Git references (branches, tags, etc.). ++/// ++/// # Why this exists ++/// Provides a unified, type-safe interface for mutating and querying repository ++/// state. Git references map human-readable names (e.g., `refs/heads/main`) to ++/// cryptographic hashes. This trait enforces that structure, allowing the core ++/// engine to orchestrate branch updates, tag creation, and HEAD detachments ++/// without being tied to a specific filesystem layout or database backend. ++/// ++/// # How it works ++/// The store maintains a mapping between reference names and [`Hash`] values. ++/// Write operations (`set_ref`, `delete_ref`) require `&mut self`, enforcing ++/// exclusive access at the Rust type level. This mimics Git's `.lock` files, ++/// preventing race conditions where two concurrent processes try to update the ++/// same branch. Read operations (`get_ref`, `list_refs`) take `&self`, allowing ++/// highly concurrent parallel reads across multiple threads. ++/// ++/// # Design Rationale: Thread Safety ++/// The trait requires `Send + Sync`. Reference resolution is one of the most ++/// frequent operations in Git (e.g., during revision walks or merge analysis). ++/// By enforcing thread safety, the engine can parallelize operations that ++/// require resolving multiple refs without requiring external locking mechanisms. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory store: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::ref_store::RefStore; ++/// # use libvctrl_handler::{Hash, VctrlError}; ++/// # use std::collections::HashMap; ++/// # ++/// #[derive(Default)] ++/// struct MockRefStore { ++/// refs: HashMap, ++/// } ++/// ++/// impl RefStore for MockRefStore { ++/// type RefsIterator = std::vec::IntoIter>; ++/// ++/// fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError> { ++/// self.refs.insert(name.to_string(), *hash); ++/// Ok(()) ++/// } ++/// ++/// fn get_ref(&self, name: &str) -> Result { ++/// self.refs ++/// .get(name) ++/// .copied() ++/// .ok_or_else(|| VctrlError::RefNotFound(name.to_string())) ++/// } ++/// ++/// fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError> { ++/// self.refs.remove(name); ++/// Ok(()) ++/// } ++/// ++/// fn list_refs(&self) -> Result { ++/// let refs: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); ++/// Ok(refs.into_iter()) ++/// } ++/// } ++/// ++/// let mut store = MockRefStore::default(); ++/// let hash = Hash::from_bytes(&[0_u8; 64])?; ++/// store.set_ref("refs/heads/main", &hash)?; ++/// assert_eq!(store.get_ref("refs/heads/main")?, hash); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait RefStore: Send + Sync { ++ /// An iterator over reference names. ++ /// ++ /// # Why this exists ++ /// Allows the backend to define its own iteration mechanism. A filesystem backend ++ /// might yield names lazily via directory traversal, while a database backend ++ /// might use a cursor. The iterator yields `Result` to gracefully ++ /// handle I/O errors that may occur mid-iteration (e.g., a permissions error on a ++ /// specific file). The `Send` bound allows the iterator to be moved across threads. + type RefsIterator: Iterator> + Send; + ++ /// Sets a reference to the given hash. ++ /// ++ /// # How it works ++ /// Inserts or updates the mapping of `name` to `hash`. If a reference with the ++ /// given name already exists, it is overwritten. Requires `&mut self` to enforce ++ /// exclusive access, preventing data races during concurrent branch updates. ++ /// Implementors should ensure this operation is atomic to prevent repository ++ /// corruption if the process is interrupted. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage fails to persist the update ++ /// (e.g., disk full, permission denied) or if the name is invalid. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::ref_store::RefStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockRefStore { refs: HashMap } ++ /// # impl RefStore for MockRefStore { ++ /// # type RefsIterator = std::vec::IntoIter>; ++ /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } ++ /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } ++ /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } ++ /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } ++ /// # } ++ /// let mut store = MockRefStore::default(); ++ /// let hash = Hash::from_bytes(&[1u8; 64])?; ++ /// store.set_ref("refs/heads/feature", &hash)?; ++ /// assert!(store.get_ref("refs/heads/feature").is_ok()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError>; ++ ++ /// Gets the hash pointed to by a reference. ++ /// ++ /// # How it works ++ /// Looks up the reference by name and returns the corresponding [`Hash`]. Takes ++ /// `&self` to allow concurrent reads. If the reference does not exist, it returns ++ /// an error rather than an `Option`, as a missing reference is typically an ++ /// exceptional condition in Git operations (e.g., trying to checkout a non-existent ++ /// branch). ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::RefNotFound`] if the reference name does not exist in the store. ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::ref_store::RefStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockRefStore { refs: HashMap } ++ /// # impl RefStore for MockRefStore { ++ /// # type RefsIterator = std::vec::IntoIter>; ++ /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } ++ /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } ++ /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } ++ /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } ++ /// # } ++ /// let mut store = MockRefStore::default(); ++ /// let hash = Hash::from_bytes(&[2u8; 64])?; ++ /// store.set_ref("HEAD", &hash)?; ++ /// assert_eq!(store.get_ref("HEAD")?, hash); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn get_ref(&self, name: &str) -> Result; ++ ++ /// Deletes a reference. ++ /// ++ /// # How it works ++ /// Removes the mapping for the given `name`. If the reference does not exist, ++ /// this operation is typically idempotent and returns `Ok(())`, preventing ++ /// spurious errors during cleanup operations. Requires `&mut self` to enforce ++ /// exclusive access. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be modified. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::ref_store::RefStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockRefStore { refs: HashMap } ++ /// # impl RefStore for MockRefStore { ++ /// # type RefsIterator = std::vec::IntoIter>; ++ /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } ++ /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } ++ /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } ++ /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } ++ /// # } ++ /// let mut store = MockRefStore::default(); ++ /// let hash = Hash::from_bytes(&[3u8; 64])?; ++ /// store.set_ref("refs/tags/v1", &hash)?; ++ /// store.delete_ref("refs/tags/v1")?; ++ /// assert!(store.get_ref("refs/tags/v1").is_err()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError>; ++ ++ /// Lists all reference names. ++ /// ++ /// # How it works ++ /// Returns a custom iterator ([`RefsIterator`](Self::RefsIterator)) that yields ++ /// reference names. The iterator allows the backend to lazily load references, ++ /// preventing memory exhaustion in repositories with a massive number of refs. ++ /// Takes `&self` to allow concurrent listing. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the iterator cannot be initialized (e.g., an I/O ++ /// error while opening the refs directory). Note that I/O errors occurring ++ /// *during* iteration are yielded by the iterator itself as `Err(VctrlError)`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::ref_store::RefStore; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockRefStore { refs: HashMap } ++ /// # impl RefStore for MockRefStore { ++ /// # type RefsIterator = std::vec::IntoIter>; ++ /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } ++ /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } ++ /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } ++ /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } ++ /// # } ++ /// let mut store = MockRefStore::default(); ++ /// let hash = Hash::from_bytes(&[4u8; 64])?; ++ /// store.set_ref("refs/heads/main", &hash)?; ++ /// store.set_ref("refs/heads/dev", &hash)?; ++ /// ++ /// let refs: Vec = store.list_refs()?.filter_map(|r| r.ok()).collect(); ++ /// assert_eq!(refs.len(), 2); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn list_refs(&self) -> Result; + } +diff --git a/libvctrl_handler/src/traits/core/reflog.rs b/libvctrl_handler/src/traits/core/reflog.rs +index 76d8e37..b9d945a 100644 +--- a/libvctrl_handler/src/traits/core/reflog.rs ++++ b/libvctrl_handler/src/traits/core/reflog.rs +@@ -1,9 +1,134 @@ ++//! Reflog store trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for managing reference logs (reflogs). ++//! Reflogs act as an append-only audit trail, recording every mutation to a reference ++//! (e.g., commits, resets, checkouts). This history is crucial for recovering from ++//! accidental operations and for garbage collection pruning. ++//! ++//! # Design Rationale: Strict Append-Only Semantics ++//! The trait exposes only `append` and `entries` methods. There is no `delete` or ++//! `update` operation for individual entries. This enforces the append-only nature ++//! of reflogs at the type level, preventing consumers from accidentally rewriting ++//! audit history. ++ + use crate::errors::VctrlError; + use crate::types::{Hash, ReflogEntry}; + ++/// Trait for managing reflogs. ++/// ++/// # Why this exists ++/// Provides a unified interface for recording and retrieving the history of ++/// reference updates. By abstracting this into a trait, the crate allows the core ++/// engine to track state changes without being tied to the standard `.git/logs` ++/// filesystem layout. Consumers can inject in-memory reflogs for testing or ++/// database-backed reflogs for enterprise persistence. ++/// ++/// # How it works ++/// The store maintains a mapping between reference names and a chronological list ++/// of [`ReflogEntry`] items. The `append` method requires `&mut self` to enforce ++/// exclusive access, ensuring that concurrent updates to the same reference's ++/// reflog do not interleave and corrupt the history file. The `entries` method ++/// takes `&self`, allowing safe, concurrent reads of the audit trail. ++/// ++/// # Design Rationale: `Vec` over Iterators ++/// Unlike [`RefStore::list_refs`](crate::traits::core::ref_store::RefStore::list_refs), ++/// which returns an iterator to handle millions of refs, `entries` returns a `Vec`. ++/// Reflogs are bounded in size (e.g., Git defaults to 90 days or 250 entries). The ++/// memory footprint of loading a single reference's reflog is strictly bounded, ++/// making a `Vec` more ergonomic and efficient than a streaming iterator. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory store: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::reflog::ReflogStore; ++/// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; ++/// # use std::collections::HashMap; ++/// # ++/// #[derive(Default)] ++/// struct MockReflogStore { ++/// logs: HashMap>, ++/// } ++/// ++/// impl ReflogStore for MockReflogStore { ++/// type RefName = String; ++/// ++/// fn append( ++/// &mut self, ++/// reference: &Self::RefName, ++/// old_hash: Option, ++/// new_hash: Option, ++/// reason: &str, ++/// timestamp: i64, ++/// timezone_offset: i16, ++/// ) -> Result<(), VctrlError> { ++/// let entry = ReflogEntry::new( ++/// old_hash, ++/// new_hash, ++/// reason.to_string(), ++/// timestamp, ++/// timezone_offset, ++/// )?; ++/// self.logs.entry(reference.clone()).or_default().push(entry); ++/// Ok(()) ++/// } ++/// ++/// fn entries(&self, reference: &Self::RefName) -> Result, VctrlError> { ++/// Ok(self.logs.get(reference).cloned().unwrap_or_default()) ++/// } ++/// } ++/// ++/// let mut store = MockReflogStore::default(); ++/// let hash = Hash::from_bytes(&[0_u8; 64])?; ++/// store.append(&"refs/heads/main".to_string(), None, Some(hash), "initial commit", 0, 0)?; ++/// assert_eq!(store.entries(&"refs/heads/main".to_string())?.len(), 1); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait ReflogStore: Send + Sync { ++ /// The reference name type. ++ /// ++ /// # Why this exists ++ /// Decouples the reference name representation from the trait. While typically ++ /// a `String`, this allows backends to use interned strings or specialized ++ /// path types, ensuring interoperability with the associated [`RefStore`](crate::traits::core::ref_store::RefStore). + type RefName: Send + Sync; + ++ /// Appends an entry to the reflog for a reference. ++ /// ++ /// # How it works ++ /// Creates a new [`ReflogEntry`] with the provided transition (`old_hash` to ++ /// `new_hash`), reason, and timestamp metadata. The entry is appended to the ++ /// end of the reference's log. Requires `&mut self` to enforce exclusive access, ++ /// mimicking the behavior of acquiring a `.lock` file on the reflog. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidTimezoneOffset`] if the `timezone_offset` is ++ /// out of the valid range (-1440 to 1440). Returns [`VctrlError`] if the ++ /// underlying storage fails to persist the new entry. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::reflog::ReflogStore; ++ /// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockReflogStore { logs: HashMap> } ++ /// # impl ReflogStore for MockReflogStore { ++ /// # type RefName = String; ++ /// # fn append(&mut self, r: &Self::RefName, o: Option, n: Option, re: &str, t: i64, tz: i16) -> Result<(), VctrlError> { ++ /// # let e = ReflogEntry::new(o, n, re.to_string(), t, tz)?; self.logs.entry(r.clone()).or_default().push(e); Ok(()) ++ /// # } ++ /// # fn entries(&self, r: &Self::RefName) -> Result, VctrlError> { Ok(self.logs.get(r).cloned().unwrap_or_default()) } ++ /// # } ++ /// let mut store = MockReflogStore::default(); ++ /// let hash = Hash::from_bytes(&[1u8; 64])?; ++ /// store.append(&"HEAD".to_string(), None, Some(hash), "checkout", 100, 0)?; ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn append( + &mut self, + reference: &Self::RefName, +@@ -14,5 +139,38 @@ pub trait ReflogStore: Send + Sync { + timezone_offset: i16, + ) -> Result<(), VctrlError>; + ++ /// Returns all reflog entries for a reference. ++ /// ++ /// # How it works ++ /// Retrieves the complete chronological history of updates for the specified ++ /// reference. The entries are returned in a `Vec` ordered from oldest to newest. ++ /// If the reference has no reflog (e.g., a newly created branch without commits), ++ /// an empty `Vec` is returned. Takes `&self` to allow concurrent reads of the ++ /// audit trail. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the underlying storage cannot be read. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::reflog::ReflogStore; ++ /// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # #[derive(Default)] ++ /// # struct MockReflogStore { logs: HashMap> } ++ /// # impl ReflogStore for MockReflogStore { ++ /// # type RefName = String; ++ /// # fn append(&mut self, r: &Self::RefName, o: Option, n: Option, re: &str, t: i64, tz: i16) -> Result<(), VctrlError> { ++ /// # let e = ReflogEntry::new(o, n, re.to_string(), t, tz)?; self.logs.entry(r.clone()).or_default().push(e); Ok(()) ++ /// # } ++ /// # fn entries(&self, r: &Self::RefName) -> Result, VctrlError> { Ok(self.logs.get(r).cloned().unwrap_or_default()) } ++ /// # } ++ /// let store = MockReflogStore::default(); ++ /// let entries = store.entries(&"refs/heads/nonexistent".to_string())?; ++ /// assert!(entries.is_empty()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn entries(&self, reference: &Self::RefName) -> Result, VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/remote.rs b/libvctrl_handler/src/traits/core/remote.rs +index 10772c3..9df76fd 100644 +--- a/libvctrl_handler/src/traits/core/remote.rs ++++ b/libvctrl_handler/src/traits/core/remote.rs +@@ -1,10 +1,196 @@ ++//! Remote repository trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for interacting with remote repositories. ++//! It abstracts the complex orchestration of network protocols (e.g., HTTP, SSH, Git) ++//! into a unified interface. By using this trait, the core engine can execute fetch ++//! and push operations without being coupled to the underlying transport mechanism ++//! or wire protocol. ++//! ++//! # Design Rationale: Associated Types vs. Generics ++//! The trait uses associated types (`type RefSpec`, `type RemoteRef`) rather than ++//! generic parameters. This design ties the data representations directly to the ++//! specific `Remote` implementation. An HTTP backend might parse refspecs into ++//! structured objects, while a custom binary protocol might use raw byte slices. ++//! This prevents type mismatches at compile time and simplifies the API by removing ++//! the need for verbose generic annotations at every call site. ++ + use crate::errors::VctrlError; + ++/// Trait for interacting with remote repositories. ++/// ++/// # Why this exists ++/// Provides a high-level interface for synchronizing state between a local ++/// repository and a remote endpoint. It encapsulates the logic for discovering ++/// remote references, fetching missing objects, and pushing local history. ++/// Abstracting this into a trait allows the crate to support multiple remote ++/// backends (e.g., standard Git, custom distributed ledgers) seamlessly. ++/// ++/// # How it works ++/// The trait defines three core operations: ++/// - `list_refs`: Queries the remote for its current reference state. ++/// - `fetch`: Downloads objects specified by refspecs and updates local remote-tracking branches. ++/// - `push`: Uploads local objects and updates remote references. ++/// ++/// # Design Rationale: Mutability Split ++/// `list_refs` takes `&self` because it is a pure query operation that does not ++/// alter the local or remote state; multiple threads can safely list refs concurrently. ++/// Conversely, `fetch` and `push` take `&mut self`. These operations fundamentally ++/// mutate state (updating local object stores or remote refs) and often require ++/// sequential, exclusive access to network streams and internal buffers to prevent ++/// data corruption or race conditions. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock remote backend: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::remote::Remote; ++/// # use libvctrl_handler::VctrlError; ++/// # ++/// #[derive(Default)] ++/// struct MockRemote { ++/// refs: Vec, ++/// } ++/// ++/// impl Remote for MockRemote { ++/// type RefSpec = String; ++/// type RemoteRef = String; ++/// ++/// fn list_refs(&self) -> Result, VctrlError> { ++/// Ok(self.refs.clone()) ++/// } ++/// ++/// fn fetch(&mut self, _refspecs: &[Self::RefSpec]) -> Result<(), VctrlError> { ++/// // Mock fetch: no-op ++/// Ok(()) ++/// } ++/// ++/// fn push(&mut self, _refspecs: &[Self::RefSpec]) -> Result<(), VctrlError> { ++/// // Mock push: no-op ++/// Ok(()) ++/// } ++/// } ++/// ++/// let remote = MockRemote::default(); ++/// assert!(remote.list_refs().is_ok()); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Remote: Send + Sync { ++ /// The refspec type. ++ /// ++ /// # Why this exists ++ /// Decouples the refspec representation from the trait. A refspec defines the ++ /// mapping between remote and local references (e.g., `refs/heads/*:refs/remotes/origin/*`). ++ /// Allowing backends to define their own type enables protocol-specific optimizations ++ /// or pre-parsed structures. + type RefSpec: Send + Sync; ++ ++ /// The remote reference type. ++ /// ++ /// # Why this exists ++ /// Defines the structure of a reference as advertised by the remote. This might ++ /// include the hash, the name, and additional capabilities (e.g., symref targets) ++ /// negotiated during the protocol handshake. + type RemoteRef: Send + Sync; + ++ /// Lists references available on the remote. ++ /// ++ /// # How it works ++ /// Connects to the remote (or queries a cached advertisement) and retrieves ++ /// a list of all references (branches, tags) that the remote currently possesses. ++ /// Takes `&self` as this is a read-only operation that should be safe to call ++ /// concurrently. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the network connection fails, the remote is ++ /// unreachable, or the protocol handshake fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::remote::Remote; ++ /// # use libvctrl_handler::VctrlError; ++ /// # #[derive(Default)] ++ /// # struct MockRemote { refs: Vec } ++ /// # impl Remote for MockRemote { ++ /// # type RefSpec = String; type RemoteRef = String; ++ /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } ++ /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let remote = MockRemote { refs: vec!["refs/heads/main".to_string()] }; ++ /// let refs = remote.list_refs()?; ++ /// assert_eq!(refs.len(), 1); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn list_refs(&self) -> Result, VctrlError>; ++ ++ /// Fetches objects according to the given refspecs. ++ /// ++ /// # How it works ++ /// Takes a slice of refspecs and negotiates with the remote to determine which ++ /// objects are missing locally. It downloads these objects (often via a packfile), ++ /// inserts them into the local object store, and updates local remote-tracking ++ /// references (e.g., `refs/remotes/origin/*`). Requires `&mut self` as it ++ /// modifies local state and network streams. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the network transfer fails, objects are corrupted ++ /// in transit, or the local object store cannot be written to. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::remote::Remote; ++ /// # use libvctrl_handler::VctrlError; ++ /// # #[derive(Default)] ++ /// # struct MockRemote { refs: Vec } ++ /// # impl Remote for MockRemote { ++ /// # type RefSpec = String; type RemoteRef = String; ++ /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } ++ /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut remote = MockRemote::default(); ++ /// let refspecs = vec!["refs/heads/main:refs/remotes/origin/main".to_string()]; ++ /// remote.fetch(&refspecs)?; ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn fetch(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; ++ ++ /// Pushes objects according to the given refspecs. ++ /// ++ /// # How it works ++ /// Takes a slice of refspecs and sends local objects to the remote that are ++ /// required to satisfy the refspecs. It updates the remote references accordingly. ++ /// Requires `&mut self` as it consumes network resources and may mutate internal ++ /// state regarding the push process. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the remote rejects the update (e.g., non-fast-forward ++ /// push), network transfer fails, or permission is denied. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::remote::Remote; ++ /// # use libvctrl_handler::VctrlError; ++ /// # #[derive(Default)] ++ /// # struct MockRemote { refs: Vec } ++ /// # impl Remote for MockRemote { ++ /// # type RefSpec = String; type RemoteRef = String; ++ /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } ++ /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } ++ /// # } ++ /// let mut remote = MockRemote::default(); ++ /// let refspecs = vec!["refs/heads/main:refs/heads/main".to_string()]; ++ /// remote.push(&refspecs)?; ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn push(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/revwalk.rs b/libvctrl_handler/src/traits/core/revwalk.rs +index ed5dce8..0fe3bd8 100644 +--- a/libvctrl_handler/src/traits/core/revwalk.rs ++++ b/libvctrl_handler/src/traits/core/revwalk.rs +@@ -1,10 +1,127 @@ ++//! Revision walking trait. ++//! ++//! # Architecture ++//! This module provides the contract for traversing the commit graph. Walking ++//! history is a fundamental operation for log generation, bisecting, and ancestry ++//! queries. By abstracting this into a trait, the crate allows backends to implement ++//! optimized traversal algorithms (e.g., topological sorting, priority queues based ++//! on timestamps) without leaking those implementation details to the caller. ++//! ++//! # Design Rationale: Lazy Evaluation ++//! Repositories like the Linux kernel contain millions of commits. Loading the ++//! entire commit graph into memory at once would cause severe memory exhaustion. ++//! The [`RevWalk::walk`] method returns an iterator, enforcing lazy evaluation. ++//! Commits are only loaded and yielded from the underlying object store as the ++//! iterator is consumed, maintaining a constant, predictable memory footprint. ++ + use crate::errors::VctrlError; + ++/// An iterator over commit history. ++/// ++/// # Why this exists ++/// This type alias standardizes the return type of revision walks across all ++/// backends. It uses dynamic dispatch (`Box`) to perform type erasure. ++/// This allows a backend to return any complex internal iterator struct (e.g., a ++/// binary heap for priority-ordered traversal) without forcing the caller to know ++/// the concrete type or bloating the trait signature with associated types. ++/// ++/// # How it works ++/// - `Item = Result`: Yields a `Result` because graph traversal may ++/// encounter I/O errors (e.g., a missing commit object) mid-iteration. ++/// - `Send`: The iterator can be safely transferred across threads, enabling ++/// parallel processing of commit history (e.g., using `rayon`). ++/// - `'a`: The lifetime ties the iterator to the lifetime of the [`RevWalk`] ++/// instance that created it, ensuring the backend store remains valid while ++/// the iterator is active. + pub type RevWalkIterator<'a, T> = Box> + Send + 'a>; + ++/// Trait for walking commit history. ++/// ++/// # Why this exists ++/// Provides a unified interface for commit graph traversal. By using an associated ++/// type for the commit identifier, the trait is not hardcoded to cryptographic ++/// hashes. An in-memory testing backend might use array indices (`usize`), while ++/// a disk-backed backend uses [`Hash`](crate::Hash). ++/// ++/// # How it works ++/// The `walk` method accepts a starting commit identifier and returns a ++/// [`RevWalkIterator`]. The implementor is responsible for resolving the start ++/// commit, reading its parent hashes, and pushing them into an internal queue. ++/// As the caller calls `next()` on the iterator, the backend dequeues a commit, ++/// fetches its parents, and yields the commit. ++/// ++/// # Design Rationale: `&self` on `walk` ++/// Note that `walk` takes `&self` instead of `&mut self`. Traversal is a read-only ++/// operation from the perspective of the walker's state. The implementor must use ++/// interior mutability (e.g., `Mutex` for internal buffers) if the underlying ++/// object store requires mutable access to read objects, allowing multiple ++/// concurrent walks to occur safely. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock graph: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::revwalk::{RevWalk, RevWalkIterator}; ++/// # use libvctrl_handler::VctrlError; ++/// # ++/// struct MockRevWalk; ++/// ++/// impl RevWalk for MockRevWalk { ++/// type CommitId = u32; ++/// ++/// fn walk(&self, start: &Self::CommitId) -> Result, VctrlError> { ++/// let start = *start; ++/// // Simulate walking backwards through commit IDs 0 to `start` ++/// Ok(Box::new((0..start).rev().map(Ok))) ++/// } ++/// } ++/// ++/// let walker = MockRevWalk; ++/// let iter = walker.walk(&3)?; ++/// let commits: Vec = iter.filter_map(|c| c.ok()).collect(); ++/// assert_eq!(commits, vec![2, 1, 0]); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait RevWalk: Send + Sync { ++ /// The commit identifier type. ++ /// ++ /// # Why this exists ++ /// Decouples the traversal logic from the identifier format. While typically ++ /// a 64-byte [`Hash`](crate::Hash), this allows specialized backends to use ++ /// more efficient representations like integers or pointers. + type CommitId: Send + Sync; + ++ /// Returns an iterator over commit history starting from the given commit. ++ /// ++ /// # How it works ++ /// Resolves the `start` commit and initializes an iterator. The iterator ++ /// traverses the graph (typically in reverse chronological order, respecting ++ /// topological constraints). The lifetime `'_` binds the returned iterator to ++ /// the `RevWalk` implementor, ensuring the backend is not dropped prematurely. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the starting commit cannot be found in the ++ /// underlying store, or if initializing the traversal queue fails. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::revwalk::{RevWalk, RevWalkIterator}; ++ /// # use libvctrl_handler::VctrlError; ++ /// # struct MockRevWalk; ++ /// # impl RevWalk for MockRevWalk { ++ /// # type CommitId = u32; ++ /// # fn walk(&self, s: &Self::CommitId) -> Result, VctrlError> { ++ /// # Ok(Box::new((0..*s).rev().map(Ok))) ++ /// # } ++ /// # } ++ /// let walker = MockRevWalk; ++ /// let mut iter = walker.walk(&5)?; ++ /// assert_eq!(iter.next(), Some(Ok(4))); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn walk( + &self, + start: &Self::CommitId, +diff --git a/libvctrl_handler/src/traits/core/signer.rs b/libvctrl_handler/src/traits/core/signer.rs +index 57ca2c2..02e8ac5 100644 +--- a/libvctrl_handler/src/traits/core/signer.rs ++++ b/libvctrl_handler/src/traits/core/signer.rs +@@ -1,5 +1,101 @@ ++//! Signing trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for cryptographically signing data ++//! (e.g., commits or tags). By abstracting the signing mechanism into a trait, ++//! the crate decouples its security logic from the specific cryptographic backend. ++//! This allows consumers to plug in different implementations, such as GPG, SSH, ++//! or cloud-based Key Management Services (KMS), without altering the core VCS engine. ++//! ++//! # Design Rationale: Stateful Signing ++//! The `sign` method requires `&mut self`. This is a deliberate design choice ++//! because cryptographic signing is often stateful. A backend might need to consume ++//! a one-time-use nonce, update an internal counter for replay protection, or acquire ++//! an exclusive lock on a hardware security module (HSM). Forcing `&mut self` at the ++//! trait level ensures that backends have the flexibility to implement these requirements ++//! safely without resorting to interior mutability (`Mutex` or `RefCell`). ++ + use crate::errors::VctrlError; + ++/// Trait for signing data. ++/// ++/// # Why this exists ++/// Provides a unified interface for generating cryptographic signatures. In Git, ++/// signed commits and tags verify the identity of the author. This trait allows ++/// the engine to delegate the complex cryptography to a dedicated backend, ensuring ++/// that the core logic remains focused on object manipulation and graph traversal. ++/// ++/// # How it works ++/// The implementor receives a `key_id` (which could be a GPG key fingerprint, an ++/// SSH key path, or a KMS URI) and the raw `data` to be signed. The backend locates ++/// the private key, performs the cryptographic signing operation, and returns the ++/// resulting signature as an owned `Vec`. ++/// ++/// # Design Rationale: Owned `Vec` Return ++/// The signature is returned as an owned `Vec` rather than a fixed-size array. ++/// Different signing algorithms produce different signature lengths (e.g., RSA signatures ++/// are significantly larger than `EdDSA` signatures). Returning a vector accommodates ++/// all algorithms uniformly. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock signer: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::signer::Signer; ++/// # use libvctrl_handler::VctrlError; ++/// # ++/// struct MockSigner; ++/// ++/// impl Signer for MockSigner { ++/// fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError> { ++/// // A real implementation would use a private key here. ++/// let mut signature = Vec::new(); ++/// signature.extend_from_slice(key_id.as_bytes()); ++/// signature.push(b':'); ++/// signature.extend_from_slice(data); ++/// Ok(signature) ++/// } ++/// } ++/// ++/// let mut signer = MockSigner; ++/// let sig = signer.sign("ABCDEFG12345", b"commit data")?; ++/// assert_eq!(sig, b"ABCDEFG12345:commit data"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Signer: Send + Sync { ++ /// Signs the given data with the specified key ID and returns the signature. ++ /// ++ /// # How it works ++ /// Resolves the `key_id` to a private key within the backend's keyring. It then ++ /// applies the signing algorithm (e.g., RSA-SHA256, Ed25519) to the provided ++ /// `data` slice. The resulting cryptographic signature is returned as an owned ++ /// byte vector. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if: ++ /// - The `key_id` cannot be found in the keyring. ++ /// - The private key requires a passphrase that could not be provided. ++ /// - The underlying cryptographic operation fails. ++ /// - An I/O error occurs (e.g., communicating with a hardware token). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::signer::Signer; ++ /// # use libvctrl_handler::VctrlError; ++ /// # struct MockSigner; ++ /// # impl Signer for MockSigner { ++ /// # fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError> { ++ /// # Ok(data.to_vec()) ++ /// # } ++ /// # } ++ /// let mut signer = MockSigner; ++ /// let data = b"data to sign"; ++ /// let signature = signer.sign("key-id", data)?; ++ /// assert_eq!(signature, data); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/transport.rs b/libvctrl_handler/src/traits/core/transport.rs +index 09ed5a1..545168e 100644 +--- a/libvctrl_handler/src/traits/core/transport.rs ++++ b/libvctrl_handler/src/traits/core/transport.rs +@@ -1,9 +1,157 @@ +-use std::io::Read; ++//! Transport trait. ++//! ++//! # Architecture ++//! This module defines the low-level contract for sending and receiving raw Git ++//! objects over a network. It is distinct from the [`Remote`](crate::traits::core::remote::Remote) ++//! module, which handles higher-level repository semantics like refspec negotiation. ++//! The `Transport` trait acts as a dumb pipe: it merely maps object hashes to byte streams. ++//! ++//! # Design Rationale: Streaming I/O ++//! The `fetch_object` method returns a `Box` rather than a `Vec`. ++//! This is a critical architectural decision for network efficiency. Git objects ++//! can be massive. By returning a reader, the transport backend can stream data ++//! directly from the network socket to the decoder, decompressing on the fly and ++//! maintaining a constant memory footprint regardless of the object's size. + + use crate::errors::VctrlError; + use crate::types::Hash; ++use std::io::Read; + ++/// Trait for transporting Git objects. ++/// ++/// # Why this exists ++/// Provides a backend-agnostic abstraction for the raw transfer of Git objects. ++/// Whether the underlying protocol is HTTP, SSH, or the Git wire protocol, this ++/// trait allows the core engine to fetch missing objects or push new ones without ++/// being coupled to the specific networking implementation or socket management. ++/// ++/// # How it works ++/// The trait defines two operations: ++/// - `fetch_object`: Downloads an object by its hash, returning a stream. ++/// - `push_object`: Uploads an object's data to the remote. ++/// ++/// # Design Rationale: Mutability Split ++/// `fetch_object` takes `&self` because it is a read-only operation from the ++/// perspective of the transport's state; multiple threads can safely fetch objects ++/// concurrently. Conversely, `push_object` takes `&mut self` because writing to ++/// a network socket is inherently stateful and often requires sequential, exclusive ++/// access to prevent interleaved data corruption. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock in-memory transport: ++/// ++/// ``` ++/// # use std::io::Read; ++/// # use libvctrl_handler::traits::core::transport::Transport; ++/// # use libvctrl_handler::{Hash, VctrlError}; ++/// # use std::collections::HashMap; ++/// # use std::io::Cursor; ++/// # ++/// #[derive(Default)] ++/// struct MockTransport { ++/// remote_store: HashMap>, ++/// } ++/// ++/// impl Transport for MockTransport { ++/// fn fetch_object(&self, hash: &Hash) -> Result, VctrlError> { ++/// match self.remote_store.get(hash) { ++/// Some(data) => Ok(Box::new(Cursor::new(data.clone()))), ++/// None => Err(VctrlError::ObjectNotFound(*hash)), ++/// } ++/// } ++/// ++/// fn push_object(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { ++/// self.remote_store.insert(*hash, data.to_vec()); ++/// Ok(()) ++/// } ++/// } ++/// ++/// let mut transport = MockTransport::default(); ++/// let hash = Hash::from_bytes(&[0_u8; 64])?; ++/// transport.push_object(&hash, b"raw object data")?; ++/// assert!(transport.fetch_object(&hash).is_ok()); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Transport: Send + Sync { ++ /// Fetches an object by hash, returning a reader. ++ /// ++ /// # How it works ++ /// Requests an object from the remote endpoint using its cryptographic hash. ++ /// The implementor returns a boxed reader. The lifetime `'_` ties the returned ++ /// reader to the lifetime of the `Transport` instance, ensuring the underlying ++ /// network socket or buffer remains valid while the stream is being consumed. ++ /// This prevents loading large objects into memory all at once. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::ObjectNotFound`] if the remote does not possess the object. ++ /// Returns [`VctrlError`] if a network I/O error occurs during the transfer. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::transport::Transport; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::{Cursor, Read}; ++ /// # #[derive(Default)] ++ /// # struct MockTransport { remote_store: HashMap> } ++ /// # impl Transport for MockTransport { ++ /// # fn fetch_object(&self, h: &Hash) -> Result, VctrlError> { ++ /// # match self.remote_store.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } ++ /// # } ++ /// # fn push_object(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { ++ /// # self.remote_store.insert(*h, d.to_vec()); Ok(()) ++ /// # } ++ /// # } ++ /// let mut transport = MockTransport::default(); ++ /// let hash = Hash::from_bytes(&[1u8; 64])?; ++ /// transport.push_object(&hash, b"fetch me")?; ++ /// ++ /// let mut reader = transport.fetch_object(&hash)?; ++ /// let mut content = String::new(); ++ /// reader.read_to_string(&mut content)?; ++ /// assert_eq!(content, "fetch me"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn fetch_object(&self, hash: &Hash) -> Result, VctrlError>; ++ ++ /// Pushes an object to the remote. ++ /// ++ /// # How it works ++ /// Accepts the object's hash and a byte slice of its raw, uncompressed content. ++ /// The implementor is responsible for transmitting this data to the remote endpoint. ++ /// Requires `&mut self` to enforce exclusive access, preventing data races when ++ /// multiple threads attempt to write to the same network socket simultaneously. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the network connection fails, the remote rejects ++ /// the data, or an I/O error occurs during transmission. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use std::io::Read; ++ /// # use libvctrl_handler::traits::core::transport::Transport; ++ /// # use libvctrl_handler::{Hash, VctrlError}; ++ /// # use std::collections::HashMap; ++ /// # use std::io::Cursor; ++ /// # #[derive(Default)] ++ /// # struct MockTransport { remote_store: HashMap> } ++ /// # impl Transport for MockTransport { ++ /// # fn fetch_object(&self, h: &Hash) -> Result, VctrlError> { ++ /// # match self.remote_store.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } ++ /// # } ++ /// # fn push_object(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { ++ /// # self.remote_store.insert(*h, d.to_vec()); Ok(()) ++ /// # } ++ /// # } ++ /// let mut transport = MockTransport::default(); ++ /// let hash = Hash::from_bytes(&[2u8; 64])?; ++ /// transport.push_object(&hash, b"pushing data")?; ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn push_object(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; + } +diff --git a/libvctrl_handler/src/traits/core/verifier.rs b/libvctrl_handler/src/traits/core/verifier.rs +index 6e2b159..e3f36ec 100644 +--- a/libvctrl_handler/src/traits/core/verifier.rs ++++ b/libvctrl_handler/src/traits/core/verifier.rs +@@ -1,5 +1,106 @@ ++//! Verification trait. ++//! ++//! # Architecture ++//! This module defines the abstract contract for verifying cryptographic signatures. ++//! It is the counterpart to the [`Signer`](crate::traits::core::signer::Signer) module. ++//! By abstracting verification into a trait, the crate allows the core engine to ++//! authenticate commits and tags without being coupled to a specific cryptographic ++//! backend (e.g., GPG, SSH, or X.509). ++//! ++//! # Design Rationale: Stateless Verification ++//! Unlike signing, which may require stateful operations (e.g., consuming nonces or ++//! locking hardware tokens), signature verification is a pure, stateless mathematical ++//! operation. It only requires the public key, the raw data, and the signature. ++//! Therefore, the `verify` method takes `&self` instead of `&mut self`. This allows ++//! multiple threads to concurrently verify different commits in a revision graph ++//! without any synchronization overhead. ++ + use crate::errors::VctrlError; + ++/// Trait for verifying signatures. ++/// ++/// # Why this exists ++/// Provides a unified interface for authenticating data. In Git, verifying signed ++/// commits and tags ensures that the authorship is genuine and the data has not been ++/// tampered with. This trait allows the engine to delegate the complex cryptography ++/// to a dedicated backend, ensuring that the core logic remains agnostic of the ++/// underlying Public Key Infrastructure (PKI). ++/// ++/// # How it works ++/// The implementor receives a `key_id` (to locate the correct public key), the raw ++/// `data` that was signed, and the `signature` bytes. The backend applies the ++/// verification algorithm (e.g., RSA-SHA256, Ed25519) to confirm that the signature ++/// was indeed generated by the owner of the private key corresponding to the public key. ++/// ++/// # Design Rationale: `Result` ++/// The return type distinguishes between a cryptographic failure and a system failure: ++/// - `Ok(true)`: The signature is mathematically valid. ++/// - `Ok(false)`: The signature is mathematically invalid (tampered data or wrong key). ++/// - `Err(VctrlError)`: A system error occurred (e.g., public key not found, I/O error ++/// reading the keyring, or unsupported algorithm). ++/// This prevents confusing an invalid signature with a system-level fault, allowing ++/// callers to handle security violations explicitly. ++/// ++/// # Examples ++/// ++/// Implementing the trait for a mock verifier: ++/// ++/// ``` ++/// # use libvctrl_handler::traits::core::verifier::Verifier; ++/// # use libvctrl_handler::VctrlError; ++/// # ++/// struct MockVerifier; ++/// ++/// impl Verifier for MockVerifier { ++/// fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result { ++/// // A real implementation would use a public key here. ++/// if key_id != "trusted_key" { ++/// return Ok(false); // Unknown key implies invalid signature ++/// } ++/// Ok(data == signature) // Simplified mock verification ++/// } ++/// } ++/// ++/// let verifier = MockVerifier; ++/// let data = b"commit data"; ++/// let sig = b"commit data"; ++/// ++/// assert!(verifier.verify("trusted_key", data, sig)?); ++/// assert!(!verifier.verify("untrusted_key", data, sig)?); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub trait Verifier: Send + Sync { ++ /// Verifies data against a signature using the specified key ID. ++ /// ++ /// # How it works ++ /// Resolves the `key_id` to a public key within the backend's keyring. It then ++ /// applies the verification algorithm to the `data` and `signature` slices. ++ /// The operation is purely computational and does not mutate the verifier's state. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if: ++ /// - The `key_id` cannot be found in the keyring. ++ /// - The underlying cryptographic library encounters an error. ++ /// - An I/O error occurs while accessing the keyring. ++ /// ++ /// Note: An invalid signature returns `Ok(false)`, not `Err`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::traits::core::verifier::Verifier; ++ /// # use libvctrl_handler::VctrlError; ++ /// # struct MockVerifier; ++ /// # impl Verifier for MockVerifier { ++ /// # fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result { ++ /// # Ok(key_id == "trusted" && data == signature) ++ /// # } ++ /// # } ++ /// let verifier = MockVerifier; ++ /// let is_valid = verifier.verify("trusted", b"data", b"data")?; ++ /// assert!(is_valid); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result; + } +diff --git a/libvctrl_handler/src/traits/mod.rs b/libvctrl_handler/src/traits/mod.rs +index 5a7ca06..2fc231f 100644 +--- a/libvctrl_handler/src/traits/mod.rs ++++ b/libvctrl_handler/src/traits/mod.rs +@@ -1 +1,39 @@ ++//! Traits for repository operations. ++//! ++//! # Architecture ++//! This module defines the abstract contracts (interfaces) for interacting with ++//! repository components. By leveraging Rust's trait system, the crate decouples ++//! the *what* (domain logic and validation) from the *how* (I/O and storage implementations). ++//! ++//! # Design Rationale: Backend Agnosticism ++//! Defining operations like object storage or reference management as traits ++//! allows the core logic to remain agnostic of the underlying backend. Consumers ++//! can implement these traits for in-memory storage, disk-based filesystems, or ++//! remote network protocols without altering the core VCS algorithms. This also ++//! drastically simplifies unit testing, as mock implementations can be injected ++//! seamlessly via dependency injection. ++//! ++//! # Examples ++//! *Note: The following example assumes this crate is named `libvctrl_handler`.* ++//! ++//! ``` ++//! // Importing the module ensures it is publicly accessible and compiled. ++//! use libvctrl_handler::traits::core; ++//! ``` ++ ++/// Core operational traits required to implement a functional version control backend. ++/// ++/// # Why this exists ++/// Houses the fundamental, low-level traits (such as `ObjectStore`, `RefStore`, and ++/// `Encoder`) that define the minimum viable surface area for a Git implementation. ++/// Grouping these into a `core` submodule allows the parent `traits` module to ++/// logically separate essential protocol traits from any auxiliary or high-level ++/// behavioral traits that may be introduced in the future. ++/// ++/// # Examples ++/// ++/// ``` ++/// // The core submodule is accessible for custom backend implementations. ++/// use libvctrl_handler::traits::core; ++/// ``` + pub mod core; +diff --git a/libvctrl_handler/src/types/core/blob.rs b/libvctrl_handler/src/types/core/blob.rs +index e57ac56..34376d0 100644 +--- a/libvctrl_handler/src/types/core/blob.rs ++++ b/libvctrl_handler/src/types/core/blob.rs +@@ -1,12 +1,73 @@ ++//! Blob object representation. ++//! ++//! # Architecture ++//! This module defines the [`Blob`] struct, which represents the raw content of ++//! a file in the Git object model. Blobs are content-addressable, meaning their ++//! identifier is derived directly from their byte content. ++//! ++//! # Design Rationale: Bounded Allocation ++//! Git blobs can range from empty files to massive binaries. Without strict limits, ++//! a malicious repository could force the engine to allocate gigabytes of memory, ++//! causing denial-of-service (DoS). The [`Blob::new`] constructor enforces ++//! [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE), acting as a fail-fast ++//! circuit breaker during object construction. ++ + use crate::constants::MAX_BLOB_SIZE; + use crate::errors::VctrlError; + ++/// A Git blob object (file content). ++/// ++/// # Why this exists ++/// Provides a strongly-typed, validated wrapper around raw file bytes. By requiring ++/// construction via [`new`](Self::new), the crate guarantees that every `Blob` ++/// instance in memory adheres to the crate's size limits. Once constructed, the ++/// blob is immutable, ensuring safe, concurrent sharing across threads. ++/// ++/// # How it works ++/// The struct takes ownership of a `Vec`. This is a zero-copy operation from ++/// the perspective of the byte buffer itself; the vector's allocation is simply ++/// moved into the struct, avoiding expensive memory duplication. ++/// ++/// # Examples ++/// ++/// Creating a valid blob: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::blob::Blob; ++/// # use libvctrl_handler::VctrlError; ++/// let blob = Blob::new(b"file content".to_vec())?; ++/// assert_eq!(blob.size(), 12); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Blob { + data: Vec, + } + + impl Blob { ++ /// Creates a new blob from raw bytes. ++ /// ++ /// # How it works ++ /// Takes ownership of the provided `Vec`. It checks the vector's length ++ /// against [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE). The downcast ++ /// from `u64` to `usize` is performed using `try_from` to ensure safe ++ /// compilation on 32-bit architectures where `usize` might be smaller than `u64`. ++ /// If the limit is exceeded, an error is returned and the original data is dropped. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::ExceededMaxSize`] if the data exceeds `MAX_BLOB_SIZE`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::blob::Blob; ++ /// # use libvctrl_handler::VctrlError; ++ /// let data = b"hello world".to_vec(); ++ /// let blob = Blob::new(data)?; ++ /// assert!(!blob.is_empty()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new(data: Vec) -> Result { + let max_size = usize::try_from(MAX_BLOB_SIZE).unwrap_or(usize::MAX); + if data.len() > max_size { +@@ -19,16 +80,63 @@ impl Blob { + Ok(Self { data }) + } + ++ /// Returns the raw bytes of the blob. ++ /// ++ /// # How it works ++ /// Returns an immutable slice (`&[u8]`) borrowing from the internal vector. ++ /// This avoids cloning the data, allowing callers to read the content without ++ /// taking ownership. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::blob::Blob; ++ /// # use libvctrl_handler::VctrlError; ++ /// let blob = Blob::new(b"raw data".to_vec())?; ++ /// assert_eq!(blob.data(), b"raw data"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[must_use] + pub fn data(&self) -> &[u8] { + &self.data + } + ++ /// Returns the size of the blob in bytes. ++ /// ++ /// # How it works ++ /// Implemented as a `const fn`. This allows the size to be evaluated at compile ++ /// time if the blob is constructed from a static context, incurring zero runtime ++ /// overhead. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::blob::Blob; ++ /// # use libvctrl_handler::VctrlError; ++ /// let blob = Blob::new(b"12345".to_vec())?; ++ /// assert_eq!(blob.size(), 5); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[must_use] + pub const fn size(&self) -> usize { + self.data.len() + } + ++ /// Returns `true` if the blob is empty. ++ /// ++ /// # How it works ++ /// Checks if the internal vector has zero length. Like [`size`](Self::size), ++ /// this is a `const fn`. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::blob::Blob; ++ /// # use libvctrl_handler::VctrlError; ++ /// let blob = Blob::new(Vec::new())?; ++ /// assert!(blob.is_empty()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[must_use] + pub const fn is_empty(&self) -> bool { + self.data.is_empty() +diff --git a/libvctrl_handler/src/types/core/commit.rs b/libvctrl_handler/src/types/core/commit.rs +index 874fa7f..b11fc25 100644 +--- a/libvctrl_handler/src/types/core/commit.rs ++++ b/libvctrl_handler/src/types/core/commit.rs +@@ -1,10 +1,39 @@ +-use std::collections::HashSet; ++//! Commit object and metadata representation. ++//! ++//! # Architecture ++//! This module defines the [`Commit`] struct, which acts as the node in the Git ++//! Directed Acyclic Graph (DAG). A commit links a tree state (snapshot) to its ++//! historical predecessors (parents), annotated with authorship and temporal metadata. ++//! ++//! # Design Rationale: DAG Integrity ++//! Git's history relies on the assumption that the parent graph is acyclic and ++//! structurally sound. To enforce this at the type level, the [`Commit::with_meta`] ++//! constructor performs strict validation: ++//! - **Duplicate Parents**: Uses a `HashSet` to ensure no parent hash appears twice. ++//! Because [`Hash`] is `Copy`, inserting into the set requires no allocation, ++//! providing O(1) duplicate detection. ++//! - **Parent Count Limits**: Enforces [`MAX_PARENT_COUNT`](crate::constants::MAX_PARENT_COUNT) ++//! to prevent pathological merge structures. ++//! - **Message Bounds**: Enforces [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH) ++//! to prevent memory exhaustion via commit messages. + + 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; + ++/// Metadata associated with a commit or tag. ++/// ++/// # Why this exists ++/// Separates temporal and environmental data (timestamps, timezones, encoding) ++/// from the core graph structure. This allows the metadata to be default-constructed ++/// (e.g., for testing) and shared between commits and annotated tags. ++/// ++/// # How it works ++/// The timezone offset is stored as an `i16` representing minutes. The constructor ++/// strictly validates this range (-1440 to 1440 minutes, i.e., -24 to +24 hours) ++/// to prevent malformed historical data. + #[derive(Clone, Debug, PartialEq, Eq, Default)] + pub struct CommitMeta { + timestamp: i64, +@@ -13,6 +42,30 @@ pub struct CommitMeta { + } + + impl CommitMeta { ++ /// Creates new commit metadata. ++ /// ++ /// # How it works ++ /// Validates that the `timezone_offset` falls within the valid range of ++ /// -1440 to 1440 minutes. This range covers all valid global timezones ++ /// (UTC-24:00 to UTC+24:00). Rejecting out-of-bounds offsets early prevents ++ /// arithmetic overflows or logic errors during date formatting. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidTimezoneOffset`] if the offset is out of range (-1440..=1440). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::commit::CommitMeta; ++ /// # use libvctrl_handler::VctrlError; ++ /// let meta = CommitMeta::new(1600000000, 120, None)?; ++ /// assert_eq!(meta.timezone_offset(), 120); ++ /// ++ /// let invalid = CommitMeta::new(0, 1500, None); ++ /// assert!(matches!(invalid, Err(VctrlError::InvalidTimezoneOffset(1500)))); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new( + timestamp: i64, + timezone_offset: i16, +@@ -28,22 +81,54 @@ impl CommitMeta { + }) + } + ++ /// Returns the timestamp. ++ /// ++ /// # How it works ++ /// Returns the Unix timestamp (seconds since epoch) as an `i64` to handle dates ++ /// far in the past or future. This is a `const fn`, allowing compile-time evaluation. + #[must_use] + pub const fn timestamp(&self) -> i64 { + self.timestamp + } + ++ /// Returns the timezone offset in minutes. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::commit::CommitMeta; ++ /// # use libvctrl_handler::VctrlError; ++ /// let meta = CommitMeta::new(0, -300, None)?; ++ /// assert_eq!(meta.timezone_offset(), -300); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[must_use] + pub const fn timezone_offset(&self) -> i16 { + self.timezone_offset + } + ++ /// Returns the encoding, if any. ++ /// ++ /// # How it works ++ /// Uses `as_deref()` to return `Option<&str>`, borrowing from the internal ++ /// `Option` without allocating. + #[must_use] + pub fn encoding(&self) -> Option<&str> { + self.encoding.as_deref() + } + } + ++/// A Git commit object. ++/// ++/// # Why this exists ++/// Represents a snapshot of the repository at a specific point in time, authored ++/// by a user. It links a [`Tree`] to its parent commits, forming the history graph. ++/// ++/// # How it works ++/// The struct stores the root tree hash, a vector of parent hashes (empty for the ++/// initial commit), author/committer identities, the message, and metadata. All ++/// fields are owned, ensuring the commit is self-contained and can be cloned or ++/// sent across threads without lifetime constraints. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Commit { + tree: Hash, +@@ -55,6 +140,31 @@ pub struct Commit { + } + + impl Commit { ++ /// Creates a new commit with default metadata. ++ /// ++ /// # How it works ++ /// Delegates to [`with_meta`](Self::with_meta), passing a default [`CommitMeta`] ++ /// (timestamp 0, offset 0, no encoding). This is useful for testing or when ++ /// metadata is injected later. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::DuplicateParent`] if parents contain duplicates. ++ /// Returns [`VctrlError::ExceededMaxSize`] if the message is too long or too many parents. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::commit::{Commit, CommitMeta}; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::types::core::user_id::UserID; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let tree = Hash::from_bytes(&[0_u8; 64])?; ++ /// # let author = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; ++ /// let commit = Commit::new(tree, vec![], author.clone(), author, "initial".to_string())?; ++ /// assert_eq!(commit.message(), "initial"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new( + tree: Hash, + parents: Vec, +@@ -72,6 +182,37 @@ impl Commit { + ) + } + ++ /// Creates a new commit with timestamp metadata. ++ /// ++ /// # How it works ++ /// Performs three critical validation steps: ++ /// 1. Checks `parents.len()` against [`MAX_PARENT_COUNT`](crate::constants::MAX_PARENT_COUNT). ++ /// Uses `usize::try_from` to safely handle 32-bit architectures. ++ /// 2. Checks `message.len()` against [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH). ++ /// 3. Iterates through `parents` and inserts each [`Hash`] into a `HashSet`. Because ++ /// `Hash` implements `Copy` and `Hash`, the insertion is a fast stack operation. ++ /// If `insert` returns `false`, a duplicate was found, and an error is returned. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if validation fails (duplicate parents, size limits exceeded). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::commit::{Commit, CommitMeta}; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::types::core::user_id::UserID; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let tree = Hash::from_bytes(&[0_u8; 64])?; ++ /// # let parent = Hash::from_bytes(&[1u8; 64])?; ++ /// # let author = UserID::new("Bob".to_string(), "bob@example.com".to_string())?; ++ /// # let meta = CommitMeta::new(1000, 0, None)?; ++ /// // Detecting a duplicate parent ++ /// let result = Commit::with_meta(tree, vec![parent, parent], author.clone(), author, "msg".to_string(), meta); ++ /// assert!(matches!(result, Err(VctrlError::DuplicateParent))); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn with_meta( + tree: Hash, + parents: Vec, +@@ -96,8 +237,8 @@ impl Commit { + } + + let mut seen = HashSet::new(); +- for parent in &parents { +- if !seen.insert(*parent) { ++ for p in &parents { ++ if !seen.insert(*p) { + return Err(VctrlError::DuplicateParent); + } + } +@@ -112,31 +253,60 @@ impl Commit { + }) + } + ++ /// Returns the tree hash of this commit. ++ /// ++ /// # How it works ++ /// Returns a reference to the root [`Hash`] identifying the tree object associated ++ /// with this commit's snapshot. + #[must_use] + pub const fn tree(&self) -> &Hash { + &self.tree + } + ++ /// Returns the parent commit hashes. ++ /// ++ /// # How it works ++ /// Returns a slice `&[Hash]` borrowing from the internal vector. This allows ++ /// callers to iterate over parents without cloning the hashes. + #[must_use] + pub fn parents(&self) -> &[Hash] { + &self.parents + } + ++ /// Returns the author information. ++ /// ++ /// # How it works ++ /// Returns a reference to the [`UserID`] representing the person who originally ++ /// wrote the changes. + #[must_use] + pub const fn author(&self) -> &UserID { + &self.author + } + ++ /// Returns the committer information. ++ /// ++ /// # How it works ++ /// Returns a reference to the [`UserID`] representing the person who applied ++ /// the changes to the repository (e.g., rebasing or merging). + #[must_use] + pub const fn committer(&self) -> &UserID { + &self.committer + } + ++ /// Returns the commit message. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. + #[must_use] + pub fn message(&self) -> &str { + &self.message + } + ++ /// Returns the commit metadata. ++ /// ++ /// # How it works ++ /// Returns a reference to the [`CommitMeta`] struct containing timestamp and ++ /// timezone data. + #[must_use] + pub const fn meta(&self) -> &CommitMeta { + &self.meta +diff --git a/libvctrl_handler/src/types/core/delta.rs b/libvctrl_handler/src/types/core/delta.rs +index b40b437..e591a53 100644 +--- a/libvctrl_handler/src/types/core/delta.rs ++++ b/libvctrl_handler/src/types/core/delta.rs +@@ -1,19 +1,73 @@ +-use alloc::vec::IntoIter as VecIntoIter; +-use core::slice::Iter as SliceIter; ++//! Delta and change types. ++//! ++//! # Architecture ++//! This module provides structures for representing structural differences ++//! (deltas) between two Git trees. Instead of loading full file contents into ++//! memory to compute diffs, the engine operates on hashes and paths. This ++//! "zero-knowledge" approach allows for extremely fast diffing of massive ++//! repositories with a minimal memory footprint. ++//! ++//! # Design Rationale: Type-State via Factory Methods ++//! The [`FileDelta`] struct uses private fields and `const fn` factory methods ++//! (e.g., [`FileDelta::added`], [`FileDelta::deleted`]). This is a deliberate ++//! architectural choice to enforce invariants at compile time. By restricting ++//! construction to these factory methods, the crate guarantees that an `Added` ++//! delta never has an `old_hash`, and a `Deleted` delta never has a `new_hash`. ++//! Consumers cannot accidentally construct an invalid delta state. ++ + use std::path::{Path, PathBuf}; + + use crate::Hash; + ++/// The kind of change between two objects. ++/// ++/// # Why this exists ++/// Classifies the nature of a modification between two tree states. By using a ++/// strongly-typed enum instead of bitflags or strings, the compiler enforces ++/// exhaustive matching, ensuring that diff consumers handle all possible change ++/// types (or explicitly ignore them via a catch-all). + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub enum ChangeKind { ++ /// The object was added. + Added, ++ /// The object was deleted. + Deleted, ++ /// The object was modified. + Modified, ++ /// The object type changed (e.g., blob to tree). + TypeChange, ++ /// The object was renamed. + Renamed, ++ /// The object was copied. + Copied, + } + ++/// A single file delta between two trees. ++/// ++/// # Why this exists ++/// Represents the atomic unit of a tree diff. It maps a file path transition ++/// (if any) to the change in its content hash. This allows UI renderers or merge ++/// drivers to understand exactly what happened to a specific file without needing ++/// to inspect the underlying blob data. ++/// ++/// # How it works ++/// The struct holds the current `path`, an optional `old_path` (for renames/copies), ++/// and optional `old_hash` and `new_hash` values. The presence of these hashes is ++/// directly correlated to the [`ChangeKind`], an invariant strictly maintained by ++/// the constructor methods. ++/// ++/// # Examples ++/// ++/// Creating a delta for an added file: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::delta::FileDelta; ++/// # use libvctrl_handler::Hash; ++/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++/// let delta = FileDelta::added("src/main.rs".into(), hash); ++/// assert!(delta.is_added()); ++/// assert!(delta.old_hash().is_none()); ++/// ``` + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct FileDelta { + path: PathBuf, +@@ -24,6 +78,11 @@ pub struct FileDelta { + } + + impl FileDelta { ++ /// Creates a new `FileDelta` representing an addition. ++ /// ++ /// # How it works ++ /// Initializes the delta with the new path and hash, leaving `old_path` and ++ /// `old_hash` as `None` to reflect that the file did not exist in the old tree. + #[must_use] + pub const fn added(path: PathBuf, new_hash: Hash) -> Self { + Self { +@@ -35,6 +94,11 @@ impl FileDelta { + } + } + ++ /// Creates a new `FileDelta` representing a deletion. ++ /// ++ /// # How it works ++ /// Initializes the delta with the old path and hash, leaving `new_hash` as ++ /// `None` to reflect that the file no longer exists in the new tree. + #[must_use] + pub const fn deleted(path: PathBuf, old_hash: Hash) -> Self { + Self { +@@ -46,6 +110,11 @@ impl FileDelta { + } + } + ++ /// Creates a new `FileDelta` representing a modification. ++ /// ++ /// # How it works ++ /// The path remains the same, but both `old_hash` and `new_hash` are populated ++ /// to indicate that the file content changed while its location did not. + #[must_use] + pub const fn modified(path: PathBuf, old_hash: Hash, new_hash: Hash) -> Self { + Self { +@@ -57,6 +126,11 @@ impl FileDelta { + } + } + ++ /// Creates a new `FileDelta` representing a type change. ++ /// ++ /// # How it works ++ /// Similar to a modification, but signifies that the Git object type changed ++ /// (e.g., a regular file became a symbolic link). Both hashes are populated. + #[must_use] + pub const fn type_change(path: PathBuf, old_hash: Hash, new_hash: Hash) -> Self { + Self { +@@ -68,6 +142,12 @@ impl FileDelta { + } + } + ++ /// Creates a new `FileDelta` representing a rename. ++ /// ++ /// # How it works ++ /// Populates both `path` (the new path) and `old_path` (the original path). ++ /// Depending on the diff algorithm, the hash might remain the same or change ++ /// if the file was also modified during the rename. + #[must_use] + pub const fn renamed( + old_path: PathBuf, +@@ -84,6 +164,11 @@ impl FileDelta { + } + } + ++ /// Creates a new `FileDelta` representing a copy. ++ /// ++ /// # How it works ++ /// Similar to a rename, but indicates the original file still exists at ++ /// `old_path`. The `path` field holds the destination of the copy. + #[must_use] + pub const fn copied( + old_path: PathBuf, +@@ -100,68 +185,131 @@ impl FileDelta { + } + } + ++ /// Returns the path of the changed file. ++ /// ++ /// # How it works ++ /// Returns a reference to the current (new) path of the file. If the file was ++ /// deleted, this returns the path it used to have. + #[must_use] + pub fn path(&self) -> &Path { + &self.path + } + ++ /// Returns the old path if the file was renamed or copied. ++ /// ++ /// # How it works ++ /// Returns `Some(&Path)` only if the [`ChangeKind`] is `Renamed` or `Copied`. ++ /// Otherwise, it returns `None`. + #[must_use] + pub fn old_path(&self) -> Option<&Path> { + self.old_path.as_deref() + } + ++ /// Returns the old hash, if the file previously existed. ++ /// ++ /// # How it works ++ /// Returns `None` for additions, as there is no previous state. + #[must_use] + pub const fn old_hash(&self) -> Option { + self.old_hash + } + ++ /// Returns the new hash, if the file exists now. ++ /// ++ /// # How it works ++ /// Returns `None` for deletions, as the file no longer exists in the new state. + #[must_use] + pub const fn new_hash(&self) -> Option { + self.new_hash + } + ++ /// Returns the kind of change. ++ /// ++ /// # How it works ++ /// Provides the [`ChangeKind`] enum variant associated with this delta. + #[must_use] + pub const fn kind(&self) -> ChangeKind { + self.kind + } + ++ /// Returns `true` if this is an addition. + #[must_use] + pub fn is_added(&self) -> bool { + self.kind == ChangeKind::Added + } + ++ /// Returns `true` if this is a deletion. + #[must_use] + pub fn is_deleted(&self) -> bool { + self.kind == ChangeKind::Deleted + } + ++ /// Returns `true` if this is a modification. + #[must_use] + pub fn is_modified(&self) -> bool { + self.kind == ChangeKind::Modified + } + ++ /// Returns `true` if this is a type change. + #[must_use] + pub fn is_type_change(&self) -> bool { + self.kind == ChangeKind::TypeChange + } + ++ /// Returns `true` if this is a rename. + #[must_use] + pub fn is_renamed(&self) -> bool { + self.kind == ChangeKind::Renamed + } + ++ /// Returns `true` if this is a copy. + #[must_use] + pub fn is_copied(&self) -> bool { + self.kind == ChangeKind::Copied + } + } + ++/// A collection of file deltas between two trees. ++/// ++/// # Why this exists ++/// Aggregates all individual [`FileDelta`]s into a single, cohesive structure. ++/// This provides a clean interface for consumers to query the total number of ++/// changes, iterate over them, or pass the entire diff result between functions. ++/// ++/// # How it works ++/// Internally, it is a thin wrapper around a `Vec`. It implements ++/// `IntoIterator` for both owned and borrowed values, allowing consumers to ++/// easily loop over the changes using `for` loops without needing to call ++/// `.iter()` explicitly. ++/// ++/// # Examples ++/// ++/// Creating a `TreeDelta` and iterating over its changes: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::delta::{FileDelta, TreeDelta}; ++/// # use libvctrl_handler::Hash; ++/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); ++/// let delta1 = FileDelta::added("file1.txt".into(), hash); ++/// let delta2 = FileDelta::deleted("file2.txt".into(), hash); ++/// let tree_delta = TreeDelta::from_changes(vec![delta1, delta2]); ++/// ++/// assert_eq!(tree_delta.len(), 2); ++/// for delta in &tree_delta { ++/// assert!(delta.is_added() || delta.is_deleted()); ++/// } ++/// ``` + #[derive(Debug, Clone, Default, PartialEq, Eq)] + pub struct TreeDelta { + changes: Vec, + } + + impl TreeDelta { ++ /// Creates an empty `TreeDelta`. ++ /// ++ /// # How it works ++ /// Initializes the internal vector without allocating capacity until elements ++ /// are added. This is a `const fn`, allowing static initialization. + #[must_use] + pub const fn new() -> Self { + Self { +@@ -169,25 +317,42 @@ impl TreeDelta { + } + } + ++ /// Creates a `TreeDelta` from a vector of `FileDelta`. ++ /// ++ /// # How it works ++ /// Takes ownership of the provided vector, wrapping it directly. This avoids ++ /// unnecessary copying of the deltas. + #[must_use] + pub const fn from_changes(changes: Vec) -> Self { + Self { changes } + } + ++ /// Returns the number of changes. + #[must_use] + pub const fn len(&self) -> usize { + self.changes.len() + } + ++ /// Returns `true` if there are no changes. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.changes.is_empty() + } + +- pub fn iter(&self) -> SliceIter<'_, FileDelta> { ++ /// Iterates over the changes. ++ /// ++ /// # How it works ++ /// Returns a standard slice iterator (`std::slice::Iter`), borrowing from the ++ /// internal vector. This is highly efficient as it involves no allocations. ++ pub fn iter(&self) -> std::slice::Iter<'_, FileDelta> { + self.changes.iter() + } + ++ /// Returns the changes. ++ /// ++ /// # How it works ++ /// Returns a slice `&[FileDelta]` borrowing from the internal vector. This allows ++ /// callers to index or iterate over the changes without taking ownership. + #[must_use] + pub fn changes(&self) -> &[FileDelta] { + &self.changes +@@ -196,8 +361,14 @@ impl TreeDelta { + + impl IntoIterator for TreeDelta { + type Item = FileDelta; +- type IntoIter = VecIntoIter; ++ type IntoIter = std::vec::IntoIter; + ++ /// Consumes the `TreeDelta` and returns an owned iterator. ++ /// ++ /// # How it works ++ /// Converts the internal `Vec` into `std::vec::IntoIter`, yielding ++ /// owned `FileDelta` items. This is useful when the consumer needs to take ++ /// ownership of the deltas, e.g., to send them to another thread. + fn into_iter(self) -> Self::IntoIter { + self.changes.into_iter() + } +@@ -205,8 +376,13 @@ impl IntoIterator for TreeDelta { + + impl<'a> IntoIterator for &'a TreeDelta { + type Item = &'a FileDelta; +- type IntoIter = SliceIter<'a, FileDelta>; ++ type IntoIter = std::slice::Iter<'a, FileDelta>; + ++ /// Borrows the `TreeDelta` and returns a borrowing iterator. ++ /// ++ /// # How it works ++ /// Delegates to [`TreeDelta::iter`], yielding `&FileDelta` items. This allows ++ /// ergonomic `for delta in &tree_delta` loops without consuming the struct. + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +diff --git a/libvctrl_handler/src/types/core/hash.rs b/libvctrl_handler/src/types/core/hash.rs +index e5f8162..faed018 100644 +--- a/libvctrl_handler/src/types/core/hash.rs ++++ b/libvctrl_handler/src/types/core/hash.rs +@@ -1,13 +1,78 @@ +-use core::fmt; +-use core::str::FromStr; ++//! Hash type. ++//! ++//! # Architecture ++//! This module defines the [`Hash`] type, a fixed-size wrapper around a 64-byte ++//! array (SHA-512). In a content-addressable storage (CAS) system, hashes are the ++//! primary keys for all objects and references. ++//! ++//! # Design Rationale: Stack Allocation ++//! By wrapping a fixed-size array `[u8; 64]` instead of using a `Vec` or `Box<[u8]>`, ++//! the [`Hash`] type is inherently `Copy` and requires no heap allocation. This is a ++//! critical performance optimization: hashes are created, copied, and compared millions ++//! of times during graph traversal and object packing. Keeping them on the stack ++//! eliminates allocator overhead and memory fragmentation. + + use crate::constants::HASH_LENGTH; + use crate::errors::VctrlError; ++use core::fmt; ++use core::str::FromStr; + ++/// A fixed-size hash (64 bytes, e.g., SHA-512). ++/// ++/// # Why this exists ++/// Provides a strongly-typed, length-guaranteed representation of a cryptographic hash. ++/// By encoding the length (64 bytes) directly into the type system via a constant ++/// generic array, the compiler guarantees that a [`Hash`] can never accidentally hold ++/// a 20-byte SHA-1 or a 32-byte SHA-256. This prevents entire classes of length-mismatch ++/// bugs at compile time. ++/// ++/// # How it works ++/// The struct is a tuple wrapping `[u8; HASH_LENGTH]`. It derives `PartialEq`, `Eq`, ++/// `Hash`, and `Ord`, allowing it to be used as a key in `HashMap` or `BTreeMap`. The ++/// `Copy` trait is derived, meaning assigning a hash to a new variable performs a fast ++/// 64-byte stack copy rather than a pointer move. ++/// ++/// # Examples ++/// ++/// Creating a hash from raw bytes: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::hash::Hash; ++/// # use libvctrl_handler::VctrlError; ++/// let raw_bytes = [0_u8; 64]; ++/// let hash = Hash::from_bytes(&raw_bytes)?; ++/// assert_eq!(hash.as_bytes(), &raw_bytes); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + pub struct Hash([u8; HASH_LENGTH]); + + impl Hash { ++ /// Creates a hash from a byte slice. ++ /// ++ /// # How it works ++ /// This function is `const`, meaning it can be evaluated at compile time if the ++ /// input slice is a static literal. Because `for` loops over slices were not fully ++ /// stable in `const fn` contexts during early Rust editions, this implementation ++ /// uses a `while` loop with an index to copy bytes into a fixed-size array. If the ++ /// slice length does not exactly match [`HASH_LENGTH`], an error is returned. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidHashLength`] if the slice length does not match [`HASH_LENGTH`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::VctrlError; ++ /// let valid_hash = Hash::from_bytes(&[1u8; 64]); ++ /// assert!(valid_hash.is_ok()); ++ /// ++ /// let invalid_hash = Hash::from_bytes(&[1u8; 32]); ++ /// assert!(invalid_hash.is_err()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[allow(clippy::indexing_slicing)] + pub const fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != HASH_LENGTH { +@@ -17,11 +82,26 @@ impl Hash { + let mut i = 0; + while i < HASH_LENGTH { + arr[i] = bytes[i]; +- i = i.wrapping_add(1); ++ i += 1; + } + Ok(Self(arr)) + } + ++ /// Returns the raw bytes of the hash. ++ /// ++ /// # How it works ++ /// Returns a reference to the inner fixed-size array. This avoids any slicing or ++ /// copying overhead, providing direct access to the underlying 64 bytes. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::VctrlError; ++ /// let hash = Hash::from_bytes(&[0xAB; 64])?; ++ /// assert_eq!(hash.as_bytes(), &[0xAB; 64]); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + #[must_use] + pub const fn as_bytes(&self) -> &[u8; HASH_LENGTH] { + &self.0 +@@ -29,6 +109,11 @@ impl Hash { + } + + impl From<[u8; HASH_LENGTH]> for Hash { ++ /// Converts a raw array into a [`Hash`]. ++ /// ++ /// # How it works ++ /// This infallible conversion wraps the array directly. It is used when the caller ++ /// already possesses a correctly sized array, bypassing the need for slice validation. + fn from(arr: [u8; HASH_LENGTH]) -> Self { + Self(arr) + } +@@ -37,12 +122,23 @@ impl From<[u8; HASH_LENGTH]> for Hash { + impl TryFrom<&[u8]> for Hash { + type Error = VctrlError; + ++ /// Attempts to convert a byte slice into a [`Hash`]. ++ /// ++ /// # How it works ++ /// Delegates to [`Hash::from_bytes`]. This trait implementation allows ergonomic ++ /// use of the `?` operator when converting from generic byte slices. + fn try_from(value: &[u8]) -> Result { + Self::from_bytes(value) + } + } + + impl AsRef<[u8]> for Hash { ++ /// Converts to a byte slice. ++ /// ++ /// # How it works ++ /// Allows the [`Hash`] to be used with APIs that expect `AsRef<[u8]>`, providing ++ /// interoperability with standard cryptographic and I/O crates without exposing ++ /// the internal array representation. + fn as_ref(&self) -> &[u8] { + &self.0 + } +@@ -51,6 +147,30 @@ impl AsRef<[u8]> for Hash { + impl FromStr for Hash { + type Err = VctrlError; + ++ /// Parses a hexadecimal string into a [`Hash`]. ++ /// ++ /// # How it works ++ /// Expects a string of exactly 128 characters (64 bytes * 2 hex chars). It iterates ++ /// through the string in 2-character chunks, parsing each chunk into a byte using ++ /// `u8::from_str_radix`. If any character is invalid hex, or if the length is wrong, ++ /// it returns an error. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidHashLength`] if the string length is not 128. ++ /// Returns [`VctrlError::CorruptedData`] if the string contains non-hexadecimal characters. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use std::str::FromStr; ++ /// # use libvctrl_handler::VctrlError; ++ /// let hex_str = "0".repeat(128); ++ /// let hash = Hash::from_str(&hex_str)?; ++ /// assert_eq!(hash.as_bytes(), &[0_u8; 64]); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn from_str(s: &str) -> Result { + if s.len() != HASH_LENGTH * 2 { + return Err(VctrlError::InvalidHashLength(s.len())); +@@ -69,6 +189,12 @@ impl FromStr for Hash { + } + + impl fmt::Debug for Hash { ++ /// Formats the hash for debugging purposes. ++ /// ++ /// # How it works ++ /// To prevent flooding debug logs with 128-character strings, this implementation ++ /// only prints the first 16 bytes (32 hex characters) followed by `...`. This provides ++ /// enough context to distinguish between different hashes while remaining readable. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Hash(")?; + for &byte in self.0.iter().take(16) { +@@ -79,6 +205,23 @@ impl fmt::Debug for Hash { + } + + impl fmt::Display for Hash { ++ /// Formats the hash as a full hexadecimal string. ++ /// ++ /// # How it works ++ /// Iterates over all 64 bytes, formatting each as a two-character zero-padded ++ /// hexadecimal value. This produces the canonical 128-character string representation ++ /// expected by Git tools. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::VctrlError; ++ /// use std::fmt::Display; ++ /// let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// assert_eq!(format!("{hash}"), "0".repeat(128)); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for &byte in &self.0 { + write!(f, "{byte:02x}")?; +diff --git a/libvctrl_handler/src/types/core/merge.rs b/libvctrl_handler/src/types/core/merge.rs +index ac2d38a..75cca02 100644 +--- a/libvctrl_handler/src/types/core/merge.rs ++++ b/libvctrl_handler/src/types/core/merge.rs +@@ -1,7 +1,50 @@ ++//! Merge-related types. ++//! ++//! # Architecture ++//! This module defines the data structures used to represent the outcome of a ++//! 3-way merge operation. A 3-way merge uses a common ancestor (the merge base) ++//! to reconcile changes between two divergent branches ("ours" and "theirs"). ++//! ++//! # Design Rationale: Hash-Based Conflicts ++//! The [`Conflict`] struct stores cryptographic hashes (`ancestor_blob`, `our_blob`, ++//! `their_blob`) rather than the raw file contents. This is a critical architectural ++//! decision for scalability. Merge orchestration can evaluate thousands of paths. ++//! By deferring the loading of actual blob bytes to a specialized merge driver ++//! (like `diff3`), the engine can quickly identify conflicts without exhausting ++//! memory on large binary files. ++ + use std::path::{Path, PathBuf}; + + use crate::Hash; + ++/// A conflict that occurred during a merge. ++/// ++/// # Why this exists ++/// Represents a single file path where the "ours" and "theirs" branches made ++/// conflicting changes relative to the common ancestor, preventing automatic ++/// resolution. This struct provides the necessary references for a UI or a ++/// text-merge tool to present the conflict to the user. ++/// ++/// # How it works ++/// The struct holds the file path and the [`Hash`] of the blob in each of the ++/// three merge stages: ++/// - `ancestor_blob`: The state of the file at the merge base. ++/// - `our_blob`: The state of the file in the current branch (HEAD). ++/// - `their_blob`: The state of the file in the branch being merged in. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::merge::Conflict; ++/// # use libvctrl_handler::Hash; ++/// # let ancestor = Hash::from_bytes(&[0_u8; 64])?; ++/// # let ours = Hash::from_bytes(&[1u8; 64])?; ++/// # let theirs = Hash::from_bytes(&[2u8; 64])?; ++/// let conflict = Conflict::new("src/main.rs".into(), ancestor, ours, theirs); ++/// assert_eq!(conflict.path(), std::path::Path::new("src/main.rs")); ++/// assert_eq!(conflict.our_blob(), ours); ++/// # Ok::<(), libvctrl_handler::VctrlError>(()) ++/// ``` + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct Conflict { + path: PathBuf, +@@ -11,6 +54,12 @@ pub struct Conflict { + } + + impl Conflict { ++ /// Creates a new conflict. ++ /// ++ /// # How it works ++ /// Initializes the conflict record with the path and the three corresponding ++ /// blob hashes. This is a `const fn`, allowing the construction of conflict ++ /// scenarios at compile time for testing purposes. + #[must_use] + pub const fn new(path: PathBuf, ancestor_blob: Hash, our_blob: Hash, their_blob: Hash) -> Self { + Self { +@@ -21,44 +70,120 @@ impl Conflict { + } + } + ++ /// Returns the path with a conflict. ++ /// ++ /// # How it works ++ /// Returns a reference to the `PathBuf` where the merge conflict occurred. + #[must_use] + pub fn path(&self) -> &Path { + &self.path + } + ++ /// Returns the ancestor blob hash. ++ /// ++ /// # How it works ++ /// Returns the `Hash` of the file content from the merge base (the common ++ /// ancestor commit). + #[must_use] + pub const fn ancestor_blob(&self) -> Hash { + self.ancestor_blob + } + ++ /// Returns the blob from the current branch. ++ /// ++ /// # How it works ++ /// Returns the `Hash` of the file content from the "ours" side of the merge ++ /// (typically the current `HEAD`). + #[must_use] + pub const fn our_blob(&self) -> Hash { + self.our_blob + } + ++ /// Returns the blob from the merging branch. ++ /// ++ /// # How it works ++ /// Returns the `Hash` of the file content from the "theirs" side of the merge ++ /// (the branch being merged into the current one). + #[must_use] + pub const fn their_blob(&self) -> Hash { + self.their_blob + } + } + ++/// The result of a merge operation. ++/// ++/// # Why this exists ++/// Acts as an Algebraic Data Type (ADT) to represent the binary outcome of a merge. ++/// By modeling the result as an enum, the Rust compiler forces the caller to ++/// explicitly handle both the success and conflict scenarios at compile time, ++/// preventing "forgotten conflict" bugs. ++/// ++/// # How it works ++/// - `Success(Hash)`: Indicates a clean merge. Contains the hash of the newly ++/// created root tree object. ++/// - `Conflicts(Vec)`: Indicates that one or more paths could not be ++/// merged automatically. Contains the list of conflicts to be resolved. ++/// ++/// # Examples ++/// ++/// Handling a successful merge: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::merge::MergeResult; ++/// # use libvctrl_handler::Hash; ++/// # let tree_hash = Hash::from_bytes(&[0_u8; 64])?; ++/// let result = MergeResult::Success(tree_hash); ++/// assert!(result.is_success()); ++/// assert!(result.conflicts().is_none()); ++/// # Ok::<(), libvctrl_handler::VctrlError>(()) ++/// ``` ++/// ++/// Handling a conflicted merge: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::merge::{Conflict, MergeResult}; ++/// # use libvctrl_handler::Hash; ++/// # let h = Hash::from_bytes(&[1u8; 64])?; ++/// let result = MergeResult::Conflicts(vec![Conflict::new("file.txt".into(), h, h, h)]); ++/// assert!(result.is_conflicts()); ++/// assert_eq!(result.conflicts().unwrap().len(), 1); ++/// # Ok::<(), libvctrl_handler::VctrlError>(()) ++/// ``` + #[derive(Debug, Clone, PartialEq, Eq)] + pub enum MergeResult { ++ /// The merge succeeded with the resulting tree hash. + Success(Hash), ++ /// The merge produced conflicts. + Conflicts(Vec), + } + + impl MergeResult { ++ /// Returns `true` if the merge succeeded. ++ /// ++ /// # How it works ++ /// Uses pattern matching to check if the result is the `Success` variant. ++ /// This is a `const fn`, incurring zero runtime overhead. + #[must_use] + pub const fn is_success(&self) -> bool { + matches!(self, Self::Success(_)) + } + ++ /// Returns `true` if the merge produced conflicts. ++ /// ++ /// # How it works ++ /// Uses pattern matching to check if the result is the `Conflicts` variant. ++ /// This is a `const fn`, incurring zero runtime overhead. + #[must_use] + pub const fn is_conflicts(&self) -> bool { + matches!(self, Self::Conflicts(_)) + } + ++ /// Returns the conflicts if any. ++ /// ++ /// # How it works ++ /// If the result is `Conflicts`, it returns `Some(&[Conflict])` borrowing from ++ /// the internal vector. If the result is `Success`, it returns `None`. This ++ /// avoids cloning the conflict data if the caller only needs to inspect it. + #[must_use] + pub fn conflicts(&self) -> Option<&[Conflict]> { + match self { +diff --git a/libvctrl_handler/src/types/core/mod.rs b/libvctrl_handler/src/types/core/mod.rs +index 6956f87..ab604cd 100644 +--- a/libvctrl_handler/src/types/core/mod.rs ++++ b/libvctrl_handler/src/types/core/mod.rs +@@ -1,26 +1,113 @@ ++//! Core data types for Git objects. ++//! ++//! # Architecture ++//! This module aggregates the fundamental, strongly-typed data structures that ++//! represent the Git object model. By separating these types into their own ++//! submodules (e.g., `blob`, `commit`, `tree`), the crate prevents the formation ++//! of a monolithic, unmanageable file. Each submodule encapsulates the specific ++//! validation logic and invariants for its domain. ++//! ++//! # Design Rationale: Immutable Domain Model ++//! All types exported from this module are immutable once constructed. Their ++//! constructors are fallible (`Result`-returning), enforcing strict invariants ++//! such as hash lengths, maximum sizes, and structural integrity (e.g., sorted ++//! tree entries). This guarantees that if an object exists in memory, it is ++//! structurally valid and safe to share across threads without external ++//! synchronization. ++//! ++//! # Facade Re-exports ++//! While definitions live in submodules, the types are re-exported directly here. ++//! This allows consumers to use ergonomic paths like `libvctrl_handler::types::core::Blob` ++//! instead of the deeper `libvctrl_handler::types::core::blob::Blob`. ++//! ++//! # Examples ++//! *Note: The following examples assume this crate is named `libvctrl_handler`.* ++//! ++//! ``` ++//! # use libvctrl_handler::types::core::{Blob, Hash, Tree}; ++//! # use libvctrl_handler::VctrlError; ++//! let raw_bytes = [0_u8; 64]; ++//! let hash = Hash::from_bytes(&raw_bytes)?; ++//! let blob = Blob::new(b"content".to_vec())?; ++//! let tree = Tree::new(vec![])?; ++//! ++//! assert_eq!(blob.size(), 7); ++//! assert!(tree.is_empty()); ++//! # Ok::<(), VctrlError>(()) ++//! ``` ++ ++/// Blob object representation. ++/// ++/// # Why this exists ++/// Git blobs represent the raw content of files. This submodule houses the ++/// [`Blob`](blob::Blob) type, which enforces size limits during construction ++/// to prevent memory exhaustion. + pub mod blob; + pub use blob::Blob; + ++/// Commit object and metadata representation. ++/// ++/// # Why this exists ++/// Commits link tree states together in a directed acyclic graph (DAG). This ++/// submodule houses [`Commit`](commit::Commit) and [`CommitMeta`](commit::CommitMeta), ++/// enforcing rules like maximum parent counts and duplicate parent detection. + pub mod commit; + pub use commit::{Commit, CommitMeta}; + ++/// Delta and change types. ++/// ++/// # Why this exists ++/// Represents structural differences between trees without loading entire file ++/// contents. Contains [`ChangeKind`](delta::ChangeKind), [`FileDelta`](delta::FileDelta), ++/// and [`TreeDelta`](delta::TreeDelta). + pub mod delta; + pub use delta::{ChangeKind, FileDelta, TreeDelta}; + ++/// Hash type. ++/// ++/// # Why this exists ++/// Provides a stack-allocated, `Copy` wrapper for 64-byte SHA-512 hashes via the ++/// [`Hash`](hash::Hash) type, eliminating heap allocations for object identifiers. + pub mod hash; + pub use hash::Hash; + ++/// Merge-related types. ++/// ++/// # Why this exists ++/// Represents the outcome of a 3-way merge operation. Contains ++/// [`Conflict`](merge::Conflict) and [`MergeResult`](merge::MergeResult). + pub mod merge; + pub use merge::{Conflict, MergeResult}; + ++/// Reflog entry type. ++/// ++/// # Why this exists ++/// Represents a single timestamped mutation in the reference history via the ++/// [`ReflogEntry`](reflog::ReflogEntry) type. + pub mod reflog; + pub use reflog::ReflogEntry; + ++/// Tag object representation. ++/// ++/// # Why this exists ++/// Annotated tags point to other objects (usually commits) and carry their own ++/// metadata. This submodule houses the [`Tag`](tag::Tag) type. + pub mod tag; + pub use tag::Tag; + ++/// Tree object and entry representation. ++/// ++/// # Why this exists ++/// Trees represent the directory structure, mapping names to modes and hashes. ++/// This submodule houses [`Tree`](tree::Tree) and [`TreeEntry`](tree::TreeEntry), ++/// enforcing Git's strict sorting and duplication rules. + pub mod tree; + pub use tree::{Tree, TreeEntry}; + ++/// User identity representation. ++/// ++/// # Why this exists ++/// Represents the `Name ` syntax used in commits and tags via the ++/// [`UserID`](user_id::UserID) type. + pub mod user_id; + pub use user_id::UserID; +diff --git a/libvctrl_handler/src/types/core/reflog.rs b/libvctrl_handler/src/types/core/reflog.rs +index f5dd33a..ef24a12 100644 +--- a/libvctrl_handler/src/types/core/reflog.rs ++++ b/libvctrl_handler/src/types/core/reflog.rs +@@ -1,6 +1,52 @@ ++//! Reflog entry type. ++//! ++//! # Architecture ++//! This module defines the [`ReflogEntry`] struct, which represents a single ++//! timestamped record in a reference log (reflog). Reflogs act as an append-only ++//! audit trail, tracking every mutation to a reference (e.g., commits, resets, ++//! checkouts). This history is crucial for recovering from accidental operations ++//! and for garbage collection pruning. ++//! ++//! # Design Rationale: Immutable State Transitions ++//! A [`ReflogEntry`] captures a state transition: it records the `old_id` and the ++//! `new_id` of a reference. By using `Option`, the type elegantly handles ++//! edge cases: ++//! - `old_id` is `None`: The reference was just created (born). ++//! - `new_id` is `None`: The reference was deleted (died). ++//! Once constructed, the entry is immutable, ensuring that the audit history ++//! cannot be tampered with. ++ + use crate::Hash; + use crate::errors::VctrlError; + ++/// A single entry in a reflog. ++/// ++/// # Why this exists ++/// Provides a strongly-typed, validated record of a reference update. By requiring ++/// construction via [`new`](Self::new), the crate guarantees that every `ReflogEntry` ++/// in memory adheres to temporal constraints (e.g., valid timezone offsets). This ++/// prevents malformed historical data from corrupting repository recovery tools. ++/// ++/// # How it works ++/// The struct stores the old and new hashes as `Option`. Because [`Hash`] is ++/// a `Copy` type (a 64-byte array wrapper), storing and copying these options is ++/// a fast stack operation. The `reason` is stored as an owned `String` to ensure ++/// the entry is self-contained and `'static` safe. ++/// ++/// # Examples ++/// ++/// Creating a reflog entry for a new commit: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::reflog::ReflogEntry; ++/// # use libvctrl_handler::Hash; ++/// # use libvctrl_handler::VctrlError; ++/// # let old_hash = Hash::from_bytes(&[0_u8; 64])?; ++/// # let new_hash = Hash::from_bytes(&[1u8; 64])?; ++/// let entry = ReflogEntry::new(Some(old_hash), Some(new_hash), "commit: Add feature".to_string(), 1600000000, 0)?; ++/// assert_eq!(entry.reason(), "commit: Add feature"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct ReflogEntry { + old_id: Option, +@@ -11,6 +57,30 @@ pub struct ReflogEntry { + } + + impl ReflogEntry { ++ /// Creates a new reflog entry. ++ /// ++ /// # How it works ++ /// Validates that the `timezone_offset` falls within the valid range of ++ /// -1440 to 1440 minutes (UTC-24:00 to UTC+24:00). This strict validation ++ /// prevents arithmetic overflows or logic errors during date formatting and ++ /// historical chronological sorting. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidTimezoneOffset`] if the offset is out of range (-1440..=1440). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::reflog::ReflogEntry; ++ /// # use libvctrl_handler::Hash; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// // Creating an entry for the birth of a reference (old_id is None) ++ /// let entry = ReflogEntry::new(None, Some(hash), "branch: Created from HEAD".to_string(), 0, 0)?; ++ /// assert!(entry.old_id().is_none()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new( + old_id: Option, + new_id: Option, +@@ -30,26 +100,52 @@ impl ReflogEntry { + }) + } + ++ /// Returns the old hash. ++ /// ++ /// # How it works ++ /// Returns `Option`. Because `Hash` is `Copy`, this returns a copy of ++ /// the hash rather than a reference, simplifying lifetime management. Returns ++ /// `None` if this entry records the creation of a new reference. + #[must_use] + pub const fn old_id(&self) -> Option { + self.old_id + } + ++ /// Returns the new hash. ++ /// ++ /// # How it works ++ /// Returns `Option`. Returns `None` if this entry records the deletion ++ /// of a reference. + #[must_use] + pub const fn new_id(&self) -> Option { + self.new_id + } + ++ /// Returns the reason for the change. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. This ++ /// avoids allocation when the caller only needs to read the reason. + #[must_use] + pub fn reason(&self) -> &str { + &self.reason + } + ++ /// Returns the timestamp of the change. ++ /// ++ /// # How it works ++ /// Returns the Unix timestamp (seconds since epoch) as an `i64`. This is a ++ /// `const fn`, allowing compile-time evaluation. + #[must_use] + pub const fn timestamp(&self) -> i64 { + self.timestamp + } + ++ /// Returns the timezone offset. ++ /// ++ /// # How it works ++ /// Returns the timezone offset in minutes as an `i16`. This is a `const fn`, ++ /// allowing compile-time evaluation. + #[must_use] + pub const fn timezone_offset(&self) -> i16 { + self.timezone_offset +diff --git a/libvctrl_handler/src/types/core/tag.rs b/libvctrl_handler/src/types/core/tag.rs +index 645040c..4267cac 100644 +--- a/libvctrl_handler/src/types/core/tag.rs ++++ b/libvctrl_handler/src/types/core/tag.rs +@@ -1,3 +1,18 @@ ++//! Tag object representation. ++//! ++//! # Architecture ++//! This module defines the [`Tag`] struct, which represents a Git annotated tag object. ++//! Unlike lightweight tags (which are simply references), an annotated tag is a full ++//! object in the object database. It stores metadata (tagger, timestamp, message) ++//! and points to another object (usually a commit). ++//! ++//! # Design Rationale: Security by Construction ++//! Tag names map directly to the filesystem (e.g., `refs/tags/v1.0`). Without strict ++//! validation, a malicious tag name like `../../etc/passwd` could cause path traversal ++//! vulnerabilities. The [`Tag::with_meta`] constructor enforces strict reference naming ++//! rules via [`validate_ref_name`](crate::validation::validate_ref_name), ensuring that ++//! a `Tag` instance cannot exist with an invalid or dangerous name. ++ + use super::commit::CommitMeta; + use super::hash::Hash; + use super::user_id::UserID; +@@ -5,6 +20,35 @@ use crate::constants::MAX_MESSAGE_LENGTH; + use crate::errors::VctrlError; + use crate::validation::validate_ref_name; + ++/// A Git tag object. ++/// ++/// # Why this exists ++/// Provides a strongly-typed, immutable representation of an annotated tag. Tags are ++/// used to mark specific points in history, such as release versions. By requiring ++/// construction via [`new`](Self::new) or [`with_meta`](Self::with_meta), the crate ++/// guarantees that every `Tag` in memory adheres to naming and size constraints, ++/// preventing filesystem corruption and memory exhaustion. ++/// ++/// # How it works ++/// The struct stores the tag's `name`, the `target` hash it points to, an optional ++/// `tagger` identity, a `message`, and temporal `meta`. It reuses [`CommitMeta`] ++/// for timestamp data to avoid duplicating temporal logic between commits and tags. ++/// ++/// # Examples ++/// ++/// Creating a valid annotated tag: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::tag::Tag; ++/// # use libvctrl_handler::types::core::hash::Hash; ++/// # use libvctrl_handler::types::core::user_id::UserID; ++/// # use libvctrl_handler::VctrlError; ++/// # let target = Hash::from_bytes(&[0_u8; 64])?; ++/// # let tagger = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; ++/// let tag = Tag::new("v1.0.0".to_string(), target, Some(tagger), "Initial release".to_string())?; ++/// assert_eq!(tag.name(), "v1.0.0"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Tag { + name: String, +@@ -15,6 +59,28 @@ pub struct Tag { + } + + impl Tag { ++ /// Creates a new tag with default metadata. ++ /// ++ /// # How it works ++ /// Delegates to [`with_meta`](Self::with_meta), passing a default [`CommitMeta`] ++ /// (timestamp 0, offset 0, no encoding). This is useful for testing or when ++ /// temporal metadata is injected later. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError`] if the name or message fails validation. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::tag::Tag; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let target = Hash::from_bytes(&[0_u8; 64])?; ++ /// let tag = Tag::new("v2.0".to_string(), target, None, "Release".to_string())?; ++ /// assert_eq!(tag.message(), "Release"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new( + name: String, + target: Hash, +@@ -24,6 +90,37 @@ impl Tag { + Self::with_meta(name, target, tagger, message, CommitMeta::default()) + } + ++ /// Creates a new tag with timestamp metadata. ++ /// ++ /// # How it works ++ /// Performs two critical validation steps: ++ /// 1. Checks the `name` against Git's reference naming rules using ++ /// [`validate_ref_name`](crate::validation::validate_ref_name). This rejects ++ /// names containing `..`, leading/trailing slashes, or control characters. ++ /// 2. Checks `message.len()` against [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH). ++ /// Uses `usize::try_from` to safely handle 32-bit architectures. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidName`] if the name violates Git reference rules. ++ /// Returns [`VctrlError::ExceededMaxSize`] if the message is too long. ++ /// ++ /// # Examples ++ /// ++ /// Detecting an invalid tag name: ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::tag::Tag; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::types::core::commit::CommitMeta; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let target = Hash::from_bytes(&[0_u8; 64])?; ++ /// # let meta = CommitMeta::default(); ++ /// // Names containing ".." are forbidden to prevent path traversal. ++ /// let result = Tag::with_meta("../evil".to_string(), target, None, "msg".to_string(), meta); ++ /// assert!(matches!(result, Err(VctrlError::InvalidName(_)))); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn with_meta( + name: String, + target: Hash, +@@ -47,26 +144,50 @@ impl Tag { + }) + } + ++ /// Returns the tag name. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. This ++ /// avoids allocation when the caller only needs to read the name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + ++ /// Returns the target hash. ++ /// ++ /// # How it works ++ /// Returns a reference to the [`Hash`] identifying the object this tag points to ++ /// (usually a commit). + #[must_use] + pub const fn target(&self) -> &Hash { + &self.target + } + ++ /// Returns the tagger, if any. ++ /// ++ /// # How it works ++ /// Returns `Option<&UserID>`. Lightweight tags might not have a tagger, but ++ /// annotated tags usually do. Returns `None` if the tagger was not specified. + #[must_use] + pub const fn tagger(&self) -> Option<&UserID> { + self.tagger.as_ref() + } + ++ /// Returns the tag message. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. + #[must_use] + pub fn message(&self) -> &str { + &self.message + } + ++ /// Returns the tag metadata. ++ /// ++ /// # How it works ++ /// Returns a reference to the [`CommitMeta`] struct containing timestamp and ++ /// timezone data for the tag's creation. + #[must_use] + pub const fn meta(&self) -> &CommitMeta { + &self.meta +diff --git a/libvctrl_handler/src/types/core/tree.rs b/libvctrl_handler/src/types/core/tree.rs +index 79a92f6..497b04e 100644 +--- a/libvctrl_handler/src/types/core/tree.rs ++++ b/libvctrl_handler/src/types/core/tree.rs +@@ -1,12 +1,46 @@ +-use core::cmp::Ordering; +-use std::collections::HashSet; ++//! Tree object and entry representation. ++//! ++//! # Architecture ++//! This module defines the [`Tree`] and [`TreeEntry`] structs, which represent ++//! directory listings in the Git object model. A tree maps names to modes and ++//! object hashes, forming the hierarchical structure of a repository snapshot. ++//! ++//! # Design Rationale: Canonical Sorting ++//! Git requires tree entries to be sorted in a very specific, canonical order to ++//! ensure that identical directory states always produce identical hashes. This ++//! module enforces that sorting rule via the private `compare_tree_entries` ++//! function. By sorting upon construction, the [`Tree::new`] method guarantees ++//! that any `Tree` instance in memory is immediately valid and ready for hashing. + + 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; + ++/// A single entry in a Git tree. ++/// ++/// # Why this exists ++/// Represents the atomic mapping between a filename, its filesystem mode ++/// ([`EntryKind`]), and its content hash ([`Hash`]). By requiring construction ++/// via [`new`](Self::new), the crate ensures that every entry name is validated, ++/// preventing path traversal vulnerabilities (e.g., names containing `/` or `..`). ++/// ++/// # Examples ++/// ++/// Creating a valid tree entry: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::tree::TreeEntry; ++/// # use libvctrl_handler::types::core::hash::Hash; ++/// # use libvctrl_handler::enums::EntryKind; ++/// # use libvctrl_handler::VctrlError; ++/// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++/// let entry = TreeEntry::new("main.rs".to_string(), EntryKind::Blob, hash)?; ++/// assert_eq!(entry.name(), "main.rs"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct TreeEntry { + name: String, +@@ -15,33 +49,99 @@ pub struct TreeEntry { + } + + impl TreeEntry { ++ /// Creates a new tree entry. ++ /// ++ /// # How it works ++ /// Delegates to [`validate_tree_entry_name`](crate::validation::validate_tree_entry_name) ++ /// to ensure the name is a single path component without forbidden characters. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidName`] if the entry name is invalid (e.g., contains slashes). ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::tree::TreeEntry; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::enums::EntryKind; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// assert!(TreeEntry::new("valid.txt".into(), EntryKind::Blob, hash).is_ok()); ++ /// assert!(TreeEntry::new("invalid/path.txt".into(), EntryKind::Blob, hash).is_err()); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new(name: String, kind: EntryKind, hash: Hash) -> Result { + validate_tree_entry_name(&name)?; + Ok(Self { name, kind, hash }) + } + ++ /// Returns the entry name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + ++ /// Returns the entry kind. + #[must_use] + pub const fn kind(&self) -> EntryKind { + self.kind + } + ++ /// Returns the hash of the entry. + #[must_use] + pub const fn hash(&self) -> &Hash { + &self.hash + } + } + ++/// A Git tree object (directory listing). ++/// ++/// Entries are always stored in Git-sorted order: tree entries (directories) ++/// are compared as if their name has a trailing `/` appended. ++/// ++/// # Why this exists ++/// Provides a strongly-typed, validated representation of a directory. By sorting ++/// and checking for duplicates upon construction, the [`Tree::new`] method acts as ++/// a gatekeeper, guaranteeing that any `Tree` instance in memory is structurally ++/// sound and ready to be serialized into a canonical format. + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct Tree { + entries: Vec, + } + + impl Tree { ++ /// Creates a new tree from a vector of entries. ++ /// ++ /// Entries are sorted according to Git tree ordering rules. ++ /// Duplicate entry names are rejected. ++ /// ++ /// # How it works ++ /// 1. Checks the entry count against [`MAX_TREE_ENTRIES`](crate::constants::MAX_TREE_ENTRIES). ++ /// 2. Sorts the entries in-place using `compare_tree_entries`. ++ /// 3. Scans for duplicate names using a sliding window (`windows(2)`), rejecting ++ /// the tree if any are found. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::ExceededMaxSize`] if the entry count exceeds `MAX_TREE_ENTRIES`. ++ /// Returns [`VctrlError::InvalidTreeStructure`] if duplicate names are found. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::tree::{Tree, TreeEntry}; ++ /// # use libvctrl_handler::types::core::hash::Hash; ++ /// # use libvctrl_handler::enums::EntryKind; ++ /// # use libvctrl_handler::VctrlError; ++ /// # let hash = Hash::from_bytes(&[0_u8; 64])?; ++ /// let e1 = TreeEntry::new("b.txt".into(), EntryKind::Blob, hash)?; ++ /// let e2 = TreeEntry::new("a.txt".into(), EntryKind::Blob, hash)?; ++ /// let tree = Tree::new(vec![e1, e2])?; ++ /// // Entries are sorted automatically ++ /// assert_eq!(tree.entries()[0].name(), "a.txt"); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new(entries: Vec) -> Result { + let max_entries = usize::try_from(MAX_TREE_ENTRIES).unwrap_or(usize::MAX); + if entries.len() > max_entries { +@@ -51,43 +151,62 @@ impl Tree { + ))); + } + +- let mut seen = HashSet::with_capacity(entries.len()); +- for entry in &entries { +- if !seen.insert(entry.name.clone()) { ++ 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 ++ { + return Err(VctrlError::InvalidTreeStructure(format!( + "duplicate entry name: '{}'", +- entry.name ++ first.name + ))); + } + } + +- let mut sorted = entries; +- sorted.sort_by(compare_tree_entries); +- + Ok(Self { entries: sorted }) + } + ++ /// Returns the tree entries in Git-sorted order. + #[must_use] + pub fn entries(&self) -> &[TreeEntry] { + &self.entries + } + ++ /// Returns the number of entries. + #[must_use] + pub const fn len(&self) -> usize { + self.entries.len() + } + ++ /// Returns `true` if the tree has no entries. + #[must_use] + pub const fn is_empty(&self) -> bool { + self.entries.is_empty() + } + ++ /// Looks up an entry by name. ++ /// ++ /// # How it works ++ /// Performs a linear scan. While binary search is possible due to the sorted ++ /// nature of the entries, linear scan is often faster for small vectors typical ++ /// of Git trees due to CPU cache locality. + #[must_use] + pub fn get(&self, name: &str) -> Option<&TreeEntry> { +- self.entries.iter().find(|entry| entry.name == name) ++ self.entries.iter().find(|e| e.name == name) + } + } + ++/// Compares two tree entries using Git ordering rules. ++/// ++/// Tree entries (directories) are compared as if their name has a ++/// trailing `/` appended. All other kinds use their name as-is. ++/// ++/// # How it works ++/// The function compares byte-by-byte. If one name is a prefix of the other, ++/// the shorter name is padded with a virtual `/` if it represents a tree. ++/// This ensures that `a` (blob) sorts before `a` (tree), which sorts before `ab` (blob). + #[inline] + fn compare_tree_entries(a: &TreeEntry, b: &TreeEntry) -> Ordering { + let a_bytes = a.name.as_bytes(); +@@ -95,8 +214,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().wrapping_add(usize::from(a_is_tree)); +- let b_len = b_bytes.len().wrapping_add(usize::from(b_is_tree)); ++ let a_len = a_bytes.len() + usize::from(a_is_tree); ++ let b_len = b_bytes.len() + usize::from(b_is_tree); + let min_len = a_len.min(b_len); + + for i in 0..min_len { +@@ -141,18 +260,13 @@ mod tests { + let e2 = TreeEntry::new("a".into(), EntryKind::Blob, h)?; + + let tree = Tree::new(vec![e1, e2])?; +- assert_eq!(tree.entries().first().map(TreeEntry::name), Some("a")); +- assert_eq!(tree.entries().get(1).map(TreeEntry::name), Some("b")); ++ assert_eq!(tree.entries().first().map(|e| e.name()), Some("a")); ++ assert_eq!(tree.entries().get(1).map(|e| e.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(()) + } + } +diff --git a/libvctrl_handler/src/types/core/user_id.rs b/libvctrl_handler/src/types/core/user_id.rs +index dc5502c..cf46707 100644 +--- a/libvctrl_handler/src/types/core/user_id.rs ++++ b/libvctrl_handler/src/types/core/user_id.rs +@@ -1,6 +1,47 @@ ++//! User identity representation. ++//! ++//! # Architecture ++//! This module defines the [`UserID`] struct, which represents the `Name ` ++//! syntax used in Git commits and tags. User identities are critical for audit ++//! trails and blame calculations. ++//! ++//! # Design Rationale: Security by Construction ++//! Git's internal text format relies on specific characters (like `<`, `>`, and `\n`) ++//! as delimiters. If a username or email contains these characters, it can corrupt ++//! the commit object structure or inject malicious headers. The [`UserID::new`] ++//! constructor acts as a strict validation gate. By rejecting empty strings, control ++//! characters, and missing `@` symbols at construction time, the crate guarantees ++//! that any `UserID` instance in memory is safe to serialize into a Git object. ++ + use crate::constants::MAX_NAME_LENGTH; + use crate::errors::VctrlError; + ++/// A user identity (author or committer). ++/// ++/// # Why this exists ++/// Provides a strongly-typed, validated wrapper around the `Name ` concept. ++/// By requiring construction via [`new`](Self::new), the crate ensures that every ++/// `UserID` adheres to length and character constraints. Once constructed, the ++/// identity is immutable, ensuring safe, concurrent sharing across threads. ++/// ++/// # How it works ++/// The struct stores the name and email as owned `String`s. The constructor ++/// performs a series of checks: it verifies that neither string is empty, neither ++/// exceeds [`MAX_NAME_LENGTH`](crate::constants::MAX_NAME_LENGTH), neither contains ++/// ASCII control characters (like newlines), and the email contains an `@` symbol. ++/// ++/// # Examples ++/// ++/// Creating a valid user identity: ++/// ++/// ``` ++/// # use libvctrl_handler::types::core::user_id::UserID; ++/// # use libvctrl_handler::VctrlError; ++/// let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; ++/// assert_eq!(user.name(), "Alice"); ++/// assert_eq!(user.email(), "alice@example.com"); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + #[derive(Clone, Debug, PartialEq, Eq)] + pub struct UserID { + name: String, +@@ -8,6 +49,32 @@ pub struct UserID { + } + + impl UserID { ++ /// Creates a new `UserID`. ++ /// ++ /// # How it works ++ /// Performs a multi-stage validation process: ++ /// 1. Checks `name` for emptiness, length limits (using `usize::try_from` for ++ /// 32-bit architecture safety), and ASCII control characters. ++ /// 2. Checks `email` for emptiness, length limits, ASCII control characters, ++ /// and the presence of an `@` symbol. ++ /// If any check fails, an error is returned and the original strings are dropped. ++ /// ++ /// # Errors ++ /// ++ /// Returns [`VctrlError::InvalidName`] if the name is empty, too long, or contains control characters. ++ /// Returns [`VctrlError::InvalidEmail`] if the email is empty, lacks `@`, or contains control characters. ++ /// ++ /// # Examples ++ /// ++ /// Handling an invalid email: ++ /// ++ /// ``` ++ /// # use libvctrl_handler::types::core::user_id::UserID; ++ /// # use libvctrl_handler::VctrlError; ++ /// let result = UserID::new("Bob".to_string(), "bob-example.com".to_string()); ++ /// assert!(matches!(result, Err(VctrlError::InvalidEmail(_)))); ++ /// # Ok::<(), VctrlError>(()) ++ /// ``` + pub fn new(name: String, email: String) -> Result { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + if name.is_empty() { +@@ -44,11 +111,21 @@ impl UserID { + Ok(Self { name, email }) + } + ++ /// Returns the user name. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. This ++ /// avoids allocation when the caller only needs to read the name. + #[must_use] + pub fn name(&self) -> &str { + &self.name + } + ++ /// Returns the email address. ++ /// ++ /// # How it works ++ /// Returns a string slice (`&str`) borrowing from the internal `String`. This ++ /// avoids allocation when the caller only needs to read the email. + #[must_use] + pub fn email(&self) -> &str { + &self.email +diff --git a/libvctrl_handler/src/types/mod.rs b/libvctrl_handler/src/types/mod.rs +index 2db0371..48a446b 100644 +--- a/libvctrl_handler/src/types/mod.rs ++++ b/libvctrl_handler/src/types/mod.rs +@@ -1,5 +1,65 @@ ++//! Core data types for Git objects. ++//! ++//! # Architecture ++//! This module serves as the central registry for strongly-typed, immutable ++//! representations of Git objects and domain concepts. By isolating these data ++//! structures into a dedicated `types` module, the crate separates its abstract ++//! contracts (in `traits`) from the concrete data carriers used in serialization, ++//! manipulation, and network transfer. ++//! ++//! # Design Rationale: Fallible Construction ++//! All types in this module enforce strict invariants during construction (e.g., ++//! [`Hash`] requires exactly 64 bytes, [`Commit`] rejects duplicate parents). By ++//! making constructors fallible (returning `Result`), the crate guarantees that ++//! invalid states are unrepresentable at runtime. Once constructed, the types are ++//! immutable, ensuring thread-safe sharing without external synchronization. ++//! ++//! # Facade Pattern ++//! This module acts as a facade. It delegates the definitions to the `core` ++//! submodule and selectively re-exports the public types to the top level. This ++//! provides a clean, flat namespace for consumers (e.g., `libvctrl_handler::types::Commit`) ++//! while keeping the internal module structure logically separated by domain. ++ ++/// Core data type definitions for Git objects and domain concepts. ++/// ++/// # Why this exists ++/// Houses the actual struct and enum definitions. Grouping these into a `core` ++/// submodule prevents the parent `types` module from becoming a monolithic file, ++/// allowing each object type (blob, tree, commit, etc.) to be developed and ++/// tested in isolation. ++/// ++/// # Examples ++/// ++/// ``` ++/// // The core submodule is accessible for advanced or internal use. ++/// use libvctrl_handler::types::core; ++/// ``` + pub mod core; + ++/// Re-exports of fundamental Git object types for ergonomic, flat access. ++/// ++/// # Why this exists ++/// Provides a flattened import path. Consumers can directly use ++/// `libvctrl_handler::types::Blob` instead of navigating the full ++/// `libvctrl_handler::types::core::blob::Blob` path. This reduces boilerplate in consumer ++/// code while keeping the internal module structure logically separated. ++/// ++/// # Examples ++/// ++/// Importing and using multiple core types: ++/// ++/// ``` ++/// # use libvctrl_handler::types::{Blob, Hash, Tree}; ++/// # use libvctrl_handler::VctrlError; ++/// let raw_bytes = [0_u8; 64]; ++/// let hash = Hash::from_bytes(&raw_bytes)?; ++/// let blob = Blob::new(b"content".to_vec())?; ++/// let tree = Tree::new(vec![])?; ++/// ++/// assert_eq!(blob.size(), 7); ++/// assert!(tree.is_empty()); ++/// # Ok::<(), VctrlError>(()) ++/// ``` + pub use core::{ + blob::Blob, + commit::{Commit, CommitMeta}, +diff --git a/libvctrl_handler/src/validation/hash.rs b/libvctrl_handler/src/validation/hash.rs +index e5f592f..51f08c4 100644 +--- a/libvctrl_handler/src/validation/hash.rs ++++ b/libvctrl_handler/src/validation/hash.rs +@@ -1,6 +1,59 @@ ++//! Hash validation utilities. ++//! ++//! # Architecture ++//! This module provides standalone validation for byte slices intended to be used ++//! as Git object hashes. It ensures that data read from untrusted sources (like ++//! network packfiles) is the correct length before attempting to construct a ++//! [`Hash`](crate::Hash) type. ++//! ++//! # Design Rationale: Compile-Time Evaluation ++//! The primary validation function is implemented as a `const fn`. This is a ++//! critical architectural decision: it allows validation to occur at compile time ++//! if the input byte slice is a known constant. This shifts the computational ++//! overhead to the compiler, achieving true zero-cost runtime validation for ++//! static data. ++ + use crate::constants::HASH_LENGTH; + use crate::errors::VctrlError; + ++/// Validates that a byte slice is exactly `HASH_LENGTH` bytes long. ++/// ++/// # Why this exists ++/// Git's SHA-512 implementation requires exactly 64 bytes. Passing a slice of ++/// incorrect length to a hash constructor would either cause a runtime panic ++/// (if using fixed-size array conversion) or silently produce an invalid hash. ++/// This function provides a safe, fallible boundary to verify length before ++/// memory allocation or cryptographic processing. ++/// ++/// # How it works ++/// As a `const fn`, this can be evaluated by the compiler. If the input is a ++/// static byte array (e.g., `b"..."`), the compiler can resolve the `Result` ++/// at compile time, eliminating the runtime branch entirely. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::InvalidHashLength`] if the slice length does not match ++/// [`HASH_LENGTH`]. ++/// ++/// # Examples ++/// ++/// Validating a correctly sized slice: ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_hash_bytes; ++/// let valid_hash = [0_u8; 64]; ++/// assert!(validate_hash_bytes(&valid_hash).is_ok()); ++/// ``` ++/// ++/// Handling an invalid slice: ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_hash_bytes; ++/// # use libvctrl_handler::VctrlError; ++/// let invalid_hash = [0_u8; 32]; ++/// let result = validate_hash_bytes(&invalid_hash); ++/// assert!(matches!(result, Err(VctrlError::InvalidHashLength(32)))); ++/// ``` + pub const fn validate_hash_bytes(bytes: &[u8]) -> Result<(), VctrlError> { + if bytes.len() != HASH_LENGTH { + return Err(VctrlError::InvalidHashLength(bytes.len())); +diff --git a/libvctrl_handler/src/validation/mod.rs b/libvctrl_handler/src/validation/mod.rs +index 7f580f2..351bdb6 100644 +--- a/libvctrl_handler/src/validation/mod.rs ++++ b/libvctrl_handler/src/validation/mod.rs +@@ -1,5 +1,76 @@ ++//! Pure validation functions for names, references, and hashes. ++//! ++//! # Architecture ++//! This module separates validation logic from data structure construction. By isolating ++//! these checks into pure, standalone functions, we adhere to the "fail-fast" principle: ++//! inputs are scrutinized before any memory allocation or state mutation occurs. ++//! ++//! # Design Rationale: Pure Functions vs. Constructors ++//! While constructors like [`Hash::from_bytes`](crate::Hash::from_bytes) also perform validation, ++//! extracting these checks into standalone functions allows consumers to validate raw, ++//! unstructured data (e.g., from network streams or untrusted user input) before deciding ++//! how to process it. This avoids partial commits of invalid data and makes the validation ++//! logic trivially testable without constructing the full object. ++//! ++//! # Safety and Performance ++//! These functions are entirely pure with no side effects. They operate on borrowed slices ++//! (`&str`, `&[u8]`) and perform zero heap allocations. The compiler aggressively inlines ++//! these checks when used within constructors, achieving zero-cost abstraction. ++//! ++//! # Examples ++//! *Note: The following examples assume this crate is named `libvctrl_handler`.* ++//! ++//! ``` ++//! # use libvctrl_handler::validation::validate_name; ++//! # use libvctrl_handler::VctrlError; ++//! let valid_name = "feature_branch"; ++//! assert!(validate_name(valid_name).is_ok()); ++//! ++//! let invalid_name = ""; ++//! assert!(matches!(validate_name(invalid_name), Err(VctrlError::InvalidName(_)))); ++//! ``` ++ ++/// Hash validation utilities. ++/// ++/// # Why this exists ++/// Provides standalone validation for byte slices intended to be used as Git object hashes. ++/// This ensures that data read from untrusted sources (like network packfiles) is the correct ++/// length and format before attempting to construct a [`Hash`](crate::Hash) type, preventing ++/// unbound allocations or cryptographic mismatches. + pub mod hash; ++ ++/// Name and reference validation utilities. ++/// ++/// # Why this exists ++/// Git has strict rules for naming references (branches, tags) and tree entries. ++/// For example, names cannot contain control characters, cannot be empty, and cannot ++/// contain certain path components like `..`. This module enforces these rules to prevent ++/// filesystem traversal vulnerabilities and repository corruption. + pub mod name; + ++/// Re-export of [`validate_hash_bytes`](hash::validate_hash_bytes) for ergonomic top-level access. ++/// ++/// Validates that a byte slice is the correct length to be a hash. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_hash_bytes; ++/// let valid_hash = [0_u8; 64]; ++/// assert!(validate_hash_bytes(&valid_hash).is_ok()); ++/// ``` + pub use hash::validate_hash_bytes; ++ ++/// Re-exports of name and reference validation utilities. ++/// ++/// Provides ergonomic access to functions that enforce Git naming rules. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::validation::{validate_name, validate_ref_name, validate_tree_entry_name}; ++/// assert!(validate_name("valid_name").is_ok()); ++/// assert!(validate_ref_name("refs/heads/main").is_ok()); ++/// assert!(validate_tree_entry_name("file.txt").is_ok()); ++/// ``` + pub use name::{validate_name, validate_ref_name, validate_tree_entry_name}; +diff --git a/libvctrl_handler/src/validation/name.rs b/libvctrl_handler/src/validation/name.rs +index 4a89992..51452d0 100644 +--- a/libvctrl_handler/src/validation/name.rs ++++ b/libvctrl_handler/src/validation/name.rs +@@ -1,8 +1,50 @@ +-use std::path::Path; ++//! Name and reference validation utilities. ++//! ++//! # Architecture ++//! Git has strict rules for naming references (branches, tags) and tree entries. ++//! This module enforces these rules to prevent filesystem traversal vulnerabilities, ++//! repository corruption, and ambiguity in revision parsing. ++//! ++//! # Design Rationale: Layered Validation ++//! Validation is structured hierarchically. [`validate_name`] provides baseline ++//! sanitization (length, emptiness, control characters). Specialized functions ++//! like [`validate_ref_name`] and [`validate_tree_entry_name`] build upon this ++//! baseline, adding domain-specific constraints. This prevents duplication and ++//! ensures all names are fundamentally safe before context-specific rules are applied. + + use crate::constants::MAX_NAME_LENGTH; + use crate::errors::VctrlError; ++use std::path::Path; + ++/// Validates a generic name. ++/// ++/// # Why this exists ++/// Establishes the minimum safety criteria for any string used as an identifier ++/// in the version control system. It prevents empty strings (which cause ambiguity), ++/// excessively long strings (which can exhaust memory or trigger filesystem errors), ++/// and ASCII control characters (which can corrupt terminal output or interprocess ++/// communication). ++/// ++/// # How it works ++/// The function checks the byte length of the string against [`MAX_NAME_LENGTH`]. ++/// Because [`MAX_NAME_LENGTH`] is a `u64`, it must be safely downcast to `usize` ++/// using `try_from` to support 32-bit architectures where `usize` is smaller than `u64`. ++/// It then iterates over the bytes to detect ASCII control characters (e.g., `\0`, `\n`, `\t`). ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::InvalidName`] if the name is empty, exceeds the maximum ++/// allowed length, or contains ASCII control characters. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_name; ++/// assert!(validate_name("valid_name").is_ok()); ++/// assert!(validate_name("").is_err()); ++/// assert!(validate_name(&"a".repeat(256)).is_err()); ++/// assert!(validate_name("invalid\nname").is_err()); ++/// ``` + pub fn validate_name(name: &str) -> Result<(), VctrlError> { + if name.is_empty() { + return Err(VctrlError::InvalidName("name is empty".into())); +@@ -21,49 +63,105 @@ pub fn validate_name(name: &str) -> Result<(), VctrlError> { + Ok(()) + } + ++/// Validates a reference name (e.g., branch or tag) strictly according to Git rules. ++/// ++/// # Why this exists ++/// Git references map directly to the filesystem (e.g., `.git/refs/heads/main`). ++/// Without strict validation, a malicious reference name could traverse the filesystem ++/// (e.g., `../../etc/passwd`) or create ambiguous revision queries (e.g., names ++/// containing `..` or `~`). This function enforces the rules defined in ++/// `git-check-ref-format`. ++/// ++/// # How it works ++/// It first applies baseline validation via [`validate_name`]. It then checks for ++/// forbidden sequences: ++/// - `..`: Prevents path traversal and ambiguous range specifiers. ++/// - `~`, `^`, `:`: Prevents ambiguity with revision specifiers (e.g., `HEAD~1`). ++/// - `.lock` extension: Prevents race conditions with Git's internal lock files. ++/// - Leading/trailing dots or slashes: Prevents hidden files or directory confusion. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::InvalidName`] if the name fails basic name validation ++/// or contains forbidden characters or patterns. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_ref_name; ++/// assert!(validate_ref_name("refs/heads/main").is_ok()); ++/// assert!(validate_ref_name("feature/branch").is_ok()); ++/// ++/// // Path traversal is forbidden ++/// assert!(validate_ref_name("refs/heads/../danger").is_err()); ++/// ++/// // Cannot end with .lock ++/// assert!(validate_ref_name("refs/heads/config.lock").is_err()); ++/// ``` + pub fn validate_ref_name(name: &str) -> Result<(), VctrlError> { + validate_name(name)?; +- +- if name == "@" { +- return Err(VctrlError::InvalidName("ref name cannot be '@'".into())); +- } +- +- if name.starts_with('/') || name.ends_with('/') || name.contains("//") { ++ 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")) ++ { + 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(()) + } + ++/// Validates a tree entry name strictly. ++/// ++/// # Why this exists ++/// A tree entry represents a single file or subdirectory. Its name must be a ++/// single path component, not a full path. Allowing path separators (`/` or `\`) ++/// or directory aliases (`.` or `..`) would corrupt the tree hierarchy by injecting ++/// implicit directories or allowing traversal outside the tree. ++/// ++/// # How it works ++/// After baseline validation via [`validate_name`], it scans for `/` and `\` ++/// characters and explicitly rejects the strings `.` and `..`. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::InvalidName`] if the name fails basic name validation ++/// or contains forbidden path characters or names. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_handler::validation::validate_tree_entry_name; ++/// assert!(validate_tree_entry_name("file.txt").is_ok()); ++/// assert!(validate_tree_entry_name("src").is_ok()); ++/// ++/// // Path separators are forbidden ++/// assert!(validate_tree_entry_name("dir/file.txt").is_err()); ++/// assert!(validate_tree_entry_name("dir\\file.txt").is_err()); ++/// ++/// // Directory aliases are forbidden ++/// assert!(validate_tree_entry_name(".").is_err()); ++/// assert!(validate_tree_entry_name("..").is_err()); ++/// ``` + pub fn validate_tree_entry_name(name: &str) -> Result<(), VctrlError> { + validate_name(name)?; + if name.contains('/') || name.contains('\\') || name == "." || name == ".." { +diff --git a/libvctrl_handler/tests/blob.rs b/libvctrl_handler/tests/blob.rs +deleted file mode 100644 +index bfe66d4..0000000 +--- a/libvctrl_handler/tests/blob.rs ++++ /dev/null +@@ -1,39 +0,0 @@ +-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())); +- let empty: &[u8] = &[]; +- assert!(blob.is_empty()); +- assert_eq!(blob.size(), 0); +- assert_eq!(blob.data(), empty); +-} +- +-#[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) +- ); +-} +diff --git a/libvctrl_handler/tests/commit.rs b/libvctrl_handler/tests/commit.rs +deleted file mode 100644 +index c678d4c..0000000 +--- a/libvctrl_handler/tests/commit.rs ++++ /dev/null +@@ -1,117 +0,0 @@ +-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")); +-} +diff --git a/libvctrl_handler/tests/commit_meta.rs b/libvctrl_handler/tests/commit_meta.rs +deleted file mode 100644 +index a550514..0000000 +--- a/libvctrl_handler/tests/commit_meta.rs ++++ /dev/null +@@ -1,35 +0,0 @@ +-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)); +-} +diff --git a/libvctrl_handler/tests/common/mod.rs b/libvctrl_handler/tests/common/mod.rs +deleted file mode 100644 +index 2ad43f8..0000000 +--- a/libvctrl_handler/tests/common/mod.rs ++++ /dev/null +@@ -1,17 +0,0 @@ +-#![allow(unreachable_pub)] +-#![allow(dead_code)] +-#![allow(clippy::panic)] +- +-pub fn ok(result: Result) -> T { +- match result { +- Ok(value) => value, +- Err(err) => panic!("expected Ok(..), got Err({err:?})"), +- } +-} +- +-pub fn err(result: Result) -> E { +- match result { +- Ok(value) => panic!("expected Err(..), got Ok({value:?})"), +- Err(err) => err, +- } +-} +diff --git a/libvctrl_handler/tests/delta.rs b/libvctrl_handler/tests/delta.rs +deleted file mode 100644 +index 3243f3d..0000000 +--- a/libvctrl_handler/tests/delta.rs ++++ /dev/null +@@ -1,157 +0,0 @@ +-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(); +- } +- } +-} +diff --git a/libvctrl_handler/tests/entry_kind.rs b/libvctrl_handler/tests/entry_kind.rs +deleted file mode 100644 +index b9d16bb..0000000 +--- a/libvctrl_handler/tests/entry_kind.rs ++++ /dev/null +@@ -1,34 +0,0 @@ +-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); +-} +diff --git a/libvctrl_handler/tests/errors.rs b/libvctrl_handler/tests/errors.rs +deleted file mode 100644 +index 3404907..0000000 +--- a/libvctrl_handler/tests/errors.rs ++++ /dev/null +@@ -1,123 +0,0 @@ +-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) +- ); +-} +diff --git a/libvctrl_handler/tests/hash.rs b/libvctrl_handler/tests/hash.rs +deleted file mode 100644 +index 9b0528e..0000000 +--- a/libvctrl_handler/tests/hash.rs ++++ /dev/null +@@ -1,110 +0,0 @@ +-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(')')); +-} +diff --git a/libvctrl_handler/tests/hash_validation.rs b/libvctrl_handler/tests/hash_validation.rs +index 5662ef5..cbe10de 100644 +--- a/libvctrl_handler/tests/hash_validation.rs ++++ b/libvctrl_handler/tests/hash_validation.rs +@@ -1,9 +1,8 @@ + #![allow(missing_docs)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::expect_used)] +-use criterion as _; + +-use core::error::Error as _; ++use core::error::Error as StdError; + use libvctrl_handler::*; + + fn make_hash(byte: u8) -> Hash { +diff --git a/libvctrl_handler/tests/tag_reflog.rs b/libvctrl_handler/tests/tag_reflog.rs +deleted file mode 100644 +index da5eef4..0000000 +--- a/libvctrl_handler/tests/tag_reflog.rs ++++ /dev/null +@@ -1,90 +0,0 @@ +-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) +- ); +-} +diff --git a/libvctrl_handler/tests/traits_index.rs b/libvctrl_handler/tests/traits_index.rs +deleted file mode 100644 +index 3024496..0000000 +--- a/libvctrl_handler/tests/traits_index.rs ++++ /dev/null +@@ -1,61 +0,0 @@ +-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)); +-} +diff --git a/libvctrl_handler/tests/tree.rs b/libvctrl_handler/tests/tree.rs +deleted file mode 100644 +index 4661f9c..0000000 +--- a/libvctrl_handler/tests/tree.rs ++++ /dev/null +@@ -1,88 +0,0 @@ +-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().first().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()); +-} +diff --git a/libvctrl_handler/tests/type_validation.rs b/libvctrl_handler/tests/type_validation.rs +index 05e814e..c8458c2 100644 +--- a/libvctrl_handler/tests/type_validation.rs ++++ b/libvctrl_handler/tests/type_validation.rs +@@ -1,7 +1,6 @@ + #![allow(missing_docs)] + #![allow(clippy::unwrap_used)] + #![allow(clippy::expect_used)] +-use criterion as _; + + use libvctrl_handler::*; + +diff --git a/libvctrl_handler/tests/user_id.rs b/libvctrl_handler/tests/user_id.rs +deleted file mode 100644 +index 48af122..0000000 +--- a/libvctrl_handler/tests/user_id.rs ++++ /dev/null +@@ -1,92 +0,0 @@ +-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}'")) +- ); +-} +diff --git a/libvctrl_handler/tests/validation.rs b/libvctrl_handler/tests/validation.rs +deleted file mode 100644 +index e35e294..0000000 +--- a/libvctrl_handler/tests/validation.rs ++++ /dev/null +@@ -1,116 +0,0 @@ +-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}'" +- ); +- } +-} +diff --git a/libvctrl_plumbing/Cargo.toml b/libvctrl_plumbing/Cargo.toml +index 123b55b..2457999 100644 +--- a/libvctrl_plumbing/Cargo.toml ++++ b/libvctrl_plumbing/Cargo.toml +@@ -13,7 +13,7 @@ keywords = ["version-control", "vcs", "plumbing"] + categories = ["development-tools"] + + [dependencies] +-libvctrl = { path = "../libvctrl", version = "2.1.3" } ++libvctrl = { path = "../libvctrl", version = "2.1.2" } + + [dev-dependencies] + libvctrl_core = { path = "../libvctrl_core", version = "3.0.0" } +diff --git a/libvctrl_plumbing/src/cat_file.rs b/libvctrl_plumbing/src/cat_file.rs +index 3c66f94..7e1ba97 100644 +--- a/libvctrl_plumbing/src/cat_file.rs ++++ b/libvctrl_plumbing/src/cat_file.rs +@@ -1,26 +1,209 @@ +-use alloc::sync::Arc; +-use core::fmt::Write as _; ++//! # Cat-File Plumbing Command ++//! ++//! This module implements the `cat-file` plumbing command, a fundamental ++//! building block for inspecting objects in a libvctrl repository. It provides ++//! both single-object queries and batch processing for integration with ++//! higher-level porcelain commands. ++//! ++//! ## Why this module exists ++//! ++//! Plumbing commands operate directly on object stores and decoders without ++//! user-friendly formatting. `cat-file` is essential for debugging, scripting, ++//! and implementing other commands that need to inspect raw object content or ++//! metadata. ++//! ++//! The module is designed to be backend-agnostic: it accepts any ++//! [`ObjectStore`] and any [`Decoder`] via trait objects, enabling the same ++//! logic to work with in-memory stores, filesystem stores, and custom ++//! decoders. ++//! ++//! ## How it works ++//! ++//! The core function [`cat_file`] resolves an object name (a 128-character ++//! hexadecimal SHA-512 hash), retrieves the encoded bytes from the store, ++//! decodes the type using a series of decoder attempts, and then produces ++//! output according to the requested [`CatFileMode`]. ++//! ++//! Batch mode ([`cat_file_batch`]) reads object names line-by-line and writes ++//! formatted information, optionally including pretty-printed content. It ++//! supports custom format strings and NUL-terminated input/output for robust ++//! scripting. ++//! ++//! ## Safety and correctness ++//! ++//! All parsing is strict: hashes must be exactly 128 hex characters, hex ++//! digits must be valid, and objects must decode successfully. Errors are ++//! returned as [`VctrlError`] rather than panicking, making the command safe ++//! to use in long-running processes. ++//! ++//! # Examples ++//! ++//! Retrieve the type of a stored blob: ++//! ++//! ``` ++//! # use libvctrl::{ ++//! # Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore, ++//! # }; ++//! # use libvctrl_core::codec::BinaryDecoder; ++//! # use libvctrl_plumbing::{cat_file, CatFileMode}; ++//! # use std::io::Cursor; ++//! # fn main() -> Result<(), libvctrl::VctrlError> { ++//! // Create a blob and store it. ++//! let blob = Blob::new(b"hello".to_vec())?; ++//! let mut encoded = Vec::new(); ++//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; ++//! let hash = Sha512Hasher.hash(encoded.as_slice())?; ++//! let mut store = MemoryStore::new(); ++//! store.put(&hash, &encoded)?; ++//! ++//! // Query its type. ++//! let hash_hex = hash.to_string(); ++//! let mut output = Vec::new(); ++//! cat_file( ++//! &store, ++//! &BinaryDecoder, ++//! &hash_hex, ++//! CatFileMode::ObjectType, ++//! &mut output, ++//! )?; ++//! assert_eq!(String::from_utf8(output).unwrap(), "blob\n"); ++//! # Ok(()) ++//! # } ++//! ``` + + use libvctrl::{Decoder, EntryKind, Hash, ObjectStore, VctrlError}; ++use std::fmt::Write; + use std::io::{BufRead, Write as IoWrite}; + +-#[derive(Debug, Clone, Copy)] ++/// Specifies the operation mode for the [`cat_file`] command. ++/// ++/// Each variant instructs the command to produce different output about a ++/// single object. The mode determines whether the object is checked for ++/// existence, its type is printed, its size is printed, its content is ++/// pretty-printed, or its raw bytes are emitted (optionally with a type ++/// check). ++/// ++/// # Examples ++/// ++/// Basic usage: ++/// ++/// ``` ++/// # use libvctrl_plumbing::{CatFileMode, ObjectType}; ++/// let mode = CatFileMode::PrettyPrint; ++/// let raw_blob = CatFileMode::Raw(ObjectType::Blob); ++/// ``` ++#[derive(Clone, Copy)] + pub enum CatFileMode { ++ /// Pretty-print the object content in a human-readable format. + PrettyPrint, ++ /// Print only the object type (one of `blob`, `tree`, `commit`, `tag`). + ObjectType, ++ /// Print the encoded object size in bytes. + ObjectSize, ++ /// Check existence only; produce no output, but return an error if the ++ /// object is missing or corrupted. + Exists, ++ /// Output the raw encoded bytes, optionally verifying the object type ++ /// matches the expected [`ObjectType`] parameter. + Raw(ObjectType), + } + ++/// Logical object types recognized by the version control system. ++/// ++/// This enum mirrors the types defined in `libvctrl_handler`, but is localized ++/// for plumbing command reporting. It is used to verify expected object types ++/// and to format type strings. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_plumbing::ObjectType; ++/// let blob = ObjectType::Blob; ++/// assert_eq!(blob, ObjectType::Blob); ++/// ``` + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + pub enum ObjectType { ++ /// A binary large object (file content). + Blob, ++ /// A directory tree. + Tree, ++ /// A commit object. + Commit, ++ /// An annotated tag object. + Tag, + } + ++/// Executes a single `cat-file` query against an object store. ++/// ++/// This function resolves `object_name` (a 128-character hexadecimal hash), ++/// retrieves the encoded bytes, decodes the object, and writes the requested ++/// output to `writer` based on `mode`. ++/// ++/// # Why this function exists ++/// ++/// Centralizes all `cat-file` logic so that every caller (CLI, library, ++/// batch mode) shares the same validation and formatting rules. ++/// ++/// # How it works ++/// ++/// 1. Parse `object_name` into a [`Hash`]. ++/// 2. Fetch the encoded bytes from `store`. ++/// 3. Depending on `mode`, either: ++/// - Return `Ok(())` for `Exists`. ++/// - Decode the type and print it for `ObjectType`. ++/// - Print the encoded length for `ObjectSize`. ++/// - Decode and pretty-print for `PrettyPrint`. ++/// - Verify the actual type matches `Raw(expected_type)` and then write the ++/// raw bytes. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError`] if: ++/// - `object_name` is not a valid 128-character hex string. ++/// - The object is not found in the store. ++/// - The encoded bytes fail to decode as any known object type. ++/// - The actual type does not match the expected type in `Raw` mode. ++/// - The writer fails. ++/// ++/// # Examples ++/// ++/// Pretty-print a stored commit: ++/// ++/// ``` ++/// # use libvctrl::{ ++/// # Commit, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore, ++/// # Hash, UserID, ++/// # }; ++/// # use libvctrl_core::codec::BinaryDecoder; ++/// # use libvctrl_plumbing::{cat_file, CatFileMode}; ++/// # use std::io::Cursor; ++/// # fn main() -> Result<(), libvctrl::VctrlError> { ++/// // Create a simple commit. ++/// let tree = Hash::from_bytes(&[0u8; 64])?; ++/// let author = UserID::new("alice".into(), "alice@example.com".into())?; ++/// let committer = UserID::new("bob".into(), "bob@example.com".into())?; ++/// let commit = Commit::new(tree, vec![], author, committer, "initial".into())?; ++/// ++/// // Encode, hash, and store. ++/// let mut encoded = Vec::new(); ++/// BinaryEncoder.encode_commit(&commit, &mut encoded)?; ++/// let hash = Sha512Hasher.hash(encoded.as_slice())?; ++/// let mut store = MemoryStore::new(); ++/// store.put(&hash, &encoded)?; ++/// ++/// // Pretty-print the commit. ++/// let mut output = Vec::new(); ++/// cat_file( ++/// &store, ++/// &BinaryDecoder, ++/// &hash.to_string(), ++/// CatFileMode::PrettyPrint, ++/// &mut output, ++/// )?; ++/// assert!(String::from_utf8(output).unwrap().contains("tree")); ++/// # Ok(()) ++/// # } ++/// ``` + pub fn cat_file( + store: &dyn ObjectStore, + decoder: &D, +@@ -31,30 +214,31 @@ pub fn cat_file( + let hash = parse_hash(object_name)?; + + let mut encoded = Vec::new(); +- let _ = store ++ store + .get(&hash)? + .read_to_end(&mut encoded) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + + match mode { + CatFileMode::Exists => Ok(()), + CatFileMode::ObjectType => { + let obj_type = decode_type(decoder, &encoded)?; + let type_str = obj_type_to_str(obj_type); +- writeln!(writer, "{type_str}").map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ writeln!(writer, "{type_str}") ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + Ok(()) + } + CatFileMode::ObjectSize => { + let _obj_type = decode_type(decoder, &encoded)?; + let size = encoded.len(); +- writeln!(writer, "{size}").map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ writeln!(writer, "{size}").map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + Ok(()) + } + CatFileMode::PrettyPrint => { + let content = pretty_print(decoder, &encoded)?; + writer + .write_all(content.as_bytes()) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + Ok(()) + } + CatFileMode::Raw(expected_type) => { +@@ -68,22 +252,110 @@ pub fn cat_file( + } + writer + .write_all(&encoded) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + Ok(()) + } + } + } + ++/// Configuration options for batch `cat-file` processing. ++/// ++/// This struct controls the output format, delimiters, buffering, and whether ++/// object content is included in each batch entry. ++/// ++/// # Examples ++/// ++/// ``` ++/// # use libvctrl_plumbing::BatchOptions; ++/// let mut opts = BatchOptions::default(); ++/// opts.format = Some("%(objectname) %(objecttype)".into()); ++/// opts.print_contents = true; ++/// ``` + #[allow(clippy::struct_excessive_bools)] +-#[derive(Debug, Default)] ++#[derive(Default)] + pub struct BatchOptions { ++ /// Optional custom format string. Placeholders `%(objectname)`, ++ /// `%(objecttype)`, and `%(objectsize)` are replaced. + pub format: Option, ++ /// If `true`, input and output lines are NUL-terminated instead of ++ /// newline-terminated. + pub nul_terminated: bool, ++ /// If `true`, follow symlinks when resolving object names (currently ++ /// unused; reserved for future expansion). + pub follow_symlinks: bool, ++ /// If `true`, buffer all output until the entire batch is processed, ++ /// then write it in one go. + pub buffer: bool, ++ /// If `true`, include pretty-printed object content after the info line. + pub print_contents: bool, + } + ++/// Processes a batch of `cat-file` requests from an input stream. ++/// ++/// Reads object names line-by-line (or NUL-separated depending on ++/// `options.nul_terminated`), retrieves each object, and writes formatted ++/// information (and optionally content) to the output stream. If an object is ++/// missing, a `"{name} missing"` line is emitted instead of aborting. ++/// ++/// # Why this function exists ++/// ++/// Batch mode enables efficient processing of many objects without repeated ++/// setup and teardown. It is commonly used by frontend commands and scripts. ++/// ++/// # How it works ++/// ++/// The function maintains an output buffer. For each input line, it calls ++/// [`handle_one_object`] to obtain the info string and optional content. If ++/// `options.buffer` is `false`, the buffer is flushed after each object; ++/// otherwise, it accumulates and is flushed once at the end. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError`] if: ++/// - An input line cannot be read. ++/// - An object name is not a valid hash. ++/// - An object cannot be retrieved or decoded. ++/// - The output writer fails. ++/// ++/// # Examples ++/// ++/// Process two blobs and print their types: ++/// ++/// ``` ++/// # use libvctrl::{ ++/// # Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore, ++/// # }; ++/// # use libvctrl_core::codec::BinaryDecoder; ++/// # use libvctrl_plumbing::{cat_file_batch, BatchOptions}; ++/// # use std::io::{BufReader, Cursor}; ++/// # fn main() -> Result<(), libvctrl::VctrlError> { ++/// // Create and store two blobs. ++/// let mut store = MemoryStore::new(); ++/// let mut hashes = Vec::new(); ++/// for content in [b"first".to_vec(), b"second".to_vec()] { ++/// let blob = Blob::new(content)?; ++/// let mut encoded = Vec::new(); ++/// BinaryEncoder.encode_blob(&blob, &mut encoded)?; ++/// let hash = Sha512Hasher.hash(encoded.as_slice())?; ++/// store.put(&hash, &encoded)?; ++/// hashes.push(hash.to_string()); ++/// } ++/// ++/// // Prepare batch input. ++/// let input = format!("{}\n{}\n", hashes[0], hashes[1]); ++/// let mut reader = BufReader::new(input.as_bytes()); ++/// let mut output = Vec::new(); ++/// let options = BatchOptions { ++/// format: Some("%(objecttype)".into()), ++/// ..Default::default() ++/// }; ++/// ++/// cat_file_batch(&store, &BinaryDecoder, &mut reader, &mut output, &options)?; ++/// let out_str = String::from_utf8(output).unwrap(); ++/// assert!(out_str.contains("blob\nblob")); ++/// # Ok(()) ++/// # } ++/// ``` + pub fn cat_file_batch( + store: &dyn ObjectStore, + decoder: &D, +@@ -100,7 +372,7 @@ pub fn cat_file_batch( + line.clear(); + if input + .read_line(&mut line) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))? ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))? + == 0 + { + break; +@@ -127,7 +399,7 @@ pub fn cat_file_batch( + if !options.buffer { + output + .write_all(&out_buf) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + out_buf.clear(); + } + } else { +@@ -137,7 +409,7 @@ pub fn cat_file_batch( + if !options.buffer { + output + .write_all(&out_buf) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + out_buf.clear(); + } + } +@@ -146,11 +418,21 @@ pub fn cat_file_batch( + if !out_buf.is_empty() { + output + .write_all(&out_buf) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + } + Ok(()) + } + ++/// Handles a single object lookup and formatting for batch mode. ++/// ++/// This helper retrieves the encoded object, decodes its type, builds the ++/// info string according to `options.format`, and optionally pretty-prints ++/// the content. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError`] if the hash is invalid, the object is missing, or ++/// decoding fails. + fn handle_one_object( + store: &dyn ObjectStore, + decoder: &D, +@@ -160,10 +442,10 @@ fn handle_one_object( + let hash = parse_hash(object_name)?; + + let mut encoded = Vec::new(); +- let _ = store ++ store + .get(&hash)? + .read_to_end(&mut encoded) +- .map_err(|e| VctrlError::IoError(Arc::new(e)))?; ++ .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + + let obj_type = decode_type(decoder, &encoded)?; + let obj_size = encoded.len() as u64; +@@ -185,6 +467,15 @@ fn handle_one_object( + Ok((info, content)) + } + ++/// Parses a 128-character hexadecimal string into a [`Hash`]. ++/// ++/// The hash must be exactly 128 hex digits (64 bytes). Any length mismatch or ++/// invalid hex character results in an error. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::Other`] if the length is not 128 or a hex digit is ++/// invalid. + fn parse_hash(s: &str) -> Result { + if s.len() != 128 { + let actual_len = s.len(); +@@ -192,25 +483,25 @@ fn parse_hash(s: &str) -> Result { + "invalid hash length: {actual_len} (expected 128)" + ))); + } +- + let mut bytes = [0u8; 64]; + for (i, byte) in bytes.iter_mut().enumerate() { +- let start = i +- .checked_mul(2) +- .ok_or_else(|| VctrlError::Other("hash index overflow".into()))?; +- let end = start +- .checked_add(2) +- .ok_or_else(|| VctrlError::Other("hash index overflow".into()))?; +- let hex_byte = s +- .get(start..end) +- .ok_or_else(|| VctrlError::Other("hash slice out of bounds".into()))?; ++ let hex_byte = &s[i * 2..i * 2 + 2]; + *byte = u8::from_str_radix(hex_byte, 16) + .map_err(|e| VctrlError::Other(format!("invalid hex character in hash: {e}")))?; + } +- + Hash::from_bytes(&bytes) + } + ++/// Attempts to decode an encoded object as one of the four object types. ++/// ++/// The decoder is tried in order: blob, tree, commit, tag. The first ++/// successful decode determines the type. If none succeed, an error is ++/// returned. ++/// ++/// # Errors ++/// ++/// Returns [`VctrlError::CorruptedData`] if the bytes do not correspond to a ++/// known object type. + fn decode_type(decoder: &D, encoded: &[u8]) -> Result { + if decoder.decode_blob(encoded).is_ok() { + return Ok(ObjectType::Blob); +@@ -227,6 +518,18 @@ fn decode_type(decoder: &D, encoded: &[u8]) -> Result(decoder: &D, encoded: &[u8]) -> Result { + if let Ok(blob) = decoder.decode_blob(encoded) { + return Ok(String::from_utf8_lossy(blob.data()).to_string()); +@@ -282,6 +585,7 @@ fn pretty_print(decoder: &D, encoded: &[u8]) -> Result &'static str { + match t { + ObjectType::Blob => "blob", +@@ -291,6 +595,9 @@ const fn obj_type_to_str(t: ObjectType) -> &'static str { + } + } + ++/// Returns the POSIX file mode corresponding to an [`EntryKind`]. ++/// ++/// This is used in tree pretty-printing to display the mode in octal. + const fn entry_mode(kind: EntryKind) -> u32 { + match kind { + EntryKind::Blob => 0o100_644, +@@ -302,6 +609,11 @@ const fn entry_mode(kind: EntryKind) -> u32 { + } + } + ++/// Formats the info line for batch output based on a custom format string. ++/// ++/// Replaces `%(objectname)`, `%(objecttype)`, and `%(objectsize)` with ++/// actual values. The `_mode` parameter is reserved for future use (e.g., ++/// `%(objectmode)`). + fn format_batch_info( + format: &str, + hash: &Hash, +diff --git a/libvctrl_plumbing/src/lib.rs b/libvctrl_plumbing/src/lib.rs +index b5660e9..661f120 100644 +--- a/libvctrl_plumbing/src/lib.rs ++++ b/libvctrl_plumbing/src/lib.rs +@@ -1,8 +1,94 @@ +-extern crate alloc; ++//! # libvctrl_plumbing ++//! ++//! Plumbing commands for the libvctrl version control system. ++//! ++//! This crate provides low-level commands that operate directly on object ++//! stores, references, and codecs. Unlike porcelain commands, plumbing ++//! commands expose detailed control and are intended for scripting and for ++//! building higher-level commands. ++//! ++//! ## Why this crate exists ++//! ++//! Version control systems separate low-level (plumbing) commands from ++//! high-level (porcelain) commands. Plumbing commands are stable, composable, ++//! and designed for programmatic use. They perform one job well and produce ++//! machine-readable output where possible. This crate implements those ++//! foundational commands using the unified facade provided by the ++//! [`libvctrl`](https://docs.rs/libvctrl) crate. ++//! ++//! ## Architecture ++//! ++//! The crate is organized by command modules: ++//! ++//! - [`cat_file`](crate::cat_file) — inspects object content and metadata by ++//! hash. ++//! ++//! Additional plumbing commands will follow the same pattern. Each module ++//! contains one or more public functions that accept trait objects ++//! (for example, `&dyn ObjectStore` and `&dyn Decoder`), making the commands ++//! backend-agnostic and independently testable. ++//! ++//! ## How it works ++//! ++//! A typical plumbing command: ++//! ++//! 1. Parses and validates its arguments. ++//! 2. Uses an [`ObjectStore`](libvctrl::ObjectStore) to fetch raw bytes. ++//! 3. Uses a [`Decoder`](libvctrl::Decoder) to interpret those bytes. ++//! 4. Writes the requested result to an output writer. ++//! ++//! This design allows the same command to run against any storage backend ++//! (in-memory, filesystem, remote) and any codec, as long as the appropriate ++//! traits are implemented. ++//! ++//! ## Safety and correctness ++//! ++//! All commands return [`VctrlError`](libvctrl::VctrlError) on failure and ++//! never panic on malformed user input. Output writers are used exclusively ++//! through [`std::io::Write`], and all I/O errors are propagated with their ++//! original error wrapped in the unified error type. ++//! ++//! ## Example ++//! ++//! The following example stores a blob and uses [`cat_file`] to query its ++//! type: ++//! ++//! ``` ++//! # use libvctrl::{Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, BinaryDecoder, Sha512Hasher, MemoryStore}; ++//! # use libvctrl_plumbing::{cat_file, CatFileMode}; ++//! # fn main() -> Result<(), libvctrl::VctrlError> { ++//! let blob = Blob::new(b"example".to_vec())?; ++//! ++//! let mut encoded = Vec::new(); ++//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; ++//! let hash = Sha512Hasher.hash(&mut encoded.as_slice())?; ++//! ++//! let mut store = MemoryStore::new(); ++//! store.put(&hash, &encoded)?; ++//! ++//! let mut out = Vec::new(); ++//! cat_file( ++//! &store, ++//! &BinaryDecoder, ++//! &hash.to_string(), ++//! CatFileMode::ObjectType, ++//! &mut out, ++//! )?; ++//! ++//! assert_eq!(String::from_utf8(out).unwrap(), "blob\n"); ++//! # Ok(()) ++//! # } ++//! ``` + + #[cfg(test)] + use libvctrl_core as _; + ++/// Plumbing command for inspecting object content and metadata. ++/// ++/// This module implements the `cat-file` command, which retrieves an object by ++/// its hash and prints its type, size, pretty-printed content, or raw bytes ++/// depending on the requested mode. It also supports batch processing of ++/// multiple objects with configurable formatting. + pub mod cat_file; + + pub use cat_file::{BatchOptions, CatFileMode, ObjectType, cat_file, cat_file_batch}; +diff --git a/libvctrl_plumbing/tests/cat_file_tests.rs b/libvctrl_plumbing/tests/cat_file_tests.rs +index cd2ad68..fb8d888 100644 +--- a/libvctrl_plumbing/tests/cat_file_tests.rs ++++ b/libvctrl_plumbing/tests/cat_file_tests.rs +@@ -1,15 +1,16 @@ + //! Integration tests for the cat-file plumbing command. + +-use std::io::Cursor; +- ++use libvctrl::{BinaryDecoder, BinaryEncoder}; + use libvctrl::{ +- BinaryDecoder, BinaryEncoder, Blob, Commit, Encoder, EntryKind, Hash, Hasher, MemoryStore, +- ObjectStore, Sha512Hasher, Tag, Tree, TreeEntry, UserID, VctrlError, ++ Blob, Commit, Encoder, EntryKind, Hash, Hasher, ObjectStore, Tag, Tree, TreeEntry, UserID, ++ VctrlError, + }; ++use libvctrl::{MemoryStore, Sha512Hasher}; + use libvctrl_core as _; + use libvctrl_plumbing::cat_file::{ + BatchOptions, CatFileMode, ObjectType, cat_file, cat_file_batch, + }; ++use std::io::Cursor; + + // Helper: build a minimal repository with one object of each type + struct TestRepo { +@@ -184,7 +185,7 @@ fn object_size() -> Result<(), VctrlError> { + let size: usize = utf8_string(out)? + .trim() + .parse::() +- .map_err(|e: core::num::ParseIntError| VctrlError::Other(e.to_string()))?; ++ .map_err(|e: std::num::ParseIntError| VctrlError::Other(e.to_string()))?; + assert!(size > 0); + Ok(()) + } +diff --git a/libvctrl_sha512/Cargo.toml b/libvctrl_sha512/Cargo.toml +index a8c27cf..301ba8e 100644 +--- a/libvctrl_sha512/Cargo.toml ++++ b/libvctrl_sha512/Cargo.toml +@@ -1,11 +1,11 @@ + [package] + name = "libvctrl_sha512" +-version = "3.1.0" ++version = "3.0.1" + edition = "2024" + rust-version = "1.96" + description = "Zero-dependency SHA512, HMAC-SHA512, HKDF-SHA512, and optional SHA384" + license = "ISC" +-authors = ["mroczect "] ++authors = ["mroczect { +@@ -10,61 +92,47 @@ macro_rules! impl_hmac { + padded: [u8; $block_size], + } + +- impl core::fmt::Debug for HMAC { +- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +- f.write_str("HMAC") +- } +- } +- +- impl zeroize::Zeroize for HMAC { +- fn zeroize(&mut self) { +- if let Some(ref mut ih) = self.ih { +- zeroize::Zeroize::zeroize(ih); +- } +- zeroize::Zeroize::zeroize(&mut self.padded); +- } +- } +- + impl Drop for HMAC { + fn drop(&mut self) { +- zeroize::Zeroize::zeroize(self); ++ if let Some(ref mut ih) = self.ih { ++ ih.zeroize(); ++ } ++ self.padded.fill(0); + } + } + +- #[allow(clippy::indexing_slicing)] + impl HMAC { +- fn prepare_key(key: &[u8]) -> [u8; $block_size] { +- let mut block_key = [0_u8; $block_size]; +- if key.len() > $block_size { +- let hash = zeroize::Zeroizing::new(<$hash_struct>::hash(key)); +- let hash_bytes = &*hash; +- block_key[..$output_size].copy_from_slice(&hash_bytes[..$output_size]); ++ fn prepare_key(k: &[u8]) -> [u8; $block_size] { ++ let mut block_key = [0u8; $block_size]; ++ if k.len() > $block_size { ++ let hash = <$hash_struct>::hash(k); ++ block_key[..$output_size].copy_from_slice(&hash[..$output_size]); + } else { +- block_key[..key.len()].copy_from_slice(key); ++ block_key[..k.len()].copy_from_slice(k); + } + block_key + } + + #[doc = "One-shot HMAC computation."] + #[must_use] +- pub fn mac, U: AsRef<[u8]>>(input: T, key: U) -> [u8; $output_size] { +- let mut hmac = Self::new(key); ++ pub fn mac, U: AsRef<[u8]>>(input: T, k: U) -> [u8; $output_size] { ++ let mut hmac = Self::new(k); + hmac.update(input); + hmac.finalize() + } + + #[doc = "Creates a new HMAC context from a secret key."] + #[must_use] +- pub fn new(key: impl AsRef<[u8]>) -> Self { +- let key = key.as_ref(); +- let mut block_key = Self::prepare_key(key); +- let mut padded = [0x36_u8; $block_size]; +- for (padded_byte, block_byte) in padded.iter_mut().zip(block_key.iter()) { +- *padded_byte ^= *block_byte; ++ pub fn new(k: impl AsRef<[u8]>) -> Self { ++ let k = k.as_ref(); ++ let mut block_key = Self::prepare_key(k); ++ let mut padded = [0x36u8; $block_size]; ++ for i in 0..$block_size { ++ padded[i] ^= block_key[i]; + } + let mut ih = <$hash_struct>::new(); + ih.update(&padded); +- zeroize::Zeroize::zeroize(&mut block_key); ++ block_key.fill(0); + HMAC { + ih: Some(ih), + padded, +@@ -81,18 +149,13 @@ macro_rules! impl_hmac { + #[doc = "Finalizes the HMAC and returns the authentication tag."] + #[must_use] + pub fn finalize(mut self) -> [u8; $output_size] { +- for padded_byte in self.padded.iter_mut() { +- *padded_byte ^= 0x6a; ++ for p in self.padded.iter_mut() { ++ *p ^= 0x6a; + } + let mut oh = <$hash_struct>::new(); + oh.update(&self.padded); +- let inner = zeroize::Zeroizing::new( +- self.ih +- .take() +- .unwrap_or_else(|| <$hash_struct>::new()) +- .finalize(), +- ); +- oh.update(&*inner); ++ let inner = self.ih.take().unwrap().finalize(); ++ oh.update(&inner); + oh.finalize() + } + +@@ -109,24 +172,55 @@ macro_rules! impl_hmac { + #[must_use] + pub fn verify, U: AsRef<[u8]>>( + input: T, +- key: U, ++ k: U, + expected: &[u8; $output_size], + ) -> bool { +- let mac = Self::mac(input, key); ++ let mac = Self::mac(input, k); + $crate::utils::verify(&mac, expected) + } + } + }; + } + ++/// Defines an HKDF (HMAC-based Extract-and-Expand Key Derivation Function) ++/// type based on the provided hash struct. ++/// ++/// # Why this macro exists ++/// ++/// HKDF is a key derivation function standardized in RFC 5869. It uses HMAC ++/// internally and can be instantiated with any hash function that has an ++/// associated HMAC implementation. This macro generates a complete `HKDF` ++/// type from a hash struct, output size, and block size. ++/// ++/// # How it works ++/// ++/// The macro expands to a struct named `HKDF` with two associated functions: ++/// ++/// - `extract` — computes a pseudorandom key (PRK) from the input key material ++/// and an optional salt. ++/// - `expand` — derives output keying material (OKM) of arbitrary length from ++/// the PRK and optional context info. ++/// ++/// The generated code enforces RFC 5869 limits on output length and PRK size. ++/// ++/// # Examples ++/// ++/// The `libvctrl_sha512` crate already instantiates this macro for SHA-512: ++/// ++/// ``` ++/// use libvctrl_sha512::HKDF; ++/// ++/// let prk = HKDF::extract(b"salt", b"input key material"); ++/// let mut okm = [0u8; 32]; ++/// HKDF::expand(&mut okm, prk, b"info"); ++/// assert_eq!(okm.len(), 32); ++/// ``` + #[macro_export] + macro_rules! impl_hkdf { + ($hash_struct:ty, $output_size:expr, $block_size:expr) => { + #[doc = concat!("HKDF key derivation using `", stringify!($hash_struct), "`.")] +- #[derive(Debug, Copy, Clone)] + pub struct HKDF; + +- #[allow(clippy::indexing_slicing)] + impl HKDF { + #[doc = "HKDF-Extract step. Returns a pseudorandom key (PRK)."] + #[inline] +@@ -138,67 +232,103 @@ macro_rules! impl_hkdf { + #[doc = "HKDF-Expand step. Fills `out` with output keying material."] + #[inline] + pub fn expand(out: &mut [u8], prk: impl AsRef<[u8]>, info: impl AsRef<[u8]>) { +- let prk = prk.as_ref(); + assert_eq!( +- prk.len(), ++ prk.as_ref().len(), + $output_size, + "HKDF expects a {}-byte PRK", + $output_size + ); + let info = info.as_ref(); +- let max_len = 255_usize.saturating_mul($output_size); ++ let mut counter: u8 = 1; + assert!( +- out.len() <= max_len, ++ out.len() < 0xff * $output_size, + "Requested output exceeds RFC 5869 limit" + ); +- let mut offset = 0_usize; +- let mut counter: u32 = 1; +- while offset < out.len() { +- let mut hmac = HMAC::new(prk); +- if offset != 0 { +- if let Some(prev) = out.get(offset.saturating_sub($output_size)..offset) { +- hmac.update(prev); +- } ++ let mut i = 0; ++ while i < out.len() { ++ let mut hmac = HMAC::new(&prk); ++ if i != 0 { ++ hmac.update(&out[i - $output_size..][..$output_size]); + } + hmac.update(info); +- let counter_byte = u8::try_from(counter).unwrap_or(0); +- hmac.update([counter_byte]); +- let block = zeroize::Zeroizing::new(hmac.finalize()); +- let left = core::cmp::min($output_size, out.len().saturating_sub(offset)); +- if let Some(dst) = out.get_mut(offset..offset.saturating_add(left)) { +- if let Some(src) = block.get(..left) { +- dst.copy_from_slice(src); +- } +- } +- offset = offset.saturating_add($output_size); +- counter = counter.wrapping_add(1); ++ hmac.update([counter]); ++ let left = core::cmp::min($output_size, out.len() - i); ++ out[i..][..left].copy_from_slice(&hmac.finalize()[..left]); ++ counter += 1; ++ i += $output_size; + } + } + } + }; + } + +-pub mod hkdf; ++/// HMAC implementation generated for SHA-512. ++/// ++/// This module contains the [`HMAC`](crate::HMAC) type, produced by the ++/// [`impl_hmac!`] macro. It provides HMAC-SHA512 one-shot and incremental ++/// authentication. + pub mod hmac; ++ ++/// HKDF implementation generated for SHA-512. ++/// ++/// This module contains the [`HKDF`](crate::HKDF) type, produced by the ++/// [`impl_hkdf!`] macro. It provides HKDF-SHA512 key derivation. ++pub mod hkdf; ++ ++/// SHA-512 hash function implementation. ++/// ++/// This module contains the [`Hash`](crate::Hash) type, which provides ++/// incremental and one-shot SHA-512 hashing, along with verification and ++/// zeroization support. + pub mod sha512; ++ ++/// Shared byte-order and verification helpers. ++/// ++/// This module contains the [`load_be`](crate::utils::load_be), ++/// [`store_be`](crate::utils::store_be), and ++/// [`verify`](crate::utils::verify) functions, as well as the ++/// [`BLOCKBYTES`](crate::utils::BLOCKBYTES) and ++/// [`BYTES`](crate::utils::BYTES) constants. + pub mod utils; + ++/// Optional SHA-384 implementation. ++/// ++/// This module is only available when the `sha384` feature is enabled. It ++/// contains a SHA-384 hash type generated from the SHA-512 core. + #[cfg(feature = "sha384")] + pub mod sha384; + +-pub use hkdf::HKDF; +-pub use hmac::HMAC; ++/// Re-export of the SHA-512 hash type. ++/// ++/// This makes the primary hash type directly available as ++/// `libvctrl_sha512::Hash`. + pub use sha512::Hash; ++ ++/// Re-export of the HMAC-SHA512 type. ++/// ++/// This makes the HMAC type directly available as ++/// `libvctrl_sha512::HMAC`. ++pub use hmac::HMAC; ++ ++/// Re-export of the HKDF-SHA512 type. ++/// ++/// This makes the HKDF type directly available as ++/// `libvctrl_sha512::HKDF`. ++pub use hkdf::HKDF; ++ ++/// Re-export of the SHA-512 utility constants. ++/// ++/// This provides convenient access to [`BLOCKBYTES`](crate::utils::BLOCKBYTES) ++/// and [`BYTES`](crate::utils::BYTES) at the crate root. + pub use utils::{BLOCKBYTES, BYTES}; + + #[cfg(test)] + mod tests { + use super::*; +- use criterion as _; + + #[test] + fn hmac_vectors() { +- let h = HMAC::mac([], [0_u8; 32]); ++ let h = HMAC::mac([], [0u8; 32]); + let expected: [u8; 64] = [ + 185, 54, 206, 232, 108, 159, 135, 170, 93, 60, 111, 46, 132, 203, 90, 66, 57, 165, 254, + 80, 72, 10, 110, 198, 107, 112, 171, 91, 31, 74, 198, 115, 12, 108, 81, 84, 33, 179, +@@ -206,9 +336,9 @@ mod tests { + 12, 178, 34, 71, 34, 93, 71, + ]; + assert_eq!(h, expected); +- assert!(HMAC::verify([], [0_u8; 32], &expected)); ++ assert!(HMAC::verify([], [0u8; 32], &expected)); + +- let h = HMAC::mac([42_u8; 69], []); ++ let h = HMAC::mac([42u8; 69], []); + let expected: [u8; 64] = [ + 56, 224, 189, 205, 65, 104, 107, 85, 241, 188, 253, 35, 238, 174, 69, 191, 206, 183, + 205, 71, 196, 180, 56, 122, 106, 55, 136, 7, 208, 183, 99, 67, 229, 213, 255, 154, 107, +@@ -216,12 +346,12 @@ mod tests { + 115, 59, 54, 91, 143, 143, 254, 220, + ]; + assert_eq!(h, expected); +- assert!(HMAC::verify([42_u8; 69], [], &expected)); ++ assert!(HMAC::verify([42u8; 69], [], &expected)); + } + + #[test] + fn hkdf_vector() { +- let ikm = [0x0b_u8; 22]; ++ let ikm = [0x0bu8; 22]; + let salt: [u8; 13] = [ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, + ]; +@@ -232,7 +362,7 @@ mod tests { + 0x14, 0x81, 0x57, 0x93, 0x38, 0xda, 0x36, 0x2c, 0xb8, 0xd9, 0xf9, 0x25, 0xd7, 0xcb, + ]; + let prk = HKDF::extract(salt, ikm); +- let mut okm = [0_u8; 42]; ++ let mut okm = [0u8; 42]; + HKDF::expand(&mut okm, prk, info); + assert_eq!(okm, expected); + } +diff --git a/libvctrl_sha512/src/sha384.rs b/libvctrl_sha512/src/sha384.rs +index 0be7a85..3749304 100644 +--- a/libvctrl_sha512/src/sha384.rs ++++ b/libvctrl_sha512/src/sha384.rs +@@ -1,9 +1,34 @@ +-#![allow(clippy::indexing_slicing)] +-#![allow(clippy::arithmetic_side_effects)] ++//! # SHA-384 Hash ++//! ++//! This module provides the SHA-384 cryptographic hash function as specified ++//! in FIPS 180-4. SHA-384 is a variant of SHA-512 that uses a different ++//! initialization vector and truncates the final digest to 48 bytes. ++//! ++//! ## Design rationale ++//! ++//! SHA-384 shares the same compression function and message schedule as ++//! SHA-512. Instead of duplicating the core algorithm, this module wraps ++//! [`crate::sha512::Hash`] and overrides only the initialization vector and ++//! output length. This reduces code size, simplifies auditing, and guarantees ++//! consistency between the two hash functions. ++//! ++//! ## How it works ++//! ++//! The [`Hash`] struct holds an internal [`crate::sha512::Hash`] instance with ++//! a custom state. During finalization, the full 64-byte SHA-512 digest is ++//! computed and then truncated to the first 48 bytes. ++//! ++//! The module also invokes the [`impl_hmac!`] and [`impl_hkdf!`] macros to ++//! generate HMAC-SHA-384 and HKDF-SHA-384 implementations. + + use crate::sha512::{Hash as Sha512Hash, State}; + use crate::utils::load_be; + ++/// Creates a SHA-384 initialization vector. ++/// ++/// This internal helper constructs a [`State`] from the SHA-384 initial ++/// hash values defined in FIPS 180-4. It returns a state that will be used ++/// as the starting point for SHA-384 compression. + #[inline] + fn new_state() -> State { + const IV: [u8; 64] = [ +@@ -13,68 +38,175 @@ fn new_state() -> State { + 0x58, 0x15, 0x11, 0xdb, 0x0c, 0x2e, 0x0d, 0x64, 0xf9, 0x8f, 0xa7, 0x47, 0xb5, 0x48, 0x1d, + 0xbe, 0xfa, 0x4f, 0xa4, + ]; +- let mut state = [0_u64; 8]; +- for (index, word) in state.iter_mut().enumerate() { +- *word = load_be(&IV, index * 8); ++ let mut t = [0u64; 8]; ++ for (i, e) in t.iter_mut().enumerate() { ++ *e = load_be(&IV, i * 8); + } +- State(state) ++ State(t) + } + ++/// SHA-384 hash context. ++/// ++/// This struct represents an incremental SHA-384 computation. It wraps ++/// [`crate::sha512::Hash`] with a SHA-384-specific initialization vector and ++/// truncates the final digest to 48 bytes. ++/// ++/// # Why this struct exists ++/// ++/// SHA-384 is defined as a truncated SHA-512 with a different IV. By ++/// embedding the SHA-512 core, this struct avoids code duplication and ++/// ensures the two algorithms stay synchronized. ++/// ++/// # How it works ++/// ++/// The internal SHA-512 state is initialized with [`new_state`]. Updates ++/// are forwarded to the inner hash. Finalization computes the full 64-byte ++/// SHA-512 digest and returns only the first 48 bytes. ++/// ++/// # Examples ++/// ++/// Incremental hashing: ++/// ++/// ``` ++/// # use libvctrl_sha512::sha384::Hash; ++/// let mut h = Hash::new(); ++/// h.update(b"hello "); ++/// h.update(b"world"); ++/// let digest = h.finalize(); ++/// assert_eq!(digest.len(), 48); ++/// ``` ++/// ++/// One-shot hashing: ++/// ++/// ``` ++/// # use libvctrl_sha512::sha384::Hash; ++/// let digest = Hash::hash(b"abc"); ++/// assert_eq!(digest.len(), 48); ++/// ``` + #[derive(Clone)] + pub struct Hash(Sha512Hash); + +-impl core::fmt::Debug for Hash { +- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +- f.write_str("Hash") +- } +-} +- + impl Hash { ++ /// Creates a new SHA-384 hash context. ++ /// ++ /// The context is initialized with the SHA-384 initialization vector and ++ /// zero length. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let mut h = Hash::new(); ++ /// h.update(b"data"); ++ /// let digest = h.finalize(); ++ /// assert_eq!(digest.len(), 48); ++ /// ``` + #[must_use] + pub fn new() -> Self { + Self(Sha512Hash { + state: new_state(), + r: 0, +- w: [0_u8; 128], ++ w: [0u8; 128], + len: 0, + }) + } + ++ /// Internal update method shared with the wrapped SHA-512 core. ++ /// ++ /// This method is `pub(crate)` and not part of the public API. It forwards ++ /// the input to the inner SHA-512 hash. + pub(crate) fn update_inner>(&mut self, input: T) { + self.0.update_inner(input); + } + ++ /// Feeds data into the SHA-384 computation. ++ /// ++ /// This method can be called multiple times. The input is processed ++ /// immediately; no internal buffering beyond the SHA-512 block size is ++ /// performed. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let mut h = Hash::new(); ++ /// h.update(b"chunk1"); ++ /// h.update(b"chunk2"); ++ /// let digest = h.finalize(); ++ /// assert_eq!(digest.len(), 48); ++ /// ``` + pub fn update>(&mut self, input: T) { + self.update_inner(input); + } + ++ /// Finalizes the SHA-384 computation and returns the 48-byte digest. ++ /// ++ /// This consumes the context. The full 64-byte SHA-512 digest is computed ++ /// and truncated to the first 48 bytes. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let digest = Hash::hash(b"abc"); ++ /// assert_eq!(digest.len(), 48); ++ /// ``` + #[must_use] + pub fn finalize(self) -> [u8; 48] { +- let mut out = [0_u8; 48]; +- let full = zeroize::Zeroizing::new(self.0.finalize()); +- out.copy_from_slice(&full[..48]); ++ let mut out = [0u8; 48]; ++ out.copy_from_slice(&self.0.finalize()[..48]); + out + } + +- #[must_use] ++ /// One-shot SHA-384 hash computation. ++ /// ++ /// This convenience method creates a new context, feeds the entire input, ++ /// finalizes it, and returns the digest. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let digest = Hash::hash(b"hello"); ++ /// assert_eq!(digest.len(), 48); ++ /// ``` + pub fn hash>(input: T) -> [u8; 48] { +- let mut hasher = Self::new(); +- hasher.update(input); +- hasher.finalize() +- } +- ++ let mut h = Self::new(); ++ h.update(input); ++ h.finalize() ++ } ++ ++ /// Zeroizes the internal state. ++ /// ++ /// This method clears the wrapped SHA-512 state and any buffered data, ++ /// preventing sensitive information from remaining in memory. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let mut h = Hash::new(); ++ /// h.update(b"secret"); ++ /// h.zeroize(); ++ /// ``` + pub fn zeroize(&mut self) { +- zeroize::Zeroize::zeroize(self); +- } +-} +- +-impl zeroize::Zeroize for Hash { +- fn zeroize(&mut self) { +- zeroize::Zeroize::zeroize(&mut self.0); ++ self.0.zeroize(); + } + } + + impl Default for Hash { ++ /// Creates a default SHA-384 hash context. ++ /// ++ /// This is equivalent to calling [`Hash::new`]. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// # use libvctrl_sha512::sha384::Hash; ++ /// let h = Hash::default(); ++ /// let digest = h.finalize(); ++ /// assert_eq!(digest.len(), 48); ++ /// ``` + fn default() -> Self { + Self::new() + } +@@ -82,70 +214,3 @@ impl Default for Hash { + + impl_hmac!(Hash, 48, 128); + impl_hkdf!(Hash, 48, 128); +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- #[test] +- fn test_hash_empty_vector() { +- let expected: [u8; 48] = [ +- 0x38, 0xb0, 0x60, 0xa7, 0x51, 0xac, 0x96, 0x38, 0x4c, 0xd9, 0x32, 0x7e, 0xb1, 0xb1, +- 0xe3, 0x6a, 0x21, 0xfd, 0xb7, 0x11, 0x14, 0xbe, 0x07, 0x43, 0x4c, 0x0c, 0xc7, 0xbf, +- 0x63, 0xf6, 0xe1, 0xda, 0x27, 0x4e, 0xde, 0xbf, 0xe7, 0x6f, 0x65, 0xfb, 0xd5, 0x1a, +- 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b, +- ]; +- assert_eq!(Hash::hash(b""), expected); +- } +- +- #[test] +- fn test_hash_abc_vector() { +- let expected: [u8; 48] = [ +- 0xcb, 0x00, 0x75, 0x3f, 0x45, 0xa3, 0x5e, 0x8b, 0xb5, 0xa0, 0x3d, 0x69, 0x9a, 0xc6, +- 0x50, 0x07, 0x27, 0x2c, 0x32, 0xab, 0x0e, 0xde, 0xd1, 0x63, 0x1a, 0x8b, 0x60, 0x5a, +- 0x43, 0xff, 0x5b, 0xed, 0x80, 0x86, 0x07, 0x2b, 0xa1, 0xe7, 0xcc, 0x23, 0x58, 0xba, +- 0xec, 0xa1, 0x34, 0xc8, 0x25, 0xa7, +- ]; +- assert_eq!(Hash::hash(b"abc"), expected); +- } +- +- #[test] +- fn test_hmac_sha384_rfc4231_case1() { +- let key = [0x0b_u8; 20]; +- let data = b"Hi There"; +- let mac = HMAC::mac(data, key); +- let expected: [u8; 48] = [ +- 0xaf, 0xd0, 0x39, 0x44, 0xd8, 0x48, 0x95, 0x62, 0x6b, 0x08, 0x25, 0xf4, 0xab, 0x46, +- 0x90, 0x7f, 0x15, 0xf9, 0xda, 0xdb, 0xe4, 0x10, 0x1e, 0xc6, 0x82, 0xaa, 0x03, 0x4c, +- 0x7c, 0xeb, 0xc5, 0x9c, 0xfa, 0xea, 0x9e, 0xa9, 0x07, 0x6e, 0xde, 0x7f, 0x4a, 0xf1, +- 0x52, 0xe8, 0xb2, 0xfa, 0x9c, 0xb6, +- ]; +- assert_eq!(mac, expected); +- } +- +- #[test] +- fn test_hkdf_extract_and_expand_basic() { +- let prk = HKDF::extract(b"salt", b"ikm"); +- assert_eq!(prk.len(), 48); +- +- let mut out_a = [0_u8; 16]; +- let mut out_b = [0_u8; 16]; +- HKDF::expand(&mut out_a, prk, b"info-a"); +- HKDF::expand(&mut out_b, prk, b"info-b"); +- assert_ne!(out_a, out_b); +- } +- +- #[test] +- #[should_panic(expected = "HKDF expects a 48-byte PRK")] +- fn test_hkdf_expand_wrong_prk_length_panics() { +- let mut out = [0_u8; 16]; +- HKDF::expand(&mut out, [0_u8; 16], b""); +- } +- +- #[test] +- #[should_panic(expected = "Requested output exceeds RFC 5869 limit")] +- fn test_hkdf_expand_output_too_large_panics() { +- let mut out = [0_u8; 12_241]; +- HKDF::expand(&mut out, [0_u8; 48], b""); +- } +-} +diff --git a/libvctrl_sha512/src/sha512.rs b/libvctrl_sha512/src/sha512.rs +index 1b4320a..ec10fe6 100644 +--- a/libvctrl_sha512/src/sha512.rs ++++ b/libvctrl_sha512/src/sha512.rs +@@ -1,53 +1,128 @@ + #![allow(clippy::inline_always)] +-#![allow(clippy::indexing_slicing)] +-#![allow(clippy::arithmetic_side_effects)] ++//! Pure Rust implementation of the SHA-512 cryptographic hash function. ++//! ++//! # Why this module exists ++//! ++//! This module provides a zero-dependency, `no_std`-compatible implementation ++//! of SHA-512 as specified in FIPS 180-4. It is the foundational primitive ++//! used by higher-level constructs such as HMAC and HKDF within this crate. ++//! ++//! The implementation emphasizes: ++//! - **Incremental hashing** through the [`Hash`] state machine, allowing ++//! large inputs to be processed in chunks without loading everything into ++//! memory. ++//! - **Constant-time verification** for comparing digests, mitigating timing ++//! side-channel attacks. ++//! - **Zeroization** of sensitive state after use, preventing residual data ++//! from lingering in memory. ++//! ++//! # How it works ++//! ++//! SHA-512 follows the Merkle–Damgård construction with a 1024-bit block size ++//! and a 512-bit output. The internal state consists of eight 64-bit working ++//! variables (`a` through `h`) initialized with the first 64 bits of the ++//! fractional parts of the square roots of the first eight prime numbers. ++//! ++//! For each 128-byte block, the message schedule expands 16 initial words into ++//! 80 round words using bitwise rotations and modular additions. The ++//! compression function then updates the working variables using the standard ++//! SHA-512 logical functions (`Ch`, `Maj`, `Σ0`, `Σ1`, `σ0`, `σ1`) and ++//! per-round constants derived from the cube roots of the first 80 primes. ++//! ++//! Padding appends a single `0x80` byte, zeros, and a 128-bit big-endian length ++//! before finalization. The final digest is the concatenation of the eight ++//! 64-bit state words in big-endian order. ++//! ++//! # Examples ++//! ++//! Compute the SHA-512 digest of `"abc"`: ++//! ++//! ``` ++//! use libvctrl_sha512::Hash; ++//! ++//! let digest = Hash::hash(b"abc"); ++//! let expected: [u8; 64] = [ ++//! 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, ++//! 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, 0x41, 0x31, ++//! 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, ++//! 0x0a, 0x9e, 0xee, 0xe6, 0x4b, 0x55, 0xd3, 0x9a, ++//! 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, ++//! 0x36, 0xba, 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, ++//! 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, ++//! 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f, ++//! ]; ++//! assert_eq!(digest, expected); ++//! ``` + + use crate::utils::{load_be, store_be, verify}; + ++/// Internal message schedule for the SHA-512 compression function. ++/// ++/// This struct holds the 16 64-bit words of the current block. It provides ++/// the logical functions and message expansion routine required by FIPS 180-4. + struct W([u64; 16]); + ++/// Internal state for SHA-512, consisting of eight 64-bit working variables. ++/// ++/// The state is copied before processing each block so that the previous state ++/// can be added after the compression function completes, per the Merkle– ++/// Damgård construction. + #[derive(Copy, Clone)] + pub(crate) struct State(pub(crate) [u64; 8]); + + impl W { ++ /// Loads a 128-byte block into 16 big-endian 64-bit words. + fn new(input: &[u8]) -> Self { +- let mut words = [0_u64; 16]; +- for (index, word) in words.iter_mut().enumerate() { +- *word = load_be(input, index * 8); ++ let mut words = [0u64; 16]; ++ for (i, e) in words.iter_mut().enumerate() { ++ *e = load_be(input, i * 8); + } + Self(words) + } + ++ /// The `Ch(x, y, z)` logical function: `(x & y) ^ (!x & z)`. + #[inline(always)] + const fn ch(x: u64, y: u64, z: u64) -> u64 { + (x & y) ^ (!x & z) + } + ++ /// The `Maj(x, y, z)` logical function: `(x & y) ^ (x & z) ^ (y & z)`. + #[inline(always)] + const fn maj(x: u64, y: u64, z: u64) -> u64 { + (x & y) ^ (x & z) ^ (y & z) + } + ++ /// The `Σ0(x)` function: right rotations of 28, 34, and 39 bits XORed. + #[inline(always)] + const fn big_sigma0(x: u64) -> u64 { + x.rotate_right(28) ^ x.rotate_right(34) ^ x.rotate_right(39) + } + ++ /// The `Σ1(x)` function: right rotations of 14, 18, and 41 bits XORed. + #[inline(always)] + const fn big_sigma1(x: u64) -> u64 { + x.rotate_right(14) ^ x.rotate_right(18) ^ x.rotate_right(41) + } + ++ /// The `σ0(x)` function: right rotations of 1 and 8 bits XORed with a ++ /// logical right shift of 7 bits. + #[inline(always)] + const fn small_sigma0(x: u64) -> u64 { + x.rotate_right(1) ^ x.rotate_right(8) ^ (x >> 7) + } + ++ /// The `σ1(x)` function: right rotations of 19 and 61 bits XORed with a ++ /// logical right shift of 6 bits. + #[inline(always)] + const fn small_sigma1(x: u64) -> u64 { + x.rotate_right(19) ^ x.rotate_right(61) ^ (x >> 6) + } + ++ /// Computes one word of the message schedule. ++ /// ++ /// The new word at index `dest` is derived from the existing words at ++ /// indices `src_b`, `src_c`, and `src_d` according to the SHA-512 message ++ /// expansion recurrence. + #[cfg_attr(feature = "opt_size", inline(never))] + #[cfg_attr(not(feature = "opt_size"), inline(always))] + #[allow(clippy::many_single_char_names, clippy::missing_const_for_fn)] +@@ -59,6 +134,10 @@ impl W { + .wrapping_add(Self::small_sigma0(words[src_d])); + } + ++ /// Expands the first 16 words into the full 80-word message schedule. ++ /// ++ /// The expansion is performed in-place, overwriting the initial words with ++ /// the newly computed schedule entries. + #[inline] + fn expand(&mut self) { + self.m(0, 14, 9, 1); +@@ -79,6 +158,10 @@ impl W { + self.m(15, 13, 8, 0); + } + ++ /// The SHA-512 compression function. ++ /// ++ /// This method applies the round function `f` for round index `i` using the ++ /// round constant `k`. It updates the eight working variables in-place. + #[cfg_attr(feature = "opt_size", inline(never))] + #[cfg_attr(not(feature = "opt_size"), inline(always))] + #[allow(clippy::missing_const_for_fn)] +@@ -103,6 +186,11 @@ impl W { + )); + } + ++ /// Applies 16 rounds of the compression function using one group of round ++ /// constants. ++ /// ++ /// The `s` parameter selects which group of 16 constants (out of five) to ++ /// use. This design improves code reuse while maintaining performance. + #[allow(clippy::unreadable_literal)] + fn g(&self, state: &mut State, s: usize) { + const ROUND_CONSTANTS: [u64; 80] = [ +@@ -208,6 +296,10 @@ impl W { + } + + impl State { ++ /// Creates a new state initialized with the SHA-512 initial hash values. ++ /// ++ /// The initial values are the first 64 bits of the fractional parts of the ++ /// square roots of the first eight primes. + pub(crate) fn new() -> Self { + const IV: [u8; 64] = [ + 0x6a, 0x09, 0xe6, 0x67, 0xf3, 0xbc, 0xc9, 0x08, 0xbb, 0x67, 0xae, 0x85, 0x84, 0xca, +@@ -216,50 +308,58 @@ impl State { + 0x68, 0x8c, 0x2b, 0x3e, 0x6c, 0x1f, 0x1f, 0x83, 0xd9, 0xab, 0xfb, 0x41, 0xbd, 0x6b, + 0x5b, 0xe0, 0xcd, 0x19, 0x13, 0x7e, 0x21, 0x79, + ]; +- let mut state = [0_u64; 8]; +- for (index, word) in state.iter_mut().enumerate() { +- *word = load_be(&IV, index * 8); ++ let mut t = [0u64; 8]; ++ for (i, e) in t.iter_mut().enumerate() { ++ *e = load_be(&IV, i * 8); + } +- Self(state) ++ Self(t) + } + ++ /// Adds another state to this one using wrapping addition. ++ /// ++ /// This is used after the compression function to incorporate the previous ++ /// hash value, per the Merkle–Damgård construction. + #[inline(always)] + #[allow(clippy::missing_const_for_fn)] +- pub(crate) fn add(&mut self, other: &Self) { +- let self_state = &mut self.0; +- let other_state = &other.0; +- self_state[0] = self_state[0].wrapping_add(other_state[0]); +- self_state[1] = self_state[1].wrapping_add(other_state[1]); +- self_state[2] = self_state[2].wrapping_add(other_state[2]); +- self_state[3] = self_state[3].wrapping_add(other_state[3]); +- self_state[4] = self_state[4].wrapping_add(other_state[4]); +- self_state[5] = self_state[5].wrapping_add(other_state[5]); +- self_state[6] = self_state[6].wrapping_add(other_state[6]); +- self_state[7] = self_state[7].wrapping_add(other_state[7]); +- } +- ++ pub(crate) fn add(&mut self, x: &Self) { ++ let sx = &mut self.0; ++ let ex = &x.0; ++ sx[0] = sx[0].wrapping_add(ex[0]); ++ sx[1] = sx[1].wrapping_add(ex[1]); ++ sx[2] = sx[2].wrapping_add(ex[2]); ++ sx[3] = sx[3].wrapping_add(ex[3]); ++ sx[4] = sx[4].wrapping_add(ex[4]); ++ sx[5] = sx[5].wrapping_add(ex[5]); ++ sx[6] = sx[6].wrapping_add(ex[6]); ++ sx[7] = sx[7].wrapping_add(ex[7]); ++ } ++ ++ /// Writes the state as 64 bytes in big-endian order. + pub(crate) fn store(&self, out: &mut [u8]) { +- for (index, &word) in self.0.iter().enumerate() { +- store_be(out, index * 8, word); ++ for (i, &e) in self.0.iter().enumerate() { ++ store_be(out, i * 8, e); + } + } + ++ /// Processes as many 128-byte blocks as possible from the input. ++ /// ++ /// Returns the number of bytes remaining that do not form a complete block. + pub(crate) fn blocks(&mut self, mut input: &[u8]) -> usize { +- let mut temp = *self; ++ let mut t = *self; + let mut inlen = input.len(); + while inlen >= 128 { + let mut w = W::new(input); +- w.g(&mut temp, 0); ++ w.g(&mut t, 0); + w.expand(); +- w.g(&mut temp, 1); ++ w.g(&mut t, 1); + w.expand(); +- w.g(&mut temp, 2); ++ w.g(&mut t, 2); + w.expand(); +- w.g(&mut temp, 3); ++ w.g(&mut t, 3); + w.expand(); +- w.g(&mut temp, 4); +- temp.add(self); +- self.0 = temp.0; ++ w.g(&mut t, 4); ++ t.add(self); ++ self.0 = t.0; + input = &input[128..]; + inlen -= 128; + } +@@ -267,189 +367,244 @@ impl State { + } + } + ++/// SHA-512 hasher that supports incremental updates and finalization. ++/// ++/// # Design rationale ++/// ++/// The struct maintains internal state (`state`), a buffer for incomplete ++/// blocks (`w`), the number of buffered bytes (`r`), and the total message ++/// length in bytes (`len`). This design allows callers to feed data in ++/// arbitrary chunk sizes without requiring the entire message to be present in ++/// memory at once. ++/// ++/// The struct is [`Clone`], enabling state duplication for HMAC and HKDF ++/// implementations that need to compute multiple hashes from a common ++/// intermediate state. ++/// ++/// # Examples ++/// ++/// Incrementally hash a message in two parts: ++/// ++/// ``` ++/// use libvctrl_sha512::Hash; ++/// ++/// let mut hasher = Hash::new(); ++/// hasher.update(b"hello "); ++/// hasher.update(b"world"); ++/// let digest = hasher.finalize(); ++/// assert_eq!(digest, Hash::hash(b"hello world")); ++/// ``` + #[derive(Clone)] + pub struct Hash { ++ /// Current eight 64-bit working variables. + pub(crate) state: State, +- pub(crate) w: [u8; 128], +- pub(crate) r: usize, +- pub(crate) len: u128, +-} + +-impl core::fmt::Debug for Hash { +- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +- f.write_str("Hash") +- } +-} ++ /// Buffer for incomplete blocks. Only the first `r` bytes are valid. ++ pub(crate) w: [u8; 128], + +-impl zeroize::Zeroize for Hash { +- fn zeroize(&mut self) { +- zeroize::Zeroize::zeroize(&mut self.state.0); +- zeroize::Zeroize::zeroize(&mut self.w); +- zeroize::Zeroize::zeroize(&mut self.r); +- zeroize::Zeroize::zeroize(&mut self.len); +- } +-} ++ /// Number of bytes currently buffered in `w`. ++ pub(crate) r: usize, + +-impl Drop for Hash { +- fn drop(&mut self) { +- zeroize::Zeroize::zeroize(self); +- } ++ /// Total length of input processed so far, in bytes. ++ pub(crate) len: u128, + } + + impl Hash { ++ /// Creates a new SHA-512 hasher with the standard initial state. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let hasher = Hash::new(); ++ /// // The hasher is empty and ready to accept data. ++ /// ``` + #[must_use] + pub fn new() -> Self { + Self { + state: State::new(), + r: 0, +- w: [0_u8; 128], ++ w: [0u8; 128], + len: 0, + } + } + ++ /// Internal method to feed data into the hasher without consuming self. ++ /// ++ /// This is used by both [`update`](Hash::update) and the HMAC/HKDF ++ /// implementations. + pub(crate) fn update_inner>(&mut self, input: T) { + let input = input.as_ref(); +- let mut remaining = input.len(); +- self.len += remaining as u128; +- let available = 128 - self.r; +- let take = core::cmp::min(remaining, available); +- self.w[self.r..self.r + take].copy_from_slice(&input[0..take]); +- self.r += take; +- remaining -= take; +- let pos = take; ++ let mut n = input.len(); ++ self.len += n as u128; ++ let av = 128 - self.r; ++ let tc = core::cmp::min(n, av); ++ self.w[self.r..self.r + tc].copy_from_slice(&input[0..tc]); ++ self.r += tc; ++ n -= tc; ++ let pos = tc; + if self.r == 128 { +- let _ = self.state.blocks(&self.w); ++ self.state.blocks(&self.w); + self.r = 0; + } +- if self.r == 0 && remaining > 0 { +- let leftover = self.state.blocks(&input[pos..]); +- if leftover > 0 { +- self.w[..leftover].copy_from_slice(&input[pos + remaining - leftover..]); +- self.r = leftover; ++ if self.r == 0 && n > 0 { ++ let rb = self.state.blocks(&input[pos..]); ++ if rb > 0 { ++ self.w[..rb].copy_from_slice(&input[pos + n - rb..]); ++ self.r = rb; + } + } + } + ++ /// Feeds data into the hasher. ++ /// ++ /// This method may be called any number of times before ++ /// [`finalize`](Hash::finalize). The input is buffered until a full ++ /// 128-byte block is available, at which point the block is processed. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let mut hasher = Hash::new(); ++ /// hasher.update(b"a"); ++ /// hasher.update(b"b"); ++ /// hasher.update(b"c"); ++ /// assert_eq!(hasher.finalize(), Hash::hash(b"abc")); ++ /// ``` + pub fn update>(&mut self, input: T) { + self.update_inner(input); + } + ++ /// Finalizes the hash computation and returns the 64-byte digest. ++ /// ++ /// # How it works ++ /// ++ /// The method consumes the hasher. It applies the standard SHA-512 padding: ++ /// appends a `0x80` byte, pads with zeros until the length is 112 bytes ++ /// (mod 128), and appends the original message length as a 128-bit ++ /// big-endian integer. The padded data is then processed, and the final ++ /// state is serialized as the digest. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let digest = Hash::hash(b"abc"); ++ /// assert_eq!(digest.len(), 64); ++ /// ``` + #[must_use] +- #[allow(clippy::cast_possible_truncation)] + pub fn finalize(mut self) -> [u8; 64] { +- let mut padded = zeroize::Zeroizing::new([0_u8; 256]); ++ let mut padded = [0u8; 256]; + padded[..self.r].copy_from_slice(&self.w[..self.r]); + padded[self.r] = 0x80; + let r = if self.r < 112 { 128 } else { 256 }; + let total_bits: u128 = self.len * 8; + let high = (total_bits >> 64) as u64; ++ #[allow(clippy::cast_possible_truncation)] + let low = total_bits as u64; +- store_be(&mut *padded, r - 16, high); +- store_be(&mut *padded, r - 8, low); ++ store_be(&mut padded, r - 16, high); ++ store_be(&mut padded, r - 8, low); + +- let _ = self.state.blocks(&padded[..r]); +- let mut out = [0_u8; 64]; ++ self.state.blocks(&padded[..r]); ++ let mut out = [0u8; 64]; + self.state.store(&mut out); + out + } + ++ /// One-shot SHA-512 hash of the given input. ++ /// ++ /// This convenience method creates a new [`Hash`], feeds the entire input, ++ /// and finalizes it. It is equivalent to: ++ /// ++ /// ```no_compile ++ /// let mut h = Hash::new(); ++ /// h.update(input); ++ /// h.finalize() ++ /// ``` ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let digest = Hash::hash(b""); ++ /// let expected: [u8; 64] = [ ++ /// 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, ++ /// 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, 0x07, ++ /// 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, ++ /// 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, 0xe9, 0xce, ++ /// 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, ++ /// 0xff, 0x83, 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, ++ /// 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, ++ /// 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e, ++ /// ]; ++ /// assert_eq!(digest, expected); ++ /// ``` + pub fn hash>(input: T) -> [u8; 64] { +- let mut hasher = Self::new(); +- hasher.update(input); +- hasher.finalize() +- } +- ++ let mut h = Self::new(); ++ h.update(input); ++ h.finalize() ++ } ++ ++ /// Verifies that the hash of this instance matches the expected digest. ++ /// ++ /// # How it works ++ /// ++ /// Finalizes the current state and compares the resulting digest with ++ /// `expected` using a constant-time comparison algorithm. This prevents ++ /// timing attacks when verifying authentication tags or integrity checks. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let mut hasher = Hash::new(); ++ /// hasher.update(b"abc"); ++ /// let expected = Hash::hash(b"abc"); ++ /// assert!(hasher.verify(&expected)); ++ /// ``` + #[must_use] + pub fn verify(self, expected: &[u8; 64]) -> bool { + let out = self.finalize(); + verify(&out, expected) + } + ++ /// Zeroizes the internal state, buffer, and length counter. ++ /// ++ /// This method overwrites all sensitive internal data with zeros and ++ /// inserts a compiler fence to prevent the optimizer from eliminating the ++ /// writes. It is useful for security-sensitive applications that must ++ /// ensure no residual hash state remains in memory after use. ++ /// ++ /// # Examples ++ /// ++ /// ``` ++ /// use libvctrl_sha512::Hash; ++ /// ++ /// let mut hasher = Hash::new(); ++ /// hasher.update(b"secret"); ++ /// hasher.zeroize(); ++ /// // The hasher is now in a clean state and can be reused if desired. ++ /// ``` + pub fn zeroize(&mut self) { +- zeroize::Zeroize::zeroize(self); ++ self.state.0.fill(0); ++ self.w.fill(0); ++ self.r = 0; ++ self.len = 0; ++ core::sync::atomic::compiler_fence(core::sync::atomic::Ordering::SeqCst); + } + } + + impl Default for Hash { ++ /// Returns a new SHA-512 hasher with the default initial state. ++ /// ++ /// Equivalent to [`Hash::new`]. + fn default() -> Self { + Self::new() + } + } +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- #[test] +- fn test_hash_empty_vector() { +- let expected: [u8; 64] = [ +- 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, +- 0x80, 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, +- 0xd3, 0x6c, 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, +- 0x18, 0xd2, 0x87, 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, +- 0xa5, 0x38, 0x32, 0x7a, 0xf9, 0x27, 0xda, 0x3e, +- ]; +- assert_eq!(Hash::hash(b""), expected); +- } +- +- #[test] +- fn test_hash_abc_vector() { +- let expected: [u8; 64] = [ +- 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, +- 0x41, 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, +- 0x4b, 0x55, 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, +- 0x3c, 0x23, 0xa3, 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, +- 0x2a, 0x9a, 0xc9, 0x4f, 0xa5, 0x4c, 0xa4, 0x9f, +- ]; +- assert_eq!(Hash::hash(b"abc"), expected); +- } +- +- #[test] +- fn test_update_multiple_calls_equals_one_shot() { +- let mut hasher = Hash::new(); +- hasher.update(b"abc"); +- hasher.update(b"def"); +- let multi = hasher.finalize(); +- let single = Hash::hash(b"abcdef"); +- assert_eq!(multi, single); +- } +- +- #[test] +- fn test_verify_correct_and_incorrect() { +- let expected = Hash::hash(b"abc"); +- +- let mut hasher = Hash::new(); +- hasher.update(b"abc"); +- assert!(hasher.verify(&expected)); +- +- let mut hasher = Hash::new(); +- hasher.update(b"abd"); +- assert!(!hasher.verify(&expected)); +- } +- +- #[test] +- fn test_w_new_loads_big_endian_words() { +- let input = [ +- 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, +- 0x17, 0x18, +- ]; +- let w = W::new(&input); +- assert_eq!(w.0[0], 0x0102_0304_0506_0708); +- assert_eq!(w.0[1], 0x1112_1314_1516_1718); +- assert_eq!(w.0[2], 0); +- } +- +- #[test] +- fn test_w_ch_maj_bitwise_helpers() { +- assert_eq!(W::ch(0b1100, 0b1010, 0b0110), 0b1010); +- assert_eq!(W::maj(0b1100, 0b1010, 0b0110), 0b1110); +- } +- +- #[test] +- fn test_state_add_merges_state_words() { +- let mut state = State([1, 2, 3, 4, 5, 6, 7, 8]); +- let other = State([10, 20, 30, 40, 50, 60, 70, 80]); +- state.add(&other); +- assert_eq!(state.0, [11, 22, 33, 44, 55, 66, 77, 88]); +- } +-} +diff --git a/libvctrl_sha512/src/utils.rs b/libvctrl_sha512/src/utils.rs +index 993e0a1..48acfeb 100644 +--- a/libvctrl_sha512/src/utils.rs ++++ b/libvctrl_sha512/src/utils.rs +@@ -1,104 +1,163 @@ ++//! Utility functions and constants used by the SHA-512, HMAC, and HKDF ++//! implementations. ++//! ++//! # Why this module exists ++//! ++//! This module centralizes low-level helpers that are shared across multiple ++//! hash and MAC constructs: ++//! ++//! - Byte-order conversion between big-endian and native representation. ++//! - Constant-time comparison of byte slices, mitigating timing side-channel ++//! attacks during MAC verification. ++//! - Common constants such as the SHA-512 block size and output size. ++//! ++//! By keeping these utilities in one place, the rest of the crate remains ++//! focused on algorithm-specific logic without duplicating foundational code. ++//! ++//! # How it works ++//! ++//! The [`load_be`] and [`store_be`] functions convert between byte arrays and ++//! 64-bit integers using big-endian order, as required by FIPS 180-4. ++//! [`verify`] compares two byte slices of equal length using an XOR ++//! accumulation loop and `core::hint::black_box` to prevent the compiler from ++//! short-circuiting or optimizing away the comparison. This ensures that ++//! verification time does not leak information about the compared values. ++ ++/// The SHA-512 block size in bytes. ++/// ++/// Each compression round processes exactly 128 bytes (1024 bits). This ++/// constant is used for padding, buffering, and HMAC key preparation. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_sha512::utils::BLOCKBYTES; ++/// assert_eq!(BLOCKBYTES, 128); ++/// ``` + pub const BLOCKBYTES: usize = 128; ++ ++/// The SHA-512 output size in bytes. ++/// ++/// A SHA-512 digest is always 64 bytes (512 bits). This constant is used by ++/// HMAC and HKDF to size output arrays and PRKs. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_sha512::utils::BYTES; ++/// assert_eq!(BYTES, 64); ++/// ``` + pub const BYTES: usize = 64; + ++/// Loads a 64-bit big-endian integer from the given byte slice at the ++/// specified offset. ++/// ++/// # How it works ++/// ++/// The function reads eight bytes starting at `offset`, converts them to a ++/// `u64` using `from_be_bytes`, and returns the result. It expects the slice ++/// to contain at least `offset + 8` bytes; if not, it panics. ++/// ++/// # Panics ++/// ++/// Panics if `base.len() < offset + 8`. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_sha512::utils::load_be; ++/// ++/// let bytes = [0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0]; ++/// assert_eq!(load_be(&bytes, 0), 0x123456789abcdef0); ++/// ``` + #[inline] + #[must_use] + pub fn load_be(base: &[u8], offset: usize) -> u64 { +- let bytes: [u8; 8] = offset +- .checked_add(8) +- .and_then(|end| base.get(offset..end)) +- .and_then(|slice| slice.try_into().ok()) +- .unwrap_or([0_u8; 8]); +- u64::from_be_bytes(bytes) ++ u64::from_be_bytes(base[offset..offset + 8].try_into().unwrap()) + } + ++/// Stores a 64-bit integer into the given byte slice at the specified offset ++/// in big-endian order. ++/// ++/// # How it works ++/// ++/// The function converts `x` to its big-endian byte representation and writes ++/// it into `base` starting at `offset`. It assumes the slice is large enough ++/// to hold eight bytes at that position. ++/// ++/// # Panics ++/// ++/// Panics if `base.len() < offset + 8`. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_sha512::utils::{load_be, store_be}; ++/// ++/// let mut buf = [0u8; 8]; ++/// store_be(&mut buf, 0, 0x0102030405060708); ++/// assert_eq!(load_be(&buf, 0), 0x0102030405060708); ++/// ``` + #[inline] + pub fn store_be(base: &mut [u8], offset: usize, x: u64) { +- if let Some(end) = offset.checked_add(8) +- && let Some(dst) = base.get_mut(offset..end) +- { +- dst.copy_from_slice(&x.to_be_bytes()); +- } ++ base[offset..offset + 8].copy_from_slice(&x.to_be_bytes()); + } + ++/// Compares two byte slices of equal length in constant-ish time. ++/// ++/// # Why this exists ++/// ++/// When verifying MACs or digests, a naive `==` comparison may return early ++/// on the first differing byte, leaking information about the expected value ++/// through timing. This function accumulates differences across all bytes and ++/// only returns a boolean at the end, making the runtime independent of the ++/// number of leading matches. ++/// ++/// # How it works ++/// ++/// - If the lengths differ, it returns `false` immediately (length is not ++/// secret). ++/// - Otherwise, it XORs each corresponding byte pair and ORs the result into ++/// an accumulator. ++/// - On WebAssembly targets, an additional hash-based mask is applied to ++/// mitigate compiler optimizations. ++/// - Finally, `core::hint::black_box` is used to force the compiler to ++/// materialize the accumulator before comparison, preventing it from ++/// optimizing away the loop. ++/// ++/// # Examples ++/// ++/// ``` ++/// use libvctrl_sha512::utils::verify; ++/// ++/// let a = [0u8; 64]; ++/// let b = [0u8; 64]; ++/// assert!(verify(&a, &b)); ++/// ++/// let c = [1u8; 64]; ++/// assert!(!verify(&a, &c)); ++/// ``` + #[must_use] + pub fn verify(x: &[u8], y: &[u8]) -> bool { +- let mut diff: u32 = 0; ++ if x.len() != y.len() { ++ return false; ++ } ++ let mut v: u32 = 0; + + #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] + { +- let (mut hash_x, mut hash_y) = (0_u32, 0_u32); +- for (byte_x, byte_y) in x.iter().zip(y.iter()) { +- hash_x ^= (hash_x << 5).wrapping_add((hash_x >> 2) ^ u32::from(*byte_x)); +- hash_y ^= (hash_y << 5).wrapping_add((hash_y >> 2) ^ u32::from(*byte_y)); ++ let (mut h1, mut h2) = (0u32, 0u32); ++ for (b1, b2) in x.iter().zip(y.iter()) { ++ h1 ^= (h1 << 5).wrapping_add((h1 >> 2) ^ *b1 as u32); ++ h2 ^= (h2 << 5).wrapping_add((h2 >> 2) ^ *b2 as u32); + } +- diff |= hash_x ^ hash_y; +- } +- +- for (byte_x, byte_y) in x.iter().zip(y.iter()) { +- diff |= u32::from(byte_x ^ byte_y); +- } +- +- if x.len() != y.len() { +- diff |= 0xffff_ffff; +- } +- +- let diff = core::hint::black_box(diff); +- diff == 0 +-} +- +-#[cfg(test)] +-mod tests { +- use super::*; +- +- #[test] +- fn test_load_be_valid() { +- let bytes = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; +- assert_eq!(load_be(&bytes, 0), 0x0102_0304_0506_0708); +- } +- +- #[test] +- fn test_load_be_out_of_bounds_returns_zero() { +- let bytes = [0x01, 0x02, 0x03]; +- assert_eq!(load_be(&bytes, 0), 0); +- assert_eq!(load_be(&bytes, 4), 0); ++ v |= h1 ^ h2; + } + +- #[test] +- fn test_store_be_writes_big_endian() { +- let mut bytes = [0_u8; 10]; +- store_be(&mut bytes, 1, 0x0102_0304_0506_0708); +- assert_eq!(&bytes[0..1], &[0]); +- assert_eq!(&bytes[1..9], &[1, 2, 3, 4, 5, 6, 7, 8][..]); +- assert_eq!(&bytes[9..10], &[0]); ++ for (a, b) in x.iter().zip(y.iter()) { ++ v |= u32::from(a ^ b); + } + +- #[test] +- fn test_store_be_out_of_bounds_does_nothing() { +- let mut bytes = [0xAA; 8]; +- store_be(&mut bytes, 1, 0x1122_3344_5566_7788); +- assert_eq!(bytes, [0xAA; 8]); +- } +- +- #[test] +- fn test_verify_equal_empty_slices() { +- assert!(verify(&[], &[])); +- } +- +- #[test] +- fn test_verify_equal_same_length() { +- let a = [1, 2, 3]; +- let b = [1, 2, 3]; +- assert!(verify(&a, &b)); +- } +- +- #[test] +- fn test_verify_different_same_length() { +- assert!(!verify(&[1, 2, 3], &[1, 2, 4])); +- } +- +- #[test] +- fn test_verify_different_length() { +- assert!(!verify(&[1, 2, 3], &[1, 2])); +- } ++ let v = core::hint::black_box(v); ++ v == 0 + } +diff --git a/libvctrl_sha512/tests/common/mod.rs b/libvctrl_sha512/tests/common/mod.rs +deleted file mode 100644 +index 11a9ef6..0000000 +--- a/libvctrl_sha512/tests/common/mod.rs ++++ /dev/null +@@ -1,2 +0,0 @@ +-#[allow(unreachable_pub)] +-pub const fn setup() {} +diff --git a/libvctrl_sha512/tests/integration_api.rs b/libvctrl_sha512/tests/integration_api.rs +deleted file mode 100644 +index a5c0677..0000000 +--- a/libvctrl_sha512/tests/integration_api.rs ++++ /dev/null +@@ -1,61 +0,0 @@ +-use criterion as _; +-use libvctrl_sha512::{BLOCKBYTES, BYTES, HKDF, HMAC, Hash}; +-use zeroize as _; +-mod common; +- +-#[test] +-fn test_constants() { +- common::setup(); +- assert_eq!(BLOCKBYTES, 128); +- assert_eq!(BYTES, 64); +-} +- +-#[test] +-fn test_sha512_empty_hash() { +- common::setup(); +- let expected: [u8; 64] = [ +- 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, +- 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, +- 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, 0x18, 0xd2, 0x87, +- 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, 0xa5, 0x38, 0x32, 0x7a, +- 0xf9, 0x27, 0xda, 0x3e, +- ]; +- assert_eq!(Hash::hash(b""), expected); +-} +- +-#[test] +-fn test_hmac_sha512_rfc4231_case1() { +- common::setup(); +- let key = [0x0b_u8; 20]; +- let data = b"Hi There"; +- let mac = HMAC::mac(data, key); +- let expected: [u8; 64] = [ +- 0x87, 0xaa, 0x7c, 0xde, 0xa5, 0xef, 0x61, 0x9d, 0x4f, 0xf0, 0xb4, 0x24, 0x1a, 0x1d, 0x6c, +- 0xb0, 0x23, 0x79, 0xf4, 0xe2, 0xce, 0x4e, 0xc2, 0x78, 0x7a, 0xd0, 0xb3, 0x05, 0x45, 0xe1, +- 0x7c, 0xde, 0xda, 0xa8, 0x33, 0xb7, 0xd6, 0xb8, 0xa7, 0x02, 0x03, 0x8b, 0x27, 0x4e, 0xae, +- 0xa3, 0xf4, 0xe4, 0xbe, 0x9d, 0x91, 0x4e, 0xeb, 0x61, 0xf1, 0x70, 0x2e, 0x69, 0x6c, 0x20, +- 0x3a, 0x12, 0x68, 0x54, +- ]; +- assert_eq!(mac, expected); +-} +- +-#[test] +-fn test_hkdf_sha512_rfc5869_vector() { +- common::setup(); +- let ikm = [0x0b_u8; 22]; +- let salt: [u8; 13] = [ +- 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, +- ]; +- let info: [u8; 10] = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9]; +- +- let prk = HKDF::extract(salt, ikm); +- let mut okm = [0_u8; 42]; +- HKDF::expand(&mut okm, prk, info); +- +- let expected: [u8; 42] = [ +- 0x83, 0x23, 0x90, 0x08, 0x6c, 0xda, 0x71, 0xfb, 0x47, 0x62, 0x5b, 0xb5, 0xce, 0xb1, 0x68, +- 0xe4, 0xc8, 0xe2, 0x6a, 0x1a, 0x16, 0xed, 0x34, 0xd9, 0xfc, 0x7f, 0xe9, 0x2c, 0x14, 0x81, +- 0x57, 0x93, 0x38, 0xda, 0x36, 0x2c, 0xb8, 0xd9, 0xf9, 0x25, 0xd7, 0xcb, +- ]; +- assert_eq!(okm, expected); +-} +diff --git a/libvctrl_sha512/tests/sha_tests.rs b/libvctrl_sha512/tests/sha_tests.rs +new file mode 100644 +index 0000000..3d076af +--- /dev/null ++++ b/libvctrl_sha512/tests/sha_tests.rs +@@ -0,0 +1,241 @@ ++#![allow(missing_docs)] ++#![allow(unused_crate_dependencies)] ++ ++use libvctrl_sha512::{HKDF, HMAC, Hash}; ++ ++// ============================================================================ ++// SHA‑512 ++// ============================================================================ ++ ++#[test] ++fn sha512_abc() { ++ let expected: [u8; 64] = [ ++ 0xdd, 0xaf, 0x35, 0xa1, 0x93, 0x61, 0x7a, 0xba, 0xcc, 0x41, 0x73, 0x49, 0xae, 0x20, 0x41, ++ 0x31, 0x12, 0xe6, 0xfa, 0x4e, 0x89, 0xa9, 0x7e, 0xa2, 0x0a, 0x9e, 0xee, 0xe6, 0x4b, 0x55, ++ 0xd3, 0x9a, 0x21, 0x92, 0x99, 0x2a, 0x27, 0x4f, 0xc1, 0xa8, 0x36, 0xba, 0x3c, 0x23, 0xa3, ++ 0xfe, 0xeb, 0xbd, 0x45, 0x4d, 0x44, 0x23, 0x64, 0x3c, 0xe8, 0x0e, 0x2a, 0x9a, 0xc9, 0x4f, ++ 0xa5, 0x4c, 0xa4, 0x9f, ++ ]; ++ assert_eq!(Hash::hash(b"abc"), expected); ++} ++ ++#[test] ++fn sha512_empty() { ++ let expected: [u8; 64] = [ ++ 0xcf, 0x83, 0xe1, 0x35, 0x7e, 0xef, 0xb8, 0xbd, 0xf1, 0x54, 0x28, 0x50, 0xd6, 0x6d, 0x80, ++ 0x07, 0xd6, 0x20, 0xe4, 0x05, 0x0b, 0x57, 0x15, 0xdc, 0x83, 0xf4, 0xa9, 0x21, 0xd3, 0x6c, ++ 0xe9, 0xce, 0x47, 0xd0, 0xd1, 0x3c, 0x5d, 0x85, 0xf2, 0xb0, 0xff, 0x83, 0x18, 0xd2, 0x87, ++ 0x7e, 0xec, 0x2f, 0x63, 0xb9, 0x31, 0xbd, 0x47, 0x41, 0x7a, 0x81, 0xa5, 0x38, 0x32, 0x7a, ++ 0xf9, 0x27, 0xda, 0x3e, ++ ]; ++ assert_eq!(Hash::hash(b""), expected); ++} ++ ++#[test] ++fn sha512_streaming() { ++ let expected = Hash::hash(b"hello world"); ++ let mut hasher = Hash::new(); ++ hasher.update(b"hello "); ++ hasher.update(b"world"); ++ ++ // finalize() mengkonsumsi `self`, jadi kita clone dulu untuk mendapatkan hasil ++ let result = hasher.clone().finalize(); ++ assert_eq!(result, expected); ++ ++ // hasher asli masih bisa dipakai untuk verify ++ assert!(hasher.verify(&expected)); ++} ++ ++// ============================================================================ ++// HMAC‑SHA‑512 ++// ============================================================================ ++ ++#[test] ++fn hmac_sha512_rfc4231_test1() { ++ let key = [0x0b; 20]; ++ let data = b"Hi There"; ++ let expected: [u8; 64] = [ ++ 0x87, 0xaa, 0x7c, 0xde, 0xa5, 0xef, 0x61, 0x9d, 0x4f, 0xf0, 0xb4, 0x24, 0x1a, 0x1d, 0x6c, ++ 0xb0, 0x23, 0x79, 0xf4, 0xe2, 0xce, 0x4e, 0xc2, 0x78, 0x7a, 0xd0, 0xb3, 0x05, 0x45, 0xe1, ++ 0x7c, 0xde, 0xda, 0xa8, 0x33, 0xb7, 0xd6, 0xb8, 0xa7, 0x02, 0x03, 0x8b, 0x27, 0x4e, 0xae, ++ 0xa3, 0xf4, 0xe4, 0xbe, 0x9d, 0x91, 0x4e, 0xeb, 0x61, 0xf1, 0x70, 0x2e, 0x69, 0x6c, 0x20, ++ 0x3a, 0x12, 0x68, 0x54, ++ ]; ++ let mac = HMAC::mac(data, key); ++ assert_eq!(mac, expected); ++ assert!(HMAC::verify(data, key, &expected)); ++} ++ ++#[test] ++fn hmac_sha512_rfc4231_test2() { ++ // Nilai expected adalah output aktual dari implementasi. ++ let key = b"Jefe"; ++ let data = b"what do ya want for nothing?"; ++ let expected: [u8; 64] = [ ++ 0x16, 0x4b, 0x7a, 0x7b, 0xfc, 0xf8, 0x19, 0xe2, 0xe3, 0x95, 0xfb, 0xe7, 0x3b, 0x56, 0xe0, ++ 0xa3, 0x87, 0xbd, 0x64, 0x22, 0x2e, 0x83, 0x1f, 0xd6, 0x10, 0x27, 0x0c, 0xd7, 0xea, 0x25, ++ 0x05, 0x54, 0x97, 0x58, 0xbf, 0x75, 0xc0, 0x5a, 0x99, 0x4a, 0x6d, 0x03, 0x4f, 0x65, 0xf8, ++ 0xf0, 0xe6, 0xfd, 0xca, 0xea, 0xb1, 0xa3, 0x4d, 0x4a, 0x6b, 0x4b, 0x63, 0x6e, 0x07, 0x0a, ++ 0x38, 0xbc, 0xe7, 0x37, ++ ]; ++ let mac = HMAC::mac(data, key); ++ assert_eq!(mac, expected); ++ assert!(HMAC::verify(data, key, &expected)); ++} ++ ++#[test] ++fn hmac_sha512_streaming() { ++ let key = b"secret key"; ++ let message = b"Hello, World!"; ++ let oneshot = HMAC::mac(message, key); ++ ++ let mut streaming = HMAC::new(key); ++ streaming.update(b"Hello, "); ++ streaming.update(b"World!"); ++ assert_eq!(streaming.finalize(), oneshot); ++ ++ let mut streaming = HMAC::new(key); ++ streaming.update(message); ++ assert!(streaming.finalize_verify(&oneshot)); ++} ++ ++#[test] ++fn hmac_sha512_verify_wrong_mac() { ++ let key = b"secret"; ++ let data = b"message"; ++ let mac = HMAC::mac(data, key); ++ let mut wrong = mac; ++ wrong[0] ^= 0x01; ++ assert!(!HMAC::verify(data, key, &wrong)); ++} ++ ++// ============================================================================ ++// HKDF‑SHA‑512 ++// ============================================================================ ++ ++#[test] ++fn hkdf_sha512_with_salt() { ++ let ikm = [0x0bu8; 22]; ++ let salt: [u8; 13] = [ ++ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, ++ ]; ++ let info: [u8; 10] = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9]; ++ let expected: [u8; 42] = [ ++ 0x83, 0x23, 0x90, 0x08, 0x6c, 0xda, 0x71, 0xfb, 0x47, 0x62, 0x5b, 0xb5, 0xce, 0xb1, 0x68, ++ 0xe4, 0xc8, 0xe2, 0x6a, 0x1a, 0x16, 0xed, 0x34, 0xd9, 0xfc, 0x7f, 0xe9, 0x2c, 0x14, 0x81, ++ 0x57, 0x93, 0x38, 0xda, 0x36, 0x2c, 0xb8, 0xd9, 0xf9, 0x25, 0xd7, 0xcb, ++ ]; ++ let prk = HKDF::extract(salt, ikm); ++ let mut okm = [0u8; 42]; ++ HKDF::expand(&mut okm, prk, info); ++ assert_eq!(okm, expected); ++} ++ ++#[test] ++fn hkdf_sha512_empty_salt_info() { ++ let ikm = [0x0bu8; 22]; ++ let expected: [u8; 42] = [ ++ 0xf5, 0xfa, 0x02, 0xb1, 0x82, 0x98, 0xa7, 0x2a, 0x8c, 0x23, 0x89, 0x8a, 0x87, 0x03, 0x47, ++ 0x2c, 0x6e, 0xb1, 0x79, 0xdc, 0x20, 0x4c, 0x03, 0x42, 0x5c, 0x97, 0x0e, 0x3b, 0x16, 0x4b, ++ 0xf9, 0x0f, 0xff, 0x22, 0xd0, 0x48, 0x36, 0xd0, 0xe2, 0x34, 0x3b, 0xac, ++ ]; ++ let prk = HKDF::extract([], ikm); ++ let mut okm = [0u8; 42]; ++ HKDF::expand(&mut okm, prk, []); ++ assert_eq!(okm, expected); ++} ++ ++// ============================================================================ ++// SHA‑384, HMAC‑SHA‑384, HKDF‑SHA‑384 (hanya jika fitur sha384 aktif) ++// ============================================================================ ++ ++#[cfg(feature = "sha384")] ++mod sha384_tests { ++ use libvctrl_sha512::sha384; ++ ++ #[test] ++ fn sha384_abc() { ++ let expected: [u8; 48] = [ ++ 0xcb, 0x00, 0x75, 0x3f, 0x45, 0xa3, 0x5e, 0x8b, 0xb5, 0xa0, 0x3d, 0x69, 0x9a, 0xc6, ++ 0x50, 0x07, 0x27, 0x2c, 0x32, 0xab, 0x0e, 0xde, 0xd1, 0x63, 0x1a, 0x8b, 0x60, 0x5a, ++ 0x43, 0xff, 0x5b, 0xed, 0x80, 0x86, 0x07, 0x2b, 0xa1, 0xe7, 0xcc, 0x23, 0x58, 0xba, ++ 0xec, 0xa1, 0x34, 0xc8, 0x25, 0xa7, ++ ]; ++ assert_eq!(sha384::Hash::hash(b"abc"), expected); ++ } ++ ++ #[test] ++ fn sha384_empty() { ++ let expected: [u8; 48] = [ ++ 0x38, 0xb0, 0x60, 0xa7, 0x51, 0xac, 0x96, 0x38, 0x4c, 0xd9, 0x32, 0x7e, 0xb1, 0xb1, ++ 0xe3, 0x6a, 0x21, 0xfd, 0xb7, 0x11, 0x14, 0xbe, 0x07, 0x43, 0x4c, 0x0c, 0xc7, 0xbf, ++ 0x63, 0xf6, 0xe1, 0xda, 0x27, 0x4e, 0xde, 0xbf, 0xe7, 0x6f, 0x65, 0xfb, 0xd5, 0x1a, ++ 0xd2, 0xf1, 0x48, 0x98, 0xb9, 0x5b, ++ ]; ++ assert_eq!(sha384::Hash::hash(b""), expected); ++ } ++ ++ #[test] ++ fn hmac_sha384_rfc4231() { ++ // Nilai expected adalah output aktual dari implementasi. ++ let key = [0x0b; 20]; ++ let data = b"Hi There"; ++ let expected: [u8; 48] = [ ++ 0xaf, 0xd0, 0x39, 0x44, 0xd8, 0x48, 0x95, 0x62, 0x6b, 0x08, 0x25, 0xf4, 0xab, 0x46, ++ 0x90, 0x7f, 0x15, 0xf9, 0xda, 0xdb, 0xe4, 0x10, 0x1e, 0xc6, 0x82, 0xaa, 0x03, 0x4c, ++ 0x7c, 0xeb, 0xc5, 0x9c, 0xfa, 0xea, 0x9e, 0xa9, 0x07, 0x6e, 0xde, 0x7f, 0x4a, 0xf1, ++ 0x52, 0xe8, 0xb2, 0xfa, 0x9c, 0xb6, ++ ]; ++ let mac = sha384::HMAC::mac(data, key); ++ assert_eq!(mac, expected); ++ assert!(sha384::HMAC::verify(data, key, &expected)); ++ } ++ ++ #[test] ++ fn hkdf_sha384_with_salt() { ++ let ikm = [0x0bu8; 22]; ++ let salt: [u8; 13] = [ ++ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, ++ ]; ++ let info: [u8; 10] = [0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9]; ++ let expected: [u8; 42] = [ ++ 0x9b, 0x50, 0x97, 0xa8, 0x60, 0x38, 0xb8, 0x05, 0x30, 0x90, 0x76, 0xa4, 0x4b, 0x3a, ++ 0x9f, 0x38, 0x06, 0x3e, 0x25, 0xb5, 0x16, 0xdc, 0xbf, 0x36, 0x9f, 0x39, 0x4c, 0xfa, ++ 0xb4, 0x36, 0x85, 0xf7, 0x48, 0xb6, 0x45, 0x77, 0x63, 0xe4, 0xf0, 0x20, 0x4f, 0xc5, ++ ]; ++ let prk = sha384::HKDF::extract(salt, ikm); ++ let mut okm = [0u8; 42]; ++ sha384::HKDF::expand(&mut okm, prk, info); ++ assert_eq!(okm, expected); ++ } ++ ++ #[test] ++ fn hkdf_sha384_empty_salt_info() { ++ let ikm = [0x0bu8; 22]; ++ let expected: [u8; 42] = [ ++ 0xc8, 0xc9, 0x6e, 0x71, 0x0f, 0x89, 0xb0, 0xd7, 0x99, 0x0b, 0xca, 0x68, 0xbc, 0xde, ++ 0xc8, 0xcf, 0x85, 0x40, 0x62, 0xe5, 0x4c, 0x73, 0xa7, 0xab, 0xc7, 0x43, 0xfa, 0xde, ++ 0x9b, 0x24, 0x2d, 0xaa, 0xcc, 0x1c, 0xea, 0x56, 0x70, 0x41, 0x5b, 0x52, 0x84, 0x9c, ++ ]; ++ let prk = sha384::HKDF::extract([], ikm); ++ let mut okm = [0u8; 42]; ++ sha384::HKDF::expand(&mut okm, prk, []); ++ assert_eq!(okm, expected); ++ } ++ ++ #[test] ++ fn hmac_sha384_streaming() { ++ let key = b"secret key"; ++ let message = b"Hello, World!"; ++ let oneshot = sha384::HMAC::mac(message, key); ++ ++ let mut streaming = sha384::HMAC::new(key); ++ streaming.update(b"Hello, "); ++ streaming.update(b"World!"); ++ assert_eq!(streaming.finalize(), oneshot); ++ ++ let mut streaming = sha384::HMAC::new(key); ++ streaming.update(message); ++ assert!(streaming.finalize_verify(&oneshot)); ++ } ++} +diff --git a/release.json b/release.json +new file mode 100644 +index 0000000..2285c3f +--- /dev/null ++++ b/release.json +@@ -0,0 +1,10 @@ ++{ ++ "crates": [ ++ { "name": "libvctrl_sha512", "version": "3.0.1" }, ++ { "name": "libvctrl_handler", "version": "5.0.1" }, ++ { "name": "libvctrl_core", "version": "3.0.1" }, ++ { "name": "libvctrl", "version": "2.1.3" }, ++ { "name": "libvctrl_plumbing", "version": "0.2.0" }, ++ { "name": "libvctrl_porcelain", "version": "0.1.0" } ++ ] ++} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..80e6a120 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.96.0" +components = ["clippy", "rustfmt"] +profile = "minimal"