use super::encoding::{
resource_id_preimage, tag_channel_state, tag_message, tag_message_payload,
tag_resource_channel, tag_resource_message, tag_resource_session, tag_resource_value,
CanonicalHeapEncoder, CanonicalHeapEncoding,
};
use std::fmt;
use std::hash::{Hash, Hasher as StdHasher};
use std::marker::PhantomData;
use telltale_types::{DefaultContentHasher, Hasher};
#[derive(Clone)]
pub struct ResourceId<H: Hasher = DefaultContentHasher> {
hash: H::Digest,
counter: u64,
_hasher: PhantomData<H>,
}
impl<H: Hasher> ResourceId<H> {
pub fn new(hash: H::Digest, counter: u64) -> Self {
Self {
hash,
counter,
_hasher: PhantomData,
}
}
pub fn from_resource(resource: &Resource, counter: u64) -> Self {
let content_bytes = resource.canonical_bytes();
let preimage = resource_id_preimage(&content_bytes, counter);
let hash = H::digest(&preimage);
Self {
hash,
counter,
_hasher: PhantomData,
}
}
pub fn to_short_hex(&self) -> String {
let hex: String = self.hash.as_ref()[..4]
.iter()
.map(|b| format!("{:02x}", b))
.collect();
format!("{}:{}", hex, self.counter)
}
#[must_use]
pub fn hash(&self) -> &H::Digest {
&self.hash
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
self.hash.as_ref()
}
#[must_use]
pub fn counter(&self) -> u64 {
self.counter
}
}
impl<H: Hasher> PartialEq for ResourceId<H> {
fn eq(&self, other: &Self) -> bool {
self.hash.as_ref() == other.hash.as_ref() && self.counter == other.counter
}
}
impl<H: Hasher> Eq for ResourceId<H> {}
impl<H: Hasher> Hash for ResourceId<H> {
fn hash<S: StdHasher>(&self, state: &mut S) {
self.hash.as_ref().hash(state);
self.counter.hash(state);
}
}
impl<H: Hasher> fmt::Debug for ResourceId<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ResourceId<{}>({})",
H::algorithm_name(),
self.to_short_hex()
)
}
}
impl<H: Hasher> fmt::Display for ResourceId<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_short_hex())
}
}
impl<H: Hasher> PartialOrd for ResourceId<H> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<H: Hasher> Ord for ResourceId<H> {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.hash.as_ref().cmp(other.hash.as_ref()) {
std::cmp::Ordering::Equal => self.counter.cmp(&other.counter),
ord => ord,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessagePayload {
pub label: String,
pub payload: Vec<u8>,
}
impl CanonicalHeapEncoding for MessagePayload {
fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
encoder.string(&self.label);
encoder.bytes(&self.payload);
}
fn canonical_tag(&self) -> u8 {
tag_message_payload()
}
}
impl MessagePayload {
pub fn new(label: impl Into<String>, payload: Vec<u8>) -> Self {
Self {
label: label.into(),
payload,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChannelState {
pub sender: String,
pub receiver: String,
pub queue: Vec<MessagePayload>,
}
impl CanonicalHeapEncoding for ChannelState {
fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
encoder.string(&self.sender);
encoder.string(&self.receiver);
encoder.u32(self.queue.len() as u32);
for payload in &self.queue {
encoder.nested(payload);
}
}
fn canonical_tag(&self) -> u8 {
tag_channel_state()
}
}
impl ChannelState {
pub fn new(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
Self {
sender: sender.into(),
receiver: receiver.into(),
queue: Vec::new(),
}
}
pub fn queue_size(&self) -> usize {
self.queue.len()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Message {
pub source: String,
pub dest: String,
pub content: MessagePayload,
pub seq_no: u64,
}
impl CanonicalHeapEncoding for Message {
fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
encoder.string(&self.source);
encoder.string(&self.dest);
encoder.nested(&self.content);
encoder.u64(self.seq_no);
}
fn canonical_tag(&self) -> u8 {
tag_message()
}
}
impl Message {
pub fn new(
source: impl Into<String>,
dest: impl Into<String>,
label: impl Into<String>,
payload: Vec<u8>,
seq_no: u64,
) -> Self {
Self {
source: source.into(),
dest: dest.into(),
content: MessagePayload::new(label, payload),
seq_no,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resource {
Channel(ChannelState),
Message(Message),
Session {
role: String,
type_hash: u64,
},
Value {
tag: String,
data: Vec<u8>,
},
}
impl Resource {
pub fn channel(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
Resource::Channel(ChannelState::new(sender, receiver))
}
pub fn message(
source: impl Into<String>,
dest: impl Into<String>,
label: impl Into<String>,
payload: Vec<u8>,
seq_no: u64,
) -> Self {
Resource::Message(Message::new(source, dest, label, payload, seq_no))
}
pub fn session(role: impl Into<String>, type_hash: u64) -> Self {
Resource::Session {
role: role.into(),
type_hash,
}
}
pub fn value(tag: impl Into<String>, data: Vec<u8>) -> Self {
Resource::Value {
tag: tag.into(),
data,
}
}
pub fn kind(&self) -> &'static str {
match self {
Resource::Channel(_) => "channel",
Resource::Message(_) => "message",
Resource::Session { .. } => "session",
Resource::Value { .. } => "value",
}
}
pub fn canonical_bytes(&self) -> Vec<u8> {
self.to_canonical_bytes()
}
}
impl CanonicalHeapEncoding for Resource {
fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
match self {
Resource::Channel(channel) => encoder.nested(channel),
Resource::Message(message) => encoder.nested(message),
Resource::Session { role, type_hash } => {
encoder.string(role);
encoder.u64(*type_hash);
}
Resource::Value { tag, data } => {
encoder.string(tag);
encoder.bytes(data);
}
}
}
fn canonical_tag(&self) -> u8 {
match self {
Resource::Channel(_) => tag_resource_channel(),
Resource::Message(_) => tag_resource_message(),
Resource::Session { .. } => tag_resource_session(),
Resource::Value { .. } => tag_resource_value(),
}
}
}
impl fmt::Display for Resource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Resource({})", self.kind())
}
}
#[derive(Clone, PartialEq, Eq)]
pub enum HeapError<H: Hasher = DefaultContentHasher> {
NotFound(ResourceId<H>),
AlreadyConsumed(ResourceId<H>),
AlreadyExists(ResourceId<H>),
TypeMismatch { expected: String, got: String },
Other(String),
}
impl<H: Hasher> fmt::Debug for HeapError<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HeapError::NotFound(rid) => f.debug_tuple("NotFound").field(rid).finish(),
HeapError::AlreadyConsumed(rid) => f.debug_tuple("AlreadyConsumed").field(rid).finish(),
HeapError::AlreadyExists(rid) => f.debug_tuple("AlreadyExists").field(rid).finish(),
HeapError::TypeMismatch { expected, got } => f
.debug_struct("TypeMismatch")
.field("expected", expected)
.field("got", got)
.finish(),
HeapError::Other(msg) => f.debug_tuple("Other").field(msg).finish(),
}
}
}
impl<H: Hasher> fmt::Display for HeapError<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HeapError::NotFound(rid) => write!(f, "Resource not found: {}", rid),
HeapError::AlreadyConsumed(rid) => write!(f, "Resource already consumed: {}", rid),
HeapError::AlreadyExists(rid) => write!(f, "Resource already exists: {}", rid),
HeapError::TypeMismatch { expected, got } => {
write!(f, "Type mismatch: expected {}, got {}", expected, got)
}
HeapError::Other(msg) => write!(f, "{}", msg),
}
}
}
impl<H: Hasher> std::error::Error for HeapError<H> {}
#[cfg(test)]
mod tests {
use super::*;
use crate::heap::{HEAP_ENCODING_MAGIC, HEAP_ENCODING_VERSION};
use telltale_types::{Blake3Hasher, DefaultContentHasher, Hasher};
fn to_hex(bytes: &[u8]) -> String {
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
}
#[test]
fn test_resource_id_creation() {
let r1 = Resource::channel("Alice", "Bob");
let r2 = Resource::channel("Alice", "Bob");
let id1 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 0);
let id2 = ResourceId::<DefaultContentHasher>::from_resource(&r2, 0);
let id3 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 1);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_resource_id_fixed_vector() {
let resource = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
let resource_id = ResourceId::<DefaultContentHasher>::from_resource(&resource, 1);
assert_eq!(
to_hex(resource.canonical_bytes().as_slice()),
"545448500100415454485001003005000000416c69636503000000426f62545448500100100500000048656c6c6f030000000102030700000000000000"
);
assert_eq!(
to_hex(resource_id.as_bytes()),
"5a22a0e61e5faa3ea4c2bee86a92761eea62364727a77a4ed7c3a24c456afd8e"
);
}
#[test]
fn test_resource_id_ordering() {
let id1 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 0);
let id2 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 1);
assert!(id1 < id2);
}
#[test]
fn test_resource_canonical_bytes_use_versioned_encoding() {
let channel = Resource::channel("Alice", "Bob");
let bytes = channel.canonical_bytes();
assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 42);
let bytes = message.canonical_bytes();
assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
}
#[test]
fn test_channel_encoding_includes_full_queue_payloads() {
let channel = Resource::Channel(ChannelState {
sender: "Alice".into(),
receiver: "Bob".into(),
queue: vec![
MessagePayload::new("Ping", vec![1, 2, 3]),
MessagePayload::new("Pong", vec![4, 5, 6]),
],
});
let bytes = channel.canonical_bytes();
assert!(bytes.windows(4).any(|window| window == b"Ping"));
assert!(bytes.windows(4).any(|window| window == b"Pong"));
assert!(bytes.windows(3).any(|window| window == [1, 2, 3]));
assert!(bytes.windows(3).any(|window| window == [4, 5, 6]));
}
#[test]
fn test_message_encoding_includes_full_payload_bytes() {
let message = Resource::message("Alice", "Bob", "Hello", vec![7, 8, 9, 10], 42);
let bytes = message.canonical_bytes();
assert!(bytes.windows(5).any(|window| window == b"Hello"));
assert!(bytes.windows(4).any(|window| window == [7, 8, 9, 10]));
}
#[test]
fn test_canonical_encoding_is_stable_per_resource_kind() {
let channel = Resource::Channel(ChannelState {
sender: "Alice".into(),
receiver: "Bob".into(),
queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
});
let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
let session = Resource::session("Alice", 12345);
let value = Resource::value("blob", vec![9, 8, 7]);
assert_eq!(channel.canonical_bytes(), channel.canonical_bytes());
assert_eq!(message.canonical_bytes(), message.canonical_bytes());
assert_eq!(session.canonical_bytes(), session.canonical_bytes());
assert_eq!(value.canonical_bytes(), value.canonical_bytes());
}
#[test]
fn test_canonical_encoding_distinguishes_semantic_changes() {
let left = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
let right = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 4], 7);
assert_ne!(left.canonical_bytes(), right.canonical_bytes());
}
#[test]
fn test_canonical_encoding_matches_for_semantically_identical_values() {
let left = Resource::Channel(ChannelState {
sender: "Alice".into(),
receiver: "Bob".into(),
queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
});
let right = Resource::Channel(ChannelState {
sender: "Alice".into(),
receiver: "Bob".into(),
queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
});
assert_eq!(left.canonical_bytes(), right.canonical_bytes());
}
#[test]
fn test_message_creation() {
let msg = Message::new("Alice", "Bob", "Ping", vec![1, 2, 3], 1);
assert_eq!(msg.source, "Alice");
assert_eq!(msg.dest, "Bob");
assert_eq!(msg.content.label, "Ping");
assert_eq!(msg.seq_no, 1);
}
#[test]
fn test_channel_state() {
let mut channel = ChannelState::new("Alice", "Bob");
assert_eq!(channel.queue_size(), 0);
channel.queue.push(MessagePayload::new("Test", vec![]));
assert_eq!(channel.queue_size(), 1);
}
#[test]
fn test_heap_error_display() {
let rid = ResourceId::<DefaultContentHasher>::new([0u8; 32], 42);
let err = HeapError::NotFound(rid);
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_default_heap_hasher_is_blake3() {
let expected = Blake3Hasher::digest(b"heap");
let actual = DefaultContentHasher::digest(b"heap");
assert_eq!(expected, actual);
}
}