use plist::{Dictionary, Value};
use crate::error::plist::PlistParseError;
const MAX_UID_DEPTH: usize = 256;
const NULL_PLACEHOLDER: &str = "$null";
pub fn parse_ns_keyed_archiver(plist: &Value) -> Result<Value, PlistParseError> {
let body = plist_as_dictionary(plist)?;
let objects = extract_array_key(body, "$objects")?;
let root = extract_uid_key(extract_dictionary(body, "$top")?, "root")?;
follow_uid(objects, root, None, None, 0)?.ok_or(PlistParseError::NoPayload)
}
fn follow_uid<'a>(
objects: &'a [Value],
root: usize,
parent: Option<&'a Value>,
item: Option<&'a Value>,
depth: usize,
) -> Result<Option<Value>, PlistParseError> {
if depth >= MAX_UID_DEPTH {
return Err(PlistParseError::RecursionLimit);
}
if item.is_none() && is_null_placeholder(objects, root) {
return Ok(None);
}
let item = match item {
Some(item) => item,
None => objects
.get(root)
.ok_or(PlistParseError::NoValueAtIndex(root))?,
};
match item {
Value::Array(arr) => {
let mut array = vec![];
for item in arr {
if let Some(idx) = item.as_uid()
&& let Some(value) =
follow_uid(objects, uid_to_index(idx)?, parent, None, depth + 1)?
{
array.push(value);
}
}
Ok(Some(plist::Value::Array(array)))
}
Value::Dictionary(dict) => {
let mut dictionary = Dictionary::new();
if let Some(relative) = dict.get("NS.relative") {
if let Some(idx) = relative.as_uid()
&& let Some(p) = parent
&& let Some(value) =
follow_uid(objects, uid_to_index(idx)?, Some(p), None, depth + 1)?
{
dictionary.insert(value_to_key_string(p), value);
}
}
else if dict.contains_key("NS.keys") && dict.contains_key("NS.objects") {
let keys = extract_array_key(dict, "NS.keys")?;
let values = extract_array_key(dict, "NS.objects")?;
if keys.len() != values.len() {
return Err(PlistParseError::InvalidDictionarySize(
keys.len(),
values.len(),
));
}
for idx in 0..keys.len() {
let key_index = extract_uid_idx(keys, idx)?;
let value_index = extract_uid_idx(values, idx)?;
let Some(key) = follow_uid(objects, key_index, None, None, depth + 1)? else {
continue;
};
let Some(value) =
follow_uid(objects, value_index, Some(&key), None, depth + 1)?
else {
continue;
};
dictionary.insert(value_to_key_string(&key), value);
}
}
else {
for (key, val) in dict {
if key == "$class" {
continue;
}
if let Some(idx) = val.as_uid() {
let key_value = Value::String(key.clone());
if let Some(value) = follow_uid(
objects,
uid_to_index(idx)?,
Some(&key_value),
None,
depth + 1,
)? {
dictionary.insert(key.clone(), value);
}
}
else if let Some(p) = parent
&& let Some(value) =
follow_uid(objects, root, Some(p), Some(val), depth + 1)?
{
dictionary.insert(value_to_key_string(p), value);
}
}
}
Ok(Some(plist::Value::Dictionary(dictionary)))
}
Value::Uid(uid) => follow_uid(objects, uid_to_index(uid)?, None, None, depth + 1),
_ => Ok(Some(item.to_owned())),
}
}
fn is_null_placeholder(objects: &[Value], index: usize) -> bool {
index == 0
&& objects
.first()
.and_then(Value::as_string)
.is_some_and(|value| value == NULL_PLACEHOLDER)
}
fn value_to_key_string(v: &Value) -> String {
match v {
Value::String(s) => s.clone(),
Value::Integer(i) => i.to_string(),
Value::Real(f) => f.to_string(),
Value::Boolean(b) => b.to_string(),
Value::Date(d) => format!("{d:?}"),
Value::Data(_) => "data".to_string(),
Value::Array(_) => "array".to_string(),
Value::Dictionary(_) => "dict".to_string(),
Value::Uid(u) => u.get().to_string(),
_ => "unknown".to_string(),
}
}
pub fn plist_as_dictionary(plist: &Value) -> Result<&Dictionary, PlistParseError> {
plist
.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidType("body".to_string(), "dictionary".to_string()))
}
pub fn rich_link_metadata_and_nested<'a>(
payload: &'a Value,
nested_key: &str,
) -> Result<(&'a Value, &'a Value), PlistParseError> {
let base = payload
.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidType("root".to_string(), "dictionary".to_string()))?
.get("richLinkMetadata")
.ok_or_else(|| PlistParseError::MissingKey("richLinkMetadata".to_string()))?;
let nested = base
.as_dictionary()
.ok_or_else(|| {
PlistParseError::InvalidType("richLinkMetadata".to_string(), "dictionary".to_string())
})?
.get(nested_key)
.ok_or_else(|| PlistParseError::MissingKey(nested_key.to_string()))?;
Ok((base, nested))
}
pub fn extract_dictionary<'a>(
body: &'a Dictionary,
key: &str,
) -> Result<&'a Dictionary, PlistParseError> {
body.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "dictionary".to_string()))
}
pub fn extract_array_key<'a>(
body: &'a Dictionary,
key: &str,
) -> Result<&'a Vec<Value>, PlistParseError> {
body.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_array()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "array".to_string()))
}
fn extract_uid_key(body: &Dictionary, key: &str) -> Result<usize, PlistParseError> {
let uid = body
.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_uid()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "uid".to_string()))?;
uid_to_index(uid)
}
pub fn extract_bytes_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a [u8], PlistParseError> {
body.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_data()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "data".to_string()))
}
pub fn extract_int_key(body: &Dictionary, key: &str) -> Result<i64, PlistParseError> {
Ok(body
.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_real()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "real".to_string()))?
as i64)
}
pub fn extract_string_key<'a>(body: &'a Dictionary, key: &str) -> Result<&'a str, PlistParseError> {
body.get(key)
.ok_or_else(|| PlistParseError::MissingKey(key.to_string()))?
.as_string()
.ok_or_else(|| PlistParseError::InvalidType(key.to_string(), "string".to_string()))
}
fn extract_uid_idx(body: &[Value], idx: usize) -> Result<usize, PlistParseError> {
let uid = body
.get(idx)
.ok_or(PlistParseError::NoValueAtIndex(idx))?
.as_uid()
.ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "uid".to_string()))?;
uid_to_index(uid)
}
fn uid_to_index(uid: &plist::Uid) -> Result<usize, PlistParseError> {
usize::try_from(uid.get()).map_err(|_| PlistParseError::UidOutOfRange(uid.get()))
}
pub fn extract_dict_idx(body: &[Value], idx: usize) -> Result<&Dictionary, PlistParseError> {
body.get(idx)
.ok_or(PlistParseError::NoValueAtIndex(idx))?
.as_dictionary()
.ok_or_else(|| PlistParseError::InvalidTypeIndex(idx, "dictionary".to_string()))
}
#[must_use]
pub fn get_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
payload
.as_dictionary()?
.get(key)?
.as_string()
.filter(|s| !s.is_empty())
}
#[must_use]
pub fn get_owned_string_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<String> {
get_string_from_dict(payload, key).map(String::from)
}
#[must_use]
pub fn get_value_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a Value> {
payload.as_dictionary()?.get(key)
}
#[must_use]
pub fn get_bool_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<bool> {
payload.as_dictionary()?.get(key)?.as_boolean()
}
#[must_use]
pub fn get_data_from_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a [u8]> {
payload.as_dictionary()?.get(key)?.as_data()
}
#[must_use]
pub fn get_string_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<&'a str> {
payload
.as_dictionary()?
.get(key)?
.as_dictionary()?
.get(key)?
.as_string()
.filter(|s| !s.is_empty())
}
#[must_use]
pub fn get_float_from_nested_dict<'a>(payload: &'a Value, key: &'a str) -> Option<f64> {
payload
.as_dictionary()?
.get(key)?
.as_dictionary()?
.get(key)?
.as_real()
}
#[cfg(test)]
mod tests {
use super::*;
use plist::Uid;
fn dict(pairs: Vec<(&str, Value)>) -> Value {
let mut dictionary = Dictionary::new();
for (key, value) in pairs {
dictionary.insert(key.to_string(), value);
}
Value::Dictionary(dictionary)
}
#[test]
fn resolves_simple_archive() {
let archive = dict(vec![
(
"$objects",
Value::Array(vec![
Value::String("$null".to_string()),
Value::String("hello".to_string()),
]),
),
("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
]);
assert_eq!(
parse_ns_keyed_archiver(&archive).unwrap(),
Value::String("hello".to_string())
);
}
#[test]
fn null_root_is_no_payload() {
let archive = dict(vec![
(
"$objects",
Value::Array(vec![Value::String(NULL_PLACEHOLDER.to_string())]),
),
("$top", dict(vec![("root", Value::Uid(Uid::new(0)))])),
]);
assert!(matches!(
parse_ns_keyed_archiver(&archive),
Err(PlistParseError::NoPayload)
));
}
#[test]
fn omits_null_values_from_normal_dictionaries() {
let archive = dict(vec![
(
"$objects",
Value::Array(vec![
Value::String(NULL_PLACEHOLDER.to_string()),
dict(vec![
("missing", Value::Uid(Uid::new(0))),
("present", Value::Uid(Uid::new(2))),
("literal", Value::Uid(Uid::new(3))),
]),
Value::String("kept".to_string()),
Value::String(NULL_PLACEHOLDER.to_string()),
]),
),
("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
]);
assert_eq!(
parse_ns_keyed_archiver(&archive).unwrap(),
dict(vec![
("present", Value::String("kept".to_string())),
("literal", Value::String(NULL_PLACEHOLDER.to_string())),
])
);
}
#[test]
fn omits_null_values_from_ns_dictionary_pairs() {
let archive = dict(vec![
(
"$objects",
Value::Array(vec![
Value::String(NULL_PLACEHOLDER.to_string()),
dict(vec![
(
"NS.keys",
Value::Array(vec![Value::Uid(Uid::new(2)), Value::Uid(Uid::new(3))]),
),
(
"NS.objects",
Value::Array(vec![Value::Uid(Uid::new(4)), Value::Uid(Uid::new(0))]),
),
]),
Value::String("present".to_string()),
Value::String("missing".to_string()),
Value::String("kept".to_string()),
]),
),
("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
]);
assert_eq!(
parse_ns_keyed_archiver(&archive).unwrap(),
dict(vec![("present", Value::String("kept".to_string()))])
);
}
#[test]
fn cyclic_uid_reference_is_rejected_not_overflowed() {
let archive = dict(vec![
(
"$objects",
Value::Array(vec![
Value::String("$null".to_string()),
Value::Uid(Uid::new(1)),
]),
),
("$top", dict(vec![("root", Value::Uid(Uid::new(1)))])),
]);
assert!(matches!(
parse_ns_keyed_archiver(&archive),
Err(PlistParseError::RecursionLimit)
));
}
}