diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5be41d2..7dc1537 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - - run: cargo test --verbose + - run: cargo build --no-default-features + - run: cargo build --features std + - run: cargo build --features foundation + - run: cargo test --verbose --all-features - run: | export VERSION=${{ github.event.release.tag_name }} sed -i "s/0.0.0/$VERSION/g" Cargo.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4001a7e..e1830d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,5 +17,8 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - - run: cargo clippy - - run: cargo test --verbose + - run: cargo build --no-default-features + - run: cargo build --features std + - run: cargo build --features foundation + - run: cargo clippy --all-targets --all-features + - run: cargo test --verbose --all-features diff --git a/Cargo.toml b/Cargo.toml index 085ea7d..3cc6a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,9 @@ categories = ["parsing", "parser-implementations", "database"] [dependencies] [features] -std=[] \ No newline at end of file +std = [] +foundation = [] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 67c66e1..6015a42 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,19 @@ The `typedstream` format is derived from the data structure used by `NeXTSTEP`'s - Robust error handling for malformed or incomplete `typedstream` data - Ergonomic `TypedStreamDeserializer` with `resolve_properties` iterator for exploring object graphs +## Feature Flags + +`crabstep` is `no_std` by default and requires no dependencies. The following optional features are purely additive: + +- `std`: enables `std`-only conveniences, such as `print_resolved` for debugging an object graph. +- `foundation`: adds typed accessors on `Property` for common Apple [Foundation](https://developer.apple.com/documentation/foundation) classes (`as_string`, `as_data`, `as_array`, `as_dictionary`, `as_date`, `as_url`, and more), so consumers do not have to hand-roll class-name matching. See the `deserializer::foundation` module. + +Enable a feature in your `Cargo.toml`: + +```toml +crabstep = { version = "0", features = ["foundation"] } +``` + ## Reverse Engineering A blog post describing the reverse engineering of `typedstream` is available as [an in-depth article](https://chrissardegna.com/blog/reverse-engineering-apples-typedstream-format/). diff --git a/src/deserializer/constants.rs b/src/deserializer/constants.rs index 9ca578e..c9d5bd2 100644 --- a/src/deserializer/constants.rs +++ b/src/deserializer/constants.rs @@ -4,6 +4,9 @@ pub const I_16: u8 = 0x81; /// Indicates an [`i32`] in the byte stream pub const I_32: u8 = 0x82; +/// Indicates an [`i64`]/[`u64`] (8-byte) in the byte stream, used for `NSNumber` +/// values whose magnitude does not fit a 32-bit integer (C type `q`/`Q`) +pub const I_64: u8 = 0x87; /// Indicates an [`f32`] or [`f64`] in the byte stream; the [`Type`](crate::models::types::Type) determines the size pub const DECIMAL: u8 = 0x83; /// Indicates the start of a new object @@ -14,5 +17,3 @@ pub const EMPTY: u8 = 0x85; pub const END: u8 = 0x86; /// Bytes equal or greater in value than the reference tag indicate an index in the table of already-seen types pub const REFERENCE_TAG: u64 = 0x92; -/// Indicates an array in the byte stream -pub const ARRAY: u8 = 0x5b; diff --git a/src/deserializer/foundation/array.rs b/src/deserializer/foundation/array.rs new file mode 100644 index 0000000..43dc12f --- /dev/null +++ b/src/deserializer/foundation/array.rs @@ -0,0 +1,288 @@ +//! `as_array` / `as_set` and the [`FoundationArray`] view. + +use crate::deserializer::foundation::helpers::split_count; +use crate::deserializer::foundation::names::{ARRAY_CLASSES, SET_CLASSES}; +use crate::deserializer::iter::{Property, PropertyIterator}; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The elements of an `NSArray` / `NSMutableArray` as a lazy [`FoundationArray`] + /// view (the leading element-count group is skipped). Supports `len` / + /// `get(index)` / `iter`; each element is a group-level [`Property`] on which + /// the other accessors (`as_string`, `as_i64`, a nested `as_array`, …) apply. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let bytes: &[u8] = &[]; // a typedstream payload + /// let mut typedstream = TypedStreamDeserializer::new(bytes); + /// let root = typedstream.oxidize().unwrap(); + /// + /// for property in typedstream.resolve_properties(root).unwrap() { + /// if let Some(array) = property.as_array() { + /// println!("{} elements", array.len()); + /// for element in &array { + /// println!("{:?}", element.as_string()); + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn as_array(&self) -> Option> { + let (elements, len) = split_count(self.object_in_classes(ARRAY_CLASSES)?)?; + Some(FoundationArray { elements, len }) + } + + /// The members of an `NSSet` / `NSMutableSet` as a lazy [`FoundationArray`] + /// view (unordered). Shares the type with [`as_array`](Self::as_array): the + /// count group is skipped and each member is a group-level [`Property`]. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let bytes: &[u8] = &[]; + /// let mut typedstream = TypedStreamDeserializer::new(bytes); + /// let root = typedstream.oxidize().unwrap(); + /// + /// for property in typedstream.resolve_properties(root).unwrap() { + /// if let Some(set) = property.as_set() { + /// for member in &set { + /// println!("{:?}", member.as_string()); + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn as_set(&self) -> Option> { + let (elements, len) = split_count(self.object_in_classes(SET_CLASSES)?)?; + Some(FoundationArray { elements, len }) + } +} + +/// A lazy view over the elements of an `NSArray` / `NSMutableArray` (or the +/// members of an `NSSet` / `NSMutableSet`), produced by [`Property::as_array`] / +/// [`Property::as_set`]. Cheap to clone and queryable any number of times; each +/// element is a group-level [`Property`], so the other accessors apply directly. +#[derive(Debug, Clone)] +pub struct FoundationArray<'a, 'b> { + elements: PropertyIterator<'a, 'b>, + len: usize, +} + +impl<'a, 'b: 'a> FoundationArray<'a, 'b> { + /// The number of elements (from the archived count). + #[must_use] + pub fn len(&self) -> usize { + self.len + } + + /// Whether the collection has no elements. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// A fresh iterator over the elements. + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let array = property.as_array().unwrap(); + /// for element in array.iter() { + /// println!("{:?}", element.as_string()); + /// } + /// ``` + #[must_use] + pub fn iter(&self) -> FoundationArrayIter<'a, 'b> { + FoundationArrayIter { + inner: self.elements.clone(), + } + } + + /// The element at `index` (a linear `O(index)` walk). + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let array = property.as_array().unwrap(); + /// println!("{:?}", array.get(2).and_then(|element| element.as_i64())); + /// ``` + #[must_use] + pub fn get(&self, index: usize) -> Option> { + self.iter().nth(index) + } + + /// The first element. + #[must_use] + pub fn first(&self) -> Option> { + self.iter().next() + } +} + +impl<'a, 'b: 'a> IntoIterator for FoundationArray<'a, 'b> { + type Item = Property<'a, 'b>; + type IntoIter = FoundationArrayIter<'a, 'b>; + + fn into_iter(self) -> Self::IntoIter { + FoundationArrayIter { + inner: self.elements, + } + } +} + +impl<'a, 'b: 'a> IntoIterator for &FoundationArray<'a, 'b> { + type Item = Property<'a, 'b>; + type IntoIter = FoundationArrayIter<'a, 'b>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// The iterator yielded by [`FoundationArray::iter`] and its [`IntoIterator`] impl. +#[derive(Debug, Clone)] +pub struct FoundationArrayIter<'a, 'b> { + inner: PropertyIterator<'a, 'b>, +} + +impl<'a, 'b: 'a> Iterator for FoundationArrayIter<'a, 'b> { + type Item = Property<'a, 'b>; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_array() { + // Root NSArray([NSString "a", NSNumber 1, NSString "b"]) via `root()`. + let bytes = load("foundation/NSArray"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.root().unwrap(); + let array = root.as_array().unwrap(); + assert_eq!(array.len(), 3); + let strings: Vec<&str> = array.iter().filter_map(|e| e.as_string()).collect(); + assert_eq!(strings, vec!["a", "b"]); + } + + #[test] + fn as_array_yields_elements_both_variants_and_empty() { + // NestedContainers root holds NSArray[1,2], NSMutableArray[3], an empty + // NSArray, then non-array elements (dicts/sets) which as_array ignores. + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let arrays: Vec> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_array()) + .map(|array| array.into_iter().filter_map(|el| el.as_i64()).collect()) + .collect(); + + assert_eq!(arrays, vec![vec![1, 2], vec![3], vec![]]); + } + + #[test] + fn as_set_yields_members_both_variants() { + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let sets: Vec> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_set()) + .map(|set| { + set.into_iter() + .filter_map(|member| member.as_string()) + .collect() + }) + .collect(); + + assert_eq!(sets, vec![vec!["s"], vec!["ms"]]); + } + + #[test] + fn nested_array_inside_array() { + let bytes = load("foundation/NSArrayNested"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let inner: Vec> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_array()) + .map(|array| array.into_iter().filter_map(|el| el.as_i64()).collect()) + .collect(); + + assert_eq!(inner, vec![vec![1, 2]]); + } + + #[test] + fn container_accessors_reject_non_containers() { + let bytes = load("foundation/NumberInt"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().unwrap(); + + assert!(group.as_array().is_none()); + assert!(group.as_set().is_none()); + assert!(group.as_dictionary().is_none()); + } + + #[test] + fn array_view_len_get_first() { + // First array element of NestedContainers is NSArray[1, 2]. + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let array = ts + .resolve_properties(root) + .unwrap() + .find_map(|group| group.as_array()) + .unwrap(); + + assert_eq!(array.len(), 2); + assert!(!array.is_empty()); + assert_eq!(array.first().and_then(|e| e.as_i64()), Some(1)); + assert_eq!(array.get(0).and_then(|e| e.as_i64()), Some(1)); + assert_eq!(array.get(1).and_then(|e| e.as_i64()), Some(2)); + assert!(array.get(2).is_none()); + } + + #[test] + fn array_view_empty() { + // NestedContainers also holds an empty NSArray. + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let empty = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_array()) + .find(|array| array.is_empty()) + .unwrap(); + + assert_eq!(empty.len(), 0); + assert!(empty.first().is_none()); + assert_eq!(empty.iter().count(), 0); + } +} diff --git a/src/deserializer/foundation/boolean.rs b/src/deserializer/foundation/boolean.rs new file mode 100644 index 0000000..fd89fd7 --- /dev/null +++ b/src/deserializer/foundation/boolean.rs @@ -0,0 +1,34 @@ +//! `as_bool`: a boolean `NSNumber` (or bare primitive). + +use crate::deserializer::iter::Property; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// An `NSNumber` (or bare primitive) interpreted as a boolean. Returns `None` + /// for integer values other than `0` and `1`. + #[must_use] + pub fn as_bool(&self) -> Option { + match self.as_i64()? { + 0 => Some(false), + 1 => Some(true), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn as_bool_reads_boolean() { + let bytes = load("foundation/NumberBool"); // NSNumber(true) -> SignedInteger(1) + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_bool(), Some(true)); + assert_eq!(group.as_i64(), Some(1)); + } +} diff --git a/src/deserializer/foundation/bytes.rs b/src/deserializer/foundation/bytes.rs new file mode 100644 index 0000000..8198f3c --- /dev/null +++ b/src/deserializer/foundation/bytes.rs @@ -0,0 +1,51 @@ +//! `as_data`: the data cluster. + +use crate::deserializer::foundation::names::DATA_CLASSES; +use crate::deserializer::iter::Property; +use crate::models::output_data::OutputData; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The raw bytes of an `NSData` / `NSMutableData`. + /// + /// crabstep does not interpret the bytes, as they may be a `bplist00`, a + /// compressed blob, an image, etc. The caller decides what they are. + #[must_use] + pub fn as_data(&self) -> Option<&'a [u8]> { + let data = self.object_in_classes(DATA_CLASSES)?; + for prop in data { + if let Property::Group(group) = prop { + for child in group { + if let Property::Primitive(OutputData::Array(bytes)) = child { + return Some(bytes); + } + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn as_data_reads_both_data_variants() { + // NSArray([NSData [1,2], NSMutableData [3,4,5]]) + let bytes = load("foundation/NestedData"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let datas: Vec<&[u8]> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_data()) + .collect(); + + assert_eq!(datas.len(), 2, "{datas:?}"); + assert!(datas.contains(&&[0x01, 0x02][..]), "{datas:?}"); + assert!(datas.contains(&&[0x03, 0x04, 0x05][..]), "{datas:?}"); + } +} diff --git a/src/deserializer/foundation/class.rs b/src/deserializer/foundation/class.rs new file mode 100644 index 0000000..34e9956 --- /dev/null +++ b/src/deserializer/foundation/class.rs @@ -0,0 +1,87 @@ +//! The `class_name` escape hatch. + +use crate::deserializer::iter::Property; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The Objective-C class name of the object this property refers to, if any. + /// + /// Resolves whether `self` is a [`Property::Object`] directly or a + /// [`Property::Group`] whose first element is an object. This is the escape + /// hatch for classes the `foundation` feature does not model: the class name + /// plus the raw subtree (via [`Property::Object`]) are always available, so a + /// consumer can handle the long tail of app-specific classes itself. + #[must_use] + pub fn class_name(&self) -> Option<&'a str> { + match self { + Property::Object { name, .. } => Some(*name), + Property::Group(group) => match group.first()? { + Property::Object { name, .. } => Some(name), + _ => None, + }, + Property::Primitive(_) => None, + } + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::iter::Property; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn class_name_resolves_element_classes() { + // Iterating an `NSArray` yields one group per element; each element group's + // `class_name()` is the element's class, and the bare count group is None. + let bytes = load("foundation/NSArray"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let names: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.class_name()) + .collect(); + + assert!(names.contains(&"NSString"), "names: {names:?}"); + assert!(names.contains(&"NSNumber"), "names: {names:?}"); + } + + #[test] + fn class_name_on_direct_object() { + // `class_name()` also works when called on a [`Property::Object`] directly + // (e.g. after stepping into a group with `iter().next()`). + let bytes = load("foundation/NSArray"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let object_names: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| match group { + Property::Group(g) => g.first(), + _ => None, + }) + .filter_map(|inner| inner.class_name()) + .collect(); + + assert!( + object_names.contains(&"NSString"), + "names: {object_names:?}" + ); + } + + #[test] + fn class_name_is_none_for_primitive() { + // The `NSArray`'s count group is a bare primitive, no class. + let bytes = load("foundation/NSArray"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let has_primitive_group = ts + .resolve_properties(root) + .unwrap() + .any(|group| group.class_name().is_none()); + + assert!(has_primitive_group); + } +} diff --git a/src/deserializer/foundation/date.rs b/src/deserializer/foundation/date.rs new file mode 100644 index 0000000..005f63e --- /dev/null +++ b/src/deserializer/foundation/date.rs @@ -0,0 +1,82 @@ +//! `as_date` / `as_unix_time`: `NSDate`. + +use crate::deserializer::iter::Property; +use crate::models::output_data::OutputData; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// An `NSDate` as seconds since the Cocoa reference epoch (2001-01-01 00:00:00 + /// UTC). Use [`as_unix_time`](Self::as_unix_time) for seconds since the Unix + /// epoch. + #[must_use] + pub fn as_date(&self) -> Option { + let mut data = self.object_in_classes(&["NSDate"])?; + match data.next()? { + Property::Group(group) => match group.first()? { + Property::Primitive(OutputData::Double(seconds)) => Some(*seconds), + _ => None, + }, + _ => None, + } + } + + /// An `NSDate` as seconds since the Unix epoch (1970-01-01 00:00:00 UTC), + /// i.e. [`as_date`](Self::as_date)` + 978_307_200.0` (the offset between the + /// Unix and Cocoa reference epochs). + #[must_use] + pub fn as_unix_time(&self) -> Option { + self.as_date().map(|seconds| seconds + 978_307_200.0) + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_date() { + // Root NSDate (timeIntervalSinceReferenceDate: 21692800) via `root()`. + let bytes = load("foundation/NSDate"); + let mut ts = TypedStreamDeserializer::new(&bytes); + assert_eq!(ts.root().unwrap().as_date(), Some(21692800.0)); + } + + #[test] + fn as_date_and_unix_time() { + // NestedScalars holds NSDate(timeIntervalSinceReferenceDate: 21692800). + let bytes = load("foundation/NestedScalars"); + + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let dates: Vec = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_date()) + .collect(); + assert_eq!(dates, vec![21692800.0]); + + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let unix: Vec = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_unix_time()) + .collect(); + assert_eq!(unix, vec![1_000_000_000.0]); + } + + #[test] + fn scalar_accessors_reject_wrong_types() { + let bytes = load("foundation/NumberInt"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().unwrap(); + + assert!(!group.is_null()); + assert_eq!(group.as_date(), None); + assert_eq!(group.as_unix_time(), None); + assert_eq!(group.as_url(), None); + } +} diff --git a/src/deserializer/foundation/dict.rs b/src/deserializer/foundation/dict.rs new file mode 100644 index 0000000..5cfc5d0 --- /dev/null +++ b/src/deserializer/foundation/dict.rs @@ -0,0 +1,259 @@ +//! `as_dictionary` and the [`FoundationDict`] view. + +use crate::deserializer::foundation::helpers::split_count; +use crate::deserializer::foundation::names::DICT_CLASSES; +use crate::deserializer::iter::{Property, PropertyIterator}; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The entries of an `NSDictionary` / `NSMutableDictionary` as a lazy + /// [`FoundationDict`] view (the leading count group is skipped). Look up a + /// string key with [`get`](FoundationDict::get), or iterate the `(key, value)` + /// pairs; each key and value is a group-level [`Property`]. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let bytes: &[u8] = &[]; + /// let mut typedstream = TypedStreamDeserializer::new(bytes); + /// let root = typedstream.oxidize().unwrap(); + /// + /// for property in typedstream.resolve_properties(root).unwrap() { + /// if let Some(dict) = property.as_dictionary() { + /// // Look up a value by its string key. + /// if let Some(part) = dict.get("__kIMMessagePartAttributeName") { + /// println!("part index = {:?}", part.as_i64()); + /// } + /// // Or iterate every entry. + /// for (key, value) in &dict { + /// println!("{:?} => {:?}", key.as_string(), value.as_i64()); + /// } + /// } + /// } + /// ``` + #[must_use] + pub fn as_dictionary(&self) -> Option> { + let (entries, len) = split_count(self.object_in_classes(DICT_CLASSES)?)?; + Some(FoundationDict { entries, len }) + } +} + +/// A lazy view over the `(key, value)` pairs of an `NSDictionary` / +/// `NSMutableDictionary`, produced by [`Property::as_dictionary`]. Cheap to clone +/// and queryable any number of times; each key and value is a group-level +/// [`Property`]. +#[derive(Debug, Clone)] +pub struct FoundationDict<'a, 'b> { + entries: PropertyIterator<'a, 'b>, + len: usize, +} + +impl<'a, 'b: 'a> FoundationDict<'a, 'b> { + /// The number of entries (from the archived count). + #[must_use] + pub fn len(&self) -> usize { + self.len + } + + /// Whether the dictionary has no entries. + #[must_use] + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + /// A fresh iterator over the `(key, value)` pairs. + #[must_use] + pub fn iter(&self) -> FoundationDictIter<'a, 'b> { + FoundationDictIter { + inner: self.entries.clone(), + } + } + + /// The value for a string `key`, or `None` if absent. + /// + /// This is a linear scan (`O(n)`); dictionaries archived in a `typedstream` + /// are typically small. Only string keys are matched. For other key types, + /// or to build an index for many lookups, use [`iter`](Self::iter). + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let dict = property.as_dictionary().unwrap(); + /// if let Some(value) = dict.get("__kIMMessagePartAttributeName") { + /// println!("{:?}", value.as_i64()); + /// } + /// ``` + #[must_use] + pub fn get(&self, key: &str) -> Option> { + self.iter() + .find_map(|(k, v)| (k.as_string() == Some(key)).then_some(v)) + } + + /// Whether the dictionary contains the given string `key`. + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let dict = property.as_dictionary().unwrap(); + /// if dict.contains_key("__kIMMessagePartAttributeName") { + /// // the message-part attribute is present + /// } + /// ``` + #[must_use] + pub fn contains_key(&self, key: &str) -> bool { + self.get(key).is_some() + } + + /// A fresh iterator over the keys. + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let dict = property.as_dictionary().unwrap(); + /// for key in dict.keys() { + /// println!("{:?}", key.as_string()); + /// } + /// ``` + pub fn keys(&self) -> impl Iterator> { + self.iter().map(|(k, _)| k) + } + + /// A fresh iterator over the values. + /// + /// # Examples + /// + /// ```no_run + /// # use crabstep::TypedStreamDeserializer; + /// # let bytes: &[u8] = &[]; + /// # let mut typedstream = TypedStreamDeserializer::new(bytes); + /// # let root = typedstream.oxidize().unwrap(); + /// # let property = typedstream.resolve_properties(root).unwrap().next().unwrap(); + /// # let dict = property.as_dictionary().unwrap(); + /// for value in dict.values() { + /// println!("{:?}", value.as_i64()); + /// } + /// ``` + pub fn values(&self) -> impl Iterator> { + self.iter().map(|(_, v)| v) + } +} + +impl<'a, 'b: 'a> IntoIterator for FoundationDict<'a, 'b> { + type Item = (Property<'a, 'b>, Property<'a, 'b>); + type IntoIter = FoundationDictIter<'a, 'b>; + + fn into_iter(self) -> Self::IntoIter { + FoundationDictIter { + inner: self.entries, + } + } +} + +impl<'a, 'b: 'a> IntoIterator for &FoundationDict<'a, 'b> { + type Item = (Property<'a, 'b>, Property<'a, 'b>); + type IntoIter = FoundationDictIter<'a, 'b>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +/// The iterator yielded by [`FoundationDict::iter`] and its [`IntoIterator`] impl. +#[derive(Debug, Clone)] +pub struct FoundationDictIter<'a, 'b> { + inner: PropertyIterator<'a, 'b>, +} + +impl<'a, 'b: 'a> Iterator for FoundationDictIter<'a, 'b> { + type Item = (Property<'a, 'b>, Property<'a, 'b>); + + fn next(&mut self) -> Option { + let key = self.inner.next()?; + // A missing value means an unpaired trailing key (malformed data); drop it. + let value = self.inner.next()?; + Some((key, value)) + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_dictionary() { + // Root NSDictionary { "k1": 1, "k2": "v" } via `root()`. + let bytes = load("foundation/NSDictionary"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.root().unwrap(); + let dict = root.as_dictionary().unwrap(); + assert_eq!(dict.len(), 2); + assert_eq!(dict.get("k1").and_then(|v| v.as_i64()), Some(1)); + assert_eq!(dict.get("k2").and_then(|v| v.as_string()), Some("v")); + } + + #[test] + fn as_dictionary_yields_pairs_both_variants() { + // NestedContainers holds NSDictionary{k:9} and NSMutableDictionary{mk:8}. + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let entries: Vec<(&str, i64)> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_dictionary()) + .flat_map(|dict| { + dict.into_iter() + .filter_map(|(key, value)| Some((key.as_string()?, value.as_i64()?))) + .collect::>() + }) + .collect(); + + assert_eq!(entries.len(), 2, "{entries:?}"); + assert!(entries.contains(&("k", 9)), "{entries:?}"); + assert!(entries.contains(&("mk", 8)), "{entries:?}"); + } + + #[test] + fn dict_view_get_contains_keys_values() { + // First dictionary of NestedContainers is NSDictionary { "k": 9 }. + let bytes = load("foundation/NestedContainers"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let dict = ts + .resolve_properties(root) + .unwrap() + .find_map(|group| group.as_dictionary()) + .unwrap(); + + assert_eq!(dict.len(), 1); + assert!(!dict.is_empty()); + assert_eq!(dict.get("k").and_then(|v| v.as_i64()), Some(9)); + assert!(dict.get("missing").is_none()); + assert!(dict.contains_key("k")); + assert!(!dict.contains_key("missing")); + + let keys: Vec<&str> = dict.keys().filter_map(|k| k.as_string()).collect(); + assert_eq!(keys, vec!["k"]); + let values: Vec = dict.values().filter_map(|v| v.as_i64()).collect(); + assert_eq!(values, vec![9]); + } +} diff --git a/src/deserializer/foundation/helpers.rs b/src/deserializer/foundation/helpers.rs new file mode 100644 index 0000000..cc70baf --- /dev/null +++ b/src/deserializer/foundation/helpers.rs @@ -0,0 +1,32 @@ +//! Helpers shared across the Foundation accessors. + +use crate::deserializer::iter::{Property, PropertyIterator}; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// If `self` is a group whose first item is an object whose class is in + /// `classes`, return that object's data iterator. + pub(crate) fn object_in_classes(&self, classes: &[&str]) -> Option> { + match self { + // `self` is the object directly (e.g. from `root()`/`resolve_object`). + Property::Object { name, data, .. } if classes.contains(name) => Some(data.clone()), + // `self` is a group whose first item is the object (iteration). + Property::Group(group) => match group.first()? { + Property::Object { name, data, .. } if classes.contains(&name) => Some(data), + _ => None, + }, + _ => None, + } + } +} + +/// Read the leading count group of a container's data, returning the remaining +/// iterator (positioned at the first element/entry) and the declared count. +pub(crate) fn split_count<'a, 'b: 'a>( + mut data: PropertyIterator<'a, 'b>, +) -> Option<(PropertyIterator<'a, 'b>, usize)> { + let count = data.next()?; + // A non-integer or negative count means this isn't a well-formed container; + // signal that rather than reporting a bogus zero length. + let len = usize::try_from(count.as_i64()?).ok()?; + Some((data, len)) +} diff --git a/src/deserializer/foundation/mod.rs b/src/deserializer/foundation/mod.rs new file mode 100644 index 0000000..028218f --- /dev/null +++ b/src/deserializer/foundation/mod.rs @@ -0,0 +1,42 @@ +/*! +Typed accessors for common Apple [Foundation](https://developer.apple.com/documentation/foundation) classes. + +Enabled by the `foundation` cargo feature. These methods interpret the generic +[`Property`](crate::deserializer::iter::Property) tree as specific Foundation +types (`NSString`, `NSDictionary`, …) so consumers do not have to re-implement +class-name matching (and re-discover its footguns, e.g. that the data cluster +archives as both `NSData` and `NSMutableData`). + +This feature is purely for convenience: the parser and the +[`Property`](crate::deserializer::iter::Property) / +[`OutputData`](crate::models::output_data::OutputData) model are unchanged whether +or not it is enabled, and any class not modeled here stays reachable through +[`Property::Object`](crate::deserializer::iter::Property::Object), so nothing is +ever lost. + +Accessors are methods on a [`Property`](crate::deserializer::iter::Property) — +either a group yielded while iterating an object's properties, or an object +obtained from [`TypedStreamDeserializer::root`](crate::TypedStreamDeserializer::root) +/ [`resolve_object`](crate::TypedStreamDeserializer::resolve_object) when the value +you want *is* the root (or a referenced) object. Each Foundation type lives in its +own module. +*/ + +mod array; +mod boolean; +mod bytes; +mod class; +mod date; +mod dict; +mod helpers; +mod names; +mod null; +mod number; +mod string; +mod url; + +#[cfg(test)] +mod test_support; + +pub use crate::deserializer::foundation::array::{FoundationArray, FoundationArrayIter}; +pub use crate::deserializer::foundation::dict::{FoundationDict, FoundationDictIter}; diff --git a/src/deserializer/foundation/names.rs b/src/deserializer/foundation/names.rs new file mode 100644 index 0000000..ba6c636 --- /dev/null +++ b/src/deserializer/foundation/names.rs @@ -0,0 +1,19 @@ +//! Cluster class-name sets. +//! +//! Foundation archives several types as both an immutable and a mutable variant +//! (and the data cluster archives as both `NSData` and `NSMutableData`); matching +//! all variants in one place keeps the footgun centralized. + +/// Denotes string data +pub(crate) const STRING_CLASSES: &[&str] = &["NSString", "NSMutableString"]; +/// Denotes string data with attributes (e.g. font, color, …) +pub(crate) const ATTRIBUTED_STRING_CLASSES: &[&str] = + &["NSAttributedString", "NSMutableAttributedString"]; +/// Denotes raw bytes +pub(crate) const DATA_CLASSES: &[&str] = &["NSData", "NSMutableData"]; +/// Denotes ordered collections of arbitrary objects. +pub(crate) const ARRAY_CLASSES: &[&str] = &["NSArray", "NSMutableArray"]; +/// Denotes unordered collections of arbitrary objects. +pub(crate) const DICT_CLASSES: &[&str] = &["NSDictionary", "NSMutableDictionary"]; +/// Denotes unordered collections of unique arbitrary objects. +pub(crate) const SET_CLASSES: &[&str] = &["NSSet", "NSMutableSet"]; diff --git a/src/deserializer/foundation/null.rs b/src/deserializer/foundation/null.rs new file mode 100644 index 0000000..2d94226 --- /dev/null +++ b/src/deserializer/foundation/null.rs @@ -0,0 +1,42 @@ +//! `is_null`: `NSNull` and nil references. + +use crate::deserializer::iter::Property; +use crate::models::output_data::OutputData; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// Whether this property is an `NSNull` instance or a nil object reference + /// ([`OutputData::Null`]). + #[must_use] + pub fn is_null(&self) -> bool { + match self { + Property::Object { name: "NSNull", .. } | Property::Primitive(OutputData::Null) => true, + Property::Group(group) => matches!( + group.first(), + Some( + Property::Object { name: "NSNull", .. } | Property::Primitive(OutputData::Null) + ) + ), + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn is_null_detects_nsnull() { + let bytes = load("foundation/NestedScalars"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let null_count = ts + .resolve_properties(root) + .unwrap() + .filter(|group| group.is_null()) + .count(); + + assert_eq!(null_count, 1); + } +} diff --git a/src/deserializer/foundation/number.rs b/src/deserializer/foundation/number.rs new file mode 100644 index 0000000..9f3c600 --- /dev/null +++ b/src/deserializer/foundation/number.rs @@ -0,0 +1,192 @@ +//! `as_i64` / `as_u64` / `as_f64`: numeric `NSNumber`s (or bare primitives). + +use crate::deserializer::iter::{Property, PropertyIterator}; +use crate::models::output_data::OutputData; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// An `NSNumber` (or bare primitive) interpreted as a signed integer. Accepts + /// either integer encoding; an unsigned value that does not fit `i64` yields + /// `None`. Float/double values are *not* coerced. + #[must_use] + pub fn as_i64(&self) -> Option { + match self.scalar()? { + OutputData::SignedInteger(v) => Some(*v), + OutputData::UnsignedInteger(v) => i64::try_from(*v).ok(), + _ => None, + } + } + + /// An `NSNumber` (or bare primitive) interpreted as an unsigned integer. + /// Accepts either integer encoding; a negative signed value yields `None`. + /// Float/double values are *not* coerced. + #[must_use] + pub fn as_u64(&self) -> Option { + match self.scalar()? { + OutputData::UnsignedInteger(v) => Some(*v), + OutputData::SignedInteger(v) => u64::try_from(*v).ok(), + _ => None, + } + } + + /// An `NSNumber` (or bare primitive) interpreted as a double. Accepts `Float` + /// and `Double`; integer values are *not* coerced. + #[must_use] + pub fn as_f64(&self) -> Option { + match self.scalar()? { + OutputData::Double(v) => Some(*v), + OutputData::Float(v) => Some(f64::from(*v)), + _ => None, + } + } + + /// The underlying scalar value of a group that is either a bare primitive or + /// an `NSNumber` wrapping one. + fn scalar(&self) -> Option<&'b OutputData<'a>> { + match self { + Property::Primitive(value) => Some(value), + Property::Object { + name: "NSNumber", + data, + .. + } => number_value(data.clone()), + Property::Group(group) => match group.first()? { + Property::Primitive(value) => Some(value), + Property::Object { + name: "NSNumber", + data, + .. + } => number_value(data), + _ => None, + }, + _ => None, + } + } +} + +/// The primitive in the last group of an `NSNumber` object. +/// +/// Group order: the `objCType` string, then the numeric value. +fn number_value<'a, 'b: 'a>(data: PropertyIterator<'a, 'b>) -> Option<&'b OutputData<'a>> { + match data.last()? { + Property::Group(inner) => match inner.first()? { + Property::Primitive(value) => Some(value), + _ => None, + }, + _ => None, + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_i64() { + let bytes = load("foundation/NumberInt"); + let mut ts = TypedStreamDeserializer::new(&bytes); + assert_eq!(ts.root().unwrap().as_i64(), Some(42)); + } + + #[test] + fn as_f64_reads_decimal_double() { + // NumberDouble: bare-primitive path; verify the DECIMAL read. + let bytes = load("foundation/NumberDouble"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_f64(), Some(100.5)); + } + + #[test] + fn as_f64_reads_float() { + let bytes = load("foundation/NumberFloat"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_f64(), Some(3.5)); + } + + #[test] + fn as_i64_reads_large_negative() { + // NumberInt64 = -9_000_000_000: bare-primitive path; 8-byte form. + let bytes = load("foundation/NumberInt64"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_i64(), Some(-9_000_000_000)); + } + + #[test] + fn integer_coercion_and_strictness() { + // NumberInt = SignedInteger(42). + let bytes = load("foundation/NumberInt"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_i64(), Some(42)); + assert_eq!(group.as_u64(), Some(42)); // non-negative signed coerces to u64 + assert_eq!(group.as_f64(), None); // no silent int -> float + assert_eq!(group.as_string(), None); // a number is not a string + assert_eq!(group.as_data(), None); + } + + #[test] + fn unsigned_coercion_rejects_negative() { + let bytes = load("foundation/NumberInt64"); // -9_000_000_000 + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); + + assert_eq!(group.as_u64(), None); + } + + #[test] + fn unwraps_nsnumber_object_and_reads_dict_entries() { + // NSDictionaryNested = { "arr": [..], "data": , "num": NSNumber(7) }. + // Keys are NSString objects; the NSNumber value exercises the unwrap path; + // the NSData value exercises as_data on a dictionary value. + let bytes = load("foundation/NSDictionaryNested"); + + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let keys: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_string()) + .collect(); + assert!( + keys.contains(&"arr") && keys.contains(&"data") && keys.contains(&"num"), + "{keys:?}" + ); + + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let ints: Vec = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_i64()) + .collect(); + assert!(ints.contains(&7), "{ints:?}"); // NSNumber(7) value, unwrapped + + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let datas: Vec<&[u8]> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_data()) + .collect(); + assert!(datas.contains(&&[0x01, 0x02][..]), "{datas:?}"); // NSData value + } +} diff --git a/src/deserializer/foundation/string.rs b/src/deserializer/foundation/string.rs new file mode 100644 index 0000000..5973376 --- /dev/null +++ b/src/deserializer/foundation/string.rs @@ -0,0 +1,106 @@ +//! `as_string`: the string cluster and attributed strings. + +use crate::deserializer::foundation::names::{ATTRIBUTED_STRING_CLASSES, STRING_CLASSES}; +use crate::deserializer::iter::{Property, PropertyIterator}; +use crate::models::output_data::OutputData; + +/// Extract the backing UTF-8 of a string-cluster object: the first `String` +/// primitive in the object's first data group. +fn backing_string<'a, 'b: 'a>(mut data: PropertyIterator<'a, 'b>) -> Option<&'a str> { + if let Property::Group(group) = data.next()? + && let Some(Property::Primitive(OutputData::String(s))) = group.first() + { + return Some(s); + } + None +} + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The backing string of an `NSString` / `NSMutableString`, or the plain text + /// of an `NSAttributedString` / `NSMutableAttributedString` (its attributes + /// remain reachable through the generic [`Property`] tree). + #[must_use] + pub fn as_string(&self) -> Option<&'a str> { + if let Some(data) = self.object_in_classes(STRING_CLASSES) { + return backing_string(data); + } + // An attributed string stores its backing store as a nested + // `NSString`/`NSMutableString`; its position among the groups varies by + // producer (the attributes dictionary often comes first), so scan. + if let Some(data) = self.object_in_classes(ATTRIBUTED_STRING_CLASSES) { + for prop in data { + if let Property::Group(group) = prop + && let Some(Property::Object { + name, data: inner, .. + }) = group.first() + && STRING_CLASSES.contains(&name) + { + return backing_string(inner); + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_string() { + let bytes = load("foundation/NSString"); + let mut ts = TypedStreamDeserializer::new(&bytes); + assert_eq!(ts.root().unwrap().as_string(), Some("Hello, world")); + } + + #[test] + fn as_string_reads_both_string_variants() { + // NSArray([NSString "imm", NSMutableString "mut"]) + let bytes = load("foundation/NestedStrings"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let strings: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_string()) + .collect(); + + assert_eq!(strings.len(), 2, "{strings:?}"); + assert!(strings.contains(&"imm"), "{strings:?}"); + assert!(strings.contains(&"mut"), "{strings:?}"); + } + + #[test] + fn as_string_reads_attributed_backing_text() { + let bytes = load("foundation/NestedAttributed"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let strings: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_string()) + .collect(); + + assert_eq!(strings, vec!["styled"]); + } + + #[test] + fn as_string_reads_real_mutable_attributed_body() { + // Real Messages NSMutableAttributedString: backing store is an + // NSMutableString reached as one of the root's groups (plain path). + let bytes = load("AttributedBodyTextOnly"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let strings: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_string()) + .collect(); + + assert!(strings.contains(&"Noter test"), "{strings:?}"); + } +} diff --git a/src/deserializer/foundation/test_support.rs b/src/deserializer/foundation/test_support.rs new file mode 100644 index 0000000..309ac19 --- /dev/null +++ b/src/deserializer/foundation/test_support.rs @@ -0,0 +1,15 @@ +//! Shared helpers for the Foundation accessor tests. + +extern crate std; + +use alloc::{vec, vec::Vec}; +use std::{env::current_dir, fs::File, io::Read}; + +/// Load a fixture by path relative to `src/test_data`. +pub(super) fn load(rel: &str) -> Vec { + let path = current_dir().unwrap().join("src/test_data").join(rel); + let mut file = File::open(&path).unwrap_or_else(|e| panic!("opening fixture {path:?}: {e}")); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + bytes +} diff --git a/src/deserializer/foundation/url.rs b/src/deserializer/foundation/url.rs new file mode 100644 index 0000000..b4822fb --- /dev/null +++ b/src/deserializer/foundation/url.rs @@ -0,0 +1,54 @@ +//! `as_url`: `NSURL`. + +use crate::deserializer::iter::Property; + +impl<'a, 'b: 'a> Property<'a, 'b> { + /// The string of an `NSURL`. For an absolute URL this is the full URL; for a + /// URL created relative to a base, it is the relative component (the base + /// `NSURL` remains reachable through the generic [`Property`] tree). + #[must_use] + pub fn as_url(&self) -> Option<&'a str> { + let data = self.object_in_classes(&["NSURL"])?; + for group in data { + if let Some(url) = group.as_string() { + return Some(url); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn root_object_resolves_as_url() { + // Root NSURL("https://example.com/path?q=1") via `root()`. + let bytes = load("foundation/NSURL"); + let mut ts = TypedStreamDeserializer::new(&bytes); + assert_eq!( + ts.root().unwrap().as_url(), + Some("https://example.com/path?q=1") + ); + } + + #[test] + fn as_url_absolute_and_relative() { + // NestedScalars holds an absolute NSURL then a relative one; the relative + // one yields its relative component. + let bytes = load("foundation/NestedScalars"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let urls: Vec<&str> = ts + .resolve_properties(root) + .unwrap() + .filter_map(|group| group.as_url()) + .collect(); + + assert_eq!(urls, vec!["https://example.com/path?q=1", "page.html"]); + } +} diff --git a/src/deserializer/iter.rs b/src/deserializer/iter.rs index d7b99fc..98dcdf5 100644 --- a/src/deserializer/iter.rs +++ b/src/deserializer/iter.rs @@ -5,10 +5,10 @@ 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}, + shared_string::SharedString, }; /// A single resolved property from an [`Archived::Object`]. @@ -18,7 +18,7 @@ pub enum Property<'a, 'b> { Object { /// The class of the object class: &'a Class, - /// The name of the class, typically a string from the type table + /// The name of the class, its entry in the string table name: &'a str, /// An iterator over the properties of this object data: PropertyIterator<'a, 'b>, @@ -43,7 +43,7 @@ pub enum Property<'a, 'b> { pub struct PropertyGroup<'a, 'b> { items: &'b [OutputData<'a>], object_table: &'b [Archived<'a>], - type_table: &'b [TypeEntry<'a>], + string_table: &'b [SharedString<'a>], } impl<'a, 'b: 'a> PropertyGroup<'a, 'b> { @@ -83,32 +83,42 @@ impl<'a, 'b: 'a> PropertyGroup<'a, 'b> { } /// Resolve a single stored value into a [`Property`]. Object references are - /// resolved against the object/type tables; everything else is a primitive. + /// resolved against the object and string tables; everything else is a primitive. fn resolve(&self, item: &'b OutputData<'a>) -> Property<'a, 'b> { if let OutputData::Object(idx) = item - && let Some(Archived::Object { class: cls, .. }) = self.object_table.get(*idx) - && let Some(Archived::Class(cls)) = self.object_table.get(*cls) - && let Some(sub_iter) = PropertyIterator::new(self.object_table, self.type_table, *idx) + && let Some(object) = object_property(self.object_table, self.string_table, *idx) { - let class_name = self - .type_table - .get(cls.name_index) - .and_then(|types| types.first()) - .and_then(|t| match t { - Type::String(name) => Some(*name), - _ => None, - }) - .unwrap_or("Unknown Class"); - return Property::Object { - class: cls, - name: class_name, - data: sub_iter, - }; + return object; } Property::Primitive(item) } } +/// Build a [`Property::Object`] for the object at `index` in the tables, or `None` +/// if the index is not an object (or its class is missing). Shared by +/// [`PropertyGroup`] resolution and the deserializer's object/root resolvers. +pub(crate) fn object_property<'a, 'b: 'a>( + object_table: &'b [Archived<'a>], + string_table: &'b [SharedString<'a>], + index: usize, +) -> Option> { + let Some(Archived::Object { class: cls, .. }) = object_table.get(index) else { + return None; + }; + let Some(Archived::Class(cls)) = object_table.get(*cls) else { + return None; + }; + let data = PropertyIterator::new(object_table, string_table, index)?; + let name = string_table + .get(cls.name_index) + .map_or("Unknown Class", |entry| entry.text); + Some(Property::Object { + class: cls, + name, + data, + }) +} + impl<'a, 'b: 'a> IntoIterator for PropertyGroup<'a, 'b> { type Item = Property<'a, 'b>; type IntoIter = PropertyGroupIter<'a, 'b>; @@ -168,7 +178,7 @@ impl<'a, 'b: 'a> ExactSizeIterator for PropertyGroupIter<'a, 'b> {} /// An iterator that resolves the top-level properties of a single [`Archived::Object`]. /// /// This iterator will yield `Property` items, which can be either nested objects or primitive values. -/// It is created from an `Archived` object and its associated type table. +/// It is created from an `Archived` object and its associated string table. /// /// It is designed to traverse the properties of an object, allowing you to access nested objects and their properties recursively. /// @@ -194,7 +204,7 @@ impl<'a, 'b: 'a> ExactSizeIterator for PropertyGroupIter<'a, 'b> {} #[derive(Debug, Clone)] pub struct PropertyIterator<'a, 'b> { object_table: &'b [Archived<'a>], - type_table: &'b [TypeEntry<'a>], + string_table: &'b [SharedString<'a>], groups: GroupSource<'a, 'b>, } @@ -205,14 +215,14 @@ 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> { pub(crate) fn new( object_table: &'b [Archived<'a>], - type_table: &'b [TypeEntry<'a>], + string_table: &'b [SharedString<'a>], root_object_index: usize, ) -> Option { let root_object = object_table.get(root_object_index)?; @@ -229,7 +239,7 @@ impl<'a, 'b> PropertyIterator<'a, 'b> { Some(Self { object_table, - type_table, + string_table, groups, }) } @@ -332,7 +342,7 @@ impl<'a, 'b: 'a> Iterator for PropertyIterator<'a, 'b> { Some(Property::Group(PropertyGroup { items, object_table: self.object_table, - type_table: self.type_table, + string_table: self.string_table, })) } } diff --git a/src/deserializer/mod.rs b/src/deserializer/mod.rs index c3e1227..5b86d8a 100644 --- a/src/deserializer/mod.rs +++ b/src/deserializer/mod.rs @@ -2,6 +2,9 @@ pub mod constants; pub mod consumed; +#[cfg(feature = "foundation")] +#[cfg_attr(docsrs, doc(cfg(feature = "foundation")))] +pub mod foundation; pub mod header; pub mod iter; pub mod number; diff --git a/src/deserializer/number.rs b/src/deserializer/number.rs index 9c248b1..b6279e9 100644 --- a/src/deserializer/number.rs +++ b/src/deserializer/number.rs @@ -2,7 +2,7 @@ use crate::{ deserializer::{ - constants::{DECIMAL, END, I_16, I_32, REFERENCE_TAG}, + constants::{DECIMAL, END, I_16, I_32, I_64, REFERENCE_TAG}, consumed::Consumed, read::{read_byte_at, read_exact_bytes}, }, @@ -51,6 +51,12 @@ pub fn read_signed_int(data: &[u8]) -> Result> { let value = i32::from_le_bytes(<[u8; 4]>::try_from(read_exact_bytes(rest, size)?)?); (i64::from(value), size + 1) } + // The number is 8 bytes long + I_64 => { + let size = 8; + let value = i64::from_le_bytes(<[u8; 8]>::try_from(read_exact_bytes(rest, size)?)?); + (value, size + 1) + } // The number is 1 byte long and is the current byte _ => { // If the current byte is greater than the REFERENCE_TAG, it indicates an index in the table of already-seen types. @@ -109,6 +115,12 @@ pub fn read_unsigned_int(data: &[u8]) -> Result> { let value = u32::from_le_bytes(<[u8; 4]>::try_from(read_exact_bytes(rest, size)?)?); (u64::from(value), size + 1) } + // The number is 8 bytes long + I_64 => { + let size = 8; + let value = u64::from_le_bytes(<[u8; 8]>::try_from(read_exact_bytes(rest, size)?)?); + (value, size + 1) + } // The number is 1 byte long _ => { // If the current byte is greater than the REFERENCE_TAG, it indicates an index in the table of already-seen types. @@ -149,7 +161,9 @@ pub fn read_float(data: &[u8]) -> Result> { match *current_byte { DECIMAL => { let size = 4; - let value = f32::from_le_bytes(<[u8; 4]>::try_from(read_exact_bytes(data, size)?)?); + // Skip the DECIMAL tag byte; the IEEE-754 value follows it. + let value = + f32::from_le_bytes(<[u8; 4]>::try_from(read_exact_bytes(&data[1..], size)?)?); Ok(Consumed::new(value, size + 1)) } I_16 | I_32 => Ok(read_signed_int(data)?.map(|v| v as f32)), @@ -179,7 +193,9 @@ pub fn read_double(data: &[u8]) -> Result> { match *current_byte { DECIMAL => { let size = 8; - let value = f64::from_le_bytes(<[u8; 8]>::try_from(read_exact_bytes(data, size)?)?); + // Skip the DECIMAL tag byte; the IEEE-754 value follows it. + let value = + f64::from_le_bytes(<[u8; 8]>::try_from(read_exact_bytes(&data[1..], size)?)?); Ok(Consumed::new(value, size + 1)) } I_16 | I_32 => Ok(read_signed_int(data)?.map(|v| v as f64)), @@ -272,12 +288,48 @@ mod int_tests { assert!(result.is_err()); } + + #[test] + fn can_read_signed_int_64() { + let data = [0x87, 0x00, 0x28, 0x6b, 0xee, 0x00, 0x00, 0x00, 0x00, 0x86]; + let result = read_signed_int(&data).unwrap(); + + assert_eq!(result.value, 4_000_000_000); + assert_eq!(result.bytes_consumed, 9); + } + + #[test] + fn can_read_signed_int_64_negative() { + let data = [0x87, 0x00, 0xe6, 0x8e, 0xe7, 0xfd, 0xff, 0xff, 0xff, 0x86]; + let result = read_signed_int(&data).unwrap(); + + assert_eq!(result.value, -9_000_000_000); + assert_eq!(result.bytes_consumed, 9); + } + + #[test] + fn can_read_unsigned_int_64() { + let data = [0x87, 0x00, 0x34, 0xe2, 0x30, 0x04, 0x00, 0x00, 0x00, 0x86]; + let result = read_unsigned_int(&data).unwrap(); + + assert_eq!(result.value, 18_000_000_000); + assert_eq!(result.bytes_consumed, 9); + } + + #[test] + fn cant_read_signed_int_64_too_short() { + // marker promises 8 bytes but only 4 are present + let data = [0x87, 0x00, 0x28, 0x6b, 0xee]; + let result = read_signed_int(&data); + + assert!(result.is_err()); + } } #[cfg(test)] mod float_tests { use crate::deserializer::{ - constants::{I_16, I_32}, + constants::{DECIMAL, I_16, I_32}, number::{read_double, read_float}, }; @@ -334,4 +386,63 @@ mod float_tests { assert_eq!(result.value, 16843009.); assert_eq!(result.bytes_consumed, 5); } + + #[test] + fn can_read_decimal_float() { + // 0x83 + little-endian 3.5f32, from NSNumber(value: Float(3.5)); 0x86 = END. + let data = [DECIMAL, 0x00, 0x00, 0x60, 0x40, 0x86]; + let result = read_float(&data).unwrap(); + + assert_eq!(result.value, 3.5); + assert_eq!(result.bytes_consumed, 5); + } + + #[test] + fn can_read_decimal_float_one() { + // 0x83 + little-endian 1.0f32 (matches this module's doc example). + let data = [DECIMAL, 0x00, 0x00, 0x80, 0x3F]; + let result = read_float(&data).unwrap(); + + assert_eq!(result.value, 1.0); + assert_eq!(result.bytes_consumed, 5); + } + + #[test] + fn can_read_decimal_double() { + // 0x83 + little-endian 100.5 f64 (exactly representable), from + // NSNumber(value: 100.5); 0x86 = END. + let data = [ + DECIMAL, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x59, 0x40, 0x86, + ]; + let result = read_double(&data).unwrap(); + + assert_eq!(result.value, 100.5); + assert_eq!(result.bytes_consumed, 9); + } + + #[test] + fn can_read_decimal_double_fractional_date() { + // `NSDate(timeIntervalSinceReferenceDate: 700000000.523)`: fractional dates + // archive their double with the DECIMAL tag (integral ones use I_32). + let data = [ + DECIMAL, 0xaa, 0xf1, 0x42, 0x80, 0x93, 0xdc, 0xc4, 0x41, 0x86, + ]; + let result = read_double(&data).unwrap(); + + assert!( + (result.value - 700_000_000.523).abs() < 1e-3, + "got {}", + result.value + ); + assert_eq!(result.bytes_consumed, 9); + } + + #[test] + fn cant_read_decimal_double_too_short() { + // tag promises 8 bytes but only 4 follow + let data = [DECIMAL, 0x9b, 0x91, 0x04, 0x8b]; + let result = read_double(&data); + + assert!(result.is_err()); + } } diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index fea6fc4..4ab4ab0 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -10,43 +10,21 @@ use crate::{ deserializer::{ constants::{EMPTY, END, START}, header::validate_header, - iter::PropertyIterator, + iter::{Property, PropertyIterator, object_property}, number::{read_double, read_float, read_signed_int, read_unsigned_int}, read::{read_byte_at, read_exact_bytes, read_pointer}, string::read_string, }, error::{Result, TypedStreamError}, models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, - types::{Type, TypeEntry}, + shared_string::SharedString, + types::Type, }, }; -/// 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. @@ -60,15 +38,15 @@ pub struct TypedStreamDeserializer<'a> { pub data: &'a [u8], /// The current index we are at in the stream pub(crate) position: usize, - /// As we parse the `typedstream`, build a table of seen [`Type`]s to reference in the future - /// - /// The first time a [`Type`] is seen, it is present in the stream literally, - /// but afterwards are only referenced by index in order of appearance. - pub type_table: Vec>, + /// The shared-string table. `NSArchiver` writes every string once and + /// refers to it by index afterward, and type descriptors, class names, + /// selectors, and `char *` values all share that one index space. This + /// means a reference byte in the stream is an index here, whatever kind of + /// string it names; see [`SharedString`] for how one entry serves as both + /// text and descriptor. + pub string_table: Vec>, /// As we parse the `typedstream`, build a table of seen [`Archived`] data to reference in the future pub object_table: Vec>, - /// We want to copy embedded types the first time they are seen, even if the types were resolved through references - pub(crate) seen_embedded_types: Vec, } impl<'a> TypedStreamDeserializer<'a> { @@ -90,9 +68,8 @@ impl<'a> TypedStreamDeserializer<'a> { Self { data, position: 0, - type_table: Vec::new(), + string_table: Vec::new(), object_table: Vec::new(), - seen_embedded_types: Vec::new(), } } @@ -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 @@ -141,26 +117,21 @@ impl<'a> TypedStreamDeserializer<'a> { // distinct-object-heavy streams); the object table is the only one that // grows large, so the others stay tight. let estimated_size = self.data.len(); - self.type_table + self.string_table .reserve((estimated_size / 64).clamp(16, 256)); self.object_table .reserve((estimated_size / 16).clamp(32, 8192)); - self.seen_embedded_types - .reserve((estimated_size / 128).clamp(8, 64)); // 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()? else { + return Err(TypedStreamError::InvalidObject); + }; + match self.read_types(type_index)?.as_slice().first() { + Some(OutputData::Object(idx)) => Ok(*idx), _ => Err(TypedStreamError::InvalidObject), } } @@ -176,7 +147,8 @@ impl<'a> TypedStreamDeserializer<'a> { /// /// # Errors /// - /// Returns [`TypedStreamError::InvalidPointer`] if the index is not a valid object reference. + /// Returns [`TypedStreamError::OutOfBounds`] if the index is past the object + /// table, or [`TypedStreamError::InvalidObject`] if it is not an object. /// /// # Examples /// @@ -189,8 +161,73 @@ impl<'a> TypedStreamDeserializer<'a> { /// let iter = ts.resolve_properties(root).unwrap(); /// ``` pub fn resolve_properties(&self, root_object_index: usize) -> Result> { - PropertyIterator::new(&self.object_table, &self.type_table, root_object_index) - .ok_or(TypedStreamError::InvalidPointer(root_object_index as u8)) + if root_object_index >= self.object_table.len() { + return Err(TypedStreamError::OutOfBounds( + root_object_index, + self.object_table.len(), + )); + } + PropertyIterator::new(&self.object_table, &self.string_table, root_object_index) + .ok_or(TypedStreamError::InvalidObject) + } + + /// Resolve the object at `object_index` into a group-level [`Property`]. + /// + /// Unlike [`resolve_properties`](Self::resolve_properties) (which iterates an + /// object's *contents*), this returns the object itself, in the form the + /// `foundation` accessors expect. Use it when the value you want is an object + /// (for example the stream's root): `ts.resolve_object(root)?.as_dictionary()`. + /// + /// # Errors + /// + /// Returns [`TypedStreamError::OutOfBounds`] if the index is past the object + /// table, or [`TypedStreamError::InvalidObject`] if it is not an object. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let mut ts = TypedStreamDeserializer::new(&[]); + /// let root = ts.oxidize().unwrap(); + /// + /// // The object as a `Property`, ready for the `foundation` accessors. + /// let object = ts.resolve_object(root).unwrap(); + /// ``` + pub fn resolve_object(&self, object_index: usize) -> Result> { + if object_index >= self.object_table.len() { + return Err(TypedStreamError::OutOfBounds( + object_index, + self.object_table.len(), + )); + } + object_property(&self.object_table, &self.string_table, object_index) + .ok_or(TypedStreamError::InvalidObject) + } + + /// Oxidize the stream and resolve its root object into a [`Property`]. + /// + /// The object-level counterpart of [`iter_root`](Self::iter_root): use it when + /// the stream's root *is* the value you want (e.g. a root `NSDictionary`), + /// rather than a container whose contents you iterate. + /// + /// # Errors + /// + /// Returns a [`TypedStreamError`] if parsing fails or the root is invalid. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let mut ts = TypedStreamDeserializer::new(&[]); + /// + /// // Resolve the root object directly, without a separate `oxidize` call. + /// let root = ts.root().unwrap(); + /// ``` + pub fn root(&mut self) -> Result> { + let root = self.oxidize()?; + self.resolve_object(root) } /// Reads the next byte from the stream, advancing the position. @@ -209,51 +246,58 @@ impl<'a> TypedStreamDeserializer<'a> { Ok(unsigned_int.value) } - /// [`Archivable`] data can be embedded on a class or in a C String marked as [`Type::EmbeddedData`] - fn read_embedded_type(&mut self) -> Result> { - match *self.consume_current_byte()? { - START => { - // 0x84 indicates the start of embedded data - self.read_type(true) - } - EMPTY => Ok(None), - ptr => { - let pointer = read_pointer(&ptr)?.map(|v| v as usize); - if let Some(Archived::Type(idx)) = self.object_table.get(pointer.value) { - Ok(Some(*idx)) - } else { - Err(TypedStreamError::InvalidPointer(pointer.value as u8)) - } - } - } - } - + /// Reads a shared string: a literal, registered as a new entry, or a + /// reference to an existing one. Returns the entry's index. fn read_string(&mut self) -> Result { - let current_byte = *self.consume_current_byte()?; - match current_byte { + match *self.consume_current_byte()? { START => { let string_data = read_string(&self.data[self.position..])?; self.position += string_data.bytes_consumed; - self.type_table - .push(TypeEntry::One(Type::new_string(string_data.value))); - Ok(self.type_table.len() - 1) + Ok(self.register_string(string_data.value)) } EMPTY => Err(TypedStreamError::EmptyString), ptr => { - let pointer = read_pointer(&ptr)?.map(|v| v as usize); - if let Some(Type::String(_)) = self - .type_table - .get(pointer.value) - .and_then(|inner| inner.first()) - { - Ok(pointer.value) + let index = read_pointer(&ptr)?.value as usize; + if index < self.string_table.len() { + Ok(index) } else { - Err(TypedStreamError::InvalidPointer(pointer.value as u8)) + Err(TypedStreamError::InvalidPointer(index as u8)) } } } } + /// The text of shared-string entry `index`, whichever kind of string it is: + /// a type descriptor, a class name, a selector, or a `char *` value. + /// + /// # Examples + /// + /// ```no_run + /// use crabstep::TypedStreamDeserializer; + /// + /// let mut ts = TypedStreamDeserializer::new(&[]); + /// ts.oxidize().unwrap(); + /// assert_eq!(ts.shared_string(0), Some("@")); + /// ``` + #[must_use] + pub fn shared_string(&self, index: usize) -> Option<&'a str> { + self.string_table.get(index).map(|entry| entry.text) + } + + /// Appends a shared string and returns its index. Every registration comes + /// through here, so the table grows in exactly the order `NSArchiver` + /// assigned indices. + /// + /// The entry starts as plain text. Whoever uses it as a descriptor parses + /// it then: `read_type` immediately for a literal, and on the first + /// reference for a name or `char *` value. + fn register_string(&mut self, text: &'a str) -> usize { + let index = self.string_table.len(); + self.string_table.push(SharedString::new(text)); + index + } + + /// Reads a class from the stream, handling nested class definitions. fn read_class(&mut self) -> Result> { // Index of the first START we encounter (the bottom-most child) let mut first_new: Option = None; @@ -316,6 +360,7 @@ impl<'a> TypedStreamDeserializer<'a> { Ok(Some(first_idx)) } + /// Reads an object from the stream, handling its class and associated data. fn read_object(&mut self) -> Result> { match *read_byte_at(self.data, self.position)? { START => { @@ -336,23 +381,18 @@ impl<'a> TypedStreamDeserializer<'a> { && *read_byte_at(self.data, self.position)? != END { // Read the next type, which should be an object - if let Some(next_index) = self.read_type(false)? { + if let Some(next_index) = self.read_type()? { // 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 }; } Ok(Some(placeholder_index)) } - EMPTY => { - self.position += 1; - Ok(None) - } + // A nil reference and a pointer are both one byte; like the `END` + // of an inline object, that byte is left for the caller to consume. + EMPTY => Ok(None), ptr => { let pointer = read_pointer(&ptr)?; Ok(Some(pointer.value as usize)) @@ -361,7 +401,7 @@ impl<'a> TypedStreamDeserializer<'a> { } /// Reads numeric types (signed, unsigned, float, double) and returns the corresponding `OutputData` - fn read_number(&mut self, ty: Type<'a>) -> Result> { + fn read_number(&mut self, ty: Type) -> Result> { match ty { Type::SignedInt => { let signed_int = read_signed_int(&self.data[self.position..])?; @@ -387,13 +427,9 @@ impl<'a> TypedStreamDeserializer<'a> { } } - /// Decodes a single, already-resolved non-embedded type descriptor into one - /// [`OutputData`] value. - /// - /// [`Type::EmbeddedData`] is handled by the caller ([`Self::read_types`]) - /// because it redirects to another type entry rather than producing a value. + /// Decodes one slot of a descriptor into one [`OutputData`] value. #[inline] - fn read_value(&mut self, ty: Type<'a>) -> Result> { + fn read_value(&mut self, ty: Type) -> Result> { match ty { Type::Utf8String => { let str_data = read_string(&self.data[self.position..])?; @@ -408,102 +444,550 @@ impl<'a> TypedStreamDeserializer<'a> { None => OutputData::Null, }) } - Type::String(s) => Ok(OutputData::String(s)), Type::Array(length) => { let array_data = read_exact_bytes(&self.data[self.position..], length)?; self.position += length; Ok(OutputData::Array(array_data)) } - Type::Unknown(byte) => Ok(OutputData::Byte(byte)), + // Selector and atom encoding: one literal, then shared-string + // references. Both are unique by text, so neither takes an + // object-table slot. + Type::Selector | Type::Atom => { + if *read_byte_at(self.data, self.position)? == EMPTY { + self.position += 1; + return Ok(OutputData::Null); + } + let index = self.read_string()?; + Ok(OutputData::String(self.string_table[index].text)) + } + // Track C strings by pointer identity through the object table: + // allocate a slot on `START`, store the shared-string index, and + // reuse an earlier slot for a pointer. + Type::CString => match *self.consume_current_byte()? { + EMPTY => Ok(OutputData::Null), + START => { + let index = self.read_string()?; + self.object_table.push(Archived::CString(index)); + Ok(OutputData::String(self.string_table[index].text)) + } + ptr => { + let slot = read_pointer(&ptr)?.value as usize; + match self.object_table.get(slot) { + Some(Archived::CString(index)) => { + Ok(OutputData::String(self.string_table[*index].text)) + } + _ => Err(TypedStreamError::InvalidPointer(slot as u8)), + } + } + }, + // A class chain, encoded exactly like an object's class header; + // `idx` is the `Archived::Class` entry, not an object. + Type::Class => Ok(match self.read_class()? { + Some(idx) => OutputData::Object(idx), + None => OutputData::Null, + }), // Handle all numeric types Type::SignedInt | Type::UnsignedInt | Type::Float | Type::Double => { self.read_number(ty) } - // `EmbeddedData` is intercepted by `read_types` before reaching here. - Type::EmbeddedData => Err(TypedStreamError::InvalidObject), } } - /// 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> { - if let Some(idx) = self.read_embedded_type()? { - self.position += 1; - self.read_types(idx) - } else { - Ok(Group::Empty) - } - } - - /// Reads all type descriptors at `types_index` into a single data group. - fn read_types(&mut self, types_index: usize) -> Result> { - let len = self.type_table[types_index].len(); + /// Reads one value per slot of the descriptor at `types_index` into a + /// single data group. + fn read_types(&mut self, types_index: usize) -> Result> { + // `read_type` parsed the entry before handing over its index; `Type` is + // `Copy`, so each slot is fetched without holding the table borrow. + let slot = |ts: &Self, i: usize| -> Result { + let parsed = ts.string_table[types_index] + .parsed() + .ok_or(TypedStreamError::InvalidObject)?; + Ok(parsed[i]) + }; + let len = self.string_table[types_index] + .parsed() + .ok_or(TypedStreamError::InvalidObject)? + .len(); // Common case: a single descriptor decodes to a single value with no Vec. if len == 1 { - let ty = self.type_table[types_index][0]; - return if matches!(ty, Type::EmbeddedData) { - self.read_embedded() - } else { - Ok(Group::One(self.read_value(ty)?)) - }; + let ty = slot(self, 0)?; + return Ok(DataGroup::One(self.read_value(ty)?)); } let mut out_v = Vec::with_capacity(len); for i in 0..len { - let ty = self.type_table[types_index][i]; - if matches!(ty, Type::EmbeddedData) { - return self.read_embedded(); - } + let ty = slot(self, i)?; out_v.push(self.read_value(ty)?); } - - Ok(Group::Many(out_v)) + Ok(DataGroup::Values(out_v)) } - /// Gets the current type from the stream, either by reading it from the stream or reading it from - /// the specified index of [`Self::type_table`]. Returns an index into the types table - /// to avoid cloning large type vectors. - fn read_type(&mut self, is_embedded_type: bool) -> Result> { - let byte = *self.consume_current_byte()?; - - match byte { + /// Reads a type descriptor: a literal, registered as a new shared string, or + /// a reference to an existing one. Returns its index in + /// [`string_table`](Self::string_table), or `None` at an [`END`]/[`EMPTY`] marker + /// where a descriptor was optional. + /// + /// A reference may resolve to an entry that was registered as a name or + /// `char *` value rather than as a descriptor. `NSValue` does exactly this: + /// it writes its `objCType` as a `char *`, then references that same string + /// as the descriptor for the value that follows. Since the entry keeps its + /// text, the deserializer parses the descriptor view on that first use and + /// the entry serves both roles from then on. + fn read_type(&mut self) -> Result> { + let index = match *self.consume_current_byte()? { START => { - // Get the type of the object - let new_types = Type::read_new_type(&self.data[self.position..])?; - let new_type_index = self.type_table.len(); - // Embedded data is stored as a Type in the objects table - if is_embedded_type { - self.object_table.push(Archived::Type(new_type_index)); - // We only want to include the first embedded reference tag, not subsequent references to the same embed - self.seen_embedded_types - .push(self.object_table.len().saturating_sub(1)); - } - - self.type_table.push(new_types.value); - self.position += new_types.bytes_consumed; - Ok(Some(self.type_table.len() - 1)) + let literal = read_string(&self.data[self.position..])?; + self.position += literal.bytes_consumed; + self.register_string(literal.value) } - END | EMPTY => Ok(None), + END | EMPTY => return Ok(None), ptr => { - let pointer = read_pointer(&ptr)?; - let ref_tag = pointer.value as usize; - - // Optimize bounds checking - if ref_tag >= self.type_table.len() { - return Ok(None); + let index = read_pointer(&ptr)?.value as usize; + if index >= self.string_table.len() { + return Err(TypedStreamError::InvalidPointer(index as u8)); } + index + } + }; + // A literal is parsed here, before any value is read with it, so a + // malformed descriptor fails at its own bytes. A reference to a name or + // `char *` value is parsed here too, on this first use as a descriptor. + let remaining = self.data.len() - self.position; + self.string_table[index].descriptor(remaining)?; + Ok(Some(index)) + } +} - if is_embedded_type { - // We only want to include the first embedded reference tag, not subsequent references to the same embed - if !self.seen_embedded_types.contains(&ref_tag) { - self.object_table.push(Archived::Type(ref_tag)); - self.seen_embedded_types.push(ref_tag); - } - } +#[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, + types::{Type, TypeEntry}, + }, + }; + + 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 + } - Ok(Some(ref_tag)) + fn unsigned(property: Property<'_, '_>) -> u64 { + let Property::Primitive(value) = property else { + panic!("expected a primitive"); + }; + value.as_u64().unwrap() + } + + #[test] + 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, + // A nil `char *` value: one `NULL` byte and one group holding `Null`. + 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(), 5); + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::UnsignedInteger(1)), + 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 properties.by_ref().take(4) { + 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]); + + 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] + 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 nil_object_reference_consumes_one_byte() { + // `@` + EMPTY is a one-byte nil. The slot after it must still line up: + // consuming a second byte would swallow the `7`. + let bytes = stream(&[START, 2, b'@', b'C', EMPTY, 7]); + 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, + &ObjectData::Groups(vec![DataGroup::Values(vec![ + OutputData::Null, + OutputData::UnsignedInteger(7), + ])]) + ); + assert_eq!(ts.position, bytes.len()); + } + + #[test] + fn atoms_are_shared_strings_without_object_slots() { + // The layout NSArchiver writes for `%`: a literal, then a reference by + // string index, then NULL. Descriptor `%%` is entry 2 and `atom` entry + // 3 (tag 0x95). Unlike a `char *`, no object-table slot is taken. + let bytes = stream(&[ + START, 2, b'%', b'%', START, 4, b'a', b't', b'o', b'm', 0x95, 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, + &ObjectData::Groups(vec![ + DataGroup::Values(vec![OutputData::String("atom"), OutputData::String("atom"),]), + DataGroup::One(OutputData::Null), + ]) + ); + assert!( + !ts.object_table + .iter() + .any(|o| matches!(o, Archived::CString(_))) + ); + assert_eq!(ts.position, bytes.len()); + } + + #[test] + fn selectors_are_shared_strings() { + // Descriptor `::` is string-table entry 2, the literal `quit:` entry 3, + // so the second selector references it with tag 0x92 + 3. + let bytes = stream(&[ + START, 2, b':', b':', START, 5, b'q', b'u', b'i', b't', b':', 0x95, 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, + &ObjectData::Groups(vec![ + DataGroup::Values(vec![ + OutputData::String("quit:"), + OutputData::String("quit:"), + ]), + DataGroup::One(OutputData::Null), + ]) + ); + assert_eq!(ts.position, bytes.len()); + } + + #[test] + fn class_references_read_a_class_chain() { + // `#` then a class header: name, version, superclass terminator. + let bytes = stream(&[ + START, 2, b'#', b'#', START, START, 3, b'F', b'o', b'o', 2, EMPTY, EMPTY, + ]); + 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 [DataGroup::Values(values)] = groups.as_slice() else { + panic!("expected one two-slot group"); + }; + let [OutputData::Object(class_idx), OutputData::Null] = values.as_slice() else { + panic!("expected a class reference and a nil class, got {values:?}"); + }; + let Archived::Class(class) = &ts.object_table[*class_idx] else { + panic!("`#` must reference a class entry"); + }; + assert_eq!(ts.string_table[class.name_index].text, "Foo"); + assert_eq!(class.version, 2); + assert_eq!(class.parent_index, None); + assert_eq!(ts.position, bytes.len()); + } + + #[test] + fn aggregates_decode_flat_in_stream() { + use OutputData::{Array as A, Double as D, SignedInteger as I, UnsignedInteger as U}; + // Integral doubles are written as one-byte ints, as NSArchiver does. + for (descriptor, values, expected) in [ + (&b"B"[..], &[1u8][..], vec![U(1)]), + (b"{_NSRange=QQ}", &[3, 4], vec![U(3), U(4)]), + (b"i{CGSize=dd}", &[7, 1, 2], vec![I(7), D(1.0), D(2.0)]), + (b"[2i]", &[5, 6], vec![I(5), I(6)]), + (b"[2[2c]]", b"abcd", vec![A(b"ab"), A(b"cd")]), + (b"i[2c]i", &[1, b'x', b'y', 2], vec![I(1), A(b"xy"), I(2)]), + ] { + let mut group = vec![START, u8::try_from(descriptor.len()).unwrap()]; + group.extend_from_slice(descriptor); + group.extend_from_slice(values); + let bytes = stream(&group); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("expected an object"); + }; + let got = match data { + ObjectData::Inline(value) => core::slice::from_ref(value), + ObjectData::Groups(groups) => groups[0].as_slice(), + ObjectData::Empty => panic!("no data"), + }; + assert_eq!( + got, + expected.as_slice(), + "{}", + core::str::from_utf8(descriptor).unwrap() + ); + assert_eq!(ts.position, bytes.len()); + } + } + + #[test] + fn unencodable_descriptor_is_an_error_not_a_desync() { + let bytes = stream(&[START, 2, b'^', b'i', 0]); + let mut ts = TypedStreamDeserializer::new(&bytes); + assert!(matches!( + ts.oxidize(), + Err(crate::error::TypedStreamError::InvalidType(b'^')) + )); + } + + #[test] + fn c_strings_share_by_pointer_through_the_object_table() { + // `stream()` prefix: object-table slots [0] (root), [1] (class), [2] + // (first C string, tag `0x94`). Shared-string entries: [0] `@`, [1] `X`, + // [2] first group descriptor, [3] C-string literal (`0x95`). + use DataGroup::{One, Values}; + use OutputData::{Null, SignedInteger as I, String as S, UnsignedInteger as U}; + for (groups, expected, c_string_slots) in [ + // Literal C string: allocate a slot, then read the shared string. + ( + vec![START, 2, b'C', b'*', 1, START, START, 2, b'h', b'i'], + vec![Values(vec![U(1), S("hi")])], + 1, + ), + ( + vec![START, 2, b'*', b'C', START, START, 2, b'h', b'i', 2], + vec![Values(vec![S("hi"), U(2)])], + 1, + ), + // `NULL`: one byte; no object-table slot. + ( + vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], + vec![Values(vec![U(1), Null, U(3)])], + 0, + ), + // Repeated pointer: a bare reference to the existing slot. + ( + vec![START, 1, b'*', START, START, 2, b'h', b'i', 0x94, 0x94], + vec![One(S("hi")), One(S("hi"))], + 1, + ), + // Distinct pointer, same text: a new object-table slot and the same + // string-table entry. + ( + vec![ + START, 1, b'*', START, START, 2, b'h', b'i', 0x94, START, 0x95, + ], + vec![One(S("hi")), One(S("hi"))], + 2, + ), + // Existing descriptor text (`i`): reuse its string-table entry for + // the C-string value. + ( + vec![START, 1, b'i', 7, START, 1, b'*', START, 0x94], + vec![One(I(7)), One(S("i"))], + 1, + ), + ] { + let bytes = stream(&groups); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { + data: ObjectData::Groups(got), + .. + } = &ts.object_table[root] + else { + panic!("expected grouped data for {groups:?}"); + }; + assert_eq!(got, &expected, "{groups:?}"); + assert_eq!( + ts.object_table + .iter() + .filter(|o| matches!(o, Archived::CString(_))) + .count(), + c_string_slots, + "{groups:?}" + ); + assert_eq!(ts.position, bytes.len(), "{groups:?}"); + } + } + + #[test] + fn c_string_text_types_the_value_that_follows() { + // `NSValue` layout: record `objCType` through `*`, then reuse that + // string to type the following value. + let bytes = stream(&[START, 1, b'*', START, START, 1, b'q', 0x95, 5]); + 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, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::String("q")), + DataGroup::One(OutputData::SignedInteger(5)), + ]) + ); + // Entry [3]: register the value string first; parse it in place when the + // descriptor references it, while preserving the original text for + // `shared_string`. + assert_eq!(ts.shared_string(3), Some("q")); + assert_eq!(ts.string_table[3].text, "q"); + assert_eq!( + ts.string_table[3].parsed(), + Some(&TypeEntry::One(Type::SignedInt)) + ); + assert_eq!(ts.object_table[2], Archived::CString(3)); + assert_eq!(ts.position, bytes.len()); + } + + #[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::Values(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/error.rs b/src/error.rs index 92be42e..10a14b4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,6 +36,8 @@ pub enum TypedStreamError { InvalidArray(usize), /// Encountered an empty string where data was expected. EmptyString, + /// A byte in a type descriptor that `NSArchiver` never writes. + InvalidType(u8), } impl Display for TypedStreamError { @@ -62,6 +64,13 @@ impl Display for TypedStreamError { write!(f, "Invalid array at index: {offset:x}") } TypedStreamError::EmptyString => write!(f, "Empty string encountered in typedstream"), + TypedStreamError::InvalidType(byte) => { + write!( + f, + "Invalid type encoding byte: {:?} (0x{byte:02x})", + *byte as char + ) + } } } } diff --git a/src/lib.rs b/src/lib.rs index 61269ac..ed6dfa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ #![deny(missing_docs)] #![doc = include_str!("../README.md")] #![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] extern crate alloc; #[cfg(feature = "std")] extern crate std; @@ -10,29 +11,56 @@ pub mod deserializer; pub mod error; pub mod models; -pub use deserializer::{iter::PropertyIterator, typedstream::TypedStreamDeserializer}; +pub use deserializer::{ + iter::{Property, PropertyIterator}, + typedstream::TypedStreamDeserializer, +}; pub use models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, output_data::OutputData, + shared_string::SharedString, types::TypeEntry, }; #[cfg(test)] mod test_typedstream_deserializer { extern crate std; - use alloc::vec; + use alloc::{vec, vec::Vec}; use std::{env::current_dir, fs::File, io::Read, println}; use crate::{ deserializer::{iter::print_resolved, typedstream::TypedStreamDeserializer}, models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, - types::{Type, TypeEntry}, }, }; + #[test] + fn resolve_object_and_properties_reject_out_of_bounds() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/AttributedBodyTextOnly"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + ts.oxidize().unwrap(); + + // An index past the object table reports the full index via OutOfBounds, + // not a `usize`-to-`u8`-truncated InvalidPointer. + assert!(matches!( + ts.resolve_object(9999), + Err(crate::error::TypedStreamError::OutOfBounds(9999, _)) + )); + assert!(matches!( + ts.resolve_properties(9999), + Err(crate::error::TypedStreamError::OutOfBounds(9999, _)) + )); + } + #[test] fn test_parse_text_iter() { let typedstream_path = current_dir() @@ -55,6 +83,8 @@ mod test_typedstream_deserializer { &OutputData::UnsignedInteger(10), &OutputData::SignedInteger(1), &OutputData::SignedInteger(0), + // `NSNumber` archive: `objCType` alongside the value. + &OutputData::String("i"), &OutputData::String("__kIMMessagePartAttributeName"), &OutputData::SignedInteger(1) ] @@ -77,9 +107,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -91,20 +121,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -169,7 +199,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -181,10 +214,10 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -204,9 +237,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -218,19 +251,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -284,7 +317,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -296,7 +332,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -305,11 +341,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -326,9 +365,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -340,25 +379,25 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(649)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", + "NSURL", + "c", + "NSData", + "[649c]", ]; let expected_objects = vec![ @@ -493,7 +532,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -505,7 +547,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -514,9 +556,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -547,7 +592,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 6, @@ -758,7 +806,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(5)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(5)], + ]), }, Archived::Object { class: 8, @@ -774,7 +825,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(11)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(11)], + ]), }, Archived::Object { class: 8, @@ -790,7 +844,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(9)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(9)], + ]), }, Archived::Object { class: 8, @@ -806,7 +863,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(8)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(8)], + ]), }, Archived::Object { class: 8, @@ -822,7 +882,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(12)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(12)], + ]), }, Archived::Object { class: 8, @@ -838,7 +901,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(4)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(4)], + ]), }, Archived::Object { class: 8, @@ -854,7 +920,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(6)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(6)], + ]), }, Archived::Object { class: 8, @@ -870,11 +939,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(10)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(10)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!( typedstream.object_table[..expected_objects.len()], expected_objects @@ -897,9 +969,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -911,23 +983,23 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSMutableData")]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(635)]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSMutableData", + "NSData", + "[635c]", + "NSNumber", + "NSValue", + "*", + "NSURL", + "c", ]; let expected_objects = vec![ @@ -1047,7 +1119,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 11, @@ -1059,7 +1134,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1090,7 +1165,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 6, @@ -1114,7 +1192,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -1134,9 +1212,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -1148,25 +1226,25 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(675)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", + "c", + "NSURL", + "NSData", + "[675c]", ]; let expected_objects = vec![ @@ -1241,7 +1319,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -1253,7 +1334,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1262,9 +1343,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("c")], + vec![OutputData::SignedInteger(1)], + ]), }, - Archived::Type(14), + Archived::CString(14), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1297,7 +1381,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 6, @@ -1359,7 +1446,7 @@ mod test_typedstream_deserializer { }), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -1379,9 +1466,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -1393,19 +1480,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -1473,7 +1560,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -1485,7 +1575,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1494,11 +1584,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -1515,9 +1608,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -1529,22 +1622,22 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(904)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "NSData", + "[904c]", ]; let expected_objects = vec![ @@ -1617,7 +1710,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -1629,7 +1725,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 8, data: ObjectData::from_groups(vec![ @@ -1712,7 +1808,7 @@ mod test_typedstream_deserializer { }), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -1732,9 +1828,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -1746,22 +1842,22 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::Double]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "d", + "q", ]; let expected_objects = vec![ @@ -1852,7 +1948,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::Double(1139.0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("d")], + vec![OutputData::Double(1139.0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -1864,7 +1963,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1873,9 +1972,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, - Archived::Type(14), + Archived::CString(14), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1884,9 +1986,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -1907,7 +2012,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::Double(952.0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("d")], + vec![OutputData::Double(952.0)], + ]), }, Archived::Object { class: 8, @@ -1933,11 +2041,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(1)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -1957,9 +2068,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -1971,21 +2082,21 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -2069,7 +2180,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(600)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(600)], + ]), }, Archived::Class(Class { name_index: 10, @@ -2081,7 +2195,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -2090,7 +2204,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Object { class: 6, @@ -2100,7 +2217,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 6, @@ -2122,11 +2242,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(660)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(660)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2143,9 +2266,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2157,13 +2280,7 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - ]; + let expected_types = ["@", "NSAttributedString", "NSObject", "NSString", "+"]; let expected_objects = vec![ Archived::Object { @@ -2191,7 +2308,7 @@ mod test_typedstream_deserializer { }), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2208,9 +2325,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2222,22 +2339,22 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(535)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "NSData", + "[535c]", ]; let expected_objects = vec![ @@ -2310,7 +2427,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -2322,7 +2442,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -2410,7 +2530,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2430,9 +2550,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2444,19 +2564,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -2517,7 +2637,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -2529,7 +2652,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -2538,7 +2661,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 6, @@ -2566,11 +2692,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(1)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2587,9 +2716,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2601,23 +2730,23 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::String("NSMutableData")]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(669)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "NSMutableData", + "NSData", + "[669c]", ]; let expected_objects = vec![ @@ -2687,7 +2816,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -2699,7 +2831,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 8, data: ObjectData::from_groups(vec![ @@ -2780,7 +2912,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2800,9 +2932,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2814,18 +2946,18 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -2879,7 +3011,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -2891,7 +3026,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -2900,11 +3035,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -2921,9 +3059,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -2935,23 +3073,23 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSMutableData")]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(667)]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSMutableData", + "NSData", + "[667c]", + "NSNumber", + "NSValue", + "*", + "NSURL", + "c", ]; let expected_objects = vec![ @@ -3079,7 +3217,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 11, @@ -3091,7 +3232,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -3132,7 +3273,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3152,9 +3293,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -3166,18 +3307,18 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3236,7 +3377,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 8, @@ -3248,7 +3392,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 6, data: ObjectData::from_groups(vec![ @@ -3267,7 +3411,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 4, @@ -3293,7 +3440,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3329,9 +3476,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -3343,19 +3490,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -3409,7 +3556,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -3421,7 +3571,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -3430,11 +3580,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3454,9 +3607,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -3468,20 +3621,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3548,7 +3701,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -3560,10 +3716,10 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3583,9 +3739,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -3597,20 +3753,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3675,7 +3831,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -3687,7 +3846,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 8, data: ObjectData::from_groups(vec![ @@ -3716,7 +3875,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3736,9 +3895,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -3750,21 +3909,21 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -3898,7 +4057,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -3910,7 +4072,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -3919,7 +4081,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 6, @@ -3947,7 +4112,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 6, @@ -3981,7 +4149,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(2)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(2)], + ]), }, Archived::Object { class: 6, @@ -4003,7 +4174,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(3)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(3)], + ]), }, Archived::Object { class: 6, @@ -4025,7 +4199,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(4)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(4)], + ]), }, Archived::Object { class: 6, @@ -4047,7 +4224,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(5)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(5)], + ]), }, Archived::Object { class: 6, @@ -4069,7 +4249,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(6)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(6)], + ]), }, Archived::Object { class: 6, @@ -4091,7 +4274,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(7)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(7)], + ]), }, Archived::Object { class: 6, @@ -4113,7 +4299,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(8)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(8)], + ]), }, Archived::Object { class: 6, @@ -4135,7 +4324,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(9)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(9)], + ]), }, Archived::Object { class: 6, @@ -4157,7 +4349,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(10)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(10)], + ]), }, Archived::Object { class: 6, @@ -4179,7 +4374,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(11)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(11)], + ]), }, Archived::Object { class: 6, @@ -4201,7 +4399,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(12)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(12)], + ]), }, Archived::Object { class: 6, @@ -4223,7 +4424,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(13)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(13)], + ]), }, Archived::Object { class: 6, @@ -4245,7 +4449,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(14)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(14)], + ]), }, Archived::Object { class: 6, @@ -4267,7 +4474,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(15)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(15)], + ]), }, Archived::Object { class: 6, @@ -4289,7 +4499,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(16)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(16)], + ]), }, Archived::Object { class: 6, @@ -4311,7 +4524,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(17)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(17)], + ]), }, Archived::Object { class: 6, @@ -4333,7 +4549,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(18)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(18)], + ]), }, Archived::Object { class: 6, @@ -4343,7 +4562,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -4363,9 +4582,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -4377,20 +4596,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -4478,7 +4697,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -4490,7 +4712,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 8, data: ObjectData::from_groups(vec![ @@ -4507,7 +4729,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 8, @@ -4527,7 +4752,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(2)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(2)], + ]), }, Archived::Object { class: 8, @@ -4539,7 +4767,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(3)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(3)], + ]), }, Archived::Object { class: 8, @@ -4559,7 +4790,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(4)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(4)], + ]), }, Archived::Object { class: 8, @@ -4571,11 +4805,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(5)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(5)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -4595,9 +4832,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -4609,20 +4846,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -4696,7 +4933,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -4708,7 +4948,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 8, data: ObjectData::from_groups(vec![ @@ -4739,7 +4979,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Object { class: 8, @@ -4757,11 +5000,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(2)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(2)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -4781,9 +5027,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -4795,23 +5041,23 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(820)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", + "NSData", + "[820c]", ]; let expected_objects = vec![ @@ -4886,7 +5132,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -4898,7 +5147,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -4907,9 +5156,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -4918,7 +5170,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Object { class: 6, @@ -5039,7 +5294,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -5059,9 +5314,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -5073,25 +5328,25 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(649)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", + "NSURL", + "c", + "NSData", + "[649c]", ]; let expected_objects = vec![ @@ -5165,7 +5420,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -5177,7 +5435,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -5186,7 +5444,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 8, @@ -5282,7 +5543,7 @@ mod test_typedstream_deserializer { }), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -5302,9 +5563,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -5316,25 +5577,25 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(591)]), - TypeEntry::from_types(vec![Type::String("NSURL")]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", + "c", + "NSData", + "[591c]", + "NSURL", ]; let expected_objects = vec![ @@ -5411,7 +5672,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 10, @@ -5423,7 +5687,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(13), + Archived::CString(13), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -5432,9 +5696,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("c")], + vec![OutputData::SignedInteger(0)], + ]), }, - Archived::Type(14), + Archived::CString(14), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -5496,7 +5763,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 6, @@ -5506,7 +5776,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(5)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(5)], + ]), }, Archived::Object { class: 6, @@ -5534,7 +5807,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -5554,9 +5827,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -5568,19 +5841,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -5656,7 +5929,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(5)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(5)], + ]), }, Archived::Class(Class { name_index: 8, @@ -5668,7 +5944,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -5677,9 +5953,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 6, data: ObjectData::from_groups(vec![ @@ -5706,7 +5985,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(11)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(11)], + ]), }, Archived::Object { class: 6, @@ -5720,7 +6002,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(9)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(9)], + ]), }, Archived::Object { class: 6, @@ -5734,7 +6019,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(8)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(8)], + ]), }, Archived::Object { class: 6, @@ -5748,7 +6036,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(12)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(12)], + ]), }, Archived::Object { class: 6, @@ -5762,7 +6053,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(4)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(4)], + ]), }, Archived::Object { class: 6, @@ -5776,7 +6070,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(6)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(6)], + ]), }, Archived::Object { class: 6, @@ -5790,11 +6087,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(10)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(10)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -5814,9 +6114,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -5828,18 +6128,18 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -5911,7 +6211,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -5923,7 +6226,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -5932,9 +6235,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), - }, - Archived::Object { + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Object { class: 6, data: ObjectData::from_groups(vec![ vec![OutputData::SignedInteger(1)], @@ -6014,7 +6320,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6034,9 +6340,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6048,19 +6354,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -6123,7 +6429,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -6135,7 +6444,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -6144,7 +6453,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 4, @@ -6154,9 +6466,12 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 6, data: ObjectData::from_groups(vec![ @@ -6187,11 +6502,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(10)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(10)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6211,9 +6529,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6225,18 +6543,18 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -6299,7 +6617,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -6311,7 +6632,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -6326,7 +6647,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 9, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Object { class: 4, @@ -6342,7 +6666,7 @@ mod test_typedstream_deserializer { }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6362,9 +6686,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6376,19 +6700,19 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -6468,7 +6792,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Class(Class { name_index: 8, @@ -6480,7 +6807,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(11), + Archived::CString(11), Archived::Object { class: 4, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -6489,11 +6816,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 13, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6510,9 +6840,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6524,25 +6854,25 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(582)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSURL", + "c", + "NSNumber", + "NSValue", + "*", + "q", + "NSData", + "[582c]", ]; let expected_objects = vec![ @@ -6639,7 +6969,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 15, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 12, @@ -6651,7 +6984,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(15), + Archived::CString(15), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -6660,7 +6993,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 15, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, Archived::Object { class: 6, @@ -6716,7 +7052,7 @@ mod test_typedstream_deserializer { }), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6736,9 +7072,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6750,26 +7086,26 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSURL")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSMutableData")]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(604)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSURL", + "c", + "NSNumber", + "NSValue", + "*", + "q", + "NSMutableData", + "NSData", + "[604c]", ]; let expected_objects = vec![ @@ -6866,7 +7202,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 15, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 12, @@ -6878,7 +7217,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(15), + Archived::CString(15), Archived::Object { class: 6, data: ObjectData::from_groups(vec![vec![OutputData::String( @@ -6945,11 +7284,14 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 15, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(-1)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("q")], + vec![OutputData::SignedInteger(-1)], + ]), }, ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -6969,9 +7311,9 @@ mod test_typedstream_deserializer { println!("Root object: {:x?}", typedstream.object_table[root]); print_resolved(typedstream.resolve_properties(root).unwrap(), 2); - println!("\nFound {:?} types:", typedstream.type_table.len()); + println!("\nFound {:?} types:", typedstream.string_table.len()); typedstream - .type_table + .string_table .iter() .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); @@ -6983,20 +7325,20 @@ mod test_typedstream_deserializer { .enumerate() .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); - let expected_types = vec![ - TypeEntry::from_types(vec![Type::Object]), - TypeEntry::from_types(vec![Type::String("NSMutableAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSAttributedString")]), - TypeEntry::from_types(vec![Type::String("NSObject")]), - TypeEntry::from_types(vec![Type::String("NSMutableString")]), - TypeEntry::from_types(vec![Type::String("NSString")]), - TypeEntry::from_types(vec![Type::Utf8String]), - TypeEntry::from_types(vec![Type::SignedInt, Type::UnsignedInt]), - TypeEntry::from_types(vec![Type::String("NSDictionary")]), - TypeEntry::from_types(vec![Type::SignedInt]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::EmbeddedData]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -7061,7 +7403,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 11, - data: ObjectData::from_groups(vec![vec![OutputData::SignedInteger(0)]]), + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), }, Archived::Class(Class { name_index: 10, @@ -7073,10 +7418,10 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -7103,4 +7448,1523 @@ mod test_typedstream_deserializer { let primitives = root_obj.primitives(); println!("\nPrimitive Values: {primitives:?}"); } + + #[test] + fn test_parse_text_address() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/Address"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSMutableData", + "NSData", + "[1187c]", + "NSNumber", + "NSValue", + "*", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(3)], + vec![ + OutputData::SignedInteger(1), + OutputData::UnsignedInteger(37), + ], + vec![OutputData::Object(5)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "1 Apple Park Way, Cupertino, CA 95014", + )]]), + }, + Archived::Class(Class { + name_index: 3, + version: 1, + parent_index: Some(2), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(7)], + vec![OutputData::Object(8)], + vec![OutputData::Object(11)], + vec![OutputData::Object(12)], + ]), + }, + Archived::Class(Class { + name_index: 6, + version: 0, + parent_index: Some(2), + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMAddressAttributeName", + )]]), + }, + Archived::Object { + class: 9, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(1187)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 46, 128, 1, 175, + 16, 47, 13, 14, 28, 36, 37, 38, 44, 45, 46, 54, 60, 64, 65, 66, 67, 72, 78, + 26, 82, 83, 84, 87, 90, 94, 100, 104, 105, 106, 107, 108, 114, 118, 119, + 120, 121, 122, 128, 132, 133, 134, 135, 141, 145, 146, 147, 148, 149, 85, + 36, 110, 117, 108, 108, 215, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 26, 82, 77, 83, 86, 36, 99, 108, 97, 115, 115, 82, 65, 82, 81, 84, + 81, 80, 82, 83, 82, 82, 86, 78, 128, 6, 128, 22, 128, 2, 128, 7, 16, 1, + 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, 35, 95, 16, 18, 78, 83, 46, 114, + 97, 110, 103, 101, 118, 97, 108, 46, 108, 101, 110, 103, 116, 104, 95, 16, + 20, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, 111, 99, 97, + 116, 105, 111, 110, 90, 78, 83, 46, 115, 112, 101, 99, 105, 97, 108, 128, + 3, 128, 4, 16, 4, 128, 5, 16, 37, 16, 0, 210, 39, 40, 41, 42, 90, 36, 99, + 108, 97, 115, 115, 110, 97, 109, 101, 88, 36, 99, 108, 97, 115, 115, 101, + 115, 87, 78, 83, 86, 97, 108, 117, 101, 162, 41, 43, 88, 78, 83, 79, 98, + 106, 101, 99, 116, 95, 16, 37, 49, 32, 65, 112, 112, 108, 101, 32, 80, 97, + 114, 107, 32, 87, 97, 121, 44, 32, 67, 117, 112, 101, 114, 116, 105, 110, + 111, 44, 32, 67, 65, 32, 57, 53, 48, 49, 52, 91, 70, 117, 108, 108, 65, + 100, 100, 114, 101, 115, 115, 210, 47, 16, 48, 53, 90, 78, 83, 46, 111, 98, + 106, 101, 99, 116, 115, 164, 49, 50, 51, 52, 128, 9, 128, 29, 128, 35, 128, + 40, 128, 21, 215, 15, 16, 17, 18, 19, 20, 21, 55, 23, 57, 58, 26, 59, 26, + 128, 12, 128, 22, 128, 10, 128, 13, 128, 14, 212, 29, 30, 31, 16, 61, 33, + 34, 35, 128, 11, 128, 4, 128, 5, 16, 16, 95, 16, 16, 49, 32, 65, 112, 112, + 108, 101, 32, 80, 97, 114, 107, 32, 87, 97, 121, 86, 83, 116, 114, 101, + 101, 116, 210, 47, 16, 68, 53, 162, 69, 70, 128, 15, 128, 23, 128, 21, 215, + 15, 16, 17, 18, 19, 20, 21, 73, 23, 75, 76, 26, 77, 26, 128, 18, 128, 22, + 128, 16, 128, 19, 128, 20, 212, 29, 30, 31, 16, 79, 33, 34, 35, 128, 17, + 128, 4, 128, 5, 81, 49, 92, 83, 116, 114, 101, 101, 116, 78, 117, 109, 98, + 101, 114, 210, 47, 16, 85, 53, 160, 128, 21, 210, 39, 40, 88, 89, 87, 78, + 83, 65, 114, 114, 97, 121, 162, 88, 43, 210, 39, 40, 91, 92, 95, 16, 15, + 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 162, + 93, 43, 95, 16, 15, 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, + 117, 108, 116, 215, 15, 16, 17, 18, 19, 20, 21, 95, 23, 97, 98, 26, 77, 26, + 128, 27, 128, 22, 128, 24, 128, 28, 128, 20, 212, 29, 30, 31, 16, 101, 102, + 34, 35, 128, 25, 128, 26, 128, 5, 16, 14, 16, 2, 94, 65, 112, 112, 108, + 101, 32, 80, 97, 114, 107, 32, 87, 97, 121, 90, 83, 116, 114, 101, 101, + 116, 78, 97, 109, 101, 215, 15, 16, 17, 18, 19, 20, 21, 109, 23, 111, 112, + 26, 77, 26, 128, 33, 128, 22, 128, 30, 128, 34, 128, 20, 212, 29, 30, 31, + 16, 115, 116, 34, 35, 128, 31, 128, 32, 128, 5, 16, 9, 16, 18, 89, 67, 117, + 112, 101, 114, 116, 105, 110, 111, 84, 67, 105, 116, 121, 215, 15, 16, 17, + 18, 19, 20, 21, 123, 23, 125, 126, 26, 77, 26, 128, 38, 128, 22, 128, 36, + 128, 39, 128, 20, 212, 29, 30, 31, 16, 102, 130, 34, 35, 128, 26, 128, 37, + 128, 5, 16, 29, 82, 67, 65, 85, 83, 116, 97, 116, 101, 215, 15, 16, 17, 18, + 19, 20, 21, 136, 23, 138, 139, 26, 77, 26, 128, 44, 128, 22, 128, 41, 128, + 45, 128, 20, 212, 29, 30, 31, 16, 142, 143, 34, 35, 128, 42, 128, 43, 128, + 5, 16, 5, 16, 32, 85, 57, 53, 48, 49, 52, 87, 90, 105, 112, 67, 111, 100, + 101, 16, 1, 0, 8, 0, 17, 0, 26, 0, 36, 0, 41, 0, 50, 0, 55, 0, 73, 0, 78, + 0, 86, 0, 96, 0, 98, 0, 100, 0, 150, 0, 156, 0, 171, 0, 174, 0, 181, 0, + 184, 0, 186, 0, 188, 0, 191, 0, 194, 0, 196, 0, 198, 0, 200, 0, 202, 0, + 204, 0, 206, 0, 215, 0, 236, 1, 3, 1, 14, 1, 16, 1, 18, 1, 20, 1, 22, 1, + 24, 1, 26, 1, 31, 1, 42, 1, 51, 1, 59, 1, 62, 1, 71, 1, 111, 1, 123, 1, + 128, 1, 139, 1, 144, 1, 146, 1, 148, 1, 150, 1, 152, 1, 154, 1, 169, 1, + 171, 1, 173, 1, 175, 1, 177, 1, 179, 1, 188, 1, 190, 1, 192, 1, 194, 1, + 196, 1, 215, 1, 222, 1, 227, 1, 230, 1, 232, 1, 234, 1, 236, 1, 251, 1, + 253, 1, 255, 2, 1, 2, 3, 2, 5, 2, 14, 2, 16, 2, 18, 2, 20, 2, 22, 2, 35, 2, + 40, 2, 41, 2, 43, 2, 48, 2, 56, 2, 59, 2, 64, 2, 82, 2, 85, 2, 103, 2, 118, + 2, 120, 2, 122, 2, 124, 2, 126, 2, 128, 2, 137, 2, 139, 2, 141, 2, 143, 2, + 145, 2, 147, 2, 162, 2, 173, 2, 188, 2, 190, 2, 192, 2, 194, 2, 196, 2, + 198, 2, 207, 2, 209, 2, 211, 2, 213, 2, 215, 2, 217, 2, 227, 2, 232, 2, + 247, 2, 249, 2, 251, 2, 253, 2, 255, 3, 1, 3, 10, 3, 12, 3, 14, 3, 16, 3, + 18, 3, 21, 3, 27, 3, 42, 3, 44, 3, 46, 3, 48, 3, 50, 3, 52, 3, 61, 3, 63, + 3, 65, 3, 67, 3, 69, 3, 71, 3, 77, 3, 85, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, + 0, 0, 0, 0, 150, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 87, + ])], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(10), + }), + Archived::Class(Class { + name_index: 9, + version: 0, + parent_index: Some(2), + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 13, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 11, + version: 0, + parent_index: Some(14), + }), + Archived::Class(Class { + name_index: 12, + version: 0, + parent_index: Some(2), + }), + Archived::CString(7), + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + #[test] + fn test_parse_text_arabic() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/Arabic"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(3)], + vec![ + OutputData::SignedInteger(1), + OutputData::UnsignedInteger(13), + ], + vec![OutputData::Object(5)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String("أهلا\u{64b} بالعالم")]]), + }, + Archived::Class(Class { + name_index: 3, + version: 1, + parent_index: Some(2), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(1)], + vec![OutputData::Object(7)], + vec![OutputData::Object(8)], + ]), + }, + Archived::Class(Class { + name_index: 6, + version: 0, + parent_index: Some(2), + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 9, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(10), + }), + Archived::Class(Class { + name_index: 9, + version: 0, + parent_index: Some(2), + }), + Archived::CString(7), + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + #[test] + fn test_parse_text_currency() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/Currency"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(3)], + vec![ + OutputData::SignedInteger(1), + OutputData::UnsignedInteger(29), + ], + vec![OutputData::Object(5)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "Money: $1234.56, £20, 100 USD", + )]]), + }, + Archived::Class(Class { + name_index: 3, + version: 1, + parent_index: Some(2), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(1)], + vec![OutputData::Object(7)], + vec![OutputData::Object(8)], + ]), + }, + Archived::Class(Class { + name_index: 6, + version: 0, + parent_index: Some(2), + }), + Archived::Object { + class: 4, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 9, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(10), + }), + Archived::Class(Class { + name_index: 9, + version: 0, + parent_index: Some(2), + }), + Archived::CString(7), + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + #[test] + fn test_parse_text_currency_temperature_volume_weight() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/CurrencyTemperatureVolumeWeight"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "NSData", + "[764c]", + "[748c]", + "[760c]", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(4)], + vec![OutputData::SignedInteger(1), OutputData::UnsignedInteger(6)], + vec![OutputData::Object(7)], + vec![OutputData::SignedInteger(2), OutputData::UnsignedInteger(3)], + vec![OutputData::Object(14)], + vec![OutputData::SignedInteger(1), OutputData::UnsignedInteger(2)], + vec![OutputData::SignedInteger(3), OutputData::UnsignedInteger(2)], + vec![OutputData::Object(18)], + vec![ + OutputData::SignedInteger(1), + OutputData::UnsignedInteger(11), + ], + vec![OutputData::SignedInteger(4), OutputData::UnsignedInteger(6)], + vec![OutputData::Object(20)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: Some(3), + }), + Archived::Class(Class { + name_index: 3, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 5, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "$100\n\n75℉\n\n1L of water\n\n225lbs", + )]]), + }, + Archived::Class(Class { + name_index: 4, + version: 1, + parent_index: Some(6), + }), + Archived::Class(Class { + name_index: 5, + version: 1, + parent_index: Some(3), + }), + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(1)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 11, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 10, + version: 0, + parent_index: Some(12), + }), + Archived::Class(Class { + name_index: 11, + version: 0, + parent_index: Some(3), + }), + Archived::CString(9), + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(15)], + vec![OutputData::Object(16)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + ]), + }, + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMDataDetectedAttributeName", + )]]), + }, + Archived::Object { + class: 17, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(764)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 24, 128, 1, 175, + 16, 25, 13, 14, 28, 36, 37, 38, 44, 45, 46, 52, 58, 62, 63, 64, 65, 68, 71, + 75, 83, 26, 87, 88, 89, 90, 91, 85, 36, 110, 117, 108, 108, 215, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 26, 82, 77, 83, 86, 36, 99, + 108, 97, 115, 115, 82, 65, 82, 81, 84, 81, 80, 82, 83, 82, 82, 86, 78, 128, + 6, 128, 16, 128, 2, 128, 7, 16, 1, 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, + 35, 95, 16, 18, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, + 101, 110, 103, 116, 104, 95, 16, 20, 78, 83, 46, 114, 97, 110, 103, 101, + 118, 97, 108, 46, 108, 111, 99, 97, 116, 105, 111, 110, 90, 78, 83, 46, + 115, 112, 101, 99, 105, 97, 108, 128, 3, 128, 4, 16, 4, 128, 5, 16, 3, 16, + 6, 210, 39, 40, 41, 42, 90, 36, 99, 108, 97, 115, 115, 110, 97, 109, 101, + 88, 36, 99, 108, 97, 115, 115, 101, 115, 87, 78, 83, 86, 97, 108, 117, 101, + 162, 41, 43, 88, 78, 83, 79, 98, 106, 101, 99, 116, 99, 0, 55, 0, 53, 33, + 9, 94, 80, 104, 121, 115, 105, 99, 97, 108, 65, 109, 111, 117, 110, 116, + 210, 47, 16, 48, 51, 90, 78, 83, 46, 111, 98, 106, 101, 99, 116, 115, 162, + 49, 50, 128, 9, 128, 17, 128, 15, 215, 15, 16, 17, 18, 19, 20, 21, 53, 23, + 55, 56, 26, 57, 26, 128, 12, 128, 16, 128, 10, 128, 13, 128, 14, 212, 29, + 30, 31, 16, 59, 33, 34, 35, 128, 11, 128, 4, 128, 5, 16, 2, 82, 55, 53, 93, + 73, 110, 116, 101, 103, 114, 97, 108, 86, 97, 108, 117, 101, 210, 47, 16, + 66, 51, 160, 128, 15, 210, 39, 40, 69, 70, 87, 78, 83, 65, 114, 114, 97, + 121, 162, 69, 43, 210, 39, 40, 72, 73, 95, 16, 15, 68, 68, 83, 99, 97, 110, + 110, 101, 114, 82, 101, 115, 117, 108, 116, 162, 74, 43, 95, 16, 15, 68, + 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 216, 15, + 16, 76, 17, 18, 19, 20, 21, 77, 23, 79, 80, 81, 26, 57, 26, 81, 86, 128, + 21, 128, 16, 128, 23, 128, 18, 128, 22, 128, 14, 212, 29, 30, 31, 16, 84, + 85, 34, 35, 128, 19, 128, 20, 128, 5, 16, 8, 97, 33, 9, 84, 85, 110, 105, + 116, 90, 102, 97, 104, 114, 101, 110, 104, 101, 105, 116, 16, 1, 0, 8, 0, + 17, 0, 26, 0, 36, 0, 41, 0, 50, 0, 55, 0, 73, 0, 78, 0, 86, 0, 96, 0, 98, + 0, 100, 0, 128, 0, 134, 0, 149, 0, 152, 0, 159, 0, 162, 0, 164, 0, 166, 0, + 169, 0, 172, 0, 174, 0, 176, 0, 178, 0, 180, 0, 182, 0, 184, 0, 193, 0, + 214, 0, 237, 0, 248, 0, 250, 0, 252, 0, 254, 1, 0, 1, 2, 1, 4, 1, 9, 1, 20, + 1, 29, 1, 37, 1, 40, 1, 49, 1, 56, 1, 71, 1, 76, 1, 87, 1, 90, 1, 92, 1, + 94, 1, 96, 1, 111, 1, 113, 1, 115, 1, 117, 1, 119, 1, 121, 1, 130, 1, 132, + 1, 134, 1, 136, 1, 138, 1, 141, 1, 155, 1, 160, 1, 161, 1, 163, 1, 168, 1, + 176, 1, 179, 1, 184, 1, 202, 1, 205, 1, 223, 1, 240, 1, 242, 1, 244, 1, + 246, 1, 248, 1, 250, 1, 252, 1, 254, 2, 7, 2, 9, 2, 11, 2, 13, 2, 15, 2, + 18, 2, 23, 2, 34, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 36, + ])], + ]), + }, + Archived::Class(Class { + name_index: 13, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(15)], + vec![OutputData::Object(19)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + ]), + }, + Archived::Object { + class: 17, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(748)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 23, 128, 1, 175, + 16, 24, 13, 14, 28, 36, 37, 38, 44, 45, 46, 52, 58, 26, 62, 63, 64, 67, 70, + 74, 82, 86, 87, 88, 89, 90, 85, 36, 110, 117, 108, 108, 215, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 26, 82, 77, 83, 86, 36, 99, 108, + 97, 115, 115, 82, 65, 82, 81, 84, 81, 80, 82, 83, 82, 82, 86, 78, 128, 6, + 128, 16, 128, 2, 128, 7, 16, 1, 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, + 35, 95, 16, 18, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, + 101, 110, 103, 116, 104, 95, 16, 20, 78, 83, 46, 114, 97, 110, 103, 101, + 118, 97, 108, 46, 108, 111, 99, 97, 116, 105, 111, 110, 90, 78, 83, 46, + 115, 112, 101, 99, 105, 97, 108, 128, 3, 128, 4, 16, 4, 128, 5, 16, 2, 16, + 11, 210, 39, 40, 41, 42, 90, 36, 99, 108, 97, 115, 115, 110, 97, 109, 101, + 88, 36, 99, 108, 97, 115, 115, 101, 115, 87, 78, 83, 86, 97, 108, 117, 101, + 162, 41, 43, 88, 78, 83, 79, 98, 106, 101, 99, 116, 82, 49, 76, 94, 80, + 104, 121, 115, 105, 99, 97, 108, 65, 109, 111, 117, 110, 116, 210, 47, 16, + 48, 51, 90, 78, 83, 46, 111, 98, 106, 101, 99, 116, 115, 162, 49, 50, 128, + 9, 128, 17, 128, 15, 215, 15, 16, 17, 18, 19, 20, 21, 53, 23, 55, 56, 26, + 57, 26, 128, 12, 128, 16, 128, 10, 128, 13, 128, 14, 212, 29, 30, 31, 16, + 59, 33, 34, 35, 128, 11, 128, 4, 128, 5, 81, 49, 93, 73, 110, 116, 101, + 103, 114, 97, 108, 86, 97, 108, 117, 101, 210, 47, 16, 65, 51, 160, 128, + 15, 210, 39, 40, 68, 69, 87, 78, 83, 65, 114, 114, 97, 121, 162, 68, 43, + 210, 39, 40, 71, 72, 95, 16, 15, 68, 68, 83, 99, 97, 110, 110, 101, 114, + 82, 101, 115, 117, 108, 116, 162, 73, 43, 95, 16, 15, 68, 68, 83, 99, 97, + 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 216, 15, 16, 75, 17, 18, + 19, 20, 21, 76, 23, 78, 79, 80, 26, 57, 26, 81, 86, 128, 20, 128, 16, 128, + 22, 128, 18, 128, 21, 128, 14, 212, 29, 30, 31, 16, 59, 84, 34, 35, 128, + 11, 128, 19, 128, 5, 16, 12, 81, 76, 84, 85, 110, 105, 116, 85, 108, 105, + 116, 101, 114, 16, 1, 0, 8, 0, 17, 0, 26, 0, 36, 0, 41, 0, 50, 0, 55, 0, + 73, 0, 78, 0, 86, 0, 96, 0, 98, 0, 100, 0, 127, 0, 133, 0, 148, 0, 151, 0, + 158, 0, 161, 0, 163, 0, 165, 0, 168, 0, 171, 0, 173, 0, 175, 0, 177, 0, + 179, 0, 181, 0, 183, 0, 192, 0, 213, 0, 236, 0, 247, 0, 249, 0, 251, 0, + 253, 0, 255, 1, 1, 1, 3, 1, 8, 1, 19, 1, 28, 1, 36, 1, 39, 1, 48, 1, 51, 1, + 66, 1, 71, 1, 82, 1, 85, 1, 87, 1, 89, 1, 91, 1, 106, 1, 108, 1, 110, 1, + 112, 1, 114, 1, 116, 1, 125, 1, 127, 1, 129, 1, 131, 1, 133, 1, 147, 1, + 152, 1, 153, 1, 155, 1, 160, 1, 168, 1, 171, 1, 176, 1, 194, 1, 197, 1, + 215, 1, 232, 1, 234, 1, 236, 1, 238, 1, 240, 1, 242, 1, 244, 1, 246, 1, + 255, 2, 1, 2, 3, 2, 5, 2, 7, 2, 9, 2, 14, 2, 20, 0, 0, 0, 0, 0, 0, 2, 1, 0, + 0, 0, 0, 0, 0, 0, 91, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 22, + ])], + ]), + }, + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(15)], + vec![OutputData::Object(21)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + ]), + }, + Archived::Object { + class: 17, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(760)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 23, 128, 1, 175, + 16, 24, 13, 14, 28, 36, 37, 38, 44, 45, 46, 52, 58, 62, 63, 64, 65, 68, 71, + 75, 83, 87, 88, 89, 90, 91, 85, 36, 110, 117, 108, 108, 215, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 26, 82, 77, 83, 86, 36, 99, 108, + 97, 115, 115, 82, 65, 82, 81, 84, 81, 80, 82, 83, 82, 82, 86, 78, 128, 6, + 128, 16, 128, 2, 128, 7, 16, 1, 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, + 35, 95, 16, 18, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, + 101, 110, 103, 116, 104, 95, 16, 20, 78, 83, 46, 114, 97, 110, 103, 101, + 118, 97, 108, 46, 108, 111, 99, 97, 116, 105, 111, 110, 90, 78, 83, 46, + 115, 112, 101, 99, 105, 97, 108, 128, 3, 128, 4, 16, 4, 128, 5, 16, 6, 16, + 24, 210, 39, 40, 41, 42, 90, 36, 99, 108, 97, 115, 115, 110, 97, 109, 101, + 88, 36, 99, 108, 97, 115, 115, 101, 115, 87, 78, 83, 86, 97, 108, 117, 101, + 162, 41, 43, 88, 78, 83, 79, 98, 106, 101, 99, 116, 86, 50, 50, 53, 108, + 98, 115, 94, 80, 104, 121, 115, 105, 99, 97, 108, 65, 109, 111, 117, 110, + 116, 210, 47, 16, 48, 51, 90, 78, 83, 46, 111, 98, 106, 101, 99, 116, 115, + 162, 49, 50, 128, 9, 128, 17, 128, 15, 215, 15, 16, 17, 18, 19, 20, 21, 53, + 23, 55, 56, 26, 57, 26, 128, 12, 128, 16, 128, 10, 128, 13, 128, 14, 212, + 29, 30, 31, 16, 59, 33, 34, 35, 128, 11, 128, 4, 128, 5, 16, 3, 83, 50, 50, + 53, 93, 73, 110, 116, 101, 103, 114, 97, 108, 86, 97, 108, 117, 101, 210, + 47, 16, 66, 51, 160, 128, 15, 210, 39, 40, 69, 70, 87, 78, 83, 65, 114, + 114, 97, 121, 162, 69, 43, 210, 39, 40, 72, 73, 95, 16, 15, 68, 68, 83, 99, + 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 162, 74, 43, 95, 16, + 15, 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, + 216, 15, 16, 76, 17, 18, 19, 20, 21, 77, 23, 79, 80, 81, 26, 57, 26, 81, + 86, 128, 20, 128, 16, 128, 22, 128, 18, 128, 21, 128, 14, 212, 29, 30, 31, + 16, 59, 85, 34, 35, 128, 11, 128, 19, 128, 5, 16, 27, 83, 108, 98, 115, 84, + 85, 110, 105, 116, 85, 112, 111, 117, 110, 100, 16, 1, 0, 8, 0, 17, 0, 26, + 0, 36, 0, 41, 0, 50, 0, 55, 0, 73, 0, 78, 0, 86, 0, 96, 0, 98, 0, 100, 0, + 127, 0, 133, 0, 148, 0, 151, 0, 158, 0, 161, 0, 163, 0, 165, 0, 168, 0, + 171, 0, 173, 0, 175, 0, 177, 0, 179, 0, 181, 0, 183, 0, 192, 0, 213, 0, + 236, 0, 247, 0, 249, 0, 251, 0, 253, 0, 255, 1, 1, 1, 3, 1, 8, 1, 19, 1, + 28, 1, 36, 1, 39, 1, 48, 1, 55, 1, 70, 1, 75, 1, 86, 1, 89, 1, 91, 1, 93, + 1, 95, 1, 110, 1, 112, 1, 114, 1, 116, 1, 118, 1, 120, 1, 129, 1, 131, 1, + 133, 1, 135, 1, 137, 1, 141, 1, 155, 1, 160, 1, 161, 1, 163, 1, 168, 1, + 176, 1, 179, 1, 184, 1, 202, 1, 205, 1, 223, 1, 240, 1, 242, 1, 244, 1, + 246, 1, 248, 1, 250, 1, 252, 1, 254, 2, 7, 2, 9, 2, 11, 2, 13, 2, 15, 2, + 19, 2, 24, 2, 30, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 32, + ])], + ]), + }, + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + #[test] + fn test_parse_text_flight() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/Flight"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSData", + "[754c]", + "NSNumber", + "NSValue", + "*", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(4)], + vec![OutputData::SignedInteger(1), OutputData::UnsignedInteger(7)], + vec![OutputData::Object(7)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: Some(3), + }), + Archived::Class(Class { + name_index: 3, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 5, + data: ObjectData::from_groups(vec![vec![OutputData::String("AS 1111")]]), + }, + Archived::Class(Class { + name_index: 4, + version: 1, + parent_index: Some(6), + }), + Archived::Class(Class { + name_index: 5, + version: 1, + parent_index: Some(3), + }), + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + vec![OutputData::Object(12)], + vec![OutputData::Object(13)], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMDataDetectedAttributeName", + )]]), + }, + Archived::Object { + class: 11, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(754)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 23, 128, 1, 175, + 16, 24, 13, 14, 28, 36, 37, 38, 44, 45, 46, 52, 58, 62, 63, 64, 65, 68, 71, + 75, 81, 34, 85, 86, 87, 88, 85, 36, 110, 117, 108, 108, 215, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 26, 82, 77, 83, 86, 36, 99, 108, + 97, 115, 115, 82, 65, 82, 81, 84, 81, 80, 82, 83, 82, 82, 86, 78, 128, 6, + 128, 16, 128, 2, 128, 7, 16, 1, 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, + 35, 95, 16, 18, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, + 101, 110, 103, 116, 104, 95, 16, 20, 78, 83, 46, 114, 97, 110, 103, 101, + 118, 97, 108, 46, 108, 111, 99, 97, 116, 105, 111, 110, 90, 78, 83, 46, + 115, 112, 101, 99, 105, 97, 108, 128, 3, 128, 4, 16, 4, 128, 5, 16, 7, 16, + 0, 210, 39, 40, 41, 42, 90, 36, 99, 108, 97, 115, 115, 110, 97, 109, 101, + 88, 36, 99, 108, 97, 115, 115, 101, 115, 87, 78, 83, 86, 97, 108, 117, 101, + 162, 41, 43, 88, 78, 83, 79, 98, 106, 101, 99, 116, 87, 65, 83, 32, 49, 49, + 49, 49, 95, 16, 17, 70, 108, 105, 103, 104, 116, 73, 110, 102, 111, 114, + 109, 97, 116, 105, 111, 110, 210, 47, 16, 48, 51, 90, 78, 83, 46, 111, 98, + 106, 101, 99, 116, 115, 162, 49, 50, 128, 9, 128, 17, 128, 15, 215, 15, 16, + 17, 18, 19, 20, 21, 53, 23, 55, 56, 26, 57, 26, 128, 12, 128, 16, 128, 10, + 128, 13, 128, 14, 212, 29, 30, 31, 16, 59, 33, 34, 35, 128, 11, 128, 4, + 128, 5, 16, 2, 82, 65, 83, 91, 65, 105, 114, 108, 105, 110, 101, 67, 111, + 100, 101, 210, 47, 16, 66, 51, 160, 128, 15, 210, 39, 40, 69, 70, 87, 78, + 83, 65, 114, 114, 97, 121, 162, 69, 43, 210, 39, 40, 72, 73, 95, 16, 15, + 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 162, + 74, 43, 95, 16, 15, 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, + 117, 108, 116, 215, 15, 16, 17, 18, 19, 20, 21, 76, 23, 78, 79, 26, 57, 26, + 128, 21, 128, 16, 128, 18, 128, 22, 128, 14, 212, 29, 30, 31, 16, 82, 83, + 34, 35, 128, 19, 128, 20, 128, 5, 16, 3, 84, 49, 49, 49, 49, 92, 70, 108, + 105, 103, 104, 116, 78, 117, 109, 98, 101, 114, 16, 1, 0, 8, 0, 17, 0, 26, + 0, 36, 0, 41, 0, 50, 0, 55, 0, 73, 0, 78, 0, 86, 0, 96, 0, 98, 0, 100, 0, + 127, 0, 133, 0, 148, 0, 151, 0, 158, 0, 161, 0, 163, 0, 165, 0, 168, 0, + 171, 0, 173, 0, 175, 0, 177, 0, 179, 0, 181, 0, 183, 0, 192, 0, 213, 0, + 236, 0, 247, 0, 249, 0, 251, 0, 253, 0, 255, 1, 1, 1, 3, 1, 8, 1, 19, 1, + 28, 1, 36, 1, 39, 1, 48, 1, 56, 1, 76, 1, 81, 1, 92, 1, 95, 1, 97, 1, 99, + 1, 101, 1, 116, 1, 118, 1, 120, 1, 122, 1, 124, 1, 126, 1, 135, 1, 137, 1, + 139, 1, 141, 1, 143, 1, 146, 1, 158, 1, 163, 1, 164, 1, 166, 1, 171, 1, + 179, 1, 182, 1, 187, 1, 205, 1, 208, 1, 226, 1, 241, 1, 243, 1, 245, 1, + 247, 1, 249, 1, 251, 2, 4, 2, 6, 2, 8, 2, 10, 2, 12, 2, 17, 2, 30, 0, 0, 0, + 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 89, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 2, 32, + ])], + ]), + }, + Archived::Class(Class { + name_index: 10, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 14, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 12, + version: 0, + parent_index: Some(15), + }), + Archived::Class(Class { + name_index: 13, + version: 0, + parent_index: Some(3), + }), + Archived::CString(9), + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + #[test] + fn test_parse_text_tracking() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/Tracking"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + + let mut typedstream = TypedStreamDeserializer::new(&bytes); + let root = typedstream.oxidize().unwrap(); + println!("\nResults:"); + println!("Root object: {:x?}", typedstream.object_table[root]); + print_resolved(typedstream.resolve_properties(root).unwrap(), 2); + + println!("\nFound {:?} types:", typedstream.string_table.len()); + typedstream + .string_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + println!("\nFound {:?} objects:", typedstream.object_table.len()); + typedstream + .object_table + .iter() + .enumerate() + .for_each(|(idx, item)| println!("\t{idx}: {item:?}")); + + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSData", + "[664c]", + "NSNumber", + "NSValue", + "*", + ]; + + let expected_objects = vec![ + Archived::Object { + class: 1, + data: ObjectData::from_groups(vec![ + vec![OutputData::Object(4)], + vec![ + OutputData::SignedInteger(1), + OutputData::UnsignedInteger(18), + ], + vec![OutputData::Object(7)], + ]), + }, + Archived::Class(Class { + name_index: 1, + version: 0, + parent_index: Some(2), + }), + Archived::Class(Class { + name_index: 2, + version: 0, + parent_index: Some(3), + }), + Archived::Class(Class { + name_index: 3, + version: 0, + parent_index: None, + }), + Archived::Object { + class: 5, + data: ObjectData::from_groups(vec![vec![OutputData::String("1Z999AA10123456784")]]), + }, + Archived::Class(Class { + name_index: 4, + version: 1, + parent_index: Some(6), + }), + Archived::Class(Class { + name_index: 5, + version: 1, + parent_index: Some(3), + }), + Archived::Object { + class: 8, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(2)], + vec![OutputData::Object(9)], + vec![OutputData::Object(10)], + vec![OutputData::Object(12)], + vec![OutputData::Object(13)], + ]), + }, + Archived::Class(Class { + name_index: 8, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMDataDetectedAttributeName", + )]]), + }, + Archived::Object { + class: 11, + data: ObjectData::from_groups(vec![ + vec![OutputData::SignedInteger(664)], + vec![OutputData::Array(&[ + 98, 112, 108, 105, 115, 116, 48, 48, 212, 1, 2, 3, 4, 5, 6, 7, 12, 88, 36, + 118, 101, 114, 115, 105, 111, 110, 89, 36, 97, 114, 99, 104, 105, 118, 101, + 114, 84, 36, 116, 111, 112, 88, 36, 111, 98, 106, 101, 99, 116, 115, 18, 0, + 1, 134, 160, 95, 16, 15, 78, 83, 75, 101, 121, 101, 100, 65, 114, 99, 104, + 105, 118, 101, 114, 210, 8, 9, 10, 11, 87, 118, 101, 114, 115, 105, 111, + 110, 89, 100, 100, 45, 114, 101, 115, 117, 108, 116, 128, 15, 128, 1, 175, + 16, 16, 13, 14, 28, 36, 37, 38, 44, 45, 46, 51, 57, 61, 62, 65, 69, 73, 85, + 36, 110, 117, 108, 108, 215, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 26, 82, 77, 83, 86, 36, 99, 108, 97, 115, 115, 82, 65, 82, 81, 84, + 81, 80, 82, 83, 82, 82, 86, 78, 128, 6, 128, 14, 128, 2, 128, 7, 16, 1, + 128, 8, 212, 29, 30, 31, 16, 32, 33, 34, 35, 95, 16, 18, 78, 83, 46, 114, + 97, 110, 103, 101, 118, 97, 108, 46, 108, 101, 110, 103, 116, 104, 95, 16, + 20, 78, 83, 46, 114, 97, 110, 103, 101, 118, 97, 108, 46, 108, 111, 99, 97, + 116, 105, 111, 110, 90, 78, 83, 46, 115, 112, 101, 99, 105, 97, 108, 128, + 3, 128, 4, 16, 4, 128, 5, 16, 18, 16, 0, 210, 39, 40, 41, 42, 90, 36, 99, + 108, 97, 115, 115, 110, 97, 109, 101, 88, 36, 99, 108, 97, 115, 115, 101, + 115, 87, 78, 83, 86, 97, 108, 117, 101, 162, 41, 43, 88, 78, 83, 79, 98, + 106, 101, 99, 116, 95, 16, 18, 49, 90, 57, 57, 57, 65, 65, 49, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 52, 94, 84, 114, 97, 99, 107, 105, 110, 103, 78, + 117, 109, 98, 101, 114, 210, 47, 16, 48, 50, 90, 78, 83, 46, 111, 98, 106, + 101, 99, 116, 115, 161, 49, 128, 9, 128, 13, 215, 15, 16, 17, 18, 19, 20, + 21, 22, 23, 54, 55, 26, 56, 26, 128, 6, 128, 14, 128, 10, 128, 11, 128, 12, + 212, 29, 30, 31, 16, 32, 33, 34, 35, 128, 3, 128, 4, 128, 5, 83, 85, 80, + 83, 210, 47, 16, 63, 50, 160, 128, 13, 210, 39, 40, 66, 67, 94, 78, 83, 77, + 117, 116, 97, 98, 108, 101, 65, 114, 114, 97, 121, 163, 66, 68, 43, 87, 78, + 83, 65, 114, 114, 97, 121, 210, 39, 40, 70, 71, 95, 16, 15, 68, 68, 83, 99, + 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, 162, 72, 43, 95, 16, + 15, 68, 68, 83, 99, 97, 110, 110, 101, 114, 82, 101, 115, 117, 108, 116, + 16, 1, 0, 8, 0, 17, 0, 26, 0, 36, 0, 41, 0, 50, 0, 55, 0, 73, 0, 78, 0, 86, + 0, 96, 0, 98, 0, 100, 0, 119, 0, 125, 0, 140, 0, 143, 0, 150, 0, 153, 0, + 155, 0, 157, 0, 160, 0, 163, 0, 165, 0, 167, 0, 169, 0, 171, 0, 173, 0, + 175, 0, 184, 0, 205, 0, 228, 0, 239, 0, 241, 0, 243, 0, 245, 0, 247, 0, + 249, 0, 251, 1, 0, 1, 11, 1, 20, 1, 28, 1, 31, 1, 40, 1, 61, 1, 76, 1, 81, + 1, 92, 1, 94, 1, 96, 1, 98, 1, 113, 1, 115, 1, 117, 1, 119, 1, 121, 1, 123, + 1, 132, 1, 134, 1, 136, 1, 138, 1, 142, 1, 147, 1, 148, 1, 150, 1, 155, 1, + 170, 1, 174, 1, 182, 1, 187, 1, 205, 1, 208, 1, 226, 0, 0, 0, 0, 0, 0, 2, + 1, 0, 0, 0, 0, 0, 0, 0, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + 228, + ])], + ]), + }, + Archived::Class(Class { + name_index: 10, + version: 0, + parent_index: Some(3), + }), + Archived::Object { + class: 6, + data: ObjectData::from_groups(vec![vec![OutputData::String( + "__kIMMessagePartAttributeName", + )]]), + }, + Archived::Object { + class: 14, + data: ObjectData::from_groups(vec![ + vec![OutputData::String("i")], + vec![OutputData::SignedInteger(0)], + ]), + }, + Archived::Class(Class { + name_index: 12, + version: 0, + parent_index: Some(15), + }), + Archived::Class(Class { + name_index: 13, + version: 0, + parent_index: Some(3), + }), + Archived::CString(9), + ]; + + assert_eq!(typedstream.string_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } + + /// A pre-keyed AppKit nib (`SDLMain.nib/objects.nib`, from the SDL project's + /// Mac template). + #[test] + fn test_parse_nib() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/SDLMainNib"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + + let root = ts.oxidize().unwrap(); + // Byte-exact: every byte was consumed and nothing was left half-read. + assert_eq!(ts.position, bytes.len()); + assert!( + !ts.object_table + .iter() + .any(|o| matches!(o, Archived::Placeholder)), + "an object was entered but never completed" + ); + + let class_name = |idx: usize| match &ts.object_table[idx] { + Archived::Class(class) => Some(ts.string_table[class.name_index].text), + _ => None, + }; + let Archived::Object { class, .. } = &ts.object_table[root] else { + panic!("root is not an object"); + }; + assert_eq!(class_name(*class), Some("NSIBObjectData")); + + let mut classes: Vec<&str> = (0..ts.object_table.len()).filter_map(class_name).collect(); + classes.sort_unstable(); + assert_eq!( + classes, + [ + "NSArray", + "NSCustomObject", + "NSCustomResource", + "NSIBObjectData", + "NSMenu", + "NSMenuItem", + "NSMutableArray", + "NSMutableSet", + "NSMutableString", + "NSNibConnector", + "NSNibControlConnector", + "NSNibOutletConnector", + "NSObject", + "NSSet", + "NSString", + ] + ); + + // Every value an object holds, whichever storage shape it took. + fn values_of<'v, 'a>(data: &'v ObjectData<'a>) -> Vec<&'v OutputData<'a>> { + match data { + ObjectData::Empty => Vec::new(), + ObjectData::Inline(value) => vec![value], + ObjectData::Groups(groups) => { + groups.iter().flat_map(|group| group.as_slice()).collect() + } + } + } + let values: Vec<&OutputData<'_>> = ts + .object_table + .iter() + .filter_map(|o| match o { + Archived::Object { data, .. } => Some(values_of(data)), + _ => None, + }) + .flatten() + .collect(); + let strings: Vec<&str> = values.iter().filter_map(|v| v.as_str()).collect(); + for selector in [ + "submenuAction:", + "orderFrontStandardAboutPanel:", + "performMiniaturize:", + "unhideAllApplications:", + "quit:", + ] { + assert!(strings.contains(&selector), "missing selector {selector}"); + } + for title in [ + "MainMenu", + "Quit DEFCON", + "Hide Others", + "Minimize", + "User Manual", + ] { + assert!(strings.contains(&title), "missing title {title}"); + } + // Nil outlets, images, and targets: the path Messages data never takes. + let nils = values + .iter() + .filter(|v| matches!(v, OutputData::Null)) + .count(); + assert!(nils >= 20, "expected many nil references, found {nils}"); + + // Every object is reachable from the root through object references, + // including the back-references from menu items to their menus. + let mut seen = vec![false; ts.object_table.len()]; + let mut stack = vec![root]; + while let Some(idx) = stack.pop() { + if core::mem::replace(&mut seen[idx], true) { + continue; + } + if let Archived::Object { data, .. } = &ts.object_table[idx] { + for value in values_of(data) { + if let OutputData::Object(child) = value { + stack.push(*child); + } + } + } + } + let unreachable: Vec = (0..ts.object_table.len()) + .filter(|&i| !seen[i] && matches!(ts.object_table[i], Archived::Object { .. })) + .collect(); + assert!( + unreachable.is_empty(), + "unreachable objects: {unreachable:?}" + ); + + // The lazy property view agrees with the table on the root's shape. + let root_groups = ts.resolve_properties(root).unwrap().count(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + unreachable!() + }; + assert_eq!(root_groups, data.group_count()); + } + + /// `NSValue`s with struct `objCType`s from the classic archiver + /// (`test_data/generators/foundation.swift`). Layout: a `char *` `objCType`, + /// followed by the values described by that string. Struct layout: flat + /// members. Slot counts: two for `{_NSRange=QQ}` and four for + /// `{CGRect={CGPoint=dd}{CGSize=dd}}`. `CGSize` contributes three scalar + /// letters to its name, not three slots. + #[test] + fn test_parse_nsvalue_structs() { + use OutputData::{Double as D, UnsignedInteger as U}; + for (fixture, obj_c_type, expected) in [ + ("NSValueRange", "{_NSRange=QQ}", vec![U(3), U(4)]), + ("NSValueSize", "{CGSize=dd}", vec![D(1.5), D(2.5)]), + ( + "NSValueRect", + "{CGRect={CGPoint=dd}{CGSize=dd}}", + vec![D(1.0), D(2.0), D(3.0), D(4.0)], + ), + ] { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/foundation") + .join(fixture); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + + let root = ts.oxidize().unwrap(); + // Consumed exactly. A byte-wise parser reads the object's END marker + // as the struct's last member and runs one byte past the file. + assert_eq!(ts.position, bytes.len(), "{fixture}"); + + let Archived::Object { class, data } = &ts.object_table[root] else { + panic!("{fixture}: root is not an object"); + }; + let Archived::Class(class) = &ts.object_table[*class] else { + panic!("{fixture}: root class is not a class"); + }; + assert_eq!( + ts.string_table[class.name_index].text, "NSValue", + "{fixture}" + ); + // Expect the `objCType` string first, followed by the members and + // nothing else. Braces, `=`, and name letters are grammar, not values. + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::String(obj_c_type)), + DataGroup::Values(expected), + ]), + "{fixture}" + ); + } + } + + /// Bare `char *` slots from the classic archiver (`CStrings` in + /// `test_data/generators/foundation.swift`). Test sequence: a literal, + /// `NULL`, the same pointer again, a distinct pointer with the same text, + /// then an `int`. Track pointer identity in the object table: reuse the + /// existing slot for the repeated pointer; allocate a new slot for the + /// second pointer and reuse the string-table entry for its text. + #[test] + fn test_parse_c_strings() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/foundation/CStrings"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + + let root = ts.oxidize().unwrap(); + assert_eq!(ts.position, bytes.len()); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("root is not an object"); + }; + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::String("hello")), + DataGroup::One(OutputData::Null), + DataGroup::One(OutputData::String("hello")), + DataGroup::One(OutputData::String("hello")), + DataGroup::One(OutputData::SignedInteger(7)), + ]) + ); + // Two pointers, two slots, one shared string. + let slots: Vec = ts + .object_table + .iter() + .filter_map(|o| match o { + Archived::CString(index) => Some(*index), + _ => None, + }) + .collect(); + assert_eq!(slots.len(), 2); + assert!(slots.iter().all(|&i| ts.shared_string(i) == Some("hello"))); + assert_eq!(slots[0], slots[1]); + } + + /// `%` (`NXAtom`) slots from the classic archiver (`Atoms` in + /// `test_data/generators/foundation.swift`): a literal, `NULL`, the same + /// atom again, then an `int`. + #[test] + fn test_parse_atoms() { + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/foundation/Atoms"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + + let root = ts.oxidize().unwrap(); + assert_eq!(ts.position, bytes.len()); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("root is not an object"); + }; + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::String("atom")), + DataGroup::One(OutputData::Null), + DataGroup::One(OutputData::String("atom")), + DataGroup::One(OutputData::SignedInteger(7)), + ]) + ); + assert!( + !ts.object_table + .iter() + .any(|o| matches!(o, Archived::CString(_))), + "an atom is shared by text, not by pointer" + ); + assert_eq!( + ts.string_table.iter().filter(|e| e.text == "atom").count(), + 1 + ); + } + + /// Which shared strings carry a descriptor view after a parse: descriptor + /// literals eagerly, class names never, and an `NSNumber`'s `objCType` on + /// the reference that types its payload. + #[test] + fn test_shared_string_views() { + use crate::models::{ + shared_string::SharedString, + types::{Type, TypeEntry}, + }; + let typedstream_path = current_dir() + .unwrap() + .as_path() + .join("src/test_data/AttributedBodyTextOnly"); + let mut file = File::open(typedstream_path).unwrap(); + let mut bytes = vec![]; + file.read_to_end(&mut bytes).unwrap(); + let mut ts = TypedStreamDeserializer::new(&bytes); + ts.oxidize().unwrap(); + + let view = |text: &str| -> Option { + let entry = ts.string_table.iter().find(|e| e.text == text).unwrap(); + entry.parsed().cloned() + }; + use Type::{CString, Object, SignedInt, UnsignedInt, Utf8String}; + assert_eq!(view("@"), Some(TypeEntry::One(Object))); + assert_eq!(view("+"), Some(TypeEntry::One(Utf8String))); + assert_eq!(view("*"), Some(TypeEntry::One(CString))); + assert_eq!( + view("iI"), + Some(TypeEntry::Many(vec![SignedInt, UnsignedInt])) + ); + // `i` was first written as the NSNumber's `char *` objCType, then + // referenced as the descriptor of its value. + assert_eq!(view("i"), Some(TypeEntry::One(SignedInt))); + for name in [ + "NSMutableAttributedString", + "NSString", + "NSDictionary", + "NSNumber", + "NSValue", + ] { + assert_eq!( + view(name), + None, + "{name} is a class name, never a descriptor" + ); + } + + // Equality is on text; a parsed view does not distinguish entries. + assert_eq!(ts.string_table[0], SharedString::new("@")); + assert_eq!( + ts.string_table, + [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*" + ] + ); + } } diff --git a/src/models/archived.rs b/src/models/archived.rs index f19aa20..7778d71 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -4,15 +4,47 @@ use alloc::{vec, vec::Vec}; use crate::models::{class::Class, output_data::OutputData}; +/// A data group within an [`ObjectData::Groups`] collection: the values decoded +/// from one type descriptor. +/// +/// 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> { + /// Exactly one value, stored inline. + One(OutputData<'a>), + /// Zero, or multiple, values. + Values(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::Values(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; every other nonempty object +/// holds its groups in stream order. #[derive(Debug, PartialEq)] pub enum ObjectData<'a> { /// The object has no data groups. @@ -20,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>), - /// The general case: one or more groups, each holding one or more values. - Groups(Vec>>), + /// 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(vec![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]]); + ObjectData::Empty => { + *self = match group { + DataGroup::One(value) => ObjectData::Inline(value), + group => ObjectData::Groups(vec![group]), + } } - } - } - - /// Append a group that contains multiple values. - #[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), + // 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], values]); + *self = ObjectData::Groups(vec![DataGroup::One(first), group]); } } } @@ -67,7 +92,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::Values(group) + } + }) + .collect(), + ) } } @@ -103,6 +139,12 @@ pub enum Archived<'a> { /// comes before the ones it inherits from. To preserve the order, we reserve the first slot to store the actual object's data /// and then later add it back to the right place. Placeholder, - /// An embedded type that describes the [`Type`](crate::models::types::Type) of the subsequent bytes, referred to by its index in the [`type_table`](crate::deserializer::typedstream::TypedStreamDeserializer::type_table). - Type(usize), + /// A `char *` value. `NSArchiver` shares C strings by pointer identity + /// through the object table, so each distinct pointer takes one slot here + /// and a repeated pointer is written as a reference to that slot. The text + /// itself is shared separately, through the + /// [`string_table`](crate::deserializer::typedstream::TypedStreamDeserializer::string_table), + /// which this index names. This means two pointers with the same text get + /// two slots but one string. + CString(usize), } diff --git a/src/models/class.rs b/src/models/class.rs index bdf5176..674dacf 100644 --- a/src/models/class.rs +++ b/src/models/class.rs @@ -3,7 +3,7 @@ /// Represents a class stored in a `typedstream` #[derive(Debug, Clone, PartialEq, Eq)] pub struct Class { - /// A reference to the class name stored in the [`type_table`](crate::deserializer::typedstream::TypedStreamDeserializer::type_table) + /// A reference to the class name stored in the [`string_table`](crate::deserializer::typedstream::TypedStreamDeserializer::string_table) pub name_index: usize, /// The encoded version of the class pub version: u64, diff --git a/src/models/mod.rs b/src/models/mod.rs index 9f2b68c..cd5591c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -3,4 +3,5 @@ pub mod archived; pub mod class; pub mod output_data; +pub mod shared_string; pub mod types; diff --git a/src/models/output_data.rs b/src/models/output_data.rs index 0551d78..2e7cc3a 100644 --- a/src/models/output_data.rs +++ b/src/models/output_data.rs @@ -3,7 +3,7 @@ /// Rust structures containing data stored in the `typedstream` #[derive(Debug, PartialEq)] pub enum OutputData<'a> { - /// Text data, denoted in the stream by [`Type::String`](crate::models::types::Type::String) + /// Text: a `+` string, a selector, a `char *`, or a class name. String(&'a str), /// Signed integer types are coerced into this container, denoted in the stream by [`Type::SignedInt`](crate::models::types::Type::SignedInt) SignedInteger(i64), @@ -13,11 +13,11 @@ pub enum OutputData<'a> { Float(f32), /// Double precision floats, denoted in the stream by [`Type::Double`](crate::models::types::Type::Double) Double(f64), - /// Bytes whose type is not known, denoted in the stream by [`Type::Unknown`](crate::models::types::Type::Unknown) - Byte(u8), /// Arbitrary collection of bytes in an array, denoted in the stream by [`Type::Array`](crate::models::types::Type::Array) Array(&'a [u8]), - /// Reference to another object by index in the [`object_table`](crate::deserializer::typedstream::TypedStreamDeserializer::object_table). + /// Reference into the [`object_table`](crate::deserializer::typedstream::TypedStreamDeserializer::object_table): + /// an [`Archived::Object`](crate::models::archived::Archived::Object) for an [`@`](crate::models::archived::Archived::Object) slot, or an + /// [`Archived::Class`](crate::models::archived::Archived::Class) for a [`#`](crate::models::archived::Archived::Class) slot. Object(usize), /// Represents a null value. Null, @@ -119,25 +119,6 @@ impl<'a> OutputData<'a> { } } - /// Returns the inner byte if this is a `Byte` variant. - /// - /// # Examples - /// - /// ```no_run - /// use crabstep::models::output_data::OutputData; - /// - /// let data = OutputData::Byte(0xFF); - /// assert_eq!(data.as_byte(), Some(0xFF)); - /// ``` - #[must_use] - pub fn as_byte(&self) -> Option { - if let OutputData::Byte(b) = self { - Some(*b) - } else { - None - } - } - /// Returns the inner byte slice if this is an `Array` variant. /// /// # Examples @@ -186,7 +167,6 @@ impl core::fmt::Display for OutputData<'_> { OutputData::UnsignedInteger(u) => write!(f, "{u}"), OutputData::Float(fp) => write!(f, "{fp}"), OutputData::Double(d) => write!(f, "{d}"), - OutputData::Byte(b) => write!(f, "0x{b:02x}"), OutputData::Array(arr) => write!(f, "[{arr:02x?}]"), OutputData::Object(idx) => write!(f, "Object({idx})"), OutputData::Null => write!(f, "Null"), diff --git a/src/models/shared_string.rs b/src/models/shared_string.rs new file mode 100644 index 0000000..2703c8e --- /dev/null +++ b/src/models/shared_string.rs @@ -0,0 +1,128 @@ +//! One entry in the shared-string table, and why an entry needs two views. + +use crate::{ + error::Result, + models::types::{Type, TypeEntry}, +}; + +/// One entry in the deserializer's shared-string table. +/// +/// `NSArchiver` writes each string into the stream once, as a literal, and +/// refers to it by index thereafter. Type descriptors, class names, +/// selectors, and `char *` values all go through this one table, so the index +/// space is shared among all of them. This matters because a single entry can +/// play more than one role: `NSValue` writes its `objCType` as a `char *` +/// value, then types the payload that follows with a reference to that same +/// string. The first use wants the text; the second wants it parsed as a +/// descriptor. +/// +/// To accomplish this, every entry keeps its text, and the descriptor view is +/// parsed at most once. A descriptor literal is parsed at registration, since +/// the deserializer is about to read values with it. A name or value is not, +/// since a class name is never a valid descriptor and parsing it would fail; +/// its view is parsed only if a descriptor reference later resolves to it. +/// This means an entry never changes meaning: the text is always there, and a +/// parsed view, once present, is exactly what the text says. +/// +/// Equality compares the text alone. The parsed view is a cache of the text, +/// so two entries with the same text are the same string whether or not +/// either has been used as a descriptor yet. +#[derive(Clone)] +pub struct SharedString<'a> { + /// The text, borrowed from the stream. + pub text: &'a str, + /// The descriptor view, once something has needed it. + parsed: Option, +} + +impl<'a> SharedString<'a> { + /// A freshly registered string, with no descriptor view yet. + #[must_use] + pub const fn new(text: &'a str) -> Self { + Self { text, parsed: None } + } + + /// The descriptor view, parsing the text the first time it is asked for. + /// + /// `max_slots` is the number of stream bytes that follow the descriptor. + /// Since every slot reads at least one byte, an array that expands past + /// that count cannot be satisfied by the stream, and the parser rejects it + /// before allocating for it. + pub fn descriptor(&mut self, max_slots: usize) -> Result<&TypeEntry> { + if self.parsed.is_none() { + self.parsed = Some(Type::parse_descriptor(self.text, max_slots)?); + } + Ok(self.parsed.as_ref().expect("set above")) + } + + /// The descriptor view, if something has already parsed it. + #[must_use] + pub fn parsed(&self) -> Option<&TypeEntry> { + self.parsed.as_ref() + } +} + +impl PartialEq for SharedString<'_> { + fn eq(&self, other: &Self) -> bool { + self.text == other.text + } +} + +impl PartialEq for SharedString<'_> { + fn eq(&self, other: &str) -> bool { + self.text == other + } +} + +impl PartialEq<&str> for SharedString<'_> { + fn eq(&self, other: &&str) -> bool { + self.text == *other + } +} + +impl core::fmt::Debug for SharedString<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{:?}", self.text)?; + if let Some(parsed) = &self.parsed { + write!(f, " ({parsed:?})")?; + } + Ok(()) + } +} + +impl core::fmt::Display for SharedString<'_> { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str(self.text) + } +} + +#[cfg(test)] +mod tests { + use super::SharedString; + use crate::models::types::{Type, TypeEntry}; + + /// The table is plain data: no interior mutability, so it stays `Sync`. + #[test] + fn shared_string_is_send_and_sync() { + fn assert_send_sync() {} + assert_send_sync::>(); + } + + #[test] + fn descriptor_parses_once_and_equality_ignores_it() { + let mut entry = SharedString::new("iI"); + assert_eq!(entry.parsed(), None); + assert_eq!( + entry.descriptor(8).unwrap(), + &TypeEntry::Many(alloc::vec![Type::SignedInt, Type::UnsignedInt]) + ); + assert!(entry.parsed().is_some()); + assert_eq!(entry, SharedString::new("iI")); + assert_eq!(entry, "iI"); + assert_ne!(entry, SharedString::new("iC")); + // A name is never a valid descriptor; the failure is not cached. + let mut name = SharedString::new("NSString"); + assert!(name.descriptor(8).is_err()); + assert_eq!(name.parsed(), None); + } +} diff --git a/src/models/types.rs b/src/models/types.rs index e1e1836..c6860f6 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -1,35 +1,58 @@ //! Type tags that denote the type of data stored in a `typedstream` -use crate::{ - deserializer::{ - constants::ARRAY, consumed::Consumed, number::read_unsigned_int, read::read_exact_bytes, - }, - error::{Result, TypedStreamError}, -}; +use crate::error::{Result, TypedStreamError}; use alloc::vec::Vec; /// Represents primitive types of data that can be stored in a `typedstream` /// /// These type encodings are partially documented [here](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1) by Apple. #[derive(Debug, Clone, Copy, PartialEq)] -pub enum Type<'a> { +pub enum Type { /// Encoded string data, usually embedded in an object. Denoted by: /// /// | Hex | UTF-8 | /// |--------|-------| - /// | `0x28` | [`+`](https://www.compart.com/en/unicode/U+002B) | + /// | `0x2B` | [`+`](https://www.compart.com/en/unicode/U+002B) | Utf8String, - /// Encoded bytes that can be parsed again as data. Denoted by: + /// A C string (`char *`) encoded as a nullable shared pointer: + /// [`EMPTY`](crate::deserializer::constants::EMPTY) for `NULL`; otherwise, + /// a new object-table slot with a shared-string index or a reference to an + /// earlier pointer's slot. Denoted by: /// /// | Hex | UTF-8 | /// |--------|-------| /// | `0x2A` | [`*`](https://www.compart.com/en/unicode/U+002A) | - EmbeddedData, + CString, + /// A method selector (`SEL`), written as a shared string: the first + /// occurrence is a literal, later ones are references to it, and a `NULL` + /// selector is [`EMPTY`](crate::deserializer::constants::EMPTY). Denoted by: + /// + /// | Hex | UTF-8 | + /// |--------|-------| + /// | `0x3A` | [`:`](https://www.compart.com/en/unicode/U+003A) | + Selector, + /// A `NeXTSTEP` atom (`NXAtom`): a uniqued C string, the ancestor of `SEL` + /// interning. Since an atom is unique by text it is written exactly like + /// a selector: a shared string, with + /// [`EMPTY`](crate::deserializer::constants::EMPTY) for `NULL` and no + /// object-table slot. Denoted by: + /// + /// | Hex | UTF-8 | + /// |--------|-------| + /// | `0x25` | [`%`](https://www.compart.com/en/unicode/U+0025) | + Atom, /// An instance of a class, usually with data. Denoted by: /// /// | Hex | UTF-8 | /// |--------|-------| /// | `0x40` | [`@`](https://www.compart.com/en/unicode/U+0040) | Object, + /// A class reference (`Class`), written as a class chain: name, version, + /// and superclass, exactly as an object's class header. Denoted by: + /// + /// | Hex | UTF-8 | + /// |--------|-------| + /// | `0x23` | [`#`](https://www.compart.com/en/unicode/U+0023) | + Class, /// An [`i8`], [`i16`], or [`i32`]. Denoted by: /// | Hex | UTF-8 | /// |--------|-------| @@ -49,8 +72,10 @@ pub enum Type<'a> { /// | `0x4c` | [`L`](https://www.compart.com/en/unicode/U+004c) | /// | `0x51` | [`Q`](https://www.compart.com/en/unicode/U+0051) | /// | `0x53` | [`S`](https://www.compart.com/en/unicode/U+0053) | + /// | `0x42` | [`B`](https://www.compart.com/en/unicode/U+0042) | /// /// The width is determined by the prefix: [`u8`] has none, [`u16`] has `0x81`, and [`u32`] has `0x82`. + /// `B` is C's `_Bool` (`BOOL` on arm64): one byte holding 0 or 1, read like `C`. UnsignedInt, /// An [`f32`]. Denoted by: /// @@ -64,99 +89,174 @@ pub enum Type<'a> { /// |--------|-------| /// | `0x64` | [`d`](https://www.compart.com/en/unicode/U+0064) | Double, - /// Some text we can reuse later, i.e. a class name. - String(&'a str), - /// An array containing some data of a given length. Denoted in the stream by braced digits: `[123]`. + /// `length` raw bytes: a `char` array, `[Nc]` or `[NC]`. An array of any + /// other element type is expanded to `N` copies of the element's types, + /// because `NSArchiver` writes each element with its own encoding. Array(usize), - /// Data for which we do not know the type, likely for something this parser does not implement. - Unknown(u8), } -impl<'a> Type<'a> { - /// Convert a byte to a Type enum variant - #[inline] - pub(crate) fn from_byte(byte: u8) -> Self { - match byte { - 0x40 => Self::Object, - 0x2B => Self::Utf8String, - 0x2A => Self::EmbeddedData, - 0x66 => Self::Float, - 0x64 => Self::Double, - 0x63 | 0x69 | 0x6c | 0x71 | 0x73 => Self::SignedInt, - 0x43 | 0x49 | 0x4c | 0x51 | 0x53 => Self::UnsignedInt, - other => Self::Unknown(other), +impl Type { + /// Parses a type descriptor into its slot types. + /// + /// Input: an Objective-C type-encoding string. + /// Leaf-value order: struct members flat; array elements one after another. + /// Thus `{_NSRange=QQ}` is two unsigned ints, `[2i]` is two signed ints, and + /// `i{?=ii}i` is four signed ints. Exception: keep a `char` array, `[Nc]`, + /// as `N` raw bytes. + /// + /// Use `max_slots` to bound the bytes available after the descriptor. At + /// least one byte belongs to each decoded slot; an array that expands past + /// this bound is malformed. + pub(crate) fn parse_descriptor(text: &str, max_slots: usize) -> Result { + let bytes = text.as_bytes(); + + // The overwhelming majority of descriptors are a single scalar letter, + // so keep that case off the heap. + if let [byte] = bytes + && let Some(ty) = Type::scalar(*byte) + { + return Ok(TypeEntry::One(ty)); } + + let mut types = Vec::new(); + let mut pos = 0; + while pos < bytes.len() { + Type::parse_one(bytes, &mut pos, &mut types, max_slots)?; + } + Ok(if types.len() == 1 { + TypeEntry::One(types.pop().unwrap()) + } else { + TypeEntry::Many(types) + }) } + /// The type a single non-aggregate encoding character denotes, if any. #[inline] - pub(crate) fn new_string(str: &'a str) -> Self { - Self::String(str) + fn scalar(byte: u8) -> Option { + Some(match byte { + b'@' => Self::Object, + b'#' => Self::Class, + b':' => Self::Selector, + b'%' => Self::Atom, + b'*' => Self::CString, + b'+' => Self::Utf8String, + b'f' => Self::Float, + b'd' => Self::Double, + b'c' | b'i' | b'l' | b'q' | b's' => Self::SignedInt, + b'C' | b'I' | b'L' | b'Q' | b'S' | b'B' => Self::UnsignedInt, + _ => return None, + }) } - /// Parse the decimal length of an array descriptor of the form `[123]`, - /// returning the length once the leading `[` and at least one digit are seen. - pub(crate) fn get_array_length(types: &'_ [u8]) -> Option { - if types.first() != Some(&ARRAY) { - return None; + /// Parses one type at `pos`, appending its leaf types to `out` and leaving + /// `pos` on the byte after it. `max_slots` caps any single array expansion. + fn parse_one( + bytes: &[u8], + pos: &mut usize, + out: &mut Vec, + max_slots: usize, + ) -> Result<()> { + // Skip method qualifiers (`const`, `in`, `out`, ...) ahead of the type. + // `NSArchiver` strips them, so this is defensive. + while matches!( + bytes.get(*pos), + Some(b'r' | b'n' | b'N' | b'o' | b'O' | b'R' | b'V') + ) { + *pos += 1; } - let mut len = 0usize; - let mut saw_digit = false; - for &byte in &types[1..] { - if !byte.is_ascii_digit() { - break; + let Some(&byte) = bytes.get(*pos) else { + return Err(TypedStreamError::InvalidType(0)); + }; + *pos += 1; + + match byte { + b'[' => { + // `[` count element `]` + let mut count = 0usize; + let mut saw_digit = false; + while let Some(digit) = bytes.get(*pos).filter(|b| b.is_ascii_digit()) { + count = count + .checked_mul(10) + .and_then(|c| c.checked_add(usize::from(digit - b'0'))) + .ok_or(TypedStreamError::InvalidArray(*pos))?; + saw_digit = true; + *pos += 1; + } + if !saw_digit { + return Err(TypedStreamError::InvalidArray(*pos)); + } + // A `char` array is the one aggregate kept whole: `N` raw bytes. + if let Some(b'c' | b'C') = bytes.get(*pos) + && bytes.get(*pos + 1) == Some(&b']') + { + *pos += 2; + out.push(Type::Array(count)); + return Ok(()); + } + // Any other element: its leaves, `count` times over. + let mut element = Vec::new(); + Type::parse_one(bytes, pos, &mut element, max_slots)?; + if bytes.get(*pos) != Some(&b']') { + return Err(TypedStreamError::InvalidType(b'[')); + } + *pos += 1; + if count + .checked_mul(element.len()) + .is_none_or(|slots| slots > max_slots) + { + return Err(TypedStreamError::InvalidArray(*pos)); + } + for _ in 0..count { + out.extend_from_slice(&element); + } + Ok(()) + } + b'{' => { + // `{` name [`=` member*] `}`. Skip the name rather than scanning + // it: `C`, `S`, and `i` in `CGSize` are not slots. + while let Some(&b) = bytes.get(*pos) { + *pos += 1; + match b { + b'=' => break, + b'}' => return Ok(()), + _ => {} + } + } + loop { + match bytes.get(*pos) { + Some(b'}') => { + *pos += 1; + return Ok(()); + } + Some(_) => Type::parse_one(bytes, pos, out, max_slots)?, + None => return Err(TypedStreamError::InvalidType(b'{')), + } + } + } + // Encodings NSArchiver refuses to write (unions, bitfields, pointers) + // or with no value at all (`void`, unknown `?`). + b'(' | b'b' | b'^' | b'v' | b'?' => Err(TypedStreamError::InvalidType(byte)), + other => { + out.push(Type::scalar(other).ok_or(TypedStreamError::InvalidType(other))?); + Ok(()) } - len = len * 10 + usize::from(byte - b'0'); - saw_digit = true; } - saw_digit.then_some(len) - } - - pub(crate) fn read_new_type(data: &'_ [u8]) -> Result>> { - // Get the type of the object - let type_length = read_unsigned_int(data)?; - - // Get the bytes for the type - let type_bytes = read_exact_bytes( - &data[type_length.bytes_consumed..], - type_length.value as usize, - )?; - let bytes_consumed = type_length.bytes_consumed + type_bytes.len(); - - // Handle array size - if type_bytes.first() == Some(&ARRAY) { - let len = - Type::get_array_length(type_bytes).ok_or(TypedStreamError::InvalidArray(0))?; - return Ok(Consumed::new( - TypeEntry::One(Type::Array(len)), - bytes_consumed, - )); - } - - // The overwhelming majority of type descriptors are a single type, so - // keep that case off the heap. - let entry = if let [byte] = type_bytes { - TypeEntry::One(Type::from_byte(*byte)) - } else { - TypeEntry::Many(type_bytes.iter().copied().map(Type::from_byte).collect()) - }; - Ok(Consumed::new(entry, bytes_consumed)) } } -/// One entry in the deserializer's type table. +/// The parsed view of a type descriptor: the types it denotes, in slot order. /// /// A type descriptor usually resolves to a single [`Type`]; storing that inline -/// avoids a heap allocation per entry (the table was previously a -/// `Vec>`). Multi-type descriptors fall back to a [`Vec`]. +/// avoids a heap allocation per entry. Multi-type descriptors fall back to a [`Vec`]. #[derive(Debug, Clone, PartialEq)] -pub enum TypeEntry<'a> { +pub enum TypeEntry { /// A single type, stored inline. - One(Type<'a>), + One(Type), /// Two or more types. - Many(Vec>), + Many(Vec), } -impl<'a> TypeEntry<'a> { +impl TypeEntry { /// The number of types in this entry. #[must_use] pub fn len(&self) -> usize { @@ -174,28 +274,16 @@ impl<'a> TypeEntry<'a> { /// The first type in the entry, if any. #[must_use] - pub fn first(&self) -> Option<&Type<'a>> { + pub fn first(&self) -> Option<&Type> { match self { TypeEntry::One(ty) => Some(ty), TypeEntry::Many(types) => types.first(), } } - - /// Build a [`TypeEntry`] from a list of types, normalizing the single-type - /// case to [`TypeEntry::One`]. Used by tests to express expected type tables - /// in the pre-existing nested style. - #[cfg(test)] - pub(crate) fn from_types(mut types: Vec>) -> Self { - if types.len() == 1 { - TypeEntry::One(types.pop().unwrap()) - } else { - TypeEntry::Many(types) - } - } } -impl<'a> core::ops::Index for TypeEntry<'a> { - type Output = Type<'a>; +impl core::ops::Index for TypeEntry { + type Output = Type; fn index(&self, index: usize) -> &Self::Output { match self { @@ -207,3 +295,157 @@ impl<'a> core::ops::Index for TypeEntry<'a> { } } } + +#[cfg(test)] +mod type_encoding_tests { + use alloc::vec; + + use super::{Type, TypeEntry}; + use crate::error::TypedStreamError; + + /// Parses an encoding string with `remaining` bytes of stream after it, + /// which bound array expansion. + fn parse_with(encoding: &str, remaining: usize) -> Result { + Type::parse_descriptor(encoding, remaining) + } + + fn parse(encoding: &str) -> Result { + parse_with(encoding, 64) + } + + #[test] + fn scalars() { + for (encoding, expected) in [ + ("@", Type::Object), + ("#", Type::Class), + (":", Type::Selector), + ("%", Type::Atom), + ("*", Type::CString), + ("+", Type::Utf8String), + ("f", Type::Float), + ("d", Type::Double), + ("c", Type::SignedInt), + ("q", Type::SignedInt), + ("C", Type::UnsignedInt), + ("Q", Type::UnsignedInt), + ("B", Type::UnsignedInt), + ] { + assert_eq!( + parse(encoding).unwrap(), + TypeEntry::One(expected), + "{encoding}" + ); + } + assert_eq!( + parse("iI").unwrap(), + TypeEntry::Many(vec![Type::SignedInt, Type::UnsignedInt]) + ); + // NSArchiver never writes an empty descriptor; parse it as no slots anyway. + assert_eq!(parse("").unwrap(), TypeEntry::Many(vec![])); + } + + #[test] + fn qualifiers_are_skipped() { + assert_eq!(parse("ri").unwrap(), TypeEntry::One(Type::SignedInt)); + assert_eq!(parse("oNi").unwrap(), TypeEntry::One(Type::SignedInt)); + } + + #[test] + fn structs_flatten_and_names_are_not_scanned() { + use Type::{Double as D, SignedInt as I, UnsignedInt as U}; + assert_eq!(parse("{_NSRange=QQ}").unwrap(), TypeEntry::Many(vec![U, U])); + // `C`, `S`, `i` in the name are letters, not slots. + assert_eq!(parse("{CGSize=dd}").unwrap(), TypeEntry::Many(vec![D, D])); + assert_eq!( + parse("{CGRect={CGPoint=dd}{CGSize=dd}}").unwrap(), + TypeEntry::Many(vec![D, D, D, D]) + ); + assert_eq!( + parse("{?={?=ii}q}").unwrap(), + TypeEntry::Many(vec![I, I, I]) + ); + assert_eq!( + parse("i{?=ii}i").unwrap(), + TypeEntry::Many(vec![I, I, I, I]) + ); + // Opaque and empty structs have no slots. + assert_eq!(parse("{opaque}").unwrap(), TypeEntry::Many(vec![])); + assert_eq!(parse("{empty=}").unwrap(), TypeEntry::Many(vec![])); + assert_eq!(parse("i{empty=}i").unwrap(), TypeEntry::Many(vec![I, I])); + } + + #[test] + fn arrays() { + use Type::{Array as A, SignedInt as I}; + // `char` arrays stay whole as raw bytes; everything else is expanded. + assert_eq!(parse("[4c]").unwrap(), TypeEntry::One(A(4))); + assert_eq!(parse("[648C]").unwrap(), TypeEntry::One(A(648))); + assert_eq!(parse("[2i]").unwrap(), TypeEntry::Many(vec![I, I])); + assert_eq!(parse("[3s]").unwrap(), TypeEntry::Many(vec![I, I, I])); + assert_eq!(parse("[2[2c]]").unwrap(), TypeEntry::Many(vec![A(2), A(2)])); + assert_eq!( + parse("[2{?=ic}]").unwrap(), + TypeEntry::Many(vec![I, I, I, I]) + ); + assert_eq!(parse("[0i]").unwrap(), TypeEntry::Many(vec![])); + assert_eq!(parse("[1i]").unwrap(), TypeEntry::One(I)); + assert_eq!(parse("i[2c]i").unwrap(), TypeEntry::Many(vec![I, A(2), I])); + } + + #[test] + fn unencodable_and_malformed_are_errors() { + for (encoding, byte) in [ + ("(u=if)", b'('), + ("b3", b'b'), + ("^i", b'^'), + ("v", b'v'), + ("?", b'?'), + ("x", b'x'), + ("{?=ii", b'{'), + ("[2i", b'['), + ("i(u=if)", b'('), + ] { + assert!( + matches!(parse(encoding), Err(TypedStreamError::InvalidType(b)) if b == byte), + "{encoding}" + ); + } + assert!(matches!( + parse("[i]"), + Err(TypedStreamError::InvalidArray(_)) + )); + assert!(matches!( + parse("[]"), + Err(TypedStreamError::InvalidArray(_)) + )); + } + + #[test] + fn array_expansion_is_bounded_by_the_stream() { + use Type::{Array as A, SignedInt as I}; + // Exactly enough bytes for the slots is fine; one short is not. + assert_eq!(parse_with("[4i]", 4).unwrap(), TypeEntry::Many(vec![I; 4])); + assert!(matches!( + parse_with("[4i]", 3), + Err(TypedStreamError::InvalidArray(_)) + )); + assert!(matches!( + parse_with("[2{?=ii}]", 3), + Err(TypedStreamError::InvalidArray(_)) + )); + // Rejected before any allocation, including on multiplication overflow. + assert!(matches!( + parse_with("[4000000000i]", 8), + Err(TypedStreamError::InvalidArray(_)) + )); + assert!(matches!( + parse_with("[18446744073709551615{?=ii}]", 8), + Err(TypedStreamError::InvalidArray(_)) + )); + // A byte array is one slot however long; its bound is checked when read. + assert_eq!( + parse_with("[4000000000c]", 0).unwrap(), + TypeEntry::One(A(4_000_000_000)) + ); + } +} diff --git a/src/test_data/Address b/src/test_data/Address new file mode 100644 index 0000000..f4eea11 Binary files /dev/null and b/src/test_data/Address differ diff --git a/src/test_data/Arabic b/src/test_data/Arabic new file mode 100644 index 0000000..eaf2091 Binary files /dev/null and b/src/test_data/Arabic differ diff --git a/src/test_data/Currency b/src/test_data/Currency new file mode 100644 index 0000000..00a492b Binary files /dev/null and b/src/test_data/Currency differ diff --git a/src/test_data/CurrencyTemperatureVolumeWeight b/src/test_data/CurrencyTemperatureVolumeWeight new file mode 100644 index 0000000..fbd5a0a Binary files /dev/null and b/src/test_data/CurrencyTemperatureVolumeWeight differ diff --git a/src/test_data/Flight b/src/test_data/Flight new file mode 100644 index 0000000..68abb52 Binary files /dev/null and b/src/test_data/Flight differ diff --git a/src/test_data/SDLMainNib b/src/test_data/SDLMainNib new file mode 100644 index 0000000..3b7ed12 Binary files /dev/null and b/src/test_data/SDLMainNib differ diff --git a/src/test_data/Tracking b/src/test_data/Tracking new file mode 100644 index 0000000..fd20f14 Binary files /dev/null and b/src/test_data/Tracking differ diff --git a/src/test_data/foundation/Atoms b/src/test_data/foundation/Atoms new file mode 100644 index 0000000..acb0854 Binary files /dev/null and b/src/test_data/foundation/Atoms differ diff --git a/src/test_data/foundation/CStrings b/src/test_data/foundation/CStrings new file mode 100644 index 0000000..babb7f5 Binary files /dev/null and b/src/test_data/foundation/CStrings differ diff --git a/src/test_data/foundation/NSArray b/src/test_data/foundation/NSArray new file mode 100644 index 0000000..56670c3 Binary files /dev/null and b/src/test_data/foundation/NSArray differ diff --git a/src/test_data/foundation/NSArrayEmpty b/src/test_data/foundation/NSArrayEmpty new file mode 100644 index 0000000..71b3135 Binary files /dev/null and b/src/test_data/foundation/NSArrayEmpty differ diff --git a/src/test_data/foundation/NSArrayNested b/src/test_data/foundation/NSArrayNested new file mode 100644 index 0000000..7929afe Binary files /dev/null and b/src/test_data/foundation/NSArrayNested differ diff --git a/src/test_data/foundation/NSArrayWithNull b/src/test_data/foundation/NSArrayWithNull new file mode 100644 index 0000000..988a9a3 Binary files /dev/null and b/src/test_data/foundation/NSArrayWithNull differ diff --git a/src/test_data/foundation/NSAttributedString b/src/test_data/foundation/NSAttributedString new file mode 100644 index 0000000..7849f94 Binary files /dev/null and b/src/test_data/foundation/NSAttributedString differ diff --git a/src/test_data/foundation/NSData b/src/test_data/foundation/NSData new file mode 100644 index 0000000..52f5657 Binary files /dev/null and b/src/test_data/foundation/NSData differ diff --git a/src/test_data/foundation/NSDate b/src/test_data/foundation/NSDate new file mode 100644 index 0000000..e2cab08 Binary files /dev/null and b/src/test_data/foundation/NSDate differ diff --git a/src/test_data/foundation/NSDateFractional b/src/test_data/foundation/NSDateFractional new file mode 100644 index 0000000..b590f35 Binary files /dev/null and b/src/test_data/foundation/NSDateFractional differ diff --git a/src/test_data/foundation/NSDictionary b/src/test_data/foundation/NSDictionary new file mode 100644 index 0000000..6ab0795 Binary files /dev/null and b/src/test_data/foundation/NSDictionary differ diff --git a/src/test_data/foundation/NSDictionaryNested b/src/test_data/foundation/NSDictionaryNested new file mode 100644 index 0000000..668066a Binary files /dev/null and b/src/test_data/foundation/NSDictionaryNested differ diff --git a/src/test_data/foundation/NSMutableArray b/src/test_data/foundation/NSMutableArray new file mode 100644 index 0000000..40bbfe6 Binary files /dev/null and b/src/test_data/foundation/NSMutableArray differ diff --git a/src/test_data/foundation/NSMutableData b/src/test_data/foundation/NSMutableData new file mode 100644 index 0000000..cb9ee0d Binary files /dev/null and b/src/test_data/foundation/NSMutableData differ diff --git a/src/test_data/foundation/NSMutableDictionary b/src/test_data/foundation/NSMutableDictionary new file mode 100644 index 0000000..66b9189 Binary files /dev/null and b/src/test_data/foundation/NSMutableDictionary differ diff --git a/src/test_data/foundation/NSMutableSet b/src/test_data/foundation/NSMutableSet new file mode 100644 index 0000000..200a16d Binary files /dev/null and b/src/test_data/foundation/NSMutableSet differ diff --git a/src/test_data/foundation/NSMutableString b/src/test_data/foundation/NSMutableString new file mode 100644 index 0000000..239177d Binary files /dev/null and b/src/test_data/foundation/NSMutableString differ diff --git a/src/test_data/foundation/NSSet b/src/test_data/foundation/NSSet new file mode 100644 index 0000000..99b1d23 Binary files /dev/null and b/src/test_data/foundation/NSSet differ diff --git a/src/test_data/foundation/NSString b/src/test_data/foundation/NSString new file mode 100644 index 0000000..03c0d57 Binary files /dev/null and b/src/test_data/foundation/NSString differ diff --git a/src/test_data/foundation/NSURL b/src/test_data/foundation/NSURL new file mode 100644 index 0000000..df613a8 Binary files /dev/null and b/src/test_data/foundation/NSURL differ diff --git a/src/test_data/foundation/NSURLRelative b/src/test_data/foundation/NSURLRelative new file mode 100644 index 0000000..298a6b9 Binary files /dev/null and b/src/test_data/foundation/NSURLRelative differ diff --git a/src/test_data/foundation/NSValueRange b/src/test_data/foundation/NSValueRange new file mode 100644 index 0000000..5a95b2f Binary files /dev/null and b/src/test_data/foundation/NSValueRange differ diff --git a/src/test_data/foundation/NSValueRect b/src/test_data/foundation/NSValueRect new file mode 100644 index 0000000..4191a39 Binary files /dev/null and b/src/test_data/foundation/NSValueRect differ diff --git a/src/test_data/foundation/NSValueSize b/src/test_data/foundation/NSValueSize new file mode 100644 index 0000000..2ef3654 Binary files /dev/null and b/src/test_data/foundation/NSValueSize differ diff --git a/src/test_data/foundation/NestedAttributed b/src/test_data/foundation/NestedAttributed new file mode 100644 index 0000000..70b8697 Binary files /dev/null and b/src/test_data/foundation/NestedAttributed differ diff --git a/src/test_data/foundation/NestedContainers b/src/test_data/foundation/NestedContainers new file mode 100644 index 0000000..3ad54ff Binary files /dev/null and b/src/test_data/foundation/NestedContainers differ diff --git a/src/test_data/foundation/NestedData b/src/test_data/foundation/NestedData new file mode 100644 index 0000000..212f2fa Binary files /dev/null and b/src/test_data/foundation/NestedData differ diff --git a/src/test_data/foundation/NestedScalars b/src/test_data/foundation/NestedScalars new file mode 100644 index 0000000..ebf2348 Binary files /dev/null and b/src/test_data/foundation/NestedScalars differ diff --git a/src/test_data/foundation/NestedStrings b/src/test_data/foundation/NestedStrings new file mode 100644 index 0000000..0ae47f6 Binary files /dev/null and b/src/test_data/foundation/NestedStrings differ diff --git a/src/test_data/foundation/NumberBool b/src/test_data/foundation/NumberBool new file mode 100644 index 0000000..3b5e3b9 Binary files /dev/null and b/src/test_data/foundation/NumberBool differ diff --git a/src/test_data/foundation/NumberDouble b/src/test_data/foundation/NumberDouble new file mode 100644 index 0000000..9661076 Binary files /dev/null and b/src/test_data/foundation/NumberDouble differ diff --git a/src/test_data/foundation/NumberFloat b/src/test_data/foundation/NumberFloat new file mode 100644 index 0000000..bc9553e Binary files /dev/null and b/src/test_data/foundation/NumberFloat differ diff --git a/src/test_data/foundation/NumberInt b/src/test_data/foundation/NumberInt new file mode 100644 index 0000000..dace512 Binary files /dev/null and b/src/test_data/foundation/NumberInt differ diff --git a/src/test_data/foundation/NumberInt64 b/src/test_data/foundation/NumberInt64 new file mode 100644 index 0000000..1e906a2 Binary files /dev/null and b/src/test_data/foundation/NumberInt64 differ diff --git a/test_data/generators/foundation.swift b/test_data/generators/foundation.swift new file mode 100644 index 0000000..8eb848b --- /dev/null +++ b/test_data/generators/foundation.swift @@ -0,0 +1,99 @@ +// Generates the Foundation typedstream test fixtures in src/test_data/foundation/ +// using the *classic* NSArchiver (the typedstream archiver). Run via generate.sh. +// +// One object is archived per process invocation: a class that refuses non-keyed +// archiving raises an NSException that aborts only this run (caught by the script +// as a nonzero exit), instead of taking down the whole batch. +// +// NSArchiver is deprecated but still emits `streamtyped` on current macOS. Only +// Tier-1 Foundation classes are generated here (see FOUNDATION_FEATURE_SCOPING.md); +// AppKit/Tier-2 classes are intentionally excluded. + +import Foundation + +func make(_ key: String) -> Any? { + switch key { + // string cluster + case "NSString": return NSString(string: "Hello, world") + case "NSMutableString": return NSMutableString(string: "Hello, world") + case "NSAttributedString": return NSAttributedString(string: "Hello, world") + // data cluster + case "NSData": return NSData(bytes: [0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x7F] as [UInt8], length: 6) + case "NSMutableData": return NSMutableData(bytes: [0xDE, 0xAD, 0xBE, 0xEF] as [UInt8], length: 4) + // numbers (exactly-representable values so tests can assert equality) + case "NumberBool": return NSNumber(value: true) + case "NumberInt": return NSNumber(value: Int32(42)) + case "NumberFloat": return NSNumber(value: Float(3.5)) + case "NumberDouble": return NSNumber(value: Double(100.5)) + case "NumberInt64": return NSNumber(value: Int64(-9_000_000_000)) // exercises the 8-byte form + // arrays + case "NSArray": return NSArray(array: [NSString(string: "a"), NSNumber(value: 1), NSString(string: "b")]) + case "NSMutableArray": return NSMutableArray(array: [NSString(string: "x"), NSNumber(value: 2)]) + case "NSArrayEmpty": return NSArray(array: []) + case "NSArrayWithNull": return NSArray(array: [NSString(string: "x"), NSNull(), NSString(string: "y")]) + case "NSArrayNested": return NSArray(array: [NSString(string: "top"), NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)])]) + // dictionaries + case "NSDictionary": return NSDictionary(objects: [NSNumber(value: 1), NSString(string: "v")], forKeys: [NSString(string: "k1"), NSString(string: "k2")]) + case "NSMutableDictionary": + let d = NSMutableDictionary() + d.setObject(NSNumber(value: 9), forKey: NSString(string: "key")) + return d + case "NSDictionaryNested": + return NSDictionary( + objects: [ + NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)]), + NSData(bytes: [0x01, 0x02] as [UInt8], length: 2), + NSNumber(value: 7), + ], + forKeys: [NSString(string: "arr"), NSString(string: "data"), NSString(string: "num")] + ) + // sets + case "NSSet": return NSSet(array: [NSString(string: "s1"), NSString(string: "s2")]) + case "NSMutableSet": return NSMutableSet(array: [NSString(string: "m1")]) + // dates (integral dodges the DECIMAL path; fractional exercises it) + case "NSDate": return NSDate(timeIntervalSinceReferenceDate: 21692800) // unix 1_000_000_000 + case "NSDateFractional": return NSDate(timeIntervalSinceReferenceDate: 700000000.523) + // urls + case "NSURL": return NSURL(string: "https://example.com/path?q=1") + case "NSURLRelative": return NSURL(string: "page.html", relativeTo: URL(string: "https://example.com/dir/")) + // nested fixtures: accessors operate on objects wrapped in a group, so these + // place each cluster variant as an array element (a root object's groups are + // its contents, not a wrapper around it). + case "NestedStrings": return NSArray(array: [NSString(string: "imm"), NSMutableString(string: "mut")]) + case "NestedData": return NSArray(array: [NSData(bytes: [0x01, 0x02] as [UInt8], length: 2), NSMutableData(bytes: [0x03, 0x04, 0x05] as [UInt8], length: 3)]) + case "NestedAttributed": return NSArray(array: [NSAttributedString(string: "styled")]) + case "NestedContainers": + // Nested containers (both cluster variants + an empty array) so the + // container accessors can be tested on objects wrapped in a group. + let md = NSMutableDictionary() + md.setObject(NSNumber(value: 8), forKey: NSString(string: "mk")) + return NSArray(array: [ + NSArray(array: [NSNumber(value: 1), NSNumber(value: 2)]), + NSMutableArray(array: [NSNumber(value: 3)]), + NSArray(array: []), + NSDictionary(objects: [NSNumber(value: 9)], forKeys: [NSString(string: "k")]), + md, + NSSet(array: [NSString(string: "s")]), + NSMutableSet(array: [NSString(string: "ms")]), + ]) + case "NestedScalars": + // NSDate / NSURL (absolute + relative) / NSNull as array elements, so the + // Phase 4 accessors can be tested on objects wrapped in a group. + return NSArray(array: [ + NSDate(timeIntervalSinceReferenceDate: 21692800), // unix 1_000_000_000 + NSURL(string: "https://example.com/path?q=1")!, + NSURL(string: "page.html", relativeTo: URL(string: "https://example.com/dir/"))!, + NSNull(), + ]) + default: return nil + } +} + +let args = CommandLine.arguments +guard args.count >= 3, let obj = make(args[1]) else { + FileHandle.standardError.write("skip \(args.count >= 2 ? args[1] : "?")\n".data(using: .utf8)!) + exit(2) +} +let data = NSArchiver.archivedData(withRootObject: obj) +try! data.write(to: URL(fileURLWithPath: args[2])) +print("ok \(args[1]) -> \(data.count) bytes") diff --git a/test_data/generators/generate.sh b/test_data/generators/generate.sh new file mode 100755 index 0000000..9279932 --- /dev/null +++ b/test_data/generators/generate.sh @@ -0,0 +1,33 @@ +#!/bin/sh +# Regenerate the Foundation typedstream test fixtures with the classic `NSArchiver`. +# +# macOS only (requires Swift + Foundation). The emitted fixtures live in +# src/test_data/foundation/ and are committed, so the Rust tests run on any +# platform / in CI; only regeneration needs a Mac. `NSArchiver` is deprecated, so +# `swiftc` prints a deprecation warning. +set -eu + +here="$(cd "$(dirname "$0")" && pwd)" +repo="$(cd "$here/../.." && pwd)" +out="$repo/src/test_data/foundation" +mkdir -p "$out" + +bin="$(mktemp -t foundation_gen)" +trap 'rm -f "$bin"' EXIT +swiftc -o "$bin" "$here/foundation.swift" + +keys="NSString NSMutableString NSAttributedString \ +NSData NSMutableData \ +NumberBool NumberInt NumberFloat NumberDouble NumberInt64 \ +NSArray NSMutableArray NSArrayEmpty NSArrayWithNull NSArrayNested \ +NSDictionary NSMutableDictionary NSDictionaryNested \ +NSSet NSMutableSet \ +NSDate NSDateFractional \ +NSURL NSURLRelative \ +NestedStrings NestedData NestedAttributed NestedContainers NestedScalars" + +for k in $keys; do + "$bin" "$k" "$out/$k" || echo "FAILED: $k" +done + +echo "Fixtures written to $out"