aboutsummaryrefslogtreecommitdiff
path: root/common/object
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2026-01-06 14:56:20 +0100
committermetamuffin <metamuffin@disroot.org>2026-01-06 14:56:20 +0100
commitffbdb9ce397a6408d5a91cbdcbaf4e13b0c3ba0b (patch)
treee84bb53bb2cb3f55617c9e44b5f07cbd964aa404 /common/object
parentc04f49adaa2cb0fa3074d6b122d1e11689c4f5de (diff)
downloadjellything-ffbdb9ce397a6408d5a91cbdcbaf4e13b0c3ba0b.tar
jellything-ffbdb9ce397a6408d5a91cbdcbaf4e13b0c3ba0b.tar.bz2
jellything-ffbdb9ce397a6408d5a91cbdcbaf4e13b0c3ba0b.tar.zst
Multi fields; object buffer constructor; unit tests
Diffstat (limited to 'common/object')
-rw-r--r--common/object/src/buffer.rs50
-rw-r--r--common/object/src/lib.rs67
-rw-r--r--common/object/src/tests.rs39
-rw-r--r--common/object/src/value.rs62
4 files changed, 186 insertions, 32 deletions
diff --git a/common/object/src/buffer.rs b/common/object/src/buffer.rs
new file mode 100644
index 0000000..56b8caf
--- /dev/null
+++ b/common/object/src/buffer.rs
@@ -0,0 +1,50 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+
+use crate::{Object, Tag, ValueStore};
+
+pub struct ObjectBuffer(pub Vec<u32>);
+
+impl ObjectBuffer {
+ pub fn empty() -> Self {
+ Self(vec![0])
+ }
+ pub fn as_object<'a>(&'a self) -> Object<'a> {
+ Object::load(&self.0).unwrap()
+ }
+ pub fn new(fields: &mut [(Tag, &dyn ValueStore)]) -> ObjectBuffer {
+ let mut tags = Vec::new();
+ let mut offsets = Vec::new();
+ let mut values = Vec::new();
+ fields.sort_by_key(|(t, _)| t.0);
+ let mut temp = Vec::new();
+ for (tag, val) in fields {
+ tags.push(tag.0);
+ if val.is_aligned() {
+ offsets.push((values.len() as u32) << 2);
+ val.store_aligned(&mut values);
+ } else {
+ temp.clear();
+ val.store_unaligned(&mut temp);
+ let mut pad = 0;
+ while temp.len() % 4 != 0 {
+ pad += 1;
+ temp.push(0);
+ }
+ offsets.push(((values.len() as u32) << 2) | pad);
+ values.extend(bytemuck::cast_slice(&temp)); // ok bc. temp length is a whole number of dwords
+ }
+ }
+ ObjectBuffer(
+ [tags.len() as u32]
+ .into_iter()
+ .chain(tags)
+ .chain(offsets)
+ .chain(values)
+ .collect(),
+ )
+ }
+}
diff --git a/common/object/src/lib.rs b/common/object/src/lib.rs
index 9f9e0be..831dee7 100644
--- a/common/object/src/lib.rs
+++ b/common/object/src/lib.rs
@@ -4,7 +4,11 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
+mod buffer;
+#[cfg(test)]
+mod tests;
mod value;
+pub use buffer::*;
pub use value::*;
use std::marker::PhantomData;
@@ -14,17 +18,7 @@ use std::marker::PhantomData;
pub struct Tag(pub u32);
pub struct TypedTag<T>(pub Tag, pub PhantomData<T>);
-pub struct ObjectBuffer(pub Vec<u32>);
-
-impl ObjectBuffer {
- pub fn new() -> Self {
- Self(vec![0])
- }
- pub fn as_object<'a>(&'a self) -> Object<'a> {
- Object::load(&self.0).unwrap()
- }
-}
-
+#[derive(Debug)]
pub struct Object<'a> {
tags: &'a [u32],
offsets: &'a [u32],
@@ -33,25 +27,28 @@ pub struct Object<'a> {
impl<'a> Object<'a> {
pub fn load(buf: &'a [u32]) -> Option<Self> {
let nf = *buf.get(0)? as usize;
+ if buf.len() < 1 + nf * 2 {
+ return None;
+ }
Some(Self {
tags: &buf[1..1 + nf],
offsets: &buf[1 + nf..1 + nf + nf],
values: &buf[1 + nf + nf..],
})
}
- fn find_field(&self, tag: Tag) -> Option<usize> {
+ pub fn find_field(&self, tag: Tag) -> Option<usize> {
// using partition as binary search for the first field (instead of regular binary_search that returns any)
- let first = self.tags.partition_point(|&x| x >= tag.0);
+ let first = self.tags.partition_point(|&x| x < tag.0);
self.tags
.get(first)
.is_some_and(|&x| x == tag.0)
.then_some(first)
}
fn get_aligned(&self, index: usize) -> Option<&[u32]> {
- let start_raw = *self.offsets.get(index)?;
+ let start_raw = self.offsets[index];
let end_raw = self
.offsets
- .get(index)
+ .get(index + 1)
.copied()
.unwrap_or((self.values.len() as u32) << 2);
@@ -61,10 +58,10 @@ impl<'a> Object<'a> {
Some(&self.values[start as usize..end as usize])
}
fn get_unaligned(&self, index: usize) -> Option<&[u8]> {
- let start_raw = *self.offsets.get(index)?;
+ let start_raw = self.offsets[index];
let end_raw = self
.offsets
- .get(index)
+ .get(index + 1)
.copied()
.unwrap_or((self.values.len() as u32) << 2);
@@ -75,12 +72,44 @@ impl<'a> Object<'a> {
let values_u8: &[u8] = bytemuck::cast_slice(self.values);
Some(&values_u8[start as usize..end as usize])
}
- pub fn get<'b: 'a, T: Value<'b>>(&'b self, tag: TypedTag<T>) -> Option<T> {
- let index = self.find_field(tag.0)?;
+ #[inline]
+ pub fn get_typed<'b: 'a, T: Value<'b>>(&'b self, index: usize) -> Option<T> {
if T::ALIGNED {
T::load_aligned(self.get_aligned(index)?)
} else {
T::load_unaligned(self.get_unaligned(index)?)
}
}
+ pub fn get<'b: 'a, T: Value<'b>>(&'b self, tag: TypedTag<T>) -> Option<T> {
+ self.get_typed(self.find_field(tag.0)?)
+ }
+ pub fn iter<'b: 'a, T>(&'b self, tag: TypedTag<T>) -> FieldIter<'b, T> {
+ FieldIter {
+ object: self,
+ index: self.tags.partition_point(|&x| x < tag.0.0),
+ tag: tag.0.0,
+ ty: PhantomData,
+ }
+ }
+}
+
+pub struct FieldIter<'a, T> {
+ object: &'a Object<'a>,
+ index: usize,
+ tag: u32,
+ ty: PhantomData<T>,
+}
+impl<'a, T: Value<'a>> Iterator for FieldIter<'a, T> {
+ type Item = T;
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.index >= self.object.tags.len() {
+ return None;
+ }
+ if self.object.tags[self.index] != self.tag {
+ return None;
+ }
+ let val = self.object.get_typed(self.index);
+ self.index += 1;
+ val
+ }
}
diff --git a/common/object/src/tests.rs b/common/object/src/tests.rs
new file mode 100644
index 0000000..35a29ba
--- /dev/null
+++ b/common/object/src/tests.rs
@@ -0,0 +1,39 @@
+/*
+ This file is part of jellything (https://codeberg.org/metamuffin/jellything)
+ which is licensed under the GNU Affero General Public License (version 3); see /COPYING.
+ Copyright (C) 2026 metamuffin <metamuffin.org>
+*/
+use crate::{ObjectBuffer, Tag, TypedTag};
+use std::marker::PhantomData;
+
+const NAME: TypedTag<&str> = TypedTag(Tag(15), PhantomData);
+const AGE: TypedTag<u32> = TypedTag(Tag(13), PhantomData);
+const FRIEND: TypedTag<&str> = TypedTag(Tag(54321), PhantomData);
+
+fn test_object() -> ObjectBuffer {
+ ObjectBuffer::new(&mut [
+ (NAME.0, &"Bob"),
+ (AGE.0, &35_u32),
+ (FRIEND.0, &"Alice"),
+ (FRIEND.0, &"Charlie"),
+ ])
+}
+
+#[test]
+fn read_single_field() {
+ let bob = test_object();
+ let bob = bob.as_object();
+ assert_eq!(bob.get(NAME), Some("Bob"));
+ assert_eq!(bob.get(AGE), Some(35));
+}
+
+#[test]
+fn read_multi_field() {
+ let bob = test_object();
+ let bob = bob.as_object();
+
+ let mut friends = bob.iter(FRIEND);
+ assert_eq!(friends.next(), Some("Alice"));
+ assert_eq!(friends.next(), Some("Charlie"));
+ assert_eq!(friends.next(), None);
+}
diff --git a/common/object/src/value.rs b/common/object/src/value.rs
index 3a8b7df..d77d53a 100644
--- a/common/object/src/value.rs
+++ b/common/object/src/value.rs
@@ -4,9 +4,9 @@
Copyright (C) 2026 metamuffin <metamuffin.org>
*/
-use crate::Object;
+use crate::{Object, ObjectBuffer};
-pub trait Value<'a>: Sized {
+pub trait Value<'a>: ValueStore + Sized {
const ALIGNED: bool;
fn load_aligned(buf: &'a [u32]) -> Option<Self> {
let _ = buf;
@@ -16,7 +16,11 @@ pub trait Value<'a>: Sized {
let _ = buf;
None
}
- fn store(&self, buf: &mut Vec<u8>);
+}
+pub trait ValueStore {
+ fn is_aligned(&self) -> bool;
+ fn store_aligned(&self, _buf: &mut Vec<u32>) {}
+ fn store_unaligned(&self, _buf: &mut Vec<u8>) {}
fn size(&self) -> usize;
}
impl<'a> Value<'a> for &'a str {
@@ -24,7 +28,12 @@ impl<'a> Value<'a> for &'a str {
fn load_unaligned(buf: &'a [u8]) -> Option<Self> {
str::from_utf8(buf).ok()
}
- fn store(&self, buf: &mut Vec<u8>) {
+}
+impl ValueStore for &str {
+ fn is_aligned(&self) -> bool {
+ false
+ }
+ fn store_unaligned(&self, buf: &mut Vec<u8>) {
buf.extend(self.as_bytes());
}
fn size(&self) -> usize {
@@ -32,12 +41,17 @@ impl<'a> Value<'a> for &'a str {
}
}
impl Value<'_> for u32 {
- const ALIGNED: bool = false;
+ const ALIGNED: bool = true;
fn load_aligned(buf: &[u32]) -> Option<Self> {
buf.get(0).copied()
}
- fn store(&self, buf: &mut Vec<u8>) {
- buf.extend(self.to_ne_bytes());
+}
+impl ValueStore for u32 {
+ fn is_aligned(&self) -> bool {
+ true
+ }
+ fn store_aligned(&self, buf: &mut Vec<u32>) {
+ buf.push(*self);
}
fn size(&self) -> usize {
4
@@ -50,8 +64,14 @@ impl Value<'_> for u64 {
let lo = *buf.get(1)? as u64;
Some(hi << 32 | lo)
}
- fn store(&self, buf: &mut Vec<u8>) {
- buf.extend(self.to_ne_bytes());
+}
+impl ValueStore for u64 {
+ fn is_aligned(&self) -> bool {
+ true
+ }
+ fn store_aligned(&self, buf: &mut Vec<u32>) {
+ buf.push((self >> 32) as u32);
+ buf.push(*self as u32);
}
fn size(&self) -> usize {
8
@@ -62,12 +82,28 @@ impl<'a> Value<'a> for Object<'a> {
fn load_aligned(buf: &'a [u32]) -> Option<Self> {
Self::load(buf)
}
- fn store(&self, buf: &mut Vec<u8>) {
- buf.extend(self.tags.iter().copied().map(u32::to_ne_bytes).flatten());
- buf.extend(self.offsets.iter().copied().map(u32::to_ne_bytes).flatten());
- buf.extend(self.values.iter().copied().map(u32::to_ne_bytes).flatten());
+}
+impl ValueStore for Object<'_> {
+ fn is_aligned(&self) -> bool {
+ true
+ }
+ fn store_aligned(&self, buf: &mut Vec<u32>) {
+ buf.extend(self.tags);
+ buf.extend(self.offsets);
+ buf.extend(self.values);
}
fn size(&self) -> usize {
(self.tags.len() + self.offsets.len() + self.values.len()) * size_of::<u32>()
}
}
+impl ValueStore for ObjectBuffer {
+ fn is_aligned(&self) -> bool {
+ true
+ }
+ fn store_aligned(&self, buf: &mut Vec<u32>) {
+ buf.extend(&self.0);
+ }
+ fn size(&self) -> usize {
+ self.0.len() * 4
+ }
+}