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/Cargo.lock b/Cargo.lock index 0f501115..89c3cd1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,7 +263,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libvctrl" -version = "2.1.2" +version = "2.2.0" dependencies = [ "libvctrl_core", "libvctrl_handler", @@ -273,7 +273,7 @@ dependencies = [ [[package]] name = "libvctrl_core" -version = "3.0.0" +version = "3.2.0" dependencies = [ "libvctrl_handler", "libvctrl_sha512", @@ -282,11 +282,14 @@ dependencies = [ [[package]] name = "libvctrl_handler" -version = "5.0.0" +version = "5.2.0" +dependencies = [ + "criterion", +] [[package]] name = "libvctrl_plumbing" -version = "0.2.0" +version = "0.3.0" dependencies = [ "libvctrl", "libvctrl_core", @@ -298,9 +301,10 @@ version = "0.1.0" [[package]] name = "libvctrl_sha512" -version = "3.0.0" +version = "3.2.0" dependencies = [ "criterion", + "zeroize", ] [[package]] @@ -717,6 +721,12 @@ 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 f3d0551e..878ee268 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,62 +1,118 @@ [workspace] +members = [ + "libvctrl", + "libvctrl_core", + "libvctrl_handler", + "libvctrl_plumbing", + "libvctrl_porcelain", + "libvctrl_sha512" +] resolver = "2" -members = ["libvctrl", "libvctrl_core", "libvctrl_handler", "libvctrl_plumbing", "libvctrl_porcelain", "libvctrl_sha512"] - -[workspace.package] -edition = "2024" -rust-version = "1.96" -license = "MIT" -authors = ["mroczect"] -repository = "https://github.com/mroczect/libvctrl" -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" +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" -needless_return = "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" + +[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" ] +license = "MIT" +repository = "https://github.com/mroczect/libvctrl" +rust-version = "1.96" diff --git a/Makefile b/Makefile index 89e24b37..bc8fbda5 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,32 @@ SHELL = /bin/bash .SHELLFLAGS = -euo pipefail -c -CARGO = cargo -MEMBERS = libvctrl_handler libvctrl_core libvctrl_plumbing libvctrl_porcelain libvctrl libvctrl_sha512 +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 -# Default package jika ingin menjalankan CI untuk satu package -PKG ?= libvctrl_handler +PKG ?= libvctrl_handler -# Flag tambahan untuk Clippy (kosong = santai) -CLIPPY_FLAGS ?= +CLIPPY_FLAGS ?= -- -D warnings -.PHONY: all -all: build +.DEFAULT_GOAL := help .PHONY: help help: - @echo "Usage: make [PKG=]" + @echo "Usage: make [PKG=] [CLIPPY_FLAGS=]" @echo "" @echo "Targets:" @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ - | awk 'BEGIN {FS = ":.*?## "}; {printf " %-18s %s\n", $$1, $$2}' + | 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 -# --------------------------------------------------------------------------- -# Global -# --------------------------------------------------------------------------- .PHONY: build build: $(CARGO) build --workspace @@ -36,10 +39,17 @@ 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 @@ -60,10 +70,16 @@ fmt: fmt-check: $(CARGO) fmt --all -- --check -# Clippy santai (tidak -D warnings) .PHONY: clippy clippy: - $(CARGO) clippy --all-targets --all-features $(CLIPPY_FLAGS) + $(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 .PHONY: lint lint: fmt clippy @@ -71,6 +87,9 @@ 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 @@ -87,6 +106,11 @@ 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 @@ -100,35 +124,21 @@ audit: fi .PHONY: publish-check -publish-check: check-readmes +publish-check: @for crate in $(MEMBERS); do \ - echo "Packaging $$crate"; \ - $(CARGO) package -p "$$crate" --no-verify || exit 1; \ + echo "🔍 Memeriksa packaging $$crate"; \ + $(CARGO) package -p "$$crate" || exit 1; \ done - @echo "All crates are ready for publish." + @echo "✅ Semua crate siap publish." .PHONY: publish-all -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" +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." .PHONY: version version: @@ -161,7 +171,7 @@ snap: .PHONY: run run: - $(CARGO) run + $(CARGO) run -p $(PKG) .PHONY: install install: @@ -174,9 +184,6 @@ uninstall: .PHONY: rebuild rebuild: release install -# --------------------------------------------------------------------------- -# Package-specific targets (pkg=) -# --------------------------------------------------------------------------- .PHONY: build-pkg build-pkg: $(CARGO) build -p $(PKG) @@ -205,20 +212,19 @@ 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) -# Alias backward-compatible -.PHONY: clippy-pkg-unwarn -clippy-pkg-unwarn: clippy-pkg +.PHONY: clippy-pkg-strict +clippy-pkg-strict: + $(CARGO) clippy -p $(PKG) --all-targets --all-features -- -D warnings .PHONY: ci-pkg ci-pkg: fmt-check-pkg clippy-pkg test-verbose-pkg -.PHONY: ci-pkg-unwarn -ci-pkg-unwarn: ci-pkg +.PHONY: ci-pkg-strict +ci-pkg-strict: fmt-check-pkg clippy-pkg-strict test-verbose-pkg .PHONY: doc-pkg doc-pkg: @@ -232,9 +238,6 @@ 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 @@ -258,8 +261,3 @@ 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 6eccb760..18bda920 100644 --- a/libvctrl/Cargo.toml +++ b/libvctrl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libvctrl" -version = "2.1.3" +version = "2.2.0" edition = "2024" description = "A precision toolkit for building custom version control systems" license = "MIT" @@ -19,9 +19,9 @@ exclude = [ ] [dependencies] -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 } +libvctrl_handler = { path = "../libvctrl_handler", version = "5.2.0" } +libvctrl_core = { path = "../libvctrl_core", version = "3.2.0" } +libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.2.0", default-features = false } [dev-dependencies] proptest = "1.11.0" diff --git a/libvctrl/src/lib.rs b/libvctrl/src/lib.rs index 10df03fd..c67ff8dd 100644 --- a/libvctrl/src/lib.rs +++ b/libvctrl/src/lib.rs @@ -1,336 +1,49 @@ -//! # 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/tests/public_api.rs b/libvctrl/tests/public_api.rs new file mode 100644 index 00000000..6ad4a64f --- /dev/null +++ b/libvctrl/tests/public_api.rs @@ -0,0 +1,215 @@ +use core::str::FromStr; + +use libvctrl_core as _; +use libvctrl_handler as _; +use libvctrl_sha512 as _; +use proptest as _; + +use libvctrl::{ + BinaryDecoder, BinaryEncoder, Blob, BlobBuilder, Commit, CommitBuilder, CommitMeta, Decoder, + Encoder, EntryKind, HASH_LENGTH, Hash, Hasher, MemoryRefStore, MemoryStore, ObjectStore, + RefStore, Sha512Hasher, Tag, TagBuilder, Tree, TreeBuilder, TreeEntry, TreeEntryBuilder, + UserID, VctrlError, validate_name, validate_ref_name, validate_tree_entry_name, +}; + +const fn make_hash(byte: u8) -> Result { + Hash::from_bytes(&[byte; 64]) +} + +fn make_user(name: &str, email: &str) -> Result { + UserID::new(name.to_string(), email.to_string()) +} + +#[test] +fn hash_roundtrip_through_public_api() -> Result<(), VctrlError> { + let hash = make_hash(0x42)?; + assert_eq!(hash.as_bytes().len(), HASH_LENGTH); + assert_eq!(Hash::from_str(&hash.to_string())?, hash); + Ok(()) +} + +#[test] +fn validation_functions_work() -> Result<(), VctrlError> { + validate_name("valid-name")?; + assert!(validate_name("").is_err()); + + validate_ref_name("refs/heads/main")?; + assert!(validate_ref_name("refs/heads/.hidden").is_err()); + assert!(validate_ref_name("refs/heads/foo.lock/bar").is_err()); + assert!(validate_ref_name("@").is_err()); + + validate_tree_entry_name("file.txt")?; + assert!(validate_tree_entry_name("dir/file.txt").is_err()); + + Ok(()) +} + +#[test] +fn tree_builder_and_entry_builder_work() -> Result<(), VctrlError> { + let hash = make_hash(0x11)?; + let entry = TreeEntryBuilder::new("file.txt".to_string(), EntryKind::Blob, hash).build()?; + let tree = TreeBuilder::new().entry(entry).build()?; + + let entries = tree.entries(); + assert_eq!(entries.len(), 1); + let first = entries + .first() + .ok_or_else(|| VctrlError::Other("expected entry".into()))?; + assert_eq!(first.name(), "file.txt"); + assert_eq!(first.kind(), EntryKind::Blob); + assert_eq!(*first.hash(), hash); + Ok(()) +} + +#[test] +fn blob_builder_works() -> Result<(), VctrlError> { + let data = vec![1_u8, 2, 3, 4]; + let blob = BlobBuilder::new().with_data(data.clone()).build()?; + assert_eq!(blob.data(), data.as_slice()); + Ok(()) +} + +#[test] +fn commit_and_tag_builders_work() -> Result<(), VctrlError> { + let tree_hash = make_hash(0x22)?; + let parent_hash = make_hash(0x23)?; + let author = make_user("Alice", "alice@example.com")?; + let committer = make_user("Bob", "bob@example.com")?; + let meta = CommitMeta::new(1_600_000_000, 0, Some("utf-8".into()))?; + + let commit = CommitBuilder::new() + .tree(tree_hash) + .parent(parent_hash) + .author(author) + .committer(committer) + .message("builder commit") + .meta(meta) + .build()?; + + assert_eq!(commit.tree(), &tree_hash); + assert_eq!(commit.parents(), &[parent_hash]); + assert_eq!(commit.author().name(), "Alice"); + assert_eq!(commit.committer().email(), "bob@example.com"); + assert_eq!(commit.message(), "builder commit"); + assert_eq!(commit.meta().timestamp(), 1_600_000_000); + assert_eq!(commit.meta().encoding(), Some("utf-8")); + + let tagger = make_user("Tagger", "tagger@example.com")?; + let tag = TagBuilder::new() + .name("v1.0") + .target(tree_hash) + .tagger(tagger) + .message("release") + .build()?; + + assert_eq!(tag.name(), "v1.0"); + assert_eq!(tag.target(), &tree_hash); + assert_eq!( + tag.tagger() + .ok_or_else(|| VctrlError::Other("expected tagger".into()))? + .name(), + "Tagger" + ); + assert_eq!(tag.message(), "release"); + Ok(()) +} + +#[test] +fn codec_roundtrip_through_public_api() -> Result<(), VctrlError> { + let encoder = BinaryEncoder; + let decoder = BinaryDecoder; + + // Blob + let blob = Blob::new(b"roundtrip".to_vec())?; + let mut buf = Vec::new(); + encoder.encode_blob(&blob, &mut buf)?; + let decoded_blob = decoder.decode_blob(std::io::Cursor::new(buf))?; + assert_eq!(decoded_blob.data(), blob.data()); + + // Tree + let hash = make_hash(0x33)?; + 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_tree = decoder.decode_tree(std::io::Cursor::new(buf))?; + assert_eq!(decoded_tree.entries().len(), 1); + + // Commit + let author = make_user("Alice", "alice@example.com")?; + let committer = make_user("Bob", "bob@example.com")?; + let meta = CommitMeta::new(1_600_000_000, 0, None)?; + let commit = Commit::with_meta( + hash, + vec![hash], + author, + committer, + "commit".to_string(), + meta, + )?; + let mut buf = Vec::new(); + encoder.encode_commit(&commit, &mut buf)?; + let decoded_commit = decoder.decode_commit(std::io::Cursor::new(buf))?; + assert_eq!(decoded_commit.message(), "commit"); + + // Tag + let tagger = make_user("Tagger", "tagger@example.com")?; + let tag = Tag::with_meta( + "v1.0".to_string(), + hash, + Some(tagger), + "tag".to_string(), + CommitMeta::new(1_600_000_000, 0, None)?, + )?; + let mut buf = Vec::new(); + encoder.encode_tag(&tag, &mut buf)?; + let decoded_tag = decoder.decode_tag(std::io::Cursor::new(buf))?; + assert_eq!(decoded_tag.name(), "v1.0"); + + Ok(()) +} + +#[test] +fn hasher_public_api_works() -> Result<(), VctrlError> { + let hasher = Sha512Hasher; + let hash = hasher.hash(std::io::Cursor::new(b"test"))?; + assert_eq!(hash.as_bytes().len(), 64); + Ok(()) +} + +#[test] +fn memory_store_works() -> Result<(), VctrlError> { + let mut store = MemoryStore::new(); + let hash = make_hash(0x44)?; + let data = vec![7_u8, 8, 9]; + + store.put(&hash, &data)?; + assert!(store.exists(&hash)?); + + { + let mut reader = store.get(&hash)?; + let mut buf = Vec::new(); + let _ = std::io::Read::read_to_end(&mut reader, &mut buf)?; + assert_eq!(buf, data); + } + + store.delete(&hash)?; + assert!(!store.exists(&hash)?); + Ok(()) +} + +#[test] +fn memory_ref_store_works() -> Result<(), VctrlError> { + let mut store = MemoryRefStore::new(); + let hash = make_hash(0x55)?; + + store.set_ref("refs/heads/main", &hash)?; + assert_eq!(store.get_ref("refs/heads/main")?, hash); + + let refs: Vec = store.list_refs()?.collect::>()?; + assert_eq!(refs, vec!["refs/heads/main".to_string()]); + + store.delete_ref("refs/heads/main")?; + assert!(store.get_ref("refs/heads/main").is_err()); + Ok(()) +} diff --git a/libvctrl_core/Cargo.toml b/libvctrl_core/Cargo.toml index 6db201c0..58578e81 100644 --- a/libvctrl_core/Cargo.toml +++ b/libvctrl_core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libvctrl_core" -version = "3.0.1" +version = "3.2.0" edition = "2024" description = "Reference implementations of the libvctrl contracts (in-memory store, SHA-512 hasher, binary codec)" license = "MIT" @@ -11,8 +11,8 @@ keywords = ["version-control", "vcs", "core"] categories = ["development-tools"] [dependencies] -libvctrl_handler = {path = "../libvctrl_handler", version = "5.0.0" } -libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.0.0" } +libvctrl_handler = {path = "../libvctrl_handler", version = "5.2.0" } +libvctrl_sha512 = { path = "../libvctrl_sha512", version = "3.2.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 960917dd..5067465b 100644 --- a/libvctrl_core/src/codec/binary_decoder.rs +++ b/libvctrl_core/src/codec/binary_decoder.rs @@ -1,75 +1,17 @@ -//! # 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 alloc::str; +use alloc::sync::Arc; 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; -/// 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. -/// ``` +#[derive(Debug, Copy, Clone)] 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() @@ -85,12 +27,6 @@ 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, @@ -100,7 +36,7 @@ impl BinaryDecoder { loop { let n = reader .read(&mut chunk) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(Arc::new(e)))?; if n == 0 { break; } @@ -114,14 +50,12 @@ 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, @@ -137,35 +71,6 @@ 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)?; @@ -193,38 +98,6 @@ 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)?; @@ -284,57 +157,15 @@ 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 @@ -350,7 +181,6 @@ 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")?; @@ -359,7 +189,6 @@ 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")?; @@ -370,7 +199,6 @@ 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 = @@ -382,7 +210,6 @@ 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 = @@ -396,7 +223,6 @@ 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 @@ -417,7 +243,6 @@ 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 @@ -434,7 +259,6 @@ 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 { @@ -456,50 +280,12 @@ 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) @@ -507,12 +293,10 @@ 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, @@ -524,7 +308,6 @@ 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; @@ -552,7 +335,6 @@ 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 @@ -573,7 +355,6 @@ 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 @@ -590,7 +371,6 @@ 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 { @@ -612,3 +392,274 @@ 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 2bd8f738..9bad0c17 100644 --- a/libvctrl_core/src/codec/binary_encoder.rs +++ b/libvctrl_core/src/codec/binary_encoder.rs @@ -1,112 +1,14 @@ -//! # 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; -/// 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); -/// ``` +#[derive(Debug, Default, Clone, Copy)] 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)?; @@ -117,47 +19,7 @@ impl Encoder for BinaryEncoder { Ok(()) } - /// 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); - /// ``` + #[allow(clippy::wildcard_enum_match_arm)] fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError> { let entries = tree.entries(); writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; @@ -182,9 +44,7 @@ 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]) @@ -196,67 +56,6 @@ 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, @@ -274,9 +73,9 @@ impl Encoder for BinaryEncoder { .write_all(&parent_count.to_le_bytes()) .map_err(VctrlError::from_io)?; - for p in parents { + for parent in parents { writer - .write_all(p.as_bytes()) + .write_all(parent.as_bytes()) .map_err(VctrlError::from_io)?; } @@ -352,67 +151,11 @@ impl Encoder for BinaryEncoder { .write_all(enc.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).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)?; @@ -430,7 +173,7 @@ impl Encoder for BinaryEncoder { match tag.tagger() { Some(tagger) => { - writer.write_all(&[1u8]).map_err(VctrlError::from_io)?; + writer.write_all(&[1_u8]).map_err(VctrlError::from_io)?; let tagger_name = tagger.name(); writer @@ -452,7 +195,7 @@ impl Encoder for BinaryEncoder { .write_all(tagger_email.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).map_err(VctrlError::from_io)?, } let msg = tag.message(); @@ -487,8 +230,113 @@ impl Encoder for BinaryEncoder { .write_all(enc.as_bytes()) .map_err(VctrlError::from_io)?; } - None => writer.write_all(&[0u8]).map_err(VctrlError::from_io)?, + None => writer.write_all(&[0_u8]).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 fdda6cec..1baa3dff 100644 --- a/libvctrl_core/src/codec/mod.rs +++ b/libvctrl_core/src/codec/mod.rs @@ -1,70 +1,5 @@ -//! # 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 4653e977..fb1573fc 100644 --- a/libvctrl_core/src/hash/mod.rs +++ b/libvctrl_core/src/hash/mod.rs @@ -1,48 +1,3 @@ -//! 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 3474d034..8926e9d2 100644 --- a/libvctrl_core/src/hash/sha512.rs +++ b/libvctrl_core/src/hash/sha512.rs @@ -1,99 +1,14 @@ -//! 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 alloc::sync::Arc; +use std::io; use libvctrl_handler::{Hash, Hasher, VctrlError}; use libvctrl_sha512::Hash as Sha512Hash; -/// 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)] +#[derive(Debug, Default, Clone, Copy)] pub struct Sha512Hasher; impl Hasher for Sha512Hasher { - /// 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 { + fn hash(&self, mut reader: R) -> Result { let mut hasher = Sha512Hash::new(); let mut buffer = [0u8; 4096]; loop { @@ -102,8 +17,8 @@ impl Hasher for Sha512Hasher { break; } let chunk = buffer.get(..n).ok_or_else(|| { - VctrlError::IoError(std::sync::Arc::new(std::io::Error::new( - std::io::ErrorKind::UnexpectedEof, + VctrlError::IoError(Arc::new(io::Error::new( + io::ErrorKind::UnexpectedEof, "read returned invalid length", ))) })?; @@ -113,3 +28,49 @@ 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 9d83e949..3ed84bef 100644 --- a/libvctrl_core/src/lib.rs +++ b/libvctrl_core/src/lib.rs @@ -1,92 +1,11 @@ -//! # 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); -//! ``` +#![allow(clippy::arithmetic_side_effects)] + +extern crate alloc; #[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 1d1e6222..ddc22d87 100644 --- a/libvctrl_core/src/object/blob.rs +++ b/libvctrl_core/src/object/blob.rs @@ -1,121 +1,43 @@ -//! # 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 7f7867e6..d1ccafc8 100644 --- a/libvctrl_core/src/object/commit.rs +++ b/libvctrl_core/src/object/commit.rs @@ -1,82 +1,5 @@ -//! 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, @@ -88,25 +11,6 @@ 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 { @@ -119,184 +23,42 @@ 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 @@ -318,3 +80,106 @@ 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 13e0941d..509cc405 100644 --- a/libvctrl_core/src/object/mod.rs +++ b/libvctrl_core/src/object/mod.rs @@ -1,96 +1,15 @@ -//! 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 0950a424..ca6ee1db 100644 --- a/libvctrl_core/src/object/tag.rs +++ b/libvctrl_core/src/object/tag.rs @@ -1,76 +1,5 @@ -//! # 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, @@ -81,19 +10,6 @@ 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 { @@ -105,185 +21,36 @@ 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 @@ -305,3 +72,78 @@ 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 6e9e8a13..4e53743f 100644 --- a/libvctrl_core/src/object/tree.rs +++ b/libvctrl_core/src/object/tree.rs @@ -1,78 +1,11 @@ -//! # 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 { @@ -80,75 +13,12 @@ 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, @@ -160,76 +30,11 @@ 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, @@ -238,58 +43,65 @@ 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 47fefa14..bf55773c 100644 --- a/libvctrl_core/src/store/memory.rs +++ b/libvctrl_core/src/store/memory.rs @@ -1,104 +1,13 @@ -//! 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 { @@ -108,65 +17,11 @@ 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 @@ -175,53 +30,67 @@ 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 0a6e1d7c..1578b12e 100644 --- a/libvctrl_core/src/store/mod.rs +++ b/libvctrl_core/src/store/mod.rs @@ -1,70 +1,5 @@ -//! # 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 2998e60b..de5f8fe5 100644 --- a/libvctrl_core/src/store/ref_store.rs +++ b/libvctrl_core/src/store/ref_store.rs @@ -1,78 +1,14 @@ -//! # 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 alloc::vec::IntoIter; +use std::collections::HashMap; 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 { @@ -82,53 +18,14 @@ impl MemoryRefStore { } impl RefStore for MemoryRefStore { - 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()); - /// ``` + type RefsIterator = IntoIter>; + 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) @@ -136,62 +33,75 @@ 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 deleted file mode 100644 index a1881ae9..00000000 --- a/libvctrl_core/tests/codec_test.rs +++ /dev/null @@ -1,424 +0,0 @@ -//! # 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 new file mode 100644 index 00000000..bee37c00 --- /dev/null +++ b/libvctrl_core/tests/common/mod.rs @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..1393e270 --- /dev/null +++ b/libvctrl_core/tests/integration_builders.rs @@ -0,0 +1,40 @@ +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 new file mode 100644 index 00000000..bdc4aaaa --- /dev/null +++ b/libvctrl_core/tests/integration_codec.rs @@ -0,0 +1,113 @@ +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 new file mode 100644 index 00000000..3070cf25 --- /dev/null +++ b/libvctrl_core/tests/integration_hash.rs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 00000000..a0e22c8d --- /dev/null +++ b/libvctrl_core/tests/integration_store.rs @@ -0,0 +1,72 @@ +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 deleted file mode 100644 index bb6e432a..00000000 --- a/libvctrl_core/tests/store_test.rs +++ /dev/null @@ -1,171 +0,0 @@ -//! # 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 dcde7cb9..b76934a7 100644 --- a/libvctrl_handler/Cargo.toml +++ b/libvctrl_handler/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libvctrl_handler" -version = "5.0.1" +version = "5.2.0" edition = "2024" description = "Fundamental contracts for building a version control system – no implementations, only traits and types" license = "MIT" @@ -13,4 +13,11 @@ keywords = ["version-control", "vcs", "library", "traits"] categories = ["development-tools", "data-structures"] [lints] -workspace = true \ No newline at end of file +workspace = true + +[dev-dependencies] +criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] } + +[[bench]] +name = "handler_bench" +harness = false \ No newline at end of file diff --git a/libvctrl_handler/benches/handler_bench.rs b/libvctrl_handler/benches/handler_bench.rs new file mode 100644 index 00000000..ed0bc736 --- /dev/null +++ b/libvctrl_handler/benches/handler_bench.rs @@ -0,0 +1,129 @@ +#![allow(missing_docs)] + +use core::hint::black_box; +use core::str::FromStr; + +use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; +use libvctrl_handler::{ + Blob, Commit, EntryKind, HASH_LENGTH, Hash, Tree, TreeEntry, UserID, validate_ref_name, +}; + +fn build_tree_entries(count: usize) -> Vec { + let hash = Hash::from([0_u8; HASH_LENGTH]); + let mut entries = Vec::with_capacity(count); + for i in 0..count { + let name = format!("file_{i:06}"); + if let Ok(entry) = TreeEntry::new(name, EntryKind::Blob, hash) { + entries.push(entry); + } + } + entries +} + +fn bench_tree_build(c: &mut Criterion) { + let entries = build_tree_entries(5_000); + let _ = c.bench_function("tree/build_5000_entries", |b| { + b.iter_batched( + || entries.clone(), + |entries| { + let _ = black_box(Tree::new(entries)); + }, + BatchSize::SmallInput, + ); + }); +} + +fn bench_validate_refs(c: &mut Criterion) { + let valid_refs = [ + "refs/heads/main", + "refs/tags/v1.0.0", + "refs/remotes/origin/feature/foo", + "refs/heads/bar", + "refs/heads/a-branch.name", + ]; + let invalid_refs = [ + "refs/heads/.hidden", + "refs/heads/foo.lock/bar", + "@", + "refs/heads//double", + ]; + + let _ = c.bench_function("validation/ref_name_valid", |b| { + b.iter(|| { + for name in &valid_refs { + let _ = black_box(validate_ref_name(name)); + } + }); + }); + + let _ = c.bench_function("validation/ref_name_invalid", |b| { + b.iter(|| { + for name in &invalid_refs { + let _ = black_box(validate_ref_name(name)); + } + }); + }); +} + +fn bench_hash_parse(c: &mut Criterion) { + let hex_str = "ab".repeat(HASH_LENGTH); // 64 byte hex = 128 char + let _ = c.bench_function("hash/from_hex_string", |b| { + b.iter(|| { + let _ = black_box(Hash::from_str(&hex_str)); + }); + }); +} + +fn bench_blob_new(c: &mut Criterion) { + let data = vec![0x42_u8; 1024 * 1024]; // 1 MiB + let _ = c.bench_function("blob/new_1MiB", |b| { + b.iter_batched( + || data.clone(), + |data| { + let _ = black_box(Blob::new(data)); + }, + BatchSize::LargeInput, + ); + }); +} + +fn build_user() -> Option { + UserID::new("Bench User".into(), "bench@example.com".into()).ok() +} + +fn bench_commit_build(c: &mut Criterion) { + let Some(user) = build_user() else { + return; + }; + let tree_hash = Hash::from([0_u8; HASH_LENGTH]); + let parents: Vec = (0..10).map(|_| Hash::from([1_u8; HASH_LENGTH])).collect(); + let message = "benchmark commit".to_string(); + + let _ = c.bench_function("commit/new_10_parents", |b| { + b.iter_batched( + || { + ( + tree_hash, + parents.clone(), + user.clone(), + user.clone(), + message.clone(), + ) + }, + |(tree, parents, author, committer, msg)| { + let _ = black_box(Commit::new(tree, parents, author, committer, msg)); + }, + BatchSize::SmallInput, + ); + }); +} + +criterion_group!( + benches, + bench_tree_build, + bench_validate_refs, + bench_hash_parse, + bench_blob_new, + bench_commit_build +); +criterion_main!(benches); diff --git a/libvctrl_handler/src/constants.rs b/libvctrl_handler/src/constants.rs index 40d04fb5..1369874d 100644 --- a/libvctrl_handler/src/constants.rs +++ b/libvctrl_handler/src/constants.rs @@ -1,187 +1,14 @@ -//! 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 01d64120..3f2a9a50 100644 --- a/libvctrl_handler/src/enums/core/entry_kind.rs +++ b/libvctrl_handler/src/enums/core/entry_kind.rs @@ -1,73 +1,16 @@ -//! 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 { @@ -79,37 +22,6 @@ 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 9bb4e585..ff38ed16 100644 --- a/libvctrl_handler/src/enums/core/mod.rs +++ b/libvctrl_handler/src/enums/core/mod.rs @@ -1,26 +1 @@ -//! 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 60222dff..f47b1738 100644 --- a/libvctrl_handler/src/enums/mod.rs +++ b/libvctrl_handler/src/enums/mod.rs @@ -1,51 +1,2 @@ -//! 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 e144c4c6..a5a24d8b 100644 --- a/libvctrl_handler/src/errors.rs +++ b/libvctrl_handler/src/errors.rs @@ -1,89 +1,27 @@ -//! 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 alloc::sync::Arc; +use core::error::Error; +use core::fmt; +use std::io; use crate::constants::HASH_LENGTH; use crate::types::Hash; -use std::error::Error; -use std::fmt; -use std::io; -use std::sync::Arc; -/// 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), } @@ -184,28 +122,6 @@ 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 fb856157..9f8fd829 100644 --- a/libvctrl_handler/src/lib.rs +++ b/libvctrl_handler/src/lib.rs @@ -1,123 +1,22 @@ -//! # `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); -//! ``` +extern crate alloc; -/// 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; +#[cfg(test)] +use criterion as _; -/// 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 constants; 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, @@ -136,18 +35,10 @@ 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 e6f24884..322fabdf 100644 --- a/libvctrl_handler/src/macros.rs +++ b/libvctrl_handler/src/macros.rs @@ -1,42 +1,3 @@ -/// 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 69dba60c..f6598014 100644 --- a/libvctrl_handler/src/traits/core/blame.rs +++ b/libvctrl_handler/src/traits/core/blame.rs @@ -1,49 +1,6 @@ -//! 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, @@ -54,39 +11,6 @@ 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, @@ -106,158 +30,32 @@ 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 8d061c0c..2860ccac 100644 --- a/libvctrl_handler/src/traits/core/config.rs +++ b/libvctrl_handler/src/traits/core/config.rs @@ -1,289 +1,10 @@ -//! 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 0141b047..45af17d7 100644 --- a/libvctrl_handler/src/traits/core/decoder.rs +++ b/libvctrl_handler/src/traits/core/decoder.rs @@ -1,223 +1,11 @@ -//! 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 std::io::Read; 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 82d52bb6..f07ad5a1 100644 --- a/libvctrl_handler/src/traits/core/diff.rs +++ b/libvctrl_handler/src/traits/core/diff.rs @@ -1,119 +1,8 @@ -//! 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 47e2fb4a..aa5641fb 100644 --- a/libvctrl_handler/src/traits/core/encoder.rs +++ b/libvctrl_handler/src/traits/core/encoder.rs @@ -1,228 +1,15 @@ -//! 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 std::io::Write; 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 74ce3cda..69ea7679 100644 --- a/libvctrl_handler/src/traits/core/hasher.rs +++ b/libvctrl_handler/src/traits/core/hasher.rs @@ -1,109 +1,8 @@ -//! 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 std::io::Read; 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 9adcba0f..de484a25 100644 --- a/libvctrl_handler/src/traits/core/index.rs +++ b/libvctrl_handler/src/traits/core/index.rs @@ -1,503 +1,20 @@ -//! 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 { - /// 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 Entry: Clone + Send + Sync; 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 8b1a09ac..4dad8b42 100644 --- a/libvctrl_handler/src/traits/core/mod.rs +++ b/libvctrl_handler/src/traits/core/mod.rs @@ -1,340 +1,16 @@ -//! 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 f11beb82..166c3fc6 100644 --- a/libvctrl_handler/src/traits/core/object_store.rs +++ b/libvctrl_handler/src/traits/core/object_store.rs @@ -1,243 +1,11 @@ -//! 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 std::io::Read; 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 3a39535a..c94d7c08 100644 --- a/libvctrl_handler/src/traits/core/pack.rs +++ b/libvctrl_handler/src/traits/core/pack.rs @@ -1,231 +1,16 @@ -//! 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 std::io::Read; 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 fe685f94..c77c6035 100644 --- a/libvctrl_handler/src/traits/core/ref_store.rs +++ b/libvctrl_handler/src/traits/core/ref_store.rs @@ -1,251 +1,11 @@ -//! 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 b9d945a0..76d8e37e 100644 --- a/libvctrl_handler/src/traits/core/reflog.rs +++ b/libvctrl_handler/src/traits/core/reflog.rs @@ -1,134 +1,9 @@ -//! 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, @@ -139,38 +14,5 @@ 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 9df76fdc..10772c3a 100644 --- a/libvctrl_handler/src/traits/core/remote.rs +++ b/libvctrl_handler/src/traits/core/remote.rs @@ -1,196 +1,10 @@ -//! 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 0fe3bd8e..ed5dce8b 100644 --- a/libvctrl_handler/src/traits/core/revwalk.rs +++ b/libvctrl_handler/src/traits/core/revwalk.rs @@ -1,127 +1,10 @@ -//! 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 02e8ac5d..57ca2c2c 100644 --- a/libvctrl_handler/src/traits/core/signer.rs +++ b/libvctrl_handler/src/traits/core/signer.rs @@ -1,101 +1,5 @@ -//! 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 545168ea..09ed5a1a 100644 --- a/libvctrl_handler/src/traits/core/transport.rs +++ b/libvctrl_handler/src/traits/core/transport.rs @@ -1,157 +1,9 @@ -//! 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 std::io::Read; 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 e3f36ec4..6e2b159e 100644 --- a/libvctrl_handler/src/traits/core/verifier.rs +++ b/libvctrl_handler/src/traits/core/verifier.rs @@ -1,106 +1,5 @@ -//! 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 2fc231f6..5a7ca06a 100644 --- a/libvctrl_handler/src/traits/mod.rs +++ b/libvctrl_handler/src/traits/mod.rs @@ -1,39 +1 @@ -//! 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 34376d06..e57ac568 100644 --- a/libvctrl_handler/src/types/core/blob.rs +++ b/libvctrl_handler/src/types/core/blob.rs @@ -1,73 +1,12 @@ -//! 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 { @@ -80,63 +19,16 @@ 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 b11fc250..874fa7f3 100644 --- a/libvctrl_handler/src/types/core/commit.rs +++ b/libvctrl_handler/src/types/core/commit.rs @@ -1,39 +1,10 @@ -//! 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 std::collections::HashSet; use super::hash::Hash; use super::user_id::UserID; use crate::constants::{MAX_MESSAGE_LENGTH, MAX_PARENT_COUNT}; use crate::errors::VctrlError; -use std::collections::HashSet; -/// 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, @@ -42,30 +13,6 @@ 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, @@ -81,54 +28,22 @@ 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, @@ -140,31 +55,6 @@ 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, @@ -182,37 +72,6 @@ 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, @@ -237,8 +96,8 @@ impl Commit { } let mut seen = HashSet::new(); - for p in &parents { - if !seen.insert(*p) { + for parent in &parents { + if !seen.insert(*parent) { return Err(VctrlError::DuplicateParent); } } @@ -253,60 +112,31 @@ 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 e591a532..b40b437d 100644 --- a/libvctrl_handler/src/types/core/delta.rs +++ b/libvctrl_handler/src/types/core/delta.rs @@ -1,73 +1,19 @@ -//! 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 alloc::vec::IntoIter as VecIntoIter; +use core::slice::Iter as SliceIter; 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, @@ -78,11 +24,6 @@ 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 { @@ -94,11 +35,6 @@ 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 { @@ -110,11 +46,6 @@ 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 { @@ -126,11 +57,6 @@ 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 { @@ -142,12 +68,6 @@ 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, @@ -164,11 +84,6 @@ 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, @@ -185,131 +100,68 @@ 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 { @@ -317,42 +169,25 @@ 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() } - /// 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> { + pub fn iter(&self) -> SliceIter<'_, 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 @@ -361,14 +196,8 @@ impl TreeDelta { impl IntoIterator for TreeDelta { type Item = FileDelta; - type IntoIter = std::vec::IntoIter; + type IntoIter = VecIntoIter; - /// 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() } @@ -376,13 +205,8 @@ impl IntoIterator for TreeDelta { impl<'a> IntoIterator for &'a TreeDelta { type Item = &'a FileDelta; - type IntoIter = std::slice::Iter<'a, FileDelta>; + type IntoIter = SliceIter<'a, FileDelta>; - /// 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 faed018d..e5f81622 100644 --- a/libvctrl_handler/src/types/core/hash.rs +++ b/libvctrl_handler/src/types/core/hash.rs @@ -1,78 +1,13 @@ -//! 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 core::fmt; +use core::str::FromStr; 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 { @@ -82,26 +17,11 @@ impl Hash { let mut i = 0; while i < HASH_LENGTH { arr[i] = bytes[i]; - i += 1; + i = i.wrapping_add(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 @@ -109,11 +29,6 @@ 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) } @@ -122,23 +37,12 @@ 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 } @@ -147,30 +51,6 @@ 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())); @@ -189,12 +69,6 @@ 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) { @@ -205,23 +79,6 @@ 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 75cca020..ac2d38a4 100644 --- a/libvctrl_handler/src/types/core/merge.rs +++ b/libvctrl_handler/src/types/core/merge.rs @@ -1,50 +1,7 @@ -//! 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, @@ -54,12 +11,6 @@ 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 { @@ -70,120 +21,44 @@ 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 ab604cdf..6956f873 100644 --- a/libvctrl_handler/src/types/core/mod.rs +++ b/libvctrl_handler/src/types/core/mod.rs @@ -1,113 +1,26 @@ -//! 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 ef24a120..f5dd33af 100644 --- a/libvctrl_handler/src/types/core/reflog.rs +++ b/libvctrl_handler/src/types/core/reflog.rs @@ -1,52 +1,6 @@ -//! 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, @@ -57,30 +11,6 @@ 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, @@ -100,52 +30,26 @@ 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 4267cace..645040cd 100644 --- a/libvctrl_handler/src/types/core/tag.rs +++ b/libvctrl_handler/src/types/core/tag.rs @@ -1,18 +1,3 @@ -//! 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; @@ -20,35 +5,6 @@ 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, @@ -59,28 +15,6 @@ 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, @@ -90,37 +24,6 @@ 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, @@ -144,50 +47,26 @@ 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 497b04e8..79a92f68 100644 --- a/libvctrl_handler/src/types/core/tree.rs +++ b/libvctrl_handler/src/types/core/tree.rs @@ -1,46 +1,12 @@ -//! 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 core::cmp::Ordering; +use std::collections::HashSet; use super::hash::Hash; use crate::constants::MAX_TREE_ENTRIES; use crate::enums::EntryKind; use crate::errors::VctrlError; use crate::validation::validate_tree_entry_name; -use std::cmp::Ordering; -/// 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, @@ -49,99 +15,33 @@ 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 { @@ -151,62 +51,43 @@ impl Tree { ))); } - let mut sorted = entries; - sorted.sort_by(compare_tree_entries); - - for window in sorted.windows(2) { - if let (Some(first), Some(second)) = (window.first(), window.get(1)) - && first.name == second.name - { + let mut seen = HashSet::with_capacity(entries.len()); + for entry in &entries { + if !seen.insert(entry.name.clone()) { return Err(VctrlError::InvalidTreeStructure(format!( "duplicate entry name: '{}'", - first.name + entry.name ))); } } + let mut sorted = entries; + sorted.sort_by(compare_tree_entries); + Ok(Self { entries: sorted }) } - /// 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(|e| e.name == name) + self.entries.iter().find(|entry| entry.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(); @@ -214,8 +95,8 @@ fn compare_tree_entries(a: &TreeEntry, b: &TreeEntry) -> Ordering { let a_is_tree = a.kind == EntryKind::Tree; let b_is_tree = b.kind == EntryKind::Tree; - let a_len = a_bytes.len() + usize::from(a_is_tree); - let b_len = b_bytes.len() + usize::from(b_is_tree); + let a_len = a_bytes.len().wrapping_add(usize::from(a_is_tree)); + let b_len = b_bytes.len().wrapping_add(usize::from(b_is_tree)); let min_len = a_len.min(b_len); for i in 0..min_len { @@ -260,13 +141,18 @@ mod tests { let e2 = TreeEntry::new("a".into(), EntryKind::Blob, h)?; let tree = Tree::new(vec![e1, e2])?; - assert_eq!(tree.entries().first().map(|e| e.name()), Some("a")); - assert_eq!(tree.entries().get(1).map(|e| e.name()), Some("b")); + assert_eq!(tree.entries().first().map(TreeEntry::name), Some("a")); + assert_eq!(tree.entries().get(1).map(TreeEntry::name), Some("b")); let dup1 = TreeEntry::new("x".into(), EntryKind::Blob, h)?; let dup2 = TreeEntry::new("x".into(), EntryKind::Tree, h)?; assert!(Tree::new(vec![dup1, dup2]).is_err()); + let tricky_dup1 = TreeEntry::new("x".into(), EntryKind::Blob, h)?; + let tricky_mid = TreeEntry::new("x.".into(), EntryKind::Blob, h)?; + let tricky_dup2 = TreeEntry::new("x".into(), EntryKind::Tree, h)?; + assert!(Tree::new(vec![tricky_dup1, tricky_mid, tricky_dup2]).is_err()); + Ok(()) } } diff --git a/libvctrl_handler/src/types/core/user_id.rs b/libvctrl_handler/src/types/core/user_id.rs index cf46707a..dc5502c2 100644 --- a/libvctrl_handler/src/types/core/user_id.rs +++ b/libvctrl_handler/src/types/core/user_id.rs @@ -1,47 +1,6 @@ -//! 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, @@ -49,32 +8,6 @@ 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() { @@ -111,21 +44,11 @@ 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 48a446b7..2db0371f 100644 --- a/libvctrl_handler/src/types/mod.rs +++ b/libvctrl_handler/src/types/mod.rs @@ -1,65 +1,5 @@ -//! 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 51f08c4a..e5f592f2 100644 --- a/libvctrl_handler/src/validation/hash.rs +++ b/libvctrl_handler/src/validation/hash.rs @@ -1,59 +1,6 @@ -//! 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 351bdb69..7f580f22 100644 --- a/libvctrl_handler/src/validation/mod.rs +++ b/libvctrl_handler/src/validation/mod.rs @@ -1,76 +1,5 @@ -//! 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 51452d02..4a89992e 100644 --- a/libvctrl_handler/src/validation/name.rs +++ b/libvctrl_handler/src/validation/name.rs @@ -1,50 +1,8 @@ -//! 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 std::path::Path; 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())); @@ -63,105 +21,49 @@ 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.contains("..") - || name.contains('~') - || name.contains('^') - || name.contains(':') - || name.contains('?') - || name.contains('*') - || name.contains('[') - || name.contains('\\') - || name.contains(' ') - || name.contains("@{") - || name.contains("//") - || name.starts_with('.') - || name.starts_with('/') - || name.ends_with('/') - || name.ends_with('.') - || name.contains('<') - || name.contains('>') - || name.contains('|') - || name.contains('"') - || Path::new(name) - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) - { + + if name == "@" { + return Err(VctrlError::InvalidName("ref name cannot be '@'".into())); + } + + if name.starts_with('/') || name.ends_with('/') || name.contains("//") { return Err(VctrlError::InvalidName(format!( "invalid ref name: '{name}'" ))); } + + for component in name.split('/') { + if component.is_empty() + || component.starts_with('.') + || Path::new(component) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("lock")) + || component.contains("..") + || component.contains('~') + || component.contains('^') + || component.contains(':') + || component.contains('?') + || component.contains('*') + || component.contains('[') + || component.contains('\\') + || component.contains(' ') + || component.contains("@{") + || component.contains('<') + || component.contains('>') + || component.contains('|') + || component.contains('"') + { + return Err(VctrlError::InvalidName(format!( + "invalid ref name: '{name}'" + ))); + } + } + Ok(()) } -/// 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 new file mode 100644 index 00000000..bfe66d4c --- /dev/null +++ b/libvctrl_handler/tests/blob.rs @@ -0,0 +1,39 @@ +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 new file mode 100644 index 00000000..c678d4cd --- /dev/null +++ b/libvctrl_handler/tests/commit.rs @@ -0,0 +1,117 @@ +use criterion as _; +use libvctrl_handler::{ + Commit, CommitMeta, HASH_LENGTH, Hash, MAX_MESSAGE_LENGTH, MAX_PARENT_COUNT, UserID, VctrlError, +}; +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +fn user() -> UserID { + common::ok(UserID::new( + "Alice".to_string(), + "alice@example.com".to_string(), + )) +} + +#[test] +fn test_commit_new_valid_empty_parents() { + let tree = h(1); + let author = user(); + let committer = user(); + + let commit = common::ok(Commit::new( + tree, + Vec::new(), + author.clone(), + committer.clone(), + "initial commit".to_string(), + )); + + assert_eq!(commit.tree(), &tree); + assert!(commit.parents().is_empty()); + assert_eq!(commit.author(), &author); + assert_eq!(commit.committer(), &committer); + assert_eq!(commit.message(), "initial commit"); + assert_eq!(commit.meta().timestamp(), 0); + assert_eq!(commit.meta().timezone_offset(), 0); +} + +#[test] +fn test_commit_new_duplicate_parent() { + let tree = h(1); + let parent = h(2); + let author = user(); + let committer = user(); + + let result = Commit::new( + tree, + vec![parent, parent], + author, + committer, + "duplicate".to_string(), + ); + + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::DuplicateParent); +} + +#[test] +fn test_commit_new_too_many_parents() { + let tree = h(1); + let parent = h(2); + let author = user(); + let committer = user(); + + let max_parents = usize::try_from(MAX_PARENT_COUNT).unwrap_or(usize::MAX); + let parents = vec![parent; max_parents + 1]; + + let result = Commit::new(tree, parents, author, committer, "many parents".to_string()); + + assert!(result.is_err()); + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_commit_new_message_too_long() { + let tree = h(1); + let author = user(); + let committer = user(); + let max_msg = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX); + let message = "a".repeat(max_msg + 1); + + let result = Commit::new(tree, Vec::new(), author, committer, message); + + assert!(result.is_err()); + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_commit_with_meta() { + let tree = h(1); + let author = user(); + let committer = user(); + let meta = common::ok(CommitMeta::new(1_700_000_000, 120, Some("utf-8".into()))); + + let commit = common::ok(Commit::with_meta( + tree, + Vec::new(), + author, + committer, + "meta commit".to_string(), + meta, + )); + + assert_eq!(commit.meta().timestamp(), 1_700_000_000); + assert_eq!(commit.meta().timezone_offset(), 120); + assert_eq!(commit.meta().encoding(), Some("utf-8")); +} diff --git a/libvctrl_handler/tests/commit_meta.rs b/libvctrl_handler/tests/commit_meta.rs new file mode 100644 index 00000000..a5505148 --- /dev/null +++ b/libvctrl_handler/tests/commit_meta.rs @@ -0,0 +1,35 @@ +use criterion as _; +use libvctrl_handler::{CommitMeta, VctrlError}; +mod common; + +#[test] +fn test_commit_meta_valid_boundaries() { + let meta_min = common::ok(CommitMeta::new(123, -1440, None)); + assert_eq!(meta_min.timestamp(), 123); + assert_eq!(meta_min.timezone_offset(), -1440); + assert_eq!(meta_min.encoding(), None); + + let meta_zero = common::ok(CommitMeta::new(0, 0, Some("utf-8".into()))); + assert_eq!(meta_zero.timestamp(), 0); + assert_eq!(meta_zero.timezone_offset(), 0); + assert_eq!(meta_zero.encoding(), Some("utf-8")); + + let meta_max = common::ok(CommitMeta::new(456, 1440, Some("iso-8859-1".into()))); + assert_eq!(meta_max.timestamp(), 456); + assert_eq!(meta_max.timezone_offset(), 1440); + assert_eq!(meta_max.encoding(), Some("iso-8859-1")); +} + +#[test] +fn test_commit_meta_invalid_timezone() { + let result = CommitMeta::new(0, -1441, None); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidTimezoneOffset(-1441) + ); + + let result = CommitMeta::new(0, 1441, None); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidTimezoneOffset(1441)); +} diff --git a/libvctrl_handler/tests/common/mod.rs b/libvctrl_handler/tests/common/mod.rs new file mode 100644 index 00000000..2ad43f8f --- /dev/null +++ b/libvctrl_handler/tests/common/mod.rs @@ -0,0 +1,17 @@ +#![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 new file mode 100644 index 00000000..3243f3df --- /dev/null +++ b/libvctrl_handler/tests/delta.rs @@ -0,0 +1,157 @@ +use criterion as _; +use libvctrl_handler::{ + ChangeKind, Conflict, FileDelta, HASH_LENGTH, Hash, MergeResult, TreeDelta, +}; +use std::path::{Path, PathBuf}; + +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +#[test] +fn test_file_delta_added() { + let h1 = h(1); + let delta = FileDelta::added(PathBuf::from("a.txt"), h1); + + assert!(delta.is_added()); + assert!(!delta.is_deleted()); + assert!(!delta.is_modified()); + assert!(!delta.is_type_change()); + assert!(!delta.is_renamed()); + assert!(!delta.is_copied()); + + assert_eq!(delta.path(), Path::new("a.txt")); + assert_eq!(delta.old_path(), None); + assert_eq!(delta.old_hash(), None); + assert_eq!(delta.new_hash(), Some(h1)); + assert_eq!(delta.kind(), ChangeKind::Added); +} + +#[test] +fn test_file_delta_deleted() { + let h1 = h(1); + let delta = FileDelta::deleted(PathBuf::from("a.txt"), h1); + + assert!(delta.is_deleted()); + assert!(!delta.is_added()); + assert_eq!(delta.path(), Path::new("a.txt")); + assert_eq!(delta.old_hash(), Some(h1)); + assert_eq!(delta.new_hash(), None); + assert_eq!(delta.kind(), ChangeKind::Deleted); +} + +#[test] +fn test_file_delta_modified_and_type_change() { + let h1 = h(1); + let h2 = h(2); + + let modified = FileDelta::modified(PathBuf::from("a.txt"), h1, h2); + assert!(modified.is_modified()); + assert_eq!(modified.old_hash(), Some(h1)); + assert_eq!(modified.new_hash(), Some(h2)); + assert_eq!(modified.kind(), ChangeKind::Modified); + + let type_change = FileDelta::type_change(PathBuf::from("a.txt"), h1, h2); + assert!(type_change.is_type_change()); + assert_eq!(type_change.old_hash(), Some(h1)); + assert_eq!(type_change.new_hash(), Some(h2)); + assert_eq!(type_change.kind(), ChangeKind::TypeChange); +} + +#[test] +fn test_file_delta_renamed_and_copied() { + let h1 = h(1); + let h2 = h(2); + + let renamed = FileDelta::renamed(PathBuf::from("old.txt"), PathBuf::from("new.txt"), h1, h2); + assert!(renamed.is_renamed()); + assert_eq!(renamed.path(), Path::new("new.txt")); + assert_eq!(renamed.old_path(), Some(Path::new("old.txt"))); + assert_eq!(renamed.old_hash(), Some(h1)); + assert_eq!(renamed.new_hash(), Some(h2)); + assert_eq!(renamed.kind(), ChangeKind::Renamed); + + let copied = FileDelta::copied(PathBuf::from("old.txt"), PathBuf::from("copy.txt"), h1, h2); + assert!(copied.is_copied()); + assert_eq!(copied.path(), Path::new("copy.txt")); + assert_eq!(copied.old_path(), Some(Path::new("old.txt"))); + assert_eq!(copied.kind(), ChangeKind::Copied); +} + +#[test] +fn test_tree_delta_basic() { + let delta = TreeDelta::new(); + assert!(delta.is_empty()); + assert_eq!(delta.len(), 0); + assert_eq!(delta.changes().len(), 0); + assert_eq!(delta.iter().count(), 0); +} + +#[test] +fn test_tree_delta_from_changes() { + let h1 = h(1); + let changes = vec![ + FileDelta::added(PathBuf::from("a.txt"), h1), + FileDelta::added(PathBuf::from("b.txt"), h1), + ]; + + let delta = TreeDelta::from_changes(changes); + assert!(!delta.is_empty()); + assert_eq!(delta.len(), 2); + assert_eq!(delta.changes().len(), 2); + assert_eq!(delta.iter().count(), 2); + assert_eq!(delta.into_iter().count(), 2); +} + +#[test] +fn test_tree_delta_iter_by_ref() { + let h1 = h(1); + let delta = TreeDelta::from_changes(vec![FileDelta::added(PathBuf::from("a.txt"), h1)]); + + let refs: Vec<&FileDelta> = (&delta).into_iter().collect(); + assert_eq!(refs.len(), 1); + assert_eq!( + refs.first().map(|delta| delta.path()), + Some(Path::new("a.txt")) + ); +} + +#[test] +fn test_conflict_accessors() { + let ancestor = h(1); + let ours = h(2); + let theirs = h(3); + + let conflict = Conflict::new(PathBuf::from("file.txt"), ancestor, ours, theirs); + + assert_eq!(conflict.path(), Path::new("file.txt")); + assert_eq!(conflict.ancestor_blob(), ancestor); + assert_eq!(conflict.our_blob(), ours); + assert_eq!(conflict.their_blob(), theirs); +} + +#[test] +fn test_merge_result_variants() { + let h1 = h(1); + let success = MergeResult::Success(h1); + assert!(success.is_success()); + assert!(!success.is_conflicts()); + assert!(success.conflicts().is_none()); + + let conflict = Conflict::new(PathBuf::from("file.txt"), h1, h(2), h(3)); + let conflicts = MergeResult::Conflicts(vec![conflict]); + assert!(!conflicts.is_success()); + assert!(conflicts.is_conflicts()); + + let conflict_list = conflicts.conflicts(); + assert!(conflict_list.is_some(), "expected conflicts"); + if let Some(c) = conflict_list { + assert_eq!(c.len(), 1); + } else { + loop { + core::hint::spin_loop(); + } + } +} diff --git a/libvctrl_handler/tests/entry_kind.rs b/libvctrl_handler/tests/entry_kind.rs new file mode 100644 index 00000000..b9d16bb3 --- /dev/null +++ b/libvctrl_handler/tests/entry_kind.rs @@ -0,0 +1,34 @@ +use criterion as _; +use libvctrl_handler::EntryKind; +use libvctrl_handler::constants::entry_mode; +mod common; + +#[test] +fn test_entry_kind_mode_matches_constants() { + assert_eq!(EntryKind::Blob.mode(), entry_mode::BLOB); + assert_eq!(EntryKind::Executable.mode(), entry_mode::EXECUTABLE); + assert_eq!(EntryKind::Symlink.mode(), entry_mode::SYMLINK); + assert_eq!(EntryKind::Tree.mode(), entry_mode::TREE); + assert_eq!(EntryKind::Submodule.mode(), entry_mode::SUBMODULE); +} + +#[test] +fn test_entry_kind_from_mode_roundtrip() { + let kinds = [ + EntryKind::Blob, + EntryKind::Executable, + EntryKind::Symlink, + EntryKind::Tree, + EntryKind::Submodule, + ]; + + for kind in kinds { + assert_eq!(EntryKind::from_mode(kind.mode()), Some(kind)); + } +} + +#[test] +fn test_entry_kind_from_mode_invalid() { + assert_eq!(EntryKind::from_mode(0), None); + assert_eq!(EntryKind::from_mode(u32::MAX), None); +} diff --git a/libvctrl_handler/tests/errors.rs b/libvctrl_handler/tests/errors.rs new file mode 100644 index 00000000..34049076 --- /dev/null +++ b/libvctrl_handler/tests/errors.rs @@ -0,0 +1,123 @@ +use core::error::Error as _; +use criterion as _; +use libvctrl_handler::{HASH_LENGTH, Hash, VctrlError}; +use std::io; + +mod common; + +#[test] +fn test_vctrl_error_display_variants() { + assert_eq!( + VctrlError::CorruptedData("x".to_string()).to_string(), + "Corrupted data: x" + ); + assert_eq!( + VctrlError::DuplicateParent.to_string(), + "Duplicate parent in commit" + ); + assert_eq!( + VctrlError::ExceededMaxSize("x".to_string()).to_string(), + "Exceeded max size: x" + ); + assert_eq!( + VctrlError::InvalidBlameRange.to_string(), + "Invalid blame range" + ); + assert_eq!( + VctrlError::InvalidEmail("a".to_string()).to_string(), + "Invalid email: 'a'" + ); + assert_eq!( + VctrlError::InvalidHashLength(10).to_string(), + "Invalid hash length: expected 64 bytes, got 10" + ); + assert_eq!( + VctrlError::InvalidName("n".to_string()).to_string(), + "Invalid name: 'n'" + ); + assert_eq!( + VctrlError::InvalidTimezoneOffset(-1441).to_string(), + "Invalid timezone offset: -1441" + ); + assert_eq!( + VctrlError::InvalidTreeStructure("t".to_string()).to_string(), + "Invalid tree structure: t" + ); + assert_eq!(VctrlError::Other("o".to_string()).to_string(), "o"); + assert_eq!( + VctrlError::RefNotFound("r".to_string()).to_string(), + "Reference not found: 'r'" + ); + assert_eq!( + VctrlError::SerializationError("s".to_string()).to_string(), + "Serialization error: s" + ); +} + +#[test] +fn test_vctrl_error_io_display_and_source() { + let io_err = io::Error::new(io::ErrorKind::NotFound, "missing"); + let err = VctrlError::from(io_err); + + assert!(err.to_string().contains("I/O error:")); + assert!(err.source().is_some()); + + assert!( + matches!(&err, VctrlError::IoError(_)), + "unexpected variant: {err:?}" + ); + + if let VctrlError::IoError(arc_err) = err { + assert_eq!(arc_err.as_ref().kind(), io::ErrorKind::NotFound); + assert_eq!(arc_err.as_ref().to_string(), "missing"); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_vctrl_error_from_io() { + let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied"); + let err = VctrlError::from_io(io_err); + + assert!( + matches!(&err, VctrlError::IoError(_)), + "unexpected variant: {err:?}" + ); + + if let VctrlError::IoError(arc_err) = err { + assert_eq!(arc_err.as_ref().kind(), io::ErrorKind::PermissionDenied); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_vctrl_error_partial_eq() { + assert_eq!( + VctrlError::InvalidName("x".to_string()), + VctrlError::InvalidName("x".to_string()) + ); + assert_ne!( + VctrlError::InvalidName("x".to_string()), + VctrlError::InvalidName("y".to_string()) + ); + + assert_eq!(VctrlError::DuplicateParent, VctrlError::DuplicateParent); + assert_ne!(VctrlError::DuplicateParent, VctrlError::InvalidBlameRange); + + let hash = Hash::from([0_u8; HASH_LENGTH]); + let hash2 = Hash::from([1_u8; HASH_LENGTH]); + assert_eq!( + VctrlError::ObjectNotFound(hash), + VctrlError::ObjectNotFound(hash) + ); + assert_ne!( + VctrlError::ObjectNotFound(hash), + VctrlError::ObjectNotFound(hash2) + ); +} diff --git a/libvctrl_handler/tests/hash.rs b/libvctrl_handler/tests/hash.rs new file mode 100644 index 00000000..9b0528e7 --- /dev/null +++ b/libvctrl_handler/tests/hash.rs @@ -0,0 +1,110 @@ +use criterion as _; +use libvctrl_handler::constants::HASH_LENGTH; +use libvctrl_handler::{Hash, VctrlError}; +mod common; + +fn valid_hex() -> String { + use core::fmt::Write; + + let mut s = String::with_capacity(HASH_LENGTH * 2); + for b in 0..HASH_LENGTH { + let _ = write!(s, "{b:02x}"); + } + s +} + +#[test] +fn test_hash_from_bytes_valid() { + let bytes = [7_u8; HASH_LENGTH]; + let hash = common::ok(Hash::from_bytes(&bytes)); + assert_eq!(&hash.as_bytes()[..], &bytes[..]); +} + +#[test] +fn test_hash_from_bytes_invalid_length() { + let result = Hash::from_bytes(&[0_u8; 10]); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(10)); +} + +#[test] +fn test_hash_from_array() { + let arr = [1_u8; HASH_LENGTH]; + let hash = Hash::from(arr); + assert_eq!(&hash.as_bytes()[..], &arr[..]); +} + +#[test] +fn test_hash_try_from_slice_valid() { + let arr = [2_u8; HASH_LENGTH]; + let hash: Hash = common::ok(Hash::try_from(&arr[..])); + assert_eq!(&hash.as_bytes()[..], &arr[..]); +} + +#[test] +fn test_hash_try_from_slice_invalid() { + let result: Result = Hash::try_from(&[0_u8; 3][..]); + assert!(result.is_err()); +} + +#[test] +fn test_hash_as_ref() { + let arr = [3_u8; HASH_LENGTH]; + let hash = Hash::from(arr); + assert_eq!(hash.as_ref(), &arr[..]); +} + +#[test] +fn test_hash_from_str_valid() { + let s = valid_hex(); + let expected: Vec = (0..HASH_LENGTH) + .map(|i| u8::try_from(i).unwrap_or(0)) + .collect(); + let hash = common::ok(s.parse::()); + assert_eq!(&hash.as_bytes()[..], expected.as_slice()); +} + +#[test] +fn test_hash_from_str_invalid_length() { + let result = "abc".parse::(); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(3)); +} + +#[test] +fn test_hash_from_str_invalid_hex() { + let s = "zz".repeat(HASH_LENGTH); + let result = s.parse::(); + assert!(result.is_err()); + + let err = common::err(result); + assert!( + matches!(&err, VctrlError::CorruptedData(_)), + "unexpected error: {err:?}" + ); + + if let VctrlError::CorruptedData(msg) = err { + assert!(msg.contains("invalid hex char in hash")); + } else { + loop { + core::hint::spin_loop(); + } + } +} + +#[test] +fn test_hash_display() { + let s = valid_hex(); + let hash = common::ok(s.parse::()); + assert_eq!(hash.to_string(), s); +} + +#[test] +fn test_hash_debug() { + let s = valid_hex(); + let hash = common::ok(s.parse::()); + let dbg = format!("{hash:?}"); + assert!(dbg.starts_with("Hash(")); + assert!(dbg.contains("...")); + assert!(dbg.ends_with(')')); +} diff --git a/libvctrl_handler/tests/hash_validation.rs b/libvctrl_handler/tests/hash_validation.rs index cbe10de5..5662ef56 100644 --- a/libvctrl_handler/tests/hash_validation.rs +++ b/libvctrl_handler/tests/hash_validation.rs @@ -1,8 +1,9 @@ #![allow(missing_docs)] #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] +use criterion as _; -use core::error::Error as StdError; +use core::error::Error as _; use libvctrl_handler::*; fn make_hash(byte: u8) -> Hash { diff --git a/libvctrl_handler/tests/tag_reflog.rs b/libvctrl_handler/tests/tag_reflog.rs new file mode 100644 index 00000000..da5eef4b --- /dev/null +++ b/libvctrl_handler/tests/tag_reflog.rs @@ -0,0 +1,90 @@ +use criterion as _; +use libvctrl_handler::{ + CommitMeta, HASH_LENGTH, Hash, MAX_MESSAGE_LENGTH, ReflogEntry, Tag, UserID, VctrlError, +}; +mod common; + +fn h(byte: u8) -> Hash { + Hash::from([byte; HASH_LENGTH]) +} + +fn tagger() -> UserID { + common::ok(UserID::new( + "Tagger".to_string(), + "tagger@example.com".to_string(), + )) +} + +#[test] +fn test_tag_valid_with_meta() { + let target = h(1); + let tagger = tagger(); + let meta = common::ok(CommitMeta::new(1_700_000_000, 300, None)); + + let tag = common::ok(Tag::with_meta( + "v1.0.0".to_string(), + target, + Some(tagger.clone()), + "release 1.0.0".to_string(), + meta, + )); + + assert_eq!(tag.name(), "v1.0.0"); + assert_eq!(tag.target(), &target); + assert_eq!(tag.tagger(), Some(&tagger)); + assert_eq!(tag.message(), "release 1.0.0"); + assert_eq!(tag.meta().timestamp(), 1_700_000_000); + assert_eq!(tag.meta().timezone_offset(), 300); +} + +#[test] +fn test_tag_invalid_ref_name() { + let target = h(1); + let result = Tag::new("bad name".to_string(), target, None, "message".to_string()); + assert!(result.is_err()); +} + +#[test] +fn test_tag_message_too_long() { + let target = h(1); + let max_msg = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX); + let message = "a".repeat(max_msg + 1); + + let result = Tag::new("v1.0.0".to_string(), target, None, message); + assert!(result.is_err()); + + let err = common::err(result); + assert!( + matches!(&err, VctrlError::ExceededMaxSize(_)), + "unexpected error: {err:?}" + ); +} + +#[test] +fn test_reflog_entry_valid() { + let old = Some(h(1)); + let new = Some(h(2)); + let entry = common::ok(ReflogEntry::new( + old, + new, + "update".to_string(), + 1_700_000_000, + 120, + )); + + assert_eq!(entry.old_id(), old); + assert_eq!(entry.new_id(), new); + assert_eq!(entry.reason(), "update"); + assert_eq!(entry.timestamp(), 1_700_000_000); + assert_eq!(entry.timezone_offset(), 120); +} + +#[test] +fn test_reflog_entry_invalid_timezone() { + let result = ReflogEntry::new(None, None, "update".to_string(), 1_700_000_000, -2000); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidTimezoneOffset(-2000) + ); +} diff --git a/libvctrl_handler/tests/traits_index.rs b/libvctrl_handler/tests/traits_index.rs new file mode 100644 index 00000000..3024496a --- /dev/null +++ b/libvctrl_handler/tests/traits_index.rs @@ -0,0 +1,61 @@ +use criterion as _; +use libvctrl_handler::{Index, VctrlError}; +mod common; + +#[derive(Debug)] +struct MockIndex { + len: usize, +} + +impl Index for MockIndex { + type Entry = i32; + type Path = String; + type TreeId = (); + + fn add(&mut self, _entry: Self::Entry) -> Result<(), VctrlError> { + Ok(()) + } + + fn remove(&mut self, _path: &Self::Path) -> Result<(), VctrlError> { + Ok(()) + } + + fn clear(&mut self) -> Result<(), VctrlError> { + Ok(()) + } + + fn get(&self, _path: &Self::Path) -> Result, VctrlError> { + Ok(None) + } + + fn contains(&self, _path: &Self::Path) -> Result { + Ok(false) + } + + fn len(&self) -> Result { + Ok(self.len) + } + + fn entries(&self) -> Result, VctrlError> { + Ok(Vec::new()) + } + + fn write_tree(&self) -> Result { + Ok(()) + } + + fn read_tree(&mut self, _tree: &Self::TreeId) -> Result<(), VctrlError> { + Ok(()) + } +} + +#[test] +fn test_index_is_empty_default_implementation() { + let empty = MockIndex { len: 0 }; + let empty_result = empty.is_empty(); + assert_eq!(empty_result, Ok(true)); + + let non_empty = MockIndex { len: 2 }; + let non_empty_result = non_empty.is_empty(); + assert_eq!(non_empty_result, Ok(false)); +} diff --git a/libvctrl_handler/tests/tree.rs b/libvctrl_handler/tests/tree.rs new file mode 100644 index 00000000..4661f9c2 --- /dev/null +++ b/libvctrl_handler/tests/tree.rs @@ -0,0 +1,88 @@ +use criterion as _; +use libvctrl_handler::{ + EntryKind, HASH_LENGTH, Hash, MAX_TREE_ENTRIES, Tree, TreeEntry, VctrlError, +}; +mod common; + +fn h() -> Hash { + Hash::from([0_u8; HASH_LENGTH]) +} + +#[test] +fn test_tree_entry_valid() { + let hash = h(); + let entry = common::ok(TreeEntry::new( + "file.txt".to_string(), + EntryKind::Blob, + hash, + )); + + assert_eq!(entry.name(), "file.txt"); + assert_eq!(entry.kind(), EntryKind::Blob); + assert_eq!(entry.hash(), &hash); +} + +#[test] +fn test_tree_entry_invalid_name() { + let result = TreeEntry::new("a/b".to_string(), EntryKind::Blob, h()); + assert!(result.is_err()); +} + +#[test] +fn test_tree_new_empty() { + let tree = common::ok(Tree::new(Vec::new())); + assert!(tree.is_empty()); + assert_eq!(tree.len(), 0); + assert_eq!(tree.entries().len(), 0); +} + +#[test] +fn test_tree_new_sorts_entries() { + let e1 = common::ok(TreeEntry::new("b".to_string(), EntryKind::Blob, h())); + let e2 = common::ok(TreeEntry::new("a".to_string(), EntryKind::Blob, h())); + + let tree = common::ok(Tree::new(vec![e1, e2])); + + assert_eq!(tree.len(), 2); + assert_eq!(tree.entries().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 c8458c21..05e814ef 100644 --- a/libvctrl_handler/tests/type_validation.rs +++ b/libvctrl_handler/tests/type_validation.rs @@ -1,6 +1,7 @@ #![allow(missing_docs)] #![allow(clippy::unwrap_used)] #![allow(clippy::expect_used)] +use criterion as _; use libvctrl_handler::*; diff --git a/libvctrl_handler/tests/user_id.rs b/libvctrl_handler/tests/user_id.rs new file mode 100644 index 00000000..48af1225 --- /dev/null +++ b/libvctrl_handler/tests/user_id.rs @@ -0,0 +1,92 @@ +use criterion as _; +use libvctrl_handler::{MAX_NAME_LENGTH, UserID, VctrlError}; +mod common; + +#[test] +fn test_user_id_valid() { + let user = common::ok(UserID::new( + "Alice".to_string(), + "alice@example.com".to_string(), + )); + assert_eq!(user.name(), "Alice"); + assert_eq!(user.email(), "alice@example.com"); +} + +#[test] +fn test_user_id_invalid_empty_name() { + let result = UserID::new(String::new(), "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName("user name is empty".to_string()) + ); +} + +#[test] +fn test_user_id_invalid_name_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let name = "a".repeat(max_len + 1); + let result = UserID::new(name, "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!( + "user name exceeds maximum length {MAX_NAME_LENGTH}" + )) + ); +} + +#[test] +fn test_user_id_invalid_name_control_chars() { + let name = "Alice\nBob".to_string(); + let result = UserID::new(name.clone(), "alice@example.com".to_string()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!("user name contains control characters: '{name}'")) + ); +} + +#[test] +fn test_user_id_invalid_empty_email() { + let result = UserID::new("Alice".to_string(), String::new()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail("email is empty".to_string()) + ); +} + +#[test] +fn test_user_id_invalid_email_no_at() { + let email = "alice.example.com".to_string(); + let result = UserID::new("Alice".to_string(), email.clone()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email must contain '@': '{email}'")) + ); +} + +#[test] +fn test_user_id_invalid_email_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let email = format!("{}@example.com", "a".repeat(max_len + 1)); + let result = UserID::new("Alice".to_string(), email); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email exceeds maximum length {MAX_NAME_LENGTH}")) + ); +} + +#[test] +fn test_user_id_invalid_email_control_chars() { + let email = "alice@example.com\n".to_string(); + let result = UserID::new("Alice".to_string(), email.clone()); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidEmail(format!("email contains control characters: '{email}'")) + ); +} diff --git a/libvctrl_handler/tests/validation.rs b/libvctrl_handler/tests/validation.rs new file mode 100644 index 00000000..e35e2941 --- /dev/null +++ b/libvctrl_handler/tests/validation.rs @@ -0,0 +1,116 @@ +use criterion as _; +use libvctrl_handler::{ + HASH_LENGTH, MAX_NAME_LENGTH, VctrlError, validate_hash_bytes, validate_name, + validate_ref_name, validate_tree_entry_name, +}; +mod common; + +#[test] +fn test_validate_hash_bytes_valid() { + let bytes = [0_u8; HASH_LENGTH]; + assert!(validate_hash_bytes(&bytes).is_ok()); +} + +#[test] +fn test_validate_hash_bytes_invalid() { + let result = validate_hash_bytes(&[0_u8; 10]); + assert!(result.is_err()); + assert_eq!(common::err(result), VctrlError::InvalidHashLength(10)); +} + +#[test] +fn test_validate_name_valid() { + assert!(validate_name("file.txt").is_ok()); + assert!(validate_name("a").is_ok()); +} + +#[test] +fn test_validate_name_invalid_empty() { + let result = validate_name(""); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName("name is empty".to_string()) + ); +} + +#[test] +fn test_validate_name_invalid_too_long() { + let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); + let name = "a".repeat(max_len + 1); + let result = validate_name(&name); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!( + "name exceeds maximum length {MAX_NAME_LENGTH}: '{name}'" + )) + ); +} + +#[test] +fn test_validate_name_invalid_control_chars() { + let name = "a\nb"; + let result = validate_name(name); + assert!(result.is_err()); + assert_eq!( + common::err(result), + VctrlError::InvalidName(format!("name contains control characters: '{name}'")) + ); +} + +#[test] +fn test_validate_ref_name_valid() { + assert!(validate_ref_name("refs/heads/main").is_ok()); + assert!(validate_ref_name("v1.0.0").is_ok()); +} + +#[test] +fn test_validate_ref_name_invalid_cases() { + let invalid_names = [ + "@", + "/leading", + "trailing/", + "double//slash", + "refs/.hidden", + "refs/heads/main.lock", + "refs/heads/main..", + "refs/heads/main~1", + "refs/heads/main^", + "refs/heads/main:", + "refs/heads/main?", + "refs/heads/main*", + "refs/heads/main[", + "refs/heads/main\\", + "refs/heads/main ", + "refs/heads/main@{", + "refs/heads/main<", + "refs/heads/main>", + "refs/heads/main|", + "refs/heads/main\"", + ]; + + for name in invalid_names { + assert!( + validate_ref_name(name).is_err(), + "expected invalid: '{name}'" + ); + } +} + +#[test] +fn test_validate_tree_entry_name_valid() { + assert!(validate_tree_entry_name("file.txt").is_ok()); +} + +#[test] +fn test_validate_tree_entry_name_invalid() { + let invalid_names = ["a/b", "a\\b", ".", ".."]; + + for name in invalid_names { + assert!( + validate_tree_entry_name(name).is_err(), + "expected invalid: '{name}'" + ); + } +} diff --git a/libvctrl_plumbing/Cargo.toml b/libvctrl_plumbing/Cargo.toml index 2457999a..87cbc5fc 100644 --- a/libvctrl_plumbing/Cargo.toml +++ b/libvctrl_plumbing/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "libvctrl_plumbing" -version = "0.2.0" +version = "0.3.0" edition = "2024" description = "Plumbing commands for the libvctrl version control system" license = "MIT" @@ -13,7 +13,7 @@ keywords = ["version-control", "vcs", "plumbing"] categories = ["development-tools"] [dependencies] -libvctrl = { path = "../libvctrl", version = "2.1.2" } +libvctrl = { path = "../libvctrl", version = "2.2.0" } [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 7e1ba97b..3c66f946 100644 --- a/libvctrl_plumbing/src/cat_file.rs +++ b/libvctrl_plumbing/src/cat_file.rs @@ -1,209 +1,26 @@ -//! # 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 alloc::sync::Arc; +use core::fmt::Write as _; use libvctrl::{Decoder, EntryKind, Hash, ObjectStore, VctrlError}; -use std::fmt::Write; use std::io::{BufRead, Write as IoWrite}; -/// 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)] +#[derive(Debug, 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, @@ -214,31 +31,30 @@ pub fn cat_file( let hash = parse_hash(object_name)?; let mut encoded = Vec::new(); - store + let _ = store .get(&hash)? .read_to_end(&mut encoded) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(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(std::sync::Arc::new(e)))?; + writeln!(writer, "{type_str}").map_err(|e| VctrlError::IoError(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(std::sync::Arc::new(e)))?; + writeln!(writer, "{size}").map_err(|e| VctrlError::IoError(Arc::new(e)))?; Ok(()) } CatFileMode::PrettyPrint => { let content = pretty_print(decoder, &encoded)?; writer .write_all(content.as_bytes()) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(Arc::new(e)))?; Ok(()) } CatFileMode::Raw(expected_type) => { @@ -252,110 +68,22 @@ pub fn cat_file( } writer .write_all(&encoded) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(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(Default)] +#[derive(Debug, 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, @@ -372,7 +100,7 @@ pub fn cat_file_batch( line.clear(); if input .read_line(&mut line) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))? + .map_err(|e| VctrlError::IoError(Arc::new(e)))? == 0 { break; @@ -399,7 +127,7 @@ pub fn cat_file_batch( if !options.buffer { output .write_all(&out_buf) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(Arc::new(e)))?; out_buf.clear(); } } else { @@ -409,7 +137,7 @@ pub fn cat_file_batch( if !options.buffer { output .write_all(&out_buf) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(Arc::new(e)))?; out_buf.clear(); } } @@ -418,21 +146,11 @@ pub fn cat_file_batch( if !out_buf.is_empty() { output .write_all(&out_buf) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(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, @@ -442,10 +160,10 @@ fn handle_one_object( let hash = parse_hash(object_name)?; let mut encoded = Vec::new(); - store + let _ = store .get(&hash)? .read_to_end(&mut encoded) - .map_err(|e| VctrlError::IoError(std::sync::Arc::new(e)))?; + .map_err(|e| VctrlError::IoError(Arc::new(e)))?; let obj_type = decode_type(decoder, &encoded)?; let obj_size = encoded.len() as u64; @@ -467,15 +185,6 @@ 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(); @@ -483,25 +192,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 hex_byte = &s[i * 2..i * 2 + 2]; + 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()))?; *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); @@ -518,18 +227,6 @@ 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()); @@ -585,7 +282,6 @@ fn pretty_print(decoder: &D, encoded: &[u8]) -> Result &'static str { match t { ObjectType::Blob => "blob", @@ -595,9 +291,6 @@ 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, @@ -609,11 +302,6 @@ 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 661f120a..b5660e97 100644 --- a/libvctrl_plumbing/src/lib.rs +++ b/libvctrl_plumbing/src/lib.rs @@ -1,94 +1,8 @@ -//! # 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(()) -//! # } -//! ``` +extern crate alloc; #[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 fb8d8883..cd2ad683 100644 --- a/libvctrl_plumbing/tests/cat_file_tests.rs +++ b/libvctrl_plumbing/tests/cat_file_tests.rs @@ -1,16 +1,15 @@ //! Integration tests for the cat-file plumbing command. -use libvctrl::{BinaryDecoder, BinaryEncoder}; +use std::io::Cursor; + use libvctrl::{ - Blob, Commit, Encoder, EntryKind, Hash, Hasher, ObjectStore, Tag, Tree, TreeEntry, UserID, - VctrlError, + BinaryDecoder, BinaryEncoder, Blob, Commit, Encoder, EntryKind, Hash, Hasher, MemoryStore, + ObjectStore, Sha512Hasher, 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 { @@ -185,7 +184,7 @@ fn object_size() -> Result<(), VctrlError> { let size: usize = utf8_string(out)? .trim() .parse::() - .map_err(|e: std::num::ParseIntError| VctrlError::Other(e.to_string()))?; + .map_err(|e: core::num::ParseIntError| VctrlError::Other(e.to_string()))?; assert!(size > 0); Ok(()) } diff --git a/libvctrl_sha512/Cargo.toml b/libvctrl_sha512/Cargo.toml index 301ba8ea..cecd2420 100644 --- a/libvctrl_sha512/Cargo.toml +++ b/libvctrl_sha512/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "libvctrl_sha512" -version = "3.0.1" +version = "3.2.0" edition = "2024" rust-version = "1.96" description = "Zero-dependency SHA512, HMAC-SHA512, HKDF-SHA512, and optional SHA384" license = "ISC" -authors = ["mroczect "] repository = "https://github.com/mroczect/libvctrl" homepage = "https://github.com/mroczect/libvctrl" documentation = "https://docs.rs/libvctrl_sha512" @@ -20,6 +20,7 @@ sha384 = [] opt_size = [] [dependencies] +zeroize = { version = "1.8", default-features = false } [dev-dependencies] criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support"] } diff --git a/libvctrl_sha512/benches/sha384_bench.rs b/libvctrl_sha512/benches/sha384_bench.rs index 1879cb1f..1d5b1058 100644 --- a/libvctrl_sha512/benches/sha384_bench.rs +++ b/libvctrl_sha512/benches/sha384_bench.rs @@ -1,20 +1,22 @@ #![allow(missing_docs)] #![cfg(feature = "sha384")] +use zeroize as _; + use criterion::{Criterion, criterion_group, criterion_main}; use libvctrl_sha512::sha384; fn bench_sha384(c: &mut Criterion) { - let data = [0x42u8; 1024]; - c.bench_function("SHA384/hash_1kb", |b| { + let data = [0x42_u8; 1024]; + let _ = c.bench_function("SHA384/hash_1kb", |b| { b.iter(|| sha384::Hash::hash(core::hint::black_box(&data))); }); } fn bench_hmac_sha384(c: &mut Criterion) { - let key = [0x01u8; 32]; - let data = [0x42u8; 1024]; - c.bench_function("HMAC-SHA384/mac_1kb", |b| { + let key = [0x01_u8; 32]; + let data = [0x42_u8; 1024]; + let _ = c.bench_function("HMAC-SHA384/mac_1kb", |b| { b.iter(|| sha384::HMAC::mac(core::hint::black_box(&data), core::hint::black_box(&key))); }); } diff --git a/libvctrl_sha512/benches/sha512_bench.rs b/libvctrl_sha512/benches/sha512_bench.rs index d719f3d3..986bcc1a 100644 --- a/libvctrl_sha512/benches/sha512_bench.rs +++ b/libvctrl_sha512/benches/sha512_bench.rs @@ -1,24 +1,26 @@ #![allow(missing_docs)] +use zeroize as _; + use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; use libvctrl_sha512::{HKDF, HMAC, Hash}; fn bench_sha512(c: &mut Criterion) { - let data = [0x42u8; 1024]; - c.bench_function("SHA512/hash_1kb", |b| { + let data = [0x42_u8; 1024]; + let _ = c.bench_function("SHA512/hash_1kb", |b| { b.iter(|| Hash::hash(core::hint::black_box(&data))); }); } fn bench_hmac(c: &mut Criterion) { - let key = [0x01u8; 32]; - let data = [0x42u8; 1024]; + let key = [0x01_u8; 32]; + let data = [0x42_u8; 1024]; - c.bench_function("HMAC-SHA512/mac_1kb", |b| { + let _ = c.bench_function("HMAC-SHA512/mac_1kb", |b| { b.iter(|| HMAC::mac(core::hint::black_box(&data), core::hint::black_box(&key))); }); - c.bench_function("HMAC-SHA512/streaming_1kb_chunked", |b| { + let _ = c.bench_function("HMAC-SHA512/streaming_1kb_chunked", |b| { b.iter_batched( || (key, data), |(k, d)| { @@ -34,18 +36,18 @@ fn bench_hmac(c: &mut Criterion) { } fn bench_hkdf(c: &mut Criterion) { - let ikm = [0x0bu8; 22]; - let salt = [0x00u8; 13]; - let info = [0xf0u8; 10]; + let ikm = [0x0b_u8; 22]; + let salt = [0x00_u8; 13]; + let info = [0xf0_u8; 10]; - c.bench_function("HKDF-SHA512/extract", |b| { + let _ = c.bench_function("HKDF-SHA512/extract", |b| { b.iter(|| HKDF::extract(core::hint::black_box(salt), core::hint::black_box(ikm))); }); let prk = HKDF::extract(salt, ikm); - c.bench_function("HKDF-SHA512/expand_64_bytes", |b| { + let _ = c.bench_function("HKDF-SHA512/expand_64_bytes", |b| { b.iter(|| { - let mut out = [0u8; 64]; + let mut out = [0_u8; 64]; HKDF::expand( &mut out, core::hint::black_box(prk), diff --git a/libvctrl_sha512/src/hkdf.rs b/libvctrl_sha512/src/hkdf.rs index cbcef17b..6904bfd4 100644 --- a/libvctrl_sha512/src/hkdf.rs +++ b/libvctrl_sha512/src/hkdf.rs @@ -1,50 +1,45 @@ -//! # HKDF Key Derivation (SHA-512) -//! -//! This module provides the HMAC-based Extract-and-Expand Key Derivation -//! Function (HKDF) as specified in RFC 5869, instantiated with SHA-512 as -//! the underlying hash function. -//! -//! ## What is HKDF? -//! -//! HKDF is a cryptographic key derivation function that turns secret input -//! keying material (IKM) into cryptographically strong output keying material -//! (OKM). It consists of two steps: -//! -//! - **Extract**: concentrates the entropy from the IKM into a fixed-size -//! pseudorandom key (PRK) using an HMAC with a salt. -//! - **Expand**: stretches the PRK into additional keys of arbitrary length -//! using HMAC with an info parameter for domain separation. -//! -//! ## How this module works -//! -//! The [`impl_hkdf!`] macro is invoked with `crate::sha512::Hash`, an output -//! size of 64 bytes, and a block size of 128 bytes. The macro generates the -//! [`HKDF`] struct with two static methods: -//! -//! - [`HKDF::extract`]: performs the extract step and returns a 64-byte PRK. -//! - [`HKDF::expand`]: performs the expand step and fills a caller-provided -//! output buffer with key material. -//! -//! Internally, both methods delegate to the HMAC implementation generated for -//! SHA-512 by the [`impl_hmac!`] macro. -//! -//! # Examples -//! -//! Derive 42 bytes of output keying material: -//! -//! ``` -//! # use libvctrl_sha512::hkdf::HKDF; -//! let ikm = b"input key material"; -//! let salt = b"salt"; -//! let info = b"context"; -//! -//! let prk = HKDF::extract(salt, ikm); -//! let mut okm = [0u8; 42]; -//! HKDF::expand(&mut okm, prk, info); -//! -//! assert_eq!(okm.len(), 42); -//! ``` - +#![allow(clippy::indexing_slicing)] use crate::hmac::HMAC; impl_hkdf!(crate::sha512::Hash, 64, 128); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_returns_64_bytes() { + let prk = HKDF::extract(b"", b""); + assert_eq!(prk.len(), 64); + } + + #[test] + fn test_expand_zero_output_does_not_panic() { + let mut out = []; + HKDF::expand(&mut out, [0_u8; 64], b""); + } + + #[test] + fn test_expand_different_info_produces_different_output() { + let prk = [0x42_u8; 64]; + let mut out_a = [0_u8; 32]; + let mut out_b = [0_u8; 32]; + HKDF::expand(&mut out_a, prk, b"a"); + HKDF::expand(&mut out_b, prk, b"b"); + assert_ne!(out_a, out_b); + } + + #[test] + #[should_panic(expected = "HKDF expects a 64-byte PRK")] + fn test_expand_wrong_prk_length_panics() { + let mut out = [0_u8; 32]; + HKDF::expand(&mut out, [0_u8; 16], b""); + } + + #[test] + #[should_panic(expected = "Requested output exceeds RFC 5869 limit")] + fn test_expand_output_too_large_panics() { + let mut out = [0_u8; 16_321]; + HKDF::expand(&mut out, [0_u8; 64], b""); + } +} diff --git a/libvctrl_sha512/src/hmac.rs b/libvctrl_sha512/src/hmac.rs index c50fe033..1ce680ff 100644 --- a/libvctrl_sha512/src/hmac.rs +++ b/libvctrl_sha512/src/hmac.rs @@ -1,61 +1,62 @@ -//! # HMAC-SHA512 -//! -//! This module provides an implementation of the Hash-based Message -//! Authentication Code (HMAC) as specified in RFC 2104, instantiated with -//! SHA-512 as the underlying hash function. -//! -//! ## What is HMAC? -//! -//! HMAC is a keyed hash function used for message authentication. It combines -//! a secret key with a message to produce a fixed-size authentication tag. -//! The construction is: -//! -//! ```text -//! HMAC(K, m) = H((K' XOR opad) || H((K' XOR ipad) || m)) -//! ``` -//! -//! Where: -//! -//! - `H` is the underlying hash function (SHA-512 here). -//! - `K'` is the key padded or hashed to the block size. -//! - `opad` is `0x5c` repeated 128 times. -//! - `ipad` is `0x36` repeated 128 times. -//! -//! ## Parameters -//! -//! This HMAC instance uses: -//! -//! - Output size: **64 bytes** -//! - Block size: **128 bytes** -//! -//! These parameters are fed into the [`impl_hmac!`] macro, which generates the -//! [`HMAC`] struct and its associated methods. -//! -//! ## Security considerations -//! -//! HMAC security depends on the secrecy and entropy of the key. A key length -//! of at least 64 bytes is recommended for 256-bit security. The -//! implementation zeroizes internal state on drop. -//! -//! # Examples -//! -//! Compute an authentication tag: -//! -//! ``` -//! # use libvctrl_sha512::hmac::HMAC; -//! let tag = HMAC::mac(b"message", b"secret key"); -//! assert_eq!(tag.len(), 64); -//! ``` -//! -//! Verify a tag: -//! -//! ``` -//! # use libvctrl_sha512::hmac::HMAC; -//! let key = b"secret key"; -//! let tag = HMAC::mac(b"message", key); -//! assert!(HMAC::verify(b"message", key, &tag)); -//! ``` - +#![allow(clippy::indexing_slicing)] use crate::sha512::Hash; impl_hmac!(Hash, 64, 128); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_prepare_key_short_pads_with_zeroes() { + let key = [1_u8, 2, 3]; + let prepared = HMAC::prepare_key(&key); + assert_eq!(&prepared[..3], &key[..]); + assert!(prepared[3..].iter().all(|&b| b == 0)); + } + + #[test] + fn test_prepare_key_exact_block_size() { + let key = [0xAB_u8; 128]; + let prepared = HMAC::prepare_key(&key); + assert_eq!(prepared, key); + } + + #[test] + fn test_prepare_key_longer_hashes_key() { + let key = [0x61_u8; 200]; + let prepared = HMAC::prepare_key(&key); + let hash = Hash::hash(key); + assert_eq!(&prepared[..64], &hash[..]); + assert!(prepared[64..].iter().all(|&b| b == 0)); + } + + #[test] + fn test_mac_equals_update_finalize() { + let key = b"secret"; + let input = b"message"; + let one_shot = HMAC::mac(input, key); + + let mut hmac = HMAC::new(key); + hmac.update(input); + assert_eq!(hmac.finalize(), one_shot); + } + + #[test] + fn test_finalize_verify_and_verify() { + let key = b"secret"; + let input = b"message"; + let tag = HMAC::mac(input, key); + + let mut hmac = HMAC::new(key); + hmac.update(input); + assert!(hmac.finalize_verify(&tag)); + assert!(HMAC::verify(input, key, &tag)); + + let bad = [0_u8; 64]; + let mut hmac = HMAC::new(key); + hmac.update(input); + assert!(!hmac.finalize_verify(&bad)); + assert!(!HMAC::verify(input, key, &bad)); + } +} diff --git a/libvctrl_sha512/src/lib.rs b/libvctrl_sha512/src/lib.rs index 2a305773..bde3864d 100644 --- a/libvctrl_sha512/src/lib.rs +++ b/libvctrl_sha512/src/lib.rs @@ -1,88 +1,6 @@ -//! Zero-dependency cryptographic primitives: SHA-512, HMAC-SHA512, HKDF-SHA512, -//! and optional SHA-384. -//! -//! # Why this crate exists -//! -//! `libvctrl_sha512` provides a pure Rust, `no_std`-compatible implementation -//! of several widely used cryptographic algorithms. It is designed to serve as -//! the content-addressing and message-authentication backbone for the larger -//! `libvcrtl` version control system, while remaining usable as a standalone -//! cryptography crate. -//! -//! The implementation prioritizes: -//! - **Auditability** — no external dependencies and readable, well-structured code. -//! - **Security** — constant-time verification, zeroization of intermediate state. -//! - **Performance** — aggressive inlining, specialized block processing, and an -//! optional `opt_size` feature for size-constrained builds. -//! -//! # Module organization -//! -//! - [`sha512`] — SHA-512 hash function. -//! - [`hmac`] — HMAC keyed-hash message authentication code instantiated with SHA-512. -//! - [`hkdf`] — HKDF key derivation function instantiated with SHA-512. -//! - [`utils`] — shared byte-order and verification helpers. -//! - [`sha384`] — optional SHA-384 implementation behind the `sha384` feature. -//! -//! The HMAC and HKDF modules are generated using the exported macros -//! [`impl_hmac!`] and [`impl_hkdf!`], which allow downstream crates to -//! instantiate these algorithms with other hash functions if needed. -//! -//! # Examples -//! -//! Compute a SHA-512 digest: -//! -//! ``` -//! use libvctrl_sha512::Hash; -//! -//! let digest = Hash::hash(b"hello world"); -//! assert_eq!(digest.len(), 64); -//! ``` -//! -//! Compute an HMAC-SHA512 authentication tag: -//! -//! ``` -//! use libvctrl_sha512::HMAC; -//! -//! let tag = HMAC::mac(b"message", b"secret-key"); -//! assert_eq!(tag.len(), 64); -//! ``` +#![cfg_attr(not(test), no_std)] +#![allow(clippy::arithmetic_side_effects)] -#![allow(clippy::indexing_slicing, clippy::unwrap_used, clippy::expect_used)] -#![allow(unused_crate_dependencies)] - -/// Defines an HMAC (Hash-based Message Authentication Code) type based on the -/// provided hash struct. -/// -/// # Why this macro exists -/// -/// HMAC is a generic construction that can be built on top of any -/// cryptographic hash function. Rather than duplicating the implementation for -/// each hash algorithm, this macro generates a complete HMAC type from a hash -/// struct, output size, and block size. The generated type provides both -/// one-shot and incremental APIs. -/// -/// # How it works -/// -/// The macro expands to a struct named `HMAC` that wraps the chosen hash -/// implementation. It follows RFC 2104: -/// -/// 1. Normalizes the key to the hash block size by hashing it if necessary. -/// 2. Computes the inner hash over the key XOR `0x36` and the message. -/// 3. Computes the outer hash over the key XOR `0x5c` and the inner digest. -/// -/// The generated struct implements [`Drop`] to zeroize internal key material -/// and padded buffers when the context goes out of scope. -/// -/// # Examples -/// -/// The `libvctrl_sha512` crate already instantiates this macro for SHA-512: -/// -/// ``` -/// use libvctrl_sha512::HMAC; -/// -/// let tag = HMAC::mac(b"message", b"key"); -/// assert_eq!(tag.len(), 64); -/// ``` #[macro_export] macro_rules! impl_hmac { ($hash_struct:ty, $output_size:expr, $block_size:expr) => { @@ -92,47 +10,61 @@ macro_rules! impl_hmac { padded: [u8; $block_size], } - impl Drop for HMAC { - fn drop(&mut self) { + 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 { - ih.zeroize(); + zeroize::Zeroize::zeroize(ih); } - self.padded.fill(0); + zeroize::Zeroize::zeroize(&mut self.padded); } } + impl Drop for HMAC { + fn drop(&mut self) { + zeroize::Zeroize::zeroize(self); + } + } + + #[allow(clippy::indexing_slicing)] impl HMAC { - 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]); + 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]); } else { - block_key[..k.len()].copy_from_slice(k); + block_key[..key.len()].copy_from_slice(key); } block_key } #[doc = "One-shot HMAC computation."] #[must_use] - pub fn mac, U: AsRef<[u8]>>(input: T, k: U) -> [u8; $output_size] { - let mut hmac = Self::new(k); + pub fn mac, U: AsRef<[u8]>>(input: T, key: U) -> [u8; $output_size] { + let mut hmac = Self::new(key); hmac.update(input); hmac.finalize() } #[doc = "Creates a new HMAC context from a secret key."] #[must_use] - 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]; + 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; } let mut ih = <$hash_struct>::new(); ih.update(&padded); - block_key.fill(0); + zeroize::Zeroize::zeroize(&mut block_key); HMAC { ih: Some(ih), padded, @@ -149,13 +81,18 @@ macro_rules! impl_hmac { #[doc = "Finalizes the HMAC and returns the authentication tag."] #[must_use] pub fn finalize(mut self) -> [u8; $output_size] { - for p in self.padded.iter_mut() { - *p ^= 0x6a; + for padded_byte in self.padded.iter_mut() { + *padded_byte ^= 0x6a; } let mut oh = <$hash_struct>::new(); oh.update(&self.padded); - let inner = self.ih.take().unwrap().finalize(); - oh.update(&inner); + let inner = zeroize::Zeroizing::new( + self.ih + .take() + .unwrap_or_else(|| <$hash_struct>::new()) + .finalize(), + ); + oh.update(&*inner); oh.finalize() } @@ -172,55 +109,24 @@ macro_rules! impl_hmac { #[must_use] pub fn verify, U: AsRef<[u8]>>( input: T, - k: U, + key: U, expected: &[u8; $output_size], ) -> bool { - let mac = Self::mac(input, k); + let mac = Self::mac(input, key); $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] @@ -232,103 +138,67 @@ 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.as_ref().len(), + prk.len(), $output_size, "HKDF expects a {}-byte PRK", $output_size ); let info = info.as_ref(); - let mut counter: u8 = 1; + let max_len = 255_usize.saturating_mul($output_size); assert!( - out.len() < 0xff * $output_size, + out.len() <= max_len, "Requested output exceeds RFC 5869 limit" ); - 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]); + 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); + } } hmac.update(info); - 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; + 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 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 hmac; 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; -/// 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 hmac::HMAC; +pub use sha512::Hash; pub use utils::{BLOCKBYTES, BYTES}; #[cfg(test)] mod tests { use super::*; + use criterion as _; #[test] fn hmac_vectors() { - let h = HMAC::mac([], [0u8; 32]); + let h = HMAC::mac([], [0_u8; 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, @@ -336,9 +206,9 @@ mod tests { 12, 178, 34, 71, 34, 93, 71, ]; assert_eq!(h, expected); - assert!(HMAC::verify([], [0u8; 32], &expected)); + assert!(HMAC::verify([], [0_u8; 32], &expected)); - let h = HMAC::mac([42u8; 69], []); + let h = HMAC::mac([42_u8; 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, @@ -346,12 +216,12 @@ mod tests { 115, 59, 54, 91, 143, 143, 254, 220, ]; assert_eq!(h, expected); - assert!(HMAC::verify([42u8; 69], [], &expected)); + assert!(HMAC::verify([42_u8; 69], [], &expected)); } #[test] fn hkdf_vector() { - let ikm = [0x0bu8; 22]; + let ikm = [0x0b_u8; 22]; let salt: [u8; 13] = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, ]; @@ -362,7 +232,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 = [0u8; 42]; + let mut okm = [0_u8; 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 37493047..0be7a85f 100644 --- a/libvctrl_sha512/src/sha384.rs +++ b/libvctrl_sha512/src/sha384.rs @@ -1,34 +1,9 @@ -//! # 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. +#![allow(clippy::indexing_slicing)] +#![allow(clippy::arithmetic_side_effects)] 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] = [ @@ -38,175 +13,68 @@ 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 t = [0u64; 8]; - for (i, e) in t.iter_mut().enumerate() { - *e = load_be(&IV, i * 8); + let mut state = [0_u64; 8]; + for (index, word) in state.iter_mut().enumerate() { + *word = load_be(&IV, index * 8); } - State(t) + State(state) } -/// 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: [0u8; 128], + w: [0_u8; 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 = [0u8; 48]; - out.copy_from_slice(&self.0.finalize()[..48]); + let mut out = [0_u8; 48]; + let full = zeroize::Zeroizing::new(self.0.finalize()); + out.copy_from_slice(&full[..48]); out } - /// 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); - /// ``` + #[must_use] pub fn hash>(input: T) -> [u8; 48] { - 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(); - /// ``` + let mut hasher = Self::new(); + hasher.update(input); + hasher.finalize() + } + pub fn zeroize(&mut self) { - self.0.zeroize(); + zeroize::Zeroize::zeroize(self); + } +} + +impl zeroize::Zeroize for Hash { + fn zeroize(&mut self) { + zeroize::Zeroize::zeroize(&mut self.0); } } 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() } @@ -214,3 +82,70 @@ 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 ec10fe68..1b4320a3 100644 --- a/libvctrl_sha512/src/sha512.rs +++ b/libvctrl_sha512/src/sha512.rs @@ -1,128 +1,53 @@ #![allow(clippy::inline_always)] -//! 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); -//! ``` +#![allow(clippy::indexing_slicing)] +#![allow(clippy::arithmetic_side_effects)] 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 = [0u64; 16]; - for (i, e) in words.iter_mut().enumerate() { - *e = load_be(input, i * 8); + let mut words = [0_u64; 16]; + for (index, word) in words.iter_mut().enumerate() { + *word = load_be(input, index * 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)] @@ -134,10 +59,6 @@ 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); @@ -158,10 +79,6 @@ 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)] @@ -186,11 +103,6 @@ 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] = [ @@ -296,10 +208,6 @@ 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, @@ -308,58 +216,50 @@ 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 t = [0u64; 8]; - for (i, e) in t.iter_mut().enumerate() { - *e = load_be(&IV, i * 8); + let mut state = [0_u64; 8]; + for (index, word) in state.iter_mut().enumerate() { + *word = load_be(&IV, index * 8); } - Self(t) + Self(state) } - /// 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, 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 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 store(&self, out: &mut [u8]) { - for (i, &e) in self.0.iter().enumerate() { - store_be(out, i * 8, e); + for (index, &word) in self.0.iter().enumerate() { + store_be(out, index * 8, word); } } - /// 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 t = *self; + let mut temp = *self; let mut inlen = input.len(); while inlen >= 128 { let mut w = W::new(input); - w.g(&mut t, 0); + w.g(&mut temp, 0); w.expand(); - w.g(&mut t, 1); + w.g(&mut temp, 1); w.expand(); - w.g(&mut t, 2); + w.g(&mut temp, 2); w.expand(); - w.g(&mut t, 3); + w.g(&mut temp, 3); w.expand(); - w.g(&mut t, 4); - t.add(self); - self.0 = t.0; + w.g(&mut temp, 4); + temp.add(self); + self.0 = temp.0; input = &input[128..]; inlen -= 128; } @@ -367,244 +267,189 @@ 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, - - /// Buffer for incomplete blocks. Only the first `r` bytes are valid. pub(crate) w: [u8; 128], - - /// Number of bytes currently buffered in `w`. pub(crate) r: usize, - - /// Total length of input processed so far, in bytes. 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") + } +} + +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); + } +} + +impl Drop for Hash { + fn drop(&mut self) { + zeroize::Zeroize::zeroize(self); + } +} + 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: [0u8; 128], + w: [0_u8; 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 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; + 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; if self.r == 128 { - self.state.blocks(&self.w); + let _ = self.state.blocks(&self.w); self.r = 0; } - 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; + 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; } } } - /// 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 = [0u8; 256]; + let mut padded = zeroize::Zeroizing::new([0_u8; 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); - self.state.blocks(&padded[..r]); - let mut out = [0u8; 64]; + let _ = self.state.blocks(&padded[..r]); + let mut out = [0_u8; 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 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)); - /// ``` + let mut hasher = Self::new(); + hasher.update(input); + hasher.finalize() + } + #[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) { - 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); + zeroize::Zeroize::zeroize(self); } } 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 48acfeb0..993e0a15 100644 --- a/libvctrl_sha512/src/utils.rs +++ b/libvctrl_sha512/src/utils.rs @@ -1,163 +1,104 @@ -//! 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 { - u64::from_be_bytes(base[offset..offset + 8].try_into().unwrap()) + 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) } -/// 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) { - base[offset..offset + 8].copy_from_slice(&x.to_be_bytes()); + if let Some(end) = offset.checked_add(8) + && let Some(dst) = base.get_mut(offset..end) + { + dst.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 { - if x.len() != y.len() { - return false; - } - let mut v: u32 = 0; + let mut diff: u32 = 0; #[cfg(any(target_arch = "wasm32", target_arch = "wasm64"))] { - 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); + 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)); } - v |= h1 ^ h2; + 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); } - for (a, b) in x.iter().zip(y.iter()) { - v |= u32::from(a ^ b); + #[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]); } - let v = core::hint::black_box(v); - v == 0 + #[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])); + } } diff --git a/libvctrl_sha512/tests/common/mod.rs b/libvctrl_sha512/tests/common/mod.rs new file mode 100644 index 00000000..11a9ef6b --- /dev/null +++ b/libvctrl_sha512/tests/common/mod.rs @@ -0,0 +1,2 @@ +#[allow(unreachable_pub)] +pub const fn setup() {} diff --git a/libvctrl_sha512/tests/integration_api.rs b/libvctrl_sha512/tests/integration_api.rs new file mode 100644 index 00000000..a5c06774 --- /dev/null +++ b/libvctrl_sha512/tests/integration_api.rs @@ -0,0 +1,61 @@ +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 deleted file mode 100644 index 3d076afb..00000000 --- a/libvctrl_sha512/tests/sha_tests.rs +++ /dev/null @@ -1,241 +0,0 @@ -#![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 deleted file mode 100644 index 2285c3f2..00000000 --- a/release.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "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"