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
108const EPOCH_2020_SECS: u64 = 1577836800;
110
111#[inline]
114pub fn secs_to_compact(secs: u64) -> u32 {
115 if secs == 0 { return 0; }
116 secs.saturating_sub(EPOCH_2020_SECS) as u32
117}
118
119#[inline]
122pub fn secs_from_compact(compact: u32) -> u64 {
123 if compact == 0 { return 0; }
124 EPOCH_2020_SECS + compact as u64
125}
126
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
138pub struct MessageFlags(u8);
139
140impl MessageFlags {
141 pub const NONE: Self = Self(0);
142 pub const MINE: Self = Self(0b00001);
143 pub const PENDING: Self = Self(0b00010);
144 pub const FAILED: Self = Self(0b00100);
145 const REPLY_ATTACH_MASK: u8 = 0b11000;
148 const REPLY_ATTACH_SHIFT: u8 = 3;
149
150 #[inline]
151 pub fn is_mine(self) -> bool {
152 self.0 & Self::MINE.0 != 0
153 }
154
155 #[inline]
156 pub fn is_pending(self) -> bool {
157 self.0 & Self::PENDING.0 != 0
158 }
159
160 #[inline]
161 pub fn is_failed(self) -> bool {
162 self.0 & Self::FAILED.0 != 0
163 }
164
165 #[inline]
168 pub fn replied_to_has_attachment(self) -> Option<bool> {
169 match (self.0 & Self::REPLY_ATTACH_MASK) >> Self::REPLY_ATTACH_SHIFT {
170 0b00 => None, 0b01 => Some(false), 0b10 => Some(true), _ => None, }
175 }
176
177 #[inline]
178 pub fn set_mine(&mut self, value: bool) {
179 if value {
180 self.0 |= Self::MINE.0;
181 } else {
182 self.0 &= !Self::MINE.0;
183 }
184 }
185
186 #[inline]
187 pub fn set_pending(&mut self, value: bool) {
188 if value {
189 self.0 |= Self::PENDING.0;
190 } else {
191 self.0 &= !Self::PENDING.0;
192 }
193 }
194
195 #[inline]
196 pub fn set_failed(&mut self, value: bool) {
197 if value {
198 self.0 |= Self::FAILED.0;
199 } else {
200 self.0 &= !Self::FAILED.0;
201 }
202 }
203
204 #[inline]
206 pub fn set_replied_to_has_attachment(&mut self, value: Option<bool>) {
207 self.0 &= !Self::REPLY_ATTACH_MASK;
209 let bits = match value {
211 None => 0b00,
212 Some(false) => 0b01,
213 Some(true) => 0b10,
214 };
215 self.0 |= bits << Self::REPLY_ATTACH_SHIFT;
216 }
217
218 #[inline]
220 pub fn from_bools(mine: bool, pending: bool, failed: bool) -> Self {
221 let mut flags = Self::NONE;
222 flags.set_mine(mine);
223 flags.set_pending(pending);
224 flags.set_failed(failed);
225 flags
226 }
227
228 #[inline]
230 pub fn from_all(mine: bool, pending: bool, failed: bool, replied_to_has_attachment: Option<bool>) -> Self {
231 let mut flags = Self::from_bools(mine, pending, failed);
232 flags.set_replied_to_has_attachment(replied_to_has_attachment);
233 flags
234 }
235}
236
237use std::alloc::{alloc, dealloc, Layout};
242use std::marker::PhantomData;
243use std::ptr::NonNull;
244
245pub struct TinyVec<T> {
261 ptr: Option<NonNull<u8>>,
263 _marker: PhantomData<T>,
264}
265
266impl<T> TinyVec<T> {
267 #[inline]
269 pub const fn new() -> Self {
270 Self {
271 ptr: None,
272 _marker: PhantomData,
273 }
274 }
275
276 pub fn from_vec(vec: Vec<T>) -> Self {
278 if vec.is_empty() {
279 return Self::new();
280 }
281
282 let len = vec.len().min(255) as u8;
283
284 let (layout, items_offset) = Self::layout_for(len as usize);
286
287 unsafe {
288 let ptr = alloc(layout);
290 if ptr.is_null() {
291 std::alloc::handle_alloc_error(layout);
292 }
293
294 *ptr = len;
296
297 let items_ptr = ptr.add(items_offset) as *mut T;
299 for (i, item) in vec.into_iter().take(len as usize).enumerate() {
300 std::ptr::write(items_ptr.add(i), item);
301 }
302
303 Self {
304 ptr: NonNull::new(ptr),
305 _marker: PhantomData,
306 }
307 }
308 }
309
310 fn layout_for(len: usize) -> (Layout, usize) {
312 let header_layout = Layout::new::<u8>();
313 let items_layout = Layout::array::<T>(len).unwrap();
314 header_layout.extend(items_layout).unwrap()
315 }
316
317 #[inline]
319 pub fn len(&self) -> usize {
320 match self.ptr {
321 None => 0,
322 Some(ptr) => unsafe { *ptr.as_ptr() as usize },
323 }
324 }
325
326 #[inline]
327 pub fn is_empty(&self) -> bool {
328 self.ptr.is_none()
329 }
330
331 #[inline]
333 fn items_offset() -> usize {
334 let header_layout = Layout::new::<u8>();
335 let items_layout = Layout::new::<T>();
336 header_layout.extend(items_layout).map(|(_, offset)| offset).unwrap_or(1)
337 }
338
339 #[inline]
341 pub fn as_slice(&self) -> &[T] {
342 match self.ptr {
343 None => &[],
344 Some(ptr) => unsafe {
345 let base = ptr.as_ptr();
346 let len = *base as usize;
347 let items_ptr = base.add(Self::items_offset()) as *const T;
348 std::slice::from_raw_parts(items_ptr, len)
349 },
350 }
351 }
352
353 #[inline]
355 pub fn as_mut_slice(&mut self) -> &mut [T] {
356 match self.ptr {
357 None => &mut [],
358 Some(ptr) => unsafe {
359 let base = ptr.as_ptr();
360 let len = *base as usize;
361 let items_ptr = base.add(Self::items_offset()) as *mut T;
362 std::slice::from_raw_parts_mut(items_ptr, len)
363 },
364 }
365 }
366
367 #[inline]
369 pub fn iter(&self) -> std::slice::Iter<'_, T> {
370 self.as_slice().iter()
371 }
372
373 #[inline]
375 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, T> {
376 self.as_mut_slice().iter_mut()
377 }
378
379 pub fn to_vec(&self) -> Vec<T>
381 where
382 T: Clone,
383 {
384 self.as_slice().to_vec()
385 }
386
387 #[inline]
389 pub fn first(&self) -> Option<&T> {
390 self.as_slice().first()
391 }
392
393 #[inline]
395 pub fn last(&self) -> Option<&T> {
396 self.as_slice().last()
397 }
398
399 #[inline]
401 pub fn last_mut(&mut self) -> Option<&mut T> {
402 self.as_mut_slice().last_mut()
403 }
404
405 #[inline]
407 pub fn get(&self, index: usize) -> Option<&T> {
408 self.as_slice().get(index)
409 }
410
411 #[inline]
413 pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
414 self.as_mut_slice().get_mut(index)
415 }
416
417 pub fn push(&mut self, item: T)
419 where
420 T: Clone,
421 {
422 let mut vec = self.to_vec();
423 vec.push(item);
424 *self = Self::from_vec(vec);
425 }
426
427 pub fn retain<F>(&mut self, f: F)
429 where
430 T: Clone,
431 F: FnMut(&T) -> bool,
432 {
433 let mut vec = self.to_vec();
434 vec.retain(f);
435 *self = Self::from_vec(vec);
436 }
437
438 pub fn any<F>(&self, f: F) -> bool
440 where
441 F: FnMut(&T) -> bool,
442 {
443 self.as_slice().iter().any(f)
444 }
445}
446
447impl<T> std::ops::Index<usize> for TinyVec<T> {
449 type Output = T;
450
451 fn index(&self, index: usize) -> &Self::Output {
452 &self.as_slice()[index]
453 }
454}
455
456impl<T> std::ops::IndexMut<usize> for TinyVec<T> {
457 fn index_mut(&mut self, index: usize) -> &mut Self::Output {
458 &mut self.as_mut_slice()[index]
459 }
460}
461
462impl<'a, T> IntoIterator for &'a TinyVec<T> {
464 type Item = &'a T;
465 type IntoIter = std::slice::Iter<'a, T>;
466
467 fn into_iter(self) -> Self::IntoIter {
468 self.as_slice().iter()
469 }
470}
471
472impl<'a, T> IntoIterator for &'a mut TinyVec<T> {
474 type Item = &'a mut T;
475 type IntoIter = std::slice::IterMut<'a, T>;
476
477 fn into_iter(self) -> Self::IntoIter {
478 self.as_mut_slice().iter_mut()
479 }
480}
481
482impl<T> Default for TinyVec<T> {
483 fn default() -> Self {
484 Self::new()
485 }
486}
487
488impl<T: Clone> Clone for TinyVec<T> {
489 fn clone(&self) -> Self {
490 Self::from_vec(self.to_vec())
491 }
492}
493
494impl<T: std::fmt::Debug> std::fmt::Debug for TinyVec<T> {
495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496 f.debug_list().entries(self.as_slice()).finish()
497 }
498}
499
500impl<T> Drop for TinyVec<T> {
501 fn drop(&mut self) {
502 if let Some(ptr) = self.ptr {
503 unsafe {
504 let base = ptr.as_ptr();
505 let len = *base as usize;
506 let items_ptr = base.add(Self::items_offset()) as *mut T;
507
508 for i in 0..len {
510 std::ptr::drop_in_place(items_ptr.add(i));
511 }
512
513 let (layout, _) = Self::layout_for(len);
515 dealloc(base, layout);
516 }
517 }
518 }
519}
520
521unsafe impl<T: Send> Send for TinyVec<T> {}
523unsafe impl<T: Sync> Sync for TinyVec<T> {}
524
525#[derive(Clone, Debug)]
537pub struct CompactReaction {
538 pub id: [u8; 32],
540 pub author_idx: u16,
542 pub emoji: Box<str>,
544 pub emoji_url: Option<Box<str>>,
547}
548
549impl CompactReaction {
550 #[inline]
552 pub fn id_hex(&self) -> String {
553 bytes_to_hex_32(&self.id)
554 }
555
556 pub fn from_reaction(reaction: &Reaction, interner: &mut NpubInterner) -> Self {
558 Self {
559 id: hex_to_bytes_32(&reaction.id),
560 author_idx: interner.intern(&reaction.author_id),
561 emoji: reaction.emoji.clone().into_boxed_str(),
562 emoji_url: reaction.emoji_url.as_deref().map(|s| s.into()),
563 }
564 }
565
566 pub fn from_reaction_owned(reaction: Reaction, interner: &mut NpubInterner) -> Self {
568 Self {
569 id: hex_to_bytes_32(&reaction.id),
570 author_idx: interner.intern(&reaction.author_id),
571 emoji: reaction.emoji.into_boxed_str(),
572 emoji_url: reaction.emoji_url.map(|s| s.into_boxed_str()),
573 }
574 }
575
576 pub fn to_reaction(&self, parent_id: &[u8; 32], interner: &NpubInterner) -> Reaction {
581 Reaction {
582 id: self.id_hex(),
583 reference_id: bytes_to_hex_32(parent_id),
584 author_id: interner.resolve(self.author_idx)
585 .map(|s| s.to_string())
586 .unwrap_or_default(),
587 emoji: self.emoji.to_string(),
588 emoji_url: self.emoji_url.as_deref().map(|s| s.to_string()),
589 }
590 }
591}
592
593#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
599pub struct AttachmentFlags(u8);
600
601impl AttachmentFlags {
602 pub const NONE: Self = Self(0);
603 const DOWNLOADING: u8 = 0b0001;
604 const DOWNLOADED: u8 = 0b0010;
605 const SHORT_NONCE: u8 = 0b0100; #[inline]
608 pub fn is_downloading(self) -> bool { self.0 & Self::DOWNLOADING != 0 }
609 #[inline]
610 pub fn is_downloaded(self) -> bool { self.0 & Self::DOWNLOADED != 0 }
611 #[inline]
612 pub fn is_short_nonce(self) -> bool { self.0 & Self::SHORT_NONCE != 0 }
613
614 #[inline]
615 pub fn set_downloading(&mut self, value: bool) {
616 if value { self.0 |= Self::DOWNLOADING; } else { self.0 &= !Self::DOWNLOADING; }
617 }
618 #[inline]
619 pub fn set_downloaded(&mut self, value: bool) {
620 if value { self.0 |= Self::DOWNLOADED; } else { self.0 &= !Self::DOWNLOADED; }
621 }
622 #[inline]
623 pub fn set_short_nonce(&mut self, value: bool) {
624 if value { self.0 |= Self::SHORT_NONCE; } else { self.0 &= !Self::SHORT_NONCE; }
625 }
626
627 pub fn from_bools(downloading: bool, downloaded: bool) -> Self {
628 let mut flags = Self::NONE;
629 flags.set_downloading(downloading);
630 flags.set_downloaded(downloaded);
631 flags
632 }
633}
634
635#[derive(Clone, Debug)]
646pub struct CompactAttachment {
647 pub id: [u8; 32],
650 pub key: [u8; 32],
652 pub nonce: [u8; 16],
654 pub size: u64,
656 pub flags: AttachmentFlags,
658
659 pub extension: Box<str>,
662 pub url: Box<str>,
664 pub path: Box<str>,
666
667 pub img_meta: Option<Box<ImageMetadata>>,
670 pub group_id: Option<Box<[u8; 32]>>,
672 pub original_hash: Option<Box<[u8; 32]>>,
674 pub webxdc_topic: Option<Box<str>>,
676 pub fallback_urls: Option<Box<[Box<str>]>>,
678 pub name: Box<str>,
680}
681
682impl CompactAttachment {
683 #[inline]
685 pub fn downloaded(&self) -> bool { self.flags.is_downloaded() }
686 #[inline]
687 pub fn downloading(&self) -> bool { self.flags.is_downloading() }
688 #[inline]
689 pub fn set_downloaded(&mut self, value: bool) { self.flags.set_downloaded(value); }
690 #[inline]
691 pub fn set_downloading(&mut self, value: bool) { self.flags.set_downloading(value); }
692
693 #[inline]
695 pub fn id_eq(&self, hex_id: &str) -> bool {
696 self.id == hex_to_bytes_32(hex_id)
697 }
698
699 #[inline]
701 pub fn id_hex(&self) -> String {
702 bytes_to_hex_32(&self.id)
703 }
704
705 pub fn key_hex(&self) -> String {
707 if self.key == [0u8; 32] {
708 String::new()
709 } else {
710 bytes_to_hex_32(&self.key)
711 }
712 }
713
714 pub fn nonce_hex(&self) -> String {
716 if self.nonce == [0u8; 16] {
717 String::new()
718 } else if self.flags.is_short_nonce() {
719 bytes_to_hex_string(&self.nonce[..12])
721 } else {
722 bytes_to_hex_string(&self.nonce)
724 }
725 }
726
727 pub fn from_attachment(att: &Attachment) -> Self {
729 let is_short_nonce = att.nonce.len() == 24;
731 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
732 flags.set_short_nonce(is_short_nonce);
733
734 Self {
735 id: hex_to_bytes_32(&att.id),
736 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
737 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
738 size: att.size,
739 flags,
740 extension: att.extension.clone().into_boxed_str(),
741 url: att.url.clone().into_boxed_str(),
742 path: att.path.clone().into_boxed_str(),
743 img_meta: att.img_meta.clone().map(Box::new),
744 group_id: att.group_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
745 original_hash: att.original_hash.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
746 webxdc_topic: att.webxdc_topic.clone().map(|s| s.into_boxed_str()),
747 fallback_urls: (!att.fallback_urls.is_empty())
748 .then(|| att.fallback_urls.iter().map(|s| s.as_str().into()).collect()),
749 name: att.name.clone().into_boxed_str(),
750 }
751 }
752
753 pub fn from_attachment_owned(att: Attachment) -> Self {
755 let is_short_nonce = att.nonce.len() == 24;
757 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
758 flags.set_short_nonce(is_short_nonce);
759
760 Self {
761 id: hex_to_bytes_32(&att.id),
762 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
763 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
764 size: att.size,
765 flags,
766 extension: att.extension.into_boxed_str(),
767 url: att.url.into_boxed_str(),
768 path: att.path.into_boxed_str(),
769 img_meta: att.img_meta.map(Box::new),
770 group_id: att.group_id.map(|s| Box::new(hex_to_bytes_32(&s))),
771 original_hash: att.original_hash.map(|s| Box::new(hex_to_bytes_32(&s))),
772 webxdc_topic: att.webxdc_topic.map(|s| s.into_boxed_str()),
773 fallback_urls: (!att.fallback_urls.is_empty())
774 .then(|| att.fallback_urls.into_iter().map(|s| s.into_boxed_str()).collect()),
775 name: att.name.into_boxed_str(),
776 }
777 }
778
779 pub fn to_attachment(&self) -> Attachment {
781 Attachment {
782 id: self.id_hex(),
783 key: self.key_hex(),
784 nonce: self.nonce_hex(),
785 extension: self.extension.to_string(),
786 name: self.name.to_string(),
787 url: self.url.to_string(),
788 path: self.path.to_string(),
789 size: self.size,
790 img_meta: self.img_meta.as_ref().map(|b| (**b).clone()),
791 downloading: self.flags.is_downloading(),
792 downloaded: self.flags.is_downloaded(),
793 webxdc_topic: self.webxdc_topic.as_ref().map(|s| s.to_string()),
794 group_id: self.group_id.as_ref().map(|b| bytes_to_hex_32(b)),
795 original_hash: self.original_hash.as_ref().map(|b| bytes_to_hex_32(b)),
796 fallback_urls: self
797 .fallback_urls
798 .as_deref()
799 .map(|urls| urls.iter().map(|u| u.to_string()).collect())
800 .unwrap_or_default(),
801 }
802 }
803}
804
805#[inline]
809fn parse_nonce(hex: &str) -> [u8; 16] {
810 hex_to_bytes_16(hex)
811}
812
813#[derive(Clone, Debug, Default)]
825pub struct NpubInterner {
826 npubs: Vec<String>,
828 sorted: Vec<u16>,
830}
831
832pub const NO_NPUB: u16 = u16::MAX;
834
835impl NpubInterner {
836 pub fn new() -> Self {
837 Self {
838 npubs: Vec::new(),
839 sorted: Vec::new(),
840 }
841 }
842
843 pub fn with_capacity(capacity: usize) -> Self {
845 Self {
846 npubs: Vec::with_capacity(capacity),
847 sorted: Vec::with_capacity(capacity),
848 }
849 }
850
851 pub fn intern(&mut self, npub: &str) -> u16 {
856 let result = self.sorted.binary_search_by(|&idx| {
858 self.npubs[idx as usize].as_str().cmp(npub)
859 });
860
861 match result {
862 Ok(pos) => self.sorted[pos], Err(insert_pos) => {
864 if self.npubs.len() >= NO_NPUB as usize {
867 return NO_NPUB;
868 }
869 let new_idx = self.npubs.len() as u16;
871 self.npubs.push(npub.to_string());
872 self.sorted.insert(insert_pos, new_idx);
873 new_idx
874 }
875 }
876 }
877
878 #[inline]
880 pub fn intern_opt(&mut self, npub: Option<&str>) -> u16 {
881 match npub {
882 Some(s) if !s.is_empty() => self.intern(s),
883 _ => NO_NPUB,
884 }
885 }
886
887 pub fn lookup(&self, npub: &str) -> Option<u16> {
889 self.sorted.binary_search_by(|&idx| {
890 self.npubs[idx as usize].as_str().cmp(npub)
891 }).ok().map(|pos| self.sorted[pos])
892 }
893
894 #[inline]
898 pub fn resolve(&self, idx: u16) -> Option<&str> {
899 if idx == NO_NPUB {
900 return None;
901 }
902 self.npubs.get(idx as usize).map(|s| s.as_str())
903 }
904
905 #[inline]
907 pub fn len(&self) -> usize {
908 self.npubs.len()
909 }
910
911 #[inline]
912 pub fn is_empty(&self) -> bool {
913 self.npubs.is_empty()
914 }
915
916 pub fn memory_usage(&self) -> usize {
918 std::mem::size_of::<Self>()
919 + self.npubs.capacity() * std::mem::size_of::<String>()
920 + self.npubs.iter().map(|s| s.capacity()).sum::<usize>()
921 + self.sorted.capacity() * std::mem::size_of::<u16>()
922 }
923}
924
925#[derive(Clone, Debug)]
941pub struct CompactMessage {
942 pub id: [u8; 32],
944 pub at: u64,
946 pub expiration_secs: u32,
950 pub flags: MessageFlags,
952 pub npub_idx: u16,
954 pub replied_to: Option<Box<[u8; 32]>>,
956 pub replied_to_npub_idx: u16,
958 pub wrapper_id: Option<Box<[u8; 32]>>,
960
961 pub content: Box<str>,
964 pub replied_to_content: Option<Box<str>>,
966 pub attachments: TinyVec<CompactAttachment>,
968 pub reactions: TinyVec<CompactReaction>,
970 #[allow(clippy::box_collection)]
972 pub edit_history: Option<Box<Vec<EditEntry>>>,
973 pub preview_metadata: Option<Box<SiteMetadata>>,
975 #[allow(clippy::box_collection)]
978 pub emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
979 #[allow(clippy::box_collection)]
982 pub addressed_bots: Option<Box<Vec<u16>>>,
983}
984
985impl CompactMessage {
986 #[inline]
988 pub fn has_reply(&self) -> bool {
989 self.replied_to.is_some()
990 }
991
992 #[inline]
994 pub fn is_edited(&self) -> bool {
995 self.edit_history.is_some()
996 }
997
998 #[inline]
1000 pub fn id_hex(&self) -> String {
1001 decode_message_id(&self.id)
1002 }
1003
1004 #[inline]
1006 pub fn replied_to_hex(&self) -> String {
1007 match &self.replied_to {
1008 Some(id) => bytes_to_hex_32(id),
1009 None => String::new(),
1010 }
1011 }
1012
1013 #[inline]
1015 pub fn wrapper_id_hex(&self) -> Option<String> {
1016 self.wrapper_id.as_ref().map(|id| bytes_to_hex_32(id))
1017 }
1018
1019 #[inline]
1021 pub fn timestamp_ms(&self) -> u64 {
1022 timestamp_from_compact(self.at)
1023 }
1024
1025 pub fn apply_edit(&mut self, new_content: String, edited_at: u64, emoji_tags: Vec<crate::types::EmojiTag>) {
1031 if self.edit_history.is_none() {
1033 self.edit_history = Some(Box::new(vec![EditEntry {
1034 content: self.content.to_string(),
1035 edited_at: self.timestamp_ms(), }]));
1037 }
1038
1039 let mut is_latest = true;
1040 if let Some(ref mut history) = self.edit_history {
1041 if history.iter().any(|e| e.edited_at == edited_at) {
1043 return;
1044 }
1045
1046 history.push(EditEntry {
1048 content: new_content.clone(),
1049 edited_at,
1050 });
1051
1052 history.sort_by_key(|e| e.edited_at);
1054 is_latest = history.last().map(|e| e.edited_at == edited_at).unwrap_or(true);
1055 }
1056
1057 if is_latest {
1059 self.content = new_content.into_boxed_str();
1060 self.emoji_tags = if emoji_tags.is_empty() { None } else { Some(Box::new(emoji_tags)) };
1061 }
1062 }
1063
1064 #[inline]
1066 pub fn replied_to_has_attachment(&self) -> Option<bool> {
1067 self.flags.replied_to_has_attachment()
1068 }
1069
1070 pub fn add_reaction(&mut self, reaction: Reaction, interner: &mut NpubInterner) -> bool {
1073 let reaction_id = hex_to_bytes_32(&reaction.id);
1075
1076 if self.reactions.iter().any(|r| r.id == reaction_id) {
1078 return false;
1079 }
1080
1081 let compact = CompactReaction::from_reaction_owned(reaction, interner);
1083 let mut reactions = self.reactions.to_vec();
1084 reactions.push(compact);
1085 self.reactions = TinyVec::from_vec(reactions);
1086 true
1087 }
1088
1089 pub fn remove_reaction(&mut self, reaction_id: &str) -> bool {
1091 let target = hex_to_bytes_32(reaction_id);
1092 if !self.reactions.iter().any(|r| r.id == target) {
1093 return false;
1094 }
1095 let mut reactions = self.reactions.to_vec();
1096 reactions.retain(|r| r.id != target);
1097 self.reactions = TinyVec::from_vec(reactions);
1098 true
1099 }
1100
1101 #[inline]
1103 pub fn is_mine(&self) -> bool { self.flags.is_mine() }
1104 #[inline]
1105 pub fn is_pending(&self) -> bool { self.flags.is_pending() }
1106 #[inline]
1107 pub fn is_failed(&self) -> bool { self.flags.is_failed() }
1108
1109 #[inline]
1111 pub fn set_pending(&mut self, value: bool) { self.flags.set_pending(value); }
1112 #[inline]
1113 pub fn set_failed(&mut self, value: bool) { self.flags.set_failed(value); }
1114 #[inline]
1115 pub fn set_mine(&mut self, value: bool) { self.flags.set_mine(value); }
1116}
1117
1118#[derive(Clone, Debug, Default)]
1127pub struct CompactMessageVec {
1128 messages: Vec<CompactMessage>,
1130 id_index: Vec<([u8; 32], u32)>,
1132}
1133
1134impl CompactMessageVec {
1135 pub fn new() -> Self {
1136 Self {
1137 messages: Vec::new(),
1138 id_index: Vec::new(),
1139 }
1140 }
1141
1142 pub fn with_capacity(capacity: usize) -> Self {
1143 Self {
1144 messages: Vec::with_capacity(capacity),
1145 id_index: Vec::with_capacity(capacity),
1146 }
1147 }
1148
1149 #[inline]
1151 pub fn len(&self) -> usize {
1152 self.messages.len()
1153 }
1154
1155 #[inline]
1156 pub fn is_empty(&self) -> bool {
1157 self.messages.is_empty()
1158 }
1159
1160 #[inline]
1162 pub fn messages(&self) -> &[CompactMessage] {
1163 &self.messages
1164 }
1165
1166 #[inline]
1168 pub fn messages_mut(&mut self) -> &mut Vec<CompactMessage> {
1169 &mut self.messages
1170 }
1171
1172 #[inline]
1174 pub fn iter(&self) -> std::slice::Iter<'_, CompactMessage> {
1175 self.messages.iter()
1176 }
1177
1178 #[inline]
1180 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, CompactMessage> {
1181 self.messages.iter_mut()
1182 }
1183
1184 #[inline]
1186 pub fn last(&self) -> Option<&CompactMessage> {
1187 self.messages.last()
1188 }
1189
1190 #[inline]
1192 pub fn last_timestamp(&self) -> Option<u64> {
1193 self.messages.last().map(|m| timestamp_from_compact(m.at))
1194 }
1195
1196 #[inline]
1198 pub fn first(&self) -> Option<&CompactMessage> {
1199 self.messages.first()
1200 }
1201
1202 pub fn find_by_id(&self, id: &[u8; 32]) -> Option<&CompactMessage> {
1204 let pos = self.id_index
1205 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1206 .ok()?;
1207 let msg_pos = self.id_index[pos].1 as usize;
1208 self.messages.get(msg_pos)
1209 }
1210
1211 pub fn find_by_id_mut(&mut self, id: &[u8; 32]) -> Option<&mut CompactMessage> {
1213 let pos = self.id_index
1214 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1215 .ok()?;
1216 let msg_pos = self.id_index[pos].1 as usize;
1217 self.messages.get_mut(msg_pos)
1218 }
1219
1220 pub fn find_by_hex_id(&self, id_str: &str) -> Option<&CompactMessage> {
1222 if id_str.is_empty() {
1223 return None;
1224 }
1225 let id = encode_message_id(id_str);
1226 self.find_by_id(&id)
1227 }
1228
1229 pub fn find_by_hex_id_mut(&mut self, id_str: &str) -> Option<&mut CompactMessage> {
1231 if id_str.is_empty() {
1232 return None;
1233 }
1234 let id = encode_message_id(id_str);
1235 self.find_by_id_mut(&id)
1236 }
1237
1238 pub fn contains_id(&self, id: &[u8; 32]) -> bool {
1240 self.id_index
1241 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1242 .is_ok()
1243 }
1244
1245 pub fn remove_by_hex_id(&mut self, id_str: &str) -> bool {
1247 if id_str.is_empty() {
1248 return false;
1249 }
1250 let id = encode_message_id(id_str);
1251 let idx_pos = match self.id_index.binary_search_by(|(idx_id, _)| idx_id.cmp(&id)) {
1253 Ok(pos) => pos,
1254 Err(_) => return false,
1255 };
1256 let msg_pos = self.id_index[idx_pos].1 as usize;
1257 self.messages.remove(msg_pos);
1259 self.rebuild_index();
1261 true
1262 }
1263
1264 pub fn contains_hex_id(&self, id_str: &str) -> bool {
1266 if id_str.is_empty() {
1267 return false;
1268 }
1269 let id = encode_message_id(id_str);
1270 self.contains_id(&id)
1271 }
1272
1273 pub fn insert(&mut self, msg: CompactMessage) -> bool {
1279 if self.contains_id(&msg.id) {
1281 return false;
1282 }
1283
1284 let msg_id = msg.id;
1285
1286 if self.messages.last().is_none_or(|last| msg.at >= last.at) {
1289 let msg_pos = self.messages.len() as u32;
1290 self.messages.push(msg);
1291
1292 let idx_pos = self.id_index
1295 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1296 .unwrap_err();
1297 self.id_index.insert(idx_pos, (msg_id, msg_pos));
1298
1299 return true;
1300 }
1301
1302 let msg_pos = match self.messages.binary_search_by(|m| m.at.cmp(&msg.at)) {
1305 Ok(pos) => pos,
1306 Err(pos) => pos,
1307 };
1308
1309 for (_, pos) in &mut self.id_index {
1311 if *pos >= msg_pos as u32 {
1312 *pos += 1;
1313 }
1314 }
1315
1316 self.messages.insert(msg_pos, msg);
1318
1319 let idx_pos = self.id_index
1321 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1322 .unwrap_err();
1323 self.id_index.insert(idx_pos, (msg_id, msg_pos as u32));
1324
1325 true
1326 }
1327
1328 pub fn rebuild_index(&mut self) {
1330 self.id_index.clear();
1331 self.id_index.reserve(self.messages.len());
1332 for (pos, msg) in self.messages.iter().enumerate() {
1333 self.id_index.push((msg.id, pos as u32));
1334 }
1335 self.id_index.sort_by(|(a, _), (b, _)| a.cmp(b));
1336 }
1337
1338 pub fn insert_batch(&mut self, messages: impl IntoIterator<Item = CompactMessage>) -> usize {
1347 let messages: Vec<_> = messages.into_iter().collect();
1348 if messages.is_empty() {
1349 return 0;
1350 }
1351
1352 let mut to_add: Vec<CompactMessage> = Vec::with_capacity(messages.len());
1354 for msg in messages {
1355 if !self.contains_id(&msg.id) {
1356 to_add.push(msg);
1357 }
1358 }
1359
1360 if to_add.is_empty() {
1361 return 0;
1362 }
1363
1364 let added = to_add.len();
1365
1366 let our_first = self.messages.first().map(|m| m.at);
1368 let our_last = self.messages.last().map(|m| m.at);
1369 let their_min = to_add.iter().map(|m| m.at).min().unwrap();
1370 let their_max = to_add.iter().map(|m| m.at).max().unwrap();
1371
1372 if self.messages.is_empty() {
1373 self.messages = to_add;
1375 self.messages.sort_by_key(|m| m.at);
1376 self.rebuild_index();
1377 } else if their_min >= our_last.unwrap() {
1378 to_add.sort_by_key(|m| m.at);
1380 let base_pos = self.messages.len() as u32;
1381 let mut new_index_entries: Vec<_> = to_add.iter()
1384 .enumerate()
1385 .map(|(i, msg)| (msg.id, base_pos + i as u32))
1386 .collect();
1387 self.messages.extend(to_add);
1388 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1389
1390 let old_index = std::mem::take(&mut self.id_index);
1393 self.id_index.reserve(old_index.len() + new_index_entries.len());
1394 let mut old_iter = old_index.into_iter().peekable();
1395 let mut new_iter = new_index_entries.into_iter().peekable();
1396 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1397 match (old_iter.peek(), new_iter.peek()) {
1398 (Some((old_id, _)), Some((new_id, _))) => {
1399 if old_id < new_id {
1400 self.id_index.push(old_iter.next().unwrap());
1401 } else {
1402 self.id_index.push(new_iter.next().unwrap());
1403 }
1404 }
1405 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1406 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1407 (None, None) => break,
1408 }
1409 }
1410 } else if their_max <= our_first.unwrap() {
1411 to_add.sort_by_key(|m| m.at);
1413 let prepend_count = to_add.len();
1414
1415 for (_, pos) in &mut self.id_index {
1417 *pos += prepend_count as u32;
1418 }
1419
1420 let mut new_index_entries: Vec<_> = to_add.iter()
1422 .enumerate()
1423 .map(|(i, msg)| (msg.id, i as u32))
1424 .collect();
1425 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1426
1427 let old_index = std::mem::take(&mut self.id_index);
1429 self.id_index.reserve(old_index.len() + new_index_entries.len());
1430
1431 let mut old_iter = old_index.into_iter().peekable();
1432 let mut new_iter = new_index_entries.into_iter().peekable();
1433
1434 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1435 match (old_iter.peek(), new_iter.peek()) {
1436 (Some((old_id, _)), Some((new_id, _))) => {
1437 if old_id < new_id {
1438 self.id_index.push(old_iter.next().unwrap());
1439 } else {
1440 self.id_index.push(new_iter.next().unwrap());
1441 }
1442 }
1443 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1444 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1445 (None, None) => break,
1446 }
1447 }
1448
1449 let mut new_messages = to_add;
1451 new_messages.append(&mut self.messages);
1452 self.messages = new_messages;
1453 } else {
1454 self.messages.extend(to_add);
1456 self.messages.sort_by_key(|m| m.at);
1457 self.rebuild_index();
1458 }
1459
1460 added
1461 }
1462
1463 pub fn memory_usage(&self) -> usize {
1465 std::mem::size_of::<Self>()
1466 + self.messages.capacity() * std::mem::size_of::<CompactMessage>()
1467 + self.id_index.capacity() * std::mem::size_of::<([u8; 32], u32)>()
1468 }
1470
1471 pub fn drain(&mut self, range: std::ops::Range<usize>) -> std::vec::Drain<'_, CompactMessage> {
1473 let drain = self.messages.drain(range);
1474 drain
1476 }
1477
1478 pub fn sort_by_key<K, F>(&mut self, f: F)
1480 where
1481 F: FnMut(&CompactMessage) -> K,
1482 K: Ord,
1483 {
1484 self.messages.sort_by_key(f);
1485 self.rebuild_index();
1486 }
1487
1488 pub fn clear(&mut self) {
1490 self.messages.clear();
1491 self.id_index.clear();
1492 }
1493}
1494
1495use crate::types::Message;
1500
1501impl CompactMessage {
1502 pub fn from_message(msg: &Message, interner: &mut NpubInterner) -> Self {
1504 Self {
1505 id: encode_message_id(&msg.id),
1506 at: timestamp_to_compact(msg.at),
1507 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1508 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1509 replied_to: if msg.replied_to.is_empty() {
1511 None
1512 } else {
1513 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1514 },
1515 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1516 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1518 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1519 content: msg.content.clone().into_boxed_str(),
1521 replied_to_content: msg.replied_to_content.as_ref().map(|s| s.clone().into_boxed_str()),
1522 attachments: TinyVec::from_vec(
1524 msg.attachments.iter()
1525 .map(CompactAttachment::from_attachment)
1526 .collect()
1527 ),
1528 reactions: TinyVec::from_vec(
1530 msg.reactions.iter()
1531 .map(|r| CompactReaction::from_reaction(r, interner))
1532 .collect()
1533 ),
1534 edit_history: msg.edit_history.clone().map(Box::new),
1536 preview_metadata: msg.preview_metadata.clone().map(Box::new),
1537 emoji_tags: if msg.emoji_tags.is_empty() {
1538 None
1539 } else {
1540 Some(Box::new(msg.emoji_tags.clone()))
1541 },
1542 addressed_bots: if msg.addressed_bots.is_empty() {
1543 None
1544 } else {
1545 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1546 },
1547 }
1548 }
1549
1550 pub fn from_message_owned(msg: Message, interner: &mut NpubInterner) -> Self {
1555 Self {
1556 id: encode_message_id(&msg.id),
1557 at: timestamp_to_compact(msg.at),
1558 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1559 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1560 replied_to: if msg.replied_to.is_empty() {
1562 None
1563 } else {
1564 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1565 },
1566 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1567 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1569 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1570 content: msg.content.into_boxed_str(),
1572 replied_to_content: msg.replied_to_content.map(|s| s.into_boxed_str()),
1573 attachments: TinyVec::from_vec(
1575 msg.attachments.into_iter()
1576 .map(CompactAttachment::from_attachment_owned)
1577 .collect()
1578 ),
1579 reactions: TinyVec::from_vec(
1581 msg.reactions.into_iter()
1582 .map(|r| CompactReaction::from_reaction_owned(r, interner))
1583 .collect()
1584 ),
1585 edit_history: msg.edit_history.map(Box::new),
1587 preview_metadata: msg.preview_metadata.map(Box::new),
1588 emoji_tags: if msg.emoji_tags.is_empty() {
1589 None
1590 } else {
1591 Some(Box::new(msg.emoji_tags))
1592 },
1593 addressed_bots: if msg.addressed_bots.is_empty() {
1594 None
1595 } else {
1596 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1597 },
1598 }
1599 }
1600
1601 pub fn to_message(&self, interner: &NpubInterner) -> Message {
1603 Message {
1604 id: self.id_hex(),
1605 at: self.timestamp_ms(), expiration: if self.expiration_secs == 0 { None } else { Some(self.expiration_secs as u64) },
1607 mine: self.flags.is_mine(),
1608 pending: self.flags.is_pending(),
1609 failed: self.flags.is_failed(),
1610 edited: self.is_edited(),
1611 npub: interner.resolve(self.npub_idx).map(|s| s.to_string()),
1612 replied_to: self.replied_to_hex(),
1613 replied_to_content: self.replied_to_content.as_ref().map(|s| s.to_string()),
1614 replied_to_npub: interner.resolve(self.replied_to_npub_idx).map(|s| s.to_string()),
1615 replied_to_has_attachment: self.flags.replied_to_has_attachment(),
1616 replied_to_attachment_extension: None,
1619 wrapper_event_id: self.wrapper_id_hex(),
1620 content: self.content.to_string(),
1621 attachments: self.attachments.iter()
1623 .map(|a| a.to_attachment())
1624 .collect(),
1625 reactions: self.reactions.iter()
1627 .map(|r| r.to_reaction(&self.id, interner))
1628 .collect(),
1629 edit_history: self.edit_history.as_ref().map(|b| (**b).clone()),
1631 preview_metadata: self.preview_metadata.as_ref().map(|b| (**b).clone()),
1632 emoji_tags: self.emoji_tags.as_ref().map(|b| (**b).clone()).unwrap_or_default(),
1633 addressed_bots: self
1634 .addressed_bots
1635 .as_ref()
1636 .map(|b| b.iter().filter_map(|&i| interner.resolve(i).map(|s| s.to_string())).collect())
1637 .unwrap_or_default(),
1638 }
1639 }
1640}
1641
1642#[cfg(test)]
1643mod tests {
1644 use super::*;
1645
1646 #[test]
1650 fn real_id_starting_with_marker_byte_roundtrips() {
1651 let id = "01b95c05179c6d6abbc60b9a35198fdee6e0ce5b80a0b7ac8d465d81d038a73d";
1652 let encoded = encode_message_id(id);
1653 assert_eq!(decode_message_id(&encoded), id);
1654 }
1655
1656 #[test]
1657 fn pending_id_roundtrips() {
1658 let id = "pending-306878031314";
1659 let encoded = encode_message_id(id);
1660 assert_eq!(encoded[0], PENDING_ID_MARKER);
1661 assert_eq!(decode_message_id(&encoded), id);
1662 }
1663
1664 #[test]
1665 fn marker_byte_without_sentinel_decodes_as_hex() {
1666 let mut bytes = [0xABu8; 32];
1668 bytes[0] = PENDING_ID_MARKER;
1669 let decoded = decode_message_id(&bytes);
1670 assert!(!decoded.starts_with("pending-"));
1671 assert_eq!(decoded.len(), 64);
1672 }
1673
1674 #[test]
1675 fn test_message_flags() {
1676 let mut flags = MessageFlags::NONE;
1677 assert!(!flags.is_mine());
1678 assert!(!flags.is_pending());
1679 assert!(!flags.is_failed());
1680
1681 flags.set_mine(true);
1682 assert!(flags.is_mine());
1683
1684 flags.set_pending(true);
1685 assert!(flags.is_pending());
1686 assert!(flags.is_mine()); flags.set_mine(false);
1689 assert!(!flags.is_mine());
1690 assert!(flags.is_pending()); }
1692
1693 #[test]
1694 fn test_npub_interner() {
1695 let mut interner = NpubInterner::new();
1696
1697 let idx1 = interner.intern("npub1alice");
1698 let idx2 = interner.intern("npub1bob");
1699 let idx3 = interner.intern("npub1alice"); assert_eq!(idx1, idx3); assert_ne!(idx1, idx2);
1703
1704 assert_eq!(interner.resolve(idx1), Some("npub1alice"));
1705 assert_eq!(interner.resolve(idx2), Some("npub1bob"));
1706 assert_eq!(interner.resolve(NO_NPUB), None);
1707 }
1708
1709 #[test]
1710 fn test_compact_message_vec_insert_and_find() {
1711 let mut vec = CompactMessageVec::new();
1712 let mut interner = NpubInterner::new();
1713
1714 let msg1 = CompactMessage {
1715 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000001"),
1716 at: 1000,
1717 expiration_secs: 0,
1718 flags: MessageFlags::NONE,
1719 npub_idx: interner.intern("npub1test"),
1720 replied_to: None,
1721 replied_to_npub_idx: NO_NPUB,
1722 wrapper_id: None,
1723 content: "First message".to_string().into_boxed_str(),
1724 replied_to_content: None,
1725 attachments: TinyVec::new(),
1726 reactions: TinyVec::new(),
1727 edit_history: None,
1728 preview_metadata: None, emoji_tags: None,
1730 addressed_bots: None,
1731 };
1732
1733 let msg2 = CompactMessage {
1734 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000002"),
1735 at: 2000,
1736 expiration_secs: 0,
1737 flags: MessageFlags::MINE,
1738 npub_idx: interner.intern("npub1me"),
1739 replied_to: None,
1740 replied_to_npub_idx: NO_NPUB,
1741 wrapper_id: None,
1742 content: "Second message".to_string().into_boxed_str(),
1743 replied_to_content: None,
1744 attachments: TinyVec::new(),
1745 reactions: TinyVec::new(),
1746 edit_history: None,
1747 preview_metadata: None, emoji_tags: None,
1749 addressed_bots: None,
1750 };
1751
1752 assert!(vec.insert(msg1));
1753 assert!(vec.insert(msg2));
1754 assert_eq!(vec.len(), 2);
1755
1756 let found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000001");
1758 assert!(found.is_some());
1759 assert_eq!(&*found.unwrap().content, "First message");
1760
1761 let not_found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000099");
1763 assert!(not_found.is_none());
1764 }
1765
1766 #[test]
1767 fn test_duplicate_insert_rejected() {
1768 let mut vec = CompactMessageVec::new();
1769
1770 let msg = CompactMessage {
1771 id: hex_to_bytes_32("abcd000000000000000000000000000000000000000000000000000000000000"),
1772 at: 1000,
1773 expiration_secs: 0,
1774 flags: MessageFlags::NONE,
1775 npub_idx: NO_NPUB,
1776 replied_to: None,
1777 replied_to_npub_idx: NO_NPUB,
1778 wrapper_id: None,
1779 content: "Test".to_string().into_boxed_str(),
1780 replied_to_content: 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(msg.clone()));
1790 assert!(!vec.insert(msg)); assert_eq!(vec.len(), 1);
1792 }
1793
1794 #[test]
1796 fn benchmark_compact_vs_message() {
1797 use std::time::Instant;
1798
1799 const NUM_MESSAGES: usize = 10_000;
1800 const NUM_UNIQUE_USERS: usize = 50; println!("\n========================================");
1803 println!(" COMPACT MESSAGE BENCHMARK");
1804 println!(" {} messages, {} unique users", NUM_MESSAGES, NUM_UNIQUE_USERS);
1805 println!("========================================\n");
1806
1807 let users: Vec<String> = (0..NUM_UNIQUE_USERS)
1809 .map(|i| format!("npub1{:0>62}", i))
1810 .collect();
1811
1812 let messages: Vec<Message> = (0..NUM_MESSAGES)
1814 .map(|i| {
1815 let user_idx = i % NUM_UNIQUE_USERS;
1816 Message {
1817 expiration: None,
1818 id: format!("{:0>64x}", i),
1819 at: 1700000000000 + (i as u64 * 1000),
1820 mine: user_idx == 0,
1821 pending: false,
1822 failed: false,
1823 edited: false,
1824 npub: Some(users[user_idx].clone()),
1825 replied_to: if i > 0 && i % 5 == 0 {
1826 format!("{:0>64x}", i - 1)
1827 } else {
1828 String::new()
1829 },
1830 replied_to_content: if i > 0 && i % 5 == 0 {
1831 Some("Previous message content".to_string())
1832 } else {
1833 None
1834 },
1835 replied_to_npub: if i > 0 && i % 5 == 0 {
1836 Some(users[(i - 1) % NUM_UNIQUE_USERS].clone())
1837 } else {
1838 None
1839 },
1840 replied_to_has_attachment: None,
1841 replied_to_attachment_extension: None,
1842 wrapper_event_id: Some(format!("{:0>64x}", i + 1000000)),
1843 content: format!("This is message number {} with some typical content length.", i),
1844 attachments: vec![],
1845 reactions: vec![],
1846 edit_history: None,
1847 preview_metadata: None,
1848 emoji_tags: Vec::new(),
1849 addressed_bots: Vec::new(),
1850 }
1851 })
1852 .collect();
1853
1854 println!("--- STRUCT SIZES ---");
1856 println!(" Message struct: {} bytes", std::mem::size_of::<Message>());
1857 println!(" CompactMessage struct: {} bytes", std::mem::size_of::<CompactMessage>());
1858 println!(" Savings per struct: {} bytes ({:.1}%)",
1859 std::mem::size_of::<Message>().saturating_sub(std::mem::size_of::<CompactMessage>()),
1860 (1.0 - std::mem::size_of::<CompactMessage>() as f64 / std::mem::size_of::<Message>() as f64) * 100.0
1861 );
1862 println!();
1863
1864 let msg_heap_estimate: usize = messages.iter().map(|m| {
1866 m.id.capacity()
1867 + m.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1868 + m.replied_to.capacity()
1869 + m.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
1870 + m.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1871 + m.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
1872 + m.content.capacity()
1873 }).sum();
1874 let msg_total = messages.len() * std::mem::size_of::<Message>() + msg_heap_estimate;
1875
1876 println!("--- INSERT BENCHMARK ---");
1878
1879 let mut interner = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1881 let mut compact_vec = CompactMessageVec::with_capacity(NUM_MESSAGES);
1882
1883 let insert_start = Instant::now();
1884 for msg in &messages {
1885 let compact = CompactMessage::from_message(msg, &mut interner);
1886 compact_vec.insert(compact);
1887 }
1888 let insert_elapsed = insert_start.elapsed();
1889
1890 println!(" Sequential insert (optimized append path):");
1891 println!(" {} messages in {:?}", NUM_MESSAGES, insert_elapsed);
1892 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / insert_elapsed.as_secs_f64());
1893 println!(" Per message: {:.3} us ({} ns)",
1894 insert_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1895 insert_elapsed.as_nanos() / NUM_MESSAGES as u128);
1896 println!();
1897
1898 let mut interner2 = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1900 let mut compact_vec2 = CompactMessageVec::with_capacity(NUM_MESSAGES);
1901
1902 let batch_start = Instant::now();
1903 let compact_messages: Vec<_> = messages.iter()
1904 .map(|msg| CompactMessage::from_message(msg, &mut interner2))
1905 .collect();
1906 let batch_added = compact_vec2.insert_batch(compact_messages);
1907 let batch_elapsed = batch_start.elapsed();
1908
1909 println!(" Batch insert (pagination/history load):");
1910 println!(" {} messages in {:?}", batch_added, batch_elapsed);
1911 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / batch_elapsed.as_secs_f64());
1912 println!(" Per message: {:.3} us ({} ns)",
1913 batch_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1914 batch_elapsed.as_nanos() / NUM_MESSAGES as u128);
1915 println!();
1916
1917 println!("--- MEMORY COMPARISON ---");
1919 let compact_heap_estimate: usize = compact_vec.iter().map(|m| {
1920 m.content.len() + m.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
1922 + m.attachments.len() * std::mem::size_of::<Attachment>() + if m.attachments.is_empty() { 0 } else { 1 }
1923 + m.reactions.len() * std::mem::size_of::<Reaction>() + if m.reactions.is_empty() { 0 } else { 1 }
1924 }).sum();
1925 let compact_struct_mem = compact_vec.len() * std::mem::size_of::<CompactMessage>();
1926 let compact_index_mem = compact_vec.len() * std::mem::size_of::<([u8; 32], u32)>();
1927 let interner_mem = interner.memory_usage();
1928 let compact_total = compact_struct_mem + compact_heap_estimate + compact_index_mem + interner_mem;
1929
1930 println!(" Regular Message storage:");
1931 println!(" Struct memory: {:>10} bytes", messages.len() * std::mem::size_of::<Message>());
1932 println!(" Heap (strings): {:>10} bytes", msg_heap_estimate);
1933 println!(" TOTAL: {:>10} bytes ({:.2} MB)", msg_total, msg_total as f64 / 1_000_000.0);
1934 println!();
1935 println!(" CompactMessage storage:");
1936 println!(" Struct memory: {:>10} bytes", compact_struct_mem);
1937 println!(" Heap (strings): {:>10} bytes", compact_heap_estimate);
1938 println!(" ID index: {:>10} bytes", compact_index_mem);
1939 println!(" Interner: {:>10} bytes ({} unique npubs)", interner_mem, interner.len());
1940 println!(" TOTAL: {:>10} bytes ({:.2} MB)", compact_total, compact_total as f64 / 1_000_000.0);
1941 println!();
1942 println!(" SAVINGS: {} bytes ({:.1}%)",
1943 msg_total.saturating_sub(compact_total),
1944 (1.0 - compact_total as f64 / msg_total as f64) * 100.0
1945 );
1946 println!(" Per message: {} -> {} bytes (avg)",
1947 msg_total / NUM_MESSAGES,
1948 compact_total / NUM_MESSAGES
1949 );
1950 println!();
1951
1952 println!("--- LOOKUP BENCHMARK ---");
1954
1955 let lookup_ids: Vec<String> = (0..1000)
1957 .map(|i| format!("{:0>64x}", i * 10)) .collect();
1959
1960 let lookup_start = Instant::now();
1962 let mut found_count = 0;
1963 for _ in 0..100 { for id in &lookup_ids {
1965 if compact_vec.find_by_hex_id(id).is_some() {
1966 found_count += 1;
1967 }
1968 }
1969 }
1970 let lookup_elapsed = lookup_start.elapsed();
1971 let total_lookups = 100 * lookup_ids.len();
1972
1973 println!(" Binary search (CompactMessageVec):");
1974 println!(" {} lookups in {:?}", total_lookups, lookup_elapsed);
1975 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / lookup_elapsed.as_secs_f64());
1976 println!(" Per lookup: {:.2} us", lookup_elapsed.as_micros() as f64 / total_lookups as f64);
1977 println!(" Found: {} / {}", found_count, total_lookups);
1978 println!();
1979
1980 let linear_start = Instant::now();
1982 let mut linear_found = 0;
1983 for _ in 0..100 {
1984 for id in &lookup_ids {
1985 if messages.iter().find(|m| &m.id == id).is_some() {
1986 linear_found += 1;
1987 }
1988 }
1989 }
1990 let linear_elapsed = linear_start.elapsed();
1991
1992 println!(" Linear search (Vec<Message>):");
1993 println!(" {} lookups in {:?}", total_lookups, linear_elapsed);
1994 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / linear_elapsed.as_secs_f64());
1995 println!(" Per lookup: {:.2} us", linear_elapsed.as_micros() as f64 / total_lookups as f64);
1996 println!();
1997
1998 let speedup = linear_elapsed.as_nanos() as f64 / lookup_elapsed.as_nanos() as f64;
1999 println!(" SPEEDUP: {:.1}x faster with binary search!", speedup);
2000 println!();
2001
2002 println!("--- INTERNER EFFICIENCY ---");
2004 let npub_string_size = 63 + 1; let naive_npub_mem = NUM_MESSAGES * npub_string_size * 2; let actual_npub_mem = interner_mem;
2007 println!(" Naive (every msg stores npubs): {} bytes", naive_npub_mem);
2008 println!(" Interned ({} unique): {} bytes", interner.len(), actual_npub_mem);
2009 println!(" SAVINGS: {} bytes ({:.1}%)",
2010 naive_npub_mem.saturating_sub(actual_npub_mem),
2011 (1.0 - actual_npub_mem as f64 / naive_npub_mem as f64) * 100.0
2012 );
2013 println!();
2014
2015 println!("========================================");
2016 println!(" BENCHMARK COMPLETE");
2017 println!("========================================\n");
2018
2019 assert_eq!(compact_vec.len(), NUM_MESSAGES);
2021 assert_eq!(interner.len(), NUM_UNIQUE_USERS);
2022 assert_eq!(found_count, linear_found);
2023 }
2024
2025 #[test]
2032 fn benchmark_profile_lookup() {
2033 use std::time::Instant;
2034 use std::hint::black_box;
2035
2036 const NUM_PROFILES: usize = 60;
2037 const NUM_LOOKUPS: usize = 100_000;
2038
2039 println!("\n========================================");
2040 println!(" PROFILE LOOKUP BENCHMARK");
2041 println!(" {} profiles, {} lookups each method", NUM_PROFILES, NUM_LOOKUPS);
2042 println!("========================================\n");
2043
2044 let npubs: Vec<String> = (0..NUM_PROFILES)
2046 .map(|i| format!("npub1{:0>58}", format!("{:x}", i * 7919 + 1000))) .collect();
2048
2049 let old_ids: Vec<String> = npubs.iter().rev().cloned().collect(); let mut sorted_ids: Vec<String> = npubs.clone();
2054 sorted_ids.sort();
2055
2056 let mut interner = NpubInterner::new();
2058 let mut profiles: Vec<crate::profile::Profile> = npubs.iter().map(|npub| {
2059 let mut p = crate::profile::Profile::new();
2060 p.id = interner.intern(npub);
2061 p
2062 }).collect();
2063 profiles.sort_by(|a, b| a.id.cmp(&b.id));
2064
2065 let lookup_targets: Vec<&str> = (0..NUM_LOOKUPS)
2067 .map(|i| npubs[i % NUM_PROFILES].as_str())
2068 .collect();
2069
2070 let handle_targets: Vec<u16> = lookup_targets.iter()
2072 .map(|&npub| interner.lookup(npub).unwrap())
2073 .collect();
2074
2075 let start = Instant::now();
2077 let mut found = 0u64;
2078 for &target in &lookup_targets {
2079 if old_ids.iter().any(|id| id == target) {
2080 found += 1;
2081 }
2082 }
2083 let linear_elapsed = start.elapsed();
2084 assert_eq!(found, NUM_LOOKUPS as u64);
2085
2086 let start = Instant::now();
2088 found = 0;
2089 for &target in &lookup_targets {
2090 if sorted_ids.binary_search_by(|id| id.as_str().cmp(target)).is_ok() {
2091 found += 1;
2092 }
2093 }
2094 let string_bs_elapsed = start.elapsed();
2095 assert_eq!(found, NUM_LOOKUPS as u64);
2096
2097 let start = Instant::now();
2099 found = 0;
2100 for &handle in &handle_targets {
2101 if profiles.binary_search_by(|p| p.id.cmp(black_box(&handle))).is_ok() {
2102 found += 1;
2103 }
2104 }
2105 let direct_elapsed = start.elapsed();
2106 assert_eq!(found, NUM_LOOKUPS as u64);
2107
2108 println!("--- LOOKUP METHODS ---");
2110 println!(" 1. Linear scan (old id: String):");
2111 println!(" {:?} total, {:.0} ns/lookup",
2112 linear_elapsed,
2113 linear_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2114 println!();
2115 println!(" 2. String binary search (intermediate):");
2116 println!(" {:?} total, {:.0} ns/lookup",
2117 string_bs_elapsed,
2118 string_bs_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2119 println!(" vs linear: {:.1}x faster",
2120 linear_elapsed.as_nanos() as f64 / string_bs_elapsed.as_nanos() as f64);
2121 println!();
2122 println!(" 3. Direct u16 handle lookup (current id: u16):");
2123 println!(" {:?} total, {:.0} ns/lookup",
2124 direct_elapsed,
2125 direct_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2126 println!(" vs linear: {:.1}x faster",
2127 linear_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2128 println!(" vs string BS: {:.1}x faster",
2129 string_bs_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2130 println!();
2131
2132 println!("--- MEMORY PER PROFILE ---");
2134 println!(" Old (id: String): ~87 bytes (24 String header + ~63 heap)");
2135 println!(" Current (id: u16): 2 bytes (inline)");
2136 println!(" Savings: ~85 bytes/profile, ~{} bytes for {} profiles",
2137 85 * NUM_PROFILES, NUM_PROFILES);
2138 println!(" Interner (shared): {} bytes (shared with message system)",
2139 interner.memory_usage());
2140 println!();
2141
2142 println!("========================================");
2143 println!(" BENCHMARK COMPLETE");
2144 println!("========================================\n");
2145
2146 for npub in &npubs {
2148 assert!(old_ids.iter().any(|id| id == npub));
2149 assert!(sorted_ids.binary_search_by(|id| id.as_str().cmp(npub.as_str())).is_ok());
2150 let h = interner.lookup(npub).unwrap();
2151 assert!(profiles.binary_search_by(|p| p.id.cmp(&h)).is_ok());
2152 }
2153 }
2154
2155 #[test]
2160 fn pending_id_roundtrip_regular_hex() {
2161 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2162 let encoded = encode_message_id(hex);
2163 let decoded = decode_message_id(&encoded);
2164 assert_eq!(decoded, hex, "regular hex ID should roundtrip exactly");
2165 }
2166
2167 #[test]
2168 fn pending_id_roundtrip_pending() {
2169 let id = "pending-1234567890123456789";
2170 let encoded = encode_message_id(id);
2171 let decoded = decode_message_id(&encoded);
2172 assert_eq!(decoded, id, "pending ID should roundtrip exactly");
2173 }
2174
2175 #[test]
2176 fn pending_id_zero_timestamp() {
2177 let id = "pending-0";
2178 let encoded = encode_message_id(id);
2179 assert_eq!(encoded[0], PENDING_ID_MARKER, "first byte should be marker");
2180 let decoded = decode_message_id(&encoded);
2181 assert_eq!(decoded, id, "pending-0 should roundtrip");
2182 }
2183
2184 #[test]
2185 fn pending_id_max_u128_timestamp() {
2186 let max = u128::MAX;
2187 let id = format!("pending-{}", max);
2188 let encoded = encode_message_id(&id);
2189 let decoded = decode_message_id(&encoded);
2190 assert_eq!(decoded, id, "pending with max u128 should roundtrip");
2191 }
2192
2193 #[test]
2194 fn pending_id_all_zero_hex() {
2195 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
2196 let encoded = encode_message_id(hex);
2197 let decoded = decode_message_id(&encoded);
2198 assert_eq!(decoded, hex, "all-zero hex ID should roundtrip");
2199 assert_ne!(encoded[0], PENDING_ID_MARKER);
2201 }
2202
2203 #[test]
2204 fn pending_id_all_ff_hex() {
2205 let hex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
2206 let encoded = encode_message_id(hex);
2207 let decoded = decode_message_id(&encoded);
2208 assert_eq!(decoded, hex, "all-ff hex ID should roundtrip");
2209 assert_eq!(encoded, [0xff; 32], "all-ff should decode to all 0xff bytes");
2210 }
2211
2212 #[test]
2213 fn pending_id_mixed_case_hex() {
2214 let lower = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2215 let mixed = "ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789";
2216 let encoded_lower = encode_message_id(lower);
2217 let encoded_mixed = encode_message_id(mixed);
2218 assert_eq!(encoded_lower, encoded_mixed, "mixed case should produce same bytes as lowercase");
2219 }
2220
2221 #[test]
2222 fn pending_id_short_hex_partial_decode() {
2223 let short = "abcdef";
2225 let encoded = encode_message_id(short);
2226 assert_eq!(encoded[0..14], [0u8; 14], "leading bytes should be zero for short input");
2228 }
2229
2230 #[test]
2231 fn pending_id_marker_distinguishes_from_real_id() {
2232 let hex = "0100000000000000000000000000000000000000000000000000000000000000";
2237 let encoded = encode_message_id(hex);
2238 let decoded = decode_message_id(&encoded);
2239 assert_eq!(decoded, hex, "marker-leading real id must roundtrip exactly");
2240 }
2241
2242 #[test]
2243 fn pending_id_large_timestamp() {
2244 let id = "pending-99999999999999999";
2245 let encoded = encode_message_id(&id);
2246 let decoded = decode_message_id(&encoded);
2247 assert_eq!(decoded, id, "large but valid timestamp should roundtrip");
2248 }
2249
2250 #[test]
2251 fn pending_id_invalid_timestamp_becomes_zero() {
2252 let id = "pending-notanumber";
2254 let encoded = encode_message_id(id);
2255 assert_eq!(encoded[0], PENDING_ID_MARKER);
2256 assert_eq!(&encoded[1..17], &[0u8; 16]);
2258 let decoded = decode_message_id(&encoded);
2259 assert_eq!(decoded, "pending-0", "invalid timestamp parses as pending-0");
2260 }
2261
2262 #[test]
2267 fn timestamp_to_compact_and_back_roundtrip() {
2268 let ms: u64 = 1705320000000;
2270 let compact = timestamp_to_compact(ms);
2271 let restored = timestamp_from_compact(compact);
2272 assert_eq!(restored / 1000, ms / 1000, "roundtrip should preserve seconds");
2274 }
2275
2276 #[test]
2277 fn secs_to_compact_and_back_roundtrip() {
2278 let secs: u64 = 1705320000; let compact = secs_to_compact(secs);
2280 let restored = secs_from_compact(compact);
2281 assert_eq!(restored, secs, "secs should roundtrip exactly");
2282 }
2283
2284 #[test]
2285 fn timestamp_zero_preservation() {
2286 assert_eq!(secs_to_compact(0), 0, "zero secs should produce zero compact");
2288 assert_eq!(secs_from_compact(0), 0, "zero compact should produce zero secs");
2289 }
2290
2291 #[test]
2292 fn timestamp_epoch_boundary() {
2293 let epoch_secs: u64 = 1577836800;
2295 let compact = secs_to_compact(epoch_secs);
2296 assert_eq!(compact, 0, "epoch boundary should map to compact 0");
2297 let restored = secs_from_compact(compact);
2298 assert_eq!(restored, 0, "compact 0 returns sentinel 0");
2300 }
2301
2302 #[test]
2303 fn timestamp_epoch_boundary_ms() {
2304 let epoch_ms: u64 = 1577836800000;
2305 let compact = timestamp_to_compact(epoch_ms);
2306 assert_eq!(compact, epoch_ms, "full u64 — identity function");
2307 let restored = timestamp_from_compact(compact);
2308 assert_eq!(restored, epoch_ms, "roundtrip preserves value");
2309 }
2310
2311 #[test]
2312 fn timestamp_current_time_roundtrip() {
2313 let secs: u64 = 1774800000;
2315 let compact = secs_to_compact(secs);
2316 let restored = secs_from_compact(compact);
2317 assert_eq!(restored, secs, "current-era timestamp should roundtrip");
2318 assert!(compact > 0, "current time should be past epoch");
2319 }
2320
2321 #[test]
2322 fn timestamp_far_future_year_2100() {
2323 let secs: u64 = 4102444800;
2325 let compact = secs_to_compact(secs);
2326 let restored = secs_from_compact(compact);
2327 assert_eq!(restored, secs, "year 2100 should roundtrip");
2328 assert!(compact <= u32::MAX, "year 2100 should fit in u32");
2330 }
2331
2332 #[test]
2333 fn timestamp_pre_epoch_saturates_to_zero() {
2334 let secs: u64 = 1500000000; let compact = secs_to_compact(secs);
2337 assert_eq!(compact, 0, "pre-epoch timestamp should saturate to 0");
2339 }
2340
2341 #[test]
2342 fn timestamp_ms_sub_second_precision_preserved() {
2343 let ms: u64 = 1705320000999;
2344 let compact = timestamp_to_compact(ms);
2345 let restored = timestamp_from_compact(compact);
2346 assert_eq!(restored, ms, "sub-second ms must be preserved");
2347 }
2348
2349 #[test]
2350 fn timestamp_one_second_after_epoch() {
2351 let secs: u64 = 1577836801; let compact = secs_to_compact(secs);
2353 assert_eq!(compact, 1, "one second after epoch should be compact 1");
2354 let restored = secs_from_compact(compact);
2355 assert_eq!(restored, secs, "should restore to original");
2356 }
2357
2358 #[test]
2363 fn message_flags_mine_independent() {
2364 let mut flags = MessageFlags::NONE;
2365 flags.set_mine(true);
2366 assert!(flags.is_mine(), "mine should be set");
2367 assert!(!flags.is_pending(), "pending should not be set");
2368 assert!(!flags.is_failed(), "failed should not be set");
2369 }
2370
2371 #[test]
2372 fn message_flags_pending_independent() {
2373 let mut flags = MessageFlags::NONE;
2374 flags.set_pending(true);
2375 assert!(!flags.is_mine(), "mine should not be set");
2376 assert!(flags.is_pending(), "pending should be set");
2377 assert!(!flags.is_failed(), "failed should not be set");
2378 }
2379
2380 #[test]
2381 fn message_flags_failed_independent() {
2382 let mut flags = MessageFlags::NONE;
2383 flags.set_failed(true);
2384 assert!(!flags.is_mine(), "mine should not be set");
2385 assert!(!flags.is_pending(), "pending should not be set");
2386 assert!(flags.is_failed(), "failed should be set");
2387 }
2388
2389 #[test]
2390 fn message_flags_from_bools_all_false() {
2391 let flags = MessageFlags::from_bools(false, false, false);
2392 assert!(!flags.is_mine());
2393 assert!(!flags.is_pending());
2394 assert!(!flags.is_failed());
2395 assert_eq!(flags, MessageFlags::NONE, "all false should equal NONE");
2396 }
2397
2398 #[test]
2399 fn message_flags_from_bools_all_true() {
2400 let flags = MessageFlags::from_bools(true, true, true);
2401 assert!(flags.is_mine(), "mine should be set");
2402 assert!(flags.is_pending(), "pending should be set");
2403 assert!(flags.is_failed(), "failed should be set");
2404 }
2405
2406 #[test]
2407 fn message_flags_from_bools_various_combos() {
2408 let flags = MessageFlags::from_bools(true, false, true);
2409 assert!(flags.is_mine());
2410 assert!(!flags.is_pending());
2411 assert!(flags.is_failed());
2412
2413 let flags = MessageFlags::from_bools(false, true, false);
2414 assert!(!flags.is_mine());
2415 assert!(flags.is_pending());
2416 assert!(!flags.is_failed());
2417 }
2418
2419 #[test]
2420 fn message_flags_from_all_replied_to_none() {
2421 let flags = MessageFlags::from_all(false, false, false, None);
2422 assert_eq!(flags.replied_to_has_attachment(), None, "None should roundtrip");
2423 }
2424
2425 #[test]
2426 fn message_flags_from_all_replied_to_some_false() {
2427 let flags = MessageFlags::from_all(false, false, false, Some(false));
2428 assert_eq!(flags.replied_to_has_attachment(), Some(false), "Some(false) should roundtrip");
2429 }
2430
2431 #[test]
2432 fn message_flags_from_all_replied_to_some_true() {
2433 let flags = MessageFlags::from_all(false, false, false, Some(true));
2434 assert_eq!(flags.replied_to_has_attachment(), Some(true), "Some(true) should roundtrip");
2435 }
2436
2437 #[test]
2438 fn message_flags_multiple_set_simultaneously() {
2439 let flags = MessageFlags::from_all(true, true, false, Some(true));
2440 assert!(flags.is_mine());
2441 assert!(flags.is_pending());
2442 assert!(!flags.is_failed());
2443 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2444 }
2445
2446 #[test]
2447 fn message_flags_default_is_all_false() {
2448 let flags = MessageFlags::default();
2449 assert!(!flags.is_mine());
2450 assert!(!flags.is_pending());
2451 assert!(!flags.is_failed());
2452 assert_eq!(flags.replied_to_has_attachment(), None);
2453 assert_eq!(flags, MessageFlags::NONE);
2454 }
2455
2456 #[test]
2457 fn message_flags_bit_patterns_correct() {
2458 assert_eq!(MessageFlags::MINE.0, 0b00001, "MINE bit pattern");
2459 assert_eq!(MessageFlags::PENDING.0, 0b00010, "PENDING bit pattern");
2460 assert_eq!(MessageFlags::FAILED.0, 0b00100, "FAILED bit pattern");
2461 }
2462
2463 #[test]
2464 fn message_flags_set_then_clear() {
2465 let mut flags = MessageFlags::from_bools(true, true, true);
2466 flags.set_mine(false);
2467 assert!(!flags.is_mine(), "mine should be cleared");
2468 assert!(flags.is_pending(), "pending should remain set");
2469 assert!(flags.is_failed(), "failed should remain set");
2470 }
2471
2472 #[test]
2473 fn message_flags_replied_to_overwrite() {
2474 let mut flags = MessageFlags::from_all(false, false, false, Some(true));
2475 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2476 flags.set_replied_to_has_attachment(Some(false));
2477 assert_eq!(flags.replied_to_has_attachment(), Some(false), "overwrite should work");
2478 flags.set_replied_to_has_attachment(None);
2479 assert_eq!(flags.replied_to_has_attachment(), None, "clearing to None should work");
2480 }
2481
2482 #[test]
2483 fn message_flags_replied_to_does_not_interfere_with_other_bits() {
2484 let mut flags = MessageFlags::from_bools(true, true, true);
2485 flags.set_replied_to_has_attachment(Some(true));
2486 assert!(flags.is_mine(), "mine should still be set");
2487 assert!(flags.is_pending(), "pending should still be set");
2488 assert!(flags.is_failed(), "failed should still be set");
2489 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2490 }
2491
2492 #[test]
2497 fn attachment_flags_downloading_independent() {
2498 let mut flags = AttachmentFlags::NONE;
2499 flags.set_downloading(true);
2500 assert!(flags.is_downloading());
2501 assert!(!flags.is_downloaded());
2502 assert!(!flags.is_short_nonce());
2503 }
2504
2505 #[test]
2506 fn attachment_flags_downloaded_independent() {
2507 let mut flags = AttachmentFlags::NONE;
2508 flags.set_downloaded(true);
2509 assert!(!flags.is_downloading());
2510 assert!(flags.is_downloaded());
2511 assert!(!flags.is_short_nonce());
2512 }
2513
2514 #[test]
2515 fn attachment_flags_short_nonce_independent() {
2516 let mut flags = AttachmentFlags::NONE;
2517 flags.set_short_nonce(true);
2518 assert!(!flags.is_downloading());
2519 assert!(!flags.is_downloaded());
2520 assert!(flags.is_short_nonce());
2521 }
2522
2523 #[test]
2524 fn attachment_flags_from_bools() {
2525 let flags = AttachmentFlags::from_bools(true, false);
2526 assert!(flags.is_downloading());
2527 assert!(!flags.is_downloaded());
2528
2529 let flags = AttachmentFlags::from_bools(false, true);
2530 assert!(!flags.is_downloading());
2531 assert!(flags.is_downloaded());
2532 }
2533
2534 #[test]
2535 fn attachment_flags_all_set() {
2536 let mut flags = AttachmentFlags::NONE;
2537 flags.set_downloading(true);
2538 flags.set_downloaded(true);
2539 flags.set_short_nonce(true);
2540 assert!(flags.is_downloading());
2541 assert!(flags.is_downloaded());
2542 assert!(flags.is_short_nonce());
2543 }
2544
2545 #[test]
2546 fn attachment_flags_set_then_clear() {
2547 let mut flags = AttachmentFlags::NONE;
2548 flags.set_downloading(true);
2549 flags.set_downloaded(true);
2550 flags.set_downloading(false);
2551 assert!(!flags.is_downloading(), "downloading should be cleared");
2552 assert!(flags.is_downloaded(), "downloaded should remain set");
2553 }
2554
2555 #[test]
2556 fn attachment_flags_default_none() {
2557 let flags = AttachmentFlags::NONE;
2558 assert!(!flags.is_downloading());
2559 assert!(!flags.is_downloaded());
2560 assert!(!flags.is_short_nonce());
2561 assert_eq!(flags, AttachmentFlags::default());
2562 }
2563
2564 #[test]
2565 fn attachment_flags_bit_values() {
2566 let mut flags = AttachmentFlags::NONE;
2568 flags.set_downloading(true);
2569 assert_eq!(flags.0, 0b0001);
2570
2571 let mut flags = AttachmentFlags::NONE;
2572 flags.set_downloaded(true);
2573 assert_eq!(flags.0, 0b0010);
2574
2575 let mut flags = AttachmentFlags::NONE;
2576 flags.set_short_nonce(true);
2577 assert_eq!(flags.0, 0b0100);
2578 }
2579
2580 #[test]
2585 fn interner_returns_incrementing_handles() {
2586 let mut interner = NpubInterner::new();
2587 let h0 = interner.intern("npub1aaa");
2588 let h1 = interner.intern("npub1bbb");
2589 let h2 = interner.intern("npub1ccc");
2590 assert_eq!(h0, 0, "first intern should be handle 0");
2591 assert_eq!(h1, 1, "second intern should be handle 1");
2592 assert_eq!(h2, 2, "third intern should be handle 2");
2593 }
2594
2595 #[test]
2596 fn interner_lookup_finds_interned() {
2597 let mut interner = NpubInterner::new();
2598 let h = interner.intern("npub1alice");
2599 let found = interner.lookup("npub1alice");
2600 assert_eq!(found, Some(h), "lookup should find interned string");
2601 }
2602
2603 #[test]
2604 fn interner_lookup_returns_none_for_unknown() {
2605 let interner = NpubInterner::new();
2606 assert_eq!(interner.lookup("npub1unknown"), None, "lookup on empty interner should be None");
2607 }
2608
2609 #[test]
2610 fn interner_lookup_returns_none_for_not_interned() {
2611 let mut interner = NpubInterner::new();
2612 interner.intern("npub1alice");
2613 assert_eq!(interner.lookup("npub1bob"), None, "lookup for non-interned should be None");
2614 }
2615
2616 #[test]
2617 fn interner_resolve_returns_string() {
2618 let mut interner = NpubInterner::new();
2619 let h = interner.intern("npub1test123");
2620 assert_eq!(interner.resolve(h), Some("npub1test123"), "resolve should return the original string");
2621 }
2622
2623 #[test]
2624 fn interner_resolve_returns_none_for_no_npub() {
2625 let interner = NpubInterner::new();
2626 assert_eq!(interner.resolve(NO_NPUB), None, "resolve(NO_NPUB) should be None");
2627 }
2628
2629 #[test]
2630 fn interner_resolve_returns_none_for_out_of_bounds() {
2631 let mut interner = NpubInterner::new();
2632 interner.intern("npub1only");
2633 assert_eq!(interner.resolve(999), None, "out-of-bounds handle should resolve to None");
2634 }
2635
2636 #[test]
2637 fn interner_duplicate_returns_same_handle() {
2638 let mut interner = NpubInterner::new();
2639 let h1 = interner.intern("npub1dup");
2640 let h2 = interner.intern("npub1dup");
2641 let h3 = interner.intern("npub1dup");
2642 assert_eq!(h1, h2, "duplicate intern should return same handle");
2643 assert_eq!(h2, h3, "duplicate intern should return same handle");
2644 assert_eq!(interner.len(), 1, "duplicates should not increase length");
2645 }
2646
2647 #[test]
2648 fn interner_100_unique_npubs_stress() {
2649 let mut interner = NpubInterner::new();
2650 let mut handles = Vec::new();
2651
2652 for i in 0..100 {
2653 let npub = format!("npub1stress{:04}", i);
2654 let h = interner.intern(&npub);
2655 handles.push((h, npub));
2656 }
2657
2658 assert_eq!(interner.len(), 100, "should have 100 unique npubs");
2659
2660 for (h, npub) in &handles {
2662 assert_eq!(interner.resolve(*h), Some(npub.as_str()),
2663 "handle {} should resolve to {}", h, npub);
2664 }
2665
2666 for (h, npub) in &handles {
2668 assert_eq!(interner.lookup(npub), Some(*h),
2669 "lookup for {} should return handle {}", npub, h);
2670 }
2671
2672 for (h, npub) in &handles {
2674 assert_eq!(interner.intern(npub), *h,
2675 "re-interning {} should return same handle {}", npub, h);
2676 }
2677 assert_eq!(interner.len(), 100, "re-interning should not grow interner");
2678 }
2679
2680 #[test]
2681 fn interner_memory_usage_reasonable() {
2682 let mut interner = NpubInterner::new();
2683 for i in 0..50 {
2684 interner.intern(&format!("npub1{:0>62}", i));
2685 }
2686 let mem = interner.memory_usage();
2687 assert!(mem > 0, "memory usage should be positive");
2689 assert!(mem < 100_000, "memory usage for 50 npubs should be under 100KB, was {}", mem);
2690 }
2691
2692 #[test]
2693 fn interner_empty() {
2694 let interner = NpubInterner::new();
2695 assert_eq!(interner.len(), 0);
2696 assert!(interner.is_empty());
2697 assert_eq!(interner.resolve(0), None);
2698 assert_eq!(interner.lookup("anything"), None);
2699 assert!(interner.memory_usage() > 0, "even empty interner has struct overhead");
2700 }
2701
2702 #[test]
2703 fn interner_intern_opt_none() {
2704 let mut interner = NpubInterner::new();
2705 let h = interner.intern_opt(None);
2706 assert_eq!(h, NO_NPUB, "intern_opt(None) should return NO_NPUB");
2707 assert_eq!(interner.len(), 0, "None should not add to interner");
2708 }
2709
2710 #[test]
2711 fn interner_intern_opt_empty_string() {
2712 let mut interner = NpubInterner::new();
2713 let h = interner.intern_opt(Some(""));
2714 assert_eq!(h, NO_NPUB, "intern_opt(Some('')) should return NO_NPUB");
2715 }
2716
2717 #[test]
2718 fn interner_intern_opt_some_value() {
2719 let mut interner = NpubInterner::new();
2720 let h = interner.intern_opt(Some("npub1real"));
2721 assert_ne!(h, NO_NPUB, "intern_opt(Some(value)) should not return NO_NPUB");
2722 assert_eq!(interner.resolve(h), Some("npub1real"));
2723 }
2724
2725 fn make_compact_msg(hex_id: &str, timestamp: u64) -> CompactMessage {
2731 CompactMessage {
2732 id: encode_message_id(hex_id),
2733 at: timestamp,
2734 expiration_secs: 0,
2735 flags: MessageFlags::NONE,
2736 npub_idx: NO_NPUB,
2737 replied_to: None,
2738 replied_to_npub_idx: NO_NPUB,
2739 wrapper_id: None,
2740 content: "test".into(),
2741 replied_to_content: None,
2742 attachments: TinyVec::new(),
2743 reactions: TinyVec::new(),
2744 edit_history: None,
2745 preview_metadata: None,
2746 emoji_tags: None,
2747 addressed_bots: None,
2748 }
2749 }
2750
2751 #[test]
2752 fn compact_vec_insert_single_message() {
2753 let mut vec = CompactMessageVec::new();
2754 let msg = make_compact_msg(
2755 "1111111111111111111111111111111111111111111111111111111111111111",
2756 100,
2757 );
2758 assert!(vec.insert(msg), "insert should succeed");
2759 assert_eq!(vec.len(), 1);
2760 assert!(!vec.is_empty());
2761 }
2762
2763 #[test]
2764 fn compact_vec_insert_duplicate_rejected() {
2765 let mut vec = CompactMessageVec::new();
2766 let id = "2222222222222222222222222222222222222222222222222222222222222222";
2767 let msg1 = make_compact_msg(id, 100);
2768 let msg2 = make_compact_msg(id, 200); assert!(vec.insert(msg1), "first insert should succeed");
2770 assert!(!vec.insert(msg2), "duplicate ID should be rejected");
2771 assert_eq!(vec.len(), 1);
2772 }
2773
2774 #[test]
2775 fn compact_vec_insert_batch_multiple() {
2776 let mut vec = CompactMessageVec::new();
2777 let msgs = vec![
2778 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2779 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2780 make_compact_msg("cc00000000000000000000000000000000000000000000000000000000000000", 300),
2781 ];
2782 let added = vec.insert_batch(msgs);
2783 assert_eq!(added, 3, "all 3 should be added");
2784 assert_eq!(vec.len(), 3);
2785 }
2786
2787 #[test]
2788 fn compact_vec_append_batch_merge_keeps_index_consistent() {
2789 let mut vec = CompactMessageVec::new();
2793 let mk = |n: u32, at: u64| make_compact_msg(&format!("{:064x}", n), at);
2794 let seed: Vec<_> = (0..50).map(|n| mk(n, n as u64)).collect();
2795 assert_eq!(vec.insert_batch(seed), 50);
2796 let batch: Vec<_> = (1000..1200).map(|n| mk(n, n as u64)).collect();
2797 assert_eq!(vec.insert_batch(batch), 200, "all newer messages appended");
2798 assert_eq!(vec.len(), 250);
2799 for n in (0..50).chain(1000..1200) {
2800 assert!(vec.find_by_hex_id(&format!("{:064x}", n)).is_some(), "id {n} must be findable via the merged index");
2801 }
2802 assert!(vec.find_by_hex_id(&format!("{:064x}", 9999u32)).is_none(), "never-inserted id must not be found");
2803 let dup: Vec<_> = (1000..1200).map(|n| mk(n, n as u64)).collect();
2804 assert_eq!(vec.insert_batch(dup), 0, "re-appended batch fully deduped via the merged index");
2805 }
2806
2807 #[test]
2808 fn compact_vec_insert_batch_dedup() {
2809 let mut vec = CompactMessageVec::new();
2810 let id = "dd00000000000000000000000000000000000000000000000000000000000000";
2811 vec.insert(make_compact_msg(id, 100));
2812
2813 let msgs = vec![
2814 make_compact_msg(id, 200), make_compact_msg("ee00000000000000000000000000000000000000000000000000000000000000", 300),
2816 ];
2817 let added = vec.insert_batch(msgs);
2818 assert_eq!(added, 1, "only non-duplicate should be added");
2819 assert_eq!(vec.len(), 2);
2820 }
2821
2822 #[test]
2823 fn compact_vec_find_by_hex_id() {
2824 let mut vec = CompactMessageVec::new();
2825 let id = "ff00000000000000000000000000000000000000000000000000000000000001";
2826 let mut msg = make_compact_msg(id, 500);
2827 msg.content = "found me".into();
2828 vec.insert(msg);
2829
2830 let found = vec.find_by_hex_id(id);
2831 assert!(found.is_some(), "should find by hex id");
2832 assert_eq!(&*found.unwrap().content, "found me");
2833 }
2834
2835 #[test]
2836 fn compact_vec_find_by_hex_id_not_found() {
2837 let mut vec = CompactMessageVec::new();
2838 vec.insert(make_compact_msg(
2839 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2840 ));
2841 let found = vec.find_by_hex_id(
2842 "bb00000000000000000000000000000000000000000000000000000000000000",
2843 );
2844 assert!(found.is_none(), "should not find non-existent ID");
2845 }
2846
2847 #[test]
2848 fn compact_vec_find_by_hex_id_empty_string() {
2849 let mut vec = CompactMessageVec::new();
2850 vec.insert(make_compact_msg(
2851 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2852 ));
2853 assert!(vec.find_by_hex_id("").is_none(), "empty string should return None");
2854 }
2855
2856 #[test]
2857 fn compact_vec_find_by_hex_id_mut() {
2858 let mut vec = CompactMessageVec::new();
2859 let id = "ff00000000000000000000000000000000000000000000000000000000000002";
2860 vec.insert(make_compact_msg(id, 500));
2861
2862 let found = vec.find_by_hex_id_mut(id);
2863 assert!(found.is_some(), "should find mutable ref by hex id");
2864 found.unwrap().content = "modified".into();
2865
2866 let found = vec.find_by_hex_id(id);
2868 assert_eq!(&*found.unwrap().content, "modified");
2869 }
2870
2871 #[test]
2872 fn compact_vec_contains_hex_id() {
2873 let mut vec = CompactMessageVec::new();
2874 let id = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
2875 vec.insert(make_compact_msg(id, 100));
2876
2877 assert!(vec.contains_hex_id(id), "should contain inserted ID");
2878 assert!(!vec.contains_hex_id(
2879 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
2880 "should not contain non-inserted ID");
2881 assert!(!vec.contains_hex_id(""), "empty string should not match");
2882 }
2883
2884 #[test]
2885 fn compact_vec_remove_by_hex_id() {
2886 let mut vec = CompactMessageVec::new();
2887 let id1 = "1100000000000000000000000000000000000000000000000000000000000000";
2888 let id2 = "2200000000000000000000000000000000000000000000000000000000000000";
2889 vec.insert(make_compact_msg(id1, 100));
2890 vec.insert(make_compact_msg(id2, 200));
2891 assert_eq!(vec.len(), 2);
2892
2893 assert!(vec.remove_by_hex_id(id1), "remove should succeed");
2894 assert_eq!(vec.len(), 1);
2895 assert!(!vec.contains_hex_id(id1), "removed ID should not be found");
2896 assert!(vec.contains_hex_id(id2), "remaining ID should still be found");
2897 }
2898
2899 #[test]
2900 fn compact_vec_remove_nonexistent() {
2901 let mut vec = CompactMessageVec::new();
2902 vec.insert(make_compact_msg(
2903 "1100000000000000000000000000000000000000000000000000000000000000", 100,
2904 ));
2905 assert!(!vec.remove_by_hex_id(
2906 "9900000000000000000000000000000000000000000000000000000000000000"),
2907 "removing non-existent should return false");
2908 assert!(!vec.remove_by_hex_id(""), "removing empty should return false");
2909 assert_eq!(vec.len(), 1);
2910 }
2911
2912 #[test]
2913 fn compact_vec_last_timestamp() {
2914 let mut vec = CompactMessageVec::new();
2915 assert_eq!(vec.last_timestamp(), None, "empty vec should have no last timestamp");
2916
2917 vec.insert(make_compact_msg(
2918 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2919 ));
2920 vec.insert(make_compact_msg(
2921 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2922 ));
2923 vec.insert(make_compact_msg(
2924 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2925 ));
2926
2927 let last_ts = vec.last_timestamp().unwrap();
2928 let expected = timestamp_from_compact(300);
2930 assert_eq!(last_ts, expected, "last timestamp should be the largest");
2931 }
2932
2933 #[test]
2934 fn compact_vec_empty_operations() {
2935 let vec = CompactMessageVec::new();
2936 assert!(vec.is_empty());
2937 assert_eq!(vec.len(), 0);
2938 assert!(vec.last().is_none());
2939 assert!(vec.first().is_none());
2940 assert!(vec.last_timestamp().is_none());
2941 assert!(!vec.contains_hex_id("anything"));
2942 assert!(vec.find_by_hex_id("anything").is_none());
2943 }
2944
2945 #[test]
2946 fn compact_vec_1000_message_stress() {
2947 let mut vec = CompactMessageVec::new();
2948
2949 for i in 0..1000u64 {
2951 let id = format!("{:0>64x}", i);
2952 let msg = make_compact_msg(&id, i * 10);
2953 assert!(vec.insert(msg), "insert {} should succeed", i);
2954 }
2955 assert_eq!(vec.len(), 1000);
2956
2957 for i in 0..1000u64 {
2959 let id = format!("{:0>64x}", i);
2960 assert!(vec.contains_hex_id(&id), "should find message {}", i);
2961 let found = vec.find_by_hex_id(&id).unwrap();
2962 assert_eq!(found.at, i * 10, "timestamp should match for message {}", i);
2963 }
2964
2965 for i in 1000..1010u64 {
2967 let id = format!("{:0>64x}", i);
2968 assert!(!vec.contains_hex_id(&id), "should not find non-existent {}", i);
2969 }
2970
2971 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2973 for w in timestamps.windows(2) {
2974 assert!(w[0] <= w[1], "messages should be sorted by timestamp: {} <= {}", w[0], w[1]);
2975 }
2976 }
2977
2978 #[test]
2979 fn compact_vec_rebuild_index_after_id_change() {
2980 let mut vec = CompactMessageVec::new();
2981 let old_id = "aa00000000000000000000000000000000000000000000000000000000000000";
2982 let new_id = "ff00000000000000000000000000000000000000000000000000000000000000";
2983 vec.insert(make_compact_msg(old_id, 100));
2984
2985 vec.messages_mut()[0].id = encode_message_id(new_id);
2987 assert!(!vec.contains_hex_id(new_id), "stale index should not find new ID");
2989
2990 vec.rebuild_index();
2992 assert!(vec.contains_hex_id(new_id), "after rebuild, new ID should be found");
2993 assert!(!vec.contains_hex_id(old_id), "after rebuild, old ID should not be found");
2994 }
2995
2996 #[test]
2997 fn compact_vec_pending_id_lookup() {
2998 let mut vec = CompactMessageVec::new();
2999 let pending = "pending-9876543210";
3000 vec.insert(make_compact_msg(pending, 500));
3001
3002 assert!(vec.contains_hex_id(pending), "should find pending ID");
3003 let found = vec.find_by_hex_id(pending);
3004 assert!(found.is_some(), "should find pending message");
3005 assert_eq!(found.unwrap().id_hex(), pending, "id_hex should match pending string");
3006 }
3007
3008 #[test]
3009 fn compact_vec_out_of_order_insert() {
3010 let mut vec = CompactMessageVec::new();
3011 vec.insert(make_compact_msg(
3013 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
3014 ));
3015 vec.insert(make_compact_msg(
3016 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
3017 ));
3018 vec.insert(make_compact_msg(
3019 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
3020 ));
3021
3022 assert_eq!(vec.len(), 3);
3023 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3025 assert_eq!(timestamps, vec![100, 200, 300], "should be sorted by timestamp");
3026
3027 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3029 assert!(vec.contains_hex_id("bb00000000000000000000000000000000000000000000000000000000000000"));
3030 assert!(vec.contains_hex_id("cc00000000000000000000000000000000000000000000000000000000000000"));
3031 }
3032
3033 #[test]
3034 fn compact_vec_batch_prepend() {
3035 let mut vec = CompactMessageVec::new();
3036 vec.insert(make_compact_msg(
3038 "cc00000000000000000000000000000000000000000000000000000000000000", 300,
3039 ));
3040 vec.insert(make_compact_msg(
3041 "dd00000000000000000000000000000000000000000000000000000000000000", 400,
3042 ));
3043
3044 let older = vec![
3046 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
3047 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
3048 ];
3049 let added = vec.insert_batch(older);
3050 assert_eq!(added, 2);
3051 assert_eq!(vec.len(), 4);
3052
3053 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3055 assert_eq!(timestamps, vec![100, 200, 300, 400]);
3056
3057 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3059 assert!(vec.contains_hex_id("dd00000000000000000000000000000000000000000000000000000000000000"));
3060 }
3061
3062 #[test]
3063 fn compact_vec_clear() {
3064 let mut vec = CompactMessageVec::new();
3065 vec.insert(make_compact_msg(
3066 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
3067 ));
3068 vec.insert(make_compact_msg(
3069 "bb00000000000000000000000000000000000000000000000000000000000000", 200,
3070 ));
3071 assert_eq!(vec.len(), 2);
3072
3073 vec.clear();
3074 assert!(vec.is_empty());
3075 assert_eq!(vec.len(), 0);
3076 assert!(!vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3077 }
3078
3079 fn make_full_message() -> Message {
3085 Message {
3086 expiration: Some(1893456000),
3087 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3088 content: "Hello, world!".into(),
3089 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3090 replied_to_content: Some("Original message".into()),
3091 replied_to_npub: Some("npub1replier".into()),
3092 replied_to_has_attachment: Some(true),
3093 replied_to_attachment_extension: None,
3094 preview_metadata: Some(SiteMetadata {
3095 domain: "example.com".into(),
3096 og_title: Some("Test Page".into()),
3097 og_description: Some("A test description".into()),
3098 og_image: Some("https://example.com/img.png".into()),
3099 og_url: Some("https://example.com".into()),
3100 og_type: Some("website".into()),
3101 title: Some("Test".into()),
3102 description: Some("Desc".into()),
3103 favicon: Some("https://example.com/favicon.ico".into()),
3104 }),
3105 attachments: vec![Attachment {
3106 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3107 key: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3108 nonce: "cccccccccccccccccccccccccccccccc".into(), extension: "png".into(),
3110 name: "photo.png".into(),
3111 url: "https://blossom.example.com".into(),
3112 path: "/tmp/photo.png".into(),
3113 size: 12345,
3114 img_meta: Some(ImageMetadata {
3115 thumbhash: "abc123".into(),
3116 width: 800,
3117 height: 600,
3118 }),
3119 downloading: false,
3120 downloaded: true,
3121 webxdc_topic: None,
3122 group_id: None,
3123 original_hash: None,
3124 fallback_urls: Vec::new(),
3125 }],
3126 reactions: vec![Reaction {
3127 id: "dddd000000000000000000000000000000000000000000000000000000000000".into(),
3128 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3129 author_id: "npub1reactor".into(),
3130 emoji: "\u{1f44d}".into(), emoji_url: None,
3132 }],
3133 at: 1705320000000, pending: false,
3135 failed: false,
3136 mine: true,
3137 npub: Some("npub1sender".into()),
3138 wrapper_event_id: Some("eeee000000000000000000000000000000000000000000000000000000000000".into()),
3139 edited: true,
3140 edit_history: Some(vec![
3141 EditEntry { content: "Original".into(), edited_at: 1705320000000 },
3142 EditEntry { content: "Edited".into(), edited_at: 1705320060000 },
3143 ]),
3144 emoji_tags: Vec::new(),
3145 addressed_bots: vec!["npub1botrouting0000000000000000000000000000000000000000000000".into()],
3146 }
3147 }
3148
3149 #[test]
3150 fn compact_message_from_message_roundtrip_all_fields() {
3151 let msg = make_full_message();
3152 let mut interner = NpubInterner::new();
3153 let compact = CompactMessage::from_message(&msg, &mut interner);
3154 let restored = compact.to_message(&interner);
3155
3156 assert_eq!(restored.id, msg.id, "id mismatch");
3157 assert_eq!(restored.content, msg.content, "content mismatch");
3158 assert_eq!(restored.mine, msg.mine, "mine mismatch");
3159 assert_eq!(restored.pending, msg.pending, "pending mismatch");
3160 assert_eq!(restored.failed, msg.failed, "failed mismatch");
3161 assert_eq!(restored.npub, msg.npub, "npub mismatch");
3162 assert_eq!(restored.replied_to, msg.replied_to, "replied_to mismatch");
3163 assert_eq!(restored.replied_to_content, msg.replied_to_content, "replied_to_content mismatch");
3164 assert_eq!(restored.replied_to_npub, msg.replied_to_npub, "replied_to_npub mismatch");
3165 assert_eq!(restored.replied_to_has_attachment, msg.replied_to_has_attachment, "replied_to_has_attachment mismatch");
3166 assert_eq!(restored.wrapper_event_id, msg.wrapper_event_id, "wrapper_event_id mismatch");
3167 assert_eq!(restored.edited, msg.edited, "edited mismatch");
3168 assert_eq!(restored.edit_history, msg.edit_history, "edit_history mismatch");
3169 assert_eq!(restored.preview_metadata, msg.preview_metadata, "preview_metadata mismatch");
3170 assert_eq!(restored.at / 1000, msg.at / 1000, "timestamp seconds mismatch");
3172 assert_eq!(restored.attachments.len(), 1, "should have 1 attachment");
3174 assert_eq!(restored.attachments[0].id, msg.attachments[0].id);
3175 assert_eq!(restored.attachments[0].name, msg.attachments[0].name);
3176 assert_eq!(restored.attachments[0].size, msg.attachments[0].size);
3177 assert_eq!(restored.reactions.len(), 1, "should have 1 reaction");
3179 assert_eq!(restored.reactions[0].emoji, msg.reactions[0].emoji);
3180 assert_eq!(restored.addressed_bots, msg.addressed_bots, "addressed_bots mismatch");
3182 }
3183
3184 #[test]
3190 fn compact_drops_replied_to_attachment_extension() {
3191 let mut msg = make_full_message();
3192 msg.replied_to_has_attachment = Some(true);
3193 msg.replied_to_attachment_extension = Some("gif".to_string());
3194
3195 let mut interner = NpubInterner::new();
3196 let restored = CompactMessage::from_message(&msg, &mut interner).to_message(&interner);
3197
3198 assert_eq!(restored.replied_to_has_attachment, Some(true), "the bool survives RAM");
3199 assert_eq!(
3200 restored.replied_to_attachment_extension, None,
3201 "the extension does not survive RAM — emitters must re-resolve it"
3202 );
3203 }
3204
3205 #[test]
3206 fn compact_message_from_message_owned_roundtrip() {
3207 let msg = make_full_message();
3208 let msg_clone = msg.clone();
3209 let mut interner = NpubInterner::new();
3210 let compact = CompactMessage::from_message_owned(msg, &mut interner);
3211 let restored = compact.to_message(&interner);
3212
3213 assert_eq!(restored.id, msg_clone.id, "id mismatch");
3214 assert_eq!(restored.content, msg_clone.content, "content mismatch");
3215 assert_eq!(restored.mine, msg_clone.mine, "mine mismatch");
3216 assert_eq!(restored.npub, msg_clone.npub, "npub mismatch");
3217 assert_eq!(restored.edit_history, msg_clone.edit_history, "edit_history mismatch");
3218 }
3219
3220 #[test]
3221 fn compact_message_pending_flag() {
3222 let msg = Message {
3223 id: "pending-1234567890".into(),
3224 pending: true,
3225 ..Message::default()
3226 };
3227 let mut interner = NpubInterner::new();
3228 let compact = CompactMessage::from_message(&msg, &mut interner);
3229 assert!(compact.is_pending(), "pending flag should be set");
3230 let restored = compact.to_message(&interner);
3231 assert!(restored.pending, "pending should roundtrip");
3232 assert_eq!(restored.id, "pending-1234567890", "pending ID should roundtrip");
3233 }
3234
3235 #[test]
3236 fn compact_message_failed_flag() {
3237 let msg = Message {
3238 id: "pending-999".into(),
3239 failed: true,
3240 pending: true,
3241 ..Message::default()
3242 };
3243 let mut interner = NpubInterner::new();
3244 let compact = CompactMessage::from_message(&msg, &mut interner);
3245 assert!(compact.is_failed(), "failed flag should be set");
3246 assert!(compact.is_pending(), "pending flag should also be set");
3247 let restored = compact.to_message(&interner);
3248 assert!(restored.failed);
3249 assert!(restored.pending);
3250 }
3251
3252 #[test]
3253 fn compact_message_with_attachments_roundtrip() {
3254 let msg = Message {
3255 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3256 attachments: vec![
3257 Attachment {
3258 id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3259 extension: "jpg".into(),
3260 name: "sunset.jpg".into(),
3261 size: 5000,
3262 downloaded: true,
3263 ..Attachment::default()
3264 },
3265 Attachment {
3266 id: "2222222222222222222222222222222222222222222222222222222222222222".into(),
3267 extension: "mp4".into(),
3268 name: "video.mp4".into(),
3269 size: 50000,
3270 downloaded: false,
3271 downloading: true,
3272 ..Attachment::default()
3273 },
3274 ],
3275 ..Message::default()
3276 };
3277 let mut interner = NpubInterner::new();
3278 let compact = CompactMessage::from_message(&msg, &mut interner);
3279 assert_eq!(compact.attachments.len(), 2);
3280
3281 let restored = compact.to_message(&interner);
3282 assert_eq!(restored.attachments.len(), 2);
3283 assert_eq!(restored.attachments[0].name, "sunset.jpg");
3284 assert_eq!(restored.attachments[0].extension, "jpg");
3285 assert!(restored.attachments[0].downloaded);
3286 assert_eq!(restored.attachments[1].name, "video.mp4");
3287 assert!(restored.attachments[1].downloading);
3288 assert!(!restored.attachments[1].downloaded);
3289 }
3290
3291 #[test]
3292 fn compact_message_with_reactions_roundtrip() {
3293 let msg = Message {
3294 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3295 reactions: vec![
3296 Reaction {
3297 id: "aaa0000000000000000000000000000000000000000000000000000000000000".into(),
3298 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3299 author_id: "npub1alice".into(),
3300 emoji: "\u{2764}".into(), emoji_url: None,
3302 },
3303 Reaction {
3304 id: "bbb0000000000000000000000000000000000000000000000000000000000000".into(),
3305 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3306 author_id: "npub1bob".into(),
3307 emoji: "\u{1f525}".into(), emoji_url: None,
3309 },
3310 ],
3311 ..Message::default()
3312 };
3313 let mut interner = NpubInterner::new();
3314 let compact = CompactMessage::from_message(&msg, &mut interner);
3315 let restored = compact.to_message(&interner);
3316
3317 assert_eq!(restored.reactions.len(), 2);
3318 assert_eq!(restored.reactions[0].emoji, "\u{2764}");
3319 assert_eq!(restored.reactions[0].author_id, "npub1alice");
3320 assert_eq!(restored.reactions[1].emoji, "\u{1f525}");
3321 assert_eq!(restored.reactions[1].author_id, "npub1bob");
3322 }
3323
3324 #[test]
3325 fn compact_message_with_edit_history_roundtrip() {
3326 let msg = Message {
3327 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3328 content: "Final version".into(),
3329 edited: true,
3330 edit_history: Some(vec![
3331 EditEntry { content: "First draft".into(), edited_at: 1000 },
3332 EditEntry { content: "Second draft".into(), edited_at: 2000 },
3333 EditEntry { content: "Final version".into(), edited_at: 3000 },
3334 ]),
3335 ..Message::default()
3336 };
3337 let mut interner = NpubInterner::new();
3338 let compact = CompactMessage::from_message(&msg, &mut interner);
3339 assert!(compact.is_edited());
3340
3341 let restored = compact.to_message(&interner);
3342 assert!(restored.edited);
3343 let history = restored.edit_history.unwrap();
3344 assert_eq!(history.len(), 3);
3345 assert_eq!(history[0].content, "First draft");
3346 assert_eq!(history[2].content, "Final version");
3347 }
3348
3349 #[test]
3350 fn compact_message_with_preview_metadata_roundtrip() {
3351 let meta = SiteMetadata {
3352 domain: "example.com".into(),
3353 og_title: Some("Title".into()),
3354 og_description: Some("Desc".into()),
3355 og_image: Some("https://example.com/img.png".into()),
3356 og_url: None,
3357 og_type: None,
3358 title: None,
3359 description: None,
3360 favicon: None,
3361 };
3362 let msg = Message {
3363 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3364 preview_metadata: Some(meta.clone()),
3365 ..Message::default()
3366 };
3367 let mut interner = NpubInterner::new();
3368 let compact = CompactMessage::from_message(&msg, &mut interner);
3369 let restored = compact.to_message(&interner);
3370
3371 let restored_meta = restored.preview_metadata.unwrap();
3372 assert_eq!(restored_meta.domain, "example.com");
3373 assert_eq!(restored_meta.og_title, Some("Title".into()));
3374 assert_eq!(restored_meta.og_image, Some("https://example.com/img.png".into()));
3375 assert_eq!(restored_meta.og_url, None);
3376 }
3377
3378 #[test]
3379 fn compact_message_with_replied_to_roundtrip() {
3380 let msg = Message {
3381 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3382 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3383 replied_to_content: Some("Original text".into()),
3384 replied_to_npub: Some("npub1original".into()),
3385 replied_to_has_attachment: Some(false),
3386 ..Message::default()
3387 };
3388 let mut interner = NpubInterner::new();
3389 let compact = CompactMessage::from_message(&msg, &mut interner);
3390 assert!(compact.has_reply());
3391
3392 let restored = compact.to_message(&interner);
3393 assert_eq!(restored.replied_to, "1111111111111111111111111111111111111111111111111111111111111111");
3394 assert_eq!(restored.replied_to_content, Some("Original text".into()));
3395 assert_eq!(restored.replied_to_npub, Some("npub1original".into()));
3396 assert_eq!(restored.replied_to_has_attachment, Some(false));
3397 }
3398
3399 #[test]
3400 fn compact_message_empty_roundtrip() {
3401 let msg = Message::default();
3402 let mut interner = NpubInterner::new();
3403 let compact = CompactMessage::from_message(&msg, &mut interner);
3404 let restored = compact.to_message(&interner);
3405
3406 assert_eq!(restored.id, "0000000000000000000000000000000000000000000000000000000000000000",
3407 "empty ID should decode as all zeros hex");
3408 assert_eq!(restored.content, "");
3409 assert!(!restored.mine);
3410 assert!(!restored.pending);
3411 assert!(!restored.failed);
3412 assert!(!restored.edited);
3413 assert_eq!(restored.npub, None);
3414 assert!(restored.replied_to.is_empty() || restored.replied_to == "0000000000000000000000000000000000000000000000000000000000000000");
3415 assert_eq!(restored.replied_to_content, None);
3416 assert_eq!(restored.replied_to_npub, None);
3417 assert_eq!(restored.replied_to_has_attachment, None);
3418 assert_eq!(restored.wrapper_event_id, None);
3419 assert!(restored.attachments.is_empty());
3420 assert!(restored.reactions.is_empty());
3421 assert_eq!(restored.edit_history, None);
3422 assert_eq!(restored.preview_metadata, None);
3423 }
3424
3425 #[test]
3430 fn compact_attachment_from_attachment_roundtrip() {
3431 let att = Attachment {
3432 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3433 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3434 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), extension: "zip".into(),
3436 name: "archive.zip".into(),
3437 url: "https://blossom.test.com".into(),
3438 path: "/downloads/archive.zip".into(),
3439 size: 99999,
3440 img_meta: None,
3441 downloading: false,
3442 downloaded: true,
3443 webxdc_topic: None,
3444 group_id: None,
3445 original_hash: None,
3446 fallback_urls: Vec::new(),
3447 };
3448
3449 let compact = CompactAttachment::from_attachment(&att);
3450 let restored = compact.to_attachment();
3451
3452 assert_eq!(restored.id, att.id, "id mismatch");
3453 assert_eq!(restored.key, att.key, "key mismatch");
3454 assert_eq!(restored.nonce, att.nonce, "nonce mismatch");
3455 assert_eq!(restored.extension, att.extension, "extension mismatch");
3456 assert_eq!(restored.name, att.name, "name mismatch");
3457 assert_eq!(restored.url, att.url, "url mismatch");
3458 assert_eq!(restored.path, att.path, "path mismatch");
3459 assert_eq!(restored.size, att.size, "size mismatch");
3460 assert_eq!(restored.downloading, att.downloading, "downloading mismatch");
3461 assert_eq!(restored.downloaded, att.downloaded, "downloaded mismatch");
3462 }
3463
3464 #[test]
3465 fn compact_attachment_from_attachment_owned_roundtrip() {
3466 let att = Attachment {
3467 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3468 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3469 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3470 extension: "pdf".into(),
3471 name: "document.pdf".into(),
3472 url: "https://server.com".into(),
3473 path: "".into(),
3474 size: 1024,
3475 img_meta: None,
3476 downloading: false,
3477 downloaded: false,
3478 webxdc_topic: None,
3479 group_id: None,
3480 original_hash: None,
3481 fallback_urls: Vec::new(),
3482 };
3483 let att_clone = att.clone();
3484
3485 let compact = CompactAttachment::from_attachment_owned(att);
3486 let restored = compact.to_attachment();
3487
3488 assert_eq!(restored.id, att_clone.id);
3489 assert_eq!(restored.name, att_clone.name);
3490 assert_eq!(restored.size, att_clone.size);
3491 }
3492
3493 #[test]
3494 fn compact_attachment_key_nonce_zeros() {
3495 let att = Attachment {
3496 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3497 key: "".into(), nonce: "".into(), ..Attachment::default()
3500 };
3501 let compact = CompactAttachment::from_attachment(&att);
3502 assert_eq!(compact.key, [0u8; 32], "empty key should be all zeros");
3503 assert_eq!(compact.nonce, [0u8; 16], "empty nonce should be all zeros");
3504
3505 let restored = compact.to_attachment();
3506 assert_eq!(restored.key, "", "zero key should restore as empty string");
3507 assert_eq!(restored.nonce, "", "zero nonce should restore as empty string");
3508 }
3509
3510 #[test]
3511 fn compact_attachment_short_nonce_mls_12byte() {
3512 let att = Attachment {
3514 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3515 nonce: "aabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3517 };
3518 let compact = CompactAttachment::from_attachment(&att);
3519 assert!(compact.flags.is_short_nonce(), "12-byte nonce should set short_nonce flag");
3520
3521 let restored = compact.to_attachment();
3522 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccdd", "short nonce should roundtrip");
3523 }
3524
3525 #[test]
3526 fn compact_attachment_long_nonce_dm_16byte() {
3527 let att = Attachment {
3529 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3530 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3532 };
3533 let compact = CompactAttachment::from_attachment(&att);
3534 assert!(!compact.flags.is_short_nonce(), "16-byte nonce should NOT set short_nonce flag");
3535
3536 let restored = compact.to_attachment();
3537 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccddaabbccdd", "long nonce should roundtrip");
3538 }
3539
3540 #[test]
3541 fn compact_attachment_id_eq_comparison() {
3542 let att = Attachment {
3543 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3544 ..Attachment::default()
3545 };
3546 let compact = CompactAttachment::from_attachment(&att);
3547
3548 assert!(compact.id_eq("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
3549 "id_eq should match same hex");
3550 assert!(!compact.id_eq("1111111111111111111111111111111111111111111111111111111111111111"),
3551 "id_eq should not match different hex");
3552 }
3553
3554 #[test]
3555 fn compact_attachment_all_optional_fields_none() {
3556 let att = Attachment::default();
3557 let compact = CompactAttachment::from_attachment(&att);
3558
3559 assert!(compact.img_meta.is_none());
3560 assert!(compact.group_id.is_none());
3561 assert!(compact.original_hash.is_none());
3562 assert!(compact.webxdc_topic.is_none());
3563
3564 let restored = compact.to_attachment();
3565 assert!(restored.img_meta.is_none());
3566 assert!(restored.group_id.is_none());
3567 assert!(restored.original_hash.is_none());
3568 assert!(restored.webxdc_topic.is_none());
3569 }
3570
3571 #[test]
3572 fn compact_attachment_all_optional_fields_some() {
3573 let att = Attachment {
3574 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3575 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3576 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3577 extension: "xdc".into(),
3578 name: "app.xdc".into(),
3579 url: "https://server.com/file".into(),
3580 path: "/local/app.xdc".into(),
3581 size: 50000,
3582 img_meta: Some(ImageMetadata {
3583 thumbhash: "hash123".into(),
3584 width: 1920,
3585 height: 1080,
3586 }),
3587 downloading: false,
3588 downloaded: true,
3589 webxdc_topic: Some("game-state".into()),
3590 group_id: Some("cccc000000000000000000000000000000000000000000000000000000000000".into()),
3591 original_hash: Some("dddd000000000000000000000000000000000000000000000000000000000000".into()),
3592 fallback_urls: Vec::new(),
3593 };
3594
3595 let compact = CompactAttachment::from_attachment(&att);
3596
3597 assert!(compact.img_meta.is_some());
3598 assert!(compact.group_id.is_some());
3599 assert!(compact.original_hash.is_some());
3600 assert!(compact.webxdc_topic.is_some());
3601
3602 let restored = compact.to_attachment();
3603 let meta = restored.img_meta.unwrap();
3604 assert_eq!(meta.thumbhash, "hash123");
3605 assert_eq!(meta.width, 1920);
3606 assert_eq!(meta.height, 1080);
3607 assert_eq!(restored.webxdc_topic, Some("game-state".into()));
3608 assert_eq!(restored.group_id.unwrap(), att.group_id.unwrap());
3609 assert_eq!(restored.original_hash.unwrap(), att.original_hash.unwrap());
3610 }
3611
3612 #[test]
3617 fn compact_reaction_from_reaction_roundtrip() {
3618 let reaction = Reaction {
3619 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3620 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3621 author_id: "npub1alice".into(),
3622 emoji: "+".into(),
3623 emoji_url: None,
3624 };
3625 let mut interner = NpubInterner::new();
3626 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3627 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3628
3629 assert_eq!(restored.id, reaction.id, "id mismatch");
3630 assert_eq!(restored.reference_id, reaction.reference_id, "reference_id mismatch");
3631 assert_eq!(restored.author_id, reaction.author_id, "author_id mismatch");
3632 assert_eq!(restored.emoji, reaction.emoji, "emoji mismatch");
3633 }
3634
3635 #[test]
3636 fn compact_reaction_author_resolved_via_interner() {
3637 let mut interner = NpubInterner::new();
3638 let alice_handle = interner.intern("npub1alice");
3639
3640 let reaction = Reaction {
3641 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3642 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3643 author_id: "npub1alice".into(),
3644 emoji: "+".into(),
3645 emoji_url: None,
3646 };
3647 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3648 assert_eq!(compact.author_idx, alice_handle, "should reuse existing interner handle");
3649
3650 let resolved = interner.resolve(compact.author_idx).unwrap();
3651 assert_eq!(resolved, "npub1alice");
3652 }
3653
3654 #[test]
3655 fn compact_reaction_unicode_emoji() {
3656 let reaction = Reaction {
3657 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3658 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3659 author_id: "npub1test".into(),
3660 emoji: "\u{1f431}\u{200d}\u{1f4bb}".into(), emoji_url: None,
3662 };
3663 let mut interner = NpubInterner::new();
3664 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3665 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3666 assert_eq!(restored.emoji, "\u{1f431}\u{200d}\u{1f4bb}", "complex unicode emoji should roundtrip");
3667 }
3668
3669 #[test]
3670 fn compact_reaction_custom_emoji() {
3671 let reaction = Reaction {
3672 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3673 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3674 author_id: "npub1test".into(),
3675 emoji: ":cat_heart_eyes:".into(),
3676 emoji_url: None,
3677 };
3678 let mut interner = NpubInterner::new();
3679 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3680 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction.reference_id), &interner);
3681 assert_eq!(restored.emoji, ":cat_heart_eyes:", "custom emoji shortcode should roundtrip");
3682 }
3683
3684 #[test]
3685 fn compact_reaction_owned_conversion() {
3686 let reaction = Reaction {
3687 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3688 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3689 author_id: "npub1bob".into(),
3690 emoji: "\u{1f44d}".into(), emoji_url: None,
3692 };
3693 let reaction_clone = reaction.clone();
3694 let mut interner = NpubInterner::new();
3695 let compact = CompactReaction::from_reaction_owned(reaction, &mut interner);
3696 let restored = compact.to_reaction(&hex_to_bytes_32(&reaction_clone.reference_id), &interner);
3697
3698 assert_eq!(restored.id, reaction_clone.id);
3699 assert_eq!(restored.author_id, reaction_clone.author_id);
3700 assert_eq!(restored.emoji, reaction_clone.emoji);
3701 }
3702
3703 #[test]
3708 fn tinyvec_empty() {
3709 let tv: TinyVec<u32> = TinyVec::new();
3710 assert!(tv.is_empty());
3711 assert_eq!(tv.len(), 0);
3712 assert_eq!(tv.as_slice(), &[] as &[u32]);
3713 assert_eq!(tv.first(), None);
3714 assert_eq!(tv.last(), None);
3715 }
3716
3717 #[test]
3718 fn tinyvec_from_vec_and_back() {
3719 let original = vec![1u32, 2, 3, 4, 5];
3720 let tv = TinyVec::from_vec(original.clone());
3721 assert_eq!(tv.len(), 5);
3722 assert_eq!(tv.to_vec(), original);
3723 }
3724
3725 #[test]
3726 fn tinyvec_indexing() {
3727 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3728 assert_eq!(tv[0], 10);
3729 assert_eq!(tv[1], 20);
3730 assert_eq!(tv[2], 30);
3731 assert_eq!(tv.get(0), Some(&10));
3732 assert_eq!(tv.get(3), None);
3733 }
3734
3735 #[test]
3736 fn tinyvec_push() {
3737 let mut tv = TinyVec::from_vec(vec![1u32, 2]);
3738 tv.push(3);
3739 assert_eq!(tv.len(), 3);
3740 assert_eq!(tv.to_vec(), vec![1, 2, 3]);
3741 }
3742
3743 #[test]
3744 fn tinyvec_clone() {
3745 let tv = TinyVec::from_vec(vec!["hello".to_string(), "world".to_string()]);
3746 let cloned = tv.clone();
3747 assert_eq!(cloned.len(), 2);
3748 assert_eq!(cloned[0], "hello");
3749 assert_eq!(cloned[1], "world");
3750 }
3751
3752 #[test]
3753 fn tinyvec_retain() {
3754 let mut tv = TinyVec::from_vec(vec![1u32, 2, 3, 4, 5]);
3755 tv.retain(|&x| x % 2 == 0);
3756 assert_eq!(tv.to_vec(), vec![2, 4]);
3757 }
3758
3759 #[test]
3760 fn tinyvec_empty_from_empty_vec() {
3761 let tv = TinyVec::<u32>::from_vec(vec![]);
3762 assert!(tv.is_empty());
3763 assert_eq!(tv.len(), 0);
3764 }
3765
3766 #[test]
3767 fn tinyvec_iter() {
3768 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3769 let sum: u32 = tv.iter().sum();
3770 assert_eq!(sum, 60);
3771 }
3772
3773 #[test]
3774 fn tinyvec_any() {
3775 let tv = TinyVec::from_vec(vec![1u32, 2, 3]);
3776 assert!(tv.any(|&x| x == 2));
3777 assert!(!tv.any(|&x| x == 99));
3778 }
3779
3780 #[test]
3785 fn hex_to_bytes_16_full_32_chars() {
3786 let hex = "aabbccddaabbccddaabbccddaabbccdd";
3787 let bytes = hex_to_bytes_16(hex);
3788 assert_eq!(bytes[0], 0xaa);
3789 assert_eq!(bytes[1], 0xbb);
3790 assert_eq!(bytes[15], 0xdd);
3791 }
3792
3793 #[test]
3794 fn hex_to_bytes_16_short_input_padded() {
3795 let hex = "aabb";
3797 let bytes = hex_to_bytes_16(hex);
3798 assert_eq!(bytes[0], 0xaa);
3799 assert_eq!(bytes[1], 0xbb);
3800 for i in 2..16 {
3802 assert_eq!(bytes[i], 0, "byte {} should be 0 from padding", i);
3803 }
3804 }
3805
3806 #[test]
3807 fn bytes_to_hex_string_roundtrip() {
3808 let bytes: Vec<u8> = vec![0xaa, 0xbb, 0xcc, 0xdd];
3809 let hex = bytes_to_hex_string(&bytes);
3810 assert_eq!(hex, "aabbccdd");
3811 }
3812}