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;
49
50const PENDING_ID_SENTINEL: [u8; 15] = [0xFE; 15];
56
57#[inline]
62pub fn encode_message_id(id: &str) -> [u8; 32] {
63 if let Some(timestamp_str) = id.strip_prefix("pending-") {
64 let mut bytes = [0u8; 32];
66 bytes[0] = PENDING_ID_MARKER;
67 if let Ok(timestamp) = timestamp_str.parse::<u128>() {
68 bytes[1..17].copy_from_slice(×tamp.to_le_bytes());
69 }
70 bytes[17..32].copy_from_slice(&PENDING_ID_SENTINEL);
71 bytes
72 } else {
73 hex_to_bytes_32(id)
74 }
75}
76
77#[inline]
79pub fn decode_message_id(bytes: &[u8; 32]) -> String {
80 if bytes[0] == PENDING_ID_MARKER && bytes[17..32] == PENDING_ID_SENTINEL {
81 let mut timestamp_bytes = [0u8; 16];
83 timestamp_bytes.copy_from_slice(&bytes[1..17]);
84 let timestamp = u128::from_le_bytes(timestamp_bytes);
85 format!("pending-{}", timestamp)
86 } else {
87 bytes_to_hex_32(bytes)
88 }
89}
90
91#[inline]
98pub fn timestamp_to_compact(ms: u64) -> u64 {
99 ms
100}
101
102#[inline]
104pub fn timestamp_from_compact(compact: u64) -> u64 {
105 compact
106}
107
108pub const MAX_REACTION_GROUPS: usize = 12;
113
114const EPOCH_2020_SECS: u64 = 1577836800;
116
117#[inline]
120pub fn secs_to_compact(secs: u64) -> u32 {
121 if secs == 0 { return 0; }
122 secs.saturating_sub(EPOCH_2020_SECS) as u32
123}
124
125#[inline]
128pub fn secs_from_compact(compact: u32) -> u64 {
129 if compact == 0 { return 0; }
130 EPOCH_2020_SECS + compact as u64
131}
132
133#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
144pub struct MessageFlags(u8);
145
146impl MessageFlags {
147 pub const NONE: Self = Self(0);
148 pub const MINE: Self = Self(0b00001);
149 pub const PENDING: Self = Self(0b00010);
150 pub const FAILED: Self = Self(0b00100);
151 const REPLY_ATTACH_MASK: u8 = 0b11000;
154 const REPLY_ATTACH_SHIFT: u8 = 3;
155
156 #[inline]
157 pub fn is_mine(self) -> bool {
158 self.0 & Self::MINE.0 != 0
159 }
160
161 #[inline]
162 pub fn is_pending(self) -> bool {
163 self.0 & Self::PENDING.0 != 0
164 }
165
166 #[inline]
167 pub fn is_failed(self) -> bool {
168 self.0 & Self::FAILED.0 != 0
169 }
170
171 #[inline]
174 pub fn replied_to_has_attachment(self) -> Option<bool> {
175 match (self.0 & Self::REPLY_ATTACH_MASK) >> Self::REPLY_ATTACH_SHIFT {
176 0b00 => None, 0b01 => Some(false), 0b10 => Some(true), _ => None, }
181 }
182
183 #[inline]
184 pub fn set_mine(&mut self, value: bool) {
185 if value {
186 self.0 |= Self::MINE.0;
187 } else {
188 self.0 &= !Self::MINE.0;
189 }
190 }
191
192 #[inline]
193 pub fn set_pending(&mut self, value: bool) {
194 if value {
195 self.0 |= Self::PENDING.0;
196 } else {
197 self.0 &= !Self::PENDING.0;
198 }
199 }
200
201 #[inline]
202 pub fn set_failed(&mut self, value: bool) {
203 if value {
204 self.0 |= Self::FAILED.0;
205 } else {
206 self.0 &= !Self::FAILED.0;
207 }
208 }
209
210 #[inline]
212 pub fn set_replied_to_has_attachment(&mut self, value: Option<bool>) {
213 self.0 &= !Self::REPLY_ATTACH_MASK;
215 let bits = match value {
217 None => 0b00,
218 Some(false) => 0b01,
219 Some(true) => 0b10,
220 };
221 self.0 |= bits << Self::REPLY_ATTACH_SHIFT;
222 }
223
224 #[inline]
226 pub fn from_bools(mine: bool, pending: bool, failed: bool) -> Self {
227 let mut flags = Self::NONE;
228 flags.set_mine(mine);
229 flags.set_pending(pending);
230 flags.set_failed(failed);
231 flags
232 }
233
234 #[inline]
236 pub fn from_all(mine: bool, pending: bool, failed: bool, replied_to_has_attachment: Option<bool>) -> Self {
237 let mut flags = Self::from_bools(mine, pending, failed);
238 flags.set_replied_to_has_attachment(replied_to_has_attachment);
239 flags
240 }
241}
242
243use std::alloc::{alloc, dealloc, Layout};
248use std::marker::PhantomData;
249use std::ptr::NonNull;
250
251pub struct TinyVec<T> {
267 ptr: Option<NonNull<u8>>,
269 _marker: PhantomData<T>,
270}
271
272impl<T> TinyVec<T> {
273 #[inline]
275 pub const fn new() -> Self {
276 Self {
277 ptr: None,
278 _marker: PhantomData,
279 }
280 }
281
282 pub fn from_vec(vec: Vec<T>) -> Self {
284 if vec.is_empty() {
285 return Self::new();
286 }
287
288 let len = vec.len().min(255) as u8;
289
290 let (layout, items_offset) = Self::layout_for(len as usize);
292
293 unsafe {
294 let ptr = alloc(layout);
296 if ptr.is_null() {
297 std::alloc::handle_alloc_error(layout);
298 }
299
300 *ptr = len;
302
303 let items_ptr = ptr.add(items_offset) as *mut T;
305 for (i, item) in vec.into_iter().take(len as usize).enumerate() {
306 std::ptr::write(items_ptr.add(i), item);
307 }
308
309 Self {
310 ptr: NonNull::new(ptr),
311 _marker: PhantomData,
312 }
313 }
314 }
315
316 fn layout_for(len: usize) -> (Layout, usize) {
318 let header_layout = Layout::new::<u8>();
319 let items_layout = Layout::array::<T>(len).unwrap();
320 header_layout.extend(items_layout).unwrap()
321 }
322
323 #[inline]
325 pub fn len(&self) -> usize {
326 match self.ptr {
327 None => 0,
328 Some(ptr) => unsafe { *ptr.as_ptr() as usize },
329 }
330 }
331
332 #[inline]
333 pub fn is_empty(&self) -> bool {
334 self.ptr.is_none()
335 }
336
337 #[inline]
339 fn items_offset() -> usize {
340 let header_layout = Layout::new::<u8>();
341 let items_layout = Layout::new::<T>();
342 header_layout.extend(items_layout).map(|(_, offset)| offset).unwrap_or(1)
343 }
344
345 #[inline]
347 pub fn as_slice(&self) -> &[T] {
348 match self.ptr {
349 None => &[],
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 *const T;
354 std::slice::from_raw_parts(items_ptr, len)
355 },
356 }
357 }
358
359 #[inline]
361 pub fn as_mut_slice(&mut self) -> &mut [T] {
362 match self.ptr {
363 None => &mut [],
364 Some(ptr) => unsafe {
365 let base = ptr.as_ptr();
366 let len = *base as usize;
367 let items_ptr = base.add(Self::items_offset()) as *mut T;
368 std::slice::from_raw_parts_mut(items_ptr, len)
369 },
370 }
371 }
372
373 #[inline]
375 pub fn iter(&self) -> std::slice::Iter<'_, T> {
376 self.as_slice().iter()
377 }
378
379 #[inline]
381 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
382 self.as_mut_slice().iter_mut()
383 }
384
385 pub fn to_vec(&self) -> Vec<T>
387 where
388 T: Clone,
389 {
390 self.as_slice().to_vec()
391 }
392
393 #[inline]
395 pub fn first(&self) -> Option<&T> {
396 self.as_slice().first()
397 }
398
399 #[inline]
401 pub fn last(&self) -> Option<&T> {
402 self.as_slice().last()
403 }
404
405 #[inline]
407 pub fn last_mut(&mut self) -> Option<&mut T> {
408 self.as_mut_slice().last_mut()
409 }
410
411 #[inline]
413 pub fn get(&self, index: usize) -> Option<&T> {
414 self.as_slice().get(index)
415 }
416
417 #[inline]
419 pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
420 self.as_mut_slice().get_mut(index)
421 }
422
423 pub fn push(&mut self, item: T)
425 where
426 T: Clone,
427 {
428 let mut vec = self.to_vec();
429 vec.push(item);
430 *self = Self::from_vec(vec);
431 }
432
433 pub fn retain<F>(&mut self, f: F)
435 where
436 T: Clone,
437 F: FnMut(&T) -> bool,
438 {
439 let mut vec = self.to_vec();
440 vec.retain(f);
441 *self = Self::from_vec(vec);
442 }
443
444 pub fn any<F>(&self, f: F) -> bool
446 where
447 F: FnMut(&T) -> bool,
448 {
449 self.as_slice().iter().any(f)
450 }
451}
452
453impl<T> std::ops::Index<usize> for TinyVec<T> {
455 type Output = T;
456
457 fn index(&self, index: usize) -> &Self::Output {
458 &self.as_slice()[index]
459 }
460}
461
462impl<T> std::ops::IndexMut<usize> for TinyVec<T> {
463 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
464 &mut self.as_mut_slice()[index]
465 }
466}
467
468impl<'a, T> IntoIterator for &'a TinyVec<T> {
470 type Item = &'a T;
471 type IntoIter = std::slice::Iter<'a, T>;
472
473 fn into_iter(self) -> Self::IntoIter {
474 self.as_slice().iter()
475 }
476}
477
478impl<'a, T> IntoIterator for &'a mut TinyVec<T> {
480 type Item = &'a mut T;
481 type IntoIter = std::slice::IterMut<'a, T>;
482
483 fn into_iter(self) -> Self::IntoIter {
484 self.as_mut_slice().iter_mut()
485 }
486}
487
488impl<T> Default for TinyVec<T> {
489 fn default() -> Self {
490 Self::new()
491 }
492}
493
494impl<T: Clone> Clone for TinyVec<T> {
495 fn clone(&self) -> Self {
496 Self::from_vec(self.to_vec())
497 }
498}
499
500impl<T: std::fmt::Debug> std::fmt::Debug for TinyVec<T> {
501 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502 f.debug_list().entries(self.as_slice()).finish()
503 }
504}
505
506impl<T> Drop for TinyVec<T> {
507 fn drop(&mut self) {
508 if let Some(ptr) = self.ptr {
509 unsafe {
510 let base = ptr.as_ptr();
511 let len = *base as usize;
512 let items_ptr = base.add(Self::items_offset()) as *mut T;
513
514 for i in 0..len {
516 std::ptr::drop_in_place(items_ptr.add(i));
517 }
518
519 let (layout, _) = Self::layout_for(len);
521 dealloc(base, layout);
522 }
523 }
524 }
525}
526
527unsafe impl<T: Send> Send for TinyVec<T> {}
529unsafe impl<T: Sync> Sync for TinyVec<T> {}
530
531#[derive(Clone, Debug)]
543pub struct CompactReaction {
544 pub id: [u8; 32],
546 pub author_idx: u16,
548 pub emoji: Box<str>,
550 pub emoji_url: Option<Box<str>>,
553}
554
555impl CompactReaction {
556 #[inline]
558 pub fn id_hex(&self) -> String {
559 bytes_to_hex_32(&self.id)
560 }
561
562 pub fn from_reaction(reaction: &Reaction, interner: &mut NpubInterner) -> Self {
564 Self {
565 id: hex_to_bytes_32(&reaction.id),
566 author_idx: interner.intern(&reaction.author_id),
567 emoji: reaction.emoji.clone().into_boxed_str(),
568 emoji_url: reaction.emoji_url.as_deref().map(|s| s.into()),
569 }
570 }
571
572 pub fn from_reaction_owned(reaction: Reaction, interner: &mut NpubInterner) -> Self {
574 Self {
575 id: hex_to_bytes_32(&reaction.id),
576 author_idx: interner.intern(&reaction.author_id),
577 emoji: reaction.emoji.into_boxed_str(),
578 emoji_url: reaction.emoji_url.map(|s| s.into_boxed_str()),
579 }
580 }
581
582 pub fn to_reaction(&self, parent_id: &[u8; 32], interner: &NpubInterner) -> Reaction {
587 Reaction {
588 id: self.id_hex(),
589 reference_id: bytes_to_hex_32(parent_id),
590 author_id: interner.resolve(self.author_idx)
591 .map(|s| s.to_string())
592 .unwrap_or_default(),
593 emoji: self.emoji.to_string(),
594 emoji_url: self.emoji_url.as_deref().map(|s| s.to_string()),
595 }
596 }
597}
598
599#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
605pub struct AttachmentFlags(u8);
606
607impl AttachmentFlags {
608 pub const NONE: Self = Self(0);
609 const DOWNLOADING: u8 = 0b0001;
610 const DOWNLOADED: u8 = 0b0010;
611 const SHORT_NONCE: u8 = 0b0100; #[inline]
614 pub fn is_downloading(self) -> bool { self.0 & Self::DOWNLOADING != 0 }
615 #[inline]
616 pub fn is_downloaded(self) -> bool { self.0 & Self::DOWNLOADED != 0 }
617 #[inline]
618 pub fn is_short_nonce(self) -> bool { self.0 & Self::SHORT_NONCE != 0 }
619
620 #[inline]
621 pub fn set_downloading(&mut self, value: bool) {
622 if value { self.0 |= Self::DOWNLOADING; } else { self.0 &= !Self::DOWNLOADING; }
623 }
624 #[inline]
625 pub fn set_downloaded(&mut self, value: bool) {
626 if value { self.0 |= Self::DOWNLOADED; } else { self.0 &= !Self::DOWNLOADED; }
627 }
628 #[inline]
629 pub fn set_short_nonce(&mut self, value: bool) {
630 if value { self.0 |= Self::SHORT_NONCE; } else { self.0 &= !Self::SHORT_NONCE; }
631 }
632
633 pub fn from_bools(downloading: bool, downloaded: bool) -> Self {
634 let mut flags = Self::NONE;
635 flags.set_downloading(downloading);
636 flags.set_downloaded(downloaded);
637 flags
638 }
639}
640
641#[derive(Clone, Debug)]
652pub struct CompactAttachment {
653 pub id: [u8; 32],
656 pub key: [u8; 32],
658 pub nonce: [u8; 16],
660 pub size: u64,
662 pub flags: AttachmentFlags,
664
665 pub extension: Box<str>,
668 pub url: Box<str>,
670 pub path: Box<str>,
672
673 pub img_meta: Option<Box<ImageMetadata>>,
676 pub group_id: Option<Box<[u8; 32]>>,
678 pub original_hash: Option<Box<[u8; 32]>>,
680 pub webxdc_topic: Option<Box<str>>,
682 pub fallback_urls: Option<Box<[Box<str>]>>,
684 pub name: Box<str>,
686}
687
688impl CompactAttachment {
689 #[inline]
691 pub fn downloaded(&self) -> bool { self.flags.is_downloaded() }
692 #[inline]
693 pub fn downloading(&self) -> bool { self.flags.is_downloading() }
694 #[inline]
695 pub fn set_downloaded(&mut self, value: bool) { self.flags.set_downloaded(value); }
696 #[inline]
697 pub fn set_downloading(&mut self, value: bool) { self.flags.set_downloading(value); }
698
699 #[inline]
701 pub fn id_eq(&self, hex_id: &str) -> bool {
702 self.id == hex_to_bytes_32(hex_id)
703 }
704
705 #[inline]
707 pub fn id_hex(&self) -> String {
708 bytes_to_hex_32(&self.id)
709 }
710
711 pub fn key_hex(&self) -> String {
713 if self.key == [0u8; 32] {
714 String::new()
715 } else {
716 bytes_to_hex_32(&self.key)
717 }
718 }
719
720 pub fn nonce_hex(&self) -> String {
722 if self.nonce == [0u8; 16] {
723 String::new()
724 } else if self.flags.is_short_nonce() {
725 bytes_to_hex_string(&self.nonce[..12])
727 } else {
728 bytes_to_hex_string(&self.nonce)
730 }
731 }
732
733 pub fn from_attachment(att: &Attachment) -> Self {
735 let is_short_nonce = att.nonce.len() == 24;
737 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
738 flags.set_short_nonce(is_short_nonce);
739
740 Self {
741 id: hex_to_bytes_32(&att.id),
742 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
743 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
744 size: att.size,
745 flags,
746 extension: att.extension.clone().into_boxed_str(),
747 url: att.url.clone().into_boxed_str(),
748 path: att.path.clone().into_boxed_str(),
749 img_meta: att.img_meta.clone().map(Box::new),
750 group_id: att.group_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
751 original_hash: att.original_hash.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
752 webxdc_topic: att.webxdc_topic.clone().map(|s| s.into_boxed_str()),
753 fallback_urls: (!att.fallback_urls.is_empty())
754 .then(|| att.fallback_urls.iter().map(|s| s.as_str().into()).collect()),
755 name: att.name.clone().into_boxed_str(),
756 }
757 }
758
759 pub fn from_attachment_owned(att: Attachment) -> Self {
761 let is_short_nonce = att.nonce.len() == 24;
763 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
764 flags.set_short_nonce(is_short_nonce);
765
766 Self {
767 id: hex_to_bytes_32(&att.id),
768 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
769 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
770 size: att.size,
771 flags,
772 extension: att.extension.into_boxed_str(),
773 url: att.url.into_boxed_str(),
774 path: att.path.into_boxed_str(),
775 img_meta: att.img_meta.map(Box::new),
776 group_id: att.group_id.map(|s| Box::new(hex_to_bytes_32(&s))),
777 original_hash: att.original_hash.map(|s| Box::new(hex_to_bytes_32(&s))),
778 webxdc_topic: att.webxdc_topic.map(|s| s.into_boxed_str()),
779 fallback_urls: (!att.fallback_urls.is_empty())
780 .then(|| att.fallback_urls.into_iter().map(|s| s.into_boxed_str()).collect()),
781 name: att.name.into_boxed_str(),
782 }
783 }
784
785 pub fn to_attachment(&self) -> Attachment {
787 Attachment {
788 id: self.id_hex(),
789 key: self.key_hex(),
790 nonce: self.nonce_hex(),
791 extension: self.extension.to_string(),
792 name: self.name.to_string(),
793 url: self.url.to_string(),
794 path: self.path.to_string(),
795 size: self.size,
796 img_meta: self.img_meta.as_ref().map(|b| (**b).clone()),
797 downloading: self.flags.is_downloading(),
798 downloaded: self.flags.is_downloaded(),
799 webxdc_topic: self.webxdc_topic.as_ref().map(|s| s.to_string()),
800 group_id: self.group_id.as_ref().map(|b| bytes_to_hex_32(b)),
801 original_hash: self.original_hash.as_ref().map(|b| bytes_to_hex_32(b)),
802 fallback_urls: self
803 .fallback_urls
804 .as_deref()
805 .map(|urls| urls.iter().map(|u| u.to_string()).collect())
806 .unwrap_or_default(),
807 }
808 }
809}
810
811#[inline]
815fn parse_nonce(hex: &str) -> [u8; 16] {
816 hex_to_bytes_16(hex)
817}
818
819#[derive(Clone, Debug, Default)]
831pub struct NpubInterner {
832 npubs: Vec<String>,
834 sorted: Vec<u16>,
836}
837
838pub const NO_NPUB: u16 = u16::MAX;
840
841impl NpubInterner {
842 pub fn new() -> Self {
843 Self {
844 npubs: Vec::new(),
845 sorted: Vec::new(),
846 }
847 }
848
849 pub fn with_capacity(capacity: usize) -> Self {
851 Self {
852 npubs: Vec::with_capacity(capacity),
853 sorted: Vec::with_capacity(capacity),
854 }
855 }
856
857 pub fn intern(&mut self, npub: &str) -> u16 {
862 let result = self.sorted.binary_search_by(|&idx| {
864 self.npubs[idx as usize].as_str().cmp(npub)
865 });
866
867 match result {
868 Ok(pos) => self.sorted[pos], Err(insert_pos) => {
870 if self.npubs.len() >= NO_NPUB as usize {
873 return NO_NPUB;
874 }
875 let new_idx = self.npubs.len() as u16;
877 self.npubs.push(npub.to_string());
878 self.sorted.insert(insert_pos, new_idx);
879 new_idx
880 }
881 }
882 }
883
884 #[inline]
886 pub fn intern_opt(&mut self, npub: Option<&str>) -> u16 {
887 match npub {
888 Some(s) if !s.is_empty() => self.intern(s),
889 _ => NO_NPUB,
890 }
891 }
892
893 pub fn lookup(&self, npub: &str) -> Option<u16> {
895 self.sorted.binary_search_by(|&idx| {
896 self.npubs[idx as usize].as_str().cmp(npub)
897 }).ok().map(|pos| self.sorted[pos])
898 }
899
900 #[inline]
904 pub fn resolve(&self, idx: u16) -> Option<&str> {
905 if idx == NO_NPUB {
906 return None;
907 }
908 self.npubs.get(idx as usize).map(|s| s.as_str())
909 }
910
911 #[inline]
913 pub fn len(&self) -> usize {
914 self.npubs.len()
915 }
916
917 #[inline]
918 pub fn is_empty(&self) -> bool {
919 self.npubs.is_empty()
920 }
921
922 pub fn memory_usage(&self) -> usize {
924 std::mem::size_of::<Self>()
925 + self.npubs.capacity() * std::mem::size_of::<String>()
926 + self.npubs.iter().map(|s| s.capacity()).sum::<usize>()
927 + self.sorted.capacity() * std::mem::size_of::<u16>()
928 }
929}
930
931#[derive(Clone, Debug)]
947pub struct CompactMessage {
948 pub id: [u8; 32],
950 pub at: u64,
952 pub expiration_secs: u32,
956 pub flags: MessageFlags,
958 pub npub_idx: u16,
960 pub replied_to: Option<Box<[u8; 32]>>,
962 pub replied_to_npub_idx: u16,
964 pub wrapper_id: Option<Box<[u8; 32]>>,
966
967 pub content: Box<str>,
970 pub replied_to_content: Option<Box<str>>,
972 pub replied_to_emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
974 pub attachments: TinyVec<CompactAttachment>,
976 pub reactions: TinyVec<CompactReaction>,
978 #[allow(clippy::box_collection)]
980 pub edit_history: Option<Box<Vec<EditEntry>>>,
981 pub preview_metadata: Option<Box<SiteMetadata>>,
983 #[allow(clippy::box_collection)]
986 pub emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
987 #[allow(clippy::box_collection)]
990 pub addressed_bots: Option<Box<Vec<u16>>>,
991}
992
993impl CompactMessage {
994 #[inline]
996 pub fn has_reply(&self) -> bool {
997 self.replied_to.is_some()
998 }
999
1000 #[inline]
1002 pub fn is_edited(&self) -> bool {
1003 self.edit_history.is_some()
1004 }
1005
1006 #[inline]
1008 pub fn id_hex(&self) -> String {
1009 decode_message_id(&self.id)
1010 }
1011
1012 #[inline]
1014 pub fn replied_to_hex(&self) -> String {
1015 match &self.replied_to {
1016 Some(id) => bytes_to_hex_32(id),
1017 None => String::new(),
1018 }
1019 }
1020
1021 #[inline]
1023 pub fn wrapper_id_hex(&self) -> Option<String> {
1024 self.wrapper_id.as_ref().map(|id| bytes_to_hex_32(id))
1025 }
1026
1027 #[inline]
1029 pub fn timestamp_ms(&self) -> u64 {
1030 timestamp_from_compact(self.at)
1031 }
1032
1033 pub fn apply_edit(&mut self, new_content: String, edited_at: u64, emoji_tags: Vec<crate::types::EmojiTag>) {
1039 if self.edit_history.is_none() {
1041 self.edit_history = Some(Box::new(vec![EditEntry {
1042 content: self.content.to_string(),
1043 edited_at: self.timestamp_ms(), }]));
1045 }
1046
1047 let mut is_latest = true;
1048 if let Some(ref mut history) = self.edit_history {
1049 if history.iter().any(|e| e.edited_at == edited_at) {
1051 return;
1052 }
1053
1054 history.push(EditEntry {
1056 content: new_content.clone(),
1057 edited_at,
1058 });
1059
1060 history.sort_by_key(|e| e.edited_at);
1062 is_latest = history.last().map(|e| e.edited_at == edited_at).unwrap_or(true);
1063 }
1064
1065 if is_latest {
1067 self.content = new_content.into_boxed_str();
1068 self.emoji_tags = if emoji_tags.is_empty() { None } else { Some(Box::new(emoji_tags)) };
1069 }
1070 }
1071
1072 #[inline]
1074 pub fn replied_to_has_attachment(&self) -> Option<bool> {
1075 self.flags.replied_to_has_attachment()
1076 }
1077
1078 pub fn add_reaction(&mut self, reaction: Reaction, interner: &mut NpubInterner) -> bool {
1082 let reaction_id = hex_to_bytes_32(&reaction.id);
1084
1085 if self.reactions.iter().any(|r| r.id == reaction_id) {
1087 return false;
1088 }
1089
1090 let author_idx = interner.intern(&reaction.author_id);
1094 if self.reactions.iter().any(|r| r.author_idx == author_idx && *r.emoji == *reaction.emoji) {
1095 return false;
1096 }
1097
1098 if !self.reactions.iter().any(|r| *r.emoji == *reaction.emoji) {
1102 let groups: std::collections::HashSet<&str> =
1103 self.reactions.iter().map(|r| &*r.emoji).collect();
1104 if groups.len() >= MAX_REACTION_GROUPS {
1105 return false;
1106 }
1107 }
1108
1109 let compact = CompactReaction::from_reaction_owned(reaction, interner);
1111 let mut reactions = self.reactions.to_vec();
1112 reactions.push(compact);
1113 self.reactions = TinyVec::from_vec(reactions);
1114 true
1115 }
1116
1117 pub fn remove_reaction(&mut self, reaction_id: &str) -> bool {
1119 let target = hex_to_bytes_32(reaction_id);
1120 if !self.reactions.iter().any(|r| r.id == target) {
1121 return false;
1122 }
1123 let mut reactions = self.reactions.to_vec();
1124 reactions.retain(|r| r.id != target);
1125 self.reactions = TinyVec::from_vec(reactions);
1126 true
1127 }
1128
1129 #[inline]
1131 pub fn is_mine(&self) -> bool { self.flags.is_mine() }
1132 #[inline]
1133 pub fn is_pending(&self) -> bool { self.flags.is_pending() }
1134 #[inline]
1135 pub fn is_failed(&self) -> bool { self.flags.is_failed() }
1136
1137 #[inline]
1139 pub fn set_pending(&mut self, value: bool) { self.flags.set_pending(value); }
1140 #[inline]
1141 pub fn set_failed(&mut self, value: bool) { self.flags.set_failed(value); }
1142 #[inline]
1143 pub fn set_mine(&mut self, value: bool) { self.flags.set_mine(value); }
1144}
1145
1146#[derive(Clone, Debug, Default)]
1155pub struct CompactMessageVec {
1156 messages: Vec<CompactMessage>,
1158 id_index: Vec<([u8; 32], u32)>,
1160}
1161
1162impl CompactMessageVec {
1163 pub fn new() -> Self {
1164 Self {
1165 messages: Vec::new(),
1166 id_index: Vec::new(),
1167 }
1168 }
1169
1170 pub fn with_capacity(capacity: usize) -> Self {
1171 Self {
1172 messages: Vec::with_capacity(capacity),
1173 id_index: Vec::with_capacity(capacity),
1174 }
1175 }
1176
1177 #[inline]
1179 pub fn len(&self) -> usize {
1180 self.messages.len()
1181 }
1182
1183 #[inline]
1184 pub fn is_empty(&self) -> bool {
1185 self.messages.is_empty()
1186 }
1187
1188 #[inline]
1190 pub fn messages(&self) -> &[CompactMessage] {
1191 &self.messages
1192 }
1193
1194 #[inline]
1196 pub fn messages_mut(&mut self) -> &mut Vec<CompactMessage> {
1197 &mut self.messages
1198 }
1199
1200 #[inline]
1202 pub fn iter(&self) -> std::slice::Iter<'_, CompactMessage> {
1203 self.messages.iter()
1204 }
1205
1206 #[inline]
1208 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, CompactMessage> {
1209 self.messages.iter_mut()
1210 }
1211
1212 #[inline]
1214 pub fn last(&self) -> Option<&CompactMessage> {
1215 self.messages.last()
1216 }
1217
1218 #[inline]
1220 pub fn last_timestamp(&self) -> Option<u64> {
1221 self.messages.last().map(|m| timestamp_from_compact(m.at))
1222 }
1223
1224 #[inline]
1226 pub fn first(&self) -> Option<&CompactMessage> {
1227 self.messages.first()
1228 }
1229
1230 pub fn find_by_id(&self, id: &[u8; 32]) -> Option<&CompactMessage> {
1232 let pos = self.id_index
1233 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1234 .ok()?;
1235 let msg_pos = self.id_index[pos].1 as usize;
1236 self.messages.get(msg_pos)
1237 }
1238
1239 pub fn find_by_id_mut(&mut self, id: &[u8; 32]) -> Option<&mut CompactMessage> {
1241 let pos = self.id_index
1242 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1243 .ok()?;
1244 let msg_pos = self.id_index[pos].1 as usize;
1245 self.messages.get_mut(msg_pos)
1246 }
1247
1248 pub fn find_by_hex_id(&self, id_str: &str) -> Option<&CompactMessage> {
1250 if id_str.is_empty() {
1251 return None;
1252 }
1253 let id = encode_message_id(id_str);
1254 self.find_by_id(&id)
1255 }
1256
1257 pub fn find_by_hex_id_mut(&mut self, id_str: &str) -> Option<&mut CompactMessage> {
1259 if id_str.is_empty() {
1260 return None;
1261 }
1262 let id = encode_message_id(id_str);
1263 self.find_by_id_mut(&id)
1264 }
1265
1266 pub fn contains_id(&self, id: &[u8; 32]) -> bool {
1268 self.id_index
1269 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1270 .is_ok()
1271 }
1272
1273 pub fn remove_by_hex_id(&mut self, id_str: &str) -> bool {
1275 if id_str.is_empty() {
1276 return false;
1277 }
1278 let id = encode_message_id(id_str);
1279 let idx_pos = match self.id_index.binary_search_by(|(idx_id, _)| idx_id.cmp(&id)) {
1281 Ok(pos) => pos,
1282 Err(_) => return false,
1283 };
1284 let msg_pos = self.id_index[idx_pos].1 as usize;
1285 self.messages.remove(msg_pos);
1287 self.rebuild_index();
1289 true
1290 }
1291
1292 pub fn contains_hex_id(&self, id_str: &str) -> bool {
1294 if id_str.is_empty() {
1295 return false;
1296 }
1297 let id = encode_message_id(id_str);
1298 self.contains_id(&id)
1299 }
1300
1301 pub fn insert(&mut self, msg: CompactMessage) -> bool {
1307 if self.contains_id(&msg.id) {
1309 return false;
1310 }
1311
1312 let msg_id = msg.id;
1313
1314 if self.messages.last().is_none_or(|last| msg.at >= last.at) {
1317 let msg_pos = self.messages.len() as u32;
1318 self.messages.push(msg);
1319
1320 let idx_pos = self.id_index
1323 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1324 .unwrap_err();
1325 self.id_index.insert(idx_pos, (msg_id, msg_pos));
1326
1327 return true;
1328 }
1329
1330 let msg_pos = match self.messages.binary_search_by(|m| m.at.cmp(&msg.at)) {
1333 Ok(pos) => pos,
1334 Err(pos) => pos,
1335 };
1336
1337 for (_, pos) in &mut self.id_index {
1339 if *pos >= msg_pos as u32 {
1340 *pos += 1;
1341 }
1342 }
1343
1344 self.messages.insert(msg_pos, msg);
1346
1347 let idx_pos = self.id_index
1349 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1350 .unwrap_err();
1351 self.id_index.insert(idx_pos, (msg_id, msg_pos as u32));
1352
1353 true
1354 }
1355
1356 pub fn rebuild_index(&mut self) {
1358 self.id_index.clear();
1359 self.id_index.reserve(self.messages.len());
1360 for (pos, msg) in self.messages.iter().enumerate() {
1361 self.id_index.push((msg.id, pos as u32));
1362 }
1363 self.id_index.sort_by(|(a, _), (b, _)| a.cmp(b));
1364 }
1365
1366 pub fn insert_batch(&mut self, messages: impl IntoIterator<Item = CompactMessage>) -> usize {
1375 let messages: Vec<_> = messages.into_iter().collect();
1376 if messages.is_empty() {
1377 return 0;
1378 }
1379
1380 let mut to_add: Vec<CompactMessage> = Vec::with_capacity(messages.len());
1382 for msg in messages {
1383 if !self.contains_id(&msg.id) {
1384 to_add.push(msg);
1385 }
1386 }
1387
1388 if to_add.is_empty() {
1389 return 0;
1390 }
1391
1392 let added = to_add.len();
1393
1394 let our_first = self.messages.first().map(|m| m.at);
1396 let our_last = self.messages.last().map(|m| m.at);
1397 let their_min = to_add.iter().map(|m| m.at).min().unwrap();
1398 let their_max = to_add.iter().map(|m| m.at).max().unwrap();
1399
1400 if self.messages.is_empty() {
1401 self.messages = to_add;
1403 self.messages.sort_by_key(|m| m.at);
1404 self.rebuild_index();
1405 } else if their_min >= our_last.unwrap() {
1406 to_add.sort_by_key(|m| m.at);
1408 let base_pos = self.messages.len() as u32;
1409 let mut new_index_entries: Vec<_> = to_add.iter()
1412 .enumerate()
1413 .map(|(i, msg)| (msg.id, base_pos + i as u32))
1414 .collect();
1415 self.messages.extend(to_add);
1416 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1417
1418 let old_index = std::mem::take(&mut self.id_index);
1421 self.id_index.reserve(old_index.len() + new_index_entries.len());
1422 let mut old_iter = old_index.into_iter().peekable();
1423 let mut new_iter = new_index_entries.into_iter().peekable();
1424 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1425 match (old_iter.peek(), new_iter.peek()) {
1426 (Some((old_id, _)), Some((new_id, _))) => {
1427 if old_id < new_id {
1428 self.id_index.push(old_iter.next().unwrap());
1429 } else {
1430 self.id_index.push(new_iter.next().unwrap());
1431 }
1432 }
1433 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1434 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1435 (None, None) => break,
1436 }
1437 }
1438 } else if their_max <= our_first.unwrap() {
1439 to_add.sort_by_key(|m| m.at);
1441 let prepend_count = to_add.len();
1442
1443 for (_, pos) in &mut self.id_index {
1445 *pos += prepend_count as u32;
1446 }
1447
1448 let mut new_index_entries: Vec<_> = to_add.iter()
1450 .enumerate()
1451 .map(|(i, msg)| (msg.id, i as u32))
1452 .collect();
1453 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1454
1455 let old_index = std::mem::take(&mut self.id_index);
1457 self.id_index.reserve(old_index.len() + new_index_entries.len());
1458
1459 let mut old_iter = old_index.into_iter().peekable();
1460 let mut new_iter = new_index_entries.into_iter().peekable();
1461
1462 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1463 match (old_iter.peek(), new_iter.peek()) {
1464 (Some((old_id, _)), Some((new_id, _))) => {
1465 if old_id < new_id {
1466 self.id_index.push(old_iter.next().unwrap());
1467 } else {
1468 self.id_index.push(new_iter.next().unwrap());
1469 }
1470 }
1471 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1472 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1473 (None, None) => break,
1474 }
1475 }
1476
1477 let mut new_messages = to_add;
1479 new_messages.append(&mut self.messages);
1480 self.messages = new_messages;
1481 } else {
1482 self.messages.extend(to_add);
1484 self.messages.sort_by_key(|m| m.at);
1485 self.rebuild_index();
1486 }
1487
1488 added
1489 }
1490
1491 pub fn memory_usage(&self) -> usize {
1493 std::mem::size_of::<Self>()
1494 + self.messages.capacity() * std::mem::size_of::<CompactMessage>()
1495 + self.id_index.capacity() * std::mem::size_of::<([u8; 32], u32)>()
1496 }
1498
1499 pub fn drain(&mut self, range: std::ops::Range<usize>) -> std::vec::Drain<'_, CompactMessage> {
1501 let drain = self.messages.drain(range);
1502 drain
1504 }
1505
1506 pub fn sort_by_key<K, F>(&mut self, f: F)
1508 where
1509 F: FnMut(&CompactMessage) -> K,
1510 K: Ord,
1511 {
1512 self.messages.sort_by_key(f);
1513 self.rebuild_index();
1514 }
1515
1516 pub fn clear(&mut self) {
1518 self.messages.clear();
1519 self.id_index.clear();
1520 }
1521}
1522
1523use crate::types::Message;
1528
1529impl CompactMessage {
1530 pub fn from_message(msg: &Message, interner: &mut NpubInterner) -> Self {
1532 Self {
1533 id: encode_message_id(&msg.id),
1534 at: timestamp_to_compact(msg.at),
1535 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1536 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1537 replied_to: if msg.replied_to.is_empty() {
1539 None
1540 } else {
1541 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1542 },
1543 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1544 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1546 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1547 content: msg.content.clone().into_boxed_str(),
1549 replied_to_content: msg.replied_to_content.as_ref().map(|s| s.clone().into_boxed_str()),
1550 replied_to_emoji_tags: msg.replied_to_emoji_tags.as_ref()
1551 .filter(|t| !t.is_empty())
1552 .map(|t| Box::new(t.clone())),
1553 attachments: TinyVec::from_vec(
1555 msg.attachments.iter()
1556 .map(CompactAttachment::from_attachment)
1557 .collect()
1558 ),
1559 reactions: TinyVec::from_vec(
1561 msg.reactions.iter()
1562 .map(|r| CompactReaction::from_reaction(r, interner))
1563 .collect()
1564 ),
1565 edit_history: msg.edit_history.clone().map(Box::new),
1567 preview_metadata: msg.preview_metadata.clone().map(Box::new),
1568 emoji_tags: if msg.emoji_tags.is_empty() {
1569 None
1570 } else {
1571 Some(Box::new(msg.emoji_tags.clone()))
1572 },
1573 addressed_bots: if msg.addressed_bots.is_empty() {
1574 None
1575 } else {
1576 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1577 },
1578 }
1579 }
1580
1581 pub fn from_message_owned(msg: Message, interner: &mut NpubInterner) -> Self {
1586 Self {
1587 id: encode_message_id(&msg.id),
1588 at: timestamp_to_compact(msg.at),
1589 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1590 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1591 replied_to: if msg.replied_to.is_empty() {
1593 None
1594 } else {
1595 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1596 },
1597 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1598 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1600 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1601 content: msg.content.into_boxed_str(),
1603 replied_to_content: msg.replied_to_content.map(|s| s.into_boxed_str()),
1604 replied_to_emoji_tags: msg.replied_to_emoji_tags
1605 .filter(|t| !t.is_empty())
1606 .map(Box::new),
1607 attachments: TinyVec::from_vec(
1609 msg.attachments.into_iter()
1610 .map(CompactAttachment::from_attachment_owned)
1611 .collect()
1612 ),
1613 reactions: TinyVec::from_vec(
1615 msg.reactions.into_iter()
1616 .map(|r| CompactReaction::from_reaction_owned(r, interner))
1617 .collect()
1618 ),
1619 edit_history: msg.edit_history.map(Box::new),
1621 preview_metadata: msg.preview_metadata.map(Box::new),
1622 emoji_tags: if msg.emoji_tags.is_empty() {
1623 None
1624 } else {
1625 Some(Box::new(msg.emoji_tags))
1626 },
1627 addressed_bots: if msg.addressed_bots.is_empty() {
1628 None
1629 } else {
1630 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1631 },
1632 }
1633 }
1634
1635 pub fn to_message(&self, interner: &NpubInterner) -> Message {
1637 Message {
1638 id: self.id_hex(),
1639 at: self.timestamp_ms(), expiration: if self.expiration_secs == 0 { None } else { Some(self.expiration_secs as u64) },
1641 mine: self.flags.is_mine(),
1642 pending: self.flags.is_pending(),
1643 failed: self.flags.is_failed(),
1644 edited: self.is_edited(),
1645 npub: interner.resolve(self.npub_idx).map(|s| s.to_string()),
1646 replied_to: self.replied_to_hex(),
1647 replied_to_content: self.replied_to_content.as_ref().map(|s| s.to_string()),
1648 replied_to_emoji_tags: self.replied_to_emoji_tags.as_ref().map(|t| (**t).clone()),
1649 replied_to_npub: interner.resolve(self.replied_to_npub_idx).map(|s| s.to_string()),
1650 replied_to_has_attachment: self.flags.replied_to_has_attachment(),
1651 replied_to_attachment_extension: None,
1654 wrapper_event_id: self.wrapper_id_hex(),
1655 content: self.content.to_string(),
1656 attachments: self.attachments.iter()
1658 .map(|a| a.to_attachment())
1659 .collect(),
1660 reactions: self.reactions.iter()
1662 .map(|r| r.to_reaction(&self.id, interner))
1663 .collect(),
1664 edit_history: self.edit_history.as_ref().map(|b| (**b).clone()),
1666 preview_metadata: self.preview_metadata.as_ref().map(|b| (**b).clone()),
1667 emoji_tags: self.emoji_tags.as_ref().map(|b| (**b).clone()).unwrap_or_default(),
1668 addressed_bots: self
1669 .addressed_bots
1670 .as_ref()
1671 .map(|b| b.iter().filter_map(|&i| interner.resolve(i).map(|s| s.to_string())).collect())
1672 .unwrap_or_default(),
1673 }
1674 }
1675}
1676
1677#[cfg(test)]
1678mod tests {
1679 use super::*;
1680
1681 #[test]
1685 fn real_id_starting_with_marker_byte_roundtrips() {
1686 let id = "01b95c05179c6d6abbc60b9a35198fdee6e0ce5b80a0b7ac8d465d81d038a73d";
1687 let encoded = encode_message_id(id);
1688 assert_eq!(decode_message_id(&encoded), id);
1689 }
1690
1691 #[test]
1692 fn pending_id_roundtrips() {
1693 let id = "pending-306878031314";
1694 let encoded = encode_message_id(id);
1695 assert_eq!(encoded[0], PENDING_ID_MARKER);
1696 assert_eq!(decode_message_id(&encoded), id);
1697 }
1698
1699 #[test]
1700 fn marker_byte_without_sentinel_decodes_as_hex() {
1701 let mut bytes = [0xABu8; 32];
1703 bytes[0] = PENDING_ID_MARKER;
1704 let decoded = decode_message_id(&bytes);
1705 assert!(!decoded.starts_with("pending-"));
1706 assert_eq!(decoded.len(), 64);
1707 }
1708
1709 #[test]
1710 fn test_message_flags() {
1711 let mut flags = MessageFlags::NONE;
1712 assert!(!flags.is_mine());
1713 assert!(!flags.is_pending());
1714 assert!(!flags.is_failed());
1715
1716 flags.set_mine(true);
1717 assert!(flags.is_mine());
1718
1719 flags.set_pending(true);
1720 assert!(flags.is_pending());
1721 assert!(flags.is_mine()); flags.set_mine(false);
1724 assert!(!flags.is_mine());
1725 assert!(flags.is_pending()); }
1727
1728 #[test]
1729 fn test_npub_interner() {
1730 let mut interner = NpubInterner::new();
1731
1732 let idx1 = interner.intern("npub1alice");
1733 let idx2 = interner.intern("npub1bob");
1734 let idx3 = interner.intern("npub1alice"); assert_eq!(idx1, idx3); assert_ne!(idx1, idx2);
1738
1739 assert_eq!(interner.resolve(idx1), Some("npub1alice"));
1740 assert_eq!(interner.resolve(idx2), Some("npub1bob"));
1741 assert_eq!(interner.resolve(NO_NPUB), None);
1742 }
1743
1744 #[test]
1745 fn test_compact_message_vec_insert_and_find() {
1746 let mut vec = CompactMessageVec::new();
1747 let mut interner = NpubInterner::new();
1748
1749 let msg1 = CompactMessage {
1750 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000001"),
1751 at: 1000,
1752 expiration_secs: 0,
1753 flags: MessageFlags::NONE,
1754 npub_idx: interner.intern("npub1test"),
1755 replied_to: None,
1756 replied_to_npub_idx: NO_NPUB,
1757 wrapper_id: None,
1758 content: "First message".to_string().into_boxed_str(),
1759 replied_to_content: None,
1760 replied_to_emoji_tags: None,
1761 attachments: TinyVec::new(),
1762 reactions: TinyVec::new(),
1763 edit_history: None,
1764 preview_metadata: None, emoji_tags: None,
1766 addressed_bots: None,
1767 };
1768
1769 let msg2 = CompactMessage {
1770 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000002"),
1771 at: 2000,
1772 expiration_secs: 0,
1773 flags: MessageFlags::MINE,
1774 npub_idx: interner.intern("npub1me"),
1775 replied_to: None,
1776 replied_to_npub_idx: NO_NPUB,
1777 wrapper_id: None,
1778 content: "Second message".to_string().into_boxed_str(),
1779 replied_to_content: None,
1780 replied_to_emoji_tags: None,
1781 attachments: TinyVec::new(),
1782 reactions: TinyVec::new(),
1783 edit_history: None,
1784 preview_metadata: None, emoji_tags: None,
1786 addressed_bots: None,
1787 };
1788
1789 assert!(vec.insert(msg1));
1790 assert!(vec.insert(msg2));
1791 assert_eq!(vec.len(), 2);
1792
1793 let found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000001");
1795 assert!(found.is_some());
1796 assert_eq!(&*found.unwrap().content, "First message");
1797
1798 let not_found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000099");
1800 assert!(not_found.is_none());
1801 }
1802
1803 #[test]
1804 fn test_duplicate_insert_rejected() {
1805 let mut vec = CompactMessageVec::new();
1806
1807 let msg = CompactMessage {
1808 id: hex_to_bytes_32("abcd000000000000000000000000000000000000000000000000000000000000"),
1809 at: 1000,
1810 expiration_secs: 0,
1811 flags: MessageFlags::NONE,
1812 npub_idx: NO_NPUB,
1813 replied_to: None,
1814 replied_to_npub_idx: NO_NPUB,
1815 wrapper_id: None,
1816 content: "Test".to_string().into_boxed_str(),
1817 replied_to_content: None,
1818 replied_to_emoji_tags: None,
1819 attachments: TinyVec::new(),
1820 reactions: TinyVec::new(),
1821 edit_history: None,
1822 preview_metadata: None, emoji_tags: None,
1824 addressed_bots: None,
1825 };
1826
1827 assert!(vec.insert(msg.clone()));
1828 assert!(!vec.insert(msg)); assert_eq!(vec.len(), 1);
1830 }
1831
1832 #[test]
1834 fn benchmark_compact_vs_message() {
1835 use std::time::Instant;
1836
1837 const NUM_MESSAGES: usize = 10_000;
1838 const NUM_UNIQUE_USERS: usize = 50; println!("\n========================================");
1841 println!(" COMPACT MESSAGE BENCHMARK");
1842 println!(" {} messages, {} unique users", NUM_MESSAGES, NUM_UNIQUE_USERS);
1843 println!("========================================\n");
1844
1845 let users: Vec<String> = (0..NUM_UNIQUE_USERS)
1847 .map(|i| format!("npub1{:0>62}", i))
1848 .collect();
1849
1850 let messages: Vec<Message> = (0..NUM_MESSAGES)
1852 .map(|i| {
1853 let user_idx = i % NUM_UNIQUE_USERS;
1854 Message {
1855 expiration: None,
1856 id: format!("{:0>64x}", i),
1857 at: 1700000000000 + (i as u64 * 1000),
1858 mine: user_idx == 0,
1859 pending: false,
1860 failed: false,
1861 edited: false,
1862 npub: Some(users[user_idx].clone()),
1863 replied_to: if i > 0 && i % 5 == 0 {
1864 format!("{:0>64x}", i - 1)
1865 } else {
1866 String::new()
1867 },
1868 replied_to_content: if i > 0 && i % 5 == 0 {
1869 Some("Previous message content".to_string())
1870 } else {
1871 None
1872 },
1873 replied_to_emoji_tags: None,
1874 replied_to_npub: if i > 0 && i % 5 == 0 {
1875 Some(users[(i - 1) % NUM_UNIQUE_USERS].clone())
1876 } else {
1877 None
1878 },
1879 replied_to_has_attachment: None,
1880 replied_to_attachment_extension: None,
1881 wrapper_event_id: Some(format!("{:0>64x}", i + 1000000)),
1882 content: format!("This is message number {} with some typical content length.", i),
1883 attachments: vec![],
1884 reactions: vec![],
1885 edit_history: None,
1886 preview_metadata: None,
1887 emoji_tags: Vec::new(),
1888 addressed_bots: Vec::new(),
1889 }
1890 })
1891 .collect();
1892
1893 println!("--- STRUCT SIZES ---");
1895 println!(" Message struct: {} bytes", std::mem::size_of::<Message>());
1896 println!(" CompactMessage struct: {} bytes", std::mem::size_of::<CompactMessage>());
1897 println!(" Savings per struct: {} bytes ({:.1}%)",
1898 std::mem::size_of::<Message>().saturating_sub(std::mem::size_of::<CompactMessage>()),
1899 (1.0 - std::mem::size_of::<CompactMessage>() as f64 / std::mem::size_of::<Message>() as f64) * 100.0
1900 );
1901 println!();
1902
1903 let msg_heap_estimate: usize = messages.iter().map(|m| {
1905 m.id.capacity()
1906 + m.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1907 + m.replied_to.capacity()
1908 + m.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
1909 + m.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1910 + m.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
1911 + m.content.capacity()
1912 }).sum();
1913 let msg_total = messages.len() * std::mem::size_of::<Message>() + msg_heap_estimate;
1914
1915 println!("--- INSERT BENCHMARK ---");
1917
1918 let mut interner = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1920 let mut compact_vec = CompactMessageVec::with_capacity(NUM_MESSAGES);
1921
1922 let insert_start = Instant::now();
1923 for msg in &messages {
1924 let compact = CompactMessage::from_message(msg, &mut interner);
1925 compact_vec.insert(compact);
1926 }
1927 let insert_elapsed = insert_start.elapsed();
1928
1929 println!(" Sequential insert (optimized append path):");
1930 println!(" {} messages in {:?}", NUM_MESSAGES, insert_elapsed);
1931 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / insert_elapsed.as_secs_f64());
1932 println!(" Per message: {:.3} us ({} ns)",
1933 insert_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1934 insert_elapsed.as_nanos() / NUM_MESSAGES as u128);
1935 println!();
1936
1937 let mut interner2 = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1939 let mut compact_vec2 = CompactMessageVec::with_capacity(NUM_MESSAGES);
1940
1941 let batch_start = Instant::now();
1942 let compact_messages: Vec<_> = messages.iter()
1943 .map(|msg| CompactMessage::from_message(msg, &mut interner2))
1944 .collect();
1945 let batch_added = compact_vec2.insert_batch(compact_messages);
1946 let batch_elapsed = batch_start.elapsed();
1947
1948 println!(" Batch insert (pagination/history load):");
1949 println!(" {} messages in {:?}", batch_added, batch_elapsed);
1950 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / batch_elapsed.as_secs_f64());
1951 println!(" Per message: {:.3} us ({} ns)",
1952 batch_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1953 batch_elapsed.as_nanos() / NUM_MESSAGES as u128);
1954 println!();
1955
1956 println!("--- MEMORY COMPARISON ---");
1958 let compact_heap_estimate: usize = compact_vec.iter().map(|m| {
1959 m.content.len() + m.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
1961 + m.attachments.len() * std::mem::size_of::<Attachment>() + if m.attachments.is_empty() { 0 } else { 1 }
1962 + m.reactions.len() * std::mem::size_of::<Reaction>() + if m.reactions.is_empty() { 0 } else { 1 }
1963 }).sum();
1964 let compact_struct_mem = compact_vec.len() * std::mem::size_of::<CompactMessage>();
1965 let compact_index_mem = compact_vec.len() * std::mem::size_of::<([u8; 32], u32)>();
1966 let interner_mem = interner.memory_usage();
1967 let compact_total = compact_struct_mem + compact_heap_estimate + compact_index_mem + interner_mem;
1968
1969 println!(" Regular Message storage:");
1970 println!(" Struct memory: {:>10} bytes", messages.len() * std::mem::size_of::<Message>());
1971 println!(" Heap (strings): {:>10} bytes", msg_heap_estimate);
1972 println!(" TOTAL: {:>10} bytes ({:.2} MB)", msg_total, msg_total as f64 / 1_000_000.0);
1973 println!();
1974 println!(" CompactMessage storage:");
1975 println!(" Struct memory: {:>10} bytes", compact_struct_mem);
1976 println!(" Heap (strings): {:>10} bytes", compact_heap_estimate);
1977 println!(" ID index: {:>10} bytes", compact_index_mem);
1978 println!(" Interner: {:>10} bytes ({} unique npubs)", interner_mem, interner.len());
1979 println!(" TOTAL: {:>10} bytes ({:.2} MB)", compact_total, compact_total as f64 / 1_000_000.0);
1980 println!();
1981 println!(" SAVINGS: {} bytes ({:.1}%)",
1982 msg_total.saturating_sub(compact_total),
1983 (1.0 - compact_total as f64 / msg_total as f64) * 100.0
1984 );
1985 println!(" Per message: {} -> {} bytes (avg)",
1986 msg_total / NUM_MESSAGES,
1987 compact_total / NUM_MESSAGES
1988 );
1989 println!();
1990
1991 println!("--- LOOKUP BENCHMARK ---");
1993
1994 let lookup_ids: Vec<String> = (0..1000)
1996 .map(|i| format!("{:0>64x}", i * 10)) .collect();
1998
1999 let lookup_start = Instant::now();
2001 let mut found_count = 0;
2002 for _ in 0..100 { for id in &lookup_ids {
2004 if compact_vec.find_by_hex_id(id).is_some() {
2005 found_count += 1;
2006 }
2007 }
2008 }
2009 let lookup_elapsed = lookup_start.elapsed();
2010 let total_lookups = 100 * lookup_ids.len();
2011
2012 println!(" Binary search (CompactMessageVec):");
2013 println!(" {} lookups in {:?}", total_lookups, lookup_elapsed);
2014 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / lookup_elapsed.as_secs_f64());
2015 println!(" Per lookup: {:.2} us", lookup_elapsed.as_micros() as f64 / total_lookups as f64);
2016 println!(" Found: {} / {}", found_count, total_lookups);
2017 println!();
2018
2019 let linear_start = Instant::now();
2021 let mut linear_found = 0;
2022 for _ in 0..100 {
2023 for id in &lookup_ids {
2024 if messages.iter().find(|m| &m.id == id).is_some() {
2025 linear_found += 1;
2026 }
2027 }
2028 }
2029 let linear_elapsed = linear_start.elapsed();
2030
2031 println!(" Linear search (Vec<Message>):");
2032 println!(" {} lookups in {:?}", total_lookups, linear_elapsed);
2033 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / linear_elapsed.as_secs_f64());
2034 println!(" Per lookup: {:.2} us", linear_elapsed.as_micros() as f64 / total_lookups as f64);
2035 println!();
2036
2037 let speedup = linear_elapsed.as_nanos() as f64 / lookup_elapsed.as_nanos() as f64;
2038 println!(" SPEEDUP: {:.1}x faster with binary search!", speedup);
2039 println!();
2040
2041 println!("--- INTERNER EFFICIENCY ---");
2043 let npub_string_size = 63 + 1; let naive_npub_mem = NUM_MESSAGES * npub_string_size * 2; let actual_npub_mem = interner_mem;
2046 println!(" Naive (every msg stores npubs): {} bytes", naive_npub_mem);
2047 println!(" Interned ({} unique): {} bytes", interner.len(), actual_npub_mem);
2048 println!(" SAVINGS: {} bytes ({:.1}%)",
2049 naive_npub_mem.saturating_sub(actual_npub_mem),
2050 (1.0 - actual_npub_mem as f64 / naive_npub_mem as f64) * 100.0
2051 );
2052 println!();
2053
2054 println!("========================================");
2055 println!(" BENCHMARK COMPLETE");
2056 println!("========================================\n");
2057
2058 assert_eq!(compact_vec.len(), NUM_MESSAGES);
2060 assert_eq!(interner.len(), NUM_UNIQUE_USERS);
2061 assert_eq!(found_count, linear_found);
2062 }
2063
2064 #[test]
2071 fn benchmark_profile_lookup() {
2072 use std::time::Instant;
2073 use std::hint::black_box;
2074
2075 const NUM_PROFILES: usize = 60;
2076 const NUM_LOOKUPS: usize = 100_000;
2077
2078 println!("\n========================================");
2079 println!(" PROFILE LOOKUP BENCHMARK");
2080 println!(" {} profiles, {} lookups each method", NUM_PROFILES, NUM_LOOKUPS);
2081 println!("========================================\n");
2082
2083 let npubs: Vec<String> = (0..NUM_PROFILES)
2085 .map(|i| format!("npub1{:0>58}", format!("{:x}", i * 7919 + 1000))) .collect();
2087
2088 let old_ids: Vec<String> = npubs.iter().rev().cloned().collect(); let mut sorted_ids: Vec<String> = npubs.clone();
2093 sorted_ids.sort();
2094
2095 let mut interner = NpubInterner::new();
2097 let mut profiles: Vec<crate::profile::Profile> = npubs.iter().map(|npub| {
2098 let mut p = crate::profile::Profile::new();
2099 p.id = interner.intern(npub);
2100 p
2101 }).collect();
2102 profiles.sort_by(|a, b| a.id.cmp(&b.id));
2103
2104 let lookup_targets: Vec<&str> = (0..NUM_LOOKUPS)
2106 .map(|i| npubs[i % NUM_PROFILES].as_str())
2107 .collect();
2108
2109 let handle_targets: Vec<u16> = lookup_targets.iter()
2111 .map(|&npub| interner.lookup(npub).unwrap())
2112 .collect();
2113
2114 let start = Instant::now();
2116 let mut found = 0u64;
2117 for &target in &lookup_targets {
2118 if old_ids.iter().any(|id| id == target) {
2119 found += 1;
2120 }
2121 }
2122 let linear_elapsed = start.elapsed();
2123 assert_eq!(found, NUM_LOOKUPS as u64);
2124
2125 let start = Instant::now();
2127 found = 0;
2128 for &target in &lookup_targets {
2129 if sorted_ids.binary_search_by(|id| id.as_str().cmp(target)).is_ok() {
2130 found += 1;
2131 }
2132 }
2133 let string_bs_elapsed = start.elapsed();
2134 assert_eq!(found, NUM_LOOKUPS as u64);
2135
2136 let start = Instant::now();
2138 found = 0;
2139 for &handle in &handle_targets {
2140 if profiles.binary_search_by(|p| p.id.cmp(black_box(&handle))).is_ok() {
2141 found += 1;
2142 }
2143 }
2144 let direct_elapsed = start.elapsed();
2145 assert_eq!(found, NUM_LOOKUPS as u64);
2146
2147 println!("--- LOOKUP METHODS ---");
2149 println!(" 1. Linear scan (old id: String):");
2150 println!(" {:?} total, {:.0} ns/lookup",
2151 linear_elapsed,
2152 linear_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2153 println!();
2154 println!(" 2. String binary search (intermediate):");
2155 println!(" {:?} total, {:.0} ns/lookup",
2156 string_bs_elapsed,
2157 string_bs_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2158 println!(" vs linear: {:.1}x faster",
2159 linear_elapsed.as_nanos() as f64 / string_bs_elapsed.as_nanos() as f64);
2160 println!();
2161 println!(" 3. Direct u16 handle lookup (current id: u16):");
2162 println!(" {:?} total, {:.0} ns/lookup",
2163 direct_elapsed,
2164 direct_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2165 println!(" vs linear: {:.1}x faster",
2166 linear_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2167 println!(" vs string BS: {:.1}x faster",
2168 string_bs_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2169 println!();
2170
2171 println!("--- MEMORY PER PROFILE ---");
2173 println!(" Old (id: String): ~87 bytes (24 String header + ~63 heap)");
2174 println!(" Current (id: u16): 2 bytes (inline)");
2175 println!(" Savings: ~85 bytes/profile, ~{} bytes for {} profiles",
2176 85 * NUM_PROFILES, NUM_PROFILES);
2177 println!(" Interner (shared): {} bytes (shared with message system)",
2178 interner.memory_usage());
2179 println!();
2180
2181 println!("========================================");
2182 println!(" BENCHMARK COMPLETE");
2183 println!("========================================\n");
2184
2185 for npub in &npubs {
2187 assert!(old_ids.iter().any(|id| id == npub));
2188 assert!(sorted_ids.binary_search_by(|id| id.as_str().cmp(npub.as_str())).is_ok());
2189 let h = interner.lookup(npub).unwrap();
2190 assert!(profiles.binary_search_by(|p| p.id.cmp(&h)).is_ok());
2191 }
2192 }
2193
2194 #[test]
2199 fn pending_id_roundtrip_regular_hex() {
2200 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2201 let encoded = encode_message_id(hex);
2202 let decoded = decode_message_id(&encoded);
2203 assert_eq!(decoded, hex, "regular hex ID should roundtrip exactly");
2204 }
2205
2206 #[test]
2207 fn pending_id_roundtrip_pending() {
2208 let id = "pending-1234567890123456789";
2209 let encoded = encode_message_id(id);
2210 let decoded = decode_message_id(&encoded);
2211 assert_eq!(decoded, id, "pending ID should roundtrip exactly");
2212 }
2213
2214 #[test]
2215 fn pending_id_zero_timestamp() {
2216 let id = "pending-0";
2217 let encoded = encode_message_id(id);
2218 assert_eq!(encoded[0], PENDING_ID_MARKER, "first byte should be marker");
2219 let decoded = decode_message_id(&encoded);
2220 assert_eq!(decoded, id, "pending-0 should roundtrip");
2221 }
2222
2223 #[test]
2224 fn pending_id_max_u128_timestamp() {
2225 let max = u128::MAX;
2226 let id = format!("pending-{}", max);
2227 let encoded = encode_message_id(&id);
2228 let decoded = decode_message_id(&encoded);
2229 assert_eq!(decoded, id, "pending with max u128 should roundtrip");
2230 }
2231
2232 #[test]
2233 fn pending_id_all_zero_hex() {
2234 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
2235 let encoded = encode_message_id(hex);
2236 let decoded = decode_message_id(&encoded);
2237 assert_eq!(decoded, hex, "all-zero hex ID should roundtrip");
2238 assert_ne!(encoded[0], PENDING_ID_MARKER);
2240 }
2241
2242 #[test]
2243 fn pending_id_all_ff_hex() {
2244 let hex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
2245 let encoded = encode_message_id(hex);
2246 let decoded = decode_message_id(&encoded);
2247 assert_eq!(decoded, hex, "all-ff hex ID should roundtrip");
2248 assert_eq!(encoded, [0xff; 32], "all-ff should decode to all 0xff bytes");
2249 }
2250
2251 #[test]
2252 fn pending_id_mixed_case_hex() {
2253 let lower = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2254 let mixed = "ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789";
2255 let encoded_lower = encode_message_id(lower);
2256 let encoded_mixed = encode_message_id(mixed);
2257 assert_eq!(encoded_lower, encoded_mixed, "mixed case should produce same bytes as lowercase");
2258 }
2259
2260 #[test]
2261 fn pending_id_short_hex_partial_decode() {
2262 let short = "abcdef";
2264 let encoded = encode_message_id(short);
2265 assert_eq!(encoded[0..14], [0u8; 14], "leading bytes should be zero for short input");
2267 }
2268
2269 #[test]
2270 fn pending_id_marker_distinguishes_from_real_id() {
2271 let hex = "0100000000000000000000000000000000000000000000000000000000000000";
2276 let encoded = encode_message_id(hex);
2277 let decoded = decode_message_id(&encoded);
2278 assert_eq!(decoded, hex, "marker-leading real id must roundtrip exactly");
2279 }
2280
2281 #[test]
2282 fn pending_id_large_timestamp() {
2283 let id = "pending-99999999999999999";
2284 let encoded = encode_message_id(&id);
2285 let decoded = decode_message_id(&encoded);
2286 assert_eq!(decoded, id, "large but valid timestamp should roundtrip");
2287 }
2288
2289 #[test]
2290 fn pending_id_invalid_timestamp_becomes_zero() {
2291 let id = "pending-notanumber";
2293 let encoded = encode_message_id(id);
2294 assert_eq!(encoded[0], PENDING_ID_MARKER);
2295 assert_eq!(&encoded[1..17], &[0u8; 16]);
2297 let decoded = decode_message_id(&encoded);
2298 assert_eq!(decoded, "pending-0", "invalid timestamp parses as pending-0");
2299 }
2300
2301 #[test]
2306 fn timestamp_to_compact_and_back_roundtrip() {
2307 let ms: u64 = 1705320000000;
2309 let compact = timestamp_to_compact(ms);
2310 let restored = timestamp_from_compact(compact);
2311 assert_eq!(restored / 1000, ms / 1000, "roundtrip should preserve seconds");
2313 }
2314
2315 #[test]
2316 fn secs_to_compact_and_back_roundtrip() {
2317 let secs: u64 = 1705320000; let compact = secs_to_compact(secs);
2319 let restored = secs_from_compact(compact);
2320 assert_eq!(restored, secs, "secs should roundtrip exactly");
2321 }
2322
2323 #[test]
2324 fn timestamp_zero_preservation() {
2325 assert_eq!(secs_to_compact(0), 0, "zero secs should produce zero compact");
2327 assert_eq!(secs_from_compact(0), 0, "zero compact should produce zero secs");
2328 }
2329
2330 #[test]
2331 fn timestamp_epoch_boundary() {
2332 let epoch_secs: u64 = 1577836800;
2334 let compact = secs_to_compact(epoch_secs);
2335 assert_eq!(compact, 0, "epoch boundary should map to compact 0");
2336 let restored = secs_from_compact(compact);
2337 assert_eq!(restored, 0, "compact 0 returns sentinel 0");
2339 }
2340
2341 #[test]
2342 fn timestamp_epoch_boundary_ms() {
2343 let epoch_ms: u64 = 1577836800000;
2344 let compact = timestamp_to_compact(epoch_ms);
2345 assert_eq!(compact, epoch_ms, "full u64 — identity function");
2346 let restored = timestamp_from_compact(compact);
2347 assert_eq!(restored, epoch_ms, "roundtrip preserves value");
2348 }
2349
2350 #[test]
2351 fn timestamp_current_time_roundtrip() {
2352 let secs: u64 = 1774800000;
2354 let compact = secs_to_compact(secs);
2355 let restored = secs_from_compact(compact);
2356 assert_eq!(restored, secs, "current-era timestamp should roundtrip");
2357 assert!(compact > 0, "current time should be past epoch");
2358 }
2359
2360 #[test]
2361 fn timestamp_far_future_year_2100() {
2362 let secs: u64 = 4102444800;
2364 let compact = secs_to_compact(secs);
2365 let restored = secs_from_compact(compact);
2366 assert_eq!(restored, secs, "year 2100 should roundtrip");
2367 assert!(compact <= u32::MAX, "year 2100 should fit in u32");
2369 }
2370
2371 #[test]
2372 fn timestamp_pre_epoch_saturates_to_zero() {
2373 let secs: u64 = 1500000000; let compact = secs_to_compact(secs);
2376 assert_eq!(compact, 0, "pre-epoch timestamp should saturate to 0");
2378 }
2379
2380 #[test]
2381 fn timestamp_ms_sub_second_precision_preserved() {
2382 let ms: u64 = 1705320000999;
2383 let compact = timestamp_to_compact(ms);
2384 let restored = timestamp_from_compact(compact);
2385 assert_eq!(restored, ms, "sub-second ms must be preserved");
2386 }
2387
2388 #[test]
2389 fn timestamp_one_second_after_epoch() {
2390 let secs: u64 = 1577836801; let compact = secs_to_compact(secs);
2392 assert_eq!(compact, 1, "one second after epoch should be compact 1");
2393 let restored = secs_from_compact(compact);
2394 assert_eq!(restored, secs, "should restore to original");
2395 }
2396
2397 #[test]
2402 fn message_flags_mine_independent() {
2403 let mut flags = MessageFlags::NONE;
2404 flags.set_mine(true);
2405 assert!(flags.is_mine(), "mine should be set");
2406 assert!(!flags.is_pending(), "pending should not be set");
2407 assert!(!flags.is_failed(), "failed should not be set");
2408 }
2409
2410 #[test]
2411 fn message_flags_pending_independent() {
2412 let mut flags = MessageFlags::NONE;
2413 flags.set_pending(true);
2414 assert!(!flags.is_mine(), "mine should not be set");
2415 assert!(flags.is_pending(), "pending should be set");
2416 assert!(!flags.is_failed(), "failed should not be set");
2417 }
2418
2419 #[test]
2420 fn message_flags_failed_independent() {
2421 let mut flags = MessageFlags::NONE;
2422 flags.set_failed(true);
2423 assert!(!flags.is_mine(), "mine should not be set");
2424 assert!(!flags.is_pending(), "pending should not be set");
2425 assert!(flags.is_failed(), "failed should be set");
2426 }
2427
2428 #[test]
2429 fn message_flags_from_bools_all_false() {
2430 let flags = MessageFlags::from_bools(false, false, false);
2431 assert!(!flags.is_mine());
2432 assert!(!flags.is_pending());
2433 assert!(!flags.is_failed());
2434 assert_eq!(flags, MessageFlags::NONE, "all false should equal NONE");
2435 }
2436
2437 #[test]
2438 fn message_flags_from_bools_all_true() {
2439 let flags = MessageFlags::from_bools(true, true, true);
2440 assert!(flags.is_mine(), "mine should be set");
2441 assert!(flags.is_pending(), "pending should be set");
2442 assert!(flags.is_failed(), "failed should be set");
2443 }
2444
2445 #[test]
2446 fn message_flags_from_bools_various_combos() {
2447 let flags = MessageFlags::from_bools(true, false, true);
2448 assert!(flags.is_mine());
2449 assert!(!flags.is_pending());
2450 assert!(flags.is_failed());
2451
2452 let flags = MessageFlags::from_bools(false, true, false);
2453 assert!(!flags.is_mine());
2454 assert!(flags.is_pending());
2455 assert!(!flags.is_failed());
2456 }
2457
2458 #[test]
2459 fn message_flags_from_all_replied_to_none() {
2460 let flags = MessageFlags::from_all(false, false, false, None);
2461 assert_eq!(flags.replied_to_has_attachment(), None, "None should roundtrip");
2462 }
2463
2464 #[test]
2465 fn message_flags_from_all_replied_to_some_false() {
2466 let flags = MessageFlags::from_all(false, false, false, Some(false));
2467 assert_eq!(flags.replied_to_has_attachment(), Some(false), "Some(false) should roundtrip");
2468 }
2469
2470 #[test]
2471 fn message_flags_from_all_replied_to_some_true() {
2472 let flags = MessageFlags::from_all(false, false, false, Some(true));
2473 assert_eq!(flags.replied_to_has_attachment(), Some(true), "Some(true) should roundtrip");
2474 }
2475
2476 #[test]
2477 fn message_flags_multiple_set_simultaneously() {
2478 let flags = MessageFlags::from_all(true, true, false, Some(true));
2479 assert!(flags.is_mine());
2480 assert!(flags.is_pending());
2481 assert!(!flags.is_failed());
2482 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2483 }
2484
2485 #[test]
2486 fn message_flags_default_is_all_false() {
2487 let flags = MessageFlags::default();
2488 assert!(!flags.is_mine());
2489 assert!(!flags.is_pending());
2490 assert!(!flags.is_failed());
2491 assert_eq!(flags.replied_to_has_attachment(), None);
2492 assert_eq!(flags, MessageFlags::NONE);
2493 }
2494
2495 #[test]
2496 fn message_flags_bit_patterns_correct() {
2497 assert_eq!(MessageFlags::MINE.0, 0b00001, "MINE bit pattern");
2498 assert_eq!(MessageFlags::PENDING.0, 0b00010, "PENDING bit pattern");
2499 assert_eq!(MessageFlags::FAILED.0, 0b00100, "FAILED bit pattern");
2500 }
2501
2502 #[test]
2503 fn message_flags_set_then_clear() {
2504 let mut flags = MessageFlags::from_bools(true, true, true);
2505 flags.set_mine(false);
2506 assert!(!flags.is_mine(), "mine should be cleared");
2507 assert!(flags.is_pending(), "pending should remain set");
2508 assert!(flags.is_failed(), "failed should remain set");
2509 }
2510
2511 #[test]
2512 fn message_flags_replied_to_overwrite() {
2513 let mut flags = MessageFlags::from_all(false, false, false, Some(true));
2514 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2515 flags.set_replied_to_has_attachment(Some(false));
2516 assert_eq!(flags.replied_to_has_attachment(), Some(false), "overwrite should work");
2517 flags.set_replied_to_has_attachment(None);
2518 assert_eq!(flags.replied_to_has_attachment(), None, "clearing to None should work");
2519 }
2520
2521 #[test]
2522 fn message_flags_replied_to_does_not_interfere_with_other_bits() {
2523 let mut flags = MessageFlags::from_bools(true, true, true);
2524 flags.set_replied_to_has_attachment(Some(true));
2525 assert!(flags.is_mine(), "mine should still be set");
2526 assert!(flags.is_pending(), "pending should still be set");
2527 assert!(flags.is_failed(), "failed should still be set");
2528 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2529 }
2530
2531 #[test]
2536 fn attachment_flags_downloading_independent() {
2537 let mut flags = AttachmentFlags::NONE;
2538 flags.set_downloading(true);
2539 assert!(flags.is_downloading());
2540 assert!(!flags.is_downloaded());
2541 assert!(!flags.is_short_nonce());
2542 }
2543
2544 #[test]
2545 fn attachment_flags_downloaded_independent() {
2546 let mut flags = AttachmentFlags::NONE;
2547 flags.set_downloaded(true);
2548 assert!(!flags.is_downloading());
2549 assert!(flags.is_downloaded());
2550 assert!(!flags.is_short_nonce());
2551 }
2552
2553 #[test]
2554 fn attachment_flags_short_nonce_independent() {
2555 let mut flags = AttachmentFlags::NONE;
2556 flags.set_short_nonce(true);
2557 assert!(!flags.is_downloading());
2558 assert!(!flags.is_downloaded());
2559 assert!(flags.is_short_nonce());
2560 }
2561
2562 #[test]
2563 fn attachment_flags_from_bools() {
2564 let flags = AttachmentFlags::from_bools(true, false);
2565 assert!(flags.is_downloading());
2566 assert!(!flags.is_downloaded());
2567
2568 let flags = AttachmentFlags::from_bools(false, true);
2569 assert!(!flags.is_downloading());
2570 assert!(flags.is_downloaded());
2571 }
2572
2573 #[test]
2574 fn attachment_flags_all_set() {
2575 let mut flags = AttachmentFlags::NONE;
2576 flags.set_downloading(true);
2577 flags.set_downloaded(true);
2578 flags.set_short_nonce(true);
2579 assert!(flags.is_downloading());
2580 assert!(flags.is_downloaded());
2581 assert!(flags.is_short_nonce());
2582 }
2583
2584 #[test]
2585 fn attachment_flags_set_then_clear() {
2586 let mut flags = AttachmentFlags::NONE;
2587 flags.set_downloading(true);
2588 flags.set_downloaded(true);
2589 flags.set_downloading(false);
2590 assert!(!flags.is_downloading(), "downloading should be cleared");
2591 assert!(flags.is_downloaded(), "downloaded should remain set");
2592 }
2593
2594 #[test]
2595 fn attachment_flags_default_none() {
2596 let flags = AttachmentFlags::NONE;
2597 assert!(!flags.is_downloading());
2598 assert!(!flags.is_downloaded());
2599 assert!(!flags.is_short_nonce());
2600 assert_eq!(flags, AttachmentFlags::default());
2601 }
2602
2603 #[test]
2604 fn attachment_flags_bit_values() {
2605 let mut flags = AttachmentFlags::NONE;
2607 flags.set_downloading(true);
2608 assert_eq!(flags.0, 0b0001);
2609
2610 let mut flags = AttachmentFlags::NONE;
2611 flags.set_downloaded(true);
2612 assert_eq!(flags.0, 0b0010);
2613
2614 let mut flags = AttachmentFlags::NONE;
2615 flags.set_short_nonce(true);
2616 assert_eq!(flags.0, 0b0100);
2617 }
2618
2619 #[test]
2624 fn interner_returns_incrementing_handles() {
2625 let mut interner = NpubInterner::new();
2626 let h0 = interner.intern("npub1aaa");
2627 let h1 = interner.intern("npub1bbb");
2628 let h2 = interner.intern("npub1ccc");
2629 assert_eq!(h0, 0, "first intern should be handle 0");
2630 assert_eq!(h1, 1, "second intern should be handle 1");
2631 assert_eq!(h2, 2, "third intern should be handle 2");
2632 }
2633
2634 #[test]
2635 fn interner_lookup_finds_interned() {
2636 let mut interner = NpubInterner::new();
2637 let h = interner.intern("npub1alice");
2638 let found = interner.lookup("npub1alice");
2639 assert_eq!(found, Some(h), "lookup should find interned string");
2640 }
2641
2642 #[test]
2643 fn interner_lookup_returns_none_for_unknown() {
2644 let interner = NpubInterner::new();
2645 assert_eq!(interner.lookup("npub1unknown"), None, "lookup on empty interner should be None");
2646 }
2647
2648 #[test]
2649 fn interner_lookup_returns_none_for_not_interned() {
2650 let mut interner = NpubInterner::new();
2651 interner.intern("npub1alice");
2652 assert_eq!(interner.lookup("npub1bob"), None, "lookup for non-interned should be None");
2653 }
2654
2655 #[test]
2656 fn interner_resolve_returns_string() {
2657 let mut interner = NpubInterner::new();
2658 let h = interner.intern("npub1test123");
2659 assert_eq!(interner.resolve(h), Some("npub1test123"), "resolve should return the original string");
2660 }
2661
2662 #[test]
2663 fn interner_resolve_returns_none_for_no_npub() {
2664 let interner = NpubInterner::new();
2665 assert_eq!(interner.resolve(NO_NPUB), None, "resolve(NO_NPUB) should be None");
2666 }
2667
2668 #[test]
2669 fn interner_resolve_returns_none_for_out_of_bounds() {
2670 let mut interner = NpubInterner::new();
2671 interner.intern("npub1only");
2672 assert_eq!(interner.resolve(999), None, "out-of-bounds handle should resolve to None");
2673 }
2674
2675 #[test]
2676 fn interner_duplicate_returns_same_handle() {
2677 let mut interner = NpubInterner::new();
2678 let h1 = interner.intern("npub1dup");
2679 let h2 = interner.intern("npub1dup");
2680 let h3 = interner.intern("npub1dup");
2681 assert_eq!(h1, h2, "duplicate intern should return same handle");
2682 assert_eq!(h2, h3, "duplicate intern should return same handle");
2683 assert_eq!(interner.len(), 1, "duplicates should not increase length");
2684 }
2685
2686 #[test]
2687 fn interner_100_unique_npubs_stress() {
2688 let mut interner = NpubInterner::new();
2689 let mut handles = Vec::new();
2690
2691 for i in 0..100 {
2692 let npub = format!("npub1stress{:04}", i);
2693 let h = interner.intern(&npub);
2694 handles.push((h, npub));
2695 }
2696
2697 assert_eq!(interner.len(), 100, "should have 100 unique npubs");
2698
2699 for (h, npub) in &handles {
2701 assert_eq!(interner.resolve(*h), Some(npub.as_str()),
2702 "handle {} should resolve to {}", h, npub);
2703 }
2704
2705 for (h, npub) in &handles {
2707 assert_eq!(interner.lookup(npub), Some(*h),
2708 "lookup for {} should return handle {}", npub, h);
2709 }
2710
2711 for (h, npub) in &handles {
2713 assert_eq!(interner.intern(npub), *h,
2714 "re-interning {} should return same handle {}", npub, h);
2715 }
2716 assert_eq!(interner.len(), 100, "re-interning should not grow interner");
2717 }
2718
2719 #[test]
2720 fn interner_memory_usage_reasonable() {
2721 let mut interner = NpubInterner::new();
2722 for i in 0..50 {
2723 interner.intern(&format!("npub1{:0>62}", i));
2724 }
2725 let mem = interner.memory_usage();
2726 assert!(mem > 0, "memory usage should be positive");
2728 assert!(mem < 100_000, "memory usage for 50 npubs should be under 100KB, was {}", mem);
2729 }
2730
2731 #[test]
2732 fn interner_empty() {
2733 let interner = NpubInterner::new();
2734 assert_eq!(interner.len(), 0);
2735 assert!(interner.is_empty());
2736 assert_eq!(interner.resolve(0), None);
2737 assert_eq!(interner.lookup("anything"), None);
2738 assert!(interner.memory_usage() > 0, "even empty interner has struct overhead");
2739 }
2740
2741 #[test]
2742 fn interner_intern_opt_none() {
2743 let mut interner = NpubInterner::new();
2744 let h = interner.intern_opt(None);
2745 assert_eq!(h, NO_NPUB, "intern_opt(None) should return NO_NPUB");
2746 assert_eq!(interner.len(), 0, "None should not add to interner");
2747 }
2748
2749 #[test]
2750 fn interner_intern_opt_empty_string() {
2751 let mut interner = NpubInterner::new();
2752 let h = interner.intern_opt(Some(""));
2753 assert_eq!(h, NO_NPUB, "intern_opt(Some('')) should return NO_NPUB");
2754 }
2755
2756 #[test]
2757 fn interner_intern_opt_some_value() {
2758 let mut interner = NpubInterner::new();
2759 let h = interner.intern_opt(Some("npub1real"));
2760 assert_ne!(h, NO_NPUB, "intern_opt(Some(value)) should not return NO_NPUB");
2761 assert_eq!(interner.resolve(h), Some("npub1real"));
2762 }
2763
2764 fn make_compact_msg(hex_id: &str, timestamp: u64) -> CompactMessage {
2770 CompactMessage {
2771 id: encode_message_id(hex_id),
2772 at: timestamp,
2773 expiration_secs: 0,
2774 flags: MessageFlags::NONE,
2775 npub_idx: NO_NPUB,
2776 replied_to: None,
2777 replied_to_npub_idx: NO_NPUB,
2778 wrapper_id: None,
2779 content: "test".into(),
2780 replied_to_content: None,
2781 replied_to_emoji_tags: None,
2782 attachments: TinyVec::new(),
2783 reactions: TinyVec::new(),
2784 edit_history: None,
2785 preview_metadata: None,
2786 emoji_tags: None,
2787 addressed_bots: None,
2788 }
2789 }
2790
2791 #[test]
2792 fn compact_vec_insert_single_message() {
2793 let mut vec = CompactMessageVec::new();
2794 let msg = make_compact_msg(
2795 "1111111111111111111111111111111111111111111111111111111111111111",
2796 100,
2797 );
2798 assert!(vec.insert(msg), "insert should succeed");
2799 assert_eq!(vec.len(), 1);
2800 assert!(!vec.is_empty());
2801 }
2802
2803 #[test]
2804 fn compact_vec_insert_duplicate_rejected() {
2805 let mut vec = CompactMessageVec::new();
2806 let id = "2222222222222222222222222222222222222222222222222222222222222222";
2807 let msg1 = make_compact_msg(id, 100);
2808 let msg2 = make_compact_msg(id, 200); assert!(vec.insert(msg1), "first insert should succeed");
2810 assert!(!vec.insert(msg2), "duplicate ID should be rejected");
2811 assert_eq!(vec.len(), 1);
2812 }
2813
2814 #[test]
2815 fn compact_vec_insert_batch_multiple() {
2816 let mut vec = CompactMessageVec::new();
2817 let msgs = vec![
2818 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2819 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2820 make_compact_msg("cc00000000000000000000000000000000000000000000000000000000000000", 300),
2821 ];
2822 let added = vec.insert_batch(msgs);
2823 assert_eq!(added, 3, "all 3 should be added");
2824 assert_eq!(vec.len(), 3);
2825 }
2826
2827 #[test]
2828 fn compact_vec_append_batch_merge_keeps_index_consistent() {
2829 let mut vec = CompactMessageVec::new();
2833 let mk = |n: u32, at: u64| make_compact_msg(&format!("{:064x}", n), at);
2834 let seed: Vec<_> = (0..50).map(|n| mk(n, n as u64)).collect();
2835 assert_eq!(vec.insert_batch(seed), 50);
2836 let batch: Vec<_> = (1000..1200).map(|n| mk(n, n as u64)).collect();
2837 assert_eq!(vec.insert_batch(batch), 200, "all newer messages appended");
2838 assert_eq!(vec.len(), 250);
2839 for n in (0..50).chain(1000..1200) {
2840 assert!(vec.find_by_hex_id(&format!("{:064x}", n)).is_some(), "id {n} must be findable via the merged index");
2841 }
2842 assert!(vec.find_by_hex_id(&format!("{:064x}", 9999u32)).is_none(), "never-inserted id must not be found");
2843 let dup: Vec<_> = (1000..1200).map(|n| mk(n, n as u64)).collect();
2844 assert_eq!(vec.insert_batch(dup), 0, "re-appended batch fully deduped via the merged index");
2845 }
2846
2847 #[test]
2848 fn compact_vec_insert_batch_dedup() {
2849 let mut vec = CompactMessageVec::new();
2850 let id = "dd00000000000000000000000000000000000000000000000000000000000000";
2851 vec.insert(make_compact_msg(id, 100));
2852
2853 let msgs = vec![
2854 make_compact_msg(id, 200), make_compact_msg("ee00000000000000000000000000000000000000000000000000000000000000", 300),
2856 ];
2857 let added = vec.insert_batch(msgs);
2858 assert_eq!(added, 1, "only non-duplicate should be added");
2859 assert_eq!(vec.len(), 2);
2860 }
2861
2862 #[test]
2863 fn compact_vec_find_by_hex_id() {
2864 let mut vec = CompactMessageVec::new();
2865 let id = "ff00000000000000000000000000000000000000000000000000000000000001";
2866 let mut msg = make_compact_msg(id, 500);
2867 msg.content = "found me".into();
2868 vec.insert(msg);
2869
2870 let found = vec.find_by_hex_id(id);
2871 assert!(found.is_some(), "should find by hex id");
2872 assert_eq!(&*found.unwrap().content, "found me");
2873 }
2874
2875 #[test]
2876 fn compact_vec_find_by_hex_id_not_found() {
2877 let mut vec = CompactMessageVec::new();
2878 vec.insert(make_compact_msg(
2879 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2880 ));
2881 let found = vec.find_by_hex_id(
2882 "bb00000000000000000000000000000000000000000000000000000000000000",
2883 );
2884 assert!(found.is_none(), "should not find non-existent ID");
2885 }
2886
2887 #[test]
2888 fn compact_vec_find_by_hex_id_empty_string() {
2889 let mut vec = CompactMessageVec::new();
2890 vec.insert(make_compact_msg(
2891 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2892 ));
2893 assert!(vec.find_by_hex_id("").is_none(), "empty string should return None");
2894 }
2895
2896 #[test]
2897 fn compact_vec_find_by_hex_id_mut() {
2898 let mut vec = CompactMessageVec::new();
2899 let id = "ff00000000000000000000000000000000000000000000000000000000000002";
2900 vec.insert(make_compact_msg(id, 500));
2901
2902 let found = vec.find_by_hex_id_mut(id);
2903 assert!(found.is_some(), "should find mutable ref by hex id");
2904 found.unwrap().content = "modified".into();
2905
2906 let found = vec.find_by_hex_id(id);
2908 assert_eq!(&*found.unwrap().content, "modified");
2909 }
2910
2911 #[test]
2912 fn compact_vec_contains_hex_id() {
2913 let mut vec = CompactMessageVec::new();
2914 let id = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
2915 vec.insert(make_compact_msg(id, 100));
2916
2917 assert!(vec.contains_hex_id(id), "should contain inserted ID");
2918 assert!(!vec.contains_hex_id(
2919 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
2920 "should not contain non-inserted ID");
2921 assert!(!vec.contains_hex_id(""), "empty string should not match");
2922 }
2923
2924 #[test]
2925 fn compact_vec_remove_by_hex_id() {
2926 let mut vec = CompactMessageVec::new();
2927 let id1 = "1100000000000000000000000000000000000000000000000000000000000000";
2928 let id2 = "2200000000000000000000000000000000000000000000000000000000000000";
2929 vec.insert(make_compact_msg(id1, 100));
2930 vec.insert(make_compact_msg(id2, 200));
2931 assert_eq!(vec.len(), 2);
2932
2933 assert!(vec.remove_by_hex_id(id1), "remove should succeed");
2934 assert_eq!(vec.len(), 1);
2935 assert!(!vec.contains_hex_id(id1), "removed ID should not be found");
2936 assert!(vec.contains_hex_id(id2), "remaining ID should still be found");
2937 }
2938
2939 #[test]
2940 fn compact_vec_remove_nonexistent() {
2941 let mut vec = CompactMessageVec::new();
2942 vec.insert(make_compact_msg(
2943 "1100000000000000000000000000000000000000000000000000000000000000", 100,
2944 ));
2945 assert!(!vec.remove_by_hex_id(
2946 "9900000000000000000000000000000000000000000000000000000000000000"),
2947 "removing non-existent should return false");
2948 assert!(!vec.remove_by_hex_id(""), "removing empty should return false");
2949 assert_eq!(vec.len(), 1);
2950 }
2951
2952 #[test]
2953 fn compact_vec_last_timestamp() {
2954 let mut vec = CompactMessageVec::new();
2955 assert_eq!(vec.last_timestamp(), None, "empty vec should have no last timestamp");
2956
2957 vec.insert(make_compact_msg(
2958 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2959 ));
2960 vec.insert(make_compact_msg(
2961 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2962 ));
2963 vec.insert(make_compact_msg(
2964 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2965 ));
2966
2967 let last_ts = vec.last_timestamp().unwrap();
2968 let expected = timestamp_from_compact(300);
2970 assert_eq!(last_ts, expected, "last timestamp should be the largest");
2971 }
2972
2973 #[test]
2974 fn compact_vec_empty_operations() {
2975 let vec = CompactMessageVec::new();
2976 assert!(vec.is_empty());
2977 assert_eq!(vec.len(), 0);
2978 assert!(vec.last().is_none());
2979 assert!(vec.first().is_none());
2980 assert!(vec.last_timestamp().is_none());
2981 assert!(!vec.contains_hex_id("anything"));
2982 assert!(vec.find_by_hex_id("anything").is_none());
2983 }
2984
2985 #[test]
2986 fn compact_vec_1000_message_stress() {
2987 let mut vec = CompactMessageVec::new();
2988
2989 for i in 0..1000u64 {
2991 let id = format!("{:0>64x}", i);
2992 let msg = make_compact_msg(&id, i * 10);
2993 assert!(vec.insert(msg), "insert {} should succeed", i);
2994 }
2995 assert_eq!(vec.len(), 1000);
2996
2997 for i in 0..1000u64 {
2999 let id = format!("{:0>64x}", i);
3000 assert!(vec.contains_hex_id(&id), "should find message {}", i);
3001 let found = vec.find_by_hex_id(&id).unwrap();
3002 assert_eq!(found.at, i * 10, "timestamp should match for message {}", i);
3003 }
3004
3005 for i in 1000..1010u64 {
3007 let id = format!("{:0>64x}", i);
3008 assert!(!vec.contains_hex_id(&id), "should not find non-existent {}", i);
3009 }
3010
3011 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3013 for w in timestamps.windows(2) {
3014 assert!(w[0] <= w[1], "messages should be sorted by timestamp: {} <= {}", w[0], w[1]);
3015 }
3016 }
3017
3018 #[test]
3019 fn compact_vec_rebuild_index_after_id_change() {
3020 let mut vec = CompactMessageVec::new();
3021 let old_id = "aa00000000000000000000000000000000000000000000000000000000000000";
3022 let new_id = "ff00000000000000000000000000000000000000000000000000000000000000";
3023 vec.insert(make_compact_msg(old_id, 100));
3024
3025 vec.messages_mut()[0].id = encode_message_id(new_id);
3027 assert!(!vec.contains_hex_id(new_id), "stale index should not find new ID");
3029
3030 vec.rebuild_index();
3032 assert!(vec.contains_hex_id(new_id), "after rebuild, new ID should be found");
3033 assert!(!vec.contains_hex_id(old_id), "after rebuild, old ID should not be found");
3034 }
3035
3036 #[test]
3037 fn compact_vec_pending_id_lookup() {
3038 let mut vec = CompactMessageVec::new();
3039 let pending = "pending-9876543210";
3040 vec.insert(make_compact_msg(pending, 500));
3041
3042 assert!(vec.contains_hex_id(pending), "should find pending ID");
3043 let found = vec.find_by_hex_id(pending);
3044 assert!(found.is_some(), "should find pending message");
3045 assert_eq!(found.unwrap().id_hex(), pending, "id_hex should match pending string");
3046 }
3047
3048 #[test]
3049 fn compact_vec_out_of_order_insert() {
3050 let mut vec = CompactMessageVec::new();
3051 vec.insert(make_compact_msg(
3053 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
3054 ));
3055 vec.insert(make_compact_msg(
3056 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
3057 ));
3058 vec.insert(make_compact_msg(
3059 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
3060 ));
3061
3062 assert_eq!(vec.len(), 3);
3063 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3065 assert_eq!(timestamps, vec![100, 200, 300], "should be sorted by timestamp");
3066
3067 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3069 assert!(vec.contains_hex_id("bb00000000000000000000000000000000000000000000000000000000000000"));
3070 assert!(vec.contains_hex_id("cc00000000000000000000000000000000000000000000000000000000000000"));
3071 }
3072
3073 #[test]
3074 fn compact_vec_batch_prepend() {
3075 let mut vec = CompactMessageVec::new();
3076 vec.insert(make_compact_msg(
3078 "cc00000000000000000000000000000000000000000000000000000000000000", 300,
3079 ));
3080 vec.insert(make_compact_msg(
3081 "dd00000000000000000000000000000000000000000000000000000000000000", 400,
3082 ));
3083
3084 let older = vec![
3086 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
3087 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
3088 ];
3089 let added = vec.insert_batch(older);
3090 assert_eq!(added, 2);
3091 assert_eq!(vec.len(), 4);
3092
3093 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3095 assert_eq!(timestamps, vec![100, 200, 300, 400]);
3096
3097 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3099 assert!(vec.contains_hex_id("dd00000000000000000000000000000000000000000000000000000000000000"));
3100 }
3101
3102 #[test]
3103 fn compact_vec_clear() {
3104 let mut vec = CompactMessageVec::new();
3105 vec.insert(make_compact_msg(
3106 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
3107 ));
3108 vec.insert(make_compact_msg(
3109 "bb00000000000000000000000000000000000000000000000000000000000000", 200,
3110 ));
3111 assert_eq!(vec.len(), 2);
3112
3113 vec.clear();
3114 assert!(vec.is_empty());
3115 assert_eq!(vec.len(), 0);
3116 assert!(!vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3117 }
3118
3119 fn make_full_message() -> Message {
3125 Message {
3126 expiration: Some(1893456000),
3127 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3128 content: "Hello, world!".into(),
3129 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3130 replied_to_content: Some("Original message".into()),
3131 replied_to_emoji_tags: Some(vec![crate::types::EmojiTag {
3132 shortcode: "catJAM".into(),
3133 url: "https://example.com/catjam.png".into(),
3134 }]),
3135 replied_to_npub: Some("npub1replier".into()),
3136 replied_to_has_attachment: Some(true),
3137 replied_to_attachment_extension: None,
3138 preview_metadata: Some(SiteMetadata {
3139 domain: "example.com".into(),
3140 og_title: Some("Test Page".into()),
3141 og_description: Some("A test description".into()),
3142 og_image: Some("https://example.com/img.png".into()),
3143 og_url: Some("https://example.com".into()),
3144 og_type: Some("website".into()),
3145 title: Some("Test".into()),
3146 description: Some("Desc".into()),
3147 favicon: Some("https://example.com/favicon.ico".into()),
3148 }),
3149 attachments: vec![Attachment {
3150 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3151 key: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3152 nonce: "cccccccccccccccccccccccccccccccc".into(), extension: "png".into(),
3154 name: "photo.png".into(),
3155 url: "https://blossom.example.com".into(),
3156 path: "/tmp/photo.png".into(),
3157 size: 12345,
3158 img_meta: Some(ImageMetadata {
3159 thumbhash: "abc123".into(),
3160 width: 800,
3161 height: 600,
3162 }),
3163 downloading: false,
3164 downloaded: true,
3165 webxdc_topic: None,
3166 group_id: None,
3167 original_hash: None,
3168 fallback_urls: Vec::new(),
3169 }],
3170 reactions: vec![Reaction {
3171 id: "dddd000000000000000000000000000000000000000000000000000000000000".into(),
3172 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3173 author_id: "npub1reactor".into(),
3174 emoji: "\u{1f44d}".into(), emoji_url: None,
3176 }],
3177 at: 1705320000000, pending: false,
3179 failed: false,
3180 mine: true,
3181 npub: Some("npub1sender".into()),
3182 wrapper_event_id: Some("eeee000000000000000000000000000000000000000000000000000000000000".into()),
3183 edited: true,
3184 edit_history: Some(vec![
3185 EditEntry { content: "Original".into(), edited_at: 1705320000000 },
3186 EditEntry { content: "Edited".into(), edited_at: 1705320060000 },
3187 ]),
3188 emoji_tags: Vec::new(),
3189 addressed_bots: vec!["npub1botrouting0000000000000000000000000000000000000000000000".into()],
3190 }
3191 }
3192
3193 #[test]
3194 fn compact_add_reaction_rejects_a_resend_under_a_new_event_id() {
3195 let mut interner = NpubInterner::new();
3196 let mut compact = CompactMessage::from_message(&Message::default(), &mut interner);
3197 let slot = |id: String, emoji: &str| Reaction {
3199 id,
3200 reference_id: "a".repeat(64),
3201 author_id: "npub1author".to_string(),
3202 emoji: emoji.to_string(),
3203 emoji_url: None,
3204 };
3205 assert!(compact.add_reaction(slot("1".repeat(64), "\u{1F44D}"), &mut interner));
3206 assert!(
3207 !compact.add_reaction(slot("2".repeat(64), "\u{1F44D}"), &mut interner),
3208 "distinct event ids but one (author, emoji) slot — this is the double-count"
3209 );
3210 assert_eq!(compact.reactions.len(), 1);
3211 assert!(
3212 compact.add_reaction(slot("3".repeat(64), "\u{2764}\u{FE0F}"), &mut interner),
3213 "a different emoji from the same author is its own reaction"
3214 );
3215 assert_eq!(compact.reactions.len(), 2);
3216 }
3217
3218 #[test]
3219 fn a_message_caps_at_twelve_reaction_groups_but_joins_stay_open() {
3220 let mut interner = NpubInterner::new();
3221 let mut compact = CompactMessage::from_message(&Message::default(), &mut interner);
3222 let react = |i: usize, author: &str, emoji: &str| Reaction {
3223 id: format!("{:064x}", i + 1),
3224 reference_id: "a".repeat(64),
3225 author_id: author.to_string(),
3226 emoji: emoji.to_string(),
3227 emoji_url: None,
3228 };
3229 for i in 0..MAX_REACTION_GROUPS {
3230 assert!(
3231 compact.add_reaction(react(i, "npub1opener", &format!("emoji{i}")), &mut interner),
3232 "group {i} fits under the ceiling"
3233 );
3234 }
3235 assert!(
3236 !compact.add_reaction(react(100, "npub1opener", "emoji-thirteen"), &mut interner),
3237 "a thirteenth distinct emoji is refused"
3238 );
3239 assert!(
3240 compact.add_reaction(react(101, "npub1joiner", "emoji0"), &mut interner),
3241 "joining an existing group is never bounded by the ceiling"
3242 );
3243 assert_eq!(compact.reactions.len(), MAX_REACTION_GROUPS + 1);
3244 }
3245
3246 #[test]
3247 fn compact_message_from_message_roundtrip_all_fields() {
3248 let msg = make_full_message();
3249 let mut interner = NpubInterner::new();
3250 let compact = CompactMessage::from_message(&msg, &mut interner);
3251 let restored = compact.to_message(&interner);
3252
3253 assert_eq!(restored.id, msg.id, "id mismatch");
3254 assert_eq!(restored.content, msg.content, "content mismatch");
3255 assert_eq!(restored.mine, msg.mine, "mine mismatch");
3256 assert_eq!(restored.pending, msg.pending, "pending mismatch");
3257 assert_eq!(restored.failed, msg.failed, "failed mismatch");
3258 assert_eq!(restored.npub, msg.npub, "npub mismatch");
3259 assert_eq!(restored.replied_to, msg.replied_to, "replied_to mismatch");
3260 assert_eq!(restored.replied_to_content, msg.replied_to_content, "replied_to_content mismatch");
3261 assert_eq!(restored.replied_to_npub, msg.replied_to_npub, "replied_to_npub mismatch");
3262 assert_eq!(restored.replied_to_has_attachment, msg.replied_to_has_attachment, "replied_to_has_attachment mismatch");
3263 assert_eq!(restored.wrapper_event_id, msg.wrapper_event_id, "wrapper_event_id mismatch");
3264 assert_eq!(restored.edited, msg.edited, "edited mismatch");
3265 assert_eq!(restored.edit_history, msg.edit_history, "edit_history mismatch");
3266 assert_eq!(restored.preview_metadata, msg.preview_metadata, "preview_metadata mismatch");
3267 assert_eq!(restored.at / 1000, msg.at / 1000, "timestamp seconds mismatch");
3269 assert_eq!(restored.attachments.len(), 1, "should have 1 attachment");
3271 assert_eq!(restored.attachments[0].id, msg.attachments[0].id);
3272 assert_eq!(restored.attachments[0].name, msg.attachments[0].name);
3273 assert_eq!(restored.attachments[0].size, msg.attachments[0].size);
3274 assert_eq!(restored.reactions.len(), 1, "should have 1 reaction");
3276 assert_eq!(restored.reactions[0].emoji, msg.reactions[0].emoji);
3277 assert_eq!(restored.addressed_bots, msg.addressed_bots, "addressed_bots mismatch");
3279 }
3280
3281 #[test]
3287 fn compact_drops_replied_to_attachment_extension() {
3288 let mut msg = make_full_message();
3289 msg.replied_to_has_attachment = Some(true);
3290 msg.replied_to_attachment_extension = Some("gif".to_string());
3291
3292 let mut interner = NpubInterner::new();
3293 let restored = CompactMessage::from_message(&msg, &mut interner).to_message(&interner);
3294
3295 assert_eq!(restored.replied_to_has_attachment, Some(true), "the bool survives RAM");
3296 assert_eq!(
3297 restored.replied_to_attachment_extension, None,
3298 "the extension does not survive RAM — emitters must re-resolve it"
3299 );
3300 }
3301
3302 #[test]
3303 fn compact_message_from_message_owned_roundtrip() {
3304 let msg = make_full_message();
3305 let msg_clone = msg.clone();
3306 let mut interner = NpubInterner::new();
3307 let compact = CompactMessage::from_message_owned(msg, &mut interner);
3308 let restored = compact.to_message(&interner);
3309
3310 assert_eq!(restored.id, msg_clone.id, "id mismatch");
3311 assert_eq!(restored.content, msg_clone.content, "content mismatch");
3312 assert_eq!(restored.mine, msg_clone.mine, "mine mismatch");
3313 assert_eq!(restored.npub, msg_clone.npub, "npub mismatch");
3314 assert_eq!(restored.edit_history, msg_clone.edit_history, "edit_history mismatch");
3315 }
3316
3317 #[test]
3318 fn compact_message_pending_flag() {
3319 let msg = Message {
3320 id: "pending-1234567890".into(),
3321 pending: true,
3322 ..Message::default()
3323 };
3324 let mut interner = NpubInterner::new();
3325 let compact = CompactMessage::from_message(&msg, &mut interner);
3326 assert!(compact.is_pending(), "pending flag should be set");
3327 let restored = compact.to_message(&interner);
3328 assert!(restored.pending, "pending should roundtrip");
3329 assert_eq!(restored.id, "pending-1234567890", "pending ID should roundtrip");
3330 }
3331
3332 #[test]
3333 fn compact_message_failed_flag() {
3334 let msg = Message {
3335 id: "pending-999".into(),
3336 failed: true,
3337 pending: true,
3338 ..Message::default()
3339 };
3340 let mut interner = NpubInterner::new();
3341 let compact = CompactMessage::from_message(&msg, &mut interner);
3342 assert!(compact.is_failed(), "failed flag should be set");
3343 assert!(compact.is_pending(), "pending flag should also be set");
3344 let restored = compact.to_message(&interner);
3345 assert!(restored.failed);
3346 assert!(restored.pending);
3347 }
3348
3349 #[test]
3350 fn compact_message_with_attachments_roundtrip() {
3351 let msg = Message {
3352 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3353 attachments: vec![
3354 Attachment {
3355 id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3356 extension: "jpg".into(),
3357 name: "sunset.jpg".into(),
3358 size: 5000,
3359 downloaded: true,
3360 ..Attachment::default()
3361 },
3362 Attachment {
3363 id: "2222222222222222222222222222222222222222222222222222222222222222".into(),
3364 extension: "mp4".into(),
3365 name: "video.mp4".into(),
3366 size: 50000,
3367 downloaded: false,
3368 downloading: true,
3369 ..Attachment::default()
3370 },
3371 ],
3372 ..Message::default()
3373 };
3374 let mut interner = NpubInterner::new();
3375 let compact = CompactMessage::from_message(&msg, &mut interner);
3376 assert_eq!(compact.attachments.len(), 2);
3377
3378 let restored = compact.to_message(&interner);
3379 assert_eq!(restored.attachments.len(), 2);
3380 assert_eq!(restored.attachments[0].name, "sunset.jpg");
3381 assert_eq!(restored.attachments[0].extension, "jpg");
3382 assert!(restored.attachments[0].downloaded);
3383 assert_eq!(restored.attachments[1].name, "video.mp4");
3384 assert!(restored.attachments[1].downloading);
3385 assert!(!restored.attachments[1].downloaded);
3386 }
3387
3388 #[test]
3389 fn compact_message_with_reactions_roundtrip() {
3390 let msg = Message {
3391 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3392 reactions: vec![
3393 Reaction {
3394 id: "aaa0000000000000000000000000000000000000000000000000000000000000".into(),
3395 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3396 author_id: "npub1alice".into(),
3397 emoji: "\u{2764}".into(), emoji_url: None,
3399 },
3400 Reaction {
3401 id: "bbb0000000000000000000000000000000000000000000000000000000000000".into(),
3402 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3403 author_id: "npub1bob".into(),
3404 emoji: "\u{1f525}".into(), emoji_url: None,
3406 },
3407 ],
3408 ..Message::default()
3409 };
3410 let mut interner = NpubInterner::new();
3411 let compact = CompactMessage::from_message(&msg, &mut interner);
3412 let restored = compact.to_message(&interner);
3413
3414 assert_eq!(restored.reactions.len(), 2);
3415 assert_eq!(restored.reactions[0].emoji, "\u{2764}");
3416 assert_eq!(restored.reactions[0].author_id, "npub1alice");
3417 assert_eq!(restored.reactions[1].emoji, "\u{1f525}");
3418 assert_eq!(restored.reactions[1].author_id, "npub1bob");
3419 }
3420
3421 #[test]
3422 fn compact_message_with_edit_history_roundtrip() {
3423 let msg = Message {
3424 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3425 content: "Final version".into(),
3426 edited: true,
3427 edit_history: Some(vec![
3428 EditEntry { content: "First draft".into(), edited_at: 1000 },
3429 EditEntry { content: "Second draft".into(), edited_at: 2000 },
3430 EditEntry { content: "Final version".into(), edited_at: 3000 },
3431 ]),
3432 ..Message::default()
3433 };
3434 let mut interner = NpubInterner::new();
3435 let compact = CompactMessage::from_message(&msg, &mut interner);
3436 assert!(compact.is_edited());
3437
3438 let restored = compact.to_message(&interner);
3439 assert!(restored.edited);
3440 let history = restored.edit_history.unwrap();
3441 assert_eq!(history.len(), 3);
3442 assert_eq!(history[0].content, "First draft");
3443 assert_eq!(history[2].content, "Final version");
3444 }
3445
3446 #[test]
3447 fn compact_message_with_preview_metadata_roundtrip() {
3448 let meta = SiteMetadata {
3449 domain: "example.com".into(),
3450 og_title: Some("Title".into()),
3451 og_description: Some("Desc".into()),
3452 og_image: Some("https://example.com/img.png".into()),
3453 og_url: None,
3454 og_type: None,
3455 title: None,
3456 description: None,
3457 favicon: None,
3458 };
3459 let msg = Message {
3460 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3461 preview_metadata: Some(meta.clone()),
3462 ..Message::default()
3463 };
3464 let mut interner = NpubInterner::new();
3465 let compact = CompactMessage::from_message(&msg, &mut interner);
3466 let restored = compact.to_message(&interner);
3467
3468 let restored_meta = restored.preview_metadata.unwrap();
3469 assert_eq!(restored_meta.domain, "example.com");
3470 assert_eq!(restored_meta.og_title, Some("Title".into()));
3471 assert_eq!(restored_meta.og_image, Some("https://example.com/img.png".into()));
3472 assert_eq!(restored_meta.og_url, None);
3473 }
3474
3475 #[test]
3476 fn compact_message_with_replied_to_roundtrip() {
3477 let msg = Message {
3478 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3479 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3480 replied_to_content: Some("Original text".into()),
3481 replied_to_npub: Some("npub1original".into()),
3482 replied_to_has_attachment: Some(false),
3483 ..Message::default()
3484 };
3485 let mut interner = NpubInterner::new();
3486 let compact = CompactMessage::from_message(&msg, &mut interner);
3487 assert!(compact.has_reply());
3488
3489 let restored = compact.to_message(&interner);
3490 assert_eq!(restored.replied_to, "1111111111111111111111111111111111111111111111111111111111111111");
3491 assert_eq!(restored.replied_to_content, Some("Original text".into()));
3492 assert_eq!(restored.replied_to_npub, Some("npub1original".into()));
3493 assert_eq!(restored.replied_to_has_attachment, Some(false));
3494 }
3495
3496 #[test]
3497 fn compact_message_empty_roundtrip() {
3498 let msg = Message::default();
3499 let mut interner = NpubInterner::new();
3500 let compact = CompactMessage::from_message(&msg, &mut interner);
3501 let restored = compact.to_message(&interner);
3502
3503 assert_eq!(restored.id, "0000000000000000000000000000000000000000000000000000000000000000",
3504 "empty ID should decode as all zeros hex");
3505 assert_eq!(restored.content, "");
3506 assert!(!restored.mine);
3507 assert!(!restored.pending);
3508 assert!(!restored.failed);
3509 assert!(!restored.edited);
3510 assert_eq!(restored.npub, None);
3511 assert!(restored.replied_to.is_empty() || restored.replied_to == "0000000000000000000000000000000000000000000000000000000000000000");
3512 assert_eq!(restored.replied_to_content, None);
3513 assert_eq!(restored.replied_to_npub, None);
3514 assert_eq!(restored.replied_to_has_attachment, None);
3515 assert_eq!(restored.wrapper_event_id, None);
3516 assert!(restored.attachments.is_empty());
3517 assert!(restored.reactions.is_empty());
3518 assert_eq!(restored.edit_history, None);
3519 assert_eq!(restored.preview_metadata, None);
3520 }
3521
3522 #[test]
3527 fn compact_attachment_from_attachment_roundtrip() {
3528 let att = Attachment {
3529 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3530 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3531 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), extension: "zip".into(),
3533 name: "archive.zip".into(),
3534 url: "https://blossom.test.com".into(),
3535 path: "/downloads/archive.zip".into(),
3536 size: 99999,
3537 img_meta: None,
3538 downloading: false,
3539 downloaded: true,
3540 webxdc_topic: None,
3541 group_id: None,
3542 original_hash: None,
3543 fallback_urls: Vec::new(),
3544 };
3545
3546 let compact = CompactAttachment::from_attachment(&att);
3547 let restored = compact.to_attachment();
3548
3549 assert_eq!(restored.id, att.id, "id mismatch");
3550 assert_eq!(restored.key, att.key, "key mismatch");
3551 assert_eq!(restored.nonce, att.nonce, "nonce mismatch");
3552 assert_eq!(restored.extension, att.extension, "extension mismatch");
3553 assert_eq!(restored.name, att.name, "name mismatch");
3554 assert_eq!(restored.url, att.url, "url mismatch");
3555 assert_eq!(restored.path, att.path, "path mismatch");
3556 assert_eq!(restored.size, att.size, "size mismatch");
3557 assert_eq!(restored.downloading, att.downloading, "downloading mismatch");
3558 assert_eq!(restored.downloaded, att.downloaded, "downloaded mismatch");
3559 }
3560
3561 #[test]
3562 fn compact_attachment_from_attachment_owned_roundtrip() {
3563 let att = Attachment {
3564 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3565 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3566 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3567 extension: "pdf".into(),
3568 name: "document.pdf".into(),
3569 url: "https://server.com".into(),
3570 path: "".into(),
3571 size: 1024,
3572 img_meta: None,
3573 downloading: false,
3574 downloaded: false,
3575 webxdc_topic: None,
3576 group_id: None,
3577 original_hash: None,
3578 fallback_urls: Vec::new(),
3579 };
3580 let att_clone = att.clone();
3581
3582 let compact = CompactAttachment::from_attachment_owned(att);
3583 let restored = compact.to_attachment();
3584
3585 assert_eq!(restored.id, att_clone.id);
3586 assert_eq!(restored.name, att_clone.name);
3587 assert_eq!(restored.size, att_clone.size);
3588 }
3589
3590 #[test]
3591 fn compact_attachment_key_nonce_zeros() {
3592 let att = Attachment {
3593 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3594 key: "".into(), nonce: "".into(), ..Attachment::default()
3597 };
3598 let compact = CompactAttachment::from_attachment(&att);
3599 assert_eq!(compact.key, [0u8; 32], "empty key should be all zeros");
3600 assert_eq!(compact.nonce, [0u8; 16], "empty nonce should be all zeros");
3601
3602 let restored = compact.to_attachment();
3603 assert_eq!(restored.key, "", "zero key should restore as empty string");
3604 assert_eq!(restored.nonce, "", "zero nonce should restore as empty string");
3605 }
3606
3607 #[test]
3608 fn compact_attachment_short_nonce_mls_12byte() {
3609 let att = Attachment {
3611 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3612 nonce: "aabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3614 };
3615 let compact = CompactAttachment::from_attachment(&att);
3616 assert!(compact.flags.is_short_nonce(), "12-byte nonce should set short_nonce flag");
3617
3618 let restored = compact.to_attachment();
3619 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccdd", "short nonce should roundtrip");
3620 }
3621
3622 #[test]
3623 fn compact_attachment_long_nonce_dm_16byte() {
3624 let att = Attachment {
3626 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3627 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3629 };
3630 let compact = CompactAttachment::from_attachment(&att);
3631 assert!(!compact.flags.is_short_nonce(), "16-byte nonce should NOT set short_nonce flag");
3632
3633 let restored = compact.to_attachment();
3634 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccddaabbccdd", "long nonce should roundtrip");
3635 }
3636
3637 #[test]
3638 fn compact_attachment_id_eq_comparison() {
3639 let att = Attachment {
3640 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3641 ..Attachment::default()
3642 };
3643 let compact = CompactAttachment::from_attachment(&att);
3644
3645 assert!(compact.id_eq("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
3646 "id_eq should match same hex");
3647 assert!(!compact.id_eq("1111111111111111111111111111111111111111111111111111111111111111"),
3648 "id_eq should not match different hex");
3649 }
3650
3651 #[test]
3652 fn compact_attachment_all_optional_fields_none() {
3653 let att = Attachment::default();
3654 let compact = CompactAttachment::from_attachment(&att);
3655
3656 assert!(compact.img_meta.is_none());
3657 assert!(compact.group_id.is_none());
3658 assert!(compact.original_hash.is_none());
3659 assert!(compact.webxdc_topic.is_none());
3660
3661 let restored = compact.to_attachment();
3662 assert!(restored.img_meta.is_none());
3663 assert!(restored.group_id.is_none());
3664 assert!(restored.original_hash.is_none());
3665 assert!(restored.webxdc_topic.is_none());
3666 }
3667
3668 #[test]
3669 fn compact_attachment_all_optional_fields_some() {
3670 let att = Attachment {
3671 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3672 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3673 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3674 extension: "xdc".into(),
3675 name: "app.xdc".into(),
3676 url: "https://server.com/file".into(),
3677 path: "/local/app.xdc".into(),
3678 size: 50000,
3679 img_meta: Some(ImageMetadata {
3680 thumbhash: "hash123".into(),
3681 width: 1920,
3682 height: 1080,
3683 }),
3684 downloading: false,
3685 downloaded: true,
3686 webxdc_topic: Some("game-state".into()),
3687 group_id: Some("cccc000000000000000000000000000000000000000000000000000000000000".into()),
3688 original_hash: Some("dddd000000000000000000000000000000000000000000000000000000000000".into()),
3689 fallback_urls: Vec::new(),
3690 };
3691
3692 let compact = CompactAttachment::from_attachment(&att);
3693
3694 assert!(compact.img_meta.is_some());
3695 assert!(compact.group_id.is_some());
3696 assert!(compact.original_hash.is_some());
3697 assert!(compact.webxdc_topic.is_some());
3698
3699 let restored = compact.to_attachment();
3700 let meta = restored.img_meta.unwrap();
3701 assert_eq!(meta.thumbhash, "hash123");
3702 assert_eq!(meta.width, 1920);
3703 assert_eq!(meta.height, 1080);
3704 assert_eq!(restored.webxdc_topic, Some("game-state".into()));
3705 assert_eq!(restored.group_id.unwrap(), att.group_id.unwrap());
3706 assert_eq!(restored.original_hash.unwrap(), att.original_hash.unwrap());
3707 }
3708
3709 #[test]
3714 fn compact_reaction_from_reaction_roundtrip() {
3715 let reaction = Reaction {
3716 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3717 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3718 author_id: "npub1alice".into(),
3719 emoji: "+".into(),
3720 emoji_url: None,
3721 };
3722 let mut interner = NpubInterner::new();
3723 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3724 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3725
3726 assert_eq!(restored.id, reaction.id, "id mismatch");
3727 assert_eq!(restored.reference_id, reaction.reference_id, "reference_id mismatch");
3728 assert_eq!(restored.author_id, reaction.author_id, "author_id mismatch");
3729 assert_eq!(restored.emoji, reaction.emoji, "emoji mismatch");
3730 }
3731
3732 #[test]
3733 fn compact_reaction_author_resolved_via_interner() {
3734 let mut interner = NpubInterner::new();
3735 let alice_handle = interner.intern("npub1alice");
3736
3737 let reaction = Reaction {
3738 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3739 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3740 author_id: "npub1alice".into(),
3741 emoji: "+".into(),
3742 emoji_url: None,
3743 };
3744 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3745 assert_eq!(compact.author_idx, alice_handle, "should reuse existing interner handle");
3746
3747 let resolved = interner.resolve(compact.author_idx).unwrap();
3748 assert_eq!(resolved, "npub1alice");
3749 }
3750
3751 #[test]
3752 fn compact_reaction_unicode_emoji() {
3753 let reaction = Reaction {
3754 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3755 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3756 author_id: "npub1test".into(),
3757 emoji: "\u{1f431}\u{200d}\u{1f4bb}".into(), emoji_url: None,
3759 };
3760 let mut interner = NpubInterner::new();
3761 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3762 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3763 assert_eq!(restored.emoji, "\u{1f431}\u{200d}\u{1f4bb}", "complex unicode emoji should roundtrip");
3764 }
3765
3766 #[test]
3767 fn compact_reaction_custom_emoji() {
3768 let reaction = Reaction {
3769 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3770 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3771 author_id: "npub1test".into(),
3772 emoji: ":cat_heart_eyes:".into(),
3773 emoji_url: None,
3774 };
3775 let mut interner = NpubInterner::new();
3776 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3777 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3778 assert_eq!(restored.emoji, ":cat_heart_eyes:", "custom emoji shortcode should roundtrip");
3779 }
3780
3781 #[test]
3782 fn compact_reaction_owned_conversion() {
3783 let reaction = Reaction {
3784 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3785 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3786 author_id: "npub1bob".into(),
3787 emoji: "\u{1f44d}".into(), emoji_url: None,
3789 };
3790 let reaction_clone = reaction.clone();
3791 let mut interner = NpubInterner::new();
3792 let compact = CompactReaction::from_reaction_owned(reaction, &mut interner);
3793 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction_clone.reference_id), &interner);
3794
3795 assert_eq!(restored.id, reaction_clone.id);
3796 assert_eq!(restored.author_id, reaction_clone.author_id);
3797 assert_eq!(restored.emoji, reaction_clone.emoji);
3798 }
3799
3800 #[test]
3805 fn tinyvec_empty() {
3806 let tv: TinyVec<u32> = TinyVec::new();
3807 assert!(tv.is_empty());
3808 assert_eq!(tv.len(), 0);
3809 assert_eq!(tv.as_slice(), &[] as &[u32]);
3810 assert_eq!(tv.first(), None);
3811 assert_eq!(tv.last(), None);
3812 }
3813
3814 #[test]
3815 fn tinyvec_from_vec_and_back() {
3816 let original = vec![1u32, 2, 3, 4, 5];
3817 let tv = TinyVec::from_vec(original.clone());
3818 assert_eq!(tv.len(), 5);
3819 assert_eq!(tv.to_vec(), original);
3820 }
3821
3822 #[test]
3823 fn tinyvec_indexing() {
3824 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3825 assert_eq!(tv[0], 10);
3826 assert_eq!(tv[1], 20);
3827 assert_eq!(tv[2], 30);
3828 assert_eq!(tv.get(0), Some(&10));
3829 assert_eq!(tv.get(3), None);
3830 }
3831
3832 #[test]
3833 fn tinyvec_push() {
3834 let mut tv = TinyVec::from_vec(vec![1u32, 2]);
3835 tv.push(3);
3836 assert_eq!(tv.len(), 3);
3837 assert_eq!(tv.to_vec(), vec![1, 2, 3]);
3838 }
3839
3840 #[test]
3841 fn tinyvec_clone() {
3842 let tv = TinyVec::from_vec(vec!["hello".to_string(), "world".to_string()]);
3843 let cloned = tv.clone();
3844 assert_eq!(cloned.len(), 2);
3845 assert_eq!(cloned[0], "hello");
3846 assert_eq!(cloned[1], "world");
3847 }
3848
3849 #[test]
3850 fn tinyvec_retain() {
3851 let mut tv = TinyVec::from_vec(vec![1u32, 2, 3, 4, 5]);
3852 tv.retain(|&x| x % 2 == 0);
3853 assert_eq!(tv.to_vec(), vec![2, 4]);
3854 }
3855
3856 #[test]
3857 fn tinyvec_empty_from_empty_vec() {
3858 let tv = TinyVec::<u32>::from_vec(vec![]);
3859 assert!(tv.is_empty());
3860 assert_eq!(tv.len(), 0);
3861 }
3862
3863 #[test]
3864 fn tinyvec_iter() {
3865 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3866 let sum: u32 = tv.iter().sum();
3867 assert_eq!(sum, 60);
3868 }
3869
3870 #[test]
3871 fn tinyvec_any() {
3872 let tv = TinyVec::from_vec(vec![1u32, 2, 3]);
3873 assert!(tv.any(|&x| x == 2));
3874 assert!(!tv.any(|&x| x == 99));
3875 }
3876
3877 #[test]
3882 fn hex_to_bytes_16_full_32_chars() {
3883 let hex = "aabbccddaabbccddaabbccddaabbccdd";
3884 let bytes = hex_to_bytes_16(hex);
3885 assert_eq!(bytes[0], 0xaa);
3886 assert_eq!(bytes[1], 0xbb);
3887 assert_eq!(bytes[15], 0xdd);
3888 }
3889
3890 #[test]
3891 fn hex_to_bytes_16_short_input_padded() {
3892 let hex = "aabb";
3894 let bytes = hex_to_bytes_16(hex);
3895 assert_eq!(bytes[0], 0xaa);
3896 assert_eq!(bytes[1], 0xbb);
3897 for i in 2..16 {
3899 assert_eq!(bytes[i], 0, "byte {} should be 0 from padding", i);
3900 }
3901 }
3902
3903 #[test]
3904 fn bytes_to_hex_string_roundtrip() {
3905 let bytes: Vec<u8> = vec![0xaa, 0xbb, 0xcc, 0xdd];
3906 let hex = bytes_to_hex_string(&bytes);
3907 assert_eq!(hex, "aabbccdd");
3908 }
3909}