1use crate::types::{Attachment, EditEntry, ImageMetadata, Reaction, SiteMetadata};
14use crate::simd::hex::{bytes_to_hex_32, bytes_to_hex_string, hex_to_bytes_32};
15
16fn hex_to_bytes_16(hex: &str) -> [u8; 16] {
19 let mut out = [0u8; 16];
20 let h = hex.as_bytes();
21 let mut padded = [b'0'; 32];
23 let copy_len = h.len().min(32);
24 padded[..copy_len].copy_from_slice(&h[..copy_len]);
25 for i in 0..16 {
26 let hi = hex_nibble(padded[i * 2]);
27 let lo = hex_nibble(padded[i * 2 + 1]);
28 out[i] = (hi << 4) | lo;
29 }
30 out
31}
32
33#[inline]
34fn hex_nibble(b: u8) -> u8 {
35 match b {
36 b'0'..=b'9' => b - b'0',
37 b'a'..=b'f' => b - b'a' + 10,
38 b'A'..=b'F' => b - b'A' + 10,
39 _ => 0,
40 }
41}
42
43const PENDING_ID_MARKER: u8 = 0x01;
50
51#[inline]
55pub fn encode_message_id(id: &str) -> [u8; 32] {
56 if let Some(timestamp_str) = id.strip_prefix("pending-") {
57 let mut bytes = [0u8; 32];
59 bytes[0] = PENDING_ID_MARKER;
60 if let Ok(timestamp) = timestamp_str.parse::<u128>() {
61 bytes[1..17].copy_from_slice(×tamp.to_le_bytes());
62 }
63 bytes
64 } else {
65 hex_to_bytes_32(id)
66 }
67}
68
69#[inline]
71pub fn decode_message_id(bytes: &[u8; 32]) -> String {
72 if bytes[0] == PENDING_ID_MARKER {
73 let mut timestamp_bytes = [0u8; 16];
75 timestamp_bytes.copy_from_slice(&bytes[1..17]);
76 let timestamp = u128::from_le_bytes(timestamp_bytes);
77 format!("pending-{}", timestamp)
78 } else {
79 bytes_to_hex_32(bytes)
80 }
81}
82
83#[inline]
90pub fn timestamp_to_compact(ms: u64) -> u64 {
91 ms
92}
93
94#[inline]
96pub fn timestamp_from_compact(compact: u64) -> u64 {
97 compact
98}
99
100const EPOCH_2020_SECS: u64 = 1577836800;
102
103#[inline]
106pub fn secs_to_compact(secs: u64) -> u32 {
107 if secs == 0 { return 0; }
108 secs.saturating_sub(EPOCH_2020_SECS) as u32
109}
110
111#[inline]
114pub fn secs_from_compact(compact: u32) -> u64 {
115 if compact == 0 { return 0; }
116 EPOCH_2020_SECS + compact as u64
117}
118
119#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
130pub struct MessageFlags(u8);
131
132impl MessageFlags {
133 pub const NONE: Self = Self(0);
134 pub const MINE: Self = Self(0b00001);
135 pub const PENDING: Self = Self(0b00010);
136 pub const FAILED: Self = Self(0b00100);
137 const REPLY_ATTACH_MASK: u8 = 0b11000;
140 const REPLY_ATTACH_SHIFT: u8 = 3;
141
142 #[inline]
143 pub fn is_mine(self) -> bool {
144 self.0 & Self::MINE.0 != 0
145 }
146
147 #[inline]
148 pub fn is_pending(self) -> bool {
149 self.0 & Self::PENDING.0 != 0
150 }
151
152 #[inline]
153 pub fn is_failed(self) -> bool {
154 self.0 & Self::FAILED.0 != 0
155 }
156
157 #[inline]
160 pub fn replied_to_has_attachment(self) -> Option<bool> {
161 match (self.0 & Self::REPLY_ATTACH_MASK) >> Self::REPLY_ATTACH_SHIFT {
162 0b00 => None, 0b01 => Some(false), 0b10 => Some(true), _ => None, }
167 }
168
169 #[inline]
170 pub fn set_mine(&mut self, value: bool) {
171 if value {
172 self.0 |= Self::MINE.0;
173 } else {
174 self.0 &= !Self::MINE.0;
175 }
176 }
177
178 #[inline]
179 pub fn set_pending(&mut self, value: bool) {
180 if value {
181 self.0 |= Self::PENDING.0;
182 } else {
183 self.0 &= !Self::PENDING.0;
184 }
185 }
186
187 #[inline]
188 pub fn set_failed(&mut self, value: bool) {
189 if value {
190 self.0 |= Self::FAILED.0;
191 } else {
192 self.0 &= !Self::FAILED.0;
193 }
194 }
195
196 #[inline]
198 pub fn set_replied_to_has_attachment(&mut self, value: Option<bool>) {
199 self.0 &= !Self::REPLY_ATTACH_MASK;
201 let bits = match value {
203 None => 0b00,
204 Some(false) => 0b01,
205 Some(true) => 0b10,
206 };
207 self.0 |= bits << Self::REPLY_ATTACH_SHIFT;
208 }
209
210 #[inline]
212 pub fn from_bools(mine: bool, pending: bool, failed: bool) -> Self {
213 let mut flags = Self::NONE;
214 flags.set_mine(mine);
215 flags.set_pending(pending);
216 flags.set_failed(failed);
217 flags
218 }
219
220 #[inline]
222 pub fn from_all(mine: bool, pending: bool, failed: bool, replied_to_has_attachment: Option<bool>) -> Self {
223 let mut flags = Self::from_bools(mine, pending, failed);
224 flags.set_replied_to_has_attachment(replied_to_has_attachment);
225 flags
226 }
227}
228
229use std::alloc::{alloc, dealloc, Layout};
234use std::marker::PhantomData;
235use std::ptr::NonNull;
236
237pub struct TinyVec<T> {
253 ptr: Option<NonNull<u8>>,
255 _marker: PhantomData<T>,
256}
257
258impl<T> TinyVec<T> {
259 #[inline]
261 pub const fn new() -> Self {
262 Self {
263 ptr: None,
264 _marker: PhantomData,
265 }
266 }
267
268 pub fn from_vec(vec: Vec<T>) -> Self {
270 if vec.is_empty() {
271 return Self::new();
272 }
273
274 let len = vec.len().min(255) as u8;
275
276 let (layout, items_offset) = Self::layout_for(len as usize);
278
279 unsafe {
280 let ptr = alloc(layout);
282 if ptr.is_null() {
283 std::alloc::handle_alloc_error(layout);
284 }
285
286 *ptr = len;
288
289 let items_ptr = ptr.add(items_offset) as *mut T;
291 for (i, item) in vec.into_iter().take(len as usize).enumerate() {
292 std::ptr::write(items_ptr.add(i), item);
293 }
294
295 Self {
296 ptr: NonNull::new(ptr),
297 _marker: PhantomData,
298 }
299 }
300 }
301
302 fn layout_for(len: usize) -> (Layout, usize) {
304 let header_layout = Layout::new::<u8>();
305 let items_layout = Layout::array::<T>(len).unwrap();
306 header_layout.extend(items_layout).unwrap()
307 }
308
309 #[inline]
311 pub fn len(&self) -> usize {
312 match self.ptr {
313 None => 0,
314 Some(ptr) => unsafe { *ptr.as_ptr() as usize },
315 }
316 }
317
318 #[inline]
319 pub fn is_empty(&self) -> bool {
320 self.ptr.is_none()
321 }
322
323 #[inline]
325 fn items_offset() -> usize {
326 let header_layout = Layout::new::<u8>();
327 let items_layout = Layout::new::<T>();
328 header_layout.extend(items_layout).map(|(_, offset)| offset).unwrap_or(1)
329 }
330
331 #[inline]
333 pub fn as_slice(&self) -> &[T] {
334 match self.ptr {
335 None => &[],
336 Some(ptr) => unsafe {
337 let base = ptr.as_ptr();
338 let len = *base as usize;
339 let items_ptr = base.add(Self::items_offset()) as *const T;
340 std::slice::from_raw_parts(items_ptr, len)
341 },
342 }
343 }
344
345 #[inline]
347 pub fn as_mut_slice(&mut self) -> &mut [T] {
348 match self.ptr {
349 None => &mut [],
350 Some(ptr) => unsafe {
351 let base = ptr.as_ptr();
352 let len = *base as usize;
353 let items_ptr = base.add(Self::items_offset()) as *mut T;
354 std::slice::from_raw_parts_mut(items_ptr, len)
355 },
356 }
357 }
358
359 #[inline]
361 pub fn iter(&self) -> std::slice::Iter<'_, T> {
362 self.as_slice().iter()
363 }
364
365 #[inline]
367 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
368 self.as_mut_slice().iter_mut()
369 }
370
371 pub fn to_vec(&self) -> Vec<T>
373 where
374 T: Clone,
375 {
376 self.as_slice().to_vec()
377 }
378
379 #[inline]
381 pub fn first(&self) -> Option<&T> {
382 self.as_slice().first()
383 }
384
385 #[inline]
387 pub fn last(&self) -> Option<&T> {
388 self.as_slice().last()
389 }
390
391 #[inline]
393 pub fn last_mut(&mut self) -> Option<&mut T> {
394 self.as_mut_slice().last_mut()
395 }
396
397 #[inline]
399 pub fn get(&self, index: usize) -> Option<&T> {
400 self.as_slice().get(index)
401 }
402
403 #[inline]
405 pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
406 self.as_mut_slice().get_mut(index)
407 }
408
409 pub fn push(&mut self, item: T)
411 where
412 T: Clone,
413 {
414 let mut vec = self.to_vec();
415 vec.push(item);
416 *self = Self::from_vec(vec);
417 }
418
419 pub fn retain<F>(&mut self, f: F)
421 where
422 T: Clone,
423 F: FnMut(&T) -> bool,
424 {
425 let mut vec = self.to_vec();
426 vec.retain(f);
427 *self = Self::from_vec(vec);
428 }
429
430 pub fn any<F>(&self, f: F) -> bool
432 where
433 F: FnMut(&T) -> bool,
434 {
435 self.as_slice().iter().any(f)
436 }
437}
438
439impl<T> std::ops::Index<usize> for TinyVec<T> {
441 type Output = T;
442
443 fn index(&self, index: usize) -> &Self::Output {
444 &self.as_slice()[index]
445 }
446}
447
448impl<T> std::ops::IndexMut<usize> for TinyVec<T> {
449 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
450 &mut self.as_mut_slice()[index]
451 }
452}
453
454impl<'a, T> IntoIterator for &'a TinyVec<T> {
456 type Item = &'a T;
457 type IntoIter = std::slice::Iter<'a, T>;
458
459 fn into_iter(self) -> Self::IntoIter {
460 self.as_slice().iter()
461 }
462}
463
464impl<'a, T> IntoIterator for &'a mut TinyVec<T> {
466 type Item = &'a mut T;
467 type IntoIter = std::slice::IterMut<'a, T>;
468
469 fn into_iter(self) -> Self::IntoIter {
470 self.as_mut_slice().iter_mut()
471 }
472}
473
474impl<T> Default for TinyVec<T> {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480impl<T: Clone> Clone for TinyVec<T> {
481 fn clone(&self) -> Self {
482 Self::from_vec(self.to_vec())
483 }
484}
485
486impl<T: std::fmt::Debug> std::fmt::Debug for TinyVec<T> {
487 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
488 f.debug_list().entries(self.as_slice()).finish()
489 }
490}
491
492impl<T> Drop for TinyVec<T> {
493 fn drop(&mut self) {
494 if let Some(ptr) = self.ptr {
495 unsafe {
496 let base = ptr.as_ptr();
497 let len = *base as usize;
498 let items_ptr = base.add(Self::items_offset()) as *mut T;
499
500 for i in 0..len {
502 std::ptr::drop_in_place(items_ptr.add(i));
503 }
504
505 let (layout, _) = Self::layout_for(len);
507 dealloc(base, layout);
508 }
509 }
510 }
511}
512
513unsafe impl<T: Send> Send for TinyVec<T> {}
515unsafe impl<T: Sync> Sync for TinyVec<T> {}
516
517#[derive(Clone, Debug)]
529pub struct CompactReaction {
530 pub id: [u8; 32],
532 pub reference_id: [u8; 32],
534 pub author_idx: u16,
536 pub emoji: Box<str>,
538 pub emoji_url: Option<Box<str>>,
541}
542
543impl CompactReaction {
544 #[inline]
546 pub fn id_hex(&self) -> String {
547 bytes_to_hex_32(&self.id)
548 }
549
550 #[inline]
552 pub fn reference_id_hex(&self) -> String {
553 bytes_to_hex_32(&self.reference_id)
554 }
555
556 pub fn from_reaction(reaction: &Reaction, interner: &mut NpubInterner) -> Self {
558 Self {
559 id: hex_to_bytes_32(&reaction.id),
560 reference_id: hex_to_bytes_32(&reaction.reference_id),
561 author_idx: interner.intern(&reaction.author_id),
562 emoji: reaction.emoji.clone().into_boxed_str(),
563 emoji_url: reaction.emoji_url.as_deref().map(|s| s.into()),
564 }
565 }
566
567 pub fn from_reaction_owned(reaction: Reaction, interner: &mut NpubInterner) -> Self {
569 Self {
570 id: hex_to_bytes_32(&reaction.id),
571 reference_id: hex_to_bytes_32(&reaction.reference_id),
572 author_idx: interner.intern(&reaction.author_id),
573 emoji: reaction.emoji.into_boxed_str(),
574 emoji_url: reaction.emoji_url.map(|s| s.into_boxed_str()),
575 }
576 }
577
578 pub fn to_reaction(&self, interner: &NpubInterner) -> Reaction {
580 Reaction {
581 id: self.id_hex(),
582 reference_id: self.reference_id_hex(),
583 author_id: interner.resolve(self.author_idx)
584 .map(|s| s.to_string())
585 .unwrap_or_default(),
586 emoji: self.emoji.to_string(),
587 emoji_url: self.emoji_url.as_deref().map(|s| s.to_string()),
588 }
589 }
590}
591
592#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
598pub struct AttachmentFlags(u8);
599
600impl AttachmentFlags {
601 pub const NONE: Self = Self(0);
602 const DOWNLOADING: u8 = 0b0001;
603 const DOWNLOADED: u8 = 0b0010;
604 const SHORT_NONCE: u8 = 0b0100; #[inline]
607 pub fn is_downloading(self) -> bool { self.0 & Self::DOWNLOADING != 0 }
608 #[inline]
609 pub fn is_downloaded(self) -> bool { self.0 & Self::DOWNLOADED != 0 }
610 #[inline]
611 pub fn is_short_nonce(self) -> bool { self.0 & Self::SHORT_NONCE != 0 }
612
613 #[inline]
614 pub fn set_downloading(&mut self, value: bool) {
615 if value { self.0 |= Self::DOWNLOADING; } else { self.0 &= !Self::DOWNLOADING; }
616 }
617 #[inline]
618 pub fn set_downloaded(&mut self, value: bool) {
619 if value { self.0 |= Self::DOWNLOADED; } else { self.0 &= !Self::DOWNLOADED; }
620 }
621 #[inline]
622 pub fn set_short_nonce(&mut self, value: bool) {
623 if value { self.0 |= Self::SHORT_NONCE; } else { self.0 &= !Self::SHORT_NONCE; }
624 }
625
626 pub fn from_bools(downloading: bool, downloaded: bool) -> Self {
627 let mut flags = Self::NONE;
628 flags.set_downloading(downloading);
629 flags.set_downloaded(downloaded);
630 flags
631 }
632}
633
634#[derive(Clone, Debug)]
645pub struct CompactAttachment {
646 pub id: [u8; 32],
649 pub key: [u8; 32],
651 pub nonce: [u8; 16],
653 pub size: u64,
655 pub flags: AttachmentFlags,
657
658 pub extension: Box<str>,
661 pub url: Box<str>,
663 pub path: Box<str>,
665
666 pub img_meta: Option<Box<ImageMetadata>>,
669 pub group_id: Option<Box<[u8; 32]>>,
671 pub original_hash: Option<Box<[u8; 32]>>,
673 pub webxdc_topic: Option<Box<str>>,
675 pub mls_filename: Option<Box<str>>,
677 pub scheme_version: Option<Box<str>>,
679 pub name: String,
681}
682
683impl CompactAttachment {
684 #[inline]
686 pub fn downloaded(&self) -> bool { self.flags.is_downloaded() }
687 #[inline]
688 pub fn downloading(&self) -> bool { self.flags.is_downloading() }
689 #[inline]
690 pub fn set_downloaded(&mut self, value: bool) { self.flags.set_downloaded(value); }
691 #[inline]
692 pub fn set_downloading(&mut self, value: bool) { self.flags.set_downloading(value); }
693
694 #[inline]
696 pub fn id_eq(&self, hex_id: &str) -> bool {
697 self.id == hex_to_bytes_32(hex_id)
698 }
699
700 #[inline]
702 pub fn id_hex(&self) -> String {
703 bytes_to_hex_32(&self.id)
704 }
705
706 pub fn key_hex(&self) -> String {
708 if self.key == [0u8; 32] {
709 String::new()
710 } else {
711 bytes_to_hex_32(&self.key)
712 }
713 }
714
715 pub fn nonce_hex(&self) -> String {
717 if self.nonce == [0u8; 16] {
718 String::new()
719 } else if self.flags.is_short_nonce() {
720 bytes_to_hex_string(&self.nonce[..12])
722 } else {
723 bytes_to_hex_string(&self.nonce)
725 }
726 }
727
728 pub fn from_attachment(att: &Attachment) -> Self {
730 let is_short_nonce = att.nonce.len() == 24;
732 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
733 flags.set_short_nonce(is_short_nonce);
734
735 Self {
736 id: hex_to_bytes_32(&att.id),
737 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
738 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
739 size: att.size,
740 flags,
741 extension: att.extension.clone().into_boxed_str(),
742 url: att.url.clone().into_boxed_str(),
743 path: att.path.clone().into_boxed_str(),
744 img_meta: att.img_meta.clone().map(Box::new),
745 group_id: att.group_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
746 original_hash: att.original_hash.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
747 webxdc_topic: att.webxdc_topic.clone().map(|s| s.into_boxed_str()),
748 mls_filename: att.mls_filename.clone().map(|s| s.into_boxed_str()),
749 scheme_version: att.scheme_version.clone().map(|s| s.into_boxed_str()),
750 name: att.name.clone(),
751 }
752 }
753
754 pub fn from_attachment_owned(att: Attachment) -> Self {
756 let is_short_nonce = att.nonce.len() == 24;
758 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
759 flags.set_short_nonce(is_short_nonce);
760
761 Self {
762 id: hex_to_bytes_32(&att.id),
763 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
764 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
765 size: att.size,
766 flags,
767 extension: att.extension.into_boxed_str(),
768 url: att.url.into_boxed_str(),
769 path: att.path.into_boxed_str(),
770 img_meta: att.img_meta.map(Box::new),
771 group_id: att.group_id.map(|s| Box::new(hex_to_bytes_32(&s))),
772 original_hash: att.original_hash.map(|s| Box::new(hex_to_bytes_32(&s))),
773 webxdc_topic: att.webxdc_topic.map(|s| s.into_boxed_str()),
774 mls_filename: att.mls_filename.map(|s| s.into_boxed_str()),
775 scheme_version: att.scheme_version.map(|s| s.into_boxed_str()),
776 name: att.name,
777 }
778 }
779
780 pub fn to_attachment(&self) -> Attachment {
782 Attachment {
783 id: self.id_hex(),
784 key: self.key_hex(),
785 nonce: self.nonce_hex(),
786 extension: self.extension.to_string(),
787 name: self.name.clone(),
788 url: self.url.to_string(),
789 path: self.path.to_string(),
790 size: self.size,
791 img_meta: self.img_meta.as_ref().map(|b| (**b).clone()),
792 downloading: self.flags.is_downloading(),
793 downloaded: self.flags.is_downloaded(),
794 webxdc_topic: self.webxdc_topic.as_ref().map(|s| s.to_string()),
795 group_id: self.group_id.as_ref().map(|b| bytes_to_hex_32(b)),
796 original_hash: self.original_hash.as_ref().map(|b| bytes_to_hex_32(b)),
797 scheme_version: self.scheme_version.as_ref().map(|s| s.to_string()),
798 mls_filename: self.mls_filename.as_ref().map(|s| s.to_string()),
799 }
800 }
801}
802
803#[inline]
807fn parse_nonce(hex: &str) -> [u8; 16] {
808 hex_to_bytes_16(hex)
809}
810
811#[derive(Clone, Debug, Default)]
823pub struct NpubInterner {
824 npubs: Vec<String>,
826 sorted: Vec<u16>,
828}
829
830pub const NO_NPUB: u16 = u16::MAX;
832
833impl NpubInterner {
834 pub fn new() -> Self {
835 Self {
836 npubs: Vec::new(),
837 sorted: Vec::new(),
838 }
839 }
840
841 pub fn with_capacity(capacity: usize) -> Self {
843 Self {
844 npubs: Vec::with_capacity(capacity),
845 sorted: Vec::with_capacity(capacity),
846 }
847 }
848
849 pub fn intern(&mut self, npub: &str) -> u16 {
854 let result = self.sorted.binary_search_by(|&idx| {
856 self.npubs[idx as usize].as_str().cmp(npub)
857 });
858
859 match result {
860 Ok(pos) => self.sorted[pos], Err(insert_pos) => {
862 if self.npubs.len() >= NO_NPUB as usize {
865 return NO_NPUB;
866 }
867 let new_idx = self.npubs.len() as u16;
869 self.npubs.push(npub.to_string());
870 self.sorted.insert(insert_pos, new_idx);
871 new_idx
872 }
873 }
874 }
875
876 #[inline]
878 pub fn intern_opt(&mut self, npub: Option<&str>) -> u16 {
879 match npub {
880 Some(s) if !s.is_empty() => self.intern(s),
881 _ => NO_NPUB,
882 }
883 }
884
885 pub fn lookup(&self, npub: &str) -> Option<u16> {
887 self.sorted.binary_search_by(|&idx| {
888 self.npubs[idx as usize].as_str().cmp(npub)
889 }).ok().map(|pos| self.sorted[pos])
890 }
891
892 #[inline]
896 pub fn resolve(&self, idx: u16) -> Option<&str> {
897 if idx == NO_NPUB {
898 return None;
899 }
900 self.npubs.get(idx as usize).map(|s| s.as_str())
901 }
902
903 #[inline]
905 pub fn len(&self) -> usize {
906 self.npubs.len()
907 }
908
909 #[inline]
910 pub fn is_empty(&self) -> bool {
911 self.npubs.is_empty()
912 }
913
914 pub fn memory_usage(&self) -> usize {
916 std::mem::size_of::<Self>()
917 + self.npubs.capacity() * std::mem::size_of::<String>()
918 + self.npubs.iter().map(|s| s.capacity()).sum::<usize>()
919 + self.sorted.capacity() * std::mem::size_of::<u16>()
920 }
921}
922
923#[derive(Clone, Debug)]
939pub struct CompactMessage {
940 pub id: [u8; 32],
942 pub at: u64,
944 pub flags: MessageFlags,
946 pub npub_idx: u16,
948 pub replied_to: Option<Box<[u8; 32]>>,
950 pub replied_to_npub_idx: u16,
952 pub wrapper_id: Option<Box<[u8; 32]>>,
954
955 pub content: Box<str>,
958 pub replied_to_content: Option<Box<str>>,
960 pub attachments: TinyVec<CompactAttachment>,
962 pub reactions: TinyVec<CompactReaction>,
964 #[allow(clippy::box_collection)]
966 pub edit_history: Option<Box<Vec<EditEntry>>>,
967 pub preview_metadata: Option<Box<SiteMetadata>>,
969 #[allow(clippy::box_collection)]
972 pub emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
973}
974
975impl CompactMessage {
976 #[inline]
978 pub fn has_reply(&self) -> bool {
979 self.replied_to.is_some()
980 }
981
982 #[inline]
984 pub fn is_edited(&self) -> bool {
985 self.edit_history.is_some()
986 }
987
988 #[inline]
990 pub fn id_hex(&self) -> String {
991 decode_message_id(&self.id)
992 }
993
994 #[inline]
996 pub fn replied_to_hex(&self) -> String {
997 match &self.replied_to {
998 Some(id) => bytes_to_hex_32(id),
999 None => String::new(),
1000 }
1001 }
1002
1003 #[inline]
1005 pub fn wrapper_id_hex(&self) -> Option<String> {
1006 self.wrapper_id.as_ref().map(|id| bytes_to_hex_32(id))
1007 }
1008
1009 #[inline]
1011 pub fn timestamp_ms(&self) -> u64 {
1012 timestamp_from_compact(self.at)
1013 }
1014
1015 pub fn apply_edit(&mut self, new_content: String, edited_at: u64, emoji_tags: Vec<crate::types::EmojiTag>) {
1021 if self.edit_history.is_none() {
1023 self.edit_history = Some(Box::new(vec![EditEntry {
1024 content: self.content.to_string(),
1025 edited_at: self.timestamp_ms(), }]));
1027 }
1028
1029 let mut is_latest = true;
1030 if let Some(ref mut history) = self.edit_history {
1031 if history.iter().any(|e| e.edited_at == edited_at) {
1033 return;
1034 }
1035
1036 history.push(EditEntry {
1038 content: new_content.clone(),
1039 edited_at,
1040 });
1041
1042 history.sort_by_key(|e| e.edited_at);
1044 is_latest = history.last().map(|e| e.edited_at == edited_at).unwrap_or(true);
1045 }
1046
1047 if is_latest {
1049 self.content = new_content.into_boxed_str();
1050 self.emoji_tags = if emoji_tags.is_empty() { None } else { Some(Box::new(emoji_tags)) };
1051 }
1052 }
1053
1054 #[inline]
1056 pub fn replied_to_has_attachment(&self) -> Option<bool> {
1057 self.flags.replied_to_has_attachment()
1058 }
1059
1060 pub fn add_reaction(&mut self, reaction: Reaction, interner: &mut NpubInterner) -> bool {
1063 let reaction_id = hex_to_bytes_32(&reaction.id);
1065
1066 if self.reactions.iter().any(|r| r.id == reaction_id) {
1068 return false;
1069 }
1070
1071 let compact = CompactReaction::from_reaction_owned(reaction, interner);
1073 let mut reactions = self.reactions.to_vec();
1074 reactions.push(compact);
1075 self.reactions = TinyVec::from_vec(reactions);
1076 true
1077 }
1078
1079 #[inline]
1081 pub fn is_mine(&self) -> bool { self.flags.is_mine() }
1082 #[inline]
1083 pub fn is_pending(&self) -> bool { self.flags.is_pending() }
1084 #[inline]
1085 pub fn is_failed(&self) -> bool { self.flags.is_failed() }
1086
1087 #[inline]
1089 pub fn set_pending(&mut self, value: bool) { self.flags.set_pending(value); }
1090 #[inline]
1091 pub fn set_failed(&mut self, value: bool) { self.flags.set_failed(value); }
1092 #[inline]
1093 pub fn set_mine(&mut self, value: bool) { self.flags.set_mine(value); }
1094}
1095
1096#[derive(Clone, Debug, Default)]
1105pub struct CompactMessageVec {
1106 messages: Vec<CompactMessage>,
1108 id_index: Vec<([u8; 32], u32)>,
1110}
1111
1112impl CompactMessageVec {
1113 pub fn new() -> Self {
1114 Self {
1115 messages: Vec::new(),
1116 id_index: Vec::new(),
1117 }
1118 }
1119
1120 pub fn with_capacity(capacity: usize) -> Self {
1121 Self {
1122 messages: Vec::with_capacity(capacity),
1123 id_index: Vec::with_capacity(capacity),
1124 }
1125 }
1126
1127 #[inline]
1129 pub fn len(&self) -> usize {
1130 self.messages.len()
1131 }
1132
1133 #[inline]
1134 pub fn is_empty(&self) -> bool {
1135 self.messages.is_empty()
1136 }
1137
1138 #[inline]
1140 pub fn messages(&self) -> &[CompactMessage] {
1141 &self.messages
1142 }
1143
1144 #[inline]
1146 pub fn messages_mut(&mut self) -> &mut Vec<CompactMessage> {
1147 &mut self.messages
1148 }
1149
1150 #[inline]
1152 pub fn iter(&self) -> std::slice::Iter<'_, CompactMessage> {
1153 self.messages.iter()
1154 }
1155
1156 #[inline]
1158 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, CompactMessage> {
1159 self.messages.iter_mut()
1160 }
1161
1162 #[inline]
1164 pub fn last(&self) -> Option<&CompactMessage> {
1165 self.messages.last()
1166 }
1167
1168 #[inline]
1170 pub fn last_timestamp(&self) -> Option<u64> {
1171 self.messages.last().map(|m| timestamp_from_compact(m.at))
1172 }
1173
1174 #[inline]
1176 pub fn first(&self) -> Option<&CompactMessage> {
1177 self.messages.first()
1178 }
1179
1180 pub fn find_by_id(&self, id: &[u8; 32]) -> Option<&CompactMessage> {
1182 let pos = self.id_index
1183 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1184 .ok()?;
1185 let msg_pos = self.id_index[pos].1 as usize;
1186 self.messages.get(msg_pos)
1187 }
1188
1189 pub fn find_by_id_mut(&mut self, id: &[u8; 32]) -> Option<&mut CompactMessage> {
1191 let pos = self.id_index
1192 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1193 .ok()?;
1194 let msg_pos = self.id_index[pos].1 as usize;
1195 self.messages.get_mut(msg_pos)
1196 }
1197
1198 pub fn find_by_hex_id(&self, id_str: &str) -> Option<&CompactMessage> {
1200 if id_str.is_empty() {
1201 return None;
1202 }
1203 let id = encode_message_id(id_str);
1204 self.find_by_id(&id)
1205 }
1206
1207 pub fn find_by_hex_id_mut(&mut self, id_str: &str) -> Option<&mut CompactMessage> {
1209 if id_str.is_empty() {
1210 return None;
1211 }
1212 let id = encode_message_id(id_str);
1213 self.find_by_id_mut(&id)
1214 }
1215
1216 pub fn contains_id(&self, id: &[u8; 32]) -> bool {
1218 self.id_index
1219 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1220 .is_ok()
1221 }
1222
1223 pub fn remove_by_hex_id(&mut self, id_str: &str) -> bool {
1225 if id_str.is_empty() {
1226 return false;
1227 }
1228 let id = encode_message_id(id_str);
1229 let idx_pos = match self.id_index.binary_search_by(|(idx_id, _)| idx_id.cmp(&id)) {
1231 Ok(pos) => pos,
1232 Err(_) => return false,
1233 };
1234 let msg_pos = self.id_index[idx_pos].1 as usize;
1235 self.messages.remove(msg_pos);
1237 self.rebuild_index();
1239 true
1240 }
1241
1242 pub fn contains_hex_id(&self, id_str: &str) -> bool {
1244 if id_str.is_empty() {
1245 return false;
1246 }
1247 let id = encode_message_id(id_str);
1248 self.contains_id(&id)
1249 }
1250
1251 pub fn insert(&mut self, msg: CompactMessage) -> bool {
1257 if self.contains_id(&msg.id) {
1259 return false;
1260 }
1261
1262 let msg_id = msg.id;
1263
1264 if self.messages.last().is_none_or(|last| msg.at >= last.at) {
1267 let msg_pos = self.messages.len() as u32;
1268 self.messages.push(msg);
1269
1270 let idx_pos = self.id_index
1273 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1274 .unwrap_err();
1275 self.id_index.insert(idx_pos, (msg_id, msg_pos));
1276
1277 return true;
1278 }
1279
1280 let msg_pos = match self.messages.binary_search_by(|m| m.at.cmp(&msg.at)) {
1283 Ok(pos) => pos,
1284 Err(pos) => pos,
1285 };
1286
1287 for (_, pos) in &mut self.id_index {
1289 if *pos >= msg_pos as u32 {
1290 *pos += 1;
1291 }
1292 }
1293
1294 self.messages.insert(msg_pos, msg);
1296
1297 let idx_pos = self.id_index
1299 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1300 .unwrap_err();
1301 self.id_index.insert(idx_pos, (msg_id, msg_pos as u32));
1302
1303 true
1304 }
1305
1306 pub fn rebuild_index(&mut self) {
1308 self.id_index.clear();
1309 self.id_index.reserve(self.messages.len());
1310 for (pos, msg) in self.messages.iter().enumerate() {
1311 self.id_index.push((msg.id, pos as u32));
1312 }
1313 self.id_index.sort_by(|(a, _), (b, _)| a.cmp(b));
1314 }
1315
1316 pub fn insert_batch(&mut self, messages: impl IntoIterator<Item = CompactMessage>) -> usize {
1325 let messages: Vec<_> = messages.into_iter().collect();
1326 if messages.is_empty() {
1327 return 0;
1328 }
1329
1330 let mut to_add: Vec<CompactMessage> = Vec::with_capacity(messages.len());
1332 for msg in messages {
1333 if !self.contains_id(&msg.id) {
1334 to_add.push(msg);
1335 }
1336 }
1337
1338 if to_add.is_empty() {
1339 return 0;
1340 }
1341
1342 let added = to_add.len();
1343
1344 let our_first = self.messages.first().map(|m| m.at);
1346 let our_last = self.messages.last().map(|m| m.at);
1347 let their_min = to_add.iter().map(|m| m.at).min().unwrap();
1348 let their_max = to_add.iter().map(|m| m.at).max().unwrap();
1349
1350 if self.messages.is_empty() {
1351 self.messages = to_add;
1353 self.messages.sort_by_key(|m| m.at);
1354 self.rebuild_index();
1355 } else if their_min >= our_last.unwrap() {
1356 to_add.sort_by_key(|m| m.at);
1358 let base_pos = self.messages.len() as u32;
1359 for (i, msg) in to_add.into_iter().enumerate() {
1360 let msg_id = msg.id;
1361 self.messages.push(msg);
1362 let idx_pos = self.id_index
1364 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1365 .unwrap_err();
1366 self.id_index.insert(idx_pos, (msg_id, base_pos + i as u32));
1367 }
1368 } else if their_max <= our_first.unwrap() {
1369 to_add.sort_by_key(|m| m.at);
1371 let prepend_count = to_add.len();
1372
1373 for (_, pos) in &mut self.id_index {
1375 *pos += prepend_count as u32;
1376 }
1377
1378 let mut new_index_entries: Vec<_> = to_add.iter()
1380 .enumerate()
1381 .map(|(i, msg)| (msg.id, i as u32))
1382 .collect();
1383 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1384
1385 let old_index = std::mem::take(&mut self.id_index);
1387 self.id_index.reserve(old_index.len() + new_index_entries.len());
1388
1389 let mut old_iter = old_index.into_iter().peekable();
1390 let mut new_iter = new_index_entries.into_iter().peekable();
1391
1392 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1393 match (old_iter.peek(), new_iter.peek()) {
1394 (Some((old_id, _)), Some((new_id, _))) => {
1395 if old_id < new_id {
1396 self.id_index.push(old_iter.next().unwrap());
1397 } else {
1398 self.id_index.push(new_iter.next().unwrap());
1399 }
1400 }
1401 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1402 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1403 (None, None) => break,
1404 }
1405 }
1406
1407 let mut new_messages = to_add;
1409 new_messages.append(&mut self.messages);
1410 self.messages = new_messages;
1411 } else {
1412 self.messages.extend(to_add);
1414 self.messages.sort_by_key(|m| m.at);
1415 self.rebuild_index();
1416 }
1417
1418 added
1419 }
1420
1421 pub fn memory_usage(&self) -> usize {
1423 std::mem::size_of::<Self>()
1424 + self.messages.capacity() * std::mem::size_of::<CompactMessage>()
1425 + self.id_index.capacity() * std::mem::size_of::<([u8; 32], u32)>()
1426 }
1428
1429 pub fn drain(&mut self, range: std::ops::Range<usize>) -> std::vec::Drain<'_, CompactMessage> {
1431 let drain = self.messages.drain(range);
1432 drain
1434 }
1435
1436 pub fn sort_by_key<K, F>(&mut self, f: F)
1438 where
1439 F: FnMut(&CompactMessage) -> K,
1440 K: Ord,
1441 {
1442 self.messages.sort_by_key(f);
1443 self.rebuild_index();
1444 }
1445
1446 pub fn clear(&mut self) {
1448 self.messages.clear();
1449 self.id_index.clear();
1450 }
1451}
1452
1453use crate::types::Message;
1458
1459impl CompactMessage {
1460 pub fn from_message(msg: &Message, interner: &mut NpubInterner) -> Self {
1462 Self {
1463 id: encode_message_id(&msg.id),
1464 at: timestamp_to_compact(msg.at),
1465 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1466 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1467 replied_to: if msg.replied_to.is_empty() {
1469 None
1470 } else {
1471 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1472 },
1473 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1474 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1476 content: msg.content.clone().into_boxed_str(),
1478 replied_to_content: msg.replied_to_content.as_ref().map(|s| s.clone().into_boxed_str()),
1479 attachments: TinyVec::from_vec(
1481 msg.attachments.iter()
1482 .map(CompactAttachment::from_attachment)
1483 .collect()
1484 ),
1485 reactions: TinyVec::from_vec(
1487 msg.reactions.iter()
1488 .map(|r| CompactReaction::from_reaction(r, interner))
1489 .collect()
1490 ),
1491 edit_history: msg.edit_history.clone().map(Box::new),
1493 preview_metadata: msg.preview_metadata.clone().map(Box::new),
1494 emoji_tags: if msg.emoji_tags.is_empty() {
1495 None
1496 } else {
1497 Some(Box::new(msg.emoji_tags.clone()))
1498 },
1499 }
1500 }
1501
1502 pub fn from_message_owned(msg: Message, interner: &mut NpubInterner) -> Self {
1507 Self {
1508 id: encode_message_id(&msg.id),
1509 at: timestamp_to_compact(msg.at),
1510 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1511 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1512 replied_to: if msg.replied_to.is_empty() {
1514 None
1515 } else {
1516 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1517 },
1518 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1519 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1521 content: msg.content.into_boxed_str(),
1523 replied_to_content: msg.replied_to_content.map(|s| s.into_boxed_str()),
1524 attachments: TinyVec::from_vec(
1526 msg.attachments.into_iter()
1527 .map(CompactAttachment::from_attachment_owned)
1528 .collect()
1529 ),
1530 reactions: TinyVec::from_vec(
1532 msg.reactions.into_iter()
1533 .map(|r| CompactReaction::from_reaction_owned(r, interner))
1534 .collect()
1535 ),
1536 edit_history: msg.edit_history.map(Box::new),
1538 preview_metadata: msg.preview_metadata.map(Box::new),
1539 emoji_tags: if msg.emoji_tags.is_empty() {
1540 None
1541 } else {
1542 Some(Box::new(msg.emoji_tags))
1543 },
1544 }
1545 }
1546
1547 pub fn to_message(&self, interner: &NpubInterner) -> Message {
1549 Message {
1550 id: self.id_hex(),
1551 at: self.timestamp_ms(), mine: self.flags.is_mine(),
1553 pending: self.flags.is_pending(),
1554 failed: self.flags.is_failed(),
1555 edited: self.is_edited(),
1556 npub: interner.resolve(self.npub_idx).map(|s| s.to_string()),
1557 replied_to: self.replied_to_hex(),
1558 replied_to_content: self.replied_to_content.as_ref().map(|s| s.to_string()),
1559 replied_to_npub: interner.resolve(self.replied_to_npub_idx).map(|s| s.to_string()),
1560 replied_to_has_attachment: self.flags.replied_to_has_attachment(),
1561 wrapper_event_id: self.wrapper_id_hex(),
1562 content: self.content.to_string(),
1563 attachments: self.attachments.iter()
1565 .map(|a| a.to_attachment())
1566 .collect(),
1567 reactions: self.reactions.iter()
1569 .map(|r| r.to_reaction(interner))
1570 .collect(),
1571 edit_history: self.edit_history.as_ref().map(|b| (**b).clone()),
1573 preview_metadata: self.preview_metadata.as_ref().map(|b| (**b).clone()),
1574 emoji_tags: self.emoji_tags.as_ref().map(|b| (**b).clone()).unwrap_or_default(),
1575 }
1576 }
1577}
1578
1579#[cfg(test)]
1580mod tests {
1581 use super::*;
1582
1583 #[test]
1584 fn test_message_flags() {
1585 let mut flags = MessageFlags::NONE;
1586 assert!(!flags.is_mine());
1587 assert!(!flags.is_pending());
1588 assert!(!flags.is_failed());
1589
1590 flags.set_mine(true);
1591 assert!(flags.is_mine());
1592
1593 flags.set_pending(true);
1594 assert!(flags.is_pending());
1595 assert!(flags.is_mine()); flags.set_mine(false);
1598 assert!(!flags.is_mine());
1599 assert!(flags.is_pending()); }
1601
1602 #[test]
1603 fn test_npub_interner() {
1604 let mut interner = NpubInterner::new();
1605
1606 let idx1 = interner.intern("npub1alice");
1607 let idx2 = interner.intern("npub1bob");
1608 let idx3 = interner.intern("npub1alice"); assert_eq!(idx1, idx3); assert_ne!(idx1, idx2);
1612
1613 assert_eq!(interner.resolve(idx1), Some("npub1alice"));
1614 assert_eq!(interner.resolve(idx2), Some("npub1bob"));
1615 assert_eq!(interner.resolve(NO_NPUB), None);
1616 }
1617
1618 #[test]
1619 fn test_compact_message_vec_insert_and_find() {
1620 let mut vec = CompactMessageVec::new();
1621 let mut interner = NpubInterner::new();
1622
1623 let msg1 = CompactMessage {
1624 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000001"),
1625 at: 1000,
1626 flags: MessageFlags::NONE,
1627 npub_idx: interner.intern("npub1test"),
1628 replied_to: None,
1629 replied_to_npub_idx: NO_NPUB,
1630 wrapper_id: None,
1631 content: "First message".to_string().into_boxed_str(),
1632 replied_to_content: None,
1633 attachments: TinyVec::new(),
1634 reactions: TinyVec::new(),
1635 edit_history: None,
1636 preview_metadata: None, emoji_tags: None,
1638 };
1639
1640 let msg2 = CompactMessage {
1641 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000002"),
1642 at: 2000,
1643 flags: MessageFlags::MINE,
1644 npub_idx: interner.intern("npub1me"),
1645 replied_to: None,
1646 replied_to_npub_idx: NO_NPUB,
1647 wrapper_id: None,
1648 content: "Second message".to_string().into_boxed_str(),
1649 replied_to_content: None,
1650 attachments: TinyVec::new(),
1651 reactions: TinyVec::new(),
1652 edit_history: None,
1653 preview_metadata: None, emoji_tags: None,
1655 };
1656
1657 assert!(vec.insert(msg1));
1658 assert!(vec.insert(msg2));
1659 assert_eq!(vec.len(), 2);
1660
1661 let found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000001");
1663 assert!(found.is_some());
1664 assert_eq!(&*found.unwrap().content, "First message");
1665
1666 let not_found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000099");
1668 assert!(not_found.is_none());
1669 }
1670
1671 #[test]
1672 fn test_duplicate_insert_rejected() {
1673 let mut vec = CompactMessageVec::new();
1674
1675 let msg = CompactMessage {
1676 id: hex_to_bytes_32("abcd000000000000000000000000000000000000000000000000000000000000"),
1677 at: 1000,
1678 flags: MessageFlags::NONE,
1679 npub_idx: NO_NPUB,
1680 replied_to: None,
1681 replied_to_npub_idx: NO_NPUB,
1682 wrapper_id: None,
1683 content: "Test".to_string().into_boxed_str(),
1684 replied_to_content: None,
1685 attachments: TinyVec::new(),
1686 reactions: TinyVec::new(),
1687 edit_history: None,
1688 preview_metadata: None, emoji_tags: None,
1690 };
1691
1692 assert!(vec.insert(msg.clone()));
1693 assert!(!vec.insert(msg)); assert_eq!(vec.len(), 1);
1695 }
1696
1697 #[test]
1699 fn benchmark_compact_vs_message() {
1700 use std::time::Instant;
1701
1702 const NUM_MESSAGES: usize = 10_000;
1703 const NUM_UNIQUE_USERS: usize = 50; println!("\n========================================");
1706 println!(" COMPACT MESSAGE BENCHMARK");
1707 println!(" {} messages, {} unique users", NUM_MESSAGES, NUM_UNIQUE_USERS);
1708 println!("========================================\n");
1709
1710 let users: Vec<String> = (0..NUM_UNIQUE_USERS)
1712 .map(|i| format!("npub1{:0>62}", i))
1713 .collect();
1714
1715 let messages: Vec<Message> = (0..NUM_MESSAGES)
1717 .map(|i| {
1718 let user_idx = i % NUM_UNIQUE_USERS;
1719 Message {
1720 id: format!("{:0>64x}", i),
1721 at: 1700000000000 + (i as u64 * 1000),
1722 mine: user_idx == 0,
1723 pending: false,
1724 failed: false,
1725 edited: false,
1726 npub: Some(users[user_idx].clone()),
1727 replied_to: if i > 0 && i % 5 == 0 {
1728 format!("{:0>64x}", i - 1)
1729 } else {
1730 String::new()
1731 },
1732 replied_to_content: if i > 0 && i % 5 == 0 {
1733 Some("Previous message content".to_string())
1734 } else {
1735 None
1736 },
1737 replied_to_npub: if i > 0 && i % 5 == 0 {
1738 Some(users[(i - 1) % NUM_UNIQUE_USERS].clone())
1739 } else {
1740 None
1741 },
1742 replied_to_has_attachment: None,
1743 wrapper_event_id: Some(format!("{:0>64x}", i + 1000000)),
1744 content: format!("This is message number {} with some typical content length.", i),
1745 attachments: vec![],
1746 reactions: vec![],
1747 edit_history: None,
1748 preview_metadata: None,
1749 emoji_tags: Vec::new(),
1750 }
1751 })
1752 .collect();
1753
1754 println!("--- STRUCT SIZES ---");
1756 println!(" Message struct: {} bytes", std::mem::size_of::<Message>());
1757 println!(" CompactMessage struct: {} bytes", std::mem::size_of::<CompactMessage>());
1758 println!(" Savings per struct: {} bytes ({:.1}%)",
1759 std::mem::size_of::<Message>().saturating_sub(std::mem::size_of::<CompactMessage>()),
1760 (1.0 - std::mem::size_of::<CompactMessage>() as f64 / std::mem::size_of::<Message>() as f64) * 100.0
1761 );
1762 println!();
1763
1764 let msg_heap_estimate: usize = messages.iter().map(|m| {
1766 m.id.capacity()
1767 + m.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1768 + m.replied_to.capacity()
1769 + m.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
1770 + m.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1771 + m.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
1772 + m.content.capacity()
1773 }).sum();
1774 let msg_total = messages.len() * std::mem::size_of::<Message>() + msg_heap_estimate;
1775
1776 println!("--- INSERT BENCHMARK ---");
1778
1779 let mut interner = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1781 let mut compact_vec = CompactMessageVec::with_capacity(NUM_MESSAGES);
1782
1783 let insert_start = Instant::now();
1784 for msg in &messages {
1785 let compact = CompactMessage::from_message(msg, &mut interner);
1786 compact_vec.insert(compact);
1787 }
1788 let insert_elapsed = insert_start.elapsed();
1789
1790 println!(" Sequential insert (optimized append path):");
1791 println!(" {} messages in {:?}", NUM_MESSAGES, insert_elapsed);
1792 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / insert_elapsed.as_secs_f64());
1793 println!(" Per message: {:.3} us ({} ns)",
1794 insert_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1795 insert_elapsed.as_nanos() / NUM_MESSAGES as u128);
1796 println!();
1797
1798 let mut interner2 = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1800 let mut compact_vec2 = CompactMessageVec::with_capacity(NUM_MESSAGES);
1801
1802 let batch_start = Instant::now();
1803 let compact_messages: Vec<_> = messages.iter()
1804 .map(|msg| CompactMessage::from_message(msg, &mut interner2))
1805 .collect();
1806 let batch_added = compact_vec2.insert_batch(compact_messages);
1807 let batch_elapsed = batch_start.elapsed();
1808
1809 println!(" Batch insert (pagination/history load):");
1810 println!(" {} messages in {:?}", batch_added, batch_elapsed);
1811 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / batch_elapsed.as_secs_f64());
1812 println!(" Per message: {:.3} us ({} ns)",
1813 batch_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1814 batch_elapsed.as_nanos() / NUM_MESSAGES as u128);
1815 println!();
1816
1817 println!("--- MEMORY COMPARISON ---");
1819 let compact_heap_estimate: usize = compact_vec.iter().map(|m| {
1820 m.content.len() + m.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
1822 + m.attachments.len() * std::mem::size_of::<Attachment>() + if m.attachments.is_empty() { 0 } else { 1 }
1823 + m.reactions.len() * std::mem::size_of::<Reaction>() + if m.reactions.is_empty() { 0 } else { 1 }
1824 }).sum();
1825 let compact_struct_mem = compact_vec.len() * std::mem::size_of::<CompactMessage>();
1826 let compact_index_mem = compact_vec.len() * std::mem::size_of::<([u8; 32], u32)>();
1827 let interner_mem = interner.memory_usage();
1828 let compact_total = compact_struct_mem + compact_heap_estimate + compact_index_mem + interner_mem;
1829
1830 println!(" Regular Message storage:");
1831 println!(" Struct memory: {:>10} bytes", messages.len() * std::mem::size_of::<Message>());
1832 println!(" Heap (strings): {:>10} bytes", msg_heap_estimate);
1833 println!(" TOTAL: {:>10} bytes ({:.2} MB)", msg_total, msg_total as f64 / 1_000_000.0);
1834 println!();
1835 println!(" CompactMessage storage:");
1836 println!(" Struct memory: {:>10} bytes", compact_struct_mem);
1837 println!(" Heap (strings): {:>10} bytes", compact_heap_estimate);
1838 println!(" ID index: {:>10} bytes", compact_index_mem);
1839 println!(" Interner: {:>10} bytes ({} unique npubs)", interner_mem, interner.len());
1840 println!(" TOTAL: {:>10} bytes ({:.2} MB)", compact_total, compact_total as f64 / 1_000_000.0);
1841 println!();
1842 println!(" SAVINGS: {} bytes ({:.1}%)",
1843 msg_total.saturating_sub(compact_total),
1844 (1.0 - compact_total as f64 / msg_total as f64) * 100.0
1845 );
1846 println!(" Per message: {} -> {} bytes (avg)",
1847 msg_total / NUM_MESSAGES,
1848 compact_total / NUM_MESSAGES
1849 );
1850 println!();
1851
1852 println!("--- LOOKUP BENCHMARK ---");
1854
1855 let lookup_ids: Vec<String> = (0..1000)
1857 .map(|i| format!("{:0>64x}", i * 10)) .collect();
1859
1860 let lookup_start = Instant::now();
1862 let mut found_count = 0;
1863 for _ in 0..100 { for id in &lookup_ids {
1865 if compact_vec.find_by_hex_id(id).is_some() {
1866 found_count += 1;
1867 }
1868 }
1869 }
1870 let lookup_elapsed = lookup_start.elapsed();
1871 let total_lookups = 100 * lookup_ids.len();
1872
1873 println!(" Binary search (CompactMessageVec):");
1874 println!(" {} lookups in {:?}", total_lookups, lookup_elapsed);
1875 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / lookup_elapsed.as_secs_f64());
1876 println!(" Per lookup: {:.2} us", lookup_elapsed.as_micros() as f64 / total_lookups as f64);
1877 println!(" Found: {} / {}", found_count, total_lookups);
1878 println!();
1879
1880 let linear_start = Instant::now();
1882 let mut linear_found = 0;
1883 for _ in 0..100 {
1884 for id in &lookup_ids {
1885 if messages.iter().find(|m| &m.id == id).is_some() {
1886 linear_found += 1;
1887 }
1888 }
1889 }
1890 let linear_elapsed = linear_start.elapsed();
1891
1892 println!(" Linear search (Vec<Message>):");
1893 println!(" {} lookups in {:?}", total_lookups, linear_elapsed);
1894 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / linear_elapsed.as_secs_f64());
1895 println!(" Per lookup: {:.2} us", linear_elapsed.as_micros() as f64 / total_lookups as f64);
1896 println!();
1897
1898 let speedup = linear_elapsed.as_nanos() as f64 / lookup_elapsed.as_nanos() as f64;
1899 println!(" SPEEDUP: {:.1}x faster with binary search!", speedup);
1900 println!();
1901
1902 println!("--- INTERNER EFFICIENCY ---");
1904 let npub_string_size = 63 + 1; let naive_npub_mem = NUM_MESSAGES * npub_string_size * 2; let actual_npub_mem = interner_mem;
1907 println!(" Naive (every msg stores npubs): {} bytes", naive_npub_mem);
1908 println!(" Interned ({} unique): {} bytes", interner.len(), actual_npub_mem);
1909 println!(" SAVINGS: {} bytes ({:.1}%)",
1910 naive_npub_mem.saturating_sub(actual_npub_mem),
1911 (1.0 - actual_npub_mem as f64 / naive_npub_mem as f64) * 100.0
1912 );
1913 println!();
1914
1915 println!("========================================");
1916 println!(" BENCHMARK COMPLETE");
1917 println!("========================================\n");
1918
1919 assert_eq!(compact_vec.len(), NUM_MESSAGES);
1921 assert_eq!(interner.len(), NUM_UNIQUE_USERS);
1922 assert_eq!(found_count, linear_found);
1923 }
1924
1925 #[test]
1932 fn benchmark_profile_lookup() {
1933 use std::time::Instant;
1934 use std::hint::black_box;
1935
1936 const NUM_PROFILES: usize = 60;
1937 const NUM_LOOKUPS: usize = 100_000;
1938
1939 println!("\n========================================");
1940 println!(" PROFILE LOOKUP BENCHMARK");
1941 println!(" {} profiles, {} lookups each method", NUM_PROFILES, NUM_LOOKUPS);
1942 println!("========================================\n");
1943
1944 let npubs: Vec<String> = (0..NUM_PROFILES)
1946 .map(|i| format!("npub1{:0>58}", format!("{:x}", i * 7919 + 1000))) .collect();
1948
1949 let old_ids: Vec<String> = npubs.iter().rev().cloned().collect(); let mut sorted_ids: Vec<String> = npubs.clone();
1954 sorted_ids.sort();
1955
1956 let mut interner = NpubInterner::new();
1958 let mut profiles: Vec<crate::profile::Profile> = npubs.iter().map(|npub| {
1959 let mut p = crate::profile::Profile::new();
1960 p.id = interner.intern(npub);
1961 p
1962 }).collect();
1963 profiles.sort_by(|a, b| a.id.cmp(&b.id));
1964
1965 let lookup_targets: Vec<&str> = (0..NUM_LOOKUPS)
1967 .map(|i| npubs[i % NUM_PROFILES].as_str())
1968 .collect();
1969
1970 let handle_targets: Vec<u16> = lookup_targets.iter()
1972 .map(|&npub| interner.lookup(npub).unwrap())
1973 .collect();
1974
1975 let start = Instant::now();
1977 let mut found = 0u64;
1978 for &target in &lookup_targets {
1979 if old_ids.iter().any(|id| id == target) {
1980 found += 1;
1981 }
1982 }
1983 let linear_elapsed = start.elapsed();
1984 assert_eq!(found, NUM_LOOKUPS as u64);
1985
1986 let start = Instant::now();
1988 found = 0;
1989 for &target in &lookup_targets {
1990 if sorted_ids.binary_search_by(|id| id.as_str().cmp(target)).is_ok() {
1991 found += 1;
1992 }
1993 }
1994 let string_bs_elapsed = start.elapsed();
1995 assert_eq!(found, NUM_LOOKUPS as u64);
1996
1997 let start = Instant::now();
1999 found = 0;
2000 for &handle in &handle_targets {
2001 if profiles.binary_search_by(|p| p.id.cmp(black_box(&handle))).is_ok() {
2002 found += 1;
2003 }
2004 }
2005 let direct_elapsed = start.elapsed();
2006 assert_eq!(found, NUM_LOOKUPS as u64);
2007
2008 println!("--- LOOKUP METHODS ---");
2010 println!(" 1. Linear scan (old id: String):");
2011 println!(" {:?} total, {:.0} ns/lookup",
2012 linear_elapsed,
2013 linear_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2014 println!();
2015 println!(" 2. String binary search (intermediate):");
2016 println!(" {:?} total, {:.0} ns/lookup",
2017 string_bs_elapsed,
2018 string_bs_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2019 println!(" vs linear: {:.1}x faster",
2020 linear_elapsed.as_nanos() as f64 / string_bs_elapsed.as_nanos() as f64);
2021 println!();
2022 println!(" 3. Direct u16 handle lookup (current id: u16):");
2023 println!(" {:?} total, {:.0} ns/lookup",
2024 direct_elapsed,
2025 direct_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2026 println!(" vs linear: {:.1}x faster",
2027 linear_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2028 println!(" vs string BS: {:.1}x faster",
2029 string_bs_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2030 println!();
2031
2032 println!("--- MEMORY PER PROFILE ---");
2034 println!(" Old (id: String): ~87 bytes (24 String header + ~63 heap)");
2035 println!(" Current (id: u16): 2 bytes (inline)");
2036 println!(" Savings: ~85 bytes/profile, ~{} bytes for {} profiles",
2037 85 * NUM_PROFILES, NUM_PROFILES);
2038 println!(" Interner (shared): {} bytes (shared with message system)",
2039 interner.memory_usage());
2040 println!();
2041
2042 println!("========================================");
2043 println!(" BENCHMARK COMPLETE");
2044 println!("========================================\n");
2045
2046 for npub in &npubs {
2048 assert!(old_ids.iter().any(|id| id == npub));
2049 assert!(sorted_ids.binary_search_by(|id| id.as_str().cmp(npub.as_str())).is_ok());
2050 let h = interner.lookup(npub).unwrap();
2051 assert!(profiles.binary_search_by(|p| p.id.cmp(&h)).is_ok());
2052 }
2053 }
2054
2055 #[test]
2060 fn pending_id_roundtrip_regular_hex() {
2061 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2062 let encoded = encode_message_id(hex);
2063 let decoded = decode_message_id(&encoded);
2064 assert_eq!(decoded, hex, "regular hex ID should roundtrip exactly");
2065 }
2066
2067 #[test]
2068 fn pending_id_roundtrip_pending() {
2069 let id = "pending-1234567890123456789";
2070 let encoded = encode_message_id(id);
2071 let decoded = decode_message_id(&encoded);
2072 assert_eq!(decoded, id, "pending ID should roundtrip exactly");
2073 }
2074
2075 #[test]
2076 fn pending_id_zero_timestamp() {
2077 let id = "pending-0";
2078 let encoded = encode_message_id(id);
2079 assert_eq!(encoded[0], PENDING_ID_MARKER, "first byte should be marker");
2080 let decoded = decode_message_id(&encoded);
2081 assert_eq!(decoded, id, "pending-0 should roundtrip");
2082 }
2083
2084 #[test]
2085 fn pending_id_max_u128_timestamp() {
2086 let max = u128::MAX;
2087 let id = format!("pending-{}", max);
2088 let encoded = encode_message_id(&id);
2089 let decoded = decode_message_id(&encoded);
2090 assert_eq!(decoded, id, "pending with max u128 should roundtrip");
2091 }
2092
2093 #[test]
2094 fn pending_id_all_zero_hex() {
2095 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
2096 let encoded = encode_message_id(hex);
2097 let decoded = decode_message_id(&encoded);
2098 assert_eq!(decoded, hex, "all-zero hex ID should roundtrip");
2099 assert_ne!(encoded[0], PENDING_ID_MARKER);
2101 }
2102
2103 #[test]
2104 fn pending_id_all_ff_hex() {
2105 let hex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
2106 let encoded = encode_message_id(hex);
2107 let decoded = decode_message_id(&encoded);
2108 assert_eq!(decoded, hex, "all-ff hex ID should roundtrip");
2109 assert_eq!(encoded, [0xff; 32], "all-ff should decode to all 0xff bytes");
2110 }
2111
2112 #[test]
2113 fn pending_id_mixed_case_hex() {
2114 let lower = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2115 let mixed = "ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789";
2116 let encoded_lower = encode_message_id(lower);
2117 let encoded_mixed = encode_message_id(mixed);
2118 assert_eq!(encoded_lower, encoded_mixed, "mixed case should produce same bytes as lowercase");
2119 }
2120
2121 #[test]
2122 fn pending_id_short_hex_partial_decode() {
2123 let short = "abcdef";
2125 let encoded = encode_message_id(short);
2126 assert_eq!(encoded[0..14], [0u8; 14], "leading bytes should be zero for short input");
2128 }
2129
2130 #[test]
2131 fn pending_id_marker_distinguishes_from_real_id() {
2132 let hex = "0100000000000000000000000000000000000000000000000000000000000000";
2134 let encoded = encode_message_id(hex);
2135 let decoded = decode_message_id(&encoded);
2139 assert!(decoded.starts_with("pending-"), "ID starting with 0x01 byte is treated as pending by design");
2141 }
2142
2143 #[test]
2144 fn pending_id_large_timestamp() {
2145 let id = "pending-99999999999999999";
2146 let encoded = encode_message_id(&id);
2147 let decoded = decode_message_id(&encoded);
2148 assert_eq!(decoded, id, "large but valid timestamp should roundtrip");
2149 }
2150
2151 #[test]
2152 fn pending_id_invalid_timestamp_becomes_zero() {
2153 let id = "pending-notanumber";
2155 let encoded = encode_message_id(id);
2156 assert_eq!(encoded[0], PENDING_ID_MARKER);
2157 assert_eq!(&encoded[1..17], &[0u8; 16]);
2159 let decoded = decode_message_id(&encoded);
2160 assert_eq!(decoded, "pending-0", "invalid timestamp parses as pending-0");
2161 }
2162
2163 #[test]
2168 fn timestamp_to_compact_and_back_roundtrip() {
2169 let ms: u64 = 1705320000000;
2171 let compact = timestamp_to_compact(ms);
2172 let restored = timestamp_from_compact(compact);
2173 assert_eq!(restored / 1000, ms / 1000, "roundtrip should preserve seconds");
2175 }
2176
2177 #[test]
2178 fn secs_to_compact_and_back_roundtrip() {
2179 let secs: u64 = 1705320000; let compact = secs_to_compact(secs);
2181 let restored = secs_from_compact(compact);
2182 assert_eq!(restored, secs, "secs should roundtrip exactly");
2183 }
2184
2185 #[test]
2186 fn timestamp_zero_preservation() {
2187 assert_eq!(secs_to_compact(0), 0, "zero secs should produce zero compact");
2189 assert_eq!(secs_from_compact(0), 0, "zero compact should produce zero secs");
2190 }
2191
2192 #[test]
2193 fn timestamp_epoch_boundary() {
2194 let epoch_secs: u64 = 1577836800;
2196 let compact = secs_to_compact(epoch_secs);
2197 assert_eq!(compact, 0, "epoch boundary should map to compact 0");
2198 let restored = secs_from_compact(compact);
2199 assert_eq!(restored, 0, "compact 0 returns sentinel 0");
2201 }
2202
2203 #[test]
2204 fn timestamp_epoch_boundary_ms() {
2205 let epoch_ms: u64 = 1577836800000;
2206 let compact = timestamp_to_compact(epoch_ms);
2207 assert_eq!(compact, epoch_ms, "full u64 — identity function");
2208 let restored = timestamp_from_compact(compact);
2209 assert_eq!(restored, epoch_ms, "roundtrip preserves value");
2210 }
2211
2212 #[test]
2213 fn timestamp_current_time_roundtrip() {
2214 let secs: u64 = 1774800000;
2216 let compact = secs_to_compact(secs);
2217 let restored = secs_from_compact(compact);
2218 assert_eq!(restored, secs, "current-era timestamp should roundtrip");
2219 assert!(compact > 0, "current time should be past epoch");
2220 }
2221
2222 #[test]
2223 fn timestamp_far_future_year_2100() {
2224 let secs: u64 = 4102444800;
2226 let compact = secs_to_compact(secs);
2227 let restored = secs_from_compact(compact);
2228 assert_eq!(restored, secs, "year 2100 should roundtrip");
2229 assert!(compact <= u32::MAX, "year 2100 should fit in u32");
2231 }
2232
2233 #[test]
2234 fn timestamp_pre_epoch_saturates_to_zero() {
2235 let secs: u64 = 1500000000; let compact = secs_to_compact(secs);
2238 assert_eq!(compact, 0, "pre-epoch timestamp should saturate to 0");
2240 }
2241
2242 #[test]
2243 fn timestamp_ms_sub_second_precision_preserved() {
2244 let ms: u64 = 1705320000999;
2245 let compact = timestamp_to_compact(ms);
2246 let restored = timestamp_from_compact(compact);
2247 assert_eq!(restored, ms, "sub-second ms must be preserved");
2248 }
2249
2250 #[test]
2251 fn timestamp_one_second_after_epoch() {
2252 let secs: u64 = 1577836801; let compact = secs_to_compact(secs);
2254 assert_eq!(compact, 1, "one second after epoch should be compact 1");
2255 let restored = secs_from_compact(compact);
2256 assert_eq!(restored, secs, "should restore to original");
2257 }
2258
2259 #[test]
2264 fn message_flags_mine_independent() {
2265 let mut flags = MessageFlags::NONE;
2266 flags.set_mine(true);
2267 assert!(flags.is_mine(), "mine should be set");
2268 assert!(!flags.is_pending(), "pending should not be set");
2269 assert!(!flags.is_failed(), "failed should not be set");
2270 }
2271
2272 #[test]
2273 fn message_flags_pending_independent() {
2274 let mut flags = MessageFlags::NONE;
2275 flags.set_pending(true);
2276 assert!(!flags.is_mine(), "mine should not be set");
2277 assert!(flags.is_pending(), "pending should be set");
2278 assert!(!flags.is_failed(), "failed should not be set");
2279 }
2280
2281 #[test]
2282 fn message_flags_failed_independent() {
2283 let mut flags = MessageFlags::NONE;
2284 flags.set_failed(true);
2285 assert!(!flags.is_mine(), "mine should not be set");
2286 assert!(!flags.is_pending(), "pending should not be set");
2287 assert!(flags.is_failed(), "failed should be set");
2288 }
2289
2290 #[test]
2291 fn message_flags_from_bools_all_false() {
2292 let flags = MessageFlags::from_bools(false, false, false);
2293 assert!(!flags.is_mine());
2294 assert!(!flags.is_pending());
2295 assert!(!flags.is_failed());
2296 assert_eq!(flags, MessageFlags::NONE, "all false should equal NONE");
2297 }
2298
2299 #[test]
2300 fn message_flags_from_bools_all_true() {
2301 let flags = MessageFlags::from_bools(true, true, true);
2302 assert!(flags.is_mine(), "mine should be set");
2303 assert!(flags.is_pending(), "pending should be set");
2304 assert!(flags.is_failed(), "failed should be set");
2305 }
2306
2307 #[test]
2308 fn message_flags_from_bools_various_combos() {
2309 let flags = MessageFlags::from_bools(true, false, true);
2310 assert!(flags.is_mine());
2311 assert!(!flags.is_pending());
2312 assert!(flags.is_failed());
2313
2314 let flags = MessageFlags::from_bools(false, true, false);
2315 assert!(!flags.is_mine());
2316 assert!(flags.is_pending());
2317 assert!(!flags.is_failed());
2318 }
2319
2320 #[test]
2321 fn message_flags_from_all_replied_to_none() {
2322 let flags = MessageFlags::from_all(false, false, false, None);
2323 assert_eq!(flags.replied_to_has_attachment(), None, "None should roundtrip");
2324 }
2325
2326 #[test]
2327 fn message_flags_from_all_replied_to_some_false() {
2328 let flags = MessageFlags::from_all(false, false, false, Some(false));
2329 assert_eq!(flags.replied_to_has_attachment(), Some(false), "Some(false) should roundtrip");
2330 }
2331
2332 #[test]
2333 fn message_flags_from_all_replied_to_some_true() {
2334 let flags = MessageFlags::from_all(false, false, false, Some(true));
2335 assert_eq!(flags.replied_to_has_attachment(), Some(true), "Some(true) should roundtrip");
2336 }
2337
2338 #[test]
2339 fn message_flags_multiple_set_simultaneously() {
2340 let flags = MessageFlags::from_all(true, true, false, Some(true));
2341 assert!(flags.is_mine());
2342 assert!(flags.is_pending());
2343 assert!(!flags.is_failed());
2344 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2345 }
2346
2347 #[test]
2348 fn message_flags_default_is_all_false() {
2349 let flags = MessageFlags::default();
2350 assert!(!flags.is_mine());
2351 assert!(!flags.is_pending());
2352 assert!(!flags.is_failed());
2353 assert_eq!(flags.replied_to_has_attachment(), None);
2354 assert_eq!(flags, MessageFlags::NONE);
2355 }
2356
2357 #[test]
2358 fn message_flags_bit_patterns_correct() {
2359 assert_eq!(MessageFlags::MINE.0, 0b00001, "MINE bit pattern");
2360 assert_eq!(MessageFlags::PENDING.0, 0b00010, "PENDING bit pattern");
2361 assert_eq!(MessageFlags::FAILED.0, 0b00100, "FAILED bit pattern");
2362 }
2363
2364 #[test]
2365 fn message_flags_set_then_clear() {
2366 let mut flags = MessageFlags::from_bools(true, true, true);
2367 flags.set_mine(false);
2368 assert!(!flags.is_mine(), "mine should be cleared");
2369 assert!(flags.is_pending(), "pending should remain set");
2370 assert!(flags.is_failed(), "failed should remain set");
2371 }
2372
2373 #[test]
2374 fn message_flags_replied_to_overwrite() {
2375 let mut flags = MessageFlags::from_all(false, false, false, Some(true));
2376 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2377 flags.set_replied_to_has_attachment(Some(false));
2378 assert_eq!(flags.replied_to_has_attachment(), Some(false), "overwrite should work");
2379 flags.set_replied_to_has_attachment(None);
2380 assert_eq!(flags.replied_to_has_attachment(), None, "clearing to None should work");
2381 }
2382
2383 #[test]
2384 fn message_flags_replied_to_does_not_interfere_with_other_bits() {
2385 let mut flags = MessageFlags::from_bools(true, true, true);
2386 flags.set_replied_to_has_attachment(Some(true));
2387 assert!(flags.is_mine(), "mine should still be set");
2388 assert!(flags.is_pending(), "pending should still be set");
2389 assert!(flags.is_failed(), "failed should still be set");
2390 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2391 }
2392
2393 #[test]
2398 fn attachment_flags_downloading_independent() {
2399 let mut flags = AttachmentFlags::NONE;
2400 flags.set_downloading(true);
2401 assert!(flags.is_downloading());
2402 assert!(!flags.is_downloaded());
2403 assert!(!flags.is_short_nonce());
2404 }
2405
2406 #[test]
2407 fn attachment_flags_downloaded_independent() {
2408 let mut flags = AttachmentFlags::NONE;
2409 flags.set_downloaded(true);
2410 assert!(!flags.is_downloading());
2411 assert!(flags.is_downloaded());
2412 assert!(!flags.is_short_nonce());
2413 }
2414
2415 #[test]
2416 fn attachment_flags_short_nonce_independent() {
2417 let mut flags = AttachmentFlags::NONE;
2418 flags.set_short_nonce(true);
2419 assert!(!flags.is_downloading());
2420 assert!(!flags.is_downloaded());
2421 assert!(flags.is_short_nonce());
2422 }
2423
2424 #[test]
2425 fn attachment_flags_from_bools() {
2426 let flags = AttachmentFlags::from_bools(true, false);
2427 assert!(flags.is_downloading());
2428 assert!(!flags.is_downloaded());
2429
2430 let flags = AttachmentFlags::from_bools(false, true);
2431 assert!(!flags.is_downloading());
2432 assert!(flags.is_downloaded());
2433 }
2434
2435 #[test]
2436 fn attachment_flags_all_set() {
2437 let mut flags = AttachmentFlags::NONE;
2438 flags.set_downloading(true);
2439 flags.set_downloaded(true);
2440 flags.set_short_nonce(true);
2441 assert!(flags.is_downloading());
2442 assert!(flags.is_downloaded());
2443 assert!(flags.is_short_nonce());
2444 }
2445
2446 #[test]
2447 fn attachment_flags_set_then_clear() {
2448 let mut flags = AttachmentFlags::NONE;
2449 flags.set_downloading(true);
2450 flags.set_downloaded(true);
2451 flags.set_downloading(false);
2452 assert!(!flags.is_downloading(), "downloading should be cleared");
2453 assert!(flags.is_downloaded(), "downloaded should remain set");
2454 }
2455
2456 #[test]
2457 fn attachment_flags_default_none() {
2458 let flags = AttachmentFlags::NONE;
2459 assert!(!flags.is_downloading());
2460 assert!(!flags.is_downloaded());
2461 assert!(!flags.is_short_nonce());
2462 assert_eq!(flags, AttachmentFlags::default());
2463 }
2464
2465 #[test]
2466 fn attachment_flags_bit_values() {
2467 let mut flags = AttachmentFlags::NONE;
2469 flags.set_downloading(true);
2470 assert_eq!(flags.0, 0b0001);
2471
2472 let mut flags = AttachmentFlags::NONE;
2473 flags.set_downloaded(true);
2474 assert_eq!(flags.0, 0b0010);
2475
2476 let mut flags = AttachmentFlags::NONE;
2477 flags.set_short_nonce(true);
2478 assert_eq!(flags.0, 0b0100);
2479 }
2480
2481 #[test]
2486 fn interner_returns_incrementing_handles() {
2487 let mut interner = NpubInterner::new();
2488 let h0 = interner.intern("npub1aaa");
2489 let h1 = interner.intern("npub1bbb");
2490 let h2 = interner.intern("npub1ccc");
2491 assert_eq!(h0, 0, "first intern should be handle 0");
2492 assert_eq!(h1, 1, "second intern should be handle 1");
2493 assert_eq!(h2, 2, "third intern should be handle 2");
2494 }
2495
2496 #[test]
2497 fn interner_lookup_finds_interned() {
2498 let mut interner = NpubInterner::new();
2499 let h = interner.intern("npub1alice");
2500 let found = interner.lookup("npub1alice");
2501 assert_eq!(found, Some(h), "lookup should find interned string");
2502 }
2503
2504 #[test]
2505 fn interner_lookup_returns_none_for_unknown() {
2506 let interner = NpubInterner::new();
2507 assert_eq!(interner.lookup("npub1unknown"), None, "lookup on empty interner should be None");
2508 }
2509
2510 #[test]
2511 fn interner_lookup_returns_none_for_not_interned() {
2512 let mut interner = NpubInterner::new();
2513 interner.intern("npub1alice");
2514 assert_eq!(interner.lookup("npub1bob"), None, "lookup for non-interned should be None");
2515 }
2516
2517 #[test]
2518 fn interner_resolve_returns_string() {
2519 let mut interner = NpubInterner::new();
2520 let h = interner.intern("npub1test123");
2521 assert_eq!(interner.resolve(h), Some("npub1test123"), "resolve should return the original string");
2522 }
2523
2524 #[test]
2525 fn interner_resolve_returns_none_for_no_npub() {
2526 let interner = NpubInterner::new();
2527 assert_eq!(interner.resolve(NO_NPUB), None, "resolve(NO_NPUB) should be None");
2528 }
2529
2530 #[test]
2531 fn interner_resolve_returns_none_for_out_of_bounds() {
2532 let mut interner = NpubInterner::new();
2533 interner.intern("npub1only");
2534 assert_eq!(interner.resolve(999), None, "out-of-bounds handle should resolve to None");
2535 }
2536
2537 #[test]
2538 fn interner_duplicate_returns_same_handle() {
2539 let mut interner = NpubInterner::new();
2540 let h1 = interner.intern("npub1dup");
2541 let h2 = interner.intern("npub1dup");
2542 let h3 = interner.intern("npub1dup");
2543 assert_eq!(h1, h2, "duplicate intern should return same handle");
2544 assert_eq!(h2, h3, "duplicate intern should return same handle");
2545 assert_eq!(interner.len(), 1, "duplicates should not increase length");
2546 }
2547
2548 #[test]
2549 fn interner_100_unique_npubs_stress() {
2550 let mut interner = NpubInterner::new();
2551 let mut handles = Vec::new();
2552
2553 for i in 0..100 {
2554 let npub = format!("npub1stress{:04}", i);
2555 let h = interner.intern(&npub);
2556 handles.push((h, npub));
2557 }
2558
2559 assert_eq!(interner.len(), 100, "should have 100 unique npubs");
2560
2561 for (h, npub) in &handles {
2563 assert_eq!(interner.resolve(*h), Some(npub.as_str()),
2564 "handle {} should resolve to {}", h, npub);
2565 }
2566
2567 for (h, npub) in &handles {
2569 assert_eq!(interner.lookup(npub), Some(*h),
2570 "lookup for {} should return handle {}", npub, h);
2571 }
2572
2573 for (h, npub) in &handles {
2575 assert_eq!(interner.intern(npub), *h,
2576 "re-interning {} should return same handle {}", npub, h);
2577 }
2578 assert_eq!(interner.len(), 100, "re-interning should not grow interner");
2579 }
2580
2581 #[test]
2582 fn interner_memory_usage_reasonable() {
2583 let mut interner = NpubInterner::new();
2584 for i in 0..50 {
2585 interner.intern(&format!("npub1{:0>62}", i));
2586 }
2587 let mem = interner.memory_usage();
2588 assert!(mem > 0, "memory usage should be positive");
2590 assert!(mem < 100_000, "memory usage for 50 npubs should be under 100KB, was {}", mem);
2591 }
2592
2593 #[test]
2594 fn interner_empty() {
2595 let interner = NpubInterner::new();
2596 assert_eq!(interner.len(), 0);
2597 assert!(interner.is_empty());
2598 assert_eq!(interner.resolve(0), None);
2599 assert_eq!(interner.lookup("anything"), None);
2600 assert!(interner.memory_usage() > 0, "even empty interner has struct overhead");
2601 }
2602
2603 #[test]
2604 fn interner_intern_opt_none() {
2605 let mut interner = NpubInterner::new();
2606 let h = interner.intern_opt(None);
2607 assert_eq!(h, NO_NPUB, "intern_opt(None) should return NO_NPUB");
2608 assert_eq!(interner.len(), 0, "None should not add to interner");
2609 }
2610
2611 #[test]
2612 fn interner_intern_opt_empty_string() {
2613 let mut interner = NpubInterner::new();
2614 let h = interner.intern_opt(Some(""));
2615 assert_eq!(h, NO_NPUB, "intern_opt(Some('')) should return NO_NPUB");
2616 }
2617
2618 #[test]
2619 fn interner_intern_opt_some_value() {
2620 let mut interner = NpubInterner::new();
2621 let h = interner.intern_opt(Some("npub1real"));
2622 assert_ne!(h, NO_NPUB, "intern_opt(Some(value)) should not return NO_NPUB");
2623 assert_eq!(interner.resolve(h), Some("npub1real"));
2624 }
2625
2626 fn make_compact_msg(hex_id: &str, timestamp: u64) -> CompactMessage {
2632 CompactMessage {
2633 id: encode_message_id(hex_id),
2634 at: timestamp,
2635 flags: MessageFlags::NONE,
2636 npub_idx: NO_NPUB,
2637 replied_to: None,
2638 replied_to_npub_idx: NO_NPUB,
2639 wrapper_id: None,
2640 content: "test".into(),
2641 replied_to_content: None,
2642 attachments: TinyVec::new(),
2643 reactions: TinyVec::new(),
2644 edit_history: None,
2645 preview_metadata: None,
2646 emoji_tags: None,
2647 }
2648 }
2649
2650 #[test]
2651 fn compact_vec_insert_single_message() {
2652 let mut vec = CompactMessageVec::new();
2653 let msg = make_compact_msg(
2654 "1111111111111111111111111111111111111111111111111111111111111111",
2655 100,
2656 );
2657 assert!(vec.insert(msg), "insert should succeed");
2658 assert_eq!(vec.len(), 1);
2659 assert!(!vec.is_empty());
2660 }
2661
2662 #[test]
2663 fn compact_vec_insert_duplicate_rejected() {
2664 let mut vec = CompactMessageVec::new();
2665 let id = "2222222222222222222222222222222222222222222222222222222222222222";
2666 let msg1 = make_compact_msg(id, 100);
2667 let msg2 = make_compact_msg(id, 200); assert!(vec.insert(msg1), "first insert should succeed");
2669 assert!(!vec.insert(msg2), "duplicate ID should be rejected");
2670 assert_eq!(vec.len(), 1);
2671 }
2672
2673 #[test]
2674 fn compact_vec_insert_batch_multiple() {
2675 let mut vec = CompactMessageVec::new();
2676 let msgs = vec![
2677 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2678 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2679 make_compact_msg("cc00000000000000000000000000000000000000000000000000000000000000", 300),
2680 ];
2681 let added = vec.insert_batch(msgs);
2682 assert_eq!(added, 3, "all 3 should be added");
2683 assert_eq!(vec.len(), 3);
2684 }
2685
2686 #[test]
2687 fn compact_vec_insert_batch_dedup() {
2688 let mut vec = CompactMessageVec::new();
2689 let id = "dd00000000000000000000000000000000000000000000000000000000000000";
2690 vec.insert(make_compact_msg(id, 100));
2691
2692 let msgs = vec![
2693 make_compact_msg(id, 200), make_compact_msg("ee00000000000000000000000000000000000000000000000000000000000000", 300),
2695 ];
2696 let added = vec.insert_batch(msgs);
2697 assert_eq!(added, 1, "only non-duplicate should be added");
2698 assert_eq!(vec.len(), 2);
2699 }
2700
2701 #[test]
2702 fn compact_vec_find_by_hex_id() {
2703 let mut vec = CompactMessageVec::new();
2704 let id = "ff00000000000000000000000000000000000000000000000000000000000001";
2705 let mut msg = make_compact_msg(id, 500);
2706 msg.content = "found me".into();
2707 vec.insert(msg);
2708
2709 let found = vec.find_by_hex_id(id);
2710 assert!(found.is_some(), "should find by hex id");
2711 assert_eq!(&*found.unwrap().content, "found me");
2712 }
2713
2714 #[test]
2715 fn compact_vec_find_by_hex_id_not_found() {
2716 let mut vec = CompactMessageVec::new();
2717 vec.insert(make_compact_msg(
2718 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2719 ));
2720 let found = vec.find_by_hex_id(
2721 "bb00000000000000000000000000000000000000000000000000000000000000",
2722 );
2723 assert!(found.is_none(), "should not find non-existent ID");
2724 }
2725
2726 #[test]
2727 fn compact_vec_find_by_hex_id_empty_string() {
2728 let mut vec = CompactMessageVec::new();
2729 vec.insert(make_compact_msg(
2730 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2731 ));
2732 assert!(vec.find_by_hex_id("").is_none(), "empty string should return None");
2733 }
2734
2735 #[test]
2736 fn compact_vec_find_by_hex_id_mut() {
2737 let mut vec = CompactMessageVec::new();
2738 let id = "ff00000000000000000000000000000000000000000000000000000000000002";
2739 vec.insert(make_compact_msg(id, 500));
2740
2741 let found = vec.find_by_hex_id_mut(id);
2742 assert!(found.is_some(), "should find mutable ref by hex id");
2743 found.unwrap().content = "modified".into();
2744
2745 let found = vec.find_by_hex_id(id);
2747 assert_eq!(&*found.unwrap().content, "modified");
2748 }
2749
2750 #[test]
2751 fn compact_vec_contains_hex_id() {
2752 let mut vec = CompactMessageVec::new();
2753 let id = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
2754 vec.insert(make_compact_msg(id, 100));
2755
2756 assert!(vec.contains_hex_id(id), "should contain inserted ID");
2757 assert!(!vec.contains_hex_id(
2758 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
2759 "should not contain non-inserted ID");
2760 assert!(!vec.contains_hex_id(""), "empty string should not match");
2761 }
2762
2763 #[test]
2764 fn compact_vec_remove_by_hex_id() {
2765 let mut vec = CompactMessageVec::new();
2766 let id1 = "1100000000000000000000000000000000000000000000000000000000000000";
2767 let id2 = "2200000000000000000000000000000000000000000000000000000000000000";
2768 vec.insert(make_compact_msg(id1, 100));
2769 vec.insert(make_compact_msg(id2, 200));
2770 assert_eq!(vec.len(), 2);
2771
2772 assert!(vec.remove_by_hex_id(id1), "remove should succeed");
2773 assert_eq!(vec.len(), 1);
2774 assert!(!vec.contains_hex_id(id1), "removed ID should not be found");
2775 assert!(vec.contains_hex_id(id2), "remaining ID should still be found");
2776 }
2777
2778 #[test]
2779 fn compact_vec_remove_nonexistent() {
2780 let mut vec = CompactMessageVec::new();
2781 vec.insert(make_compact_msg(
2782 "1100000000000000000000000000000000000000000000000000000000000000", 100,
2783 ));
2784 assert!(!vec.remove_by_hex_id(
2785 "9900000000000000000000000000000000000000000000000000000000000000"),
2786 "removing non-existent should return false");
2787 assert!(!vec.remove_by_hex_id(""), "removing empty should return false");
2788 assert_eq!(vec.len(), 1);
2789 }
2790
2791 #[test]
2792 fn compact_vec_last_timestamp() {
2793 let mut vec = CompactMessageVec::new();
2794 assert_eq!(vec.last_timestamp(), None, "empty vec should have no last timestamp");
2795
2796 vec.insert(make_compact_msg(
2797 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2798 ));
2799 vec.insert(make_compact_msg(
2800 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2801 ));
2802 vec.insert(make_compact_msg(
2803 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2804 ));
2805
2806 let last_ts = vec.last_timestamp().unwrap();
2807 let expected = timestamp_from_compact(300);
2809 assert_eq!(last_ts, expected, "last timestamp should be the largest");
2810 }
2811
2812 #[test]
2813 fn compact_vec_empty_operations() {
2814 let vec = CompactMessageVec::new();
2815 assert!(vec.is_empty());
2816 assert_eq!(vec.len(), 0);
2817 assert!(vec.last().is_none());
2818 assert!(vec.first().is_none());
2819 assert!(vec.last_timestamp().is_none());
2820 assert!(!vec.contains_hex_id("anything"));
2821 assert!(vec.find_by_hex_id("anything").is_none());
2822 }
2823
2824 #[test]
2825 fn compact_vec_1000_message_stress() {
2826 let mut vec = CompactMessageVec::new();
2827
2828 for i in 0..1000u64 {
2830 let id = format!("{:0>64x}", i);
2831 let msg = make_compact_msg(&id, i * 10);
2832 assert!(vec.insert(msg), "insert {} should succeed", i);
2833 }
2834 assert_eq!(vec.len(), 1000);
2835
2836 for i in 0..1000u64 {
2838 let id = format!("{:0>64x}", i);
2839 assert!(vec.contains_hex_id(&id), "should find message {}", i);
2840 let found = vec.find_by_hex_id(&id).unwrap();
2841 assert_eq!(found.at, i * 10, "timestamp should match for message {}", i);
2842 }
2843
2844 for i in 1000..1010u64 {
2846 let id = format!("{:0>64x}", i);
2847 assert!(!vec.contains_hex_id(&id), "should not find non-existent {}", i);
2848 }
2849
2850 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2852 for w in timestamps.windows(2) {
2853 assert!(w[0] <= w[1], "messages should be sorted by timestamp: {} <= {}", w[0], w[1]);
2854 }
2855 }
2856
2857 #[test]
2858 fn compact_vec_rebuild_index_after_id_change() {
2859 let mut vec = CompactMessageVec::new();
2860 let old_id = "aa00000000000000000000000000000000000000000000000000000000000000";
2861 let new_id = "ff00000000000000000000000000000000000000000000000000000000000000";
2862 vec.insert(make_compact_msg(old_id, 100));
2863
2864 vec.messages_mut()[0].id = encode_message_id(new_id);
2866 assert!(!vec.contains_hex_id(new_id), "stale index should not find new ID");
2868
2869 vec.rebuild_index();
2871 assert!(vec.contains_hex_id(new_id), "after rebuild, new ID should be found");
2872 assert!(!vec.contains_hex_id(old_id), "after rebuild, old ID should not be found");
2873 }
2874
2875 #[test]
2876 fn compact_vec_pending_id_lookup() {
2877 let mut vec = CompactMessageVec::new();
2878 let pending = "pending-9876543210";
2879 vec.insert(make_compact_msg(pending, 500));
2880
2881 assert!(vec.contains_hex_id(pending), "should find pending ID");
2882 let found = vec.find_by_hex_id(pending);
2883 assert!(found.is_some(), "should find pending message");
2884 assert_eq!(found.unwrap().id_hex(), pending, "id_hex should match pending string");
2885 }
2886
2887 #[test]
2888 fn compact_vec_out_of_order_insert() {
2889 let mut vec = CompactMessageVec::new();
2890 vec.insert(make_compact_msg(
2892 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2893 ));
2894 vec.insert(make_compact_msg(
2895 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2896 ));
2897 vec.insert(make_compact_msg(
2898 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2899 ));
2900
2901 assert_eq!(vec.len(), 3);
2902 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2904 assert_eq!(timestamps, vec![100, 200, 300], "should be sorted by timestamp");
2905
2906 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2908 assert!(vec.contains_hex_id("bb00000000000000000000000000000000000000000000000000000000000000"));
2909 assert!(vec.contains_hex_id("cc00000000000000000000000000000000000000000000000000000000000000"));
2910 }
2911
2912 #[test]
2913 fn compact_vec_batch_prepend() {
2914 let mut vec = CompactMessageVec::new();
2915 vec.insert(make_compact_msg(
2917 "cc00000000000000000000000000000000000000000000000000000000000000", 300,
2918 ));
2919 vec.insert(make_compact_msg(
2920 "dd00000000000000000000000000000000000000000000000000000000000000", 400,
2921 ));
2922
2923 let older = vec![
2925 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2926 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2927 ];
2928 let added = vec.insert_batch(older);
2929 assert_eq!(added, 2);
2930 assert_eq!(vec.len(), 4);
2931
2932 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2934 assert_eq!(timestamps, vec![100, 200, 300, 400]);
2935
2936 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2938 assert!(vec.contains_hex_id("dd00000000000000000000000000000000000000000000000000000000000000"));
2939 }
2940
2941 #[test]
2942 fn compact_vec_clear() {
2943 let mut vec = CompactMessageVec::new();
2944 vec.insert(make_compact_msg(
2945 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2946 ));
2947 vec.insert(make_compact_msg(
2948 "bb00000000000000000000000000000000000000000000000000000000000000", 200,
2949 ));
2950 assert_eq!(vec.len(), 2);
2951
2952 vec.clear();
2953 assert!(vec.is_empty());
2954 assert_eq!(vec.len(), 0);
2955 assert!(!vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2956 }
2957
2958 fn make_full_message() -> Message {
2964 Message {
2965 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
2966 content: "Hello, world!".into(),
2967 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
2968 replied_to_content: Some("Original message".into()),
2969 replied_to_npub: Some("npub1replier".into()),
2970 replied_to_has_attachment: Some(true),
2971 preview_metadata: Some(SiteMetadata {
2972 domain: "example.com".into(),
2973 og_title: Some("Test Page".into()),
2974 og_description: Some("A test description".into()),
2975 og_image: Some("https://example.com/img.png".into()),
2976 og_url: Some("https://example.com".into()),
2977 og_type: Some("website".into()),
2978 title: Some("Test".into()),
2979 description: Some("Desc".into()),
2980 favicon: Some("https://example.com/favicon.ico".into()),
2981 }),
2982 attachments: vec![Attachment {
2983 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
2984 key: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
2985 nonce: "cccccccccccccccccccccccccccccccc".into(), extension: "png".into(),
2987 name: "photo.png".into(),
2988 url: "https://blossom.example.com".into(),
2989 path: "/tmp/photo.png".into(),
2990 size: 12345,
2991 img_meta: Some(ImageMetadata {
2992 thumbhash: "abc123".into(),
2993 width: 800,
2994 height: 600,
2995 }),
2996 downloading: false,
2997 downloaded: true,
2998 webxdc_topic: None,
2999 group_id: None,
3000 original_hash: None,
3001 scheme_version: None,
3002 mls_filename: None,
3003 }],
3004 reactions: vec![Reaction {
3005 id: "dddd000000000000000000000000000000000000000000000000000000000000".into(),
3006 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3007 author_id: "npub1reactor".into(),
3008 emoji: "\u{1f44d}".into(), emoji_url: None,
3010 }],
3011 at: 1705320000000, pending: false,
3013 failed: false,
3014 mine: true,
3015 npub: Some("npub1sender".into()),
3016 wrapper_event_id: Some("eeee000000000000000000000000000000000000000000000000000000000000".into()),
3017 edited: true,
3018 edit_history: Some(vec![
3019 EditEntry { content: "Original".into(), edited_at: 1705320000000 },
3020 EditEntry { content: "Edited".into(), edited_at: 1705320060000 },
3021 ]),
3022 emoji_tags: Vec::new(),
3023 }
3024 }
3025
3026 #[test]
3027 fn compact_message_from_message_roundtrip_all_fields() {
3028 let msg = make_full_message();
3029 let mut interner = NpubInterner::new();
3030 let compact = CompactMessage::from_message(&msg, &mut interner);
3031 let restored = compact.to_message(&interner);
3032
3033 assert_eq!(restored.id, msg.id, "id mismatch");
3034 assert_eq!(restored.content, msg.content, "content mismatch");
3035 assert_eq!(restored.mine, msg.mine, "mine mismatch");
3036 assert_eq!(restored.pending, msg.pending, "pending mismatch");
3037 assert_eq!(restored.failed, msg.failed, "failed mismatch");
3038 assert_eq!(restored.npub, msg.npub, "npub mismatch");
3039 assert_eq!(restored.replied_to, msg.replied_to, "replied_to mismatch");
3040 assert_eq!(restored.replied_to_content, msg.replied_to_content, "replied_to_content mismatch");
3041 assert_eq!(restored.replied_to_npub, msg.replied_to_npub, "replied_to_npub mismatch");
3042 assert_eq!(restored.replied_to_has_attachment, msg.replied_to_has_attachment, "replied_to_has_attachment mismatch");
3043 assert_eq!(restored.wrapper_event_id, msg.wrapper_event_id, "wrapper_event_id mismatch");
3044 assert_eq!(restored.edited, msg.edited, "edited mismatch");
3045 assert_eq!(restored.edit_history, msg.edit_history, "edit_history mismatch");
3046 assert_eq!(restored.preview_metadata, msg.preview_metadata, "preview_metadata mismatch");
3047 assert_eq!(restored.at / 1000, msg.at / 1000, "timestamp seconds mismatch");
3049 assert_eq!(restored.attachments.len(), 1, "should have 1 attachment");
3051 assert_eq!(restored.attachments[0].id, msg.attachments[0].id);
3052 assert_eq!(restored.attachments[0].name, msg.attachments[0].name);
3053 assert_eq!(restored.attachments[0].size, msg.attachments[0].size);
3054 assert_eq!(restored.reactions.len(), 1, "should have 1 reaction");
3056 assert_eq!(restored.reactions[0].emoji, msg.reactions[0].emoji);
3057 }
3058
3059 #[test]
3060 fn compact_message_from_message_owned_roundtrip() {
3061 let msg = make_full_message();
3062 let msg_clone = msg.clone();
3063 let mut interner = NpubInterner::new();
3064 let compact = CompactMessage::from_message_owned(msg, &mut interner);
3065 let restored = compact.to_message(&interner);
3066
3067 assert_eq!(restored.id, msg_clone.id, "id mismatch");
3068 assert_eq!(restored.content, msg_clone.content, "content mismatch");
3069 assert_eq!(restored.mine, msg_clone.mine, "mine mismatch");
3070 assert_eq!(restored.npub, msg_clone.npub, "npub mismatch");
3071 assert_eq!(restored.edit_history, msg_clone.edit_history, "edit_history mismatch");
3072 }
3073
3074 #[test]
3075 fn compact_message_pending_flag() {
3076 let msg = Message {
3077 id: "pending-1234567890".into(),
3078 pending: true,
3079 ..Message::default()
3080 };
3081 let mut interner = NpubInterner::new();
3082 let compact = CompactMessage::from_message(&msg, &mut interner);
3083 assert!(compact.is_pending(), "pending flag should be set");
3084 let restored = compact.to_message(&interner);
3085 assert!(restored.pending, "pending should roundtrip");
3086 assert_eq!(restored.id, "pending-1234567890", "pending ID should roundtrip");
3087 }
3088
3089 #[test]
3090 fn compact_message_failed_flag() {
3091 let msg = Message {
3092 id: "pending-999".into(),
3093 failed: true,
3094 pending: true,
3095 ..Message::default()
3096 };
3097 let mut interner = NpubInterner::new();
3098 let compact = CompactMessage::from_message(&msg, &mut interner);
3099 assert!(compact.is_failed(), "failed flag should be set");
3100 assert!(compact.is_pending(), "pending flag should also be set");
3101 let restored = compact.to_message(&interner);
3102 assert!(restored.failed);
3103 assert!(restored.pending);
3104 }
3105
3106 #[test]
3107 fn compact_message_with_attachments_roundtrip() {
3108 let msg = Message {
3109 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3110 attachments: vec![
3111 Attachment {
3112 id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3113 extension: "jpg".into(),
3114 name: "sunset.jpg".into(),
3115 size: 5000,
3116 downloaded: true,
3117 ..Attachment::default()
3118 },
3119 Attachment {
3120 id: "2222222222222222222222222222222222222222222222222222222222222222".into(),
3121 extension: "mp4".into(),
3122 name: "video.mp4".into(),
3123 size: 50000,
3124 downloaded: false,
3125 downloading: true,
3126 ..Attachment::default()
3127 },
3128 ],
3129 ..Message::default()
3130 };
3131 let mut interner = NpubInterner::new();
3132 let compact = CompactMessage::from_message(&msg, &mut interner);
3133 assert_eq!(compact.attachments.len(), 2);
3134
3135 let restored = compact.to_message(&interner);
3136 assert_eq!(restored.attachments.len(), 2);
3137 assert_eq!(restored.attachments[0].name, "sunset.jpg");
3138 assert_eq!(restored.attachments[0].extension, "jpg");
3139 assert!(restored.attachments[0].downloaded);
3140 assert_eq!(restored.attachments[1].name, "video.mp4");
3141 assert!(restored.attachments[1].downloading);
3142 assert!(!restored.attachments[1].downloaded);
3143 }
3144
3145 #[test]
3146 fn compact_message_with_reactions_roundtrip() {
3147 let msg = Message {
3148 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3149 reactions: vec![
3150 Reaction {
3151 id: "aaa0000000000000000000000000000000000000000000000000000000000000".into(),
3152 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3153 author_id: "npub1alice".into(),
3154 emoji: "\u{2764}".into(), emoji_url: None,
3156 },
3157 Reaction {
3158 id: "bbb0000000000000000000000000000000000000000000000000000000000000".into(),
3159 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3160 author_id: "npub1bob".into(),
3161 emoji: "\u{1f525}".into(), emoji_url: None,
3163 },
3164 ],
3165 ..Message::default()
3166 };
3167 let mut interner = NpubInterner::new();
3168 let compact = CompactMessage::from_message(&msg, &mut interner);
3169 let restored = compact.to_message(&interner);
3170
3171 assert_eq!(restored.reactions.len(), 2);
3172 assert_eq!(restored.reactions[0].emoji, "\u{2764}");
3173 assert_eq!(restored.reactions[0].author_id, "npub1alice");
3174 assert_eq!(restored.reactions[1].emoji, "\u{1f525}");
3175 assert_eq!(restored.reactions[1].author_id, "npub1bob");
3176 }
3177
3178 #[test]
3179 fn compact_message_with_edit_history_roundtrip() {
3180 let msg = Message {
3181 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3182 content: "Final version".into(),
3183 edited: true,
3184 edit_history: Some(vec![
3185 EditEntry { content: "First draft".into(), edited_at: 1000 },
3186 EditEntry { content: "Second draft".into(), edited_at: 2000 },
3187 EditEntry { content: "Final version".into(), edited_at: 3000 },
3188 ]),
3189 ..Message::default()
3190 };
3191 let mut interner = NpubInterner::new();
3192 let compact = CompactMessage::from_message(&msg, &mut interner);
3193 assert!(compact.is_edited());
3194
3195 let restored = compact.to_message(&interner);
3196 assert!(restored.edited);
3197 let history = restored.edit_history.unwrap();
3198 assert_eq!(history.len(), 3);
3199 assert_eq!(history[0].content, "First draft");
3200 assert_eq!(history[2].content, "Final version");
3201 }
3202
3203 #[test]
3204 fn compact_message_with_preview_metadata_roundtrip() {
3205 let meta = SiteMetadata {
3206 domain: "example.com".into(),
3207 og_title: Some("Title".into()),
3208 og_description: Some("Desc".into()),
3209 og_image: Some("https://example.com/img.png".into()),
3210 og_url: None,
3211 og_type: None,
3212 title: None,
3213 description: None,
3214 favicon: None,
3215 };
3216 let msg = Message {
3217 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3218 preview_metadata: Some(meta.clone()),
3219 ..Message::default()
3220 };
3221 let mut interner = NpubInterner::new();
3222 let compact = CompactMessage::from_message(&msg, &mut interner);
3223 let restored = compact.to_message(&interner);
3224
3225 let restored_meta = restored.preview_metadata.unwrap();
3226 assert_eq!(restored_meta.domain, "example.com");
3227 assert_eq!(restored_meta.og_title, Some("Title".into()));
3228 assert_eq!(restored_meta.og_image, Some("https://example.com/img.png".into()));
3229 assert_eq!(restored_meta.og_url, None);
3230 }
3231
3232 #[test]
3233 fn compact_message_with_replied_to_roundtrip() {
3234 let msg = Message {
3235 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3236 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3237 replied_to_content: Some("Original text".into()),
3238 replied_to_npub: Some("npub1original".into()),
3239 replied_to_has_attachment: Some(false),
3240 ..Message::default()
3241 };
3242 let mut interner = NpubInterner::new();
3243 let compact = CompactMessage::from_message(&msg, &mut interner);
3244 assert!(compact.has_reply());
3245
3246 let restored = compact.to_message(&interner);
3247 assert_eq!(restored.replied_to, "1111111111111111111111111111111111111111111111111111111111111111");
3248 assert_eq!(restored.replied_to_content, Some("Original text".into()));
3249 assert_eq!(restored.replied_to_npub, Some("npub1original".into()));
3250 assert_eq!(restored.replied_to_has_attachment, Some(false));
3251 }
3252
3253 #[test]
3254 fn compact_message_empty_roundtrip() {
3255 let msg = Message::default();
3256 let mut interner = NpubInterner::new();
3257 let compact = CompactMessage::from_message(&msg, &mut interner);
3258 let restored = compact.to_message(&interner);
3259
3260 assert_eq!(restored.id, "0000000000000000000000000000000000000000000000000000000000000000",
3261 "empty ID should decode as all zeros hex");
3262 assert_eq!(restored.content, "");
3263 assert!(!restored.mine);
3264 assert!(!restored.pending);
3265 assert!(!restored.failed);
3266 assert!(!restored.edited);
3267 assert_eq!(restored.npub, None);
3268 assert!(restored.replied_to.is_empty() || restored.replied_to == "0000000000000000000000000000000000000000000000000000000000000000");
3269 assert_eq!(restored.replied_to_content, None);
3270 assert_eq!(restored.replied_to_npub, None);
3271 assert_eq!(restored.replied_to_has_attachment, None);
3272 assert_eq!(restored.wrapper_event_id, None);
3273 assert!(restored.attachments.is_empty());
3274 assert!(restored.reactions.is_empty());
3275 assert_eq!(restored.edit_history, None);
3276 assert_eq!(restored.preview_metadata, None);
3277 }
3278
3279 #[test]
3284 fn compact_attachment_from_attachment_roundtrip() {
3285 let att = Attachment {
3286 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3287 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3288 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), extension: "zip".into(),
3290 name: "archive.zip".into(),
3291 url: "https://blossom.test.com".into(),
3292 path: "/downloads/archive.zip".into(),
3293 size: 99999,
3294 img_meta: None,
3295 downloading: false,
3296 downloaded: true,
3297 webxdc_topic: None,
3298 group_id: None,
3299 original_hash: None,
3300 scheme_version: None,
3301 mls_filename: None,
3302 };
3303
3304 let compact = CompactAttachment::from_attachment(&att);
3305 let restored = compact.to_attachment();
3306
3307 assert_eq!(restored.id, att.id, "id mismatch");
3308 assert_eq!(restored.key, att.key, "key mismatch");
3309 assert_eq!(restored.nonce, att.nonce, "nonce mismatch");
3310 assert_eq!(restored.extension, att.extension, "extension mismatch");
3311 assert_eq!(restored.name, att.name, "name mismatch");
3312 assert_eq!(restored.url, att.url, "url mismatch");
3313 assert_eq!(restored.path, att.path, "path mismatch");
3314 assert_eq!(restored.size, att.size, "size mismatch");
3315 assert_eq!(restored.downloading, att.downloading, "downloading mismatch");
3316 assert_eq!(restored.downloaded, att.downloaded, "downloaded mismatch");
3317 }
3318
3319 #[test]
3320 fn compact_attachment_from_attachment_owned_roundtrip() {
3321 let att = Attachment {
3322 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3323 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3324 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3325 extension: "pdf".into(),
3326 name: "document.pdf".into(),
3327 url: "https://server.com".into(),
3328 path: "".into(),
3329 size: 1024,
3330 img_meta: None,
3331 downloading: false,
3332 downloaded: false,
3333 webxdc_topic: None,
3334 group_id: None,
3335 original_hash: None,
3336 scheme_version: None,
3337 mls_filename: None,
3338 };
3339 let att_clone = att.clone();
3340
3341 let compact = CompactAttachment::from_attachment_owned(att);
3342 let restored = compact.to_attachment();
3343
3344 assert_eq!(restored.id, att_clone.id);
3345 assert_eq!(restored.name, att_clone.name);
3346 assert_eq!(restored.size, att_clone.size);
3347 }
3348
3349 #[test]
3350 fn compact_attachment_key_nonce_zeros() {
3351 let att = Attachment {
3352 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3353 key: "".into(), nonce: "".into(), ..Attachment::default()
3356 };
3357 let compact = CompactAttachment::from_attachment(&att);
3358 assert_eq!(compact.key, [0u8; 32], "empty key should be all zeros");
3359 assert_eq!(compact.nonce, [0u8; 16], "empty nonce should be all zeros");
3360
3361 let restored = compact.to_attachment();
3362 assert_eq!(restored.key, "", "zero key should restore as empty string");
3363 assert_eq!(restored.nonce, "", "zero nonce should restore as empty string");
3364 }
3365
3366 #[test]
3367 fn compact_attachment_short_nonce_mls_12byte() {
3368 let att = Attachment {
3370 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3371 nonce: "aabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3373 };
3374 let compact = CompactAttachment::from_attachment(&att);
3375 assert!(compact.flags.is_short_nonce(), "12-byte nonce should set short_nonce flag");
3376
3377 let restored = compact.to_attachment();
3378 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccdd", "short nonce should roundtrip");
3379 }
3380
3381 #[test]
3382 fn compact_attachment_long_nonce_dm_16byte() {
3383 let att = Attachment {
3385 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3386 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3388 };
3389 let compact = CompactAttachment::from_attachment(&att);
3390 assert!(!compact.flags.is_short_nonce(), "16-byte nonce should NOT set short_nonce flag");
3391
3392 let restored = compact.to_attachment();
3393 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccddaabbccdd", "long nonce should roundtrip");
3394 }
3395
3396 #[test]
3397 fn compact_attachment_id_eq_comparison() {
3398 let att = Attachment {
3399 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3400 ..Attachment::default()
3401 };
3402 let compact = CompactAttachment::from_attachment(&att);
3403
3404 assert!(compact.id_eq("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
3405 "id_eq should match same hex");
3406 assert!(!compact.id_eq("1111111111111111111111111111111111111111111111111111111111111111"),
3407 "id_eq should not match different hex");
3408 }
3409
3410 #[test]
3411 fn compact_attachment_all_optional_fields_none() {
3412 let att = Attachment::default();
3413 let compact = CompactAttachment::from_attachment(&att);
3414
3415 assert!(compact.img_meta.is_none());
3416 assert!(compact.group_id.is_none());
3417 assert!(compact.original_hash.is_none());
3418 assert!(compact.webxdc_topic.is_none());
3419 assert!(compact.mls_filename.is_none());
3420 assert!(compact.scheme_version.is_none());
3421
3422 let restored = compact.to_attachment();
3423 assert!(restored.img_meta.is_none());
3424 assert!(restored.group_id.is_none());
3425 assert!(restored.original_hash.is_none());
3426 assert!(restored.webxdc_topic.is_none());
3427 assert!(restored.mls_filename.is_none());
3428 assert!(restored.scheme_version.is_none());
3429 }
3430
3431 #[test]
3432 fn compact_attachment_all_optional_fields_some() {
3433 let att = Attachment {
3434 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3435 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3436 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3437 extension: "xdc".into(),
3438 name: "app.xdc".into(),
3439 url: "https://server.com/file".into(),
3440 path: "/local/app.xdc".into(),
3441 size: 50000,
3442 img_meta: Some(ImageMetadata {
3443 thumbhash: "hash123".into(),
3444 width: 1920,
3445 height: 1080,
3446 }),
3447 downloading: false,
3448 downloaded: true,
3449 webxdc_topic: Some("game-state".into()),
3450 group_id: Some("cccc000000000000000000000000000000000000000000000000000000000000".into()),
3451 original_hash: Some("dddd000000000000000000000000000000000000000000000000000000000000".into()),
3452 scheme_version: Some("mip04-v1".into()),
3453 mls_filename: Some("encrypted.bin".into()),
3454 };
3455
3456 let compact = CompactAttachment::from_attachment(&att);
3457
3458 assert!(compact.img_meta.is_some());
3459 assert!(compact.group_id.is_some());
3460 assert!(compact.original_hash.is_some());
3461 assert!(compact.webxdc_topic.is_some());
3462 assert!(compact.mls_filename.is_some());
3463 assert!(compact.scheme_version.is_some());
3464
3465 let restored = compact.to_attachment();
3466 let meta = restored.img_meta.unwrap();
3467 assert_eq!(meta.thumbhash, "hash123");
3468 assert_eq!(meta.width, 1920);
3469 assert_eq!(meta.height, 1080);
3470 assert_eq!(restored.webxdc_topic, Some("game-state".into()));
3471 assert_eq!(restored.group_id.unwrap(), att.group_id.unwrap());
3472 assert_eq!(restored.original_hash.unwrap(), att.original_hash.unwrap());
3473 assert_eq!(restored.scheme_version, Some("mip04-v1".into()));
3474 assert_eq!(restored.mls_filename, Some("encrypted.bin".into()));
3475 }
3476
3477 #[test]
3482 fn compact_reaction_from_reaction_roundtrip() {
3483 let reaction = Reaction {
3484 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3485 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3486 author_id: "npub1alice".into(),
3487 emoji: "+".into(),
3488 emoji_url: None,
3489 };
3490 let mut interner = NpubInterner::new();
3491 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3492 let restored = compact.to_reaction(&interner);
3493
3494 assert_eq!(restored.id, reaction.id, "id mismatch");
3495 assert_eq!(restored.reference_id, reaction.reference_id, "reference_id mismatch");
3496 assert_eq!(restored.author_id, reaction.author_id, "author_id mismatch");
3497 assert_eq!(restored.emoji, reaction.emoji, "emoji mismatch");
3498 }
3499
3500 #[test]
3501 fn compact_reaction_author_resolved_via_interner() {
3502 let mut interner = NpubInterner::new();
3503 let alice_handle = interner.intern("npub1alice");
3504
3505 let reaction = Reaction {
3506 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3507 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3508 author_id: "npub1alice".into(),
3509 emoji: "+".into(),
3510 emoji_url: None,
3511 };
3512 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3513 assert_eq!(compact.author_idx, alice_handle, "should reuse existing interner handle");
3514
3515 let resolved = interner.resolve(compact.author_idx).unwrap();
3516 assert_eq!(resolved, "npub1alice");
3517 }
3518
3519 #[test]
3520 fn compact_reaction_unicode_emoji() {
3521 let reaction = Reaction {
3522 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3523 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3524 author_id: "npub1test".into(),
3525 emoji: "\u{1f431}\u{200d}\u{1f4bb}".into(), emoji_url: None,
3527 };
3528 let mut interner = NpubInterner::new();
3529 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3530 let restored = compact.to_reaction(&interner);
3531 assert_eq!(restored.emoji, "\u{1f431}\u{200d}\u{1f4bb}", "complex unicode emoji should roundtrip");
3532 }
3533
3534 #[test]
3535 fn compact_reaction_custom_emoji() {
3536 let reaction = Reaction {
3537 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3538 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3539 author_id: "npub1test".into(),
3540 emoji: ":cat_heart_eyes:".into(),
3541 emoji_url: None,
3542 };
3543 let mut interner = NpubInterner::new();
3544 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3545 let restored = compact.to_reaction(&interner);
3546 assert_eq!(restored.emoji, ":cat_heart_eyes:", "custom emoji shortcode should roundtrip");
3547 }
3548
3549 #[test]
3550 fn compact_reaction_owned_conversion() {
3551 let reaction = Reaction {
3552 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3553 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3554 author_id: "npub1bob".into(),
3555 emoji: "\u{1f44d}".into(), emoji_url: None,
3557 };
3558 let reaction_clone = reaction.clone();
3559 let mut interner = NpubInterner::new();
3560 let compact = CompactReaction::from_reaction_owned(reaction, &mut interner);
3561 let restored = compact.to_reaction(&interner);
3562
3563 assert_eq!(restored.id, reaction_clone.id);
3564 assert_eq!(restored.author_id, reaction_clone.author_id);
3565 assert_eq!(restored.emoji, reaction_clone.emoji);
3566 }
3567
3568 #[test]
3573 fn tinyvec_empty() {
3574 let tv: TinyVec<u32> = TinyVec::new();
3575 assert!(tv.is_empty());
3576 assert_eq!(tv.len(), 0);
3577 assert_eq!(tv.as_slice(), &[] as &[u32]);
3578 assert_eq!(tv.first(), None);
3579 assert_eq!(tv.last(), None);
3580 }
3581
3582 #[test]
3583 fn tinyvec_from_vec_and_back() {
3584 let original = vec![1u32, 2, 3, 4, 5];
3585 let tv = TinyVec::from_vec(original.clone());
3586 assert_eq!(tv.len(), 5);
3587 assert_eq!(tv.to_vec(), original);
3588 }
3589
3590 #[test]
3591 fn tinyvec_indexing() {
3592 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3593 assert_eq!(tv[0], 10);
3594 assert_eq!(tv[1], 20);
3595 assert_eq!(tv[2], 30);
3596 assert_eq!(tv.get(0), Some(&10));
3597 assert_eq!(tv.get(3), None);
3598 }
3599
3600 #[test]
3601 fn tinyvec_push() {
3602 let mut tv = TinyVec::from_vec(vec![1u32, 2]);
3603 tv.push(3);
3604 assert_eq!(tv.len(), 3);
3605 assert_eq!(tv.to_vec(), vec![1, 2, 3]);
3606 }
3607
3608 #[test]
3609 fn tinyvec_clone() {
3610 let tv = TinyVec::from_vec(vec!["hello".to_string(), "world".to_string()]);
3611 let cloned = tv.clone();
3612 assert_eq!(cloned.len(), 2);
3613 assert_eq!(cloned[0], "hello");
3614 assert_eq!(cloned[1], "world");
3615 }
3616
3617 #[test]
3618 fn tinyvec_retain() {
3619 let mut tv = TinyVec::from_vec(vec![1u32, 2, 3, 4, 5]);
3620 tv.retain(|&x| x % 2 == 0);
3621 assert_eq!(tv.to_vec(), vec![2, 4]);
3622 }
3623
3624 #[test]
3625 fn tinyvec_empty_from_empty_vec() {
3626 let tv = TinyVec::<u32>::from_vec(vec![]);
3627 assert!(tv.is_empty());
3628 assert_eq!(tv.len(), 0);
3629 }
3630
3631 #[test]
3632 fn tinyvec_iter() {
3633 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3634 let sum: u32 = tv.iter().sum();
3635 assert_eq!(sum, 60);
3636 }
3637
3638 #[test]
3639 fn tinyvec_any() {
3640 let tv = TinyVec::from_vec(vec![1u32, 2, 3]);
3641 assert!(tv.any(|&x| x == 2));
3642 assert!(!tv.any(|&x| x == 99));
3643 }
3644
3645 #[test]
3650 fn hex_to_bytes_16_full_32_chars() {
3651 let hex = "aabbccddaabbccddaabbccddaabbccdd";
3652 let bytes = hex_to_bytes_16(hex);
3653 assert_eq!(bytes[0], 0xaa);
3654 assert_eq!(bytes[1], 0xbb);
3655 assert_eq!(bytes[15], 0xdd);
3656 }
3657
3658 #[test]
3659 fn hex_to_bytes_16_short_input_padded() {
3660 let hex = "aabb";
3662 let bytes = hex_to_bytes_16(hex);
3663 assert_eq!(bytes[0], 0xaa);
3664 assert_eq!(bytes[1], 0xbb);
3665 for i in 2..16 {
3667 assert_eq!(bytes[i], 0, "byte {} should be 0 from padding", i);
3668 }
3669 }
3670
3671 #[test]
3672 fn bytes_to_hex_string_roundtrip() {
3673 let bytes: Vec<u8> = vec![0xaa, 0xbb, 0xcc, 0xdd];
3674 let hex = bytes_to_hex_string(&bytes);
3675 assert_eq!(hex, "aabbccdd");
3676 }
3677}