#![deny(unsafe_code)]
#![warn(missing_docs)]
#[cfg(feature = "signing")]
pub mod signing;
pub mod chunked;
use std::collections::BTreeMap;
use freenet_git_encoding::canonical::{MapBuilder, Value};
use freenet_git_encoding::signed::build as build_payload;
use freenet_git_encoding::WIRE_VERSION;
use serde::{Deserialize, Serialize};
pub mod limits {
pub const MAX_NAME_BYTES: usize = 256;
pub const MAX_DESCRIPTION_BYTES: usize = 4096;
pub const MAX_REF_NAME_BYTES: usize = 256;
pub const MAX_EXTENSION_KEY_BYTES: usize = 256;
pub const MAX_EXTENSION_VALUE_BYTES: usize = 64 * 1024;
pub const MIN_PREFIX_LEN: usize = 4;
pub const MAX_PREFIX_LEN: usize = 32;
pub const DEFAULT_PREFIX_LEN: usize = 12;
}
pub type PublicKey = [u8; 32];
pub type Signature = [u8; 64];
pub type PackHash = [u8; 32];
pub type ManifestHash = [u8; 32];
pub type CommitHash = [u8; 20];
pub type ObjectBundleId = [u8; 32];
pub type RepoKey = [u8; 32];
pub type RefName = String;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepoParams {
pub prefix: String,
}
impl RepoParams {
pub fn from_owner(owner: &PublicKey, len: usize) -> Self {
Self {
prefix: pubkey_prefix(owner, len),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).expect("RepoParams serialization is infallible")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeParams(e.to_string()))
}
}
pub fn pubkey_prefix(owner: &PublicKey, len: usize) -> String {
let encoded = bs58::encode(owner).into_string();
let take = len.min(encoded.len());
encoded[..take].to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SignedField<T> {
pub value: T,
pub update_seq: u64,
#[serde(with = "serde_bytes_array_64")]
pub signature: Signature,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WriterGrant {
pub granted_at_epoch: u64,
pub revoked_at_epoch: Option<u64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct AclState {
pub epoch: u64,
pub grants: BTreeMap<PublicKey, WriterGrant>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RefEntry {
pub target: CommitHash,
pub update_seq: u64,
pub updater: PublicKey,
pub auth_epoch: u64,
#[serde(with = "serde_bytes_array_64")]
pub signature: Signature,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ObjectBundle {
SinglePack {
pack_hash: PackHash,
size_bytes: u64,
},
ChunkedPack {
manifest_hash: ManifestHash,
total_size: u64,
chunk_count: u32,
},
}
impl ObjectBundle {
pub fn id(&self) -> ObjectBundleId {
let value = match self {
Self::SinglePack {
pack_hash,
size_bytes,
} => MapBuilder::default()
.text_entry("kind", Value::text("single-pack"))
.text_entry("pack_hash", Value::bytes(pack_hash.to_vec()))
.text_entry("size_bytes", Value::uint(*size_bytes))
.build(),
Self::ChunkedPack {
manifest_hash,
total_size,
chunk_count,
} => MapBuilder::default()
.text_entry("kind", Value::text("chunked-pack"))
.text_entry("manifest_hash", Value::bytes(manifest_hash.to_vec()))
.text_entry("total_size", Value::uint(*total_size))
.text_entry("chunk_count", Value::uint(u64::from(*chunk_count)))
.build(),
};
*blake3::hash(&value.encode()).as_bytes()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectBundleRecord {
pub bundle: ObjectBundle,
pub added_by: PublicKey,
pub auth_epoch: u64,
#[serde(with = "serde_bytes_array_64")]
pub signature: Signature,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExtensionEntry {
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
pub update_seq: u64,
#[serde(with = "serde_bytes_array_64")]
pub signature: Signature,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepoState {
pub owner: PublicKey,
pub name: Option<SignedField<String>>,
pub description: Option<SignedField<String>>,
pub default_branch: Option<SignedField<RefName>>,
pub force_push_allowed: Option<SignedField<Vec<RefName>>>,
pub acl: Option<SignedField<AclState>>,
pub upgrade: Option<SignedField<Option<RepoKey>>>,
pub refs: BTreeMap<RefName, RefEntry>,
pub object_index: BTreeMap<ObjectBundleId, ObjectBundleRecord>,
pub extensions: BTreeMap<String, ExtensionEntry>,
}
impl RepoState {
pub fn to_bytes(&self) -> Vec<u8> {
bincode::serialize(self).expect("RepoState serialization is infallible")
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ValidateError> {
if bytes.is_empty() {
return Ok(Self::default());
}
bincode::deserialize(bytes).map_err(|e| ValidateError::DecodeState(e.to_string()))
}
}
#[derive(Debug, thiserror::Error)]
pub enum ValidateError {
#[error("decode parameters: {0}")]
DecodeParams(String),
#[error("decode state: {0}")]
DecodeState(String),
#[error("signature verification failed for {0}")]
InvalidSignature(&'static str),
#[error("entry signed by non-owner key (Phase 1.0 is single-writer)")]
NonOwnerSigner,
#[error("field {field} exceeds limit ({len} > {max})")]
FieldTooLong {
field: &'static str,
len: usize,
max: usize,
},
#[error("object_index entry has wrong bundle id")]
BundleIdMismatch,
#[error("monotonic invariant violated: {0}")]
NonMonotonic(&'static str),
#[error("prefix length {len} outside valid range [{min}..={max}]")]
InvalidPrefixLength {
len: usize,
min: usize,
max: usize,
},
#[error("owner pubkey does not match parameters prefix: expected {expected}, got {actual}")]
PrefixMismatch {
expected: String,
actual: String,
},
#[error("prefix contains invalid base58 characters")]
InvalidPrefixChars,
}
pub fn validate_state(params: &RepoParams, state: &RepoState) -> Result<(), ValidateError> {
if params.prefix.len() < limits::MIN_PREFIX_LEN || params.prefix.len() > limits::MAX_PREFIX_LEN
{
return Err(ValidateError::InvalidPrefixLength {
len: params.prefix.len(),
min: limits::MIN_PREFIX_LEN,
max: limits::MAX_PREFIX_LEN,
});
}
if bs58::decode(¶ms.prefix).into_vec().is_err() {
return Err(ValidateError::InvalidPrefixChars);
}
let actual_prefix = pubkey_prefix(&state.owner, params.prefix.len());
if actual_prefix != params.prefix {
return Err(ValidateError::PrefixMismatch {
expected: params.prefix.clone(),
actual: actual_prefix,
});
}
let repo_key = params_repo_key(params, &state.owner);
if let Some(field) = &state.name {
check_size("name", field.value.len(), limits::MAX_NAME_BYTES)?;
verify_signed_field_string(&repo_key, "name", &state.owner, field, "name")?;
}
if let Some(field) = &state.description {
check_size(
"description",
field.value.len(),
limits::MAX_DESCRIPTION_BYTES,
)?;
verify_signed_field_string(&repo_key, "description", &state.owner, field, "description")?;
}
if let Some(field) = &state.default_branch {
check_size(
"default_branch",
field.value.len(),
limits::MAX_REF_NAME_BYTES,
)?;
verify_signed_field_string(
&repo_key,
"default_branch",
&state.owner,
field,
"default_branch",
)?;
}
if let Some(field) = &state.force_push_allowed {
verify_signed_field_ref_list(
&repo_key,
"force_push_allowed",
&state.owner,
field,
"force_push_allowed",
)?;
}
if let Some(field) = &state.acl {
verify_signed_field_acl(&repo_key, "acl", &state.owner, field, "acl")?;
}
if let Some(field) = &state.upgrade {
verify_signed_field_optional_repo_key(
&repo_key,
"upgrade",
&state.owner,
field,
"upgrade",
)?;
}
for (ref_name, entry) in &state.refs {
check_size("ref name", ref_name.len(), limits::MAX_REF_NAME_BYTES)?;
if entry.updater != state.owner {
return Err(ValidateError::NonOwnerSigner);
}
verify_ref_entry(&repo_key, ref_name, entry)?;
}
for (bundle_id, record) in &state.object_index {
if record.bundle.id() != *bundle_id {
return Err(ValidateError::BundleIdMismatch);
}
if record.added_by != state.owner {
return Err(ValidateError::NonOwnerSigner);
}
verify_bundle_record(&repo_key, record)?;
}
for (ext_key, entry) in &state.extensions {
check_size(
"extension key",
ext_key.len(),
limits::MAX_EXTENSION_KEY_BYTES,
)?;
check_size(
"extension value",
entry.value.len(),
limits::MAX_EXTENSION_VALUE_BYTES,
)?;
verify_extension_entry(&repo_key, ext_key, &state.owner, entry)?;
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
pub enum UpdateError {
#[error(transparent)]
Invalid(#[from] ValidateError),
#[error("non-fast-forward update on protected ref")]
NonFastForward,
}
pub fn merge_state(current: &RepoState, incoming: &RepoState) -> RepoState {
let mut out = current.clone();
out.name = pick_signed_field(out.name, incoming.name.clone());
out.description = pick_signed_field(out.description, incoming.description.clone());
out.default_branch = pick_signed_field(out.default_branch, incoming.default_branch.clone());
out.force_push_allowed =
pick_signed_field(out.force_push_allowed, incoming.force_push_allowed.clone());
out.acl = pick_signed_field(out.acl, incoming.acl.clone());
out.upgrade = pick_signed_field(out.upgrade, incoming.upgrade.clone());
for (k, v) in &incoming.refs {
let pick = match out.refs.remove(k) {
None => v.clone(),
Some(existing) => pick_ref_entry(existing, v.clone()),
};
out.refs.insert(k.clone(), pick);
}
for (k, v) in &incoming.object_index {
out.object_index.entry(*k).or_insert_with(|| v.clone());
}
for (k, v) in &incoming.extensions {
let pick = match out.extensions.remove(k) {
None => v.clone(),
Some(existing) => pick_extension_entry(existing, v.clone()),
};
out.extensions.insert(k.clone(), pick);
}
out
}
pub fn update_state(
params: &RepoParams,
current: &RepoState,
delta: &RepoState,
) -> Result<RepoState, UpdateError> {
let merged = merge_state(current, delta);
let _ = params;
validate_state(params, &merged)?;
Ok(merged)
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct RepoSummary {
pub field_seqs: BTreeMap<String, u64>,
pub ref_seqs: BTreeMap<RefName, u64>,
pub bundle_ids: Vec<ObjectBundleId>,
pub extension_seqs: BTreeMap<String, u64>,
}
pub fn summarize_state(state: &RepoState) -> RepoSummary {
let mut s = RepoSummary::default();
if let Some(f) = &state.name {
s.field_seqs.insert("name".into(), f.update_seq);
}
if let Some(f) = &state.description {
s.field_seqs.insert("description".into(), f.update_seq);
}
if let Some(f) = &state.default_branch {
s.field_seqs.insert("default_branch".into(), f.update_seq);
}
if let Some(f) = &state.force_push_allowed {
s.field_seqs
.insert("force_push_allowed".into(), f.update_seq);
}
if let Some(f) = &state.acl {
s.field_seqs.insert("acl".into(), f.update_seq);
}
if let Some(f) = &state.upgrade {
s.field_seqs.insert("upgrade".into(), f.update_seq);
}
for (k, v) in &state.refs {
s.ref_seqs.insert(k.clone(), v.update_seq);
}
for k in state.object_index.keys() {
s.bundle_ids.push(*k);
}
for (k, v) in &state.extensions {
s.extension_seqs.insert(k.clone(), v.update_seq);
}
s
}
pub fn get_state_delta(state: &RepoState, summary: &RepoSummary) -> RepoState {
let mut d = RepoState::default();
if state.name.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("name").copied() {
d.name = state.name.clone();
}
if state.description.as_ref().map(|f| f.update_seq)
> summary.field_seqs.get("description").copied()
{
d.description = state.description.clone();
}
if state.default_branch.as_ref().map(|f| f.update_seq)
> summary.field_seqs.get("default_branch").copied()
{
d.default_branch = state.default_branch.clone();
}
if state.force_push_allowed.as_ref().map(|f| f.update_seq)
> summary.field_seqs.get("force_push_allowed").copied()
{
d.force_push_allowed = state.force_push_allowed.clone();
}
if state.acl.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("acl").copied() {
d.acl = state.acl.clone();
}
if state.upgrade.as_ref().map(|f| f.update_seq) > summary.field_seqs.get("upgrade").copied() {
d.upgrade = state.upgrade.clone();
}
for (k, v) in &state.refs {
if summary.ref_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
d.refs.insert(k.clone(), v.clone());
}
}
let known: std::collections::HashSet<&ObjectBundleId> = summary.bundle_ids.iter().collect();
for (k, v) in &state.object_index {
if !known.contains(k) {
d.object_index.insert(*k, v.clone());
}
}
for (k, v) in &state.extensions {
if summary.extension_seqs.get(k).copied().unwrap_or(0) < v.update_seq {
d.extensions.insert(k.clone(), v.clone());
}
}
d
}
pub fn signed_payload_string_field(
repo_key: &RepoKey,
field_name: &str,
value: &str,
update_seq: u64,
) -> Vec<u8> {
build_payload(field_name, |b| {
b.field_bytes(repo_key);
b.field_str(value);
b.field_u64(update_seq);
})
}
pub fn signed_payload_ref_list_field(
repo_key: &RepoKey,
field_name: &str,
refs: &[RefName],
update_seq: u64,
) -> Vec<u8> {
build_payload(field_name, |b| {
b.field_bytes(repo_key);
b.field_u32(u32::try_from(refs.len()).unwrap_or(u32::MAX));
for r in refs {
b.field_str(r);
}
b.field_u64(update_seq);
})
}
pub fn signed_payload_acl_field(
repo_key: &RepoKey,
field_name: &str,
acl: &AclState,
update_seq: u64,
) -> Vec<u8> {
build_payload(field_name, |b| {
b.field_bytes(repo_key);
b.field_u64(acl.epoch);
b.field_u32(u32::try_from(acl.grants.len()).unwrap_or(u32::MAX));
for (writer, grant) in &acl.grants {
b.field_bytes(writer);
b.field_u64(grant.granted_at_epoch);
b.field_option_bytes(
grant
.revoked_at_epoch
.map(|e| e.to_le_bytes())
.as_ref()
.map(|x| x.as_slice()),
);
}
b.field_u64(update_seq);
})
}
pub fn signed_payload_optional_repo_key_field(
repo_key: &RepoKey,
field_name: &str,
successor: Option<&RepoKey>,
update_seq: u64,
) -> Vec<u8> {
build_payload(field_name, |b| {
b.field_bytes(repo_key);
b.field_option_bytes(successor.map(|k| k.as_slice()));
b.field_u64(update_seq);
})
}
pub fn signed_payload_ref_entry(
repo_key: &RepoKey,
ref_name: &str,
target: &CommitHash,
update_seq: u64,
auth_epoch: u64,
) -> Vec<u8> {
build_payload("ref-update", |b| {
b.field_bytes(repo_key);
b.field_str(ref_name);
b.field_bytes(target);
b.field_u64(update_seq);
b.field_u64(auth_epoch);
})
}
pub fn signed_payload_bundle_record(
repo_key: &RepoKey,
bundle: &ObjectBundle,
auth_epoch: u64,
) -> Vec<u8> {
let bundle_id = bundle.id();
build_payload("object-bundle", |b| {
b.field_bytes(repo_key);
b.field_bytes(&bundle_id);
b.field_u64(auth_epoch);
})
}
pub fn signed_payload_extension(
repo_key: &RepoKey,
ext_key: &str,
value: &[u8],
update_seq: u64,
) -> Vec<u8> {
build_payload("extension", |b| {
b.field_bytes(repo_key);
b.field_str(ext_key);
b.field_bytes(value);
b.field_u64(update_seq);
})
}
fn params_repo_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
let mut h = blake3::Hasher::new();
h.update(WIRE_VERSION.as_bytes());
h.update(params.prefix.as_bytes());
h.update(owner);
*h.finalize().as_bytes()
}
pub fn signature_domain_key(params: &RepoParams, owner: &PublicKey) -> RepoKey {
params_repo_key(params, owner)
}
fn check_size(field: &'static str, len: usize, max: usize) -> Result<(), ValidateError> {
if len > max {
Err(ValidateError::FieldTooLong { field, len, max })
} else {
Ok(())
}
}
fn verify_signature(
payload: &[u8],
signer: &PublicKey,
signature: &Signature,
label: &'static str,
) -> Result<(), ValidateError> {
let pk = ed25519_compact::PublicKey::from_slice(signer)
.map_err(|_| ValidateError::InvalidSignature(label))?;
let sig = ed25519_compact::Signature::from_slice(signature)
.map_err(|_| ValidateError::InvalidSignature(label))?;
pk.verify(payload, &sig)
.map_err(|_| ValidateError::InvalidSignature(label))
}
fn verify_signed_field_string(
repo_key: &RepoKey,
field_name: &str,
owner: &PublicKey,
field: &SignedField<String>,
label: &'static str,
) -> Result<(), ValidateError> {
let payload = signed_payload_string_field(repo_key, field_name, &field.value, field.update_seq);
verify_signature(&payload, owner, &field.signature, label)
}
fn verify_signed_field_ref_list(
repo_key: &RepoKey,
field_name: &str,
owner: &PublicKey,
field: &SignedField<Vec<RefName>>,
label: &'static str,
) -> Result<(), ValidateError> {
let payload =
signed_payload_ref_list_field(repo_key, field_name, &field.value, field.update_seq);
verify_signature(&payload, owner, &field.signature, label)
}
fn verify_signed_field_acl(
repo_key: &RepoKey,
field_name: &str,
owner: &PublicKey,
field: &SignedField<AclState>,
label: &'static str,
) -> Result<(), ValidateError> {
let payload = signed_payload_acl_field(repo_key, field_name, &field.value, field.update_seq);
verify_signature(&payload, owner, &field.signature, label)
}
fn verify_signed_field_optional_repo_key(
repo_key: &RepoKey,
field_name: &str,
owner: &PublicKey,
field: &SignedField<Option<RepoKey>>,
label: &'static str,
) -> Result<(), ValidateError> {
let payload = signed_payload_optional_repo_key_field(
repo_key,
field_name,
field.value.as_ref(),
field.update_seq,
);
verify_signature(&payload, owner, &field.signature, label)
}
fn verify_ref_entry(
repo_key: &RepoKey,
ref_name: &str,
entry: &RefEntry,
) -> Result<(), ValidateError> {
let payload = signed_payload_ref_entry(
repo_key,
ref_name,
&entry.target,
entry.update_seq,
entry.auth_epoch,
);
verify_signature(&payload, &entry.updater, &entry.signature, "ref entry")
}
fn verify_bundle_record(
repo_key: &RepoKey,
record: &ObjectBundleRecord,
) -> Result<(), ValidateError> {
let payload = signed_payload_bundle_record(repo_key, &record.bundle, record.auth_epoch);
verify_signature(
&payload,
&record.added_by,
&record.signature,
"bundle record",
)
}
fn verify_extension_entry(
repo_key: &RepoKey,
ext_key: &str,
owner: &PublicKey,
entry: &ExtensionEntry,
) -> Result<(), ValidateError> {
let payload = signed_payload_extension(repo_key, ext_key, &entry.value, entry.update_seq);
verify_signature(&payload, owner, &entry.signature, "extension entry")
}
fn pick_signed_field<T: Clone>(
a: Option<SignedField<T>>,
b: Option<SignedField<T>>,
) -> Option<SignedField<T>> {
match (a, b) {
(None, x) | (x, None) => x,
(Some(x), Some(y)) => Some(
if pick_higher_seq(x.update_seq, &x.signature, y.update_seq, &y.signature) {
x
} else {
y
},
),
}
}
fn pick_ref_entry(a: RefEntry, b: RefEntry) -> RefEntry {
if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
a
} else {
b
}
}
fn pick_extension_entry(a: ExtensionEntry, b: ExtensionEntry) -> ExtensionEntry {
if pick_higher_seq(a.update_seq, &a.signature, b.update_seq, &b.signature) {
a
} else {
b
}
}
fn pick_higher_seq(a_seq: u64, a_sig: &Signature, b_seq: u64, b_sig: &Signature) -> bool {
match a_seq.cmp(&b_seq) {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Less => false,
std::cmp::Ordering::Equal => a_sig <= b_sig,
}
}
mod serde_bytes_array_64 {
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(value: &[u8; 64], ser: S) -> Result<S::Ok, S::Error> {
serde_bytes::Bytes::new(value).serialize(ser)
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 64], D::Error> {
let bytes: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(de)?;
bytes
.as_ref()
.try_into()
.map_err(|_| D::Error::custom("expected 64-byte signature"))
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
#[test]
fn signature_domain_key_is_wasm_independent() {
let owner = [1u8; 32];
let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
let k = signature_domain_key(¶ms, &owner);
assert_eq!(k.len(), 32);
assert_eq!(signature_domain_key(¶ms, &owner), k);
}
#[test]
fn default_state_with_matching_prefix_validates() {
let owner = [0u8; 32];
let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
assert!(validate_state(¶ms, &RepoState::default()).is_ok());
}
#[test]
fn prefix_mismatch_rejected() {
let owner = [3u8; 32];
let other = [4u8; 32];
let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
let mut state = RepoState::default();
state.owner = other;
match validate_state(¶ms, &state) {
Err(ValidateError::PrefixMismatch { .. }) => {}
other => panic!("expected PrefixMismatch, got {:?}", other),
}
}
#[test]
fn invalid_prefix_length_rejected() {
let too_short = RepoParams {
prefix: "abc".into(),
};
match validate_state(&too_short, &RepoState::default()) {
Err(ValidateError::InvalidPrefixLength { len: 3, .. }) => {}
other => panic!("expected InvalidPrefixLength, got {:?}", other),
}
let too_long = RepoParams {
prefix: "a".repeat(33),
};
match validate_state(&too_long, &RepoState::default()) {
Err(ValidateError::InvalidPrefixLength { len: 33, .. }) => {}
other => panic!("expected InvalidPrefixLength, got {:?}", other),
}
}
#[test]
fn bundle_id_matches_canonical_hash() {
let bundle = ObjectBundle::SinglePack {
pack_hash: [0xAA; 32],
size_bytes: 4096,
};
let id = bundle.id();
assert_eq!(id, bundle.id());
let bundle2 = ObjectBundle::SinglePack {
pack_hash: [0xAA; 32],
size_bytes: 4097,
};
assert_ne!(id, bundle2.id());
}
#[test]
fn merge_picks_higher_seq() {
let mut a: SignedField<String> = SignedField {
value: "a".into(),
update_seq: 1,
signature: [0u8; 64],
};
let b: SignedField<String> = SignedField {
value: "b".into(),
update_seq: 2,
signature: [0u8; 64],
};
let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
assert_eq!(pick.update_seq, 2);
a.update_seq = 2;
a.signature[0] = 0; let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
assert_eq!(pick.value, "a");
a.signature[0] = 1;
let pick = pick_signed_field(Some(a.clone()), Some(b.clone())).unwrap();
assert_eq!(pick.value, "b");
}
#[test]
fn name_size_limit_is_enforced() {
let owner = [5u8; 32];
let params = RepoParams::from_owner(&owner, limits::DEFAULT_PREFIX_LEN);
let mut state = RepoState::default();
state.owner = owner;
state.name = Some(SignedField {
value: "x".repeat(limits::MAX_NAME_BYTES + 1),
update_seq: 1,
signature: [0u8; 64],
});
match validate_state(¶ms, &state) {
Err(ValidateError::FieldTooLong { field, .. }) => assert_eq!(field, "name"),
other => panic!("expected FieldTooLong, got {:?}", other),
}
}
}