-
Notifications
You must be signed in to change notification settings - Fork 4
Feat/cs/foundation api #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
8c3f8aa
Fix comment format
ReagentX d76066e
Add foundation feature scaffold
ReagentX 23c11a2
Add foundation test data
ReagentX 80a1e3b
Add nested test data
ReagentX d8e51c4
Implement typed accessors for `Foundation` classes
ReagentX f8f1d34
Add nested container tests
ReagentX 19b8df5
Add accessors for Foundation collections: arrays, sets, and dictionaries
ReagentX 7d5b469
Add support for NestedScalars in Foundation typedstream test fixtures
ReagentX 211bfd7
Add date, URL, and null accessors for Foundation properties
ReagentX 3147644
Include new crate features in ci tests
ReagentX 56101c6
Improve Foundation accessors with lazy views for arrays and dictionaries
ReagentX 84002b8
Re-export `Property`
ReagentX 88699cd
Refactor into type modules
ReagentX bdfa499
Add build steps for foundation features in CI workflows
ReagentX a9a874b
Update docs for Foundation accessors; improve error handling in deser…
ReagentX a1f13d2
Add test cases
ReagentX 1501e84
Better errors for out-of-bounds root access
ReagentX File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<FoundationArray<'a, 'b>> { | ||
| 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<FoundationArray<'a, 'b>> { | ||
| 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<Property<'a, 'b>> { | ||
| self.iter().nth(index) | ||
| } | ||
|
|
||
| /// The first element. | ||
| #[must_use] | ||
| pub fn first(&self) -> Option<Property<'a, 'b>> { | ||
| 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::Item> { | ||
| 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<Vec<i64>> = 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<Vec<&str>> = 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<Vec<i64>> = 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); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| //! `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<bool> { | ||
| 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(); | ||
| let group = ts.resolve_properties(root).unwrap().next().unwrap(); | ||
|
|
||
| assert_eq!(group.as_bool(), Some(true)); | ||
| assert_eq!(group.as_i64(), Some(1)); | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.