use std::collections::BTreeMap;
use crate::document::{DocumentError, DocumentResult, Value};
const BINARY_MAGIC: &[u8] = b"bplist";
pub(crate) const WRONG_FORMAT_DETAIL: &str =
"the input is a binary property list; read it with --input-format plist";
pub(crate) const WRONG_FORMAT_XML_DETAIL: &str =
"the input is an XML property list; read it with --input-format plist";
pub(crate) fn looks_like_xml(bytes: &[u8]) -> bool {
let bytes = bytes.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(bytes);
let start = bytes
.iter()
.position(|byte| !byte.is_ascii_whitespace())
.unwrap_or(bytes.len());
let head = &bytes[start..];
head.starts_with(b"<?xml")
|| head.starts_with(b"<plist")
|| head.starts_with(b"<!DOCTYPE plist")
}
pub(crate) fn is_binary(bytes: &[u8]) -> bool {
bytes.starts_with(BINARY_MAGIC)
}
pub(crate) fn wrong_format(format: &str) -> DocumentError {
DocumentError::ParseError {
format: format.to_string(),
detail: WRONG_FORMAT_DETAIL.to_string(),
}
}
pub(crate) fn wrong_format_xml(format: &str) -> DocumentError {
DocumentError::ParseError {
format: format.to_string(),
detail: WRONG_FORMAT_XML_DETAIL.to_string(),
}
}
pub(crate) fn load_binary(bytes: &[u8]) -> DocumentResult<Value> {
let parsed = ::plist::Value::from_reader(std::io::Cursor::new(bytes)).map_err(|error| {
DocumentError::ParseError {
format: "plist".to_string(),
detail: error.to_string(),
}
})?;
convert(parsed)
}
pub fn load(content: &str) -> DocumentResult<Value> {
let parsed = ::plist::Value::from_reader_xml(content.as_bytes()).map_err(|error| {
DocumentError::ParseError {
format: "plist".to_string(),
detail: error.to_string(),
}
})?;
convert(parsed)
}
fn convert(value: ::plist::Value) -> DocumentResult<Value> {
Ok(match value {
::plist::Value::Array(items) => Value::Array(
items
.into_iter()
.map(convert)
.collect::<DocumentResult<_>>()?,
),
::plist::Value::Dictionary(map) => Value::Object(
map.into_iter()
.map(|(key, value)| Ok((key, convert(value)?)))
.collect::<DocumentResult<BTreeMap<_, _>>>()?,
),
::plist::Value::Boolean(value) => Value::Bool(value),
::plist::Value::Data(bytes) => Value::String(base64(&bytes)),
::plist::Value::Date(date) => Value::String(date.to_xml_format()),
::plist::Value::Real(value) => Value::Float(value),
::plist::Value::Integer(value) => match (value.as_signed(), value.as_unsigned()) {
(Some(signed), _) => Value::Integer(signed),
(None, Some(unsigned)) => Value::Unsigned(unsigned),
(None, None) => return Err(unmapped("integer")),
},
::plist::Value::String(value) => Value::String(value),
::plist::Value::Uid(uid) => Value::Unsigned(uid.get()),
_ => return Err(unmapped("value")),
})
}
fn unmapped(kind: &str) -> DocumentError {
DocumentError::ParseError {
format: "plist".to_string(),
detail: format!("plist {kind} has no value mapping"),
}
}
fn base64(bytes: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
for chunk in bytes.chunks(3) {
let b0 = u32::from(chunk[0]);
let b1 = u32::from(chunk.get(1).copied().unwrap_or(0));
let b2 = u32::from(chunk.get(2).copied().unwrap_or(0));
let n = (b0 << 16) | (b1 << 8) | b2;
let sextet = |shift: u32| char::from(ALPHABET[((n >> shift) & 63) as usize]);
out.push(sextet(18));
out.push(sextet(12));
out.push(if chunk.len() > 1 { sextet(6) } else { '=' });
out.push(if chunk.len() > 2 { sextet(0) } else { '=' });
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_pads_like_the_standard_alphabet() {
assert_eq!(base64(b""), "");
assert_eq!(base64(b"f"), "Zg==");
assert_eq!(base64(b"fo"), "Zm8=");
assert_eq!(base64(b"foo"), "Zm9v");
assert_eq!(base64(b"foob"), "Zm9vYg==");
}
}