use alloc::string::String;
use alloc::vec::Vec;
#[derive(Clone, Debug, PartialEq)]
pub enum PlistValue {
Bool(bool),
Integer(i64),
Real(f64),
Date(f64),
Data(Vec<u8>),
String(String),
Uid(u64),
Array(Vec<PlistValue>),
Dict(Vec<(String, PlistValue)>),
}
const MAX_DEPTH: usize = 32;
pub fn parse_bplist(data: &[u8]) -> Option<PlistValue> {
if data.len() < 8 + 32 || &data[..8] != b"bplist00" {
return None;
}
let trailer = &data[data.len() - 32..];
let offset_size = *trailer.get(6)? as usize;
let ref_size = *trailer.get(7)? as usize;
if offset_size == 0 || offset_size > 8 || ref_size == 0 || ref_size > 8 {
return None;
}
let num_objects = read_uint_be(trailer, 8, 8)? as usize;
let top_object = read_uint_be(trailer, 16, 8)? as usize;
let offset_table_off = read_uint_be(trailer, 24, 8)? as usize;
if top_object >= num_objects {
return None;
}
let table_end = offset_table_off.checked_add(num_objects.checked_mul(offset_size)?)?;
if table_end > data.len() - 32 {
return None;
}
let ctx = Ctx {
data,
num_objects,
offset_size,
ref_size,
offset_table_off,
};
ctx.read_object(top_object, 0)
}
struct Ctx<'a> {
data: &'a [u8],
num_objects: usize,
offset_size: usize,
ref_size: usize,
offset_table_off: usize,
}
impl Ctx<'_> {
fn object_offset(&self, index: usize) -> Option<usize> {
if index >= self.num_objects {
return None;
}
let pos = self.offset_table_off + index * self.offset_size;
read_uint_be(self.data, pos, self.offset_size).map(|v| v as usize)
}
fn read_ref(&self, pos: usize) -> Option<usize> {
read_uint_be(self.data, pos, self.ref_size).map(|v| v as usize)
}
fn read_object(&self, index: usize, depth: usize) -> Option<PlistValue> {
if depth > MAX_DEPTH {
return None;
}
let off = self.object_offset(index)?;
let marker = *self.data.get(off)?;
let hi = marker >> 4;
let lo = (marker & 0x0F) as usize;
match hi {
0x0 => match marker {
0x08 => Some(PlistValue::Bool(false)),
0x09 => Some(PlistValue::Bool(true)),
_ => None, },
0x1 => {
let n = 1usize << lo;
let raw = read_uint_be(self.data, off + 1, n.min(8))?;
Some(PlistValue::Integer(raw as i64))
}
0x2 => {
let n = 1usize << lo;
let bytes = self.data.get(off + 1..off + 1 + n)?;
let v = match n {
4 => f32::from_be_bytes(bytes.try_into().ok()?) as f64,
8 => f64::from_be_bytes(bytes.try_into().ok()?),
_ => return None,
};
Some(PlistValue::Real(v))
}
0x3 => {
let bytes = self.data.get(off + 1..off + 9)?;
Some(PlistValue::Date(f64::from_be_bytes(bytes.try_into().ok()?)))
}
0x4 => {
let (count, base) = self.read_count(off, lo)?;
let bytes = self.data.get(base..base + count)?;
Some(PlistValue::Data(bytes.to_vec()))
}
0x5 => {
let (count, base) = self.read_count(off, lo)?;
let bytes = self.data.get(base..base + count)?;
Some(PlistValue::String(core::str::from_utf8(bytes).ok()?.into()))
}
0x6 => {
let (count, base) = self.read_count(off, lo)?;
let end = count.checked_mul(2).and_then(|n| base.checked_add(n))?;
let bytes = self.data.get(base..end)?;
let mut units = Vec::with_capacity(count);
for c in bytes.chunks_exact(2) {
units.push(u16::from_be_bytes([c[0], c[1]]));
}
let s: String = char::decode_utf16(units)
.map(|r| r.unwrap_or('\u{FFFD}'))
.collect();
Some(PlistValue::String(s))
}
0x8 => {
let n = lo + 1;
Some(PlistValue::Uid(read_uint_be(self.data, off + 1, n.min(8))?))
}
0xA | 0xC => {
let (count, base) = self.read_count(off, lo)?;
let mut out = Vec::with_capacity(count.min(self.data.len()));
for i in 0..count {
let r = self.read_ref(base + i * self.ref_size)?;
out.push(self.read_object(r, depth + 1)?);
}
Some(PlistValue::Array(out))
}
0xD => {
let (count, base) = self.read_count(off, lo)?;
let keys_base = base;
let vals_base = count
.checked_mul(self.ref_size)
.and_then(|n| base.checked_add(n))?;
let mut out = Vec::with_capacity(count.min(self.data.len()));
for i in 0..count {
let k = self.read_ref(keys_base + i * self.ref_size)?;
let v = self.read_ref(vals_base + i * self.ref_size)?;
let key = match self.read_object(k, depth + 1)? {
PlistValue::String(s) => s,
_ => return None, };
out.push((key, self.read_object(v, depth + 1)?));
}
Some(PlistValue::Dict(out))
}
_ => None,
}
}
fn read_count(&self, off: usize, lo: usize) -> Option<(usize, usize)> {
if lo != 0x0F {
return Some((lo, off + 1));
}
let size_marker = *self.data.get(off + 1)?;
if size_marker >> 4 != 0x1 {
return None;
}
let n = 1usize << (size_marker & 0x0F);
let count = read_uint_be(self.data, off + 2, n.min(8))? as usize;
Some((count, off + 2 + n))
}
}
fn read_uint_be(data: &[u8], off: usize, size: usize) -> Option<u64> {
if size == 0 || size > 8 {
return None;
}
let bytes = data.get(off..off + size)?;
let mut v = 0u64;
for &b in bytes {
v = (v << 8) | b as u64;
}
Some(v)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_dict() {
let mut d = Vec::new();
d.extend_from_slice(b"bplist00");
let o0 = d.len();
d.push(0xD2); d.extend_from_slice(&[1, 2]); d.extend_from_slice(&[3, 4]); let o1 = d.len();
d.extend_from_slice(&[0x51, b'a']); let o2 = d.len();
d.extend_from_slice(&[0x51, b'b']); let o3 = d.len();
d.push(0x09); let o4 = d.len();
d.extend_from_slice(&[0x10, 5]); let table = d.len();
for off in [o0, o1, o2, o3, o4] {
d.push(off as u8); }
d.extend_from_slice(&[0; 6]); d.push(1); d.push(1); d.extend_from_slice(&(5u64).to_be_bytes()); d.extend_from_slice(&(0u64).to_be_bytes()); d.extend_from_slice(&(table as u64).to_be_bytes());
let v = parse_bplist(&d).unwrap();
let PlistValue::Dict(entries) = v else {
panic!("expected dict, got {v:?}");
};
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].0, "a");
assert_eq!(entries[0].1, PlistValue::Bool(true));
assert_eq!(entries[1].0, "b");
assert_eq!(entries[1].1, PlistValue::Integer(5));
}
#[test]
fn rejects_non_bplist() {
assert_eq!(parse_bplist(b"not a plist at all..............."), None);
assert_eq!(parse_bplist(b"bplist00"), None); assert_eq!(parse_bplist(&[0u8; 4]), None);
}
}