imessage_database/util/plist.rs
1/*!
2 Contains logic and data structures used to parse and deserialize [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) property list files into native Rust data structures.
3
4 The main entry point is [`parse_ns_keyed_archiver()`]. For normal property lists, use [`plist_as_dictionary()`].
5
6 ## Overview
7
8 The `NSKeyedArchiver` format is a property list-based serialization protocol used by Apple's Foundation framework.
9 It stores object graphs in a keyed format, allowing for more flexible deserialization and better handling of
10 object references compared to the older typedstream format.
11
12 ## Origin
13
14 Introduced in Mac OS X 10.2 as part of the Foundation framework, `NSKeyedArchiver` replaced `NSArchiver`
15 ([`typedstream`](crate::util::typedstream)) system as Apple's primary object serialization mechanism.
16
17 ## Features
18
19 - Pure Rust implementation for efficient and safe deserialization
20 - Support for both XML and binary property list formats
21 - No dependencies on Apple frameworks
22 - Robust error handling for malformed or invalid archives
23*/
24
25use plist::{Dictionary, Value};
26
27use crate::error::plist::PlistParseError;
28
29/// Serialize a message's `payload_data` BLOB in the [`NSKeyedArchiver`](https://developer.apple.com/documentation/foundation/nskeyedarchiver) format to a [`Dictionary`]
30/// that follows the references in the XML document's UID pointers. First, we find the root of the
31/// document, then walk the structure, promoting values to the places where their pointers are stored.
32///
33/// For example, a document with a root pointing to some `XML` like
34///
35/// ```xml
36/// <array>
37/// <dict>
38/// <key>link</key>
39/// <dict>
40/// <key>CF$UID</key>
41/// <integer>2</integer>
42/// </dict>
43/// </dict>
44/// <string>https://chrissardegna.com</string>
45/// </array>
46/// ```
47///
48/// Will serialize to a dictionary that looks like:
49///
50/// ```json
51/// {
52/// link: https://chrissardegna.com
53/// }
54/// ```
55///
56/// Some detail on this format is described [here](https://en.wikipedia.org/wiki/Property_list#Serializing_to_plist):
57///
58/// > Internally, `NSKeyedArchiver` somewhat recapitulates the binary plist format by
59/// > storing an object table array called `$objects` in the dictionary. Everything else,
60/// > including class information, is referenced by a UID pointer. A `$top` entry under
61/// > the dict points to the top-level object the programmer was meaning to encode.
62///
63/// # Data Source
64///
65/// The source plist data generally comes from [`Message::payload_data()`](crate::tables::messages::message::Message::payload_data).
66pub fn parse_ns_keyed_archiver(plist: &Value) -> Result<Value, PlistParseError> {
67 let body = plist_as_dictionary(plist)?;
68 let objects = extract_array_key(body, "$objects")?;
69
70 // Index of root object
71 let root = extract_uid_key(extract_dictionary(body, "$top")?, "root")?;
72
73 follow_uid(objects, root, None, None)
74}
75
76/// Recursively follows pointers in an `NSKeyedArchiver` format, promoting the values
77/// to the positions where the pointers live
78fn follow_uid<'a>(
79 objects: &'a [Value],
80 root: usize,
81 parent: Option<&str>,
82 item: Option<&'a Value>,
83) -> Result<Value, PlistParseError> {
84 let item = match item {
85 Some(item) => item,
86 None => objects
87 .get(root)
88 .ok_or(PlistParseError::NoValueAtIndex(root))?,
89 };
90
91 match item {
92 Value::Array(arr) => {
93 let mut array = vec![];
94 for item in arr {
95 if let Some(idx) = item.as_uid() {
96 array.push(follow_uid(objects, idx.get() as usize, parent, None)?);
97 }
98 }
99 Ok(plist::Value::Array(array))
100 }
101 Value::Dictionary(dict) => {
102 let mut dictionary = Dictionary::new();
103 // Handle where type is a Dictionary that points to another single value
104 if let Some(relative) = dict.get("NS.relative") {
105 if let Some(idx) = relative.as_uid() {
106 if let Some(p) = &parent {
107 dictionary.insert(
108 p.to_string(),
109 follow_uid(objects, idx.get() as usize, Some(p), None)?,
110 );
111 }
112 }
113 }
114 // Handle the NSDictionary and NSMutableDictionary types
115 else if dict.contains_key("NS.keys") && dict.contains_key("NS.objects") {
116 let keys = extract_array_key(dict, "NS.keys")?;
117 // These are the values in the objects list
118 let values = extract_array_key(dict, "NS.objects")?;
119 // Die here if the data is invalid
120 if keys.len() != values.len() {
121 return Err(PlistParseError::InvalidDictionarySize(
122 keys.len(),
123 values.len(),
124 ));
125 }
126
127 for idx in 0..keys.len() {
128 let key_index = extract_uid_idx(keys, idx)?;
129 let value_index = extract_uid_idx(values, idx)?;
130 let key = extract_string_idx(objects, key_index)?;
131
132 dictionary.insert(
133 key.into(),
134 follow_uid(objects, value_index, Some(key), None)?,
135 );
136 }
137 }
138 // Handle a normal `{key: value}` style dictionary
139 else {
140 for (key, val) in dict {
141 // Skip class names; we don't need them
142 if key == "$class" {
143 continue;
144 }
145 // If the value is a pointer, follow it
146 if let Some(idx) = val.as_uid() {
147 dictionary.insert(
148 key.into(),
149 follow_uid(objects, idx.get() as usize, Some(key), None)?,
150 );
151 }
152 // If the value is not a pointer, try and follow the data itself
153 else if let Some(p) = parent {
154 dictionary.insert(p.into(), follow_uid(objects, root, Some(p), Some(val))?);
155 }
156 }
157 }
158 Ok(plist::Value::Dictionary(dictionary))
159 }
160 Value::Uid(uid) => follow_uid(objects, uid.get() as usize, None, None),
161 _ => Ok(item.to_owned()),
162 }
163}
164
165/// Extract a dictionary from table `plist` data.
166pub fn plist_as_dictionary(plist: &Value) -> Result<&Dictionary, PlistParseError> {
167 plist
168 .as_dictionary()
169 .ok_or_else(|| PlistParseError::InvalidType("body".to_string(), "dictionary".to_string()))
170}
171
172/// Extract a dictionary from a specific key in a collection
173pub fn extract_dictionary<'a>(
174 body: &'a Dictionary,
175 key: &str,
176) -> Result<&'a Dictionary, PlistParseError> {
177 body.get(key)
178 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
179 .as_dictionary()
180 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "dictionary".to_string()))
181}
182
183/// Extract an array from a specific key in a collection
184pub fn extract_array_key<'a>(
185 body: &'a Dictionary,
186 key: &str,
187) -> Result<&'a Vec<Value>, PlistParseError> {
188 body.get(key)
189 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
190 .as_array()
191 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "array".to_string()))
192}
193
194/// Extract a Uid from a specific key in a collection
195fn extract_uid_key(body: &Dictionary, key: &str) -> Result<usize, PlistParseError> {
196 Ok(body
197 .get(key)
198 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
199 .as_uid()
200 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "uid".to_string()))?
201 .get() as usize)
202}
203
204/// Extract bytes from a specific key in a collection
205pub fn extract_bytes_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a [u8], PlistParseError> {
206 body.get(key)
207 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
208 .as_data()
209 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "data".to_string()))
210}
211
212/// Extract an int from a specific key in a collection
213pub fn extract_int_key(body: &Dictionary, key: &str) -> Result<i64, PlistParseError> {
214 Ok(body
215 .get(key)
216 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
217 .as_real()
218 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "int".to_string()))?
219 as i64)
220}
221
222/// Extract a Uid from a specific index in a collection
223fn extract_uid_idx(body: &[Value], idx: usize) -> Result<usize, PlistParseError> {
224 Ok(body
225 .get(idx)
226 .ok_or(PlistParseError::NoValueAtIndex(idx))?
227 .as_uid()
228 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "uid".to_string()))?
229 .get() as usize)
230}
231
232/// Extract a string from a specific index in a collection
233fn extract_string_idx(body: &[Value], idx: usize) -> Result<&str, PlistParseError> {
234 body.get(idx)
235 .ok_or(PlistParseError::NoValueAtIndex(idx))?
236 .as_string()
237 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "string".to_string()))
238}
239
240/// Extract a string from a key-value pair that looks like `{key: String("value")}`
241pub fn get_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
242 payload
243 .as_dictionary()?
244 .get(key)?
245 .as_string()
246 .filter(|s| !s.is_empty())
247}
248
249/// Extract an inner dict from a key-value pair that looks like `{key: {key2: val}}`
250pub fn get_value_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a Value> {
251 payload.as_dictionary()?.get(key)
252}
253
254/// Extract a bool from a key-value pair that looks like `{key: true}`
255pub fn get_bool_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<bool> {
256 payload.as_dictionary()?.get(key)?.as_boolean()
257}
258
259/// Extract a string from a key-value pair that looks like `{key: {key: String("value")}}`
260pub fn get_string_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
261 payload
262 .as_dictionary()?
263 .get(key)?
264 .as_dictionary()?
265 .get(key)?
266 .as_string()
267 .filter(|s| !s.is_empty())
268}
269
270/// Extract a float from a key-value pair that looks like `{key: {key: 1.2}}`
271pub fn get_float_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<f64> {
272 payload
273 .as_dictionary()?
274 .get(key)?
275 .as_dictionary()?
276 .get(key)?
277 .as_real()
278}