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