alkblobs 0.2.1

Content-addressed blob storage: multi-hash put/get/verify store with pluggable backends, transport- and protocol-agnostic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
//! The hash side of the content-address layer (ADR-002;
//! hashing-and-keys.md): the algorithm family, the canonical git-blob
//! derivation, and the [`Digest`] value type.
//!
//! **One-way door.** The algorithm bytes and digest lengths this
//! module defines are persisted in backend rows and `local`-engine
//! filenames the moment data is written, with no schema-migration
//! story; they are implemented byte-exactly to ADR-002 and reviewed
//! line-by-line against it (AGENTS.md convention 5). The key bytes are
//! frozen once a backend ships data.
//!
//! **The preamble as abstraction.** An algorithm = a derivation + a
//! digest. The blob members derive git's oid form
//! `H("blob <len>\0" + content)` — the preamble is the derivation's
//! domain separation, not a special case: the length lands *inside*
//! the hashed input for all blob algorithms, which is what forces the
//! two put paths (ADR-003) and caps verification at whole-blob
//! (ADR-006) — both accepted rather than worked around. Known-length
//! derivation ([`Algorithm::derive`]) is the primary constructor —
//! explicit, never a per-family wrapper with a hidden default (ADR-020
//! §Decision 5); the unknown-length path's derivation-over-staged-bytes
//! restart is wave 2 facade logic (ADR-003), not here.
//!
//! **The git-object derivation family (ADR-018).** The registry also
//! carries [`registry::GIT_OBJECT_SHA1`] / [`registry::GIT_OBJECT_SHA256`]
//! — a single derivation family (git's oid function is uniform across
//! kinds), registered for git's non-blob objects (trees, commits,
//! tags). Its derivation is **preimage-identity**: the stored preimage
//! is git's loose-object form (`"<type> <len>\0" + content` — the
//! preamble lives inside the value), so [`Algorithm::derive`] for a
//! family member is the plain hash of `content` (the `len` parameter
//! is inert), and the digest is byte-identical to git's oid of the
//! object, every kind included. This is ADR-002's annotated exception
//! (the domain separation arrives inside the caller-provided
//! preimage); its invariants — digest length fixed per algorithm,
//! rejects-never-guesses key decode, total order, verification-on-get
//! — survive verbatim.
//!
//! **Hash policy.** Two derivation families, per ADR-002's three
//! classes as amended by ADR-018: the blob family
//! ([`Algorithm::GitBlobSha256`] canonical, [`Algorithm::GitBlobSha1`]
//! tolerated legacy — git's own collision-hardened SHA-1 threat model
//! inherited verbatim) and the git-object family
//! ([`Algorithm::GitObjectSha1`] / [`Algorithm::GitObjectSha256`]).
//! BLAKE3 is absent from the crate entirely (ADR-002 Decision class 3).

use sha1::Sha1;
use sha2::{Digest as _, Sha256};

/// The ADR-002 key-encoding registry (ADR-002 §Key encoding).
///
/// Algorithm bytes are assigned from this registry; a new algorithm is
/// a new ADR (another one-way-door byte). `RESERVED` (`0x00`) is
/// reserved and never assigned — a future encoding revision would need
/// a different key universe or a migration ADR.
pub mod registry {
    /// `git-blob-sha-256` — 32-byte digest (total key length 33).
    pub const GIT_BLOB_SHA256: u8 = 0x01;

    /// `git-blob-sha-1` — 20-byte digest (total key length 21).
    pub const GIT_BLOB_SHA1: u8 = 0x02;

    /// `git-object-sha-1` — 20-byte digest (total key length 21). The
    /// git-object derivation family (ADR-018): the stored preimage is
    /// git's own loose-object form (`"<type> <len>\0" + content`), so
    /// the digest is the plain SHA-1 over it — exactly git's oid of the
    /// object, every kind included.
    pub const GIT_OBJECT_SHA1: u8 = 0x03;

    /// `git-object-sha-256` — 32-byte digest (total key length 33). The
    /// sha-256 sibling of [`GIT_OBJECT_SHA1`], registered in advance so
    /// the object-format=sha256 passthrough lands without renumbering.
    pub const GIT_OBJECT_SHA256: u8 = 0x04;

    /// Reserved and never assigned; unknown-algorithm and this tag are
    /// rejects, never guesses.
    pub const RESERVED: u8 = 0x00;
}

/// The hash algorithm of a [`Digest`] (ADR-002 §Hash policy; ADR-018).
///
/// The variant order matches the registry tag order
/// ([`registry::GIT_BLOB_SHA256`] `0x01` < [`registry::GIT_BLOB_SHA1`]
/// `0x02` < [`registry::GIT_OBJECT_SHA1`] `0x03` <
/// [`registry::GIT_OBJECT_SHA256`] `0x04`; ADR-018 §Decision 1), so
/// derived ordering over algorithm-then-digest pairs is the keys'
/// total order in byte form (required for the sweep's live-set diffs
/// over range scans; ADR-002/ADR-005).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Algorithm {
    /// Canonical (ADR-002 Decision class 1): `H("blob <len>\0" +
    /// content)` under SHA-256; one algorithm domain = one address
    /// space (ADR-005).
    GitBlobSha256,
    /// Tolerated legacy (ADR-002 Decision class 2): the same
    /// derivation under SHA-1, for existing-repo interop.
    GitBlobSha1,
    /// The git-object family (ADR-018): the digest is the plain SHA-1
    /// over the *stored preimage* — git's loose-object form
    /// `"<type> <len>\0" + content` — which makes the digest
    /// byte-identical to git's oid of the object, every kind included.
    /// The preamble git derives over already lives in the stored
    /// value; what this crate enforces is exactly git's own oid
    /// function (ADR-002's annotated exception — git's header is the
    /// domain separation, inside the preimage the caller provides).
    /// The put path verifies the declared length against the stream;
    /// git's header carries the length inside the preimage, so
    /// [`Algorithm::derive`] ignores the `len` parameter here.
    GitObjectSha1,
    /// The sha-256 sibling of [`Algorithm::GitObjectSha1`] (ADR-018
    /// §Decision 1 — registered in advance for the
    /// object-format=sha256 passthrough).
    GitObjectSha256,
}

impl Algorithm {
    /// The algorithm's registry byte (ADR-002 §Key encoding).
    pub const fn byte(self) -> u8 {
        match self {
            Self::GitBlobSha256 => registry::GIT_BLOB_SHA256,
            Self::GitBlobSha1 => registry::GIT_BLOB_SHA1,
            Self::GitObjectSha1 => registry::GIT_OBJECT_SHA1,
            Self::GitObjectSha256 => registry::GIT_OBJECT_SHA256,
        }
    }

    /// The registry byte back into an algorithm. Unknown bytes —
    /// including reserved `0x00` — are `None`; callers reject, never
    /// guess.
    pub const fn try_from_byte(byte: u8) -> Option<Self> {
        match byte {
            registry::GIT_BLOB_SHA256 => Some(Self::GitBlobSha256),
            registry::GIT_BLOB_SHA1 => Some(Self::GitBlobSha1),
            registry::GIT_OBJECT_SHA1 => Some(Self::GitObjectSha1),
            registry::GIT_OBJECT_SHA256 => Some(Self::GitObjectSha256),
            _ => None,
        }
    }

    /// The fixed digest length (`32` / `20`); total key lengths 33/21.
    pub const fn digest_len(self) -> usize {
        match self {
            Self::GitBlobSha256 | Self::GitObjectSha256 => 32,
            Self::GitBlobSha1 | Self::GitObjectSha1 => 20,
        }
    }

    /// The algorithm's known-length derivation (ADR-002 §The preamble
    /// as abstraction; ADR-018 for the family): for the blob members,
    /// `H("blob <len>\0" + content)` — the preamble is built the same
    /// way for both, the hasher is the only per-algorithm choice. For
    /// the git-object family, the **plain hash of `content`** — the
    /// stored preimage *is* git's loose-object form (the preamble
    /// lives inside the value), so this arm ignores the `len`
    /// parameter (the uniform-signature quirk ADR-018 §Neutral keeps
    /// deliberately; see [`registry::GIT_OBJECT_SHA1`]).
    pub fn derive(self, content: &[u8], len: u64) -> Digest {
        let bytes = match self {
            Self::GitBlobSha256 => blob_digest::<Sha256>(&blob_preamble(len), content),
            Self::GitBlobSha1 => blob_digest::<Sha1>(&blob_preamble(len), content),
            Self::GitObjectSha1 => blob_digest::<Sha1>(&[], content),
            Self::GitObjectSha256 => blob_digest::<Sha256>(&[], content),
        };
        Digest::from_parts(self, bytes)
    }
}

/// The derivation's preamble: `"blob <len>\0"` (git's format, shared
/// by both algorithms).
fn blob_preamble(len: u64) -> Vec<u8> {
    format!("blob {len}\0").into_bytes()
}

/// The shared derivation body: feed the preamble, then the content,
/// to one hasher — no algorithm special-cases the input.
fn blob_digest<H>(preamble: &[u8], content: &[u8]) -> Vec<u8>
where
    H: sha2::Digest,
{
    let mut hasher = H::new();
    hasher.update(preamble);
    hasher.update(content);
    hasher.finalize().to_vec()
}

/// The streaming form of the derivation: one hasher fed the preamble
/// at initialization (`"blob <len>\0"`, the length known up front on
/// the known-length put path) and the content as it flows. The finish
/// is byte-identical to [`Algorithm::derive`] over the same input —
/// this is the same derivation, driven incrementally so content can
/// stream once (ADR-003's put paths; the wave-2 facade logic this
/// module's doc anticipated).
///
/// Crate-internal: the derivation is a store-core concern; this type
/// is the put path's tool, not public API.
pub(crate) struct BlobHasher {
    algorithm: Algorithm,
    inner: InnerHasher,
}

enum InnerHasher {
    Sha256(Sha256),
    Sha1(Sha1),
}

impl BlobHasher {
    /// A hasher initialized with the preamble for an artifact of `len`
    /// bytes — the known-length path's one-pass form (the preamble is
    /// known before content flows). The git-object family feeds no
    /// preamble (the loose-object header is inside the stored
    /// preimage; ADR-018); its `len` is the preimage length, verified
    /// against the stream by the put path like the blob family's is.
    pub(crate) fn new(algorithm: Algorithm, len: u64) -> BlobHasher {
        let mut hasher = BlobHasher {
            algorithm,
            inner: match algorithm {
                Algorithm::GitBlobSha256 => InnerHasher::Sha256(Sha256::new()),
                Algorithm::GitBlobSha1 => InnerHasher::Sha1(Sha1::new()),
                Algorithm::GitObjectSha1 => InnerHasher::Sha1(Sha1::new()),
                Algorithm::GitObjectSha256 => InnerHasher::Sha256(Sha256::new()),
            },
        };
        if !matches!(
            algorithm,
            Algorithm::GitObjectSha1 | Algorithm::GitObjectSha256
        ) {
            hasher.update(&blob_preamble(len));
        }
        hasher
    }

    /// Feed the next chunk of content.
    pub(crate) fn update(&mut self, chunk: &[u8]) {
        match &mut self.inner {
            InnerHasher::Sha256(inner) => inner.update(chunk),
            InnerHasher::Sha1(inner) => inner.update(chunk),
        }
    }

    /// The derivation's answer over preamble + everything fed so far —
    /// pinned equal to [`Algorithm::derive`] by the in-module tests.
    pub(crate) fn finish(self) -> Digest {
        let bytes = match self.inner {
            InnerHasher::Sha256(inner) => inner.finalize().to_vec(),
            InnerHasher::Sha1(inner) => inner.finalize().to_vec(),
        };
        Digest::from_parts(self.algorithm, bytes)
    }
}

/// An algorithm + digest pair — the hash of a blob under one of this
/// crate's derivations (ADR-002; hashing-and-keys.md §The
/// Key/Digest surface).
///
/// Digest ordering is the key total order (algorithm registry byte
/// first, then digest bytes — see [`Algorithm`]).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Digest {
    algorithm: Algorithm,
    bytes: Vec<u8>,
}

impl Digest {
    /// Algorithm accessor.
    pub const fn algorithm(&self) -> Algorithm {
        self.algorithm
    }

    /// The raw digest bytes under the derivation (length fixed by
    /// [`Algorithm::digest_len`]).
    pub fn bytes(&self) -> &[u8] {
        &self.bytes
    }

    /// The pair constructor; the only way a `Digest` is built. Full
    /// pair validation is the typed paths' job — the derivations
    /// (length fixed by the hash) and `Key`'s boundary parsing all
    /// validate before arriving here. What this constructor itself
    /// carries is the debug-integrity check: debug builds assert the
    /// bytes' length agrees with the algorithm's digest length (review-001 D-5),
    /// release builds trust the typed paths.
    pub(crate) const fn from_parts(algorithm: Algorithm, bytes: Vec<u8>) -> Digest {
        debug_assert!(
            bytes.len() == algorithm.digest_len(),
            "digest length disagrees with its algorithm"
        );
        Digest { algorithm, bytes }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::keys::Key;
    use crate::Error;

    /// Byte-exact hardcoded vectors, themselves produced by
    /// `git hash-object` (the POC quality note: known-answer tests
    /// are only as trustworthy as their source of truth — these were
    /// computed with the CLI at implementation time, and the CLI test
    /// in `tests/git_interop.rs` keeps them honest).
    #[test]
    fn derivation_matches_git_computed_vectors() {
        assert_eq!(
            Algorithm::GitBlobSha256
                .derive(b"hello", 5)
                .bytes()
                .to_vec(),
            hex::decode("8aec4e4876f854f688d0ebfc8f37598f38e5fd6903cccc850ca36591175aeb60")
                .expect("sha256 'hello' vector decodes"),
            "sha256 'hello'",
        );
        assert_eq!(
            Algorithm::GitBlobSha256.derive(b"", 0).bytes().to_vec(),
            hex::decode("473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813")
                .expect("sha256 empty vector decodes"),
            "sha256 empty",
        );
        assert_eq!(
            Algorithm::GitBlobSha1.derive(b"hello", 5).bytes().to_vec(),
            hex::decode("b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0").unwrap(),
            "sha1 'hello'",
        );
        assert_eq!(
            Algorithm::GitBlobSha1.derive(b"", 0).bytes().to_vec(),
            hex::decode("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
            "sha1 empty",
        );
    }

    /// The git-object family's known-answer vectors (ADR-018):
    /// the digest over git's loose-object form is the object's git oid.
    /// The empty tree's oid — sha1("tree 0\0") — is git's own famous
    /// constant; the sha-256 sibling vector rides it (empty-tree oids
    /// are git's own constants, kept honest by the CLI test in
    /// `tests/git_interop.rs`).
    #[test]
    fn git_object_derivation_matches_git_oids() {
        let empty_tree = b"tree 0\0";
        assert_eq!(
            Algorithm::GitObjectSha1
                .derive(empty_tree, empty_tree.len() as u64)
                .bytes()
                .to_vec(),
            hex::decode("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(),
            "sha1 empty tree via the git-object derivation",
        );
        assert_eq!(
            Algorithm::GitObjectSha256
                .derive(empty_tree, empty_tree.len() as u64)
                .bytes()
                .to_vec(),
            hex::decode("6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321")
                .expect("sha256 empty tree vector decodes"),
            "sha256 empty tree via the git-object derivation",
        );
        // A blob's loose form hashes equal under both sha-1 arms —
        // the derivations agree byte-for-byte; only the key tags differ
        // (ADR-018 §Consequences: family-level identity, never a
        // cross-family collision, and the docs never imply "one content,
        // one key" across families).
        assert_eq!(
            Algorithm::GitObjectSha1
                .derive(b"blob 5\0hello", 13)
                .bytes()
                .to_vec(),
            Algorithm::GitBlobSha1.derive(b"hello", 5).bytes().to_vec(),
            "blob loose form through the git-object derivation == the blob oid",
        );
    }

    /// The family's `len` parameter is inert (ADR-018 §Decision 1 —
    /// documented, not inherited silently): `derive` over a family
    /// member is the plain hash of the content, so the same preimage
    /// derives the same digest at every `len`, boundary values
    /// included.
    #[test]
    fn git_object_derive_len_parameter_is_inert() {
        for content in [
            &b"tree 0\0"[..],
            b"blob 5\0hello",
            &(0..1000u32).map(|i| (i % 251) as u8).collect::<Vec<u8>>()[..],
        ] {
            for algorithm in [Algorithm::GitObjectSha1, Algorithm::GitObjectSha256] {
                let baseline = algorithm.derive(content, content.len() as u64);
                for len in [0u64, 1, content.len() as u64 - 1, u64::MAX] {
                    assert_eq!(
                        algorithm.derive(content, len),
                        baseline,
                        "{algorithm:?}: derive over len {len} equals the plain hash"
                    );
                }
            }
        }
    }

    /// Registry bytes and total order of the extended family (ADR-018
    /// §Decision 1): the new members sort after the blob members and
    /// decode round-trip.
    #[test]
    fn git_object_registry_bytes_and_order() {
        assert_eq!(Algorithm::GitObjectSha1.byte(), registry::GIT_OBJECT_SHA1);
        assert_eq!(
            Algorithm::try_from_byte(registry::GIT_OBJECT_SHA256),
            Some(Algorithm::GitObjectSha256)
        );
        assert!(
            Algorithm::GitBlobSha1 < Algorithm::GitObjectSha1
                && Algorithm::GitObjectSha1 < Algorithm::GitObjectSha256,
            "ordering follows the registry bytes (0x02 < 0x03 < 0x04)"
        );
        assert_eq!(Algorithm::GitObjectSha1.digest_len(), 20);
        assert_eq!(Algorithm::GitObjectSha256.digest_len(), 32);
    }

    /// Digest lengths are pinned by their algorithm.
    #[test]
    fn digest_lengths_pinned() {
        assert_eq!(Algorithm::GitBlobSha256.derive(b"x", 1).bytes().len(), 32);
        assert_eq!(Algorithm::GitBlobSha1.derive(b"x", 1).bytes().len(), 20);
        assert_eq!(Algorithm::GitObjectSha1.derive(b"x", 1).bytes().len(), 20);
        assert_eq!(Algorithm::GitObjectSha256.derive(b"x", 1).bytes().len(), 32);
    }

    /// The registry byte + `Key::from_bytes` form of external-oid
    /// construction (ADR-020 §Decision 5 — the interop constructor died
    /// with its length-implied family): the git-vector oids decode into
    /// the blob family keys through the physical encoding, and the
    /// derived digest round-trips to the same key. Wrong length,
    /// unparseable, and tag-mismatched input never decodes.
    #[test]
    fn blob_oid_keys_decode_through_the_physical_encoding() {
        for algorithm in [Algorithm::GitBlobSha256, Algorithm::GitBlobSha1] {
            let derived = algorithm.derive(b"hello", 5);
            let oid_bytes = derived.bytes().to_vec();
            let tagged = {
                let mut tagged = vec![algorithm.byte()];
                tagged.extend_from_slice(&oid_bytes);
                tagged
            };
            let key = Key::from_bytes(&tagged)
                .expect("the tagged oid key decodes through the physical encoding");
            assert_eq!(key.algorithm(), algorithm);
            assert_eq!(key.digest().bytes(), derived.bytes());
            assert_eq!(
                Key::from_bytes(&key.as_bytes()),
                Ok(key),
                "round-trips through the physical encoding"
            );

            let truncated = tagged[..tagged.len() - 1].to_vec();
            assert_eq!(Key::from_bytes(&truncated), Err(Error::KeyInvalid));
            let wrong_tag = {
                let other = if algorithm == Algorithm::GitBlobSha256 {
                    Algorithm::GitBlobSha1
                } else {
                    Algorithm::GitBlobSha256
                };
                let mut wrong = vec![other.byte()];
                wrong.extend_from_slice(&oid_bytes);
                wrong
            };
            assert_eq!(Key::from_bytes(&wrong_tag), Err(Error::KeyInvalid));
        }
    }

    /// The streaming form is the same derivation: a `BlobHasher` fed
    /// the content in arbitrary chunk boundaries finishes byte-identical
    /// to [`Algorithm::derive`] — every algorithm (the blob preamble
    /// arms and the family's no-preamble arms), boundaries around the
    /// known-answer vectors, and a multi-chunk bulk body. The family
    /// members stream git's loose-object form as the content itself.
    #[test]
    fn streaming_hasher_matches_known_length_derivation() {
        for algorithm in [
            Algorithm::GitBlobSha256,
            Algorithm::GitBlobSha1,
            Algorithm::GitObjectSha1,
            Algorithm::GitObjectSha256,
        ] {
            let bodies: [&[u8]; 5] = [b"", b"hello", b"tree 0\0", &[0u8; 100_003], &[7u8; 9]];

            for body in bodies {
                let expected = algorithm.derive(body, body.len() as u64);

                // whole-body update.
                let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
                hasher.update(body);
                assert_eq!(hasher.finish(), expected, "whole-body, {algorithm:?}");

                // byte-at-a-time.
                let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
                for byte in body {
                    hasher.update(std::slice::from_ref(byte));
                }
                assert_eq!(hasher.finish(), expected, "byte-at-a-time, {algorithm:?}");

                // awkward fixed chunks (7, including past the end).
                let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
                for chunk in body.chunks(7) {
                    hasher.update(chunk);
                }
                assert_eq!(hasher.finish(), expected, "chunks of 7, {algorithm:?}");
            }

            // The canonical 'hello' and empty vectors, streamed.
            let expected = algorithm.derive(b"hello", 5).bytes().to_vec();
            let mut hasher = BlobHasher::new(algorithm, 5);
            hasher.update(b"hel");
            hasher.update(b"lo");
            assert_eq!(hasher.finish().bytes().to_vec(), expected);

            let hasher = BlobHasher::new(algorithm, 0);
            assert_eq!(
                hasher.finish().bytes().to_vec(),
                algorithm.derive(b"", 0).bytes().to_vec(),
                "empty content streams with no update"
            );
        }
    }

    /// Algorithm ordering equals registry-byte ordering, so
    /// digest-derived sort is the keys' byte total order.
    #[test]
    fn algorithm_order_matches_registry_bytes() {
        assert!(Algorithm::GitBlobSha256.byte() < Algorithm::GitBlobSha1.byte());
        assert!(Algorithm::GitBlobSha256 < Algorithm::GitBlobSha1);
        assert!(Algorithm::GitBlobSha1 < Algorithm::GitObjectSha1);
        assert!(Algorithm::GitObjectSha1 < Algorithm::GitObjectSha256);
        assert!(Algorithm::GitObjectSha256.byte() < 0xFF);
    }
}