brushkit_preview/
procreate.rs1use 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;
18
19pub fn read_zip_entry(
23 zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
24 path: &str,
25) -> Result<Vec<u8>, String> {
26 let file = zip.by_name(path).map_err(|_| format!("{path} not found"))?;
27 if file.size() > MAX_ENTRY_BYTES as u64 {
28 return Err(format!(
29 "{path}: declared size {} exceeds limit",
30 file.size()
31 ));
32 }
33 let mut buf = Vec::with_capacity((file.size() as usize).min(MAX_ENTRY_BYTES));
34 file.take(MAX_ENTRY_BYTES as u64 + 1)
35 .read_to_end(&mut buf)
36 .map_err(|e| format!("failed to read {path}: {e}"))?;
37 if buf.len() > MAX_ENTRY_BYTES {
38 return Err(format!("{path}: entry exceeds size limit"));
39 }
40 Ok(buf)
41}
42
43pub fn parse_plist_guarded(bytes: &[u8], label: &str) -> Result<plist::Value, String> {
47 if bytes.len() > MAX_PLIST_BYTES {
48 return Err(format!("{label}: plist size {} exceeds limit", bytes.len()));
49 }
50 let mut depth: usize = 0;
51 for event in plist::stream::Reader::new(Cursor::new(bytes)) {
52 match event.map_err(|e| format!("failed to parse {label}: {e}"))? {
53 plist::stream::Event::StartArray(_) | plist::stream::Event::StartDictionary(_) => {
54 depth += 1;
55 if depth > MAX_PLIST_DEPTH {
56 return Err(format!("{label}: plist nesting depth exceeds limit"));
57 }
58 }
59 plist::stream::Event::EndCollection => depth = depth.saturating_sub(1),
60 _ => {}
61 }
62 }
63 plist::Value::from_reader(Cursor::new(bytes))
64 .map_err(|e| format!("failed to parse {label}: {e}"))
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub enum ShapePngError {
70 TooLarge { width: u32, height: u32 },
74 Corrupt(String),
77}
78
79impl std::fmt::Display for ShapePngError {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 match self {
82 ShapePngError::TooLarge { width, height } => write!(
83 f,
84 "Shape.png is {width}x{height}px; a brush tip must be at most {MAX_PNG_DIMENSION}px per side and {} MiB to decode",
85 MAX_ENTRY_BYTES / (1024 * 1024)
86 ),
87 ShapePngError::Corrupt(msg) => f.write_str(msg),
88 }
89 }
90}
91
92impl std::error::Error for ShapePngError {}
93
94pub fn decode_tip_png(bytes: &[u8]) -> Result<GrayscaleBitmap, ShapePngError> {
98 let image =
99 decode_guarded(bytes, MAX_PNG_DIMENSION, MAX_ENTRY_BYTES as u64).map_err(|e| match e {
100 GuardedDecodeError::TooLarge { width, height } => {
101 ShapePngError::TooLarge { width, height }
102 }
103 GuardedDecodeError::Decode(e) => {
104 ShapePngError::Corrupt(format!("failed to decode Shape.png: {e}"))
105 }
106 })?;
107 Ok(GrayscaleBitmap {
108 width: image.width,
109 height: image.height,
110 data: tip_plane(image, TipSample::Luma),
111 })
112}
113
114pub fn parse_brushset_plist(bytes: &[u8]) -> Result<(Option<String>, Vec<String>), String> {
116 let value = parse_plist_guarded(bytes, "brushset.plist")?;
117 let dict = value
118 .as_dictionary()
119 .ok_or("brushset.plist root is not a dictionary")?;
120 let name = dict
121 .get("name")
122 .and_then(|v| v.as_string())
123 .map(str::to_string);
124 let array = dict
125 .get("brushes")
126 .and_then(|v| v.as_array())
127 .ok_or("brushset.plist missing brushes array")?;
128 let uuids = array
129 .iter()
130 .enumerate()
131 .map(|(i, v)| {
132 v.as_string()
133 .map(|s| s.to_string())
134 .ok_or_else(|| format!("brushset.plist brushes[{i}] is not a string"))
135 })
136 .collect::<Result<Vec<_>, _>>()?;
137 Ok((name, uuids))
138}
139
140pub fn archive_objects_and_main(
144 value: &plist::Value,
145) -> Result<(&[plist::Value], &plist::Dictionary), String> {
146 let root = value
147 .as_dictionary()
148 .ok_or("Brush.archive root is not a dictionary")?;
149 let objects = root
150 .get("$objects")
151 .and_then(|v| v.as_array())
152 .ok_or("Brush.archive missing $objects array")?;
153 let main_dict = objects
154 .get(1)
155 .and_then(|v| v.as_dictionary())
156 .ok_or("$objects[1] is not a dictionary")?;
157 Ok((objects, main_dict))
158}
159
160pub fn resolve_string(
163 objects: &[plist::Value],
164 main_dict: &plist::Dictionary,
165 key: &str,
166) -> Option<String> {
167 main_dict
168 .get(key)
169 .and_then(|v| v.as_uid())
170 .and_then(|u| objects.get(u.get() as usize))
171 .and_then(|v| v.as_string())
172 .filter(|s| *s != "$null")
173 .map(str::to_string)
174}
175
176pub fn brush_name(archive_bytes: &[u8]) -> Result<Option<String>, String> {
179 let value = parse_plist_guarded(archive_bytes, "Brush.archive")?;
180 let (objects, main_dict) = archive_objects_and_main(&value)?;
181 Ok(resolve_string(objects, main_dict, "name"))
182}
183
184pub fn members_in_zip_order(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<String> {
188 (0..zip.len())
191 .filter_map(|i| {
192 let dir = zip.name_for_index(i)?.strip_suffix("/Brush.archive")?;
193 (!dir.is_empty() && !dir.contains('/')).then(|| dir.to_string())
194 })
195 .collect()
196}