nostrdb-rs

nostrdb in rust!
git clone git://jb55.com/nostrdb-rs
Log | Files | Refs | Submodules | README | LICENSE

commit b59268afde43cbf0c8f9f1ae584679ea82191a11
parent 31ea02a27a384618b44e02f38e6176fc4040c982
Author: William Casarin <jb55@jb55.com>
Date:   Fri, 28 Jun 2024 10:55:58 -0700

Add note builder api

Fixes: https://github.com/damus-io/nostrdb-rs/issues/10
Changelog-Fixed: Fixed double-free issue for owned notes
Changelog-Added: Added note builder interface
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mshell.nix | 2+-
Msrc/lib.rs | 2+-
Msrc/ndb_str.rs | 6+++---
Msrc/note.rs | 326++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/tags.rs | 49+++++++++++++++++++++++--------------------------
Msrc/util/nip10.rs | 1+
6 files changed, 353 insertions(+), 33 deletions(-)

diff --git a/shell.nix b/shell.nix @@ -1,7 +1,7 @@ { pkgs ? import <nixpkgs> {} }: with pkgs; mkShell { - nativeBuildInputs = [ rustPlatform.bindgenHook cargo clippy rustc rustfmt libiconv pkg-config valgrind ]; + nativeBuildInputs = [ rustPlatform.bindgenHook cargo clippy rustc rustfmt libiconv pkg-config valgrind gdb ]; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; } diff --git a/src/lib.rs b/src/lib.rs @@ -33,7 +33,7 @@ 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 note::{Note, NoteBuildOptions, NoteBuilder, NoteKey}; pub use profile::{ProfileKey, ProfileRecord}; pub use query::QueryResult; pub use result::Result; diff --git a/src/ndb_str.rs b/src/ndb_str.rs @@ -2,7 +2,7 @@ use crate::{bindings, Note}; pub struct NdbStr<'a> { ndb_str: bindings::ndb_str, - note: Note<'a>, + note: &'a Note<'a>, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -39,10 +39,10 @@ impl bindings::ndb_str { impl<'a> NdbStr<'a> { pub fn note(&self) -> &Note<'a> { - &self.note + self.note } - pub(crate) fn new(ndb_str: bindings::ndb_str, note: Note<'a>) -> Self { + pub(crate) fn new(ndb_str: bindings::ndb_str, note: &'a Note<'a>) -> Self { NdbStr { ndb_str, note } } diff --git a/src/note.rs b/src/note.rs @@ -1,6 +1,7 @@ use crate::bindings; use crate::tags::Tags; use crate::transaction::Transaction; +use ::std::os::raw::c_uchar; use std::hash::Hash; #[derive(Debug, Clone, Copy, Eq, Ord, PartialEq, PartialOrd, Hash)] @@ -16,6 +17,35 @@ impl NoteKey { } } +pub struct NoteBuildOptions<'a> { + /// Generate the created_at based on the current time, otherwise the id field will remain untouched + pub set_created_at: bool, + + /// Sign with the secret key, otherwise sig field will remain untouched + pub sign_key: Option<&'a [u8; 32]>, +} + +impl<'a> Default for NoteBuildOptions<'a> { + fn default() -> Self { + NoteBuildOptions { + set_created_at: true, + sign_key: None, + } + } +} + +impl<'a> NoteBuildOptions<'a> { + pub fn created_at(mut self, set_created_at: bool) -> Self { + self.set_created_at = set_created_at; + self + } + + pub fn sign(mut self, seckey: &'a [u8; 32]) -> NoteBuildOptions<'a> { + self.sign_key = Some(seckey); + self + } +} + #[derive(Debug, Clone)] pub enum Note<'a> { /// A note in-memory outside of nostrdb. This note is a pointer to a note in @@ -97,6 +127,12 @@ impl<'a> Note<'a> { } } + /* + pub fn json() -> String { + unsafe { bindings::ndb_note_json() } + } + */ + fn content_size(&self) -> usize { unsafe { bindings::ndb_note_content_length(self.as_ptr()) as usize } } @@ -137,9 +173,9 @@ impl<'a> Note<'a> { unsafe { bindings::ndb_note_kind(self.as_ptr()) } } - pub fn tags(&self) -> Tags<'a> { + pub fn tags(&'a self) -> Tags<'a> { let tags = unsafe { bindings::ndb_note_tags(self.as_ptr()) }; - Tags::new(tags, self.clone()) + Tags::new(tags, self) } pub fn sig(&self) -> &'a [u8; 64] { @@ -158,6 +194,235 @@ impl<'a> Drop for Note<'a> { } } +impl bindings::ndb_builder { + fn as_mut_ptr(&mut self) -> *mut bindings::ndb_builder { + self as *mut bindings::ndb_builder + } +} + +impl bindings::ndb_keypair { + fn as_mut_ptr(&mut self) -> *mut bindings::ndb_keypair { + self as *mut bindings::ndb_keypair + } +} + +impl Default for bindings::ndb_keypair { + fn default() -> Self { + bindings::ndb_keypair { + pubkey: [0; 32], + secret: [0; 32], + pair: [0; 96], + } + } +} + +impl Default for bindings::ndb_builder { + fn default() -> Self { + bindings::ndb_builder { + mem: bindings::cursor::default(), + note_cur: bindings::cursor::default(), + strings: bindings::cursor::default(), + str_indices: bindings::cursor::default(), + note: std::ptr::null_mut(), + current_tag: std::ptr::null_mut(), + } + } +} + +impl Default for bindings::cursor { + fn default() -> Self { + Self { + start: std::ptr::null_mut(), + p: std::ptr::null_mut(), + end: std::ptr::null_mut(), + } + } +} + +pub struct NoteBuilder<'a> { + buffer: *mut ::std::os::raw::c_uchar, + builder: bindings::ndb_builder, + options: NoteBuildOptions<'a>, +} + +impl<'a> Default for NoteBuilder<'a> { + fn default() -> Self { + NoteBuilder::new() + } +} + +impl<'a> NoteBuilder<'a> { + pub fn with_bufsize(size: usize) -> Option<Self> { + let buffer: *mut c_uchar = unsafe { libc::malloc(size as libc::size_t) as *mut c_uchar }; + if buffer.is_null() { + return None; + } + + let mut builder = NoteBuilder { + buffer, + options: NoteBuildOptions::default(), + builder: bindings::ndb_builder::default(), + }; + + let ok = unsafe { + bindings::ndb_builder_init(builder.builder.as_mut_ptr(), builder.buffer, size) != 0 + }; + + if !ok { + // this really shouldn't happen + return None; + } + + Some(builder) + } + + /// Create a note builder with a 1mb buffer, if you need bigger notes + /// then use with_bufsize with a custom buffer size + pub fn new() -> Self { + let default_bufsize = 1024usize * 1024usize; + Self::with_bufsize(default_bufsize).expect("OOM when creating NoteBuilder") + } + + pub fn as_mut_ptr(&mut self) -> *mut bindings::ndb_builder { + &mut self.builder as *mut bindings::ndb_builder + } + + pub fn sig(mut self, signature: &[u8; 64]) -> Self { + self.options.sign_key = None; + unsafe { + bindings::ndb_builder_set_sig(self.as_mut_ptr(), signature.as_ptr() as *mut c_uchar) + }; + self + } + + pub fn id(mut self, id: &[u8; 32]) -> Self { + unsafe { bindings::ndb_builder_set_id(self.as_mut_ptr(), id.as_ptr() as *mut c_uchar) }; + self + } + + pub fn content(mut self, content: &str) -> Self { + unsafe { + // Call the external C function with the appropriate arguments + bindings::ndb_builder_set_content( + self.as_mut_ptr(), + content.as_ptr() as *const ::std::os::raw::c_char, + content.len() as ::std::os::raw::c_int, + ); + } + self + } + + pub fn created_at(mut self, created_at: u64) -> Self { + self.options.set_created_at = false; + self.set_created_at(created_at); + self + } + + pub fn kind(mut self, kind: u32) -> Self { + unsafe { + bindings::ndb_builder_set_kind(self.as_mut_ptr(), kind); + }; + self + } + + pub fn pubkey(mut self, pubkey: &[u8; 32]) -> Self { + self.set_pubkey(pubkey); + self + } + + fn set_pubkey(&mut self, pubkey: &[u8; 32]) { + unsafe { + bindings::ndb_builder_set_pubkey(self.as_mut_ptr(), pubkey.as_ptr() as *mut c_uchar) + }; + } + + fn set_created_at(&mut self, created_at: u64) { + unsafe { + bindings::ndb_builder_set_created_at(self.as_mut_ptr(), created_at); + }; + } + + pub fn start_tag(mut self) -> Self { + unsafe { + bindings::ndb_builder_new_tag(self.as_mut_ptr()); + }; + self + } + + pub fn tag_str(mut self, str: &str) -> Self { + unsafe { + // Call the external C function with the appropriate arguments + bindings::ndb_builder_push_tag_str( + self.as_mut_ptr(), + str.as_ptr() as *const ::std::os::raw::c_char, + str.len() as ::std::os::raw::c_int, + ); + } + self + } + + pub fn options(mut self, options: NoteBuildOptions<'a>) -> NoteBuilder<'a> { + self.options = options; + self + } + + pub fn sign(mut self, seckey: &'a [u8; 32]) -> NoteBuilder<'a> { + self.options = self.options.sign(seckey); + self + } + + pub fn build(&mut self) -> Option<Note<'static>> { + let mut note_ptr: *mut bindings::ndb_note = std::ptr::null_mut(); + let mut keypair = bindings::ndb_keypair::default(); + + if self.options.set_created_at { + let start = std::time::SystemTime::now(); + if let Ok(since_the_epoch) = start.duration_since(std::time::UNIX_EPOCH) { + let timestamp = since_the_epoch.as_secs(); + self.set_created_at(timestamp); + } else { + return None; + } + } + + let keypair_ptr = if let Some(sec) = self.options.sign_key { + keypair.secret.copy_from_slice(sec); + let ok = unsafe { bindings::ndb_create_keypair(keypair.as_mut_ptr()) != 0 }; + if ok { + // if we're signing, we should set the pubkey as well + self.set_pubkey(&keypair.pubkey); + keypair.as_mut_ptr() + } else { + std::ptr::null_mut() + } + } else { + std::ptr::null_mut() + }; + + let size = unsafe { + bindings::ndb_builder_finalize( + self.as_mut_ptr(), + &mut note_ptr as *mut *mut bindings::ndb_note, + keypair_ptr, + ) as usize + }; + + if size == 0 { + return None; + } + + note_ptr = unsafe { + libc::realloc(note_ptr as *mut libc::c_void, size) as *mut bindings::ndb_note + }; + + if note_ptr.is_null() { + return None; + } + + Some(Note::new_owned(note_ptr, size)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -185,4 +450,61 @@ mod tests { test_util::cleanup_db(db); } + + #[test] + fn note_builder_works() { + let pubkey: [u8; 32] = [ + 0x6c, 0x54, 0x0e, 0xd0, 0x60, 0xbf, 0xc2, 0xb0, 0xc5, 0xb6, 0xf0, 0x9c, 0xd3, 0xeb, + 0xed, 0xf9, 0x80, 0xef, 0x7b, 0xc8, 0x36, 0xd6, 0x95, 0x82, 0x36, 0x1d, 0x20, 0xf2, + 0xad, 0x12, 0x4f, 0x23, + ]; + + let seckey: [u8; 32] = [ + 0xd8, 0x62, 0x2e, 0x92, 0x47, 0xab, 0x39, 0x30, 0x11, 0x7e, 0x66, 0x45, 0xd5, 0xf7, + 0x8b, 0x66, 0xbd, 0xd3, 0xaf, 0xe2, 0x46, 0x4f, 0x90, 0xbc, 0xd9, 0xe0, 0x38, 0x75, + 0x8d, 0x2d, 0x55, 0x34, + ]; + + let id: [u8; 32] = [ + 0xfb, 0x16, 0x5b, 0xe2, 0x2c, 0x7b, 0x25, 0x18, 0xb7, 0x49, 0xaa, 0xbb, 0x71, 0x40, + 0xc7, 0x3f, 0x08, 0x87, 0xfe, 0x84, 0x47, 0x5c, 0x82, 0x78, 0x57, 0x00, 0x66, 0x3b, + 0xe8, 0x5b, 0xa8, 0x59, + ]; + + let note = NoteBuilder::new() + .kind(1) + .content("this is the content") + .created_at(42) + .start_tag() + .tag_str("comment") + .tag_str("this is a comment") + .start_tag() + .tag_str("blah") + .tag_str("something") + .sign(&seckey) + .build() + .expect("expected build to work"); + + assert_eq!(note.created_at(), 42); + assert_eq!(note.content(), "this is the content"); + assert_eq!(note.kind(), 1); + assert_eq!(note.pubkey(), &pubkey); + assert!(note.sig() != &[0; 64]); + assert_eq!(note.id(), &id); + + for tag in note.tags() { + assert_eq!(tag.get_unchecked(0).variant().str().unwrap(), "comment"); + assert_eq!( + tag.get_unchecked(1).variant().str().unwrap(), + "this is a comment" + ); + break; + } + + for tag in note.tags().iter().skip(1) { + assert_eq!(tag.get_unchecked(0).variant().str().unwrap(), "blah"); + assert_eq!(tag.get_unchecked(1).variant().str().unwrap(), "something"); + break; + } + } } diff --git a/src/tags.rs b/src/tags.rs @@ -1,13 +1,13 @@ use crate::{bindings, NdbStr, Note}; #[derive(Debug, Clone)] -pub struct Tag<'a> { +pub struct Tag<'n> { ptr: *mut bindings::ndb_tag, - note: Note<'a>, + note: &'n Note<'n>, } -impl<'a> Tag<'a> { - pub(crate) fn new(ptr: *mut bindings::ndb_tag, note: Note<'a>) -> Self { +impl<'n> Tag<'n> { + pub(crate) fn new(ptr: *mut bindings::ndb_tag, note: &'n Note<'n>) -> Self { Tag { ptr, note } } @@ -15,7 +15,7 @@ impl<'a> Tag<'a> { unsafe { bindings::ndb_tag_count(self.as_ptr()) } } - pub fn get_unchecked(&self, ind: u16) -> NdbStr<'a> { + pub fn get_unchecked(&self, ind: u16) -> NdbStr<'n> { let nstr = unsafe { bindings::ndb_tag_str( self.note().as_ptr(), @@ -23,18 +23,18 @@ impl<'a> Tag<'a> { ind as ::std::os::raw::c_int, ) }; - NdbStr::new(nstr, self.note.clone()) + NdbStr::new(nstr, self.note) } - pub fn get(&self, ind: u16) -> Option<NdbStr<'a>> { + pub fn get(&self, ind: u16) -> Option<NdbStr<'n>> { if ind >= self.count() { return None; } Some(self.get_unchecked(ind)) } - pub fn note(&self) -> &Note<'a> { - &self.note + pub fn note(&'n self) -> &'n Note<'n> { + self.note } pub fn as_ptr(&self) -> *mut bindings::ndb_tag { @@ -54,20 +54,20 @@ impl<'a> IntoIterator for Tag<'a> { #[derive(Debug, Clone)] pub struct Tags<'a> { ptr: *mut bindings::ndb_tags, - note: Note<'a>, + note: &'a Note<'a>, } impl<'a> IntoIterator for Tags<'a> { type Item = Tag<'a>; type IntoIter = TagsIter<'a>; - fn into_iter(self) -> Self::IntoIter { - TagsIter::new(self.note().clone()) + fn into_iter(self) -> TagsIter<'a> { + TagsIter::new(self.note()) } } impl<'a> Tags<'a> { - pub(crate) fn new(ptr: *mut bindings::ndb_tags, note: Note<'a>) -> Self { + pub(crate) fn new(ptr: *mut bindings::ndb_tags, note: &'a Note<'a>) -> Self { Tags { ptr, note } } @@ -76,11 +76,11 @@ impl<'a> Tags<'a> { } pub fn iter(&self) -> TagsIter<'a> { - TagsIter::new(self.note.clone()) + TagsIter::new(self.note) } - pub fn note(&self) -> &Note<'a> { - &self.note + pub fn note(&self) -> &'a Note<'a> { + self.note } pub fn as_ptr(&self) -> *mut bindings::ndb_tags { @@ -91,20 +91,17 @@ impl<'a> Tags<'a> { #[derive(Debug, Clone)] pub struct TagsIter<'a> { iter: bindings::ndb_iterator, - note: Note<'a>, + note: &'a Note<'a>, } impl<'a> TagsIter<'a> { - pub fn new(note: Note<'a>) -> Self { + 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: note.clone(), - iter, - }; + let mut iter = TagsIter { note, iter }; unsafe { bindings::ndb_tags_iterate_start(note.as_ptr(), &mut iter.iter); }; @@ -116,12 +113,12 @@ impl<'a> TagsIter<'a> { if tag_ptr.is_null() { None } else { - Some(Tag::new(tag_ptr, self.note().clone())) + Some(Tag::new(tag_ptr, self.note())) } } - pub fn note(&self) -> &Note<'a> { - &self.note + pub fn note(&self) -> &'a Note<'a> { + self.note } pub fn as_ptr(&self) -> *const bindings::ndb_iterator { @@ -153,7 +150,7 @@ impl<'a> TagIter<'a> { impl<'a> Iterator for TagIter<'a> { type Item = NdbStr<'a>; - fn next(&mut self) -> Option<Self::Item> { + fn next(&mut self) -> Option<NdbStr<'a>> { let tag = self.tag.get(self.index); if tag.is_some() { self.index += 1; diff --git a/src/util/nip10.rs b/src/util/nip10.rs @@ -242,6 +242,7 @@ pub fn tag_to_noteid_ref(tag: Tag<'_>, index: u16) -> Result<NoteIdRef<'_>, Erro .get(2) .and_then(|t| t.variant().str()) .filter(|x| !x.is_empty()); + let marker = tag .get(3) .and_then(|t| t.variant().str())