From b8f969ea0330a0d6eda76953fafc7553b2490035 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 20:59:44 -0700 Subject: [PATCH 1/3] Replace inner vectors with `One(OutputData) | Many(Vec)` --- src/deserializer/iter.rs | 6 +- src/deserializer/typedstream.rs | 166 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/models/archived.rs | 74 +++++++++++--- 4 files changed, 228 insertions(+), 20 deletions(-) diff --git a/src/deserializer/iter.rs b/src/deserializer/iter.rs index b601be2..f002fb7 100644 --- a/src/deserializer/iter.rs +++ b/src/deserializer/iter.rs @@ -5,7 +5,7 @@ use core::slice::Iter; use alloc::vec::Vec; use crate::models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, types::{Type, TypeEntry}, @@ -220,8 +220,8 @@ pub struct PropertyIterator<'a, 'b> { enum GroupSource<'a, 'b> { /// A single inline value that forms one group; yielded exactly once. Inline(Option<&'b OutputData<'a>>), - /// An iterator over the object's group vectors. - Groups(Iter<'b, Vec>>), + /// An iterator over the object's data groups. + Groups(Iter<'b, DataGroup<'a>>), } impl<'a, 'b> PropertyIterator<'a, 'b> { diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 64acb47..3276afe 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -573,3 +573,169 @@ impl<'a> TypedStreamDeserializer<'a> { } } } + +#[cfg(test)] +mod group_tests { + use alloc::{vec, vec::Vec}; + + use super::TypedStreamDeserializer; + use crate::{ + DataGroup, ObjectData, OutputData, Property, + deserializer::constants::{EMPTY, END, START}, + models::archived::Archived, + }; + + fn stream(groups: &[u8]) -> Vec { + let mut bytes = vec![ + 4, 11, b's', b't', b'r', b'e', b'a', b'm', b't', b'y', b'p', b'e', b'd', 0x81, 0xe8, 3, + START, 1, b'@', START, START, START, 1, b'X', 0, EMPTY, + ]; + bytes.extend_from_slice(groups); + bytes.push(END); + bytes + } + + fn unsigned(property: Property<'_, '_>) -> u64 { + let Property::Primitive(value) = property else { + panic!("expected a primitive"); + }; + value.as_u64().unwrap() + } + + #[test] + fn preserves_empty_groups_and_item_order() { + let bytes = stream(&[ + START, 1, b'C', 1, START, 0, START, 2, b'C', b'C', 2, 3, START, 1, b'C', 4, + // An empty embedded value contributes no group. An empty descriptor does. + START, 1, b'*', EMPTY, + ]); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("expected an object"); + }; + assert_eq!(data.group_count(), 4); + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::UnsignedInteger(1)), + DataGroup::Many(vec![]), + DataGroup::Many(vec![ + OutputData::UnsignedInteger(2), + OutputData::UnsignedInteger(3), + ]), + DataGroup::One(OutputData::UnsignedInteger(4)), + ]) + ); + + let mut lengths = Vec::new(); + let mut values = Vec::new(); + for property in ts.resolve_properties(root).unwrap() { + let Property::Group(group) = property else { + panic!("expected a group"); + }; + lengths.push(group.len()); + let forward: Vec<_> = group.iter().map(unsigned).collect(); + let reverse: Vec<_> = group.iter().rev().map(unsigned).collect(); + assert_eq!(reverse, forward.iter().rev().copied().collect::>()); + values.extend_from_slice(&forward); + + let mut iter = group.iter(); + assert_eq!(iter.len(), forward.len()); + if !forward.is_empty() { + assert_eq!(unsigned(iter.next().unwrap()), forward[0]); + assert_eq!(iter.len(), forward.len() - 1); + } + if forward.len() == 2 { + assert_eq!(unsigned(iter.next_back().unwrap()), forward[1]); + } + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + assert!(iter.next_back().is_none()); + } + assert_eq!(lengths, [1, 0, 2, 1]); + assert_eq!(values, [1, 2, 3, 4]); + } + + #[test] + fn preserves_groups_when_promoting_inline_data() { + for (descriptors, expected) in [ + ( + vec![START, 1, b'C', 1, START, 1, b'C', 2], + vec![vec![1], vec![2]], + ), + ( + vec![START, 1, b'C', 1, START, 2, b'C', b'C', 2, 3], + vec![vec![1], vec![2, 3]], + ), + ( + vec![START, 2, b'C', b'C', 1, 2, START, 1, b'C', 3], + vec![vec![1, 2], vec![3]], + ), + ] { + let bytes = stream(&descriptors); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { + data: ObjectData::Groups(groups), + .. + } = &ts.object_table[root] + else { + panic!("expected grouped data"); + }; + let values: Vec> = groups + .iter() + .map(|group| { + group + .as_slice() + .iter() + .map(|value| value.as_u64().unwrap()) + .collect() + }) + .collect(); + assert_eq!(values, expected); + assert!(groups.iter().all(|group| !group.is_empty())); + for group in groups { + assert_eq!(matches!(group, DataGroup::One(_)), group.len() == 1); + } + } + } + + #[test] + fn preserves_shared_and_self_references() { + let bytes = stream(&[ + // The child reuses class 1 and occupies object slot 2. + START, 1, b'@', START, 0x93, START, 1, b'C', 7, END, START, 1, b'@', 0x94, START, 2, + b'@', b'@', 0x92, 0x94, + ]); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + assert_eq!(root, 0); + assert_eq!(ts.object_table.len(), 3); + assert_eq!( + ts.object_table[root], + Archived::Object { + class: 1, + data: ObjectData::Groups(vec![ + DataGroup::One(OutputData::Object(2)), + DataGroup::One(OutputData::Object(2)), + DataGroup::Many(vec![OutputData::Object(0), OutputData::Object(2)]), + ]), + } + ); + assert_eq!( + ts.object_table[2], + Archived::Object { + class: 1, + data: ObjectData::Inline(OutputData::UnsignedInteger(7)), + } + ); + let primitives = ts + .resolve_properties(root) + .unwrap() + .primitives_with_limits(10, 100); + assert!(!primitives.is_empty()); + assert!(primitives.len() <= 100); + assert!(primitives.iter().all(|value| value.as_u64() == Some(7))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5705a18..f0b47f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub use deserializer::{ typedstream::TypedStreamDeserializer, }; pub use models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, output_data::OutputData, types::TypeEntry, }; diff --git a/src/models/archived.rs b/src/models/archived.rs index f19aa20..e0b0012 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -4,15 +4,46 @@ use alloc::{vec, vec::Vec}; use crate::models::{class::Class, output_data::OutputData}; +/// A data group within an [`ObjectData::Groups`] collection. +/// +/// Singleton values are stored inline so each dictionary key and value does not +/// need a separate allocation. Empty and multi-value groups retain a [`Vec`]. +#[derive(Debug, PartialEq)] +pub enum DataGroup<'a> { + /// One value, stored inline. + One(OutputData<'a>), + /// An empty or multi-value group. + Many(Vec>), +} + +impl<'a> DataGroup<'a> { + /// Borrow the group's values in stream order. + #[must_use] + pub fn as_slice(&self) -> &[OutputData<'a>] { + match self { + Self::One(value) => core::slice::from_ref(value), + Self::Many(values) => values, + } + } + + /// The number of values in the group. + #[must_use] + pub fn len(&self) -> usize { + self.as_slice().len() + } + + /// Whether the group contains no values. + #[must_use] + pub fn is_empty(&self) -> bool { + self.as_slice().is_empty() + } +} + /// The data attached to an [`Archived::Object`]. /// -/// `typedstream` objects store their values in one or more *groups* (each group -/// is the result of decoding one type descriptor). In practice the overwhelming -/// majority of objects hold a single group containing a single value (an -/// `NSString`'s text, an `NSNumber`'s number, a reference to another object) so -/// that case is stored inline without any heap allocation. Only objects with -/// multiple groups, or a group holding multiple values, fall back to the nested -/// [`Vec`] representation. +/// Each group holds the values decoded from one type descriptor. An object with +/// one single-value group stores that value inline. Other nonempty objects use +/// a group vector, whose singleton values also remain inline. #[derive(Debug, PartialEq)] pub enum ObjectData<'a> { /// The object has no data groups. @@ -20,8 +51,8 @@ pub enum ObjectData<'a> { /// A single group containing a single value, stored inline. This is by far /// the most common shape and avoids two heap allocations per object. Inline(OutputData<'a>), - /// The general case: one or more groups, each holding one or more values. - Groups(Vec>>), + /// One or more groups, preserving empty groups and value order. + Groups(Vec>), } impl<'a> ObjectData<'a> { @@ -31,28 +62,28 @@ impl<'a> ObjectData<'a> { match self { // Common path: the object's first (and usually only) group. ObjectData::Empty => *self = ObjectData::Inline(value), - ObjectData::Groups(groups) => groups.push(vec![value]), + ObjectData::Groups(groups) => groups.push(DataGroup::One(value)), // Promote a previously-inline object to the general representation. ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![vec![first], vec![value]]); + *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::One(value)]); } } } - /// Append a group that contains multiple values. + /// Append an empty or multi-value group. #[inline] pub(crate) fn push_many(&mut self, values: Vec>) { match self { - ObjectData::Empty => *self = ObjectData::Groups(vec![values]), - ObjectData::Groups(groups) => groups.push(values), + ObjectData::Empty => *self = ObjectData::Groups(vec![DataGroup::Many(values)]), + ObjectData::Groups(groups) => groups.push(DataGroup::Many(values)), ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![vec![first], values]); + *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::Many(values)]); } } } @@ -67,7 +98,18 @@ impl<'a> ObjectData<'a> { } else if groups.len() == 1 && groups[0].len() == 1 { ObjectData::Inline(groups.pop().unwrap().pop().unwrap()) } else { - ObjectData::Groups(groups) + ObjectData::Groups( + groups + .into_iter() + .map(|mut group| { + if group.len() == 1 { + DataGroup::One(group.pop().unwrap()) + } else { + DataGroup::Many(group) + } + }) + .collect(), + ) } } From e9b6d847303c7cb37d7a0384c76004ab647d1bfd Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 21:18:51 -0700 Subject: [PATCH 2/3] Fix stream desync for embedded structures like `encodeValuesOfObjCTypes:"i*"` --- src/deserializer/typedstream.rs | 75 ++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 3276afe..30ed70a 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -519,12 +519,23 @@ impl<'a> TypedStreamDeserializer<'a> { for i in 0..len { let ty = self.type_table[types_index][i]; if matches!(ty, Type::EmbeddedData) { - return self.read_embedded(); + // Read the embedded group and merge its contents into the current output vector. + match self.read_embedded()? { + Group::Empty => {} + Group::One(value) => out_v.push(value), + Group::Many(values) => out_v.extend(values), + } + continue; } out_v.push(self.read_value(ty)?); } - Ok(Group::Many(out_v)) + // Convert the output vector into the appropriate group variant. + Ok(if out_v.len() == 1 { + Group::One(out_v.pop().unwrap()) + } else { + Group::Many(out_v) + }) } /// Gets the current type from the stream, either by reading it from the stream or reading it from @@ -701,6 +712,66 @@ mod group_tests { } } + #[test] + fn splices_embedded_values_into_multi_slot_groups() { + // A descriptor with several slots fills one group; an `EmbeddedData` + // slot contributes the embedded values in place. + use OutputData::UnsignedInteger as U; + for (descriptors, expected) in [ + // Embed last: the values decoded before it must survive. + ( + vec![START, 2, b'C', b'*', 1, START, START, 1, b'C', 0x94, 2], + vec![U(1), U(2)], + ), + // Embed first: the slots after it must still be read. + ( + vec![START, 2, b'*', b'C', START, START, 1, b'C', 0x94, 1, 2], + vec![U(1), U(2)], + ), + // Embed in the middle. + ( + vec![ + START, 3, b'C', b'*', b'C', 1, START, START, 1, b'C', 0x94, 2, 3, + ], + vec![U(1), U(2), U(3)], + ), + // A multi-value embedded type flattens into the enclosing group. + ( + vec![ + START, 2, b'C', b'*', 1, START, START, 2, b'C', b'C', 0x94, 2, 3, + ], + vec![U(1), U(2), U(3)], + ), + // A nil embed contributes nothing; the slot after it is still read. + ( + vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], + vec![U(1), U(3)], + ), + // A nil embed can leave a multi-slot descriptor with one value. + (vec![START, 2, b'C', b'*', 1, EMPTY], vec![U(1)]), + ] { + let bytes = stream(&descriptors); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("expected an object"); + }; + assert_eq!(data.group_count(), 1, "{descriptors:?}"); + let values = match data { + ObjectData::Inline(value) => core::slice::from_ref(value), + ObjectData::Groups(groups) => groups[0].as_slice(), + ObjectData::Empty => panic!("expected data for {descriptors:?}"), + }; + assert_eq!(values, expected.as_slice(), "{descriptors:?}"); + // A single value never lands in `Many`, whichever path produced it. + assert_eq!( + matches!(data, ObjectData::Inline(_)), + expected.len() == 1, + "{descriptors:?}" + ); + } + } + #[test] fn preserves_shared_and_self_references() { let bytes = stream(&[ From 09829839965175ed2f04c494a5d20800fb33c2d1 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 22:47:16 -0700 Subject: [PATCH 3/3] Refactor DataGroup and ObjectData to reduce indirection --- src/deserializer/typedstream.rs | 116 +++++++++++++++----------------- src/models/archived.rs | 56 +++++++-------- 2 files changed, 78 insertions(+), 94 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 30ed70a..16d118a 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -17,36 +17,13 @@ use crate::{ }, error::{Result, TypedStreamError}, models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, types::{Type, TypeEntry}, }, }; -/// The decoded contents of a single data group, produced by -/// [`TypedStreamDeserializer::read_types`]. The single-value case is kept out of -/// a `Vec` so the common object shape costs no heap allocation. -enum Group<'a> { - /// The group produced no values (e.g. an empty `EmbeddedData`). - Empty, - /// Exactly one value. - One(OutputData<'a>), - /// Two or more values. - Many(Vec>), -} - -impl<'a> Group<'a> { - /// The first value in the group, if any. - fn first(&self) -> Option<&OutputData<'a>> { - match self { - Group::Empty => None, - Group::One(value) => Some(value), - Group::Many(values) => values.first(), - } - } -} - /// Contains logic and data used to deserialize data from a `typedstream`. /// /// `typedstream` is a binary serialization format developed by `NeXTSTEP` and later adopted by Apple. @@ -131,7 +108,6 @@ impl<'a> TypedStreamDeserializer<'a> { /// let result = deserializer.oxidize(); /// ``` pub fn oxidize(&mut self) -> Result { - let mut obj = Group::Empty; let validation = validate_header(self.data)?; // Reserve table capacity now that the input is known to be a valid @@ -151,16 +127,13 @@ impl<'a> TypedStreamDeserializer<'a> { // Advance by the number of bytes consumed by the header validation self.position += validation.bytes_consumed; - // while self.position <= self.data.len() { - let found_type = self.read_type(false)?; - - if let Some(type_index) = found_type { - // Read the types at the specified index - obj = self.read_types(type_index)?; - } - - match obj.first().ok_or(TypedStreamError::InvalidObject)? { - OutputData::Object(idx) => Ok(*idx), + // The root must be an object: a stream with no root descriptor, or one + // whose first value is not an object reference, has nothing to walk. + let Some(type_index) = self.read_type(false)? else { + return Err(TypedStreamError::InvalidObject); + }; + match self.read_types(type_index)?.as_slice().first() { + Some(OutputData::Object(idx)) => Ok(*idx), _ => Err(TypedStreamError::InvalidObject), } } @@ -404,11 +377,7 @@ impl<'a> TypedStreamDeserializer<'a> { // Read the next type, which should be an object if let Some(next_index) = self.read_type(false)? { // Recursively read the types for this object - match self.read_types(next_index)? { - Group::Empty => {} - Group::One(value) => data.push_one(value), - Group::Many(values) => data.push_many(values), - } + data.push(self.read_types(next_index)?); } } self.object_table[placeholder_index] = Archived::Object { class: cls, data }; @@ -491,18 +460,22 @@ impl<'a> TypedStreamDeserializer<'a> { } /// Reads an `EmbeddedData` descriptor, redirecting to the embedded type - /// entry. Returns the group decoded from that entry, or [`Group::Empty`]. - fn read_embedded(&mut self) -> Result> { + /// entry. Returns the group decoded from that entry. + /// + /// A nil embedded type is a `NULL` pointer written into the slot, the same + /// thing a nil object reference is, so it decodes to + /// [`OutputData::Null`] and keeps its position in the object's groups. + fn read_embedded(&mut self) -> Result> { if let Some(idx) = self.read_embedded_type()? { self.position += 1; self.read_types(idx) } else { - Ok(Group::Empty) + Ok(DataGroup::One(OutputData::Null)) } } /// Reads all type descriptors at `types_index` into a single data group. - fn read_types(&mut self, types_index: usize) -> Result> { + fn read_types(&mut self, types_index: usize) -> Result> { let len = self.type_table[types_index].len(); // Common case: a single descriptor decodes to a single value with no Vec. @@ -511,7 +484,7 @@ impl<'a> TypedStreamDeserializer<'a> { return if matches!(ty, Type::EmbeddedData) { self.read_embedded() } else { - Ok(Group::One(self.read_value(ty)?)) + Ok(DataGroup::One(self.read_value(ty)?)) }; } @@ -521,20 +494,20 @@ impl<'a> TypedStreamDeserializer<'a> { if matches!(ty, Type::EmbeddedData) { // Read the embedded group and merge its contents into the current output vector. match self.read_embedded()? { - Group::Empty => {} - Group::One(value) => out_v.push(value), - Group::Many(values) => out_v.extend(values), + DataGroup::One(value) => out_v.push(value), + DataGroup::Values(values) => out_v.extend(values), } continue; } out_v.push(self.read_value(ty)?); } - // Convert the output vector into the appropriate group variant. + // An embed with no types can leave a multi-slot descriptor holding one + // value; keep `One` exact. Ok(if out_v.len() == 1 { - Group::One(out_v.pop().unwrap()) + DataGroup::One(out_v.pop().unwrap()) } else { - Group::Many(out_v) + DataGroup::Values(out_v) }) } @@ -614,10 +587,11 @@ mod group_tests { } #[test] - fn preserves_empty_groups_and_item_order() { + fn preserves_empty_groups_null_slots_and_item_order() { let bytes = stream(&[ + // A zero-length descriptor is a group with no values; it keeps its position. START, 1, b'C', 1, START, 0, START, 2, b'C', b'C', 2, 3, START, 1, b'C', 4, - // An empty embedded value contributes no group. An empty descriptor does. + // A nil `EmbeddedData` is a slot written as `NULL`: one group holding `Null`. START, 1, b'*', EMPTY, ]); let mut ts = TypedStreamDeserializer::new(&bytes); @@ -625,23 +599,25 @@ mod group_tests { let Archived::Object { data, .. } = &ts.object_table[root] else { panic!("expected an object"); }; - assert_eq!(data.group_count(), 4); + assert_eq!(data.group_count(), 5); assert_eq!( data, &ObjectData::Groups(vec![ DataGroup::One(OutputData::UnsignedInteger(1)), - DataGroup::Many(vec![]), - DataGroup::Many(vec![ + DataGroup::Values(vec![]), + DataGroup::Values(vec![ OutputData::UnsignedInteger(2), OutputData::UnsignedInteger(3), ]), DataGroup::One(OutputData::UnsignedInteger(4)), + DataGroup::One(OutputData::Null), ]) ); + let mut properties = ts.resolve_properties(root).unwrap(); let mut lengths = Vec::new(); let mut values = Vec::new(); - for property in ts.resolve_properties(root).unwrap() { + for property in properties.by_ref().take(4) { let Property::Group(group) = property else { panic!("expected a group"); }; @@ -666,6 +642,16 @@ mod group_tests { } assert_eq!(lengths, [1, 0, 2, 1]); assert_eq!(values, [1, 2, 3, 4]); + + let Some(Property::Group(group)) = properties.next() else { + panic!("expected the nil embed's group"); + }; + assert_eq!(group.len(), 1); + assert!(matches!( + group.iter().next(), + Some(Property::Primitive(OutputData::Null)) + )); + assert!(properties.next().is_none()); } #[test] @@ -742,13 +728,17 @@ mod group_tests { ], vec![U(1), U(2), U(3)], ), - // A nil embed contributes nothing; the slot after it is still read. + // A nil embed is a `NULL` slot; the slot after it is still read. ( vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], - vec![U(1), U(3)], + vec![U(1), OutputData::Null, U(3)], + ), + // An embedded type with no types contributes nothing, which can + // leave a multi-slot descriptor with one value. + ( + vec![START, 2, b'C', b'*', 1, START, START, 0, 0x94], + vec![U(1)], ), - // A nil embed can leave a multi-slot descriptor with one value. - (vec![START, 2, b'C', b'*', 1, EMPTY], vec![U(1)]), ] { let bytes = stream(&descriptors); let mut ts = TypedStreamDeserializer::new(&bytes); @@ -763,7 +753,7 @@ mod group_tests { ObjectData::Empty => panic!("expected data for {descriptors:?}"), }; assert_eq!(values, expected.as_slice(), "{descriptors:?}"); - // A single value never lands in `Many`, whichever path produced it. + // A single value never lands in `Values`, whichever path produced it. assert_eq!( matches!(data, ObjectData::Inline(_)), expected.len() == 1, @@ -790,7 +780,7 @@ mod group_tests { data: ObjectData::Groups(vec![ DataGroup::One(OutputData::Object(2)), DataGroup::One(OutputData::Object(2)), - DataGroup::Many(vec![OutputData::Object(0), OutputData::Object(2)]), + DataGroup::Values(vec![OutputData::Object(0), OutputData::Object(2)]), ]), } ); diff --git a/src/models/archived.rs b/src/models/archived.rs index e0b0012..b80a422 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -4,16 +4,17 @@ use alloc::{vec, vec::Vec}; use crate::models::{class::Class, output_data::OutputData}; -/// A data group within an [`ObjectData::Groups`] collection. +/// A data group within an [`ObjectData::Groups`] collection: the values decoded +/// from one type descriptor. /// -/// Singleton values are stored inline so each dictionary key and value does not -/// need a separate allocation. Empty and multi-value groups retain a [`Vec`]. +/// A lone value is stored inline so each dictionary key and value costs no +/// allocation; anything else lives in a [`Vec`]. #[derive(Debug, PartialEq)] pub enum DataGroup<'a> { - /// One value, stored inline. + /// Exactly one value, stored inline. One(OutputData<'a>), - /// An empty or multi-value group. - Many(Vec>), + /// Zero, or multiple, values. + Values(Vec>), } impl<'a> DataGroup<'a> { @@ -22,7 +23,7 @@ impl<'a> DataGroup<'a> { pub fn as_slice(&self) -> &[OutputData<'a>] { match self { Self::One(value) => core::slice::from_ref(value), - Self::Many(values) => values, + Self::Values(values) => values, } } @@ -42,8 +43,8 @@ impl<'a> DataGroup<'a> { /// The data attached to an [`Archived::Object`]. /// /// Each group holds the values decoded from one type descriptor. An object with -/// one single-value group stores that value inline. Other nonempty objects use -/// a group vector, whose singleton values also remain inline. +/// one single-value group stores that value inline; every other nonempty object +/// holds its groups in stream order. #[derive(Debug, PartialEq)] pub enum ObjectData<'a> { /// The object has no data groups. @@ -51,39 +52,32 @@ pub enum ObjectData<'a> { /// A single group containing a single value, stored inline. This is by far /// the most common shape and avoids two heap allocations per object. Inline(OutputData<'a>), - /// One or more groups, preserving empty groups and value order. + /// One or more groups in stream order. Groups(Vec>), } impl<'a> ObjectData<'a> { - /// Append a group that contains exactly one value. + /// Append a group in stream order. + /// + /// The first single-value group is stored inline; a second group of any + /// shape promotes the object to [`Groups`](Self::Groups). #[inline] - pub(crate) fn push_one(&mut self, value: OutputData<'a>) { + pub(crate) fn push(&mut self, group: DataGroup<'a>) { match self { + ObjectData::Groups(groups) => groups.push(group), // Common path: the object's first (and usually only) group. - ObjectData::Empty => *self = ObjectData::Inline(value), - ObjectData::Groups(groups) => groups.push(DataGroup::One(value)), - // Promote a previously-inline object to the general representation. - ObjectData::Inline(_) => { - let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { - unreachable!() - }; - *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::One(value)]); + ObjectData::Empty => { + *self = match group { + DataGroup::One(value) => ObjectData::Inline(value), + group => ObjectData::Groups(vec![group]), + } } - } - } - - /// Append an empty or multi-value group. - #[inline] - pub(crate) fn push_many(&mut self, values: Vec>) { - match self { - ObjectData::Empty => *self = ObjectData::Groups(vec![DataGroup::Many(values)]), - ObjectData::Groups(groups) => groups.push(DataGroup::Many(values)), + // Promote a previously-inline object to the general representation. ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::Many(values)]); + *self = ObjectData::Groups(vec![DataGroup::One(first), group]); } } } @@ -105,7 +99,7 @@ impl<'a> ObjectData<'a> { if group.len() == 1 { DataGroup::One(group.pop().unwrap()) } else { - DataGroup::Many(group) + DataGroup::Values(group) } }) .collect(),