libvctrl
A robust, content-addressed version control engine for arbitrary data, designed for embedding into applications.
libvctrl provides the core data model, storage abstractions, hashing, encoding, commands, diffing, three-way merging, and cryptographic signing needed to build version control functionality directly into applications -- without shelling out to an external VCS or depending on a CLI tool. It is a library only and does not ship a binary.
Table of Contents
- Overview
- Architecture
- Installation
- Dependencies
- Quick Start
- Domain Model
- Storage
- Hashing
- Encoding
- Commands
- Diffing
- Merging
- Cryptographic Signing
- Error Handling
- Module Reference
- Testing
- Build and Lint
- Security Considerations
- Limitations
- Migration from v0.1.0
- Roadmap
- License
Overview
libvctrl implements a content-addressed version control engine similar in principle to Git's object model, but with key differences:
- SHA-512 hashes instead of SHA-1, providing 256 bits of collision resistance.
- Ed25519 signing for commit integrity verification, using the
ed25519-dalekcrate. - Validated user identity through the
UserIDtype from the
age-credentials crate, enforcing name and email format rules.
- Trait-based abstractions for storage, hashing, encoding, signing, diffing, and merging, allowing custom backends and algorithms without modifying the core.
- Command pattern for all operations, providing a uniform interface that
accepts mutable references to an
ObjectStoreand aRefStore. - Embedded design -- no CLI, no subprocess calls, no filesystem
assumptions beyond what the storage backend requires. The provided
MemoryStoreandMemoryRefStorerequire no filesystem at all.
Architecture
src/
lib.rs Crate root, re-exports all modules
error.rs VctrlError enum
codec/ Encoding format
mod.rs Encoder trait and re-exports
binary.rs BinaryEncoder implementation
command/ Command pattern operations
mod.rs Command trait and re-exports
branch.rs CreateBranch, DeleteBranch, GetBranch, SetHead
checkout.rs Checkout (recursive tree materialization)
create_commit.rs CreateCommit
log.rs Log (commit history traversal)
merge.rs MergeCommand
crypto/ Cryptographic signing
mod.rs Signer trait and re-exports
signer.rs LibrageSigner (Ed25519)
diff/ Tree diffing
mod.rs TreeDiff trait, DiffKind, DiffEntry
tree_diff.rs TreeDiffer implementation
domain/ Core domain types
mod.rs Re-exports all domain types
blob.rs Blob (content-addressed data)
commit.rs Commit (snapshot record)
hash.rs Hash (64-byte SHA-512), HashError
object.rs Object enum (Blob, Tree, Commit)
tree.rs Tree, TreeEntry, EntryKind, TreeError
user.rs UserID (from age-credentials), UserInfo
hashing/ Hashing trait and implementation
mod.rs Hasher trait and re-exports
sha512.rs Sha512Hasher
merge/ Three-way merge
mod.rs ThreeWayMerge trait
resolver.rs ConflictResolver trait
three_way.rs ThreeWayMerger implementation
storage/ Storage backends
mod.rs Re-exports
traits.rs ObjectStore, RefStore traits
memory.rs MemoryStore, MemoryRefStore
Installation
There are several ways to add libvctrl to your Rust project:
1. Using cargo add
This command will automatically add the dependency line to Cargo.toml.
2. Adding Manually in Cargo.toml
Add the following line to the [dependencies] section:
[]
= { = "https://github.com/mroczect/libvctrl.git" }
3. Clone the repository and use it as a local dependency (path)
If you want to develop or modify the library alongside your project, clone the repository first:
Then, in your project's Cargo.toml, navigate to the cloned path:
[]
= { = "../libvctrl" } # adjust the directory location
With this method, changes you make to the library will be immediately reflected in the main project upon compilation.
4. Fork and use it as a Git dependency from your fork
You can fork the repository to your own GitHub account, then use it the same way as method 1 or 2, just replace the URL to your fork repository:
[]
= { = "https://github.com/your-username/libvctrl.git" }
Toolchain Requirements
This library uses Rust edition 2024. Make sure your toolchain supports that edition (Rust 1.85.0 or later). To check the installed Rust version:
If your toolchain is older, update it with:
If for some reason you need to use an older edition (e.g., 2021), you can change the edition line in libvctrl's Cargo.toml from "2024" to "2021". However, keep in mind that some syntactic features may not be available.
Dependencies
libvctrl depends on the following crates (handled automatically by Cargo):
chrono0.4.45 (serdefeature)serde1.0.229 (derivefeature)serde_json1.0.151sha20.11.0thiserror2.0.19
All public types and traits are exported directly in the crate root, so you can import them easily:
use ;
See the tests/ directory in the repository for complete usage examples.
Dependencies
| Crate | Version | Purpose |
|---|---|---|
| chrono | 0.4.45 | Timestamps for commits (with serde feature) |
| serde | 1.0.229 | Serialization framework (with derive feature) |
| serde_json | 1.0.151 | JSON serialization |
| sha2 | 0.11.0 | SHA-512 digest computation |
| thiserror | 2.0.19 | Error derive macro |
| ed25519-dalek | 3.0.0 | Ed25519 signing and verification |
| rand | 0.8.7 | CSPRNG for signing key generation |
| age-credentials | 0.3.0 | Validated UserID type |
| librage | 1.1.0 | age encryption backend |
| age | 0.12.1 | age library (transitive, used by age-credentials) |
| tempfile | 3.27.0 | Atomic file writes (used by age-credentials) |
Quick Start
use ;
// Set up storage
let mut store = new;
let mut refs = new;
// Create a blob and store it
let blob = new;
let hasher = Sha512Hasher;
let blob_hash = hasher.hash_blob;
store.put.unwrap;
// Create a tree with one entry
let entry = new;
let tree = new.unwrap;
let encoder = BinaryEncoder;
let mut buf = Vecnew;
encoder.encode_tree;
let tree_hash = hasher.hash_tree_encoded;
store.put.unwrap;
// Create a branch and set HEAD
let author = new.unwrap;
CreateBranch
.execute.unwrap;
SetHead
.execute.unwrap;
// Create a commit
let commit_hash = CreateCommit .execute.unwrap;
// Sign the commit
let signer = generate;
let signature = signer.sign.unwrap;
let verifying_key = signer.verifying_key;
println!;
println!;
Domain Model
Blob
A content-addressed data container. The inner data field is private to
enforce encapsulation.
| Method | Signature | Description |
|---|---|---|
new |
(data: Vec<u8>) -> Self |
Construct from raw bytes |
as_bytes |
(&self) -> &[u8] |
Borrow the inner bytes |
into_bytes |
(self) -> Vec<u8> |
Consume and return the inner bytes |
Implements Debug, Clone, Serialize, Deserialize.
Hash
;
A 64-byte (512-bit) SHA-512 hash value. Copy because 64 bytes fits on
the stack.
| Method | Signature | Description |
|---|---|---|
from_bytes |
(bytes: [u8; 64]) -> Self |
Construct from a fixed-size array (const) |
from_slice |
(&[u8]) -> Result<Self, HashError> |
Construct from a slice; fails if length is not 64 |
from_hex |
(&str) -> Result<Self, HashError> |
Construct from a 128-character hex string |
as_bytes |
(&self) -> &[u8; 64] |
Borrow the inner byte array |
to_hex |
(&self) -> String |
Produce a 128-character lowercase hex string |
Implements Debug, Display, FromStr, Serialize (as hex),
Deserialize (from hex with validation), Clone, Copy, PartialEq,
Eq, Hash.
Tree and TreeEntry
// private, sorted by name
| Tree method | Signature | Description |
|---|---|---|
new |
(entries: Vec<TreeEntry>) -> Result<Self, TreeError> |
Construct, sort by name, detect duplicates |
entries |
(&self) -> &[TreeEntry] |
Borrow the sorted entries |
into_entries |
(self) -> Vec<TreeEntry> |
Consume and return the entries |
is_empty |
(&self) -> bool |
Check if there are no entries |
Entries are sorted on construction, making tree hashes deterministic regardless of input order.
Commit
| Field | Type | Description |
|---|---|---|
tree |
Hash |
Hash of the root tree this commit captures |
parents |
Vec<Hash> |
Zero or more parent commit hashes |
author |
UserID |
The original author (validated) |
committer |
UserID |
The identity that created this commit (validated) |
timestamp |
DateTime<Utc> |
Automatically set to Utc::now() on construction |
message |
String |
Commit message |
signature |
Option<Vec<u8>> |
Optional cryptographic signature bytes |
The timestamp is always Utc::now() at construction time. There is no way to
set a custom timestamp through Commit::new.
UserID and UserInfo
UserID is re-exported from the age-credentials crate. It is a validated
user identity with the following rules:
- Name: non-empty after trimming, minimum 2 characters, maximum 255 characters, only alphabetic, numeric, space, hyphen, apostrophe, or period characters.
- Email: non-empty after trimming, exactly one
@, non-empty local part and domain, maximum 254 characters, only alphanumeric, period, hyphen, underscore,@, or plus characters.
let uid = new?;
assert_eq!;
UserInfo is a plain, unvalidated struct retained for backward compatibility:
Commit and CreateCommit use UserID. Use UserInfo only if you need
an unvalidated identity (for example, when reading data that has already
been validated elsewhere).
Object
Tagged union of all storable types. Commit is boxed to avoid infinite
type recursion. obj_type() returns "blob", "tree", or "commit".
Storage
ObjectStore Trait
Trait for content-addressed object storage.
RefStore Trait
Trait for named reference and HEAD management. head() resolves HEAD to a
Hash (through symbolic reference or direct hex). head_ref_name() returns
the symbolic reference name, or None if HEAD is a direct hash or unset.
MemoryStore
In-memory ObjectStore backed by HashMap<Hash, Object>. Implements Default.
MemoryRefStore
In-memory RefStore backed by HashMap<String, Hash> and Option<String>
for HEAD. HEAD can be a symbolic reference (starting with refs/) or a
direct hex hash. Implements Default.
Hashing
Hasher Trait
Sha512Hasher
Computes SHA-512(prefix || length_u64_be || 0x00 || data).
| Method | Prefix |
|---|---|
hash_blob |
"blob " |
hash_tree_encoded |
"tree " |
hash_commit_encoded |
"commit " |
The type prefix and length header prevent cross-type hash collisions.
Encoding
Encoder Trait
BinaryEncoder
Binary format implementation. Appends to the provided buffer.
Binary Format Specification
Tree: version byte (0x01), entry count (u32 BE), per-entry: name length (u16 BE), name bytes, kind byte (0x00=Blob, 0x01=Tree), hash (64 bytes).
Commit: version byte (0x01), tree hash (64 bytes), parent count (u32 BE), parent hashes, author, committer, timestamp seconds (i64 BE), timestamp nanoseconds (u32 BE), message length (u32 BE"BE), message bytes, signature length (u32 BE), signature bytes.
User (author/committer): name length (u16 BE), name bytes, email length (u16 BE), email bytes.
Commands
Command Trait
Branch Operations
| Command | Output | Description |
|---|---|---|
CreateBranch { name, hash } |
() |
Create/update a reference; name must start with refs/heads/ |
DeleteBranch { name } |
() |
Delete a reference; name must start with refs/heads/ |
GetBranch { name } |
Option<Hash> |
Look up a reference; name must start with refs/heads/ |
SetHead
Sets HEAD. Target must start with refs/ or be a valid 128-char hex hash.
CreateCommit
Creates a commit with timestamp: Utc::now() and signature: None, stores
it, and updates the current branch reference if HEAD is symbolic. Returns
the commit hash.
Log
;
Traverses commit history from HEAD, following the first parent. Returns commits newest-first. Returns empty vector if HEAD is unset.
Checkout
Recursively materializes a tree into Vec<(String, Vec<u8>)> (path, data).
Depth capped at 1000.
MergeCommand
Executes three-way merge, returning the merged tree hash.
Diffing
TreeDiff Trait
DiffKind and DiffEntry
TreeDiffer
Converts both trees to BTreeMap, compares keys, and classifies entries
as Added, Removed, or Modified. Output is sorted by name.
Merging
ThreeWayMerge Trait
ConflictResolver Trait
Returns Some(resolved_data) to resolve a conflict, or None to fail.
ThreeWayMerger
Handles all nine combinations of (base, ours, theirs) presence/absence. Recurses into subtrees when both sides are trees. Calls ConflictResolver when both sides modified the same blob. Depth capped at 1000.
Cryptographic Signing
Signer Trait
A trait for signing arbitrary byte slices, intended for signing commit
hashes. The input should be the 64-byte Hash of the commit. The output
is a signature as raw bytes.
LibrageSigner
An Ed25519 signing implementation using ed25519_dalek::SigningKey.
LibrageSigner::generate
Generates a new Ed25519 signing key using the operating system CSPRNG
(rand::rngs::OsRng). The signing key is created from a random 32-byte
seed. This method cannot fail because OsRng is infallible in the
rand 0.8 API.
LibrageSigner::from_seed_file
Loads a signing key from a 32-byte seed file on disk.
- Reads the file using
std::fs::read. - If the file cannot be read, returns
Err(VctrlError::Io). - If the file is not exactly 32 bytes, returns
Err(VctrlError::Other)with message"seed file must be exactly 32 bytes". - Constructs the
SigningKeyfrom the seed and returnsOk(LrageSigner).
LibrageSigner::verifying_key
Returns the ed25519_dalek::VerifyingKey (public key) corresponding to
the signing key. The verifying key can be used to verify signatures
produced by this signer.
Signer trait implementation
Signs the provided bytes using the Ed25519 signing key. Returns the
64-byte signature as a Vec<u8>. This method cannot fail because
Ed25519 signing is inf(ally infallible for any input length.
Signing and Verification Workflow
libvctrl provides the building blocks for signing and verification but
does not automatically sign commits during CreateCommit. Applications
must integrate signing explicitly.
Signing a commit:
use ;
let signer = generate;
let commit_hash_bytes = commit_hash.as_bytes;
let signature = signer.sign.unwrap;
// Store signature[;]signature bytes, e.g. in Commit::signature
Verifying a commit signature:
use ;
let verifying_key = signer.verifying_key;
let sig = try_from.expect;
assert!;
Persisting and loading a signing key:
// Save the seed (32 bytes) to a file
use SigningKey;
// Access the seed through the SigningKey's internal bytes
// (application-specific; LibrageSigner wraps this)
// Later, load from file
let signer = from_seed_file?;
Important: The seed file contains the private signing key. Protect it with appropriate filesystem permissions (0600 on POSIX systems).
Error Handling
VctrlError
| Variant | When produced |
|---|---|
Hash |
Invalid hash length or hex string |
Tree |
Duplicate tree entry name |
NotFound |
Object or tree not found |
InvalidRef |
Invalid reference name or HEAD target |
MergeConflict |
Unresolvable merge conflict |
Io |
I/O failure |
Serialization |
Serialization failure |
Backend |
Backend-specific error |
Other |
Catch-all (e.g., seed file wrong size, depth exceeded) |
HashError
TreeError
Module Reference
| Module | Status | Description |
|---|---|---|
codec |
Implemented | Encoder trait and BinaryEncoder |
command |
Implemented | Command trait and all command implementations |
crypto |
Implemented | Signer trait and LibrageSigner |
diff |
Implemented | TreeDiff trait, DiffKind, DiffEntry, TreeDiffer |
domain |
Implemented | Blob, Hash, Tree, TreeEntry, Commit, UserID, UserInfo, Object |
error |
Implemented | VctrlError, HashError, TreeError |
hashing |
Implemented | Hasher trait and Sha512Hasher |
merge |
Implemented | ThreeWayMerge trait, ConflictResolver trait, ThreeWayMerger |
storage |
Implemented | ObjectStore and RefStore traits, MemoryStore, MemoryRefStore |
Testing
29 tests across 8 test files:
| Test file | Count | Verifies |
|---|---|---|
| blob_test | 2 | Blob construction and access |
| branch_test | 3 | Branch create/get/delete, invalid name, SetHead |
| checkout_test | 4 | Flat tree, recursive, empty tree, nonexistent tree |
| commit_test | 3 | Create+log, commit chain, field access |
| diff_test | 2 | Added/removed/modified, no changes |
| merge_test | 3 | No conflict, blob conflict, resolved conflict |
| sign_tests | 7 | Generate+sign, deterministic, different hashes, wrong hash, empty hash, seed file, invalid seed file |
| tree_test | 5 | Sort, duplicate error, hash determinism, empty, into_entries |
Run the test suite:
Build and Lint
The project includes a Makefile. Run make ci for the full CI pipeline
(format check, clippy with all targets and features, and tests<stests).
Security Considerations
- SHA-512 collision resistance. 256 bits of collision resistance, stronger than Git's SHA-1.
- Type-prefixed hashing. Prevents cross-type hash collisions.
- Content-addressed integrity. Objects are stored and retrieved by hash.
- Ed25519 signing. Provides 128-bit security level. Signatures are deterministic for the same key and message.
- Seed file protection. The 32-byte seed file contains the private signing key. Applications must protect it with filesystem permissions.
- CSPRNG.
LibrageSigner::generateusesOsRng, which draws from the operating system's secure random number generator. - No encryption. libvctrl does not encrypt objects. Applications must encrypt data before storing as blobs if confidentiality is required.
- Depth limits. Checkout and ThreeWayMerger cap recursion at 1000 levels.
- Memory store has no persistence. Data is lost on process exit.
Limitations
- Only in-memory storage backend.
Logonly follows first parent (linear history).Checkoutproduces in-memory file lists, not filesystem writes.- Branch names must start with
refs/heads/. Tags and remotes not supported. MergeCommandproduces merged tree but not merge commit.ConflictResolverhas no path context.Commit::newalways setstimestamptoUtc::now().CreateCommitdoes not automatically sign commits.LibrageSignerdoes not persist the signing key.
Migration from v0.1.0
Commit and CreateCommit use UserID instead of UserInfo
The author and committer fields of Commit and CreateCommit now use
UserID (validated, from age-credentials) instead of UserInfo
(unvalidated). Update all construction sites:
// Before (v0.1.0):
let author = new;
// After (v0.2.0):
let author = new?;
UserID::new can return an error if the name or email fails validation.
Ensure your code handles the Result.
SetHead accepts any refs/ prefix
In v0.1.0, SetHead required the target to start with "refs/heads/".
In v0.2.0, it accepts any target starting with "refs/". Code that relied
on "refs/heads/" being the only accepted prefix still works but can now
also use "refs/tags/" and other namespaces.
Roadmap
- Filesystem storage backend.
- Tag support.
- Remote reference namespace.
- Full ancestry traversal (all parents).
- Merge commit creation as part of MergeCommand.
- Path-aware conflict resolver.
- Custom commit timestamps.
- Automatic commit signing in CreateCommit.
- Signing key persistence helpers.
- Streaming encoding and decoding.
- Pack format for efficient storage.
- crates.io publication.
License
This project is licensed under the MIT License. See the LICENSE file in the repository for the full text.