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
78///
79/// # Parameters
80///
81/// * `objects` - The array of objects from the `$objects` key in the `NSKeyedArchiver` format
82/// * `root` - The index into the `objects` array to resolve the current object
83/// * `parent` - Optional reference to the parent object in the recursion chain. Used when
84/// processing dictionary values to provide context for key generation and relative references.
85/// For example, when processing a dictionary entry like `{key: uid_pointer}`, the parent
86/// would be the [`Value`] representing the key itself.
87/// * `item` - Optional reference to a specific item to process instead of looking up `root`
88/// in the `objects` array. This is used when recursing into sub-objects that are already
89/// resolved, such as when processing array elements or dictionary values that don't
90/// contain `UID` pointers.
91fn follow_uid<'a>(
92 objects: &'a [Value],
93 root: usize,
94 parent: Option<&'a Value>,
95 item: Option<&'a Value>,
96) -> Result<Value, PlistParseError> {
97 let item = match item {
98 Some(item) => item,
99 None => objects
100 .get(root)
101 .ok_or(PlistParseError::NoValueAtIndex(root))?,
102 };
103
104 match item {
105 Value::Array(arr) => {
106 let mut array = vec![];
107 for item in arr {
108 if let Some(idx) = item.as_uid() {
109 array.push(follow_uid(objects, idx.get() as usize, parent, None)?);
110 }
111 }
112 Ok(plist::Value::Array(array))
113 }
114 Value::Dictionary(dict) => {
115 let mut dictionary = Dictionary::new();
116 // Handle where type is a Dictionary that points to another single value
117 if let Some(relative) = dict.get("NS.relative") {
118 if let Some(idx) = relative.as_uid()
119 && let Some(p) = &parent
120 {
121 dictionary.insert(
122 value_to_key_string(p),
123 follow_uid(objects, idx.get() as usize, Some(p), None)?,
124 );
125 }
126 }
127 // Handle the NSDictionary and NSMutableDictionary types
128 else if dict.contains_key("NS.keys") && dict.contains_key("NS.objects") {
129 let keys = extract_array_key(dict, "NS.keys")?;
130 // These are the values in the objects list
131 let values = extract_array_key(dict, "NS.objects")?;
132 // Die here if the data is invalid
133 if keys.len() != values.len() {
134 return Err(PlistParseError::InvalidDictionarySize(
135 keys.len(),
136 values.len(),
137 ));
138 }
139
140 for idx in 0..keys.len() {
141 let key_index = extract_uid_idx(keys, idx)?;
142 let value_index = extract_uid_idx(values, idx)?;
143 let key = follow_uid(objects, key_index, None, None)?;
144 let value = follow_uid(objects, value_index, Some(&key), None)?;
145
146 dictionary.insert(value_to_key_string(&key), value);
147 }
148 }
149 // Handle a normal `{key: value}` style dictionary
150 else {
151 for (key, val) in dict {
152 // Skip class names; we don't need them
153 if key == "$class" {
154 continue;
155 }
156 // If the value is a pointer, follow it
157 if let Some(idx) = val.as_uid() {
158 let key_value = Value::String(key.clone());
159 dictionary.insert(
160 key.clone(),
161 follow_uid(objects, idx.get() as usize, Some(&key_value), None)?,
162 );
163 }
164 // If the value is not a pointer, try and follow the data itself
165 else if let Some(p) = parent {
166 dictionary.insert(
167 value_to_key_string(p),
168 follow_uid(objects, root, Some(p), Some(val))?,
169 );
170 }
171 }
172 }
173 Ok(plist::Value::Dictionary(dictionary))
174 }
175 Value::Uid(uid) => follow_uid(objects, uid.get() as usize, None, None),
176 _ => Ok(item.to_owned()),
177 }
178}
179
180/// Helper function to convert a [`Value`] to a string representation for use as dictionary key
181fn value_to_key_string(v: &Value) -> String {
182 match v {
183 Value::String(s) => s.clone(),
184 Value::Integer(i) => i.to_string(),
185 Value::Real(f) => f.to_string(),
186 Value::Boolean(b) => b.to_string(),
187 Value::Date(d) => format!("{d:?}"),
188 Value::Data(_) => "data".to_string(),
189 Value::Array(_) => "array".to_string(),
190 Value::Dictionary(_) => "dict".to_string(),
191 Value::Uid(u) => u.get().to_string(),
192 _ => "unknown".to_string(),
193 }
194}
195
196/// Extract a dictionary from table `plist` data.
197pub fn plist_as_dictionary(plist: &Value) -> Result<&Dictionary, PlistParseError> {
198 plist
199 .as_dictionary()
200 .ok_or_else(|| PlistParseError::InvalidType("body".to_string(), "dictionary".to_string()))
201}
202
203/// Extract a dictionary from a specific key in a collection
204pub fn extract_dictionary<'a>(
205 body: &'a Dictionary,
206 key: &str,
207) -> Result<&'a Dictionary, PlistParseError> {
208 body.get(key)
209 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
210 .as_dictionary()
211 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "dictionary".to_string()))
212}
213
214/// Extract an array from a specific key in a collection
215pub fn extract_array_key<'a>(
216 body: &'a Dictionary,
217 key: &str,
218) -> Result<&'a Vec<Value>, PlistParseError> {
219 body.get(key)
220 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
221 .as_array()
222 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "array".to_string()))
223}
224
225/// Extract a Uid from a specific key in a collection
226fn extract_uid_key(body: &Dictionary, key: &str) -> Result<usize, PlistParseError> {
227 Ok(body
228 .get(key)
229 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
230 .as_uid()
231 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "uid".to_string()))?
232 .get() as usize)
233}
234
235/// Extract bytes from a specific key in a collection
236pub fn extract_bytes_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a [u8], PlistParseError> {
237 body.get(key)
238 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
239 .as_data()
240 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "data".to_string()))
241}
242
243/// Extract an int from a specific key in a collection
244pub fn extract_int_key(body: &Dictionary, key: &str) -> Result<i64, PlistParseError> {
245 Ok(body
246 .get(key)
247 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
248 .as_real()
249 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "int".to_string()))?
250 as i64)
251}
252
253/// Extract an &str from a specific key in a collection
254pub fn extract_string_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a str, PlistParseError> {
255 body.get(key)
256 .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
257 .as_string()
258 .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "string".to_string()))
259}
260
261/// Extract a Uid from a specific index in a collection
262fn extract_uid_idx(body: &[Value], idx: usize) -> Result<usize, PlistParseError> {
263 Ok(body
264 .get(idx)
265 .ok_or(PlistParseError::NoValueAtIndex(idx))?
266 .as_uid()
267 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "uid".to_string()))?
268 .get() as usize)
269}
270
271/// Extract a dictionary from a specific index in a collection
272pub fn extract_dict_idx(body: &[Value], idx: usize) -> Result<&Dictionary, PlistParseError> {
273 body.get(idx)
274 .ok_or(PlistParseError::NoValueAtIndex(idx))?
275 .as_dictionary()
276 .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "dictionary".to_string()))
277}
278
279/// Extract a string from a key-value pair that looks like `{key: String("value")}`
280#[must_use]
281pub fn get_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
282 payload
283 .as_dictionary()?
284 .get(key)?
285 .as_string()
286 .filter(|s| !s.is_empty())
287}
288
289/// Extract an owned string from a key-value pair that looks like `{key: String("value")}`
290#[must_use]
291pub fn get_owned_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<String> {
292 get_string_from_dict(payload, key).map(String::from)
293}
294
295/// Extract an inner dict from a key-value pair that looks like `{key: {key2: val}}`
296#[must_use]
297pub fn get_value_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a Value> {
298 payload.as_dictionary()?.get(key)
299}
300
301/// Extract a bool from a key-value pair that looks like `{key: true}`
302#[must_use]
303pub fn get_bool_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<bool> {
304 payload.as_dictionary()?.get(key)?.as_boolean()
305}
306
307/// Extract a string from a key-value pair that looks like `{key: {key: String("value")}}`
308#[must_use]
309pub fn get_string_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
310 payload
311 .as_dictionary()?
312 .get(key)?
313 .as_dictionary()?
314 .get(key)?
315 .as_string()
316 .filter(|s| !s.is_empty())
317}
318
319/// Extract a float from a key-value pair that looks like `{key: {key: 1.2}}`
320#[must_use]
321pub fn get_float_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<f64> {
322 payload
323 .as_dictionary()?
324 .get(key)?
325 .as_dictionary()?
326 .get(key)?
327 .as_real()
328}