1use super::encoding::{
22 resource_id_preimage, tag_channel_state, tag_message, tag_message_payload,
23 tag_resource_channel, tag_resource_message, tag_resource_session, tag_resource_value,
24 CanonicalHeapEncoder, CanonicalHeapEncoding,
25};
26use std::fmt;
27use std::hash::{Hash, Hasher as StdHasher};
28use std::marker::PhantomData;
29use telltale_types::{DefaultContentHasher, Hasher};
30
31#[derive(Clone)]
37pub struct ResourceId<H: Hasher = DefaultContentHasher> {
38 hash: H::Digest,
40 counter: u64,
42 _hasher: PhantomData<H>,
43}
44
45impl<H: Hasher> ResourceId<H> {
46 pub fn new(hash: H::Digest, counter: u64) -> Self {
48 Self {
49 hash,
50 counter,
51 _hasher: PhantomData,
52 }
53 }
54
55 pub fn from_resource(resource: &Resource, counter: u64) -> Self {
60 let content_bytes = resource.canonical_bytes();
61 let preimage = resource_id_preimage(&content_bytes, counter);
62 let hash = H::digest(&preimage);
63
64 Self {
65 hash,
66 counter,
67 _hasher: PhantomData,
68 }
69 }
70
71 pub fn to_short_hex(&self) -> String {
73 let hex: String = self.hash.as_ref()[..4]
74 .iter()
75 .map(|b| format!("{:02x}", b))
76 .collect();
77 format!("{}:{}", hex, self.counter)
78 }
79
80 #[must_use]
82 pub fn hash(&self) -> &H::Digest {
83 &self.hash
84 }
85
86 #[must_use]
88 pub fn as_bytes(&self) -> &[u8] {
89 self.hash.as_ref()
90 }
91
92 #[must_use]
94 pub fn counter(&self) -> u64 {
95 self.counter
96 }
97}
98
99impl<H: Hasher> PartialEq for ResourceId<H> {
100 fn eq(&self, other: &Self) -> bool {
101 self.hash.as_ref() == other.hash.as_ref() && self.counter == other.counter
102 }
103}
104
105impl<H: Hasher> Eq for ResourceId<H> {}
106
107impl<H: Hasher> Hash for ResourceId<H> {
108 fn hash<S: StdHasher>(&self, state: &mut S) {
109 self.hash.as_ref().hash(state);
110 self.counter.hash(state);
111 }
112}
113
114impl<H: Hasher> fmt::Debug for ResourceId<H> {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 write!(
117 f,
118 "ResourceId<{}>({})",
119 H::algorithm_name(),
120 self.to_short_hex()
121 )
122 }
123}
124
125impl<H: Hasher> fmt::Display for ResourceId<H> {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 write!(f, "{}", self.to_short_hex())
128 }
129}
130
131impl<H: Hasher> PartialOrd for ResourceId<H> {
132 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
133 Some(self.cmp(other))
134 }
135}
136
137impl<H: Hasher> Ord for ResourceId<H> {
138 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
139 match self.hash.as_ref().cmp(other.hash.as_ref()) {
140 std::cmp::Ordering::Equal => self.counter.cmp(&other.counter),
141 ord => ord,
142 }
143 }
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct MessagePayload {
149 pub label: String,
151 pub payload: Vec<u8>,
153}
154
155impl CanonicalHeapEncoding for MessagePayload {
156 fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
157 encoder.string(&self.label);
158 encoder.bytes(&self.payload);
159 }
160
161 fn canonical_tag(&self) -> u8 {
162 tag_message_payload()
163 }
164}
165
166impl MessagePayload {
167 pub fn new(label: impl Into<String>, payload: Vec<u8>) -> Self {
169 Self {
170 label: label.into(),
171 payload,
172 }
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ChannelState {
179 pub sender: String,
181 pub receiver: String,
183 pub queue: Vec<MessagePayload>,
185}
186
187impl CanonicalHeapEncoding for ChannelState {
188 fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
189 encoder.string(&self.sender);
190 encoder.string(&self.receiver);
191 encoder.u32(self.queue.len() as u32);
192 for payload in &self.queue {
193 encoder.nested(payload);
194 }
195 }
196
197 fn canonical_tag(&self) -> u8 {
198 tag_channel_state()
199 }
200}
201
202impl ChannelState {
203 pub fn new(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
205 Self {
206 sender: sender.into(),
207 receiver: receiver.into(),
208 queue: Vec::new(),
209 }
210 }
211
212 pub fn queue_size(&self) -> usize {
214 self.queue.len()
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct Message {
221 pub source: String,
223 pub dest: String,
225 pub content: MessagePayload,
227 pub seq_no: u64,
229}
230
231impl CanonicalHeapEncoding for Message {
232 fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
233 encoder.string(&self.source);
234 encoder.string(&self.dest);
235 encoder.nested(&self.content);
236 encoder.u64(self.seq_no);
237 }
238
239 fn canonical_tag(&self) -> u8 {
240 tag_message()
241 }
242}
243
244impl Message {
245 pub fn new(
247 source: impl Into<String>,
248 dest: impl Into<String>,
249 label: impl Into<String>,
250 payload: Vec<u8>,
251 seq_no: u64,
252 ) -> Self {
253 Self {
254 source: source.into(),
255 dest: dest.into(),
256 content: MessagePayload::new(label, payload),
257 seq_no,
258 }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq)]
266pub enum Resource {
267 Channel(ChannelState),
269 Message(Message),
271 Session {
273 role: String,
275 type_hash: u64,
277 },
278 Value {
280 tag: String,
282 data: Vec<u8>,
284 },
285}
286
287impl Resource {
288 pub fn channel(sender: impl Into<String>, receiver: impl Into<String>) -> Self {
290 Resource::Channel(ChannelState::new(sender, receiver))
291 }
292
293 pub fn message(
295 source: impl Into<String>,
296 dest: impl Into<String>,
297 label: impl Into<String>,
298 payload: Vec<u8>,
299 seq_no: u64,
300 ) -> Self {
301 Resource::Message(Message::new(source, dest, label, payload, seq_no))
302 }
303
304 pub fn session(role: impl Into<String>, type_hash: u64) -> Self {
306 Resource::Session {
307 role: role.into(),
308 type_hash,
309 }
310 }
311
312 pub fn value(tag: impl Into<String>, data: Vec<u8>) -> Self {
314 Resource::Value {
315 tag: tag.into(),
316 data,
317 }
318 }
319
320 pub fn kind(&self) -> &'static str {
322 match self {
323 Resource::Channel(_) => "channel",
324 Resource::Message(_) => "message",
325 Resource::Session { .. } => "session",
326 Resource::Value { .. } => "value",
327 }
328 }
329
330 pub fn canonical_bytes(&self) -> Vec<u8> {
335 self.to_canonical_bytes()
336 }
337}
338
339impl CanonicalHeapEncoding for Resource {
340 fn encode_canonical_body(&self, encoder: &mut CanonicalHeapEncoder) {
341 match self {
342 Resource::Channel(channel) => encoder.nested(channel),
343 Resource::Message(message) => encoder.nested(message),
344 Resource::Session { role, type_hash } => {
345 encoder.string(role);
346 encoder.u64(*type_hash);
347 }
348 Resource::Value { tag, data } => {
349 encoder.string(tag);
350 encoder.bytes(data);
351 }
352 }
353 }
354
355 fn canonical_tag(&self) -> u8 {
356 match self {
357 Resource::Channel(_) => tag_resource_channel(),
358 Resource::Message(_) => tag_resource_message(),
359 Resource::Session { .. } => tag_resource_session(),
360 Resource::Value { .. } => tag_resource_value(),
361 }
362 }
363}
364
365impl fmt::Display for Resource {
366 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367 write!(f, "Resource({})", self.kind())
368 }
369}
370
371#[derive(Clone, PartialEq, Eq)]
373pub enum HeapError<H: Hasher = DefaultContentHasher> {
374 NotFound(ResourceId<H>),
376 AlreadyConsumed(ResourceId<H>),
378 AlreadyExists(ResourceId<H>),
380 TypeMismatch { expected: String, got: String },
382 Other(String),
384}
385
386impl<H: Hasher> fmt::Debug for HeapError<H> {
387 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
388 match self {
389 HeapError::NotFound(rid) => f.debug_tuple("NotFound").field(rid).finish(),
390 HeapError::AlreadyConsumed(rid) => f.debug_tuple("AlreadyConsumed").field(rid).finish(),
391 HeapError::AlreadyExists(rid) => f.debug_tuple("AlreadyExists").field(rid).finish(),
392 HeapError::TypeMismatch { expected, got } => f
393 .debug_struct("TypeMismatch")
394 .field("expected", expected)
395 .field("got", got)
396 .finish(),
397 HeapError::Other(msg) => f.debug_tuple("Other").field(msg).finish(),
398 }
399 }
400}
401
402impl<H: Hasher> fmt::Display for HeapError<H> {
403 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404 match self {
405 HeapError::NotFound(rid) => write!(f, "Resource not found: {}", rid),
406 HeapError::AlreadyConsumed(rid) => write!(f, "Resource already consumed: {}", rid),
407 HeapError::AlreadyExists(rid) => write!(f, "Resource already exists: {}", rid),
408 HeapError::TypeMismatch { expected, got } => {
409 write!(f, "Type mismatch: expected {}, got {}", expected, got)
410 }
411 HeapError::Other(msg) => write!(f, "{}", msg),
412 }
413 }
414}
415
416impl<H: Hasher> std::error::Error for HeapError<H> {}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::heap::{HEAP_ENCODING_MAGIC, HEAP_ENCODING_VERSION};
422 use telltale_types::{Blake3Hasher, DefaultContentHasher, Hasher};
423
424 fn to_hex(bytes: &[u8]) -> String {
425 bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
426 }
427
428 #[test]
429 fn test_resource_id_creation() {
430 let r1 = Resource::channel("Alice", "Bob");
431 let r2 = Resource::channel("Alice", "Bob");
432
433 let id1 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 0);
434 let id2 = ResourceId::<DefaultContentHasher>::from_resource(&r2, 0);
435 let id3 = ResourceId::<DefaultContentHasher>::from_resource(&r1, 1);
436
437 assert_eq!(id1, id2);
439 assert_ne!(id1, id3);
441 }
442
443 #[test]
444 fn test_resource_id_fixed_vector() {
445 let resource = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
446 let resource_id = ResourceId::<DefaultContentHasher>::from_resource(&resource, 1);
447
448 assert_eq!(
449 to_hex(resource.canonical_bytes().as_slice()),
450 "545448500100415454485001003005000000416c69636503000000426f62545448500100100500000048656c6c6f030000000102030700000000000000"
451 );
452 assert_eq!(
453 to_hex(resource_id.as_bytes()),
454 "5a22a0e61e5faa3ea4c2bee86a92761eea62364727a77a4ed7c3a24c456afd8e"
455 );
456 }
457
458 #[test]
459 fn test_resource_id_ordering() {
460 let id1 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 0);
461 let id2 = ResourceId::<DefaultContentHasher>::new([0u8; 32], 1);
462
463 assert!(id1 < id2);
465 }
466
467 #[test]
468 fn test_resource_canonical_bytes_use_versioned_encoding() {
469 let channel = Resource::channel("Alice", "Bob");
470 let bytes = channel.canonical_bytes();
471 assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
472 assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
473
474 let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 42);
475 let bytes = message.canonical_bytes();
476 assert_eq!(&bytes[..4], &HEAP_ENCODING_MAGIC);
477 assert_eq!(&bytes[4..6], &HEAP_ENCODING_VERSION.to_le_bytes());
478 }
479
480 #[test]
481 fn test_channel_encoding_includes_full_queue_payloads() {
482 let channel = Resource::Channel(ChannelState {
483 sender: "Alice".into(),
484 receiver: "Bob".into(),
485 queue: vec![
486 MessagePayload::new("Ping", vec![1, 2, 3]),
487 MessagePayload::new("Pong", vec![4, 5, 6]),
488 ],
489 });
490 let bytes = channel.canonical_bytes();
491
492 assert!(bytes.windows(4).any(|window| window == b"Ping"));
493 assert!(bytes.windows(4).any(|window| window == b"Pong"));
494 assert!(bytes.windows(3).any(|window| window == [1, 2, 3]));
495 assert!(bytes.windows(3).any(|window| window == [4, 5, 6]));
496 }
497
498 #[test]
499 fn test_message_encoding_includes_full_payload_bytes() {
500 let message = Resource::message("Alice", "Bob", "Hello", vec![7, 8, 9, 10], 42);
501 let bytes = message.canonical_bytes();
502
503 assert!(bytes.windows(5).any(|window| window == b"Hello"));
504 assert!(bytes.windows(4).any(|window| window == [7, 8, 9, 10]));
505 }
506
507 #[test]
508 fn test_canonical_encoding_is_stable_per_resource_kind() {
509 let channel = Resource::Channel(ChannelState {
510 sender: "Alice".into(),
511 receiver: "Bob".into(),
512 queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
513 });
514 let message = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
515 let session = Resource::session("Alice", 12345);
516 let value = Resource::value("blob", vec![9, 8, 7]);
517
518 assert_eq!(channel.canonical_bytes(), channel.canonical_bytes());
519 assert_eq!(message.canonical_bytes(), message.canonical_bytes());
520 assert_eq!(session.canonical_bytes(), session.canonical_bytes());
521 assert_eq!(value.canonical_bytes(), value.canonical_bytes());
522 }
523
524 #[test]
525 fn test_canonical_encoding_distinguishes_semantic_changes() {
526 let left = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 3], 7);
527 let right = Resource::message("Alice", "Bob", "Hello", vec![1, 2, 4], 7);
528
529 assert_ne!(left.canonical_bytes(), right.canonical_bytes());
530 }
531
532 #[test]
533 fn test_canonical_encoding_matches_for_semantically_identical_values() {
534 let left = Resource::Channel(ChannelState {
535 sender: "Alice".into(),
536 receiver: "Bob".into(),
537 queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
538 });
539 let right = Resource::Channel(ChannelState {
540 sender: "Alice".into(),
541 receiver: "Bob".into(),
542 queue: vec![MessagePayload::new("Ping", vec![1, 2, 3])],
543 });
544
545 assert_eq!(left.canonical_bytes(), right.canonical_bytes());
546 }
547
548 #[test]
549 fn test_message_creation() {
550 let msg = Message::new("Alice", "Bob", "Ping", vec![1, 2, 3], 1);
551 assert_eq!(msg.source, "Alice");
552 assert_eq!(msg.dest, "Bob");
553 assert_eq!(msg.content.label, "Ping");
554 assert_eq!(msg.seq_no, 1);
555 }
556
557 #[test]
558 fn test_channel_state() {
559 let mut channel = ChannelState::new("Alice", "Bob");
560 assert_eq!(channel.queue_size(), 0);
561
562 channel.queue.push(MessagePayload::new("Test", vec![]));
563 assert_eq!(channel.queue_size(), 1);
564 }
565
566 #[test]
567 fn test_heap_error_display() {
568 let rid = ResourceId::<DefaultContentHasher>::new([0u8; 32], 42);
569 let err = HeapError::NotFound(rid);
570 assert!(err.to_string().contains("not found"));
571 }
572
573 #[test]
574 fn test_default_heap_hasher_is_blake3() {
575 let expected = Blake3Hasher::digest(b"heap");
576 let actual = DefaultContentHasher::digest(b"heap");
577 assert_eq!(expected, actual);
578 }
579}