From bb6bf588131ff5275425893d65130be70f8aac92 Mon Sep 17 00:00:00 2001 From: Mike Dilger Date: Thu, 23 Nov 2023 09:51:10 +1300 Subject: [PATCH] types: Tags --- src/types/mod.rs | 3 + src/types/tags.rs | 287 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 290 insertions(+) create mode 100644 src/types/tags.rs diff --git a/src/types/mod.rs b/src/types/mod.rs index 21cd2c1..f5d4677 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -10,5 +10,8 @@ pub use pubkey::Pubkey; mod sig; pub use sig::Sig; +mod tags; +pub use tags::{Tags, TagsIter, TagsStringIter}; + mod time; pub use time::Time; diff --git a/src/types/tags.rs b/src/types/tags.rs new file mode 100644 index 0000000..64250e9 --- /dev/null +++ b/src/types/tags.rs @@ -0,0 +1,287 @@ +use crate::Error; +use std::fmt; + +/* + * 0 .. 2 u16 Length of the tags section + * 2 .. 4 u16 num_tags + * 4 .. 6 u16 offset of zeroeth tag + * 6 .. 8 u16 offset of first tag + * ... + * + * 4+num_tags*2 .. beginning of actual tag data + * + * Tag data looks like this for each tag: + * count, (len, data), (len, data), ... + */ + +/// This stores an array of tags, each tag being an array of byte-strings. +/// It is stored in a single packed linear byte array. +#[derive(Debug, Clone)] +pub struct Tags<'a>(&'a [u8]); + +impl<'a> Tags<'a> { + // this marks off the slice of bytes that represent the tags from a potentially longer input + pub fn delineate(input: &'a [u8]) -> Result, Error> { + if input.len() < 2 { + return Err(Error::EndOfInput); + } + let len = parse_u16!(input, 0) as usize; + if input.len() < len { + return Err(Error::EndOfInput); + } + Ok(Tags(&input[0..len])) + } + + // This copies + pub fn copy(&self, output: &mut [u8]) -> Result<(), Error> { + if output.len() < self.0.len() { + return Err(Error::BufferTooSmall); + } + output[..self.0.len()].copy_from_slice(self.0); + Ok(()) + } + + #[inline] + pub fn as_bytes(&self) -> &[u8] { + self.0 + } + + #[inline] + pub fn len(&self) -> usize { + parse_u16!(self.0, 2) as usize + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn iter(&'a self) -> TagsIter<'a> { + TagsIter { + tags: self, + next: 0, + } + } + + pub fn get_string(&'a self, tag: usize, string: usize) -> Option<&'a [u8]> { + if tag >= self.len() { + return None; + } + + let mut offset = parse_u16!(self.0, 4 + tag * 2) as usize; + let count = parse_u16!(self.0, offset) as usize; + offset += 2; + if string >= count { + return None; + } + + let end = parse_u16!(self.0, 0) as usize; + + // pass the fields we aren't reading + for _ in 0..string { + let len = parse_u16!(self.0, offset) as usize; + offset += 2 + len; + if offset > end { + // safety check + return None; + } + } + let len = parse_u16!(self.0, offset) as usize; + offset += 2; + if offset + len > end { + // safety check + return None; + } + + Some(&self.0[offset..offset + len]) + } + + pub fn get_value(&'a self, key: &[u8]) -> Option<&'a [u8]> { + for tag in 0..self.len() { + if let Some(thing) = self.get_string(tag, 0) { + if thing == key { + return self.get_string(tag, 1); + } + } + } + None + } + + pub fn matches(&self, letter: &[u8], value: &[u8]) -> bool { + for mut tag in self.iter() { + if tag.next() == Some(letter) && tag.next() == Some(value) { + return true; + } + } + false + } + + pub fn as_json(&self) -> Vec { + let mut output: Vec = Vec::with_capacity(256); + let mut first = true; + output.push(b'['); + for tag in self.iter() { + if !first { + output.push(b','); + } + output.push(b'['); + let mut firststring = true; + for bytes in tag { + if !firststring { + output.push(b','); + } + output.push(b'"'); + output.extend(bytes); + output.push(b'"'); + firststring = false; + } + output.push(b']'); + first = false; + } + output.push(b']'); + output + } +} + +impl fmt::Display for Tags<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let bytes = self.as_json(); + let s = unsafe { std::str::from_utf8_unchecked(&bytes) }; + write!(f, "{s}") + } +} + +#[derive(Debug)] +pub struct TagsIter<'a> { + tags: &'a Tags<'a>, + next: usize, +} + +impl<'a> Iterator for TagsIter<'a> { + type Item = TagsStringIter<'a>; + + fn next(&mut self) -> Option { + if self.next >= self.tags.len() { + None + } else { + let offset_slot = 4 + self.next * 2; + let offset = parse_u16!(self.tags.0, offset_slot) as usize; + let count = parse_u16!(self.tags.0, offset) as usize; + self.next += 1; + Some(TagsStringIter { + tags: self.tags, + count, + cur_offset: offset + 2, + next: 0, + }) + } + } +} + +#[derive(Debug)] +pub struct TagsStringIter<'a> { + tags: &'a Tags<'a>, + count: usize, + cur_offset: usize, + next: usize, +} + +impl<'a> Iterator for TagsStringIter<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option { + if self.next >= self.count { + None + } else { + // Read len + let len = parse_u16!(self.tags.0, self.cur_offset) as usize; + let s = &self.tags.0[self.cur_offset + 2..self.cur_offset + 2 + len]; + self.cur_offset += 2 + len; + self.next += 1; + Some(s) + } + } +} + +#[cfg(test)] +mod test { + use super::Tags; + + #[test] + fn test_tag() { + /* + * [ + * ["Hello world!", "Hello", "world!"], + * ["p", ""ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"], + * ] + */ + + let data: Vec = vec![ + 110, 0, // tags_len + 2, 0, // num_tags + 8, 0, // first tag at offset 8 + 39, 0, // second tag at offset 39 + // 8: + 3, 0, // three fields long + 12, 0, // first field 12 bytes long + 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, // "Hello world!" + 5, 0, // second field 5 bytes long + 72, 101, 108, 108, 111, // "Hello" + 6, 0, // third field 6 bytes long + 119, 111, 114, 108, 100, 33, // world! + // 39: + 2, 0, // two fields long + 1, 0, // first field 1 bytes long + 112, // "p" + 64, 0, // second field 64 bytes long + 101, 101, 49, 49, 97, 53, 100, 102, 102, 52, 48, 99, 49, 57, 97, 53, 53, 53, 102, 52, + 49, 102, 101, 52, 50, 98, 52, 56, 102, 48, 48, 101, 54, 49, 56, 99, 57, 49, 50, 50, 53, + 54, 50, 50, 97, 101, 51, 55, 98, 54, 99, 50, 98, 98, 54, 55, 98, 55, 54, 99, 52, 101, + 52, 57, + // "ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49" + ]; + + let tags = Tags::delineate(&data).unwrap(); + + // Test tag access with get_string() + + assert_eq!(tags.get_string(0, 0), Some(b"Hello world!".as_slice())); + assert_eq!(tags.get_string(0, 1), Some(b"Hello".as_slice())); + assert_eq!(tags.get_string(0, 2), Some(b"world!".as_slice())); + assert_eq!(tags.get_string(0, 3), None); + assert_eq!(tags.get_string(1, 0), Some(b"p".as_slice())); + assert_eq!( + tags.get_string(1, 1), + Some(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49".as_slice()) + ); + assert_eq!(tags.get_string(1, 2), None); + assert_eq!(tags.get_string(2, 0), None); + + // Test tag access with iterators + + let mut iter = tags.iter(); + + let mut tag1 = iter.next().unwrap(); + println!("TagsStringIter 1 {:?}", tag1); + assert_eq!(tag1.next(), Some(b"Hello world!".as_slice())); + assert_eq!(tag1.next(), Some(b"Hello".as_slice())); + assert_eq!(tag1.next(), Some(b"world!".as_slice())); + assert!(tag1.next().is_none()); + + let mut tag2 = iter.next().unwrap(); + assert_eq!(tag2.next(), Some(b"p".as_slice())); + assert_eq!( + tag2.next(), + Some(b"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49".as_slice()) + ); + assert!(tag2.next().is_none()); + + let tag3 = iter.next(); + assert!(tag3.is_none()); + + assert_eq!( + format!("{tags}"), + r#"[["Hello world!","Hello","world!"],["p","ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49"]]"# + ); + } +}