1use 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
21pub 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
29pub 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
67pub 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
88pub 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
117fn 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 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
212fn 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
309struct 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 if !id.is_valid() {
414 return Err(MkitError::InvalidIdentity);
415 }
416 Ok(id)
417 }
418}
419
420fn 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 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 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 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 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 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 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#[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 #[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 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 let mut buf = vec![0x07, b'M', b'K', b'T', b'1', 0x01];
865 buf.extend_from_slice(&[0u8; 32]); buf.push(ObjectType::Commit as u8); buf.extend_from_slice(&3u32.to_le_bytes()); buf.extend_from_slice(b"a/b");
869 assert_eq!(deserialize(&buf), Err(MkitError::TagNameInvalid));
870 }
871
872 #[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 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 let mut buf = vec![0x02, b'M', b'K', b'T', b'1', 0x01];
911 buf.extend_from_slice(&2u32.to_le_bytes());
912 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 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]); buf.extend_from_slice(&0u32.to_le_bytes()); buf.push(IdentityKind::Opaque as u8);
941 buf.extend_from_slice(&0u16.to_le_bytes()); 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); 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()); buf.extend_from_slice(&[0xAA; 32]); assert_eq!(deserialize(&buf), Err(MkitError::UnexpectedEof));
993 }
994
995 #[test]
998 fn serialize_rejects_invalid_identity_in_commit() {
999 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 let id_bytes = |payload: &[u8]| {
1023 let mut b = vec![0x02u8]; 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 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 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 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 #[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]); buf.extend_from_slice(&1_000u32.to_le_bytes()); buf.extend_from_slice(&[0xAA; 32]); assert_eq!(deserialize(&buf), Err(MkitError::UnexpectedEof));
1080 }
1081
1082 #[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]); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&10_000u32.to_le_bytes()); 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]); buf.extend_from_slice(&(MAX_PARENTS + 1).to_le_bytes()); 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]); buf.extend_from_slice(&(MAX_PARENTS + 1).to_le_bytes()); 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]); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(MAX_REMIX_SOURCES + 1).to_le_bytes()); 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()); buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(MAX_CHUNKS + 1).to_le_bytes()); assert_eq!(deserialize(&buf), Err(MkitError::TooManyChunks));
1128 }
1129
1130 #[test]
1131 fn rejects_remix_out_of_order_sources() {
1132 let sources = vec![
1141 RemixSource {
1142 upstream_id: [2u8; 32],
1143 commit_hash: [0u8; 32],
1144 },
1145 RemixSource {
1146 upstream_id: [1u8; 32], 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 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 proptest::proptest! {
1191 #[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 #[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}