1use crate::hash::{Hash, ZERO};
16use core::fmt;
17
18pub const MAGIC: [u8; 4] = *b"MKT1";
20pub const SCHEMA_VERSION: u8 = 0x01;
22pub const IDENTITY_MAX_LEN: u16 = 4096;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28#[repr(u8)]
29pub enum ObjectType {
30 Blob = 0x01,
31 Tree = 0x02,
32 Commit = 0x03,
33 Remix = 0x04,
34 ChunkedBlob = 0x05,
35 Delta = 0x06,
36 Tag = 0x07,
39}
40
41impl ObjectType {
42 #[must_use]
48 pub fn is_merkle(self) -> bool {
49 matches!(self, Self::Tree | Self::ChunkedBlob)
50 }
51
52 #[must_use]
54 pub fn name(self) -> &'static str {
55 match self {
56 Self::Blob => "blob",
57 Self::Tree => "tree",
58 Self::Commit => "commit",
59 Self::Remix => "remix",
60 Self::ChunkedBlob => "chunked_blob",
61 Self::Delta => "delta",
62 Self::Tag => "tag",
63 }
64 }
65
66 pub(crate) fn from_u8(b: u8) -> Result<Self, MkitError> {
68 Ok(match b {
69 0x01 => Self::Blob,
70 0x02 => Self::Tree,
71 0x03 => Self::Commit,
72 0x04 => Self::Remix,
73 0x05 => Self::ChunkedBlob,
74 0x06 => Self::Delta,
75 0x07 => Self::Tag,
76 other => return Err(MkitError::InvalidObjectType(other)),
77 })
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83#[repr(u8)]
84pub enum EntryMode {
85 Blob = 0x01,
86 Tree = 0x02,
87 Symlink = 0x03,
88 Executable = 0x04,
91}
92
93impl EntryMode {
94 pub(crate) fn from_u8(b: u8) -> Result<Self, MkitError> {
95 Ok(match b {
96 0x01 => Self::Blob,
97 0x02 => Self::Tree,
98 0x03 => Self::Symlink,
99 0x04 => Self::Executable,
100 other => return Err(MkitError::InvalidEntryMode(other)),
101 })
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107#[repr(u8)]
108pub enum IdentityKind {
109 Ed25519 = 0x01,
111 DidKey = 0x02,
117 Opaque = 0x03,
119}
120
121impl IdentityKind {
122 pub(crate) fn from_u8(b: u8) -> Result<Self, MkitError> {
123 Ok(match b {
124 0x01 => Self::Ed25519,
125 0x02 => Self::DidKey,
126 0x03 => Self::Opaque,
127 other => return Err(MkitError::UnknownIdentityKind(other)),
128 })
129 }
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Hash)]
135pub struct Identity {
136 pub kind: IdentityKind,
137 pub bytes: Vec<u8>,
138}
139
140impl Identity {
141 #[must_use]
143 pub fn ed25519(pubkey: [u8; 32]) -> Self {
144 Self {
145 kind: IdentityKind::Ed25519,
146 bytes: pubkey.to_vec(),
147 }
148 }
149
150 #[must_use]
152 pub fn opaque(bytes: impl Into<Vec<u8>>) -> Self {
153 Self {
154 kind: IdentityKind::Opaque,
155 bytes: bytes.into(),
156 }
157 }
158
159 #[must_use]
165 pub fn is_valid(&self) -> bool {
166 if self.bytes.is_empty() || self.bytes.len() > IDENTITY_MAX_LEN as usize {
167 return false;
168 }
169 match self.kind {
170 IdentityKind::Ed25519 => self.bytes.len() == 32,
171 IdentityKind::DidKey => self.bytes.iter().all(u8::is_ascii_graphic),
175 IdentityKind::Opaque => true,
176 }
177 }
178}
179
180#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct TreeEntry {
183 pub name: Vec<u8>,
185 pub mode: EntryMode,
186 pub object_hash: Hash,
187}
188
189impl TreeEntry {
190 #[must_use]
206 pub fn validate_name(name: &[u8]) -> bool {
207 if name.is_empty() || name.len() > 255 {
208 return false;
209 }
210 if name == b"." || name == b".." {
211 return false;
212 }
213 if name.iter().any(|&b| matches!(b, 0 | b'/' | b'\\')) {
214 return false;
215 }
216 if matches!(name.last(), Some(b'.' | b' ')) {
219 return false;
220 }
221 if name.eq_ignore_ascii_case(b".mkit") || name.eq_ignore_ascii_case(b".git") {
223 return false;
224 }
225 let stem = match name.iter().position(|&b| b == b'.') {
228 Some(i) => &name[..i],
229 None => name,
230 };
231 if is_windows_reserved_stem(stem) {
232 return false;
233 }
234 true
235 }
236}
237
238fn is_windows_reserved_stem(stem: &[u8]) -> bool {
242 match stem.len() {
243 3 => {
244 stem.eq_ignore_ascii_case(b"CON")
245 || stem.eq_ignore_ascii_case(b"PRN")
246 || stem.eq_ignore_ascii_case(b"AUX")
247 || stem.eq_ignore_ascii_case(b"NUL")
248 }
249 4 => {
250 let head = &stem[..3];
252 let tail = stem[3];
253 let is_digit_1_9 = matches!(tail, b'1'..=b'9');
254 is_digit_1_9 && (head.eq_ignore_ascii_case(b"COM") || head.eq_ignore_ascii_case(b"LPT"))
255 }
256 _ => false,
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
263pub struct RemixSource {
264 pub upstream_id: Hash,
265 pub commit_hash: Hash,
266}
267
268#[derive(Debug, Clone, PartialEq, Eq)]
270pub struct Blob {
271 pub data: Vec<u8>,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct Tree {
277 pub entries: Vec<TreeEntry>,
278}
279
280impl Tree {
281 #[must_use]
284 pub fn is_sorted(&self) -> bool {
285 self.entries
286 .windows(2)
287 .all(|w| w[0].name.as_slice() < w[1].name.as_slice())
288 }
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
293pub struct Commit {
294 pub tree_hash: Hash,
295 pub parents: Vec<Hash>,
296 pub author: Identity,
297 pub signer: [u8; 32],
298 pub message: Vec<u8>,
299 pub timestamp: u64,
300 pub message_hash: Hash,
303 pub content_digest: Hash,
306 pub signature: [u8; 64],
307}
308
309impl Commit {
310 #[must_use]
312 pub fn new_unannotated(
313 tree_hash: Hash,
314 parents: Vec<Hash>,
315 author: Identity,
316 signer: [u8; 32],
317 message: Vec<u8>,
318 timestamp: u64,
319 signature: [u8; 64],
320 ) -> Self {
321 Self {
322 tree_hash,
323 parents,
324 author,
325 signer,
326 message,
327 timestamp,
328 message_hash: ZERO,
329 content_digest: ZERO,
330 signature,
331 }
332 }
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
337pub struct Remix {
338 pub tree_hash: Hash,
339 pub parents: Vec<Hash>,
340 pub sources: Vec<RemixSource>,
341 pub author: Identity,
342 pub signer: [u8; 32],
343 pub message: Vec<u8>,
344 pub timestamp: u64,
345 pub signature: [u8; 64],
346}
347
348impl Remix {
349 #[must_use]
352 pub fn sources_sorted(&self) -> bool {
353 self.sources.windows(2).all(|w| {
354 let a = &w[0];
355 let b = &w[1];
356 match a.upstream_id.cmp(&b.upstream_id) {
357 core::cmp::Ordering::Less => true,
358 core::cmp::Ordering::Greater => false,
359 core::cmp::Ordering::Equal => a.commit_hash < b.commit_hash,
360 }
361 })
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
381pub struct Tag {
382 pub target: Hash,
383 pub target_type: ObjectType,
384 pub name: Vec<u8>,
385 pub tagger: Identity,
386 pub signer: [u8; 32],
387 pub message: Vec<u8>,
388 pub timestamp: u64,
389 pub signature: [u8; 64],
390}
391
392pub const TAG_NAME_MAX_LEN: u16 = 4096;
395
396impl Tag {
397 #[must_use]
403 pub fn name_is_valid(&self) -> bool {
404 if self.name.is_empty() || self.name.len() > TAG_NAME_MAX_LEN as usize {
405 return false;
406 }
407 !self.name.iter().any(|&b| matches!(b, 0 | b'/' | b'\\'))
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub struct ChunkedBlob {
414 pub total_size: u64,
415 pub chunk_size: u32,
417 pub chunks: Vec<Hash>,
418}
419
420impl ChunkedBlob {
421 pub fn check_reassembled_size(&self, reassembled_len: usize) -> Result<(), MkitError> {
431 let actual = reassembled_len as u64;
432 if actual != self.total_size {
433 return Err(MkitError::ChunkedBlobSizeMismatch {
434 expected: self.total_size,
435 actual,
436 });
437 }
438 Ok(())
439 }
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
444pub struct Delta {
445 pub base_hash: Hash,
446 pub result_size: u32,
447 pub instructions: Vec<u8>,
448}
449
450#[derive(Debug, Clone, PartialEq, Eq)]
452pub enum Object {
453 Blob(Blob),
454 Tree(Tree),
455 Commit(Commit),
456 Remix(Remix),
457 ChunkedBlob(ChunkedBlob),
458 Delta(Delta),
459 Tag(Tag),
460}
461
462impl Object {
463 #[must_use]
465 pub fn object_type(&self) -> ObjectType {
466 match self {
467 Self::Blob(_) => ObjectType::Blob,
468 Self::Tree(_) => ObjectType::Tree,
469 Self::Commit(_) => ObjectType::Commit,
470 Self::Remix(_) => ObjectType::Remix,
471 Self::ChunkedBlob(_) => ObjectType::ChunkedBlob,
472 Self::Delta(_) => ObjectType::Delta,
473 Self::Tag(_) => ObjectType::Tag,
474 }
475 }
476
477 pub fn id(&self) -> Result<Hash, MkitError> {
493 match merkle_id(self) {
494 Some(h) => Ok(h),
495 None => Ok(crate::hash::hash(&crate::serialize::serialize(self)?)),
496 }
497 }
498}
499
500#[must_use]
507fn merkle_id(obj: &Object) -> Option<Hash> {
508 match obj {
509 Object::Tree(t) => Some(crate::merkle::compute_tree_id(t)),
510 Object::ChunkedBlob(cb) => Some(crate::merkle::compute_chunked_id(cb)),
511 _ => None,
512 }
513}
514
515#[must_use]
532pub fn id_from_object(obj: &Object, bytes: &[u8]) -> Hash {
533 merkle_id(obj).unwrap_or_else(|| crate::hash::hash(bytes))
534}
535
536#[must_use]
546pub(crate) fn object_id_from_bytes(bytes: &[u8]) -> Hash {
547 let is_merkle = bytes
548 .first()
549 .and_then(|b| ObjectType::from_u8(*b).ok())
550 .is_some_and(ObjectType::is_merkle);
551 if is_merkle && let Ok(obj) = crate::serialize::deserialize(bytes) {
552 return id_from_object(&obj, bytes);
553 }
554 crate::hash::hash(bytes)
555}
556
557#[must_use]
567pub(crate) fn object_id_from_parts(parts: &[&[u8]]) -> Hash {
568 let merkle = parts
569 .first()
570 .and_then(|p| p.first())
571 .and_then(|b| ObjectType::from_u8(*b).ok())
572 .is_some_and(ObjectType::is_merkle);
573 if merkle {
574 return object_id_from_bytes(&parts.concat());
575 }
576 let mut hasher = crate::hash::Hasher::new();
577 for p in parts {
578 hasher.update(p);
579 }
580 hasher.finalize()
581}
582
583#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
586pub enum MkitError {
587 #[error("input is shorter than the 6-byte v1 prologue")]
588 EmptyData,
589 #[error("object_type byte {0:#04x} is not in 0x01..=0x07")]
590 InvalidObjectType(u8),
591 #[error("magic at offset 1 is not \"MKT1\"")]
592 InvalidMagic,
593 #[error("schema_version byte is not 0x01")]
594 UnsupportedObjectVersion,
595 #[error("input ended before a complete field could be read")]
596 UnexpectedEof,
597 #[error("non-empty trailing bytes after a complete object")]
598 TrailingData,
599 #[error("tree.entry_count > 1_000_000")]
600 TooManyEntries,
601 #[error("tree entry name is empty, too long, or contains a forbidden byte")]
602 InvalidEntryName,
603 #[error("tree entry mode byte {0:#04x} is not one of 0x01..=0x04")]
604 InvalidEntryMode(u8),
605 #[error("tree entries are not lexicographically sorted / contain duplicates")]
606 InvalidEntryOrder,
607 #[error("parent_count > 1_000")]
608 TooManyParents,
609 #[error("remix.source_count > 10_000")]
610 TooManySources,
611 #[error("tag name is empty, too long, or contains a forbidden byte (\\0 / \\)")]
612 TagNameInvalid,
613 #[error("tag target_type byte {0:#04x} is not a storable object type")]
614 TagTargetTypeInvalid(u8),
615 #[error("remix sources are not sorted by (upstream_id, commit_hash)")]
616 InvalidSourceOrder,
617 #[error("chunked_blob.chunk_count > 1_000_000")]
618 TooManyChunks,
619 #[error("chunked blob reassembles to {actual} bytes, manifest total_size is {expected}")]
623 ChunkedBlobSizeMismatch { expected: u64, actual: u64 },
624 #[error("identity kind byte {0:#04x} is not 0x01..=0x03")]
625 UnknownIdentityKind(u8),
626 #[error("identity has zero-length payload, or is Ed25519 with len != 32")]
627 InvalidIdentity,
628 #[error("identity payload len > {}", IDENTITY_MAX_LEN)]
629 IdentityTooLarge,
630 #[error("oversized payload in field `{field}`: {len} bytes > u32::MAX")]
634 OversizePayload { field: &'static str, len: usize },
635 #[error("rng failed to produce key material")]
638 RngFailure,
639 #[error("signature verification failed")]
642 SignatureInvalid,
643 #[error("public key is not a valid Ed25519 point")]
645 InvalidPublicKey,
646 #[error("key file mode {actual:#o} is broader than 0600")]
649 InsecureKeyPermissions { actual: u32 },
650 #[error("key file owner uid {actual} does not match process euid {euid}")]
654 InsecureKeyOwner { actual: u32, euid: u32 },
655 #[error("key directory mode {actual:#o} is broader than 0700")]
659 InsecureKeyDir { actual: u32 },
660 #[error("key path {0} is a symlink — refused")]
665 KeyPathIsSymlink(String),
666 #[error("key file size {actual} is not 32 bytes (raw Ed25519 seed)")]
668 InvalidKeyLength { actual: usize },
669 #[error("key file I/O error: {0}")]
672 KeyIo(String),
673 #[error("delta length {len} exceeds u32::MAX for field `{field}`")]
680 DeltaLengthOverflow { field: &'static str, len: usize },
681}
682
683impl fmt::Display for Object {
684 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
685 write!(f, "Object::{}", self.object_type().name())
686 }
687}
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692
693 #[test]
694 fn object_type_names() {
695 assert_eq!(ObjectType::Blob.name(), "blob");
696 assert_eq!(ObjectType::Tree.name(), "tree");
697 assert_eq!(ObjectType::Commit.name(), "commit");
698 assert_eq!(ObjectType::Remix.name(), "remix");
699 assert_eq!(ObjectType::ChunkedBlob.name(), "chunked_blob");
700 assert_eq!(ObjectType::Delta.name(), "delta");
701 assert_eq!(ObjectType::Tag.name(), "tag");
702 }
703
704 #[test]
709 fn object_id_and_id_from_object_agree_for_every_variant() {
710 let samples = [
711 Object::Blob(Blob {
712 data: vec![1, 2, 3, 4],
713 }),
714 Object::Tree(Tree { entries: vec![] }),
715 Object::Tree(Tree {
716 entries: vec![TreeEntry {
717 name: b"a".to_vec(),
718 mode: EntryMode::Blob,
719 object_hash: [9u8; 32],
720 }],
721 }),
722 Object::ChunkedBlob(ChunkedBlob {
723 total_size: 4,
724 chunk_size: 0,
725 chunks: vec![[7u8; 32], [8u8; 32]],
726 }),
727 Object::Commit(Commit::new_unannotated(
728 [1u8; 32],
729 vec![[2u8; 32]],
730 Identity::ed25519([3u8; 32]),
731 [4u8; 32],
732 b"msg".to_vec(),
733 1_700_000_000,
734 [9u8; 64],
735 )),
736 Object::Remix(Remix {
737 tree_hash: [5u8; 32],
738 parents: vec![[6u8; 32]],
739 sources: vec![RemixSource {
740 upstream_id: [10u8; 32],
741 commit_hash: [11u8; 32],
742 }],
743 author: Identity::ed25519([12u8; 32]),
744 signer: [13u8; 32],
745 message: b"remix".to_vec(),
746 timestamp: 1_700_000_001,
747 signature: [14u8; 64],
748 }),
749 Object::Delta(Delta {
750 base_hash: [15u8; 32],
751 result_size: 4,
752 instructions: vec![0u8; 4],
753 }),
754 Object::Tag(Tag {
755 target: [16u8; 32],
756 target_type: ObjectType::Commit,
757 name: b"v1".to_vec(),
758 tagger: Identity::ed25519([17u8; 32]),
759 signer: [18u8; 32],
760 message: b"tag".to_vec(),
761 timestamp: 1_700_000_002,
762 signature: [19u8; 64],
763 }),
764 ];
765 for obj in &samples {
766 let bytes = crate::serialize::serialize(obj).unwrap();
767 assert_eq!(
768 obj.id().unwrap(),
769 id_from_object(obj, &bytes),
770 "id paths diverged for {obj}"
771 );
772 }
773 }
774
775 #[test]
776 fn object_type_from_u8_accepts_valid_range() {
777 for b in 0x01u8..=0x07 {
778 assert!(
779 ObjectType::from_u8(b).is_ok(),
780 "byte {b:#04x} should decode"
781 );
782 }
783 }
784
785 #[test]
786 fn object_type_from_u8_rejects_zero_and_high() {
787 assert!(matches!(
788 ObjectType::from_u8(0x00),
789 Err(MkitError::InvalidObjectType(0))
790 ));
791 assert!(matches!(
792 ObjectType::from_u8(0xFF),
793 Err(MkitError::InvalidObjectType(0xFF))
794 ));
795 assert!(matches!(
796 ObjectType::from_u8(0x08),
797 Err(MkitError::InvalidObjectType(0x08))
798 ));
799 }
800
801 #[test]
802 fn tag_name_validity() {
803 let t = |name: &[u8]| Tag {
804 target: ZERO,
805 target_type: ObjectType::Commit,
806 name: name.to_vec(),
807 tagger: Identity::ed25519([0xaa; 32]),
808 signer: [0; 32],
809 message: vec![],
810 timestamp: 0,
811 signature: [0; 64],
812 };
813 assert!(t(b"v1.0.0").name_is_valid());
814 assert!(!t(b"").name_is_valid());
815 assert!(!t(b"a/b").name_is_valid());
816 assert!(!t(b"a\\b").name_is_valid());
817 assert!(!t(b"a\0b").name_is_valid());
818 assert!(!t(&vec![b'a'; TAG_NAME_MAX_LEN as usize + 1]).name_is_valid());
819 }
820
821 #[test]
822 fn tree_entry_name_rejects_empty() {
823 assert!(!TreeEntry::validate_name(b""));
824 }
825
826 #[test]
827 fn tree_entry_name_rejects_separators_and_null() {
828 assert!(!TreeEntry::validate_name(b"foo/bar"));
829 assert!(!TreeEntry::validate_name(b"foo\\bar"));
830 assert!(!TreeEntry::validate_name(b"fo\0o"));
831 }
832
833 #[test]
834 fn tree_entry_name_rejects_dot_and_dotdot() {
835 assert!(!TreeEntry::validate_name(b"."));
836 assert!(!TreeEntry::validate_name(b".."));
837 }
838
839 #[test]
840 fn tree_entry_name_accepts_common() {
841 assert!(TreeEntry::validate_name(b"file.txt"));
842 assert!(TreeEntry::validate_name(b"a"));
843 assert!(TreeEntry::validate_name(b"foo-bar_baz.rs"));
844 }
845
846 #[test]
847 fn tree_entry_name_rejects_over_255() {
848 let long = vec![b'a'; 256];
849 assert!(!TreeEntry::validate_name(&long));
850 }
851
852 #[test]
853 fn tree_entry_name_rejects_dot_mkit_and_dot_git_case_insensitive() {
854 assert!(!TreeEntry::validate_name(b".mkit"));
856 assert!(!TreeEntry::validate_name(b".git"));
857 assert!(!TreeEntry::validate_name(b".MKIT"));
859 assert!(!TreeEntry::validate_name(b".Mkit"));
860 assert!(!TreeEntry::validate_name(b".GIT"));
861 assert!(!TreeEntry::validate_name(b".Git"));
862 assert!(TreeEntry::validate_name(b".mkitignore"));
864 assert!(TreeEntry::validate_name(b".gitignore"));
865 }
866
867 #[test]
868 fn tree_entry_name_rejects_trailing_dot_or_space() {
869 assert!(!TreeEntry::validate_name(b"foo."));
872 assert!(!TreeEntry::validate_name(b"foo "));
873 assert!(!TreeEntry::validate_name(b"foo..."));
874 assert!(!TreeEntry::validate_name(b"foo "));
875 assert!(TreeEntry::validate_name(b"foo.bar"));
877 assert!(TreeEntry::validate_name(b"foo bar"));
878 }
879
880 #[test]
881 fn tree_entry_name_rejects_windows_reserved_device_names() {
882 for n in [
883 b"CON".as_slice(),
884 b"PRN",
885 b"AUX",
886 b"NUL",
887 b"COM1",
888 b"COM9",
889 b"LPT1",
890 b"LPT9",
891 b"con",
893 b"Nul",
894 b"lpt3",
895 b"CON.txt",
897 b"nul.log",
898 b"COM1.dat",
899 ] {
900 assert!(
901 !TreeEntry::validate_name(n),
902 "expected Windows reserved name rejected: {:?}",
903 std::str::from_utf8(n).unwrap_or("?")
904 );
905 }
906 assert!(TreeEntry::validate_name(b"COM0"));
908 assert!(TreeEntry::validate_name(b"LPT0"));
909 assert!(TreeEntry::validate_name(b"COM10"));
910 assert!(TreeEntry::validate_name(b"CONSOLE"));
911 assert!(TreeEntry::validate_name(b"NULL"));
912 }
913
914 #[test]
915 fn identity_rejects_empty_payload_all_kinds() {
916 for kind in [
917 IdentityKind::Ed25519,
918 IdentityKind::DidKey,
919 IdentityKind::Opaque,
920 ] {
921 assert!(
922 !Identity {
923 kind,
924 bytes: Vec::new()
925 }
926 .is_valid()
927 );
928 }
929 }
930
931 #[test]
932 fn identity_rejects_oversize() {
933 let bytes = vec![0xaa; IDENTITY_MAX_LEN as usize + 1];
934 assert!(
935 !Identity {
936 kind: IdentityKind::Opaque,
937 bytes
938 }
939 .is_valid()
940 );
941 }
942
943 #[test]
944 fn identity_requires_32_bytes_for_ed25519() {
945 assert!(
946 !Identity {
947 kind: IdentityKind::Ed25519,
948 bytes: vec![0xaa; 16]
949 }
950 .is_valid()
951 );
952 assert!(Identity::ed25519([0xaa; 32]).is_valid());
953 }
954
955 #[test]
956 fn didkey_requires_printable_ascii_multibase() {
957 let didkey = |b: &[u8]| Identity {
958 kind: IdentityKind::DidKey,
959 bytes: b.to_vec(),
960 };
961 assert!(didkey(b"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").is_valid());
963 assert!(didkey(b"mEiB1234").is_valid());
965 assert!(!didkey(b"z\0\x01\x02").is_valid());
967 assert!(!didkey(&[0xde, 0xad, 0xbe, 0xef]).is_valid());
968 assert!(!didkey(b"z6Mk has space").is_valid());
970 assert!(!didkey(b"z6Mk\n").is_valid());
971 }
972
973 #[test]
974 fn tree_is_sorted_checks() {
975 let e = |n: &[u8]| TreeEntry {
976 name: n.to_vec(),
977 mode: EntryMode::Blob,
978 object_hash: ZERO,
979 };
980 let sorted = Tree {
981 entries: vec![e(b"alpha"), e(b"beta"), e(b"gamma")],
982 };
983 assert!(sorted.is_sorted());
984 let unsorted = Tree {
985 entries: vec![e(b"beta"), e(b"alpha")],
986 };
987 assert!(!unsorted.is_sorted());
988 let dup = Tree {
989 entries: vec![e(b"alpha"), e(b"alpha")],
990 };
991 assert!(!dup.is_sorted());
992 }
993
994 #[test]
995 fn remix_sources_sorted_checks() {
996 let src = |u: u8, c: u8| RemixSource {
997 upstream_id: [u; 32],
998 commit_hash: [c; 32],
999 };
1000 let r = |sources| Remix {
1001 tree_hash: ZERO,
1002 parents: vec![],
1003 sources,
1004 author: Identity::ed25519([0xaa; 32]),
1005 signer: [0; 32],
1006 message: vec![],
1007 timestamp: 0,
1008 signature: [0; 64],
1009 };
1010 assert!(r(vec![src(1, 1), src(1, 2), src(2, 1)]).sources_sorted());
1011 assert!(!r(vec![src(2, 1), src(1, 1)]).sources_sorted());
1012 assert!(!r(vec![src(1, 1), src(1, 1)]).sources_sorted());
1013 }
1014}