use data_encoding::BASE32_NOPAD;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use std::{fmt, str::FromStr};
#[cfg(feature = "principal-self-authenticating")]
mod self_authenticating;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Principal {
len: u8,
bytes: [u8; 29],
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
pub enum PrincipalError {
#[error("Bytes is longer than 29 bytes.")]
BytesTooLong(),
#[error("Text must be in valid Base32 encoding.")]
InvalidBase32(),
#[error("Text is too short.")]
TextTooShort(),
#[error("Text is too long.")]
TextTooLong(),
#[error("CRC32 check sequence doesn't match with calculated from Principal bytes.")]
CheckSequenceNotMatch(),
#[error("Text should be separated by - (dash) every 5 characters: expected \"{0}\"")]
AbnormalGrouped(Principal),
}
impl Principal {
pub const MAX_LENGTH_IN_BYTES: usize = 29;
pub const MAX_LENGTH_IN_TEXT: usize = 63;
pub const fn management_canister() -> Self {
Self::from_slice(&[])
}
pub const fn anonymous() -> Self {
Self::from_slice(&[4])
}
pub const fn from_slice(slice: &[u8]) -> Self {
match Self::try_from_slice(slice) {
Ok(principal) => principal,
Err(_) => panic!("a Principal cannot contain more than 29 bytes"),
}
}
pub const fn try_from_slice(slice: &[u8]) -> Result<Self, PrincipalError> {
if slice.len() > Self::MAX_LENGTH_IN_BYTES {
return Err(PrincipalError::BytesTooLong());
}
let mut principal = Self {
len: slice.len() as u8,
bytes: [0; Self::MAX_LENGTH_IN_BYTES],
};
let mut index = 0;
while index < slice.len() {
principal.bytes[index] = slice[index];
index += 1;
}
Ok(principal)
}
pub fn from_text<S: AsRef<str>>(text: S) -> Result<Self, PrincipalError> {
let text = text.as_ref();
if text.len() > Self::MAX_LENGTH_IN_TEXT {
return Err(PrincipalError::TextTooLong());
}
let mut symbols = [0_u8; Self::MAX_LENGTH_IN_TEXT];
let mut symbol_count = 0;
for byte in text.bytes() {
if byte != b'-' {
symbols[symbol_count] = byte.to_ascii_uppercase();
symbol_count += 1;
}
}
let decoded_len = BASE32_NOPAD
.decode_len(symbol_count)
.map_err(|_| PrincipalError::InvalidBase32())?;
let mut decoded = [0_u8; 39];
let used = BASE32_NOPAD
.decode_mut(&symbols[..symbol_count], &mut decoded[..decoded_len])
.map_err(|_| PrincipalError::InvalidBase32())?;
if used < 4 {
return Err(PrincipalError::TextTooShort());
}
let payload = &decoded[4..used];
if payload.len() > Self::MAX_LENGTH_IN_BYTES {
return Err(PrincipalError::TextTooLong());
}
if decoded[..4] != crc32fast::hash(payload).to_be_bytes() {
return Err(PrincipalError::CheckSequenceNotMatch());
}
let principal = Self::from_slice(payload);
let mut canonical = [0_u8; Self::MAX_LENGTH_IN_TEXT];
if !principal
.text_into(&mut canonical)
.eq_ignore_ascii_case(text)
{
return Err(PrincipalError::AbnormalGrouped(principal));
}
Ok(principal)
}
pub fn to_text(&self) -> String {
let mut text = [0_u8; Self::MAX_LENGTH_IN_TEXT];
self.text_into(&mut text).to_owned()
}
pub const fn as_slice(&self) -> &[u8] {
self.bytes.split_at(self.len as usize).0
}
#[expect(
clippy::len_without_is_empty,
reason = "A zero-length Principal identifies the management canister"
)]
pub const fn len(&self) -> u8 {
self.len
}
pub const fn as_fixed_bytes(&self) -> &[u8; 29] {
&self.bytes
}
fn text_into<'a>(&self, output: &'a mut [u8; Self::MAX_LENGTH_IN_TEXT]) -> &'a str {
let mut checked = [0_u8; 4 + Self::MAX_LENGTH_IN_BYTES];
checked[..4].copy_from_slice(&crc32fast::hash(self.as_slice()).to_be_bytes());
let checked_len = 4 + usize::from(self.len);
checked[4..checked_len].copy_from_slice(self.as_slice());
let encoded_len = BASE32_NOPAD.encode_len(checked_len);
let mut encoded = [0_u8; 53];
BASE32_NOPAD.encode_mut(&checked[..checked_len], &mut encoded[..encoded_len]);
let mut written = 0;
for (index, byte) in encoded[..encoded_len].iter().enumerate() {
if index != 0 && index % 5 == 0 {
output[written] = b'-';
written += 1;
}
output[written] = byte.to_ascii_lowercase();
written += 1;
}
std::str::from_utf8(&output[..written]).expect("Base32 text is ASCII")
}
}
impl fmt::Display for Principal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut text = [0_u8; Self::MAX_LENGTH_IN_TEXT];
formatter.write_str(self.text_into(&mut text))
}
}
impl FromStr for Principal {
type Err = PrincipalError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
Self::from_text(text)
}
}
impl AsRef<[u8]> for Principal {
fn as_ref(&self) -> &[u8] {
self.as_slice()
}
}
impl TryFrom<&str> for Principal {
type Error = PrincipalError;
fn try_from(text: &str) -> Result<Self, Self::Error> {
Self::from_text(text)
}
}
impl TryFrom<Vec<u8>> for Principal {
type Error = PrincipalError;
fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from_slice(&bytes)
}
}
impl TryFrom<&Vec<u8>> for Principal {
type Error = PrincipalError;
fn try_from(bytes: &Vec<u8>) -> Result<Self, Self::Error> {
Self::try_from_slice(bytes)
}
}
impl TryFrom<&[u8]> for Principal {
type Error = PrincipalError;
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_slice(bytes)
}
}
impl Serialize for Principal {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
let mut text = [0_u8; Self::MAX_LENGTH_IN_TEXT];
serializer.serialize_str(self.text_into(&mut text))
} else {
serializer.serialize_bytes(self.as_slice())
}
}
}
struct PrincipalVisitor;
impl de::Visitor<'_> for PrincipalVisitor {
type Value = Principal;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("bytes or string")
}
fn visit_str<E: de::Error>(self, text: &str) -> Result<Principal, E> {
Principal::from_text(text).map_err(E::custom)
}
fn visit_bytes<E: de::Error>(self, bytes: &[u8]) -> Result<Principal, E> {
Principal::try_from_slice(bytes).map_err(E::custom)
}
}
impl<'de> Deserialize<'de> for Principal {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
if deserializer.is_human_readable() {
deserializer.deserialize_str(PrincipalVisitor)
} else {
deserializer.deserialize_bytes(PrincipalVisitor)
}
}
}
#[cfg(test)]
mod tests;