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,
203 pub update_seq: u64,
205 pub updater: PublicKey,
208 pub auth_epoch: u64,
211 #[serde(with = "serde_bytes_array_64")]
213 pub signature: Signature,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218pub enum ObjectBundle {
219 SinglePack {
221 pack_hash: PackHash,
223 size_bytes: u64,
225 },
226 ChunkedPack {
230 manifest_hash: ManifestHash,
232 total_size: u64,
234 chunk_count: u32,
236 },
237}
238
239impl ObjectBundle {
240 pub fn id(&self) -> ObjectBundleId {
246 let value = match self {
247 Self::SinglePack {
248 pack_hash,
249 size_bytes,
250 } => MapBuilder::default()
251 .text_entry("kind", Value::text("single-pack"))
252 .text_entry("pack_hash", Value::bytes(pack_hash.to_vec()))
253 .text_entry("size_bytes", Value::uint(*size_bytes))
254 .build(),
255 Self::ChunkedPack {
256 manifest_hash,
257 total_size,
258 chunk_count,
259 } => MapBuilder::default()
260 .text_entry("kind", Value::text("chunked-pack"))
261 .text_entry("manifest_hash", Value::bytes(manifest_hash.to_vec()))
262 .text_entry("total_size", Value::uint(*total_size))
263 .text_entry("chunk_count", Value::uint(u64::from(*chunk_count)))
264 .build(),
265 };
266 *blake3::hash(&value.encode()).as_bytes()
267 }
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
273pub struct ObjectBundleRecord {
274 pub bundle: ObjectBundle,
277 pub added_by: PublicKey,
280 pub auth_epoch: u64,
282 #[serde(with = "serde_bytes_array_64")]
284 pub signature: Signature,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
289pub struct ExtensionEntry {
290 #[serde(with = "serde_bytes")]
292 pub value: Vec<u8>,
293 pub update_seq: u64,
295 #[serde(with = "serde_bytes_array_64")]
297 pub signature: Signature,
298}
299
300#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
302pub struct RepoState {
303 pub owner: PublicKey,
309 pub name: Option<SignedField<String>>,
311 pub description: Option<SignedField<String>>,
313 pub default_branch: Option<SignedField<RefName>>,
315 pub force_push_allowed: Option<SignedField<Vec<RefName>>>,
318 pub acl: Option<SignedField<AclState>>,
320 pub upgrade: Option<SignedField<Option<RepoKey>>>,
322 pub refs: BTreeMap<RefName, RefEntry>,
324 pub object_index: BTreeMap<ObjectBundleId, ObjectBundleRecord>,
326 pub extensions: BTreeMap<String, ExtensionEntry>,
328}
329
330impl RepoState {
331 pub fn to_bytes(&self) -> Vec<u8> {
333 bincode::serialize(self).expect("RepoState serialization is infallible")
334 }
335
336 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
338 if bytes.is_empty() {
339 return Ok(Self::default());
340 }
341 bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeState(e.to_string()))
342 }
343}
344
345#[derive(Debug, thiserror::Error)]
348pub enum ValidateError {
349 #[error("decode parameters: {0}")]
351 DecodeParams(String),
352 #[error("decode state: {0}")]
354 DecodeState(String),
355 #[error("signature verification failed for {0}")]
357 InvalidSignature(&'static str),
358 #[error("entry signed by non-owner key (Phase 1.0 is single-writer)")]
361 NonOwnerSigner,
362 #[error("field {field} exceeds limit ({len} > {max})")]
364 FieldTooLong {
365 field: &'static str,
367 len: usize,
369 max: usize,
371 },
372 #[error("object_index entry has wrong bundle id")]
374 BundleIdMismatch,
375 #[error("monotonic invariant violated: {0}")]
377 NonMonotonic(&'static str),
378 #[error("prefix length {len} outside valid range [{min}..={max}]")]
380 InvalidPrefixLength {
381 len: usize,
383 min: usize,
385 max: usize,
387 },
388 #[error("owner pubkey does not match parameters prefix: expected {expected}, got {actual}")]
390 PrefixMismatch {
391 expected: String,
393 actual: String,
395 },
396 #[error("prefix contains invalid base58 characters")]
399 InvalidPrefixChars,
400}
401
402pub fn validate_state(params: &RepoParams, state: &RepoState) -> Result<(), ValidateError> {
409 if params.prefix.len() < limits::MIN_PREFIX_LEN || params.prefix.len() > limits::MAX_PREFIX_LEN
411 {
412 return Err(ValidateError::InvalidPrefixLength {
413 len: params.prefix.len(),
414 min: limits::MIN_PREFIX_LEN,
415 max: limits::MAX_PREFIX_LEN,
416 });
417 }
418 if bs58::decode(¶ms.prefix).into_vec().is_err() {
421 return Err(ValidateError::InvalidPrefixChars);
422 }
423 let actual_prefix = pubkey_prefix(&state.owner, params.prefix.len());
425 if actual_prefix != params.prefix {
426 return Err(ValidateError::PrefixMismatch {
427 expected: params.prefix.clone(),
428 actual: actual_prefix,
429 });
430 }
431 let repo_key = params_repo_key(params, &state.owner);
432
433 if let Some(field) = &state.name {
434 check_size("name", field.value.len(), limits::MAX_NAME_BYTES)?;
435 verify_signed_field_string(&repo_key, "name", &state.owner, field, "name")?;
436 }
437 if let Some(field) = &state.description {
438 check_size(
439 "description",
440 field.value.len(),
441 limits::MAX_DESCRIPTION_BYTES,
442 )?;
443 verify_signed_field_string(&repo_key, "description", &state.owner, field, "description")?;
444 }
445 if let Some(field) = &state.default_branch {
446 check_size(
447 "default_branch",
448 field.value.len(),
449 limits::MAX_REF_NAME_BYTES,
450 )?;
451 verify_signed_field_string(
452 &repo_key,
453 "default_branch",
454 &state.owner,
455 field,
456 "default_branch",
457 )?;
458 }
459 if let Some(field) = &state.force_push_allowed {
460 verify_signed_field_ref_list(
461 &repo_key,
462 "force_push_allowed",
463 &state.owner,
464 field,
465 "force_push_allowed",
466 )?;
467 }
468 if let Some(field) = &state.acl {
469 verify_signed_field_acl(&repo_key, "acl", &state.owner, field, "acl")?;
470 }
471 if let Some(field) = &state.upgrade {
472 verify_signed_field_optional_repo_key(
473 &repo_key,
474 "upgrade",
475 &state.owner,
476 field,
477 "upgrade",
478 )?;
479 }
480
481 for (ref_name, entry) in &state.refs {
482 check_size("ref name", ref_name.len(), limits::MAX_REF_NAME_BYTES)?;
483 if entry.updater != state.owner {
485 return Err(ValidateError::NonOwnerSigner);
486 }
487 verify_ref_entry(&repo_key, ref_name, entry)?;
488 }
489
490 for (bundle_id, record) in &state.object_index {
491 if record.bundle.id() != *bundle_id {
492 return Err(ValidateError::BundleIdMismatch);
493 }
494 if record.added_by != state.owner {
495 return Err(ValidateError::NonOwnerSigner);
496 }
497 verify_bundle_record(&repo_key, record)?;
498 }
499
500 for (ext_key, entry) in &state.extensions {
501 check_size(
502 "extension key",
503 ext_key.len(),
504 limits::MAX_EXTENSION_KEY_BYTES,
505 )?;
506 check_size(
507 "extension value",
508 entry.value.len(),
509 limits::MAX_EXTENSION_VALUE_BYTES,
510 )?;
511 verify_extension_entry(&repo_key, ext_key, &state.owner, entry)?;
512 }
513
514 Ok(())
515}
516
517#[derive(Debug, thiserror::Error)]
519pub enum UpdateError {
520 #[error(transparent)]
522 Invalid(#[from] ValidateError),
523 #[error("non-fast-forward update on protected ref")]
526 NonFastForward,
527}
528
529pub fn merge_state(current: &RepoState, incoming: &RepoState) -> RepoState {
535 let mut out = current.clone();
536 out.name = pick_signed_field(out.name, incoming.name.clone());
537 out.description = pick_signed_field(out.description, incoming.description.clone());
538 out.default_branch = pick_signed_field(out.default_branch, incoming.default_branch.clone());
539 out.force_push_allowed =
540 pick_signed_field(out.force_push_allowed, incoming.force_push_allowed.clone());
541 out.acl = pick_signed_field(out.acl, incoming.acl.clone());
542 out.upgrade = pick_signed_field(out.upgrade, incoming.upgrade.clone());
543
544 for (k, v) in &incoming.refs {
545 let pick = match out.refs.remove(k) {
546 None => v.clone(),
547 Some(existing) => pick_ref_entry(existing, v.clone()),
548 };
549 out.refs.insert(k.clone(), pick);
550 }
551 for (k, v) in &incoming.object_index {
552 out.object_index.entry(*k).or_insert_with(|| v.clone());
553 }
554 for (k, v) in &incoming.extensions {
555 let pick = match out.extensions.remove(k) {
556 None => v.clone(),
557 Some(existing) => pick_extension_entry(existing, v.clone()),
558 };
559 out.extensions.insert(k.clone(), pick);
560 }
561 out
562}
563
564pub fn update_state(
569 params: &RepoParams,
570 current: &RepoState,
571 delta: &RepoState,
572) -> Result<RepoState, UpdateError> {
573 let merged = merge_state(current, delta);
578
579 let _ = params;
591
592 validate_state(params, &merged)?;
593 Ok(merged)
594}
595
596#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
598pub struct RepoSummary {
599 pub field_seqs: BTreeMap<String, u64>,
601 pub ref_seqs: BTreeMap<RefName, u64>,
603 pub bundle_ids: Vec<ObjectBundleId>,
605 pub extension_seqs: BTreeMap<String, u64>,
607}
608
609pub fn summarize_state(state: &RepoState) -> RepoSummary {
612 let mut s = RepoSummary::default();
613 if let Some(f) = &state.name {
614 s.field_seqs.insert("name".into(), f.update_seq);
615 }
616 if let Some(f) = &state.description {
617 s.field_seqs.insert("description".into(), f.update_seq);
618 }
619 if let Some(f) = &state.default_branch {
620 s.field_seqs.insert("default_branch".into(), f.update_seq);
621 }
622 if let Some(f) = &state.force_push_allowed {
623 s.field_seqs
624 .insert("force_push_allowed".into(), f.update_seq);
625 }
626 if let Some(f) = &state.acl {
627 s.field_seqs.insert("acl".into(), f.update_seq);
628 }
629 if let Some(f) = &state.upgrade {
630 s.field_seqs.insert("upgrade".into(), f.update_seq);
631 }
632 for (k, v) in &state.refs {
633 s.ref_seqs.insert(k.clone(), v.update_seq);
634 }
635 for k in state.object_index.keys() {
636 s.bundle_ids.push(*k);
637 }
638 for (k, v) in &state.extensions {
639 s.extension_seqs.insert(k.clone(), v.update_seq);
640 }
641 s
642}
643
644pub fn get_state_delta(state: &RepoState, summary: &RepoSummary) -> RepoState {
646 let mut d = RepoState::default();
647
648 if state.name.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("name").copied() {
649 d.name = state.name.clone();
650 }
651 if state.description.as_ref().map(|f| f.update_seq)
652 > summary.field_seqs.get("description").copied()
653 {
654 d.description = state.description.clone();
655 }
656 if state.default_branch.as_ref().map(|f| f.update_seq)
657 > summary.field_seqs.get("default_branch").copied()
658 {
659 d.default_branch = state.default_branch.clone();
660 }
661 if state.force_push_allowed.as_ref().map(|f| f.update_seq)
662 > summary.field_seqs.get("force_push_allowed").copied()
663 {
664 d.force_push_allowed = state.force_push_allowed.clone();
665 }
666 if state.acl.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("acl").copied() {
667 d.acl = state.acl.clone();
668 }
669 if state.upgrade.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("upgrade").copied() {
670 d.upgrade = state.upgrade.clone();
671 }
672
673 for (k, v) in &state.refs {
674 if summary.ref_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
675 d.refs.insert(k.clone(), v.clone());
676 }
677 }
678 let known: std::collections::HashSet<&ObjectBundleId> = summary.bundle_ids.iter().collect();
679 for (k, v) in &state.object_index {
680 if !known.contains(k) {
681 d.object_index.insert(*k, v.clone());
682 }
683 }
684 for (k, v) in &state.extensions {
685 if summary.extension_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
686 d.extensions.insert(k.clone(), v.clone());
687 }
688 }
689 d
690}
691
692pub fn signed_payload_string_field(
699 repo_key: &RepoKey,
700 field_name: &str,
701 value: &str,
702 update_seq: u64,
703) -> Vec<u8> {
704 build_payload(field_name, |b| {
705 b.field_bytes(repo_key);
706 b.field_str(value);
707 b.field_u64(update_seq);
708 })
709}
710
711pub fn signed_payload_ref_list_field(
713 repo_key: &RepoKey,
714 field_name: &str,
715 refs: &[RefName],
716 update_seq: u64,
717) -> Vec<u8> {
718 build_payload(field_name, |b| {
719 b.field_bytes(repo_key);
720 b.field_u32(u32::try_from(refs.len()).unwrap_or(u32::MAX));
721 for r in refs {
722 b.field_str(r);
723 }
724 b.field_u64(update_seq);
725 })
726}
727
728pub fn signed_payload_acl_field(
730 repo_key: &RepoKey,
731 field_name: &str,
732 acl: &AclState,
733 update_seq: u64,
734) -> Vec<u8> {
735 build_payload(field_name, |b| {
736 b.field_bytes(repo_key);
737 b.field_u64(acl.epoch);
738 b.field_u32(u32::try_from(acl.grants.len()).unwrap_or(u32::MAX));
739 for (writer, grant) in &acl.grants {
740 b.field_bytes(writer);
741 b.field_u64(grant.granted_at_epoch);
742 b.field_option_bytes(
743 grant
744 .revoked_at_epoch
745 .map(|e| e.to_le_bytes())
746 .as_ref()
747 .map(|x| x.as_slice()),
748 );
749 }
750 b.field_u64(update_seq);
751 })
752}
753
754pub fn signed_payload_optional_repo_key_field(
756 repo_key: &RepoKey,
757 field_name: &str,
758 successor: Option<&RepoKey>,
759 update_seq: u64,
760) -> Vec<u8> {
761 build_payload(field_name, |b| {
762 b.field_bytes(repo_key);
763 b.field_option_bytes(successor.map(|k| k.as_slice()));
764 b.field_u64(update_seq);
765 })
766}
767
768pub fn signed_payload_ref_entry(
770 repo_key: &RepoKey,
771 ref_name: &str,
772 target: &CommitHash,
773 update_seq: u64,
774 auth_epoch: u64,
775) -> Vec<u8> {
776 build_payload("ref-update", |b| {
777 b.field_bytes(repo_key);
778 b.field_str(ref_name);
779 b.field_bytes(target);
780 b.field_u64(update_seq);
781 b.field_u64(auth_epoch);
782 })
783}
784
785pub fn signed_payload_bundle_record(
787 repo_key: &RepoKey,
788 bundle: &ObjectBundle,
789 auth_epoch: u64,
790) -> Vec<u8> {
791 let bundle_id = bundle.id();
792 build_payload("object-bundle", |b| {
793 b.field_bytes(repo_key);
794 b.field_bytes(&bundle_id);
795 b.field_u64(auth_epoch);
796 })
797}
798
799pub fn signed_payload_extension(
801 repo_key: &RepoKey,
802 ext_key: &str,
803 value: &[u8],
804 update_seq: u64,
805) -> Vec<u8> {
806 build_payload("extension", |b| {
807 b.field_bytes(repo_key);
808 b.field_str(ext_key);
809 b.field_bytes(value);
810 b.field_u64(update_seq);
811 })
812}
813
814fn params_repo_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
828 let mut h = blake3::Hasher::new();
829 h.update(WIRE_VERSION.as_bytes());
830 h.update(params.prefix.as_bytes());
831 h.update(owner);
832 *h.finalize().as_bytes()
833}
834
835pub fn signature_domain_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
838 params_repo_key(params, owner)
839}
840
841fn check_size(field: &'static str, len: usize, max: usize) -> Result<(), ValidateError> {
842 if len > max {
843 Err(ValidateError::FieldTooLong { field, len, max })
844 } else {
845 Ok(())
846 }
847}
848
849fn verify_signature(
850 payload: &[u8],
851 signer: &PublicKey,
852 signature: &Signature,
853 label: &'static str,
854) -> Result<(), ValidateError> {
855 let pk = ed25519_compact::PublicKey::from_slice(signer)
856 .map_err(|_| ValidateError::InvalidSignature(label))?;
857 let sig = ed25519_compact::Signature::from_slice(signature)
858 .map_err(|_| ValidateError::InvalidSignature(label))?;
859 pk.verify(payload, &sig)
860 .map_err(|_| ValidateError::InvalidSignature(label))
861}
862
863fn verify_signed_field_string(
864 repo_key: &RepoKey,
865 field_name: &str,
866 owner: &PublicKey,
867 field: &SignedField<String>,
868 label: &'static str,
869) -> Result<(), ValidateError> {
870 let payload = signed_payload_string_field(repo_key, field_name, &field.value, field.update_seq);
871 verify_signature(&payload, owner, &field.signature, label)
872}
873
874fn verify_signed_field_ref_list(
875 repo_key: &RepoKey,
876 field_name: &str,
877 owner: &PublicKey,
878 field: &SignedField<Vec<RefName>>,
879 label: &'static str,
880) -> Result<(), ValidateError> {
881 let payload =
882 signed_payload_ref_list_field(repo_key, field_name, &field.value, field.update_seq);
883 verify_signature(&payload, owner, &field.signature, label)
884}
885
886fn verify_signed_field_acl(
887 repo_key: &RepoKey,
888 field_name: &str,
889 owner: &PublicKey,
890 field: &SignedField<AclState>,
891 label: &'static str,
892) -> Result<(), ValidateError> {
893 let payload = signed_payload_acl_field(repo_key, field_name, &field.value, field.update_seq);
894 verify_signature(&payload, owner, &field.signature, label)
895}
896
897fn verify_signed_field_optional_repo_key(
898 repo_key: &RepoKey,
899 field_name: &str,
900 owner: &PublicKey,
901 field: &SignedField<Option<RepoKey>>,
902 label: &'static str,
903) -> Result<(), ValidateError> {
904 let payload = signed_payload_optional_repo_key_field(
905 repo_key,
906 field_name,
907 field.value.as_ref(),
908 field.update_seq,
909 );
910 verify_signature(&payload, owner, &field.signature, label)
911}
912
913fn verify_ref_entry(
914 repo_key: &RepoKey,
915 ref_name: &str,
916 entry: &RefEntry,
917) -> Result<(), ValidateError> {
918 let payload = signed_payload_ref_entry(
919 repo_key,
920 ref_name,
921 &entry.target,
922 entry.update_seq,
923 entry.auth_epoch,
924 );
925 verify_signature(&payload, &entry.updater, &entry.signature, "ref entry")
926}
927
928fn verify_bundle_record(
929 repo_key: &RepoKey,
930 record: &ObjectBundleRecord,
931) -> Result<(), ValidateError> {
932 let payload = signed_payload_bundle_record(repo_key, &record.bundle, record.auth_epoch);
933 verify_signature(
934 &payload,
935 &record.added_by,
936 &record.signature,
937 "bundle record",
938 )
939}
940
941fn verify_extension_entry(
942 repo_key: &RepoKey,
943 ext_key: &str,
944 owner: &PublicKey,
945 entry: &ExtensionEntry,
946) -> Result<(), ValidateError> {
947 let payload = signed_payload_extension(repo_key, ext_key, &entry.value, entry.update_seq);
948 verify_signature(&payload, owner, &entry.signature, "extension entry")
949}
950
951fn pick_signed_field<T: Clone>(
952 a: Option<SignedField<T>>,
953 b: Option<SignedField<T>>,
954) -> Option<SignedField<T>> {
955 match (a, b) {
956 (None, x) | (x, None) => x,
957 (Some(x), Some(y)) => Some(
958 if pick_higher_seq(x.update_seq, &x.signature, y.update_seq, &y.signature) {
959 x
960 } else {
961 y
962 },
963 ),
964 }
965}
966
967fn pick_ref_entry(a: RefEntry, b: RefEntry) -> RefEntry {
968 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
969 a
970 } else {
971 b
972 }
973}
974
975fn pick_extension_entry(a: ExtensionEntry, b: ExtensionEntry) -> ExtensionEntry {
976 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
977 a
978 } else {
979 b
980 }
981}
982
983fn pick_higher_seq(a_seq: u64, a_sig: &Signature, b_seq: u64, b_sig: &Signature) -> bool {
987 match a_seq.cmp(&b_seq) {
988 std::cmp::Ordering::Greater => true,
989 std::cmp::Ordering::Less => false,
990 std::cmp::Ordering::Equal => a_sig <= b_sig,
991 }
992}
993
994mod serde_bytes_array_64 {
998 use serde::de::Error as _;
999 use serde::{Deserialize, Deserializer, Serialize, Serializer};
1000
1001 pub fn serialize<S: Serializer>(value: &[u8; 64], ser: S) -> Result<S::Ok, S::Error> {
1002 serde_bytes::Bytes::new(value).serialize(ser)
1003 }
1004
1005 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 64], D::Error> {
1006 let bytes: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(de)?;
1007 bytes
1008 .as_ref()
1009 .try_into()
1010 .map_err(|_| D::Error::custom("expected 64-byte signature"))
1011 }
1012}
1013
1014#[cfg(test)]
1015#[allow(clippy::field_reassign_with_default)]
1016mod tests {
1017 use super::*;
1018
1019 #[test]
1024 fn signature_domain_key_is_wasm_independent() {
1025 let owner = [1u8; 32];
1026 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1027 let k = signature_domain_key(¶ms, &owner);
1028 assert_eq!(k.len(), 32);
1029 assert_eq!(signature_domain_key(¶ms, &owner), k);
1031 }
1032
1033 #[test]
1034 fn default_state_with_matching_prefix_validates() {
1035 let owner = [0u8; 32];
1037 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1038 assert!(validate_state(¶ms, &RepoState::default()).is_ok());
1039 }
1040
1041 #[test]
1042 fn prefix_mismatch_rejected() {
1043 let owner = [3u8; 32];
1044 let other = [4u8; 32];
1045 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1046 let mut state = RepoState::default();
1047 state.owner = other;
1048 match validate_state(¶ms, &state) {
1049 Err(ValidateError::PrefixMismatch { .. }) => {}
1050 other => panic!("expected PrefixMismatch, got {:?}", other),
1051 }
1052 }
1053
1054 #[test]
1055 fn invalid_prefix_length_rejected() {
1056 let too_short = RepoParams {
1057 prefix: "abc".into(),
1058 };
1059 match validate_state(&too_short, &RepoState::default()) {
1060 Err(ValidateError::InvalidPrefixLength { len: 3, .. }) => {}
1061 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1062 }
1063 let too_long = RepoParams {
1064 prefix: "a".repeat(33),
1065 };
1066 match validate_state(&too_long, &RepoState::default()) {
1067 Err(ValidateError::InvalidPrefixLength { len: 33, .. }) => {}
1068 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1069 }
1070 }
1071
1072 #[test]
1073 fn bundle_id_matches_canonical_hash() {
1074 let bundle = ObjectBundle::SinglePack {
1075 pack_hash: [0xAA; 32],
1076 size_bytes: 4096,
1077 };
1078 let id = bundle.id();
1079 assert_eq!(id, bundle.id());
1081 let bundle2 = ObjectBundle::SinglePack {
1083 pack_hash: [0xAA; 32],
1084 size_bytes: 4097,
1085 };
1086 assert_ne!(id, bundle2.id());
1087 }
1088
1089 #[test]
1090 fn merge_picks_higher_seq() {
1091 let mut a: SignedField<String> = SignedField {
1092 value: "a".into(),
1093 update_seq: 1,
1094 signature: [0u8; 64],
1095 };
1096 let b: SignedField<String> = SignedField {
1097 value: "b".into(),
1098 update_seq: 2,
1099 signature: [0u8; 64],
1100 };
1101 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1102 assert_eq!(pick.update_seq, 2);
1103 a.update_seq = 2;
1105 a.signature[0] = 0; let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1107 assert_eq!(pick.value, "a");
1108 a.signature[0] = 1;
1110 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1111 assert_eq!(pick.value, "b");
1112 }
1113
1114 #[test]
1115 fn name_size_limit_is_enforced() {
1116 let owner = [5u8; 32];
1117 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1118 let mut state = RepoState::default();
1119 state.owner = owner;
1120 state.name = Some(SignedField {
1121 value: "x".repeat(limits::MAX_NAME_BYTES + 1),
1122 update_seq: 1,
1123 signature: [0u8; 64],
1126 });
1127 match validate_state(¶ms, &state) {
1128 Err(ValidateError::FieldTooLong { field, .. }) => assert_eq!(field, "name"),
1129 other => panic!("expected FieldTooLong, got {:?}", other),
1130 }
1131 }
1132}