This commit is contained in:
2026-09-19 08:39:25 +07:00
parent 0328d35945
commit 1cf5ecba51
9 changed files with 951 additions and 71 deletions
+35
View File
@@ -1,9 +1,13 @@
pub mod base64url;
pub mod derive;
use anyhow::{Result, anyhow, bail};
use data_encoding::HEXLOWER;
use rand::TryRng as _;
use rand::rngs::SysRng;
use serde::Serialize;
use crate::Extra;
/// Uppercase and other non-canonical spellings are rejected.
pub(crate) fn decode_hex_32(value: &str) -> Result<[u8; 32]> {
@@ -38,3 +42,34 @@ pub(crate) fn random_32() -> Result<[u8; 32]> {
fill_random(&mut bytes)?;
Ok(bytes)
}
/// Hex to unpadded base64url for one 32-byte §8 value.
pub(crate) fn hex32_to_base64(value: &str) -> Result<String> {
Ok(base64url::encode(&decode_hex_32(value)?))
}
/// Unpadded base64url to lowercase hex for one 32-byte §8 value.
pub(crate) fn base64_to_hex32(value: &str) -> Result<String> {
Ok(HEXLOWER.encode(&base64url::decode_32(value)?))
}
/// Canonical JSON bytes: the total-order tie-break every content merge uses.
pub(crate) fn canonical<T: Serialize>(value: &T) -> String {
serde_json::to_string(value).unwrap_or_default()
}
/// Unions an unknown-field map. Where both sides carry a key, the
/// lexicographically lowest canonical bytes win, so two devices converge
/// instead of flapping.
pub(crate) fn union(into: &mut Extra, other: Extra) {
for (key, value) in other {
let replace = match into.get(&key) {
Some(existing) => canonical(&value) < canonical(existing),
None => true,
};
if replace {
into.insert(key, value);
}
}
}