From fba19826b1f42a5f4a0e6e2dd1e77729438c0096 Mon Sep 17 00:00:00 2001 From: mroczect Date: Thu, 20 Aug 2026 17:45:49 +0700 Subject: [PATCH] refactor(docs): refactor all doc, and make everything robust --- Cargo.lock | 8 +- Cargo.toml | 169 +++- libvctrl/src/lib.rs | 540 +++++----- libvctrl_core/src/codec/binary_decoder.rs | 444 ++++----- libvctrl_core/src/codec/binary_encoder.rs | 512 +++++----- libvctrl_core/src/codec/mod.rs | 130 +-- libvctrl_core/src/hash/mod.rs | 90 +- libvctrl_core/src/hash/sha512.rs | 174 ++-- libvctrl_core/src/lib.rs | 162 +-- libvctrl_core/src/object/blob.rs | 194 ++-- libvctrl_core/src/object/commit.rs | 474 ++++----- libvctrl_core/src/object/mod.rs | 162 +-- libvctrl_core/src/object/tag.rs | 464 ++++----- libvctrl_core/src/object/tree.rs | 480 ++++----- libvctrl_core/src/store/memory.rs | 370 +++---- libvctrl_core/src/store/mod.rs | 130 +-- libvctrl_core/src/store/ref_store.rs | 304 +++--- libvctrl_handler/src/constants.rs | 328 +++--- libvctrl_handler/src/enums/core/entry_kind.rs | 174 ++-- libvctrl_handler/src/enums/core/mod.rs | 50 +- libvctrl_handler/src/enums/mod.rs | 96 +- libvctrl_handler/src/errors.rs | 168 ++-- libvctrl_handler/src/lib.rs | 206 ++-- libvctrl_handler/src/macros.rs | 78 +- libvctrl_handler/src/traits/core/blame.rs | 402 ++++---- libvctrl_handler/src/traits/core/config.rs | 546 +++++----- libvctrl_handler/src/traits/core/decoder.rs | 418 ++++---- libvctrl_handler/src/traits/core/diff.rs | 220 ++-- libvctrl_handler/src/traits/core/encoder.rs | 420 ++++---- libvctrl_handler/src/traits/core/hasher.rs | 202 ++-- libvctrl_handler/src/traits/core/index.rs | 942 +++++++++--------- libvctrl_handler/src/traits/core/mod.rs | 618 ++++++------ .../src/traits/core/object_store.rs | 458 ++++----- libvctrl_handler/src/traits/core/pack.rs | 428 ++++---- libvctrl_handler/src/traits/core/ref_store.rs | 472 ++++----- libvctrl_handler/src/traits/core/reflog.rs | 314 +++--- libvctrl_handler/src/traits/core/remote.rs | 364 +++---- libvctrl_handler/src/traits/core/revwalk.rs | 232 ++--- libvctrl_handler/src/traits/core/signer.rs | 190 ++-- libvctrl_handler/src/traits/core/transport.rs | 294 +++--- libvctrl_handler/src/traits/core/verifier.rs | 200 ++-- libvctrl_handler/src/traits/mod.rs | 76 +- libvctrl_handler/src/types/core/blob.rs | 214 ++-- libvctrl_handler/src/types/core/commit.rs | 340 +++---- libvctrl_handler/src/types/core/delta.rs | 354 +++---- libvctrl_handler/src/types/core/hash.rs | 286 +++--- libvctrl_handler/src/types/core/merge.rs | 248 ++--- libvctrl_handler/src/types/core/mod.rs | 174 ++-- libvctrl_handler/src/types/core/reflog.rs | 190 ++-- libvctrl_handler/src/types/core/tag.rs | 240 ++--- libvctrl_handler/src/types/core/tree.rs | 238 ++--- libvctrl_handler/src/types/core/user_id.rs | 152 +-- libvctrl_handler/src/types/mod.rs | 120 +-- libvctrl_handler/src/validation/hash.rs | 104 +- libvctrl_handler/src/validation/mod.rs | 138 +-- libvctrl_handler/src/validation/name.rs | 218 ++-- libvctrl_plumbing/src/cat_file.rs | 644 ++++++------ libvctrl_plumbing/src/lib.rs | 174 ++-- libvctrl_sha512/src/hkdf.rs | 92 +- libvctrl_sha512/src/hmac.rs | 114 +-- libvctrl_sha512/src/lib.rs | 310 +++--- libvctrl_sha512/src/sha384.rs | 296 +++--- libvctrl_sha512/src/sha512.rs | 498 ++++----- libvctrl_sha512/src/utils.rs | 248 ++--- release.json | 10 - 65 files changed, 9077 insertions(+), 9028 deletions(-) delete mode 100644 release.json diff --git a/Cargo.lock b/Cargo.lock index 0f501115..5e40504c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -263,7 +263,7 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libvctrl" -version = "2.1.2" +version = "2.1.3" dependencies = [ "libvctrl_core", "libvctrl_handler", @@ -273,7 +273,7 @@ dependencies = [ [[package]] name = "libvctrl_core" -version = "3.0.0" +version = "3.0.1" dependencies = [ "libvctrl_handler", "libvctrl_sha512", @@ -282,7 +282,7 @@ dependencies = [ [[package]] name = "libvctrl_handler" -version = "5.0.0" +version = "5.0.1" [[package]] name = "libvctrl_plumbing" @@ -298,7 +298,7 @@ version = "0.1.0" [[package]] name = "libvctrl_sha512" -version = "3.0.0" +version = "3.0.1" dependencies = [ "criterion", ] diff --git a/Cargo.toml b/Cargo.toml index f3d0551e..8088861a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,62 +1,121 @@ [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 = "deny" +alloc_instead_of_core = "deny" +allow_attributes = "allow" +allow_attributes_without_reason = "allow" +arithmetic_side_effects = "deny" +cargo = "deny" +complexity = "deny" +correctness = "deny" 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 = "deny" +panic = "deny" +pedantic = "deny" +perf = "deny" +restriction = "deny" +std_instead_of_alloc = "deny" +std_instead_of_core = "deny" +style = "deny" +suspicious = "deny" 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 = "deny" +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 = "deny" +rust_2021_compatibility = "deny" +rust_2024_compatibility = "deny" +single_use_lifetimes = "deny" +trivial_bounds = "deny" +trivial_casts = "deny" +trivial_numeric_casts = "deny" +unaligned_references = "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 = "deny" +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_tuple_struct_fields = "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/libvctrl/src/lib.rs b/libvctrl/src/lib.rs index 10df03fd..11e55458 100644 --- a/libvctrl/src/lib.rs +++ b/libvctrl/src/lib.rs @@ -1,336 +1,336 @@ -//! # libvctrl -//! -//! A unified facade for the libvctrl ecosystem. -//! -//! This crate aggregates the foundational crates of the version control -//! system into a single, coherent namespace. It re-exports all core types, -//! traits, constants, validation functions, and reference implementations -//! from: -//! -//! - [`libvctrl_handler`](https://docs.rs/libvctrl_handler) — abstract -//! contracts, immutable data types, and system limits. -//! - [`libvctrl_core`](https://docs.rs/libvctrl_core) — production-ready -//! reference implementations: binary codec, SHA-512 hasher, builders, and -//! in-memory stores. -//! - [`libvctrl_sha512`](https://docs.rs/libvctrl_sha512) — zero-dependency -//! cryptographic primitives. -//! -//! By re-exporting these crates under one roof, `libvctrl` allows downstream -//! applications to bootstrap a complete version control system without -//! manually stitching together multiple dependencies. It also serves as the -//! public API surface for the main binary crate. -//! -//! ## Architecture -//! -//! The crate exposes three top-level namespaces: -//! -//! - [`handler`](crate::handler) — the original `libvctrl_handler` crate. -//! - [`reference`](crate::reference) — the `libvctrl_core` reference -//! implementation crate. -//! - [`crypto`](crate::crypto) — the `libvctrl_sha512` crate. -//! -//! In addition, the most commonly used items are re-exported directly at the -//! crate root for ergonomic access. -//! -//! ### Handler re-exports -//! -//! Core contracts and types: -//! -//! - Traits: [`Encoder`](crate::Encoder), [`Decoder`](crate::Decoder), -//! [`Hasher`](crate::Hasher), [`ObjectStore`](crate::ObjectStore), -//! [`RefStore`](crate::RefStore), [`Signer`](crate::Signer), -//! [`Verifier`](crate::Verifier), [`Transport`](crate::Transport). -//! - Types: [`Blob`](crate::Blob), [`Tree`](crate::Tree), -//! [`TreeEntry`](crate::TreeEntry), [`Commit`](crate::Commit), -//! [`CommitMeta`](crate::CommitMeta), [`Tag`](crate::Tag), -//! [`Hash`](crate::Hash), [`UserID`](crate::UserID), -//! [`EntryKind`](crate::EntryKind). -//! - Error type: [`VctrlError`](crate::VctrlError). -//! -//! System limits and validation: -//! -//! - Constants such as [`HASH_LENGTH`](crate::HASH_LENGTH), -//! [`MAX_BLOB_SIZE`](crate::MAX_BLOB_SIZE), -//! [`MAX_MESSAGE_LENGTH`](crate::MAX_MESSAGE_LENGTH), -//! [`MAX_NAME_LENGTH`](crate::MAX_NAME_LENGTH), -//! [`MAX_PARENT_COUNT`](crate::MAX_PARENT_COUNT), and -//! [`MAX_TREE_ENTRIES`](crate::MAX_TREE_ENTRIES). -//! - Validation functions: -//! [`validate_hash_bytes`](crate::validate_hash_bytes), -//! [`validate_name`](crate::validate_name), -//! [`validate_ref_name`](crate::validate_ref_name), and -//! [`validate_tree_entry_name`](crate::validate_tree_entry_name). -//! -//! ### Core re-exports -//! -//! Reference implementations: -//! -//! - Codec: [`BinaryEncoder`](crate::BinaryEncoder) and -//! [`BinaryDecoder`](crate::BinaryDecoder) for deterministic binary -//! serialization. -//! - Hasher: [`Sha512Hasher`](crate::Sha512Hasher) for content addressing. -//! - Builders: [`BlobBuilder`](crate::BlobBuilder), -//! [`CommitBuilder`](crate::CommitBuilder), -//! [`TagBuilder`](crate::TagBuilder), -//! [`TreeBuilder`](crate::TreeBuilder), and -//! [`TreeEntryBuilder`](crate::TreeEntryBuilder). -//! - Stores: [`MemoryStore`](crate::MemoryStore) and -//! [`MemoryRefStore`](crate::MemoryRefStore). -//! -//! ## Why a unified facade? -//! -//! The libvctrl workspace is designed around strict separation of concerns. -//! However, end users often need a single dependency that exposes the full -//! stack. This crate provides that convenience without hiding the underlying -//! modularity. Developers can still access the original crates through the -//! `handler`, `reference`, and `crypto` namespaces. -//! -//! ## How it works -//! -//! All re-exports are compile-time aliases. There is no runtime overhead, and -//! no code is duplicated. The only cost is a slightly larger public API -//! surface. -//! -//! ## Safety and quality -//! -//! This crate inherits the strict safety guarantees of its dependencies: -//! -//! - `#![forbid(unsafe_code)]` — no unsafe code, period. -//! - Strict Clippy, rustc, and documentation lints are denied. -//! - All public items are documented and have doctests where applicable. -//! -//! ## Example -//! -//! The following example demonstrates a typical workflow: create a blob, -//! encode it, hash it, store it, and retrieve it. -//! -//! ``` -//! # use libvctrl::{Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore}; -//! # fn main() -> Result<(), libvctrl::VctrlError> { -//! let blob = Blob::new(b"my content".to_vec())?; -//! -//! // Encode the blob into deterministic bytes. -//! let mut encoded = Vec::new(); -//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; -//! -//! // Hash the encoded bytes to obtain a content address. -//! let hash = Sha512Hasher.hash(&mut encoded.as_slice())?; -//! -//! // Store the encoded object in memory. -//! let mut store = MemoryStore::new(); -//! store.put(&hash, &encoded)?; -//! -//! // Verify the object exists. -//! assert!(store.exists(&hash)?); -//! # Ok(()) -//! # } -//! ``` -//! -//! Use [`handler`](crate::handler), [`reference`](crate::reference), or -//! [`crypto`](crate::crypto) if you need direct access to the underlying -//! crates. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[cfg(test)] use proptest as _; -/// Re-export of the `libvctrl_core` reference implementation crate. -/// -/// This namespace contains production-ready implementations of the handler -/// contracts: binary codec, SHA-512 hasher, builders, and in-memory stores. + + + + pub use libvctrl_core as reference; -/// Re-export of the `libvctrl_handler` contracts and types crate. -/// -/// This namespace contains the abstract traits, immutable data types, -/// validation functions, and system constants that define the core VCS model. + + + + pub use libvctrl_handler as handler; -/// Re-export of the `libvctrl_sha512` cryptographic primitives crate. -/// -/// This namespace exposes zero-dependency SHA-512, HMAC-SHA512, HKDF-SHA512, -/// and optional SHA-384 implementations. + + + + pub use libvctrl_sha512 as crypto; -/// Handler module re-exports. -/// -/// These modules are re-exported for direct access to the original crate's -/// internal organization. Most users will prefer the flattened root items, -/// but these are available for advanced use cases. + + + + + pub use handler::constants; -/// Enumerations and kind discriminants. -/// -/// Contains [`EntryKind`](crate::EntryKind) and any other enum types defined -/// by the handler crate. + + + + pub use handler::enums; -/// Error types and constructors. -/// -/// Contains [`VctrlError`](crate::VctrlError) and associated error variants. + + + pub use handler::errors; -/// Macros exported by the handler crate. -/// -/// These macros assist in implementing common traits or validation logic. + + + pub use handler::macros; -/// Core behavior traits. -/// -/// Contains the trait definitions for [`Encoder`](crate::Encoder), -/// [`Decoder`](crate::Decoder), [`Hasher`](crate::Hasher), -/// [`ObjectStore`](crate::ObjectStore), [`RefStore`](crate::RefStore), -/// [`Signer`](crate::Signer), [`Verifier`](crate::Verifier), and -/// [`Transport`](crate::Transport). + + + + + + + pub use handler::traits; -/// Immutable data types. -/// -/// Contains the core object model: [`Blob`](crate::Blob), -/// [`Tree`](crate::Tree), [`TreeEntry`](crate::TreeEntry), -/// [`Commit`](crate::Commit), [`CommitMeta`](crate::CommitMeta), -/// [`Tag`](crate::Tag), [`Hash`](crate::Hash), [`UserID`](crate::UserID), -/// and related types. + + + + + + + pub use handler::types; -/// Validation helper functions. -/// -/// Contains functions like [`validate_name`](crate::validate_name) and -/// [`validate_ref_name`](crate::validate_ref_name) used to enforce safety -/// invariants. + + + + + pub use handler::validation; -/// System limit constants. -/// -/// Re-exports the following constants at the crate root: -/// -/// - [`HASH_LENGTH`](crate::HASH_LENGTH) -/// - [`MAX_BLOB_SIZE`](crate::MAX_BLOB_SIZE) -/// - [`MAX_MESSAGE_LENGTH`](crate::MAX_MESSAGE_LENGTH) -/// - [`MAX_NAME_LENGTH`](crate::MAX_NAME_LENGTH) -/// - [`MAX_PARENT_COUNT`](crate::MAX_PARENT_COUNT) -/// - [`MAX_TREE_ENTRIES`](crate::MAX_TREE_ENTRIES) + + + + + + + + + + pub use handler::{ HASH_LENGTH, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, MAX_NAME_LENGTH, MAX_PARENT_COUNT, MAX_TREE_ENTRIES, }; -/// Represents the kind of a tree entry. -/// -/// This enum distinguishes blobs, executable files, symlinks, trees, and -/// submodules. + + + + pub use handler::EntryKind; -/// Unified error type for all libvctrl operations. -/// -/// All fallible operations across the ecosystem return this error type. + + + pub use handler::VctrlError; -/// Core behavior traits. -/// -/// Re-exports the following traits at the crate root: -/// -/// - [`Decoder`](crate::Decoder) -/// - [`Encoder`](crate::Encoder) -/// - [`Hasher`](crate::Hasher) -/// - [`ObjectStore`](crate::ObjectStore) -/// - [`RefStore`](crate::RefStore) -/// - [`Signer`](crate::Signer) -/// - [`Transport`](crate::Transport) -/// - [`Verifier`](crate::Verifier) + + + + + + + + + + + + pub use handler::{Decoder, Encoder, Hasher, ObjectStore, RefStore, Signer, Transport, Verifier}; -/// Immutable data types. -/// -/// Re-exports the following types at the crate root: -/// -/// - [`Blob`](crate::Blob) -/// - [`Commit`](crate::Commit) -/// - [`CommitMeta`](crate::CommitMeta) -/// - [`Hash`](crate::Hash) -/// - [`Tag`](crate::Tag) -/// - [`Tree`](crate::Tree) -/// - [`TreeEntry`](crate::TreeEntry) -/// - [`UserID`](crate::UserID) + + + + + + + + + + + + pub use handler::{Blob, Commit, CommitMeta, Hash, Tag, Tree, TreeEntry, UserID}; -/// Validation functions. -/// -/// Re-exports the following functions at the crate root: -/// -/// - [`validate_hash_bytes`](crate::validate_hash_bytes) -/// - [`validate_name`](crate::validate_name) -/// - [`validate_ref_name`](crate::validate_ref_name) -/// - [`validate_tree_entry_name`](crate::validate_tree_entry_name) + + + + + + + + pub use handler::{ validate_hash_bytes, validate_name, validate_ref_name, validate_tree_entry_name, }; -/// Core reference implementation re-exports. -/// -/// These items provide concrete implementations of the handler contracts. + + + pub use reference::codec; -/// Object builders for ergonomic construction. -/// -/// This module contains builder types for blobs, commits, tags, trees, and -/// tree entries. + + + + pub use reference::object; -/// In-memory object and reference stores. -/// -/// This module contains [`MemoryStore`](crate::MemoryStore) and -/// [`MemoryRefStore`](crate::MemoryRefStore). + + + + pub use reference::store; -/// Decoder for the binary format. -/// -/// This zero-sized type implements [`Decoder`](crate::Decoder) and parses -/// versioned binary payloads with strict bounds checking. + + + + pub use reference::codec::BinaryDecoder; -/// Encoder for the binary format. -/// -/// This zero-sized type implements [`Encoder`](crate::Encoder) and produces -/// deterministic, versioned binary payloads. + + + + pub use reference::codec::BinaryEncoder; -/// SHA-512 content hasher. -/// -/// This type implements [`Hasher`](crate::Hasher) and produces 64-byte -/// content addresses. + + + + pub use reference::hash::Sha512Hasher; -/// Builder for [`Blob`] objects. -/// -/// Provides a fluent API for constructing validated blobs. + + + pub use reference::object::BlobBuilder; -/// Builder for [`Commit`] objects. -/// -/// Provides a fluent API for constructing validated commits. + + + pub use reference::object::CommitBuilder; -/// Builder for [`Tag`] objects. -/// -/// Provides a fluent API for constructing validated tags. + + + pub use reference::object::TagBuilder; -/// Builder for [`Tree`] objects. -/// -/// Provides a fluent API for constructing validated trees. + + + pub use reference::object::TreeBuilder; -/// Builder for [`TreeEntry`] objects. -/// -/// Provides a fluent API for constructing validated tree entries. + + + pub use reference::object::TreeEntryBuilder; -/// In-memory reference store. -/// -/// Implements [`RefStore`](crate::RefStore) using a `HashMap`. + + + pub use reference::store::MemoryRefStore; -/// In-memory object store. -/// -/// Implements [`ObjectStore`](crate::ObjectStore) using a `HashMap`. + + + pub use reference::store::MemoryStore; diff --git a/libvctrl_core/src/codec/binary_decoder.rs b/libvctrl_core/src/codec/binary_decoder.rs index 960917dd..db2c9f76 100644 --- a/libvctrl_core/src/codec/binary_decoder.rs +++ b/libvctrl_core/src/codec/binary_decoder.rs @@ -1,29 +1,29 @@ -//! # Binary Decoder -//! -//! This module provides a strict, bounds-checked decoder for the binary -//! serialization format defined by the sibling encoder. It is the inverse of -//! the encoder: every byte sequence produced by the encoder is accepted by -//! this decoder, and every decoded object is guaranteed to satisfy the -//! invariants of the corresponding `libvctrl_handler` types. -//! -//! ## Design rationale -//! -//! Decoding untrusted input is one of the most dangerous operations in a -//! version control system. A naive implementation might trust length prefixes -//! and parse out of bounds. This decoder therefore follows a "defense in -//! depth" strategy: -//! -//! - The stream is first bounded by a conservative maximum size. -//! - Every offset is checked before slicing. -//! - Every string is validated as UTF-8. -//! - System limits are re-checked after numeric conversion. -//! -//! ## How it works -//! -//! Each `decode_*` method first calls [`read_bounded`] to slurp the input into -//! a bounded `Vec`, then calls [`check_version`] to strip and validate the -//! version byte, and finally parses the remaining bytes with explicit offset -//! checks. No slice indexing is performed without a preceding bounds check. + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{ Blob, Commit, CommitMeta, Decoder, EntryKind, HASH_LENGTH, Hash, MAX_BLOB_SIZE, @@ -31,45 +31,45 @@ use libvctrl_handler::{ }; 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. -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +85,12 @@ impl BinaryDecoder { .ok_or_else(|| VctrlError::CorruptedData("missing payload after version".into())) } - /// Reads the reader into memory while enforcing a hard size bound. - /// - /// This helper prevents denial-of-service attacks by refusing to allocate - /// more than `max_size` bytes. It uses a fixed 4 KiB buffer to avoid - /// reallocation on each byte and returns [`VctrlError::IoError`] if the - /// underlying reader fails. + + + + + + fn read_bounded( reader: &mut R, max_size: usize, @@ -114,14 +114,14 @@ impl BinaryDecoder { Ok(buf) } - /// Returns a single byte at `pos`, or a structured error. + fn require_byte(data: &[u8], pos: usize, what: &str) -> Result { data.get(pos) .copied() .ok_or_else(|| VctrlError::CorruptedData(format!("missing {what}"))) } - /// Returns a slice `data[start..start+len]`, with overflow and bounds checks. + fn require_slice<'a>( data: &'a [u8], start: usize, @@ -137,35 +137,35 @@ impl BinaryDecoder { } impl Decoder for BinaryDecoder { - /// Decodes a binary blob. - /// - /// # Format - /// - /// The encoded blob starts with a version byte (currently `3`), followed by - /// an 8-byte little-endian length prefix and exactly that many data bytes. - /// The declared byte length is re-checked against [`MAX_BLOB_SIZE`]. - /// - /// # Errors - /// - /// Returns [`VctrlError::CorruptedData`] if the version is wrong, the length - /// prefix is truncated, the blob exceeds the limit, or the declared length - /// does not match the remaining bytes. Returns [`VctrlError::IoError`] if the - /// reader fails. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Blob, Decoder, Encoder}; - /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; - /// let original = Blob::new(b"hello world".to_vec()).unwrap(); - /// - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_blob(&original, &mut encoded).unwrap(); - /// - /// let decoded = BinaryDecoder.decode_blob(Cursor::new(encoded.as_slice())).unwrap(); - /// assert_eq!(decoded, original); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_blob(&self, mut reader: R) -> Result { let max_size = usize::try_from(MAX_BLOB_SIZE).unwrap_or(usize::MAX) + 16; let data = Self::read_bounded(&mut reader, max_size)?; @@ -193,38 +193,38 @@ impl Decoder for BinaryDecoder { Blob::new(payload.to_vec()) } - /// Decodes a binary tree. - /// - /// # Format - /// - /// After the version byte, a 4-byte little-endian count is followed by that - /// many entries. Each entry starts with a one-byte name length, a UTF-8 name, - /// a one-byte kind tag, and a 64-byte hash. - /// - /// # Errors - /// - /// Returns [`VctrlError::CorruptedData`] if any prefix is truncated, the entry - /// count exceeds [`MAX_TREE_ENTRIES`], the name is not valid UTF-8, the kind - /// byte is unknown, the hash is invalid, or the final parsed position does not - /// equal the total byte length. Also returns validation errors from - /// [`Tree::new`] and [`TreeEntry::new`]. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Decoder, Encoder, EntryKind, Hash, Tree, TreeEntry}; - /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; - /// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); - /// let entry = TreeEntry::new("a.txt".to_owned(), EntryKind::Blob, hash).unwrap(); - /// let original = Tree::new(vec![entry]).unwrap(); - /// - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_tree(&original, &mut encoded).unwrap(); - /// - /// let decoded = BinaryDecoder.decode_tree(Cursor::new(encoded.as_slice())).unwrap(); - /// assert_eq!(decoded, original); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_tree(&self, mut reader: R) -> Result { let max_size = usize::try_from(MAX_TREE_ENTRIES).unwrap_or(usize::MAX) * 321 + 5; let data = Self::read_bounded(&mut reader, max_size)?; @@ -284,57 +284,57 @@ impl Decoder for BinaryDecoder { Tree::new(entries) } - /// Decodes a binary commit. - /// - /// # Format - /// - /// The commit layout is fixed: tree hash, u16 parent count, parent hashes, - /// author name/email with u8 length prefixes, committer name/email, u32 - /// message length, message bytes, i64 timestamp, i16 timezone offset, and an - /// optional encoding string. All integer fields are little-endian. - /// - /// # Errors - /// - /// Returns [`VctrlError::CorruptedData`] for structural issues and - /// [`VctrlError::SerializationError`] if the message exceeds - /// [`MAX_MESSAGE_LENGTH`]. Also returns validation errors from - /// [`Commit::with_meta`] and [`UserID::new`]. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Commit, Decoder, Encoder, Hash, UserID}; - /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; - /// let tree = Hash::from_bytes(&[1u8; 64]).unwrap(); - /// let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); - /// let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); - /// let original = Commit::new( - /// tree, - /// vec![], - /// author, - /// committer, - /// "Initial commit".to_owned(), - /// ) - /// .unwrap(); - /// - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_commit(&original, &mut encoded).unwrap(); - /// - /// let decoded = BinaryDecoder.decode_commit(Cursor::new(encoded.as_slice())).unwrap(); - /// assert_eq!(decoded, original); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[allow(clippy::too_many_lines)] fn decode_commit(&self, mut reader: R) -> Result { let max_size = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX) + 1024; let data = Self::read_bounded(&mut reader, max_size)?; let data = Self::check_version(&data)?; - // Tree hash + let tree_hash = Self::require_slice(data, 0, HASH_LENGTH, "commit tree hash")?; let tree = Hash::from_bytes(tree_hash)?; - // Parent count and parents + let parent_count_bytes = Self::require_slice(data, HASH_LENGTH, 2, "commit parent count")?; let parent_count = u16::from_le_bytes( parent_count_bytes @@ -350,7 +350,7 @@ impl Decoder for BinaryDecoder { pos += HASH_LENGTH; } - // Author name + let author_name_len = Self::require_byte(data, pos, "author name length")? as usize; pos += 1; let author_name_bytes = Self::require_slice(data, pos, author_name_len, "author name")?; @@ -359,7 +359,7 @@ impl Decoder for BinaryDecoder { .to_string(); pos += author_name_len; - // Author email + let author_email_len = Self::require_byte(data, pos, "author email length")? as usize; pos += 1; let author_email_bytes = Self::require_slice(data, pos, author_email_len, "author email")?; @@ -370,7 +370,7 @@ impl Decoder for BinaryDecoder { let author = UserID::new(author_name, author_email)?; - // Committer name + let committer_name_len = Self::require_byte(data, pos, "committer name length")? as usize; pos += 1; let committer_name_bytes = @@ -382,7 +382,7 @@ impl Decoder for BinaryDecoder { .to_string(); pos += committer_name_len; - // Committer email + let committer_email_len = Self::require_byte(data, pos, "committer email length")? as usize; pos += 1; let committer_email_bytes = @@ -396,7 +396,7 @@ impl Decoder for BinaryDecoder { let committer = UserID::new(committer_name, committer_email)?; - // Message + let msg_len_bytes = Self::require_slice(data, pos, 4, "commit message length")?; let msg_len = u32::from_le_bytes( msg_len_bytes @@ -417,7 +417,7 @@ impl Decoder for BinaryDecoder { .to_string(); pos += msg_len; - // Timestamp and timezone + let timestamp_bytes = Self::require_slice(data, pos, 8, "commit timestamp")?; let timestamp = i64::from_le_bytes( timestamp_bytes @@ -434,7 +434,7 @@ impl Decoder for BinaryDecoder { ); pos += 2; - // Optional encoding + let encoding_len = Self::require_byte(data, pos, "commit encoding length")? as usize; pos += 1; let encoding = if encoding_len > 0 { @@ -456,50 +456,50 @@ impl Decoder for BinaryDecoder { Commit::with_meta(tree, parents, author, committer, message, meta) } - /// Decodes a binary tag. - /// - /// # Format - /// - /// Tag starts with a one-byte name length and name, a 64-byte target hash, a - /// tagger presence byte, optional tagger name/email, u32 message length, - /// message, timestamp, timezone offset, and optional encoding. - /// - /// # Errors - /// - /// Returns [`VctrlError::CorruptedData`] for structural issues and - /// [`VctrlError::SerializationError`] if the message exceeds - /// [`MAX_MESSAGE_LENGTH`]. Also returns validation errors from - /// [`Tag::with_meta`] and [`UserID::new`]. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Decoder, Encoder, Hash, Tag, UserID}; - /// # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; - /// let target = Hash::from_bytes(&[2u8; 64]).unwrap(); - /// let tagger = UserID::new("Tagger".to_owned(), "tagger@example.com".to_owned()).unwrap(); - /// let original = Tag::new( - /// "v1.0.0".to_owned(), - /// target, - /// Some(tagger), - /// "Release".to_owned(), - /// ) - /// .unwrap(); - /// - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_tag(&original, &mut encoded).unwrap(); - /// - /// let decoded = BinaryDecoder.decode_tag(Cursor::new(encoded.as_slice())).unwrap(); - /// assert_eq!(decoded, original); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[allow(clippy::too_many_lines)] fn decode_tag(&self, mut reader: R) -> Result { let max_size = usize::try_from(MAX_MESSAGE_LENGTH).unwrap_or(usize::MAX) + 1024; let data = Self::read_bounded(&mut reader, max_size)?; let data = Self::check_version(&data)?; - // Tag name + let name_len = Self::require_byte(data, 0, "tag name length")? as usize; let name_bytes = Self::require_slice(data, 1, name_len, "tag name")?; let name = str::from_utf8(name_bytes) @@ -507,12 +507,12 @@ impl Decoder for BinaryDecoder { .to_string(); let mut pos = 1 + name_len; - // Target hash + let target_bytes = Self::require_slice(data, pos, HASH_LENGTH, "tag target hash")?; let target = Hash::from_bytes(target_bytes)?; pos += HASH_LENGTH; - // Tagger presence + let has_tagger = match Self::require_byte(data, pos, "tagger presence byte")? { 0 => false, 1 => true, @@ -524,7 +524,7 @@ impl Decoder for BinaryDecoder { }; pos += 1; - // Optional tagger + let tagger = if has_tagger { let tagger_name_len = Self::require_byte(data, pos, "tagger name length")? as usize; pos += 1; @@ -552,7 +552,7 @@ impl Decoder for BinaryDecoder { None }; - // Message + let msg_len_bytes = Self::require_slice(data, pos, 4, "tag message length")?; let msg_len = u32::from_le_bytes( msg_len_bytes @@ -573,7 +573,7 @@ impl Decoder for BinaryDecoder { .to_string(); pos += msg_len; - // Timestamp and timezone + let timestamp_bytes = Self::require_slice(data, pos, 8, "tag timestamp")?; let timestamp = i64::from_le_bytes( timestamp_bytes @@ -590,7 +590,7 @@ impl Decoder for BinaryDecoder { ); pos += 2; - // Optional encoding + let encoding_len = Self::require_byte(data, pos, "tag encoding length")? as usize; pos += 1; let encoding = if encoding_len > 0 { diff --git a/libvctrl_core/src/codec/binary_encoder.rs b/libvctrl_core/src/codec/binary_encoder.rs index 2bd8f738..4e3fd1f7 100644 --- a/libvctrl_core/src/codec/binary_encoder.rs +++ b/libvctrl_core/src/codec/binary_encoder.rs @@ -1,112 +1,112 @@ -//! # Binary Encoder -//! -//! This module provides a deterministic, versioned, little-endian binary -//! encoder for every core object type defined by `libvctrl_handler`. -//! -//! The encoder is the counterpart to [`BinaryDecoder`](super::binary_decoder::BinaryDecoder). -//! Data written by this encoder can always be decoded back into an equivalent -//! object, provided the same system limits and version are used. -//! -//! ## Design rationale -//! -//! Version control objects are content-addressed. Deterministic serialization -//! is therefore critical: the same object must always produce exactly the same -//! bytes, otherwise the hash changes and the object becomes unreachable. -//! -//! The encoder achieves determinism by: -//! -//! - Using a fixed version byte. -//! - Using little-endian integer encoding on all supported platforms. -//! - Writing fields in a strict, documented order. -//! - Never depending on platform-specific layouts. -//! -//! ## How it works -//! -//! Every `encode_*` method writes directly to the supplied writer. Length -//! prefixes are validated before conversion to prevent silent truncation. -//! All string fields are encoded as a one-byte length followed by UTF-8 bytes. -//! The writer uses [`std::io::Write::write_all`] to guarantee complete writes. + + + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{ Blob, Commit, Encoder, EntryKind, MAX_MESSAGE_LENGTH, Tag, Tree, VctrlError, }; use std::io::Write; -/// The current version of the binary encoding format. -/// -/// This version byte is written as the first byte of every encoded object. -/// The decoder rejects any input whose first byte does not equal this value. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_core::codec::VERSION; -/// assert_eq!(VERSION, 3); -/// ``` + + + + + + + + + + + pub const VERSION: u8 = 3; -/// An encoder for the binary format of Git objects. -/// -/// `BinaryEncoder` is a stateless, zero-sized type that implements the -/// [`Encoder`] trait. It converts high-level objects such as [`Blob`], -/// [`Tree`], [`Commit`], and [`Tag`] into a compact, versioned byte stream. -/// -/// # Why this struct exists -/// -/// Serialization is isolated behind a trait so that different storage backends -/// can use different wire formats. `BinaryEncoder` is the reference -/// implementation and defines the canonical on-disk format for the workspace. -/// -/// # How it works -/// -/// Each method writes to a [`std::io::Write`] implementation. The encoder does -/// not allocate the entire payload upfront; it streams fields directly to the -/// writer. However, all length conversions are checked with `try_from`, so -/// impossible lengths are reported as [`VctrlError::SerializationError`] -/// instead of causing silent truncation. -/// -/// # Examples -/// -/// ``` -/// # use std::io::Cursor; -/// # use libvctrl_handler::{Blob, Encoder}; -/// # use libvctrl_core::codec::BinaryEncoder; -/// let blob = Blob::new(b"hello".to_vec()).unwrap(); -/// let mut buf = Vec::new(); -/// BinaryEncoder.encode_blob(&blob, &mut buf).unwrap(); -/// assert_eq!(buf[0], 3); -/// assert_eq!(buf.len(), 1 + 8 + 5); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub struct BinaryEncoder; impl Encoder for BinaryEncoder { - /// Encodes a [`Blob`] into the binary format. - /// - /// The output layout is: - /// - /// | Offset | Size | Field | - /// |--------|------------|---------------------| - /// | 0 | 1 | Version byte | - /// | 1 | 8 | `data_len` (u64 LE) | - /// | 9 | `data_len` | Raw blob data | - /// - /// # Errors - /// - /// Returns [`VctrlError::IoError`] if the writer fails. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Blob, Encoder}; - /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; - /// let blob = Blob::new(b"hello world".to_vec()).unwrap(); - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_blob(&blob, &mut encoded).unwrap(); - /// - /// assert_eq!(encoded[0], VERSION); - /// assert_eq!(encoded.len(), 1 + 8 + blob.data().len()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { let data = blob.data(); writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; @@ -117,47 +117,47 @@ 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); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError> { let entries = tree.entries(); writer.write_all(&[VERSION]).map_err(VctrlError::from_io)?; @@ -196,67 +196,67 @@ impl Encoder for BinaryEncoder { Ok(()) } - /// Encodes a [`Commit`] into the binary format. - /// - /// The output layout is fixed and ordered: - /// - /// | Field | Size | - /// |-----------------------|---------------| - /// | Version | 1 | - /// | Tree hash | 64 | - /// | Parent count | 2 (u16 LE) | - /// | Parent hashes | 64 * count | - /// | Author name length | 1 | - /// | Author name | length | - /// | Author email length | 1 | - /// | Author email | length | - /// | Committer name length | 1 | - /// | Committer name | length | - /// | Committer email length| 1 | - /// | Committer email | length | - /// | Message length | 4 (u32 LE) | - /// | Message | length | - /// | Timestamp | 8 (i64 LE) | - /// | Timezone offset | 2 (i16 LE) | - /// | Encoding length | 1 | - /// | Encoding | length or 0 | - /// - /// # Errors - /// - /// Returns [`VctrlError::SerializationError`] if: - /// - /// - the commit has more than `u16::MAX` parents, - /// - any name or email is longer than `u8::MAX` bytes, - /// - the message length cannot be represented as `u32`, - /// - the message exceeds [`MAX_MESSAGE_LENGTH`], - /// - the encoding string is longer than `u8::MAX` bytes. - /// - /// Returns [`VctrlError::IoError`] if the writer fails. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Cursor; - /// # use libvctrl_handler::{Commit, Encoder, Hash, UserID}; - /// # use libvctrl_core::codec::{BinaryEncoder, VERSION}; - /// let tree = Hash::from_bytes(&[1u8; 64]).unwrap(); - /// let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); - /// let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); - /// let commit = Commit::new( - /// tree, - /// vec![], - /// author, - /// committer, - /// "Initial commit".to_owned(), - /// ) - /// .unwrap(); - /// - /// let mut encoded = Vec::new(); - /// BinaryEncoder.encode_commit(&commit, &mut encoded).unwrap(); - /// - /// assert_eq!(encoded[0], VERSION); - /// assert!(encoded.len() > 1 + 64 + 2); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_commit( &self, commit: &Commit, @@ -357,62 +357,62 @@ impl Encoder for BinaryEncoder { 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)?; diff --git a/libvctrl_core/src/codec/mod.rs b/libvctrl_core/src/codec/mod.rs index fdda6cec..4e33d525 100644 --- a/libvctrl_core/src/codec/mod.rs +++ b/libvctrl_core/src/codec/mod.rs @@ -1,70 +1,70 @@ -//! # Binary Codec -//! -//! This module provides the reference implementation of the binary -//! serialization format for Git objects. It contains two zero-sized types: -//! -//! - [`BinaryEncoder`](binary_encoder::BinaryEncoder): writes objects into a -//! deterministic, versioned byte stream. -//! - [`BinaryDecoder`](binary_decoder::BinaryDecoder): reads such byte streams -//! back into strongly validated, immutable objects. -//! -//! ## Why this module exists -//! -//! Version control systems rely on content addressing. To compute a stable -//! hash, objects must be serialized in a way that is independent of platform, -//! compiler, and runtime conditions. This module defines such a canonical -//! encoding and the corresponding decoding logic. -//! -//! The encoder and decoder are deliberately separate to enforce a clear -//! boundary between producing bytes and consuming untrusted bytes. The decoder -//! performs extensive bounds and validity checks, whereas the encoder assumes -//! its input objects are already valid. -//! -//! ## How it works -//! -//! Every encoded object begins with a single version byte. The current version -//! is [`VERSION`](binary_encoder::VERSION) = 3. The decoder rejects any input -//! whose first byte does not match this value. -//! -//! After the version byte, fields are written in a strict order using -//! little-endian integer encoding. Strings are length-prefixed with a single -//! byte; larger payloads (like blob content or commit messages) use dedicated -//! 32-bit or 64-bit length prefixes. -//! -//! ## Examples -//! -//! The following example shows a complete round-trip through the encoder and -//! decoder. It encodes a [`Blob`], then decodes it back and asserts equality. -//! -//! ``` -//! # use std::io::Cursor; -//! # use libvctrl_handler::{Blob, Decoder, Encoder}; -//! # use libvctrl_core::codec::{BinaryDecoder, BinaryEncoder}; -//! let original = Blob::new(b"round trip".to_vec()).unwrap(); -//! -//! let mut encoded = Vec::new(); -//! BinaryEncoder.encode_blob(&original, &mut encoded).unwrap(); -//! -//! let decoded = BinaryDecoder -//! .decode_blob(Cursor::new(encoded.as_slice())) -//! .unwrap(); -//! -//! assert_eq!(original, decoded); -//! ``` - -/// Binary decoder for Git objects. -/// -/// This submodule provides [`BinaryDecoder`](self::BinaryDecoder), the -/// strictly validated inverse of the encoder. It accepts any -/// [`std::io::Read`] source and returns either a fully constructed object or a -/// [`VctrlError`] describing the exact corruption encountered. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod binary_decoder; -/// Binary encoder for Git objects. -/// -/// This submodule provides [`BinaryEncoder`](self::BinaryEncoder), the -/// canonical producer of binary object data. It writes directly to any -/// [`std::io::Write`] sink without intermediate heap allocations. + + + + + pub mod binary_encoder; pub use binary_decoder::BinaryDecoder; diff --git a/libvctrl_core/src/hash/mod.rs b/libvctrl_core/src/hash/mod.rs index 4653e977..b83c8ad9 100644 --- a/libvctrl_core/src/hash/mod.rs +++ b/libvctrl_core/src/hash/mod.rs @@ -1,48 +1,48 @@ -//! SHA-512 hasher implementation for content addressing. -//! -//! # Why this module exists -//! -//! The [`libvctrl_handler`] crate defines the [`Hasher`](libvctrl_handler::Hasher) -//! trait as the abstraction for content-addressable object hashing. This module -//! provides a concrete implementation using the SHA-512 algorithm from the -//! [`libvctrl_sha512`] crate. It bridges the raw SHA-512 digest computation to -//! the handler's [`Hash`] type, ensuring that all hashes produced by this -//! crate are compatible with the rest of the VCS ecosystem. -//! -//! # How it works -//! -//! The [`Sha512Hasher`] is a zero-sized struct. It holds no state because -//! hashing is stateless across invocations. The [`hash`](Sha512Hasher::hash) -//! method reads from a generic [`Read`](std::io::Read) stream in fixed-size -//! chunks, feeds each chunk into the underlying [`Sha512Hash`] engine, and -//! finalizes the digest into a 64-byte [`Hash`]. The result length always -//! matches [`HASH_LENGTH`](libvctrl_handler::HASH_LENGTH), so conversion -//! cannot fail. -//! -//! # Examples -//! -//! Hash a byte slice: -//! -//! ``` -//! use libvctrl_core::hash::Sha512Hasher; -//! use libvctrl_handler::Hasher; -//! -//! let hasher = Sha512Hasher; -//! let hash = hasher.hash(b"hello world".as_ref()).unwrap(); -//! assert_eq!(hash.as_bytes().len(), 64); -//! ``` - -/// SHA-512 hasher implementation. -/// -/// This submodule contains the [`Sha512Hasher`] type, which implements the -/// [`Hasher`](libvctrl_handler::Hasher) trait using the SHA-512 algorithm. -/// The implementation is stateless, thread-safe, and suitable for both small -/// byte slices and large streaming inputs. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod sha512; -/// Re-export of [`Sha512Hasher`] for convenient access at the module root. -/// -/// By re-exporting, users can refer to `libvctrl_core::hash::Sha512Hasher` -/// instead of the longer `libvctrl_core::hash::sha512::Sha512Hasher`. This -/// aligns with the crate's goal of providing ergonomic, discoverable APIs. + + + + + pub use sha512::Sha512Hasher; diff --git a/libvctrl_core/src/hash/sha512.rs b/libvctrl_core/src/hash/sha512.rs index 3474d034..edd74187 100644 --- a/libvctrl_core/src/hash/sha512.rs +++ b/libvctrl_core/src/hash/sha512.rs @@ -1,98 +1,98 @@ -//! SHA-512 hasher implementation for content addressing. -//! -//! # Why this module exists -//! -//! The [`libvctrl_handler`] crate defines the [`Hasher`](libvctrl_handler::Hasher) -//! trait as the abstraction for content-addressable object hashing. This module -//! provides a concrete implementation using the SHA-512 algorithm from the -//! [`libvctrl_sha512`] crate. It bridges the raw SHA-512 digest computation to -//! the handler's [`Hash`] type, ensuring that all hashes produced by this -//! crate are compatible with the rest of the VCS ecosystem. -//! -//! # How it works -//! -//! The [`Sha512Hasher`] is a zero-sized struct. It holds no state because -//! hashing is stateless across invocations. The [`hash`](Sha512Hasher::hash) -//! method reads from a generic [`Read`](std::io::Read) stream in fixed-size -//! chunks, feeds each chunk into the underlying [`Sha512Hash`] engine, and -//! finalizes the digest into a 64-byte [`Hash`]. The result length always -//! matches [`HASH_LENGTH`](libvctrl_handler::HASH_LENGTH), so conversion -//! cannot fail. -//! -//! # Examples -//! -//! Hash a byte slice: -//! -//! ``` -//! use libvctrl_core::hash::Sha512Hasher; -//! use libvctrl_handler::Hasher; -//! -//! let hasher = Sha512Hasher; -//! let hash = hasher.hash(b"hello world".as_ref()).unwrap(); -//! assert_eq!(hash.as_bytes().len(), 64); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{Hash, Hasher, VctrlError}; use libvctrl_sha512::Hash as Sha512Hash; -/// A hasher that uses the SHA-512 algorithm. -/// -/// # Design rationale -/// -/// This is a zero-sized struct (ZST) because the SHA-512 algorithm does not -/// require any persistent state between calls. Each call to -/// [`hash`](Sha512Hasher::hash) creates a fresh [`Sha512Hash`] engine, -/// processes the input, and drops it. This makes the hasher trivially -/// [`Clone`], [`Default`], and [`Debug`], and allows it to be passed by value -/// without overhead. -/// -/// The struct name follows the convention of naming the concrete implementation -/// after the algorithm it uses, making it obvious to users what cryptographic -/// function will be applied. -/// -/// # Examples -/// -/// Create a hasher instance: -/// -/// ``` -/// # use libvctrl_core::hash::Sha512Hasher; -/// let hasher = Sha512Hasher::default(); -/// // The hasher is stateless and can be reused for multiple inputs. -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default, Clone)] pub struct Sha512Hasher; impl Hasher for Sha512Hasher { - /// Hashes the contents of a reader using SHA-512. - /// - /// # How it works - /// - /// The method reads from `reader` in 4096-byte chunks to avoid loading - /// large objects entirely into memory. For each chunk, it calls - /// [`update`](Sha512Hash::update) on a fresh [`Sha512Hash`] engine. Once - /// EOF is reached (read returns 0), the engine is finalized and the raw - /// 64-byte digest is converted into a [`Hash`] via - /// [`Hash::from_bytes`]. Because SHA-512 always produces 64 bytes, the - /// conversion cannot fail and the `?` operator is safe to use. - /// - /// # Errors - /// - /// Returns [`VctrlError::IoError`] if an I/O error occurs while reading - /// from the underlying reader. Hash computation itself is infallible. - /// - /// # Examples - /// - /// Hash data from a [`Cursor`](std::io::Cursor): - /// - /// ``` - /// # use libvctrl_core::hash::Sha512Hasher; - /// # use libvctrl_handler::Hasher; - /// # use std::io::Cursor; - /// let hasher = Sha512Hasher; - /// let data = b"streaming data"; - /// let hash = hasher.hash(Cursor::new(data)).unwrap(); - /// assert_eq!(hash.as_bytes().len(), 64); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn hash(&self, mut reader: R) -> Result { let mut hasher = Sha512Hash::new(); let mut buffer = [0u8; 4096]; diff --git a/libvctrl_core/src/lib.rs b/libvctrl_core/src/lib.rs index 9d83e949..93049709 100644 --- a/libvctrl_core/src/lib.rs +++ b/libvctrl_core/src/lib.rs @@ -1,92 +1,92 @@ -//! # libvctrl_core -//! -//! Reference implementations for the contracts defined by -//! [`libvctrl_handler`](https://docs.rs/libvctrl_handler). -//! -//! This crate provides production-ready, safe implementations of hashing, -//! binary serialization, in-memory storage, reference management, and builder -//! utilities. It is the first concrete consumer of the `libvctrl_handler` -//! traits and serves as a quality exemplar for downstream custom backends. -//! -//! ## Architecture -//! -//! The crate is organized by domain responsibility: -//! -//! - [`codec`](crate::codec) — deterministic binary encoding and decoding. -//! - [`hash`](crate::hash) — SHA-512 content addressing. -//! - [`object`](crate::object) — ergonomic builder patterns. -//! - [`store`](crate::store) — in-memory object and reference stores. -//! -//! Each module depends only on the public contracts exposed by -//! `libvctrl_handler`, plus the SHA-512 implementation from -//! `libvctrl_sha512`. No module contains unsafe code. -//! -//! ## Safety and quality -//! -//! The crate forbids unsafe code and denies a strict set of Clippy and -//! rustc lints. Every public item is documented and has doctests where -//! applicable. The binary decoder is especially defensive: it bounds all -//! input reads, verifies version bytes, validates UTF-8, and re-checks system -//! limits before constructing any object. -//! -//! ## Example -//! -//! A common workflow encodes an object, hashes it, stores it, and retrieves -//! it through the in-memory store: -//! -//! ``` -//! # use libvctrl_handler::{Blob, Encoder, Hasher, ObjectStore}; -//! # use libvctrl_core::codec::BinaryEncoder; -//! # use libvctrl_core::hash::Sha512Hasher; -//! # use libvctrl_core::store::MemoryStore; -//! # use std::io::Read; -//! let blob = Blob::new(b"my content".to_vec()).unwrap(); -//! -//! let mut encoded = Vec::new(); -//! BinaryEncoder.encode_blob(&blob, &mut encoded).unwrap(); -//! -//! let hash = Sha512Hasher.hash(&mut encoded.as_slice()).unwrap(); -//! -//! let mut store = MemoryStore::new(); -//! store.put(&hash, &encoded).unwrap(); -//! -//! let mut reader = store.get(&hash).unwrap(); -//! let mut decoded = Vec::new(); -//! reader.read_to_end(&mut decoded).unwrap(); -//! -//! assert_eq!(decoded, encoded); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[cfg(test)] use proptest as _; -/// Binary codec for encoding and decoding objects. -/// -/// This module contains the reference binary serialization format. The -/// encoder and decoder are separated to isolate trusted production of bytes -/// from untrusted parsing. See [`crate::codec`] for the module-level details. + + + + + pub mod codec; -/// Hashing algorithms. -/// -/// This module bridges the pure SHA-512 implementation from -/// `libvctrl_sha512` to the [`Hasher`](libvctrl_handler::Hasher) trait. -/// The result is a content-addressing primitive that produces 64-byte hashes -/// matching `libvctrl_handler::HASH_LENGTH`. + + + + + + pub mod hash; -/// Object builders for ergonomic construction. -/// -/// These builders provide fluent APIs for creating blobs, commits, tags, -/// trees, and tree entries. They defer validation until the final build step, -/// allowing fields to be supplied in any order while keeping the resulting -/// objects immutable and validated. + + + + + + pub mod object; -/// In-memory object and reference stores. -/// -/// These stores implement the [`ObjectStore`](libvctrl_handler::ObjectStore) -/// and [`RefStore`](libvctrl_handler::RefStore) contracts using -/// [`std::collections::HashMap`]. They are ideal for tests, prototypes, and -/// short-lived embedded use cases. + + + + + + pub mod store; diff --git a/libvctrl_core/src/object/blob.rs b/libvctrl_core/src/object/blob.rs index 1d1e6222..e517c5fe 100644 --- a/libvctrl_core/src/object/blob.rs +++ b/libvctrl_core/src/object/blob.rs @@ -1,120 +1,120 @@ -//! # 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) } diff --git a/libvctrl_core/src/object/commit.rs b/libvctrl_core/src/object/commit.rs index 7f7867e6..e99b8927 100644 --- a/libvctrl_core/src/object/commit.rs +++ b/libvctrl_core/src/object/commit.rs @@ -1,82 +1,82 @@ -//! Builder for constructing [`Commit`] objects with a fluent, type-safe API. -//! -//! # Why this module exists -//! -//! A [`Commit`] aggregates several mandatory pieces of metadata: a tree hash, -//! one or more parent hashes, author and committer identities, a message, and -//! optional metadata such as timestamp and encoding. Direct construction would -//! force every caller to provide all fields at once, even when they are built -//! incrementally or derived from different sources. The builder pattern solves -//! this by separating field assignment from final validation. -//! -//! # How it works -//! -//! The builder stores each field as an `Option` (or a `Vec` for parents) and -//! consumes `self` on every setter, returning `Self`. This ensures that each -//! setter is used exactly once in a chain and that the builder cannot be reused -//! after partial construction. The final [`build`](CommitBuilder::build) -//! method extracts all required fields, reports a descriptive [`VctrlError`] -//! if any are missing, and delegates to either [`Commit::with_meta`] or -//! [`Commit::new`] depending on whether metadata was supplied. -//! -//! # Examples -//! -//! ``` -//! use libvctrl_core::object::CommitBuilder; -//! use libvctrl_handler::{Hash, UserID}; -//! -//! let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); -//! let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); -//! let committer = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); -//! -//! let commit = CommitBuilder::new() -//! .tree(tree) -//! .author(author) -//! .committer(committer) -//! .message("Initial commit") -//! .build() -//! .unwrap(); -//! -//! assert_eq!(commit.message(), "Initial commit"); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{Commit, CommitMeta, Hash, UserID, VctrlError}; -/// A builder for creating [`Commit`] objects. -/// -/// # Design rationale -/// -/// This type follows the *consuming builder* pattern. Each setter takes `self` -/// by value and returns `Self`, which makes the builder single-use and prevents -/// accidental reuse of a partially configured builder. Fields are stored -/// internally as `Option` (or a `Vec` for parents) because the builder must -/// remain `Default` while allowing the final [`build`](CommitBuilder::build) -/// to distinguish between “not provided” and “explicitly set to `None`”. -/// -/// The struct is `#[derive(Default)]` so that callers may start from -/// `CommitBuilder::default()` if they prefer, but the explicit -/// [`new`](CommitBuilder::new) constructor is provided for clarity. -/// -/// # Examples -/// -/// Basic construction with all required fields: -/// -/// ``` -/// # use libvctrl_core::object::CommitBuilder; -/// # use libvctrl_handler::{Hash, UserID}; -/// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); -/// # let author = UserID::new("A".into(), "a@b.c".into()).unwrap(); -/// # let committer = author.clone(); -/// let commit = CommitBuilder::new() -/// .tree(tree) -/// .author(author) -/// .committer(committer) -/// .message("Initial commit") -/// .build() -/// .unwrap(); -/// -/// assert!(commit.parents().is_empty()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default)] pub struct CommitBuilder { tree: Option, @@ -88,25 +88,25 @@ pub struct CommitBuilder { } impl CommitBuilder { - /// Creates a new `CommitBuilder` with no fields set. - /// - /// # Why this is `const` - /// - /// Marking the constructor as `const fn` allows the builder to be created - /// in constant contexts and gives the compiler more opportunities for - /// compile-time evaluation. The returned builder is a plain value on the - /// stack with all `Option` fields set to `None` and the `parents` vector - /// empty; no heap allocation occurs until the first `parent` call or - /// message assignment. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// let builder = CommitBuilder::new(); - /// // builder is empty; calling build() now would fail with a missing-field error - /// assert!(builder.build().is_err()); - /// ``` + + + + + + + + + + + + + + + + + + + #[must_use] pub const fn new() -> Self { Self { @@ -119,184 +119,184 @@ impl CommitBuilder { } } - /// Sets the tree hash for the commit. - /// - /// The tree hash points to the root tree object that represents the - /// snapshot of the project at the time of the commit. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::Hash; - /// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); - /// let builder = CommitBuilder::new().tree(tree); - /// assert!(builder.build().is_err()); // other fields still missing - /// ``` + + + + + + + + + + + + + + #[must_use] pub const fn tree(mut self, tree: Hash) -> Self { self.tree = Some(tree); self } - /// Adds a parent commit hash. - /// - /// This method may be called multiple times to create a commit with - /// multiple parents (e.g., a merge commit). Parents are stored in the - /// order they are added, preserving the caller’s intended ordering for - /// serialization. - /// - /// # Examples - /// - /// Adding two parents: - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::Hash; - /// # let parent1 = Hash::from_bytes(&[1u8; 64]).unwrap(); - /// # let parent2 = Hash::from_bytes(&[2u8; 64]).unwrap(); - /// let builder = CommitBuilder::new() - /// .parent(parent1) - /// .parent(parent2); - /// // Use builder further or build after setting other fields - /// ``` + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn parent(mut self, parent: Hash) -> Self { self.parents.push(parent); self } - /// Sets the author of the commit. - /// - /// The author is the person who originally wrote the changes, which may - /// differ from the committer (for example, when applying a patch). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::UserID; - /// # let author = UserID::new("Alice".into(), "alice@example.com".into()).unwrap(); - /// let builder = CommitBuilder::new().author(author); - /// assert!(builder.build().is_err()); // tree and committer still missing - /// ``` + + + + + + + + + + + + + + #[must_use] pub fn author(mut self, author: UserID) -> Self { self.author = Some(author); self } - /// Sets the committer of the commit. - /// - /// The committer is the person who created the commit object. In simple - /// workflows the author and committer are identical, but they are kept - /// separate to preserve Git’s distinction. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::UserID; - /// # let committer = UserID::new("Bob".into(), "bob@example.com".into()).unwrap(); - /// let builder = CommitBuilder::new().committer(committer); - /// assert!(builder.build().is_err()); // tree and author still missing - /// ``` + + + + + + + + + + + + + + + #[must_use] pub fn committer(mut self, committer: UserID) -> Self { self.committer = Some(committer); self } - /// Sets the commit message. - /// - /// The method accepts any type that implements `Into`, including - /// `&str`, `String`, and `Cow`, making call sites ergonomic. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// let builder = CommitBuilder::new().message("Initial commit"); - /// // The message is stored internally as a String. - /// assert!(builder.build().is_err()); // other required fields missing - /// ``` + + + + + + + + + + + + + #[must_use] pub fn message(mut self, msg: impl Into) -> Self { self.message = Some(msg.into()); self } - /// Sets the optional commit metadata. - /// - /// Metadata includes the timestamp, timezone offset, and optional character - /// encoding. If this method is not called, [`build`](CommitBuilder::build) - /// delegates to [`Commit::new`], which uses default metadata. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::CommitMeta; - /// # let meta = CommitMeta::new(1_700_000_000, 0, None).unwrap(); - /// let builder = CommitBuilder::new().meta(meta); - /// assert!(builder.build().is_err()); // other required fields missing - /// ``` + + + + + + + + + + + + + + + #[must_use] pub fn meta(mut self, meta: CommitMeta) -> Self { self.meta = Some(meta); self } - /// Builds the [`Commit`] object after validating all required fields. - /// - /// # How it works - /// - /// The method checks the four mandatory fields (`tree`, `author`, - /// `committer`, and `message`) in order. If any is missing, it returns a - /// [`VctrlError::Other`] with a descriptive message and does not allocate - /// a commit. If all mandatory fields are present, it constructs the - /// [`Commit`] by calling [`Commit::with_meta`] when metadata was supplied, - /// or [`Commit::new`] otherwise. - /// - /// # Errors - /// - /// Returns [`VctrlError::Other`] if any of the required fields is missing: - /// - `tree` - /// - `author` - /// - `committer` - /// - `message` - /// - /// Also returns any [`VctrlError`] produced by the underlying - /// [`Commit::new`] or [`Commit::with_meta`] validation. - /// - /// # Examples - /// - /// Successful build: - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// # use libvctrl_handler::{Hash, UserID}; - /// # let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); - /// # let author = UserID::new("Alice".into(), "alice@example.com".into()).unwrap(); - /// # let committer = UserID::new("Bob".into(), "bob@example.com".into()).unwrap(); - /// let commit = CommitBuilder::new() - /// .tree(tree) - /// .author(author) - /// .committer(committer) - /// .message("Initial commit") - /// .build() - /// .unwrap(); - /// - /// assert_eq!(commit.message(), "Initial commit"); - /// ``` - /// - /// Missing field error: - /// - /// ``` - /// # use libvctrl_core::object::CommitBuilder; - /// let result = CommitBuilder::new().build(); - /// assert!(result.is_err()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn build(self) -> Result { let tree = self .tree diff --git a/libvctrl_core/src/object/mod.rs b/libvctrl_core/src/object/mod.rs index 13e0941d..473c2c11 100644 --- a/libvctrl_core/src/object/mod.rs +++ b/libvctrl_core/src/object/mod.rs @@ -1,96 +1,96 @@ -//! Object builders for ergonomic construction of Git objects. -//! -//! # Why this module exists -//! -//! The data types in [`libvctrl_handler`] are immutable and enforce their own -//! invariants through constructors such as -//! [`Commit::new`](libvctrl_handler::Commit::new). While those constructors -//! are safe and correct, they often require every field to be supplied at once. -//! In real applications, fields may arrive gradually from parsing, user input, -//! or configuration. The builder pattern separates gradual assembly from final -//! validation. -//! -//! Each builder in this module consumes `self` on every setter, returns `Self`, -//! and exposes a single `build` method that performs validation and constructs -//! the final object. This design prevents partially configured builders from -//! being used accidentally after construction, while still allowing fluent -//! chains. -//! -//! # Module organization -//! -//! The module mirrors the object type hierarchy: -//! -//! - [`blob`] contains [`BlobBuilder`] for [`Blob`](libvctrl_handler::Blob). -//! - [`tree`] contains [`TreeBuilder`] and [`TreeEntryBuilder`] for -//! [`Tree`](libvctrl_handler::Tree) and -//! [`TreeEntry`](libvctrl_handler::TreeEntry). -//! - [`commit`] contains [`CommitBuilder`] for -//! [`Commit`](libvctrl_handler::Commit). -//! - [`tag`] contains [`TagBuilder`] for [`Tag`](libvctrl_handler::Tag). -//! -//! All builders are re-exported at this module level so callers can use -//! `libvctrl_core::object::CommitBuilder` instead of the longer submodule path. -//! -//! # Examples -//! -//! Construct a commit using the builder: -//! -//! ``` -//! use libvctrl_core::object::CommitBuilder; -//! use libvctrl_handler::{Hash, UserID}; -//! -//! let tree = Hash::from_bytes(&[0u8; 64]).unwrap(); -//! let author = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); -//! let committer = author.clone(); -//! -//! let commit = CommitBuilder::new() -//! .tree(tree) -//! .author(author) -//! .committer(committer) -//! .message("Initial commit") -//! .build() -//! .unwrap(); -//! -//! assert_eq!(commit.message(), "Initial commit"); -//! ``` - -/// Blob builder. -/// -/// This submodule contains [`BlobBuilder`], a builder for constructing -/// [`Blob`](libvctrl_handler::Blob) objects from arbitrary byte data. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod blob; -/// Commit builder. -/// -/// This submodule contains [`CommitBuilder`], a builder for constructing -/// [`Commit`](libvctrl_handler::Commit) objects with tree, parents, author, -/// committer, message, and optional metadata. + + + + + pub mod commit; -/// Tag builder. -/// -/// This submodule contains [`TagBuilder`], a builder for constructing -/// [`Tag`](libvctrl_handler::Tag) objects with a name, target hash, optional -/// tagger, message, and optional metadata. + + + + + pub mod tag; -/// Tree builder. -/// -/// This submodule contains [`TreeBuilder`] and [`TreeEntryBuilder`], builders -/// for constructing [`Tree`](libvctrl_handler::Tree) and -/// [`TreeEntry`](libvctrl_handler::TreeEntry) objects with sorted entries and -/// entry kinds. + + + + + + pub mod tree; -/// Re-export of [`BlobBuilder`] for convenient access at the module root. + pub use blob::BlobBuilder; -/// Re-export of [`CommitBuilder`] for convenient access at the module root. + pub use commit::CommitBuilder; -/// Re-export of [`TagBuilder`] for convenient access at the module root. + pub use tag::TagBuilder; -/// Re-export of [`TreeBuilder`] and [`TreeEntryBuilder`] for convenient access -/// at the module root. + + pub use tree::{TreeBuilder, TreeEntryBuilder}; diff --git a/libvctrl_core/src/object/tag.rs b/libvctrl_core/src/object/tag.rs index 0950a424..2ff04b2a 100644 --- a/libvctrl_core/src/object/tag.rs +++ b/libvctrl_core/src/object/tag.rs @@ -1,76 +1,76 @@ -//! # Tag Builder -//! -//! This module provides a fluent, ownership-driven builder for constructing -//! [`Tag`] objects. The builder pattern is used because a [`Tag`] is an -//! immutable value object with several fields, some mandatory and some -//! optional. The builder allows setting each field separately and defers -//! validation and object creation to the final `build()` call. + + + + + + + use libvctrl_handler::{CommitMeta, Hash, Tag, UserID, VctrlError}; -/// A builder for creating [`Tag`] objects. -/// -/// `TagBuilder` provides a safe, ergonomic way to construct a [`Tag`] by -/// setting fields individually. The builder consumes itself with each method -/// and returns a new builder state, enabling method chaining. The final -/// `build()` call validates required fields and constructs the [`Tag`]. -/// -/// # Why this struct exists -/// -/// The [`Tag`] constructor may fail if required fields are missing or -/// validation fails. A builder delays those operations, allowing callers to -/// supply fields in any order and to provide optional values only when -/// necessary. It also gives a uniform construction API across all object -/// types in this crate. -/// -/// # How it works -/// -/// The builder stores each field in an `Option`. Required fields (`name`, -/// `target`) must be set before `build()`; otherwise `build()` returns a -/// [`VctrlError::Other`] describing the missing field. Optional fields -/// (`tagger`, `message`, `meta`) default to `None` (or an empty string for -/// message). `build()` consumes the builder and moves the values into the new -/// [`Tag`]. -/// -/// # Examples -/// -/// Basic construction with a tagger: -/// -/// ``` -/// # use libvctrl_core::object::TagBuilder; -/// # use libvctrl_handler::{Hash, UserID}; -/// let target = Hash::from_bytes(&[0u8; 64]).unwrap(); -/// let tagger = UserID::new("Alice".to_owned(), "alice@example.com".to_owned()).unwrap(); -/// -/// let tag = TagBuilder::new() -/// .name("v1.0.0") -/// .target(target) -/// .tagger(tagger) -/// .message("Release 1.0") -/// .build() -/// .unwrap(); -/// -/// assert_eq!(tag.name(), "v1.0.0"); -/// assert!(tag.tagger().is_some()); -/// assert_eq!(tag.message(), "Release 1.0"); -/// ``` -/// -/// Building without a tagger: -/// -/// ``` -/// # use libvctrl_core::object::TagBuilder; -/// # use libvctrl_handler::Hash; -/// let target = Hash::from_bytes(&[1u8; 64]).unwrap(); -/// -/// let tag = TagBuilder::new() -/// .name("v2.0.0") -/// .target(target) -/// .build() -/// .unwrap(); -/// -/// assert_eq!(tag.name(), "v2.0.0"); -/// assert!(tag.tagger().is_none()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default)] pub struct TagBuilder { name: Option, @@ -81,19 +81,19 @@ pub struct TagBuilder { } impl TagBuilder { - /// Creates a new `TagBuilder` with all fields unset. - /// - /// The builder is initially empty. Use the setter methods to populate - /// fields, then call [`build`](Self::build) to produce a [`Tag`]. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// let builder = TagBuilder::new(); - /// // The builder can be consumed by chaining setters: - /// let _ = builder.name("v0.0.0"); // Example only; typically followed by target() - /// ``` + + + + + + + + + + + + + #[must_use] pub const fn new() -> Self { Self { @@ -105,185 +105,185 @@ impl TagBuilder { } } - /// Sets the tag name. - /// - /// This method consumes the builder and returns a new builder with `name` - /// set. The name must be a non-empty string and is validated during - /// [`build`](Self::build). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::Hash; - /// let target = Hash::from_bytes(&[2u8; 64]).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.2.3") - /// .target(target) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tag.name(), "v1.2.3"); - /// ``` + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn name(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } - /// Sets the target hash. - /// - /// This method consumes the builder and returns a new builder with - /// `target` set. The target must point to another object (usually a commit - /// or tree) and is validated during [`build`](Self::build). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::Hash; - /// let target = Hash::from_bytes(&[3u8; 64]).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.0.0") - /// .target(target) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tag.target(), &target); - /// ``` + + + + + + + + + + + + + + + + + + + + + #[must_use] pub const fn target(mut self, target: Hash) -> Self { self.target = Some(target); self } - /// Sets the tagger. - /// - /// This method consumes the builder and returns a new builder with - /// `tagger` set. The tagger is optional; omit this method to create an - /// unsigned tag. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::{Hash, UserID}; - /// let target = Hash::from_bytes(&[4u8; 64]).unwrap(); - /// let tagger = UserID::new("Bob".to_owned(), "bob@example.com".to_owned()).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.0.0") - /// .target(target) - /// .tagger(tagger) - /// .build() - /// .unwrap(); - /// - /// assert!(tag.tagger().is_some()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn tagger(mut self, tagger: UserID) -> Self { self.tagger = Some(tagger); self } - /// Sets the tag message. - /// - /// This method consumes the builder and returns a new builder with - /// `message` set. The message is optional and defaults to an empty string - /// if not set. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::Hash; - /// let target = Hash::from_bytes(&[5u8; 64]).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.0.0") - /// .target(target) - /// .message("Annotated tag") - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tag.message(), "Annotated tag"); - /// ``` + + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn message(mut self, msg: impl Into) -> Self { self.message = Some(msg.into()); self } - /// Sets the tag metadata. - /// - /// This method consumes the builder and returns a new builder with `meta` - /// set. Metadata includes timestamp, timezone offset, and optional - /// encoding. If omitted, the [`Tag`] is created without metadata. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::{CommitMeta, Hash}; - /// let target = Hash::from_bytes(&[6u8; 64]).unwrap(); - /// let meta = CommitMeta::new(1_700_000_000, 0, None).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.0.0") - /// .target(target) - /// .meta(meta) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tag.meta().timestamp(), 1_700_000_000); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn meta(mut self, meta: CommitMeta) -> Self { self.meta = Some(meta); self } - /// Builds the [`Tag`]. - /// - /// This consumes the builder, moves all fields into the new [`Tag`], and - /// performs validation. Required fields (`name` and `target`) must be set; - /// otherwise an error is returned. - /// - /// # Errors - /// - /// Returns [`VctrlError::Other`] if `name` or `target` is missing. - /// If metadata is present, validation errors from - /// [`Tag::with_meta`](libvctrl_handler::Tag::with_meta) may also be - /// returned. Similarly, if metadata is absent, errors from - /// [`Tag::new`](libvctrl_handler::Tag::new) are propagated. - /// - /// # Examples - /// - /// Successful build: - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// # use libvctrl_handler::Hash; - /// let target = Hash::from_bytes(&[7u8; 64]).unwrap(); - /// - /// let tag = TagBuilder::new() - /// .name("v1.0.0") - /// .target(target) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tag.name(), "v1.0.0"); - /// ``` - /// - /// Missing required field: - /// - /// ``` - /// # use libvctrl_core::object::TagBuilder; - /// let result = TagBuilder::new().name("v1.0.0").build(); - /// assert!(result.is_err()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn build(self) -> Result { let name = self .name diff --git a/libvctrl_core/src/object/tree.rs b/libvctrl_core/src/object/tree.rs index 6e9e8a13..a83cfc35 100644 --- a/libvctrl_core/src/object/tree.rs +++ b/libvctrl_core/src/object/tree.rs @@ -1,78 +1,78 @@ -//! # Tree Builders -//! -//! This module provides ergonomic builders for constructing [`Tree`] and -//! [`TreeEntry`] objects. -//! -//! A [`Tree`] is a sorted collection of entries. The invariant is enforced by -//! [`Tree::new`], which rejects unsorted or duplicate entry names. These -//! builders defer that validation to the final `build()` step, allowing -//! callers to assemble entries incrementally. -//! -//! The module exposes two builder types: -//! -//! - [`TreeBuilder`] for building a full tree from individual entries. -//! - [`TreeEntryBuilder`] for building a single entry. + + + + + + + + + + + + + + use libvctrl_handler::{EntryKind, Hash, Tree, TreeEntry, VctrlError}; -/// A builder for creating [`Tree`] objects. -/// -/// `TreeBuilder` accumulates [`TreeEntry`] values and produces a validated -/// [`Tree`] when [`build`](Self::build) is called. -/// -/// # Why this struct exists -/// -/// A [`Tree`] requires its entries to be sorted and free of duplicates. If -/// callers constructed a [`Tree`] directly and supplied entries one by one, -/// they would need to sort and validate manually. This builder centralizes -/// that concern and provides a chainable API. -/// -/// # How it works -/// -/// The builder stores entries in an internal `Vec`. The `entry` and -/// `add_entry` methods push entries without performing any ordering checks. -/// Validation occurs only when [`build`](Self::build) consumes the builder and -/// calls [`Tree::new`], which enforces the ordering invariant. -/// -/// # Examples -/// -/// Building a tree with two sorted entries: -/// -/// ``` -/// # use libvctrl_core::object::TreeBuilder; -/// # use libvctrl_handler::{EntryKind, Hash}; -/// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); -/// -/// let tree = TreeBuilder::new() -/// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) -/// .unwrap() -/// .add_entry("b.txt".to_owned(), EntryKind::Blob, hash) -/// .unwrap() -/// .build() -/// .unwrap(); -/// -/// assert_eq!(tree.entries().len(), 2); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default)] pub struct TreeBuilder { entries: Vec, } impl TreeBuilder { - /// Creates a new `TreeBuilder` with no entries. - /// - /// The builder is initially empty. Use [`entry`](Self::entry) or - /// [`add_entry`](Self::add_entry) to add entries, then call - /// [`build`](Self::build) to construct the [`Tree`]. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TreeBuilder; - /// let builder = TreeBuilder::new(); - /// let tree = builder.build().unwrap(); - /// assert!(tree.entries().is_empty()); - /// ``` + + + + + + + + + + + + + + #[must_use] pub const fn new() -> Self { Self { @@ -80,75 +80,75 @@ impl TreeBuilder { } } - /// Adds an existing [`TreeEntry`]. - /// - /// This method consumes the builder and returns a new builder with the - /// given entry appended. No validation is performed at this point. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::{TreeBuilder, TreeEntryBuilder}; - /// # use libvctrl_handler::{EntryKind, Hash}; - /// let hash = Hash::from_bytes(&[1u8; 64]).unwrap(); - /// let entry = TreeEntryBuilder::new("file.txt".to_owned(), EntryKind::Blob, hash) - /// .build() - /// .unwrap(); - /// - /// let tree = TreeBuilder::new() - /// .entry(entry) - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tree.entries().len(), 1); - /// ``` + + + + + + + + + + + + + + + + + + + + + + #[must_use] pub fn entry(mut self, entry: TreeEntry) -> Self { self.entries.push(entry); self } - /// Creates and adds a new [`TreeEntry`]. - /// - /// This method consumes the builder, constructs a [`TreeEntry`] using - /// [`TreeEntry::new`], appends it, and returns the updated builder. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the entry name is invalid according to - /// [`TreeEntry::new`]. No ordering validation is performed here; it is - /// deferred to [`build`](Self::build). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::object::TreeBuilder; - /// # use libvctrl_handler::{EntryKind, Hash}; - /// let hash = Hash::from_bytes(&[2u8; 64]).unwrap(); - /// - /// let builder = TreeBuilder::new() - /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) - /// .unwrap(); - /// - /// let tree = builder.build().unwrap(); - /// assert_eq!(tree.len(), 1); - /// # Ok::<(), libvctrl_handler::VctrlError>(()) - /// ``` - /// - /// This example uses `?` inside a function returning `Result`: - /// - /// ``` - /// # use libvctrl_core::object::TreeBuilder; - /// # use libvctrl_handler::{EntryKind, Hash, VctrlError}; - /// # fn example() -> Result<(), VctrlError> { - /// let hash = Hash::from_bytes(&[3u8; 64])?; - /// let tree = TreeBuilder::new() - /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash)? - /// .build()?; - /// assert_eq!(tree.entries().len(), 1); - /// # Ok(()) - /// # } - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn add_entry( mut self, name: String, @@ -160,76 +160,76 @@ impl TreeBuilder { Ok(self) } - /// Builds the [`Tree`]. - /// - /// Consumes the builder, moves all entries into the new [`Tree`], and - /// validates the ordering invariant. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the entries are not sorted lexicographically - /// by name or if duplicate names exist. The exact variant depends on the - /// `libvctrl_handler` implementation. - /// - /// # Examples - /// - /// Successful build: - /// - /// ``` - /// # use libvctrl_core::object::TreeBuilder; - /// # use libvctrl_handler::{EntryKind, Hash}; - /// let hash = Hash::from_bytes(&[4u8; 64]).unwrap(); - /// - /// let tree = TreeBuilder::new() - /// .add_entry("a.txt".to_owned(), EntryKind::Blob, hash) - /// .unwrap() - /// .add_entry("b.txt".to_owned(), EntryKind::Blob, hash) - /// .unwrap() - /// .build() - /// .unwrap(); - /// - /// assert_eq!(tree.entries().len(), 2); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn build(self) -> Result { Tree::new(self.entries) } } -/// A builder for creating [`TreeEntry`] objects. -/// -/// `TreeEntryBuilder` holds the fields required to construct a [`TreeEntry`]: -/// name, kind, and hash. It performs validation only when -/// [`build`](Self::build) is called. -/// -/// # Why this struct exists -/// -/// [`TreeEntry::new`] can fail if the name is invalid. This builder gives -/// callers an explicit place to defer that error while keeping construction -/// straightforward. It is particularly useful when entries are generated or -/// configured dynamically. -/// -/// # How it works -/// -/// The builder stores the three fields by value. `build` moves them into -/// [`TreeEntry::new`] and returns the result, consuming the builder. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_core::object::TreeEntryBuilder; -/// # use libvctrl_handler::{EntryKind, Hash}; -/// let hash = Hash::from_bytes(&[5u8; 64]).unwrap(); -/// let entry = TreeEntryBuilder::new( -/// "file.txt".to_owned(), -/// EntryKind::Blob, -/// hash, -/// ) -/// .build() -/// .unwrap(); -/// -/// assert_eq!(entry.name(), "file.txt"); -/// assert_eq!(entry.kind(), EntryKind::Blob); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug)] pub struct TreeEntryBuilder { name: String, @@ -238,57 +238,57 @@ 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) } diff --git a/libvctrl_core/src/store/memory.rs b/libvctrl_core/src/store/memory.rs index 47fefa14..8abe85ce 100644 --- a/libvctrl_core/src/store/memory.rs +++ b/libvctrl_core/src/store/memory.rs @@ -1,104 +1,104 @@ -//! In-memory [`ObjectStore`] implementation backed by a [`HashMap`]. -//! -//! # Why this module exists -//! -//! The [`MemoryStore`] type provides a lightweight, ephemeral storage backend -//! for version-control objects. It implements the [`ObjectStore`] contract -//! without requiring disk I/O, network access, or persistent state. This makes -//! it ideal for: -//! -//! - Unit tests that need an isolated object database. -//! - Caching and temporary storage. -//! - Embedded or ephemeral applications where persistence is not desired. -//! -//! # How it works -//! -//! Objects are stored as raw byte vectors (`Vec`) keyed by their content -//! hash ([`Hash`]). The use of a [`HashMap`] gives average O(1) lookup, -//! insertion, and deletion. The raw bytes are not parsed or validated on -//! insertion; validation is the responsibility of higher layers. This keeps -//! the store fast and agnostic to object type. -//! -//! The [`get`](MemoryStore::get) method returns a -//! `Box` rather than a `Vec` to support streaming -//! reads of large objects without forcing the entire object into a contiguous -//! buffer. Internally, it wraps the stored slice in a [`Cursor`]. -//! -//! # Examples -//! -//! Store and retrieve an object: -//! -//! ``` -//! use libvctrl_core::store::MemoryStore; -//! use libvctrl_handler::{Hash, ObjectStore}; -//! use std::io::Read; -//! -//! let mut store = MemoryStore::new(); -//! let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); -//! -//! store.put(&hash, b"hello world").unwrap(); -//! -//! let mut reader = store.get(&hash).unwrap(); -//! let mut buf = Vec::new(); -//! reader.read_to_end(&mut buf).unwrap(); -//! assert_eq!(buf, b"hello world"); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{Hash, ObjectStore, VctrlError}; use std::collections::HashMap; use std::io::{Cursor, Read}; -/// An in-memory implementation of [`ObjectStore`]. -/// -/// # Design rationale -/// -/// The struct uses a [`HashMap>`] as its sole storage. This -/// choice provides: -/// -/// - **Fast average O(1) access** — hashing is performed by the [`Hash`] key. -/// - **No parsing overhead** — objects are stored as opaque byte sequences. -/// - **Simple ownership model** — the map owns both keys and values, so the -/// store can be dropped without manual cleanup. -/// -/// The type derives [`Default`], allowing `MemoryStore::default()` to create a -/// new empty store without requiring a custom constructor. However, an explicit -/// [`new`](MemoryStore::new) is still provided for symmetry with other store -/// implementations. -/// -/// # Examples -/// -/// Create an empty store and verify it is initially empty: -/// -/// ``` -/// # use libvctrl_core::store::MemoryStore; -/// # use libvctrl_handler::{Hash, ObjectStore}; -/// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); -/// let store = MemoryStore::new(); -/// assert!(!store.exists(&hash).unwrap()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default)] pub struct MemoryStore { objects: HashMap>, } impl MemoryStore { - /// Creates a new empty `MemoryStore`. - /// - /// # Why this is `const` - /// - /// The constructor is a `const fn` because constructing an empty - /// [`HashMap`] does not require any runtime heap allocation. The map is - /// allocated lazily on the first insertion. This allows the store to be - /// created in constant contexts and enables potential compile-time - /// evaluation by the compiler. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::store::MemoryStore; - /// let store = MemoryStore::new(); - /// // store is ready to use, but contains no objects - /// ``` + + + + + + + + + + + + + + + + + #[must_use] pub fn new() -> Self { Self { @@ -108,65 +108,65 @@ impl MemoryStore { } impl ObjectStore for MemoryStore { - /// Stores an object under the given hash. - /// - /// # How it works - /// - /// The method copies the provided byte slice into a new `Vec` and - /// inserts it into the internal [`HashMap`]. If an object with the same - /// hash already exists, the old value is silently replaced. The method - /// always returns `Ok(())` because an in-memory map has no failure modes - /// under normal conditions (excluding allocation failure, which panics). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_core::store::MemoryStore; - /// # use libvctrl_handler::{Hash, ObjectStore}; - /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); - /// let mut store = MemoryStore::new(); - /// store.put(&hash, b"data").unwrap(); - /// assert!(store.exists(&hash).unwrap()); - /// ``` + + + + + + + + + + + + + + + + + + + + fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { let _ = self.objects.insert(*hash, data.to_vec()); Ok(()) } - /// Retrieves an object as a streaming reader. - /// - /// # Design rationale - /// - /// Returning `Box` instead of `Vec` allows - /// callers to consume large objects incrementally. The lifetime `'_` is - /// tied to `&self`, enabling the returned reader to borrow the stored bytes - /// without cloning the entire object. - /// - /// Internally, the stored slice is wrapped in a [`Cursor`], which - /// implements both [`Read`] and [`Send`]. - /// - /// # Errors - /// - /// Returns [`VctrlError::ObjectNotFound`] if no object with the given hash - /// exists in the store. - /// - /// # Examples - /// - /// Read back a stored object: - /// - /// ``` - /// # use libvctrl_core::store::MemoryStore; - /// # use libvctrl_handler::{Hash, ObjectStore}; - /// # use std::io::Read; - /// # let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); - /// let mut store = MemoryStore::new(); - /// store.put(&hash, b"hello").unwrap(); - /// - /// let mut reader = store.get(&hash).unwrap(); - /// let mut buf = Vec::new(); - /// reader.read_to_end(&mut buf).unwrap(); - /// assert_eq!(buf, b"hello"); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get(&self, hash: &Hash) -> Result, VctrlError> { let data = self .objects @@ -175,52 +175,52 @@ 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)) } diff --git a/libvctrl_core/src/store/mod.rs b/libvctrl_core/src/store/mod.rs index 0a6e1d7c..d450243a 100644 --- a/libvctrl_core/src/store/mod.rs +++ b/libvctrl_core/src/store/mod.rs @@ -1,70 +1,70 @@ -//! # In-Memory Stores -//! -//! This module provides ephemeral, in-memory implementations of the core -//! storage contracts defined in `libvctrl_handler`: -//! -//! - [`MemoryStore`] implements [`ObjectStore`](libvctrl_handler::ObjectStore) -//! for storing and retrieving raw object bytes. -//! - [`MemoryRefStore`] implements [`RefStore`](libvctrl_handler::RefStore) -//! for managing named references such as branches and tags. -//! -//! ## Why this module exists -//! -//! Version control backends must persist objects and references. However, -//! persistent storage requires platform-specific I/O and error handling. The -//! in-memory implementations decouple core VCS logic from those concerns. -//! They serve as: -//! -//! - Reference implementations for the traits. -//! - Test doubles for unit and integration tests. -//! - Backends for short-lived or embedded scenarios. -//! -//! ## How it works -//! -//! Both stores use [`std::collections::HashMap`] under the hood. -//! -//! - [`MemoryStore`] maps a [`Hash`] to raw encoded bytes (`Vec`). -//! - [`MemoryRefStore`] maps a reference name (`String`) to a [`Hash`]. -//! -//! Lookups are O(1) on average. The reference store sorts names before -//! returning them from [`list_refs`](libvctrl_handler::RefStore::list_refs) to -//! provide deterministic iteration. -//! -//! ## Examples -//! -//! The following example shows how the two stores can be used together: an -//! object is placed into [`MemoryStore`], and a reference pointing to it is -//! stored in [`MemoryRefStore`]. -//! -//! ``` -//! # use libvctrl_handler::{Hash, ObjectStore, RefStore}; -//! # use libvctrl_core::store::{MemoryStore, MemoryRefStore}; -//! let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); -//! -//! let mut object_store = MemoryStore::new(); -//! object_store.put(&hash, b"encoded object bytes").unwrap(); -//! -//! let mut ref_store = MemoryRefStore::new(); -//! ref_store.set_ref("refs/heads/main", &hash).unwrap(); -//! -//! assert!(object_store.exists(&hash).unwrap()); -//! assert_eq!(ref_store.get_ref("refs/heads/main").unwrap(), hash); -//! ``` - -/// In-memory object store. -/// -/// This submodule contains [`MemoryStore`](self::MemoryStore), a -/// [`HashMap`]-backed implementation of -/// [`ObjectStore`](libvctrl_handler::ObjectStore). It stores raw object bytes -/// and is suitable for testing and ephemeral storage. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod memory; -/// In-memory reference store. -/// -/// This submodule contains [`MemoryRefStore`](self::MemoryRefStore), a -/// [`HashMap`]-backed implementation of -/// [`RefStore`](libvctrl_handler::RefStore). It manages named references and -/// returns sorted reference names. + + + + + + pub mod ref_store; pub use memory::MemoryStore; diff --git a/libvctrl_core/src/store/ref_store.rs b/libvctrl_core/src/store/ref_store.rs index 2998e60b..f467a57b 100644 --- a/libvctrl_core/src/store/ref_store.rs +++ b/libvctrl_core/src/store/ref_store.rs @@ -1,78 +1,78 @@ -//! # In-Memory Reference Store -//! -//! This module provides [`MemoryRefStore`], a lightweight implementation of the -//! [`RefStore`](libvctrl_handler::RefStore) trait backed by a -//! [`std::collections::HashMap`]. -//! -//! The store is intended for testing, prototyping, and scenarios where -//! persistence is not required. It stores references in memory only and loses -//! all data when dropped. -//! -//! ## Why this exists -//! -//! The [`RefStore`](libvctrl_handler::RefStore) trait defines the contract for -//! managing named references such as branches and tags. A concrete in-memory -//! implementation is essential for unit tests, examples, and as a reference -//! backend. It also demonstrates the expected behavior of the trait without -//! any disk or network dependencies. -//! -//! ## How it works -//! -//! References are stored in a private `HashMap`. The `set_ref` -//! method validates the reference name using -//! [`validate_ref_name`](libvctrl_handler::validate_ref_name) before inserting. -//! The `list_refs` method collects and sorts all keys to provide deterministic -//! iteration order. + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl_handler::{Hash, RefStore, VctrlError}; use std::collections::HashMap; -/// An in-memory implementation of [`RefStore`]. -/// -/// `MemoryRefStore` stores named references such as branches and tags in a -/// `HashMap`. It is suitable for ephemeral use cases and testing. -/// -/// # Why this struct exists -/// -/// The [`RefStore`] trait requires an implementation to be useful. This struct -/// provides a minimal, safe, and deterministic reference store that can be -/// embedded in applications or used as a baseline for tests. -/// -/// # How it works -/// -/// Internally, references are keyed by name and mapped to their target -/// [`Hash`]. The store validates names on insertion and returns errors when -/// lookups fail. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_core::store::MemoryRefStore; -/// # use libvctrl_handler::{Hash, RefStore}; -/// let mut store = MemoryRefStore::new(); -/// let hash = Hash::from_bytes(&[0u8; 64]).unwrap(); -/// -/// store.set_ref("refs/heads/main", &hash).unwrap(); -/// assert_eq!(store.get_ref("refs/heads/main").unwrap(), hash); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Default)] pub struct MemoryRefStore { refs: HashMap, } impl MemoryRefStore { - /// Creates a new empty `MemoryRefStore`. - /// - /// The store contains no references initially. - /// - /// # Examples - /// - /// ``` - /// use libvctrl_core::store::MemoryRefStore; - /// use libvctrl_handler::RefStore; - /// let store = MemoryRefStore::new(); - /// assert!(store.list_refs().unwrap().next().is_none()); - /// ``` + + + + + + + + + + + + #[must_use] pub fn new() -> Self { Self { @@ -84,51 +84,51 @@ 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()); - /// ``` + + + + + + + + + + + + + + + + + + + + + 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,59 +136,59 @@ 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(); diff --git a/libvctrl_handler/src/constants.rs b/libvctrl_handler/src/constants.rs index 40d04fb5..16273981 100644 --- a/libvctrl_handler/src/constants.rs +++ b/libvctrl_handler/src/constants.rs @@ -1,187 +1,187 @@ -//! Constants related to Git object formats and operational limits. -//! -//! # Architecture -//! This module centralizes all magic numbers and structural limits used across the crate. -//! By extracting these into named constants, we eliminate "magic numbers" from the business -//! logic, making the codebase easier to audit and maintain. -//! -//! # Design Rationale: Resource Exhaustion Prevention -//! Version control systems frequently handle untrusted or malformed data. Without strict -//! upper limits, a maliciously crafted repository could instruct the parser to allocate -//! gigabytes of memory (e.g., a blob claiming to be 10 Exabytes). The `MAX_*` constants -//! act as fail-fast circuit breakers during object construction, ensuring that memory -//! allocation remains bounded and predictable. -//! -//! # Git Protocol Compliance -//! Constants like [`HASH_LENGTH`] and the modes in [`entry_mode`] are dictated by the Git -//! core specification. Hardcoding them ensures strict compliance with standard Git clients -//! and servers, preventing protocol violations. - -/// Git object entry modes. -/// -/// # Architecture -/// In Git, filesystem objects are identified by a 32-bit mode. This module exposes -/// the specific constants recognized by the Git protocol. Using named constants -/// instead of raw integers prevents invalid mode combinations and makes tree -/// manipulation code self-documenting. -/// -/// # How it works -/// The modes combine Unix permission bits with Git-specific object types. -/// For example, `0o100_644` indicates a regular file (`0o100`) with read/write -/// permissions for the owner and read-only for others (`0o644`). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod entry_mode { - /// Regular file mode. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::constants::entry_mode::BLOB; - /// assert_eq!(BLOB, 0o100_644); - /// ``` + + + + + + + + pub const BLOB: u32 = 0o100_644; - /// Executable file mode. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::constants::entry_mode::EXECUTABLE; - /// assert_eq!(EXECUTABLE, 0o100_755); - /// ``` + + + + + + + + pub const EXECUTABLE: u32 = 0o100_755; - /// Symbolic link mode. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::constants::entry_mode::SYMLINK; - /// assert_eq!(SYMLINK, 0o120_000); - /// ``` + + + + + + + + pub const SYMLINK: u32 = 0o120_000; - /// Directory (tree) mode. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::constants::entry_mode::TREE; - /// assert_eq!(TREE, 0o40_000); - /// ``` + + + + + + + + pub const TREE: u32 = 0o40_000; - /// Submodule commit mode. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::constants::entry_mode::SUBMODULE; - /// assert_eq!(SUBMODULE, 0o160_000); - /// ``` + + + + + + + + pub const SUBMODULE: u32 = 0o160_000; } -/// The length of a hash in bytes (SHA-512 = 64). -/// -/// # Why this exists -/// This crate mandates SHA-512 for cryptographic integrity. By hardcoding the length -/// to 64 bytes, we enable the use of fixed-size arrays (e.g., `[u8; HASH_LENGTH]`) -/// instead of dynamically allocated `Vec`. This shifts memory management to the -/// compile-time stack, eliminating heap allocation overhead and fragmentation for -/// every hash operation. -/// -/// # How it works -/// The constant is evaluated at compile time. Any array sized with this constant -/// benefits from fixed stack layout, and the compiler can aggressively optimize -/// loops iterating exactly `HASH_LENGTH` times. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::HASH_LENGTH; -/// assert_eq!(HASH_LENGTH, 64); -/// let hash_array = [0_u8; HASH_LENGTH]; -/// assert_eq!(hash_array.len(), 64); -/// ``` + + + + + + + + + + + + + + + + + + + + + + pub const HASH_LENGTH: usize = 64; -/// The maximum allowed length for names (in bytes). -/// -/// # Why this exists -/// Enforces a sane upper bound on file, directory, and reference names. This aligns -/// closely with typical filesystem limits (e.g., 255 bytes in most Unix filesystems). -/// It prevents malicious inputs from causing excessive memory consumption or -/// triggering filesystem errors during checkout operations. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::MAX_NAME_LENGTH; -/// assert_eq!(MAX_NAME_LENGTH, 255); -/// ``` + + + + + + + + + + + + + + pub const MAX_NAME_LENGTH: u64 = 255; -/// The maximum allowed size for blob objects (in bytes). -/// -/// # Why this exists -/// To prevent denial-of-service (`DoS`) via memory exhaustion. If unbounded, a parser -/// reading a malformed packfile could attempt to allocate gigabytes of memory for a -/// single blob. The 100 MiB limit provides ample room for legitimate source code and -/// small binary assets while acting as a circuit breaker against malicious payloads. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::MAX_BLOB_SIZE; -/// assert_eq!(MAX_BLOB_SIZE, 100 * 1024 * 1024); -/// ``` + + + + + + + + + + + + + + pub const MAX_BLOB_SIZE: u64 = 100 * 1024 * 1024; -/// The maximum number of entries allowed in a tree. -/// -/// # Why this exists -/// While Git allows a technically unlimited number of entries in a tree object, -/// performance degrades quadratically if entries are not handled correctly. Capping -/// this at 100,000 ensures that tree parsing, diffing, and serialization remain -/// performant and bounded in memory usage. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::MAX_TREE_ENTRIES; -/// assert_eq!(MAX_TREE_ENTRIES, 100_000); -/// ``` + + + + + + + + + + + + + + pub const MAX_TREE_ENTRIES: u64 = 100_000; -/// The maximum allowed length for commit/tag messages (in bytes). -/// -/// # Why this exists -/// Commit and tag messages are metadata. A 1 MiB limit is exceedingly generous for -/// textual descriptions but strictly prevents malicious actors from embedding massive -/// payloads (e.g., encoded binaries) into commit logs, which would bloat repository -/// history and memory usage during traversal. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::MAX_MESSAGE_LENGTH; -/// assert_eq!(MAX_MESSAGE_LENGTH, 1024 * 1024); -/// ``` + + + + + + + + + + + + + + pub const MAX_MESSAGE_LENGTH: u64 = 1024 * 1024; -/// The maximum number of parent commits allowed (binary format uses u16). -/// -/// # Why this exists -/// Restricts the complexity of octopus merges. While Git supports many parents, -/// allowing an unbounded number can lead to pathological graph structures that are -/// expensive to traverse. The limit of 65,535 corresponds to the maximum value of -/// an unsigned 16-bit integer, ensuring it can be packed efficiently if a binary -/// format is introduced. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::constants::MAX_PARENT_COUNT; -/// assert_eq!(MAX_PARENT_COUNT, 0xFFFF); -/// ``` + + + + + + + + + + + + + + + pub const MAX_PARENT_COUNT: u64 = 0xFFFF; diff --git a/libvctrl_handler/src/enums/core/entry_kind.rs b/libvctrl_handler/src/enums/core/entry_kind.rs index 01d64120..74ed570a 100644 --- a/libvctrl_handler/src/enums/core/entry_kind.rs +++ b/libvctrl_handler/src/enums/core/entry_kind.rs @@ -1,73 +1,73 @@ -//! Core enum definitions for Git object types. -//! -//! # Architecture -//! This module replaces raw integer mode bits (e.g., `0o100644`) with strongly-typed -//! enumerations. By using [`EntryKind`], the compiler enforces exhaustive matching, -//! preventing invalid or unrecognized file modes from propagating through the system. -//! -//! # Design Rationale -//! Raw mode bits are error-prone; a typo like `0o100646` is a valid integer but an invalid Git -//! mode. Enum variants encode domain logic directly into the type system, making the API -//! self-documenting and eliminating entire classes of runtime errors associated with -//! bit manipulation. + + + + + + + + + + + + use crate::constants::entry_mode; -/// The kind of an entry in a Git tree. -/// -/// # Why this exists -/// Git stores filesystem objects (files, directories, symlinks) in tree objects. -/// Each entry is identified by a 32-bit mode. This enum abstracts those raw bits into -/// a strongly-typed domain model. It ensures that only valid Git object types can be -/// represented, preventing invalid states (e.g., a mode of `0o000000`) from being -/// constructed. -/// -/// # How it works -/// The enum is marked as `#[non_exhaustive]` to allow for the addition of new Git -/// object types in the future without breaking downstream API compatibility. Consumers -/// must include a `_` catch-all arm when matching. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::enums::EntryKind; -/// let kind = EntryKind::Blob; -/// assert_eq!(kind.mode(), 0o100_644); -/// ``` + + + + + + + + + + + + + + + + + + + + + #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum EntryKind { - /// A regular file. + Blob, - /// An executable file. + Executable, - /// A symbolic link. + Symlink, - /// A directory (tree). + Tree, - /// A submodule commit. + Submodule, } impl EntryKind { - /// Returns the Git mode bits for this entry kind. - /// - /// # Why this exists - /// Provides a seamless conversion from the strongly-typed [`EntryKind`] back to the - /// raw `u32` mode bits required for serializing Git tree objects or interacting with - /// lower-level filesystem APIs. - /// - /// # How it works - /// Implemented as a `const fn`, this allows the conversion to be evaluated at compile - /// time if the variant is known statically. This incurs zero runtime cost and enables - /// its use in other `const` contexts. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::enums::EntryKind; - /// assert_eq!(EntryKind::Executable.mode(), 0o100_755); - /// ``` + + + + + + + + + + + + + + + + + + #[must_use] pub const fn mode(self) -> u32 { match self { @@ -79,37 +79,37 @@ impl EntryKind { } } - /// Converts raw Git mode bits into an [`EntryKind`]. - /// - /// # Why this exists - /// When parsing raw Git packfiles or loose objects, data is read as integers. This - /// function safely translates those integers into the domain model. By returning an - /// `Option`, it gracefully handles malformed or unrecognized mode bits without - /// panicking, allowing the caller to decide whether to ignore the entry or error out. - /// - /// # How it works - /// Matches the input against known Git mode constants defined in [`entry_mode`]. - /// If no match is found, `None` is returned. Like [`mode`](Self::mode), this is a - /// `const fn` to enable compile-time evaluation. - /// - /// # Examples - /// - /// Parsing a valid mode: - /// - /// ``` - /// # use libvctrl_handler::enums::EntryKind; - /// let mode = 0o120_000; // Symlink - /// let kind = EntryKind::from_mode(mode); - /// assert_eq!(kind, Some(EntryKind::Symlink)); - /// ``` - /// - /// Handling an invalid mode: - /// - /// ``` - /// # use libvctrl_handler::enums::EntryKind; - /// let invalid_mode = 0o000_000; - /// assert_eq!(EntryKind::from_mode(invalid_mode), None); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[must_use] pub const fn from_mode(mode: u32) -> Option { match mode { diff --git a/libvctrl_handler/src/enums/core/mod.rs b/libvctrl_handler/src/enums/core/mod.rs index 9bb4e585..488fdbb2 100644 --- a/libvctrl_handler/src/enums/core/mod.rs +++ b/libvctrl_handler/src/enums/core/mod.rs @@ -1,26 +1,26 @@ -//! Core enum definitions for Git object types. -//! -//! # Architecture -//! This module acts as the central registry for enumerations that represent -//! discrete, finite states in the Git protocol. By isolating these enums into -//! a dedicated `core` submodule, the crate separates raw protocol definitions -//! from higher-level domain logic and data structures. -//! -//! # Design Rationale: Strong Typing over Raw Integers -//! The Git protocol frequently relies on raw integers or specific byte sequences -//! to denote object types (e.g., mode bits in tree objects). Parsing these directly -//! into integers throughout the codebase invites logic errors and security vulnerabilities. -//! This module transforms those raw values into strongly-typed enums, allowing the -//! Rust compiler to enforce exhaustive matching and guarantee that invalid states -//! are unrepresentable at compile time. - -/// Provides the [`EntryKind`](crate::enums::EntryKind) enum, which classifies -/// the type of filesystem objects stored within a Git tree. -/// -/// # Why this exists -/// Git tree objects map directory structures. Each entry in a tree requires a -/// mode to distinguish between regular files, executable files, symbolic links, -/// subdirectories (trees), and submodule commits. This submodule exposes the -/// canonical enum for those classifications, ensuring that mode handling across -/// the crate is type-safe and self-documenting. + + + + + + + + + + + + + + + + + + + + + + + + + pub mod entry_kind; diff --git a/libvctrl_handler/src/enums/mod.rs b/libvctrl_handler/src/enums/mod.rs index 60222dff..d3df5d8d 100644 --- a/libvctrl_handler/src/enums/mod.rs +++ b/libvctrl_handler/src/enums/mod.rs @@ -1,51 +1,51 @@ -//! Enums for Git object types. -//! -//! # Architecture -//! This module serves as the central registry for enumerations representing -//! discrete, finite states within the Git protocol. By grouping these types -//! together, the crate isolates protocol-level definitions from higher-level -//! domain logic and data structures. -//! -//! # Design Rationale: Strong Typing over Raw Integers -//! The Git protocol frequently relies on raw integers or specific byte sequences -//! to denote object types (such as mode bits in tree objects). Parsing these -//! directly into integers throughout the codebase invites logic errors and -//! security vulnerabilities. This module transforms those raw values into -//! strongly-typed enums, allowing the Rust compiler to enforce exhaustive -//! matching and guarantee that invalid states are unrepresentable at compile time. -//! -//! # Examples -//! *Note: The following example assumes this crate is named `libvctrl_handler`.* -//! -//! ``` -//! # use libvctrl_handler::enums::EntryKind; -//! let kind = EntryKind::Tree; -//! assert_eq!(kind.mode(), 0o40_000); -//! ``` - -/// Core enum definitions representing fundamental Git protocol types. -/// -/// # Why this exists -/// This submodule houses the primary enumerations used across the crate. -/// Separating them into a `core` module allows the top-level `enums` module -/// to remain organized, distinguishing between essential protocol types and -/// any auxiliary or implementation-specific enums that may be added in the future. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod core; -/// Re-export of the [`EntryKind`](core::entry_kind::EntryKind) enum for ergonomic access. -/// -/// # Why this exists -/// Provides a flattened import path. Consumers can directly use -/// `libvctrl_handler::enums::EntryKind` instead of navigating the full -/// `libvctrl_handler::enums::core::entry_kind::EntryKind` path. This reduces -/// boilerplate in consumer code while keeping the internal module -/// structure logically separated. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::enums::EntryKind; -/// let kind = EntryKind::Blob; -/// assert_eq!(kind.mode(), 0o100_644); -/// ``` + + + + + + + + + + + + + + + + pub use core::entry_kind::EntryKind; diff --git a/libvctrl_handler/src/errors.rs b/libvctrl_handler/src/errors.rs index e144c4c6..0bad1b87 100644 --- a/libvctrl_handler/src/errors.rs +++ b/libvctrl_handler/src/errors.rs @@ -1,38 +1,38 @@ -//! Error types used throughout the crate. -//! -//! # Architecture -//! This module centralizes all error handling into a single, comprehensive [`VctrlError`] enum. -//! By using a unified error type, the crate ensures that consumers can handle failures -//! uniformly using the `?` operator across different subsystems (I/O, validation, parsing) -//! without needing to manually box or wrap disparate error types. -//! -//! # Design Rationale: `Arc` -//! The standard library's [`std::io::Error`] does not implement the `Clone` trait because -//! it may contain custom payloads that are not safely cloneable. To allow [`VctrlError`] -//! to be `Clone`, I/O errors are wrapped in an `Arc`. This provides thread-safe -//! reference counting, allowing the error to be cloned cheaply (a single atomic increment) -//! and shared across threads if necessary, while maintaining the original error's context. -//! -//! # Custom `PartialEq` Implementation -//! Because [`std::io::Error`] lacks a `PartialEq` implementation, a manual comparison is -//! provided for [`VctrlError::IoError`]. Two I/O errors are considered equal if their -//! [`std::io::Error::kind()`] and their string representations match. This heuristic -//! allows for predictable testing and equality checks without discarding the error details. -//! -//! # Examples -//! *Note: The following examples assume this crate is named `libvctrl_handler`.* -//! -//! Handling errors from I/O operations: -//! -//! ``` -//! # use libvctrl_handler::VctrlError; -//! use std::io::{self, ErrorKind}; -//! -//! let io_err = io::Error::new(ErrorKind::NotFound, "file missing"); -//! let vctrl_err = VctrlError::from_io(io_err); -//! -//! assert!(matches!(vctrl_err, VctrlError::IoError(_))); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use crate::constants::HASH_LENGTH; use crate::types::Hash; @@ -41,49 +41,49 @@ 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 +184,28 @@ impl From for VctrlError { } impl VctrlError { - /// Creates a [`VctrlError::IoError`] from a [`std::io::Error`]. - /// - /// This is the canonical way to convert I/O errors within the crate, - /// ensuring the `Arc` wrapping is applied consistently. - /// - /// # How it works - /// It wraps the provided error in an `Arc`, allowing the resulting - /// [`VctrlError`] to be cloned and shared across threads cheaply, despite - /// [`std::io::Error`] not natively implementing `Clone`. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::VctrlError; - /// use std::io::{self, ErrorKind}; - /// - /// let io_err = io::Error::new(ErrorKind::PermissionDenied, "access denied"); - /// let vctrl_err = VctrlError::from_io(io_err); - /// - /// let cloned_err = vctrl_err.clone(); - /// assert_eq!(vctrl_err, cloned_err); - /// ``` + + + + + + + + + + + + + + + + + + + + + + #[must_use] #[inline] pub fn from_io(err: io::Error) -> Self { diff --git a/libvctrl_handler/src/lib.rs b/libvctrl_handler/src/lib.rs index fb856157..f686d756 100644 --- a/libvctrl_handler/src/lib.rs +++ b/libvctrl_handler/src/lib.rs @@ -1,123 +1,123 @@ -//! # `libvctrl_handler` -//! -//! A robust, pure-Rust implementation of Git internals, designed for -//! high-performance and enterprise-grade reliability. -//! -//! ## Architecture -//! -//! The crate is strictly separated into distinct domains of responsibility: -//! -//! - **[`constants`]**: Defines hard limits and magic numbers used across the crate to prevent -//! unbounded memory allocation and ensure protocol compliance. -//! - **[`enums`]**: Provides exhaustive enumerations for Git-specific types, such as tree entry kinds. -//! - **[`errors`]**: Centralizes all error handling via the [`VctrlError`] enum, ensuring consistent -//! error propagation and diagnostics. -//! - **[`macros`]**: Exposes declarative macros to reduce boilerplate for error construction. -//! - **[`traits`]**: Defines the core abstract behaviors (e.g., [`Encoder`], [`Decoder`], [`ObjectStore`]). -//! This allows consumers to plug in their own backends (in-memory, filesystem, network). -//! - **[`types`]**: Contains strongly-typed representations of Git objects (e.g., [`Blob`], [`Tree`], [`Commit`]). -//! - **[`validation`]**: Provides pure functions to validate inputs like names, hashes, and references -//! before they enter the system state. -//! -//! ## Safety and Idioms -//! -//! This crate enforces `#![forbid(unsafe_code)]` to guarantee memory safety without compromise. -//! It also aggressively denies clippy lints (all, pedantic, nursery) and enforces -//! `missing_docs` to ensure the public API is fully documented. The design relies on -//! Rust's zero-cost abstractions, utilizing `const fn` where possible to shift computations -//! to compile time. -//! -//! ## Examples -//! -//! *Note: The following examples assume this crate is named `libvctrl_handler`.* -//! -//! Creating a valid [`Hash`] and inspecting an [`EntryKind`]: -//! -//! ``` -//! # use libvctrl_handler::{EntryKind, Hash}; -//! // Hash requires exactly 64 bytes (SHA-512). -//! let raw_bytes = [0_u8; 64]; -//! let hash = Hash::from_bytes(&raw_bytes); -//! assert!(hash.is_ok()); -//! -//! // Git object modes can be inspected via the EntryKind enum. -//! let blob_mode = EntryKind::Blob.mode(); -//! assert_eq!(blob_mode, 0o100_644); -//! ``` - -/// Constants related to Git object formats and operational limits. -/// -/// # Why this exists -/// Git has implicit and explicit limits (like maximum blob size or tree entries). -/// Centralizing these constants prevents magic numbers across the codebase and -/// ensures that limits are uniformly enforced at the type construction level. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod constants; -/// Enums for Git object types. -/// -/// # Why this exists -/// Using strongly-typed enums instead of raw integers (like `u32` mode bits) -/// allows the compiler to exhaustively match object kinds, preventing invalid states -/// and making the API self-documenting. + + + + + + pub mod enums; -/// Error types used throughout the crate. -/// -/// # Why this exists -/// Centralizes all error variants into a single [`VctrlError`] enum. This allows -/// consumers to handle errors uniformly using the `?` operator across different subsystems -/// without needing to box or wrap disparate error types manually. + + + + + + pub mod errors; -/// Helper macros for the crate. -/// -/// # Why this exists -/// Provides syntactic sugar for error creation, reducing boilerplate when wrapping -/// strings into [`VctrlError::Other`] and ensuring consistent error formatting. + + + + + pub mod macros; -/// Traits defining repository operations. -/// -/// # Why this exists -/// By defining traits like [`ObjectStore`] or [`Encoder`], the crate decouples -/// the business logic from the underlying I/O backend. This enables mocking -/// for tests and allows for custom storage implementations (e.g., in-memory vs. disk). + + + + + + pub mod traits; -/// Core data types for Git objects. -/// -/// # Why this exists -/// Provides immutable, validated structures like [`Commit`] and [`Tree`]. -/// Construction is fallible, ensuring that invalid objects cannot exist at runtime. + + + + + pub mod types; -/// Pure validation functions for Git inputs. -/// -/// # Why this exists -/// Separating validation from data structures allows the same logic to be -/// applied to raw inputs before attempting object construction, failing fast -/// on malformed data and preventing invalid states from ever being created. + + + + + + pub mod validation; -/// Re-exports of fundamental constants for easy access. -/// -/// These limits are enforced during object construction to prevent memory exhaustion -/// and maintain Git protocol compliance. + + + + pub use constants::{ HASH_LENGTH, MAX_BLOB_SIZE, MAX_MESSAGE_LENGTH, MAX_NAME_LENGTH, MAX_PARENT_COUNT, MAX_TREE_ENTRIES, }; -/// Re-export of the [`EntryKind`] enum for classifying tree entries. + pub use enums::EntryKind; -/// Re-export of the primary error type [`VctrlError`]. + pub use errors::VctrlError; -/// Re-exports of core operational traits for backend implementation. -/// -/// Implement these traits to create a custom Git backend or to interact with -/// repository data generically. + + + + pub use traits::core::{ blame::{Blame, BlameEntry}, config::ConfigStore, @@ -137,17 +137,17 @@ pub use traits::core::{ 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..41f804a1 100644 --- a/libvctrl_handler/src/macros.rs +++ b/libvctrl_handler/src/macros.rs @@ -1,42 +1,42 @@ -/// Constructs a [`VctrlError::Other`](crate::VctrlError::Other) from a format string and arguments. -/// -/// # Why this exists -/// In Rust, formatting a string and wrapping it into a custom error variant often requires -/// verbose syntax like `VctrlError::Other(format!(...))`. This declarative macro provides -/// syntactic sugar to eliminate this boilerplate. It ensures that ad-hoc errors are -/// constructed consistently and concisely across the codebase, mirroring the ergonomics -/// of the standard library's `println!` or `format!` macros. -/// -/// # How it works -/// Under the hood, this macro delegates to the standard `format!` macro to allocate -/// a new `String` on the heap. It then wraps this `String` in the -/// [`VctrlError::Other`](crate::VctrlError::Other) variant. -/// -/// The use of `$crate` in the expansion is critical. It guarantees that the path to -/// `VctrlError` resolves correctly to this crate's root, even if the macro is invoked -/// from an external crate that has brought the macro into scope via a glob import. -/// This prevents shadowing issues and ensures absolute path resolution without requiring -/// the consumer to manually import the error enum alongside the macro. -/// -/// # Examples -/// -/// Creating a simple error message: -/// -/// ``` -/// # use libvctrl_handler::{VctrlError, vctrl_error_other}; -/// let err = vctrl_error_other!("file not found"); -/// assert_eq!(err.to_string(), "file not found"); -/// ``` -/// -/// Formatting arguments into the error message: -/// -/// ``` -/// # use libvctrl_handler::{VctrlError, vctrl_error_other}; -/// let filename = "config.toml"; -/// let code = 404; -/// let err = vctrl_error_other!("missing configuration file: {} (code {})", filename, code); -/// assert_eq!(err.to_string(), "missing configuration file: config.toml (code 404)"); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[macro_export] macro_rules! vctrl_error_other { ($($arg:tt)*) => { diff --git a/libvctrl_handler/src/traits/core/blame.rs b/libvctrl_handler/src/traits/core/blame.rs index 69dba60c..56790789 100644 --- a/libvctrl_handler/src/traits/core/blame.rs +++ b/libvctrl_handler/src/traits/core/blame.rs @@ -1,49 +1,49 @@ -//! Blame computation trait. -//! -//! # Architecture -//! This module provides the contracts for attributing lines in a file to specific commits. -//! Blame computation is fundamentally different from standard diffing; it requires traversing -//! history in reverse and tracking line movements across revisions. By isolating this into -//! a dedicated trait, the crate allows consumers to plug in different blame algorithms -//! (e.g., linear history vs. merge-aware) without altering the core engine. -//! -//! # Design Rationale: Immutability and Validation -//! The [`BlameEntry`] struct is constructed via a fallible constructor (`new`). This ensures -//! that invalid states—such as a line range starting at 0 or having a length of 0—cannot -//! exist at runtime. Once constructed, the entry is immutable, guaranteeing that the blame -//! history remains tamper-proof. + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::Hash; -/// A single line range in a file attributed to a commit. -/// -/// # Why this exists -/// Represents the atomic unit of blame data. Instead of attributing an entire file to a single -/// commit, Git blame operates on line ranges. This struct encapsulates the mapping between a -/// specific range of lines in a file and the commit that last modified them. -/// -/// # How it works -/// The struct holds a reference to the committing [`Hash`], the 1-based line number range, -/// the file path, and an optional commit summary. The `commit_id` is stored as a copied `Hash` -/// (which is a fixed 64-byte array) rather than a reference, to simplify lifetime management -/// when returning vectors of blame entries from background threads. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::blame::BlameEntry; -/// # use libvctrl_handler::Hash; -/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); -/// let entry = BlameEntry::new( -/// hash, -/// 10, -/// 5, -/// "src/main.rs".to_string(), -/// Some("Initial commit".to_string()), -/// ); -/// assert!(entry.is_ok()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, PartialEq, Eq)] pub struct BlameEntry { commit_id: Hash, @@ -54,39 +54,39 @@ pub struct BlameEntry { } impl BlameEntry { - /// Creates a new `BlameEntry`. - /// - /// # Why this exists - /// Acts as a validation gate. In text file representations, line numbers are strictly - /// 1-based and must have a positive length. Allowing a `start_line` of 0 or a - /// `line_count` of 0 would violate these invariants and cause off-by-one errors - /// in downstream UI rendering or analysis. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidBlameRange`] if `start_line` is 0 or `line_count` is 0. - /// - /// # Examples - /// - /// Valid construction: - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 1, 10, "file.txt".into(), None); - /// assert!(entry.is_ok()); - /// ``` - /// - /// Invalid construction (zero start line): - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 0, 10, "file.txt".into(), None); - /// assert!(matches!(entry, Err(VctrlError::InvalidBlameRange))); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn new( commit_id: Hash, start_line: usize, @@ -106,158 +106,158 @@ impl BlameEntry { }) } - /// Returns the commit that last modified these lines. - /// - /// # How it works - /// Because [`Hash`] is a `Copy` type (a fixed-size array wrapper), this accessor returns - /// a copy rather than a reference. This eliminates the need for lifetime annotations - /// on the returned value, making it easier to pass the hash to asynchronous tasks or - /// store in independent data structures. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 1, 1, "f".into(), None).unwrap(); - /// assert_eq!(entry.commit_id(), hash); - /// ``` + + + + + + + + + + + + + + + + + #[must_use] pub const fn commit_id(&self) -> Hash { self.commit_id } - /// Returns the first line number (1-based). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 42, 1, "f".into(), None).unwrap(); - /// assert_eq!(entry.start_line(), 42); - /// ``` + + + + + + + + + + + #[must_use] pub const fn start_line(&self) -> usize { self.start_line } - /// Returns the number of lines in this range. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 1, 5, "f".into(), None).unwrap(); - /// assert_eq!(entry.line_count(), 5); - /// ``` + + + + + + + + + + + #[must_use] pub const fn line_count(&self) -> usize { self.line_count } - /// Returns the path of the file. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. This avoids - /// allocation when the caller only needs to read the path. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 1, 1, "src/main.rs".into(), None).unwrap(); - /// assert_eq!(entry.path(), "src/main.rs"); - /// ``` + + + + + + + + + + + + + + + #[must_use] pub fn path(&self) -> &str { &self.path } - /// Returns an optional summary of the commit message. - /// - /// # How it works - /// Uses `as_deref()` to transparently convert `&Option` to `Option<&str>`, - /// avoiding the need to clone the `String` if the caller only wishes to read the summary. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::BlameEntry; - /// # use libvctrl_handler::Hash; - /// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); - /// let entry = BlameEntry::new(hash, 1, 1, "f".into(), Some("Fix bug".into())).unwrap(); - /// assert_eq!(entry.summary(), Some("Fix bug")); - /// ``` + + + + + + + + + + + + + + + #[must_use] pub fn summary(&self) -> Option<&str> { self.summary.as_deref() } } -/// Trait for computing blame information for files. -/// -/// # Why this exists -/// Defines the abstract contract for attributing file lines to commits. By using a trait, -/// the crate decouples the blame algorithm from the repository backend. This allows for -/// different implementations (e.g., a simple linear walker vs. a complex graph traversal -/// that handles merges). -/// -/// # Design Rationale: `Send + Sync` -/// The trait requires `Send + Sync` because blame computation is highly parallelizable. -/// File-level blame operations are independent of one another. Implementors can safely -/// distribute `&self` across multiple threads to compute blame for different files -/// concurrently, leveraging multi-core processors without data races. -/// -/// # Examples -/// -/// Implementing the trait for a mock repository: -/// -/// ``` -/// # use libvctrl_handler::traits::core::blame::{Blame, BlameEntry}; -/// # use libvctrl_handler::{Hash, VctrlError}; -/// # -/// struct MockRepo; -/// -/// impl Blame for MockRepo { -/// fn blame_file(&self, _path: &str) -> Result, VctrlError> { -/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); -/// let entry = BlameEntry::new(hash, 1, 10, "file.txt".into(), None)?; -/// Ok(vec![entry]) -/// } -/// } -/// -/// let repo = MockRepo; -/// let entries = repo.blame_file("file.txt").unwrap(); -/// assert_eq!(entries.len(), 1); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Blame: Send + Sync { - /// Returns blame entries for the given file path. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the file cannot be found or the blame calculation fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::blame::{Blame, BlameEntry}; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # - /// # struct MockRepo; - /// # impl Blame for MockRepo { - /// # fn blame_file(&self, _path: &str) -> Result, VctrlError> { - /// # Ok(Vec::new()) - /// # } - /// # } - /// let repo = MockRepo; - /// assert!(repo.blame_file("nonexistent.txt").is_ok()); - /// ``` + + + + + + + + + + + + + + + + + + + + + fn blame_file(&self, path: &str) -> Result, VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/config.rs b/libvctrl_handler/src/traits/core/config.rs index 8d061c0c..f472658a 100644 --- a/libvctrl_handler/src/traits/core/config.rs +++ b/libvctrl_handler/src/traits/core/config.rs @@ -1,289 +1,289 @@ -//! Configuration store trait. -//! -//! # Architecture -//! This module defines the abstract contract for reading and writing repository -//! configuration settings (e.g., `.git/config`). By abstracting this into a trait, -//! the crate decouples the core engine from the underlying storage mechanism, -//! allowing consumers to use INI files, databases, or in-memory hash maps. -//! -//! # Design Rationale: `Option` vs `Result` -//! Configuration is inherently sparse. A missing key is often a valid state indicating -//! that a default value should be used, not an exceptional error. Therefore, read -//! operations return `Option`. An `Err(VctrlError)` is reserved strictly for -//! I/O failures or parsing corruption, ensuring a clear distinction between -//! "key not set" and "failed to read configuration". + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// A trait for reading and writing configuration values. -/// -/// # Why this exists -/// Provides a unified, type-safe interface for managing repository settings. Git -/// configurations are segmented by sections (e.g., `user`, `core`) and keys. -/// This trait enforces that structure, preventing malformed configuration access -/// and allowing backend-agnostic validation. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync`. Configuration is frequently read by multiple -/// concurrent operations (e.g., checking commit hooks, resolving user identities) -/// but rarely written. This trait design allows implementors to use `RwLock` -/// internally or rely on immutable snapshots, enabling safe parallel reads across -/// threads without locking the entire repository state. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory store: -/// -/// ``` -/// # use libvctrl_handler::traits::core::config::ConfigStore; -/// # use libvctrl_handler::VctrlError; -/// # use std::collections::HashMap; -/// # -/// #[derive(Default)] -/// struct MockConfig { -/// data: HashMap, -/// } -/// -/// impl ConfigStore for MockConfig { -/// fn get_string(&self, section: &str, key: &str) -> Result, VctrlError> { -/// let full_key = format!("{section}.{key}"); -/// Ok(self.data.get(&full_key).cloned()) -/// } -/// -/// fn set_string(&mut self, section: &str, key: &str, value: &str) -> Result<(), VctrlError> { -/// let full_key = format!("{section}.{key}"); -/// self.data.insert(full_key, value.to_string()); -/// Ok(()) -/// } -/// -/// fn get_bool(&self, section: &str, key: &str) -> Result, VctrlError> { -/// Ok(self.get_string(section, key)?.map(|v| v == "true")) -/// } -/// -/// fn set_bool(&mut self, section: &str, key: &str, value: bool) -> Result<(), VctrlError> { -/// self.set_string(section, key, if value { "true" } else { "false" }) -/// } -/// -/// fn remove(&mut self, section: &str, key: &str) -> Result<(), VctrlError> { -/// let full_key = format!("{section}.{key}"); -/// self.data.remove(&full_key); -/// Ok(()) -/// } -/// -/// fn exists(&self, section: &str, key: &str) -> Result { -/// let full_key = format!("{section}.{key}"); -/// Ok(self.data.contains_key(&full_key)) -/// } -/// } -/// -/// let mut cfg = MockConfig::default(); -/// cfg.set_string("user", "name", "Alice")?; -/// assert_eq!(cfg.get_string("user", "name")?, Some("Alice".to_string())); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait ConfigStore: Send + Sync { - /// Returns the string value for the given section and key. - /// - /// # How it works - /// Looks up the configuration value in the specified section. If the section - /// or key does not exist, it returns `Ok(None)` rather than an error, allowing - /// the caller to fall back to default values gracefully. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be read (e.g., due to - /// an I/O failure or corrupted configuration file). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let mut cfg = MockConfig::default(); - /// cfg.set_string("core", "editor", "vim")?; - /// assert_eq!(cfg.get_string("core", "editor")?, Some("vim".to_string())); - /// assert_eq!(cfg.get_string("core", "missing")?, None); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get_string(&self, section: &str, key: &str) -> Result, VctrlError>; - /// Sets the string value for the given section and key. - /// - /// # How it works - /// Requires `&mut self`, enforcing exclusive access for write operations. This - /// ensures that no other thread can read a partially written configuration state, - /// maintaining atomicity at the trait level. Implementors are responsible for - /// persisting this change to the underlying storage medium. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be written (e.g., due to - /// insufficient permissions or disk full). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let mut cfg = MockConfig::default(); - /// cfg.set_string("user", "email", "test@example.com")?; - /// assert!(cfg.exists("user", "email")?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn set_string(&mut self, section: &str, key: &str, value: &str) -> Result<(), VctrlError>; - /// Returns the boolean value for the given section and key. - /// - /// # How it works - /// Retrieves the string representation and attempts to parse it as a boolean. - /// If the key exists but is not a valid boolean (e.g., "yes", "1", "true"), - /// the implementor should return a [`VctrlError::SerializationError`] or similar, - /// as this indicates a corrupted or malformed configuration. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be read or is not a boolean. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let mut cfg = MockConfig::default(); - /// cfg.set_bool("core", "bare", true)?; - /// assert_eq!(cfg.get_bool("core", "bare")?, Some(true)); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get_bool(&self, section: &str, key: &str) -> Result, VctrlError>; - /// Sets the boolean value for the given section and key. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be written. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let mut cfg = MockConfig::default(); - /// cfg.set_bool("core", "autocrlf", false)?; - /// assert_eq!(cfg.get_string("core", "autocrlf")?, Some("false".to_string())); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + fn set_bool(&mut self, section: &str, key: &str, value: bool) -> Result<(), VctrlError>; - /// Removes a key from the configuration. - /// - /// # How it works - /// Deletes the specified key within the given section. If the key or section - /// does not exist, this operation is idempotent and returns `Ok(())`, ensuring - /// that cleanup operations do not fail spuriously on missing data. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be modified (e.g., due to - /// file permission issues). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let mut cfg = MockConfig::default(); - /// cfg.set_string("remote", "origin", "url")?; - /// cfg.remove("remote", "origin")?; - /// assert!(!cfg.exists("remote", "origin")?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn remove(&mut self, section: &str, key: &str) -> Result<(), VctrlError>; - /// Checks if a key exists in the configuration. - /// - /// # How it works - /// Performs a lightweight existence check without retrieving the value. This is - /// useful for validating configuration prerequisites before attempting complex - /// operations. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the configuration cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::config::ConfigStore; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockConfig { data: HashMap } - /// # impl ConfigStore for MockConfig { - /// # fn get_string(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.data.get(&format!("{s}.{k}")).cloned()) } - /// # fn set_string(&mut self, s: &str, k: &str, v: &str) -> Result<(), VctrlError> { self.data.insert(format!("{s}.{k}"), v.to_string()); Ok(()) } - /// # fn get_bool(&self, s: &str, k: &str) -> Result, VctrlError> { Ok(self.get_string(s, k)?.map(|v| v == "true")) } - /// # fn set_bool(&mut self, s: &str, k: &str, v: bool) -> Result<(), VctrlError> { self.set_string(s, k, if v { "true" } else { "false" }) } - /// # fn remove(&mut self, s: &str, k: &str) -> Result<(), VctrlError> { self.data.remove(&format!("{s}.{k}")); Ok(()) } - /// # fn exists(&self, s: &str, k: &str) -> Result { Ok(self.data.contains_key(&format!("{s}.{k}"))) } - /// # } - /// let cfg = MockConfig::default(); - /// assert!(!cfg.exists("nonexistent", "key")?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn exists(&self, section: &str, key: &str) -> Result; } diff --git a/libvctrl_handler/src/traits/core/decoder.rs b/libvctrl_handler/src/traits/core/decoder.rs index 0141b047..7f61d538 100644 --- a/libvctrl_handler/src/traits/core/decoder.rs +++ b/libvctrl_handler/src/traits/core/decoder.rs @@ -1,223 +1,223 @@ -//! Object decoder trait. -//! -//! # Architecture -//! This module defines the contract for deserializing raw byte streams into -//! strongly-typed Git domain objects ([`Blob`], [`Tree`], [`Commit`], [`Tag`]). -//! It acts as the bridge between unstructured I/O data and the crate's type-safe -//! in-memory representations. -//! -//! # Design Rationale: Streaming Deserialization -//! Instead of accepting a `&[u8]` or `Vec`, the decoder methods require a -//! generic `R: Read` bound. This is a critical architectural decision: it forces -//! streaming deserialization. Git objects (especially blobs) can be massive. -//! By reading from a stream, the decoder can process gigabytes of data with a -//! fixed memory footprint, preventing denial-of-service (DoS) vulnerabilities -//! associated with unbounded memory allocation. + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::{Blob, Commit, Tag, Tree}; use std::io::Read; -/// Trait for decoding raw Git object bytes into structured types. -/// -/// # Why this exists -/// Abstracts the parsing logic away from the storage backend. Whether objects -/// are being read from loose files on disk, extracted from a compressed packfile, -/// or streamed over a network socket, the decoding logic remains identical. -/// This allows the crate to support multiple wire formats or compression -/// algorithms by simply providing different implementations of this trait. -/// -/// # How it works -/// The trait uses generic methods (``) rather than dynamic -/// trait objects (`&mut dyn Read`). This design leverages Rust's monomorphization: -/// the compiler generates a specific version of the decode function for every -/// concrete reader type used at runtime. This eliminates dynamic dispatch overhead, -/// allowing the compiler to aggressively inline the reading logic. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync` on `Self`, and `Send` on the reader `R`. -/// This ensures that decoding operations can be safely dispatched to a thread pool. -/// For example, when parsing a multi-object packfile, the engine can distribute -/// object streams across multiple worker threads to utilize multi-core parallelism -/// without risking data races. -/// -/// # Examples -/// -/// Implementing the trait for a mock streaming parser: -/// -/// ``` -/// # use libvctrl_handler::traits::core::decoder::Decoder; -/// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; -/// # use std::io::{Cursor, Read}; -/// # -/// struct MockDecoder; -/// -/// impl Decoder for MockDecoder { -/// fn decode_blob(&self, mut reader: R) -> Result { -/// let mut buf = Vec::new(); -/// reader.read_to_end(&mut buf)?; -/// Blob::new(buf) -/// } -/// -/// fn decode_tree(&self, _reader: R) -> Result { -/// // Mock implementation returns an empty tree -/// Tree::new(vec![]) -/// } -/// -/// fn decode_commit(&self, _reader: R) -> Result { -/// // Mock implementation returns an error for brevity -/// Err(VctrlError::Other("mock commit decode".into())) -/// } -/// -/// fn decode_tag(&self, _reader: R) -> Result { -/// Err(VctrlError::Other("mock tag decode".into())) -/// } -/// } -/// -/// let decoder = MockDecoder; -/// let raw_data = Cursor::new(b"file content".to_vec()); -/// let blob = decoder.decode_blob(raw_data)?; -/// assert_eq!(blob.data(), b"file content"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Decoder: Send + Sync { - /// Decodes a blob object from a reader. - /// - /// # How it works - /// Reads bytes from the provided reader until EOF, enforcing the - /// [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE) limit during the - /// construction of the [`Blob`] type. This prevents memory exhaustion - /// from maliciously large streams. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if decoding fails. This can occur if the reader - /// encounters an I/O error, or if the parsed data exceeds the maximum - /// allowed size limits. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::decoder::Decoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::{Cursor, Read}; - /// # - /// # struct MockDecoder; - /// # impl Decoder for MockDecoder { - /// # fn decode_blob(&self, mut reader: R) -> Result { - /// # let mut buf = Vec::new(); - /// # reader.read_to_end(&mut buf)?; - /// # Blob::new(buf) - /// # } - /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } - /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # } - /// let decoder = MockDecoder; - /// let stream = Cursor::new(b"binary data".to_vec()); - /// assert!(decoder.decode_blob(stream).is_ok()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_blob(&self, reader: R) -> Result; - /// Decodes a tree object from a reader. - /// - /// # How it works - /// Parses the binary tree format, reading entry modes, names, and hashes - /// sequentially. It enforces Git's strict sorting rules (directories are - /// sorted as if they have a trailing `/`) and rejects duplicate entries - /// during the construction of the [`Tree`] type. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if decoding fails. This can occur if the stream - /// is truncated, contains invalid mode bits, or violates tree structural - /// integrity (e.g., unsorted entries). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::decoder::Decoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::{Cursor, Read}; - /// # - /// # struct MockDecoder; - /// # impl Decoder for MockDecoder { - /// # fn decode_blob(&self, mut reader: R) -> Result { Blob::new(Vec::new()) } - /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } - /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # } - /// let decoder = MockDecoder; - /// let stream = Cursor::new(Vec::new()); - /// assert!(decoder.decode_tree(stream).is_ok()); - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_tree(&self, reader: R) -> Result; - /// Decodes a commit object from a reader. - /// - /// # How it works - /// Parses the textual commit format, extracting tree references, parent - /// hashes, author/committer metadata, and the commit message. It validates - /// parent counts and message lengths against crate constants before - /// constructing the [`Commit`] type. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if decoding fails. This can occur if the commit - /// contains duplicate parents, if the timestamp is malformed, or if an - /// I/O error occurs while reading the stream. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::decoder::Decoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::{Cursor, Read}; - /// # - /// # struct MockDecoder; - /// # impl Decoder for MockDecoder { - /// # fn decode_blob(&self, _reader: R) -> Result { Blob::new(Vec::new()) } - /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } - /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # } - /// let decoder = MockDecoder; - /// let stream = Cursor::new(Vec::new()); - /// assert!(decoder.decode_commit(stream).is_err()); // Mock returns err - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_commit(&self, reader: R) -> Result; - /// Decodes a tag object from a reader. - /// - /// # How it works - /// Parses the annotated tag format, extracting the target object hash, - /// tagger identity, and tag message. It enforces reference naming rules - /// (via [`validate_ref_name`](crate::validation::validate_ref_name)) on the - /// tag's name during construction. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if decoding fails. This can occur if the tag name - /// is invalid, if the message exceeds the maximum length, or if the stream - /// is corrupted. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::decoder::Decoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::{Cursor, Read}; - /// # - /// # struct MockDecoder; - /// # impl Decoder for MockDecoder { - /// # fn decode_blob(&self, _reader: R) -> Result { Blob::new(Vec::new()) } - /// # fn decode_tree(&self, _reader: R) -> Result { Tree::new(vec![]) } - /// # fn decode_commit(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # fn decode_tag(&self, _reader: R) -> Result { Err(VctrlError::Other("mock".into())) } - /// # } - /// let decoder = MockDecoder; - /// let stream = Cursor::new(Vec::new()); - /// assert!(decoder.decode_tag(stream).is_err()); // Mock returns err - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn decode_tag(&self, reader: R) -> Result; } diff --git a/libvctrl_handler/src/traits/core/diff.rs b/libvctrl_handler/src/traits/core/diff.rs index 82d52bb6..a5efbe5a 100644 --- a/libvctrl_handler/src/traits/core/diff.rs +++ b/libvctrl_handler/src/traits/core/diff.rs @@ -1,119 +1,119 @@ -//! Tree differencing trait. -//! -//! # Architecture -//! This module provides the abstract contract for computing structural deltas -//! between two tree objects. It abstracts the diffing algorithm (e.g., Myers, -//! Histogram) away from the core engine, allowing consumers to plug in -//! optimized or specialized diffing strategies. -//! -//! # Design Rationale: Associated Types over Generics -//! The trait uses an associated type (`type TreeId`) rather than a generic -//! parameter (``). This design choice is deliberate: it ties the -//! identifier type to the specific `TreeDiffer` implementation. A differ that -//! reads from an in-memory store might use array indices as IDs, while a -//! filesystem-based differ uses `Hash`. Associated types prevent the need to -//! annotate the trait with generics at every call site, simplifying the API -//! while preserving flexibility. + + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::TreeDelta; -/// Trait for computing differences between two trees. -/// -/// # Why this exists -/// Comparing two trees to find file additions, deletions, modifications, and -/// renames is a fundamental operation in version control. By defining this as -/// a trait, the crate ensures that the core logic does not depend on a specific -/// algorithm or storage backend. The output is a strongly-typed [`TreeDelta`], -/// which aggregates [`FileDelta`](crate::FileDelta) entries, ensuring that -/// downstream consumers (like UI renderers or merge drivers) receive a -/// consistent, validated data structure. -/// -/// # How it works -/// The implementor receives references to two tree identifiers (`old` and `new`). -/// It is responsible for resolving these IDs to actual tree data (if necessary), -/// comparing their entries recursively, and classifying the changes. The -/// resulting [`TreeDelta`] provides an iterator-like interface over these -/// atomic file changes. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync` on both `Self` and the associated `TreeId`. -/// This is critical for performance: diffing large repositories is highly -/// parallelizable. By enforcing thread safety, the engine can dispatch -/// multiple `diff_trees` calls across a thread pool (e.g., using `rayon`) -/// to compare different directory branches concurrently without data races. -/// -/// # Examples -/// -/// Implementing the trait for a mock store that always reports no changes: -/// -/// ``` -/// # use libvctrl_handler::traits::core::diff::TreeDiffer; -/// # use libvctrl_handler::{TreeDelta, Hash, VctrlError}; -/// # -/// struct MockDiffer; -/// -/// impl TreeDiffer for MockDiffer { -/// type TreeId = Hash; -/// -/// fn diff_trees(&self, _old: &Self::TreeId, _new: &Self::TreeId) -> Result { -/// // In a real implementation, this would load trees and compare entries. -/// Ok(TreeDelta::new()) -/// } -/// } -/// -/// let differ = MockDiffer; -/// let old_hash = Hash::from_bytes(&[0_u8; 64])?; -/// let new_hash = Hash::from_bytes(&[1u8; 64])?; -/// -/// let delta = differ.diff_trees(&old_hash, &new_hash)?; -/// assert!(delta.is_empty()); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait TreeDiffer: Send + Sync { - /// The identifier type for a tree. - /// - /// # Why this exists - /// Allows the differ implementation to define its own lookup mechanism. While - /// typically a [`Hash`], it could also be a database primary key or an - /// in-memory pointer, decoupling the diff logic from the object storage format. + + + + + + type TreeId: Send + Sync; - /// Computes the list of changes between two trees. - /// - /// # How it works - /// Resolves the `old` and `new` identifiers and performs a structural - /// comparison. The method returns a [`TreeDelta`] containing a list of - /// [`FileDelta`](crate::FileDelta)s. If a file exists in `new` but not `old`, - /// it is classified as `Added`; if it exists in `old` but not `new`, it is - /// `Deleted`. If the hashes differ but paths match, it is `Modified`. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if either tree cannot be loaded (e.g., - /// [`VctrlError::ObjectNotFound`]) or if the diffing process fails due to - /// corrupted data. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::diff::TreeDiffer; - /// # use libvctrl_handler::{TreeDelta, Hash, VctrlError}; - /// # - /// # struct MockDiffer; - /// # impl TreeDiffer for MockDiffer { - /// # type TreeId = Hash; - /// # fn diff_trees(&self, _old: &Self::TreeId, _new: &Self::TreeId) -> Result { - /// # Ok(TreeDelta::new()) - /// # } - /// # } - /// let differ = MockDiffer; - /// let hash = Hash::from_bytes(&[0_u8; 64])?; - /// - /// // Diffing a tree against itself should yield an empty delta. - /// let delta = differ.diff_trees(&hash, &hash)?; - /// assert_eq!(delta.len(), 0); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn diff_trees(&self, old: &Self::TreeId, new: &Self::TreeId) -> Result; } diff --git a/libvctrl_handler/src/traits/core/encoder.rs b/libvctrl_handler/src/traits/core/encoder.rs index 47e2fb4a..ad1456a4 100644 --- a/libvctrl_handler/src/traits/core/encoder.rs +++ b/libvctrl_handler/src/traits/core/encoder.rs @@ -1,228 +1,228 @@ -//! Object encoder trait. -//! -//! # Architecture -//! This module defines the contract for serializing strongly-typed Git domain -//! objects ([`Blob`], [`Tree`], [`Commit`], [`Tag`]) into raw byte streams. -//! It acts as the bridge between the crate's type-safe in-memory representations -//! and unstructured I/O data storage or network transmission. -//! -//! # Design Rationale: Streaming Serialization -//! Instead of returning a `Vec` or `Box<[u8]>`, the encoder methods require a -//! generic `W: Write` bound. This is a critical architectural decision: it forces -//! streaming serialization. Git objects (especially blobs) can be massive. By writing -//! directly to a stream, the encoder can process gigabytes of data with a fixed memory -//! footprint, preventing out-of-memory (OOM) errors and avoiding the CPU overhead of -//! allocating and resizing temporary heap buffers. + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::{Blob, Commit, Tag, Tree}; use std::io::Write; -/// Trait for encoding structured Git objects into raw bytes. -/// -/// # Why this exists -/// Abstracts the serialization logic away from the storage backend. Whether objects -/// are being written to loose files on disk, compressed into a packfile, or streamed -/// over a network socket, the encoding logic remains identical. This allows the crate -/// to support multiple wire formats or compression algorithms by simply providing -/// different implementations of this trait. -/// -/// # How it works -/// The trait uses generic methods (``) rather than dynamic trait -/// objects (`&mut dyn Write`). This design leverages Rust's monomorphization: the -/// compiler generates a specific version of the encode function for every concrete -/// writer type used at runtime. This eliminates dynamic dispatch overhead, allowing -/// the compiler to aggressively inline the writing logic and optimize away function -/// call boundaries. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync` on `Self`, and `Send` on the writer `W`. This -/// ensures that encoding operations can be safely dispatched to a thread pool. For -/// example, when writing a multi-object packfile, the engine can distribute object -/// serialization across multiple worker threads to utilize multi-core parallelism -/// without risking data races on the underlying writer or encoder state. -/// -/// # Examples -/// -/// Implementing the trait for a mock streaming writer: -/// -/// ``` -/// # use libvctrl_handler::traits::core::encoder::Encoder; -/// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; -/// # use std::io::Write; -/// # -/// struct MockEncoder; -/// -/// impl Encoder for MockEncoder { -/// fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { -/// // Write the raw blob data directly to the stream -/// writer.write_all(blob.data())?; -/// Ok(()) -/// } -/// -/// fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { -/// // Mock implementation -/// Ok(()) -/// } -/// -/// fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { -/// // Mock implementation -/// Ok(()) -/// } -/// -/// fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { -/// // Mock implementation -/// Ok(()) -/// } -/// } -/// -/// let encoder = MockEncoder; -/// let blob = Blob::new(b"file content".to_vec())?; -/// let mut buffer = Vec::new(); -/// encoder.encode_blob(&blob, &mut buffer)?; -/// assert_eq!(&buffer, b"file content"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Encoder: Send + Sync { - /// Encodes a blob object into a writer. - /// - /// # How it works - /// Writes the raw byte content of the [`Blob`] directly to the provided writer. - /// Because [`Blob`] enforces size limits during construction, this method does - /// not need to re-validate the payload size, allowing for a high-throughput, - /// direct memory-to-stream copy. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if encoding fails. This typically occurs if the underlying - /// writer experiences an I/O error (e.g., disk full, broken pipe). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::encoder::Encoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::Write; - /// # struct MockEncoder; - /// # impl Encoder for MockEncoder { - /// # fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError> { writer.write_all(blob.data())?; Ok(()) } - /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let encoder = MockEncoder; - /// let blob = Blob::new(b"binary data".to_vec())?; - /// let mut buffer = Vec::new(); - /// assert!(encoder.encode_blob(&blob, &mut buffer).is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_blob(&self, blob: &Blob, writer: &mut W) -> Result<(), VctrlError>; - /// Encodes a tree object into a writer. - /// - /// # How it works - /// Serializes the tree entries into the canonical Git binary format. It writes the - /// mode bits (as octal ASCII), a null byte, the entry name, and the 64-byte SHA-512 - /// hash for each entry. Entries are guaranteed to be in Git-sorted order, as enforced - /// by the [`Tree`] constructor, ensuring the output is deterministic and canonical. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying writer fails. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Read; - /// # use libvctrl_handler::traits::core::encoder::Encoder; - /// # use libvctrl_handler::{Blob, Commit, Tag, Tree, VctrlError}; - /// # use std::io::Write; - /// # struct MockEncoder; - /// # impl Encoder for MockEncoder { - /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let encoder = MockEncoder; - /// let tree = Tree::new(vec![])?; - /// let mut buffer = Vec::new(); - /// assert!(encoder.encode_tree(&tree, &mut buffer).is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_tree(&self, tree: &Tree, writer: &mut W) -> Result<(), VctrlError>; - /// Encodes a commit object into a writer. - /// - /// # How it works - /// Formats the commit into the canonical Git text format. It writes tree references, - /// parent hashes, author/committer metadata (with timestamps and timezone offsets), - /// and the commit message. The formatting adheres strictly to Git specifications to - /// ensure interoperability with standard Git clients. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying writer fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::encoder::Encoder; - /// # use libvctrl_handler::{Blob, Commit, CommitMeta, Hash, Tag, Tree, UserID, VctrlError}; - /// # use std::io::Write; - /// # struct MockEncoder; - /// # impl Encoder for MockEncoder { - /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// # let hash = Hash::from_bytes(&[0_u8; 64])?; - /// # let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; - /// let encoder = MockEncoder; - /// let commit = Commit::new(hash, vec![], user.clone(), user, "message".to_string())?; /// - /// let mut buffer = Vec::new(); - /// assert!(encoder.encode_commit(&commit, &mut buffer).is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_commit( &self, commit: &Commit, writer: &mut W, ) -> Result<(), VctrlError>; - /// Encodes a tag object into a writer. - /// - /// # How it works - /// Formats the annotated tag into the canonical Git text format. It writes the target - /// object hash, tagger identity, and tag message. As with [`encode_commit`](Self::encode_commit), - /// strict adherence to the Git specification ensures that the resulting tag is recognized - /// by standard Git tooling. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying writer fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::encoder::Encoder; - /// # use libvctrl_handler::{Blob, Commit, Hash, Tag, Tree, UserID, VctrlError}; - /// # use std::io::Write; - /// # struct MockEncoder; - /// # impl Encoder for MockEncoder { - /// # fn encode_blob(&self, _blob: &Blob, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tree(&self, _tree: &Tree, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_commit(&self, _commit: &Commit, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # fn encode_tag(&self, _tag: &Tag, _writer: &mut W) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// # let hash = Hash::from_bytes(&[0_u8; 64])?; - /// # let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; - /// let encoder = MockEncoder; - /// let tag = Tag::new("v1.0".to_string(), hash, Some(user), "release".to_string())?; - /// let mut buffer = Vec::new(); - /// assert!(encoder.encode_tag(&tag, &mut buffer).is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn encode_tag(&self, tag: &Tag, writer: &mut W) -> Result<(), VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/hasher.rs b/libvctrl_handler/src/traits/core/hasher.rs index 74ce3cda..41e2beda 100644 --- a/libvctrl_handler/src/traits/core/hasher.rs +++ b/libvctrl_handler/src/traits/core/hasher.rs @@ -1,109 +1,109 @@ -//! Hashing trait. -//! -//! # Architecture -//! This module defines the abstract contract for computing cryptographic hashes. -//! By abstracting the hashing mechanism into a trait, the crate decouples its -//! content-addressing logic from the specific cryptographic algorithm (e.g., SHA-1, -//! SHA-256, SHA-512). This allows consumers to swap algorithms or inject hardware-accelerated -//! implementations without modifying the core object database logic. -//! -//! # Design Rationale: Streaming Cryptography -//! The trait operates on `R: Read` rather than `&[u8]` or `Vec`. This is a critical -//! architectural decision for performance and security. Git objects, particularly blobs, -//! can be gigabytes in size. Loading an entire object into memory to hash it would cause -//! severe memory fragmentation and potential out-of-memory (OOM) errors. By requiring a -//! reader, the hasher processes data in fixed-size chunks, maintaining a constant memory -//! footprint regardless of the input size. + + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::Hash; use std::io::Read; -/// Trait for computing hash values. -/// -/// # Why this exists -/// In a content-addressable storage (CAS) system, the identifier of an object is derived -/// from its content. This trait provides the contract for that derivation. Separating it -/// from the encoder or storage backend allows for independent optimization and testing -/// of the cryptographic pipeline. -/// -/// # How it works -/// The trait uses a generic method (``) instead of a dynamic trait object -/// (`&mut dyn Read`). This leverages Rust's monomorphization: the compiler generates a -/// specialized version of the `hash` method for every concrete reader type used at runtime. -/// This eliminates dynamic dispatch overhead, allowing the compiler to aggressively inline -/// the read loops and buffering logic. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync` on `Self`, and `Send` on the reader `R`. Hashing is -/// a CPU-bound, stateless operation (from the perspective of the hasher). By enforcing -/// thread safety, the engine can safely distribute hashing tasks across a thread pool. -/// For example, when writing a packfile, multiple objects can be hashed concurrently on -/// different threads without requiring external synchronization. -/// -/// # Examples -/// -/// Implementing the trait for a mock hasher that reads stream to completion: -/// -/// ``` -/// # use libvctrl_handler::traits::core::hasher::Hasher; -/// # use libvctrl_handler::{Hash, VctrlError}; -/// # use std::io::Read; -/// # -/// struct MockHasher; -/// -/// impl Hasher for MockHasher { -/// fn hash(&self, mut reader: R) -> Result { -/// // In a real implementation, this would update a cryptographic state -/// // (e.g., SHA-512) and finalize it. Here, we just drain the reader. -/// let mut buf = Vec::new(); -/// reader.read_to_end(&mut buf)?; -/// // Return a deterministic mock hash -/// Hash::from_bytes(&[0_u8; 64]) -/// } -/// } -/// -/// let hasher = MockHasher; -/// let data = std::io::Cursor::new(b"some data".to_vec()); -/// let hash = hasher.hash(data)?; -/// assert_eq!(hash.as_bytes(), &[0_u8; 64]); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Hasher: Send + Sync { - /// Returns the hash of the data read from the given reader. - /// - /// # How it works - /// Reads bytes from the provided reader in chunks until EOF is reached. As data is - /// read, it is fed into the underlying hashing algorithm's state machine. Once the - /// stream is exhausted, the final digest is computed and returned as a strongly-typed - /// [`Hash`]. This ensures that the hash is always the correct length (64 bytes for - /// SHA-512) as validated by [`Hash::from_bytes`]. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if hashing fails. This typically occurs if the underlying - /// reader experiences an I/O error (e.g., a broken pipe or disk read failure) during - /// the streaming process. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::hasher::Hasher; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::io::Read; - /// # struct MockHasher; - /// # impl Hasher for MockHasher { - /// # fn hash(&self, mut reader: R) -> Result { - /// # let mut buf = Vec::new(); - /// # reader.read_to_end(&mut buf)?; - /// # Hash::from_bytes(&[0_u8; 64]) - /// # } - /// # } - /// let hasher = MockHasher; - /// let stream = std::io::Cursor::new(b"hash this content".to_vec()); - /// let result = hasher.hash(stream); - /// assert!(result.is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn hash(&self, reader: R) -> Result; } diff --git a/libvctrl_handler/src/traits/core/index.rs b/libvctrl_handler/src/traits/core/index.rs index 9adcba0f..dfdbe067 100644 --- a/libvctrl_handler/src/traits/core/index.rs +++ b/libvctrl_handler/src/traits/core/index.rs @@ -1,503 +1,503 @@ -//! Index (staging area) trait. -//! -//! # Architecture -//! This module defines the abstract contract for managing the Git index, commonly -//! known as the staging area. The index acts as the crucial intermediate state -//! between the working directory and the object database, tracking planned changes -//! for the next commit. -//! -//! # Design Rationale: Associated Types over Generics -//! The trait uses associated types (`type Entry`, `type Path`, `type TreeId`) -//! rather than generic parameters. This design ties the data representations -//! directly to the specific `Index` implementation. An in-memory index might use -//! `Rc` and `String`, while a disk-backed index might use `TreeEntry` -//! and `PathBuf`. This prevents type mismatches at compile time and simplifies -//! the API by removing the need for verbose generic annotations at every call site. + + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// A trait for managing a Git index (staging area). -/// -/// # Why this exists -/// The staging area allows users to stage partial changes (hunks) before committing -/// them to history. By abstracting this into a trait, the crate allows the core -/// engine to orchestrate commits, diffs, and merges without being tied to a specific -/// binary format (like the `.git/index` file) or an in-memory representation. -/// -/// # How it works -/// The index maintains a mapping between file paths and their staged object entries. -/// It supports adding, removing, and querying entries. The `write_tree` method -/// serializes the current state into one or more tree objects in the object database, -/// returning the root tree identifier. `read_tree` performs the inverse, populating -/// the index from an existing tree. -/// -/// # Design Rationale: `&self` on `write_tree` -/// Note that `write_tree` takes `&self` instead of `&mut self`. This is because -/// writing a tree does not mutate the logical state of the index itself. The -/// implementor is responsible for handling any necessary interior mutability -/// (e.g., using `RefCell` or `Mutex`) when interacting with the underlying -/// `ObjectStore` to persist the tree objects. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory store: -/// -/// ``` -/// # use libvctrl_handler::traits::core::index::Index; -/// # use libvctrl_handler::VctrlError; -/// # use std::collections::HashMap; -/// # -/// #[derive(Default)] -/// struct MockIndex { -/// data: HashMap, -/// } -/// -/// impl Index for MockIndex { -/// type Entry = String; -/// type Path = String; -/// type TreeId = u32; -/// -/// fn add(&mut self, entry: Self::Entry) -> Result<(), VctrlError> { -/// self.data.insert(entry.clone(), entry); -/// Ok(()) -/// } -/// -/// fn remove(&mut self, path: &Self::Path) -> Result<(), VctrlError> { -/// self.data.remove(path); -/// Ok(()) -/// } -/// -/// fn clear(&mut self) -> Result<(), VctrlError> { -/// self.data.clear(); -/// Ok(()) -/// } -/// -/// fn get(&self, path: &Self::Path) -> Result, VctrlError> { -/// Ok(self.data.get(path).cloned()) -/// } -/// -/// fn contains(&self, path: &Self::Path) -> Result { -/// Ok(self.data.contains_key(path)) -/// } -/// -/// fn len(&self) -> Result { -/// Ok(self.data.len()) -/// } -/// -/// fn entries(&self) -> Result, VctrlError> { -/// Ok(self.data.values().cloned().collect()) -/// } -/// -/// fn write_tree(&self) -> Result { -/// // In a real impl, this would write to an ObjectStore. -/// Ok(1) -/// } -/// -/// fn read_tree(&mut self, _tree: &Self::TreeId) -> Result<(), VctrlError> { -/// // Mock implementation -/// Ok(()) -/// } -/// } -/// -/// let mut index = MockIndex::default(); -/// index.add("file.txt".to_string())?; -/// assert_eq!(index.len()?, 1); -/// assert!(index.contains(&"file.txt".to_string())?); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Index: Send + Sync { - /// The entry type used by the index. - /// - /// # Why this exists - /// Allows the backend to define its own representation of a staged file, which - /// might include mode bits, object hashes, and filesystem stat data (mtime, ctime) - /// for optimization. + + + + + + type Entry: Send + Sync; - /// The path type used by the index. - /// - /// # Why this exists - /// Decouples the path representation. While typically a `String` or `PathBuf`, - /// this allows backends to use interned strings or OS-specific paths. + + + + + type Path: Send + Sync; - /// The tree identifier type. - /// - /// # Why this exists - /// Matches the identifier type used by the backend's `ObjectStore` or `TreeDiffer`, - /// ensuring seamless interoperability when writing or reading trees. + + + + + type TreeId: Send + Sync; - /// Adds an entry to the index. - /// - /// # How it works - /// Inserts or updates the entry in the index. If an entry with the same path already - /// exists, it is overwritten. Requires `&mut self` as it mutates the logical state - /// of the staging area. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage fails to persist the update - /// or if the entry is invalid. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("new_file.txt".to_string())?; - /// assert_eq!(index.len()?, 1); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn add(&mut self, entry: Self::Entry) -> Result<(), VctrlError>; - /// Removes an entry from the index by path. - /// - /// # How it works - /// Locates the entry by its path and removes it. If the path does not exist, - /// this operation is typically idempotent and returns `Ok(())`. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage fails to persist the deletion. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("file.txt".to_string())?; - /// index.remove(&"file.txt".to_string())?; - /// assert!(index.is_empty()?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn remove(&mut self, path: &Self::Path) -> Result<(), VctrlError>; - /// Clears all entries from the index. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be cleared. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("a".to_string())?; - /// index.clear()?; - /// assert_eq!(index.len()?, 0); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn clear(&mut self) -> Result<(), VctrlError>; - /// Retrieves an entry by path. - /// - /// # How it works - /// Performs a lookup. Returns `Ok(None)` if the path is not staged, maintaining - /// a clear distinction between "not staged" and "I/O error". - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("file.txt".to_string())?; - /// assert!(index.get(&"file.txt".to_string())?.is_some()); - /// assert!(index.get(&"missing.txt".to_string())?.is_none()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get(&self, path: &Self::Path) -> Result, VctrlError>; - /// Checks if an entry exists by path. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("file.txt".to_string())?; - /// assert!(index.contains(&"file.txt".to_string())?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn contains(&self, path: &Self::Path) -> Result; - /// Returns the number of entries in the index. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("a".to_string())?; - /// index.add("b".to_string())?; - /// assert_eq!(index.len()?, 2); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn len(&self) -> Result; - /// Returns `true` if the index is empty. - /// - /// # How it works - /// This is a provided method that default-implements by calling `len()`. It - /// exists to provide ergonomic, self-documenting code at call sites. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let index = MockIndex::default(); - /// assert!(index.is_empty()?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn is_empty(&self) -> Result { Ok(self.len()? == 0) } - /// Returns all entries in the index. - /// - /// # How it works - /// Collects all staged entries into a `Vec`. This requires heap allocation. - /// Callers should prefer `get` or `contains` if they only need to query a - /// specific path, to avoid the overhead of collecting the entire index. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("a".to_string())?; - /// let entries = index.entries()?; - /// assert_eq!(entries.len(), 1); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn entries(&self) -> Result, VctrlError>; - /// Writes the current index to a tree object and returns its identifier. - /// - /// # How it works - /// Traverses the staged entries, recursively building tree objects for directories. - /// It persists these trees to the `ObjectStore` (handled internally by the implementor) - /// and returns the hash (or ID) of the root tree. This is the final step before - /// creating a commit object. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the tree cannot be constructed or persisted, typically - /// due to I/O failures or invalid index states (e.g., unsorted entries). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(42) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.add("file.txt".to_string())?; - /// let tree_id = index.write_tree()?; - /// assert_eq!(tree_id, 42); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn write_tree(&self) -> Result; - /// Reads a tree into the index. - /// - /// # How it works - /// Clears the current index state and populates it with the entries from the - /// specified tree object. This is commonly used during `checkout` or `reset` - /// operations to synchronize the staging area with a specific commit's state. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the tree cannot be found or if the index cannot be - /// mutated (e.g., I/O errors). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::index::Index; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockIndex { data: HashMap } - /// # impl Index for MockIndex { - /// # type Entry = String; type Path = String; type TreeId = u32; - /// # fn add(&mut self, e: Self::Entry) -> Result<(), VctrlError> { self.data.insert(e.clone(), e); Ok(()) } - /// # fn remove(&mut self, p: &Self::Path) -> Result<(), VctrlError> { self.data.remove(p); Ok(()) } - /// # fn clear(&mut self) -> Result<(), VctrlError> { self.data.clear(); Ok(()) } - /// # fn get(&self, p: &Self::Path) -> Result, VctrlError> { Ok(self.data.get(p).cloned()) } - /// # fn contains(&self, p: &Self::Path) -> Result { Ok(self.data.contains_key(p)) } - /// # fn len(&self) -> Result { Ok(self.data.len()) } - /// # fn entries(&self) -> Result, VctrlError> { Ok(self.data.values().cloned().collect()) } - /// # fn write_tree(&self) -> Result { Ok(1) } - /// # fn read_tree(&mut self, _t: &Self::TreeId) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut index = MockIndex::default(); - /// index.read_tree(&99)?; - /// assert!(index.is_empty()?); // Mock implementation does not populate - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn read_tree(&mut self, tree: &Self::TreeId) -> Result<(), VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/mod.rs b/libvctrl_handler/src/traits/core/mod.rs index 8b1a09ac..ef5359bd 100644 --- a/libvctrl_handler/src/traits/core/mod.rs +++ b/libvctrl_handler/src/traits/core/mod.rs @@ -1,340 +1,340 @@ -//! Core traits for repository operations. -//! -//! # Architecture -//! This module defines the fundamental contracts required to build a functional -//! version control backend. By segregating these traits into a dedicated `core` -//! module, we establish a strict boundary between abstract domain logic and -//! concrete I/O implementations. -//! -//! # Design Rationale: Dependency Inversion -//! The entire crate operates against these traits, never against concrete types. -//! This allows consumers to inject custom backends (in-memory, disk-based, or -//! network-attached) seamlessly. It also simplifies unit testing, as mock -//! implementations can be substituted without altering the core algorithms. -//! -//! # Bounded Contexts -//! Each submodule represents a distinct bounded context within the Git architecture: -//! - **Storage**: [`object_store`], [`pack`] -//! - **State**: [`ref_store`], [`reflog`], [`index`] -//! - **Serialization**: [`encoder`], [`decoder`], [`hasher`] -//! - **Analysis**: [`diff`], [`blame`], [`revwalk`] -//! - **Security**: [`signer`], [`verifier`] -//! - **Networking**: [`remote`], [`transport`] -//! - **Configuration**: [`config`] -//! -//! # Examples -//! *Note: The following example assumes this crate is named `libvctrl_handler`.* -//! -//! ``` -//! # use libvctrl_handler::traits::core::{ -//! # blame, config, decoder, diff, encoder, hasher, index, object_store, -//! # pack, ref_store, reflog, remote, revwalk, signer, transport, verifier, -//! # }; -//! // All core trait modules are publicly accessible. -//! ``` - -/// Blame computation trait. -/// -/// # Why this exists -/// Provides the contract for attributing lines in a file to specific commits. -/// This is separated from standard diffing because blame requires traversing -/// history and tracking line movements across revisions, which is computationally -/// distinct from simple tree-to-tree comparisons. -/// -/// # How it works -/// Implementors will analyze the history of a given path and return a sequence -/// of [`BlameEntry`](blame::BlameEntry) items, mapping line ranges to commits. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::blame; -/// // The blame submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod blame; -/// Configuration store trait. -/// -/// # Why this exists -/// Abstracts the reading and writing of repository configuration (e.g., `.git/config`). -/// Decoupling this allows the core engine to query settings (like user name or -/// signing keys) without being tied to a specific file format or key-value backend. -/// -/// # How it works -/// Defines a key-value interface segmented by sections, enabling persistent -/// configuration management across different storage mediums. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::config; -/// // The config submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod config; -/// Object decoder trait. -/// -/// # Why this exists -/// Defines the contract for deserializing raw bytes into strongly-typed Git objects -/// (e.g., [`Blob`](crate::Blob), [`Tree`](crate::Tree)). This abstraction allows -/// the engine to support multiple wire formats or compression algorithms. -/// -/// # How it works -/// Implementors read from a generic `std::io::Read` source, parse the headers -/// and payloads, and construct the corresponding domain types, enforcing structural -/// validity during the process. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::decoder; -/// // The decoder submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + + pub mod decoder; -/// Tree differencing trait. -/// -/// # Why this exists -/// Provides the contract for computing the delta between two tree objects. -/// Separating this logic allows for different diffing algorithms (e.g., Myers, -/// patience) to be plugged in without modifying the core comparison logic. -/// -/// # How it works -/// Accepts two tree identifiers and returns a [`TreeDelta`](crate::TreeDelta), -/// enumerating all added, deleted, or modified entries between the two states. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::diff; -/// // The diff submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod diff; -/// Object encoder trait. -/// -/// # Why this exists -/// Defines the contract for serializing strongly-typed Git objects into raw bytes. -/// This is the inverse of the [`decoder`] module, ensuring that objects can be -/// written to disk or transmitted over the network in a standardized format. -/// -/// # How it works -/// Implementors write the canonical Git representation of the object to a generic -/// `std::io::Write` destination, handling headers and payload formatting. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::encoder; -/// // The encoder submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod encoder; -/// Hashing trait. -/// -/// # Why this exists -/// Abstracts the cryptographic hashing mechanism. While Git traditionally uses -/// SHA-1 or SHA-256, this trait allows the engine to support arbitrary hash -/// functions or custom hashing contexts. -/// -/// # How it works -/// Reads data from a generic `std::io::Read` source and computes the final -/// [`Hash`](crate::Hash) digest, ensuring that the object's content matches its -/// identifier. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::hasher; -/// // The hasher submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + + pub mod hasher; -/// Index (staging area) trait. -/// -/// # Why this exists -/// Defines the contract for managing the staging area between the working directory -/// and the object database. This abstraction is crucial for orchestrating commits -/// and tracking file states. -/// -/// # How it works -/// Provides methods to add, remove, and query entries by path, and to serialize -/// the staged state into a tree object ready for committing. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::index; -/// // The index submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod index; -/// Object storage trait. -/// -/// # Why this exists -/// Provides the fundamental contract for storing and retrieving content-addressed -/// objects. This is the backbone of the version control system, allowing backends -/// to use plain directories, packed files, or databases. -/// -/// # How it works -/// Defines `put`, `get`, `delete`, and `exists` operations keyed by [`Hash`](crate::Hash), -/// ensuring that object retrieval is opaque to the caller. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::object_store; -/// // The object_store submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod object_store; -/// Pack file reader/writer traits. -/// -/// # Why this exists -/// Packfiles are Git's compressed archive format for objects. This module defines -/// contracts for both writing and reading packfiles, isolating the complex -/// delta-compression and indexing logic from the standard object store. -/// -/// # How it works -/// The writer trait handles object insertion and finalization, while the reader -/// trait provides random access to objects within the pack via their identifiers. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::pack; -/// // The pack submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod pack; -/// Reference store trait. -/// -/// # Why this exists -/// Abstracts the management of symbolic references (branches, tags, HEAD). -/// Decoupling this allows the engine to manage mutable state independently of -/// the immutable object database. -/// -/// # How it works -/// Defines operations to set, get, delete, and list references, mapping human-readable -/// names to [`Hash`](crate::Hash) values. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::ref_store; -/// // The ref_store submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod ref_store; -/// Reflog store trait. -/// -/// # Why this exists -/// Provides the contract for recording the history of reference updates. -/// Reflogs are essential for recovering from mistakes and tracking branch movement. -/// -/// # How it works -/// Appends timestamped entries to a reference's log and retrieves them, ensuring -/// that the chronological history of repository mutations is preserved. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::reflog; -/// // The reflog submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + pub mod reflog; -/// Remote repository trait. -/// -/// # Why this exists -/// Defines the contract for interacting with remote repositories. -/// This abstraction normalizes operations like fetching and pushing across -/// different protocols (e.g., HTTP, SSH, Git). -/// -/// # How it works -/// Manages refspecs and remote references, coordinating the transfer of objects -/// and updates between local and remote states. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::remote; -/// // The remote submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod remote; -/// Revision walking trait. -/// -/// # Why this exists -/// Provides the contract for traversing the commit graph. -/// Walking history is a fundamental operation for log generation, bisecting, -/// and ancestry queries. -/// -/// # How it works -/// Returns a lazy iterator over commit identifiers starting from a given point, -/// allowing efficient traversal without loading the entire graph into memory. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::revwalk; -/// // The revwalk submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod revwalk; -/// Signing trait. -/// -/// # Why this exists -/// Abstracts the cryptographic signing of data (e.g., commits or tags). -/// This allows the engine to support various signing backends (GPG, SSH, X.509) -/// without hardcoding the cryptographic primitives. -/// -/// # How it works -/// Accepts a key identifier and raw data, returning a cryptographic signature -/// that can be appended to the object. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::signer; -/// // The signer submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod signer; -/// Transport trait. -/// -/// # Why this exists -/// Defines the low-level contract for sending and receiving raw Git objects -/// over a network. This is distinct from the [`remote`] module, which handles -/// higher-level repository semantics. -/// -/// # How it works -/// Provides simple fetch and push primitives based on object hashes, acting as -/// the pipe between local and remote object stores. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::transport; -/// // The transport submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod transport; -/// Verification trait. -/// -/// # Why this exists -/// Abstracts the verification of cryptographic signatures. It is the counterpart -/// to the [`signer`] module, ensuring that objects can be authenticated against -/// trusted keys. -/// -/// # How it works -/// Accepts a key identifier, raw data, and a signature, returning a boolean -/// indicating the validity of the signature. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::traits::core::verifier; -/// // The verifier submodule is accessible. -/// ``` + + + + + + + + + + + + + + + + + pub mod verifier; diff --git a/libvctrl_handler/src/traits/core/object_store.rs b/libvctrl_handler/src/traits/core/object_store.rs index f11beb82..14bb0c08 100644 --- a/libvctrl_handler/src/traits/core/object_store.rs +++ b/libvctrl_handler/src/traits/core/object_store.rs @@ -1,243 +1,243 @@ -//! Object storage trait. -//! -//! # Architecture -//! This module defines the abstract contract for a Content-Addressable Storage (CAS) -//! backend. In a CAS system, the identifier of an object is derived directly from its -//! content (typically via a cryptographic hash). This trait abstracts the underlying -//! storage mechanism, allowing the engine to use loose files on disk, packed objects, -//! or entirely in-memory representations. -//! -//! # Design Rationale: Streaming I/O -//! The `get` method returns a `Box` rather than a `Vec` or `&[u8]`. -//! This is a critical architectural decision for performance and memory safety. Git -//! objects, particularly blobs, can be gigabytes in size. Loading an entire object -//! into memory could cause severe memory fragmentation and potential out-of-memory -//! (OOM) errors. By returning a reader, the storage backend allows the caller to -//! stream the data in fixed-size chunks, maintaining a constant memory footprint -//! regardless of the object's size. + + + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::Hash; use std::io::Read; -/// A trait for storing and retrieving Git objects. -/// -/// # Why this exists -/// Provides the fundamental contract for interacting with the Git object database. -/// By using a trait, the crate decouples the core VCS logic from the specific I/O -/// backend. This allows consumers to inject custom backends (e.g., S3 storage, -/// encrypted databases, or mock memory stores for testing) without altering the -/// core algorithms. -/// -/// # How it works -/// The store maps [`Hash`] keys to raw byte payloads. Write operations (`put`, -/// `delete`) require `&mut self`, enforcing exclusive access to prevent data races -/// during mutations. Read operations (`get`, `exists`) take `&self`, allowing -/// highly concurrent parallel reads across multiple threads. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync`. Object storage is frequently accessed by -/// multiple concurrent operations (e.g., packing objects, resolving diffs, checking -/// out files). The `Send + Sync` bound guarantees that the implementor is thread-safe, -/// enabling the engine to parallelize object retrieval without external synchronization. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory store: -/// -/// ``` -/// # use std::io::Read; -/// # use libvctrl_handler::traits::core::object_store::ObjectStore; -/// # use libvctrl_handler::{Hash, VctrlError}; -/// # use std::collections::HashMap; -/// # use std::io::Cursor; -/// # -/// #[derive(Default)] -/// struct MockStore { -/// data: HashMap>, -/// } -/// -/// impl ObjectStore for MockStore { -/// fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { -/// self.data.insert(*hash, data.to_vec()); -/// Ok(()) -/// } -/// -/// fn get(&self, hash: &Hash) -> Result, VctrlError> { -/// match self.data.get(hash) { -/// Some(data) => Ok(Box::new(Cursor::new(data.clone()))), -/// None => Err(VctrlError::ObjectNotFound(*hash)), -/// } -/// } -/// -/// fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError> { -/// self.data.remove(hash); -/// Ok(()) -/// } -/// -/// fn exists(&self, hash: &Hash) -> Result { -/// Ok(self.data.contains_key(hash)) -/// } -/// } -/// -/// let mut store = MockStore::default(); -/// let hash = Hash::from_bytes(&[0_u8; 64])?; -/// store.put(&hash, b"blob content")?; -/// assert!(store.exists(&hash)?); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait ObjectStore: Send + Sync { - /// Stores an object under the given hash. - /// - /// # How it works - /// Accepts a reference to the [`Hash`] and a byte slice of the object's raw, - /// uncompressed content. The implementor is responsible for persisting this - /// data (e.g., writing to disk, compressing into a packfile, or inserting - /// into a database). Requires `&mut self` as it mutates the underlying storage. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage fails (e.g., disk full, - /// permission denied) or if the data violates storage constraints. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Read; - /// # use libvctrl_handler::traits::core::object_store::ObjectStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::Cursor; - /// # #[derive(Default)] - /// # struct MockStore { data: HashMap> } - /// # impl ObjectStore for MockStore { - /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } - /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } - /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } - /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } - /// # } - /// let mut store = MockStore::default(); - /// let hash = Hash::from_bytes(&[1u8; 64])?; - /// store.put(&hash, b"new data")?; - /// assert!(store.exists(&hash)?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn put(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; - /// Retrieves an object by hash, returning a reader. - /// - /// # How it works - /// Looks up the object by its [`Hash`] and returns a boxed reader. The reader - /// abstracts the underlying storage medium (file handle, network socket, or - /// memory cursor). The lifetime `'_` ties the returned reader to the lifetime - /// of the `ObjectStore` instance, ensuring the underlying storage remains valid - /// while the stream is active. This prevents loading large objects into memory - /// all at once. - /// - /// # Errors - /// - /// Returns [`VctrlError::ObjectNotFound`] if the hash does not exist in the store. - /// Returns [`VctrlError`] if an I/O error occurs while initializing the stream. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::object_store::ObjectStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::{Cursor, Read}; - /// # #[derive(Default)] - /// # struct MockStore { data: HashMap> } - /// # impl ObjectStore for MockStore { - /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } - /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } - /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } - /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } - /// # } - /// let mut store = MockStore::default(); - /// let hash = Hash::from_bytes(&[2u8; 64])?; - /// store.put(&hash, b"readable data")?; - /// - /// let mut reader = store.get(&hash)?; - /// let mut content = String::new(); - /// reader.read_to_string(&mut content)?; - /// assert_eq!(content, "readable data"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get(&self, hash: &Hash) -> Result, VctrlError>; - /// Deletes an object by hash. - /// - /// # How it works - /// Locates the object by its [`Hash`] and removes it from the underlying storage. - /// If the object does not exist, this operation is typically idempotent and - /// returns `Ok(())`, preventing spurious errors during garbage collection. - /// Requires `&mut self` to enforce exclusive access during mutation. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be modified (e.g., - /// file permission issues or read-only filesystem). - /// - /// # Examples - /// - /// ``` - /// # use std::io::Read; - /// # use libvctrl_handler::traits::core::object_store::ObjectStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::Cursor; - /// # #[derive(Default)] - /// # struct MockStore { data: HashMap> } - /// # impl ObjectStore for MockStore { - /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } - /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } - /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } - /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } - /// # } - /// let mut store = MockStore::default(); - /// let hash = Hash::from_bytes(&[3u8; 64])?; - /// store.put(&hash, b"to be deleted")?; - /// store.delete(&hash)?; - /// assert!(!store.exists(&hash)?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn delete(&mut self, hash: &Hash) -> Result<(), VctrlError>; - /// Checks whether an object exists. - /// - /// # How it works - /// Performs a lightweight existence check without retrieving the object's data - /// or initializing a stream. This is significantly faster than calling `get` - /// and checking for `ObjectNotFound`, especially on network-backed storage. - /// Takes `&self` to allow concurrent existence checks. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be queried (e.g., - /// an I/O error while listing directory contents). - /// - /// # Examples - /// - /// ``` - /// # use std::io::Read; - /// # use libvctrl_handler::traits::core::object_store::ObjectStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::Cursor; - /// # #[derive(Default)] - /// # struct MockStore { data: HashMap> } - /// # impl ObjectStore for MockStore { - /// # fn put(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { self.data.insert(*h, d.to_vec()); Ok(()) } - /// # fn get(&self, h: &Hash) -> Result, VctrlError> { match self.data.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } } - /// # fn delete(&mut self, h: &Hash) -> Result<(), VctrlError> { self.data.remove(h); Ok(()) } - /// # fn exists(&self, h: &Hash) -> Result { Ok(self.data.contains_key(h)) } - /// # } - /// let store = MockStore::default(); - /// let hash = Hash::from_bytes(&[4u8; 64])?; - /// // Check a missing object - /// assert!(!store.exists(&hash)?); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn exists(&self, hash: &Hash) -> Result; } diff --git a/libvctrl_handler/src/traits/core/pack.rs b/libvctrl_handler/src/traits/core/pack.rs index 3a39535a..78e64767 100644 --- a/libvctrl_handler/src/traits/core/pack.rs +++ b/libvctrl_handler/src/traits/core/pack.rs @@ -1,231 +1,231 @@ -//! Pack file reader/writer traits. -//! -//! # Architecture -//! Packfiles are Git's highly compressed archive format for storing multiple objects. -//! This module defines the contracts for both writing and reading packfiles, isolating -//! the complex delta-compression and indexing logic from the standard object store. -//! -//! # Design Rationale: Streaming I/O -//! Packfiles can contain thousands of objects and span gigabytes. The reader trait -//! returns a `Box` rather than a `Vec`. This is a critical architectural -//! decision: it forces streaming deserialization. It allows the engine to resolve -//! deltas and decompress zlib streams on the fly, maintaining a constant memory -//! footprint regardless of the packfile's total size. + + + + + + + + + + + + + use crate::errors::VctrlError; use std::io::Read; -/// Trait for writing Git pack files. -/// -/// # Why this exists -/// Provides the contract for building a packfile. Packfiles are essential for -/// network transfers and repository garbage collection, as they compress objects -/// using delta encoding to save space. Abstracting this into a trait allows the -/// crate to support different compression levels or custom delta algorithms. -/// -/// # How it works -/// The writer maintains internal state, tracking the offsets of each written object -/// to build a final index. As objects are written via `write_object`, the implementor -/// compresses the data and appends it to the underlying stream. The `finish` method -/// is required to flush any remaining buffers, write the packfile trailer, and -/// finalize the corresponding index file. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory writer: -/// -/// ``` -/// # use libvctrl_handler::traits::core::pack::PackWriter; -/// # use libvctrl_handler::VctrlError; -/// # use std::collections::HashMap; -/// # -/// struct MockPackWriter { -/// objects: HashMap, Vec>, -/// } -/// -/// impl PackWriter for MockPackWriter { -/// type ObjectId = Vec; -/// -/// fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { -/// self.objects.insert(id.clone(), data.to_vec()); -/// Ok(()) -/// } -/// -/// fn finish(&mut self) -> Result<(), VctrlError> { -/// // In a real impl, this would write the checksum and flush the stream. -/// Ok(()) -/// } -/// } -/// -/// let mut writer = MockPackWriter { objects: HashMap::new() }; -/// writer.write_object(&vec![1, 2, 3], b"blob data")?; -/// writer.finish()?; -/// assert_eq!(writer.objects.len(), 1); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait PackWriter: Send + Sync { - /// The object identifier type. - /// - /// # Why this exists - /// Allows the writer backend to define its own representation of an object hash, - /// ensuring compatibility with the associated `ObjectStore` implementation. + + + + + type ObjectId: Send + Sync; - /// Writes an object to the pack. - /// - /// # How it works - /// Accepts an identifier and the raw, uncompressed byte slice of the object. - /// The implementor is responsible for compressing the data (e.g., using zlib), - /// calculating offsets, and potentially encoding the object as a delta against - /// a previously written base object. Requires `&mut self` because writing - /// mutates the packfile's internal offset tracker and compression state. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if an I/O error occurs during writing or if the - /// compression algorithm fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::pack::PackWriter; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # struct MockPackWriter { objects: HashMap, Vec> } - /// # impl PackWriter for MockPackWriter { - /// # type ObjectId = Vec; - /// # fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { - /// # self.objects.insert(id.clone(), data.to_vec()); Ok(()) - /// # } - /// # fn finish(&mut self) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut writer = MockPackWriter { objects: HashMap::new() }; - /// writer.write_object(&vec![0_u8; 20], b"data")?; - /// assert!(writer.objects.contains_key(&vec![0_u8; 20])); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError>; - /// Finishes writing the pack file. - /// - /// # How it works - /// This method must be called exactly once after all objects have been written. - /// It flushes any remaining data in the compression buffers, writes the 20-byte - /// SHA-1 trailer for the packfile, and finalizes the index. Failing to call this - /// method will result in a corrupted, unreadable packfile. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying stream cannot be flushed or if the - /// final checksum calculation fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::pack::PackWriter; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # struct MockPackWriter { objects: HashMap, Vec> } - /// # impl PackWriter for MockPackWriter { - /// # type ObjectId = Vec; - /// # fn write_object(&mut self, id: &Self::ObjectId, data: &[u8]) -> Result<(), VctrlError> { - /// # self.objects.insert(id.clone(), data.to_vec()); Ok(()) - /// # } - /// # fn finish(&mut self) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut writer = MockPackWriter { objects: HashMap::new() }; - /// assert!(writer.finish().is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn finish(&mut self) -> Result<(), VctrlError>; } -/// Trait for reading Git pack files. -/// -/// # Why this exists -/// Provides the contract for random access reading of objects within a packfile. -/// By abstracting this, the crate allows backends to use memory-mapped files, -/// direct file I/O, or entirely in-memory representations for testing. -/// -/// # Design Rationale: `&self` and Thread Safety -/// The trait requires `&self` for `read_object` (not `&mut self`). This is crucial -/// for concurrency. Packfiles are immutable once written. By taking an immutable -/// reference, multiple threads can safely read different objects from the same -/// packfile concurrently without requiring external locking. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory reader: -/// -/// ``` -/// # use libvctrl_handler::traits::core::pack::PackReader; -/// # use libvctrl_handler::VctrlError; -/// # use std::collections::HashMap; -/// # use std::io::{Cursor, Read}; -/// # -/// struct MockPackReader { -/// objects: HashMap, Vec>, -/// } -/// -/// impl PackReader for MockPackReader { -/// type ObjectId = Vec; -/// -/// fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError> { -/// let data = self.objects.get(id).cloned().unwrap_or_default(); -/// Ok(Box::new(Cursor::new(data))) -/// } -/// } -/// -/// let reader = MockPackReader { objects: HashMap::from([(vec![1], b"data".to_vec())]) }; -/// let mut r = reader.read_object(&vec![1])?; -/// let mut buf = String::new(); -/// r.read_to_string(&mut buf)?; -/// assert_eq!(buf, "data"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait PackReader: Send + Sync { - /// The object identifier type. - /// - /// # Why this exists - /// Matches the identifier type used by the corresponding `PackWriter` and - /// `ObjectStore`, ensuring type-safe lookups across the storage layer. + + + + + type ObjectId: Send + Sync; - /// Reads an object from the pack, returning a reader. - /// - /// # How it works - /// Looks up the object's offset in the packfile index, seeks to that position, - /// and returns a boxed reader. The returned reader handles zlib decompression - /// and, if the object is stored as a delta, resolves the delta against its base - /// object lazily as bytes are read. The lifetime `'_` ties the returned reader - /// to the lifetime of the `PackReader` instance, ensuring the underlying file - /// handle or memory mapping remains valid. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the object is not found in the pack, if the - /// data is corrupted, or if an I/O error occurs while seeking or reading. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::pack::PackReader; - /// # use libvctrl_handler::VctrlError; - /// # use std::collections::HashMap; - /// # use std::io::{Cursor, Read}; - /// # struct MockPackReader { objects: HashMap, Vec> } - /// # impl PackReader for MockPackReader { - /// # type ObjectId = Vec; - /// # fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError> { - /// # let data = self.objects.get(id).cloned().unwrap_or_default(); - /// # Ok(Box::new(Cursor::new(data))) - /// # } - /// # } - /// let reader = MockPackReader { objects: HashMap::new() }; - /// let result = reader.read_object(&vec![1, 2, 3]); - /// // Mock returns empty cursor for missing keys, but real impls return ObjectNotFound. - /// assert!(result.is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn read_object(&self, id: &Self::ObjectId) -> Result, VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/ref_store.rs b/libvctrl_handler/src/traits/core/ref_store.rs index fe685f94..f47c85df 100644 --- a/libvctrl_handler/src/traits/core/ref_store.rs +++ b/libvctrl_handler/src/traits/core/ref_store.rs @@ -1,251 +1,251 @@ -//! Reference store trait. -//! -//! # Architecture -//! This module defines the abstract contract for managing Git references (branches, -//! tags, HEAD). In Git's architecture, the object database is strictly immutable, -//! while references provide the mutable pointers that track the current state of -//! branches and tags. By isolating reference management into a dedicated trait, -//! the crate decouples state mutations from content storage. -//! -//! # Design Rationale: Lazy Iteration -//! The [`RefStore::list_refs`] method returns a custom associated iterator type -//! (`type RefsIterator`) rather than a `Vec`. This is a critical architectural -//! decision for scalability. Repositories like the Linux kernel contain millions of -//! references. Returning a `Vec` would require loading all names into memory -//! simultaneously, risking out-of-memory (OOM) errors. By returning an iterator, -//! backends can stream reference names lazily from disk or a database cursor, -//! maintaining a constant memory footprint. + + + + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::Hash; -/// A trait for managing Git references (branches, tags, etc.). -/// -/// # Why this exists -/// Provides a unified, type-safe interface for mutating and querying repository -/// state. Git references map human-readable names (e.g., `refs/heads/main`) to -/// cryptographic hashes. This trait enforces that structure, allowing the core -/// engine to orchestrate branch updates, tag creation, and HEAD detachments -/// without being tied to a specific filesystem layout or database backend. -/// -/// # How it works -/// The store maintains a mapping between reference names and [`Hash`] values. -/// Write operations (`set_ref`, `delete_ref`) require `&mut self`, enforcing -/// exclusive access at the Rust type level. This mimics Git's `.lock` files, -/// preventing race conditions where two concurrent processes try to update the -/// same branch. Read operations (`get_ref`, `list_refs`) take `&self`, allowing -/// highly concurrent parallel reads across multiple threads. -/// -/// # Design Rationale: Thread Safety -/// The trait requires `Send + Sync`. Reference resolution is one of the most -/// frequent operations in Git (e.g., during revision walks or merge analysis). -/// By enforcing thread safety, the engine can parallelize operations that -/// require resolving multiple refs without requiring external locking mechanisms. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory store: -/// -/// ``` -/// # use libvctrl_handler::traits::core::ref_store::RefStore; -/// # use libvctrl_handler::{Hash, VctrlError}; -/// # use std::collections::HashMap; -/// # -/// #[derive(Default)] -/// struct MockRefStore { -/// refs: HashMap, -/// } -/// -/// impl RefStore for MockRefStore { -/// type RefsIterator = std::vec::IntoIter>; -/// -/// fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError> { -/// self.refs.insert(name.to_string(), *hash); -/// Ok(()) -/// } -/// -/// fn get_ref(&self, name: &str) -> Result { -/// self.refs -/// .get(name) -/// .copied() -/// .ok_or_else(|| VctrlError::RefNotFound(name.to_string())) -/// } -/// -/// fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError> { -/// self.refs.remove(name); -/// Ok(()) -/// } -/// -/// fn list_refs(&self) -> Result { -/// let refs: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); -/// Ok(refs.into_iter()) -/// } -/// } -/// -/// let mut store = MockRefStore::default(); -/// let hash = Hash::from_bytes(&[0_u8; 64])?; -/// store.set_ref("refs/heads/main", &hash)?; -/// assert_eq!(store.get_ref("refs/heads/main")?, hash); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait RefStore: Send + Sync { - /// An iterator over reference names. - /// - /// # Why this exists - /// Allows the backend to define its own iteration mechanism. A filesystem backend - /// might yield names lazily via directory traversal, while a database backend - /// might use a cursor. The iterator yields `Result` to gracefully - /// handle I/O errors that may occur mid-iteration (e.g., a permissions error on a - /// specific file). The `Send` bound allows the iterator to be moved across threads. + + + + + + + + type RefsIterator: Iterator> + Send; - /// Sets a reference to the given hash. - /// - /// # How it works - /// Inserts or updates the mapping of `name` to `hash`. If a reference with the - /// given name already exists, it is overwritten. Requires `&mut self` to enforce - /// exclusive access, preventing data races during concurrent branch updates. - /// Implementors should ensure this operation is atomic to prevent repository - /// corruption if the process is interrupted. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage fails to persist the update - /// (e.g., disk full, permission denied) or if the name is invalid. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::ref_store::RefStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockRefStore { refs: HashMap } - /// # impl RefStore for MockRefStore { - /// # type RefsIterator = std::vec::IntoIter>; - /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } - /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } - /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } - /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } - /// # } - /// let mut store = MockRefStore::default(); - /// let hash = Hash::from_bytes(&[1u8; 64])?; - /// store.set_ref("refs/heads/feature", &hash)?; - /// assert!(store.get_ref("refs/heads/feature").is_ok()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn set_ref(&mut self, name: &str, hash: &Hash) -> Result<(), VctrlError>; - /// Gets the hash pointed to by a reference. - /// - /// # How it works - /// Looks up the reference by name and returns the corresponding [`Hash`]. Takes - /// `&self` to allow concurrent reads. If the reference does not exist, it returns - /// an error rather than an `Option`, as a missing reference is typically an - /// exceptional condition in Git operations (e.g., trying to checkout a non-existent - /// branch). - /// - /// # Errors - /// - /// Returns [`VctrlError::RefNotFound`] if the reference name does not exist in the store. - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::ref_store::RefStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockRefStore { refs: HashMap } - /// # impl RefStore for MockRefStore { - /// # type RefsIterator = std::vec::IntoIter>; - /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } - /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } - /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } - /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } - /// # } - /// let mut store = MockRefStore::default(); - /// let hash = Hash::from_bytes(&[2u8; 64])?; - /// store.set_ref("HEAD", &hash)?; - /// assert_eq!(store.get_ref("HEAD")?, hash); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn get_ref(&self, name: &str) -> Result; - /// Deletes a reference. - /// - /// # How it works - /// Removes the mapping for the given `name`. If the reference does not exist, - /// this operation is typically idempotent and returns `Ok(())`, preventing - /// spurious errors during cleanup operations. Requires `&mut self` to enforce - /// exclusive access. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be modified. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::ref_store::RefStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockRefStore { refs: HashMap } - /// # impl RefStore for MockRefStore { - /// # type RefsIterator = std::vec::IntoIter>; - /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } - /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } - /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } - /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } - /// # } - /// let mut store = MockRefStore::default(); - /// let hash = Hash::from_bytes(&[3u8; 64])?; - /// store.set_ref("refs/tags/v1", &hash)?; - /// store.delete_ref("refs/tags/v1")?; - /// assert!(store.get_ref("refs/tags/v1").is_err()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn delete_ref(&mut self, name: &str) -> Result<(), VctrlError>; - /// Lists all reference names. - /// - /// # How it works - /// Returns a custom iterator ([`RefsIterator`](Self::RefsIterator)) that yields - /// reference names. The iterator allows the backend to lazily load references, - /// preventing memory exhaustion in repositories with a massive number of refs. - /// Takes `&self` to allow concurrent listing. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the iterator cannot be initialized (e.g., an I/O - /// error while opening the refs directory). Note that I/O errors occurring - /// *during* iteration are yielded by the iterator itself as `Err(VctrlError)`. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::ref_store::RefStore; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockRefStore { refs: HashMap } - /// # impl RefStore for MockRefStore { - /// # type RefsIterator = std::vec::IntoIter>; - /// # fn set_ref(&mut self, n: &str, h: &Hash) -> Result<(), VctrlError> { self.refs.insert(n.to_string(), *h); Ok(()) } - /// # fn get_ref(&self, n: &str) -> Result { self.refs.get(n).copied().ok_or_else(|| VctrlError::RefNotFound(n.to_string())) } - /// # fn delete_ref(&mut self, n: &str) -> Result<(), VctrlError> { self.refs.remove(n); Ok(()) } - /// # fn list_refs(&self) -> Result { let v: Vec<_> = self.refs.keys().map(|k| Ok(k.clone())).collect(); Ok(v.into_iter()) } - /// # } - /// let mut store = MockRefStore::default(); - /// let hash = Hash::from_bytes(&[4u8; 64])?; - /// store.set_ref("refs/heads/main", &hash)?; - /// store.set_ref("refs/heads/dev", &hash)?; - /// - /// let refs: Vec = store.list_refs()?.filter_map(|r| r.ok()).collect(); - /// assert_eq!(refs.len(), 2); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn list_refs(&self) -> Result; } diff --git a/libvctrl_handler/src/traits/core/reflog.rs b/libvctrl_handler/src/traits/core/reflog.rs index b9d945a0..3ea20a90 100644 --- a/libvctrl_handler/src/traits/core/reflog.rs +++ b/libvctrl_handler/src/traits/core/reflog.rs @@ -1,134 +1,134 @@ -//! Reflog store trait. -//! -//! # Architecture -//! This module defines the abstract contract for managing reference logs (reflogs). -//! Reflogs act as an append-only audit trail, recording every mutation to a reference -//! (e.g., commits, resets, checkouts). This history is crucial for recovering from -//! accidental operations and for garbage collection pruning. -//! -//! # Design Rationale: Strict Append-Only Semantics -//! The trait exposes only `append` and `entries` methods. There is no `delete` or -//! `update` operation for individual entries. This enforces the append-only nature -//! of reflogs at the type level, preventing consumers from accidentally rewriting -//! audit history. + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::{Hash, ReflogEntry}; -/// Trait for managing reflogs. -/// -/// # Why this exists -/// Provides a unified interface for recording and retrieving the history of -/// reference updates. By abstracting this into a trait, the crate allows the core -/// engine to track state changes without being tied to the standard `.git/logs` -/// filesystem layout. Consumers can inject in-memory reflogs for testing or -/// database-backed reflogs for enterprise persistence. -/// -/// # How it works -/// The store maintains a mapping between reference names and a chronological list -/// of [`ReflogEntry`] items. The `append` method requires `&mut self` to enforce -/// exclusive access, ensuring that concurrent updates to the same reference's -/// reflog do not interleave and corrupt the history file. The `entries` method -/// takes `&self`, allowing safe, concurrent reads of the audit trail. -/// -/// # Design Rationale: `Vec` over Iterators -/// Unlike [`RefStore::list_refs`](crate::traits::core::ref_store::RefStore::list_refs), -/// which returns an iterator to handle millions of refs, `entries` returns a `Vec`. -/// Reflogs are bounded in size (e.g., Git defaults to 90 days or 250 entries). The -/// memory footprint of loading a single reference's reflog is strictly bounded, -/// making a `Vec` more ergonomic and efficient than a streaming iterator. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory store: -/// -/// ``` -/// # use libvctrl_handler::traits::core::reflog::ReflogStore; -/// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; -/// # use std::collections::HashMap; -/// # -/// #[derive(Default)] -/// struct MockReflogStore { -/// logs: HashMap>, -/// } -/// -/// impl ReflogStore for MockReflogStore { -/// type RefName = String; -/// -/// fn append( -/// &mut self, -/// reference: &Self::RefName, -/// old_hash: Option, -/// new_hash: Option, -/// reason: &str, -/// timestamp: i64, -/// timezone_offset: i16, -/// ) -> Result<(), VctrlError> { -/// let entry = ReflogEntry::new( -/// old_hash, -/// new_hash, -/// reason.to_string(), -/// timestamp, -/// timezone_offset, -/// )?; -/// self.logs.entry(reference.clone()).or_default().push(entry); -/// Ok(()) -/// } -/// -/// fn entries(&self, reference: &Self::RefName) -> Result, VctrlError> { -/// Ok(self.logs.get(reference).cloned().unwrap_or_default()) -/// } -/// } -/// -/// let mut store = MockReflogStore::default(); -/// let hash = Hash::from_bytes(&[0_u8; 64])?; -/// store.append(&"refs/heads/main".to_string(), None, Some(hash), "initial commit", 0, 0)?; -/// assert_eq!(store.entries(&"refs/heads/main".to_string())?.len(), 1); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait ReflogStore: Send + Sync { - /// The reference name type. - /// - /// # Why this exists - /// Decouples the reference name representation from the trait. While typically - /// a `String`, this allows backends to use interned strings or specialized - /// path types, ensuring interoperability with the associated [`RefStore`](crate::traits::core::ref_store::RefStore). + + + + + + type RefName: Send + Sync; - /// Appends an entry to the reflog for a reference. - /// - /// # How it works - /// Creates a new [`ReflogEntry`] with the provided transition (`old_hash` to - /// `new_hash`), reason, and timestamp metadata. The entry is appended to the - /// end of the reference's log. Requires `&mut self` to enforce exclusive access, - /// mimicking the behavior of acquiring a `.lock` file on the reflog. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidTimezoneOffset`] if the `timezone_offset` is - /// out of the valid range (-1440 to 1440). Returns [`VctrlError`] if the - /// underlying storage fails to persist the new entry. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::reflog::ReflogStore; - /// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockReflogStore { logs: HashMap> } - /// # impl ReflogStore for MockReflogStore { - /// # type RefName = String; - /// # fn append(&mut self, r: &Self::RefName, o: Option, n: Option, re: &str, t: i64, tz: i16) -> Result<(), VctrlError> { - /// # let e = ReflogEntry::new(o, n, re.to_string(), t, tz)?; self.logs.entry(r.clone()).or_default().push(e); Ok(()) - /// # } - /// # fn entries(&self, r: &Self::RefName) -> Result, VctrlError> { Ok(self.logs.get(r).cloned().unwrap_or_default()) } - /// # } - /// let mut store = MockReflogStore::default(); - /// let hash = Hash::from_bytes(&[1u8; 64])?; - /// store.append(&"HEAD".to_string(), None, Some(hash), "checkout", 100, 0)?; - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn append( &mut self, reference: &Self::RefName, @@ -139,38 +139,38 @@ pub trait ReflogStore: Send + Sync { timezone_offset: i16, ) -> Result<(), VctrlError>; - /// Returns all reflog entries for a reference. - /// - /// # How it works - /// Retrieves the complete chronological history of updates for the specified - /// reference. The entries are returned in a `Vec` ordered from oldest to newest. - /// If the reference has no reflog (e.g., a newly created branch without commits), - /// an empty `Vec` is returned. Takes `&self` to allow concurrent reads of the - /// audit trail. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the underlying storage cannot be read. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::reflog::ReflogStore; - /// # use libvctrl_handler::{Hash, ReflogEntry, VctrlError}; - /// # use std::collections::HashMap; - /// # #[derive(Default)] - /// # struct MockReflogStore { logs: HashMap> } - /// # impl ReflogStore for MockReflogStore { - /// # type RefName = String; - /// # fn append(&mut self, r: &Self::RefName, o: Option, n: Option, re: &str, t: i64, tz: i16) -> Result<(), VctrlError> { - /// # let e = ReflogEntry::new(o, n, re.to_string(), t, tz)?; self.logs.entry(r.clone()).or_default().push(e); Ok(()) - /// # } - /// # fn entries(&self, r: &Self::RefName) -> Result, VctrlError> { Ok(self.logs.get(r).cloned().unwrap_or_default()) } - /// # } - /// let store = MockReflogStore::default(); - /// let entries = store.entries(&"refs/heads/nonexistent".to_string())?; - /// assert!(entries.is_empty()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn entries(&self, reference: &Self::RefName) -> Result, VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/remote.rs b/libvctrl_handler/src/traits/core/remote.rs index 9df76fdc..1e2a996e 100644 --- a/libvctrl_handler/src/traits/core/remote.rs +++ b/libvctrl_handler/src/traits/core/remote.rs @@ -1,196 +1,196 @@ -//! Remote repository trait. -//! -//! # Architecture -//! This module defines the abstract contract for interacting with remote repositories. -//! It abstracts the complex orchestration of network protocols (e.g., HTTP, SSH, Git) -//! into a unified interface. By using this trait, the core engine can execute fetch -//! and push operations without being coupled to the underlying transport mechanism -//! or wire protocol. -//! -//! # Design Rationale: Associated Types vs. Generics -//! The trait uses associated types (`type RefSpec`, `type RemoteRef`) rather than -//! generic parameters. This design ties the data representations directly to the -//! specific `Remote` implementation. An HTTP backend might parse refspecs into -//! structured objects, while a custom binary protocol might use raw byte slices. -//! This prevents type mismatches at compile time and simplifies the API by removing -//! the need for verbose generic annotations at every call site. + + + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// Trait for interacting with remote repositories. -/// -/// # Why this exists -/// Provides a high-level interface for synchronizing state between a local -/// repository and a remote endpoint. It encapsulates the logic for discovering -/// remote references, fetching missing objects, and pushing local history. -/// Abstracting this into a trait allows the crate to support multiple remote -/// backends (e.g., standard Git, custom distributed ledgers) seamlessly. -/// -/// # How it works -/// The trait defines three core operations: -/// - `list_refs`: Queries the remote for its current reference state. -/// - `fetch`: Downloads objects specified by refspecs and updates local remote-tracking branches. -/// - `push`: Uploads local objects and updates remote references. -/// -/// # Design Rationale: Mutability Split -/// `list_refs` takes `&self` because it is a pure query operation that does not -/// alter the local or remote state; multiple threads can safely list refs concurrently. -/// Conversely, `fetch` and `push` take `&mut self`. These operations fundamentally -/// mutate state (updating local object stores or remote refs) and often require -/// sequential, exclusive access to network streams and internal buffers to prevent -/// data corruption or race conditions. -/// -/// # Examples -/// -/// Implementing the trait for a mock remote backend: -/// -/// ``` -/// # use libvctrl_handler::traits::core::remote::Remote; -/// # use libvctrl_handler::VctrlError; -/// # -/// #[derive(Default)] -/// struct MockRemote { -/// refs: Vec, -/// } -/// -/// impl Remote for MockRemote { -/// type RefSpec = String; -/// type RemoteRef = String; -/// -/// fn list_refs(&self) -> Result, VctrlError> { -/// Ok(self.refs.clone()) -/// } -/// -/// fn fetch(&mut self, _refspecs: &[Self::RefSpec]) -> Result<(), VctrlError> { -/// // Mock fetch: no-op -/// Ok(()) -/// } -/// -/// fn push(&mut self, _refspecs: &[Self::RefSpec]) -> Result<(), VctrlError> { -/// // Mock push: no-op -/// Ok(()) -/// } -/// } -/// -/// let remote = MockRemote::default(); -/// assert!(remote.list_refs().is_ok()); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Remote: Send + Sync { - /// The refspec type. - /// - /// # Why this exists - /// Decouples the refspec representation from the trait. A refspec defines the - /// mapping between remote and local references (e.g., `refs/heads/*:refs/remotes/origin/*`). - /// Allowing backends to define their own type enables protocol-specific optimizations - /// or pre-parsed structures. + + + + + + + type RefSpec: Send + Sync; - /// The remote reference type. - /// - /// # Why this exists - /// Defines the structure of a reference as advertised by the remote. This might - /// include the hash, the name, and additional capabilities (e.g., symref targets) - /// negotiated during the protocol handshake. + + + + + + type RemoteRef: Send + Sync; - /// Lists references available on the remote. - /// - /// # How it works - /// Connects to the remote (or queries a cached advertisement) and retrieves - /// a list of all references (branches, tags) that the remote currently possesses. - /// Takes `&self` as this is a read-only operation that should be safe to call - /// concurrently. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the network connection fails, the remote is - /// unreachable, or the protocol handshake fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::remote::Remote; - /// # use libvctrl_handler::VctrlError; - /// # #[derive(Default)] - /// # struct MockRemote { refs: Vec } - /// # impl Remote for MockRemote { - /// # type RefSpec = String; type RemoteRef = String; - /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } - /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let remote = MockRemote { refs: vec!["refs/heads/main".to_string()] }; - /// let refs = remote.list_refs()?; - /// assert_eq!(refs.len(), 1); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn list_refs(&self) -> Result, VctrlError>; - /// Fetches objects according to the given refspecs. - /// - /// # How it works - /// Takes a slice of refspecs and negotiates with the remote to determine which - /// objects are missing locally. It downloads these objects (often via a packfile), - /// inserts them into the local object store, and updates local remote-tracking - /// references (e.g., `refs/remotes/origin/*`). Requires `&mut self` as it - /// modifies local state and network streams. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the network transfer fails, objects are corrupted - /// in transit, or the local object store cannot be written to. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::remote::Remote; - /// # use libvctrl_handler::VctrlError; - /// # #[derive(Default)] - /// # struct MockRemote { refs: Vec } - /// # impl Remote for MockRemote { - /// # type RefSpec = String; type RemoteRef = String; - /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } - /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut remote = MockRemote::default(); - /// let refspecs = vec!["refs/heads/main:refs/remotes/origin/main".to_string()]; - /// remote.fetch(&refspecs)?; - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn fetch(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; - /// Pushes objects according to the given refspecs. - /// - /// # How it works - /// Takes a slice of refspecs and sends local objects to the remote that are - /// required to satisfy the refspecs. It updates the remote references accordingly. - /// Requires `&mut self` as it consumes network resources and may mutate internal - /// state regarding the push process. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the remote rejects the update (e.g., non-fast-forward - /// push), network transfer fails, or permission is denied. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::remote::Remote; - /// # use libvctrl_handler::VctrlError; - /// # #[derive(Default)] - /// # struct MockRemote { refs: Vec } - /// # impl Remote for MockRemote { - /// # type RefSpec = String; type RemoteRef = String; - /// # fn list_refs(&self) -> Result, VctrlError> { Ok(self.refs.clone()) } - /// # fn fetch(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # fn push(&mut self, _r: &[Self::RefSpec]) -> Result<(), VctrlError> { Ok(()) } - /// # } - /// let mut remote = MockRemote::default(); - /// let refspecs = vec!["refs/heads/main:refs/heads/main".to_string()]; - /// remote.push(&refspecs)?; - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn push(&mut self, refspecs: &[Self::RefSpec]) -> Result<(), VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/revwalk.rs b/libvctrl_handler/src/traits/core/revwalk.rs index 0fe3bd8e..98c011f0 100644 --- a/libvctrl_handler/src/traits/core/revwalk.rs +++ b/libvctrl_handler/src/traits/core/revwalk.rs @@ -1,127 +1,127 @@ -//! Revision walking trait. -//! -//! # Architecture -//! This module provides the contract for traversing the commit graph. Walking -//! history is a fundamental operation for log generation, bisecting, and ancestry -//! queries. By abstracting this into a trait, the crate allows backends to implement -//! optimized traversal algorithms (e.g., topological sorting, priority queues based -//! on timestamps) without leaking those implementation details to the caller. -//! -//! # Design Rationale: Lazy Evaluation -//! Repositories like the Linux kernel contain millions of commits. Loading the -//! entire commit graph into memory at once would cause severe memory exhaustion. -//! The [`RevWalk::walk`] method returns an iterator, enforcing lazy evaluation. -//! Commits are only loaded and yielded from the underlying object store as the -//! iterator is consumed, maintaining a constant, predictable memory footprint. + + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// An iterator over commit history. -/// -/// # Why this exists -/// This type alias standardizes the return type of revision walks across all -/// backends. It uses dynamic dispatch (`Box`) to perform type erasure. -/// This allows a backend to return any complex internal iterator struct (e.g., a -/// binary heap for priority-ordered traversal) without forcing the caller to know -/// the concrete type or bloating the trait signature with associated types. -/// -/// # How it works -/// - `Item = Result`: Yields a `Result` because graph traversal may -/// encounter I/O errors (e.g., a missing commit object) mid-iteration. -/// - `Send`: The iterator can be safely transferred across threads, enabling -/// parallel processing of commit history (e.g., using `rayon`). -/// - `'a`: The lifetime ties the iterator to the lifetime of the [`RevWalk`] -/// instance that created it, ensuring the backend store remains valid while -/// the iterator is active. + + + + + + + + + + + + + + + + + pub type RevWalkIterator<'a, T> = Box> + Send + 'a>; -/// Trait for walking commit history. -/// -/// # Why this exists -/// Provides a unified interface for commit graph traversal. By using an associated -/// type for the commit identifier, the trait is not hardcoded to cryptographic -/// hashes. An in-memory testing backend might use array indices (`usize`), while -/// a disk-backed backend uses [`Hash`](crate::Hash). -/// -/// # How it works -/// The `walk` method accepts a starting commit identifier and returns a -/// [`RevWalkIterator`]. The implementor is responsible for resolving the start -/// commit, reading its parent hashes, and pushing them into an internal queue. -/// As the caller calls `next()` on the iterator, the backend dequeues a commit, -/// fetches its parents, and yields the commit. -/// -/// # Design Rationale: `&self` on `walk` -/// Note that `walk` takes `&self` instead of `&mut self`. Traversal is a read-only -/// operation from the perspective of the walker's state. The implementor must use -/// interior mutability (e.g., `Mutex` for internal buffers) if the underlying -/// object store requires mutable access to read objects, allowing multiple -/// concurrent walks to occur safely. -/// -/// # Examples -/// -/// Implementing the trait for a mock graph: -/// -/// ``` -/// # use libvctrl_handler::traits::core::revwalk::{RevWalk, RevWalkIterator}; -/// # use libvctrl_handler::VctrlError; -/// # -/// struct MockRevWalk; -/// -/// impl RevWalk for MockRevWalk { -/// type CommitId = u32; -/// -/// fn walk(&self, start: &Self::CommitId) -> Result, VctrlError> { -/// let start = *start; -/// // Simulate walking backwards through commit IDs 0 to `start` -/// Ok(Box::new((0..start).rev().map(Ok))) -/// } -/// } -/// -/// let walker = MockRevWalk; -/// let iter = walker.walk(&3)?; -/// let commits: Vec = iter.filter_map(|c| c.ok()).collect(); -/// assert_eq!(commits, vec![2, 1, 0]); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait RevWalk: Send + Sync { - /// The commit identifier type. - /// - /// # Why this exists - /// Decouples the traversal logic from the identifier format. While typically - /// a 64-byte [`Hash`](crate::Hash), this allows specialized backends to use - /// more efficient representations like integers or pointers. + + + + + + type CommitId: Send + Sync; - /// Returns an iterator over commit history starting from the given commit. - /// - /// # How it works - /// Resolves the `start` commit and initializes an iterator. The iterator - /// traverses the graph (typically in reverse chronological order, respecting - /// topological constraints). The lifetime `'_` binds the returned iterator to - /// the `RevWalk` implementor, ensuring the backend is not dropped prematurely. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the starting commit cannot be found in the - /// underlying store, or if initializing the traversal queue fails. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::revwalk::{RevWalk, RevWalkIterator}; - /// # use libvctrl_handler::VctrlError; - /// # struct MockRevWalk; - /// # impl RevWalk for MockRevWalk { - /// # type CommitId = u32; - /// # fn walk(&self, s: &Self::CommitId) -> Result, VctrlError> { - /// # Ok(Box::new((0..*s).rev().map(Ok))) - /// # } - /// # } - /// let walker = MockRevWalk; - /// let mut iter = walker.walk(&5)?; - /// assert_eq!(iter.next(), Some(Ok(4))); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn walk( &self, start: &Self::CommitId, diff --git a/libvctrl_handler/src/traits/core/signer.rs b/libvctrl_handler/src/traits/core/signer.rs index 02e8ac5d..65f4a222 100644 --- a/libvctrl_handler/src/traits/core/signer.rs +++ b/libvctrl_handler/src/traits/core/signer.rs @@ -1,101 +1,101 @@ -//! Signing trait. -//! -//! # Architecture -//! This module defines the abstract contract for cryptographically signing data -//! (e.g., commits or tags). By abstracting the signing mechanism into a trait, -//! the crate decouples its security logic from the specific cryptographic backend. -//! This allows consumers to plug in different implementations, such as GPG, SSH, -//! or cloud-based Key Management Services (KMS), without altering the core VCS engine. -//! -//! # Design Rationale: Stateful Signing -//! The `sign` method requires `&mut self`. This is a deliberate design choice -//! because cryptographic signing is often stateful. A backend might need to consume -//! a one-time-use nonce, update an internal counter for replay protection, or acquire -//! an exclusive lock on a hardware security module (HSM). Forcing `&mut self` at the -//! trait level ensures that backends have the flexibility to implement these requirements -//! safely without resorting to interior mutability (`Mutex` or `RefCell`). + + + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// Trait for signing data. -/// -/// # Why this exists -/// Provides a unified interface for generating cryptographic signatures. In Git, -/// signed commits and tags verify the identity of the author. This trait allows -/// the engine to delegate the complex cryptography to a dedicated backend, ensuring -/// that the core logic remains focused on object manipulation and graph traversal. -/// -/// # How it works -/// The implementor receives a `key_id` (which could be a GPG key fingerprint, an -/// SSH key path, or a KMS URI) and the raw `data` to be signed. The backend locates -/// the private key, performs the cryptographic signing operation, and returns the -/// resulting signature as an owned `Vec`. -/// -/// # Design Rationale: Owned `Vec` Return -/// The signature is returned as an owned `Vec` rather than a fixed-size array. -/// Different signing algorithms produce different signature lengths (e.g., RSA signatures -/// are significantly larger than `EdDSA` signatures). Returning a vector accommodates -/// all algorithms uniformly. -/// -/// # Examples -/// -/// Implementing the trait for a mock signer: -/// -/// ``` -/// # use libvctrl_handler::traits::core::signer::Signer; -/// # use libvctrl_handler::VctrlError; -/// # -/// struct MockSigner; -/// -/// impl Signer for MockSigner { -/// fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError> { -/// // A real implementation would use a private key here. -/// let mut signature = Vec::new(); -/// signature.extend_from_slice(key_id.as_bytes()); -/// signature.push(b':'); -/// signature.extend_from_slice(data); -/// Ok(signature) -/// } -/// } -/// -/// let mut signer = MockSigner; -/// let sig = signer.sign("ABCDEFG12345", b"commit data")?; -/// assert_eq!(sig, b"ABCDEFG12345:commit data"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Signer: Send + Sync { - /// Signs the given data with the specified key ID and returns the signature. - /// - /// # How it works - /// Resolves the `key_id` to a private key within the backend's keyring. It then - /// applies the signing algorithm (e.g., RSA-SHA256, Ed25519) to the provided - /// `data` slice. The resulting cryptographic signature is returned as an owned - /// byte vector. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if: - /// - The `key_id` cannot be found in the keyring. - /// - The private key requires a passphrase that could not be provided. - /// - The underlying cryptographic operation fails. - /// - An I/O error occurs (e.g., communicating with a hardware token). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::signer::Signer; - /// # use libvctrl_handler::VctrlError; - /// # struct MockSigner; - /// # impl Signer for MockSigner { - /// # fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError> { - /// # Ok(data.to_vec()) - /// # } - /// # } - /// let mut signer = MockSigner; - /// let data = b"data to sign"; - /// let signature = signer.sign("key-id", data)?; - /// assert_eq!(signature, data); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn sign(&mut self, key_id: &str, data: &[u8]) -> Result, VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/transport.rs b/libvctrl_handler/src/traits/core/transport.rs index 545168ea..b057e761 100644 --- a/libvctrl_handler/src/traits/core/transport.rs +++ b/libvctrl_handler/src/traits/core/transport.rs @@ -1,157 +1,157 @@ -//! Transport trait. -//! -//! # Architecture -//! This module defines the low-level contract for sending and receiving raw Git -//! objects over a network. It is distinct from the [`Remote`](crate::traits::core::remote::Remote) -//! module, which handles higher-level repository semantics like refspec negotiation. -//! The `Transport` trait acts as a dumb pipe: it merely maps object hashes to byte streams. -//! -//! # Design Rationale: Streaming I/O -//! The `fetch_object` method returns a `Box` rather than a `Vec`. -//! This is a critical architectural decision for network efficiency. Git objects -//! can be massive. By returning a reader, the transport backend can stream data -//! directly from the network socket to the decoder, decompressing on the fly and -//! maintaining a constant memory footprint regardless of the object's size. + + + + + + + + + + + + + + use crate::errors::VctrlError; use crate::types::Hash; use std::io::Read; -/// Trait for transporting Git objects. -/// -/// # Why this exists -/// Provides a backend-agnostic abstraction for the raw transfer of Git objects. -/// Whether the underlying protocol is HTTP, SSH, or the Git wire protocol, this -/// trait allows the core engine to fetch missing objects or push new ones without -/// being coupled to the specific networking implementation or socket management. -/// -/// # How it works -/// The trait defines two operations: -/// - `fetch_object`: Downloads an object by its hash, returning a stream. -/// - `push_object`: Uploads an object's data to the remote. -/// -/// # Design Rationale: Mutability Split -/// `fetch_object` takes `&self` because it is a read-only operation from the -/// perspective of the transport's state; multiple threads can safely fetch objects -/// concurrently. Conversely, `push_object` takes `&mut self` because writing to -/// a network socket is inherently stateful and often requires sequential, exclusive -/// access to prevent interleaved data corruption. -/// -/// # Examples -/// -/// Implementing the trait for a mock in-memory transport: -/// -/// ``` -/// # use std::io::Read; -/// # use libvctrl_handler::traits::core::transport::Transport; -/// # use libvctrl_handler::{Hash, VctrlError}; -/// # use std::collections::HashMap; -/// # use std::io::Cursor; -/// # -/// #[derive(Default)] -/// struct MockTransport { -/// remote_store: HashMap>, -/// } -/// -/// impl Transport for MockTransport { -/// fn fetch_object(&self, hash: &Hash) -> Result, VctrlError> { -/// match self.remote_store.get(hash) { -/// Some(data) => Ok(Box::new(Cursor::new(data.clone()))), -/// None => Err(VctrlError::ObjectNotFound(*hash)), -/// } -/// } -/// -/// fn push_object(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError> { -/// self.remote_store.insert(*hash, data.to_vec()); -/// Ok(()) -/// } -/// } -/// -/// let mut transport = MockTransport::default(); -/// let hash = Hash::from_bytes(&[0_u8; 64])?; -/// transport.push_object(&hash, b"raw object data")?; -/// assert!(transport.fetch_object(&hash).is_ok()); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Transport: Send + Sync { - /// Fetches an object by hash, returning a reader. - /// - /// # How it works - /// Requests an object from the remote endpoint using its cryptographic hash. - /// The implementor returns a boxed reader. The lifetime `'_` ties the returned - /// reader to the lifetime of the `Transport` instance, ensuring the underlying - /// network socket or buffer remains valid while the stream is being consumed. - /// This prevents loading large objects into memory all at once. - /// - /// # Errors - /// - /// Returns [`VctrlError::ObjectNotFound`] if the remote does not possess the object. - /// Returns [`VctrlError`] if a network I/O error occurs during the transfer. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::transport::Transport; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::{Cursor, Read}; - /// # #[derive(Default)] - /// # struct MockTransport { remote_store: HashMap> } - /// # impl Transport for MockTransport { - /// # fn fetch_object(&self, h: &Hash) -> Result, VctrlError> { - /// # match self.remote_store.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } - /// # } - /// # fn push_object(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { - /// # self.remote_store.insert(*h, d.to_vec()); Ok(()) - /// # } - /// # } - /// let mut transport = MockTransport::default(); - /// let hash = Hash::from_bytes(&[1u8; 64])?; - /// transport.push_object(&hash, b"fetch me")?; - /// - /// let mut reader = transport.fetch_object(&hash)?; - /// let mut content = String::new(); - /// reader.read_to_string(&mut content)?; - /// assert_eq!(content, "fetch me"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn fetch_object(&self, hash: &Hash) -> Result, VctrlError>; - /// Pushes an object to the remote. - /// - /// # How it works - /// Accepts the object's hash and a byte slice of its raw, uncompressed content. - /// The implementor is responsible for transmitting this data to the remote endpoint. - /// Requires `&mut self` to enforce exclusive access, preventing data races when - /// multiple threads attempt to write to the same network socket simultaneously. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the network connection fails, the remote rejects - /// the data, or an I/O error occurs during transmission. - /// - /// # Examples - /// - /// ``` - /// # use std::io::Read; - /// # use libvctrl_handler::traits::core::transport::Transport; - /// # use libvctrl_handler::{Hash, VctrlError}; - /// # use std::collections::HashMap; - /// # use std::io::Cursor; - /// # #[derive(Default)] - /// # struct MockTransport { remote_store: HashMap> } - /// # impl Transport for MockTransport { - /// # fn fetch_object(&self, h: &Hash) -> Result, VctrlError> { - /// # match self.remote_store.get(h) { Some(d) => Ok(Box::new(Cursor::new(d.clone()))), None => Err(VctrlError::ObjectNotFound(*h)) } - /// # } - /// # fn push_object(&mut self, h: &Hash, d: &[u8]) -> Result<(), VctrlError> { - /// # self.remote_store.insert(*h, d.to_vec()); Ok(()) - /// # } - /// # } - /// let mut transport = MockTransport::default(); - /// let hash = Hash::from_bytes(&[2u8; 64])?; - /// transport.push_object(&hash, b"pushing data")?; - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn push_object(&mut self, hash: &Hash, data: &[u8]) -> Result<(), VctrlError>; } diff --git a/libvctrl_handler/src/traits/core/verifier.rs b/libvctrl_handler/src/traits/core/verifier.rs index e3f36ec4..be34100f 100644 --- a/libvctrl_handler/src/traits/core/verifier.rs +++ b/libvctrl_handler/src/traits/core/verifier.rs @@ -1,106 +1,106 @@ -//! Verification trait. -//! -//! # Architecture -//! This module defines the abstract contract for verifying cryptographic signatures. -//! It is the counterpart to the [`Signer`](crate::traits::core::signer::Signer) module. -//! By abstracting verification into a trait, the crate allows the core engine to -//! authenticate commits and tags without being coupled to a specific cryptographic -//! backend (e.g., GPG, SSH, or X.509). -//! -//! # Design Rationale: Stateless Verification -//! Unlike signing, which may require stateful operations (e.g., consuming nonces or -//! locking hardware tokens), signature verification is a pure, stateless mathematical -//! operation. It only requires the public key, the raw data, and the signature. -//! Therefore, the `verify` method takes `&self` instead of `&mut self`. This allows -//! multiple threads to concurrently verify different commits in a revision graph -//! without any synchronization overhead. + + + + + + + + + + + + + + + + use crate::errors::VctrlError; -/// Trait for verifying signatures. -/// -/// # Why this exists -/// Provides a unified interface for authenticating data. In Git, verifying signed -/// commits and tags ensures that the authorship is genuine and the data has not been -/// tampered with. This trait allows the engine to delegate the complex cryptography -/// to a dedicated backend, ensuring that the core logic remains agnostic of the -/// underlying Public Key Infrastructure (PKI). -/// -/// # How it works -/// The implementor receives a `key_id` (to locate the correct public key), the raw -/// `data` that was signed, and the `signature` bytes. The backend applies the -/// verification algorithm (e.g., RSA-SHA256, Ed25519) to confirm that the signature -/// was indeed generated by the owner of the private key corresponding to the public key. -/// -/// # Design Rationale: `Result` -/// The return type distinguishes between a cryptographic failure and a system failure: -/// - `Ok(true)`: The signature is mathematically valid. -/// - `Ok(false)`: The signature is mathematically invalid (tampered data or wrong key). -/// - `Err(VctrlError)`: A system error occurred (e.g., public key not found, I/O error -/// reading the keyring, or unsupported algorithm). -/// This prevents confusing an invalid signature with a system-level fault, allowing -/// callers to handle security violations explicitly. -/// -/// # Examples -/// -/// Implementing the trait for a mock verifier: -/// -/// ``` -/// # use libvctrl_handler::traits::core::verifier::Verifier; -/// # use libvctrl_handler::VctrlError; -/// # -/// struct MockVerifier; -/// -/// impl Verifier for MockVerifier { -/// fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result { -/// // A real implementation would use a public key here. -/// if key_id != "trusted_key" { -/// return Ok(false); // Unknown key implies invalid signature -/// } -/// Ok(data == signature) // Simplified mock verification -/// } -/// } -/// -/// let verifier = MockVerifier; -/// let data = b"commit data"; -/// let sig = b"commit data"; -/// -/// assert!(verifier.verify("trusted_key", data, sig)?); -/// assert!(!verifier.verify("untrusted_key", data, sig)?); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub trait Verifier: Send + Sync { - /// Verifies data against a signature using the specified key ID. - /// - /// # How it works - /// Resolves the `key_id` to a public key within the backend's keyring. It then - /// applies the verification algorithm to the `data` and `signature` slices. - /// The operation is purely computational and does not mutate the verifier's state. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if: - /// - The `key_id` cannot be found in the keyring. - /// - The underlying cryptographic library encounters an error. - /// - An I/O error occurs while accessing the keyring. - /// - /// Note: An invalid signature returns `Ok(false)`, not `Err`. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::traits::core::verifier::Verifier; - /// # use libvctrl_handler::VctrlError; - /// # struct MockVerifier; - /// # impl Verifier for MockVerifier { - /// # fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result { - /// # Ok(key_id == "trusted" && data == signature) - /// # } - /// # } - /// let verifier = MockVerifier; - /// let is_valid = verifier.verify("trusted", b"data", b"data")?; - /// assert!(is_valid); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fn verify(&self, key_id: &str, data: &[u8], signature: &[u8]) -> Result; } diff --git a/libvctrl_handler/src/traits/mod.rs b/libvctrl_handler/src/traits/mod.rs index 2fc231f6..d17e5111 100644 --- a/libvctrl_handler/src/traits/mod.rs +++ b/libvctrl_handler/src/traits/mod.rs @@ -1,39 +1,39 @@ -//! Traits for repository operations. -//! -//! # Architecture -//! This module defines the abstract contracts (interfaces) for interacting with -//! repository components. By leveraging Rust's trait system, the crate decouples -//! the *what* (domain logic and validation) from the *how* (I/O and storage implementations). -//! -//! # Design Rationale: Backend Agnosticism -//! Defining operations like object storage or reference management as traits -//! allows the core logic to remain agnostic of the underlying backend. Consumers -//! can implement these traits for in-memory storage, disk-based filesystems, or -//! remote network protocols without altering the core VCS algorithms. This also -//! drastically simplifies unit testing, as mock implementations can be injected -//! seamlessly via dependency injection. -//! -//! # Examples -//! *Note: The following example assumes this crate is named `libvctrl_handler`.* -//! -//! ``` -//! // Importing the module ensures it is publicly accessible and compiled. -//! use libvctrl_handler::traits::core; -//! ``` - -/// Core operational traits required to implement a functional version control backend. -/// -/// # Why this exists -/// Houses the fundamental, low-level traits (such as `ObjectStore`, `RefStore`, and -/// `Encoder`) that define the minimum viable surface area for a Git implementation. -/// Grouping these into a `core` submodule allows the parent `traits` module to -/// logically separate essential protocol traits from any auxiliary or high-level -/// behavioral traits that may be introduced in the future. -/// -/// # Examples -/// -/// ``` -/// // The core submodule is accessible for custom backend implementations. -/// use libvctrl_handler::traits::core; -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod core; diff --git a/libvctrl_handler/src/types/core/blob.rs b/libvctrl_handler/src/types/core/blob.rs index 34376d06..ed6ef5a1 100644 --- a/libvctrl_handler/src/types/core/blob.rs +++ b/libvctrl_handler/src/types/core/blob.rs @@ -1,73 +1,73 @@ -//! Blob object representation. -//! -//! # Architecture -//! This module defines the [`Blob`] struct, which represents the raw content of -//! a file in the Git object model. Blobs are content-addressable, meaning their -//! identifier is derived directly from their byte content. -//! -//! # Design Rationale: Bounded Allocation -//! Git blobs can range from empty files to massive binaries. Without strict limits, -//! a malicious repository could force the engine to allocate gigabytes of memory, -//! causing denial-of-service (DoS). The [`Blob::new`] constructor enforces -//! [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE), acting as a fail-fast -//! circuit breaker during object construction. + + + + + + + + + + + + + use crate::constants::MAX_BLOB_SIZE; use crate::errors::VctrlError; -/// A Git blob object (file content). -/// -/// # Why this exists -/// Provides a strongly-typed, validated wrapper around raw file bytes. By requiring -/// construction via [`new`](Self::new), the crate guarantees that every `Blob` -/// instance in memory adheres to the crate's size limits. Once constructed, the -/// blob is immutable, ensuring safe, concurrent sharing across threads. -/// -/// # How it works -/// The struct takes ownership of a `Vec`. This is a zero-copy operation from -/// the perspective of the byte buffer itself; the vector's allocation is simply -/// moved into the struct, avoiding expensive memory duplication. -/// -/// # Examples -/// -/// Creating a valid blob: -/// -/// ``` -/// # use libvctrl_handler::types::core::blob::Blob; -/// # use libvctrl_handler::VctrlError; -/// let blob = Blob::new(b"file content".to_vec())?; -/// assert_eq!(blob.size(), 12); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Blob { data: Vec, } impl Blob { - /// Creates a new blob from raw bytes. - /// - /// # How it works - /// Takes ownership of the provided `Vec`. It checks the vector's length - /// against [`MAX_BLOB_SIZE`](crate::constants::MAX_BLOB_SIZE). The downcast - /// from `u64` to `usize` is performed using `try_from` to ensure safe - /// compilation on 32-bit architectures where `usize` might be smaller than `u64`. - /// If the limit is exceeded, an error is returned and the original data is dropped. - /// - /// # Errors - /// - /// Returns [`VctrlError::ExceededMaxSize`] if the data exceeds `MAX_BLOB_SIZE`. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::blob::Blob; - /// # use libvctrl_handler::VctrlError; - /// let data = b"hello world".to_vec(); - /// let blob = Blob::new(data)?; - /// assert!(!blob.is_empty()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + pub fn new(data: Vec) -> Result { let max_size = usize::try_from(MAX_BLOB_SIZE).unwrap_or(usize::MAX); if data.len() > max_size { @@ -80,63 +80,63 @@ impl Blob { Ok(Self { data }) } - /// Returns the raw bytes of the blob. - /// - /// # How it works - /// Returns an immutable slice (`&[u8]`) borrowing from the internal vector. - /// This avoids cloning the data, allowing callers to read the content without - /// taking ownership. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::blob::Blob; - /// # use libvctrl_handler::VctrlError; - /// let blob = Blob::new(b"raw data".to_vec())?; - /// assert_eq!(blob.data(), b"raw data"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + #[must_use] pub fn data(&self) -> &[u8] { &self.data } - /// Returns the size of the blob in bytes. - /// - /// # How it works - /// Implemented as a `const fn`. This allows the size to be evaluated at compile - /// time if the blob is constructed from a static context, incurring zero runtime - /// overhead. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::blob::Blob; - /// # use libvctrl_handler::VctrlError; - /// let blob = Blob::new(b"12345".to_vec())?; - /// assert_eq!(blob.size(), 5); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + #[must_use] pub const fn size(&self) -> usize { self.data.len() } - /// Returns `true` if the blob is empty. - /// - /// # How it works - /// Checks if the internal vector has zero length. Like [`size`](Self::size), - /// this is a `const fn`. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::blob::Blob; - /// # use libvctrl_handler::VctrlError; - /// let blob = Blob::new(Vec::new())?; - /// assert!(blob.is_empty()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + #[must_use] pub const fn is_empty(&self) -> bool { self.data.is_empty() diff --git a/libvctrl_handler/src/types/core/commit.rs b/libvctrl_handler/src/types/core/commit.rs index b11fc250..ed09c941 100644 --- a/libvctrl_handler/src/types/core/commit.rs +++ b/libvctrl_handler/src/types/core/commit.rs @@ -1,21 +1,21 @@ -//! Commit object and metadata representation. -//! -//! # Architecture -//! This module defines the [`Commit`] struct, which acts as the node in the Git -//! Directed Acyclic Graph (DAG). A commit links a tree state (snapshot) to its -//! historical predecessors (parents), annotated with authorship and temporal metadata. -//! -//! # Design Rationale: DAG Integrity -//! Git's history relies on the assumption that the parent graph is acyclic and -//! structurally sound. To enforce this at the type level, the [`Commit::with_meta`] -//! constructor performs strict validation: -//! - **Duplicate Parents**: Uses a `HashSet` to ensure no parent hash appears twice. -//! Because [`Hash`] is `Copy`, inserting into the set requires no allocation, -//! providing O(1) duplicate detection. -//! - **Parent Count Limits**: Enforces [`MAX_PARENT_COUNT`](crate::constants::MAX_PARENT_COUNT) -//! to prevent pathological merge structures. -//! - **Message Bounds**: Enforces [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH) -//! to prevent memory exhaustion via commit messages. + + + + + + + + + + + + + + + + + + use super::hash::Hash; use super::user_id::UserID; @@ -23,17 +23,17 @@ 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 +42,30 @@ pub struct CommitMeta { } impl CommitMeta { - /// Creates new commit metadata. - /// - /// # How it works - /// Validates that the `timezone_offset` falls within the valid range of - /// -1440 to 1440 minutes. This range covers all valid global timezones - /// (UTC-24:00 to UTC+24:00). Rejecting out-of-bounds offsets early prevents - /// arithmetic overflows or logic errors during date formatting. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidTimezoneOffset`] if the offset is out of range (-1440..=1440). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::commit::CommitMeta; - /// # use libvctrl_handler::VctrlError; - /// let meta = CommitMeta::new(1600000000, 120, None)?; - /// assert_eq!(meta.timezone_offset(), 120); - /// - /// let invalid = CommitMeta::new(0, 1500, None); - /// assert!(matches!(invalid, Err(VctrlError::InvalidTimezoneOffset(1500)))); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + pub fn new( timestamp: i64, timezone_offset: i16, @@ -81,54 +81,54 @@ impl CommitMeta { }) } - /// Returns the timestamp. - /// - /// # How it works - /// Returns the Unix timestamp (seconds since epoch) as an `i64` to handle dates - /// far in the past or future. This is a `const fn`, allowing compile-time evaluation. + + + + + #[must_use] pub const fn timestamp(&self) -> i64 { self.timestamp } - /// Returns the timezone offset in minutes. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::commit::CommitMeta; - /// # use libvctrl_handler::VctrlError; - /// let meta = CommitMeta::new(0, -300, None)?; - /// assert_eq!(meta.timezone_offset(), -300); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + #[must_use] pub const fn timezone_offset(&self) -> i16 { self.timezone_offset } - /// Returns the encoding, if any. - /// - /// # How it works - /// Uses `as_deref()` to return `Option<&str>`, borrowing from the internal - /// `Option` without allocating. + + + + + #[must_use] pub fn encoding(&self) -> Option<&str> { self.encoding.as_deref() } } -/// A Git commit object. -/// -/// # Why this exists -/// Represents a snapshot of the repository at a specific point in time, authored -/// by a user. It links a [`Tree`] to its parent commits, forming the history graph. -/// -/// # How it works -/// The struct stores the root tree hash, a vector of parent hashes (empty for the -/// initial commit), author/committer identities, the message, and metadata. All -/// fields are owned, ensuring the commit is self-contained and can be cloned or -/// sent across threads without lifetime constraints. + + + + + + + + + + + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Commit { tree: Hash, @@ -140,31 +140,31 @@ pub struct Commit { } impl Commit { - /// Creates a new commit with default metadata. - /// - /// # How it works - /// Delegates to [`with_meta`](Self::with_meta), passing a default [`CommitMeta`] - /// (timestamp 0, offset 0, no encoding). This is useful for testing or when - /// metadata is injected later. - /// - /// # Errors - /// - /// Returns [`VctrlError::DuplicateParent`] if parents contain duplicates. - /// Returns [`VctrlError::ExceededMaxSize`] if the message is too long or too many parents. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::commit::{Commit, CommitMeta}; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::types::core::user_id::UserID; - /// # use libvctrl_handler::VctrlError; - /// # let tree = Hash::from_bytes(&[0_u8; 64])?; - /// # let author = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; - /// let commit = Commit::new(tree, vec![], author.clone(), author, "initial".to_string())?; - /// assert_eq!(commit.message(), "initial"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + pub fn new( tree: Hash, parents: Vec, @@ -182,37 +182,37 @@ impl Commit { ) } - /// Creates a new commit with timestamp metadata. - /// - /// # How it works - /// Performs three critical validation steps: - /// 1. Checks `parents.len()` against [`MAX_PARENT_COUNT`](crate::constants::MAX_PARENT_COUNT). - /// Uses `usize::try_from` to safely handle 32-bit architectures. - /// 2. Checks `message.len()` against [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH). - /// 3. Iterates through `parents` and inserts each [`Hash`] into a `HashSet`. Because - /// `Hash` implements `Copy` and `Hash`, the insertion is a fast stack operation. - /// If `insert` returns `false`, a duplicate was found, and an error is returned. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if validation fails (duplicate parents, size limits exceeded). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::commit::{Commit, CommitMeta}; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::types::core::user_id::UserID; - /// # use libvctrl_handler::VctrlError; - /// # let tree = Hash::from_bytes(&[0_u8; 64])?; - /// # let parent = Hash::from_bytes(&[1u8; 64])?; - /// # let author = UserID::new("Bob".to_string(), "bob@example.com".to_string())?; - /// # let meta = CommitMeta::new(1000, 0, None)?; - /// // Detecting a duplicate parent - /// let result = Commit::with_meta(tree, vec![parent, parent], author.clone(), author, "msg".to_string(), meta); - /// assert!(matches!(result, Err(VctrlError::DuplicateParent))); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn with_meta( tree: Hash, parents: Vec, @@ -253,60 +253,60 @@ impl Commit { }) } - /// Returns the tree hash of this commit. - /// - /// # How it works - /// Returns a reference to the root [`Hash`] identifying the tree object associated - /// with this commit's snapshot. + + + + + #[must_use] pub const fn tree(&self) -> &Hash { &self.tree } - /// Returns the parent commit hashes. - /// - /// # How it works - /// Returns a slice `&[Hash]` borrowing from the internal vector. This allows - /// callers to iterate over parents without cloning the hashes. + + + + + #[must_use] pub fn parents(&self) -> &[Hash] { &self.parents } - /// Returns the author information. - /// - /// # How it works - /// Returns a reference to the [`UserID`] representing the person who originally - /// wrote the changes. + + + + + #[must_use] pub const fn author(&self) -> &UserID { &self.author } - /// Returns the committer information. - /// - /// # How it works - /// Returns a reference to the [`UserID`] representing the person who applied - /// the changes to the repository (e.g., rebasing or merging). + + + + + #[must_use] pub const fn committer(&self) -> &UserID { &self.committer } - /// Returns the commit message. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. + + + + #[must_use] pub fn message(&self) -> &str { &self.message } - /// Returns the commit metadata. - /// - /// # How it works - /// Returns a reference to the [`CommitMeta`] struct containing timestamp and - /// timezone data. + + + + + #[must_use] pub const fn meta(&self) -> &CommitMeta { &self.meta diff --git a/libvctrl_handler/src/types/core/delta.rs b/libvctrl_handler/src/types/core/delta.rs index e591a532..75d245d2 100644 --- a/libvctrl_handler/src/types/core/delta.rs +++ b/libvctrl_handler/src/types/core/delta.rs @@ -1,73 +1,73 @@ -//! Delta and change types. -//! -//! # Architecture -//! This module provides structures for representing structural differences -//! (deltas) between two Git trees. Instead of loading full file contents into -//! memory to compute diffs, the engine operates on hashes and paths. This -//! "zero-knowledge" approach allows for extremely fast diffing of massive -//! repositories with a minimal memory footprint. -//! -//! # Design Rationale: Type-State via Factory Methods -//! The [`FileDelta`] struct uses private fields and `const fn` factory methods -//! (e.g., [`FileDelta::added`], [`FileDelta::deleted`]). This is a deliberate -//! architectural choice to enforce invariants at compile time. By restricting -//! construction to these factory methods, the crate guarantees that an `Added` -//! delta never has an `old_hash`, and a `Deleted` delta never has a `new_hash`. -//! Consumers cannot accidentally construct an invalid delta state. + + + + + + + + + + + + + + + + use std::path::{Path, PathBuf}; use crate::Hash; -/// The kind of change between two objects. -/// -/// # Why this exists -/// Classifies the nature of a modification between two tree states. By using a -/// strongly-typed enum instead of bitflags or strings, the compiler enforces -/// exhaustive matching, ensuring that diff consumers handle all possible change -/// types (or explicitly ignore them via a catch-all). + + + + + + + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum ChangeKind { - /// The object was added. + Added, - /// The object was deleted. + Deleted, - /// The object was modified. + Modified, - /// The object type changed (e.g., blob to tree). + TypeChange, - /// The object was renamed. + Renamed, - /// The object was copied. + Copied, } -/// A single file delta between two trees. -/// -/// # Why this exists -/// Represents the atomic unit of a tree diff. It maps a file path transition -/// (if any) to the change in its content hash. This allows UI renderers or merge -/// drivers to understand exactly what happened to a specific file without needing -/// to inspect the underlying blob data. -/// -/// # How it works -/// The struct holds the current `path`, an optional `old_path` (for renames/copies), -/// and optional `old_hash` and `new_hash` values. The presence of these hashes is -/// directly correlated to the [`ChangeKind`], an invariant strictly maintained by -/// the constructor methods. -/// -/// # Examples -/// -/// Creating a delta for an added file: -/// -/// ``` -/// # use libvctrl_handler::types::core::delta::FileDelta; -/// # use libvctrl_handler::Hash; -/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); -/// let delta = FileDelta::added("src/main.rs".into(), hash); -/// assert!(delta.is_added()); -/// assert!(delta.old_hash().is_none()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct FileDelta { path: PathBuf, @@ -78,11 +78,11 @@ pub struct FileDelta { } impl FileDelta { - /// Creates a new `FileDelta` representing an addition. - /// - /// # How it works - /// Initializes the delta with the new path and hash, leaving `old_path` and - /// `old_hash` as `None` to reflect that the file did not exist in the old tree. + + + + + #[must_use] pub const fn added(path: PathBuf, new_hash: Hash) -> Self { Self { @@ -94,11 +94,11 @@ impl FileDelta { } } - /// Creates a new `FileDelta` representing a deletion. - /// - /// # How it works - /// Initializes the delta with the old path and hash, leaving `new_hash` as - /// `None` to reflect that the file no longer exists in the new tree. + + + + + #[must_use] pub const fn deleted(path: PathBuf, old_hash: Hash) -> Self { Self { @@ -110,11 +110,11 @@ impl FileDelta { } } - /// Creates a new `FileDelta` representing a modification. - /// - /// # How it works - /// The path remains the same, but both `old_hash` and `new_hash` are populated - /// to indicate that the file content changed while its location did not. + + + + + #[must_use] pub const fn modified(path: PathBuf, old_hash: Hash, new_hash: Hash) -> Self { Self { @@ -126,11 +126,11 @@ impl FileDelta { } } - /// Creates a new `FileDelta` representing a type change. - /// - /// # How it works - /// Similar to a modification, but signifies that the Git object type changed - /// (e.g., a regular file became a symbolic link). Both hashes are populated. + + + + + #[must_use] pub const fn type_change(path: PathBuf, old_hash: Hash, new_hash: Hash) -> Self { Self { @@ -142,12 +142,12 @@ impl FileDelta { } } - /// Creates a new `FileDelta` representing a rename. - /// - /// # How it works - /// Populates both `path` (the new path) and `old_path` (the original path). - /// Depending on the diff algorithm, the hash might remain the same or change - /// if the file was also modified during the rename. + + + + + + #[must_use] pub const fn renamed( old_path: PathBuf, @@ -164,11 +164,11 @@ impl FileDelta { } } - /// Creates a new `FileDelta` representing a copy. - /// - /// # How it works - /// Similar to a rename, but indicates the original file still exists at - /// `old_path`. The `path` field holds the destination of the copy. + + + + + #[must_use] pub const fn copied( old_path: PathBuf, @@ -185,131 +185,131 @@ impl FileDelta { } } - /// Returns the path of the changed file. - /// - /// # How it works - /// Returns a reference to the current (new) path of the file. If the file was - /// deleted, this returns the path it used to have. + + + + + #[must_use] pub fn path(&self) -> &Path { &self.path } - /// Returns the old path if the file was renamed or copied. - /// - /// # How it works - /// Returns `Some(&Path)` only if the [`ChangeKind`] is `Renamed` or `Copied`. - /// Otherwise, it returns `None`. + + + + + #[must_use] pub fn old_path(&self) -> Option<&Path> { self.old_path.as_deref() } - /// Returns the old hash, if the file previously existed. - /// - /// # How it works - /// Returns `None` for additions, as there is no previous state. + + + + #[must_use] pub const fn old_hash(&self) -> Option { self.old_hash } - /// Returns the new hash, if the file exists now. - /// - /// # How it works - /// Returns `None` for deletions, as the file no longer exists in the new state. + + + + #[must_use] pub const fn new_hash(&self) -> Option { self.new_hash } - /// Returns the kind of change. - /// - /// # How it works - /// Provides the [`ChangeKind`] enum variant associated with this delta. + + + + #[must_use] pub const fn kind(&self) -> ChangeKind { self.kind } - /// Returns `true` if this is an addition. + #[must_use] pub fn is_added(&self) -> bool { self.kind == ChangeKind::Added } - /// Returns `true` if this is a deletion. + #[must_use] pub fn is_deleted(&self) -> bool { self.kind == ChangeKind::Deleted } - /// Returns `true` if this is a modification. + #[must_use] pub fn is_modified(&self) -> bool { self.kind == ChangeKind::Modified } - /// Returns `true` if this is a type change. + #[must_use] pub fn is_type_change(&self) -> bool { self.kind == ChangeKind::TypeChange } - /// Returns `true` if this is a rename. + #[must_use] pub fn is_renamed(&self) -> bool { self.kind == ChangeKind::Renamed } - /// Returns `true` if this is a copy. + #[must_use] pub fn is_copied(&self) -> bool { self.kind == ChangeKind::Copied } } -/// A collection of file deltas between two trees. -/// -/// # Why this exists -/// Aggregates all individual [`FileDelta`]s into a single, cohesive structure. -/// This provides a clean interface for consumers to query the total number of -/// changes, iterate over them, or pass the entire diff result between functions. -/// -/// # How it works -/// Internally, it is a thin wrapper around a `Vec`. It implements -/// `IntoIterator` for both owned and borrowed values, allowing consumers to -/// easily loop over the changes using `for` loops without needing to call -/// `.iter()` explicitly. -/// -/// # Examples -/// -/// Creating a `TreeDelta` and iterating over its changes: -/// -/// ``` -/// # use libvctrl_handler::types::core::delta::{FileDelta, TreeDelta}; -/// # use libvctrl_handler::Hash; -/// # let hash = Hash::from_bytes(&[0_u8; 64]).unwrap(); -/// let delta1 = FileDelta::added("file1.txt".into(), hash); -/// let delta2 = FileDelta::deleted("file2.txt".into(), hash); -/// let tree_delta = TreeDelta::from_changes(vec![delta1, delta2]); -/// -/// assert_eq!(tree_delta.len(), 2); -/// for delta in &tree_delta { -/// assert!(delta.is_added() || delta.is_deleted()); -/// } -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct TreeDelta { changes: Vec, } impl TreeDelta { - /// Creates an empty `TreeDelta`. - /// - /// # How it works - /// Initializes the internal vector without allocating capacity until elements - /// are added. This is a `const fn`, allowing static initialization. + + + + + #[must_use] pub const fn new() -> Self { Self { @@ -317,42 +317,42 @@ impl TreeDelta { } } - /// Creates a `TreeDelta` from a vector of `FileDelta`. - /// - /// # How it works - /// Takes ownership of the provided vector, wrapping it directly. This avoids - /// unnecessary copying of the deltas. + + + + + #[must_use] pub const fn from_changes(changes: Vec) -> Self { Self { changes } } - /// Returns the number of changes. + #[must_use] pub const fn len(&self) -> usize { self.changes.len() } - /// Returns `true` if there are no changes. + #[must_use] pub const fn is_empty(&self) -> bool { self.changes.is_empty() } - /// Iterates over the changes. - /// - /// # How it works - /// Returns a standard slice iterator (`std::slice::Iter`), borrowing from the - /// internal vector. This is highly efficient as it involves no allocations. + + + + + pub fn iter(&self) -> std::slice::Iter<'_, FileDelta> { self.changes.iter() } - /// Returns the changes. - /// - /// # How it works - /// Returns a slice `&[FileDelta]` borrowing from the internal vector. This allows - /// callers to index or iterate over the changes without taking ownership. + + + + + #[must_use] pub fn changes(&self) -> &[FileDelta] { &self.changes @@ -363,12 +363,12 @@ impl IntoIterator for TreeDelta { type Item = FileDelta; type IntoIter = std::vec::IntoIter; - /// Consumes the `TreeDelta` and returns an owned iterator. - /// - /// # How it works - /// Converts the internal `Vec` into `std::vec::IntoIter`, yielding - /// owned `FileDelta` items. This is useful when the consumer needs to take - /// ownership of the deltas, e.g., to send them to another thread. + + + + + + fn into_iter(self) -> Self::IntoIter { self.changes.into_iter() } @@ -378,11 +378,11 @@ impl<'a> IntoIterator for &'a TreeDelta { type Item = &'a FileDelta; type IntoIter = std::slice::Iter<'a, FileDelta>; - /// Borrows the `TreeDelta` and returns a borrowing iterator. - /// - /// # How it works - /// Delegates to [`TreeDelta::iter`], yielding `&FileDelta` items. This allows - /// ergonomic `for delta in &tree_delta` loops without consuming the struct. + + + + + fn into_iter(self) -> Self::IntoIter { self.iter() } diff --git a/libvctrl_handler/src/types/core/hash.rs b/libvctrl_handler/src/types/core/hash.rs index faed018d..b8cad490 100644 --- a/libvctrl_handler/src/types/core/hash.rs +++ b/libvctrl_handler/src/types/core/hash.rs @@ -1,78 +1,78 @@ -//! Hash type. -//! -//! # Architecture -//! This module defines the [`Hash`] type, a fixed-size wrapper around a 64-byte -//! array (SHA-512). In a content-addressable storage (CAS) system, hashes are the -//! primary keys for all objects and references. -//! -//! # Design Rationale: Stack Allocation -//! By wrapping a fixed-size array `[u8; 64]` instead of using a `Vec` or `Box<[u8]>`, -//! the [`Hash`] type is inherently `Copy` and requires no heap allocation. This is a -//! critical performance optimization: hashes are created, copied, and compared millions -//! of times during graph traversal and object packing. Keeping them on the stack -//! eliminates allocator overhead and memory fragmentation. + + + + + + + + + + + + + use crate::constants::HASH_LENGTH; use crate::errors::VctrlError; use core::fmt; use core::str::FromStr; -/// A fixed-size hash (64 bytes, e.g., SHA-512). -/// -/// # Why this exists -/// Provides a strongly-typed, length-guaranteed representation of a cryptographic hash. -/// By encoding the length (64 bytes) directly into the type system via a constant -/// generic array, the compiler guarantees that a [`Hash`] can never accidentally hold -/// a 20-byte SHA-1 or a 32-byte SHA-256. This prevents entire classes of length-mismatch -/// bugs at compile time. -/// -/// # How it works -/// The struct is a tuple wrapping `[u8; HASH_LENGTH]`. It derives `PartialEq`, `Eq`, -/// `Hash`, and `Ord`, allowing it to be used as a key in `HashMap` or `BTreeMap`. The -/// `Copy` trait is derived, meaning assigning a hash to a new variable performs a fast -/// 64-byte stack copy rather than a pointer move. -/// -/// # Examples -/// -/// Creating a hash from raw bytes: -/// -/// ``` -/// # use libvctrl_handler::types::core::hash::Hash; -/// # use libvctrl_handler::VctrlError; -/// let raw_bytes = [0_u8; 64]; -/// let hash = Hash::from_bytes(&raw_bytes)?; -/// assert_eq!(hash.as_bytes(), &raw_bytes); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct Hash([u8; HASH_LENGTH]); impl Hash { - /// Creates a hash from a byte slice. - /// - /// # How it works - /// This function is `const`, meaning it can be evaluated at compile time if the - /// input slice is a static literal. Because `for` loops over slices were not fully - /// stable in `const fn` contexts during early Rust editions, this implementation - /// uses a `while` loop with an index to copy bytes into a fixed-size array. If the - /// slice length does not exactly match [`HASH_LENGTH`], an error is returned. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidHashLength`] if the slice length does not match [`HASH_LENGTH`]. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::VctrlError; - /// let valid_hash = Hash::from_bytes(&[1u8; 64]); - /// assert!(valid_hash.is_ok()); - /// - /// let invalid_hash = Hash::from_bytes(&[1u8; 32]); - /// assert!(invalid_hash.is_err()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + #[allow(clippy::indexing_slicing)] pub const fn from_bytes(bytes: &[u8]) -> Result { if bytes.len() != HASH_LENGTH { @@ -87,21 +87,21 @@ impl Hash { 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 +109,11 @@ impl Hash { } impl From<[u8; HASH_LENGTH]> for Hash { - /// Converts a raw array into a [`Hash`]. - /// - /// # How it works - /// This infallible conversion wraps the array directly. It is used when the caller - /// already possesses a correctly sized array, bypassing the need for slice validation. + + + + + fn from(arr: [u8; HASH_LENGTH]) -> Self { Self(arr) } @@ -122,23 +122,23 @@ impl From<[u8; HASH_LENGTH]> for Hash { impl TryFrom<&[u8]> for Hash { type Error = VctrlError; - /// Attempts to convert a byte slice into a [`Hash`]. - /// - /// # How it works - /// Delegates to [`Hash::from_bytes`]. This trait implementation allows ergonomic - /// use of the `?` operator when converting from generic byte slices. + + + + + fn try_from(value: &[u8]) -> Result { Self::from_bytes(value) } } impl AsRef<[u8]> for Hash { - /// Converts to a byte slice. - /// - /// # How it works - /// Allows the [`Hash`] to be used with APIs that expect `AsRef<[u8]>`, providing - /// interoperability with standard cryptographic and I/O crates without exposing - /// the internal array representation. + + + + + + fn as_ref(&self) -> &[u8] { &self.0 } @@ -147,30 +147,30 @@ impl AsRef<[u8]> for Hash { impl FromStr for Hash { type Err = VctrlError; - /// Parses a hexadecimal string into a [`Hash`]. - /// - /// # How it works - /// Expects a string of exactly 128 characters (64 bytes * 2 hex chars). It iterates - /// through the string in 2-character chunks, parsing each chunk into a byte using - /// `u8::from_str_radix`. If any character is invalid hex, or if the length is wrong, - /// it returns an error. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidHashLength`] if the string length is not 128. - /// Returns [`VctrlError::CorruptedData`] if the string contains non-hexadecimal characters. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use std::str::FromStr; - /// # use libvctrl_handler::VctrlError; - /// let hex_str = "0".repeat(128); - /// let hash = Hash::from_str(&hex_str)?; - /// assert_eq!(hash.as_bytes(), &[0_u8; 64]); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + fn from_str(s: &str) -> Result { if s.len() != HASH_LENGTH * 2 { return Err(VctrlError::InvalidHashLength(s.len())); @@ -189,12 +189,12 @@ impl FromStr for Hash { } impl fmt::Debug for Hash { - /// Formats the hash for debugging purposes. - /// - /// # How it works - /// To prevent flooding debug logs with 128-character strings, this implementation - /// only prints the first 16 bytes (32 hex characters) followed by `...`. This provides - /// enough context to distinguish between different hashes while remaining readable. + + + + + + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Hash(")?; for &byte in self.0.iter().take(16) { @@ -205,23 +205,23 @@ impl fmt::Debug for Hash { } impl fmt::Display for Hash { - /// Formats the hash as a full hexadecimal string. - /// - /// # How it works - /// Iterates over all 64 bytes, formatting each as a two-character zero-padded - /// hexadecimal value. This produces the canonical 128-character string representation - /// expected by Git tools. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::VctrlError; - /// use std::fmt::Display; - /// let hash = Hash::from_bytes(&[0_u8; 64])?; - /// assert_eq!(format!("{hash}"), "0".repeat(128)); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for &byte in &self.0 { write!(f, "{byte:02x}")?; diff --git a/libvctrl_handler/src/types/core/merge.rs b/libvctrl_handler/src/types/core/merge.rs index 75cca020..c9c239a9 100644 --- a/libvctrl_handler/src/types/core/merge.rs +++ b/libvctrl_handler/src/types/core/merge.rs @@ -1,50 +1,50 @@ -//! Merge-related types. -//! -//! # Architecture -//! This module defines the data structures used to represent the outcome of a -//! 3-way merge operation. A 3-way merge uses a common ancestor (the merge base) -//! to reconcile changes between two divergent branches ("ours" and "theirs"). -//! -//! # Design Rationale: Hash-Based Conflicts -//! The [`Conflict`] struct stores cryptographic hashes (`ancestor_blob`, `our_blob`, -//! `their_blob`) rather than the raw file contents. This is a critical architectural -//! decision for scalability. Merge orchestration can evaluate thousands of paths. -//! By deferring the loading of actual blob bytes to a specialized merge driver -//! (like `diff3`), the engine can quickly identify conflicts without exhausting -//! memory on large binary files. + + + + + + + + + + + + + + use std::path::{Path, PathBuf}; use crate::Hash; -/// A conflict that occurred during a merge. -/// -/// # Why this exists -/// Represents a single file path where the "ours" and "theirs" branches made -/// conflicting changes relative to the common ancestor, preventing automatic -/// resolution. This struct provides the necessary references for a UI or a -/// text-merge tool to present the conflict to the user. -/// -/// # How it works -/// The struct holds the file path and the [`Hash`] of the blob in each of the -/// three merge stages: -/// - `ancestor_blob`: The state of the file at the merge base. -/// - `our_blob`: The state of the file in the current branch (HEAD). -/// - `their_blob`: The state of the file in the branch being merged in. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::types::core::merge::Conflict; -/// # use libvctrl_handler::Hash; -/// # let ancestor = Hash::from_bytes(&[0_u8; 64])?; -/// # let ours = Hash::from_bytes(&[1u8; 64])?; -/// # let theirs = Hash::from_bytes(&[2u8; 64])?; -/// let conflict = Conflict::new("src/main.rs".into(), ancestor, ours, theirs); -/// assert_eq!(conflict.path(), std::path::Path::new("src/main.rs")); -/// assert_eq!(conflict.our_blob(), ours); -/// # Ok::<(), libvctrl_handler::VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, PartialEq, Eq)] pub struct Conflict { path: PathBuf, @@ -54,12 +54,12 @@ pub struct Conflict { } impl Conflict { - /// Creates a new conflict. - /// - /// # How it works - /// Initializes the conflict record with the path and the three corresponding - /// blob hashes. This is a `const fn`, allowing the construction of conflict - /// scenarios at compile time for testing purposes. + + + + + + #[must_use] pub const fn new(path: PathBuf, ancestor_blob: Hash, our_blob: Hash, their_blob: Hash) -> Self { Self { @@ -70,120 +70,120 @@ impl Conflict { } } - /// Returns the path with a conflict. - /// - /// # How it works - /// Returns a reference to the `PathBuf` where the merge conflict occurred. + + + + #[must_use] pub fn path(&self) -> &Path { &self.path } - /// Returns the ancestor blob hash. - /// - /// # How it works - /// Returns the `Hash` of the file content from the merge base (the common - /// ancestor commit). + + + + + #[must_use] pub const fn ancestor_blob(&self) -> Hash { self.ancestor_blob } - /// Returns the blob from the current branch. - /// - /// # How it works - /// Returns the `Hash` of the file content from the "ours" side of the merge - /// (typically the current `HEAD`). + + + + + #[must_use] pub const fn our_blob(&self) -> Hash { self.our_blob } - /// Returns the blob from the merging branch. - /// - /// # How it works - /// Returns the `Hash` of the file content from the "theirs" side of the merge - /// (the branch being merged into the current one). + + + + + #[must_use] pub const fn their_blob(&self) -> Hash { self.their_blob } } -/// The result of a merge operation. -/// -/// # Why this exists -/// Acts as an Algebraic Data Type (ADT) to represent the binary outcome of a merge. -/// By modeling the result as an enum, the Rust compiler forces the caller to -/// explicitly handle both the success and conflict scenarios at compile time, -/// preventing "forgotten conflict" bugs. -/// -/// # How it works -/// - `Success(Hash)`: Indicates a clean merge. Contains the hash of the newly -/// created root tree object. -/// - `Conflicts(Vec)`: Indicates that one or more paths could not be -/// merged automatically. Contains the list of conflicts to be resolved. -/// -/// # Examples -/// -/// Handling a successful merge: -/// -/// ``` -/// # use libvctrl_handler::types::core::merge::MergeResult; -/// # use libvctrl_handler::Hash; -/// # let tree_hash = Hash::from_bytes(&[0_u8; 64])?; -/// let result = MergeResult::Success(tree_hash); -/// assert!(result.is_success()); -/// assert!(result.conflicts().is_none()); -/// # Ok::<(), libvctrl_handler::VctrlError>(()) -/// ``` -/// -/// Handling a conflicted merge: -/// -/// ``` -/// # use libvctrl_handler::types::core::merge::{Conflict, MergeResult}; -/// # use libvctrl_handler::Hash; -/// # let h = Hash::from_bytes(&[1u8; 64])?; -/// let result = MergeResult::Conflicts(vec![Conflict::new("file.txt".into(), h, h, h)]); -/// assert!(result.is_conflicts()); -/// assert_eq!(result.conflicts().unwrap().len(), 1); -/// # Ok::<(), libvctrl_handler::VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, PartialEq, Eq)] pub enum MergeResult { - /// The merge succeeded with the resulting tree hash. + Success(Hash), - /// The merge produced conflicts. + Conflicts(Vec), } impl MergeResult { - /// Returns `true` if the merge succeeded. - /// - /// # How it works - /// Uses pattern matching to check if the result is the `Success` variant. - /// This is a `const fn`, incurring zero runtime overhead. + + + + + #[must_use] pub const fn is_success(&self) -> bool { matches!(self, Self::Success(_)) } - /// Returns `true` if the merge produced conflicts. - /// - /// # How it works - /// Uses pattern matching to check if the result is the `Conflicts` variant. - /// This is a `const fn`, incurring zero runtime overhead. + + + + + #[must_use] pub const fn is_conflicts(&self) -> bool { matches!(self, Self::Conflicts(_)) } - /// Returns the conflicts if any. - /// - /// # How it works - /// If the result is `Conflicts`, it returns `Some(&[Conflict])` borrowing from - /// the internal vector. If the result is `Success`, it returns `None`. This - /// avoids cloning the conflict data if the caller only needs to inspect it. + + + + + + #[must_use] pub fn conflicts(&self) -> Option<&[Conflict]> { match self { diff --git a/libvctrl_handler/src/types/core/mod.rs b/libvctrl_handler/src/types/core/mod.rs index ab604cdf..339bfd3b 100644 --- a/libvctrl_handler/src/types/core/mod.rs +++ b/libvctrl_handler/src/types/core/mod.rs @@ -1,113 +1,113 @@ -//! Core data types for Git objects. -//! -//! # Architecture -//! This module aggregates the fundamental, strongly-typed data structures that -//! represent the Git object model. By separating these types into their own -//! submodules (e.g., `blob`, `commit`, `tree`), the crate prevents the formation -//! of a monolithic, unmanageable file. Each submodule encapsulates the specific -//! validation logic and invariants for its domain. -//! -//! # Design Rationale: Immutable Domain Model -//! All types exported from this module are immutable once constructed. Their -//! constructors are fallible (`Result`-returning), enforcing strict invariants -//! such as hash lengths, maximum sizes, and structural integrity (e.g., sorted -//! tree entries). This guarantees that if an object exists in memory, it is -//! structurally valid and safe to share across threads without external -//! synchronization. -//! -//! # Facade Re-exports -//! While definitions live in submodules, the types are re-exported directly here. -//! This allows consumers to use ergonomic paths like `libvctrl_handler::types::core::Blob` -//! instead of the deeper `libvctrl_handler::types::core::blob::Blob`. -//! -//! # Examples -//! *Note: The following examples assume this crate is named `libvctrl_handler`.* -//! -//! ``` -//! # use libvctrl_handler::types::core::{Blob, Hash, Tree}; -//! # use libvctrl_handler::VctrlError; -//! let raw_bytes = [0_u8; 64]; -//! let hash = Hash::from_bytes(&raw_bytes)?; -//! let blob = Blob::new(b"content".to_vec())?; -//! let tree = Tree::new(vec![])?; -//! -//! assert_eq!(blob.size(), 7); -//! assert!(tree.is_empty()); -//! # Ok::<(), VctrlError>(()) -//! ``` - -/// Blob object representation. -/// -/// # Why this exists -/// Git blobs represent the raw content of files. This submodule houses the -/// [`Blob`](blob::Blob) type, which enforces size limits during construction -/// to prevent memory exhaustion. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod blob; pub use blob::Blob; -/// Commit object and metadata representation. -/// -/// # Why this exists -/// Commits link tree states together in a directed acyclic graph (DAG). This -/// submodule houses [`Commit`](commit::Commit) and [`CommitMeta`](commit::CommitMeta), -/// enforcing rules like maximum parent counts and duplicate parent detection. + + + + + + pub mod commit; pub use commit::{Commit, CommitMeta}; -/// Delta and change types. -/// -/// # Why this exists -/// Represents structural differences between trees without loading entire file -/// contents. Contains [`ChangeKind`](delta::ChangeKind), [`FileDelta`](delta::FileDelta), -/// and [`TreeDelta`](delta::TreeDelta). + + + + + + pub mod delta; pub use delta::{ChangeKind, FileDelta, TreeDelta}; -/// Hash type. -/// -/// # Why this exists -/// Provides a stack-allocated, `Copy` wrapper for 64-byte SHA-512 hashes via the -/// [`Hash`](hash::Hash) type, eliminating heap allocations for object identifiers. + + + + + pub mod hash; pub use hash::Hash; -/// Merge-related types. -/// -/// # Why this exists -/// Represents the outcome of a 3-way merge operation. Contains -/// [`Conflict`](merge::Conflict) and [`MergeResult`](merge::MergeResult). + + + + + pub mod merge; pub use merge::{Conflict, MergeResult}; -/// Reflog entry type. -/// -/// # Why this exists -/// Represents a single timestamped mutation in the reference history via the -/// [`ReflogEntry`](reflog::ReflogEntry) type. + + + + + pub mod reflog; pub use reflog::ReflogEntry; -/// Tag object representation. -/// -/// # Why this exists -/// Annotated tags point to other objects (usually commits) and carry their own -/// metadata. This submodule houses the [`Tag`](tag::Tag) type. + + + + + pub mod tag; pub use tag::Tag; -/// Tree object and entry representation. -/// -/// # Why this exists -/// Trees represent the directory structure, mapping names to modes and hashes. -/// This submodule houses [`Tree`](tree::Tree) and [`TreeEntry`](tree::TreeEntry), -/// enforcing Git's strict sorting and duplication rules. + + + + + + pub mod tree; pub use tree::{Tree, TreeEntry}; -/// User identity representation. -/// -/// # Why this exists -/// Represents the `Name ` syntax used in commits and tags via the -/// [`UserID`](user_id::UserID) type. + + + + + pub mod user_id; pub use user_id::UserID; diff --git a/libvctrl_handler/src/types/core/reflog.rs b/libvctrl_handler/src/types/core/reflog.rs index ef24a120..c69f9fa7 100644 --- a/libvctrl_handler/src/types/core/reflog.rs +++ b/libvctrl_handler/src/types/core/reflog.rs @@ -1,52 +1,52 @@ -//! Reflog entry type. -//! -//! # Architecture -//! This module defines the [`ReflogEntry`] struct, which represents a single -//! timestamped record in a reference log (reflog). Reflogs act as an append-only -//! audit trail, tracking every mutation to a reference (e.g., commits, resets, -//! checkouts). This history is crucial for recovering from accidental operations -//! and for garbage collection pruning. -//! -//! # Design Rationale: Immutable State Transitions -//! A [`ReflogEntry`] captures a state transition: it records the `old_id` and the -//! `new_id` of a reference. By using `Option`, the type elegantly handles -//! edge cases: -//! - `old_id` is `None`: The reference was just created (born). -//! - `new_id` is `None`: The reference was deleted (died). -//! Once constructed, the entry is immutable, ensuring that the audit history -//! cannot be tampered with. + + + + + + + + + + + + + + + + + use crate::Hash; use crate::errors::VctrlError; -/// A single entry in a reflog. -/// -/// # Why this exists -/// Provides a strongly-typed, validated record of a reference update. By requiring -/// construction via [`new`](Self::new), the crate guarantees that every `ReflogEntry` -/// in memory adheres to temporal constraints (e.g., valid timezone offsets). This -/// prevents malformed historical data from corrupting repository recovery tools. -/// -/// # How it works -/// The struct stores the old and new hashes as `Option`. Because [`Hash`] is -/// a `Copy` type (a 64-byte array wrapper), storing and copying these options is -/// a fast stack operation. The `reason` is stored as an owned `String` to ensure -/// the entry is self-contained and `'static` safe. -/// -/// # Examples -/// -/// Creating a reflog entry for a new commit: -/// -/// ``` -/// # use libvctrl_handler::types::core::reflog::ReflogEntry; -/// # use libvctrl_handler::Hash; -/// # use libvctrl_handler::VctrlError; -/// # let old_hash = Hash::from_bytes(&[0_u8; 64])?; -/// # let new_hash = Hash::from_bytes(&[1u8; 64])?; -/// let entry = ReflogEntry::new(Some(old_hash), Some(new_hash), "commit: Add feature".to_string(), 1600000000, 0)?; -/// assert_eq!(entry.reason(), "commit: Add feature"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ReflogEntry { old_id: Option, @@ -57,30 +57,30 @@ pub struct ReflogEntry { } impl ReflogEntry { - /// Creates a new reflog entry. - /// - /// # How it works - /// Validates that the `timezone_offset` falls within the valid range of - /// -1440 to 1440 minutes (UTC-24:00 to UTC+24:00). This strict validation - /// prevents arithmetic overflows or logic errors during date formatting and - /// historical chronological sorting. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidTimezoneOffset`] if the offset is out of range (-1440..=1440). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::reflog::ReflogEntry; - /// # use libvctrl_handler::Hash; - /// # use libvctrl_handler::VctrlError; - /// # let hash = Hash::from_bytes(&[0_u8; 64])?; - /// // Creating an entry for the birth of a reference (old_id is None) - /// let entry = ReflogEntry::new(None, Some(hash), "branch: Created from HEAD".to_string(), 0, 0)?; - /// assert!(entry.old_id().is_none()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + pub fn new( old_id: Option, new_id: Option, @@ -100,52 +100,52 @@ impl ReflogEntry { }) } - /// Returns the old hash. - /// - /// # How it works - /// Returns `Option`. Because `Hash` is `Copy`, this returns a copy of - /// the hash rather than a reference, simplifying lifetime management. Returns - /// `None` if this entry records the creation of a new reference. + + + + + + #[must_use] pub const fn old_id(&self) -> Option { self.old_id } - /// Returns the new hash. - /// - /// # How it works - /// Returns `Option`. Returns `None` if this entry records the deletion - /// of a reference. + + + + + #[must_use] pub const fn new_id(&self) -> Option { self.new_id } - /// Returns the reason for the change. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. This - /// avoids allocation when the caller only needs to read the reason. + + + + + #[must_use] pub fn reason(&self) -> &str { &self.reason } - /// Returns the timestamp of the change. - /// - /// # How it works - /// Returns the Unix timestamp (seconds since epoch) as an `i64`. This is a - /// `const fn`, allowing compile-time evaluation. + + + + + #[must_use] pub const fn timestamp(&self) -> i64 { self.timestamp } - /// Returns the timezone offset. - /// - /// # How it works - /// Returns the timezone offset in minutes as an `i16`. This is a `const fn`, - /// allowing compile-time evaluation. + + + + + #[must_use] pub const fn timezone_offset(&self) -> i16 { self.timezone_offset diff --git a/libvctrl_handler/src/types/core/tag.rs b/libvctrl_handler/src/types/core/tag.rs index 4267cace..4665824d 100644 --- a/libvctrl_handler/src/types/core/tag.rs +++ b/libvctrl_handler/src/types/core/tag.rs @@ -1,17 +1,17 @@ -//! 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; @@ -20,35 +20,35 @@ use crate::constants::MAX_MESSAGE_LENGTH; use crate::errors::VctrlError; use crate::validation::validate_ref_name; -/// A Git tag object. -/// -/// # Why this exists -/// Provides a strongly-typed, immutable representation of an annotated tag. Tags are -/// used to mark specific points in history, such as release versions. By requiring -/// construction via [`new`](Self::new) or [`with_meta`](Self::with_meta), the crate -/// guarantees that every `Tag` in memory adheres to naming and size constraints, -/// preventing filesystem corruption and memory exhaustion. -/// -/// # How it works -/// The struct stores the tag's `name`, the `target` hash it points to, an optional -/// `tagger` identity, a `message`, and temporal `meta`. It reuses [`CommitMeta`] -/// for timestamp data to avoid duplicating temporal logic between commits and tags. -/// -/// # Examples -/// -/// Creating a valid annotated tag: -/// -/// ``` -/// # use libvctrl_handler::types::core::tag::Tag; -/// # use libvctrl_handler::types::core::hash::Hash; -/// # use libvctrl_handler::types::core::user_id::UserID; -/// # use libvctrl_handler::VctrlError; -/// # let target = Hash::from_bytes(&[0_u8; 64])?; -/// # let tagger = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; -/// let tag = Tag::new("v1.0.0".to_string(), target, Some(tagger), "Initial release".to_string())?; -/// assert_eq!(tag.name(), "v1.0.0"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Tag { name: String, @@ -59,28 +59,28 @@ pub struct Tag { } impl Tag { - /// Creates a new tag with default metadata. - /// - /// # How it works - /// Delegates to [`with_meta`](Self::with_meta), passing a default [`CommitMeta`] - /// (timestamp 0, offset 0, no encoding). This is useful for testing or when - /// temporal metadata is injected later. - /// - /// # Errors - /// - /// Returns [`VctrlError`] if the name or message fails validation. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::tag::Tag; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::VctrlError; - /// # let target = Hash::from_bytes(&[0_u8; 64])?; - /// let tag = Tag::new("v2.0".to_string(), target, None, "Release".to_string())?; - /// assert_eq!(tag.message(), "Release"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + pub fn new( name: String, target: Hash, @@ -90,37 +90,37 @@ impl Tag { Self::with_meta(name, target, tagger, message, CommitMeta::default()) } - /// Creates a new tag with timestamp metadata. - /// - /// # How it works - /// Performs two critical validation steps: - /// 1. Checks the `name` against Git's reference naming rules using - /// [`validate_ref_name`](crate::validation::validate_ref_name). This rejects - /// names containing `..`, leading/trailing slashes, or control characters. - /// 2. Checks `message.len()` against [`MAX_MESSAGE_LENGTH`](crate::constants::MAX_MESSAGE_LENGTH). - /// Uses `usize::try_from` to safely handle 32-bit architectures. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidName`] if the name violates Git reference rules. - /// Returns [`VctrlError::ExceededMaxSize`] if the message is too long. - /// - /// # Examples - /// - /// Detecting an invalid tag name: - /// - /// ``` - /// # use libvctrl_handler::types::core::tag::Tag; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::types::core::commit::CommitMeta; - /// # use libvctrl_handler::VctrlError; - /// # let target = Hash::from_bytes(&[0_u8; 64])?; - /// # let meta = CommitMeta::default(); - /// // Names containing ".." are forbidden to prevent path traversal. - /// let result = Tag::with_meta("../evil".to_string(), target, None, "msg".to_string(), meta); - /// assert!(matches!(result, Err(VctrlError::InvalidName(_)))); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn with_meta( name: String, target: Hash, @@ -144,50 +144,50 @@ impl Tag { }) } - /// Returns the tag name. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. This - /// avoids allocation when the caller only needs to read the name. + + + + + #[must_use] pub fn name(&self) -> &str { &self.name } - /// Returns the target hash. - /// - /// # How it works - /// Returns a reference to the [`Hash`] identifying the object this tag points to - /// (usually a commit). + + + + + #[must_use] pub const fn target(&self) -> &Hash { &self.target } - /// Returns the tagger, if any. - /// - /// # How it works - /// Returns `Option<&UserID>`. Lightweight tags might not have a tagger, but - /// annotated tags usually do. Returns `None` if the tagger was not specified. + + + + + #[must_use] pub const fn tagger(&self) -> Option<&UserID> { self.tagger.as_ref() } - /// Returns the tag message. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. + + + + #[must_use] pub fn message(&self) -> &str { &self.message } - /// Returns the tag metadata. - /// - /// # How it works - /// Returns a reference to the [`CommitMeta`] struct containing timestamp and - /// timezone data for the tag's creation. + + + + + #[must_use] pub const fn meta(&self) -> &CommitMeta { &self.meta diff --git a/libvctrl_handler/src/types/core/tree.rs b/libvctrl_handler/src/types/core/tree.rs index 497b04e8..d9780895 100644 --- a/libvctrl_handler/src/types/core/tree.rs +++ b/libvctrl_handler/src/types/core/tree.rs @@ -1,16 +1,16 @@ -//! Tree object and entry representation. -//! -//! # Architecture -//! This module defines the [`Tree`] and [`TreeEntry`] structs, which represent -//! directory listings in the Git object model. A tree maps names to modes and -//! object hashes, forming the hierarchical structure of a repository snapshot. -//! -//! # Design Rationale: Canonical Sorting -//! Git requires tree entries to be sorted in a very specific, canonical order to -//! ensure that identical directory states always produce identical hashes. This -//! module enforces that sorting rule via the private `compare_tree_entries` -//! function. By sorting upon construction, the [`Tree::new`] method guarantees -//! that any `Tree` instance in memory is immediately valid and ready for hashing. + + + + + + + + + + + + + use super::hash::Hash; use crate::constants::MAX_TREE_ENTRIES; @@ -19,28 +19,28 @@ 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 +49,99 @@ pub struct TreeEntry { } impl TreeEntry { - /// Creates a new tree entry. - /// - /// # How it works - /// Delegates to [`validate_tree_entry_name`](crate::validation::validate_tree_entry_name) - /// to ensure the name is a single path component without forbidden characters. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidName`] if the entry name is invalid (e.g., contains slashes). - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::tree::TreeEntry; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::enums::EntryKind; - /// # use libvctrl_handler::VctrlError; - /// # let hash = Hash::from_bytes(&[0_u8; 64])?; - /// assert!(TreeEntry::new("valid.txt".into(), EntryKind::Blob, hash).is_ok()); - /// assert!(TreeEntry::new("invalid/path.txt".into(), EntryKind::Blob, hash).is_err()); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + pub fn new(name: String, kind: EntryKind, hash: Hash) -> Result { validate_tree_entry_name(&name)?; Ok(Self { name, kind, hash }) } - /// Returns the entry name. + #[must_use] pub fn name(&self) -> &str { &self.name } - /// Returns the entry kind. + #[must_use] pub const fn kind(&self) -> EntryKind { self.kind } - /// Returns the hash of the entry. + #[must_use] pub const fn hash(&self) -> &Hash { &self.hash } } -/// A Git tree object (directory listing). -/// -/// Entries are always stored in Git-sorted order: tree entries (directories) -/// are compared as if their name has a trailing `/` appended. -/// -/// # Why this exists -/// Provides a strongly-typed, validated representation of a directory. By sorting -/// and checking for duplicates upon construction, the [`Tree::new`] method acts as -/// a gatekeeper, guaranteeing that any `Tree` instance in memory is structurally -/// sound and ready to be serialized into a canonical format. + + + + + + + + + + #[derive(Clone, Debug, PartialEq, Eq)] pub struct Tree { entries: Vec, } impl Tree { - /// Creates a new tree from a vector of entries. - /// - /// Entries are sorted according to Git tree ordering rules. - /// Duplicate entry names are rejected. - /// - /// # How it works - /// 1. Checks the entry count against [`MAX_TREE_ENTRIES`](crate::constants::MAX_TREE_ENTRIES). - /// 2. Sorts the entries in-place using `compare_tree_entries`. - /// 3. Scans for duplicate names using a sliding window (`windows(2)`), rejecting - /// the tree if any are found. - /// - /// # Errors - /// - /// Returns [`VctrlError::ExceededMaxSize`] if the entry count exceeds `MAX_TREE_ENTRIES`. - /// Returns [`VctrlError::InvalidTreeStructure`] if duplicate names are found. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_handler::types::core::tree::{Tree, TreeEntry}; - /// # use libvctrl_handler::types::core::hash::Hash; - /// # use libvctrl_handler::enums::EntryKind; - /// # use libvctrl_handler::VctrlError; - /// # let hash = Hash::from_bytes(&[0_u8; 64])?; - /// let e1 = TreeEntry::new("b.txt".into(), EntryKind::Blob, hash)?; - /// let e2 = TreeEntry::new("a.txt".into(), EntryKind::Blob, hash)?; - /// let tree = Tree::new(vec![e1, e2])?; - /// // Entries are sorted automatically - /// assert_eq!(tree.entries()[0].name(), "a.txt"); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn new(entries: Vec) -> Result { let max_entries = usize::try_from(MAX_TREE_ENTRIES).unwrap_or(usize::MAX); if entries.len() > max_entries { @@ -168,45 +168,45 @@ impl Tree { 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) } } -/// 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(); diff --git a/libvctrl_handler/src/types/core/user_id.rs b/libvctrl_handler/src/types/core/user_id.rs index cf46707a..a93c66a7 100644 --- a/libvctrl_handler/src/types/core/user_id.rs +++ b/libvctrl_handler/src/types/core/user_id.rs @@ -1,47 +1,47 @@ -//! User identity representation. -//! -//! # Architecture -//! This module defines the [`UserID`] struct, which represents the `Name ` -//! syntax used in Git commits and tags. User identities are critical for audit -//! trails and blame calculations. -//! -//! # Design Rationale: Security by Construction -//! Git's internal text format relies on specific characters (like `<`, `>`, and `\n`) -//! as delimiters. If a username or email contains these characters, it can corrupt -//! the commit object structure or inject malicious headers. The [`UserID::new`] -//! constructor acts as a strict validation gate. By rejecting empty strings, control -//! characters, and missing `@` symbols at construction time, the crate guarantees -//! that any `UserID` instance in memory is safe to serialize into a Git object. + + + + + + + + + + + + + + use crate::constants::MAX_NAME_LENGTH; use crate::errors::VctrlError; -/// A user identity (author or committer). -/// -/// # Why this exists -/// Provides a strongly-typed, validated wrapper around the `Name ` concept. -/// By requiring construction via [`new`](Self::new), the crate ensures that every -/// `UserID` adheres to length and character constraints. Once constructed, the -/// identity is immutable, ensuring safe, concurrent sharing across threads. -/// -/// # How it works -/// The struct stores the name and email as owned `String`s. The constructor -/// performs a series of checks: it verifies that neither string is empty, neither -/// exceeds [`MAX_NAME_LENGTH`](crate::constants::MAX_NAME_LENGTH), neither contains -/// ASCII control characters (like newlines), and the email contains an `@` symbol. -/// -/// # Examples -/// -/// Creating a valid user identity: -/// -/// ``` -/// # use libvctrl_handler::types::core::user_id::UserID; -/// # use libvctrl_handler::VctrlError; -/// let user = UserID::new("Alice".to_string(), "alice@example.com".to_string())?; -/// assert_eq!(user.name(), "Alice"); -/// assert_eq!(user.email(), "alice@example.com"); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Clone, Debug, PartialEq, Eq)] pub struct UserID { name: String, @@ -49,32 +49,32 @@ pub struct UserID { } impl UserID { - /// Creates a new `UserID`. - /// - /// # How it works - /// Performs a multi-stage validation process: - /// 1. Checks `name` for emptiness, length limits (using `usize::try_from` for - /// 32-bit architecture safety), and ASCII control characters. - /// 2. Checks `email` for emptiness, length limits, ASCII control characters, - /// and the presence of an `@` symbol. - /// If any check fails, an error is returned and the original strings are dropped. - /// - /// # Errors - /// - /// Returns [`VctrlError::InvalidName`] if the name is empty, too long, or contains control characters. - /// Returns [`VctrlError::InvalidEmail`] if the email is empty, lacks `@`, or contains control characters. - /// - /// # Examples - /// - /// Handling an invalid email: - /// - /// ``` - /// # use libvctrl_handler::types::core::user_id::UserID; - /// # use libvctrl_handler::VctrlError; - /// let result = UserID::new("Bob".to_string(), "bob-example.com".to_string()); - /// assert!(matches!(result, Err(VctrlError::InvalidEmail(_)))); - /// # Ok::<(), VctrlError>(()) - /// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn new(name: String, email: String) -> Result { let max_len = usize::try_from(MAX_NAME_LENGTH).unwrap_or(usize::MAX); if name.is_empty() { @@ -111,21 +111,21 @@ impl UserID { Ok(Self { name, email }) } - /// Returns the user name. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. This - /// avoids allocation when the caller only needs to read the name. + + + + + #[must_use] pub fn name(&self) -> &str { &self.name } - /// Returns the email address. - /// - /// # How it works - /// Returns a string slice (`&str`) borrowing from the internal `String`. This - /// avoids allocation when the caller only needs to read the email. + + + + + #[must_use] pub fn email(&self) -> &str { &self.email diff --git a/libvctrl_handler/src/types/mod.rs b/libvctrl_handler/src/types/mod.rs index 48a446b7..4eddfdbe 100644 --- a/libvctrl_handler/src/types/mod.rs +++ b/libvctrl_handler/src/types/mod.rs @@ -1,65 +1,65 @@ -//! Core data types for Git objects. -//! -//! # Architecture -//! This module serves as the central registry for strongly-typed, immutable -//! representations of Git objects and domain concepts. By isolating these data -//! structures into a dedicated `types` module, the crate separates its abstract -//! contracts (in `traits`) from the concrete data carriers used in serialization, -//! manipulation, and network transfer. -//! -//! # Design Rationale: Fallible Construction -//! All types in this module enforce strict invariants during construction (e.g., -//! [`Hash`] requires exactly 64 bytes, [`Commit`] rejects duplicate parents). By -//! making constructors fallible (returning `Result`), the crate guarantees that -//! invalid states are unrepresentable at runtime. Once constructed, the types are -//! immutable, ensuring thread-safe sharing without external synchronization. -//! -//! # Facade Pattern -//! This module acts as a facade. It delegates the definitions to the `core` -//! submodule and selectively re-exports the public types to the top level. This -//! provides a clean, flat namespace for consumers (e.g., `libvctrl_handler::types::Commit`) -//! while keeping the internal module structure logically separated by domain. - -/// Core data type definitions for Git objects and domain concepts. -/// -/// # Why this exists -/// Houses the actual struct and enum definitions. Grouping these into a `core` -/// submodule prevents the parent `types` module from becoming a monolithic file, -/// allowing each object type (blob, tree, commit, etc.) to be developed and -/// tested in isolation. -/// -/// # Examples -/// -/// ``` -/// // The core submodule is accessible for advanced or internal use. -/// use libvctrl_handler::types::core; -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod core; -/// Re-exports of fundamental Git object types for ergonomic, flat access. -/// -/// # Why this exists -/// Provides a flattened import path. Consumers can directly use -/// `libvctrl_handler::types::Blob` instead of navigating the full -/// `libvctrl_handler::types::core::blob::Blob` path. This reduces boilerplate in consumer -/// code while keeping the internal module structure logically separated. -/// -/// # Examples -/// -/// Importing and using multiple core types: -/// -/// ``` -/// # use libvctrl_handler::types::{Blob, Hash, Tree}; -/// # use libvctrl_handler::VctrlError; -/// let raw_bytes = [0_u8; 64]; -/// let hash = Hash::from_bytes(&raw_bytes)?; -/// let blob = Blob::new(b"content".to_vec())?; -/// let tree = Tree::new(vec![])?; -/// -/// assert_eq!(blob.size(), 7); -/// assert!(tree.is_empty()); -/// # Ok::<(), VctrlError>(()) -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + pub use core::{ blob::Blob, commit::{Commit, CommitMeta}, diff --git a/libvctrl_handler/src/validation/hash.rs b/libvctrl_handler/src/validation/hash.rs index 51f08c4a..5b00858b 100644 --- a/libvctrl_handler/src/validation/hash.rs +++ b/libvctrl_handler/src/validation/hash.rs @@ -1,59 +1,59 @@ -//! Hash validation utilities. -//! -//! # Architecture -//! This module provides standalone validation for byte slices intended to be used -//! as Git object hashes. It ensures that data read from untrusted sources (like -//! network packfiles) is the correct length before attempting to construct a -//! [`Hash`](crate::Hash) type. -//! -//! # Design Rationale: Compile-Time Evaluation -//! The primary validation function is implemented as a `const fn`. This is a -//! critical architectural decision: it allows validation to occur at compile time -//! if the input byte slice is a known constant. This shifts the computational -//! overhead to the compiler, achieving true zero-cost runtime validation for -//! static data. + + + + + + + + + + + + + + use crate::constants::HASH_LENGTH; use crate::errors::VctrlError; -/// Validates that a byte slice is exactly `HASH_LENGTH` bytes long. -/// -/// # Why this exists -/// Git's SHA-512 implementation requires exactly 64 bytes. Passing a slice of -/// incorrect length to a hash constructor would either cause a runtime panic -/// (if using fixed-size array conversion) or silently produce an invalid hash. -/// This function provides a safe, fallible boundary to verify length before -/// memory allocation or cryptographic processing. -/// -/// # How it works -/// As a `const fn`, this can be evaluated by the compiler. If the input is a -/// static byte array (e.g., `b"..."`), the compiler can resolve the `Result` -/// at compile time, eliminating the runtime branch entirely. -/// -/// # Errors -/// -/// Returns [`VctrlError::InvalidHashLength`] if the slice length does not match -/// [`HASH_LENGTH`]. -/// -/// # Examples -/// -/// Validating a correctly sized slice: -/// -/// ``` -/// # use libvctrl_handler::validation::validate_hash_bytes; -/// let valid_hash = [0_u8; 64]; -/// assert!(validate_hash_bytes(&valid_hash).is_ok()); -/// ``` -/// -/// Handling an invalid slice: -/// -/// ``` -/// # use libvctrl_handler::validation::validate_hash_bytes; -/// # use libvctrl_handler::VctrlError; -/// let invalid_hash = [0_u8; 32]; -/// let result = validate_hash_bytes(&invalid_hash); -/// assert!(matches!(result, Err(VctrlError::InvalidHashLength(32)))); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub const fn validate_hash_bytes(bytes: &[u8]) -> Result<(), VctrlError> { if bytes.len() != HASH_LENGTH { return Err(VctrlError::InvalidHashLength(bytes.len())); diff --git a/libvctrl_handler/src/validation/mod.rs b/libvctrl_handler/src/validation/mod.rs index 351bdb69..13a713fc 100644 --- a/libvctrl_handler/src/validation/mod.rs +++ b/libvctrl_handler/src/validation/mod.rs @@ -1,76 +1,76 @@ -//! Pure validation functions for names, references, and hashes. -//! -//! # Architecture -//! This module separates validation logic from data structure construction. By isolating -//! these checks into pure, standalone functions, we adhere to the "fail-fast" principle: -//! inputs are scrutinized before any memory allocation or state mutation occurs. -//! -//! # Design Rationale: Pure Functions vs. Constructors -//! While constructors like [`Hash::from_bytes`](crate::Hash::from_bytes) also perform validation, -//! extracting these checks into standalone functions allows consumers to validate raw, -//! unstructured data (e.g., from network streams or untrusted user input) before deciding -//! how to process it. This avoids partial commits of invalid data and makes the validation -//! logic trivially testable without constructing the full object. -//! -//! # Safety and Performance -//! These functions are entirely pure with no side effects. They operate on borrowed slices -//! (`&str`, `&[u8]`) and perform zero heap allocations. The compiler aggressively inlines -//! these checks when used within constructors, achieving zero-cost abstraction. -//! -//! # Examples -//! *Note: The following examples assume this crate is named `libvctrl_handler`.* -//! -//! ``` -//! # use libvctrl_handler::validation::validate_name; -//! # use libvctrl_handler::VctrlError; -//! let valid_name = "feature_branch"; -//! assert!(validate_name(valid_name).is_ok()); -//! -//! let invalid_name = ""; -//! assert!(matches!(validate_name(invalid_name), Err(VctrlError::InvalidName(_)))); -//! ``` - -/// Hash validation utilities. -/// -/// # Why this exists -/// Provides standalone validation for byte slices intended to be used as Git object hashes. -/// This ensures that data read from untrusted sources (like network packfiles) is the correct -/// length and format before attempting to construct a [`Hash`](crate::Hash) type, preventing -/// unbound allocations or cryptographic mismatches. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub mod hash; -/// Name and reference validation utilities. -/// -/// # Why this exists -/// Git has strict rules for naming references (branches, tags) and tree entries. -/// For example, names cannot contain control characters, cannot be empty, and cannot -/// contain certain path components like `..`. This module enforces these rules to prevent -/// filesystem traversal vulnerabilities and repository corruption. + + + + + + + pub mod name; -/// Re-export of [`validate_hash_bytes`](hash::validate_hash_bytes) for ergonomic top-level access. -/// -/// Validates that a byte slice is the correct length to be a hash. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::validation::validate_hash_bytes; -/// let valid_hash = [0_u8; 64]; -/// assert!(validate_hash_bytes(&valid_hash).is_ok()); -/// ``` + + + + + + + + + + + pub use hash::validate_hash_bytes; -/// Re-exports of name and reference validation utilities. -/// -/// Provides ergonomic access to functions that enforce Git naming rules. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::validation::{validate_name, validate_ref_name, validate_tree_entry_name}; -/// assert!(validate_name("valid_name").is_ok()); -/// assert!(validate_ref_name("refs/heads/main").is_ok()); -/// assert!(validate_tree_entry_name("file.txt").is_ok()); -/// ``` + + + + + + + + + + + + pub use name::{validate_name, validate_ref_name, validate_tree_entry_name}; diff --git a/libvctrl_handler/src/validation/name.rs b/libvctrl_handler/src/validation/name.rs index 51452d02..897bfa14 100644 --- a/libvctrl_handler/src/validation/name.rs +++ b/libvctrl_handler/src/validation/name.rs @@ -1,50 +1,50 @@ -//! Name and reference validation utilities. -//! -//! # Architecture -//! Git has strict rules for naming references (branches, tags) and tree entries. -//! This module enforces these rules to prevent filesystem traversal vulnerabilities, -//! repository corruption, and ambiguity in revision parsing. -//! -//! # Design Rationale: Layered Validation -//! Validation is structured hierarchically. [`validate_name`] provides baseline -//! sanitization (length, emptiness, control characters). Specialized functions -//! like [`validate_ref_name`] and [`validate_tree_entry_name`] build upon this -//! baseline, adding domain-specific constraints. This prevents duplication and -//! ensures all names are fundamentally safe before context-specific rules are applied. + + + + + + + + + + + + + use crate::constants::MAX_NAME_LENGTH; use crate::errors::VctrlError; use std::path::Path; -/// Validates a generic name. -/// -/// # Why this exists -/// Establishes the minimum safety criteria for any string used as an identifier -/// in the version control system. It prevents empty strings (which cause ambiguity), -/// excessively long strings (which can exhaust memory or trigger filesystem errors), -/// and ASCII control characters (which can corrupt terminal output or interprocess -/// communication). -/// -/// # How it works -/// The function checks the byte length of the string against [`MAX_NAME_LENGTH`]. -/// Because [`MAX_NAME_LENGTH`] is a `u64`, it must be safely downcast to `usize` -/// using `try_from` to support 32-bit architectures where `usize` is smaller than `u64`. -/// It then iterates over the bytes to detect ASCII control characters (e.g., `\0`, `\n`, `\t`). -/// -/// # Errors -/// -/// Returns [`VctrlError::InvalidName`] if the name is empty, exceeds the maximum -/// allowed length, or contains ASCII control characters. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_handler::validation::validate_name; -/// assert!(validate_name("valid_name").is_ok()); -/// assert!(validate_name("").is_err()); -/// assert!(validate_name(&"a".repeat(256)).is_err()); -/// assert!(validate_name("invalid\nname").is_err()); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn validate_name(name: &str) -> Result<(), VctrlError> { if name.is_empty() { return Err(VctrlError::InvalidName("name is empty".into())); @@ -63,41 +63,41 @@ 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("..") @@ -130,38 +130,38 @@ pub fn validate_ref_name(name: &str) -> Result<(), VctrlError> { 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_plumbing/src/cat_file.rs b/libvctrl_plumbing/src/cat_file.rs index 7e1ba97b..4f316b39 100644 --- a/libvctrl_plumbing/src/cat_file.rs +++ b/libvctrl_plumbing/src/cat_file.rs @@ -1,209 +1,209 @@ -//! # Cat-File Plumbing Command -//! -//! This module implements the `cat-file` plumbing command, a fundamental -//! building block for inspecting objects in a libvctrl repository. It provides -//! both single-object queries and batch processing for integration with -//! higher-level porcelain commands. -//! -//! ## Why this module exists -//! -//! Plumbing commands operate directly on object stores and decoders without -//! user-friendly formatting. `cat-file` is essential for debugging, scripting, -//! and implementing other commands that need to inspect raw object content or -//! metadata. -//! -//! The module is designed to be backend-agnostic: it accepts any -//! [`ObjectStore`] and any [`Decoder`] via trait objects, enabling the same -//! logic to work with in-memory stores, filesystem stores, and custom -//! decoders. -//! -//! ## How it works -//! -//! The core function [`cat_file`] resolves an object name (a 128-character -//! hexadecimal SHA-512 hash), retrieves the encoded bytes from the store, -//! decodes the type using a series of decoder attempts, and then produces -//! output according to the requested [`CatFileMode`]. -//! -//! Batch mode ([`cat_file_batch`]) reads object names line-by-line and writes -//! formatted information, optionally including pretty-printed content. It -//! supports custom format strings and NUL-terminated input/output for robust -//! scripting. -//! -//! ## Safety and correctness -//! -//! All parsing is strict: hashes must be exactly 128 hex characters, hex -//! digits must be valid, and objects must decode successfully. Errors are -//! returned as [`VctrlError`] rather than panicking, making the command safe -//! to use in long-running processes. -//! -//! # Examples -//! -//! Retrieve the type of a stored blob: -//! -//! ``` -//! # use libvctrl::{ -//! # Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore, -//! # }; -//! # use libvctrl_core::codec::BinaryDecoder; -//! # use libvctrl_plumbing::{cat_file, CatFileMode}; -//! # use std::io::Cursor; -//! # fn main() -> Result<(), libvctrl::VctrlError> { -//! // Create a blob and store it. -//! let blob = Blob::new(b"hello".to_vec())?; -//! let mut encoded = Vec::new(); -//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; -//! let hash = Sha512Hasher.hash(encoded.as_slice())?; -//! let mut store = MemoryStore::new(); -//! store.put(&hash, &encoded)?; -//! -//! // Query its type. -//! let hash_hex = hash.to_string(); -//! let mut output = Vec::new(); -//! cat_file( -//! &store, -//! &BinaryDecoder, -//! &hash_hex, -//! CatFileMode::ObjectType, -//! &mut output, -//! )?; -//! assert_eq!(String::from_utf8(output).unwrap(), "blob\n"); -//! # Ok(()) -//! # } -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use libvctrl::{Decoder, EntryKind, Hash, ObjectStore, VctrlError}; use std::fmt::Write; use std::io::{BufRead, Write as IoWrite}; -/// Specifies the operation mode for the [`cat_file`] command. -/// -/// Each variant instructs the command to produce different output about a -/// single object. The mode determines whether the object is checked for -/// existence, its type is printed, its size is printed, its content is -/// pretty-printed, or its raw bytes are emitted (optionally with a type -/// check). -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use libvctrl_plumbing::{CatFileMode, ObjectType}; -/// let mode = CatFileMode::PrettyPrint; -/// let raw_blob = CatFileMode::Raw(ObjectType::Blob); -/// ``` + + + + + + + + + + + + + + + + + #[derive(Clone, Copy)] pub enum CatFileMode { - /// Pretty-print the object content in a human-readable format. + PrettyPrint, - /// Print only the object type (one of `blob`, `tree`, `commit`, `tag`). + ObjectType, - /// Print the encoded object size in bytes. + ObjectSize, - /// Check existence only; produce no output, but return an error if the - /// object is missing or corrupted. + + Exists, - /// Output the raw encoded bytes, optionally verifying the object type - /// matches the expected [`ObjectType`] parameter. + + Raw(ObjectType), } -/// Logical object types recognized by the version control system. -/// -/// This enum mirrors the types defined in `libvctrl_handler`, but is localized -/// for plumbing command reporting. It is used to verify expected object types -/// and to format type strings. -/// -/// # Examples -/// -/// ``` -/// # use libvctrl_plumbing::ObjectType; -/// let blob = ObjectType::Blob; -/// assert_eq!(blob, ObjectType::Blob); -/// ``` + + + + + + + + + + + + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ObjectType { - /// A binary large object (file content). + Blob, - /// A directory tree. + Tree, - /// A commit object. + Commit, - /// An annotated tag object. + Tag, } -/// Executes a single `cat-file` query against an object store. -/// -/// This function resolves `object_name` (a 128-character hexadecimal hash), -/// retrieves the encoded bytes, decodes the object, and writes the requested -/// output to `writer` based on `mode`. -/// -/// # Why this function exists -/// -/// Centralizes all `cat-file` logic so that every caller (CLI, library, -/// batch mode) shares the same validation and formatting rules. -/// -/// # How it works -/// -/// 1. Parse `object_name` into a [`Hash`]. -/// 2. Fetch the encoded bytes from `store`. -/// 3. Depending on `mode`, either: -/// - Return `Ok(())` for `Exists`. -/// - Decode the type and print it for `ObjectType`. -/// - Print the encoded length for `ObjectSize`. -/// - Decode and pretty-print for `PrettyPrint`. -/// - Verify the actual type matches `Raw(expected_type)` and then write the -/// raw bytes. -/// -/// # Errors -/// -/// Returns [`VctrlError`] if: -/// - `object_name` is not a valid 128-character hex string. -/// - The object is not found in the store. -/// - The encoded bytes fail to decode as any known object type. -/// - The actual type does not match the expected type in `Raw` mode. -/// - The writer fails. -/// -/// # Examples -/// -/// Pretty-print a stored commit: -/// -/// ``` -/// # use libvctrl::{ -/// # Commit, Encoder, Hasher, ObjectStore, BinaryEncoder, Sha512Hasher, MemoryStore, -/// # Hash, UserID, -/// # }; -/// # use libvctrl_core::codec::BinaryDecoder; -/// # use libvctrl_plumbing::{cat_file, CatFileMode}; -/// # use std::io::Cursor; -/// # fn main() -> Result<(), libvctrl::VctrlError> { -/// // Create a simple commit. -/// let tree = Hash::from_bytes(&[0u8; 64])?; -/// let author = UserID::new("alice".into(), "alice@example.com".into())?; -/// let committer = UserID::new("bob".into(), "bob@example.com".into())?; -/// let commit = Commit::new(tree, vec![], author, committer, "initial".into())?; -/// -/// // Encode, hash, and store. -/// let mut encoded = Vec::new(); -/// BinaryEncoder.encode_commit(&commit, &mut encoded)?; -/// let hash = Sha512Hasher.hash(encoded.as_slice())?; -/// let mut store = MemoryStore::new(); -/// store.put(&hash, &encoded)?; -/// -/// // Pretty-print the commit. -/// let mut output = Vec::new(); -/// cat_file( -/// &store, -/// &BinaryDecoder, -/// &hash.to_string(), -/// CatFileMode::PrettyPrint, -/// &mut output, -/// )?; -/// assert!(String::from_utf8(output).unwrap().contains("tree")); -/// # Ok(()) -/// # } -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pub fn cat_file( store: &dyn ObjectStore, decoder: &D, @@ -258,104 +258,104 @@ pub fn cat_file( } } -/// 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)] 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, @@ -423,16 +423,16 @@ pub fn cat_file_batch( 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, @@ -467,15 +467,15 @@ fn handle_one_object( Ok((info, content)) } -/// Parses a 128-character hexadecimal string into a [`Hash`]. -/// -/// The hash must be exactly 128 hex digits (64 bytes). Any length mismatch or -/// invalid hex character results in an error. -/// -/// # Errors -/// -/// Returns [`VctrlError::Other`] if the length is not 128 or a hex digit is -/// invalid. + + + + + + + + + fn parse_hash(s: &str) -> Result { if s.len() != 128 { let actual_len = s.len(); @@ -492,16 +492,16 @@ fn parse_hash(s: &str) -> Result { 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 +518,18 @@ fn decode_type(decoder: &D, encoded: &[u8]) -> Result(decoder: &D, encoded: &[u8]) -> Result { if let Ok(blob) = decoder.decode_blob(encoded) { return Ok(String::from_utf8_lossy(blob.data()).to_string()); @@ -585,7 +585,7 @@ fn pretty_print(decoder: &D, encoded: &[u8]) -> Result &'static str { match t { ObjectType::Blob => "blob", @@ -595,9 +595,9 @@ const fn obj_type_to_str(t: ObjectType) -> &'static str { } } -/// Returns the POSIX file mode corresponding to an [`EntryKind`]. -/// -/// This is used in tree pretty-printing to display the mode in octal. + + + const fn entry_mode(kind: EntryKind) -> u32 { match kind { EntryKind::Blob => 0o100_644, @@ -609,11 +609,11 @@ const fn entry_mode(kind: EntryKind) -> u32 { } } -/// Formats the info line for batch output based on a custom format string. -/// -/// Replaces `%(objectname)`, `%(objecttype)`, and `%(objectsize)` with -/// actual values. The `_mode` parameter is reserved for future use (e.g., -/// `%(objectmode)`). + + + + + fn format_batch_info( format: &str, hash: &Hash, diff --git a/libvctrl_plumbing/src/lib.rs b/libvctrl_plumbing/src/lib.rs index 661f120a..59393364 100644 --- a/libvctrl_plumbing/src/lib.rs +++ b/libvctrl_plumbing/src/lib.rs @@ -1,94 +1,94 @@ -//! # libvctrl_plumbing -//! -//! Plumbing commands for the libvctrl version control system. -//! -//! This crate provides low-level commands that operate directly on object -//! stores, references, and codecs. Unlike porcelain commands, plumbing -//! commands expose detailed control and are intended for scripting and for -//! building higher-level commands. -//! -//! ## Why this crate exists -//! -//! Version control systems separate low-level (plumbing) commands from -//! high-level (porcelain) commands. Plumbing commands are stable, composable, -//! and designed for programmatic use. They perform one job well and produce -//! machine-readable output where possible. This crate implements those -//! foundational commands using the unified facade provided by the -//! [`libvctrl`](https://docs.rs/libvctrl) crate. -//! -//! ## Architecture -//! -//! The crate is organized by command modules: -//! -//! - [`cat_file`](crate::cat_file) — inspects object content and metadata by -//! hash. -//! -//! Additional plumbing commands will follow the same pattern. Each module -//! contains one or more public functions that accept trait objects -//! (for example, `&dyn ObjectStore` and `&dyn Decoder`), making the commands -//! backend-agnostic and independently testable. -//! -//! ## How it works -//! -//! A typical plumbing command: -//! -//! 1. Parses and validates its arguments. -//! 2. Uses an [`ObjectStore`](libvctrl::ObjectStore) to fetch raw bytes. -//! 3. Uses a [`Decoder`](libvctrl::Decoder) to interpret those bytes. -//! 4. Writes the requested result to an output writer. -//! -//! This design allows the same command to run against any storage backend -//! (in-memory, filesystem, remote) and any codec, as long as the appropriate -//! traits are implemented. -//! -//! ## Safety and correctness -//! -//! All commands return [`VctrlError`](libvctrl::VctrlError) on failure and -//! never panic on malformed user input. Output writers are used exclusively -//! through [`std::io::Write`], and all I/O errors are propagated with their -//! original error wrapped in the unified error type. -//! -//! ## Example -//! -//! The following example stores a blob and uses [`cat_file`] to query its -//! type: -//! -//! ``` -//! # use libvctrl::{Blob, Encoder, Hasher, ObjectStore, BinaryEncoder, BinaryDecoder, Sha512Hasher, MemoryStore}; -//! # use libvctrl_plumbing::{cat_file, CatFileMode}; -//! # fn main() -> Result<(), libvctrl::VctrlError> { -//! let blob = Blob::new(b"example".to_vec())?; -//! -//! let mut encoded = Vec::new(); -//! BinaryEncoder.encode_blob(&blob, &mut encoded)?; -//! let hash = Sha512Hasher.hash(&mut encoded.as_slice())?; -//! -//! let mut store = MemoryStore::new(); -//! store.put(&hash, &encoded)?; -//! -//! let mut out = Vec::new(); -//! cat_file( -//! &store, -//! &BinaryDecoder, -//! &hash.to_string(), -//! CatFileMode::ObjectType, -//! &mut out, -//! )?; -//! -//! assert_eq!(String::from_utf8(out).unwrap(), "blob\n"); -//! # Ok(()) -//! # } -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[cfg(test)] use libvctrl_core as _; -/// Plumbing command for inspecting object content and metadata. -/// -/// This module implements the `cat-file` command, which retrieves an object by -/// its hash and prints its type, size, pretty-printed content, or raw bytes -/// depending on the requested mode. It also supports batch processing of -/// multiple objects with configurable formatting. + + + + + + pub mod cat_file; pub use cat_file::{BatchOptions, CatFileMode, ObjectType, cat_file, cat_file_batch}; diff --git a/libvctrl_sha512/src/hkdf.rs b/libvctrl_sha512/src/hkdf.rs index cbcef17b..5d7e06dc 100644 --- a/libvctrl_sha512/src/hkdf.rs +++ b/libvctrl_sha512/src/hkdf.rs @@ -1,49 +1,49 @@ -//! # 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); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use crate::hmac::HMAC; diff --git a/libvctrl_sha512/src/hmac.rs b/libvctrl_sha512/src/hmac.rs index c50fe033..2682f9d4 100644 --- a/libvctrl_sha512/src/hmac.rs +++ b/libvctrl_sha512/src/hmac.rs @@ -1,60 +1,60 @@ -//! # 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)); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + use crate::sha512::Hash; diff --git a/libvctrl_sha512/src/lib.rs b/libvctrl_sha512/src/lib.rs index 2a305773..755f54f7 100644 --- a/libvctrl_sha512/src/lib.rs +++ b/libvctrl_sha512/src/lib.rs @@ -1,88 +1,88 @@ -//! 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); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #![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) => { @@ -182,39 +182,39 @@ macro_rules! impl_hmac { }; } -/// 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) => { @@ -262,64 +262,64 @@ macro_rules! impl_hkdf { }; } -/// HMAC implementation generated for SHA-512. -/// -/// This module contains the [`HMAC`](crate::HMAC) type, produced by the -/// [`impl_hmac!`] macro. It provides HMAC-SHA512 one-shot and incremental -/// authentication. + + + + + pub mod hmac; -/// HKDF implementation generated for SHA-512. -/// -/// This module contains the [`HKDF`](crate::HKDF) type, produced by the -/// [`impl_hkdf!`] macro. It provides HKDF-SHA512 key derivation. + + + + pub mod hkdf; -/// SHA-512 hash function implementation. -/// -/// This module contains the [`Hash`](crate::Hash) type, which provides -/// incremental and one-shot SHA-512 hashing, along with verification and -/// zeroization support. + + + + + pub mod sha512; -/// Shared byte-order and verification helpers. -/// -/// This module contains the [`load_be`](crate::utils::load_be), -/// [`store_be`](crate::utils::store_be), and -/// [`verify`](crate::utils::verify) functions, as well as the -/// [`BLOCKBYTES`](crate::utils::BLOCKBYTES) and -/// [`BYTES`](crate::utils::BYTES) constants. + + + + + + + pub mod utils; -/// Optional SHA-384 implementation. -/// -/// This module is only available when the `sha384` feature is enabled. It -/// contains a SHA-384 hash type generated from the SHA-512 core. + + + + #[cfg(feature = "sha384")] pub mod sha384; -/// Re-export of the SHA-512 hash type. -/// -/// This makes the primary hash type directly available as -/// `libvctrl_sha512::Hash`. + + + + pub use sha512::Hash; -/// Re-export of the HMAC-SHA512 type. -/// -/// This makes the HMAC type directly available as -/// `libvctrl_sha512::HMAC`. + + + + pub use hmac::HMAC; -/// Re-export of the HKDF-SHA512 type. -/// -/// This makes the HKDF type directly available as -/// `libvctrl_sha512::HKDF`. + + + + pub use hkdf::HKDF; -/// Re-export of the SHA-512 utility constants. -/// -/// This provides convenient access to [`BLOCKBYTES`](crate::utils::BLOCKBYTES) -/// and [`BYTES`](crate::utils::BYTES) at the crate root. + + + + pub use utils::{BLOCKBYTES, BYTES}; #[cfg(test)] diff --git a/libvctrl_sha512/src/sha384.rs b/libvctrl_sha512/src/sha384.rs index 37493047..f0881f20 100644 --- a/libvctrl_sha512/src/sha384.rs +++ b/libvctrl_sha512/src/sha384.rs @@ -1,34 +1,34 @@ -//! # SHA-384 Hash -//! -//! This module provides the SHA-384 cryptographic hash function as specified -//! in FIPS 180-4. SHA-384 is a variant of SHA-512 that uses a different -//! initialization vector and truncates the final digest to 48 bytes. -//! -//! ## Design rationale -//! -//! SHA-384 shares the same compression function and message schedule as -//! SHA-512. Instead of duplicating the core algorithm, this module wraps -//! [`crate::sha512::Hash`] and overrides only the initialization vector and -//! output length. This reduces code size, simplifies auditing, and guarantees -//! consistency between the two hash functions. -//! -//! ## How it works -//! -//! The [`Hash`] struct holds an internal [`crate::sha512::Hash`] instance with -//! a custom state. During finalization, the full 64-byte SHA-512 digest is -//! computed and then truncated to the first 48 bytes. -//! -//! The module also invokes the [`impl_hmac!`] and [`impl_hkdf!`] macros to -//! generate HMAC-SHA-384 and HKDF-SHA-384 implementations. + + + + + + + + + + + + + + + + + + + + + + use crate::sha512::{Hash as Sha512Hash, State}; use crate::utils::load_be; -/// Creates a SHA-384 initialization vector. -/// -/// This internal helper constructs a [`State`] from the SHA-384 initial -/// hash values defined in FIPS 180-4. It returns a state that will be used -/// as the starting point for SHA-384 compression. + + + + + #[inline] fn new_state() -> State { const IV: [u8; 64] = [ @@ -45,62 +45,62 @@ fn new_state() -> State { State(t) } -/// SHA-384 hash context. -/// -/// This struct represents an incremental SHA-384 computation. It wraps -/// [`crate::sha512::Hash`] with a SHA-384-specific initialization vector and -/// truncates the final digest to 48 bytes. -/// -/// # Why this struct exists -/// -/// SHA-384 is defined as a truncated SHA-512 with a different IV. By -/// embedding the SHA-512 core, this struct avoids code duplication and -/// ensures the two algorithms stay synchronized. -/// -/// # How it works -/// -/// The internal SHA-512 state is initialized with [`new_state`]. Updates -/// are forwarded to the inner hash. Finalization computes the full 64-byte -/// SHA-512 digest and returns only the first 48 bytes. -/// -/// # Examples -/// -/// Incremental hashing: -/// -/// ``` -/// # use libvctrl_sha512::sha384::Hash; -/// let mut h = Hash::new(); -/// h.update(b"hello "); -/// h.update(b"world"); -/// let digest = h.finalize(); -/// assert_eq!(digest.len(), 48); -/// ``` -/// -/// One-shot hashing: -/// -/// ``` -/// # use libvctrl_sha512::sha384::Hash; -/// let digest = Hash::hash(b"abc"); -/// assert_eq!(digest.len(), 48); -/// ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + #[derive(Clone)] pub struct Hash(Sha512Hash); impl 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 { @@ -111,46 +111,46 @@ impl Hash { }) } - /// 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]; @@ -158,55 +158,55 @@ impl Hash { 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); - /// ``` + + + + + + + + + + + + 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(); - /// ``` + + + + + + + + + + + + + pub fn zeroize(&mut self) { self.0.zeroize(); } } impl Default for Hash { - /// Creates a default SHA-384 hash context. - /// - /// This is equivalent to calling [`Hash::new`]. - /// - /// # Examples - /// - /// ``` - /// # use libvctrl_sha512::sha384::Hash; - /// let h = Hash::default(); - /// let digest = h.finalize(); - /// assert_eq!(digest.len(), 48); - /// ``` + + + + + + + + + + + + fn default() -> Self { Self::new() } diff --git a/libvctrl_sha512/src/sha512.rs b/libvctrl_sha512/src/sha512.rs index ec10fe68..ed399591 100644 --- a/libvctrl_sha512/src/sha512.rs +++ b/libvctrl_sha512/src/sha512.rs @@ -1,77 +1,77 @@ #![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); -//! ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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() { @@ -80,49 +80,49 @@ impl W { 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 +134,10 @@ impl W { .wrapping_add(Self::small_sigma0(words[src_d])); } - /// Expands the first 16 words into the full 80-word message schedule. - /// - /// The expansion is performed in-place, overwriting the initial words with - /// the newly computed schedule entries. + + + + #[inline] fn expand(&mut self) { self.m(0, 14, 9, 1); @@ -158,10 +158,10 @@ impl W { self.m(15, 13, 8, 0); } - /// The SHA-512 compression function. - /// - /// This method applies the round function `f` for round index `i` using the - /// round constant `k`. It updates the eight working variables in-place. + + + + #[cfg_attr(feature = "opt_size", inline(never))] #[cfg_attr(not(feature = "opt_size"), inline(always))] #[allow(clippy::missing_const_for_fn)] @@ -186,11 +186,11 @@ impl W { )); } - /// Applies 16 rounds of the compression function using one group of round - /// constants. - /// - /// The `s` parameter selects which group of 16 constants (out of five) to - /// use. This design improves code reuse while maintaining performance. + + + + + #[allow(clippy::unreadable_literal)] fn g(&self, state: &mut State, s: usize) { const ROUND_CONSTANTS: [u64; 80] = [ @@ -296,10 +296,10 @@ impl W { } impl State { - /// Creates a new state initialized with the SHA-512 initial hash values. - /// - /// The initial values are the first 64 bits of the fractional parts of the - /// square roots of the first eight primes. + + + + pub(crate) fn new() -> Self { const IV: [u8; 64] = [ 0x6a, 0x09, 0xe6, 0x67, 0xf3, 0xbc, 0xc9, 0x08, 0xbb, 0x67, 0xae, 0x85, 0x84, 0xca, @@ -315,10 +315,10 @@ impl State { Self(t) } - /// Adds another state to this one using wrapping addition. - /// - /// This is used after the compression function to incorporate the previous - /// hash value, per the Merkle–Damgård construction. + + + + #[inline(always)] #[allow(clippy::missing_const_for_fn)] pub(crate) fn add(&mut self, x: &Self) { @@ -334,16 +334,16 @@ impl State { sx[7] = sx[7].wrapping_add(ex[7]); } - /// Writes the state as 64 bytes in big-endian order. + pub(crate) fn store(&self, out: &mut [u8]) { for (i, &e) in self.0.iter().enumerate() { store_be(out, i * 8, e); } } - /// Processes as many 128-byte blocks as possible from the input. - /// - /// Returns the number of bytes remaining that do not form a complete block. + + + pub(crate) fn blocks(&mut self, mut input: &[u8]) -> usize { let mut t = *self; let mut inlen = input.len(); @@ -367,59 +367,59 @@ 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 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 { @@ -430,10 +430,10 @@ impl Hash { } } - /// 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(); @@ -457,45 +457,45 @@ impl Hash { } } - /// 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] pub fn finalize(mut self) -> [u8; 64] { let mut padded = [0u8; 256]; @@ -515,82 +515,82 @@ impl Hash { 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)); - /// ``` + + + + + + + + + + + + + + + + + + #[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); @@ -601,9 +601,9 @@ impl Hash { } impl Default for Hash { - /// Returns a new SHA-512 hasher with the default initial state. - /// - /// Equivalent to [`Hash::new`]. + + + fn default() -> Self { Self::new() } diff --git a/libvctrl_sha512/src/utils.rs b/libvctrl_sha512/src/utils.rs index 48acfeb0..8899a549 100644 --- a/libvctrl_sha512/src/utils.rs +++ b/libvctrl_sha512/src/utils.rs @@ -1,142 +1,142 @@ -//! 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()) } -/// 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()); } -/// 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() { 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" } - ] -}