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