1use crate::bitmap::{decode_guarded, tip_plane, GuardedDecodeError, TipSample};
9use crate::GrayscaleBitmap;
10use std::io::{Cursor, Read};
11
12pub const MAX_ENTRY_BYTES: usize = 256 * 1024 * 1024;
15pub const MAX_PNG_DIMENSION: u32 = 16384;
16pub const MAX_PLIST_BYTES: usize = 1024 * 1024;
21pub const MAX_PLIST_DEPTH: usize = 64;
22pub const MAX_PLIST_VALUES: usize = 10_000;
30pub const MAX_PLIST_PAYLOAD_BYTES: usize = 1024 * 1024;
37
38pub fn read_zip_entry(
41 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
42 path: &str,
43) -> Result<Vec<u8>, String> {
44 read_capped(zip, path, MAX_ENTRY_BYTES)
45}
46
47pub fn read_zip_plist(
50 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
51 path: &str,
52) -> Result<Vec<u8>, String> {
53 read_capped(zip, path, MAX_PLIST_BYTES)
54}
55
56fn read_capped(
62 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
63 path: &str,
64 limit: usize,
65) -> Result<Vec<u8>, String> {
66 let mut file = zip.by_name(path).map_err(|_| format!("{path} not found"))?;
67 let size = file.size();
68 if size > limit as u64 {
69 return Err(format!("{path}: declared size {size} exceeds limit"));
70 }
71 let read_error = |e: std::io::Error| format!("failed to read {path}: {e}");
72 let mut buf = Vec::with_capacity(size as usize);
73 (&mut file)
74 .take(size)
75 .read_to_end(&mut buf)
76 .map_err(read_error)?;
77 if file.read(&mut [0]).map_err(read_error)? > 0 {
78 return Err(format!("{path}: entry exceeds its declared size"));
79 }
80 Ok(buf)
81}
82
83pub fn parse_plist_guarded(bytes: &[u8], label: &str) -> Result<plist::Value, String> {
91 use plist::stream::Event;
92 if bytes.len() > MAX_PLIST_BYTES {
93 return Err(format!("{label}: plist size {} exceeds limit", bytes.len()));
94 }
95 check_binary_tables(bytes, label)?;
96 let mut depth: usize = 0;
97 let mut values: usize = 0;
98 let mut payload: usize = 0;
99 for event in plist::stream::Reader::new(Cursor::new(bytes)) {
100 match event.map_err(|e| format!("failed to parse {label}: {e}"))? {
101 Event::StartArray(_) | Event::StartDictionary(_) => {
102 depth += 1;
103 if depth > MAX_PLIST_DEPTH {
104 return Err(format!("{label}: plist nesting depth exceeds limit"));
105 }
106 }
107 Event::EndCollection => {
108 depth = depth.saturating_sub(1);
109 continue;
110 }
111 Event::Data(data) => payload = payload.saturating_add(data.len()),
112 Event::String(string) => payload = payload.saturating_add(string.len()),
113 _ => {}
114 }
115 values += 1;
116 if values > MAX_PLIST_VALUES {
117 return Err(format!(
118 "{label}: plist expands to over {MAX_PLIST_VALUES} values"
119 ));
120 }
121 if payload > MAX_PLIST_PAYLOAD_BYTES {
122 return Err(format!(
123 "{label}: plist expands to over {MAX_PLIST_PAYLOAD_BYTES} bytes of strings and data"
124 ));
125 }
126 }
127 plist::Value::from_reader(Cursor::new(bytes))
128 .map_err(|e| format!("failed to parse {label}: {e}"))
129}
130
131fn check_binary_tables(bytes: &[u8], label: &str) -> Result<(), String> {
140 let Some(tail) = bytes
141 .strip_prefix(b"bplist00")
142 .and_then(|body| body.last_chunk::<32>())
143 else {
144 return Ok(());
145 };
146 let trailer = bytes.len() - tail.len();
147 let width = usize::from(tail[6]);
148 let count = big_endian(&tail[8..16]);
149 let table = big_endian(&tail[24..32]);
150 if !matches!(width, 1 | 2 | 3 | 4 | 8) {
151 return Ok(());
152 }
153 let Some(entries) = count
154 .checked_mul(width as u64)
155 .and_then(|n| n.checked_add(table))
156 .filter(|&end| end <= trailer as u64)
157 .and_then(|end| bytes.get(table as usize..end as usize))
158 else {
159 return Ok(());
160 };
161 if count > MAX_PLIST_VALUES as u64 {
162 return Err(format!(
163 "{label}: plist declares over {MAX_PLIST_VALUES} objects"
164 ));
165 }
166 let mut references: u64 = 0;
167 let mut payload: u64 = 0;
168 for entry in entries.chunks_exact(width) {
169 let offset = big_endian(entry);
170 if offset >= trailer as u64 {
171 continue;
172 }
173 let (object_references, object_payload) = declared(bytes, trailer, offset as usize);
174 references = references.saturating_add(object_references);
175 payload = payload.saturating_add(object_payload);
176 if references > MAX_PLIST_VALUES as u64 {
177 return Err(format!(
178 "{label}: plist collections declare over {MAX_PLIST_VALUES} references"
179 ));
180 }
181 if payload > MAX_PLIST_PAYLOAD_BYTES as u64 {
182 return Err(format!(
183 "{label}: plist declares over {MAX_PLIST_PAYLOAD_BYTES} bytes of strings and data"
184 ));
185 }
186 }
187 Ok(())
188}
189
190fn declared(bytes: &[u8], trailer: usize, at: usize) -> (u64, u64) {
193 let Some(&token) = bytes.get(at) else {
194 return (0, 0);
195 };
196 let (len, start) = if token & 0xF == 0xF {
197 let Some(&marker) = bytes.get(at.saturating_add(1)) else {
198 return (0, 0);
199 };
200 let start = at.saturating_add(2);
201 let end = start.saturating_add(1 << (marker & 3));
202 let Some(field) = bytes.get(start..end) else {
203 return (0, 0);
204 };
205 (big_endian(field), end)
206 } else {
207 (u64::from(token & 0xF), at + 1)
208 };
209 let content = |unit: u64| {
213 let size = usize::try_from(len.checked_mul(unit)?).ok()?;
214 at.checked_add(size).filter(|&end| end <= trailer)?;
215 bytes.get(start..start.checked_add(size)?)
216 };
217 match token >> 4 {
218 0x4 | 0x5 => (0, content(1).map_or(0, |data| data.len() as u64)),
219 0x6 => (0, content(2).map_or(0, utf8_len)),
220 0xA => (len, 0),
221 0xD => (len.saturating_mul(2), 0),
222 _ => (0, 0),
223 }
224}
225
226fn utf8_len(units: &[u8]) -> u64 {
230 units
231 .as_chunks()
232 .0
233 .iter()
234 .map(|&unit| match u16::from_be_bytes(unit) {
235 0..=0x7F => 1,
236 0x80..=0x7FF | 0xD800..=0xDFFF => 2,
237 _ => 3,
238 })
239 .sum()
240}
241
242fn big_endian(field: &[u8]) -> u64 {
244 field.iter().fold(0, |n, &b| n << 8 | u64::from(b))
245}
246
247#[derive(Debug, Clone, PartialEq, Eq)]
249pub enum ShapePngError {
250 TooLarge { width: u32, height: u32 },
254 Corrupt(String),
257}
258
259impl std::fmt::Display for ShapePngError {
260 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261 match self {
262 ShapePngError::TooLarge { width, height } => write!(
263 f,
264 "Shape.png is {width}x{height}px; a brush tip must be at most {MAX_PNG_DIMENSION}px per side and {} MiB to decode",
265 MAX_ENTRY_BYTES / (1024 * 1024)
266 ),
267 ShapePngError::Corrupt(msg) => f.write_str(msg),
268 }
269 }
270}
271
272impl std::error::Error for ShapePngError {}
273
274pub fn decode_tip_png(bytes: &[u8]) -> Result<GrayscaleBitmap, ShapePngError> {
278 let image =
279 decode_guarded(bytes, MAX_PNG_DIMENSION, MAX_ENTRY_BYTES as u64).map_err(|e| match e {
280 GuardedDecodeError::TooLarge { width, height } => {
281 ShapePngError::TooLarge { width, height }
282 }
283 GuardedDecodeError::Decode(e) => {
284 ShapePngError::Corrupt(format!("failed to decode Shape.png: {e}"))
285 }
286 })?;
287 Ok(GrayscaleBitmap {
288 width: image.width,
289 height: image.height,
290 data: tip_plane(image, TipSample::Luma),
291 })
292}
293
294pub fn parse_brushset_plist(bytes: &[u8]) -> Result<(Option<String>, Vec<String>), String> {
296 let value = parse_plist_guarded(bytes, "brushset.plist")?;
297 let dict = value
298 .as_dictionary()
299 .ok_or("brushset.plist root is not a dictionary")?;
300 let name = dict
301 .get("name")
302 .and_then(|v| v.as_string())
303 .map(str::to_string);
304 let array = dict
305 .get("brushes")
306 .and_then(|v| v.as_array())
307 .ok_or("brushset.plist missing brushes array")?;
308 let uuids = array
309 .iter()
310 .enumerate()
311 .map(|(i, v)| {
312 v.as_string()
313 .map(|s| s.to_string())
314 .ok_or_else(|| format!("brushset.plist brushes[{i}] is not a string"))
315 })
316 .collect::<Result<Vec<_>, _>>()?;
317 Ok((name, uuids))
318}
319
320pub fn archive_objects_and_main(
324 value: &plist::Value,
325) -> Result<(&[plist::Value], &plist::Dictionary), String> {
326 let root = value
327 .as_dictionary()
328 .ok_or("Brush.archive root is not a dictionary")?;
329 let objects = root
330 .get("$objects")
331 .and_then(|v| v.as_array())
332 .ok_or("Brush.archive missing $objects array")?;
333 let main_dict = objects
334 .get(1)
335 .and_then(|v| v.as_dictionary())
336 .ok_or("$objects[1] is not a dictionary")?;
337 Ok((objects, main_dict))
338}
339
340pub fn resolve_string(
343 objects: &[plist::Value],
344 main_dict: &plist::Dictionary,
345 key: &str,
346) -> Option<String> {
347 main_dict
348 .get(key)
349 .and_then(|v| v.as_uid())
350 .and_then(|u| objects.get(u.get() as usize))
351 .and_then(|v| v.as_string())
352 .filter(|s| *s != "$null")
353 .map(str::to_string)
354}
355
356pub fn brush_name(archive_bytes: &[u8]) -> Result<Option<String>, String> {
359 let value = parse_plist_guarded(archive_bytes, "Brush.archive")?;
360 let (objects, main_dict) = archive_objects_and_main(&value)?;
361 Ok(resolve_string(objects, main_dict, "name"))
362}
363
364pub fn members_in_zip_order(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<String> {
368 (0..zip.len())
371 .filter_map(|i| {
372 let dir = zip.name_for_index(i)?.strip_suffix("/Brush.archive")?;
373 (!dir.is_empty() && !dir.contains('/')).then(|| dir.to_string())
374 })
375 .collect()
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn bplist(body: &[u8], offset_width: u8, count: u64, table: u64) -> Vec<u8> {
384 let mut out = b"bplist00".to_vec();
385 out.extend_from_slice(body);
386 out.extend_from_slice(&[0, 0, 0, 0, 0, 0, offset_width, 1]);
387 out.extend_from_slice(&count.to_be_bytes());
388 out.extend_from_slice(&0u64.to_be_bytes());
389 out.extend_from_slice(&table.to_be_bytes());
390 out
391 }
392
393 #[test]
394 fn tables_that_overflow_or_do_not_fit_are_left_to_plist() {
395 let over = MAX_PLIST_VALUES as u64 + 1;
396 for (width, count, table) in [
397 (8, u64::MAX, 8),
398 (1, 1, u64::MAX),
399 (1, over, 8),
400 (5, over, 8),
401 ] {
402 let plist = bplist(&[0x10, 0, 8], width, count, table);
403 let case = format!("{width} {count} {table}");
404 assert_eq!(check_binary_tables(&plist, "t"), Ok(()), "{case}");
405 let err = parse_plist_guarded(&plist, "t").expect_err(&case);
406 assert!(err.starts_with("failed to parse t:"), "{case}: {err}");
407 }
408 }
409
410 #[test]
411 fn extended_lengths_are_read_at_the_marked_width() {
412 let over = (MAX_PLIST_VALUES as u64 + 1).to_be_bytes();
413 let rejected = Err(format!(
414 "t: plist collections declare over {MAX_PLIST_VALUES} references"
415 ));
416 for (marker, expected) in [(0x10, Ok(())), (0x13, rejected.clone()), (0xF3, rejected)] {
417 let mut body = vec![0xAF, marker];
418 body.extend_from_slice(&over);
419 body.push(8);
420 assert_eq!(
421 check_binary_tables(&bplist(&body, 1, 1, 18), "t"),
422 expected,
423 "{marker:#x}"
424 );
425 }
426 }
427
428 #[test]
429 fn utf16_strings_are_counted_as_utf8() {
430 let plist = |ascii: usize| {
431 let string = "\u{5b57}".repeat(MAX_PLIST_PAYLOAD_BYTES / 3) + &"a".repeat(ascii);
432 let mut out = Vec::new();
433 plist::Value::String(string)
434 .to_writer_binary(&mut out)
435 .expect("binary plist");
436 out
437 };
438 let fits = MAX_PLIST_PAYLOAD_BYTES % 3;
439 assert!(parse_plist_guarded(&plist(fits), "t").is_ok());
440 assert_eq!(
441 parse_plist_guarded(&plist(fits + 1), "t"),
442 declared_payload()
443 );
444 }
445
446 fn declared_payload() -> Result<plist::Value, String> {
447 Err(format!(
448 "t: plist declares over {MAX_PLIST_PAYLOAD_BYTES} bytes of strings and data"
449 ))
450 }
451
452 fn utf16_string(units: &[u16], into_trailer: usize) -> Vec<u8> {
455 let mut body = vec![0x6F, 0x12];
456 body.extend_from_slice(&(units.len() as u32).to_be_bytes());
457 body.extend(units.iter().flat_map(|unit| unit.to_be_bytes()));
458 body.truncate(body.len() - into_trailer);
459 *body.last_mut().expect("content") = 8;
460 let table = 8 + body.len() as u64 - 1;
461 bplist(&body, 1, 1, table)
462 }
463
464 #[test]
465 fn utf16_strings_that_plist_fails_or_reads_into_the_trailer_are_counted() {
466 let cjk = vec![0x5B57; MAX_PLIST_PAYLOAD_BYTES / 3 + 3];
467 let lone_surrogate = [&[0xDC00][..], &cjk[3..]].concat();
468 for plist in [utf16_string(&lone_surrogate, 0), utf16_string(&cjk, 6)] {
469 assert_eq!(parse_plist_guarded(&plist, "t"), declared_payload());
470 }
471 }
472}