use core::fmt;
use core::str::FromStr;
use prikk_error::{PrikkError, Result};
use prikk_hash::{sha256, to_hex};
pub const OBJECT_ID_DOMAIN: &[u8] = b"PRIKK-OBJECT-ID-v1";
const RETIRED_CODES: &[(u16, &str)] = &[(0x0A, "project-genesis")];
macro_rules! object_types {
(
$(
$(#[$doc:meta])*
$variant:ident = $code:literal => $name:literal,
)+
) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u16)]
pub enum ObjectType {
$(
$(#[$doc])*
$variant = $code,
)+
}
impl ObjectType {
pub const ALL: &'static [Self] = &[$(Self::$variant),+];
#[must_use]
pub const fn code(self) -> u16 {
self as u16
}
pub fn from_code(code: u16) -> Result<Self> {
if let Some((_, name)) = RETIRED_CODES.iter().find(|&&(retired, _)| retired == code) {
return Err(PrikkError::MalformedData(format!(
"object type code {code} is retired (formerly {name}) and must never be reused"
)));
}
match code {
$($code => Ok(Self::$variant),)+
other => Err(PrikkError::MalformedData(format!(
"unknown object type code: {other}"
))),
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
$(Self::$variant => $name,)+
}
}
}
};
}
object_types! {
Patch = 0x01 => "patch",
Block = 0x02 => "block",
RefState = 0x03 => "ref-state",
RefUpdate = 0x04 => "ref-update",
Tag = 0x05 => "tag",
Attestation = 0x06 => "attestation",
Blob = 0x07 => "blob",
BlockSummaryCache = 0x08 => "block-summary-cache",
RecoveryNote = 0x09 => "recovery-note",
RecognitionClaim = 0x0B => "recognition-claim",
}
impl fmt::Display for ObjectType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId([u8; 32]);
impl ObjectId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn from_canonical_payload(
object_type: ObjectType,
schema_version: u32,
canonical_payload: &[u8],
) -> Self {
let mut preimage =
Vec::with_capacity(OBJECT_ID_DOMAIN.len() + 2 + 4 + 8 + canonical_payload.len());
preimage.extend_from_slice(OBJECT_ID_DOMAIN);
preimage.extend_from_slice(&object_type.code().to_be_bytes());
preimage.extend_from_slice(&schema_version.to_be_bytes());
preimage.extend_from_slice(&(canonical_payload.len() as u64).to_be_bytes());
preimage.extend_from_slice(canonical_payload);
Self(sha256(&preimage))
}
#[must_use]
pub fn to_hex(&self) -> String {
to_hex(&self.0)
}
}
impl fmt::Debug for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ObjectId({})", self.to_hex())
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl FromStr for ObjectId {
type Err = PrikkError;
fn from_str(s: &str) -> Result<Self> {
if s.len() != 64 {
return Err(PrikkError::InvalidObjectId(format!(
"expected 64 lowercase hex chars, got {}",
s.len()
)));
}
let mut out = [0_u8; 32];
for (slot, pair) in out.iter_mut().zip(s.as_bytes().chunks_exact(2)) {
let mut bytes = pair.iter().copied();
let high = bytes.next().ok_or_else(|| {
PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
})?;
let low = bytes.next().ok_or_else(|| {
PrikkError::InvalidObjectId("hex pair is unexpectedly short".to_string())
})?;
*slot = (hex_value(high)? << 4) | hex_value(low)?;
}
Ok(Self(out))
}
}
fn hex_value(byte: u8) -> Result<u8> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
_ => Err(PrikkError::InvalidObjectId(
"object IDs must use lowercase hex only".to_string(),
)),
}
}
#[cfg(test)]
mod tests;