From 5010624faf49f174e0a1761b535ad65c1644ecc3 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Tue, 2 Apr 2024 21:12:29 +0200 Subject: [PATCH 1/2] Initial note tag support This commit adds support for iterating over note tags and tag elements. This is zero-copy and heavily leverages rust lifetimes to ensure we always have access within a transaction and note reference. New types ========= NdbStr - ndb note strings, offsets into the note string table NdbStrVariant - ndb note string variants. can be 32-byte values or strings Tag - A tag ["e", "abcdef..."] Tags - Note tags [["hi", "3"], ["proxy", "..."]] TagIter - An iterator over tag elements, producing NdbStr's TagsIter - An iterator over tags elements, producing Tag's Usage ===== for tag in note.tags().iter() { for nstr in tag.iter() { match nstr.variant() { NdbStrVariant::Str(s) => // string NdbStrVariant::Id(s) => // 32-byte id } } } Changelog-Added: Add tags and tag iterators to Note Fixes: https://github.com/damus-io/nostrdb-rs/issues/2 Cc: yukikishimoto@protonmail.com --- src/bindings.rs | 2 +- src/lib.rs | 3 + src/ndb.rs | 16 ++-- src/ndb_str.rs | 54 ++++++++++++ src/note.rs | 8 +- src/tags.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 src/ndb_str.rs create mode 100644 src/tags.rs diff --git a/src/bindings.rs b/src/bindings.rs index 656f1dd..675544a 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -1,4 +1,4 @@ -/* automatically generated by rust-bindgen 0.69.2 */ +/* automatically generated by rust-bindgen 0.69.1 */ #[repr(C)] #[derive(Default)] diff --git a/src/lib.rs b/src/lib.rs index 469b467..625ee87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,11 +13,13 @@ mod config; mod error; mod filter; mod ndb; +mod ndb_str; mod note; mod profile; mod query; mod result; mod subscription; +mod tags; mod transaction; pub use block::{Block, BlockType, Blocks, Mention}; @@ -26,6 +28,7 @@ pub use error::Error; pub use filter::Filter; pub use ndb::Ndb; pub use ndb_profile::{NdbProfile, NdbProfileRecord}; +pub use ndb_str::{NdbStr, NdbStrVariant}; pub use note::{Note, NoteKey}; pub use profile::ProfileRecord; pub use query::QueryResult; diff --git a/src/ndb.rs b/src/ndb.rs index 3e2aaf6..f0b1021 100644 --- a/src/ndb.rs +++ b/src/ndb.rs @@ -129,7 +129,7 @@ impl Ndb { } } - pub fn poll_for_notes(&self, sub: &Subscription, max_notes: u32) -> Vec { + pub fn poll_for_notes(&self, sub: &Subscription, max_notes: u32) -> Vec { let mut vec = vec![]; vec.reserve_exact(max_notes as usize); let sub_id = sub.id; @@ -144,14 +144,14 @@ impl Ndb { vec.set_len(res as usize); }; - vec + vec.into_iter().map(|n| NoteKey::new(n)).collect() } - pub async fn wait_for_notes(&self, sub: &Subscription, max_notes: u32) -> Result> { + pub async fn wait_for_notes(&self, sub: &Subscription, max_notes: u32) -> Result> { let ndb = self.clone(); let sub_id = sub.id; let handle = task::spawn_blocking(move || { - let mut vec = vec![]; + let mut vec: Vec = vec![]; vec.reserve_exact(max_notes as usize); let res = unsafe { bindings::ndb_wait_for_notes( @@ -172,7 +172,7 @@ impl Ndb { }); match handle.await { - Ok(Ok(res)) => Ok(res), + Ok(Ok(res)) => Ok(res.into_iter().map(|n| NoteKey::new(n)).collect()), Ok(Err(err)) => Err(err), Err(_) => Err(Error::SubscriptionError), } @@ -335,7 +335,7 @@ mod tests { let waiter = ndb.wait_for_notes(&sub, 1); ndb.process_event(r#"["EVENT","b",{"id": "702555e52e82cc24ad517ba78c21879f6e47a7c0692b9b20df147916ae8731a3","pubkey": "32bf915904bfde2d136ba45dde32c88f4aca863783999faea2e847a8fafd2f15","created_at": 1702675561,"kind": 1,"tags": [],"content": "hello, world","sig": "2275c5f5417abfd644b7bc74f0388d70feb5d08b6f90fa18655dda5c95d013bfbc5258ea77c05b7e40e0ee51d8a2efa931dc7a0ec1db4c0a94519762c6625675"}]"#).expect("process ok"); let res = waiter.await.expect("await ok"); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); let txn = Transaction::new(&ndb).expect("txn"); let res = ndb.query(&txn, filters, 1).expect("query ok"); assert_eq!(res.len(), 1); @@ -360,7 +360,7 @@ mod tests { let waiter = ndb.wait_for_notes(&sub, 1); ndb.process_event(r#"["EVENT","b",{"id": "702555e52e82cc24ad517ba78c21879f6e47a7c0692b9b20df147916ae8731a3","pubkey": "32bf915904bfde2d136ba45dde32c88f4aca863783999faea2e847a8fafd2f15","created_at": 1702675561,"kind": 1,"tags": [],"content": "hello, world","sig": "2275c5f5417abfd644b7bc74f0388d70feb5d08b6f90fa18655dda5c95d013bfbc5258ea77c05b7e40e0ee51d8a2efa931dc7a0ec1db4c0a94519762c6625675"}]"#).expect("process ok"); let res = waiter.await.expect("await ok"); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); } } @@ -383,7 +383,7 @@ mod tests { std::thread::sleep(std::time::Duration::from_millis(100)); // now we should have something let res = ndb.poll_for_notes(&sub, 1); - assert_eq!(res, vec![1]); + assert_eq!(res, vec![NoteKey::new(1)]); } } diff --git a/src/ndb_str.rs b/src/ndb_str.rs new file mode 100644 index 0000000..d1b959d --- /dev/null +++ b/src/ndb_str.rs @@ -0,0 +1,54 @@ +use crate::{bindings, Note}; + +pub struct NdbStr<'a> { + ndb_str: bindings::ndb_str, + note: &'a Note<'a>, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum NdbStrVariant<'a> { + Id(&'a [u8; 32]), + Str(&'a str), +} + +impl bindings::ndb_str { + pub fn str(&self) -> *const ::std::os::raw::c_char { + unsafe { self.__bindgen_anon_1.str_ } + } + + pub fn id(&self) -> *const ::std::os::raw::c_uchar { + unsafe { self.__bindgen_anon_1.id } + } +} + +impl<'a> NdbStr<'a> { + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub(crate) fn new(ndb_str: bindings::ndb_str, note: &'a Note<'a>) -> Self { + NdbStr { ndb_str, note } + } + + pub fn len(&self) -> usize { + if self.ndb_str.flag == (bindings::NDB_PACKED_ID as u8) { + 32 + } else { + unsafe { libc::strlen(self.ndb_str.str()) } + } + } + + pub fn variant(&self) -> NdbStrVariant<'a> { + if self.ndb_str.flag == (bindings::NDB_PACKED_ID as u8) { + unsafe { NdbStrVariant::Id(&*(self.ndb_str.id() as *const [u8; 32])) } + } else { + let s = unsafe { + let byte_slice = + std::slice::from_raw_parts(self.ndb_str.str() as *const u8, self.len()); + std::str::from_utf8_unchecked(byte_slice) + }; + + NdbStrVariant::Str(s) + } + } +} diff --git a/src/note.rs b/src/note.rs index 66ab265..37936ec 100644 --- a/src/note.rs +++ b/src/note.rs @@ -1,4 +1,5 @@ use crate::bindings; +use crate::tags::Tags; use crate::transaction::Transaction; use std::hash::Hash; @@ -15,7 +16,7 @@ impl NoteKey { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Note<'a> { /// A note in-memory outside of nostrdb. This note is a pointer to a note in /// memory and will be free'd when [Drop]ped. Method such as [Note::from_json] @@ -135,6 +136,11 @@ impl<'a> Note<'a> { pub fn kind(&self) -> u32 { unsafe { bindings::ndb_note_kind(self.as_ptr()) } } + + pub fn tags(&'a self) -> Tags<'a> { + let tags = unsafe { bindings::ndb_note_tags(self.as_ptr()) }; + Tags::new(tags, self) + } } impl<'a> Drop for Note<'a> { diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..4b62384 --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,221 @@ +use crate::{bindings, NdbStr, Note}; + +#[derive(Debug, Copy, Clone)] +pub struct Tag<'a> { + ptr: *mut bindings::ndb_tag, + note: &'a Note<'a>, +} + +impl<'a> Tag<'a> { + pub(crate) fn new(ptr: *mut bindings::ndb_tag, note: &'a Note<'a>) -> Self { + Tag { ptr, note } + } + + pub fn count(&self) -> u16 { + unsafe { bindings::ndb_tag_count(self.as_ptr()) } + } + + pub fn get(&self, ind: u16) -> Option> { + if ind >= self.count() { + return None; + } + let nstr = unsafe { + bindings::ndb_tag_str( + self.note().as_ptr(), + self.as_ptr(), + ind as ::std::os::raw::c_int, + ) + }; + Some(NdbStr::new(nstr, self.note)) + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *mut bindings::ndb_tag { + self.ptr + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Tags<'a> { + ptr: *mut bindings::ndb_tags, + note: &'a Note<'a>, +} + +impl<'a> Tags<'a> { + pub(crate) fn new(ptr: *mut bindings::ndb_tags, note: &'a Note<'a>) -> Self { + Tags { ptr, note } + } + + pub fn count(&self) -> u16 { + unsafe { bindings::ndb_tags_count(self.as_ptr()) } + } + + pub fn iter(&self) -> TagsIter<'a> { + TagsIter::new(self.note) + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *mut bindings::ndb_tags { + self.ptr + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TagsIter<'a> { + iter: bindings::ndb_iterator, + note: &'a Note<'a>, +} + +impl<'a> TagsIter<'a> { + pub fn new(note: &'a Note<'a>) -> Self { + let iter = bindings::ndb_iterator { + note: std::ptr::null_mut(), + tag: std::ptr::null_mut(), + index: 0, + }; + let mut iter = TagsIter { note, iter }; + unsafe { + bindings::ndb_tags_iterate_start(note.as_ptr(), &mut iter.iter); + }; + iter + } + + pub fn tag(&self) -> Option> { + let tag_ptr = unsafe { *self.as_ptr() }.tag; + if tag_ptr.is_null() { + None + } else { + Some(Tag::new(tag_ptr, self.note())) + } + } + + pub fn note(&self) -> &'a Note<'a> { + self.note + } + + pub fn as_ptr(&self) -> *const bindings::ndb_iterator { + &self.iter + } + + pub fn as_mut_ptr(&mut self) -> *mut bindings::ndb_iterator { + &mut self.iter + } +} + +#[derive(Debug, Copy, Clone)] +pub struct TagIter<'a> { + tag: Tag<'a>, + index: u16, +} + +impl<'a> TagIter<'a> { + pub fn new(tag: Tag<'a>) -> Self { + let index = 0; + TagIter { tag, index } + } + + pub fn done(&self) -> bool { + self.index >= self.tag.count() + } +} + +impl<'a> Iterator for TagIter<'a> { + type Item = NdbStr<'a>; + + fn next(&mut self) -> Option { + let tag = self.tag.get(self.index); + if tag.is_some() { + self.index += 1; + tag + } else { + None + } + } +} + +impl<'a> Iterator for TagsIter<'a> { + type Item = Tag<'a>; + + fn next(&mut self) -> Option { + unsafe { + bindings::ndb_tags_iterate_next(self.as_mut_ptr()); + }; + self.tag() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::test_util; + use crate::{Filter, Ndb, NdbStrVariant, Transaction}; + + #[tokio::test] + async fn tag_iter_works() { + let db = "target/testdbs/tag_iter_works"; + test_util::cleanup_db(&db); + + { + let ndb = Ndb::new(db, &Config::new()).expect("ndb"); + let sub = ndb + .subscribe(vec![Filter::new() + .ids(vec![[ + 0xc5, 0xd9, 0x8c, 0xbf, 0x4b, 0xcd, 0x81, 0x1e, 0x28, 0x66, 0x77, 0x0c, + 0x3d, 0x38, 0x0c, 0x02, 0x84, 0xce, 0x1d, 0xaf, 0x3a, 0xe9, 0x98, 0x3d, + 0x22, 0x56, 0x5c, 0xb0, 0x66, 0xcf, 0x2a, 0x19, + ]]) + .build()]) + .expect("sub"); + let waiter = ndb.wait_for_notes(&sub, 1); + ndb.process_event(r#"["EVENT","s",{"id": "c5d98cbf4bcd811e2866770c3d380c0284ce1daf3ae9983d22565cb066cf2a19","pubkey": "083727b7a6051673f399102dc48c229c0ec08186ecd7e54ad0e9116d38429c4f","created_at": 1712517119,"kind": 1,"tags": [["e","b9e548b4aa30fa4ce9edf552adaf458385716704994fbaa9e0aa0042a5a5e01e"],["p","140ee9ff21da6e6671f750a0a747c5a3487ee8835159c7ca863e867a1c537b4f"],["hi","3"]],"content": "hi","sig": "1eed792e4db69c2bde2f5be33a383ef8b17c6afd1411598d0c4618fbdf4dbcb9689354276a74614511907a45eec234e0786733e8a6fbb312e6abf153f15fd437"}]"#).expect("process ok"); + let res = waiter.await.expect("await ok"); + assert_eq!(res.len(), 1); + let note_key = res[0]; + let txn = Transaction::new(&ndb).expect("txn"); + let note = ndb.get_note_by_key(&txn, note_key).expect("note"); + let tags = note.tags(); + assert_eq!(tags.count(), 3); + + let mut tags_iter = tags.iter(); + + let t0 = tags_iter.next().expect("t0"); + let t0_e0 = t0.get(0).expect("e tag ok"); + let t0_e1 = t0.get(1).expect("e id ok"); + assert_eq!(t0_e0.variant(), NdbStrVariant::Str("e")); + assert_eq!( + t0_e1.variant(), + NdbStrVariant::Id(&[ + 0xb9, 0xe5, 0x48, 0xb4, 0xaa, 0x30, 0xfa, 0x4c, 0xe9, 0xed, 0xf5, 0x52, 0xad, + 0xaf, 0x45, 0x83, 0x85, 0x71, 0x67, 0x04, 0x99, 0x4f, 0xba, 0xa9, 0xe0, 0xaa, + 0x00, 0x42, 0xa5, 0xa5, 0xe0, 0x1e + ]) + ); + + let t1 = tags_iter.next().expect("t1"); + let t1_e0 = t1.get(0).expect("p tag ok"); + let t1_e1 = t1.get(1).expect("p id ok"); + assert_eq!(t1_e0.variant(), NdbStrVariant::Str("p")); + assert_eq!( + t1_e1.variant(), + NdbStrVariant::Id(&[ + 0x14, 0x0e, 0xe9, 0xff, 0x21, 0xda, 0x6e, 0x66, 0x71, 0xf7, 0x50, 0xa0, 0xa7, + 0x47, 0xc5, 0xa3, 0x48, 0x7e, 0xe8, 0x83, 0x51, 0x59, 0xc7, 0xca, 0x86, 0x3e, + 0x86, 0x7a, 0x1c, 0x53, 0x7b, 0x4f + ]) + ); + + let t2 = tags_iter.next().expect("t2"); + let t2_e0 = t2.get(0).expect("hi tag ok"); + let t2_e1 = t2.get(1).expect("hi value ok"); + assert_eq!(t2_e0.variant(), NdbStrVariant::Str("hi")); + assert_eq!(t2_e1.variant(), NdbStrVariant::Str("3")); + } + } +} From 87bf139e699c2493b3242e3d7472acb2cf2683f6 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sun, 7 Apr 2024 13:19:42 -0700 Subject: [PATCH 2/2] note: expose signature field Signed-off-by: William Casarin --- src/note.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/note.rs b/src/note.rs index 37936ec..3c8e0d5 100644 --- a/src/note.rs +++ b/src/note.rs @@ -141,6 +141,13 @@ impl<'a> Note<'a> { let tags = unsafe { bindings::ndb_note_tags(self.as_ptr()) }; Tags::new(tags, self) } + + pub fn sig(&self) -> &'a [u8; 32] { + unsafe { + let ptr = bindings::ndb_note_sig(self.as_ptr()); + &*(ptr as *const [u8; 32]) + } + } } impl<'a> Drop for Note<'a> {