use plist::{Dictionary, Value};
use crate::error::plist::PlistParseError;
const MAX_UID_DEPTH: usize = 256;
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)
}
fn follow_uid<'a>(
objects: &'a [Value],
root: usize,
parent: Option<&'a Value>,
item: Option<&'a Value>,
depth: usize,
) -> Result<Value, PlistParseError> {
if depth >= MAX_UID_DEPTH {
return Err(PlistParseError::RecursionLimit);
}
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() {
array.push(follow_uid(
objects,
uid_to_index(idx)?,
parent,
None,
depth + 1,
)?);
}
}
Ok(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
{
dictionary.insert(
value_to_key_string(p),
follow_uid(objects, uid_to_index(idx)?, Some(p), None, depth + 1)?,
);
}
}
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 key = follow_uid(objects, key_index, None, None, depth + 1)?;
let value = follow_uid(objects, value_index, Some(&key), None, depth + 1)?;
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());
dictionary.insert(
key.clone(),
follow_uid(
objects,
uid_to_index(idx)?,
Some(&key_value),
None,
depth + 1,
)?,
);
}
else if let Some(p) = parent {
dictionary.insert(
value_to_key_string(p),
follow_uid(objects, root, Some(p), Some(val), depth + 1)?,
);
}
}
}
Ok(plist::Value::Dictionary(dictionary))
}
Value::Uid(uid) => follow_uid(objects, uid_to_index(uid)?, None, None, depth + 1),
_ => Ok(item.to_owned()),
}
}
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 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)
));
}
}