1#![deny(unsafe_code)]
19#![warn(missing_docs)]
20
21#[cfg(feature = "signing")]
22pub mod signing;
23
24use std::collections::BTreeMap;
25
26use freenet_git_encoding::canonical::{MapBuilder, Value};
27use freenet_git_encoding::signed::build as build_payload;
28use freenet_git_encoding::WIRE_VERSION;
29use serde::{Deserialize, Serialize};
30
31pub mod limits {
36 pub const MAX_NAME_BYTES: usize = 256;
39 pub const MAX_DESCRIPTION_BYTES: usize = 4096;
42 pub const MAX_REF_NAME_BYTES: usize = 256;
44 pub const MAX_EXTENSION_KEY_BYTES: usize = 256;
46 pub const MAX_EXTENSION_VALUE_BYTES: usize = 64 * 1024;
48 pub const MIN_PREFIX_LEN: usize = 4;
51 pub const MAX_PREFIX_LEN: usize = 32;
56 pub const DEFAULT_PREFIX_LEN: usize = 12;
64}
65
66pub type PublicKey = [u8; 32];
69
70pub type Signature = [u8; 64];
72
73pub type PackHash = [u8; 32];
75
76pub type ManifestHash = [u8; 32];
78
79pub type CommitHash = [u8; 20];
83
84pub type ObjectBundleId = [u8; 32];
86
87pub type RepoKey = [u8; 32];
92
93pub type RefName = String;
106
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123pub struct RepoParams {
124 pub prefix: String,
127}
128
129impl RepoParams {
130 pub fn from_owner(owner: &PublicKey, len: usize) -> Self {
134 Self {
135 prefix: pubkey_prefix(owner, len),
136 }
137 }
138
139 pub fn to_bytes(&self) -> Vec<u8> {
141 bincode::serialize(self).expect("RepoParams serialization is infallible")
142 }
143
144 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
146 bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeParams(e.to_string()))
147 }
148}
149
150pub fn pubkey_prefix(owner: &PublicKey, len: usize) -> String {
154 let encoded = bs58::encode(owner).into_string();
155 let take = len.min(encoded.len());
156 encoded[..take].to_string()
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
162pub struct SignedField<T> {
163 pub value: T,
165 pub update_seq: u64,
168 #[serde(with = "serde_bytes_array_64")]
170 pub signature: Signature,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176pub struct WriterGrant {
177 pub granted_at_epoch: u64,
179 pub revoked_at_epoch: Option<u64>,
181}
182
183#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
187pub struct AclState {
188 pub epoch: u64,
190 pub grants: BTreeMap<PublicKey, WriterGrant>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
198pub struct RefEntry {
199 pub target: CommitHash,
201 pub update_seq: u64,
203 pub updater: PublicKey,
206 pub auth_epoch: u64,
209 #[serde(with = "serde_bytes_array_64")]
211 pub signature: Signature,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
216pub enum ObjectBundle {
217 SinglePack {
219 pack_hash: PackHash,
221 size_bytes: u64,
223 },
224 ChunkedPack {
228 manifest_hash: ManifestHash,
230 total_size: u64,
232 chunk_count: u32,
234 },
235}
236
237impl ObjectBundle {
238 pub fn id(&self) -> ObjectBundleId {
244 let value = match self {
245 Self::SinglePack {
246 pack_hash,
247 size_bytes,
248 } => MapBuilder::default()
249 .text_entry("kind", Value::text("single-pack"))
250 .text_entry("pack_hash", Value::bytes(pack_hash.to_vec()))
251 .text_entry("size_bytes", Value::uint(*size_bytes))
252 .build(),
253 Self::ChunkedPack {
254 manifest_hash,
255 total_size,
256 chunk_count,
257 } => MapBuilder::default()
258 .text_entry("kind", Value::text("chunked-pack"))
259 .text_entry("manifest_hash", Value::bytes(manifest_hash.to_vec()))
260 .text_entry("total_size", Value::uint(*total_size))
261 .text_entry("chunk_count", Value::uint(u64::from(*chunk_count)))
262 .build(),
263 };
264 *blake3::hash(&value.encode()).as_bytes()
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
271pub struct ObjectBundleRecord {
272 pub bundle: ObjectBundle,
275 pub added_by: PublicKey,
278 pub auth_epoch: u64,
280 #[serde(with = "serde_bytes_array_64")]
282 pub signature: Signature,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
287pub struct ExtensionEntry {
288 #[serde(with = "serde_bytes")]
290 pub value: Vec<u8>,
291 pub update_seq: u64,
293 #[serde(with = "serde_bytes_array_64")]
295 pub signature: Signature,
296}
297
298#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
300pub struct RepoState {
301 pub owner: PublicKey,
307 pub name: Option<SignedField<String>>,
309 pub description: Option<SignedField<String>>,
311 pub default_branch: Option<SignedField<RefName>>,
313 pub force_push_allowed: Option<SignedField<Vec<RefName>>>,
316 pub acl: Option<SignedField<AclState>>,
318 pub upgrade: Option<SignedField<Option<RepoKey>>>,
320 pub refs: BTreeMap<RefName, RefEntry>,
322 pub object_index: BTreeMap<ObjectBundleId, ObjectBundleRecord>,
324 pub extensions: BTreeMap<String, ExtensionEntry>,
326}
327
328impl RepoState {
329 pub fn to_bytes(&self) -> Vec<u8> {
331 bincode::serialize(self).expect("RepoState serialization is infallible")
332 }
333
334 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
336 if bytes.is_empty() {
337 return Ok(Self::default());
338 }
339 bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeState(e.to_string()))
340 }
341}
342
343#[derive(Debug, thiserror::Error)]
346pub enum ValidateError {
347 #[error("decode parameters: {0}")]
349 DecodeParams(String),
350 #[error("decode state: {0}")]
352 DecodeState(String),
353 #[error("signature verification failed for {0}")]
355 InvalidSignature(&'static str),
356 #[error("entry signed by non-owner key (Phase 1.0 is single-writer)")]
359 NonOwnerSigner,
360 #[error("field {field} exceeds limit ({len} > {max})")]
362 FieldTooLong {
363 field: &'static str,
365 len: usize,
367 max: usize,
369 },
370 #[error("object_index entry has wrong bundle id")]
372 BundleIdMismatch,
373 #[error("monotonic invariant violated: {0}")]
375 NonMonotonic(&'static str),
376 #[error("prefix length {len} outside valid range [{min}..={max}]")]
378 InvalidPrefixLength {
379 len: usize,
381 min: usize,
383 max: usize,
385 },
386 #[error("owner pubkey does not match parameters prefix: expected {expected}, got {actual}")]
388 PrefixMismatch {
389 expected: String,
391 actual: String,
393 },
394 #[error("prefix contains invalid base58 characters")]
397 InvalidPrefixChars,
398}
399
400pub fn validate_state(params: &RepoParams, state: &RepoState) -> Result<(), ValidateError> {
407 if params.prefix.len() < limits::MIN_PREFIX_LEN || params.prefix.len() > limits::MAX_PREFIX_LEN
409 {
410 return Err(ValidateError::InvalidPrefixLength {
411 len: params.prefix.len(),
412 min: limits::MIN_PREFIX_LEN,
413 max: limits::MAX_PREFIX_LEN,
414 });
415 }
416 if bs58::decode(¶ms.prefix).into_vec().is_err() {
419 return Err(ValidateError::InvalidPrefixChars);
420 }
421 let actual_prefix = pubkey_prefix(&state.owner, params.prefix.len());
423 if actual_prefix != params.prefix {
424 return Err(ValidateError::PrefixMismatch {
425 expected: params.prefix.clone(),
426 actual: actual_prefix,
427 });
428 }
429 let repo_key = params_repo_key(params, &state.owner);
430
431 if let Some(field) = &state.name {
432 check_size("name", field.value.len(), limits::MAX_NAME_BYTES)?;
433 verify_signed_field_string(&repo_key, "name", &state.owner, field, "name")?;
434 }
435 if let Some(field) = &state.description {
436 check_size(
437 "description",
438 field.value.len(),
439 limits::MAX_DESCRIPTION_BYTES,
440 )?;
441 verify_signed_field_string(&repo_key, "description", &state.owner, field, "description")?;
442 }
443 if let Some(field) = &state.default_branch {
444 check_size(
445 "default_branch",
446 field.value.len(),
447 limits::MAX_REF_NAME_BYTES,
448 )?;
449 verify_signed_field_string(
450 &repo_key,
451 "default_branch",
452 &state.owner,
453 field,
454 "default_branch",
455 )?;
456 }
457 if let Some(field) = &state.force_push_allowed {
458 verify_signed_field_ref_list(
459 &repo_key,
460 "force_push_allowed",
461 &state.owner,
462 field,
463 "force_push_allowed",
464 )?;
465 }
466 if let Some(field) = &state.acl {
467 verify_signed_field_acl(&repo_key, "acl", &state.owner, field, "acl")?;
468 }
469 if let Some(field) = &state.upgrade {
470 verify_signed_field_optional_repo_key(
471 &repo_key,
472 "upgrade",
473 &state.owner,
474 field,
475 "upgrade",
476 )?;
477 }
478
479 for (ref_name, entry) in &state.refs {
480 check_size("ref name", ref_name.len(), limits::MAX_REF_NAME_BYTES)?;
481 if entry.updater != state.owner {
483 return Err(ValidateError::NonOwnerSigner);
484 }
485 verify_ref_entry(&repo_key, ref_name, entry)?;
486 }
487
488 for (bundle_id, record) in &state.object_index {
489 if record.bundle.id() != *bundle_id {
490 return Err(ValidateError::BundleIdMismatch);
491 }
492 if record.added_by != state.owner {
493 return Err(ValidateError::NonOwnerSigner);
494 }
495 verify_bundle_record(&repo_key, record)?;
496 }
497
498 for (ext_key, entry) in &state.extensions {
499 check_size(
500 "extension key",
501 ext_key.len(),
502 limits::MAX_EXTENSION_KEY_BYTES,
503 )?;
504 check_size(
505 "extension value",
506 entry.value.len(),
507 limits::MAX_EXTENSION_VALUE_BYTES,
508 )?;
509 verify_extension_entry(&repo_key, ext_key, &state.owner, entry)?;
510 }
511
512 Ok(())
513}
514
515#[derive(Debug, thiserror::Error)]
517pub enum UpdateError {
518 #[error(transparent)]
520 Invalid(#[from] ValidateError),
521 #[error("non-fast-forward update on protected ref")]
524 NonFastForward,
525}
526
527pub fn merge_state(current: &RepoState, incoming: &RepoState) -> RepoState {
533 let mut out = current.clone();
534 out.name = pick_signed_field(out.name, incoming.name.clone());
535 out.description = pick_signed_field(out.description, incoming.description.clone());
536 out.default_branch = pick_signed_field(out.default_branch, incoming.default_branch.clone());
537 out.force_push_allowed =
538 pick_signed_field(out.force_push_allowed, incoming.force_push_allowed.clone());
539 out.acl = pick_signed_field(out.acl, incoming.acl.clone());
540 out.upgrade = pick_signed_field(out.upgrade, incoming.upgrade.clone());
541
542 for (k, v) in &incoming.refs {
543 let pick = match out.refs.remove(k) {
544 None => v.clone(),
545 Some(existing) => pick_ref_entry(existing, v.clone()),
546 };
547 out.refs.insert(k.clone(), pick);
548 }
549 for (k, v) in &incoming.object_index {
550 out.object_index.entry(*k).or_insert_with(|| v.clone());
551 }
552 for (k, v) in &incoming.extensions {
553 let pick = match out.extensions.remove(k) {
554 None => v.clone(),
555 Some(existing) => pick_extension_entry(existing, v.clone()),
556 };
557 out.extensions.insert(k.clone(), pick);
558 }
559 out
560}
561
562pub fn update_state(
567 params: &RepoParams,
568 current: &RepoState,
569 delta: &RepoState,
570) -> Result<RepoState, UpdateError> {
571 let merged = merge_state(current, delta);
576
577 let _ = params;
589
590 validate_state(params, &merged)?;
591 Ok(merged)
592}
593
594#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
596pub struct RepoSummary {
597 pub field_seqs: BTreeMap<String, u64>,
599 pub ref_seqs: BTreeMap<RefName, u64>,
601 pub bundle_ids: Vec<ObjectBundleId>,
603 pub extension_seqs: BTreeMap<String, u64>,
605}
606
607pub fn summarize_state(state: &RepoState) -> RepoSummary {
610 let mut s = RepoSummary::default();
611 if let Some(f) = &state.name {
612 s.field_seqs.insert("name".into(), f.update_seq);
613 }
614 if let Some(f) = &state.description {
615 s.field_seqs.insert("description".into(), f.update_seq);
616 }
617 if let Some(f) = &state.default_branch {
618 s.field_seqs.insert("default_branch".into(), f.update_seq);
619 }
620 if let Some(f) = &state.force_push_allowed {
621 s.field_seqs
622 .insert("force_push_allowed".into(), f.update_seq);
623 }
624 if let Some(f) = &state.acl {
625 s.field_seqs.insert("acl".into(), f.update_seq);
626 }
627 if let Some(f) = &state.upgrade {
628 s.field_seqs.insert("upgrade".into(), f.update_seq);
629 }
630 for (k, v) in &state.refs {
631 s.ref_seqs.insert(k.clone(), v.update_seq);
632 }
633 for k in state.object_index.keys() {
634 s.bundle_ids.push(*k);
635 }
636 for (k, v) in &state.extensions {
637 s.extension_seqs.insert(k.clone(), v.update_seq);
638 }
639 s
640}
641
642pub fn get_state_delta(state: &RepoState, summary: &RepoSummary) -> RepoState {
644 let mut d = RepoState::default();
645
646 if state.name.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("name").copied() {
647 d.name = state.name.clone();
648 }
649 if state.description.as_ref().map(|f| f.update_seq)
650 > summary.field_seqs.get("description").copied()
651 {
652 d.description = state.description.clone();
653 }
654 if state.default_branch.as_ref().map(|f| f.update_seq)
655 > summary.field_seqs.get("default_branch").copied()
656 {
657 d.default_branch = state.default_branch.clone();
658 }
659 if state.force_push_allowed.as_ref().map(|f| f.update_seq)
660 > summary.field_seqs.get("force_push_allowed").copied()
661 {
662 d.force_push_allowed = state.force_push_allowed.clone();
663 }
664 if state.acl.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("acl").copied() {
665 d.acl = state.acl.clone();
666 }
667 if state.upgrade.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("upgrade").copied() {
668 d.upgrade = state.upgrade.clone();
669 }
670
671 for (k, v) in &state.refs {
672 if summary.ref_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
673 d.refs.insert(k.clone(), v.clone());
674 }
675 }
676 let known: std::collections::HashSet<&ObjectBundleId> = summary.bundle_ids.iter().collect();
677 for (k, v) in &state.object_index {
678 if !known.contains(k) {
679 d.object_index.insert(*k, v.clone());
680 }
681 }
682 for (k, v) in &state.extensions {
683 if summary.extension_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
684 d.extensions.insert(k.clone(), v.clone());
685 }
686 }
687 d
688}
689
690pub fn signed_payload_string_field(
697 repo_key: &RepoKey,
698 field_name: &str,
699 value: &str,
700 update_seq: u64,
701) -> Vec<u8> {
702 build_payload(field_name, |b| {
703 b.field_bytes(repo_key);
704 b.field_str(value);
705 b.field_u64(update_seq);
706 })
707}
708
709pub fn signed_payload_ref_list_field(
711 repo_key: &RepoKey,
712 field_name: &str,
713 refs: &[RefName],
714 update_seq: u64,
715) -> Vec<u8> {
716 build_payload(field_name, |b| {
717 b.field_bytes(repo_key);
718 b.field_u32(u32::try_from(refs.len()).unwrap_or(u32::MAX));
719 for r in refs {
720 b.field_str(r);
721 }
722 b.field_u64(update_seq);
723 })
724}
725
726pub fn signed_payload_acl_field(
728 repo_key: &RepoKey,
729 field_name: &str,
730 acl: &AclState,
731 update_seq: u64,
732) -> Vec<u8> {
733 build_payload(field_name, |b| {
734 b.field_bytes(repo_key);
735 b.field_u64(acl.epoch);
736 b.field_u32(u32::try_from(acl.grants.len()).unwrap_or(u32::MAX));
737 for (writer, grant) in &acl.grants {
738 b.field_bytes(writer);
739 b.field_u64(grant.granted_at_epoch);
740 b.field_option_bytes(
741 grant
742 .revoked_at_epoch
743 .map(|e| e.to_le_bytes())
744 .as_ref()
745 .map(|x| x.as_slice()),
746 );
747 }
748 b.field_u64(update_seq);
749 })
750}
751
752pub fn signed_payload_optional_repo_key_field(
754 repo_key: &RepoKey,
755 field_name: &str,
756 successor: Option<&RepoKey>,
757 update_seq: u64,
758) -> Vec<u8> {
759 build_payload(field_name, |b| {
760 b.field_bytes(repo_key);
761 b.field_option_bytes(successor.map(|k| k.as_slice()));
762 b.field_u64(update_seq);
763 })
764}
765
766pub fn signed_payload_ref_entry(
768 repo_key: &RepoKey,
769 ref_name: &str,
770 target: &CommitHash,
771 update_seq: u64,
772 auth_epoch: u64,
773) -> Vec<u8> {
774 build_payload("ref-update", |b| {
775 b.field_bytes(repo_key);
776 b.field_str(ref_name);
777 b.field_bytes(target);
778 b.field_u64(update_seq);
779 b.field_u64(auth_epoch);
780 })
781}
782
783pub fn signed_payload_bundle_record(
785 repo_key: &RepoKey,
786 bundle: &ObjectBundle,
787 auth_epoch: u64,
788) -> Vec<u8> {
789 let bundle_id = bundle.id();
790 build_payload("object-bundle", |b| {
791 b.field_bytes(repo_key);
792 b.field_bytes(&bundle_id);
793 b.field_u64(auth_epoch);
794 })
795}
796
797pub fn signed_payload_extension(
799 repo_key: &RepoKey,
800 ext_key: &str,
801 value: &[u8],
802 update_seq: u64,
803) -> Vec<u8> {
804 build_payload("extension", |b| {
805 b.field_bytes(repo_key);
806 b.field_str(ext_key);
807 b.field_bytes(value);
808 b.field_u64(update_seq);
809 })
810}
811
812fn params_repo_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
826 let mut h = blake3::Hasher::new();
827 h.update(WIRE_VERSION.as_bytes());
828 h.update(params.prefix.as_bytes());
829 h.update(owner);
830 *h.finalize().as_bytes()
831}
832
833pub fn signature_domain_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
836 params_repo_key(params, owner)
837}
838
839fn check_size(field: &'static str, len: usize, max: usize) -> Result<(), ValidateError> {
840 if len > max {
841 Err(ValidateError::FieldTooLong { field, len, max })
842 } else {
843 Ok(())
844 }
845}
846
847fn verify_signature(
848 payload: &[u8],
849 signer: &PublicKey,
850 signature: &Signature,
851 label: &'static str,
852) -> Result<(), ValidateError> {
853 let pk = ed25519_compact::PublicKey::from_slice(signer)
854 .map_err(|_| ValidateError::InvalidSignature(label))?;
855 let sig = ed25519_compact::Signature::from_slice(signature)
856 .map_err(|_| ValidateError::InvalidSignature(label))?;
857 pk.verify(payload, &sig)
858 .map_err(|_| ValidateError::InvalidSignature(label))
859}
860
861fn verify_signed_field_string(
862 repo_key: &RepoKey,
863 field_name: &str,
864 owner: &PublicKey,
865 field: &SignedField<String>,
866 label: &'static str,
867) -> Result<(), ValidateError> {
868 let payload = signed_payload_string_field(repo_key, field_name, &field.value, field.update_seq);
869 verify_signature(&payload, owner, &field.signature, label)
870}
871
872fn verify_signed_field_ref_list(
873 repo_key: &RepoKey,
874 field_name: &str,
875 owner: &PublicKey,
876 field: &SignedField<Vec<RefName>>,
877 label: &'static str,
878) -> Result<(), ValidateError> {
879 let payload =
880 signed_payload_ref_list_field(repo_key, field_name, &field.value, field.update_seq);
881 verify_signature(&payload, owner, &field.signature, label)
882}
883
884fn verify_signed_field_acl(
885 repo_key: &RepoKey,
886 field_name: &str,
887 owner: &PublicKey,
888 field: &SignedField<AclState>,
889 label: &'static str,
890) -> Result<(), ValidateError> {
891 let payload = signed_payload_acl_field(repo_key, field_name, &field.value, field.update_seq);
892 verify_signature(&payload, owner, &field.signature, label)
893}
894
895fn verify_signed_field_optional_repo_key(
896 repo_key: &RepoKey,
897 field_name: &str,
898 owner: &PublicKey,
899 field: &SignedField<Option<RepoKey>>,
900 label: &'static str,
901) -> Result<(), ValidateError> {
902 let payload = signed_payload_optional_repo_key_field(
903 repo_key,
904 field_name,
905 field.value.as_ref(),
906 field.update_seq,
907 );
908 verify_signature(&payload, owner, &field.signature, label)
909}
910
911fn verify_ref_entry(
912 repo_key: &RepoKey,
913 ref_name: &str,
914 entry: &RefEntry,
915) -> Result<(), ValidateError> {
916 let payload = signed_payload_ref_entry(
917 repo_key,
918 ref_name,
919 &entry.target,
920 entry.update_seq,
921 entry.auth_epoch,
922 );
923 verify_signature(&payload, &entry.updater, &entry.signature, "ref entry")
924}
925
926fn verify_bundle_record(
927 repo_key: &RepoKey,
928 record: &ObjectBundleRecord,
929) -> Result<(), ValidateError> {
930 let payload = signed_payload_bundle_record(repo_key, &record.bundle, record.auth_epoch);
931 verify_signature(
932 &payload,
933 &record.added_by,
934 &record.signature,
935 "bundle record",
936 )
937}
938
939fn verify_extension_entry(
940 repo_key: &RepoKey,
941 ext_key: &str,
942 owner: &PublicKey,
943 entry: &ExtensionEntry,
944) -> Result<(), ValidateError> {
945 let payload = signed_payload_extension(repo_key, ext_key, &entry.value, entry.update_seq);
946 verify_signature(&payload, owner, &entry.signature, "extension entry")
947}
948
949fn pick_signed_field<T: Clone>(
950 a: Option<SignedField<T>>,
951 b: Option<SignedField<T>>,
952) -> Option<SignedField<T>> {
953 match (a, b) {
954 (None, x) | (x, None) => x,
955 (Some(x), Some(y)) => Some(
956 if pick_higher_seq(x.update_seq, &x.signature, y.update_seq, &y.signature) {
957 x
958 } else {
959 y
960 },
961 ),
962 }
963}
964
965fn pick_ref_entry(a: RefEntry, b: RefEntry) -> RefEntry {
966 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
967 a
968 } else {
969 b
970 }
971}
972
973fn pick_extension_entry(a: ExtensionEntry, b: ExtensionEntry) -> ExtensionEntry {
974 if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
975 a
976 } else {
977 b
978 }
979}
980
981fn pick_higher_seq(a_seq: u64, a_sig: &Signature, b_seq: u64, b_sig: &Signature) -> bool {
985 match a_seq.cmp(&b_seq) {
986 std::cmp::Ordering::Greater => true,
987 std::cmp::Ordering::Less => false,
988 std::cmp::Ordering::Equal => a_sig <= b_sig,
989 }
990}
991
992mod serde_bytes_array_64 {
996 use serde::de::Error as _;
997 use serde::{Deserialize, Deserializer, Serialize, Serializer};
998
999 pub fn serialize<S: Serializer>(value: &[u8; 64], ser: S) -> Result<S::Ok, S::Error> {
1000 serde_bytes::Bytes::new(value).serialize(ser)
1001 }
1002
1003 pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 64], D::Error> {
1004 let bytes: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(de)?;
1005 bytes
1006 .as_ref()
1007 .try_into()
1008 .map_err(|_| D::Error::custom("expected 64-byte signature"))
1009 }
1010}
1011
1012#[cfg(test)]
1013#[allow(clippy::field_reassign_with_default)]
1014mod tests {
1015 use super::*;
1016
1017 #[test]
1022 fn signature_domain_key_is_wasm_independent() {
1023 let owner = [1u8; 32];
1024 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1025 let k = signature_domain_key(¶ms, &owner);
1026 assert_eq!(k.len(), 32);
1027 assert_eq!(signature_domain_key(¶ms, &owner), k);
1029 }
1030
1031 #[test]
1032 fn default_state_with_matching_prefix_validates() {
1033 let owner = [0u8; 32];
1035 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1036 assert!(validate_state(¶ms, &RepoState::default()).is_ok());
1037 }
1038
1039 #[test]
1040 fn prefix_mismatch_rejected() {
1041 let owner = [3u8; 32];
1042 let other = [4u8; 32];
1043 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1044 let mut state = RepoState::default();
1045 state.owner = other;
1046 match validate_state(¶ms, &state) {
1047 Err(ValidateError::PrefixMismatch { .. }) => {}
1048 other => panic!("expected PrefixMismatch, got {:?}", other),
1049 }
1050 }
1051
1052 #[test]
1053 fn invalid_prefix_length_rejected() {
1054 let too_short = RepoParams {
1055 prefix: "abc".into(),
1056 };
1057 match validate_state(&too_short, &RepoState::default()) {
1058 Err(ValidateError::InvalidPrefixLength { len: 3, .. }) => {}
1059 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1060 }
1061 let too_long = RepoParams {
1062 prefix: "a".repeat(33),
1063 };
1064 match validate_state(&too_long, &RepoState::default()) {
1065 Err(ValidateError::InvalidPrefixLength { len: 33, .. }) => {}
1066 other => panic!("expected InvalidPrefixLength, got {:?}", other),
1067 }
1068 }
1069
1070 #[test]
1071 fn bundle_id_matches_canonical_hash() {
1072 let bundle = ObjectBundle::SinglePack {
1073 pack_hash: [0xAA; 32],
1074 size_bytes: 4096,
1075 };
1076 let id = bundle.id();
1077 assert_eq!(id, bundle.id());
1079 let bundle2 = ObjectBundle::SinglePack {
1081 pack_hash: [0xAA; 32],
1082 size_bytes: 4097,
1083 };
1084 assert_ne!(id, bundle2.id());
1085 }
1086
1087 #[test]
1088 fn merge_picks_higher_seq() {
1089 let mut a: SignedField<String> = SignedField {
1090 value: "a".into(),
1091 update_seq: 1,
1092 signature: [0u8; 64],
1093 };
1094 let b: SignedField<String> = SignedField {
1095 value: "b".into(),
1096 update_seq: 2,
1097 signature: [0u8; 64],
1098 };
1099 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1100 assert_eq!(pick.update_seq, 2);
1101 a.update_seq = 2;
1103 a.signature[0] = 0; let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1105 assert_eq!(pick.value, "a");
1106 a.signature[0] = 1;
1108 let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
1109 assert_eq!(pick.value, "b");
1110 }
1111
1112 #[test]
1113 fn name_size_limit_is_enforced() {
1114 let owner = [5u8; 32];
1115 let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
1116 let mut state = RepoState::default();
1117 state.owner = owner;
1118 state.name = Some(SignedField {
1119 value: "x".repeat(limits::MAX_NAME_BYTES + 1),
1120 update_seq: 1,
1121 signature: [0u8; 64],
1124 });
1125 match validate_state(¶ms, &state) {
1126 Err(ValidateError::FieldTooLong { field, .. }) => assert_eq!(field, "name"),
1127 other => panic!("expected FieldTooLong, got {:?}", other),
1128 }
1129 }
1130}