Skip to main content

imessage_database/util/
plist.rs

1/*!
2 Helpers for reading message property-list payloads.
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/// Maximum depth of UID-reference resolution before bailing out.
30const MAX_UID_DEPTH: usize = 256;
31
32/// Deserialize an `NSKeyedArchiver` property list by resolving UID references.
33///
34/// The archive stores objects in `$objects` and points `$top.root` at the root
35/// object. This walks those references and returns the reconstructed value.
36///
37/// For example, a document with a root pointing to some `XML` like
38///
39/// ```xml
40/// <array>
41///     <dict>
42///         <key>link</key>
43///         <dict>
44///              <key>CF$UID</key>
45///              <integer>2</integer>
46///         </dict>
47///     </dict>
48///     <string>https://chrissardegna.com</string>
49/// </array>
50/// ```
51///
52/// parses into a dictionary that looks like:
53///
54/// ```json
55/// {
56///     link: https://chrissardegna.com
57/// }
58/// ```
59///
60/// Some detail on this format is described [here](https://en.wikipedia.org/wiki/Property_list#Serializing_to_plist):
61///
62/// > Internally, `NSKeyedArchiver` somewhat recapitulates the binary plist format by
63/// > storing an object table array called `$objects` in the dictionary. Everything else,
64/// > including class information, is referenced by a UID pointer. A `$top` entry under
65/// > the dict points to the top-level object the programmer was meaning to encode.
66///
67/// # Data Source
68///
69/// The source plist data generally comes from [`Message::payload_data()`](crate::tables::messages::message::Message::payload_data).
70pub fn parse_ns_keyed_archiver(plist: &Value) -> Result<Value, PlistParseError> {
71    let body = plist_as_dictionary(plist)?;
72    let objects = extract_array_key(body, "$objects")?;
73
74    // Index of root object
75    let root = extract_uid_key(extract_dictionary(body, "$top")?, "root")?;
76
77    follow_uid(objects, root, None, None, 0)
78}
79
80/// Resolve one archived object and any UID references it contains.
81fn follow_uid<'a>(
82    objects: &'a [Value],
83    root: usize,
84    parent: Option<&'a Value>,
85    item: Option<&'a Value>,
86    depth: usize,
87) -> Result<Value, PlistParseError> {
88    if depth >= MAX_UID_DEPTH {
89        return Err(PlistParseError::RecursionLimit);
90    }
91    let item = match item {
92        Some(item) => item,
93        None => objects
94            .get(root)
95            .ok_or(PlistParseError::NoValueAtIndex(root))?,
96    };
97
98    match item {
99        Value::Array(arr) => {
100            let mut array = vec![];
101            for item in arr {
102                if let Some(idx) = item.as_uid() {
103                    array.push(follow_uid(
104                        objects,
105                        uid_to_index(idx)?,
106                        parent,
107                        None,
108                        depth + 1,
109                    )?);
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, uid_to_index(idx)?, Some(p), None, depth + 1)?,
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, depth + 1)?;
144                    let value = follow_uid(objects, value_index, Some(&key), None, depth + 1)?;
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(
162                                objects,
163                                uid_to_index(idx)?,
164                                Some(&key_value),
165                                None,
166                                depth + 1,
167                            )?,
168                        );
169                    }
170                    // If the value is not a pointer, try and follow the data itself
171                    else if let Some(p) = parent {
172                        dictionary.insert(
173                            value_to_key_string(p),
174                            follow_uid(objects, root, Some(p), Some(val), depth + 1)?,
175                        );
176                    }
177                }
178            }
179            Ok(plist::Value::Dictionary(dictionary))
180        }
181        Value::Uid(uid) => follow_uid(objects, uid_to_index(uid)?, None, None, depth + 1),
182        _ => Ok(item.to_owned()),
183    }
184}
185
186/// Convert a plist value into a dictionary key.
187fn value_to_key_string(v: &Value) -> String {
188    match v {
189        Value::String(s) => s.clone(),
190        Value::Integer(i) => i.to_string(),
191        Value::Real(f) => f.to_string(),
192        Value::Boolean(b) => b.to_string(),
193        Value::Date(d) => format!("{d:?}"),
194        Value::Data(_) => "data".to_string(),
195        Value::Array(_) => "array".to_string(),
196        Value::Dictionary(_) => "dict".to_string(),
197        Value::Uid(u) => u.get().to_string(),
198        _ => "unknown".to_string(),
199    }
200}
201
202/// Extract a dictionary from table `plist` data.
203pub fn plist_as_dictionary(plist: &Value) -> Result<&Dictionary, PlistParseError> {
204    plist
205        .as_dictionary()
206        .ok_or_else(|| PlistParseError::InvalidType("body".to_string(), "dictionary".to_string()))
207}
208
209/// Extract the shared `richLinkMetadata` payload and one nested metadata value.
210///
211/// Returns `(richLinkMetadata, nested_value)`.
212pub fn rich_link_metadata_and_nested<'a>(
213    payload: &'a Value,
214    nested_key: &str,
215) -> Result<(&'a Value, &'a Value), PlistParseError> {
216    let base = payload
217        .as_dictionary()
218        .ok_or_else(|| PlistParseError::InvalidType("root".to_string(), "dictionary".to_string()))?
219        .get("richLinkMetadata")
220        .ok_or_else(|| PlistParseError::MissingKey("richLinkMetadata".to_string()))?;
221
222    let nested = base
223        .as_dictionary()
224        .ok_or_else(|| {
225            PlistParseError::InvalidType("richLinkMetadata".to_string(), "dictionary".to_string())
226        })?
227        .get(nested_key)
228        .ok_or_else(|| PlistParseError::MissingKey(nested_key.to_string()))?;
229
230    Ok((base, nested))
231}
232
233/// Extract a dictionary from a collection key.
234pub fn extract_dictionary<'a>(
235    body: &'a Dictionary,
236    key: &str,
237) -> Result<&'a Dictionary, PlistParseError> {
238    body.get(key)
239        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
240        .as_dictionary()
241        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "dictionary".to_string()))
242}
243
244/// Extract an array from a collection key.
245pub fn extract_array_key<'a>(
246    body: &'a Dictionary,
247    key: &str,
248) -> Result<&'a Vec<Value>, PlistParseError> {
249    body.get(key)
250        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
251        .as_array()
252        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "array".to_string()))
253}
254
255/// Extract a `UID` from a collection key.
256fn extract_uid_key(body: &Dictionary, key: &str) -> Result<usize, PlistParseError> {
257    let uid = body
258        .get(key)
259        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
260        .as_uid()
261        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "uid".to_string()))?;
262    uid_to_index(uid)
263}
264
265/// Extract bytes from a collection key.
266pub fn extract_bytes_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a [u8], PlistParseError> {
267    body.get(key)
268        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
269        .as_data()
270        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "data".to_string()))
271}
272
273/// Extract a real value from a collection key and coerce it to `i64`.
274pub fn extract_int_key(body: &Dictionary, key: &str) -> Result<i64, PlistParseError> {
275    Ok(body
276        .get(key)
277        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
278        .as_real()
279        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "real".to_string()))?
280        as i64)
281}
282
283/// Extract a string slice from a collection key.
284pub fn extract_string_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a str, PlistParseError> {
285    body.get(key)
286        .ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
287        .as_string()
288        .ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "string".to_string()))
289}
290
291/// Extract a UID from a collection index.
292fn extract_uid_idx(body: &[Value], idx: usize) -> Result<usize, PlistParseError> {
293    let uid = body
294        .get(idx)
295        .ok_or(PlistParseError::NoValueAtIndex(idx))?
296        .as_uid()
297        .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "uid".to_string()))?;
298    uid_to_index(uid)
299}
300
301/// Convert a plist UID into an object-table index without narrowing.
302fn uid_to_index(uid: &plist::Uid) -> Result<usize, PlistParseError> {
303    usize::try_from(uid.get()).map_err(|_| PlistParseError::UidOutOfRange(uid.get()))
304}
305
306/// Extract a dictionary from a collection index.
307pub fn extract_dict_idx(body: &[Value], idx: usize) -> Result<&Dictionary, PlistParseError> {
308    body.get(idx)
309        .ok_or(PlistParseError::NoValueAtIndex(idx))?
310        .as_dictionary()
311        .ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "dictionary".to_string()))
312}
313
314/// Extract a non-empty string from `{key: String("value")}`.
315#[must_use]
316pub fn get_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
317    payload
318        .as_dictionary()?
319        .get(key)?
320        .as_string()
321        .filter(|s| !s.is_empty())
322}
323
324/// Extract an owned non-empty string from `{key: String("value")}`.
325#[must_use]
326pub fn get_owned_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<String> {
327    get_string_from_dict(payload, key).map(String::from)
328}
329
330/// Extract a value from `{key: value}`.
331#[must_use]
332pub fn get_value_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a Value> {
333    payload.as_dictionary()?.get(key)
334}
335
336/// Extract a boolean from `{key: true}`.
337#[must_use]
338pub fn get_bool_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<bool> {
339    payload.as_dictionary()?.get(key)?.as_boolean()
340}
341
342/// Extract a byte slice from `{key: Data(...)}`.
343#[must_use]
344pub fn get_data_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a [u8]> {
345    payload.as_dictionary()?.get(key)?.as_data()
346}
347
348/// Extract a non-empty string from `{key: {key: String("value")}}`.
349#[must_use]
350pub fn get_string_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
351    payload
352        .as_dictionary()?
353        .get(key)?
354        .as_dictionary()?
355        .get(key)?
356        .as_string()
357        .filter(|s| !s.is_empty())
358}
359
360/// Extract a float from `{key: {key: 1.2}}`.
361#[must_use]
362pub fn get_float_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<f64> {
363    payload
364        .as_dictionary()?
365        .get(key)?
366        .as_dictionary()?
367        .get(key)?
368        .as_real()
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use plist::Uid;
375
376    /// Build a plist dictionary from key/value pairs.
377    fn dict(pairs: Vec<(&str, Value)>) -> Value {
378        let mut dictionary = Dictionary::new();
379        for (key, value) in pairs {
380            dictionary.insert(key.to_string(), value);
381        }
382        Value::Dictionary(dictionary)
383    }
384
385    #[test]
386    fn resolves_simple_archive() {
387        // `$top.root` -> `$objects[1]` == "hello"
388        let archive = dict(vec![
389            (
390                "$objects",
391                Value::Array(vec![
392                    Value::String("$null".to_string()),
393                    Value::String("hello".to_string()),
394                ]),
395            ),
396            ("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
397        ]);
398
399        assert_eq!(
400            parse_ns_keyed_archiver(&archive).unwrap(),
401            Value::String("hello".to_string())
402        );
403    }
404
405    #[test]
406    fn cyclic_uid_reference_is_rejected_not_overflowed() {
407        // `$objects[1]` points at itself and `$top.root` points at index 1. Without
408        // a depth bound this recurses forever and aborts the process via stack
409        // overflow; the bound converts it into a recoverable error instead.
410        let archive = dict(vec![
411            (
412                "$objects",
413                Value::Array(vec![
414                    Value::String("$null".to_string()),
415                    Value::Uid(Uid::new(1)),
416                ]),
417            ),
418            ("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
419        ]);
420
421        assert!(matches!(
422            parse_ns_keyed_archiver(&archive),
423            Err(PlistParseError::RecursionLimit)
424        ));
425    }
426}