Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions src/deserializer/typedstream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand Down
40 changes: 40 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions src/models/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -308,6 +319,7 @@ mod type_encoding_tests {
("@", Type::Object),
("#", Type::Class),
(":", Type::Selector),
("%", Type::Atom),
("*", Type::CString),
("+", Type::Utf8String),
("f", Type::Float),
Expand Down
Binary file added src/test_data/foundation/Atoms
Binary file not shown.
Loading