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 reference_id: [u8; 32],
542 pub author_idx: u16,
544 pub emoji: Box<str>,
546 pub emoji_url: Option<Box<str>>,
549}
550
551impl CompactReaction {
552 #[inline]
554 pub fn id_hex(&self) -> String {
555 bytes_to_hex_32(&self.id)
556 }
557
558 #[inline]
560 pub fn reference_id_hex(&self) -> String {
561 bytes_to_hex_32(&self.reference_id)
562 }
563
564 pub fn from_reaction(reaction: &Reaction, interner: &mut NpubInterner) -> Self {
566 Self {
567 id: hex_to_bytes_32(&reaction.id),
568 reference_id: hex_to_bytes_32(&reaction.reference_id),
569 author_idx: interner.intern(&reaction.author_id),
570 emoji: reaction.emoji.clone().into_boxed_str(),
571 emoji_url: reaction.emoji_url.as_deref().map(|s| s.into()),
572 }
573 }
574
575 pub fn from_reaction_owned(reaction: Reaction, interner: &mut NpubInterner) -> Self {
577 Self {
578 id: hex_to_bytes_32(&reaction.id),
579 reference_id: hex_to_bytes_32(&reaction.reference_id),
580 author_idx: interner.intern(&reaction.author_id),
581 emoji: reaction.emoji.into_boxed_str(),
582 emoji_url: reaction.emoji_url.map(|s| s.into_boxed_str()),
583 }
584 }
585
586 pub fn to_reaction(&self, interner: &NpubInterner) -> Reaction {
588 Reaction {
589 id: self.id_hex(),
590 reference_id: self.reference_id_hex(),
591 author_id: interner.resolve(self.author_idx)
592 .map(|s| s.to_string())
593 .unwrap_or_default(),
594 emoji: self.emoji.to_string(),
595 emoji_url: self.emoji_url.as_deref().map(|s| s.to_string()),
596 }
597 }
598}
599
600#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
606pub struct AttachmentFlags(u8);
607
608impl AttachmentFlags {
609 pub const NONE: Self = Self(0);
610 const DOWNLOADING: u8 = 0b0001;
611 const DOWNLOADED: u8 = 0b0010;
612 const SHORT_NONCE: u8 = 0b0100; #[inline]
615 pub fn is_downloading(self) -> bool { self.0 & Self::DOWNLOADING != 0 }
616 #[inline]
617 pub fn is_downloaded(self) -> bool { self.0 & Self::DOWNLOADED != 0 }
618 #[inline]
619 pub fn is_short_nonce(self) -> bool { self.0 & Self::SHORT_NONCE != 0 }
620
621 #[inline]
622 pub fn set_downloading(&mut self, value: bool) {
623 if value { self.0 |= Self::DOWNLOADING; } else { self.0 &= !Self::DOWNLOADING; }
624 }
625 #[inline]
626 pub fn set_downloaded(&mut self, value: bool) {
627 if value { self.0 |= Self::DOWNLOADED; } else { self.0 &= !Self::DOWNLOADED; }
628 }
629 #[inline]
630 pub fn set_short_nonce(&mut self, value: bool) {
631 if value { self.0 |= Self::SHORT_NONCE; } else { self.0 &= !Self::SHORT_NONCE; }
632 }
633
634 pub fn from_bools(downloading: bool, downloaded: bool) -> Self {
635 let mut flags = Self::NONE;
636 flags.set_downloading(downloading);
637 flags.set_downloaded(downloaded);
638 flags
639 }
640}
641
642#[derive(Clone, Debug)]
653pub struct CompactAttachment {
654 pub id: [u8; 32],
657 pub key: [u8; 32],
659 pub nonce: [u8; 16],
661 pub size: u64,
663 pub flags: AttachmentFlags,
665
666 pub extension: Box<str>,
669 pub url: Box<str>,
671 pub path: Box<str>,
673
674 pub img_meta: Option<Box<ImageMetadata>>,
677 pub group_id: Option<Box<[u8; 32]>>,
679 pub original_hash: Option<Box<[u8; 32]>>,
681 pub webxdc_topic: Option<Box<str>>,
683 pub mls_filename: Option<Box<str>>,
685 pub scheme_version: Option<Box<str>>,
687 pub name: String,
689}
690
691impl CompactAttachment {
692 #[inline]
694 pub fn downloaded(&self) -> bool { self.flags.is_downloaded() }
695 #[inline]
696 pub fn downloading(&self) -> bool { self.flags.is_downloading() }
697 #[inline]
698 pub fn set_downloaded(&mut self, value: bool) { self.flags.set_downloaded(value); }
699 #[inline]
700 pub fn set_downloading(&mut self, value: bool) { self.flags.set_downloading(value); }
701
702 #[inline]
704 pub fn id_eq(&self, hex_id: &str) -> bool {
705 self.id == hex_to_bytes_32(hex_id)
706 }
707
708 #[inline]
710 pub fn id_hex(&self) -> String {
711 bytes_to_hex_32(&self.id)
712 }
713
714 pub fn key_hex(&self) -> String {
716 if self.key == [0u8; 32] {
717 String::new()
718 } else {
719 bytes_to_hex_32(&self.key)
720 }
721 }
722
723 pub fn nonce_hex(&self) -> String {
725 if self.nonce == [0u8; 16] {
726 String::new()
727 } else if self.flags.is_short_nonce() {
728 bytes_to_hex_string(&self.nonce[..12])
730 } else {
731 bytes_to_hex_string(&self.nonce)
733 }
734 }
735
736 pub fn from_attachment(att: &Attachment) -> Self {
738 let is_short_nonce = att.nonce.len() == 24;
740 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
741 flags.set_short_nonce(is_short_nonce);
742
743 Self {
744 id: hex_to_bytes_32(&att.id),
745 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
746 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
747 size: att.size,
748 flags,
749 extension: att.extension.clone().into_boxed_str(),
750 url: att.url.clone().into_boxed_str(),
751 path: att.path.clone().into_boxed_str(),
752 img_meta: att.img_meta.clone().map(Box::new),
753 group_id: att.group_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
754 original_hash: att.original_hash.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
755 webxdc_topic: att.webxdc_topic.clone().map(|s| s.into_boxed_str()),
756 mls_filename: att.mls_filename.clone().map(|s| s.into_boxed_str()),
757 scheme_version: att.scheme_version.clone().map(|s| s.into_boxed_str()),
758 name: att.name.clone(),
759 }
760 }
761
762 pub fn from_attachment_owned(att: Attachment) -> Self {
764 let is_short_nonce = att.nonce.len() == 24;
766 let mut flags = AttachmentFlags::from_bools(att.downloading, att.downloaded);
767 flags.set_short_nonce(is_short_nonce);
768
769 Self {
770 id: hex_to_bytes_32(&att.id),
771 key: if att.key.is_empty() { [0u8; 32] } else { hex_to_bytes_32(&att.key) },
772 nonce: if att.nonce.is_empty() { [0u8; 16] } else { parse_nonce(&att.nonce) },
773 size: att.size,
774 flags,
775 extension: att.extension.into_boxed_str(),
776 url: att.url.into_boxed_str(),
777 path: att.path.into_boxed_str(),
778 img_meta: att.img_meta.map(Box::new),
779 group_id: att.group_id.map(|s| Box::new(hex_to_bytes_32(&s))),
780 original_hash: att.original_hash.map(|s| Box::new(hex_to_bytes_32(&s))),
781 webxdc_topic: att.webxdc_topic.map(|s| s.into_boxed_str()),
782 mls_filename: att.mls_filename.map(|s| s.into_boxed_str()),
783 scheme_version: att.scheme_version.map(|s| s.into_boxed_str()),
784 name: att.name,
785 }
786 }
787
788 pub fn to_attachment(&self) -> Attachment {
790 Attachment {
791 id: self.id_hex(),
792 key: self.key_hex(),
793 nonce: self.nonce_hex(),
794 extension: self.extension.to_string(),
795 name: self.name.clone(),
796 url: self.url.to_string(),
797 path: self.path.to_string(),
798 size: self.size,
799 img_meta: self.img_meta.as_ref().map(|b| (**b).clone()),
800 downloading: self.flags.is_downloading(),
801 downloaded: self.flags.is_downloaded(),
802 webxdc_topic: self.webxdc_topic.as_ref().map(|s| s.to_string()),
803 group_id: self.group_id.as_ref().map(|b| bytes_to_hex_32(b)),
804 original_hash: self.original_hash.as_ref().map(|b| bytes_to_hex_32(b)),
805 scheme_version: self.scheme_version.as_ref().map(|s| s.to_string()),
806 mls_filename: self.mls_filename.as_ref().map(|s| s.to_string()),
807 }
808 }
809}
810
811#[inline]
815fn parse_nonce(hex: &str) -> [u8; 16] {
816 hex_to_bytes_16(hex)
817}
818
819#[derive(Clone, Debug, Default)]
831pub struct NpubInterner {
832 npubs: Vec<String>,
834 sorted: Vec<u16>,
836}
837
838pub const NO_NPUB: u16 = u16::MAX;
840
841impl NpubInterner {
842 pub fn new() -> Self {
843 Self {
844 npubs: Vec::new(),
845 sorted: Vec::new(),
846 }
847 }
848
849 pub fn with_capacity(capacity: usize) -> Self {
851 Self {
852 npubs: Vec::with_capacity(capacity),
853 sorted: Vec::with_capacity(capacity),
854 }
855 }
856
857 pub fn intern(&mut self, npub: &str) -> u16 {
862 let result = self.sorted.binary_search_by(|&idx| {
864 self.npubs[idx as usize].as_str().cmp(npub)
865 });
866
867 match result {
868 Ok(pos) => self.sorted[pos], Err(insert_pos) => {
870 if self.npubs.len() >= NO_NPUB as usize {
873 return NO_NPUB;
874 }
875 let new_idx = self.npubs.len() as u16;
877 self.npubs.push(npub.to_string());
878 self.sorted.insert(insert_pos, new_idx);
879 new_idx
880 }
881 }
882 }
883
884 #[inline]
886 pub fn intern_opt(&mut self, npub: Option<&str>) -> u16 {
887 match npub {
888 Some(s) if !s.is_empty() => self.intern(s),
889 _ => NO_NPUB,
890 }
891 }
892
893 pub fn lookup(&self, npub: &str) -> Option<u16> {
895 self.sorted.binary_search_by(|&idx| {
896 self.npubs[idx as usize].as_str().cmp(npub)
897 }).ok().map(|pos| self.sorted[pos])
898 }
899
900 #[inline]
904 pub fn resolve(&self, idx: u16) -> Option<&str> {
905 if idx == NO_NPUB {
906 return None;
907 }
908 self.npubs.get(idx as usize).map(|s| s.as_str())
909 }
910
911 #[inline]
913 pub fn len(&self) -> usize {
914 self.npubs.len()
915 }
916
917 #[inline]
918 pub fn is_empty(&self) -> bool {
919 self.npubs.is_empty()
920 }
921
922 pub fn memory_usage(&self) -> usize {
924 std::mem::size_of::<Self>()
925 + self.npubs.capacity() * std::mem::size_of::<String>()
926 + self.npubs.iter().map(|s| s.capacity()).sum::<usize>()
927 + self.sorted.capacity() * std::mem::size_of::<u16>()
928 }
929}
930
931#[derive(Clone, Debug)]
947pub struct CompactMessage {
948 pub id: [u8; 32],
950 pub at: u64,
952 pub expiration_secs: u32,
956 pub flags: MessageFlags,
958 pub npub_idx: u16,
960 pub replied_to: Option<Box<[u8; 32]>>,
962 pub replied_to_npub_idx: u16,
964 pub wrapper_id: Option<Box<[u8; 32]>>,
966
967 pub content: Box<str>,
970 pub replied_to_content: Option<Box<str>>,
972 pub attachments: TinyVec<CompactAttachment>,
974 pub reactions: TinyVec<CompactReaction>,
976 #[allow(clippy::box_collection)]
978 pub edit_history: Option<Box<Vec<EditEntry>>>,
979 pub preview_metadata: Option<Box<SiteMetadata>>,
981 #[allow(clippy::box_collection)]
984 pub emoji_tags: Option<Box<Vec<crate::types::EmojiTag>>>,
985 #[allow(clippy::box_collection)]
988 pub addressed_bots: Option<Box<Vec<u16>>>,
989}
990
991impl CompactMessage {
992 #[inline]
994 pub fn has_reply(&self) -> bool {
995 self.replied_to.is_some()
996 }
997
998 #[inline]
1000 pub fn is_edited(&self) -> bool {
1001 self.edit_history.is_some()
1002 }
1003
1004 #[inline]
1006 pub fn id_hex(&self) -> String {
1007 decode_message_id(&self.id)
1008 }
1009
1010 #[inline]
1012 pub fn replied_to_hex(&self) -> String {
1013 match &self.replied_to {
1014 Some(id) => bytes_to_hex_32(id),
1015 None => String::new(),
1016 }
1017 }
1018
1019 #[inline]
1021 pub fn wrapper_id_hex(&self) -> Option<String> {
1022 self.wrapper_id.as_ref().map(|id| bytes_to_hex_32(id))
1023 }
1024
1025 #[inline]
1027 pub fn timestamp_ms(&self) -> u64 {
1028 timestamp_from_compact(self.at)
1029 }
1030
1031 pub fn apply_edit(&mut self, new_content: String, edited_at: u64, emoji_tags: Vec<crate::types::EmojiTag>) {
1037 if self.edit_history.is_none() {
1039 self.edit_history = Some(Box::new(vec![EditEntry {
1040 content: self.content.to_string(),
1041 edited_at: self.timestamp_ms(), }]));
1043 }
1044
1045 let mut is_latest = true;
1046 if let Some(ref mut history) = self.edit_history {
1047 if history.iter().any(|e| e.edited_at == edited_at) {
1049 return;
1050 }
1051
1052 history.push(EditEntry {
1054 content: new_content.clone(),
1055 edited_at,
1056 });
1057
1058 history.sort_by_key(|e| e.edited_at);
1060 is_latest = history.last().map(|e| e.edited_at == edited_at).unwrap_or(true);
1061 }
1062
1063 if is_latest {
1065 self.content = new_content.into_boxed_str();
1066 self.emoji_tags = if emoji_tags.is_empty() { None } else { Some(Box::new(emoji_tags)) };
1067 }
1068 }
1069
1070 #[inline]
1072 pub fn replied_to_has_attachment(&self) -> Option<bool> {
1073 self.flags.replied_to_has_attachment()
1074 }
1075
1076 pub fn add_reaction(&mut self, reaction: Reaction, interner: &mut NpubInterner) -> bool {
1079 let reaction_id = hex_to_bytes_32(&reaction.id);
1081
1082 if self.reactions.iter().any(|r| r.id == reaction_id) {
1084 return false;
1085 }
1086
1087 let compact = CompactReaction::from_reaction_owned(reaction, interner);
1089 let mut reactions = self.reactions.to_vec();
1090 reactions.push(compact);
1091 self.reactions = TinyVec::from_vec(reactions);
1092 true
1093 }
1094
1095 pub fn remove_reaction(&mut self, reaction_id: &str) -> bool {
1097 let target = hex_to_bytes_32(reaction_id);
1098 if !self.reactions.iter().any(|r| r.id == target) {
1099 return false;
1100 }
1101 let mut reactions = self.reactions.to_vec();
1102 reactions.retain(|r| r.id != target);
1103 self.reactions = TinyVec::from_vec(reactions);
1104 true
1105 }
1106
1107 #[inline]
1109 pub fn is_mine(&self) -> bool { self.flags.is_mine() }
1110 #[inline]
1111 pub fn is_pending(&self) -> bool { self.flags.is_pending() }
1112 #[inline]
1113 pub fn is_failed(&self) -> bool { self.flags.is_failed() }
1114
1115 #[inline]
1117 pub fn set_pending(&mut self, value: bool) { self.flags.set_pending(value); }
1118 #[inline]
1119 pub fn set_failed(&mut self, value: bool) { self.flags.set_failed(value); }
1120 #[inline]
1121 pub fn set_mine(&mut self, value: bool) { self.flags.set_mine(value); }
1122}
1123
1124#[derive(Clone, Debug, Default)]
1133pub struct CompactMessageVec {
1134 messages: Vec<CompactMessage>,
1136 id_index: Vec<([u8; 32], u32)>,
1138}
1139
1140impl CompactMessageVec {
1141 pub fn new() -> Self {
1142 Self {
1143 messages: Vec::new(),
1144 id_index: Vec::new(),
1145 }
1146 }
1147
1148 pub fn with_capacity(capacity: usize) -> Self {
1149 Self {
1150 messages: Vec::with_capacity(capacity),
1151 id_index: Vec::with_capacity(capacity),
1152 }
1153 }
1154
1155 #[inline]
1157 pub fn len(&self) -> usize {
1158 self.messages.len()
1159 }
1160
1161 #[inline]
1162 pub fn is_empty(&self) -> bool {
1163 self.messages.is_empty()
1164 }
1165
1166 #[inline]
1168 pub fn messages(&self) -> &[CompactMessage] {
1169 &self.messages
1170 }
1171
1172 #[inline]
1174 pub fn messages_mut(&mut self) -> &mut Vec<CompactMessage> {
1175 &mut self.messages
1176 }
1177
1178 #[inline]
1180 pub fn iter(&self) -> std::slice::Iter<'_, CompactMessage> {
1181 self.messages.iter()
1182 }
1183
1184 #[inline]
1186 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, CompactMessage> {
1187 self.messages.iter_mut()
1188 }
1189
1190 #[inline]
1192 pub fn last(&self) -> Option<&CompactMessage> {
1193 self.messages.last()
1194 }
1195
1196 #[inline]
1198 pub fn last_timestamp(&self) -> Option<u64> {
1199 self.messages.last().map(|m| timestamp_from_compact(m.at))
1200 }
1201
1202 #[inline]
1204 pub fn first(&self) -> Option<&CompactMessage> {
1205 self.messages.first()
1206 }
1207
1208 pub fn find_by_id(&self, id: &[u8; 32]) -> Option<&CompactMessage> {
1210 let pos = self.id_index
1211 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1212 .ok()?;
1213 let msg_pos = self.id_index[pos].1 as usize;
1214 self.messages.get(msg_pos)
1215 }
1216
1217 pub fn find_by_id_mut(&mut self, id: &[u8; 32]) -> Option<&mut CompactMessage> {
1219 let pos = self.id_index
1220 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1221 .ok()?;
1222 let msg_pos = self.id_index[pos].1 as usize;
1223 self.messages.get_mut(msg_pos)
1224 }
1225
1226 pub fn find_by_hex_id(&self, id_str: &str) -> Option<&CompactMessage> {
1228 if id_str.is_empty() {
1229 return None;
1230 }
1231 let id = encode_message_id(id_str);
1232 self.find_by_id(&id)
1233 }
1234
1235 pub fn find_by_hex_id_mut(&mut self, id_str: &str) -> Option<&mut CompactMessage> {
1237 if id_str.is_empty() {
1238 return None;
1239 }
1240 let id = encode_message_id(id_str);
1241 self.find_by_id_mut(&id)
1242 }
1243
1244 pub fn contains_id(&self, id: &[u8; 32]) -> bool {
1246 self.id_index
1247 .binary_search_by(|(idx_id, _)| idx_id.cmp(id))
1248 .is_ok()
1249 }
1250
1251 pub fn remove_by_hex_id(&mut self, id_str: &str) -> bool {
1253 if id_str.is_empty() {
1254 return false;
1255 }
1256 let id = encode_message_id(id_str);
1257 let idx_pos = match self.id_index.binary_search_by(|(idx_id, _)| idx_id.cmp(&id)) {
1259 Ok(pos) => pos,
1260 Err(_) => return false,
1261 };
1262 let msg_pos = self.id_index[idx_pos].1 as usize;
1263 self.messages.remove(msg_pos);
1265 self.rebuild_index();
1267 true
1268 }
1269
1270 pub fn contains_hex_id(&self, id_str: &str) -> bool {
1272 if id_str.is_empty() {
1273 return false;
1274 }
1275 let id = encode_message_id(id_str);
1276 self.contains_id(&id)
1277 }
1278
1279 pub fn insert(&mut self, msg: CompactMessage) -> bool {
1285 if self.contains_id(&msg.id) {
1287 return false;
1288 }
1289
1290 let msg_id = msg.id;
1291
1292 if self.messages.last().is_none_or(|last| msg.at >= last.at) {
1295 let msg_pos = self.messages.len() as u32;
1296 self.messages.push(msg);
1297
1298 let idx_pos = self.id_index
1301 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1302 .unwrap_err();
1303 self.id_index.insert(idx_pos, (msg_id, msg_pos));
1304
1305 return true;
1306 }
1307
1308 let msg_pos = match self.messages.binary_search_by(|m| m.at.cmp(&msg.at)) {
1311 Ok(pos) => pos,
1312 Err(pos) => pos,
1313 };
1314
1315 for (_, pos) in &mut self.id_index {
1317 if *pos >= msg_pos as u32 {
1318 *pos += 1;
1319 }
1320 }
1321
1322 self.messages.insert(msg_pos, msg);
1324
1325 let idx_pos = self.id_index
1327 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1328 .unwrap_err();
1329 self.id_index.insert(idx_pos, (msg_id, msg_pos as u32));
1330
1331 true
1332 }
1333
1334 pub fn rebuild_index(&mut self) {
1336 self.id_index.clear();
1337 self.id_index.reserve(self.messages.len());
1338 for (pos, msg) in self.messages.iter().enumerate() {
1339 self.id_index.push((msg.id, pos as u32));
1340 }
1341 self.id_index.sort_by(|(a, _), (b, _)| a.cmp(b));
1342 }
1343
1344 pub fn insert_batch(&mut self, messages: impl IntoIterator<Item = CompactMessage>) -> usize {
1353 let messages: Vec<_> = messages.into_iter().collect();
1354 if messages.is_empty() {
1355 return 0;
1356 }
1357
1358 let mut to_add: Vec<CompactMessage> = Vec::with_capacity(messages.len());
1360 for msg in messages {
1361 if !self.contains_id(&msg.id) {
1362 to_add.push(msg);
1363 }
1364 }
1365
1366 if to_add.is_empty() {
1367 return 0;
1368 }
1369
1370 let added = to_add.len();
1371
1372 let our_first = self.messages.first().map(|m| m.at);
1374 let our_last = self.messages.last().map(|m| m.at);
1375 let their_min = to_add.iter().map(|m| m.at).min().unwrap();
1376 let their_max = to_add.iter().map(|m| m.at).max().unwrap();
1377
1378 if self.messages.is_empty() {
1379 self.messages = to_add;
1381 self.messages.sort_by_key(|m| m.at);
1382 self.rebuild_index();
1383 } else if their_min >= our_last.unwrap() {
1384 to_add.sort_by_key(|m| m.at);
1386 let base_pos = self.messages.len() as u32;
1387 for (i, msg) in to_add.into_iter().enumerate() {
1388 let msg_id = msg.id;
1389 self.messages.push(msg);
1390 let idx_pos = self.id_index
1392 .binary_search_by(|(id, _)| id.cmp(&msg_id))
1393 .unwrap_err();
1394 self.id_index.insert(idx_pos, (msg_id, base_pos + i as u32));
1395 }
1396 } else if their_max <= our_first.unwrap() {
1397 to_add.sort_by_key(|m| m.at);
1399 let prepend_count = to_add.len();
1400
1401 for (_, pos) in &mut self.id_index {
1403 *pos += prepend_count as u32;
1404 }
1405
1406 let mut new_index_entries: Vec<_> = to_add.iter()
1408 .enumerate()
1409 .map(|(i, msg)| (msg.id, i as u32))
1410 .collect();
1411 new_index_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
1412
1413 let old_index = std::mem::take(&mut self.id_index);
1415 self.id_index.reserve(old_index.len() + new_index_entries.len());
1416
1417 let mut old_iter = old_index.into_iter().peekable();
1418 let mut new_iter = new_index_entries.into_iter().peekable();
1419
1420 while old_iter.peek().is_some() || new_iter.peek().is_some() {
1421 match (old_iter.peek(), new_iter.peek()) {
1422 (Some((old_id, _)), Some((new_id, _))) => {
1423 if old_id < new_id {
1424 self.id_index.push(old_iter.next().unwrap());
1425 } else {
1426 self.id_index.push(new_iter.next().unwrap());
1427 }
1428 }
1429 (Some(_), None) => self.id_index.push(old_iter.next().unwrap()),
1430 (None, Some(_)) => self.id_index.push(new_iter.next().unwrap()),
1431 (None, None) => break,
1432 }
1433 }
1434
1435 let mut new_messages = to_add;
1437 new_messages.append(&mut self.messages);
1438 self.messages = new_messages;
1439 } else {
1440 self.messages.extend(to_add);
1442 self.messages.sort_by_key(|m| m.at);
1443 self.rebuild_index();
1444 }
1445
1446 added
1447 }
1448
1449 pub fn memory_usage(&self) -> usize {
1451 std::mem::size_of::<Self>()
1452 + self.messages.capacity() * std::mem::size_of::<CompactMessage>()
1453 + self.id_index.capacity() * std::mem::size_of::<([u8; 32], u32)>()
1454 }
1456
1457 pub fn drain(&mut self, range: std::ops::Range<usize>) -> std::vec::Drain<'_, CompactMessage> {
1459 let drain = self.messages.drain(range);
1460 drain
1462 }
1463
1464 pub fn sort_by_key<K, F>(&mut self, f: F)
1466 where
1467 F: FnMut(&CompactMessage) -> K,
1468 K: Ord,
1469 {
1470 self.messages.sort_by_key(f);
1471 self.rebuild_index();
1472 }
1473
1474 pub fn clear(&mut self) {
1476 self.messages.clear();
1477 self.id_index.clear();
1478 }
1479}
1480
1481use crate::types::Message;
1486
1487impl CompactMessage {
1488 pub fn from_message(msg: &Message, interner: &mut NpubInterner) -> Self {
1490 Self {
1491 id: encode_message_id(&msg.id),
1492 at: timestamp_to_compact(msg.at),
1493 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1494 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1495 replied_to: if msg.replied_to.is_empty() {
1497 None
1498 } else {
1499 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1500 },
1501 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1502 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1504 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1505 content: msg.content.clone().into_boxed_str(),
1507 replied_to_content: msg.replied_to_content.as_ref().map(|s| s.clone().into_boxed_str()),
1508 attachments: TinyVec::from_vec(
1510 msg.attachments.iter()
1511 .map(CompactAttachment::from_attachment)
1512 .collect()
1513 ),
1514 reactions: TinyVec::from_vec(
1516 msg.reactions.iter()
1517 .map(|r| CompactReaction::from_reaction(r, interner))
1518 .collect()
1519 ),
1520 edit_history: msg.edit_history.clone().map(Box::new),
1522 preview_metadata: msg.preview_metadata.clone().map(Box::new),
1523 emoji_tags: if msg.emoji_tags.is_empty() {
1524 None
1525 } else {
1526 Some(Box::new(msg.emoji_tags.clone()))
1527 },
1528 addressed_bots: if msg.addressed_bots.is_empty() {
1529 None
1530 } else {
1531 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1532 },
1533 }
1534 }
1535
1536 pub fn from_message_owned(msg: Message, interner: &mut NpubInterner) -> Self {
1541 Self {
1542 id: encode_message_id(&msg.id),
1543 at: timestamp_to_compact(msg.at),
1544 flags: MessageFlags::from_all(msg.mine, msg.pending, msg.failed, msg.replied_to_has_attachment),
1545 npub_idx: interner.intern_opt(msg.npub.as_deref()),
1546 replied_to: if msg.replied_to.is_empty() {
1548 None
1549 } else {
1550 Some(Box::new(hex_to_bytes_32(&msg.replied_to)))
1551 },
1552 replied_to_npub_idx: interner.intern_opt(msg.replied_to_npub.as_deref()),
1553 wrapper_id: msg.wrapper_event_id.as_ref().map(|s| Box::new(hex_to_bytes_32(s))),
1555 expiration_secs: msg.expiration.map(|e| e as u32).unwrap_or(0),
1556 content: msg.content.into_boxed_str(),
1558 replied_to_content: msg.replied_to_content.map(|s| s.into_boxed_str()),
1559 attachments: TinyVec::from_vec(
1561 msg.attachments.into_iter()
1562 .map(CompactAttachment::from_attachment_owned)
1563 .collect()
1564 ),
1565 reactions: TinyVec::from_vec(
1567 msg.reactions.into_iter()
1568 .map(|r| CompactReaction::from_reaction_owned(r, interner))
1569 .collect()
1570 ),
1571 edit_history: msg.edit_history.map(Box::new),
1573 preview_metadata: msg.preview_metadata.map(Box::new),
1574 emoji_tags: if msg.emoji_tags.is_empty() {
1575 None
1576 } else {
1577 Some(Box::new(msg.emoji_tags))
1578 },
1579 addressed_bots: if msg.addressed_bots.is_empty() {
1580 None
1581 } else {
1582 Some(Box::new(msg.addressed_bots.iter().map(|n| interner.intern(n)).collect()))
1583 },
1584 }
1585 }
1586
1587 pub fn to_message(&self, interner: &NpubInterner) -> Message {
1589 Message {
1590 id: self.id_hex(),
1591 at: self.timestamp_ms(), expiration: if self.expiration_secs == 0 { None } else { Some(self.expiration_secs as u64) },
1593 mine: self.flags.is_mine(),
1594 pending: self.flags.is_pending(),
1595 failed: self.flags.is_failed(),
1596 edited: self.is_edited(),
1597 npub: interner.resolve(self.npub_idx).map(|s| s.to_string()),
1598 replied_to: self.replied_to_hex(),
1599 replied_to_content: self.replied_to_content.as_ref().map(|s| s.to_string()),
1600 replied_to_npub: interner.resolve(self.replied_to_npub_idx).map(|s| s.to_string()),
1601 replied_to_has_attachment: self.flags.replied_to_has_attachment(),
1602 replied_to_attachment_extension: None,
1605 wrapper_event_id: self.wrapper_id_hex(),
1606 content: self.content.to_string(),
1607 attachments: self.attachments.iter()
1609 .map(|a| a.to_attachment())
1610 .collect(),
1611 reactions: self.reactions.iter()
1613 .map(|r| r.to_reaction(interner))
1614 .collect(),
1615 edit_history: self.edit_history.as_ref().map(|b| (**b).clone()),
1617 preview_metadata: self.preview_metadata.as_ref().map(|b| (**b).clone()),
1618 emoji_tags: self.emoji_tags.as_ref().map(|b| (**b).clone()).unwrap_or_default(),
1619 addressed_bots: self
1620 .addressed_bots
1621 .as_ref()
1622 .map(|b| b.iter().filter_map(|&i| interner.resolve(i).map(|s| s.to_string())).collect())
1623 .unwrap_or_default(),
1624 }
1625 }
1626}
1627
1628#[cfg(test)]
1629mod tests {
1630 use super::*;
1631
1632 #[test]
1636 fn real_id_starting_with_marker_byte_roundtrips() {
1637 let id = "01b95c05179c6d6abbc60b9a35198fdee6e0ce5b80a0b7ac8d465d81d038a73d";
1638 let encoded = encode_message_id(id);
1639 assert_eq!(decode_message_id(&encoded), id);
1640 }
1641
1642 #[test]
1643 fn pending_id_roundtrips() {
1644 let id = "pending-306878031314";
1645 let encoded = encode_message_id(id);
1646 assert_eq!(encoded[0], PENDING_ID_MARKER);
1647 assert_eq!(decode_message_id(&encoded), id);
1648 }
1649
1650 #[test]
1651 fn marker_byte_without_sentinel_decodes_as_hex() {
1652 let mut bytes = [0xABu8; 32];
1654 bytes[0] = PENDING_ID_MARKER;
1655 let decoded = decode_message_id(&bytes);
1656 assert!(!decoded.starts_with("pending-"));
1657 assert_eq!(decoded.len(), 64);
1658 }
1659
1660 #[test]
1661 fn test_message_flags() {
1662 let mut flags = MessageFlags::NONE;
1663 assert!(!flags.is_mine());
1664 assert!(!flags.is_pending());
1665 assert!(!flags.is_failed());
1666
1667 flags.set_mine(true);
1668 assert!(flags.is_mine());
1669
1670 flags.set_pending(true);
1671 assert!(flags.is_pending());
1672 assert!(flags.is_mine()); flags.set_mine(false);
1675 assert!(!flags.is_mine());
1676 assert!(flags.is_pending()); }
1678
1679 #[test]
1680 fn test_npub_interner() {
1681 let mut interner = NpubInterner::new();
1682
1683 let idx1 = interner.intern("npub1alice");
1684 let idx2 = interner.intern("npub1bob");
1685 let idx3 = interner.intern("npub1alice"); assert_eq!(idx1, idx3); assert_ne!(idx1, idx2);
1689
1690 assert_eq!(interner.resolve(idx1), Some("npub1alice"));
1691 assert_eq!(interner.resolve(idx2), Some("npub1bob"));
1692 assert_eq!(interner.resolve(NO_NPUB), None);
1693 }
1694
1695 #[test]
1696 fn test_compact_message_vec_insert_and_find() {
1697 let mut vec = CompactMessageVec::new();
1698 let mut interner = NpubInterner::new();
1699
1700 let msg1 = CompactMessage {
1701 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000001"),
1702 at: 1000,
1703 expiration_secs: 0,
1704 flags: MessageFlags::NONE,
1705 npub_idx: interner.intern("npub1test"),
1706 replied_to: None,
1707 replied_to_npub_idx: NO_NPUB,
1708 wrapper_id: None,
1709 content: "First message".to_string().into_boxed_str(),
1710 replied_to_content: None,
1711 attachments: TinyVec::new(),
1712 reactions: TinyVec::new(),
1713 edit_history: None,
1714 preview_metadata: None, emoji_tags: None,
1716 addressed_bots: None,
1717 };
1718
1719 let msg2 = CompactMessage {
1720 id: hex_to_bytes_32("0000000000000000000000000000000000000000000000000000000000000002"),
1721 at: 2000,
1722 expiration_secs: 0,
1723 flags: MessageFlags::MINE,
1724 npub_idx: interner.intern("npub1me"),
1725 replied_to: None,
1726 replied_to_npub_idx: NO_NPUB,
1727 wrapper_id: None,
1728 content: "Second message".to_string().into_boxed_str(),
1729 replied_to_content: None,
1730 attachments: TinyVec::new(),
1731 reactions: TinyVec::new(),
1732 edit_history: None,
1733 preview_metadata: None, emoji_tags: None,
1735 addressed_bots: None,
1736 };
1737
1738 assert!(vec.insert(msg1));
1739 assert!(vec.insert(msg2));
1740 assert_eq!(vec.len(), 2);
1741
1742 let found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000001");
1744 assert!(found.is_some());
1745 assert_eq!(&*found.unwrap().content, "First message");
1746
1747 let not_found = vec.find_by_hex_id("0000000000000000000000000000000000000000000000000000000000000099");
1749 assert!(not_found.is_none());
1750 }
1751
1752 #[test]
1753 fn test_duplicate_insert_rejected() {
1754 let mut vec = CompactMessageVec::new();
1755
1756 let msg = CompactMessage {
1757 id: hex_to_bytes_32("abcd000000000000000000000000000000000000000000000000000000000000"),
1758 at: 1000,
1759 expiration_secs: 0,
1760 flags: MessageFlags::NONE,
1761 npub_idx: NO_NPUB,
1762 replied_to: None,
1763 replied_to_npub_idx: NO_NPUB,
1764 wrapper_id: None,
1765 content: "Test".to_string().into_boxed_str(),
1766 replied_to_content: None,
1767 attachments: TinyVec::new(),
1768 reactions: TinyVec::new(),
1769 edit_history: None,
1770 preview_metadata: None, emoji_tags: None,
1772 addressed_bots: None,
1773 };
1774
1775 assert!(vec.insert(msg.clone()));
1776 assert!(!vec.insert(msg)); assert_eq!(vec.len(), 1);
1778 }
1779
1780 #[test]
1782 fn benchmark_compact_vs_message() {
1783 use std::time::Instant;
1784
1785 const NUM_MESSAGES: usize = 10_000;
1786 const NUM_UNIQUE_USERS: usize = 50; println!("\n========================================");
1789 println!(" COMPACT MESSAGE BENCHMARK");
1790 println!(" {} messages, {} unique users", NUM_MESSAGES, NUM_UNIQUE_USERS);
1791 println!("========================================\n");
1792
1793 let users: Vec<String> = (0..NUM_UNIQUE_USERS)
1795 .map(|i| format!("npub1{:0>62}", i))
1796 .collect();
1797
1798 let messages: Vec<Message> = (0..NUM_MESSAGES)
1800 .map(|i| {
1801 let user_idx = i % NUM_UNIQUE_USERS;
1802 Message {
1803 expiration: None,
1804 id: format!("{:0>64x}", i),
1805 at: 1700000000000 + (i as u64 * 1000),
1806 mine: user_idx == 0,
1807 pending: false,
1808 failed: false,
1809 edited: false,
1810 npub: Some(users[user_idx].clone()),
1811 replied_to: if i > 0 && i % 5 == 0 {
1812 format!("{:0>64x}", i - 1)
1813 } else {
1814 String::new()
1815 },
1816 replied_to_content: if i > 0 && i % 5 == 0 {
1817 Some("Previous message content".to_string())
1818 } else {
1819 None
1820 },
1821 replied_to_npub: if i > 0 && i % 5 == 0 {
1822 Some(users[(i - 1) % NUM_UNIQUE_USERS].clone())
1823 } else {
1824 None
1825 },
1826 replied_to_has_attachment: None,
1827 replied_to_attachment_extension: None,
1828 wrapper_event_id: Some(format!("{:0>64x}", i + 1000000)),
1829 content: format!("This is message number {} with some typical content length.", i),
1830 attachments: vec![],
1831 reactions: vec![],
1832 edit_history: None,
1833 preview_metadata: None,
1834 emoji_tags: Vec::new(),
1835 addressed_bots: Vec::new(),
1836 }
1837 })
1838 .collect();
1839
1840 println!("--- STRUCT SIZES ---");
1842 println!(" Message struct: {} bytes", std::mem::size_of::<Message>());
1843 println!(" CompactMessage struct: {} bytes", std::mem::size_of::<CompactMessage>());
1844 println!(" Savings per struct: {} bytes ({:.1}%)",
1845 std::mem::size_of::<Message>().saturating_sub(std::mem::size_of::<CompactMessage>()),
1846 (1.0 - std::mem::size_of::<CompactMessage>() as f64 / std::mem::size_of::<Message>() as f64) * 100.0
1847 );
1848 println!();
1849
1850 let msg_heap_estimate: usize = messages.iter().map(|m| {
1852 m.id.capacity()
1853 + m.npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1854 + m.replied_to.capacity()
1855 + m.replied_to_content.as_ref().map(|s| s.capacity()).unwrap_or(0)
1856 + m.replied_to_npub.as_ref().map(|s| s.capacity()).unwrap_or(0)
1857 + m.wrapper_event_id.as_ref().map(|s| s.capacity()).unwrap_or(0)
1858 + m.content.capacity()
1859 }).sum();
1860 let msg_total = messages.len() * std::mem::size_of::<Message>() + msg_heap_estimate;
1861
1862 println!("--- INSERT BENCHMARK ---");
1864
1865 let mut interner = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1867 let mut compact_vec = CompactMessageVec::with_capacity(NUM_MESSAGES);
1868
1869 let insert_start = Instant::now();
1870 for msg in &messages {
1871 let compact = CompactMessage::from_message(msg, &mut interner);
1872 compact_vec.insert(compact);
1873 }
1874 let insert_elapsed = insert_start.elapsed();
1875
1876 println!(" Sequential insert (optimized append path):");
1877 println!(" {} messages in {:?}", NUM_MESSAGES, insert_elapsed);
1878 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / insert_elapsed.as_secs_f64());
1879 println!(" Per message: {:.3} us ({} ns)",
1880 insert_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1881 insert_elapsed.as_nanos() / NUM_MESSAGES as u128);
1882 println!();
1883
1884 let mut interner2 = NpubInterner::with_capacity(NUM_UNIQUE_USERS);
1886 let mut compact_vec2 = CompactMessageVec::with_capacity(NUM_MESSAGES);
1887
1888 let batch_start = Instant::now();
1889 let compact_messages: Vec<_> = messages.iter()
1890 .map(|msg| CompactMessage::from_message(msg, &mut interner2))
1891 .collect();
1892 let batch_added = compact_vec2.insert_batch(compact_messages);
1893 let batch_elapsed = batch_start.elapsed();
1894
1895 println!(" Batch insert (pagination/history load):");
1896 println!(" {} messages in {:?}", batch_added, batch_elapsed);
1897 println!(" Rate: {:.0} msgs/sec", NUM_MESSAGES as f64 / batch_elapsed.as_secs_f64());
1898 println!(" Per message: {:.3} us ({} ns)",
1899 batch_elapsed.as_micros() as f64 / NUM_MESSAGES as f64,
1900 batch_elapsed.as_nanos() / NUM_MESSAGES as u128);
1901 println!();
1902
1903 println!("--- MEMORY COMPARISON ---");
1905 let compact_heap_estimate: usize = compact_vec.iter().map(|m| {
1906 m.content.len() + m.replied_to_content.as_ref().map(|s| s.len()).unwrap_or(0)
1908 + m.attachments.len() * std::mem::size_of::<Attachment>() + if m.attachments.is_empty() { 0 } else { 1 }
1909 + m.reactions.len() * std::mem::size_of::<Reaction>() + if m.reactions.is_empty() { 0 } else { 1 }
1910 }).sum();
1911 let compact_struct_mem = compact_vec.len() * std::mem::size_of::<CompactMessage>();
1912 let compact_index_mem = compact_vec.len() * std::mem::size_of::<([u8; 32], u32)>();
1913 let interner_mem = interner.memory_usage();
1914 let compact_total = compact_struct_mem + compact_heap_estimate + compact_index_mem + interner_mem;
1915
1916 println!(" Regular Message storage:");
1917 println!(" Struct memory: {:>10} bytes", messages.len() * std::mem::size_of::<Message>());
1918 println!(" Heap (strings): {:>10} bytes", msg_heap_estimate);
1919 println!(" TOTAL: {:>10} bytes ({:.2} MB)", msg_total, msg_total as f64 / 1_000_000.0);
1920 println!();
1921 println!(" CompactMessage storage:");
1922 println!(" Struct memory: {:>10} bytes", compact_struct_mem);
1923 println!(" Heap (strings): {:>10} bytes", compact_heap_estimate);
1924 println!(" ID index: {:>10} bytes", compact_index_mem);
1925 println!(" Interner: {:>10} bytes ({} unique npubs)", interner_mem, interner.len());
1926 println!(" TOTAL: {:>10} bytes ({:.2} MB)", compact_total, compact_total as f64 / 1_000_000.0);
1927 println!();
1928 println!(" SAVINGS: {} bytes ({:.1}%)",
1929 msg_total.saturating_sub(compact_total),
1930 (1.0 - compact_total as f64 / msg_total as f64) * 100.0
1931 );
1932 println!(" Per message: {} -> {} bytes (avg)",
1933 msg_total / NUM_MESSAGES,
1934 compact_total / NUM_MESSAGES
1935 );
1936 println!();
1937
1938 println!("--- LOOKUP BENCHMARK ---");
1940
1941 let lookup_ids: Vec<String> = (0..1000)
1943 .map(|i| format!("{:0>64x}", i * 10)) .collect();
1945
1946 let lookup_start = Instant::now();
1948 let mut found_count = 0;
1949 for _ in 0..100 { for id in &lookup_ids {
1951 if compact_vec.find_by_hex_id(id).is_some() {
1952 found_count += 1;
1953 }
1954 }
1955 }
1956 let lookup_elapsed = lookup_start.elapsed();
1957 let total_lookups = 100 * lookup_ids.len();
1958
1959 println!(" Binary search (CompactMessageVec):");
1960 println!(" {} lookups in {:?}", total_lookups, lookup_elapsed);
1961 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / lookup_elapsed.as_secs_f64());
1962 println!(" Per lookup: {:.2} us", lookup_elapsed.as_micros() as f64 / total_lookups as f64);
1963 println!(" Found: {} / {}", found_count, total_lookups);
1964 println!();
1965
1966 let linear_start = Instant::now();
1968 let mut linear_found = 0;
1969 for _ in 0..100 {
1970 for id in &lookup_ids {
1971 if messages.iter().find(|m| &m.id == id).is_some() {
1972 linear_found += 1;
1973 }
1974 }
1975 }
1976 let linear_elapsed = linear_start.elapsed();
1977
1978 println!(" Linear search (Vec<Message>):");
1979 println!(" {} lookups in {:?}", total_lookups, linear_elapsed);
1980 println!(" Rate: {:.0} lookups/sec", total_lookups as f64 / linear_elapsed.as_secs_f64());
1981 println!(" Per lookup: {:.2} us", linear_elapsed.as_micros() as f64 / total_lookups as f64);
1982 println!();
1983
1984 let speedup = linear_elapsed.as_nanos() as f64 / lookup_elapsed.as_nanos() as f64;
1985 println!(" SPEEDUP: {:.1}x faster with binary search!", speedup);
1986 println!();
1987
1988 println!("--- INTERNER EFFICIENCY ---");
1990 let npub_string_size = 63 + 1; let naive_npub_mem = NUM_MESSAGES * npub_string_size * 2; let actual_npub_mem = interner_mem;
1993 println!(" Naive (every msg stores npubs): {} bytes", naive_npub_mem);
1994 println!(" Interned ({} unique): {} bytes", interner.len(), actual_npub_mem);
1995 println!(" SAVINGS: {} bytes ({:.1}%)",
1996 naive_npub_mem.saturating_sub(actual_npub_mem),
1997 (1.0 - actual_npub_mem as f64 / naive_npub_mem as f64) * 100.0
1998 );
1999 println!();
2000
2001 println!("========================================");
2002 println!(" BENCHMARK COMPLETE");
2003 println!("========================================\n");
2004
2005 assert_eq!(compact_vec.len(), NUM_MESSAGES);
2007 assert_eq!(interner.len(), NUM_UNIQUE_USERS);
2008 assert_eq!(found_count, linear_found);
2009 }
2010
2011 #[test]
2018 fn benchmark_profile_lookup() {
2019 use std::time::Instant;
2020 use std::hint::black_box;
2021
2022 const NUM_PROFILES: usize = 60;
2023 const NUM_LOOKUPS: usize = 100_000;
2024
2025 println!("\n========================================");
2026 println!(" PROFILE LOOKUP BENCHMARK");
2027 println!(" {} profiles, {} lookups each method", NUM_PROFILES, NUM_LOOKUPS);
2028 println!("========================================\n");
2029
2030 let npubs: Vec<String> = (0..NUM_PROFILES)
2032 .map(|i| format!("npub1{:0>58}", format!("{:x}", i * 7919 + 1000))) .collect();
2034
2035 let old_ids: Vec<String> = npubs.iter().rev().cloned().collect(); let mut sorted_ids: Vec<String> = npubs.clone();
2040 sorted_ids.sort();
2041
2042 let mut interner = NpubInterner::new();
2044 let mut profiles: Vec<crate::profile::Profile> = npubs.iter().map(|npub| {
2045 let mut p = crate::profile::Profile::new();
2046 p.id = interner.intern(npub);
2047 p
2048 }).collect();
2049 profiles.sort_by(|a, b| a.id.cmp(&b.id));
2050
2051 let lookup_targets: Vec<&str> = (0..NUM_LOOKUPS)
2053 .map(|i| npubs[i % NUM_PROFILES].as_str())
2054 .collect();
2055
2056 let handle_targets: Vec<u16> = lookup_targets.iter()
2058 .map(|&npub| interner.lookup(npub).unwrap())
2059 .collect();
2060
2061 let start = Instant::now();
2063 let mut found = 0u64;
2064 for &target in &lookup_targets {
2065 if old_ids.iter().any(|id| id == target) {
2066 found += 1;
2067 }
2068 }
2069 let linear_elapsed = start.elapsed();
2070 assert_eq!(found, NUM_LOOKUPS as u64);
2071
2072 let start = Instant::now();
2074 found = 0;
2075 for &target in &lookup_targets {
2076 if sorted_ids.binary_search_by(|id| id.as_str().cmp(target)).is_ok() {
2077 found += 1;
2078 }
2079 }
2080 let string_bs_elapsed = start.elapsed();
2081 assert_eq!(found, NUM_LOOKUPS as u64);
2082
2083 let start = Instant::now();
2085 found = 0;
2086 for &handle in &handle_targets {
2087 if profiles.binary_search_by(|p| p.id.cmp(black_box(&handle))).is_ok() {
2088 found += 1;
2089 }
2090 }
2091 let direct_elapsed = start.elapsed();
2092 assert_eq!(found, NUM_LOOKUPS as u64);
2093
2094 println!("--- LOOKUP METHODS ---");
2096 println!(" 1. Linear scan (old id: String):");
2097 println!(" {:?} total, {:.0} ns/lookup",
2098 linear_elapsed,
2099 linear_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2100 println!();
2101 println!(" 2. String binary search (intermediate):");
2102 println!(" {:?} total, {:.0} ns/lookup",
2103 string_bs_elapsed,
2104 string_bs_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2105 println!(" vs linear: {:.1}x faster",
2106 linear_elapsed.as_nanos() as f64 / string_bs_elapsed.as_nanos() as f64);
2107 println!();
2108 println!(" 3. Direct u16 handle lookup (current id: u16):");
2109 println!(" {:?} total, {:.0} ns/lookup",
2110 direct_elapsed,
2111 direct_elapsed.as_nanos() as f64 / NUM_LOOKUPS as f64);
2112 println!(" vs linear: {:.1}x faster",
2113 linear_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2114 println!(" vs string BS: {:.1}x faster",
2115 string_bs_elapsed.as_nanos() as f64 / direct_elapsed.as_nanos() as f64);
2116 println!();
2117
2118 println!("--- MEMORY PER PROFILE ---");
2120 println!(" Old (id: String): ~87 bytes (24 String header + ~63 heap)");
2121 println!(" Current (id: u16): 2 bytes (inline)");
2122 println!(" Savings: ~85 bytes/profile, ~{} bytes for {} profiles",
2123 85 * NUM_PROFILES, NUM_PROFILES);
2124 println!(" Interner (shared): {} bytes (shared with message system)",
2125 interner.memory_usage());
2126 println!();
2127
2128 println!("========================================");
2129 println!(" BENCHMARK COMPLETE");
2130 println!("========================================\n");
2131
2132 for npub in &npubs {
2134 assert!(old_ids.iter().any(|id| id == npub));
2135 assert!(sorted_ids.binary_search_by(|id| id.as_str().cmp(npub.as_str())).is_ok());
2136 let h = interner.lookup(npub).unwrap();
2137 assert!(profiles.binary_search_by(|p| p.id.cmp(&h)).is_ok());
2138 }
2139 }
2140
2141 #[test]
2146 fn pending_id_roundtrip_regular_hex() {
2147 let hex = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2148 let encoded = encode_message_id(hex);
2149 let decoded = decode_message_id(&encoded);
2150 assert_eq!(decoded, hex, "regular hex ID should roundtrip exactly");
2151 }
2152
2153 #[test]
2154 fn pending_id_roundtrip_pending() {
2155 let id = "pending-1234567890123456789";
2156 let encoded = encode_message_id(id);
2157 let decoded = decode_message_id(&encoded);
2158 assert_eq!(decoded, id, "pending ID should roundtrip exactly");
2159 }
2160
2161 #[test]
2162 fn pending_id_zero_timestamp() {
2163 let id = "pending-0";
2164 let encoded = encode_message_id(id);
2165 assert_eq!(encoded[0], PENDING_ID_MARKER, "first byte should be marker");
2166 let decoded = decode_message_id(&encoded);
2167 assert_eq!(decoded, id, "pending-0 should roundtrip");
2168 }
2169
2170 #[test]
2171 fn pending_id_max_u128_timestamp() {
2172 let max = u128::MAX;
2173 let id = format!("pending-{}", max);
2174 let encoded = encode_message_id(&id);
2175 let decoded = decode_message_id(&encoded);
2176 assert_eq!(decoded, id, "pending with max u128 should roundtrip");
2177 }
2178
2179 #[test]
2180 fn pending_id_all_zero_hex() {
2181 let hex = "0000000000000000000000000000000000000000000000000000000000000000";
2182 let encoded = encode_message_id(hex);
2183 let decoded = decode_message_id(&encoded);
2184 assert_eq!(decoded, hex, "all-zero hex ID should roundtrip");
2185 assert_ne!(encoded[0], PENDING_ID_MARKER);
2187 }
2188
2189 #[test]
2190 fn pending_id_all_ff_hex() {
2191 let hex = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
2192 let encoded = encode_message_id(hex);
2193 let decoded = decode_message_id(&encoded);
2194 assert_eq!(decoded, hex, "all-ff hex ID should roundtrip");
2195 assert_eq!(encoded, [0xff; 32], "all-ff should decode to all 0xff bytes");
2196 }
2197
2198 #[test]
2199 fn pending_id_mixed_case_hex() {
2200 let lower = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
2201 let mixed = "ABCDEF0123456789abcdef0123456789ABCDEF0123456789abcdef0123456789";
2202 let encoded_lower = encode_message_id(lower);
2203 let encoded_mixed = encode_message_id(mixed);
2204 assert_eq!(encoded_lower, encoded_mixed, "mixed case should produce same bytes as lowercase");
2205 }
2206
2207 #[test]
2208 fn pending_id_short_hex_partial_decode() {
2209 let short = "abcdef";
2211 let encoded = encode_message_id(short);
2212 assert_eq!(encoded[0..14], [0u8; 14], "leading bytes should be zero for short input");
2214 }
2215
2216 #[test]
2217 fn pending_id_marker_distinguishes_from_real_id() {
2218 let hex = "0100000000000000000000000000000000000000000000000000000000000000";
2223 let encoded = encode_message_id(hex);
2224 let decoded = decode_message_id(&encoded);
2225 assert_eq!(decoded, hex, "marker-leading real id must roundtrip exactly");
2226 }
2227
2228 #[test]
2229 fn pending_id_large_timestamp() {
2230 let id = "pending-99999999999999999";
2231 let encoded = encode_message_id(&id);
2232 let decoded = decode_message_id(&encoded);
2233 assert_eq!(decoded, id, "large but valid timestamp should roundtrip");
2234 }
2235
2236 #[test]
2237 fn pending_id_invalid_timestamp_becomes_zero() {
2238 let id = "pending-notanumber";
2240 let encoded = encode_message_id(id);
2241 assert_eq!(encoded[0], PENDING_ID_MARKER);
2242 assert_eq!(&encoded[1..17], &[0u8; 16]);
2244 let decoded = decode_message_id(&encoded);
2245 assert_eq!(decoded, "pending-0", "invalid timestamp parses as pending-0");
2246 }
2247
2248 #[test]
2253 fn timestamp_to_compact_and_back_roundtrip() {
2254 let ms: u64 = 1705320000000;
2256 let compact = timestamp_to_compact(ms);
2257 let restored = timestamp_from_compact(compact);
2258 assert_eq!(restored / 1000, ms / 1000, "roundtrip should preserve seconds");
2260 }
2261
2262 #[test]
2263 fn secs_to_compact_and_back_roundtrip() {
2264 let secs: u64 = 1705320000; let compact = secs_to_compact(secs);
2266 let restored = secs_from_compact(compact);
2267 assert_eq!(restored, secs, "secs should roundtrip exactly");
2268 }
2269
2270 #[test]
2271 fn timestamp_zero_preservation() {
2272 assert_eq!(secs_to_compact(0), 0, "zero secs should produce zero compact");
2274 assert_eq!(secs_from_compact(0), 0, "zero compact should produce zero secs");
2275 }
2276
2277 #[test]
2278 fn timestamp_epoch_boundary() {
2279 let epoch_secs: u64 = 1577836800;
2281 let compact = secs_to_compact(epoch_secs);
2282 assert_eq!(compact, 0, "epoch boundary should map to compact 0");
2283 let restored = secs_from_compact(compact);
2284 assert_eq!(restored, 0, "compact 0 returns sentinel 0");
2286 }
2287
2288 #[test]
2289 fn timestamp_epoch_boundary_ms() {
2290 let epoch_ms: u64 = 1577836800000;
2291 let compact = timestamp_to_compact(epoch_ms);
2292 assert_eq!(compact, epoch_ms, "full u64 — identity function");
2293 let restored = timestamp_from_compact(compact);
2294 assert_eq!(restored, epoch_ms, "roundtrip preserves value");
2295 }
2296
2297 #[test]
2298 fn timestamp_current_time_roundtrip() {
2299 let secs: u64 = 1774800000;
2301 let compact = secs_to_compact(secs);
2302 let restored = secs_from_compact(compact);
2303 assert_eq!(restored, secs, "current-era timestamp should roundtrip");
2304 assert!(compact > 0, "current time should be past epoch");
2305 }
2306
2307 #[test]
2308 fn timestamp_far_future_year_2100() {
2309 let secs: u64 = 4102444800;
2311 let compact = secs_to_compact(secs);
2312 let restored = secs_from_compact(compact);
2313 assert_eq!(restored, secs, "year 2100 should roundtrip");
2314 assert!(compact <= u32::MAX, "year 2100 should fit in u32");
2316 }
2317
2318 #[test]
2319 fn timestamp_pre_epoch_saturates_to_zero() {
2320 let secs: u64 = 1500000000; let compact = secs_to_compact(secs);
2323 assert_eq!(compact, 0, "pre-epoch timestamp should saturate to 0");
2325 }
2326
2327 #[test]
2328 fn timestamp_ms_sub_second_precision_preserved() {
2329 let ms: u64 = 1705320000999;
2330 let compact = timestamp_to_compact(ms);
2331 let restored = timestamp_from_compact(compact);
2332 assert_eq!(restored, ms, "sub-second ms must be preserved");
2333 }
2334
2335 #[test]
2336 fn timestamp_one_second_after_epoch() {
2337 let secs: u64 = 1577836801; let compact = secs_to_compact(secs);
2339 assert_eq!(compact, 1, "one second after epoch should be compact 1");
2340 let restored = secs_from_compact(compact);
2341 assert_eq!(restored, secs, "should restore to original");
2342 }
2343
2344 #[test]
2349 fn message_flags_mine_independent() {
2350 let mut flags = MessageFlags::NONE;
2351 flags.set_mine(true);
2352 assert!(flags.is_mine(), "mine should be set");
2353 assert!(!flags.is_pending(), "pending should not be set");
2354 assert!(!flags.is_failed(), "failed should not be set");
2355 }
2356
2357 #[test]
2358 fn message_flags_pending_independent() {
2359 let mut flags = MessageFlags::NONE;
2360 flags.set_pending(true);
2361 assert!(!flags.is_mine(), "mine should not be set");
2362 assert!(flags.is_pending(), "pending should be set");
2363 assert!(!flags.is_failed(), "failed should not be set");
2364 }
2365
2366 #[test]
2367 fn message_flags_failed_independent() {
2368 let mut flags = MessageFlags::NONE;
2369 flags.set_failed(true);
2370 assert!(!flags.is_mine(), "mine should not be set");
2371 assert!(!flags.is_pending(), "pending should not be set");
2372 assert!(flags.is_failed(), "failed should be set");
2373 }
2374
2375 #[test]
2376 fn message_flags_from_bools_all_false() {
2377 let flags = MessageFlags::from_bools(false, false, false);
2378 assert!(!flags.is_mine());
2379 assert!(!flags.is_pending());
2380 assert!(!flags.is_failed());
2381 assert_eq!(flags, MessageFlags::NONE, "all false should equal NONE");
2382 }
2383
2384 #[test]
2385 fn message_flags_from_bools_all_true() {
2386 let flags = MessageFlags::from_bools(true, true, true);
2387 assert!(flags.is_mine(), "mine should be set");
2388 assert!(flags.is_pending(), "pending should be set");
2389 assert!(flags.is_failed(), "failed should be set");
2390 }
2391
2392 #[test]
2393 fn message_flags_from_bools_various_combos() {
2394 let flags = MessageFlags::from_bools(true, false, true);
2395 assert!(flags.is_mine());
2396 assert!(!flags.is_pending());
2397 assert!(flags.is_failed());
2398
2399 let flags = MessageFlags::from_bools(false, true, false);
2400 assert!(!flags.is_mine());
2401 assert!(flags.is_pending());
2402 assert!(!flags.is_failed());
2403 }
2404
2405 #[test]
2406 fn message_flags_from_all_replied_to_none() {
2407 let flags = MessageFlags::from_all(false, false, false, None);
2408 assert_eq!(flags.replied_to_has_attachment(), None, "None should roundtrip");
2409 }
2410
2411 #[test]
2412 fn message_flags_from_all_replied_to_some_false() {
2413 let flags = MessageFlags::from_all(false, false, false, Some(false));
2414 assert_eq!(flags.replied_to_has_attachment(), Some(false), "Some(false) should roundtrip");
2415 }
2416
2417 #[test]
2418 fn message_flags_from_all_replied_to_some_true() {
2419 let flags = MessageFlags::from_all(false, false, false, Some(true));
2420 assert_eq!(flags.replied_to_has_attachment(), Some(true), "Some(true) should roundtrip");
2421 }
2422
2423 #[test]
2424 fn message_flags_multiple_set_simultaneously() {
2425 let flags = MessageFlags::from_all(true, true, false, Some(true));
2426 assert!(flags.is_mine());
2427 assert!(flags.is_pending());
2428 assert!(!flags.is_failed());
2429 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2430 }
2431
2432 #[test]
2433 fn message_flags_default_is_all_false() {
2434 let flags = MessageFlags::default();
2435 assert!(!flags.is_mine());
2436 assert!(!flags.is_pending());
2437 assert!(!flags.is_failed());
2438 assert_eq!(flags.replied_to_has_attachment(), None);
2439 assert_eq!(flags, MessageFlags::NONE);
2440 }
2441
2442 #[test]
2443 fn message_flags_bit_patterns_correct() {
2444 assert_eq!(MessageFlags::MINE.0, 0b00001, "MINE bit pattern");
2445 assert_eq!(MessageFlags::PENDING.0, 0b00010, "PENDING bit pattern");
2446 assert_eq!(MessageFlags::FAILED.0, 0b00100, "FAILED bit pattern");
2447 }
2448
2449 #[test]
2450 fn message_flags_set_then_clear() {
2451 let mut flags = MessageFlags::from_bools(true, true, true);
2452 flags.set_mine(false);
2453 assert!(!flags.is_mine(), "mine should be cleared");
2454 assert!(flags.is_pending(), "pending should remain set");
2455 assert!(flags.is_failed(), "failed should remain set");
2456 }
2457
2458 #[test]
2459 fn message_flags_replied_to_overwrite() {
2460 let mut flags = MessageFlags::from_all(false, false, false, Some(true));
2461 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2462 flags.set_replied_to_has_attachment(Some(false));
2463 assert_eq!(flags.replied_to_has_attachment(), Some(false), "overwrite should work");
2464 flags.set_replied_to_has_attachment(None);
2465 assert_eq!(flags.replied_to_has_attachment(), None, "clearing to None should work");
2466 }
2467
2468 #[test]
2469 fn message_flags_replied_to_does_not_interfere_with_other_bits() {
2470 let mut flags = MessageFlags::from_bools(true, true, true);
2471 flags.set_replied_to_has_attachment(Some(true));
2472 assert!(flags.is_mine(), "mine should still be set");
2473 assert!(flags.is_pending(), "pending should still be set");
2474 assert!(flags.is_failed(), "failed should still be set");
2475 assert_eq!(flags.replied_to_has_attachment(), Some(true));
2476 }
2477
2478 #[test]
2483 fn attachment_flags_downloading_independent() {
2484 let mut flags = AttachmentFlags::NONE;
2485 flags.set_downloading(true);
2486 assert!(flags.is_downloading());
2487 assert!(!flags.is_downloaded());
2488 assert!(!flags.is_short_nonce());
2489 }
2490
2491 #[test]
2492 fn attachment_flags_downloaded_independent() {
2493 let mut flags = AttachmentFlags::NONE;
2494 flags.set_downloaded(true);
2495 assert!(!flags.is_downloading());
2496 assert!(flags.is_downloaded());
2497 assert!(!flags.is_short_nonce());
2498 }
2499
2500 #[test]
2501 fn attachment_flags_short_nonce_independent() {
2502 let mut flags = AttachmentFlags::NONE;
2503 flags.set_short_nonce(true);
2504 assert!(!flags.is_downloading());
2505 assert!(!flags.is_downloaded());
2506 assert!(flags.is_short_nonce());
2507 }
2508
2509 #[test]
2510 fn attachment_flags_from_bools() {
2511 let flags = AttachmentFlags::from_bools(true, false);
2512 assert!(flags.is_downloading());
2513 assert!(!flags.is_downloaded());
2514
2515 let flags = AttachmentFlags::from_bools(false, true);
2516 assert!(!flags.is_downloading());
2517 assert!(flags.is_downloaded());
2518 }
2519
2520 #[test]
2521 fn attachment_flags_all_set() {
2522 let mut flags = AttachmentFlags::NONE;
2523 flags.set_downloading(true);
2524 flags.set_downloaded(true);
2525 flags.set_short_nonce(true);
2526 assert!(flags.is_downloading());
2527 assert!(flags.is_downloaded());
2528 assert!(flags.is_short_nonce());
2529 }
2530
2531 #[test]
2532 fn attachment_flags_set_then_clear() {
2533 let mut flags = AttachmentFlags::NONE;
2534 flags.set_downloading(true);
2535 flags.set_downloaded(true);
2536 flags.set_downloading(false);
2537 assert!(!flags.is_downloading(), "downloading should be cleared");
2538 assert!(flags.is_downloaded(), "downloaded should remain set");
2539 }
2540
2541 #[test]
2542 fn attachment_flags_default_none() {
2543 let flags = AttachmentFlags::NONE;
2544 assert!(!flags.is_downloading());
2545 assert!(!flags.is_downloaded());
2546 assert!(!flags.is_short_nonce());
2547 assert_eq!(flags, AttachmentFlags::default());
2548 }
2549
2550 #[test]
2551 fn attachment_flags_bit_values() {
2552 let mut flags = AttachmentFlags::NONE;
2554 flags.set_downloading(true);
2555 assert_eq!(flags.0, 0b0001);
2556
2557 let mut flags = AttachmentFlags::NONE;
2558 flags.set_downloaded(true);
2559 assert_eq!(flags.0, 0b0010);
2560
2561 let mut flags = AttachmentFlags::NONE;
2562 flags.set_short_nonce(true);
2563 assert_eq!(flags.0, 0b0100);
2564 }
2565
2566 #[test]
2571 fn interner_returns_incrementing_handles() {
2572 let mut interner = NpubInterner::new();
2573 let h0 = interner.intern("npub1aaa");
2574 let h1 = interner.intern("npub1bbb");
2575 let h2 = interner.intern("npub1ccc");
2576 assert_eq!(h0, 0, "first intern should be handle 0");
2577 assert_eq!(h1, 1, "second intern should be handle 1");
2578 assert_eq!(h2, 2, "third intern should be handle 2");
2579 }
2580
2581 #[test]
2582 fn interner_lookup_finds_interned() {
2583 let mut interner = NpubInterner::new();
2584 let h = interner.intern("npub1alice");
2585 let found = interner.lookup("npub1alice");
2586 assert_eq!(found, Some(h), "lookup should find interned string");
2587 }
2588
2589 #[test]
2590 fn interner_lookup_returns_none_for_unknown() {
2591 let interner = NpubInterner::new();
2592 assert_eq!(interner.lookup("npub1unknown"), None, "lookup on empty interner should be None");
2593 }
2594
2595 #[test]
2596 fn interner_lookup_returns_none_for_not_interned() {
2597 let mut interner = NpubInterner::new();
2598 interner.intern("npub1alice");
2599 assert_eq!(interner.lookup("npub1bob"), None, "lookup for non-interned should be None");
2600 }
2601
2602 #[test]
2603 fn interner_resolve_returns_string() {
2604 let mut interner = NpubInterner::new();
2605 let h = interner.intern("npub1test123");
2606 assert_eq!(interner.resolve(h), Some("npub1test123"), "resolve should return the original string");
2607 }
2608
2609 #[test]
2610 fn interner_resolve_returns_none_for_no_npub() {
2611 let interner = NpubInterner::new();
2612 assert_eq!(interner.resolve(NO_NPUB), None, "resolve(NO_NPUB) should be None");
2613 }
2614
2615 #[test]
2616 fn interner_resolve_returns_none_for_out_of_bounds() {
2617 let mut interner = NpubInterner::new();
2618 interner.intern("npub1only");
2619 assert_eq!(interner.resolve(999), None, "out-of-bounds handle should resolve to None");
2620 }
2621
2622 #[test]
2623 fn interner_duplicate_returns_same_handle() {
2624 let mut interner = NpubInterner::new();
2625 let h1 = interner.intern("npub1dup");
2626 let h2 = interner.intern("npub1dup");
2627 let h3 = interner.intern("npub1dup");
2628 assert_eq!(h1, h2, "duplicate intern should return same handle");
2629 assert_eq!(h2, h3, "duplicate intern should return same handle");
2630 assert_eq!(interner.len(), 1, "duplicates should not increase length");
2631 }
2632
2633 #[test]
2634 fn interner_100_unique_npubs_stress() {
2635 let mut interner = NpubInterner::new();
2636 let mut handles = Vec::new();
2637
2638 for i in 0..100 {
2639 let npub = format!("npub1stress{:04}", i);
2640 let h = interner.intern(&npub);
2641 handles.push((h, npub));
2642 }
2643
2644 assert_eq!(interner.len(), 100, "should have 100 unique npubs");
2645
2646 for (h, npub) in &handles {
2648 assert_eq!(interner.resolve(*h), Some(npub.as_str()),
2649 "handle {} should resolve to {}", h, npub);
2650 }
2651
2652 for (h, npub) in &handles {
2654 assert_eq!(interner.lookup(npub), Some(*h),
2655 "lookup for {} should return handle {}", npub, h);
2656 }
2657
2658 for (h, npub) in &handles {
2660 assert_eq!(interner.intern(npub), *h,
2661 "re-interning {} should return same handle {}", npub, h);
2662 }
2663 assert_eq!(interner.len(), 100, "re-interning should not grow interner");
2664 }
2665
2666 #[test]
2667 fn interner_memory_usage_reasonable() {
2668 let mut interner = NpubInterner::new();
2669 for i in 0..50 {
2670 interner.intern(&format!("npub1{:0>62}", i));
2671 }
2672 let mem = interner.memory_usage();
2673 assert!(mem > 0, "memory usage should be positive");
2675 assert!(mem < 100_000, "memory usage for 50 npubs should be under 100KB, was {}", mem);
2676 }
2677
2678 #[test]
2679 fn interner_empty() {
2680 let interner = NpubInterner::new();
2681 assert_eq!(interner.len(), 0);
2682 assert!(interner.is_empty());
2683 assert_eq!(interner.resolve(0), None);
2684 assert_eq!(interner.lookup("anything"), None);
2685 assert!(interner.memory_usage() > 0, "even empty interner has struct overhead");
2686 }
2687
2688 #[test]
2689 fn interner_intern_opt_none() {
2690 let mut interner = NpubInterner::new();
2691 let h = interner.intern_opt(None);
2692 assert_eq!(h, NO_NPUB, "intern_opt(None) should return NO_NPUB");
2693 assert_eq!(interner.len(), 0, "None should not add to interner");
2694 }
2695
2696 #[test]
2697 fn interner_intern_opt_empty_string() {
2698 let mut interner = NpubInterner::new();
2699 let h = interner.intern_opt(Some(""));
2700 assert_eq!(h, NO_NPUB, "intern_opt(Some('')) should return NO_NPUB");
2701 }
2702
2703 #[test]
2704 fn interner_intern_opt_some_value() {
2705 let mut interner = NpubInterner::new();
2706 let h = interner.intern_opt(Some("npub1real"));
2707 assert_ne!(h, NO_NPUB, "intern_opt(Some(value)) should not return NO_NPUB");
2708 assert_eq!(interner.resolve(h), Some("npub1real"));
2709 }
2710
2711 fn make_compact_msg(hex_id: &str, timestamp: u64) -> CompactMessage {
2717 CompactMessage {
2718 id: encode_message_id(hex_id),
2719 at: timestamp,
2720 expiration_secs: 0,
2721 flags: MessageFlags::NONE,
2722 npub_idx: NO_NPUB,
2723 replied_to: None,
2724 replied_to_npub_idx: NO_NPUB,
2725 wrapper_id: None,
2726 content: "test".into(),
2727 replied_to_content: None,
2728 attachments: TinyVec::new(),
2729 reactions: TinyVec::new(),
2730 edit_history: None,
2731 preview_metadata: None,
2732 emoji_tags: None,
2733 addressed_bots: None,
2734 }
2735 }
2736
2737 #[test]
2738 fn compact_vec_insert_single_message() {
2739 let mut vec = CompactMessageVec::new();
2740 let msg = make_compact_msg(
2741 "1111111111111111111111111111111111111111111111111111111111111111",
2742 100,
2743 );
2744 assert!(vec.insert(msg), "insert should succeed");
2745 assert_eq!(vec.len(), 1);
2746 assert!(!vec.is_empty());
2747 }
2748
2749 #[test]
2750 fn compact_vec_insert_duplicate_rejected() {
2751 let mut vec = CompactMessageVec::new();
2752 let id = "2222222222222222222222222222222222222222222222222222222222222222";
2753 let msg1 = make_compact_msg(id, 100);
2754 let msg2 = make_compact_msg(id, 200); assert!(vec.insert(msg1), "first insert should succeed");
2756 assert!(!vec.insert(msg2), "duplicate ID should be rejected");
2757 assert_eq!(vec.len(), 1);
2758 }
2759
2760 #[test]
2761 fn compact_vec_insert_batch_multiple() {
2762 let mut vec = CompactMessageVec::new();
2763 let msgs = vec![
2764 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
2765 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
2766 make_compact_msg("cc00000000000000000000000000000000000000000000000000000000000000", 300),
2767 ];
2768 let added = vec.insert_batch(msgs);
2769 assert_eq!(added, 3, "all 3 should be added");
2770 assert_eq!(vec.len(), 3);
2771 }
2772
2773 #[test]
2774 fn compact_vec_insert_batch_dedup() {
2775 let mut vec = CompactMessageVec::new();
2776 let id = "dd00000000000000000000000000000000000000000000000000000000000000";
2777 vec.insert(make_compact_msg(id, 100));
2778
2779 let msgs = vec![
2780 make_compact_msg(id, 200), make_compact_msg("ee00000000000000000000000000000000000000000000000000000000000000", 300),
2782 ];
2783 let added = vec.insert_batch(msgs);
2784 assert_eq!(added, 1, "only non-duplicate should be added");
2785 assert_eq!(vec.len(), 2);
2786 }
2787
2788 #[test]
2789 fn compact_vec_find_by_hex_id() {
2790 let mut vec = CompactMessageVec::new();
2791 let id = "ff00000000000000000000000000000000000000000000000000000000000001";
2792 let mut msg = make_compact_msg(id, 500);
2793 msg.content = "found me".into();
2794 vec.insert(msg);
2795
2796 let found = vec.find_by_hex_id(id);
2797 assert!(found.is_some(), "should find by hex id");
2798 assert_eq!(&*found.unwrap().content, "found me");
2799 }
2800
2801 #[test]
2802 fn compact_vec_find_by_hex_id_not_found() {
2803 let mut vec = CompactMessageVec::new();
2804 vec.insert(make_compact_msg(
2805 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2806 ));
2807 let found = vec.find_by_hex_id(
2808 "bb00000000000000000000000000000000000000000000000000000000000000",
2809 );
2810 assert!(found.is_none(), "should not find non-existent ID");
2811 }
2812
2813 #[test]
2814 fn compact_vec_find_by_hex_id_empty_string() {
2815 let mut vec = CompactMessageVec::new();
2816 vec.insert(make_compact_msg(
2817 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2818 ));
2819 assert!(vec.find_by_hex_id("").is_none(), "empty string should return None");
2820 }
2821
2822 #[test]
2823 fn compact_vec_find_by_hex_id_mut() {
2824 let mut vec = CompactMessageVec::new();
2825 let id = "ff00000000000000000000000000000000000000000000000000000000000002";
2826 vec.insert(make_compact_msg(id, 500));
2827
2828 let found = vec.find_by_hex_id_mut(id);
2829 assert!(found.is_some(), "should find mutable ref by hex id");
2830 found.unwrap().content = "modified".into();
2831
2832 let found = vec.find_by_hex_id(id);
2834 assert_eq!(&*found.unwrap().content, "modified");
2835 }
2836
2837 #[test]
2838 fn compact_vec_contains_hex_id() {
2839 let mut vec = CompactMessageVec::new();
2840 let id = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
2841 vec.insert(make_compact_msg(id, 100));
2842
2843 assert!(vec.contains_hex_id(id), "should contain inserted ID");
2844 assert!(!vec.contains_hex_id(
2845 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
2846 "should not contain non-inserted ID");
2847 assert!(!vec.contains_hex_id(""), "empty string should not match");
2848 }
2849
2850 #[test]
2851 fn compact_vec_remove_by_hex_id() {
2852 let mut vec = CompactMessageVec::new();
2853 let id1 = "1100000000000000000000000000000000000000000000000000000000000000";
2854 let id2 = "2200000000000000000000000000000000000000000000000000000000000000";
2855 vec.insert(make_compact_msg(id1, 100));
2856 vec.insert(make_compact_msg(id2, 200));
2857 assert_eq!(vec.len(), 2);
2858
2859 assert!(vec.remove_by_hex_id(id1), "remove should succeed");
2860 assert_eq!(vec.len(), 1);
2861 assert!(!vec.contains_hex_id(id1), "removed ID should not be found");
2862 assert!(vec.contains_hex_id(id2), "remaining ID should still be found");
2863 }
2864
2865 #[test]
2866 fn compact_vec_remove_nonexistent() {
2867 let mut vec = CompactMessageVec::new();
2868 vec.insert(make_compact_msg(
2869 "1100000000000000000000000000000000000000000000000000000000000000", 100,
2870 ));
2871 assert!(!vec.remove_by_hex_id(
2872 "9900000000000000000000000000000000000000000000000000000000000000"),
2873 "removing non-existent should return false");
2874 assert!(!vec.remove_by_hex_id(""), "removing empty should return false");
2875 assert_eq!(vec.len(), 1);
2876 }
2877
2878 #[test]
2879 fn compact_vec_last_timestamp() {
2880 let mut vec = CompactMessageVec::new();
2881 assert_eq!(vec.last_timestamp(), None, "empty vec should have no last timestamp");
2882
2883 vec.insert(make_compact_msg(
2884 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2885 ));
2886 vec.insert(make_compact_msg(
2887 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2888 ));
2889 vec.insert(make_compact_msg(
2890 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2891 ));
2892
2893 let last_ts = vec.last_timestamp().unwrap();
2894 let expected = timestamp_from_compact(300);
2896 assert_eq!(last_ts, expected, "last timestamp should be the largest");
2897 }
2898
2899 #[test]
2900 fn compact_vec_empty_operations() {
2901 let vec = CompactMessageVec::new();
2902 assert!(vec.is_empty());
2903 assert_eq!(vec.len(), 0);
2904 assert!(vec.last().is_none());
2905 assert!(vec.first().is_none());
2906 assert!(vec.last_timestamp().is_none());
2907 assert!(!vec.contains_hex_id("anything"));
2908 assert!(vec.find_by_hex_id("anything").is_none());
2909 }
2910
2911 #[test]
2912 fn compact_vec_1000_message_stress() {
2913 let mut vec = CompactMessageVec::new();
2914
2915 for i in 0..1000u64 {
2917 let id = format!("{:0>64x}", i);
2918 let msg = make_compact_msg(&id, i * 10);
2919 assert!(vec.insert(msg), "insert {} should succeed", i);
2920 }
2921 assert_eq!(vec.len(), 1000);
2922
2923 for i in 0..1000u64 {
2925 let id = format!("{:0>64x}", i);
2926 assert!(vec.contains_hex_id(&id), "should find message {}", i);
2927 let found = vec.find_by_hex_id(&id).unwrap();
2928 assert_eq!(found.at, i * 10, "timestamp should match for message {}", i);
2929 }
2930
2931 for i in 1000..1010u64 {
2933 let id = format!("{:0>64x}", i);
2934 assert!(!vec.contains_hex_id(&id), "should not find non-existent {}", i);
2935 }
2936
2937 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2939 for w in timestamps.windows(2) {
2940 assert!(w[0] <= w[1], "messages should be sorted by timestamp: {} <= {}", w[0], w[1]);
2941 }
2942 }
2943
2944 #[test]
2945 fn compact_vec_rebuild_index_after_id_change() {
2946 let mut vec = CompactMessageVec::new();
2947 let old_id = "aa00000000000000000000000000000000000000000000000000000000000000";
2948 let new_id = "ff00000000000000000000000000000000000000000000000000000000000000";
2949 vec.insert(make_compact_msg(old_id, 100));
2950
2951 vec.messages_mut()[0].id = encode_message_id(new_id);
2953 assert!(!vec.contains_hex_id(new_id), "stale index should not find new ID");
2955
2956 vec.rebuild_index();
2958 assert!(vec.contains_hex_id(new_id), "after rebuild, new ID should be found");
2959 assert!(!vec.contains_hex_id(old_id), "after rebuild, old ID should not be found");
2960 }
2961
2962 #[test]
2963 fn compact_vec_pending_id_lookup() {
2964 let mut vec = CompactMessageVec::new();
2965 let pending = "pending-9876543210";
2966 vec.insert(make_compact_msg(pending, 500));
2967
2968 assert!(vec.contains_hex_id(pending), "should find pending ID");
2969 let found = vec.find_by_hex_id(pending);
2970 assert!(found.is_some(), "should find pending message");
2971 assert_eq!(found.unwrap().id_hex(), pending, "id_hex should match pending string");
2972 }
2973
2974 #[test]
2975 fn compact_vec_out_of_order_insert() {
2976 let mut vec = CompactMessageVec::new();
2977 vec.insert(make_compact_msg(
2979 "bb00000000000000000000000000000000000000000000000000000000000000", 300,
2980 ));
2981 vec.insert(make_compact_msg(
2982 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
2983 ));
2984 vec.insert(make_compact_msg(
2985 "cc00000000000000000000000000000000000000000000000000000000000000", 200,
2986 ));
2987
2988 assert_eq!(vec.len(), 3);
2989 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
2991 assert_eq!(timestamps, vec![100, 200, 300], "should be sorted by timestamp");
2992
2993 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
2995 assert!(vec.contains_hex_id("bb00000000000000000000000000000000000000000000000000000000000000"));
2996 assert!(vec.contains_hex_id("cc00000000000000000000000000000000000000000000000000000000000000"));
2997 }
2998
2999 #[test]
3000 fn compact_vec_batch_prepend() {
3001 let mut vec = CompactMessageVec::new();
3002 vec.insert(make_compact_msg(
3004 "cc00000000000000000000000000000000000000000000000000000000000000", 300,
3005 ));
3006 vec.insert(make_compact_msg(
3007 "dd00000000000000000000000000000000000000000000000000000000000000", 400,
3008 ));
3009
3010 let older = vec![
3012 make_compact_msg("aa00000000000000000000000000000000000000000000000000000000000000", 100),
3013 make_compact_msg("bb00000000000000000000000000000000000000000000000000000000000000", 200),
3014 ];
3015 let added = vec.insert_batch(older);
3016 assert_eq!(added, 2);
3017 assert_eq!(vec.len(), 4);
3018
3019 let timestamps: Vec<u64> = vec.iter().map(|m| m.at).collect();
3021 assert_eq!(timestamps, vec![100, 200, 300, 400]);
3022
3023 assert!(vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3025 assert!(vec.contains_hex_id("dd00000000000000000000000000000000000000000000000000000000000000"));
3026 }
3027
3028 #[test]
3029 fn compact_vec_clear() {
3030 let mut vec = CompactMessageVec::new();
3031 vec.insert(make_compact_msg(
3032 "aa00000000000000000000000000000000000000000000000000000000000000", 100,
3033 ));
3034 vec.insert(make_compact_msg(
3035 "bb00000000000000000000000000000000000000000000000000000000000000", 200,
3036 ));
3037 assert_eq!(vec.len(), 2);
3038
3039 vec.clear();
3040 assert!(vec.is_empty());
3041 assert_eq!(vec.len(), 0);
3042 assert!(!vec.contains_hex_id("aa00000000000000000000000000000000000000000000000000000000000000"));
3043 }
3044
3045 fn make_full_message() -> Message {
3051 Message {
3052 expiration: Some(1893456000),
3053 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3054 content: "Hello, world!".into(),
3055 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3056 replied_to_content: Some("Original message".into()),
3057 replied_to_npub: Some("npub1replier".into()),
3058 replied_to_has_attachment: Some(true),
3059 replied_to_attachment_extension: None,
3060 preview_metadata: Some(SiteMetadata {
3061 domain: "example.com".into(),
3062 og_title: Some("Test Page".into()),
3063 og_description: Some("A test description".into()),
3064 og_image: Some("https://example.com/img.png".into()),
3065 og_url: Some("https://example.com".into()),
3066 og_type: Some("website".into()),
3067 title: Some("Test".into()),
3068 description: Some("Desc".into()),
3069 favicon: Some("https://example.com/favicon.ico".into()),
3070 }),
3071 attachments: vec![Attachment {
3072 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3073 key: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3074 nonce: "cccccccccccccccccccccccccccccccc".into(), extension: "png".into(),
3076 name: "photo.png".into(),
3077 url: "https://blossom.example.com".into(),
3078 path: "/tmp/photo.png".into(),
3079 size: 12345,
3080 img_meta: Some(ImageMetadata {
3081 thumbhash: "abc123".into(),
3082 width: 800,
3083 height: 600,
3084 }),
3085 downloading: false,
3086 downloaded: true,
3087 webxdc_topic: None,
3088 group_id: None,
3089 original_hash: None,
3090 scheme_version: None,
3091 mls_filename: None,
3092 }],
3093 reactions: vec![Reaction {
3094 id: "dddd000000000000000000000000000000000000000000000000000000000000".into(),
3095 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3096 author_id: "npub1reactor".into(),
3097 emoji: "\u{1f44d}".into(), emoji_url: None,
3099 }],
3100 at: 1705320000000, pending: false,
3102 failed: false,
3103 mine: true,
3104 npub: Some("npub1sender".into()),
3105 wrapper_event_id: Some("eeee000000000000000000000000000000000000000000000000000000000000".into()),
3106 edited: true,
3107 edit_history: Some(vec![
3108 EditEntry { content: "Original".into(), edited_at: 1705320000000 },
3109 EditEntry { content: "Edited".into(), edited_at: 1705320060000 },
3110 ]),
3111 emoji_tags: Vec::new(),
3112 addressed_bots: vec!["npub1botrouting0000000000000000000000000000000000000000000000".into()],
3113 }
3114 }
3115
3116 #[test]
3117 fn compact_message_from_message_roundtrip_all_fields() {
3118 let msg = make_full_message();
3119 let mut interner = NpubInterner::new();
3120 let compact = CompactMessage::from_message(&msg, &mut interner);
3121 let restored = compact.to_message(&interner);
3122
3123 assert_eq!(restored.id, msg.id, "id mismatch");
3124 assert_eq!(restored.content, msg.content, "content mismatch");
3125 assert_eq!(restored.mine, msg.mine, "mine mismatch");
3126 assert_eq!(restored.pending, msg.pending, "pending mismatch");
3127 assert_eq!(restored.failed, msg.failed, "failed mismatch");
3128 assert_eq!(restored.npub, msg.npub, "npub mismatch");
3129 assert_eq!(restored.replied_to, msg.replied_to, "replied_to mismatch");
3130 assert_eq!(restored.replied_to_content, msg.replied_to_content, "replied_to_content mismatch");
3131 assert_eq!(restored.replied_to_npub, msg.replied_to_npub, "replied_to_npub mismatch");
3132 assert_eq!(restored.replied_to_has_attachment, msg.replied_to_has_attachment, "replied_to_has_attachment mismatch");
3133 assert_eq!(restored.wrapper_event_id, msg.wrapper_event_id, "wrapper_event_id mismatch");
3134 assert_eq!(restored.edited, msg.edited, "edited mismatch");
3135 assert_eq!(restored.edit_history, msg.edit_history, "edit_history mismatch");
3136 assert_eq!(restored.preview_metadata, msg.preview_metadata, "preview_metadata mismatch");
3137 assert_eq!(restored.at / 1000, msg.at / 1000, "timestamp seconds mismatch");
3139 assert_eq!(restored.attachments.len(), 1, "should have 1 attachment");
3141 assert_eq!(restored.attachments[0].id, msg.attachments[0].id);
3142 assert_eq!(restored.attachments[0].name, msg.attachments[0].name);
3143 assert_eq!(restored.attachments[0].size, msg.attachments[0].size);
3144 assert_eq!(restored.reactions.len(), 1, "should have 1 reaction");
3146 assert_eq!(restored.reactions[0].emoji, msg.reactions[0].emoji);
3147 assert_eq!(restored.addressed_bots, msg.addressed_bots, "addressed_bots mismatch");
3149 }
3150
3151 #[test]
3152 fn compact_message_from_message_owned_roundtrip() {
3153 let msg = make_full_message();
3154 let msg_clone = msg.clone();
3155 let mut interner = NpubInterner::new();
3156 let compact = CompactMessage::from_message_owned(msg, &mut interner);
3157 let restored = compact.to_message(&interner);
3158
3159 assert_eq!(restored.id, msg_clone.id, "id mismatch");
3160 assert_eq!(restored.content, msg_clone.content, "content mismatch");
3161 assert_eq!(restored.mine, msg_clone.mine, "mine mismatch");
3162 assert_eq!(restored.npub, msg_clone.npub, "npub mismatch");
3163 assert_eq!(restored.edit_history, msg_clone.edit_history, "edit_history mismatch");
3164 }
3165
3166 #[test]
3167 fn compact_message_pending_flag() {
3168 let msg = Message {
3169 id: "pending-1234567890".into(),
3170 pending: true,
3171 ..Message::default()
3172 };
3173 let mut interner = NpubInterner::new();
3174 let compact = CompactMessage::from_message(&msg, &mut interner);
3175 assert!(compact.is_pending(), "pending flag should be set");
3176 let restored = compact.to_message(&interner);
3177 assert!(restored.pending, "pending should roundtrip");
3178 assert_eq!(restored.id, "pending-1234567890", "pending ID should roundtrip");
3179 }
3180
3181 #[test]
3182 fn compact_message_failed_flag() {
3183 let msg = Message {
3184 id: "pending-999".into(),
3185 failed: true,
3186 pending: true,
3187 ..Message::default()
3188 };
3189 let mut interner = NpubInterner::new();
3190 let compact = CompactMessage::from_message(&msg, &mut interner);
3191 assert!(compact.is_failed(), "failed flag should be set");
3192 assert!(compact.is_pending(), "pending flag should also be set");
3193 let restored = compact.to_message(&interner);
3194 assert!(restored.failed);
3195 assert!(restored.pending);
3196 }
3197
3198 #[test]
3199 fn compact_message_with_attachments_roundtrip() {
3200 let msg = Message {
3201 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3202 attachments: vec![
3203 Attachment {
3204 id: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3205 extension: "jpg".into(),
3206 name: "sunset.jpg".into(),
3207 size: 5000,
3208 downloaded: true,
3209 ..Attachment::default()
3210 },
3211 Attachment {
3212 id: "2222222222222222222222222222222222222222222222222222222222222222".into(),
3213 extension: "mp4".into(),
3214 name: "video.mp4".into(),
3215 size: 50000,
3216 downloaded: false,
3217 downloading: true,
3218 ..Attachment::default()
3219 },
3220 ],
3221 ..Message::default()
3222 };
3223 let mut interner = NpubInterner::new();
3224 let compact = CompactMessage::from_message(&msg, &mut interner);
3225 assert_eq!(compact.attachments.len(), 2);
3226
3227 let restored = compact.to_message(&interner);
3228 assert_eq!(restored.attachments.len(), 2);
3229 assert_eq!(restored.attachments[0].name, "sunset.jpg");
3230 assert_eq!(restored.attachments[0].extension, "jpg");
3231 assert!(restored.attachments[0].downloaded);
3232 assert_eq!(restored.attachments[1].name, "video.mp4");
3233 assert!(restored.attachments[1].downloading);
3234 assert!(!restored.attachments[1].downloaded);
3235 }
3236
3237 #[test]
3238 fn compact_message_with_reactions_roundtrip() {
3239 let msg = Message {
3240 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3241 reactions: vec![
3242 Reaction {
3243 id: "aaa0000000000000000000000000000000000000000000000000000000000000".into(),
3244 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3245 author_id: "npub1alice".into(),
3246 emoji: "\u{2764}".into(), emoji_url: None,
3248 },
3249 Reaction {
3250 id: "bbb0000000000000000000000000000000000000000000000000000000000000".into(),
3251 reference_id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3252 author_id: "npub1bob".into(),
3253 emoji: "\u{1f525}".into(), emoji_url: None,
3255 },
3256 ],
3257 ..Message::default()
3258 };
3259 let mut interner = NpubInterner::new();
3260 let compact = CompactMessage::from_message(&msg, &mut interner);
3261 let restored = compact.to_message(&interner);
3262
3263 assert_eq!(restored.reactions.len(), 2);
3264 assert_eq!(restored.reactions[0].emoji, "\u{2764}");
3265 assert_eq!(restored.reactions[0].author_id, "npub1alice");
3266 assert_eq!(restored.reactions[1].emoji, "\u{1f525}");
3267 assert_eq!(restored.reactions[1].author_id, "npub1bob");
3268 }
3269
3270 #[test]
3271 fn compact_message_with_edit_history_roundtrip() {
3272 let msg = Message {
3273 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3274 content: "Final version".into(),
3275 edited: true,
3276 edit_history: Some(vec![
3277 EditEntry { content: "First draft".into(), edited_at: 1000 },
3278 EditEntry { content: "Second draft".into(), edited_at: 2000 },
3279 EditEntry { content: "Final version".into(), edited_at: 3000 },
3280 ]),
3281 ..Message::default()
3282 };
3283 let mut interner = NpubInterner::new();
3284 let compact = CompactMessage::from_message(&msg, &mut interner);
3285 assert!(compact.is_edited());
3286
3287 let restored = compact.to_message(&interner);
3288 assert!(restored.edited);
3289 let history = restored.edit_history.unwrap();
3290 assert_eq!(history.len(), 3);
3291 assert_eq!(history[0].content, "First draft");
3292 assert_eq!(history[2].content, "Final version");
3293 }
3294
3295 #[test]
3296 fn compact_message_with_preview_metadata_roundtrip() {
3297 let meta = SiteMetadata {
3298 domain: "example.com".into(),
3299 og_title: Some("Title".into()),
3300 og_description: Some("Desc".into()),
3301 og_image: Some("https://example.com/img.png".into()),
3302 og_url: None,
3303 og_type: None,
3304 title: None,
3305 description: None,
3306 favicon: None,
3307 };
3308 let msg = Message {
3309 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3310 preview_metadata: Some(meta.clone()),
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 let restored_meta = restored.preview_metadata.unwrap();
3318 assert_eq!(restored_meta.domain, "example.com");
3319 assert_eq!(restored_meta.og_title, Some("Title".into()));
3320 assert_eq!(restored_meta.og_image, Some("https://example.com/img.png".into()));
3321 assert_eq!(restored_meta.og_url, None);
3322 }
3323
3324 #[test]
3325 fn compact_message_with_replied_to_roundtrip() {
3326 let msg = Message {
3327 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3328 replied_to: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3329 replied_to_content: Some("Original text".into()),
3330 replied_to_npub: Some("npub1original".into()),
3331 replied_to_has_attachment: Some(false),
3332 ..Message::default()
3333 };
3334 let mut interner = NpubInterner::new();
3335 let compact = CompactMessage::from_message(&msg, &mut interner);
3336 assert!(compact.has_reply());
3337
3338 let restored = compact.to_message(&interner);
3339 assert_eq!(restored.replied_to, "1111111111111111111111111111111111111111111111111111111111111111");
3340 assert_eq!(restored.replied_to_content, Some("Original text".into()));
3341 assert_eq!(restored.replied_to_npub, Some("npub1original".into()));
3342 assert_eq!(restored.replied_to_has_attachment, Some(false));
3343 }
3344
3345 #[test]
3346 fn compact_message_empty_roundtrip() {
3347 let msg = Message::default();
3348 let mut interner = NpubInterner::new();
3349 let compact = CompactMessage::from_message(&msg, &mut interner);
3350 let restored = compact.to_message(&interner);
3351
3352 assert_eq!(restored.id, "0000000000000000000000000000000000000000000000000000000000000000",
3353 "empty ID should decode as all zeros hex");
3354 assert_eq!(restored.content, "");
3355 assert!(!restored.mine);
3356 assert!(!restored.pending);
3357 assert!(!restored.failed);
3358 assert!(!restored.edited);
3359 assert_eq!(restored.npub, None);
3360 assert!(restored.replied_to.is_empty() || restored.replied_to == "0000000000000000000000000000000000000000000000000000000000000000");
3361 assert_eq!(restored.replied_to_content, None);
3362 assert_eq!(restored.replied_to_npub, None);
3363 assert_eq!(restored.replied_to_has_attachment, None);
3364 assert_eq!(restored.wrapper_event_id, None);
3365 assert!(restored.attachments.is_empty());
3366 assert!(restored.reactions.is_empty());
3367 assert_eq!(restored.edit_history, None);
3368 assert_eq!(restored.preview_metadata, None);
3369 }
3370
3371 #[test]
3376 fn compact_attachment_from_attachment_roundtrip() {
3377 let att = Attachment {
3378 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3379 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3380 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), extension: "zip".into(),
3382 name: "archive.zip".into(),
3383 url: "https://blossom.test.com".into(),
3384 path: "/downloads/archive.zip".into(),
3385 size: 99999,
3386 img_meta: None,
3387 downloading: false,
3388 downloaded: true,
3389 webxdc_topic: None,
3390 group_id: None,
3391 original_hash: None,
3392 scheme_version: None,
3393 mls_filename: None,
3394 };
3395
3396 let compact = CompactAttachment::from_attachment(&att);
3397 let restored = compact.to_attachment();
3398
3399 assert_eq!(restored.id, att.id, "id mismatch");
3400 assert_eq!(restored.key, att.key, "key mismatch");
3401 assert_eq!(restored.nonce, att.nonce, "nonce mismatch");
3402 assert_eq!(restored.extension, att.extension, "extension mismatch");
3403 assert_eq!(restored.name, att.name, "name mismatch");
3404 assert_eq!(restored.url, att.url, "url mismatch");
3405 assert_eq!(restored.path, att.path, "path mismatch");
3406 assert_eq!(restored.size, att.size, "size mismatch");
3407 assert_eq!(restored.downloading, att.downloading, "downloading mismatch");
3408 assert_eq!(restored.downloaded, att.downloaded, "downloaded mismatch");
3409 }
3410
3411 #[test]
3412 fn compact_attachment_from_attachment_owned_roundtrip() {
3413 let att = Attachment {
3414 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3415 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3416 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3417 extension: "pdf".into(),
3418 name: "document.pdf".into(),
3419 url: "https://server.com".into(),
3420 path: "".into(),
3421 size: 1024,
3422 img_meta: None,
3423 downloading: false,
3424 downloaded: false,
3425 webxdc_topic: None,
3426 group_id: None,
3427 original_hash: None,
3428 scheme_version: None,
3429 mls_filename: None,
3430 };
3431 let att_clone = att.clone();
3432
3433 let compact = CompactAttachment::from_attachment_owned(att);
3434 let restored = compact.to_attachment();
3435
3436 assert_eq!(restored.id, att_clone.id);
3437 assert_eq!(restored.name, att_clone.name);
3438 assert_eq!(restored.size, att_clone.size);
3439 }
3440
3441 #[test]
3442 fn compact_attachment_key_nonce_zeros() {
3443 let att = Attachment {
3444 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3445 key: "".into(), nonce: "".into(), ..Attachment::default()
3448 };
3449 let compact = CompactAttachment::from_attachment(&att);
3450 assert_eq!(compact.key, [0u8; 32], "empty key should be all zeros");
3451 assert_eq!(compact.nonce, [0u8; 16], "empty nonce should be all zeros");
3452
3453 let restored = compact.to_attachment();
3454 assert_eq!(restored.key, "", "zero key should restore as empty string");
3455 assert_eq!(restored.nonce, "", "zero nonce should restore as empty string");
3456 }
3457
3458 #[test]
3459 fn compact_attachment_short_nonce_mls_12byte() {
3460 let att = Attachment {
3462 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3463 nonce: "aabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3465 };
3466 let compact = CompactAttachment::from_attachment(&att);
3467 assert!(compact.flags.is_short_nonce(), "12-byte nonce should set short_nonce flag");
3468
3469 let restored = compact.to_attachment();
3470 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccdd", "short nonce should roundtrip");
3471 }
3472
3473 #[test]
3474 fn compact_attachment_long_nonce_dm_16byte() {
3475 let att = Attachment {
3477 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3478 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(), ..Attachment::default()
3480 };
3481 let compact = CompactAttachment::from_attachment(&att);
3482 assert!(!compact.flags.is_short_nonce(), "16-byte nonce should NOT set short_nonce flag");
3483
3484 let restored = compact.to_attachment();
3485 assert_eq!(restored.nonce, "aabbccddaabbccddaabbccddaabbccdd", "long nonce should roundtrip");
3486 }
3487
3488 #[test]
3489 fn compact_attachment_id_eq_comparison() {
3490 let att = Attachment {
3491 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3492 ..Attachment::default()
3493 };
3494 let compact = CompactAttachment::from_attachment(&att);
3495
3496 assert!(compact.id_eq("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"),
3497 "id_eq should match same hex");
3498 assert!(!compact.id_eq("1111111111111111111111111111111111111111111111111111111111111111"),
3499 "id_eq should not match different hex");
3500 }
3501
3502 #[test]
3503 fn compact_attachment_all_optional_fields_none() {
3504 let att = Attachment::default();
3505 let compact = CompactAttachment::from_attachment(&att);
3506
3507 assert!(compact.img_meta.is_none());
3508 assert!(compact.group_id.is_none());
3509 assert!(compact.original_hash.is_none());
3510 assert!(compact.webxdc_topic.is_none());
3511 assert!(compact.mls_filename.is_none());
3512 assert!(compact.scheme_version.is_none());
3513
3514 let restored = compact.to_attachment();
3515 assert!(restored.img_meta.is_none());
3516 assert!(restored.group_id.is_none());
3517 assert!(restored.original_hash.is_none());
3518 assert!(restored.webxdc_topic.is_none());
3519 assert!(restored.mls_filename.is_none());
3520 assert!(restored.scheme_version.is_none());
3521 }
3522
3523 #[test]
3524 fn compact_attachment_all_optional_fields_some() {
3525 let att = Attachment {
3526 id: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".into(),
3527 key: "1111111111111111111111111111111111111111111111111111111111111111".into(),
3528 nonce: "aabbccddaabbccddaabbccddaabbccdd".into(),
3529 extension: "xdc".into(),
3530 name: "app.xdc".into(),
3531 url: "https://server.com/file".into(),
3532 path: "/local/app.xdc".into(),
3533 size: 50000,
3534 img_meta: Some(ImageMetadata {
3535 thumbhash: "hash123".into(),
3536 width: 1920,
3537 height: 1080,
3538 }),
3539 downloading: false,
3540 downloaded: true,
3541 webxdc_topic: Some("game-state".into()),
3542 group_id: Some("cccc000000000000000000000000000000000000000000000000000000000000".into()),
3543 original_hash: Some("dddd000000000000000000000000000000000000000000000000000000000000".into()),
3544 scheme_version: Some("mip04-v1".into()),
3545 mls_filename: Some("encrypted.bin".into()),
3546 };
3547
3548 let compact = CompactAttachment::from_attachment(&att);
3549
3550 assert!(compact.img_meta.is_some());
3551 assert!(compact.group_id.is_some());
3552 assert!(compact.original_hash.is_some());
3553 assert!(compact.webxdc_topic.is_some());
3554 assert!(compact.mls_filename.is_some());
3555 assert!(compact.scheme_version.is_some());
3556
3557 let restored = compact.to_attachment();
3558 let meta = restored.img_meta.unwrap();
3559 assert_eq!(meta.thumbhash, "hash123");
3560 assert_eq!(meta.width, 1920);
3561 assert_eq!(meta.height, 1080);
3562 assert_eq!(restored.webxdc_topic, Some("game-state".into()));
3563 assert_eq!(restored.group_id.unwrap(), att.group_id.unwrap());
3564 assert_eq!(restored.original_hash.unwrap(), att.original_hash.unwrap());
3565 assert_eq!(restored.scheme_version, Some("mip04-v1".into()));
3566 assert_eq!(restored.mls_filename, Some("encrypted.bin".into()));
3567 }
3568
3569 #[test]
3574 fn compact_reaction_from_reaction_roundtrip() {
3575 let reaction = Reaction {
3576 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3577 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3578 author_id: "npub1alice".into(),
3579 emoji: "+".into(),
3580 emoji_url: None,
3581 };
3582 let mut interner = NpubInterner::new();
3583 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3584 let restored = compact.to_reaction(&interner);
3585
3586 assert_eq!(restored.id, reaction.id, "id mismatch");
3587 assert_eq!(restored.reference_id, reaction.reference_id, "reference_id mismatch");
3588 assert_eq!(restored.author_id, reaction.author_id, "author_id mismatch");
3589 assert_eq!(restored.emoji, reaction.emoji, "emoji mismatch");
3590 }
3591
3592 #[test]
3593 fn compact_reaction_author_resolved_via_interner() {
3594 let mut interner = NpubInterner::new();
3595 let alice_handle = interner.intern("npub1alice");
3596
3597 let reaction = Reaction {
3598 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3599 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3600 author_id: "npub1alice".into(),
3601 emoji: "+".into(),
3602 emoji_url: None,
3603 };
3604 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3605 assert_eq!(compact.author_idx, alice_handle, "should reuse existing interner handle");
3606
3607 let resolved = interner.resolve(compact.author_idx).unwrap();
3608 assert_eq!(resolved, "npub1alice");
3609 }
3610
3611 #[test]
3612 fn compact_reaction_unicode_emoji() {
3613 let reaction = Reaction {
3614 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3615 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3616 author_id: "npub1test".into(),
3617 emoji: "\u{1f431}\u{200d}\u{1f4bb}".into(), emoji_url: None,
3619 };
3620 let mut interner = NpubInterner::new();
3621 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3622 let restored = compact.to_reaction(&interner);
3623 assert_eq!(restored.emoji, "\u{1f431}\u{200d}\u{1f4bb}", "complex unicode emoji should roundtrip");
3624 }
3625
3626 #[test]
3627 fn compact_reaction_custom_emoji() {
3628 let reaction = Reaction {
3629 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3630 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3631 author_id: "npub1test".into(),
3632 emoji: ":cat_heart_eyes:".into(),
3633 emoji_url: None,
3634 };
3635 let mut interner = NpubInterner::new();
3636 let compact = CompactReaction::from_reaction(&reaction, &mut interner);
3637 let restored = compact.to_reaction(&interner);
3638 assert_eq!(restored.emoji, ":cat_heart_eyes:", "custom emoji shortcode should roundtrip");
3639 }
3640
3641 #[test]
3642 fn compact_reaction_owned_conversion() {
3643 let reaction = Reaction {
3644 id: "aaaa000000000000000000000000000000000000000000000000000000000000".into(),
3645 reference_id: "bbbb000000000000000000000000000000000000000000000000000000000000".into(),
3646 author_id: "npub1bob".into(),
3647 emoji: "\u{1f44d}".into(), emoji_url: None,
3649 };
3650 let reaction_clone = reaction.clone();
3651 let mut interner = NpubInterner::new();
3652 let compact = CompactReaction::from_reaction_owned(reaction, &mut interner);
3653 let restored = compact.to_reaction(&interner);
3654
3655 assert_eq!(restored.id, reaction_clone.id);
3656 assert_eq!(restored.author_id, reaction_clone.author_id);
3657 assert_eq!(restored.emoji, reaction_clone.emoji);
3658 }
3659
3660 #[test]
3665 fn tinyvec_empty() {
3666 let tv: TinyVec<u32> = TinyVec::new();
3667 assert!(tv.is_empty());
3668 assert_eq!(tv.len(), 0);
3669 assert_eq!(tv.as_slice(), &[] as &[u32]);
3670 assert_eq!(tv.first(), None);
3671 assert_eq!(tv.last(), None);
3672 }
3673
3674 #[test]
3675 fn tinyvec_from_vec_and_back() {
3676 let original = vec![1u32, 2, 3, 4, 5];
3677 let tv = TinyVec::from_vec(original.clone());
3678 assert_eq!(tv.len(), 5);
3679 assert_eq!(tv.to_vec(), original);
3680 }
3681
3682 #[test]
3683 fn tinyvec_indexing() {
3684 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3685 assert_eq!(tv[0], 10);
3686 assert_eq!(tv[1], 20);
3687 assert_eq!(tv[2], 30);
3688 assert_eq!(tv.get(0), Some(&10));
3689 assert_eq!(tv.get(3), None);
3690 }
3691
3692 #[test]
3693 fn tinyvec_push() {
3694 let mut tv = TinyVec::from_vec(vec![1u32, 2]);
3695 tv.push(3);
3696 assert_eq!(tv.len(), 3);
3697 assert_eq!(tv.to_vec(), vec![1, 2, 3]);
3698 }
3699
3700 #[test]
3701 fn tinyvec_clone() {
3702 let tv = TinyVec::from_vec(vec!["hello".to_string(), "world".to_string()]);
3703 let cloned = tv.clone();
3704 assert_eq!(cloned.len(), 2);
3705 assert_eq!(cloned[0], "hello");
3706 assert_eq!(cloned[1], "world");
3707 }
3708
3709 #[test]
3710 fn tinyvec_retain() {
3711 let mut tv = TinyVec::from_vec(vec![1u32, 2, 3, 4, 5]);
3712 tv.retain(|&x| x % 2 == 0);
3713 assert_eq!(tv.to_vec(), vec![2, 4]);
3714 }
3715
3716 #[test]
3717 fn tinyvec_empty_from_empty_vec() {
3718 let tv = TinyVec::<u32>::from_vec(vec![]);
3719 assert!(tv.is_empty());
3720 assert_eq!(tv.len(), 0);
3721 }
3722
3723 #[test]
3724 fn tinyvec_iter() {
3725 let tv = TinyVec::from_vec(vec![10u32, 20, 30]);
3726 let sum: u32 = tv.iter().sum();
3727 assert_eq!(sum, 60);
3728 }
3729
3730 #[test]
3731 fn tinyvec_any() {
3732 let tv = TinyVec::from_vec(vec![1u32, 2, 3]);
3733 assert!(tv.any(|&x| x == 2));
3734 assert!(!tv.any(|&x| x == 99));
3735 }
3736
3737 #[test]
3742 fn hex_to_bytes_16_full_32_chars() {
3743 let hex = "aabbccddaabbccddaabbccddaabbccdd";
3744 let bytes = hex_to_bytes_16(hex);
3745 assert_eq!(bytes[0], 0xaa);
3746 assert_eq!(bytes[1], 0xbb);
3747 assert_eq!(bytes[15], 0xdd);
3748 }
3749
3750 #[test]
3751 fn hex_to_bytes_16_short_input_padded() {
3752 let hex = "aabb";
3754 let bytes = hex_to_bytes_16(hex);
3755 assert_eq!(bytes[0], 0xaa);
3756 assert_eq!(bytes[1], 0xbb);
3757 for i in 2..16 {
3759 assert_eq!(bytes[i], 0, "byte {} should be 0 from padding", i);
3760 }
3761 }
3762
3763 #[test]
3764 fn bytes_to_hex_string_roundtrip() {
3765 let bytes: Vec<u8> = vec![0xaa, 0xbb, 0xcc, 0xdd];
3766 let hex = bytes_to_hex_string(&bytes);
3767 assert_eq!(hex, "aabbccdd");
3768 }
3769}