From 2eb4e2a2f70ec08e7fcdaac969d2d76344664714 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 12:35:55 -0700 Subject: [PATCH 01/41] Add new binary test data files for various new categories --- src/lib.rs | 1157 +++++++++++++++++ src/test_data/Address | Bin 0 -> 1474 bytes src/test_data/Arabic | Bin 0 -> 200 bytes src/test_data/Currency | Bin 0 -> 205 bytes src/test_data/CurrencyTemperatureVolumeWeight | Bin 0 -> 2670 bytes src/test_data/Flight | Bin 0 -> 1046 bytes src/test_data/Tracking | Bin 0 -> 967 bytes 7 files changed, 1157 insertions(+) create mode 100644 src/test_data/Address create mode 100644 src/test_data/Arabic create mode 100644 src/test_data/Currency create mode 100644 src/test_data/CurrencyTemperatureVolumeWeight create mode 100644 src/test_data/Flight create mode 100644 src/test_data/Tracking diff --git a/src/lib.rs b/src/lib.rs index 61269ac..0ed20f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7103,4 +7103,1161 @@ 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.type_table.len()); + typedstream + .type_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 = 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(1187)]), + TypeEntry::from_types(vec![Type::String("NSNumber")]), + TypeEntry::from_types(vec![Type::String("NSValue")]), + TypeEntry::from_types(vec![Type::EmbeddedData]), + ]; + + 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::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::Type(7), + ]; + + assert_eq!(typedstream.type_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.type_table.len()); + typedstream + .type_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 = 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_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::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::Type(7), + ]; + + assert_eq!(typedstream.type_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.type_table.len()); + typedstream + .type_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 = 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_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::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::Type(7), + ]; + + assert_eq!(typedstream.type_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.type_table.len()); + typedstream + .type_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 = 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(764)]), + TypeEntry::from_types(vec![Type::Array(748)]), + TypeEntry::from_types(vec![Type::Array(760)]), + ]; + + 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::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::Type(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.type_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.type_table.len()); + typedstream + .type_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 = 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("NSData")]), + TypeEntry::from_types(vec![Type::Array(754)]), + TypeEntry::from_types(vec![Type::String("NSNumber")]), + TypeEntry::from_types(vec![Type::String("NSValue")]), + TypeEntry::from_types(vec![Type::EmbeddedData]), + ]; + + 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::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::Type(9), + ]; + + assert_eq!(typedstream.type_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.type_table.len()); + typedstream + .type_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 = 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("NSData")]), + TypeEntry::from_types(vec![Type::Array(664)]), + TypeEntry::from_types(vec![Type::String("NSNumber")]), + TypeEntry::from_types(vec![Type::String("NSValue")]), + TypeEntry::from_types(vec![Type::EmbeddedData]), + ]; + + 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::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::Type(9), + ]; + + assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.object_table, expected_objects); + } } diff --git a/src/test_data/Address b/src/test_data/Address new file mode 100644 index 0000000000000000000000000000000000000000..f4eea11f223057269ffb4626ea1123465316ba9f GIT binary patch literal 1474 zcma)6ZA?>F7(VZN`2M_9pio4r6a@-M5r&Sr(G_*XQKhA{fZE-*SGZEz=Ju9Z-OSNk zyAR@=$jDgs!?68Xj5Eu!WLbzagqUSnw#>PZ{n;Org)IJG7GvDGrLZvn?EbmuoO_?= zd7t+^@0(Q^R~1Q|Qj;+$ba9Pp(5q>h!RfZEsv-vxsuXhbMP!_4TAtH=EO15&s^s!4 z|A}wcKf#;^)&`r|9*cz~^Kns`G<(Hli`kKgNs1~*qI_vLALwegb*40w`=LRH7dhR1 za!{3{5m8AJP9fv0Ha}nO_fPf@*h3*jipMvDaEen>N=Fqt-2(|#41}dVQ5AVmIh;mc z%`IH~BumR3v)T4{2TulKVL7h0w?6=+S=l+cMLx^4q{O+{h(%O_6EfcoS=4CEXNjgQ z#SH{fH~d1W(|uG*N+EmZr~7&N1%=)XY$(*q!%Boz*2dtru(!BmyQN`wOKaP6FZ9}9 zcD&*=x!gnE(Nm!b`OIWknST9j@*LAHr{219ZT9-y2Odi#5e~1G3T1|E z$u-~}wgkgsJnpi)28RZZyWB3~0G9Wlc1N9H-npyZFYwB=DPm+?nij)t zVJR}MPWXlLEr-#dsAqMQAI4)+P!_|CvMeEsWeW`gxxc5esd;o=Fv7FyTVG0XuhTs& zh7;0alhwCTXut6P$~tzK`^wbp{$8QIOBc^1_ETHOzD}0UYFRyNWEC6kyNw5)H}#lU z5w}^uidjk8)A*&Nyg0u?}}8rUH_}NjzOQSOrgrZ-kq-D~gz0^jZ0S>;%6EeSPkr7>P)V zD_!x$lh$VsH_Z5rVnCO!;nG!Xdq!G1&84fEiJK`A@YL4!7|q1OOR&K*WsJsjmYtks z2iuiliL0EYj+r@2zF-*HY^ouLtR^?0=Z$Y((4o6I)WY^;pr4FC zcF<0GkiJCi)Ikr?BXodzNcurofAjood8n6~b?bs*lTNZDE0=QmKCc7gMxA literal 0 HcmV?d00001 diff --git a/src/test_data/Arabic b/src/test_data/Arabic new file mode 100644 index 0000000000000000000000000000000000000000..eaf2091eb597e761a7ccfc9d8975880df3ba86b5 GIT binary patch literal 200 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&ABIFnBSW;4ynN(Vmni33T2B>qDls!woJxJEz_pS#>Z!S`ue687bm8t1|$}hz%BPn%uQ{BTIN@po0M7v YbO*a%a9CnaX)4G7t(KWH8QR*~0CQMU82|tP literal 0 HcmV?d00001 diff --git a/src/test_data/Currency b/src/test_data/Currency new file mode 100644 index 0000000000000000000000000000000000000000..00a492ba2d518c44fa073d3c3581c06da6df4073 GIT binary patch literal 205 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&ABIFnBSW;4ynN(Vmni33TA_&19U^GHse{e0;X2uWxE`abkLEKw?n|+y#D#xv6bX g8~sXilTwR-j$!u;4ol1_O$8aC)iQG?Lt9%L0OEB>u>b%7 literal 0 HcmV?d00001 diff --git a/src/test_data/CurrencyTemperatureVolumeWeight b/src/test_data/CurrencyTemperatureVolumeWeight new file mode 100644 index 0000000000000000000000000000000000000000..fbd5a0ac7a18491b8c9c73e79099336bc1601577 GIT binary patch literal 2670 zcmd7UU2GIp6bJD0zuRux-9o1ybj$K#!9_kQb=y*lnkdEPqqJSx-F91Apv>+L?c#K{ z&dh>+Fids2iis~(V(S{Et(qECQ2gLgO?)x&g`oIm;uA4wj4_Ic;N976RZ2`SAnwb| z+{`_5?z#J$GdWI8S0!;!A0CpTBj;@pF zAH2?JzCXA#d_aomG&;s36wO7*%!6t2JF@3TO;lBSYP?>DqoMvA^N7Py(^47VUwKH> zCDq}mt*w{CTGC{(CfL)#3jIM}ETYHail`0~pB=-PIX=FuukVqj)>cW=#D1wwRQ0*E z{NkXLOwZ)c^d|*C9pc zhqLoU_ziS4HFBAm_TGkjH$?V?hvb;1d%b7DY&p61yuxnJK}pqkw!1u{8X1W3s}7GI zAL{nRtqN%+gygY4f!C;68s^W*-gG9vz~Stgog0m=S0ybW>qdov6T;#J3zvFU-F5f+ z>J9fl_|T?C-?rwUyQ_OwPj9D3NyzfKBB8jXv}{qit75Txi966540$55sA+-5Kzm1f zTOb$+g#3ox5DaGI2{7`{E?a(wP`P5|oqYn|(rQ&y`lW-STrEpVzdq0>l>Kcm9ud>Q@x( zZ5tTYVi8ep9E>Lv{mearw=Nw%TcoL)TBE=yHi~9fR_|)q=)TWw6!IlFj0MI*YXJ}N z1sF~vod*jv)bD9hbg5qzWoxT>%*}Pjj3S=imaROMMpYGur?yn_>+MZ^1AM+ z4Oo?)+EF!s@T(%B#j0$pYiC+Wk8DQ0w|w~$xR+wOSOFUP2{H#?V~V7$)F@1rEz+Nrs!3Ao!+1~>1{em@6vnp z0iC9g=}Y>KF485sOjkf)$UyIkdaxUNu@7-P3JnPy!Z4nIi6oxJ zGk6v$9K$50F@uvhh12*9pW|!%fFJP_e#S*y!ev}xd8~jr*#cI?N|=kSBMtBk+XS+1pGAuG%L?dLS~UZbIWR)cdQhxe^NHC)W}3h(XEFxxBs zAFaYJRNRn;Q080UW{VJPK^}OXid)hv=BY^c3McOtxx$hl|IW#{7~(B3dP`k2U@Mvt kvP|qj-<*kqcr0t;DIED1eWH|Au;tgtxVBGJW@IG)0Qwvc{Qv*} literal 0 HcmV?d00001 diff --git a/src/test_data/Flight b/src/test_data/Flight new file mode 100644 index 0000000000000000000000000000000000000000..68abb521c452a6efa84aa76e0f50e6b682fb4b76 GIT binary patch literal 1046 zcmZuw>u(HE6hCKn?XI?Twk>5*#duVydZiu-mi1V4=uBs;wH>Cj&CK0KMYxi& zA|jGTqe&B?9+8NM2tN4YACO2xeD}!*Ul0)y;qIdn;eI*yo}6=j=lst7`Kw*#P}0W+ z`)TI<2cL&qJkOh{>YaH;(iV*}=9uX`qZwUX>@0X*ovQ!G8&dUs=@ZmoaA8PPsQpu+ zoQIw#A}aS`4_kv#y?LcL1y3^L7%b-0sCvvan3=PQGYBHcl!m;Kk)~AYWV|y*7>Q9P z0BGh9yh{3Lp_m#FsUiS|N6&wjyujg(wQG!{>3+*}S$q2%lzje4fnZHi8K90U>K{~y zWAvKh)vd5xe^SYnT6gOJ3pZ1;XnqeJ6gZWj@zCU|>Yj<*OlG-5-MqzkBgaSbj44xR zD+?DbTi&*2!=}w!qOqNOy5=89^dt``cHXky)ym+0^>uj!hWPG4`UtNPgH-V;d}?WojbaR5 z&5PYgT2wzAH66>e>9$;kii%rT-{iHT#qB#pJ5k3WgJauU#d}v=>>w7?4Px8W(Mn{G z?QZ?oIRU=6QQcPrpC-WT`1A_6rVH?T-XOqz-NnClq&$|I?@LohMhHO|BA`GEtbz`R zg9-^a25HE^Ik*7V;5yuZTW}lh!hLuE58)9!h9~e8p2JIc3m@Sde1{+K6MiA04?|dw zvr)mt*pBOPFLvP(Od&x7DKZ?uQ+OKB;90zgm+&%P!K*lgH}E#z!F%`spWrimfv@m2 zzQ<4a1;64q{Ej~)zZ8%{Qk7IAg{3Aa9SO@84pwZ%NXV!Z literal 0 HcmV?d00001 diff --git a/src/test_data/Tracking b/src/test_data/Tracking new file mode 100644 index 0000000000000000000000000000000000000000..fd20f14e7594a7e17b25eb3ff283d004829570fb GIT binary patch literal 967 zcmZuw*-sN;5T7Xp%Dq%Ps2ma}Z3zvg8sHno7IToQ}D?%2B zy3Rau`n--dbL+6f;cz-z?X7L?9mhMnx;qlOA>u}J8eYl^u862cCZj}-1M`m<=JaH; zE)W=Zdt5{%E~>IzG_+L-PbO$04J}|$<^c1l*`z@)xZ2s-AzlwoNfAZ0+t<;MXUs1s zEcIKbsjRU0SFA)9$0F=CU{xol{MN}#7m67$u^8Z39~Wty<;gj%nTm=_hBmRGP_s;x zn51eI8m@3$S$U21(BY%aE$xn;6V5a4L2EQ7NuMgXO7o7Yox67LskZF3cs+v8DoR9A zyiRZbK!2ZC@Opi`R-lz@2Ca}o&9q)ySI_O+f8bz%V{Es`BpRmEL~4;}G^~yVxT=4Q zlOjnoHO!346cr;x((*JTXVmgJGY4x&8XB91vk9Y&B!5<=$`CL3h!mqsjW+-0@L0a> za~T_S-LHmZi=-Mt#qrGbWvKqFxV%s z2q)VXwK7&(@9B}PO>xRHi7)rMY^<`3&i=y>#BT-N+%Bpb{)# zg+^$DE^q@6J{W}{gkT14!+m%N3-AO|@Dvtd8J@!ncnPoJ4XnU>ScMJv3g6&6{D7bE z3w|SD307eZ)?)+q;3e$GYdC_V7{r^XU<_w4j<-<91m4Aa_y8Z_V_d)#zQVWo4nN>3 ze#Fm^`3#tqa3Lpi0yo&kCX=<9t@cnwAz|7_WOe&ev)jp%&+e} Date: Sat, 6 Jun 2026 13:33:52 -0700 Subject: [PATCH 02/41] Add constant for i64/u64 byte stream indicator --- src/deserializer/constants.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/deserializer/constants.rs b/src/deserializer/constants.rs index 9ca578e..c6ed017 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 From ff8522e04e3627f8d875df6de494fc0f3f533f3d Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 13:34:00 -0700 Subject: [PATCH 03/41] Add support for reading 64-bit signed and unsigned integers from byte stream --- src/deserializer/number.rs | 119 +++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 4 deletions(-) diff --git a/src/deserializer/number.rs b/src/deserializer/number.rs index 9c248b1..268aafd 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()); + } } From 8c3f8aaa29e1b811f8af85218783e0835f5829ee Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:08:23 -0700 Subject: [PATCH 04/41] Fix comment format --- src/deserializer/number.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deserializer/number.rs b/src/deserializer/number.rs index 268aafd..f20255a 100644 --- a/src/deserializer/number.rs +++ b/src/deserializer/number.rs @@ -422,7 +422,7 @@ mod float_tests { #[test] fn can_read_decimal_double_fractional_date() { - // NSDate(timeIntervalSinceReferenceDate: 700000000.523) — fractional dates + // `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, From d76066e6d3405801765bb59826a6baa8326bb0a6 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:09:18 -0700 Subject: [PATCH 05/41] Add foundation feature scaffold --- Cargo.toml | 3 +- src/deserializer/foundation.rs | 131 +++++++++++++++++++++++++++++++++ src/deserializer/mod.rs | 2 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 src/deserializer/foundation.rs diff --git a/Cargo.toml b/Cargo.toml index 085ea7d..6e81228 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ categories = ["parsing", "parser-implementations", "database"] [dependencies] [features] -std=[] \ No newline at end of file +std=[] +foundation=[] \ No newline at end of file diff --git a/src/deserializer/foundation.rs b/src/deserializer/foundation.rs new file mode 100644 index 0000000..5f7045e --- /dev/null +++ b/src/deserializer/foundation.rs @@ -0,0 +1,131 @@ +/*! +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`] 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`]/ +[`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`], so nothing is ever lost. + +Accessors are called on a *group-level* [`Property`]: the value yielded while +iterating an object's properties. +*/ + +use crate::deserializer::iter::Property; + +// 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. Consumed by the scalar/container accessors added from Phase 2 on. +#[allow(dead_code)] +pub(crate) const STRING_CLASSES: &[&str] = &["NSString", "NSMutableString", "NSAttributedString"]; +#[allow(dead_code)] +pub(crate) const DATA_CLASSES: &[&str] = &["NSData", "NSMutableData"]; +#[allow(dead_code)] +pub(crate) const ARRAY_CLASSES: &[&str] = &["NSArray", "NSMutableArray"]; +#[allow(dead_code)] +pub(crate) const DICT_CLASSES: &[&str] = &["NSDictionary", "NSMutableDictionary"]; +#[allow(dead_code)] +pub(crate) const SET_CLASSES: &[&str] = &["NSSet", "NSMutableSet"]; + +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 { + extern crate std; + + use alloc::{vec, vec::Vec}; + use std::{env::current_dir, fs::File, io::Read}; + + use crate::deserializer::typedstream::TypedStreamDeserializer; + + fn load(name: &str) -> Vec { + let path = current_dir() + .unwrap() + .join("src/test_data/foundation") + .join(name); + 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 + } + + #[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("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("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 { + crate::deserializer::iter::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("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/mod.rs b/src/deserializer/mod.rs index c3e1227..305b430 100644 --- a/src/deserializer/mod.rs +++ b/src/deserializer/mod.rs @@ -2,6 +2,8 @@ pub mod constants; pub mod consumed; +#[cfg(feature = "foundation")] +pub mod foundation; pub mod header; pub mod iter; pub mod number; From 23c11a2fca821ddd5ab2286b8a5336fc5b67c947 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:09:26 -0700 Subject: [PATCH 06/41] Add foundation test data --- src/test_data/foundation/NSArray | Bin 0 -> 113 bytes src/test_data/foundation/NSArrayEmpty | Bin 0 -> 49 bytes src/test_data/foundation/NSArrayNested | Bin 0 -> 122 bytes src/test_data/foundation/NSArrayWithNull | Bin 0 -> 91 bytes src/test_data/foundation/NSAttributedString | Bin 0 -> 118 bytes src/test_data/foundation/NSData | Bin 0 -> 60 bytes src/test_data/foundation/NSDate | Bin 0 -> 52 bytes src/test_data/foundation/NSDateFractional | Bin 0 -> 56 bytes src/test_data/foundation/NSDictionary | Bin 0 -> 127 bytes src/test_data/foundation/NSDictionaryNested | Bin 0 -> 189 bytes src/test_data/foundation/NSMutableArray | Bin 0 -> 124 bytes src/test_data/foundation/NSMutableData | Bin 0 -> 75 bytes src/test_data/foundation/NSMutableDictionary | Bin 0 -> 136 bytes src/test_data/foundation/NSMutableSet | Bin 0 -> 85 bytes src/test_data/foundation/NSMutableString | Bin 0 -> 81 bytes src/test_data/foundation/NSSet | Bin 0 -> 77 bytes src/test_data/foundation/NSString | Bin 0 -> 62 bytes src/test_data/foundation/NSURL | Bin 0 -> 95 bytes src/test_data/foundation/NSURLRelative | Bin 0 -> 112 bytes src/test_data/foundation/NumberBool | Bin 0 -> 66 bytes src/test_data/foundation/NumberDouble | Bin 0 -> 74 bytes src/test_data/foundation/NumberFloat | Bin 0 -> 70 bytes src/test_data/foundation/NumberInt | Bin 0 -> 66 bytes src/test_data/foundation/NumberInt64 | Bin 0 -> 74 bytes test_data/generators/foundation.swift | 70 +++++++++++++++++++ test_data/generators/generate.sh | 32 +++++++++ 26 files changed, 102 insertions(+) create mode 100644 src/test_data/foundation/NSArray create mode 100644 src/test_data/foundation/NSArrayEmpty create mode 100644 src/test_data/foundation/NSArrayNested create mode 100644 src/test_data/foundation/NSArrayWithNull create mode 100644 src/test_data/foundation/NSAttributedString create mode 100644 src/test_data/foundation/NSData create mode 100644 src/test_data/foundation/NSDate create mode 100644 src/test_data/foundation/NSDateFractional create mode 100644 src/test_data/foundation/NSDictionary create mode 100644 src/test_data/foundation/NSDictionaryNested create mode 100644 src/test_data/foundation/NSMutableArray create mode 100644 src/test_data/foundation/NSMutableData create mode 100644 src/test_data/foundation/NSMutableDictionary create mode 100644 src/test_data/foundation/NSMutableSet create mode 100644 src/test_data/foundation/NSMutableString create mode 100644 src/test_data/foundation/NSSet create mode 100644 src/test_data/foundation/NSString create mode 100644 src/test_data/foundation/NSURL create mode 100644 src/test_data/foundation/NSURLRelative create mode 100644 src/test_data/foundation/NumberBool create mode 100644 src/test_data/foundation/NumberDouble create mode 100644 src/test_data/foundation/NumberFloat create mode 100644 src/test_data/foundation/NumberInt create mode 100644 src/test_data/foundation/NumberInt64 create mode 100644 test_data/generators/foundation.swift create mode 100755 test_data/generators/generate.sh diff --git a/src/test_data/foundation/NSArray b/src/test_data/foundation/NSArray new file mode 100644 index 0000000000000000000000000000000000000000..56670c30208aaf86cc52fc490640d8360a277e14 GIT binary patch literal 113 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNLBSX7i)$Fc!{c1ge=foiVAc F4FK$AD186` literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NSArrayEmpty b/src/test_data/foundation/NSArrayEmpty new file mode 100644 index 0000000000000000000000000000000000000000..71b31358d42ba7993eecde0b75644c1792722367 GIT binary patch literal 49 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNLBSX7i)$K Og)?W(p3T(O)&>BJWi1f^ literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NSArrayWithNull b/src/test_data/foundation/NSArrayWithNull new file mode 100644 index 0000000000000000000000000000000000000000..988a9a3242daf53fda389567d5a65d0f1685ee48 GIT binary patch literal 91 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNLBSX7i)$KYO6 literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NSDateFractional b/src/test_data/foundation/NSDateFractional new file mode 100644 index 0000000000000000000000000000000000000000..b590f358300cd9d2c425b238bdaeda6bf82d236b GIT binary patch literal 56 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!sZw3l30?;(9*);7wn&um6}|_(AvV7(!A=U LQ^VvtM;zM#M5z_@ literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NSDictionary b/src/test_data/foundation/NSDictionary new file mode 100644 index 0000000000000000000000000000000000000000..6ab0795a46231571897921bf111dfdc3732e7879 GIT binary patch literal 127 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!s8e0l9^nRnV*+fRLRiN!r>R}pOlrFT*A=W z!kEc438(}p6kJl2nU~HurG-(ODch)RQp>dIjAdYoDccZ8gBAIe<|d^U0nK3d3l2-n TDNSVns@H01VJw`@*wzLBmF+H2 literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NSDictionaryNested b/src/test_data/foundation/NSDictionaryNested new file mode 100644 index 0000000000000000000000000000000000000000..668066a007481b6b19ab00aa8d1eb9cb843017b7 GIT binary patch literal 189 zcmXYrK?=e!5Ji)wsGuj&ohMLm9Tx#N;$qqciP)-Ry693Ea{#Fd+5>tFr=`2kpZ6aV zkFOprpYxRZ41!3bBzqbv5ZCOK1p7}|QM z%@PqRQ9q*|(`BAmWDDOma4fCh#gs^18p)f_w`rol;q{I)@X%=n`~c_HLh*KmPs&P7E@5bGVa#Nl1k?!>3N9(i%u8pS+QO*KoSjPjTvC*om(B=g`zK|kCYLa@wlHe*c%6^Xe8IjMTd`MLTjnML|-ZIfE2PvR}SDKrYTEx)O!tNIwmY7qT3S{gbj% TlS>#{TNt%kS{Re3GqwQ$6touv literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NumberDouble b/src/test_data/foundation/NumberDouble new file mode 100644 index 0000000000000000000000000000000000000000..9661076106ea60cf4a4ccc00fccf7cdc7ac90742 GIT binary patch literal 74 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!r>R}SDKrYTEx)O!tNIwmY7qT3S{gbj% YlS>#{TNt%kS{PHNH#2~MLZm|*0ME}DKmY&$ literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NumberFloat b/src/test_data/foundation/NumberFloat new file mode 100644 index 0000000000000000000000000000000000000000..bc9553ef2de2e61a9c6223aacb65bfc0a8af236b GIT binary patch literal 70 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!r>R}SDKrYTEx)O!tNIwmY7qT3S{gbj% XlS>#{TNt%kS{T!&H#0CKIJ5x(b8r|b literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NumberInt b/src/test_data/foundation/NumberInt new file mode 100644 index 0000000000000000000000000000000000000000..dace5121aadca7895bf9a7d4f279bc8a0dd14d7e GIT binary patch literal 66 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!r>R}SDKrYTEx)O!tNIwmY7qT3S{gbj% TlS>#{TNt%kS{O5@YqbFY6&@Ee literal 0 HcmV?d00001 diff --git a/src/test_data/foundation/NumberInt64 b/src/test_data/foundation/NumberInt64 new file mode 100644 index 0000000000000000000000000000000000000000..1e906a2351679315bf21dc2366b2504f3ee0f41e GIT binary patch literal 74 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!r>R}SDKrYTEx)O!tNIwmY7qT3S{gbj% blS>#{TNt%kS{Mtbw=+EJd;a(T|Nm_O?Cc%w literal 0 HcmV?d00001 diff --git a/test_data/generators/foundation.swift b/test_data/generators/foundation.swift new file mode 100644 index 0000000..ceaed87 --- /dev/null +++ b/test_data/generators/foundation.swift @@ -0,0 +1,70 @@ +// 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/")) + 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..f10df71 --- /dev/null +++ b/test_data/generators/generate.sh @@ -0,0 +1,32 @@ +#!/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" + +for k in $keys; do + "$bin" "$k" "$out/$k" || echo "FAILED: $k" +done + +echo "Fixtures written to $out" From 80a1e3bde7005b576b08aa33956d1926cea4b1b0 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:26:22 -0700 Subject: [PATCH 07/41] Add nested test data --- src/test_data/foundation/NestedAttributed | Bin 0 -> 129 bytes src/test_data/foundation/NestedData | Bin 0 -> 105 bytes src/test_data/foundation/NestedStrings | Bin 0 -> 100 bytes test_data/generators/foundation.swift | 6 ++++++ test_data/generators/generate.sh | 3 ++- 5 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/test_data/foundation/NestedAttributed create mode 100644 src/test_data/foundation/NestedData create mode 100644 src/test_data/foundation/NestedStrings diff --git a/src/test_data/foundation/NestedAttributed b/src/test_data/foundation/NestedAttributed new file mode 100644 index 0000000000000000000000000000000000000000..70b86976854864ade7b7fc8b06a88a0d15766587 GIT binary patch literal 129 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNLBSX7i)$9&b|jPl literal 0 HcmV?d00001 diff --git a/test_data/generators/foundation.swift b/test_data/generators/foundation.swift index ceaed87..3c10e0f 100644 --- a/test_data/generators/foundation.swift +++ b/test_data/generators/foundation.swift @@ -56,6 +56,12 @@ func make(_ key: String) -> Any? { // 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")]) default: return nil } } diff --git a/test_data/generators/generate.sh b/test_data/generators/generate.sh index f10df71..344c639 100755 --- a/test_data/generators/generate.sh +++ b/test_data/generators/generate.sh @@ -23,7 +23,8 @@ NSArray NSMutableArray NSArrayEmpty NSArrayWithNull NSArrayNested \ NSDictionary NSMutableDictionary NSDictionaryNested \ NSSet NSMutableSet \ NSDate NSDateFractional \ -NSURL NSURLRelative" +NSURL NSURLRelative \ +NestedStrings NestedData NestedAttributed" for k in $keys; do "$bin" "$k" "$out/$k" || echo "FAILED: $k" From d8e51c4ea3be1ebfbc17beb0f89041fa8d781543 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:32:37 -0700 Subject: [PATCH 08/41] Implement typed accessors for `Foundation` classes --- src/deserializer/foundation.rs | 371 +++++++++++++++++++++++++++++++-- 1 file changed, 354 insertions(+), 17 deletions(-) diff --git a/src/deserializer/foundation.rs b/src/deserializer/foundation.rs index 5f7045e..944b9ec 100644 --- a/src/deserializer/foundation.rs +++ b/src/deserializer/foundation.rs @@ -5,27 +5,32 @@ Enabled by the `foundation` cargo feature. These methods interpret the generic [`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`. +`NSMutableData`). This feature is purely for convenience: the parser and the [`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`], so nothing is ever lost. -Accessors are called on a *group-level* [`Property`]: the value yielded while +Accessors are called on a group-level [`Property`]: the value yielded while iterating an object's properties. */ -use crate::deserializer::iter::Property; +use crate::{ + deserializer::iter::{Property, PropertyIterator}, + models::output_data::OutputData, +}; +// MARK: Names // 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. Consumed by the scalar/container accessors added from Phase 2 on. -#[allow(dead_code)] -pub(crate) const STRING_CLASSES: &[&str] = &["NSString", "NSMutableString", "NSAttributedString"]; -#[allow(dead_code)] +// centralized. +pub(crate) const STRING_CLASSES: &[&str] = &["NSString", "NSMutableString"]; +pub(crate) const ATTRIBUTED_STRING_CLASSES: &[&str] = + &["NSAttributedString", "NSMutableAttributedString"]; pub(crate) const DATA_CLASSES: &[&str] = &["NSData", "NSMutableData"]; +// Consumed by the container accessors added in Phase 3. #[allow(dead_code)] pub(crate) const ARRAY_CLASSES: &[&str] = &["NSArray", "NSMutableArray"]; #[allow(dead_code)] @@ -33,6 +38,23 @@ pub(crate) const DICT_CLASSES: &[&str] = &["NSDictionary", "NSMutableDictionary" #[allow(dead_code)] pub(crate) const SET_CLASSES: &[&str] = &["NSSet", "NSMutableSet"]; +/// 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 +} + +// MARK: Property +/// Typed accessors for Foundation classes, implemented as methods on [`Property`] so +/// they are available while iterating any object's properties. Each accessor resolves +/// whether `self` is a [`Property::Group`] whose first element is an object of the +/// expected class(es), or (for scalar types) a group wrapping a bare primitive or +/// an `NSNumber` object wrapping one, and returns the interpreted value if successful. impl<'a, 'b: 'a> Property<'a, 'b> { /// The Objective-C class name of the object this property refers to, if any. /// @@ -52,8 +74,142 @@ impl<'a, 'b: 'a> Property<'a, 'b> { Property::Primitive(_) => None, } } + + // MARK: String + /// 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 + } + + // MARK: Bytes + /// The raw bytes of an `NSData` / `NSMutableData`. + /// + /// crabstep does not interpret the bytes — 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 + } + + // MARK: Boolean + /// 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, + } + } + + // MARK: i64 + /// 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, + } + } + + // MARK: u64 + /// 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, + } + } + + // MARK: f64 + /// 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, + } + } + + // MARK: Helpers + /// If `self` is a group whose first item is an object whose class is in + /// `classes`, return that object's data iterator. + fn object_in_classes(&self, classes: &[&str]) -> Option> { + let Property::Group(group) = self else { + return None; + }; + match group.first()? { + Property::Object { name, data, .. } if classes.contains(&name) => Some(data), + _ => 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>> { + let Property::Group(group) = self else { + return None; + }; + match group.first()? { + Property::Primitive(value) => Some(value), + Property::Object { + name: "NSNumber", + mut data, + .. + } => match data.next()? { + Property::Group(inner) => match inner.first()? { + Property::Primitive(value) => Some(value), + _ => None, + }, + _ => None, + }, + _ => None, + } + } } +// MARK: Tests #[cfg(test)] mod tests { extern crate std; @@ -61,13 +217,12 @@ mod tests { use alloc::{vec, vec::Vec}; use std::{env::current_dir, fs::File, io::Read}; + use crate::deserializer::iter::Property; use crate::deserializer::typedstream::TypedStreamDeserializer; - fn load(name: &str) -> Vec { - let path = current_dir() - .unwrap() - .join("src/test_data/foundation") - .join(name); + /// Load a fixture by path relative to `src/test_data`. + 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![]; @@ -75,11 +230,13 @@ mod tests { bytes } + // MARK: class_name + #[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("NSArray"); + // `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 @@ -96,14 +253,14 @@ mod tests { 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("NSArray"); + 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 { - crate::deserializer::iter::Property::Group(g) => g.first(), + Property::Group(g) => g.first(), _ => None, }) .filter_map(|inner| inner.class_name()) @@ -118,7 +275,7 @@ mod tests { #[test] fn class_name_is_none_for_primitive() { // The `NSArray`'s count group is a bare primitive, no class. - let bytes = load("NSArray"); + let bytes = load("foundation/NSArray"); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); let has_primitive_group = ts @@ -128,4 +285,184 @@ mod tests { assert!(has_primitive_group); } + + // MARK: as_string + + #[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() { + // NSArray([NSAttributedString "styled"]) — exercises the scan path. + 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:?}"); + } + + // MARK: as_data + + #[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:?}"); + } + + // MARK: numbers + + #[test] + fn as_f64_reads_decimal_double() { + // NumberDouble root: bare-primitive path, exercises the B1 DECIMAL fix. + let bytes = load("foundation/NumberDouble"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().unwrap(); + + assert_eq!(group.as_f64(), Some(3.5)); + } + + #[test] + fn as_i64_reads_large_negative() { + // NumberInt64 = -9_000_000_000: bare-primitive path, exercises the B2 fix. + let bytes = load("foundation/NumberInt64"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().unwrap(); + + assert_eq!(group.as_u64(), None); + } + + // MARK: as_bool + #[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)); + } + + // MARK: wrappers + + #[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 + } } From f8f1d3436a2add7a265c0c0caf879803d00d55e8 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:43:00 -0700 Subject: [PATCH 09/41] Add nested container tests --- src/test_data/foundation/NestedContainers | Bin 0 -> 287 bytes test_data/generators/foundation.swift | 14 ++++++++++++++ test_data/generators/generate.sh | 2 +- 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 src/test_data/foundation/NestedContainers diff --git a/src/test_data/foundation/NestedContainers b/src/test_data/foundation/NestedContainers new file mode 100644 index 0000000000000000000000000000000000000000..3ad54ffb94a7379942223daae30949ffc0403e77 GIT binary patch literal 287 zcmYk0%MQUn6o%&%L5PjF;3Y_GER4k@_ERQO)MYwdbSXLd0KJ4RBx2{yJcgN0iQUQh zzyCX@-U*Zx+*i?2tj_Nm!Uz$Yl#Qk25kYKGHg`8-smKMvYbKcHPNEULr=jl(sTqwg z_%0MALl_{!sem}a>#Zo9SPzIj%4VVB?oL>3nuE=2CQoxxQeDa>-cosoJ(m&53{&1= zN_zV>X!#JfmAGxi{h#=+`7q7r-SNM0%fb!Hm{8`)X$e4J_xf)2orzzMs;$%b106(h AU;qFB literal 0 HcmV?d00001 diff --git a/test_data/generators/foundation.swift b/test_data/generators/foundation.swift index 3c10e0f..7252b76 100644 --- a/test_data/generators/foundation.swift +++ b/test_data/generators/foundation.swift @@ -62,6 +62,20 @@ func make(_ key: String) -> Any? { 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")]), + ]) default: return nil } } diff --git a/test_data/generators/generate.sh b/test_data/generators/generate.sh index 344c639..ee1c7a3 100755 --- a/test_data/generators/generate.sh +++ b/test_data/generators/generate.sh @@ -24,7 +24,7 @@ NSDictionary NSMutableDictionary NSDictionaryNested \ NSSet NSMutableSet \ NSDate NSDateFractional \ NSURL NSURLRelative \ -NestedStrings NestedData NestedAttributed" +NestedStrings NestedData NestedAttributed NestedContainers" for k in $keys; do "$bin" "$k" "$out/$k" || echo "FAILED: $k" From 19b8df56281af6012c96435eae1852dab7f75784 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:49:49 -0700 Subject: [PATCH 10/41] Add accessors for Foundation collections: arrays, sets, and dictionaries --- src/deserializer/foundation.rs | 170 ++++++++++++++++++++++++++++++++- 1 file changed, 166 insertions(+), 4 deletions(-) diff --git a/src/deserializer/foundation.rs b/src/deserializer/foundation.rs index 944b9ec..b4cf782 100644 --- a/src/deserializer/foundation.rs +++ b/src/deserializer/foundation.rs @@ -26,16 +26,18 @@ use crate::{ // 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"]; -// Consumed by the container accessors added in Phase 3. -#[allow(dead_code)] +/// Denotes ordered collections of arbitrary objects. pub(crate) const ARRAY_CLASSES: &[&str] = &["NSArray", "NSMutableArray"]; -#[allow(dead_code)] +/// Denotes unordered collections of arbitrary objects. pub(crate) const DICT_CLASSES: &[&str] = &["NSDictionary", "NSMutableDictionary"]; -#[allow(dead_code)] +/// Denotes unordered collections of unique arbitrary objects. pub(crate) const SET_CLASSES: &[&str] = &["NSSet", "NSMutableSet"]; /// Extract the backing UTF-8 of a string-cluster object: the first `String` @@ -172,6 +174,41 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } } + // MARK: Array + /// The elements of an `NSArray` / `NSMutableArray` as a lazy iterator. The + /// leading element-count group is skipped; each yielded [`Property`] is an + /// element on which the other accessors (`as_string`, `as_i64`, a nested + /// `as_array`, …) can be called. + #[must_use] + pub fn as_array(&self) -> Option> { + let mut data = self.object_in_classes(ARRAY_CLASSES)?; + data.next(); // discard the count group + Some(FoundationArray { inner: data }) + } + + // MARK: Set + /// The members of an `NSSet` / `NSMutableSet` as a lazy iterator (unordered). + /// Shares [`FoundationArray`] with [`as_array`](Self::as_array): the count + /// group is skipped and each member is yielded as a [`Property`]. + #[must_use] + pub fn as_set(&self) -> Option> { + let mut data = self.object_in_classes(SET_CLASSES)?; + data.next(); // discard the count group + Some(FoundationArray { inner: data }) + } + + // MARK: Dictionary + /// The key/value pairs of an `NSDictionary` / `NSMutableDictionary` as a lazy + /// iterator. The leading count group is skipped and the remaining groups are + /// paired `(key, value)`; a trailing unpaired key (only reachable from + /// malformed data) is dropped. + #[must_use] + pub fn as_dictionary(&self) -> Option> { + let mut data = self.object_in_classes(DICT_CLASSES)?; + data.next(); // discard the count group + Some(FoundationDict { inner: data }) + } + // MARK: Helpers /// If `self` is a group whose first item is an object whose class is in /// `classes`, return that object's data iterator. @@ -209,6 +246,43 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } } +// MARK: Iterators +/// A lazy iterator over the elements of an `NSArray` / `NSMutableArray` (or the +/// members of an `NSSet` / `NSMutableSet`), produced by [`Property::as_array`] / +/// [`Property::as_set`]. Each item is the element's group-level [`Property`], so +/// the other accessors apply to it directly. +#[derive(Debug, Clone)] +pub struct FoundationArray<'a, 'b> { + inner: PropertyIterator<'a, 'b>, +} + +impl<'a, 'b: 'a> Iterator for FoundationArray<'a, 'b> { + type Item = Property<'a, 'b>; + + fn next(&mut self) -> Option { + self.inner.next() + } +} + +/// A lazy iterator over the `(key, value)` pairs of an `NSDictionary` / +/// `NSMutableDictionary`, produced by [`Property::as_dictionary`]. Each key and +/// value is a group-level [`Property`]. +#[derive(Debug, Clone)] +pub struct FoundationDict<'a, 'b> { + inner: PropertyIterator<'a, 'b>, +} + +impl<'a, 'b: 'a> Iterator for FoundationDict<'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)) + } +} + // MARK: Tests #[cfg(test)] mod tests { @@ -465,4 +539,92 @@ mod tests { .collect(); assert!(datas.contains(&&[0x01, 0x02][..]), "{datas:?}"); // NSData value } + + // MARK: as_array / as_set + + #[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.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.filter_map(|member| member.as_string()).collect()) + .collect(); + + assert_eq!(sets, vec![vec!["s"], vec!["ms"]]); + } + + // MARK: as_dictionary + + #[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.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 nested_array_inside_array() { + // NSArrayNested = [NSString "top", NSArray[1, 2]] — as_array on the nested + // element resolves the inner 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.filter_map(|el| el.as_i64()).collect()) + .collect(); + + assert_eq!(inner, vec![vec![1, 2]]); + } + + // MARK: container negatives + + #[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()); + } } From 7d5b469eefea495af336a2b337b4e2e2d077680b Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:57:15 -0700 Subject: [PATCH 11/41] Add support for NestedScalars in Foundation typedstream test fixtures --- src/test_data/foundation/NestedScalars | Bin 0 -> 207 bytes test_data/generators/foundation.swift | 9 +++++++++ test_data/generators/generate.sh | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 src/test_data/foundation/NestedScalars diff --git a/src/test_data/foundation/NestedScalars b/src/test_data/foundation/NestedScalars new file mode 100644 index 0000000000000000000000000000000000000000..ebf2348b945f1ede704c657850779ad1be8ceb6d GIT binary patch literal 207 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNLBSX7i)$U5=>?Q@qrqHONuh{(iwq5+AB?ZM+ z`ueFAiMa(isd~xzx%vf(B^maGwuWs$17^%*1Q9^3vt~ Any? { 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 } } diff --git a/test_data/generators/generate.sh b/test_data/generators/generate.sh index ee1c7a3..9279932 100755 --- a/test_data/generators/generate.sh +++ b/test_data/generators/generate.sh @@ -24,7 +24,7 @@ NSDictionary NSMutableDictionary NSDictionaryNested \ NSSet NSMutableSet \ NSDate NSDateFractional \ NSURL NSURLRelative \ -NestedStrings NestedData NestedAttributed NestedContainers" +NestedStrings NestedData NestedAttributed NestedContainers NestedScalars" for k in $keys; do "$bin" "$k" "$out/$k" || echo "FAILED: $k" From 211bfd730068ba3dd3ca93b8aa13c4faf47c9d4d Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 14:57:54 -0700 Subject: [PATCH 12/41] Add date, URL, and null accessors for Foundation properties --- src/deserializer/foundation.rs | 129 +++++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/src/deserializer/foundation.rs b/src/deserializer/foundation.rs index b4cf782..846d788 100644 --- a/src/deserializer/foundation.rs +++ b/src/deserializer/foundation.rs @@ -209,6 +209,62 @@ impl<'a, 'b: 'a> Property<'a, 'b> { Some(FoundationDict { inner: data }) } + // MARK: Date + /// 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) + } + + // MARK: URL + /// 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 + } + + // MARK: Null + /// 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, + } + } + // MARK: Helpers /// If `self` is a group whose first item is an object whose class is in /// `classes`, return that object's data iterator. @@ -627,4 +683,77 @@ mod tests { assert!(group.as_set().is_none()); assert!(group.as_dictionary().is_none()); } + + // MARK: as_date / as_unix_time + + #[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]); + } + + // MARK: as_url + + #[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"]); + } + + // MARK: is_null + + #[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); + } + + #[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); + } } From 31476444fd32b2f1c83f0dd78a17d3c73161d059 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 15:40:07 -0700 Subject: [PATCH 13/41] Include new crate features in ci tests --- .github/workflows/release.yml | 2 +- .github/workflows/test.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5be41d2..61086be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - - run: cargo test --verbose + - 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..83a4e69 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,5 +17,5 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable - - run: cargo clippy - - run: cargo test --verbose + - run: cargo clippy --all-targets --all-features + - run: cargo test --verbose --all-features From 56101c60122a8a4a667ab2d69ffbf255152431f2 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 15:47:47 -0700 Subject: [PATCH 14/41] Improve Foundation accessors with lazy views for arrays and dictionaries --- Cargo.toml | 8 +- README.md | 13 + src/deserializer/foundation.rs | 423 ++++++++++++++++++++++++++++++--- src/deserializer/mod.rs | 1 + src/lib.rs | 1 + 5 files changed, 411 insertions(+), 35 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e81228..3cc6a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,5 +14,9 @@ categories = ["parsing", "parser-implementations", "database"] [dependencies] [features] -std=[] -foundation=[] \ 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..4dfb3cb 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 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/foundation.rs b/src/deserializer/foundation.rs index 846d788..34e0f76 100644 --- a/src/deserializer/foundation.rs +++ b/src/deserializer/foundation.rs @@ -8,7 +8,7 @@ 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`]/ -[`OutputData`](crate::models::output_data::OutputData) model are unchanged +[`OutputData`] model are unchanged whether or not it is enabled, and any class not modeled here stays reachable through [`Property::Object`], so nothing is ever lost. @@ -175,38 +175,95 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } // MARK: Array - /// The elements of an `NSArray` / `NSMutableArray` as a lazy iterator. The - /// leading element-count group is skipped; each yielded [`Property`] is an - /// element on which the other accessors (`as_string`, `as_i64`, a nested - /// `as_array`, …) can be called. + /// 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 mut data = self.object_in_classes(ARRAY_CLASSES)?; - data.next(); // discard the count group - Some(FoundationArray { inner: data }) + let (elements, len) = split_count(self.object_in_classes(ARRAY_CLASSES)?)?; + Some(FoundationArray { elements, len }) } // MARK: Set - /// The members of an `NSSet` / `NSMutableSet` as a lazy iterator (unordered). - /// Shares [`FoundationArray`] with [`as_array`](Self::as_array): the count - /// group is skipped and each member is yielded as a [`Property`]. + /// 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 mut data = self.object_in_classes(SET_CLASSES)?; - data.next(); // discard the count group - Some(FoundationArray { inner: data }) + let (elements, len) = split_count(self.object_in_classes(SET_CLASSES)?)?; + Some(FoundationArray { elements, len }) } // MARK: Dictionary - /// The key/value pairs of an `NSDictionary` / `NSMutableDictionary` as a lazy - /// iterator. The leading count group is skipped and the remaining groups are - /// paired `(key, value)`; a trailing unpaired key (only reachable from - /// malformed data) is dropped. + /// 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 mut data = self.object_in_classes(DICT_CLASSES)?; - data.next(); // discard the count group - Some(FoundationDict { inner: data }) + let (entries, len) = split_count(self.object_in_classes(DICT_CLASSES)?)?; + Some(FoundationDict { entries, len }) } // MARK: Date @@ -303,16 +360,113 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } // MARK: Iterators -/// A lazy iterator over the elements of an `NSArray` / `NSMutableArray` (or the +/// Read the leading count group of a container's data, returning the remaining +/// iterator (positioned at the first element/entry) and the declared count. +fn split_count<'a, 'b: 'a>( + mut data: PropertyIterator<'a, 'b>, +) -> Option<(PropertyIterator<'a, 'b>, usize)> { + let count = data.next()?; + let len = usize::try_from(count.as_i64().unwrap_or(0)).unwrap_or(0); + Some((data, 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`]. Each item is the element's group-level [`Property`], so -/// the other accessors apply to it directly. +/// [`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 FoundationArray<'a, 'b> { +impl<'a, 'b: 'a> Iterator for FoundationArrayIter<'a, 'b> { type Item = Property<'a, 'b>; fn next(&mut self) -> Option { @@ -320,15 +474,148 @@ impl<'a, 'b: 'a> Iterator for FoundationArray<'a, 'b> { } } -/// A lazy iterator over the `(key, value)` pairs of an `NSDictionary` / -/// `NSMutableDictionary`, produced by [`Property::as_dictionary`]. Each key and -/// value is a group-level [`Property`]. +/// 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 FoundationDict<'a, 'b> { +impl<'a, 'b: 'a> Iterator for FoundationDictIter<'a, 'b> { type Item = (Property<'a, 'b>, Property<'a, 'b>); fn next(&mut self) -> Option { @@ -609,7 +896,7 @@ mod tests { .resolve_properties(root) .unwrap() .filter_map(|group| group.as_array()) - .map(|array| array.filter_map(|el| el.as_i64()).collect()) + .map(|array| array.into_iter().filter_map(|el| el.as_i64()).collect()) .collect(); assert_eq!(arrays, vec![vec![1, 2], vec![3], vec![]]); @@ -624,7 +911,11 @@ mod tests { .resolve_properties(root) .unwrap() .filter_map(|group| group.as_set()) - .map(|set| set.filter_map(|member| member.as_string()).collect()) + .map(|set| { + set.into_iter() + .filter_map(|member| member.as_string()) + .collect() + }) .collect(); assert_eq!(sets, vec![vec!["s"], vec!["ms"]]); @@ -643,7 +934,8 @@ mod tests { .unwrap() .filter_map(|group| group.as_dictionary()) .flat_map(|dict| { - dict.filter_map(|(key, value)| Some((key.as_string()?, value.as_i64()?))) + dict.into_iter() + .filter_map(|(key, value)| Some((key.as_string()?, value.as_i64()?))) .collect::>() }) .collect(); @@ -664,7 +956,7 @@ mod tests { .resolve_properties(root) .unwrap() .filter_map(|group| group.as_array()) - .map(|array| array.filter_map(|el| el.as_i64()).collect()) + .map(|array| array.into_iter().filter_map(|el| el.as_i64()).collect()) .collect(); assert_eq!(inner, vec![vec![1, 2]]); @@ -756,4 +1048,69 @@ mod tests { assert_eq!(group.as_unix_time(), None); assert_eq!(group.as_url(), None); } + + // MARK: container views + + #[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); + } + + #[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/mod.rs b/src/deserializer/mod.rs index 305b430..5b86d8a 100644 --- a/src/deserializer/mod.rs +++ b/src/deserializer/mod.rs @@ -3,6 +3,7 @@ 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; diff --git a/src/lib.rs b/src/lib.rs index 0ed20f5..3f9e17d 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; From 84002b841a5064241d8c5f08061f9383b9b81081 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 16:12:42 -0700 Subject: [PATCH 15/41] Re-export `Property` --- src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3f9e17d..0e67d66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,10 @@ 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}, output_data::OutputData, From 88699cdddbd85b1569a3f431fee585f950b77487 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 16:12:55 -0700 Subject: [PATCH 16/41] Refactor into type modules --- src/deserializer/foundation.rs | 1116 ------------------- src/deserializer/foundation/array.rs | 278 +++++ src/deserializer/foundation/boolean.rs | 33 + src/deserializer/foundation/bytes.rs | 51 + src/deserializer/foundation/class.rs | 87 ++ src/deserializer/foundation/date.rs | 74 ++ src/deserializer/foundation/dict.rs | 247 ++++ src/deserializer/foundation/helpers.rs | 27 + src/deserializer/foundation/mod.rs | 39 + src/deserializer/foundation/names.rs | 19 + src/deserializer/foundation/null.rs | 42 + src/deserializer/foundation/number.rs | 167 +++ src/deserializer/foundation/string.rs | 100 ++ src/deserializer/foundation/test_support.rs | 15 + src/deserializer/foundation/url.rs | 43 + 15 files changed, 1222 insertions(+), 1116 deletions(-) delete mode 100644 src/deserializer/foundation.rs create mode 100644 src/deserializer/foundation/array.rs create mode 100644 src/deserializer/foundation/boolean.rs create mode 100644 src/deserializer/foundation/bytes.rs create mode 100644 src/deserializer/foundation/class.rs create mode 100644 src/deserializer/foundation/date.rs create mode 100644 src/deserializer/foundation/dict.rs create mode 100644 src/deserializer/foundation/helpers.rs create mode 100644 src/deserializer/foundation/mod.rs create mode 100644 src/deserializer/foundation/names.rs create mode 100644 src/deserializer/foundation/null.rs create mode 100644 src/deserializer/foundation/number.rs create mode 100644 src/deserializer/foundation/string.rs create mode 100644 src/deserializer/foundation/test_support.rs create mode 100644 src/deserializer/foundation/url.rs diff --git a/src/deserializer/foundation.rs b/src/deserializer/foundation.rs deleted file mode 100644 index 34e0f76..0000000 --- a/src/deserializer/foundation.rs +++ /dev/null @@ -1,1116 +0,0 @@ -/*! -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`] 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`]/ -[`OutputData`] model are unchanged -whether or not it is enabled, and any class not modeled here stays reachable -through [`Property::Object`], so nothing is ever lost. - -Accessors are called on a group-level [`Property`]: the value yielded while -iterating an object's properties. -*/ - -use crate::{ - deserializer::iter::{Property, PropertyIterator}, - models::output_data::OutputData, -}; - -// MARK: Names -// 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"]; - -/// 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 -} - -// MARK: Property -/// Typed accessors for Foundation classes, implemented as methods on [`Property`] so -/// they are available while iterating any object's properties. Each accessor resolves -/// whether `self` is a [`Property::Group`] whose first element is an object of the -/// expected class(es), or (for scalar types) a group wrapping a bare primitive or -/// an `NSNumber` object wrapping one, and returns the interpreted value if successful. -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, - } - } - - // MARK: String - /// 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 - } - - // MARK: Bytes - /// The raw bytes of an `NSData` / `NSMutableData`. - /// - /// crabstep does not interpret the bytes — 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 - } - - // MARK: Boolean - /// 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, - } - } - - // MARK: i64 - /// 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, - } - } - - // MARK: u64 - /// 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, - } - } - - // MARK: f64 - /// 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, - } - } - - // MARK: Array - /// 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 }) - } - - // MARK: Set - /// 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 }) - } - - // MARK: Dictionary - /// 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 }) - } - - // MARK: Date - /// 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) - } - - // MARK: URL - /// 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 - } - - // MARK: Null - /// 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, - } - } - - // MARK: Helpers - /// If `self` is a group whose first item is an object whose class is in - /// `classes`, return that object's data iterator. - fn object_in_classes(&self, classes: &[&str]) -> Option> { - let Property::Group(group) = self else { - return None; - }; - match group.first()? { - Property::Object { name, data, .. } if classes.contains(&name) => Some(data), - _ => 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>> { - let Property::Group(group) = self else { - return None; - }; - match group.first()? { - Property::Primitive(value) => Some(value), - Property::Object { - name: "NSNumber", - mut data, - .. - } => match data.next()? { - Property::Group(inner) => match inner.first()? { - Property::Primitive(value) => Some(value), - _ => None, - }, - _ => None, - }, - _ => None, - } - } -} - -// MARK: Iterators -/// Read the leading count group of a container's data, returning the remaining -/// iterator (positioned at the first element/entry) and the declared count. -fn split_count<'a, 'b: 'a>( - mut data: PropertyIterator<'a, 'b>, -) -> Option<(PropertyIterator<'a, 'b>, usize)> { - let count = data.next()?; - let len = usize::try_from(count.as_i64().unwrap_or(0)).unwrap_or(0); - Some((data, 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() - } -} - -/// 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)) - } -} - -// MARK: Tests -#[cfg(test)] -mod tests { - extern crate std; - - use alloc::{vec, vec::Vec}; - use std::{env::current_dir, fs::File, io::Read}; - - use crate::deserializer::iter::Property; - use crate::deserializer::typedstream::TypedStreamDeserializer; - - /// Load a fixture by path relative to `src/test_data`. - 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 - } - - // MARK: class_name - - #[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); - } - - // MARK: as_string - - #[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() { - // NSArray([NSAttributedString "styled"]) — exercises the scan path. - 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:?}"); - } - - // MARK: as_data - - #[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:?}"); - } - - // MARK: numbers - - #[test] - fn as_f64_reads_decimal_double() { - // NumberDouble root: bare-primitive path, exercises the B1 DECIMAL fix. - let bytes = load("foundation/NumberDouble"); - let mut ts = TypedStreamDeserializer::new(&bytes); - let root = ts.oxidize().unwrap(); - let group = ts.resolve_properties(root).unwrap().next().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(); - let group = ts.resolve_properties(root).unwrap().next().unwrap(); - - assert_eq!(group.as_f64(), Some(3.5)); - } - - #[test] - fn as_i64_reads_large_negative() { - // NumberInt64 = -9_000_000_000: bare-primitive path, exercises the B2 fix. - let bytes = load("foundation/NumberInt64"); - let mut ts = TypedStreamDeserializer::new(&bytes); - let root = ts.oxidize().unwrap(); - let group = ts.resolve_properties(root).unwrap().next().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(); - let group = ts.resolve_properties(root).unwrap().next().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(); - let group = ts.resolve_properties(root).unwrap().next().unwrap(); - - assert_eq!(group.as_u64(), None); - } - - // MARK: as_bool - #[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)); - } - - // MARK: wrappers - - #[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 - } - - // MARK: as_array / as_set - - #[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"]]); - } - - // MARK: as_dictionary - - #[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 nested_array_inside_array() { - // NSArrayNested = [NSString "top", NSArray[1, 2]] — as_array on the nested - // element resolves the inner 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]]); - } - - // MARK: container negatives - - #[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()); - } - - // MARK: as_date / as_unix_time - - #[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]); - } - - // MARK: as_url - - #[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"]); - } - - // MARK: is_null - - #[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); - } - - #[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); - } - - // MARK: container views - - #[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); - } - - #[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/array.rs b/src/deserializer/foundation/array.rs new file mode 100644 index 0000000..912c4d9 --- /dev/null +++ b/src/deserializer/foundation/array.rs @@ -0,0 +1,278 @@ +//! `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 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() { + // NSArrayNested = [NSString "top", NSArray[1, 2]] — as_array on the nested + // element resolves the inner 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..383098e --- /dev/null +++ b/src/deserializer/foundation/boolean.rs @@ -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 { + 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)); + } +} diff --git a/src/deserializer/foundation/bytes.rs b/src/deserializer/foundation/bytes.rs new file mode 100644 index 0000000..3081b00 --- /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 — 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..d6f565d --- /dev/null +++ b/src/deserializer/foundation/date.rs @@ -0,0 +1,74 @@ +//! `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 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..09f658a --- /dev/null +++ b/src/deserializer/foundation/dict.rs @@ -0,0 +1,247 @@ +//! `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 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..0c3a0ce --- /dev/null +++ b/src/deserializer/foundation/helpers.rs @@ -0,0 +1,27 @@ +//! 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> { + let Property::Group(group) = self else { + return None; + }; + match group.first()? { + Property::Object { name, data, .. } if classes.contains(&name) => Some(data), + _ => 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()?; + let len = usize::try_from(count.as_i64().unwrap_or(0)).unwrap_or(0); + Some((data, len)) +} diff --git a/src/deserializer/foundation/mod.rs b/src/deserializer/foundation/mod.rs new file mode 100644 index 0000000..15bfcf5 --- /dev/null +++ b/src/deserializer/foundation/mod.rs @@ -0,0 +1,39 @@ +/*! +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 group-level +[`Property`](crate::deserializer::iter::Property): the value yielded while +iterating an object's properties. 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..f4b516e --- /dev/null +++ b/src/deserializer/foundation/number.rs @@ -0,0 +1,167 @@ +//! `as_i64` / `as_u64` / `as_f64`: numeric `NSNumber`s (or bare primitives). + +use crate::deserializer::iter::Property; +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>> { + let Property::Group(group) = self else { + return None; + }; + match group.first()? { + Property::Primitive(value) => Some(value), + Property::Object { + name: "NSNumber", + mut data, + .. + } => match data.next()? { + Property::Group(inner) => match inner.first()? { + Property::Primitive(value) => Some(value), + _ => None, + }, + _ => None, + }, + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use alloc::vec::Vec; + + use crate::deserializer::foundation::test_support::load; + use crate::deserializer::typedstream::TypedStreamDeserializer; + + #[test] + fn as_f64_reads_decimal_double() { + // NumberDouble root: bare-primitive path, exercises the B1 DECIMAL fix. + let bytes = load("foundation/NumberDouble"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().unwrap(); + + assert_eq!(group.as_f64(), Some(3.5)); + } + + #[test] + fn as_i64_reads_large_negative() { + // NumberInt64 = -9_000_000_000: bare-primitive path, exercises the B2 fix. + let bytes = load("foundation/NumberInt64"); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().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(); + let group = ts.resolve_properties(root).unwrap().next().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..467e38a --- /dev/null +++ b/src/deserializer/foundation/string.rs @@ -0,0 +1,100 @@ +//! `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 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() { + // NSArray([NSAttributedString "styled"]) — exercises the scan path. + 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..2b858f8 --- /dev/null +++ b/src/deserializer/foundation/url.rs @@ -0,0 +1,43 @@ +//! `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 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"]); + } +} From bdfa499ef311426e0b49f142f0c238386ca64955 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 16:54:23 -0700 Subject: [PATCH 17/41] Add build steps for foundation features in CI workflows --- .github/workflows/release.yml | 3 +++ .github/workflows/test.yml | 3 +++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 61086be..7dc1537 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 - run: rustup update stable && rustup default stable + - 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 }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83a4e69..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 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 From a9a874b2f2c791cab8aca41c31c6daaf0bbc0383 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 16:59:55 -0700 Subject: [PATCH 18/41] Update docs for Foundation accessors; improve error handling in deserializer --- README.md | 4 +- src/deserializer/foundation/bytes.rs | 2 +- src/deserializer/foundation/helpers.rs | 17 +++++--- src/deserializer/foundation/mod.rs | 9 +++-- src/deserializer/foundation/number.rs | 40 +++++++++++++------ src/deserializer/iter.rs | 49 +++++++++++++++-------- src/deserializer/typedstream.rs | 54 +++++++++++++++++++++++++- 7 files changed, 134 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 4dfb3cb..6015a42 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,8 @@ The `typedstream` format is derived from the data structure used by `NeXTSTEP`'s `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 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. +- `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`: diff --git a/src/deserializer/foundation/bytes.rs b/src/deserializer/foundation/bytes.rs index 3081b00..8198f3c 100644 --- a/src/deserializer/foundation/bytes.rs +++ b/src/deserializer/foundation/bytes.rs @@ -7,7 +7,7 @@ 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 — they may be a `bplist00`, a + /// 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]> { diff --git a/src/deserializer/foundation/helpers.rs b/src/deserializer/foundation/helpers.rs index 0c3a0ce..cc70baf 100644 --- a/src/deserializer/foundation/helpers.rs +++ b/src/deserializer/foundation/helpers.rs @@ -6,11 +6,14 @@ 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> { - let Property::Group(group) = self else { - return None; - }; - match group.first()? { - Property::Object { name, data, .. } if classes.contains(&name) => Some(data), + 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, } } @@ -22,6 +25,8 @@ pub(crate) fn split_count<'a, 'b: 'a>( mut data: PropertyIterator<'a, 'b>, ) -> Option<(PropertyIterator<'a, 'b>, usize)> { let count = data.next()?; - let len = usize::try_from(count.as_i64().unwrap_or(0)).unwrap_or(0); + // 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 index 15bfcf5..028218f 100644 --- a/src/deserializer/foundation/mod.rs +++ b/src/deserializer/foundation/mod.rs @@ -14,9 +14,12 @@ 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 group-level -[`Property`](crate::deserializer::iter::Property): the value yielded while -iterating an object's properties. Each Foundation type lives in its own module. +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; diff --git a/src/deserializer/foundation/number.rs b/src/deserializer/foundation/number.rs index f4b516e..edc46b4 100644 --- a/src/deserializer/foundation/number.rs +++ b/src/deserializer/foundation/number.rs @@ -1,6 +1,6 @@ //! `as_i64` / `as_u64` / `as_f64`: numeric `NSNumber`s (or bare primitives). -use crate::deserializer::iter::Property; +use crate::deserializer::iter::{Property, PropertyIterator}; use crate::models::output_data::OutputData; impl<'a, 'b: 'a> Property<'a, 'b> { @@ -42,20 +42,20 @@ impl<'a, 'b: 'a> Property<'a, 'b> { /// 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>> { - let Property::Group(group) = self else { - return None; - }; - match group.first()? { + match self { Property::Primitive(value) => Some(value), Property::Object { name: "NSNumber", - mut data, + data, .. - } => match data.next()? { - Property::Group(inner) => match inner.first()? { - Property::Primitive(value) => Some(value), - _ => None, - }, + } => 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, @@ -63,6 +63,17 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } } +/// The first primitive in an `NSNumber` object's first group. +fn number_value<'a, 'b: 'a>(mut data: PropertyIterator<'a, 'b>) -> Option<&'b OutputData<'a>> { + match data.next()? { + Property::Group(inner) => match inner.first()? { + Property::Primitive(value) => Some(value), + _ => None, + }, + _ => None, + } +} + #[cfg(test)] mod tests { use alloc::vec::Vec; @@ -70,6 +81,13 @@ mod tests { 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 root: bare-primitive path, exercises the B1 DECIMAL fix. diff --git a/src/deserializer/iter.rs b/src/deserializer/iter.rs index d7b99fc..b601be2 100644 --- a/src/deserializer/iter.rs +++ b/src/deserializer/iter.rs @@ -86,29 +86,44 @@ impl<'a, 'b: 'a> PropertyGroup<'a, 'b> { /// resolved against the object/type 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.type_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>], + type_table: &'b [TypeEntry<'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, type_table, index)?; + let name = 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"); + 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>; diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index fea6fc4..5a08dd4 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -10,7 +10,7 @@ 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, @@ -193,6 +193,58 @@ impl<'a> TypedStreamDeserializer<'a> { .ok_or(TypedStreamError::InvalidPointer(root_object_index as u8)) } + /// 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::InvalidPointer`] if the index 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> { + object_property(&self.object_table, &self.type_table, object_index) + .ok_or(TypedStreamError::InvalidPointer(object_index as u8)) + } + + /// 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. #[inline(always)] fn consume_current_byte(&mut self) -> Result<&u8> { From a1f13d26f7ea6703eb2553e630df02d99e55d3b9 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 17:00:28 -0700 Subject: [PATCH 19/41] Add test cases --- src/deserializer/foundation/array.rs | 14 ++++++++++++-- src/deserializer/foundation/date.rs | 8 ++++++++ src/deserializer/foundation/dict.rs | 14 +++++++++++++- src/deserializer/foundation/string.rs | 8 +++++++- src/deserializer/foundation/url.rs | 11 +++++++++++ src/deserializer/number.rs | 2 +- 6 files changed, 52 insertions(+), 5 deletions(-) diff --git a/src/deserializer/foundation/array.rs b/src/deserializer/foundation/array.rs index 912c4d9..43dc12f 100644 --- a/src/deserializer/foundation/array.rs +++ b/src/deserializer/foundation/array.rs @@ -173,6 +173,18 @@ mod tests { 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 @@ -211,8 +223,6 @@ mod tests { #[test] fn nested_array_inside_array() { - // NSArrayNested = [NSString "top", NSArray[1, 2]] — as_array on the nested - // element resolves the inner array. let bytes = load("foundation/NSArrayNested"); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); diff --git a/src/deserializer/foundation/date.rs b/src/deserializer/foundation/date.rs index d6f565d..005f63e 100644 --- a/src/deserializer/foundation/date.rs +++ b/src/deserializer/foundation/date.rs @@ -35,6 +35,14 @@ mod tests { 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). diff --git a/src/deserializer/foundation/dict.rs b/src/deserializer/foundation/dict.rs index 09f658a..5cfc5d0 100644 --- a/src/deserializer/foundation/dict.rs +++ b/src/deserializer/foundation/dict.rs @@ -73,7 +73,7 @@ impl<'a, 'b: 'a> FoundationDict<'a, 'b> { /// 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, + /// 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 @@ -198,6 +198,18 @@ mod tests { 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}. diff --git a/src/deserializer/foundation/string.rs b/src/deserializer/foundation/string.rs index 467e38a..5973376 100644 --- a/src/deserializer/foundation/string.rs +++ b/src/deserializer/foundation/string.rs @@ -50,6 +50,13 @@ mod tests { 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"]) @@ -69,7 +76,6 @@ mod tests { #[test] fn as_string_reads_attributed_backing_text() { - // NSArray([NSAttributedString "styled"]) — exercises the scan path. let bytes = load("foundation/NestedAttributed"); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); diff --git a/src/deserializer/foundation/url.rs b/src/deserializer/foundation/url.rs index 2b858f8..b4822fb 100644 --- a/src/deserializer/foundation/url.rs +++ b/src/deserializer/foundation/url.rs @@ -25,6 +25,17 @@ mod tests { 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 diff --git a/src/deserializer/number.rs b/src/deserializer/number.rs index f20255a..b6279e9 100644 --- a/src/deserializer/number.rs +++ b/src/deserializer/number.rs @@ -422,7 +422,7 @@ mod float_tests { #[test] fn can_read_decimal_double_fractional_date() { - // `NSDate(timeIntervalSinceReferenceDate: 700000000.523)``: fractional dates + // `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, From 1501e84d1e7c76ac01705767ddf9ee846d256a04 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Sat, 6 Jun 2026 17:17:04 -0700 Subject: [PATCH 20/41] Better errors for out-of-bounds root access --- src/deserializer/typedstream.rs | 22 ++++++++++++++++++---- src/lib.rs | 24 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 5a08dd4..64acb47 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -176,7 +176,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 +190,14 @@ impl<'a> TypedStreamDeserializer<'a> { /// let iter = ts.resolve_properties(root).unwrap(); /// ``` pub fn resolve_properties(&self, root_object_index: usize) -> Result> { + 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.type_table, root_object_index) - .ok_or(TypedStreamError::InvalidPointer(root_object_index as u8)) + .ok_or(TypedStreamError::InvalidObject) } /// Resolve the object at `object_index` into a group-level [`Property`]. @@ -202,7 +209,8 @@ impl<'a> TypedStreamDeserializer<'a> { /// /// # Errors /// - /// Returns [`TypedStreamError::InvalidPointer`] if the index is not an object. + /// Returns [`TypedStreamError::OutOfBounds`] if the index is past the object + /// table, or [`TypedStreamError::InvalidObject`] if it is not an object. /// /// # Examples /// @@ -216,8 +224,14 @@ impl<'a> TypedStreamDeserializer<'a> { /// 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.type_table, object_index) - .ok_or(TypedStreamError::InvalidPointer(object_index as u8)) + .ok_or(TypedStreamError::InvalidObject) } /// Oxidize the stream and resolve its root object into a [`Property`]. diff --git a/src/lib.rs b/src/lib.rs index 0e67d66..5705a18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,6 +37,30 @@ mod test_typedstream_deserializer { }, }; + #[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() From b8f969ea0330a0d6eda76953fafc7553b2490035 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 20:59:44 -0700 Subject: [PATCH 21/41] Replace inner vectors with `One(OutputData) | Many(Vec)` --- src/deserializer/iter.rs | 6 +- src/deserializer/typedstream.rs | 166 ++++++++++++++++++++++++++++++++ src/lib.rs | 2 +- src/models/archived.rs | 74 +++++++++++--- 4 files changed, 228 insertions(+), 20 deletions(-) diff --git a/src/deserializer/iter.rs b/src/deserializer/iter.rs index b601be2..f002fb7 100644 --- a/src/deserializer/iter.rs +++ b/src/deserializer/iter.rs @@ -5,7 +5,7 @@ use core::slice::Iter; use alloc::vec::Vec; use crate::models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, types::{Type, TypeEntry}, @@ -220,8 +220,8 @@ pub struct PropertyIterator<'a, 'b> { enum GroupSource<'a, 'b> { /// A single inline value that forms one group; yielded exactly once. Inline(Option<&'b OutputData<'a>>), - /// An iterator over the object's group vectors. - Groups(Iter<'b, Vec>>), + /// An iterator over the object's data groups. + Groups(Iter<'b, DataGroup<'a>>), } impl<'a, 'b> PropertyIterator<'a, 'b> { diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 64acb47..3276afe 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -573,3 +573,169 @@ impl<'a> TypedStreamDeserializer<'a> { } } } + +#[cfg(test)] +mod group_tests { + use alloc::{vec, vec::Vec}; + + use super::TypedStreamDeserializer; + use crate::{ + DataGroup, ObjectData, OutputData, Property, + deserializer::constants::{EMPTY, END, START}, + models::archived::Archived, + }; + + fn stream(groups: &[u8]) -> Vec { + let mut bytes = vec![ + 4, 11, b's', b't', b'r', b'e', b'a', b'm', b't', b'y', b'p', b'e', b'd', 0x81, 0xe8, 3, + START, 1, b'@', START, START, START, 1, b'X', 0, EMPTY, + ]; + bytes.extend_from_slice(groups); + bytes.push(END); + bytes + } + + fn unsigned(property: Property<'_, '_>) -> u64 { + let Property::Primitive(value) = property else { + panic!("expected a primitive"); + }; + value.as_u64().unwrap() + } + + #[test] + fn preserves_empty_groups_and_item_order() { + let bytes = stream(&[ + START, 1, b'C', 1, START, 0, START, 2, b'C', b'C', 2, 3, START, 1, b'C', 4, + // An empty embedded value contributes no group. An empty descriptor does. + START, 1, b'*', EMPTY, + ]); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("expected an object"); + }; + assert_eq!(data.group_count(), 4); + assert_eq!( + data, + &ObjectData::Groups(vec![ + DataGroup::One(OutputData::UnsignedInteger(1)), + DataGroup::Many(vec![]), + DataGroup::Many(vec![ + OutputData::UnsignedInteger(2), + OutputData::UnsignedInteger(3), + ]), + DataGroup::One(OutputData::UnsignedInteger(4)), + ]) + ); + + let mut lengths = Vec::new(); + let mut values = Vec::new(); + for property in ts.resolve_properties(root).unwrap() { + let Property::Group(group) = property else { + panic!("expected a group"); + }; + lengths.push(group.len()); + let forward: Vec<_> = group.iter().map(unsigned).collect(); + let reverse: Vec<_> = group.iter().rev().map(unsigned).collect(); + assert_eq!(reverse, forward.iter().rev().copied().collect::>()); + values.extend_from_slice(&forward); + + let mut iter = group.iter(); + assert_eq!(iter.len(), forward.len()); + if !forward.is_empty() { + assert_eq!(unsigned(iter.next().unwrap()), forward[0]); + assert_eq!(iter.len(), forward.len() - 1); + } + if forward.len() == 2 { + assert_eq!(unsigned(iter.next_back().unwrap()), forward[1]); + } + assert_eq!(iter.len(), 0); + assert!(iter.next().is_none()); + assert!(iter.next_back().is_none()); + } + assert_eq!(lengths, [1, 0, 2, 1]); + assert_eq!(values, [1, 2, 3, 4]); + } + + #[test] + fn preserves_groups_when_promoting_inline_data() { + for (descriptors, expected) in [ + ( + vec![START, 1, b'C', 1, START, 1, b'C', 2], + vec![vec![1], vec![2]], + ), + ( + vec![START, 1, b'C', 1, START, 2, b'C', b'C', 2, 3], + vec![vec![1], vec![2, 3]], + ), + ( + vec![START, 2, b'C', b'C', 1, 2, START, 1, b'C', 3], + vec![vec![1, 2], vec![3]], + ), + ] { + let bytes = stream(&descriptors); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { + data: ObjectData::Groups(groups), + .. + } = &ts.object_table[root] + else { + panic!("expected grouped data"); + }; + let values: Vec> = groups + .iter() + .map(|group| { + group + .as_slice() + .iter() + .map(|value| value.as_u64().unwrap()) + .collect() + }) + .collect(); + assert_eq!(values, expected); + assert!(groups.iter().all(|group| !group.is_empty())); + for group in groups { + assert_eq!(matches!(group, DataGroup::One(_)), group.len() == 1); + } + } + } + + #[test] + fn preserves_shared_and_self_references() { + let bytes = stream(&[ + // The child reuses class 1 and occupies object slot 2. + START, 1, b'@', START, 0x93, START, 1, b'C', 7, END, START, 1, b'@', 0x94, START, 2, + b'@', b'@', 0x92, 0x94, + ]); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + assert_eq!(root, 0); + assert_eq!(ts.object_table.len(), 3); + assert_eq!( + ts.object_table[root], + Archived::Object { + class: 1, + data: ObjectData::Groups(vec![ + DataGroup::One(OutputData::Object(2)), + DataGroup::One(OutputData::Object(2)), + DataGroup::Many(vec![OutputData::Object(0), OutputData::Object(2)]), + ]), + } + ); + assert_eq!( + ts.object_table[2], + Archived::Object { + class: 1, + data: ObjectData::Inline(OutputData::UnsignedInteger(7)), + } + ); + let primitives = ts + .resolve_properties(root) + .unwrap() + .primitives_with_limits(10, 100); + assert!(!primitives.is_empty()); + assert!(primitives.len() <= 100); + assert!(primitives.iter().all(|value| value.as_u64() == Some(7))); + } +} diff --git a/src/lib.rs b/src/lib.rs index 5705a18..f0b47f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ pub use deserializer::{ typedstream::TypedStreamDeserializer, }; pub use models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, output_data::OutputData, types::TypeEntry, }; diff --git a/src/models/archived.rs b/src/models/archived.rs index f19aa20..e0b0012 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -4,15 +4,46 @@ use alloc::{vec, vec::Vec}; use crate::models::{class::Class, output_data::OutputData}; +/// A data group within an [`ObjectData::Groups`] collection. +/// +/// Singleton values are stored inline so each dictionary key and value does not +/// need a separate allocation. Empty and multi-value groups retain a [`Vec`]. +#[derive(Debug, PartialEq)] +pub enum DataGroup<'a> { + /// One value, stored inline. + One(OutputData<'a>), + /// An empty or multi-value group. + Many(Vec>), +} + +impl<'a> DataGroup<'a> { + /// Borrow the group's values in stream order. + #[must_use] + pub fn as_slice(&self) -> &[OutputData<'a>] { + match self { + Self::One(value) => core::slice::from_ref(value), + Self::Many(values) => values, + } + } + + /// The number of values in the group. + #[must_use] + pub fn len(&self) -> usize { + self.as_slice().len() + } + + /// Whether the group contains no values. + #[must_use] + pub fn is_empty(&self) -> bool { + self.as_slice().is_empty() + } +} + /// The data attached to an [`Archived::Object`]. /// -/// `typedstream` objects store their values in one or more *groups* (each group -/// is the result of decoding one type descriptor). In practice the overwhelming -/// majority of objects hold a single group containing a single value (an -/// `NSString`'s text, an `NSNumber`'s number, a reference to another object) so -/// that case is stored inline without any heap allocation. Only objects with -/// multiple groups, or a group holding multiple values, fall back to the nested -/// [`Vec`] representation. +/// Each group holds the values decoded from one type descriptor. An object with +/// one single-value group stores that value inline. Other nonempty objects use +/// a group vector, whose singleton values also remain inline. #[derive(Debug, PartialEq)] pub enum ObjectData<'a> { /// The object has no data groups. @@ -20,8 +51,8 @@ pub enum ObjectData<'a> { /// A single group containing a single value, stored inline. This is by far /// the most common shape and avoids two heap allocations per object. Inline(OutputData<'a>), - /// The general case: one or more groups, each holding one or more values. - Groups(Vec>>), + /// One or more groups, preserving empty groups and value order. + Groups(Vec>), } impl<'a> ObjectData<'a> { @@ -31,28 +62,28 @@ impl<'a> ObjectData<'a> { match self { // Common path: the object's first (and usually only) group. ObjectData::Empty => *self = ObjectData::Inline(value), - ObjectData::Groups(groups) => groups.push(vec![value]), + ObjectData::Groups(groups) => groups.push(DataGroup::One(value)), // Promote a previously-inline object to the general representation. ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![vec![first], vec![value]]); + *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::One(value)]); } } } - /// Append a group that contains multiple values. + /// Append an empty or multi-value group. #[inline] pub(crate) fn push_many(&mut self, values: Vec>) { match self { - ObjectData::Empty => *self = ObjectData::Groups(vec![values]), - ObjectData::Groups(groups) => groups.push(values), + ObjectData::Empty => *self = ObjectData::Groups(vec![DataGroup::Many(values)]), + ObjectData::Groups(groups) => groups.push(DataGroup::Many(values)), ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![vec![first], values]); + *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::Many(values)]); } } } @@ -67,7 +98,18 @@ impl<'a> ObjectData<'a> { } else if groups.len() == 1 && groups[0].len() == 1 { ObjectData::Inline(groups.pop().unwrap().pop().unwrap()) } else { - ObjectData::Groups(groups) + ObjectData::Groups( + groups + .into_iter() + .map(|mut group| { + if group.len() == 1 { + DataGroup::One(group.pop().unwrap()) + } else { + DataGroup::Many(group) + } + }) + .collect(), + ) } } From e9b6d847303c7cb37d7a0384c76004ab647d1bfd Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 21:18:51 -0700 Subject: [PATCH 22/41] Fix stream desync for embedded structures like `encodeValuesOfObjCTypes:"i*"` --- src/deserializer/typedstream.rs | 75 ++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 3276afe..30ed70a 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -519,12 +519,23 @@ impl<'a> TypedStreamDeserializer<'a> { for i in 0..len { let ty = self.type_table[types_index][i]; if matches!(ty, Type::EmbeddedData) { - return self.read_embedded(); + // Read the embedded group and merge its contents into the current output vector. + match self.read_embedded()? { + Group::Empty => {} + Group::One(value) => out_v.push(value), + Group::Many(values) => out_v.extend(values), + } + continue; } out_v.push(self.read_value(ty)?); } - Ok(Group::Many(out_v)) + // Convert the output vector into the appropriate group variant. + Ok(if out_v.len() == 1 { + Group::One(out_v.pop().unwrap()) + } else { + Group::Many(out_v) + }) } /// Gets the current type from the stream, either by reading it from the stream or reading it from @@ -701,6 +712,66 @@ mod group_tests { } } + #[test] + fn splices_embedded_values_into_multi_slot_groups() { + // A descriptor with several slots fills one group; an `EmbeddedData` + // slot contributes the embedded values in place. + use OutputData::UnsignedInteger as U; + for (descriptors, expected) in [ + // Embed last: the values decoded before it must survive. + ( + vec![START, 2, b'C', b'*', 1, START, START, 1, b'C', 0x94, 2], + vec![U(1), U(2)], + ), + // Embed first: the slots after it must still be read. + ( + vec![START, 2, b'*', b'C', START, START, 1, b'C', 0x94, 1, 2], + vec![U(1), U(2)], + ), + // Embed in the middle. + ( + vec![ + START, 3, b'C', b'*', b'C', 1, START, START, 1, b'C', 0x94, 2, 3, + ], + vec![U(1), U(2), U(3)], + ), + // A multi-value embedded type flattens into the enclosing group. + ( + vec![ + START, 2, b'C', b'*', 1, START, START, 2, b'C', b'C', 0x94, 2, 3, + ], + vec![U(1), U(2), U(3)], + ), + // A nil embed contributes nothing; the slot after it is still read. + ( + vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], + vec![U(1), U(3)], + ), + // A nil embed can leave a multi-slot descriptor with one value. + (vec![START, 2, b'C', b'*', 1, EMPTY], vec![U(1)]), + ] { + let bytes = stream(&descriptors); + let mut ts = TypedStreamDeserializer::new(&bytes); + let root = ts.oxidize().unwrap(); + let Archived::Object { data, .. } = &ts.object_table[root] else { + panic!("expected an object"); + }; + assert_eq!(data.group_count(), 1, "{descriptors:?}"); + let values = match data { + ObjectData::Inline(value) => core::slice::from_ref(value), + ObjectData::Groups(groups) => groups[0].as_slice(), + ObjectData::Empty => panic!("expected data for {descriptors:?}"), + }; + assert_eq!(values, expected.as_slice(), "{descriptors:?}"); + // A single value never lands in `Many`, whichever path produced it. + assert_eq!( + matches!(data, ObjectData::Inline(_)), + expected.len() == 1, + "{descriptors:?}" + ); + } + } + #[test] fn preserves_shared_and_self_references() { let bytes = stream(&[ From 09829839965175ed2f04c494a5d20800fb33c2d1 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 22:47:16 -0700 Subject: [PATCH 23/41] Refactor DataGroup and ObjectData to reduce indirection --- src/deserializer/typedstream.rs | 116 +++++++++++++++----------------- src/models/archived.rs | 56 +++++++-------- 2 files changed, 78 insertions(+), 94 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 30ed70a..16d118a 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -17,36 +17,13 @@ use crate::{ }, error::{Result, TypedStreamError}, models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, types::{Type, TypeEntry}, }, }; -/// The decoded contents of a single data group, produced by -/// [`TypedStreamDeserializer::read_types`]. The single-value case is kept out of -/// a `Vec` so the common object shape costs no heap allocation. -enum Group<'a> { - /// The group produced no values (e.g. an empty `EmbeddedData`). - Empty, - /// Exactly one value. - One(OutputData<'a>), - /// Two or more values. - Many(Vec>), -} - -impl<'a> Group<'a> { - /// The first value in the group, if any. - fn first(&self) -> Option<&OutputData<'a>> { - match self { - Group::Empty => None, - Group::One(value) => Some(value), - Group::Many(values) => values.first(), - } - } -} - /// Contains logic and data used to deserialize data from a `typedstream`. /// /// `typedstream` is a binary serialization format developed by `NeXTSTEP` and later adopted by Apple. @@ -131,7 +108,6 @@ impl<'a> TypedStreamDeserializer<'a> { /// let result = deserializer.oxidize(); /// ``` pub fn oxidize(&mut self) -> Result { - let mut obj = Group::Empty; let validation = validate_header(self.data)?; // Reserve table capacity now that the input is known to be a valid @@ -151,16 +127,13 @@ impl<'a> TypedStreamDeserializer<'a> { // Advance by the number of bytes consumed by the header validation self.position += validation.bytes_consumed; - // while self.position <= self.data.len() { - let found_type = self.read_type(false)?; - - if let Some(type_index) = found_type { - // Read the types at the specified index - obj = self.read_types(type_index)?; - } - - match obj.first().ok_or(TypedStreamError::InvalidObject)? { - OutputData::Object(idx) => Ok(*idx), + // The root must be an object: a stream with no root descriptor, or one + // whose first value is not an object reference, has nothing to walk. + let Some(type_index) = self.read_type(false)? else { + return Err(TypedStreamError::InvalidObject); + }; + match self.read_types(type_index)?.as_slice().first() { + Some(OutputData::Object(idx)) => Ok(*idx), _ => Err(TypedStreamError::InvalidObject), } } @@ -404,11 +377,7 @@ impl<'a> TypedStreamDeserializer<'a> { // Read the next type, which should be an object if let Some(next_index) = self.read_type(false)? { // Recursively read the types for this object - match self.read_types(next_index)? { - Group::Empty => {} - Group::One(value) => data.push_one(value), - Group::Many(values) => data.push_many(values), - } + data.push(self.read_types(next_index)?); } } self.object_table[placeholder_index] = Archived::Object { class: cls, data }; @@ -491,18 +460,22 @@ impl<'a> TypedStreamDeserializer<'a> { } /// Reads an `EmbeddedData` descriptor, redirecting to the embedded type - /// entry. Returns the group decoded from that entry, or [`Group::Empty`]. - fn read_embedded(&mut self) -> Result> { + /// entry. Returns the group decoded from that entry. + /// + /// A nil embedded type is a `NULL` pointer written into the slot, the same + /// thing a nil object reference is, so it decodes to + /// [`OutputData::Null`] and keeps its position in the object's groups. + fn read_embedded(&mut self) -> Result> { if let Some(idx) = self.read_embedded_type()? { self.position += 1; self.read_types(idx) } else { - Ok(Group::Empty) + Ok(DataGroup::One(OutputData::Null)) } } /// Reads all type descriptors at `types_index` into a single data group. - fn read_types(&mut self, types_index: usize) -> Result> { + fn read_types(&mut self, types_index: usize) -> Result> { let len = self.type_table[types_index].len(); // Common case: a single descriptor decodes to a single value with no Vec. @@ -511,7 +484,7 @@ impl<'a> TypedStreamDeserializer<'a> { return if matches!(ty, Type::EmbeddedData) { self.read_embedded() } else { - Ok(Group::One(self.read_value(ty)?)) + Ok(DataGroup::One(self.read_value(ty)?)) }; } @@ -521,20 +494,20 @@ impl<'a> TypedStreamDeserializer<'a> { if matches!(ty, Type::EmbeddedData) { // Read the embedded group and merge its contents into the current output vector. match self.read_embedded()? { - Group::Empty => {} - Group::One(value) => out_v.push(value), - Group::Many(values) => out_v.extend(values), + DataGroup::One(value) => out_v.push(value), + DataGroup::Values(values) => out_v.extend(values), } continue; } out_v.push(self.read_value(ty)?); } - // Convert the output vector into the appropriate group variant. + // An embed with no types can leave a multi-slot descriptor holding one + // value; keep `One` exact. Ok(if out_v.len() == 1 { - Group::One(out_v.pop().unwrap()) + DataGroup::One(out_v.pop().unwrap()) } else { - Group::Many(out_v) + DataGroup::Values(out_v) }) } @@ -614,10 +587,11 @@ mod group_tests { } #[test] - fn preserves_empty_groups_and_item_order() { + fn preserves_empty_groups_null_slots_and_item_order() { let bytes = stream(&[ + // A zero-length descriptor is a group with no values; it keeps its position. START, 1, b'C', 1, START, 0, START, 2, b'C', b'C', 2, 3, START, 1, b'C', 4, - // An empty embedded value contributes no group. An empty descriptor does. + // A nil `EmbeddedData` is a slot written as `NULL`: one group holding `Null`. START, 1, b'*', EMPTY, ]); let mut ts = TypedStreamDeserializer::new(&bytes); @@ -625,23 +599,25 @@ mod group_tests { let Archived::Object { data, .. } = &ts.object_table[root] else { panic!("expected an object"); }; - assert_eq!(data.group_count(), 4); + assert_eq!(data.group_count(), 5); assert_eq!( data, &ObjectData::Groups(vec![ DataGroup::One(OutputData::UnsignedInteger(1)), - DataGroup::Many(vec![]), - DataGroup::Many(vec![ + DataGroup::Values(vec![]), + DataGroup::Values(vec![ OutputData::UnsignedInteger(2), OutputData::UnsignedInteger(3), ]), DataGroup::One(OutputData::UnsignedInteger(4)), + DataGroup::One(OutputData::Null), ]) ); + let mut properties = ts.resolve_properties(root).unwrap(); let mut lengths = Vec::new(); let mut values = Vec::new(); - for property in ts.resolve_properties(root).unwrap() { + for property in properties.by_ref().take(4) { let Property::Group(group) = property else { panic!("expected a group"); }; @@ -666,6 +642,16 @@ mod group_tests { } assert_eq!(lengths, [1, 0, 2, 1]); assert_eq!(values, [1, 2, 3, 4]); + + let Some(Property::Group(group)) = properties.next() else { + panic!("expected the nil embed's group"); + }; + assert_eq!(group.len(), 1); + assert!(matches!( + group.iter().next(), + Some(Property::Primitive(OutputData::Null)) + )); + assert!(properties.next().is_none()); } #[test] @@ -742,13 +728,17 @@ mod group_tests { ], vec![U(1), U(2), U(3)], ), - // A nil embed contributes nothing; the slot after it is still read. + // A nil embed is a `NULL` slot; the slot after it is still read. ( vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], - vec![U(1), U(3)], + vec![U(1), OutputData::Null, U(3)], + ), + // An embedded type with no types contributes nothing, which can + // leave a multi-slot descriptor with one value. + ( + vec![START, 2, b'C', b'*', 1, START, START, 0, 0x94], + vec![U(1)], ), - // A nil embed can leave a multi-slot descriptor with one value. - (vec![START, 2, b'C', b'*', 1, EMPTY], vec![U(1)]), ] { let bytes = stream(&descriptors); let mut ts = TypedStreamDeserializer::new(&bytes); @@ -763,7 +753,7 @@ mod group_tests { ObjectData::Empty => panic!("expected data for {descriptors:?}"), }; assert_eq!(values, expected.as_slice(), "{descriptors:?}"); - // A single value never lands in `Many`, whichever path produced it. + // A single value never lands in `Values`, whichever path produced it. assert_eq!( matches!(data, ObjectData::Inline(_)), expected.len() == 1, @@ -790,7 +780,7 @@ mod group_tests { data: ObjectData::Groups(vec![ DataGroup::One(OutputData::Object(2)), DataGroup::One(OutputData::Object(2)), - DataGroup::Many(vec![OutputData::Object(0), OutputData::Object(2)]), + DataGroup::Values(vec![OutputData::Object(0), OutputData::Object(2)]), ]), } ); diff --git a/src/models/archived.rs b/src/models/archived.rs index e0b0012..b80a422 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -4,16 +4,17 @@ use alloc::{vec, vec::Vec}; use crate::models::{class::Class, output_data::OutputData}; -/// A data group within an [`ObjectData::Groups`] collection. +/// A data group within an [`ObjectData::Groups`] collection: the values decoded +/// from one type descriptor. /// -/// Singleton values are stored inline so each dictionary key and value does not -/// need a separate allocation. Empty and multi-value groups retain a [`Vec`]. +/// A lone value is stored inline so each dictionary key and value costs no +/// allocation; anything else lives in a [`Vec`]. #[derive(Debug, PartialEq)] pub enum DataGroup<'a> { - /// One value, stored inline. + /// Exactly one value, stored inline. One(OutputData<'a>), - /// An empty or multi-value group. - Many(Vec>), + /// Zero, or multiple, values. + Values(Vec>), } impl<'a> DataGroup<'a> { @@ -22,7 +23,7 @@ impl<'a> DataGroup<'a> { pub fn as_slice(&self) -> &[OutputData<'a>] { match self { Self::One(value) => core::slice::from_ref(value), - Self::Many(values) => values, + Self::Values(values) => values, } } @@ -42,8 +43,8 @@ impl<'a> DataGroup<'a> { /// The data attached to an [`Archived::Object`]. /// /// Each group holds the values decoded from one type descriptor. An object with -/// one single-value group stores that value inline. Other nonempty objects use -/// a group vector, whose singleton values also remain inline. +/// one single-value group stores that value inline; every other nonempty object +/// holds its groups in stream order. #[derive(Debug, PartialEq)] pub enum ObjectData<'a> { /// The object has no data groups. @@ -51,39 +52,32 @@ pub enum ObjectData<'a> { /// A single group containing a single value, stored inline. This is by far /// the most common shape and avoids two heap allocations per object. Inline(OutputData<'a>), - /// One or more groups, preserving empty groups and value order. + /// One or more groups in stream order. Groups(Vec>), } impl<'a> ObjectData<'a> { - /// Append a group that contains exactly one value. + /// Append a group in stream order. + /// + /// The first single-value group is stored inline; a second group of any + /// shape promotes the object to [`Groups`](Self::Groups). #[inline] - pub(crate) fn push_one(&mut self, value: OutputData<'a>) { + pub(crate) fn push(&mut self, group: DataGroup<'a>) { match self { + ObjectData::Groups(groups) => groups.push(group), // Common path: the object's first (and usually only) group. - ObjectData::Empty => *self = ObjectData::Inline(value), - ObjectData::Groups(groups) => groups.push(DataGroup::One(value)), - // Promote a previously-inline object to the general representation. - ObjectData::Inline(_) => { - let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { - unreachable!() - }; - *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::One(value)]); + ObjectData::Empty => { + *self = match group { + DataGroup::One(value) => ObjectData::Inline(value), + group => ObjectData::Groups(vec![group]), + } } - } - } - - /// Append an empty or multi-value group. - #[inline] - pub(crate) fn push_many(&mut self, values: Vec>) { - match self { - ObjectData::Empty => *self = ObjectData::Groups(vec![DataGroup::Many(values)]), - ObjectData::Groups(groups) => groups.push(DataGroup::Many(values)), + // Promote a previously-inline object to the general representation. ObjectData::Inline(_) => { let ObjectData::Inline(first) = core::mem::replace(self, ObjectData::Empty) else { unreachable!() }; - *self = ObjectData::Groups(vec![DataGroup::One(first), DataGroup::Many(values)]); + *self = ObjectData::Groups(vec![DataGroup::One(first), group]); } } } @@ -105,7 +99,7 @@ impl<'a> ObjectData<'a> { if group.len() == 1 { DataGroup::One(group.pop().unwrap()) } else { - DataGroup::Many(group) + DataGroup::Values(group) } }) .collect(), From 6966a76bdd708d8012771c22d4d8c5b3754736ab Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:07:02 -0700 Subject: [PATCH 24/41] Add support for method selectors in typedstream deserialization --- src/deserializer/typedstream.rs | 66 +++++++++++++++++++++++++++++++-- src/models/types.rs | 9 +++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 16d118a..8012eb8 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -384,10 +384,9 @@ impl<'a> TypedStreamDeserializer<'a> { } 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)) @@ -449,6 +448,19 @@ impl<'a> TypedStreamDeserializer<'a> { self.position += length; Ok(OutputData::Array(array_data)) } + // Selectors share the class-name string table, so a repeated + // selector arrives as a reference and resolves through it. + Type::Selector => { + if *read_byte_at(self.data, self.position)? == EMPTY { + self.position += 1; + return Ok(OutputData::Null); + } + let name_idx = self.read_string()?; + match self.type_table[name_idx].first() { + Some(Type::String(selector)) => Ok(OutputData::String(selector)), + _ => Err(TypedStreamError::InvalidObject), + } + } Type::Unknown(byte) => Ok(OutputData::Byte(byte)), // Handle all numeric types Type::SignedInt | Type::UnsignedInt | Type::Float | Type::Double => { @@ -698,6 +710,52 @@ mod group_tests { } } + #[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 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 splices_embedded_values_into_multi_slot_groups() { // A descriptor with several slots fills one group; an `EmbeddedData` diff --git a/src/models/types.rs b/src/models/types.rs index e1e1836..1068822 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -24,6 +24,14 @@ pub enum Type<'a> { /// |--------|-------| /// | `0x2A` | [`*`](https://www.compart.com/en/unicode/U+002A) | EmbeddedData, + /// 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, /// An instance of a class, usually with data. Denoted by: /// /// | Hex | UTF-8 | @@ -80,6 +88,7 @@ impl<'a> Type<'a> { 0x40 => Self::Object, 0x2B => Self::Utf8String, 0x2A => Self::EmbeddedData, + 0x3A => Self::Selector, 0x66 => Self::Float, 0x64 => Self::Double, 0x63 | 0x69 | 0x6c | 0x71 | 0x73 => Self::SignedInt, From ead337b1c7b0acc518ea6a29ed62ab9c1898e096 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:07:07 -0700 Subject: [PATCH 25/41] Fix typo in Utf8String type encoding documentation --- src/models/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/types.rs b/src/models/types.rs index 1068822..ed17c06 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -16,7 +16,7 @@ pub enum Type<'a> { /// /// | 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: /// From ad68a15f0255cf673144d7d361e46b7e146e6a40 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:07:14 -0700 Subject: [PATCH 26/41] Add test for parsing AppKit nib files and include sample nib data --- src/lib.rs | 143 ++++++++++++++++++++++++++++++++++++++- src/test_data/SDLMainNib | Bin 0 -> 1912 bytes 2 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 src/test_data/SDLMainNib diff --git a/src/lib.rs b/src/lib.rs index f0b47f6..3b4b893 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,7 @@ pub use models::{ #[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::{ @@ -8288,4 +8288,145 @@ mod test_typedstream_deserializer { assert_eq!(typedstream.type_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) => { + ts.type_table[class.name_index] + .first() + .and_then(|t| match t { + Type::String(name) => Some(*name), + _ => None, + }) + } + _ => 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}"); + // No byte fell through as an unrecognized type. + assert!(!values.iter().any(|v| matches!(v, OutputData::Byte(_)))); + + // 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()); + } } diff --git a/src/test_data/SDLMainNib b/src/test_data/SDLMainNib new file mode 100644 index 0000000000000000000000000000000000000000..3b7ed12fc83ef49f445b919ab9714665218f5d6b GIT binary patch literal 1912 zcma)7-D@LN6u;B24NbPTxPE{Z@}g^9U)Cq>Q@ZVj4cJs2#K&ct+)Zb9KC+V;YJHKr z++2JT49R6XX0g<&^$w+_lr8k31iKI@1#ABTK}AIr5fKrjo;x$`Mp~_X>2S{Pch2ve zd(KHuvf?^eGu^o|E|TxNsgj|T4&>F`m8t2QxZqA`u13B>lqT|OWI@$h1R3`#u5Cuf zkvi?lNa`bbb<%USX#=aSqgy2}QJsuC2)HLYc z<*h;@$eo1Ta(biGA-C^M-74C%|DE&*1Zlfu+IC1yOzM_y>Tlyyb3VikUA0`$m-P%I ztri@NVSAuUH~y{D(mMc3BW0DiPfdP5;A;Wj2zax>gGIg`@RfkKeZCs-ofhA2^1U|S z3ix7^@3uMR;pQBzYn3(lX24t9u>%f4*0x;e8SIqq+o|^3<;Fe)OBAtzOW`q-5_YSg z_0ckRZrF|~$4_%T2PQC1w<#&Tw_&AY!*<5kob<&kXR!OiPlZ=HF*z- zHW^mLc1if;X+``<=8^b;+(qJdGNOoI$wfu1k_Sj^k!KX~C%L4Eh9cI<`$+sk(u&w6 zPbp%Pyo*HevN#}rA+ckNJ@Tv~7GDy-k>?b#Pd-OtgK#8%B0ESlhsDaMPhL=bRD*5} T=B|v}1zXEHnu%v^`HS&4P;5>n literal 0 HcmV?d00001 From 5ed00f85ebb90ac451f9068aa33aab9b96a8636e Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:57:07 -0700 Subject: [PATCH 27/41] Add support for reading aggregates in typedstream deserialization --- src/error.rs | 9 ++ src/models/types.rs | 368 +++++++++++++++++++++++++++++++++++++------- 2 files changed, 321 insertions(+), 56 deletions(-) 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/models/types.rs b/src/models/types.rs index ed17c06..f918d4a 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -1,8 +1,6 @@ //! 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, - }, + deserializer::{consumed::Consumed, number::read_unsigned_int, read::read_exact_bytes}, error::{Result, TypedStreamError}, }; use alloc::vec::Vec; @@ -38,6 +36,13 @@ pub enum Type<'a> { /// |--------|-------| /// | `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 | /// |--------|-------| @@ -57,8 +62,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: /// @@ -74,82 +81,168 @@ pub enum Type<'a> { 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, - 0x3A => Self::Selector, - 0x66 => Self::Float, - 0x64 => Self::Double, - 0x63 | 0x69 | 0x6c | 0x71 | 0x73 => Self::SignedInt, - 0x43 | 0x49 | 0x4c | 0x51 | 0x53 => Self::UnsignedInt, - other => Self::Unknown(other), - } - } - #[inline] pub(crate) fn new_string(str: &'a str) -> Self { Self::String(str) } - /// 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; - } - let mut len = 0usize; - let mut saw_digit = false; - for &byte in &types[1..] { - if !byte.is_ascii_digit() { - break; - } - len = len * 10 + usize::from(byte - b'0'); - saw_digit = true; - } - saw_digit.then_some(len) - } - + /// Parses a length-prefixed type descriptor into the types it denotes, in + /// slot order. + /// + /// A descriptor is an Objective-C type-encoding string, and `NSArchiver` + /// writes one value per leaf type in it: 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. The only aggregate kept whole is a `char` array, `[Nc]`, which is + /// `N` raw bytes. 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 descriptors are a single scalar letter, + // so keep that case off the heap. + if let [byte] = type_bytes + && let Some(ty) = Type::scalar(*byte) + { + return Ok(Consumed::new(TypeEntry::One(ty), 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)) + let max_slots = data.len() - bytes_consumed; + let mut types = Vec::new(); + let mut pos = 0; + while pos < type_bytes.len() { + Type::parse_one(type_bytes, &mut pos, &mut types, max_slots)?; + } + let entry = if types.len() == 1 { + TypeEntry::One(types.pop().unwrap()) } else { - TypeEntry::Many(type_bytes.iter().copied().map(Type::from_byte).collect()) + TypeEntry::Many(types) }; Ok(Consumed::new(entry, bytes_consumed)) } + + /// The type a single non-aggregate encoding character denotes, if any. + #[inline] + fn scalar(byte: u8) -> Option { + Some(match byte { + b'@' => Self::Object, + b'#' => Self::Class, + b':' => Self::Selector, + b'*' => Self::EmbeddedData, + 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, + }) + } + + /// 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 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(()) + } + } + } } /// One entry in the deserializer's type table. @@ -216,3 +309,166 @@ 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 a bare encoding string as a length-prefixed descriptor followed by + /// `remaining` bytes of stream, which bound array expansion. + fn parse_with( + encoding: &str, + remaining: usize, + ) -> Result, TypedStreamError> { + let mut bytes = vec![u8::try_from(encoding.len()).unwrap()]; + bytes.extend_from_slice(encoding.as_bytes()); + bytes.resize(bytes.len() + remaining, 0); + // The parser borrows nothing from the descriptor, so the lifetime is free. + Type::read_new_type(bytes.leak()).map(|c| { + assert_eq!(c.bytes_consumed, encoding.len() + 1); + c.value + }) + } + + fn parse(encoding: &str) -> Result, TypedStreamError> { + parse_with(encoding, 64) + } + + #[test] + fn scalars() { + for (encoding, expected) in [ + ("@", Type::Object), + ("#", Type::Class), + (":", Type::Selector), + ("*", Type::EmbeddedData), + ("+", 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)) + ); + } +} From f898fb018edb549816d3fb5926a31b699db975f7 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:57:13 -0700 Subject: [PATCH 28/41] Remove ARRAY constant from byte stream indicators --- src/deserializer/constants.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/deserializer/constants.rs b/src/deserializer/constants.rs index c6ed017..c9d5bd2 100644 --- a/src/deserializer/constants.rs +++ b/src/deserializer/constants.rs @@ -17,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; From c8ac0f5b370338b534aaa76d23225832477402f6 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:57:21 -0700 Subject: [PATCH 29/41] Enhance TypedStreamDeserializer to handle class references and aggregates decoding --- src/deserializer/typedstream.rs | 92 +++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 8012eb8..9e9bc70 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -448,8 +448,8 @@ impl<'a> TypedStreamDeserializer<'a> { self.position += length; Ok(OutputData::Array(array_data)) } - // Selectors share the class-name string table, so a repeated - // selector arrives as a reference and resolves through it. + // Selectors are stored in the class-name string table, so a repeated + // one is a reference, resolved through `read_string`. Type::Selector => { if *read_byte_at(self.data, self.position)? == EMPTY { self.position += 1; @@ -461,7 +461,12 @@ impl<'a> TypedStreamDeserializer<'a> { _ => Err(TypedStreamError::InvalidObject), } } - Type::Unknown(byte) => Ok(OutputData::Byte(byte)), + // 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) @@ -578,7 +583,7 @@ mod group_tests { use crate::{ DataGroup, ObjectData, OutputData, Property, deserializer::constants::{EMPTY, END, START}, - models::archived::Archived, + models::{archived::Archived, types::Type}, }; fn stream(groups: &[u8]) -> Vec { @@ -756,6 +761,85 @@ mod group_tests { 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.type_table[class.name_index].first(), + Some(&Type::String("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 splices_embedded_values_into_multi_slot_groups() { // A descriptor with several slots fills one group; an `EmbeddedData` From 7aa8513544049a8994877c7fd63ca4247deee161 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:57:30 -0700 Subject: [PATCH 30/41] Refactor OutputData enum: remove Byte variant and update Object reference documentation --- src/models/output_data.rs | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/models/output_data.rs b/src/models/output_data.rs index 0551d78..0f66444 100644 --- a/src/models/output_data.rs +++ b/src/models/output_data.rs @@ -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"), From 0ac9ea41814f19ce9c8fef950acdb6f7a0880f0f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Tue, 15 Sep 2026 23:57:39 -0700 Subject: [PATCH 31/41] Add tests for parsing NSValue structs and include sample data files --- src/lib.rs | 52 ++++++++++++++++++++++++-- src/test_data/foundation/NSValueRange | Bin 0 -> 67 bytes src/test_data/foundation/NSValueRect | Bin 0 -> 88 bytes src/test_data/foundation/NSValueSize | Bin 0 -> 81 bytes 4 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/test_data/foundation/NSValueRange create mode 100644 src/test_data/foundation/NSValueRect create mode 100644 src/test_data/foundation/NSValueSize diff --git a/src/lib.rs b/src/lib.rs index 3b4b893..f75fe6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ mod test_typedstream_deserializer { use crate::{ deserializer::{iter::print_resolved, typedstream::TypedStreamDeserializer}, models::{ - archived::{Archived, ObjectData}, + archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, types::{Type, TypeEntry}, @@ -8395,8 +8395,6 @@ mod test_typedstream_deserializer { .filter(|v| matches!(v, OutputData::Null)) .count(); assert!(nils >= 20, "expected many nil references, found {nils}"); - // No byte fell through as an unrecognized type. - assert!(!values.iter().any(|v| matches!(v, OutputData::Byte(_)))); // Every object is reachable from the root through object references, // including the back-references from menu items to their menus. @@ -8429,4 +8427,52 @@ mod test_typedstream_deserializer { }; assert_eq!(root_groups, data.group_count()); } + + /// `NSValue`s with struct `objCType`s from the classic archiver + /// (`test_data/generators/foundation.swift`). Struct members are written + /// flat, so `{_NSRange=QQ}` is two slots and `{CGRect={CGPoint=dd}{CGSize=dd}}` + /// four. `CGSize` spells three scalar letters that are not slots. + #[test] + fn test_parse_nsvalue_structs() { + use OutputData::{Double as D, UnsignedInteger as U}; + for (fixture, expected) in [ + ("NSValueRange", vec![U(3), U(4)]), + ("NSValueSize", vec![D(1.5), D(2.5)]), + ("NSValueRect", 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.type_table[class.name_index].first(), + Some(&Type::String("NSValue")), + "{fixture}" + ); + // Expect the members and nothing else: braces, `=`, and name + // letters are grammar, not values. + assert_eq!( + data, + &ObjectData::Groups(vec![DataGroup::Values(expected)]), + "{fixture}" + ); + } + } } diff --git a/src/test_data/foundation/NSValueRange b/src/test_data/foundation/NSValueRange new file mode 100644 index 0000000000000000000000000000000000000000..5a95b2f913bf006c3bd4f5fe17caa82f632ffa79 GIT binary patch literal 67 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!tNIwmY7qT%Fxon;TP45yqyb{}#lv*G=II}7h#H^jh$i&Rj1^{tN9o03vHt(gG~e%M19EDmh|d(0U! literal 0 HcmV?d00001 From e92b1544f134cf6ef2376a7d4cfc985bd1a71513 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 15:58:09 -0700 Subject: [PATCH 32/41] Refactor TypedStreamDeserializer to support shared strings and update type handling for C strings --- src/deserializer/typedstream.rs | 365 +++++++++++++++++--------------- src/models/archived.rs | 8 +- src/models/types.rs | 60 ++++-- 3 files changed, 232 insertions(+), 201 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 9e9bc70..23dc890 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -37,15 +37,20 @@ 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 shared-string table, with descriptor literals stored as parsed types. /// - /// 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. + /// Shared-string layout: literal once, index thereafter. Entry kinds: + /// type descriptors, class names, selectors, and `char *` values. Register + /// names and values as [`Type::String`]; parse an entry in place before + /// using it as a descriptor. Keep every entry's original text available + /// through [`shared_string`](Self::shared_string). pub type_table: Vec>, + /// Text for each [`type_table`](Self::type_table) entry, by index. Keep both + /// vectors aligned by routing every registration through + /// [`register_string`](Self::register_string). + pub(crate) strings: Vec<&'a str>, /// 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> { @@ -68,8 +73,8 @@ impl<'a> TypedStreamDeserializer<'a> { data, position: 0, type_table: Vec::new(), + strings: Vec::new(), object_table: Vec::new(), - seen_embedded_types: Vec::new(), } } @@ -117,19 +122,18 @@ 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 - .reserve((estimated_size / 64).clamp(16, 256)); + let strings = (estimated_size / 64).clamp(16, 256); + self.type_table.reserve(strings); + self.strings.reserve(strings); 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; // The root must be an object: a stream with no root descriptor, or one // whose first value is not an object reference, has nothing to walk. - let Some(type_index) = self.read_type(false)? else { + let Some(type_index) = self.read_type()? else { return Err(TypedStreamError::InvalidObject); }; match self.read_types(type_index)?.as_slice().first() { @@ -248,51 +252,59 @@ 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, None)) } 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.strings.len() { + Ok(index) } else { - Err(TypedStreamError::InvalidPointer(pointer.value as u8)) + Err(TypedStreamError::InvalidPointer(index as u8)) } } } } + /// Returns the text of shared-string entry `index`: a type descriptor, class + /// name, selector, or `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> { + debug_assert_eq!(self.strings.len(), self.type_table.len()); + self.strings.get(index).copied() + } + + /// Appends a shared string to both tables and returns its index. Route every + /// registration through this function so the tables remain aligned. + /// + /// `parsed`: the descriptor view for a descriptor literal. For a name or + /// value, omit parsed types; parse the entry only for descriptor use. + fn register_string(&mut self, text: &'a str, parsed: Option>) -> usize { + self.type_table + .push(parsed.unwrap_or(TypeEntry::One(Type::new_string(text)))); + self.strings.push(text); + debug_assert_eq!(self.strings.len(), self.type_table.len()); + self.strings.len() - 1 + } + + /// 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; @@ -355,6 +367,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 => { @@ -375,7 +388,7 @@ 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 data.push(self.read_types(next_index)?); } @@ -421,11 +434,7 @@ 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> { match ty { @@ -442,25 +451,42 @@ impl<'a> TypedStreamDeserializer<'a> { None => OutputData::Null, }) } - Type::String(s) => Ok(OutputData::String(s)), + // Parse every string-table entry in `read_type` before passing it to `read_types` + Type::String(_) => Err(TypedStreamError::InvalidObject), Type::Array(length) => { let array_data = read_exact_bytes(&self.data[self.position..], length)?; self.position += length; Ok(OutputData::Array(array_data)) } - // Selectors are stored in the class-name string table, so a repeated - // one is a reference, resolved through `read_string`. + // Selector encoding: one literal, then shared-string references. Type::Selector => { if *read_byte_at(self.data, self.position)? == EMPTY { self.position += 1; return Ok(OutputData::Null); } - let name_idx = self.read_string()?; - match self.type_table[name_idx].first() { - Some(Type::String(selector)) => Ok(OutputData::String(selector)), - _ => Err(TypedStreamError::InvalidObject), - } + let index = self.read_string()?; + Ok(OutputData::String(self.strings[index])) } + // 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.strings[index])) + } + ptr => { + let slot = read_pointer(&ptr)?.value as usize; + match self.object_table.get(slot) { + Some(Archived::CString(index)) => { + Ok(OutputData::String(self.strings[*index])) + } + _ => 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()? { @@ -471,105 +497,56 @@ impl<'a> TypedStreamDeserializer<'a> { 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. - /// - /// A nil embedded type is a `NULL` pointer written into the slot, the same - /// thing a nil object reference is, so it decodes to - /// [`OutputData::Null`] and keeps its position in the object's groups. - fn read_embedded(&mut self) -> Result> { - if let Some(idx) = self.read_embedded_type()? { - self.position += 1; - self.read_types(idx) - } else { - Ok(DataGroup::One(OutputData::Null)) } } - /// Reads all type descriptors at `types_index` into a single data group. + /// 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> { let len = self.type_table[types_index].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(DataGroup::One(self.read_value(ty)?)) - }; + 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) { - // Read the embedded group and merge its contents into the current output vector. - match self.read_embedded()? { - DataGroup::One(value) => out_v.push(value), - DataGroup::Values(values) => out_v.extend(values), - } - continue; - } out_v.push(self.read_value(ty)?); } - - // An embed with no types can leave a multi-slot descriptor holding one - // value; keep `One` exact. - Ok(if out_v.len() == 1 { - DataGroup::One(out_v.pop().unwrap()) - } else { - DataGroup::Values(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 + /// [`type_table`](Self::type_table), or `None` at an [`END`]/[`EMPTY`] marker + /// where a descriptor was optional. + /// + /// Descriptor-reference case: a name or `char *` value already in the + /// referenced entry. For `NSValue`'s `objCType`, parse that entry in place, + /// then use its text to decode the following value. Keep the text in + /// [`strings`](Self::strings). + fn read_type(&mut self) -> Result> { + 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 = Type::read_new_type(&self.data[self.position..])?; + self.position += literal.bytes_consumed; + let (text, entry) = literal.value; + Ok(Some(self.register_string(text, Some(entry)))) } END | EMPTY => 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.type_table.len() { + return Err(TypedStreamError::InvalidPointer(index as u8)); } - - 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); - } + if let Some(Type::String(text)) = self.type_table[index].first() { + let remaining = self.data.len() - self.position; + self.type_table[index] = Type::parse_descriptor(text, remaining)?; } - - Ok(Some(ref_tag)) + Ok(Some(index)) } } } @@ -583,7 +560,10 @@ mod group_tests { use crate::{ DataGroup, ObjectData, OutputData, Property, deserializer::constants::{EMPTY, END, START}, - models::{archived::Archived, types::Type}, + models::{ + archived::Archived, + types::{Type, TypeEntry}, + }, }; fn stream(groups: &[u8]) -> Vec { @@ -608,7 +588,7 @@ mod group_tests { 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 `EmbeddedData` is a slot written as `NULL`: one group holding `Null`. + // A nil `char *` value: one `NULL` byte and one group holding `Null`. START, 1, b'*', EMPTY, ]); let mut ts = TypedStreamDeserializer::new(&bytes); @@ -841,69 +821,102 @@ mod group_tests { } #[test] - fn splices_embedded_values_into_multi_slot_groups() { - // A descriptor with several slots fills one group; an `EmbeddedData` - // slot contributes the embedded values in place. - use OutputData::UnsignedInteger as U; - for (descriptors, expected) in [ - // Embed last: the values decoded before it must survive. + 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, 1, b'C', 0x94, 2], - vec![U(1), U(2)], + vec![START, 2, b'C', b'*', 1, START, START, 2, b'h', b'i'], + vec![Values(vec![U(1), S("hi")])], + 1, ), - // Embed first: the slots after it must still be read. ( - vec![START, 2, b'*', b'C', START, START, 1, b'C', 0x94, 1, 2], - vec![U(1), U(2)], + vec![START, 2, b'*', b'C', START, START, 2, b'h', b'i', 2], + vec![Values(vec![S("hi"), U(2)])], + 1, ), - // Embed in the middle. + // `NULL`: one byte; no object-table slot. ( - vec![ - START, 3, b'C', b'*', b'C', 1, START, START, 1, b'C', 0x94, 2, 3, - ], - vec![U(1), U(2), U(3)], + vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], + vec![Values(vec![U(1), Null, U(3)])], + 0, ), - // A multi-value embedded type flattens into the enclosing group. + // Repeated pointer: a bare reference to the existing slot. ( - vec![ - START, 2, b'C', b'*', 1, START, START, 2, b'C', b'C', 0x94, 2, 3, - ], - vec![U(1), U(2), U(3)], + vec![START, 1, b'*', START, START, 2, b'h', b'i', 0x94, 0x94], + vec![One(S("hi")), One(S("hi"))], + 1, ), - // A nil embed is a `NULL` slot; the slot after it is still read. + // Distinct pointer, same text: a new object-table slot and the same + // string-table entry. ( - vec![START, 3, b'C', b'*', b'C', 1, EMPTY, 3], - vec![U(1), OutputData::Null, U(3)], + vec![ + START, 1, b'*', START, START, 2, b'h', b'i', 0x94, START, 0x95, + ], + vec![One(S("hi")), One(S("hi"))], + 2, ), - // An embedded type with no types contributes nothing, which can - // leave a multi-slot descriptor with one value. + // Existing descriptor text (`i`): reuse its string-table entry for + // the C-string value. ( - vec![START, 2, b'C', b'*', 1, START, START, 0, 0x94], - vec![U(1)], + vec![START, 1, b'i', 7, START, 1, b'*', START, 0x94], + vec![One(I(7)), One(S("i"))], + 1, ), ] { - let bytes = stream(&descriptors); + let bytes = stream(&groups); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); - let Archived::Object { data, .. } = &ts.object_table[root] else { - panic!("expected an object"); - }; - assert_eq!(data.group_count(), 1, "{descriptors:?}"); - let values = match data { - ObjectData::Inline(value) => core::slice::from_ref(value), - ObjectData::Groups(groups) => groups[0].as_slice(), - ObjectData::Empty => panic!("expected data for {descriptors:?}"), + let Archived::Object { + data: ObjectData::Groups(got), + .. + } = &ts.object_table[root] + else { + panic!("expected grouped data for {groups:?}"); }; - assert_eq!(values, expected.as_slice(), "{descriptors:?}"); - // A single value never lands in `Values`, whichever path produced it. + assert_eq!(got, &expected, "{groups:?}"); assert_eq!( - matches!(data, ObjectData::Inline(_)), - expected.len() == 1, - "{descriptors:?}" + 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.type_table[3], 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(&[ diff --git a/src/models/archived.rs b/src/models/archived.rs index b80a422..3770c21 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -139,6 +139,10 @@ 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 represented by a shared-string index. During decoding, + /// allocate one object-table slot per distinct pointer and store the string + /// index in that slot. Look up the text in the + /// [`type_table`](crate::deserializer::typedstream::TypedStreamDeserializer::type_table); + /// reuse that entry for descriptors and class names. + CString(usize), } diff --git a/src/models/types.rs b/src/models/types.rs index f918d4a..69af46b 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -16,12 +16,15 @@ pub enum Type<'a> { /// |--------|-------| /// | `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: @@ -93,43 +96,53 @@ impl<'a> Type<'a> { Self::String(str) } - /// Parses a length-prefixed type descriptor into the types it denotes, in - /// slot order. - /// - /// A descriptor is an Objective-C type-encoding string, and `NSArchiver` - /// writes one value per leaf type in it: 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. The only aggregate kept whole is a `char` array, `[Nc]`, which is - /// `N` raw bytes. - pub(crate) fn read_new_type(data: &'_ [u8]) -> Result>> { + /// Reads a length-prefixed type descriptor literal and returns its text with + /// its slot types. Preserve the text for the shared-string table; use this + /// entry by index when decoding a later `char *` value. + pub(crate) fn read_new_type(data: &'_ [u8]) -> Result)>> { let type_length = read_unsigned_int(data)?; 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(); + let text = core::str::from_utf8(type_bytes)?; + let entry = Type::parse_descriptor(text, data.len() - bytes_consumed)?; + Ok(Consumed::new((text, entry), bytes_consumed)) + } + + /// 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] = type_bytes + if let [byte] = bytes && let Some(ty) = Type::scalar(*byte) { - return Ok(Consumed::new(TypeEntry::One(ty), bytes_consumed)); + return Ok(TypeEntry::One(ty)); } - let max_slots = data.len() - bytes_consumed; let mut types = Vec::new(); let mut pos = 0; - while pos < type_bytes.len() { - Type::parse_one(type_bytes, &mut pos, &mut types, max_slots)?; + while pos < bytes.len() { + Type::parse_one(bytes, &mut pos, &mut types, max_slots)?; } - let entry = if types.len() == 1 { + Ok(if types.len() == 1 { TypeEntry::One(types.pop().unwrap()) } else { TypeEntry::Many(types) - }; - Ok(Consumed::new(entry, bytes_consumed)) + }) } /// The type a single non-aggregate encoding character denotes, if any. @@ -139,7 +152,7 @@ impl<'a> Type<'a> { b'@' => Self::Object, b'#' => Self::Class, b':' => Self::Selector, - b'*' => Self::EmbeddedData, + b'*' => Self::CString, b'+' => Self::Utf8String, b'f' => Self::Float, b'd' => Self::Double, @@ -329,7 +342,8 @@ mod type_encoding_tests { // The parser borrows nothing from the descriptor, so the lifetime is free. Type::read_new_type(bytes.leak()).map(|c| { assert_eq!(c.bytes_consumed, encoding.len() + 1); - c.value + assert_eq!(c.value.0, encoding); + c.value.1 }) } @@ -343,7 +357,7 @@ mod type_encoding_tests { ("@", Type::Object), ("#", Type::Class), (":", Type::Selector), - ("*", Type::EmbeddedData), + ("*", Type::CString), ("+", Type::Utf8String), ("f", Type::Float), ("d", Type::Double), From 566322589d29d285095aa3b47025391120639e9f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 15:58:16 -0700 Subject: [PATCH 33/41] Update test cases to retrieve last group in NSNumber properties --- src/deserializer/foundation/boolean.rs | 3 ++- src/deserializer/foundation/number.rs | 27 ++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/deserializer/foundation/boolean.rs b/src/deserializer/foundation/boolean.rs index 383098e..fd89fd7 100644 --- a/src/deserializer/foundation/boolean.rs +++ b/src/deserializer/foundation/boolean.rs @@ -25,7 +25,8 @@ mod tests { 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(); + // 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/number.rs b/src/deserializer/foundation/number.rs index edc46b4..9f3c600 100644 --- a/src/deserializer/foundation/number.rs +++ b/src/deserializer/foundation/number.rs @@ -63,9 +63,11 @@ impl<'a, 'b: 'a> Property<'a, 'b> { } } -/// The first primitive in an `NSNumber` object's first group. -fn number_value<'a, 'b: 'a>(mut data: PropertyIterator<'a, 'b>) -> Option<&'b OutputData<'a>> { - match data.next()? { +/// 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, @@ -90,11 +92,12 @@ mod tests { #[test] fn as_f64_reads_decimal_double() { - // NumberDouble root: bare-primitive path, exercises the B1 DECIMAL fix. + // NumberDouble: bare-primitive path; verify the DECIMAL read. let bytes = load("foundation/NumberDouble"); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); - let group = ts.resolve_properties(root).unwrap().next().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)); } @@ -104,18 +107,20 @@ mod tests { let bytes = load("foundation/NumberFloat"); let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); - let group = ts.resolve_properties(root).unwrap().next().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, exercises the B2 fix. + // 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(); - let group = ts.resolve_properties(root).unwrap().next().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)); } @@ -126,7 +131,8 @@ mod tests { 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(); + // 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 @@ -140,7 +146,8 @@ mod tests { let bytes = load("foundation/NumberInt64"); // -9_000_000_000 let mut ts = TypedStreamDeserializer::new(&bytes); let root = ts.oxidize().unwrap(); - let group = ts.resolve_properties(root).unwrap().next().unwrap(); + // Root `NSNumber` groups: `objCType` first, numeric value last. + let group = ts.resolve_properties(root).unwrap().last().unwrap(); assert_eq!(group.as_u64(), None); } From 8c8c83c153b7f615461475922540d4726b34190f Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 15:58:27 -0700 Subject: [PATCH 34/41] Add new binary file CStrings to test data foundation, update test fixture shape --- src/lib.rs | 810 ++++++++++++++++++++++-------- src/test_data/foundation/CStrings | Bin 0 -> 68 bytes 2 files changed, 603 insertions(+), 207 deletions(-) create mode 100644 src/test_data/foundation/CStrings diff --git a/src/lib.rs b/src/lib.rs index f75fe6a..e4af89c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,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) ] @@ -132,7 +134,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -197,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, @@ -209,7 +214,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; assert_eq!(typedstream.type_table, expected_types); @@ -257,7 +262,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -312,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, @@ -324,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( @@ -333,7 +341,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)], + ]), }, ]; @@ -381,7 +392,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSURL")]), TypeEntry::from_types(vec![Type::SignedInt]), @@ -521,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, @@ -533,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( @@ -542,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( @@ -575,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, @@ -786,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, @@ -802,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, @@ -818,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, @@ -834,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, @@ -850,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, @@ -866,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, @@ -882,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, @@ -898,7 +939,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)], + ]), }, ]; @@ -953,7 +997,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSURL")]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -1075,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, @@ -1087,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( @@ -1118,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, @@ -1189,7 +1239,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSURL")]), @@ -1269,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, @@ -1281,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( @@ -1290,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( @@ -1325,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, @@ -1432,7 +1491,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -1501,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, @@ -1513,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( @@ -1522,7 +1584,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)], + ]), }, ]; @@ -1570,7 +1635,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(904)]), ]; @@ -1645,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, @@ -1657,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![ @@ -1787,7 +1855,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::Double]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -1880,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, @@ -1892,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( @@ -1901,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( @@ -1912,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( @@ -1935,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, @@ -1961,7 +2041,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)], + ]), }, ]; @@ -2012,7 +2095,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -2097,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, @@ -2109,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( @@ -2118,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, @@ -2128,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, @@ -2150,7 +2242,10 @@ 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)], + ]), }, ]; @@ -2263,7 +2358,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(535)]), ]; @@ -2338,7 +2433,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, @@ -2350,7 +2448,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( @@ -2483,7 +2581,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -2545,7 +2643,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, @@ -2557,7 +2658,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( @@ -2566,7 +2667,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, @@ -2594,7 +2698,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)], + ]), }, ]; @@ -2642,7 +2749,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSMutableData")]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(669)]), @@ -2715,7 +2822,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, @@ -2727,7 +2837,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![ @@ -2853,7 +2963,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -2907,7 +3017,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, @@ -2919,7 +3032,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( @@ -2928,7 +3041,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)], + ]), }, ]; @@ -2977,7 +3093,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSURL")]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -3107,7 +3223,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, @@ -3119,7 +3238,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( @@ -3205,7 +3324,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -3264,7 +3383,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, @@ -3276,7 +3398,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![ @@ -3295,7 +3417,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, @@ -3382,7 +3507,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -3437,7 +3562,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, @@ -3449,7 +3577,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( @@ -3458,7 +3586,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)], + ]), }, ]; @@ -3509,7 +3640,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -3576,7 +3707,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, @@ -3588,7 +3722,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; assert_eq!(typedstream.type_table, expected_types); @@ -3638,7 +3772,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -3703,7 +3837,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, @@ -3715,7 +3852,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![ @@ -3791,7 +3928,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -3926,7 +4063,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, @@ -3938,7 +4078,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( @@ -3947,7 +4087,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, @@ -3975,7 +4118,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, @@ -4009,7 +4155,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, @@ -4031,7 +4180,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, @@ -4053,7 +4205,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, @@ -4075,7 +4230,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, @@ -4097,7 +4255,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, @@ -4119,7 +4280,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, @@ -4141,7 +4305,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, @@ -4163,7 +4330,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, @@ -4185,7 +4355,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, @@ -4207,7 +4380,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, @@ -4229,7 +4405,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, @@ -4251,7 +4430,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, @@ -4273,7 +4455,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, @@ -4295,7 +4480,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, @@ -4317,7 +4505,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, @@ -4339,7 +4530,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, @@ -4361,7 +4555,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, @@ -4418,7 +4615,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -4506,7 +4703,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, @@ -4518,7 +4718,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![ @@ -4535,7 +4735,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, @@ -4555,7 +4758,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, @@ -4567,7 +4773,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, @@ -4587,7 +4796,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, @@ -4599,7 +4811,10 @@ 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)], + ]), }, ]; @@ -4650,7 +4865,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -4724,7 +4939,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, @@ -4736,7 +4954,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![ @@ -4767,7 +4985,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, @@ -4785,7 +5006,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("i")], + vec![OutputData::SignedInteger(2)], + ]), }, ]; @@ -4836,7 +5060,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(820)]), @@ -4914,7 +5138,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, @@ -4926,7 +5153,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( @@ -4935,9 +5162,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( @@ -4946,7 +5176,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, @@ -5114,7 +5347,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSURL")]), TypeEntry::from_types(vec![Type::SignedInt]), @@ -5193,7 +5426,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, @@ -5205,7 +5441,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( @@ -5214,7 +5450,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, @@ -5357,7 +5596,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSData")]), @@ -5439,7 +5678,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, @@ -5451,7 +5693,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( @@ -5460,9 +5702,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( @@ -5524,7 +5769,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, @@ -5534,7 +5782,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, @@ -5607,7 +5858,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -5684,7 +5935,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, @@ -5696,7 +5950,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( @@ -5705,9 +5959,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![ @@ -5734,7 +5991,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, @@ -5748,7 +6008,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, @@ -5762,7 +6025,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, @@ -5776,7 +6042,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, @@ -5790,7 +6059,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, @@ -5804,7 +6076,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, @@ -5818,7 +6093,10 @@ 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)], + ]), }, ]; @@ -5867,7 +6145,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -5939,7 +6217,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, @@ -5951,7 +6232,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( @@ -5960,7 +6241,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: 6, @@ -6087,7 +6371,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -6151,7 +6435,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, @@ -6163,7 +6450,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( @@ -6172,7 +6459,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, @@ -6182,9 +6472,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![ @@ -6215,7 +6508,10 @@ 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)], + ]), }, ]; @@ -6264,7 +6560,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -6327,7 +6623,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, @@ -6339,7 +6638,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( @@ -6354,7 +6653,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, @@ -6415,7 +6717,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), ]; @@ -6496,7 +6798,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, @@ -6508,7 +6813,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( @@ -6517,7 +6822,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("q")], + vec![OutputData::SignedInteger(0)], + ]), }, ]; @@ -6567,7 +6875,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(582)]), @@ -6667,7 +6975,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, @@ -6679,7 +6990,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( @@ -6688,7 +6999,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, @@ -6793,7 +7107,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::SignedInt]), TypeEntry::from_types(vec![Type::String("NSMutableData")]), TypeEntry::from_types(vec![Type::String("NSData")]), @@ -6894,7 +7208,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, @@ -6906,7 +7223,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( @@ -6973,7 +7290,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)], + ]), }, ]; @@ -7024,7 +7344,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -7089,7 +7409,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, @@ -7101,7 +7424,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; assert_eq!(typedstream.type_table, expected_types); @@ -7176,7 +7499,7 @@ mod test_typedstream_deserializer { TypeEntry::from_types(vec![Type::Array(1187)]), 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::CString]), ]; let expected_objects = vec![ @@ -7326,7 +7649,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, @@ -7338,7 +7664,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), ]; assert_eq!(typedstream.type_table, expected_types); @@ -7386,7 +7712,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -7441,7 +7767,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, @@ -7453,7 +7782,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), ]; assert_eq!(typedstream.type_table, expected_types); @@ -7501,7 +7830,7 @@ mod test_typedstream_deserializer { 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::CString]), ]; let expected_objects = vec![ @@ -7558,7 +7887,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, @@ -7570,7 +7902,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(2), }), - Archived::Type(7), + Archived::CString(7), ]; assert_eq!(typedstream.type_table, expected_types); @@ -7620,7 +7952,7 @@ mod test_typedstream_deserializer { 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::CString]), TypeEntry::from_types(vec![Type::String("NSData")]), TypeEntry::from_types(vec![Type::Array(764)]), TypeEntry::from_types(vec![Type::Array(748)]), @@ -7699,7 +8031,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, @@ -7711,7 +8046,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![ @@ -7956,7 +8291,7 @@ mod test_typedstream_deserializer { TypeEntry::from_types(vec![Type::Array(754)]), 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::CString]), ]; let expected_objects = vec![ @@ -8082,7 +8417,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 14, - 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: 12, @@ -8094,7 +8432,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; assert_eq!(typedstream.type_table, expected_types); @@ -8146,7 +8484,7 @@ mod test_typedstream_deserializer { TypeEntry::from_types(vec![Type::Array(664)]), 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::CString]), ]; let expected_objects = vec![ @@ -8270,7 +8608,10 @@ mod test_typedstream_deserializer { }, Archived::Object { class: 14, - 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: 12, @@ -8282,7 +8623,7 @@ mod test_typedstream_deserializer { version: 0, parent_index: Some(3), }), - Archived::Type(9), + Archived::CString(9), ]; assert_eq!(typedstream.type_table, expected_types); @@ -8429,16 +8770,22 @@ mod test_typedstream_deserializer { } /// `NSValue`s with struct `objCType`s from the classic archiver - /// (`test_data/generators/foundation.swift`). Struct members are written - /// flat, so `{_NSRange=QQ}` is two slots and `{CGRect={CGPoint=dd}{CGSize=dd}}` - /// four. `CGSize` spells three scalar letters that are not slots. + /// (`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, expected) in [ - ("NSValueRange", vec![U(3), U(4)]), - ("NSValueSize", vec![D(1.5), D(2.5)]), - ("NSValueRect", vec![D(1.0), D(2.0), D(3.0), D(4.0)]), + 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() @@ -8466,13 +8813,62 @@ mod test_typedstream_deserializer { Some(&Type::String("NSValue")), "{fixture}" ); - // Expect the members and nothing else: braces, `=`, and name - // letters are grammar, not values. + // 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::Values(expected)]), + &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]); + } } diff --git a/src/test_data/foundation/CStrings b/src/test_data/foundation/CStrings new file mode 100644 index 0000000000000000000000000000000000000000..babb7f5a2c72c4fe8d7a7c559a2f8c28aaed6961 GIT binary patch literal 68 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!r>fTQk0pOUd#Yw`vv Date: Wed, 16 Sep 2026 16:37:00 -0700 Subject: [PATCH 35/41] Add `SharedString` struct for managing shared strings in deserialization --- src/models/shared_string.rs | 128 ++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/models/shared_string.rs 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); + } +} From 2fd95008ba3376994d988ac0363613627d562a06 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:38:07 -0700 Subject: [PATCH 36/41] Refactor documentation for CString and Class name reference; update OutputData enum comment for clarity --- src/models/archived.rs | 12 +++++++----- src/models/class.rs | 2 +- src/models/mod.rs | 1 + src/models/output_data.rs | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/models/archived.rs b/src/models/archived.rs index 3770c21..7778d71 100644 --- a/src/models/archived.rs +++ b/src/models/archived.rs @@ -139,10 +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, - /// A `char *` value represented by a shared-string index. During decoding, - /// allocate one object-table slot per distinct pointer and store the string - /// index in that slot. Look up the text in the - /// [`type_table`](crate::deserializer::typedstream::TypedStreamDeserializer::type_table); - /// reuse that entry for descriptors and class names. + /// 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 0f66444..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), From aab95ea3b3b0e8370d9c7403cbb9d1eafd933f73 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:38:24 -0700 Subject: [PATCH 37/41] Refactor `PropertyGroup` and `PropertyIterator` to use `SharedString` instead of `TypeEntry` for string management --- src/deserializer/iter.rs | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/deserializer/iter.rs b/src/deserializer/iter.rs index f002fb7..98dcdf5 100644 --- a/src/deserializer/iter.rs +++ b/src/deserializer/iter.rs @@ -8,7 +8,7 @@ use crate::models::{ 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,10 +83,10 @@ 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(object) = object_property(self.object_table, self.type_table, *idx) + && let Some(object) = object_property(self.object_table, self.string_table, *idx) { return object; } @@ -99,7 +99,7 @@ impl<'a, 'b: 'a> PropertyGroup<'a, 'b> { /// [`PropertyGroup`] resolution and the deserializer's object/root resolvers. pub(crate) fn object_property<'a, 'b: 'a>( object_table: &'b [Archived<'a>], - type_table: &'b [TypeEntry<'a>], + string_table: &'b [SharedString<'a>], index: usize, ) -> Option> { let Some(Archived::Object { class: cls, .. }) = object_table.get(index) else { @@ -108,15 +108,10 @@ pub(crate) fn object_property<'a, 'b: 'a>( let Some(Archived::Class(cls)) = object_table.get(*cls) else { return None; }; - let data = PropertyIterator::new(object_table, type_table, index)?; - let name = type_table + let data = PropertyIterator::new(object_table, string_table, index)?; + let name = string_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"); + .map_or("Unknown Class", |entry| entry.text); Some(Property::Object { class: cls, name, @@ -183,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. /// @@ -209,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>, } @@ -227,7 +222,7 @@ enum GroupSource<'a, 'b> { 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)?; @@ -244,7 +239,7 @@ impl<'a, 'b> PropertyIterator<'a, 'b> { Some(Self { object_table, - type_table, + string_table, groups, }) } @@ -347,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, })) } } From dfcac732b03f8f2fd439d50cd873e1545f01efa2 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:40:52 -0700 Subject: [PATCH 38/41] Refactor `TypedStreamDeserializer` to replace `type_table` and `strings` with `string_table` for improved shared string management --- src/deserializer/typedstream.rs | 144 +++++++++++++++++--------------- 1 file changed, 75 insertions(+), 69 deletions(-) diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index 23dc890..f581fdf 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -20,7 +20,8 @@ use crate::{ archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, - types::{Type, TypeEntry}, + shared_string::SharedString, + types::Type, }, }; @@ -37,18 +38,13 @@ pub struct TypedStreamDeserializer<'a> { pub data: &'a [u8], /// The current index we are at in the stream pub(crate) position: usize, - /// The shared-string table, with descriptor literals stored as parsed types. - /// - /// Shared-string layout: literal once, index thereafter. Entry kinds: - /// type descriptors, class names, selectors, and `char *` values. Register - /// names and values as [`Type::String`]; parse an entry in place before - /// using it as a descriptor. Keep every entry's original text available - /// through [`shared_string`](Self::shared_string). - pub type_table: Vec>, - /// Text for each [`type_table`](Self::type_table) entry, by index. Keep both - /// vectors aligned by routing every registration through - /// [`register_string`](Self::register_string). - pub(crate) strings: Vec<&'a str>, + /// 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>, } @@ -72,8 +68,7 @@ impl<'a> TypedStreamDeserializer<'a> { Self { data, position: 0, - type_table: Vec::new(), - strings: Vec::new(), + string_table: Vec::new(), object_table: Vec::new(), } } @@ -122,9 +117,8 @@ 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(); - let strings = (estimated_size / 64).clamp(16, 256); - self.type_table.reserve(strings); - self.strings.reserve(strings); + self.string_table + .reserve((estimated_size / 64).clamp(16, 256)); self.object_table .reserve((estimated_size / 16).clamp(32, 8192)); @@ -173,7 +167,7 @@ impl<'a> TypedStreamDeserializer<'a> { self.object_table.len(), )); } - PropertyIterator::new(&self.object_table, &self.type_table, root_object_index) + PropertyIterator::new(&self.object_table, &self.string_table, root_object_index) .ok_or(TypedStreamError::InvalidObject) } @@ -207,7 +201,7 @@ impl<'a> TypedStreamDeserializer<'a> { self.object_table.len(), )); } - object_property(&self.object_table, &self.type_table, object_index) + object_property(&self.object_table, &self.string_table, object_index) .ok_or(TypedStreamError::InvalidObject) } @@ -259,12 +253,12 @@ impl<'a> TypedStreamDeserializer<'a> { START => { let string_data = read_string(&self.data[self.position..])?; self.position += string_data.bytes_consumed; - Ok(self.register_string(string_data.value, None)) + Ok(self.register_string(string_data.value)) } EMPTY => Err(TypedStreamError::EmptyString), ptr => { let index = read_pointer(&ptr)?.value as usize; - if index < self.strings.len() { + if index < self.string_table.len() { Ok(index) } else { Err(TypedStreamError::InvalidPointer(index as u8)) @@ -273,8 +267,8 @@ impl<'a> TypedStreamDeserializer<'a> { } } - /// Returns the text of shared-string entry `index`: a type descriptor, class - /// name, selector, or `char *` value. + /// 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 /// @@ -287,21 +281,20 @@ impl<'a> TypedStreamDeserializer<'a> { /// ``` #[must_use] pub fn shared_string(&self, index: usize) -> Option<&'a str> { - debug_assert_eq!(self.strings.len(), self.type_table.len()); - self.strings.get(index).copied() + self.string_table.get(index).map(|entry| entry.text) } - /// Appends a shared string to both tables and returns its index. Route every - /// registration through this function so the tables remain aligned. - /// - /// `parsed`: the descriptor view for a descriptor literal. For a name or - /// value, omit parsed types; parse the entry only for descriptor use. - fn register_string(&mut self, text: &'a str, parsed: Option>) -> usize { - self.type_table - .push(parsed.unwrap_or(TypeEntry::One(Type::new_string(text)))); - self.strings.push(text); - debug_assert_eq!(self.strings.len(), self.type_table.len()); - self.strings.len() - 1 + /// 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. @@ -408,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..])?; @@ -436,7 +429,7 @@ impl<'a> TypedStreamDeserializer<'a> { /// 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..])?; @@ -451,8 +444,6 @@ impl<'a> TypedStreamDeserializer<'a> { None => OutputData::Null, }) } - // Parse every string-table entry in `read_type` before passing it to `read_types` - Type::String(_) => Err(TypedStreamError::InvalidObject), Type::Array(length) => { let array_data = read_exact_bytes(&self.data[self.position..], length)?; self.position += length; @@ -465,7 +456,7 @@ impl<'a> TypedStreamDeserializer<'a> { return Ok(OutputData::Null); } let index = self.read_string()?; - Ok(OutputData::String(self.strings[index])) + 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 @@ -475,13 +466,13 @@ impl<'a> TypedStreamDeserializer<'a> { START => { let index = self.read_string()?; self.object_table.push(Archived::CString(index)); - Ok(OutputData::String(self.strings[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.strings[*index])) + Ok(OutputData::String(self.string_table[*index].text)) } _ => Err(TypedStreamError::InvalidPointer(slot as u8)), } @@ -503,17 +494,28 @@ impl<'a> TypedStreamDeserializer<'a> { /// 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> { - let len = self.type_table[types_index].len(); + // `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]; + 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]; + let ty = slot(self, i)?; out_v.push(self.read_value(ty)?); } Ok(DataGroup::Values(out_v)) @@ -521,34 +523,37 @@ impl<'a> TypedStreamDeserializer<'a> { /// Reads a type descriptor: a literal, registered as a new shared string, or /// a reference to an existing one. Returns its index in - /// [`type_table`](Self::type_table), or `None` at an [`END`]/[`EMPTY`] marker + /// [`string_table`](Self::string_table), or `None` at an [`END`]/[`EMPTY`] marker /// where a descriptor was optional. /// - /// Descriptor-reference case: a name or `char *` value already in the - /// referenced entry. For `NSValue`'s `objCType`, parse that entry in place, - /// then use its text to decode the following value. Keep the text in - /// [`strings`](Self::strings). + /// 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> { - match *self.consume_current_byte()? { + let index = match *self.consume_current_byte()? { START => { - let literal = Type::read_new_type(&self.data[self.position..])?; + let literal = read_string(&self.data[self.position..])?; self.position += literal.bytes_consumed; - let (text, entry) = literal.value; - Ok(Some(self.register_string(text, Some(entry)))) + self.register_string(literal.value) } - END | EMPTY => Ok(None), + END | EMPTY => return Ok(None), ptr => { let index = read_pointer(&ptr)?.value as usize; - if index >= self.type_table.len() { + if index >= self.string_table.len() { return Err(TypedStreamError::InvalidPointer(index as u8)); } - if let Some(Type::String(text)) = self.type_table[index].first() { - let remaining = self.data.len() - self.position; - self.type_table[index] = Type::parse_descriptor(text, remaining)?; - } - Ok(Some(index)) + 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)) } } @@ -765,10 +770,7 @@ mod group_tests { let Archived::Class(class) = &ts.object_table[*class_idx] else { panic!("`#` must reference a class entry"); }; - assert_eq!( - ts.type_table[class.name_index].first(), - Some(&Type::String("Foo")) - ); + 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()); @@ -912,7 +914,11 @@ mod group_tests { // descriptor references it, while preserving the original text for // `shared_string`. assert_eq!(ts.shared_string(3), Some("q")); - assert_eq!(ts.type_table[3], TypeEntry::One(Type::SignedInt)); + 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()); } From e46cbde627341816bd3df432d01a307e5f34b4eb Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:41:04 -0700 Subject: [PATCH 39/41] Refactor `Type` and `TypeEntry` enums to remove lifetime parameters and simplify type management --- src/models/types.rs | 87 ++++++++++----------------------------------- 1 file changed, 19 insertions(+), 68 deletions(-) diff --git a/src/models/types.rs b/src/models/types.rs index 69af46b..56cd0c9 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -1,15 +1,12 @@ //! Type tags that denote the type of data stored in a `typedstream` -use crate::{ - deserializer::{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 | @@ -82,35 +79,13 @@ 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), /// `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), } -impl<'a> Type<'a> { - #[inline] - pub(crate) fn new_string(str: &'a str) -> Self { - Self::String(str) - } - - /// Reads a length-prefixed type descriptor literal and returns its text with - /// its slot types. Preserve the text for the shared-string table; use this - /// entry by index when decoding a later `char *` value. - pub(crate) fn read_new_type(data: &'_ [u8]) -> Result)>> { - let type_length = read_unsigned_int(data)?; - 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(); - let text = core::str::from_utf8(type_bytes)?; - let entry = Type::parse_descriptor(text, data.len() - bytes_consumed)?; - Ok(Consumed::new((text, entry), bytes_consumed)) - } - +impl Type { /// Parses a type descriptor into its slot types. /// /// Input: an Objective-C type-encoding string. @@ -122,7 +97,7 @@ impl<'a> Type<'a> { /// 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> { + 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, @@ -167,7 +142,7 @@ impl<'a> Type<'a> { fn parse_one( bytes: &[u8], pos: &mut usize, - out: &mut Vec>, + out: &mut Vec, max_slots: usize, ) -> Result<()> { // Skip method qualifiers (`const`, `in`, `out`, ...) ahead of the type. @@ -258,20 +233,19 @@ impl<'a> Type<'a> { } } -/// 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 { @@ -289,28 +263,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 { @@ -330,24 +292,13 @@ mod type_encoding_tests { use super::{Type, TypeEntry}; use crate::error::TypedStreamError; - /// Parses a bare encoding string as a length-prefixed descriptor followed by - /// `remaining` bytes of stream, which bound array expansion. - fn parse_with( - encoding: &str, - remaining: usize, - ) -> Result, TypedStreamError> { - let mut bytes = vec![u8::try_from(encoding.len()).unwrap()]; - bytes.extend_from_slice(encoding.as_bytes()); - bytes.resize(bytes.len() + remaining, 0); - // The parser borrows nothing from the descriptor, so the lifetime is free. - Type::read_new_type(bytes.leak()).map(|c| { - assert_eq!(c.bytes_consumed, encoding.len() + 1); - assert_eq!(c.value.0, encoding); - c.value.1 - }) + /// 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, TypedStreamError> { + fn parse(encoding: &str) -> Result { parse_with(encoding, 64) } From 586b9656e3146a7ec833a4830484a6d3ae099581 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:41:21 -0700 Subject: [PATCH 40/41] Simplify test cases for new `SharedString` equality test --- src/lib.rs | 1472 +++++++++++++++++++++++++++------------------------- 1 file changed, 764 insertions(+), 708 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e4af89c..f849bfc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,7 @@ pub use deserializer::{ pub use models::{ archived::{Archived, DataGroup, ObjectData}, output_data::OutputData, + shared_string::SharedString, types::TypeEntry, }; @@ -33,7 +34,6 @@ mod test_typedstream_deserializer { archived::{Archived, DataGroup, ObjectData}, class::Class, output_data::OutputData, - types::{Type, TypeEntry}, }, }; @@ -107,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:?}")); @@ -121,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -217,7 +217,7 @@ mod test_typedstream_deserializer { Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -237,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:?}")); @@ -251,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -348,7 +348,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); } @@ -365,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:?}")); @@ -379,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::CString]), - 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![ @@ -946,7 +946,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.len()], expected_objects @@ -969,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:?}")); @@ -983,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::CString]), - 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![ @@ -1192,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); } @@ -1212,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:?}")); @@ -1226,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::CString]), - 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![ @@ -1446,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); } @@ -1466,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:?}")); @@ -1480,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -1591,7 +1591,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); } @@ -1608,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:?}")); @@ -1622,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::CString]), - 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![ @@ -1808,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); } @@ -1828,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:?}")); @@ -1842,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::CString]), - 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![ @@ -2048,7 +2048,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); } @@ -2068,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:?}")); @@ -2082,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -2249,7 +2249,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); } @@ -2266,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:?}")); @@ -2280,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 { @@ -2314,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); } @@ -2331,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:?}")); @@ -2345,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::CString]), - 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![ @@ -2536,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); } @@ -2556,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:?}")); @@ -2570,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -2705,7 +2699,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); } @@ -2722,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:?}")); @@ -2736,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::CString]), - 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![ @@ -2918,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); } @@ -2938,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:?}")); @@ -2952,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3048,7 +3042,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); } @@ -3065,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:?}")); @@ -3079,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::CString]), - 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![ @@ -3279,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); } @@ -3299,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:?}")); @@ -3313,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3446,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); } @@ -3482,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:?}")); @@ -3496,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -3593,7 +3587,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); } @@ -3613,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:?}")); @@ -3627,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3725,7 +3719,7 @@ mod test_typedstream_deserializer { Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -3745,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:?}")); @@ -3759,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -3881,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); } @@ -3901,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:?}")); @@ -3915,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -4568,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); } @@ -4588,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:?}")); @@ -4602,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -4818,7 +4812,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); } @@ -4838,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:?}")); @@ -4852,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -5013,7 +5007,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); } @@ -5033,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:?}")); @@ -5047,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::CString]), - 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![ @@ -5300,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); } @@ -5320,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:?}")); @@ -5334,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::CString]), - 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![ @@ -5549,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); } @@ -5569,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:?}")); @@ -5583,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::CString]), - 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![ @@ -5813,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); } @@ -5833,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:?}")); @@ -5847,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -6100,7 +6094,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); } @@ -6120,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:?}")); @@ -6134,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -6326,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); } @@ -6346,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:?}")); @@ -6360,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -6515,7 +6509,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); } @@ -6535,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:?}")); @@ -6549,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -6672,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); } @@ -6692,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:?}")); @@ -6706,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::CString]), - TypeEntry::from_types(vec![Type::SignedInt]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "q", ]; let expected_objects = vec![ @@ -6829,7 +6823,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); } @@ -6846,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:?}")); @@ -6860,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::CString]), - 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![ @@ -7058,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); } @@ -7078,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:?}")); @@ -7092,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::CString]), - 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![ @@ -7297,7 +7291,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); } @@ -7317,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:?}")); @@ -7331,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::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -7427,7 +7421,7 @@ mod test_typedstream_deserializer { Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -7471,9 +7465,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:?}")); @@ -7485,21 +7479,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("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(1187)]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSMutableData", + "NSData", + "[1187c]", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -7667,7 +7661,7 @@ mod test_typedstream_deserializer { Archived::CString(7), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -7687,9 +7681,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:?}")); @@ -7701,18 +7695,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -7785,7 +7779,7 @@ mod test_typedstream_deserializer { Archived::CString(7), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -7805,9 +7799,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:?}")); @@ -7819,18 +7813,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::CString]), + let expected_types = [ + "@", + "NSAttributedString", + "NSObject", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -7905,7 +7899,7 @@ mod test_typedstream_deserializer { Archived::CString(7), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -7925,9 +7919,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:?}")); @@ -7939,24 +7933,24 @@ 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::CString]), - TypeEntry::from_types(vec![Type::String("NSData")]), - TypeEntry::from_types(vec![Type::Array(764)]), - TypeEntry::from_types(vec![Type::Array(748)]), - TypeEntry::from_types(vec![Type::Array(760)]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSNumber", + "NSValue", + "*", + "NSData", + "[764c]", + "[748c]", + "[760c]", ]; let expected_objects = vec![ @@ -8242,7 +8236,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); } @@ -8262,9 +8256,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:?}")); @@ -8276,22 +8270,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("NSData")]), - TypeEntry::from_types(vec![Type::Array(754)]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSData", + "[754c]", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -8435,7 +8429,7 @@ mod test_typedstream_deserializer { Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -8455,9 +8449,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:?}")); @@ -8469,22 +8463,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("NSData")]), - TypeEntry::from_types(vec![Type::Array(664)]), - TypeEntry::from_types(vec![Type::String("NSNumber")]), - TypeEntry::from_types(vec![Type::String("NSValue")]), - TypeEntry::from_types(vec![Type::CString]), + let expected_types = [ + "@", + "NSMutableAttributedString", + "NSAttributedString", + "NSObject", + "NSMutableString", + "NSString", + "+", + "iI", + "NSDictionary", + "i", + "NSData", + "[664c]", + "NSNumber", + "NSValue", + "*", ]; let expected_objects = vec![ @@ -8626,7 +8620,7 @@ mod test_typedstream_deserializer { Archived::CString(9), ]; - assert_eq!(typedstream.type_table, expected_types); + assert_eq!(typedstream.string_table, expected_types); assert_eq!(typedstream.object_table, expected_objects); } @@ -8654,14 +8648,7 @@ mod test_typedstream_deserializer { ); let class_name = |idx: usize| match &ts.object_table[idx] { - Archived::Class(class) => { - ts.type_table[class.name_index] - .first() - .and_then(|t| match t { - Type::String(name) => Some(*name), - _ => None, - }) - } + Archived::Class(class) => Some(ts.string_table[class.name_index].text), _ => None, }; let Archived::Object { class, .. } = &ts.object_table[root] else { @@ -8809,8 +8796,7 @@ mod test_typedstream_deserializer { panic!("{fixture}: root class is not a class"); }; assert_eq!( - ts.type_table[class.name_index].first(), - Some(&Type::String("NSValue")), + ts.string_table[class.name_index].text, "NSValue", "{fixture}" ); // Expect the `objCType` string first, followed by the members and @@ -8871,4 +8857,74 @@ mod test_typedstream_deserializer { assert!(slots.iter().all(|&i| ts.shared_string(i) == Some("hello"))); assert_eq!(slots[0], slots[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", + "*" + ] + ); + } } From 03d318b854350078427673c7bb2feec6ee8c1985 Mon Sep 17 00:00:00 2001 From: Christopher Sardegna Date: Wed, 16 Sep 2026 16:51:15 -0700 Subject: [PATCH 41/41] Add support for `Atom` type in `TypedStreamDeserializer` and related tests --- src/deserializer/typedstream.rs | 34 +++++++++++++++++++++++++-- src/lib.rs | 40 ++++++++++++++++++++++++++++++++ src/models/types.rs | 12 ++++++++++ src/test_data/foundation/Atoms | Bin 0 -> 60 bytes 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 src/test_data/foundation/Atoms diff --git a/src/deserializer/typedstream.rs b/src/deserializer/typedstream.rs index f581fdf..4ab4ab0 100644 --- a/src/deserializer/typedstream.rs +++ b/src/deserializer/typedstream.rs @@ -449,8 +449,10 @@ impl<'a> TypedStreamDeserializer<'a> { self.position += length; Ok(OutputData::Array(array_data)) } - // Selector encoding: one literal, then shared-string references. - Type::Selector => { + // 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); @@ -720,6 +722,34 @@ mod group_tests { 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, diff --git a/src/lib.rs b/src/lib.rs index f849bfc..ed6dfa5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8858,6 +8858,46 @@ mod test_typedstream_deserializer { 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. diff --git a/src/models/types.rs b/src/models/types.rs index 56cd0c9..c6860f6 100644 --- a/src/models/types.rs +++ b/src/models/types.rs @@ -30,6 +30,16 @@ pub enum Type { /// |--------|-------| /// | `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 | @@ -127,6 +137,7 @@ impl Type { b'@' => Self::Object, b'#' => Self::Class, b':' => Self::Selector, + b'%' => Self::Atom, b'*' => Self::CString, b'+' => Self::Utf8String, b'f' => Self::Float, @@ -308,6 +319,7 @@ mod type_encoding_tests { ("@", Type::Object), ("#", Type::Class), (":", Type::Selector), + ("%", Type::Atom), ("*", Type::CString), ("+", Type::Utf8String), ("f", Type::Float), diff --git a/src/test_data/foundation/Atoms b/src/test_data/foundation/Atoms new file mode 100644 index 0000000000000000000000000000000000000000..acb0854978774020555166cd283148469988c036 GIT binary patch literal 60 zcmZSKE-oobP0TH+EJ#ghe8Jqp=+M&A!s=L(pIgk((!${v?4OjCnq0!r+QO*X!jcFS OpV~TgS_@+)dm8|gmKC@F literal 0 HcmV?d00001