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 verified_id_and_object(bytes).0
548}
549
550#[must_use]
560pub(crate) fn verified_id_and_object(bytes: &[u8]) -> (Hash, Option<Object>) {
561 let is_merkle = bytes
562 .first()
563 .and_then(|b| ObjectType::from_u8(*b).ok())
564 .is_some_and(ObjectType::is_merkle);
565 if is_merkle && let Ok(obj) = crate::serialize::deserialize(bytes) {
566 let id = id_from_object(&obj, bytes);
567 return (id, Some(obj));
568 }
569 (crate::hash::hash(bytes), None)
570}
571
572#[must_use]
582pub(crate) fn object_id_from_parts(parts: &[&[u8]]) -> Hash {
583 let merkle = parts
584 .first()
585 .and_then(|p| p.first())
586 .and_then(|b| ObjectType::from_u8(*b).ok())
587 .is_some_and(ObjectType::is_merkle);
588 if merkle {
589 return object_id_from_bytes(&parts.concat());
590 }
591 let mut hasher = crate::hash::Hasher::new();
592 for p in parts {
593 hasher.update(p);
594 }
595 hasher.finalize()
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
601pub enum MkitError {
602 #[error("input is shorter than the 6-byte v1 prologue")]
603 EmptyData,
604 #[error("object_type byte {0:#04x} is not in 0x01..=0x07")]
605 InvalidObjectType(u8),
606 #[error("magic at offset 1 is not \"MKT1\"")]
607 InvalidMagic,
608 #[error("schema_version byte is not 0x01")]
609 UnsupportedObjectVersion,
610 #[error("input ended before a complete field could be read")]
611 UnexpectedEof,
612 #[error("non-empty trailing bytes after a complete object")]
613 TrailingData,
614 #[error("tree.entry_count > 1_000_000")]
615 TooManyEntries,
616 #[error("tree entry name is empty, too long, or contains a forbidden byte")]
617 InvalidEntryName,
618 #[error("tree entry mode byte {0:#04x} is not one of 0x01..=0x04")]
619 InvalidEntryMode(u8),
620 #[error("tree entries are not lexicographically sorted / contain duplicates")]
621 InvalidEntryOrder,
622 #[error("parent_count > 1_000")]
623 TooManyParents,
624 #[error("remix.source_count > 10_000")]
625 TooManySources,
626 #[error("tag name is empty, too long, or contains a forbidden byte (\\0 / \\)")]
627 TagNameInvalid,
628 #[error("tag target_type byte {0:#04x} is not a storable object type")]
629 TagTargetTypeInvalid(u8),
630 #[error("remix sources are not sorted by (upstream_id, commit_hash)")]
631 InvalidSourceOrder,
632 #[error("chunked_blob.chunk_count > 1_000_000")]
633 TooManyChunks,
634 #[error("chunked blob reassembles to {actual} bytes, manifest total_size is {expected}")]
638 ChunkedBlobSizeMismatch { expected: u64, actual: u64 },
639 #[error("identity kind byte {0:#04x} is not 0x01..=0x03")]
640 UnknownIdentityKind(u8),
641 #[error("identity has zero-length payload, or is Ed25519 with len != 32")]
642 InvalidIdentity,
643 #[error("identity payload len > {}", IDENTITY_MAX_LEN)]
644 IdentityTooLarge,
645 #[error("oversized payload in field `{field}`: {len} bytes > u32::MAX")]
649 OversizePayload { field: &'static str, len: usize },
650 #[error("rng failed to produce key material")]
653 RngFailure,
654 #[error("signature verification failed")]
657 SignatureInvalid,
658 #[error("public key is not a valid Ed25519 point")]
660 InvalidPublicKey,
661 #[error("key file mode {actual:#o} is broader than 0600")]
664 InsecureKeyPermissions { actual: u32 },
665 #[error("key file owner uid {actual} does not match process euid {euid}")]
669 InsecureKeyOwner { actual: u32, euid: u32 },
670 #[error("key directory mode {actual:#o} is broader than 0700")]
674 InsecureKeyDir { actual: u32 },
675 #[error("key path {0} is a symlink — refused")]
680 KeyPathIsSymlink(String),
681 #[error("key file size {actual} is not 32 bytes (raw Ed25519 seed)")]
683 InvalidKeyLength { actual: usize },
684 #[error("key file I/O error: {0}")]
687 KeyIo(String),
688 #[error("delta length {len} exceeds u32::MAX for field `{field}`")]
695 DeltaLengthOverflow { field: &'static str, len: usize },
696 #[error("delta stream is structurally corrupt: {0}")]
708 DeltaCorrupt(crate::delta::DeltaCorruption),
709}
710
711impl fmt::Display for Object {
712 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
713 write!(f, "Object::{}", self.object_type().name())
714 }
715}
716
717#[cfg(test)]
718mod tests {
719 use super::*;
720
721 #[test]
722 fn object_type_names() {
723 assert_eq!(ObjectType::Blob.name(), "blob");
724 assert_eq!(ObjectType::Tree.name(), "tree");
725 assert_eq!(ObjectType::Commit.name(), "commit");
726 assert_eq!(ObjectType::Remix.name(), "remix");
727 assert_eq!(ObjectType::ChunkedBlob.name(), "chunked_blob");
728 assert_eq!(ObjectType::Delta.name(), "delta");
729 assert_eq!(ObjectType::Tag.name(), "tag");
730 }
731
732 #[test]
737 fn object_id_and_id_from_object_agree_for_every_variant() {
738 let samples = [
739 Object::Blob(Blob {
740 data: vec![1, 2, 3, 4],
741 }),
742 Object::Tree(Tree { entries: vec![] }),
743 Object::Tree(Tree {
744 entries: vec![TreeEntry {
745 name: b"a".to_vec(),
746 mode: EntryMode::Blob,
747 object_hash: [9u8; 32],
748 }],
749 }),
750 Object::ChunkedBlob(ChunkedBlob {
751 total_size: 4,
752 chunk_size: 0,
753 chunks: vec![[7u8; 32], [8u8; 32]],
754 }),
755 Object::Commit(Commit::new_unannotated(
756 [1u8; 32],
757 vec![[2u8; 32]],
758 Identity::ed25519([3u8; 32]),
759 [4u8; 32],
760 b"msg".to_vec(),
761 1_700_000_000,
762 [9u8; 64],
763 )),
764 Object::Remix(Remix {
765 tree_hash: [5u8; 32],
766 parents: vec![[6u8; 32]],
767 sources: vec![RemixSource {
768 upstream_id: [10u8; 32],
769 commit_hash: [11u8; 32],
770 }],
771 author: Identity::ed25519([12u8; 32]),
772 signer: [13u8; 32],
773 message: b"remix".to_vec(),
774 timestamp: 1_700_000_001,
775 signature: [14u8; 64],
776 }),
777 Object::Delta(Delta {
778 base_hash: [15u8; 32],
779 result_size: 4,
780 instructions: vec![0u8; 4],
781 }),
782 Object::Tag(Tag {
783 target: [16u8; 32],
784 target_type: ObjectType::Commit,
785 name: b"v1".to_vec(),
786 tagger: Identity::ed25519([17u8; 32]),
787 signer: [18u8; 32],
788 message: b"tag".to_vec(),
789 timestamp: 1_700_000_002,
790 signature: [19u8; 64],
791 }),
792 ];
793 for obj in &samples {
794 let bytes = crate::serialize::serialize(obj).unwrap();
795 assert_eq!(
796 obj.id().unwrap(),
797 id_from_object(obj, &bytes),
798 "id paths diverged for {obj}"
799 );
800 }
801 }
802
803 #[test]
804 fn object_type_from_u8_accepts_valid_range() {
805 for b in 0x01u8..=0x07 {
806 assert!(
807 ObjectType::from_u8(b).is_ok(),
808 "byte {b:#04x} should decode"
809 );
810 }
811 }
812
813 #[test]
814 fn object_type_from_u8_rejects_zero_and_high() {
815 assert!(matches!(
816 ObjectType::from_u8(0x00),
817 Err(MkitError::InvalidObjectType(0))
818 ));
819 assert!(matches!(
820 ObjectType::from_u8(0xFF),
821 Err(MkitError::InvalidObjectType(0xFF))
822 ));
823 assert!(matches!(
824 ObjectType::from_u8(0x08),
825 Err(MkitError::InvalidObjectType(0x08))
826 ));
827 }
828
829 #[test]
830 fn tag_name_validity() {
831 let t = |name: &[u8]| Tag {
832 target: ZERO,
833 target_type: ObjectType::Commit,
834 name: name.to_vec(),
835 tagger: Identity::ed25519([0xaa; 32]),
836 signer: [0; 32],
837 message: vec![],
838 timestamp: 0,
839 signature: [0; 64],
840 };
841 assert!(t(b"v1.0.0").name_is_valid());
842 assert!(!t(b"").name_is_valid());
843 assert!(!t(b"a/b").name_is_valid());
844 assert!(!t(b"a\\b").name_is_valid());
845 assert!(!t(b"a\0b").name_is_valid());
846 assert!(!t(&vec![b'a'; TAG_NAME_MAX_LEN as usize + 1]).name_is_valid());
847 }
848
849 #[test]
850 fn tree_entry_name_rejects_empty() {
851 assert!(!TreeEntry::validate_name(b""));
852 }
853
854 #[test]
855 fn tree_entry_name_rejects_separators_and_null() {
856 assert!(!TreeEntry::validate_name(b"foo/bar"));
857 assert!(!TreeEntry::validate_name(b"foo\\bar"));
858 assert!(!TreeEntry::validate_name(b"fo\0o"));
859 }
860
861 #[test]
862 fn tree_entry_name_rejects_dot_and_dotdot() {
863 assert!(!TreeEntry::validate_name(b"."));
864 assert!(!TreeEntry::validate_name(b".."));
865 }
866
867 #[test]
868 fn tree_entry_name_accepts_common() {
869 assert!(TreeEntry::validate_name(b"file.txt"));
870 assert!(TreeEntry::validate_name(b"a"));
871 assert!(TreeEntry::validate_name(b"foo-bar_baz.rs"));
872 }
873
874 #[test]
875 fn tree_entry_name_rejects_over_255() {
876 let long = vec![b'a'; 256];
877 assert!(!TreeEntry::validate_name(&long));
878 }
879
880 #[test]
881 fn tree_entry_name_rejects_dot_mkit_and_dot_git_case_insensitive() {
882 assert!(!TreeEntry::validate_name(b".mkit"));
884 assert!(!TreeEntry::validate_name(b".git"));
885 assert!(!TreeEntry::validate_name(b".MKIT"));
887 assert!(!TreeEntry::validate_name(b".Mkit"));
888 assert!(!TreeEntry::validate_name(b".GIT"));
889 assert!(!TreeEntry::validate_name(b".Git"));
890 assert!(TreeEntry::validate_name(b".mkitignore"));
892 assert!(TreeEntry::validate_name(b".gitignore"));
893 }
894
895 #[test]
896 fn tree_entry_name_rejects_trailing_dot_or_space() {
897 assert!(!TreeEntry::validate_name(b"foo."));
900 assert!(!TreeEntry::validate_name(b"foo "));
901 assert!(!TreeEntry::validate_name(b"foo..."));
902 assert!(!TreeEntry::validate_name(b"foo "));
903 assert!(TreeEntry::validate_name(b"foo.bar"));
905 assert!(TreeEntry::validate_name(b"foo bar"));
906 }
907
908 #[test]
909 fn tree_entry_name_rejects_windows_reserved_device_names() {
910 for n in [
911 b"CON".as_slice(),
912 b"PRN",
913 b"AUX",
914 b"NUL",
915 b"COM1",
916 b"COM9",
917 b"LPT1",
918 b"LPT9",
919 b"con",
921 b"Nul",
922 b"lpt3",
923 b"CON.txt",
925 b"nul.log",
926 b"COM1.dat",
927 ] {
928 assert!(
929 !TreeEntry::validate_name(n),
930 "expected Windows reserved name rejected: {:?}",
931 std::str::from_utf8(n).unwrap_or("?")
932 );
933 }
934 assert!(TreeEntry::validate_name(b"COM0"));
936 assert!(TreeEntry::validate_name(b"LPT0"));
937 assert!(TreeEntry::validate_name(b"COM10"));
938 assert!(TreeEntry::validate_name(b"CONSOLE"));
939 assert!(TreeEntry::validate_name(b"NULL"));
940 }
941
942 #[test]
943 fn identity_rejects_empty_payload_all_kinds() {
944 for kind in [
945 IdentityKind::Ed25519,
946 IdentityKind::DidKey,
947 IdentityKind::Opaque,
948 ] {
949 assert!(
950 !Identity {
951 kind,
952 bytes: Vec::new()
953 }
954 .is_valid()
955 );
956 }
957 }
958
959 #[test]
960 fn identity_rejects_oversize() {
961 let bytes = vec![0xaa; IDENTITY_MAX_LEN as usize + 1];
962 assert!(
963 !Identity {
964 kind: IdentityKind::Opaque,
965 bytes
966 }
967 .is_valid()
968 );
969 }
970
971 #[test]
972 fn identity_requires_32_bytes_for_ed25519() {
973 assert!(
974 !Identity {
975 kind: IdentityKind::Ed25519,
976 bytes: vec![0xaa; 16]
977 }
978 .is_valid()
979 );
980 assert!(Identity::ed25519([0xaa; 32]).is_valid());
981 }
982
983 #[test]
984 fn didkey_requires_printable_ascii_multibase() {
985 let didkey = |b: &[u8]| Identity {
986 kind: IdentityKind::DidKey,
987 bytes: b.to_vec(),
988 };
989 assert!(didkey(b"z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK").is_valid());
991 assert!(didkey(b"mEiB1234").is_valid());
993 assert!(!didkey(b"z\0\x01\x02").is_valid());
995 assert!(!didkey(&[0xde, 0xad, 0xbe, 0xef]).is_valid());
996 assert!(!didkey(b"z6Mk has space").is_valid());
998 assert!(!didkey(b"z6Mk\n").is_valid());
999 }
1000
1001 #[test]
1002 fn tree_is_sorted_checks() {
1003 let e = |n: &[u8]| TreeEntry {
1004 name: n.to_vec(),
1005 mode: EntryMode::Blob,
1006 object_hash: ZERO,
1007 };
1008 let sorted = Tree {
1009 entries: vec![e(b"alpha"), e(b"beta"), e(b"gamma")],
1010 };
1011 assert!(sorted.is_sorted());
1012 let unsorted = Tree {
1013 entries: vec![e(b"beta"), e(b"alpha")],
1014 };
1015 assert!(!unsorted.is_sorted());
1016 let dup = Tree {
1017 entries: vec![e(b"alpha"), e(b"alpha")],
1018 };
1019 assert!(!dup.is_sorted());
1020 }
1021
1022 #[test]
1023 fn remix_sources_sorted_checks() {
1024 let src = |u: u8, c: u8| RemixSource {
1025 upstream_id: [u; 32],
1026 commit_hash: [c; 32],
1027 };
1028 let r = |sources| Remix {
1029 tree_hash: ZERO,
1030 parents: vec![],
1031 sources,
1032 author: Identity::ed25519([0xaa; 32]),
1033 signer: [0; 32],
1034 message: vec![],
1035 timestamp: 0,
1036 signature: [0; 64],
1037 };
1038 assert!(r(vec![src(1, 1), src(1, 2), src(2, 1)]).sources_sorted());
1039 assert!(!r(vec![src(2, 1), src(1, 1)]).sources_sorted());
1040 assert!(!r(vec![src(1, 1), src(1, 1)]).sources_sorted());
1041 }
1042}