use core::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ParseIdError {
expected_digits: usize,
}
impl ParseIdError {
const fn new(expected_digits: usize) -> Self {
Self { expected_digits }
}
#[must_use]
pub const fn expected_digits(self) -> usize {
self.expected_digits
}
}
impl fmt::Display for ParseIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"identifier must be exactly {} lowercase hexadecimal digits",
self.expected_digits
)
}
}
impl std::error::Error for ParseIdError {}
const fn validate_hex_digits(value: &str, expected_digits: usize) -> Result<(), ParseIdError> {
if value.len() != expected_digits {
return Err(ParseIdError::new(expected_digits));
}
let bytes = value.as_bytes();
let mut index = 0;
while index < bytes.len() {
let byte = bytes[index];
if !(byte.is_ascii_digit() || (byte >= b'a' && byte <= b'f')) {
return Err(ParseIdError::new(expected_digits));
}
index += 1;
}
Ok(())
}
const ID_DIGITS: usize = 32;
const REVISION_DIGITS: usize = 64;
fn parse_id(value: &str) -> Result<u128, ParseIdError> {
validate_hex_digits(value, ID_DIGITS)?;
u128::from_str_radix(value, 16).map_err(|_| ParseIdError::new(ID_DIGITS))
}
macro_rules! id128 {
($(#[$doc:meta])* $name:ident) => {
$(#[$doc])*
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(pub u128);
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:032x}", self.0)
}
}
impl FromStr for $name {
type Err = ParseIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
parse_id(value).map(Self)
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct IdVisitor;
impl serde::de::Visitor<'_> for IdVisitor {
type Value = $name;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"an identifier containing exactly 32 lowercase hexadecimal digits",
)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse().map_err(E::custom)
}
}
deserializer.deserialize_str(IdVisitor)
}
}
};
}
id128!(
AccountId
);
id128!(
KeyId
);
id128!(
LeaseId
);
id128!(
RequestId
);
id128!(
Principal
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct FencingToken(pub u64);
impl fmt::Display for FencingToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Generation(pub u64);
impl fmt::Display for Generation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct PolicyRevision(pub [u8; 32]);
impl PolicyRevision {
pub const UNSTATED: Self = Self([0; 32]);
#[must_use]
pub const fn is_unstated(self) -> bool {
let mut index = 0;
while index < self.0.len() {
if self.0[index] != 0 {
return false;
}
index += 1;
}
true
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Display for PolicyRevision {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for byte in self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
impl FromStr for PolicyRevision {
type Err = ParseIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
validate_hex_digits(value, REVISION_DIGITS)?;
let mut bytes = [0u8; 32];
for (index, byte) in bytes.iter_mut().enumerate() {
let pair = &value[index * 2..index * 2 + 2];
*byte = u8::from_str_radix(pair, 16).map_err(|_| ParseIdError::new(REVISION_DIGITS))?;
}
Ok(Self(bytes))
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for PolicyRevision {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for PolicyRevision {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct RevisionVisitor;
impl serde::de::Visitor<'_> for RevisionVisitor {
type Value = PolicyRevision;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(
"a policy revision containing exactly 64 lowercase hexadecimal digits",
)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
value.parse().map_err(E::custom)
}
}
deserializer.deserialize_str(RevisionVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! canonical_id_tests {
($($name:ident),+ $(,)?) => {
$(
assert_eq!($name(0).to_string(), "00000000000000000000000000000000");
assert_eq!($name(u128::MAX).to_string(), "ffffffffffffffffffffffffffffffff");
assert_eq!(
"8000000000000000000000000000002a".parse::<$name>(),
Ok($name((1u128 << 127) | 0x2a)),
);
)+
};
}
#[test]
fn every_id_uses_the_same_fixed_width_lowercase_hexadecimal_text() {
canonical_id_tests!(AccountId, KeyId, LeaseId, RequestId, Principal);
}
#[test]
fn noncanonical_spellings_are_rejected() {
for value in [
"1",
"00000000000000000000000000000001 ",
"0x00000000000000000000000000000001",
"0000000000000000000000000000000A",
"gggggggggggggggggggggggggggggggg",
] {
assert_eq!(
value.parse::<AccountId>(),
Err(ParseIdError::new(ID_DIGITS)),
"{value:?}"
);
}
}
#[test]
fn noncanonical_revision_spellings_are_rejected() {
let ok = "8".repeat(64);
assert!(
ok.parse::<PolicyRevision>().is_ok(),
"the corpus baseline parses"
);
for value in [
"1".to_string(),
format!("{}{}", "0".repeat(63), "1 "),
format!("0x{}", "0".repeat(64)),
format!("{}A", "0".repeat(63)),
"g".repeat(64),
] {
assert_eq!(
value.parse::<PolicyRevision>(),
Err(ParseIdError::new(REVISION_DIGITS)),
"{value:?}"
);
}
}
#[test]
fn the_two_identifier_widths_reject_each_others_canonical_form() {
let id_width = "0".repeat(32);
let revision_width = "0".repeat(64);
assert!(id_width.parse::<AccountId>().is_ok());
assert_eq!(
id_width.parse::<PolicyRevision>(),
Err(ParseIdError::new(REVISION_DIGITS))
);
assert!(revision_width.parse::<PolicyRevision>().is_ok());
assert_eq!(
revision_width.parse::<AccountId>(),
Err(ParseIdError::new(ID_DIGITS))
);
}
#[test]
fn the_parse_error_names_the_width_it_expected() {
let id_error = "".parse::<AccountId>().unwrap_err();
assert_eq!(id_error.expected_digits(), 32);
assert_eq!(
id_error.to_string(),
"identifier must be exactly 32 lowercase hexadecimal digits"
);
let revision_error = "".parse::<PolicyRevision>().unwrap_err();
assert_eq!(revision_error.expected_digits(), 64);
assert_eq!(
revision_error.to_string(),
"identifier must be exactly 64 lowercase hexadecimal digits"
);
}
#[test]
fn the_unstated_revision_is_all_zeroes_and_round_trips() {
let unstated = PolicyRevision::default();
assert_eq!(unstated, PolicyRevision::UNSTATED);
assert!(unstated.is_unstated());
assert_eq!(unstated.to_string(), "0".repeat(64));
assert_eq!(unstated.to_string().parse::<PolicyRevision>(), Ok(unstated));
let stated = PolicyRevision([0xab; 32]);
assert!(!stated.is_unstated());
assert_eq!(stated.to_string(), "ab".repeat(32));
assert_eq!(stated.to_string().parse::<PolicyRevision>(), Ok(stated));
}
#[test]
fn a_revision_round_trips_every_byte_position_in_order() {
let mut bytes = [0u8; 32];
for (index, byte) in bytes.iter_mut().enumerate() {
*byte = index as u8;
}
let revision = PolicyRevision(bytes);
assert_eq!(
revision.to_string(),
"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
);
assert_eq!(revision.to_string().parse::<PolicyRevision>(), Ok(revision));
}
#[cfg(feature = "serde")]
#[test]
fn human_readable_serde_is_textual_and_strict() {
macro_rules! assert_textual {
($name:ident) => {{
let value = $name((1u128 << 127) | 0x2a);
let encoded = serde_json::to_string(&value).unwrap();
assert_eq!(encoded, r#""8000000000000000000000000000002a""#);
assert_eq!(serde_json::from_str::<$name>(&encoded).unwrap(), value);
assert!(serde_json::from_str::<$name>("42").is_err());
}};
}
assert_textual!(AccountId);
assert_textual!(KeyId);
assert_textual!(LeaseId);
assert_textual!(RequestId);
assert_textual!(Principal);
}
#[cfg(feature = "serde")]
#[test]
fn revision_serde_is_textual_and_strict() {
let value = PolicyRevision([0x8f; 32]);
let encoded = serde_json::to_string(&value).unwrap();
assert_eq!(encoded, format!("\"{}\"", "8f".repeat(32)));
assert_eq!(
serde_json::from_str::<PolicyRevision>(&encoded).unwrap(),
value
);
let wrong_type =
serde_json::from_str::<PolicyRevision>("42").expect_err("a number is not a revision");
assert!(
wrong_type
.to_string()
.contains("exactly 64 lowercase hexadecimal digits"),
"the type error must name the expected form, got {wrong_type}"
);
let wrong_width =
serde_json::from_str::<PolicyRevision>(&format!("\"{}\"", "0".repeat(32)))
.expect_err("32 digits is not a revision");
assert!(
wrong_width
.to_string()
.contains("exactly 64 lowercase hexadecimal digits"),
"the width error must name the expected width, got {wrong_width}"
);
}
}