Skip to main content

mkit_core/
serialize.rs

1//! Canonical byte (de)serialization for [`Object`].
2//!
3//! Spec: `docs/specs/SPEC-OBJECTS.md`. The byte layout produced here is the
4//! v1 on-disk format; the golden-vector tests in `tests/golden.rs` pin
5//! it byte-for-byte.
6//!
7//! Every deserializer:
8//! * Validates the 6-byte v1 prologue first.
9//! * Enforces per-type bounds (entry counts, identity len, etc.).
10//! * Rejects non-empty trailing bytes via [`MkitError::TrailingData`].
11
12use crate::hash::{HASH_LEN, Hash};
13use crate::object::{
14    Blob, ChunkedBlob, Commit, Delta, EntryMode, IDENTITY_MAX_LEN, Identity, IdentityKind, MAGIC,
15    MkitError, Object, ObjectType, Remix, RemixSource, SCHEMA_VERSION, TAG_NAME_MAX_LEN, Tag, Tree,
16    TreeEntry,
17};
18
19const PROLOGUE_LEN: usize = 6;
20
21/// Decode-side cap on tree entry count; writers (and the git
22/// importer) must refuse anything larger or the store gains an
23/// undecodable signed object.
24pub const MAX_TREE_ENTRIES: u32 = 1_000_000;
25const MAX_PARENTS: u32 = 1_000;
26const MAX_REMIX_SOURCES: u32 = 10_000;
27const MAX_CHUNKS: u32 = 1_000_000;
28
29// ---------------------------------------------------------------------
30// Public API
31// ---------------------------------------------------------------------
32
33/// Serialize an [`Object`] to its canonical byte form. Allocates fresh
34/// each call; the result is fully owned.
35///
36/// Returns [`MkitError::OversizePayload`] if any length-prefixed field
37/// exceeds the wire-format `u32` cap, and [`MkitError::InvalidIdentity`]
38/// if the object carries a structurally invalid [`Identity`].
39///
40/// # Caller precondition: ordering
41///
42/// The writer does not reorder or validate entry/source ordering — it
43/// encodes them verbatim — but [`deserialize`] enforces a strict
44/// ascending order (`Tree` entries by `name`, `Remix` sources by
45/// `(upstream_id, commit_hash)`). An `Object` built by hand with
46/// out-of-order `Tree::entries` or `Remix::sources` will therefore
47/// serialize cleanly yet fail to round-trip (`deserialize` rejects it
48/// with `InvalidEntryOrder` / `InvalidSourceOrder`). Callers that bypass
49/// the ordered-building helpers MUST ensure [`Tree::is_sorted`] /
50/// [`Remix::sources_sorted`] hold before serializing — otherwise the
51/// resulting (possibly signed) bytes are undecodable.
52pub fn serialize(obj: &Object) -> Result<Vec<u8>, MkitError> {
53    let mut buf = Vec::with_capacity(PROLOGUE_LEN + estimated_body_len(obj));
54    write_prologue(&mut buf, obj.object_type());
55    match obj {
56        Object::Blob(b) => write_blob(&mut buf, b)?,
57        Object::Tree(t) => write_tree(&mut buf, t)?,
58        Object::Commit(c) => write_commit(&mut buf, c)?,
59        Object::Remix(r) => write_remix(&mut buf, r)?,
60        Object::ChunkedBlob(cb) => write_chunked_blob(&mut buf, cb)?,
61        Object::Delta(d) => write_delta(&mut buf, d)?,
62        Object::Tag(t) => write_tag(&mut buf, t)?,
63    }
64    Ok(buf)
65}
66
67/// The exact byte prefix of `serialize(Object::Blob(..))` for a payload
68/// of `len` bytes: 6-byte object prologue plus the `u32` LE data
69/// length. Lets ingest write a chunk as `prologue ‖ payload` straight
70/// from the source buffer — no `Blob` allocation, no serialize copy.
71/// Equivalence with [`serialize`] is pinned by proptest and,
72/// transitively, the golden blob vectors.
73///
74/// # Errors
75///
76/// [`MkitError::OversizePayload`] if `len` exceeds the wire-format
77/// `u32` cap.
78pub fn blob_prologue(len: usize) -> Result<[u8; PROLOGUE_LEN + 4], MkitError> {
79    let len_le = checked_u32("blob.data", len)?.to_le_bytes();
80    let mut out = [0u8; PROLOGUE_LEN + 4];
81    out[0] = ObjectType::Blob as u8;
82    out[1..5].copy_from_slice(&MAGIC);
83    out[5] = SCHEMA_VERSION;
84    out[6..10].copy_from_slice(&len_le);
85    Ok(out)
86}
87
88/// Deserialize bytes into an owned [`Object`]. Validates the prologue
89/// and every per-type bound; rejects trailing data.
90pub fn deserialize(data: &[u8]) -> Result<Object, MkitError> {
91    if data.len() < PROLOGUE_LEN {
92        return Err(MkitError::EmptyData);
93    }
94    let tag = ObjectType::from_u8(data[0])?;
95    if data[1..5] != MAGIC {
96        return Err(MkitError::InvalidMagic);
97    }
98    if data[5] != SCHEMA_VERSION {
99        return Err(MkitError::UnsupportedObjectVersion);
100    }
101    let mut r = Reader::new(&data[PROLOGUE_LEN..]);
102    let obj = match tag {
103        ObjectType::Blob => Object::Blob(read_blob(&mut r)?),
104        ObjectType::Tree => Object::Tree(read_tree(&mut r)?),
105        ObjectType::Commit => Object::Commit(read_commit(&mut r)?),
106        ObjectType::Remix => Object::Remix(read_remix(&mut r)?),
107        ObjectType::ChunkedBlob => Object::ChunkedBlob(read_chunked_blob(&mut r)?),
108        ObjectType::Delta => Object::Delta(read_delta(&mut r)?),
109        ObjectType::Tag => Object::Tag(read_tag(&mut r)?),
110    };
111    if r.remaining() != 0 {
112        return Err(MkitError::TrailingData);
113    }
114    Ok(obj)
115}
116
117// ---------------------------------------------------------------------
118// Writers
119// ---------------------------------------------------------------------
120
121fn write_prologue(buf: &mut Vec<u8>, t: ObjectType) {
122    buf.push(t as u8);
123    buf.extend_from_slice(&MAGIC);
124    buf.push(SCHEMA_VERSION);
125}
126
127fn write_u16_le(buf: &mut Vec<u8>, v: u16) {
128    buf.extend_from_slice(&v.to_le_bytes());
129}
130
131fn write_u32_le(buf: &mut Vec<u8>, v: u32) {
132    buf.extend_from_slice(&v.to_le_bytes());
133}
134
135fn write_u64_le(buf: &mut Vec<u8>, v: u64) {
136    buf.extend_from_slice(&v.to_le_bytes());
137}
138
139fn checked_u32(field: &'static str, len: usize) -> Result<u32, MkitError> {
140    u32::try_from(len).map_err(|_| MkitError::OversizePayload { field, len })
141}
142
143fn write_lp_bytes(buf: &mut Vec<u8>, field: &'static str, data: &[u8]) -> Result<(), MkitError> {
144    write_u32_le(buf, checked_u32(field, data.len())?);
145    buf.extend_from_slice(data);
146    Ok(())
147}
148
149fn write_identity(buf: &mut Vec<u8>, id: &Identity) -> Result<(), MkitError> {
150    if !id.is_valid() {
151        return Err(MkitError::InvalidIdentity);
152    }
153    buf.push(id.kind as u8);
154    // `is_valid` already enforces 1..=IDENTITY_MAX_LEN, so the cast is
155    // safe — but keep the guard so the encoder can never silently lose
156    // bytes if `is_valid` is ever loosened.
157    let len = u16::try_from(id.bytes.len()).map_err(|_| MkitError::InvalidIdentity)?;
158    write_u16_le(buf, len);
159    buf.extend_from_slice(&id.bytes);
160    Ok(())
161}
162
163fn write_blob(buf: &mut Vec<u8>, b: &Blob) -> Result<(), MkitError> {
164    write_lp_bytes(buf, "blob.data", &b.data)
165}
166
167fn write_tree(buf: &mut Vec<u8>, t: &Tree) -> Result<(), MkitError> {
168    write_u32_le(buf, checked_u32("tree.entries", t.entries.len())?);
169    for e in &t.entries {
170        write_lp_bytes(buf, "tree.entry.name", &e.name)?;
171        buf.push(e.mode as u8);
172        buf.extend_from_slice(&e.object_hash);
173    }
174    Ok(())
175}
176
177fn write_commit(buf: &mut Vec<u8>, c: &Commit) -> Result<(), MkitError> {
178    buf.extend_from_slice(&c.tree_hash);
179    write_u32_le(buf, checked_u32("commit.parents", c.parents.len())?);
180    for p in &c.parents {
181        buf.extend_from_slice(p);
182    }
183    write_identity(buf, &c.author)?;
184    write_lp_bytes(buf, "commit.message", &c.message)?;
185    write_u64_le(buf, c.timestamp);
186    buf.extend_from_slice(&c.signer);
187    buf.extend_from_slice(&c.message_hash);
188    buf.extend_from_slice(&c.content_digest);
189    buf.extend_from_slice(&c.signature);
190    Ok(())
191}
192
193fn write_remix(buf: &mut Vec<u8>, r: &Remix) -> Result<(), MkitError> {
194    buf.extend_from_slice(&r.tree_hash);
195    write_u32_le(buf, checked_u32("remix.parents", r.parents.len())?);
196    for p in &r.parents {
197        buf.extend_from_slice(p);
198    }
199    write_u32_le(buf, checked_u32("remix.sources", r.sources.len())?);
200    for s in &r.sources {
201        buf.extend_from_slice(&s.upstream_id);
202        buf.extend_from_slice(&s.commit_hash);
203    }
204    write_identity(buf, &r.author)?;
205    write_lp_bytes(buf, "remix.message", &r.message)?;
206    write_u64_le(buf, r.timestamp);
207    buf.extend_from_slice(&r.signer);
208    buf.extend_from_slice(&r.signature);
209    Ok(())
210}
211
212/// Reject pack-only / non-storable target types. A tag MUST point at a
213/// type that can live in the object store (`Delta` is pack-only).
214fn check_tag_target_type(t: ObjectType) -> Result<(), MkitError> {
215    if matches!(t, ObjectType::Delta) {
216        return Err(MkitError::TagTargetTypeInvalid(t as u8));
217    }
218    Ok(())
219}
220
221fn write_tag(buf: &mut Vec<u8>, t: &Tag) -> Result<(), MkitError> {
222    if !t.name_is_valid() {
223        return Err(MkitError::TagNameInvalid);
224    }
225    check_tag_target_type(t.target_type)?;
226    buf.extend_from_slice(&t.target);
227    buf.push(t.target_type as u8);
228    write_lp_bytes(buf, "tag.name", &t.name)?;
229    write_identity(buf, &t.tagger)?;
230    write_lp_bytes(buf, "tag.message", &t.message)?;
231    write_u64_le(buf, t.timestamp);
232    buf.extend_from_slice(&t.signer);
233    buf.extend_from_slice(&t.signature);
234    Ok(())
235}
236
237fn write_chunked_blob(buf: &mut Vec<u8>, cb: &ChunkedBlob) -> Result<(), MkitError> {
238    write_u64_le(buf, cb.total_size);
239    write_u32_le(buf, cb.chunk_size);
240    write_u32_le(buf, checked_u32("chunked_blob.chunks", cb.chunks.len())?);
241    for c in &cb.chunks {
242        buf.extend_from_slice(c);
243    }
244    Ok(())
245}
246
247fn write_delta(buf: &mut Vec<u8>, d: &Delta) -> Result<(), MkitError> {
248    buf.extend_from_slice(&d.base_hash);
249    write_u32_le(buf, d.result_size);
250    write_lp_bytes(buf, "delta.instructions", &d.instructions)
251}
252
253fn estimated_body_len(obj: &Object) -> usize {
254    match obj {
255        Object::Blob(b) => 4 + b.data.len(),
256        Object::Tree(t) => {
257            4 + t
258                .entries
259                .iter()
260                .map(|e| 4 + e.name.len() + 1 + 32)
261                .sum::<usize>()
262        }
263        Object::Commit(c) => {
264            32 + 4
265                + c.parents.len() * 32
266                + 1
267                + 2
268                + c.author.bytes.len()
269                + 4
270                + c.message.len()
271                + 8
272                + 32
273                + 32
274                + 32
275                + 64
276        }
277        Object::Remix(r) => {
278            32 + 4
279                + r.parents.len() * 32
280                + 4
281                + r.sources.len() * 64
282                + 1
283                + 2
284                + r.author.bytes.len()
285                + 4
286                + r.message.len()
287                + 8
288                + 32
289                + 64
290        }
291        Object::ChunkedBlob(cb) => 8 + 4 + 4 + cb.chunks.len() * 32,
292        Object::Delta(d) => 32 + 4 + 4 + d.instructions.len(),
293        Object::Tag(t) => {
294            32 + 1
295                + 4
296                + t.name.len()
297                + 1
298                + 2
299                + t.tagger.bytes.len()
300                + 4
301                + t.message.len()
302                + 8
303                + 32
304                + 64
305        }
306    }
307}
308
309// ---------------------------------------------------------------------
310// Reader
311// ---------------------------------------------------------------------
312
313struct Reader<'a> {
314    data: &'a [u8],
315    pos: usize,
316}
317
318impl<'a> Reader<'a> {
319    fn new(data: &'a [u8]) -> Self {
320        Self { data, pos: 0 }
321    }
322
323    fn remaining(&self) -> usize {
324        self.data.len() - self.pos
325    }
326
327    fn need(&self, n: usize) -> Result<(), MkitError> {
328        if self.remaining() < n {
329            Err(MkitError::UnexpectedEof)
330        } else {
331            Ok(())
332        }
333    }
334
335    fn read_u8(&mut self) -> Result<u8, MkitError> {
336        self.need(1)?;
337        let v = self.data[self.pos];
338        self.pos += 1;
339        Ok(v)
340    }
341
342    fn read_u16(&mut self) -> Result<u16, MkitError> {
343        self.need(2)?;
344        let mut a = [0u8; 2];
345        a.copy_from_slice(&self.data[self.pos..self.pos + 2]);
346        self.pos += 2;
347        Ok(u16::from_le_bytes(a))
348    }
349
350    fn read_u32(&mut self) -> Result<u32, MkitError> {
351        self.need(4)?;
352        let mut a = [0u8; 4];
353        a.copy_from_slice(&self.data[self.pos..self.pos + 4]);
354        self.pos += 4;
355        Ok(u32::from_le_bytes(a))
356    }
357
358    fn read_u64(&mut self) -> Result<u64, MkitError> {
359        self.need(8)?;
360        let mut a = [0u8; 8];
361        a.copy_from_slice(&self.data[self.pos..self.pos + 8]);
362        self.pos += 8;
363        Ok(u64::from_le_bytes(a))
364    }
365
366    fn read_hash(&mut self) -> Result<Hash, MkitError> {
367        self.need(HASH_LEN)?;
368        let mut h = [0u8; HASH_LEN];
369        h.copy_from_slice(&self.data[self.pos..self.pos + HASH_LEN]);
370        self.pos += HASH_LEN;
371        Ok(h)
372    }
373
374    fn read_fixed<const N: usize>(&mut self) -> Result<[u8; N], MkitError> {
375        self.need(N)?;
376        let mut out = [0u8; N];
377        out.copy_from_slice(&self.data[self.pos..self.pos + N]);
378        self.pos += N;
379        Ok(out)
380    }
381
382    fn read_lp_bytes(&mut self) -> Result<Vec<u8>, MkitError> {
383        let len = self.read_u32()? as usize;
384        self.need(len)?;
385        let v = self.data[self.pos..self.pos + len].to_vec();
386        self.pos += len;
387        Ok(v)
388    }
389
390    fn read_identity(&mut self) -> Result<Identity, MkitError> {
391        let kind = IdentityKind::from_u8(self.read_u8()?)?;
392        let len = self.read_u16()?;
393        if len == 0 {
394            return Err(MkitError::InvalidIdentity);
395        }
396        if len > IDENTITY_MAX_LEN {
397            return Err(MkitError::IdentityTooLarge);
398        }
399        match kind {
400            IdentityKind::Ed25519 if len != 32 => return Err(MkitError::InvalidIdentity),
401            _ => {}
402        }
403        let len = len as usize;
404        self.need(len)?;
405        let bytes = self.data[self.pos..self.pos + len].to_vec();
406        self.pos += len;
407        let id = Identity { kind, bytes };
408        // Enforce the full structural invariant at the read boundary so a
409        // malformed object from disk/remote can't deserialize with an
410        // invalid payload (e.g. a binary `DidKey` that isn't a printable
411        // multibase string). `is_valid` is the single source of truth and
412        // the serialize side already gates on it (#223).
413        if !id.is_valid() {
414            return Err(MkitError::InvalidIdentity);
415        }
416        Ok(id)
417    }
418}
419
420// ---------------------------------------------------------------------
421// Readers
422// ---------------------------------------------------------------------
423
424fn read_blob(r: &mut Reader<'_>) -> Result<Blob, MkitError> {
425    Ok(Blob {
426        data: r.read_lp_bytes()?,
427    })
428}
429
430fn read_tree(r: &mut Reader<'_>) -> Result<Tree, MkitError> {
431    let count = r.read_u32()?;
432    if count > MAX_TREE_ENTRIES {
433        return Err(MkitError::TooManyEntries);
434    }
435    // Cheap upper bound: each entry is at least name_len(4) + mode(1) +
436    // hash(32) = 37 bytes plus a 1-byte name. Reject impossible counts
437    // before we allocate the entry vec.
438    if (count as usize).saturating_mul(4 + 1 + 1 + HASH_LEN) > r.remaining() {
439        return Err(MkitError::UnexpectedEof);
440    }
441    let mut entries = Vec::with_capacity(count as usize);
442    let mut prev: Option<Vec<u8>> = None;
443    for _ in 0..count {
444        let name = r.read_lp_bytes()?;
445        if !TreeEntry::validate_name(&name) {
446            return Err(MkitError::InvalidEntryName);
447        }
448        if let Some(p) = &prev
449            && p.as_slice() >= name.as_slice()
450        {
451            return Err(MkitError::InvalidEntryOrder);
452        }
453        let mode = EntryMode::from_u8(r.read_u8()?)?;
454        let object_hash = r.read_hash()?;
455        prev = Some(name.clone());
456        entries.push(TreeEntry {
457            name,
458            mode,
459            object_hash,
460        });
461    }
462    Ok(Tree { entries })
463}
464
465fn read_commit(r: &mut Reader<'_>) -> Result<Commit, MkitError> {
466    let tree_hash = r.read_hash()?;
467    let parent_count = r.read_u32()?;
468    if parent_count > MAX_PARENTS {
469        return Err(MkitError::TooManyParents);
470    }
471    // Cheap upper bound: each parent is HASH_LEN bytes on the wire. If
472    // the remaining buffer can't even hold the parent hashes, the
473    // header is lying and we must not pre-allocate for it.
474    if (parent_count as usize).saturating_mul(HASH_LEN) > r.remaining() {
475        return Err(MkitError::UnexpectedEof);
476    }
477    let mut parents = Vec::with_capacity(parent_count as usize);
478    for _ in 0..parent_count {
479        parents.push(r.read_hash()?);
480    }
481    let author = r.read_identity()?;
482    let message = r.read_lp_bytes()?;
483    let timestamp = r.read_u64()?;
484    let signer = r.read_fixed::<32>()?;
485    let message_hash = r.read_hash()?;
486    let content_digest = r.read_hash()?;
487    let signature = r.read_fixed::<64>()?;
488    Ok(Commit {
489        tree_hash,
490        parents,
491        author,
492        signer,
493        message,
494        timestamp,
495        message_hash,
496        content_digest,
497        signature,
498    })
499}
500
501fn read_remix(r: &mut Reader<'_>) -> Result<Remix, MkitError> {
502    let tree_hash = r.read_hash()?;
503    let parent_count = r.read_u32()?;
504    if parent_count > MAX_PARENTS {
505        return Err(MkitError::TooManyParents);
506    }
507    // Cheap upper bound: each parent is HASH_LEN bytes on the wire.
508    if (parent_count as usize).saturating_mul(HASH_LEN) > r.remaining() {
509        return Err(MkitError::UnexpectedEof);
510    }
511    let mut parents = Vec::with_capacity(parent_count as usize);
512    for _ in 0..parent_count {
513        parents.push(r.read_hash()?);
514    }
515    let source_count = r.read_u32()?;
516    if source_count > MAX_REMIX_SOURCES {
517        return Err(MkitError::TooManySources);
518    }
519    // Each source is two hashes (upstream_id + commit_hash) = 2 *
520    // HASH_LEN bytes. Reject impossible counts before allocating.
521    if (source_count as usize).saturating_mul(2 * HASH_LEN) > r.remaining() {
522        return Err(MkitError::UnexpectedEof);
523    }
524    let mut sources = Vec::with_capacity(source_count as usize);
525    for _ in 0..source_count {
526        let upstream_id = r.read_hash()?;
527        let commit_hash = r.read_hash()?;
528        sources.push(RemixSource {
529            upstream_id,
530            commit_hash,
531        });
532    }
533    let author = r.read_identity()?;
534    let message = r.read_lp_bytes()?;
535    let timestamp = r.read_u64()?;
536    let signer = r.read_fixed::<32>()?;
537    let signature = r.read_fixed::<64>()?;
538    // Sort check: strict ascending by (upstream_id, commit_hash).
539    if sources.len() > 1 {
540        for w in sources.windows(2) {
541            let a = &w[0];
542            let b = &w[1];
543            let bad = match a.upstream_id.cmp(&b.upstream_id) {
544                core::cmp::Ordering::Greater => true,
545                core::cmp::Ordering::Equal => a.commit_hash >= b.commit_hash,
546                core::cmp::Ordering::Less => false,
547            };
548            if bad {
549                return Err(MkitError::InvalidSourceOrder);
550            }
551        }
552    }
553    Ok(Remix {
554        tree_hash,
555        parents,
556        sources,
557        author,
558        signer,
559        message,
560        timestamp,
561        signature,
562    })
563}
564
565fn read_tag(r: &mut Reader<'_>) -> Result<Tag, MkitError> {
566    let target = r.read_hash()?;
567    let target_type = ObjectType::from_u8(r.read_u8()?)?;
568    check_tag_target_type(target_type)?;
569    // `name` is length-prefixed; bound it by TAG_NAME_MAX_LEN before we
570    // copy so a bogus header can't force a large allocation.
571    let name_len = r.read_u32()? as usize;
572    if name_len == 0 || name_len > TAG_NAME_MAX_LEN as usize {
573        return Err(MkitError::TagNameInvalid);
574    }
575    r.need(name_len)?;
576    let name = r.data[r.pos..r.pos + name_len].to_vec();
577    r.pos += name_len;
578    if name.iter().any(|&b| matches!(b, 0 | b'/' | b'\\')) {
579        return Err(MkitError::TagNameInvalid);
580    }
581    let tagger = r.read_identity()?;
582    let message = r.read_lp_bytes()?;
583    let timestamp = r.read_u64()?;
584    let signer = r.read_fixed::<32>()?;
585    let signature = r.read_fixed::<64>()?;
586    Ok(Tag {
587        target,
588        target_type,
589        name,
590        tagger,
591        signer,
592        message,
593        timestamp,
594        signature,
595    })
596}
597
598fn read_chunked_blob(r: &mut Reader<'_>) -> Result<ChunkedBlob, MkitError> {
599    let total_size = r.read_u64()?;
600    let chunk_size = r.read_u32()?;
601    let chunk_count = r.read_u32()?;
602    if chunk_count > MAX_CHUNKS {
603        return Err(MkitError::TooManyChunks);
604    }
605    if (chunk_count as usize).saturating_mul(HASH_LEN) > r.remaining() {
606        return Err(MkitError::UnexpectedEof);
607    }
608    let mut chunks = Vec::with_capacity(chunk_count as usize);
609    for _ in 0..chunk_count {
610        chunks.push(r.read_hash()?);
611    }
612    Ok(ChunkedBlob {
613        total_size,
614        chunk_size,
615        chunks,
616    })
617}
618
619fn read_delta(r: &mut Reader<'_>) -> Result<Delta, MkitError> {
620    let base_hash = r.read_hash()?;
621    let result_size = r.read_u32()?;
622    let instructions = r.read_lp_bytes()?;
623    Ok(Delta {
624        base_hash,
625        result_size,
626        instructions,
627    })
628}
629
630// ---------------------------------------------------------------------
631// Tests
632// ---------------------------------------------------------------------
633
634#[cfg(test)]
635mod tests {
636    use super::*;
637    use crate::hash::{ZERO, hash};
638    use proptest::prelude::*;
639
640    fn ed25519_id() -> Identity {
641        Identity::ed25519([0xAA; 32])
642    }
643
644    proptest! {
645        /// `blob_prologue(len) ‖ payload` must be byte-identical to
646        /// `serialize(Object::Blob(payload))` — the zero-copy chunk
647        /// write path depends on this equivalence, which transitively
648        /// pins it to the golden blob vectors.
649        #[test]
650        fn blob_prologue_plus_payload_equals_serialize_blob(
651            payload in proptest::collection::vec(any::<u8>(), 0..2048)
652        ) {
653            let via_serialize = serialize(&Object::Blob(Blob {
654                data: payload.clone(),
655            })).unwrap();
656            let header = blob_prologue(payload.len()).unwrap();
657            let mut via_parts = header.to_vec();
658            via_parts.extend_from_slice(&payload);
659            prop_assert_eq!(via_parts, via_serialize);
660        }
661    }
662
663    #[test]
664    fn blob_prologue_rejects_oversize_len() {
665        assert!(blob_prologue(u32::MAX as usize + 1).is_err());
666        assert!(blob_prologue(0).is_ok());
667    }
668
669    #[test]
670    fn blob_roundtrip() {
671        let obj = Object::Blob(Blob {
672            data: b"hello world".to_vec(),
673        });
674        let bytes = serialize(&obj).expect("valid blob serialises");
675        // Prologue
676        assert_eq!(bytes[0], 0x01);
677        assert_eq!(&bytes[1..5], b"MKT1");
678        assert_eq!(bytes[5], 0x01);
679        let parsed = deserialize(&bytes).unwrap();
680        assert_eq!(obj, parsed);
681    }
682
683    #[test]
684    fn empty_blob_size_is_10() {
685        let obj = Object::Blob(Blob { data: vec![] });
686        let bytes = serialize(&obj).unwrap();
687        assert_eq!(bytes.len(), 10);
688        assert_eq!(deserialize(&bytes).unwrap(), obj);
689    }
690
691    #[test]
692    fn empty_tree_roundtrip() {
693        let obj = Object::Tree(Tree { entries: vec![] });
694        let bytes = serialize(&obj).unwrap();
695        assert_eq!(deserialize(&bytes).unwrap(), obj);
696    }
697
698    #[test]
699    fn tree_with_three_entries_roundtrip() {
700        let obj = Object::Tree(Tree {
701            entries: vec![
702                TreeEntry {
703                    name: b"alpha".to_vec(),
704                    mode: EntryMode::Blob,
705                    object_hash: hash(b"a"),
706                },
707                TreeEntry {
708                    name: b"beta".to_vec(),
709                    mode: EntryMode::Tree,
710                    object_hash: hash(b"b"),
711                },
712                TreeEntry {
713                    name: b"gamma".to_vec(),
714                    mode: EntryMode::Executable,
715                    object_hash: hash(b"g"),
716                },
717            ],
718        });
719        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
720    }
721
722    #[test]
723    fn commit_with_one_parent_roundtrip() {
724        let obj = Object::Commit(Commit::new_unannotated(
725            hash(b"tree"),
726            vec![hash(b"parent")],
727            ed25519_id(),
728            [0xAA; 32],
729            b"initial".to_vec(),
730            1_711_300_000,
731            [0xBB; 64],
732        ));
733        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
734    }
735
736    #[test]
737    fn root_commit_roundtrip() {
738        let obj = Object::Commit(Commit::new_unannotated(
739            hash(b"tree"),
740            vec![],
741            ed25519_id(),
742            [0x11; 32],
743            b"genesis".to_vec(),
744            1_000_000,
745            [0x22; 64],
746        ));
747        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
748    }
749
750    #[test]
751    fn commit_with_opaque_identity_roundtrip() {
752        let mid = vec![42u8, 0, 0, 0, 0, 0, 0, 0];
753        let obj = Object::Commit(Commit::new_unannotated(
754            hash(b"tree"),
755            vec![],
756            Identity::opaque(mid.clone()),
757            [0xAA; 32],
758            b"opaque author".to_vec(),
759            1_700_000_000,
760            [0xBB; 64],
761        ));
762        let parsed = deserialize(&serialize(&obj).unwrap()).unwrap();
763        if let Object::Commit(c) = &parsed {
764            assert_eq!(c.author.kind, IdentityKind::Opaque);
765            assert_eq!(c.author.bytes, mid);
766        } else {
767            panic!("not a commit");
768        }
769        assert_eq!(parsed, obj);
770    }
771
772    #[test]
773    fn remix_with_one_source_roundtrip() {
774        let obj = Object::Remix(Remix {
775            tree_hash: hash(b"tree"),
776            parents: vec![],
777            sources: vec![RemixSource {
778                upstream_id: hash(b"project-a"),
779                commit_hash: hash(b"commit-x"),
780            }],
781            author: ed25519_id(),
782            signer: [0xCC; 32],
783            message: b"remixed".to_vec(),
784            timestamp: 1_711_300_100,
785            signature: [0xDD; 64],
786        });
787        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
788    }
789
790    #[test]
791    fn chunked_blob_roundtrip() {
792        let obj = Object::ChunkedBlob(ChunkedBlob {
793            total_size: 3 * 65536,
794            chunk_size: 65536,
795            chunks: vec![hash(b"c1"), hash(b"c2"), hash(b"c3")],
796        });
797        let bytes = serialize(&obj).unwrap();
798        assert_eq!(bytes[0], 0x05);
799        assert_eq!(deserialize(&bytes).unwrap(), obj);
800    }
801
802    #[test]
803    fn chunked_blob_cdc_marker_roundtrips() {
804        let obj = Object::ChunkedBlob(ChunkedBlob {
805            total_size: 100_000,
806            chunk_size: 0,
807            chunks: vec![hash(b"x"), hash(b"y")],
808        });
809        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
810    }
811
812    fn sample_tag() -> Tag {
813        Tag {
814            target: hash(b"target-commit"),
815            target_type: ObjectType::Commit,
816            name: b"v1.0.0".to_vec(),
817            tagger: ed25519_id(),
818            signer: [0xAA; 32],
819            message: b"release 1.0.0".to_vec(),
820            timestamp: 1_711_300_000,
821            signature: [0xCC; 64],
822        }
823    }
824
825    #[test]
826    fn tag_roundtrip() {
827        let obj = Object::Tag(sample_tag());
828        let bytes = serialize(&obj).unwrap();
829        assert_eq!(bytes[0], 0x07, "tag object_type tag");
830        assert_eq!(&bytes[1..5], b"MKT1");
831        assert_eq!(bytes[5], 0x01);
832        assert_eq!(deserialize(&bytes).unwrap(), obj);
833    }
834
835    #[test]
836    fn tag_empty_message_roundtrip() {
837        let mut t = sample_tag();
838        t.message = vec![];
839        let obj = Object::Tag(t);
840        assert_eq!(deserialize(&serialize(&obj).unwrap()).unwrap(), obj);
841    }
842
843    #[test]
844    fn tag_rejects_empty_name() {
845        let mut t = sample_tag();
846        t.name = vec![];
847        assert_eq!(serialize(&Object::Tag(t)), Err(MkitError::TagNameInvalid));
848    }
849
850    #[test]
851    fn tag_rejects_delta_target_type() {
852        let mut t = sample_tag();
853        t.target_type = ObjectType::Delta;
854        assert_eq!(
855            serialize(&Object::Tag(t)),
856            Err(MkitError::TagTargetTypeInvalid(ObjectType::Delta as u8))
857        );
858    }
859
860    #[test]
861    fn tag_decode_rejects_forbidden_name_byte() {
862        // Hand-craft a tag whose name embeds a `/`. The writer would
863        // reject it, so build the wire bytes directly.
864        let mut buf = vec![0x07, b'M', b'K', b'T', b'1', 0x01];
865        buf.extend_from_slice(&[0u8; 32]); // target
866        buf.push(ObjectType::Commit as u8); // target_type
867        buf.extend_from_slice(&3u32.to_le_bytes()); // name_len
868        buf.extend_from_slice(b"a/b");
869        assert_eq!(deserialize(&buf), Err(MkitError::TagNameInvalid));
870    }
871
872    // ---- Negative tests ----
873
874    #[test]
875    fn deserialize_empty_input() {
876        assert_eq!(deserialize(&[]), Err(MkitError::EmptyData));
877    }
878
879    #[test]
880    fn rejects_invalid_object_type() {
881        let bad = [0xFF, b'M', b'K', b'T', b'1', 0x01];
882        assert_eq!(deserialize(&bad), Err(MkitError::InvalidObjectType(0xFF)));
883    }
884
885    #[test]
886    fn rejects_bad_magic() {
887        let bad = [0x01, b'X', b'Y', b'Z', b'W', 0x01, 0, 0, 0, 0];
888        assert_eq!(deserialize(&bad), Err(MkitError::InvalidMagic));
889    }
890
891    #[test]
892    fn rejects_unsupported_schema_version() {
893        let bad = [0x01, b'M', b'K', b'T', b'1', 0x02, 0, 0, 0, 0];
894        assert_eq!(deserialize(&bad), Err(MkitError::UnsupportedObjectVersion));
895    }
896
897    #[test]
898    fn rejects_truncated_blob() {
899        // length=100 but only 2 bytes follow
900        let bad = [
901            0x01, b'M', b'K', b'T', b'1', 0x01, 0x64, 0x00, 0x00, 0x00, 0xAA, 0xBB,
902        ];
903        assert_eq!(deserialize(&bad), Err(MkitError::UnexpectedEof));
904    }
905
906    #[test]
907    fn rejects_unsorted_tree_entries() {
908        // Build an unsorted tree by hand — can't go through serialize()
909        // because writers don't validate ordering today.
910        let mut buf = vec![0x02, b'M', b'K', b'T', b'1', 0x01];
911        buf.extend_from_slice(&2u32.to_le_bytes());
912        // entry "z.txt"
913        buf.extend_from_slice(&5u32.to_le_bytes());
914        buf.extend_from_slice(b"z.txt");
915        buf.push(EntryMode::Blob as u8);
916        buf.extend_from_slice(&[0u8; 32]);
917        // entry "a.txt"
918        buf.extend_from_slice(&5u32.to_le_bytes());
919        buf.extend_from_slice(b"a.txt");
920        buf.push(EntryMode::Blob as u8);
921        buf.extend_from_slice(&[0u8; 32]);
922        assert_eq!(deserialize(&buf), Err(MkitError::InvalidEntryOrder));
923    }
924
925    #[test]
926    fn rejects_trailing_bytes() {
927        let obj = Object::Blob(Blob {
928            data: b"hello".to_vec(),
929        });
930        let mut bytes = serialize(&obj).unwrap();
931        bytes.push(0xFF);
932        assert_eq!(deserialize(&bytes), Err(MkitError::TrailingData));
933    }
934
935    #[test]
936    fn rejects_zero_length_identity() {
937        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
938        buf.extend_from_slice(&[0u8; 32]); // tree_hash
939        buf.extend_from_slice(&0u32.to_le_bytes()); // parent_count
940        buf.push(IdentityKind::Opaque as u8);
941        buf.extend_from_slice(&0u16.to_le_bytes()); // len = 0
942        assert_eq!(deserialize(&buf), Err(MkitError::InvalidIdentity));
943    }
944
945    #[test]
946    fn rejects_unknown_identity_kind() {
947        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
948        buf.extend_from_slice(&[0u8; 32]);
949        buf.extend_from_slice(&0u32.to_le_bytes());
950        buf.push(0xEE); // unknown kind
951        buf.extend_from_slice(&4u16.to_le_bytes());
952        buf.extend_from_slice(b"xxxx");
953        assert_eq!(deserialize(&buf), Err(MkitError::UnknownIdentityKind(0xEE)));
954    }
955
956    #[test]
957    fn rejects_ed25519_with_wrong_length() {
958        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
959        buf.extend_from_slice(&[0u8; 32]);
960        buf.extend_from_slice(&0u32.to_le_bytes());
961        buf.push(IdentityKind::Ed25519 as u8);
962        buf.extend_from_slice(&8u16.to_le_bytes());
963        buf.extend_from_slice(b"12345678");
964        assert_eq!(deserialize(&buf), Err(MkitError::InvalidIdentity));
965    }
966
967    #[test]
968    fn rejects_oversize_identity() {
969        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
970        buf.extend_from_slice(&[0u8; 32]);
971        buf.extend_from_slice(&0u32.to_le_bytes());
972        buf.push(IdentityKind::Opaque as u8);
973        buf.extend_from_slice(&(IDENTITY_MAX_LEN + 1).to_le_bytes());
974        buf.extend(core::iter::repeat_n(0u8, IDENTITY_MAX_LEN as usize + 1));
975        assert_eq!(deserialize(&buf), Err(MkitError::IdentityTooLarge));
976    }
977
978    #[test]
979    fn rejects_too_many_tree_entries() {
980        let mut buf = vec![0x02, b'M', b'K', b'T', b'1', 0x01];
981        buf.extend_from_slice(&(MAX_TREE_ENTRIES + 1).to_le_bytes());
982        assert_eq!(deserialize(&buf), Err(MkitError::TooManyEntries));
983    }
984
985    #[test]
986    fn rejects_truncated_chunk_list() {
987        let mut buf = vec![0x05, b'M', b'K', b'T', b'1', 0x01];
988        buf.extend_from_slice(&1024u64.to_le_bytes());
989        buf.extend_from_slice(&0u32.to_le_bytes());
990        buf.extend_from_slice(&2u32.to_le_bytes()); // chunk_count = 2
991        buf.extend_from_slice(&[0xAA; 32]); // only one chunk
992        assert_eq!(deserialize(&buf), Err(MkitError::UnexpectedEof));
993    }
994
995    // ---- Fallible-serialize tests (review follow-up #22) ----
996
997    #[test]
998    fn serialize_rejects_invalid_identity_in_commit() {
999        // Empty payload is structurally invalid for every kind.
1000        let bad_id = Identity {
1001            kind: IdentityKind::Opaque,
1002            bytes: Vec::new(),
1003        };
1004        let obj = Object::Commit(Commit::new_unannotated(
1005            hash(b"tree"),
1006            vec![],
1007            bad_id,
1008            [0; 32],
1009            b"x".to_vec(),
1010            0,
1011            [0; 64],
1012        ));
1013        assert_eq!(serialize(&obj), Err(MkitError::InvalidIdentity));
1014    }
1015
1016    #[test]
1017    fn read_identity_rejects_non_multibase_didkey() {
1018        // Wire format: [u8 kind][u16 LE len][payload]. A DidKey payload must
1019        // be a printable-ASCII multibase string, so a malformed object with a
1020        // binary/whitespace DidKey payload must be rejected at the read
1021        // boundary, not silently deserialized (#223).
1022        let id_bytes = |payload: &[u8]| {
1023            let mut b = vec![0x02u8]; // IdentityKind::DidKey
1024            let len = u16::try_from(payload.len()).expect("test payload fits u16");
1025            b.extend_from_slice(&len.to_le_bytes());
1026            b.extend_from_slice(payload);
1027            b
1028        };
1029        // NUL, high byte, and whitespace payloads all reject.
1030        for bad in [b"z\x00ab".as_slice(), b"z\xff", b"z6Mk has space"] {
1031            let buf = id_bytes(bad);
1032            assert_eq!(
1033                Reader::new(&buf).read_identity(),
1034                Err(MkitError::InvalidIdentity),
1035                "should reject DidKey payload {bad:?} at the read boundary"
1036            );
1037        }
1038        // A real did:key multibase payload round-trips.
1039        let good = id_bytes(b"z6MkExample");
1040        let id = Reader::new(&good).read_identity().unwrap();
1041        assert_eq!(id.kind, IdentityKind::DidKey);
1042        assert_eq!(id.bytes, b"z6MkExample");
1043    }
1044
1045    #[test]
1046    fn serialize_rejects_invalid_identity_in_remix() {
1047        // Ed25519 with non-32-byte payload.
1048        let bad_id = Identity {
1049            kind: IdentityKind::Ed25519,
1050            bytes: vec![0u8; 16],
1051        };
1052        let obj = Object::Remix(Remix {
1053            tree_hash: ZERO,
1054            parents: vec![],
1055            sources: vec![],
1056            author: bad_id,
1057            signer: [0; 32],
1058            message: b"x".to_vec(),
1059            timestamp: 0,
1060            signature: [0; 64],
1061        });
1062        assert_eq!(serialize(&obj), Err(MkitError::InvalidIdentity));
1063    }
1064
1065    /// `read_commit` claims `parent_count = MAX_PARENTS` (`1_000`) but
1066    /// the remaining buffer is too small to ever hold that many
1067    /// 32-byte parent hashes. The pre-allocation guard must reject the
1068    /// header before the parent vec is sized from attacker input.
1069    #[test]
1070    fn rejects_truncated_commit_parents() {
1071        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
1072        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1073        // Within MAX_PARENTS (1_000) so the existing TooManyParents
1074        // guard doesn't fire — we want to confirm the capacity-vs-
1075        // remaining check rejects too. 1_000 parents = 32_000 bytes,
1076        // but only a single 32-byte hash follows.
1077        buf.extend_from_slice(&1_000u32.to_le_bytes()); // parent_count
1078        buf.extend_from_slice(&[0xAA; 32]); // only one parent worth
1079        assert_eq!(deserialize(&buf), Err(MkitError::UnexpectedEof));
1080    }
1081
1082    /// `read_remix` claims `source_count = MAX_REMIX_SOURCES` (`10_000`)
1083    /// but the remaining buffer cannot accommodate even one source
1084    /// (which is 64 bytes — two hashes). Reject without allocating.
1085    #[test]
1086    fn rejects_truncated_remix_sources() {
1087        let mut buf = vec![0x04, b'M', b'K', b'T', b'1', 0x01];
1088        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1089        buf.extend_from_slice(&0u32.to_le_bytes()); // parent_count
1090        // 10_000 sources × 64 bytes = 640_000 bytes required, but the
1091        // buffer is empty after this point.
1092        buf.extend_from_slice(&10_000u32.to_le_bytes()); // source_count
1093        assert_eq!(deserialize(&buf), Err(MkitError::UnexpectedEof));
1094    }
1095
1096    #[test]
1097    fn rejects_too_many_commit_parents() {
1098        let mut buf = vec![0x03, b'M', b'K', b'T', b'1', 0x01];
1099        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1100        buf.extend_from_slice(&(MAX_PARENTS + 1).to_le_bytes()); // parent_count
1101        assert_eq!(deserialize(&buf), Err(MkitError::TooManyParents));
1102    }
1103
1104    #[test]
1105    fn rejects_too_many_remix_parents() {
1106        let mut buf = vec![0x04, b'M', b'K', b'T', b'1', 0x01];
1107        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1108        buf.extend_from_slice(&(MAX_PARENTS + 1).to_le_bytes()); // parent_count
1109        assert_eq!(deserialize(&buf), Err(MkitError::TooManyParents));
1110    }
1111
1112    #[test]
1113    fn rejects_too_many_remix_sources() {
1114        let mut buf = vec![0x04, b'M', b'K', b'T', b'1', 0x01];
1115        buf.extend_from_slice(&[0u8; 32]); // tree_hash
1116        buf.extend_from_slice(&0u32.to_le_bytes()); // parent_count
1117        buf.extend_from_slice(&(MAX_REMIX_SOURCES + 1).to_le_bytes()); // source_count
1118        assert_eq!(deserialize(&buf), Err(MkitError::TooManySources));
1119    }
1120
1121    #[test]
1122    fn rejects_too_many_chunks() {
1123        let mut buf = vec![0x05, b'M', b'K', b'T', b'1', 0x01];
1124        buf.extend_from_slice(&1024u64.to_le_bytes()); // total_size
1125        buf.extend_from_slice(&0u32.to_le_bytes()); // chunk_size
1126        buf.extend_from_slice(&(MAX_CHUNKS + 1).to_le_bytes()); // chunk_count
1127        assert_eq!(deserialize(&buf), Err(MkitError::TooManyChunks));
1128    }
1129
1130    #[test]
1131    fn rejects_remix_out_of_order_sources() {
1132        // read_remix's decode-path sort check: sources must be strictly
1133        // ascending by (upstream_id, commit_hash) (SPEC-OBJECTS remix
1134        // source ordering). The encoder (write_remix) does not itself
1135        // sort or validate — it trusts the caller — so a decode-path
1136        // regression here would only be caught by feeding a crafted
1137        // (but encoder-producible) out-of-order Remix through
1138        // `serialize` + `deserialize`, not by round-tripping a
1139        // well-formed one.
1140        let sources = vec![
1141            RemixSource {
1142                upstream_id: [2u8; 32],
1143                commit_hash: [0u8; 32],
1144            },
1145            RemixSource {
1146                upstream_id: [1u8; 32], // decreases -> violates strict ascending order
1147                commit_hash: [0u8; 32],
1148            },
1149        ];
1150        let remix = Remix {
1151            tree_hash: hash(b"tree"),
1152            parents: vec![],
1153            sources,
1154            author: Identity::ed25519([0u8; 32]),
1155            signer: [0u8; 32],
1156            message: b"msg".to_vec(),
1157            timestamp: 0,
1158            signature: [0u8; 64],
1159        };
1160        let bytes = serialize(&Object::Remix(remix)).unwrap();
1161        assert_eq!(deserialize(&bytes), Err(MkitError::InvalidSourceOrder));
1162    }
1163
1164    #[cfg(target_pointer_width = "64")]
1165    #[test]
1166    fn checked_u32_rejects_oversize() {
1167        // Direct unit test on the bounds helper — we cannot allocate a
1168        // Vec with > u32::MAX entries in a unit test, so exercise the
1169        // guard surface itself. This pins the field-name string so
1170        // downstream consumers can grep on it. 32-bit targets cannot
1171        // even construct `n`, so the test is gated on pointer width.
1172        let n: usize = u32::MAX as usize + 1;
1173        let err = checked_u32("blob.data", n).unwrap_err();
1174        assert_eq!(
1175            err,
1176            MkitError::OversizePayload {
1177                field: "blob.data",
1178                len: n,
1179            }
1180        );
1181    }
1182
1183    // -- Property tests -------------------------------------------------
1184    //
1185    // Round-trip invariants exercised against arbitrary inputs via
1186    // `proptest`. The example tests above cover specific vectors and
1187    // the goldens pin wire bytes; the properties below catch the
1188    // boundary cases the examples miss (empty payloads, max-length
1189    // strings, non-ASCII bytes, etc.).
1190    proptest::proptest! {
1191        /// Any blob round-trips byte-for-byte through serialize/deserialize.
1192        #[test]
1193        fn proptest_blob_roundtrip(data in proptest::collection::vec(proptest::num::u8::ANY, 0..4096)) {
1194            let obj = Object::Blob(Blob { data });
1195            let bytes = serialize(&obj).expect("blob serialises");
1196            let parsed = deserialize(&bytes).expect("blob deserialises");
1197            proptest::prop_assert_eq!(obj, parsed);
1198        }
1199
1200        /// Any commit (single parent, fixed identity) round-trips
1201        /// byte-for-byte. Covers arbitrary tree hashes, arbitrary parent
1202        /// hashes, arbitrary message bytes including non-UTF-8 sequences
1203        /// (commit messages are bytes per SPEC-OBJECTS §5). Signer + sig
1204        /// arrays are constructed from a u8 seed (proptest only ships
1205        /// `uniform32` natively; 64-byte signatures get a tiled seed).
1206        #[test]
1207        fn proptest_commit_roundtrip(
1208            tree in proptest::array::uniform32(proptest::num::u8::ANY),
1209            parent in proptest::array::uniform32(proptest::num::u8::ANY),
1210            signer in proptest::array::uniform32(proptest::num::u8::ANY),
1211            msg in proptest::collection::vec(proptest::num::u8::ANY, 0..2048),
1212            sig_seed in proptest::num::u8::ANY,
1213            ts in 0u64..u64::from(u32::MAX),
1214        ) {
1215            let mut sig = [0u8; 64];
1216            sig.fill(sig_seed);
1217            let commit = Commit::new_unannotated(
1218                tree,
1219                vec![parent],
1220                ed25519_id(),
1221                signer,
1222                msg,
1223                ts,
1224                sig,
1225            );
1226            let obj = Object::Commit(commit);
1227            let bytes = serialize(&obj).expect("commit serialises");
1228            let parsed = deserialize(&bytes).expect("commit deserialises");
1229            proptest::prop_assert_eq!(obj, parsed);
1230        }
1231    }
1232}