1#![deny(unsafe_code)]
19#![warn(missing_docs)]
20
21#[cfg(feature = "signing")]
22pub mod signing;
23
24pub mod chunked;
25
26use std::collections::BTreeMap;
27
28use freenet_git_encoding::canonical::{MapBuilder, Value};
29use freenet_git_encoding::signed::build as build_payload;
30use freenet_git_encoding::WIRE_VERSION;
31use serde::{Deserialize, Serialize};
32
33pub mod limits {
38 pub const MAX_NAME_BYTES: usize = 256;
41 pub const MAX_DESCRIPTION_BYTES: usize = 4096;
44 pub const MAX_REF_NAME_BYTES: usize = 256;
46 pub const MAX_EXTENSION_KEY_BYTES: usize = 256;
48 pub const MAX_EXTENSION_VALUE_BYTES: usize = 64 * 1024;
50 pub const MIN_PREFIX_LEN: usize = 4;
53 pub const MAX_PREFIX_LEN: usize = 32;
58 pub const DEFAULT_PREFIX_LEN: usize = 12;
66}
67
68pub type PublicKey = [u8; 32];
71
72pub type Signature = [u8; 64];
74
75pub type PackHash = [u8; 32];
77
78pub type ManifestHash = [u8; 32];
80
81pub type CommitHash = [u8; 20];
85
86pub type ObjectBundleId = [u8; 32];
88
89pub type RepoKey = [u8; 32];
94
95pub type RefName = String;
108
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
125pub struct RepoParams {
126 pub prefix: String,
129}
130
131impl RepoParams {
132 pub fn from_owner(owner: &PublicKey, len: usize) -> Self {
136 Self {
137 prefix: pubkey_prefix(owner, len),
138 }
139 }
140
141 pub fn to_bytes(&self) -> Vec<u8> {
143 bincode::serialize(self).expect("RepoParams serialization is infallible")
144 }
145
146 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
148 bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeParams(e.to_string()))
149 }
150}
151
152pub fn pubkey_prefix(owner: &PublicKey, len: usize) -> String {
156 let encoded = bs58::encode(owner).into_string();
157 let take = len.min(encoded.len());
158 encoded[..take].to_string()
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
164pub struct SignedField<T> {
165 pub value: T,
167 pub update_seq: u64,
170 #[serde(with = "serde_bytes_array_64")]
172 pub signature: Signature,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
178pub struct WriterGrant {
179 pub granted_at_epoch: u64,
181 pub revoked_at_epoch: Option<u64>,
183}
184
185#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
189pub struct AclState {
190 pub epoch: u64,
192 pub grants: BTreeMap<PublicKey, WriterGrant>,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200pub struct RefEntry {
201 pub target: CommitHash,
216 pub update_seq: u64,
218 pub updater: PublicKey,
221 pub auth_epoch: u64,
224 #[serde(with = "serde_bytes_array_64")]
226 pub signature: Signature,
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
231pub enum ObjectBundle {
232 SinglePack {
234 pack_hash: PackHash,
236 size_bytes: u64,
238 },
239 ChunkedPack {
243 manifest_hash: ManifestHash,
245 total_size: u64,
247 chunk_count: u32,
249 },
250}
251
252impl ObjectBundle {
253 pub fn id(&self) -> ObjectBundleId {
259 let value = match self {
260 Self::SinglePack {
261 pack_hash,
262 size_bytes,
263 } => MapBuilder::default()
264 .text_entry("kind", Value::text("single-pack"))
265 .text_entry("pack_hash", Value::bytes(pack_hash.to_vec()))
266 .text_entry("size_bytes", Value::uint(*size_bytes))
267 .build(),
268 Self::ChunkedPack {
269 manifest_hash,
270 total_size,
271 chunk_count,
272 } => MapBuilder::default()
273 .text_entry("kind", Value::text("chunked-pack"))
274 .text_entry("manifest_hash", Value::bytes(manifest_hash.to_vec()))
275 .text_entry("total_size", Value::uint(*total_size))
276 .text_entry("chunk_count", Value::uint(u64::from(*chunk_count)))
277 .build(),
278 };
279 *blake3::hash(&value.encode()).as_bytes()
280 }
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
286pub struct ObjectBundleRecord {
287 pub bundle: ObjectBundle,
290 pub added_by: PublicKey,
293 pub auth_epoch: u64,
295 #[serde(with = "serde_bytes_array_64")]
297 pub signature: Signature,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
302pub struct ExtensionEntry {
303 #[serde(with = "serde_bytes")]
305 pub value: Vec<u8>,
306 pub update_seq: u64,
308 #[serde(with = "serde_bytes_array_64")]
310 pub signature: Signature,
311}
312
313#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
315pub struct RepoState {
316 pub owner: PublicKey,
322 pub name: Option<SignedField<String>>,
324 pub description: Option<SignedField<String>>,
326 pub default_branch: Option<SignedField<RefName>>,
328 pub force_push_allowed: Option<SignedField<Vec<RefName>>>,
331 pub acl: Option<SignedField<AclState>>,
333 pub upgrade: Option<SignedField<Option<RepoKey>>>,
335 pub refs: BTreeMap<RefName, RefEntry>,
337 pub object_index: BTreeMap<ObjectBundleId, ObjectBundleRecord>,
339 pub extensions: BTreeMap<String, ExtensionEntry>,
341}
342
343impl RepoState {
344 pub fn to_bytes(&self) -> Vec<u8> {
346 bincode::serialize(self).expect("RepoState serialization is infallible")
347 }
348
349 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
351 if bytes.is_empty() {
352 return Ok(Self::default());
353 }
354 bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeState(e.to_string()))
355 }
356}
357
358#[derive(Debug, thiserror::Error)]
361pub enum ValidateError {
362 #[error("decode parameters: {0}")]
364 DecodeParams(String),
365 #[error("decode state: {0}")]
367 DecodeState(String),
368 #[error("signature verification failed for {0}")]
370 InvalidSignature(&'static str),
371 #[error("entry signed by non-owner key (Phase 1.0 is single-writer)")]
374 NonOwnerSigner,
375 #[error("field {field} exceeds limit ({len} > {max})")]
377 FieldTooLong {
378 field: &'static str,
380 len: usize,
382 max: usize,
384 },
385 #[error("object_index entry has wrong bundle id")]
387 BundleIdMismatch,
388 #[error("monotonic invariant violated: {0}")]
390 NonMonotonic(&'static str),
391 #[error("prefix length {len} outside valid range [{min}..={max}]")]
393 InvalidPrefixLength {
394 len: usize,
396 min: usize,
398 max: usize,
400 },
401 #[error("owner pubkey does not match parameters prefix: expected {expected}, got {actual}")]
403 PrefixMismatch {
404 expected: String,
406 actual: String,
408 },
409 #[error("prefix contains invalid base58 characters")]
412 InvalidPrefixChars,
413}
414
415pub fn validate_state(params: &RepoParams, state: &RepoState) -> Result<(), ValidateError> {
422 if params.prefix.len() < limits::MIN_PREFIX_LEN || params.prefix.len() > limits::MAX_PREFIX_LEN
424 {
425 return Err(ValidateError::InvalidPrefixLength {
426 len: params.prefix.len(),
427 min: limits::MIN_PREFIX_LEN,
428 max: limits::MAX_PREFIX_LEN,
429 });
430 }
431 if bs58::decode(¶ms.prefix).into_vec().is_err() {
434 return Err(ValidateError::InvalidPrefixChars);
435 }
436 let actual_prefix = pubkey_prefix(&state.owner, params.prefix.len());
438 if actual_prefix != params.prefix {
439 return Err(ValidateError::PrefixMismatch {
440 expected: params.prefix.clone(),
441 actual: actual_prefix,
442 });
443 }
444 let repo_key = params_repo_key(params, &state.owner);
445
446 if let Some(field) = &state.name {
447 check_size("name", field.value.len(), limits::MAX_NAME_BYTES)?;
448 verify_signed_field_string(&repo_key, "name", &state.owner, field, "name")?;
449 }
450 if let Some(field) = &state.description {
451 check_size(
452 "description",
453 field.value.len(),
454 limits::MAX_DESCRIPTION_BYTES,
455 )?;
456 verify_signed_field_string(&repo_key, "description", &state.owner, field, "description")?;
457 }
458 if let Some(field) = &state.default_branch {
459 check_size(
460 "default_branch",
461 field.value.len(),
462 limits::MAX_REF_NAME_BYTES,
463 )?;
464 verify_signed_field_string(
465 &repo_key,
466 "default_branch",
467 &state.owner,
468 field,
469 "default_branch",
470 )?;
471 }
472 if let Some(field) = &state.force_push_allowed {
473 verify_signed_field_ref_list(
474 &repo_key,
475 "force_push_allowed",
476 &state.owner,
477 field,
478 "force_push_allowed",
479 )?;
480 }
481 if let Some(field) = &state.acl {
482 verify_signed_field_acl(&repo_key, "acl", &state.owner, field, "acl")?;
483 }
484 if let Some(field) = &state.upgrade {
485 verify_signed_field_optional_repo_key(
486 &repo_key,
487 "upgrade",
488 &state.owner,
489 field,
490 "upgrade",
491 )?;
492 }
493
494 for (ref_name, entry) in &state.refs {
495 check_size("ref name", ref_name.len(), limits::MAX_REF_NAME_BYTES)?;
496 if entry.updater != state.owner {
498 return Err(ValidateError::NonOwnerSigner);
499 }
500 verify_ref_entry(&repo_key, ref_name, entry)?;
501 }
502
503 for (bundle_id, record) in &state.object_index {
504 if record.bundle.id() != *bundle_id {
505 return Err(ValidateError::BundleIdMismatch);
506 }
507 if record.added_by != state.owner {
508 return Err(ValidateError::NonOwnerSigner);
509 }
510 verify_bundle_record(&repo_key, record)?;
511 }
512
513 for (ext_key, entry) in &state.extensions {
514 check_size(
515 "extension key",
516 ext_key.len(),
517 limits::MAX_EXTENSION_KEY_BYTES,
518 )?;
519 check_size(
520 "extension value",
521 entry.value.len(),
522 limits::MAX_EXTENSION_VALUE_BYTES,
523 )?;
524 verify_extension_entry(&repo_key, ext_key, &state.owner, entry)?;
525 }
526
527 Ok(())
528}
529
530#[derive(Debug, thiserror::Error)]
532pub enum UpdateError {
533 #[error(transparent)]
535 Invalid(#[from] ValidateError),
536 #[error("non-fast-forward update on protected ref")]
539 NonFastForward,
540}
541
542pub fn merge_state(current: &RepoState, incoming: &RepoState) -> RepoState {
548 let mut out = current.clone();
549 out.name = pick_signed_field(out.name, incoming.name.clone());
550 out.description = pick_signed_field(out.description, incoming.description.clone());
551 out.default_branch = pick_signed_field(out.default_branch, incoming.default_branch.clone());
552 out.force_push_allowed =
553 pick_signed_field(out.force_push_allowed, incoming.force_push_allowed.clone());
554 out.acl = pick_signed_field(out.acl, incoming.acl.clone());
555 out.upgrade = pick_signed_field(out.upgrade, incoming.upgrade.clone());
556
557 for (k, v) in &incoming.refs {
558 let pick = match out.refs.remove(k) {
559 None => v.clone(),
560 Some(existing) => pick_ref_entry(existing, v.clone()),
561 };
562 out.refs.insert(k.clone(), pick);
563 }
564 for (k, v) in &incoming.object_index {
565 out.object_index.entry(*k).or_insert_with(|| v.clone());
566 }
567 for (k, v) in &incoming.extensions {
568 let pick = match out.extensions.remove(k) {
569 None => v.clone(),
570 Some(existing) => pick_extension_entry(existing, v.clone()),
571 };
572 out.extensions.insert(k.clone(), pick);
573 }
574 out
575}
576
577pub fn update_state(
582 params: &RepoParams,
583 current: &RepoState,
584 delta: &RepoState,
585) -> Result<RepoState, UpdateError> {
586 let merged = merge_state(current, delta);
591
592 let _ = params;
604
605 validate_state(params, &merged)?;
606 Ok(merged)
607}
608
609#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
611pub struct RepoSummary {
612 pub field_seqs: BTreeMap<String, u64>,
614 pub ref_seqs: BTreeMap<RefName, u64>,
616 pub bundle_ids: Vec<ObjectBundleId>,
618 pub extension_seqs: BTreeMap<String, u64>,
620}
621
622pub fn summarize_state(state: &RepoState) -> RepoSummary {
625 let mut s = RepoSummary::default();
626 if let Some(f) = &state.name {
627 s.field_seqs.insert("name".into(), f.update_seq);
628 }
629 if let Some(f) = &state.description {
630 s.field_seqs.insert("description".into(), f.update_seq);
631 }
632 if let Some(f) = &state.default_branch {
633 s.field_seqs.insert("default_branch".into(), f.update_seq);
634 }
635 if let Some(f) = &state.force_push_allowed {
636 s.field_seqs
637 .insert("force_push_allowed".into(), f.update_seq);
638 }
639 if let Some(f) = &state.acl {
640 s.field_seqs.insert("acl".into(), f.update_seq);
641 }
642 if let Some(f) = &state.upgrade {
643 s.field_seqs.insert("upgrade".into(), f.update_seq);
644 }
645 for (k, v) in &state.refs {
646 s.ref_seqs.insert(k.clone(), v.update_seq);
647 }
648 for k in state.object_index.keys() {
649 s.bundle_ids.push(*k);
650 }
651 for (k, v) in &state.extensions {
652 s.extension_seqs.insert(k.clone(), v.update_seq);
653 }
654 s
655}
656
657pub fn get_state_delta(state: &RepoState, summary: &RepoSummary) -> RepoState {
659 let mut d = RepoState::default();
660
661 if state.name.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("name").copied() {
662 d.name = state.name.clone();
663 }
664 if state.description.as_ref().map(|f| f.update_seq)
665 > summary.field_seqs.get("description").copied()
666 {
667 d.description = state.description.clone();
668 }
669 if state.default_branch.as_ref().map(|f| f.update_seq)
670 > summary.field_seqs.get("default_branch").copied()
671 {
672 d.default_branch = state.default_branch.clone();
673 }
674 if state.force_push_allowed.as_ref().map(|f| f.update_seq)
675 > summary.field_seqs.get("force_push_allowed").copied()
676 {
677 d.force_push_allowed = state.force_push_allowed.clone();
678 }
679 if state.acl.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("acl").copied() {
680 d.acl = state.acl.clone();
681 }
682 if state.upgrade.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("upgrade").copied() {
683 d.upgrade = state.upgrade.clone();
684 }
685
686 for (k, v) in &state.refs {
687 if summary.ref_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
688 d.refs.insert(k.clone(), v.clone());
689 }
690 }
691 let known: std::collections::HashSet<&ObjectBundleId> = summary.bundle_ids.iter().collect();
692 for (k, v) in &state.object_index {
693 if !known.contains(k) {
694 d.object_index.insert(*k, v.clone());
695 }
696 }
697 for (k, v) in &state.extensions {
698 if summary.extension_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
699 d.extensions.insert(k.clone(), v.clone());
700 }
701 }
702 d
703}
704
705pub fn signed_payload_string_field(
712 repo_key: &RepoKey,
713 field_name: &str,
714 value: &str,
715 update_seq: u64,
716) -> Vec<u8> {
717 build_payload(field_name, |b| {
718 b.field_bytes(repo_key);
719 b.field_str(value);
720 b.field_u64(update_seq);
721 })
722}
723
724pub fn signed_payload_ref_list_field(
726 repo_key: &RepoKey,
727 field_name: &str,
728 refs: &[RefName],
729 update_seq: u64,
730) -> Vec<u8> {
731 build_payload(field_name, |b| {
732 b.field_bytes(repo_key);
733 b.field_u32(u32::try_from(refs.len()).unwrap_or(u32::MAX));
734 for r in refs {
735 b.field_str(r);
736 }
737 b.field_u64(update_seq);
738 })
739}
740
741pub fn signed_payload_acl_field(
743 repo_key: &RepoKey,
744 field_name: &str,
745 acl: &AclState,
746 update_seq: u64,
747) -> Vec<u8> {
748 build_payload(field_name, |b| {
749 b.field_bytes(repo_key);
750 b.field_u64(acl.epoch);
751 b.field_u32(u32::try_from(acl.grants.len()).unwrap_or(u32::MAX));
752 for (writer, grant) in &acl.grants {
753 b.field_bytes(writer);
754 b.field_u64(grant.granted_at_epoch);
755 b.field_option_bytes(
756 grant
757 .revoked_at_epoch
758 .map(|e| e.to_le_bytes())
759 .as_ref()
760 .map(|x| x.as_slice()),
761 );
762 }
763 b.field_u64(update_seq);
764 })
765}
766
767pub fn signed_payload_optional_repo_key_field(
769 repo_key: &RepoKey,
770 field_name: &str,
771 successor: Option<&RepoKey>,
772 update_seq: u64,
773) -> Vec<u8> {
774 build_payload(field_name, |b| {
775 b.field_bytes(repo_key);
776 b.field_option_bytes(successor.map(|k| k.as_slice()));
777 b.field_u64(update_seq);
778 })
779}
780
781pub fn signed_payload_ref_entry(
783 repo_key: &RepoKey,
784 ref_name: &str,
785 target: &CommitHash,
786 update_seq: u64,
787 auth_epoch: u64,
788) -> Vec<u8> {
789 build_payload("ref-update", |b| {
790 b.field_bytes(repo_key);
791 b.field_str(ref_name);
792 b.field_bytes(target);
793 b.field_u64(update_seq);
794 b.field_u64(auth_epoch);
795 })
796}
797
798pub fn signed_payload_bundle_record(
800 repo_key: &RepoKey,
801 bundle: &ObjectBundle,
802 auth_epoch: u64,
803) -> Vec<u8> {
804 let bundle_id = bundle.id();
805 build_payload("object-bundle", |b| {
806 b.field_bytes(repo_key);
807 b.field_bytes(&bundle_id);
808 b.field_u64(auth_epoch);
809 })
810}
811
812pub fn signed_payload_extension(
814 repo_key: &RepoKey,
815 ext_key: &str,
816 value: &[u8],
817 update_seq: u64,
818) -> Vec<u8> {
819 build_payload("extension", |b| {
820 b.field_bytes(repo_key);
821 b.field_str(ext_key);
822 b.field_bytes(value);
823 b.field_u64(update_seq);
824 })
825}
826
827fn params_repo_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
841 let mut h = blake3::Hasher::new();
842 h.update(WIRE_VERSION.as_bytes());
843 h.update(params.prefix.as_bytes());
844 h.update(owner);
845 *h.finalize().as_bytes()
846}
847
848pub fn signature_domain_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
851 params_repo_key(params, owner)
852}
853
854fn check_size(field: &'static str, len: usize, max: usize) -> Result<(), ValidateError> {
855 if len > max {
856 Err(ValidateError::FieldTooLong { field, len, max })
857 } else {
858 Ok(())
859 }
860}
861
862fn verify_signature(
863 payload: &[u8],
864 signer: &PublicKey,
865 signature: &Signature,
866 label: &'static str,
867) -> Result<(), ValidateError> {
868 let pk = ed25519_compact::PublicKey::from_slice(signer)
869 .map_err(|_| ValidateError::InvalidSignature(label))?;
870 let sig = ed25519_compact::Signature::from_slice(signature)
871 .map_err(|_| ValidateError::InvalidSignature(label))?;
872 pk.verify(payload, &sig)
873 .map_err(|_| ValidateError::InvalidSignature(label))
874}
875
876fn verify_signed_field_string(
877 repo_key: &RepoKey,
878 field_name: &str,
879 owner: &PublicKey,
880 field: &SignedField<String>,
881 label: &'static str,
882) -> Result<(), ValidateError> {
883 let payload = signed_payload_string_field(repo_key, field_name, &field.value, field.update_seq);
884 verify_signature(&payload, owner, &field.signature, label)
885}
886
887fn verify_signed_field_ref_list(
888 repo_key: &RepoKey,
889 field_name: &str,
890 owner: &PublicKey,
891 field: &SignedField<Vec<RefName>>,
892 label: &'static str,
893) -> Result<(), ValidateError> {
894 let payload =
895 signed_payload_ref_list_field(repo_key, field_name, &field.value, field.update_seq);
896 verify_signature(&payload, owner, &field.signature, label)
897}
898
899fn verify_signed_field_acl(
900 repo_key: &RepoKey,
901 field_name: &str,
902 owner: &PublicKey,
903 field: &SignedField<AclState>,
904 label: &'static str,
905) -> Result<(), ValidateError> {
906 let payload = signed_payload_acl_field(repo_key, field_name, &field.value, field.update_seq);
907 verify_signature(&payload, owner, &field.signature, label)
908}
909
910fn verify_signed_field_optional_repo_key(
911 repo_key: &RepoKey,
912 field_name: &str,
913 owner: &PublicKey,
914 field: &SignedField<Option<RepoKey>>,
915 label: &'static str,
916) -> Result<(), ValidateError> {
917 let payload = signed_payload_optional_repo_key_field(
918 repo_key,
919 field_name,
920 field.value.as_ref(),
921 field.update_seq,
922 );
923 verify_signature(&payload, owner, &field.signature, label)
924}
925
926fn verify_ref_entry(
927 repo_key: &RepoKey,
928 ref_name: &str,
929 entry: &RefEntry,
930) -> Result<(), ValidateError> {
931 let payload = signed_payload_ref_entry(
932 repo_key,
933 ref_name,
934 &entry.target,
935 entry.update_seq,
936 entry.auth_epoch,
937 );
938 verify_signature(&payload, &entry.updater, &entry.signature, "ref entry")
939}
940
941fn verify_bundle_record(
942 repo_key: &RepoKey,
943 record: &ObjectBundleRecord,
944) -> Result<(), ValidateError> {
945 let payload = signed_payload_bundle_record(repo_key, &record.bundle, record.auth_epoch);
946 verify_signature(
947 &payload,
948 &record.added_by,
949 &record.signature,
950 "bundle record",
951 )
952}
953
954fn verify_extension_entry(
955 repo_key: &RepoKey,
956 ext_key: &str,
957 owner: &PublicKey,
958 entry: &ExtensionEntry,
959) -> Result<(), ValidateError> {
960 let payload = signed_payload_extension(repo_key, ext_key, &entry.value, entry.update_seq);
961 verify_signature(&payload, owner, &entry.signature, "extension entry")
962}
963
964fn pick_signed_field<T: Clone>(
965 a: Option<SignedField<T>>,
966 b: Option<SignedField<T>>,
967) -> Option<SignedField<T>> {
968 match (a, b) {
969 (None, x) | (x, None) => x,
970 (Some(x), Some(y)) => Some(
971 if pick_higher_seq(x.update_seq, &x.signature, y.update_seq, &y.signature) {
972 x
973 } else {
974 y
975 },
976 ),
977 }
978}
979
980fn pick_ref_entry(a: RefEntry, b: RefEntry) -> RefEntry {
981 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
982 a
983 } else {
984 b
985 }
986}
987
988fn pick_extension_entry(a: ExtensionEntry, b: ExtensionEntry) -> ExtensionEntry {
989 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
990 a
991 } else {
992 b
993 }
994}
995
996fn pick_higher_seq(a_seq: u64, a_sig: &Signature, b_seq: u64, b_sig: &Signature) -> bool {
1000 match a_seq.cmp(&b_seq) {
1001 std::cmp::Ordering::Greater => true,
1002 std::cmp::Ordering::Less => false,
1003 std::cmp::Ordering::Equal => a_sig <= b_sig,
1004 }
1005}
1006
1007mod serde_bytes_array_64 {
1011 use serde::de::Error as _;
1012 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1013
1014 pub fn serialize<S: Serializer>(value: &[u8; 64], ser: S) -> Result<S::Ok, S::Error> {
1015 serde_bytes::Bytes::new(value).serialize(ser)
1016 }
1017
1018 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 64], D::Error> {
1019 let bytes: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(de)?;
1020 bytes
1021 .as_ref()
1022 .try_into()
1023 .map_err(|_| D::Error::custom("expected 64-byte signature"))
1024 }
1025}
1026
1027#[cfg(test)]
1028#[allow(clippy::field_reassign_with_default)]
1029mod tests {
1030 use super::*;
1031
1032 #[test]
1037 fn signature_domain_key_is_wasm_independent() {
1038 let owner = [1u8; 32];
1039 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1040 let k = signature_domain_key(¶ms, &owner);
1041 assert_eq!(k.len(), 32);
1042 assert_eq!(signature_domain_key(¶ms, &owner), k);
1044 }
1045
1046 #[test]
1047 fn default_state_with_matching_prefix_validates() {
1048 let owner = [0u8; 32];
1050 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1051 assert!(validate_state(¶ms, &RepoState::default()).is_ok());
1052 }
1053
1054 #[test]
1055 fn prefix_mismatch_rejected() {
1056 let owner = [3u8; 32];
1057 let other = [4u8; 32];
1058 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1059 let mut state = RepoState::default();
1060 state.owner = other;
1061 match validate_state(¶ms, &state) {
1062 Err(ValidateError::PrefixMismatch { .. }) => {}
1063 other => panic!("expected PrefixMismatch, got {:?}", other),
1064 }
1065 }
1066
1067 #[test]
1068 fn invalid_prefix_length_rejected() {
1069 let too_short = RepoParams {
1070 prefix: "abc".into(),
1071 };
1072 match validate_state(&too_short, &RepoState::default()) {
1073 Err(ValidateError::InvalidPrefixLength { len: 3, .. }) => {}
1074 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1075 }
1076 let too_long = RepoParams {
1077 prefix: "a".repeat(33),
1078 };
1079 match validate_state(&too_long, &RepoState::default()) {
1080 Err(ValidateError::InvalidPrefixLength { len: 33, .. }) => {}
1081 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1082 }
1083 }
1084
1085 #[test]
1086 fn bundle_id_matches_canonical_hash() {
1087 let bundle = ObjectBundle::SinglePack {
1088 pack_hash: [0xAA; 32],
1089 size_bytes: 4096,
1090 };
1091 let id = bundle.id();
1092 assert_eq!(id, bundle.id());
1094 let bundle2 = ObjectBundle::SinglePack {
1096 pack_hash: [0xAA; 32],
1097 size_bytes: 4097,
1098 };
1099 assert_ne!(id, bundle2.id());
1100 }
1101
1102 #[test]
1103 fn merge_picks_higher_seq() {
1104 let mut a: SignedField<String> = SignedField {
1105 value: "a".into(),
1106 update_seq: 1,
1107 signature: [0u8; 64],
1108 };
1109 let b: SignedField<String> = SignedField {
1110 value: "b".into(),
1111 update_seq: 2,
1112 signature: [0u8; 64],
1113 };
1114 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1115 assert_eq!(pick.update_seq, 2);
1116 a.update_seq = 2;
1118 a.signature[0] = 0; let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1120 assert_eq!(pick.value, "a");
1121 a.signature[0] = 1;
1123 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1124 assert_eq!(pick.value, "b");
1125 }
1126
1127 #[test]
1128 fn name_size_limit_is_enforced() {
1129 let owner = [5u8; 32];
1130 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1131 let mut state = RepoState::default();
1132 state.owner = owner;
1133 state.name = Some(SignedField {
1134 value: "x".repeat(limits::MAX_NAME_BYTES + 1),
1135 update_seq: 1,
1136 signature: [0u8; 64],
1139 });
1140 match validate_state(¶ms, &state) {
1141 Err(ValidateError::FieldTooLong { field, .. }) => assert_eq!(field, "name"),
1142 other => panic!("expected FieldTooLong, got {:?}", other),
1143 }
1144 }
1145}