1use alloc::string::String;
17use alloc::vec::Vec;
18
19#[derive(Clone, Debug, PartialEq)]
21pub enum PlistValue {
22 Bool(bool),
24 Integer(i64),
26 Real(f64),
28 Date(f64),
30 Data(Vec<u8>),
32 String(String),
34 Uid(u64),
36 Array(Vec<PlistValue>),
38 Dict(Vec<(String, PlistValue)>),
40}
41
42const MAX_DEPTH: usize = 32;
44const MAX_NODES: usize = 65_536;
45const MAX_BYTES: usize = 16 * 1024 * 1024;
46
47struct Budget {
48 nodes: usize,
49 bytes: usize,
50}
51
52impl Budget {
53 fn charge(&mut self, bytes: usize) -> Option<()> {
54 self.bytes = self.bytes.checked_sub(bytes)?;
55 Some(())
56 }
57}
58
59pub fn parse_bplist(data: &[u8]) -> Option<PlistValue> {
64 if data.len() < 8 + 32 || data.len() > MAX_BYTES || &data[..8] != b"bplist00" {
65 return None;
66 }
67 let trailer = &data[data.len() - 32..];
68 let offset_size = *trailer.get(6)? as usize;
69 let ref_size = *trailer.get(7)? as usize;
70 if offset_size == 0 || offset_size > 8 || ref_size == 0 || ref_size > 8 {
71 return None;
72 }
73 let num_objects = usize::try_from(read_uint_be(trailer, 8, 8)?).ok()?;
74 let top_object = usize::try_from(read_uint_be(trailer, 16, 8)?).ok()?;
75 let offset_table_off = usize::try_from(read_uint_be(trailer, 24, 8)?).ok()?;
76 if top_object >= num_objects || offset_table_off < 8 {
77 return None;
78 }
79 let table_end = offset_table_off.checked_add(num_objects.checked_mul(offset_size)?)?;
81 if table_end > data.len() - 32 {
82 return None;
83 }
84 let ctx = Ctx {
85 data,
86 num_objects,
87 offset_size,
88 ref_size,
89 offset_table_off,
90 };
91 ctx.read_object(
92 top_object,
93 0,
94 &mut Budget {
95 nodes: MAX_NODES,
96 bytes: MAX_BYTES,
97 },
98 )
99}
100
101struct Ctx<'a> {
103 data: &'a [u8],
104 num_objects: usize,
105 offset_size: usize,
106 ref_size: usize,
107 offset_table_off: usize,
108}
109
110impl Ctx<'_> {
111 fn object_offset(&self, index: usize) -> Option<usize> {
113 if index >= self.num_objects {
114 return None;
115 }
116 let pos = self.offset_table_off + index * self.offset_size;
117 let offset = usize::try_from(read_uint_be(self.data, pos, self.offset_size)?).ok()?;
118 (offset >= 8 && offset < self.offset_table_off).then_some(offset)
119 }
120
121 fn read_ref(&self, pos: usize) -> Option<usize> {
123 usize::try_from(read_uint_be(self.payload(), pos, self.ref_size)?).ok()
124 }
125
126 fn payload(&self) -> &[u8] {
127 &self.data[..self.offset_table_off]
128 }
129
130 fn read_object(&self, index: usize, depth: usize, budget: &mut Budget) -> Option<PlistValue> {
131 if depth > MAX_DEPTH {
132 return None;
133 }
134 budget.nodes = budget.nodes.checked_sub(1)?;
135 let off = self.object_offset(index)?;
136 let data = self.payload();
137 let marker = *data.get(off)?;
138 let hi = marker >> 4;
139 let lo = (marker & 0x0F) as usize;
140 match hi {
141 0x0 => match marker {
142 0x08 => Some(PlistValue::Bool(false)),
143 0x09 => Some(PlistValue::Bool(true)),
144 _ => None, },
146 0x1 => {
147 let n = 1usize << lo;
151 let bytes = data.get(off + 1..off.checked_add(1)?.checked_add(n)?)?;
152 let raw = read_uint_be(bytes, n.saturating_sub(8), n.min(8))?;
153 Some(PlistValue::Integer(raw as i64))
154 }
155 0x2 => {
156 let n = 1usize << lo;
158 let bytes = data.get(off + 1..off.checked_add(1)?.checked_add(n)?)?;
159 let v = match n {
160 4 => f32::from_be_bytes(bytes.try_into().ok()?) as f64,
161 8 => f64::from_be_bytes(bytes.try_into().ok()?),
162 _ => return None,
163 };
164 Some(PlistValue::Real(v))
165 }
166 0x3 => {
167 if marker != 0x33 {
169 return None;
170 }
171 let bytes = data.get(off + 1..off.checked_add(9)?)?;
172 Some(PlistValue::Date(f64::from_be_bytes(bytes.try_into().ok()?)))
173 }
174 0x4 => {
175 let (count, base) = self.read_count(off, lo)?;
176 let bytes = data.get(base..base.checked_add(count)?)?;
177 budget.charge(count)?;
178 let mut out = Vec::new();
179 out.try_reserve_exact(count).ok()?;
180 out.extend_from_slice(bytes);
181 Some(PlistValue::Data(out))
182 }
183 0x5 => {
184 let (count, base) = self.read_count(off, lo)?;
186 let bytes = data.get(base..base.checked_add(count)?)?;
187 let text = core::str::from_utf8(bytes).ok()?;
188 budget.charge(count)?;
189 let mut out = String::new();
190 out.try_reserve_exact(count).ok()?;
191 out.push_str(text);
192 Some(PlistValue::String(out))
193 }
194 0x6 => {
195 let (count, base) = self.read_count(off, lo)?;
197 let end = count.checked_mul(2).and_then(|n| base.checked_add(n))?;
201 let bytes = data.get(base..end)?;
202 let capacity = count.checked_mul(3)?;
203 budget.charge(capacity)?;
204 let mut s = String::new();
205 s.try_reserve_exact(capacity).ok()?;
206 let units = bytes
207 .as_chunks::<2>()
208 .0
209 .iter()
210 .map(|c| u16::from_be_bytes(*c));
211 for ch in char::decode_utf16(units) {
212 s.push(ch.unwrap_or('\u{FFFD}'));
213 }
214 Some(PlistValue::String(s))
215 }
216 0x8 => {
217 let n = lo + 1;
219 let bytes = data.get(off + 1..off.checked_add(1)?.checked_add(n)?)?;
220 Some(PlistValue::Uid(read_uint_be(
221 bytes,
222 n.saturating_sub(8),
223 n.min(8),
224 )?))
225 }
226 0xA | 0xC => {
227 let (count, base) = self.read_count(off, lo)?;
229 let end = base.checked_add(count.checked_mul(self.ref_size)?)?;
230 data.get(base..end)?;
231 if count > budget.nodes {
232 return None;
233 }
234 budget.charge(count.checked_mul(core::mem::size_of::<PlistValue>())?)?;
235 let mut out = Vec::new();
236 out.try_reserve_exact(count).ok()?;
237 for i in 0..count {
238 let r = self.read_ref(base + i * self.ref_size)?;
239 out.push(self.read_object(r, depth + 1, budget)?);
240 }
241 Some(PlistValue::Array(out))
242 }
243 0xD => {
244 let (count, base) = self.read_count(off, lo)?;
246 let keys_base = base;
247 let vals_base = count
248 .checked_mul(self.ref_size)
249 .and_then(|n| base.checked_add(n))?;
250 let end = vals_base.checked_add(count.checked_mul(self.ref_size)?)?;
251 data.get(base..end)?;
252 if count.checked_mul(2)? > budget.nodes {
253 return None;
254 }
255 budget.charge(count.checked_mul(core::mem::size_of::<(String, PlistValue)>())?)?;
256 let mut out = Vec::new();
257 out.try_reserve_exact(count).ok()?;
258 for i in 0..count {
259 let k = self.read_ref(keys_base + i * self.ref_size)?;
260 let v = self.read_ref(vals_base + i * self.ref_size)?;
261 let key = match self.read_object(k, depth + 1, budget)? {
262 PlistValue::String(s) => s,
263 _ => return None, };
265 out.push((key, self.read_object(v, depth + 1, budget)?));
266 }
267 Some(PlistValue::Dict(out))
268 }
269 _ => None,
270 }
271 }
272
273 fn read_count(&self, off: usize, lo: usize) -> Option<(usize, usize)> {
277 if lo != 0x0F {
278 return Some((lo, off + 1));
279 }
280 let size_marker = *self.payload().get(off.checked_add(1)?)?;
281 if size_marker >> 4 != 0x1 {
282 return None;
283 }
284 let n = 1usize << (size_marker & 0x0F);
285 if n > 8 {
287 return None;
288 }
289 let count = usize::try_from(read_uint_be(self.payload(), off + 2, n)?).ok()?;
290 Some((count, off.checked_add(2)?.checked_add(n)?))
291 }
292}
293
294fn read_uint_be(data: &[u8], off: usize, size: usize) -> Option<u64> {
296 if size == 0 || size > 8 {
297 return None;
298 }
299 let bytes = data.get(off..off + size)?;
300 let mut v = 0u64;
301 for &b in bytes {
302 v = (v << 8) | b as u64;
303 }
304 Some(v)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 fn plist(objects: &[&[u8]]) -> Vec<u8> {
312 let mut data = b"bplist00".to_vec();
313 let mut offsets = Vec::new();
314 for object in objects {
315 offsets.push(u8::try_from(data.len()).unwrap());
316 data.extend_from_slice(object);
317 }
318 let table = data.len();
319 data.extend_from_slice(&offsets);
320 data.extend_from_slice(&[0; 6]);
321 data.extend_from_slice(&[1, 1]);
322 data.extend_from_slice(&(objects.len() as u64).to_be_bytes());
323 data.extend_from_slice(&0u64.to_be_bytes());
324 data.extend_from_slice(&(table as u64).to_be_bytes());
325 data
326 }
327
328 #[test]
329 fn rejects_exponential_shared_reference_expansion() {
330 let mut objects: Vec<Vec<u8>> = (1..=17).map(|n| alloc::vec![0xa2, n, n]).collect();
331 objects.push(alloc::vec![0x09]);
332 let refs: Vec<_> = objects.iter().map(Vec::as_slice).collect();
333 assert!(parse_bplist(&plist(&refs)).is_none());
334 }
335
336 #[test]
337 fn oversized_data_length_is_rejected_without_overflow() {
338 let mut object = alloc::vec![0x4f, 0x13];
339 object.extend_from_slice(&u64::MAX.to_be_bytes());
340 assert!(parse_bplist(&plist(&[&object])).is_none());
341 }
342
343 #[test]
344 fn wide_integer_uses_documented_low_bits_and_requires_complete_value() {
345 let mut object = alloc::vec![0x14];
346 object.extend_from_slice(&1u64.to_be_bytes());
347 object.extend_from_slice(&7u64.to_be_bytes());
348 assert_eq!(
349 parse_bplist(&plist(&[&object])),
350 Some(PlistValue::Integer(7))
351 );
352 assert!(parse_bplist(&plist(&[&object[..9]])).is_none());
354 }
355
356 #[test]
359 fn parses_simple_dict() {
360 let mut d = Vec::new();
364 d.extend_from_slice(b"bplist00");
365 let o0 = d.len();
366 d.push(0xD2); d.extend_from_slice(&[1, 2]); d.extend_from_slice(&[3, 4]); let o1 = d.len();
370 d.extend_from_slice(&[0x51, b'a']); let o2 = d.len();
372 d.extend_from_slice(&[0x51, b'b']); let o3 = d.len();
374 d.push(0x09); let o4 = d.len();
376 d.extend_from_slice(&[0x10, 5]); let table = d.len();
378 for off in [o0, o1, o2, o3, o4] {
379 d.push(off as u8); }
381 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();
390 let PlistValue::Dict(entries) = v else {
391 panic!("expected dict, got {v:?}");
392 };
393 assert_eq!(entries.len(), 2);
394 assert_eq!(entries[0].0, "a");
395 assert_eq!(entries[0].1, PlistValue::Bool(true));
396 assert_eq!(entries[1].0, "b");
397 assert_eq!(entries[1].1, PlistValue::Integer(5));
398 }
399
400 #[test]
401 fn rejects_non_bplist() {
402 assert_eq!(parse_bplist(b"not a plist at all..............."), None);
403 assert_eq!(parse_bplist(b"bplist00"), None); assert_eq!(parse_bplist(&[0u8; 4]), None);
405 }
406}