use crate::GrayscaleBitmap;
use std::io::{Cursor, Read};
pub const MAX_ENTRY_BYTES: usize = 256 * 1024 * 1024;
pub const MAX_PNG_DIMENSION: u32 = 16384;
pub const MAX_PLIST_BYTES: usize = 16 * 1024 * 1024;
pub const MAX_PLIST_DEPTH: usize = 64;
pub fn read_zip_entry(
zip: &mut zip::ZipArchive<Cursor<&[u8]>>,
path: &str,
) -> Result<Vec<u8>, String> {
let file = zip.by_name(path).map_err(|_| format!("{path} not found"))?;
if file.size() > MAX_ENTRY_BYTES as u64 {
return Err(format!(
"{path}: declared size {} exceeds limit",
file.size()
));
}
let mut buf = Vec::with_capacity((file.size() as usize).min(MAX_ENTRY_BYTES));
file.take(MAX_ENTRY_BYTES as u64 + 1)
.read_to_end(&mut buf)
.map_err(|e| format!("failed to read {path}: {e}"))?;
if buf.len() > MAX_ENTRY_BYTES {
return Err(format!("{path}: entry exceeds size limit"));
}
Ok(buf)
}
pub fn parse_plist_guarded(bytes: &[u8], label: &str) -> Result<plist::Value, String> {
if bytes.len() > MAX_PLIST_BYTES {
return Err(format!("{label}: plist size {} exceeds limit", bytes.len()));
}
let mut depth: usize = 0;
for event in plist::stream::Reader::new(Cursor::new(bytes)) {
match event.map_err(|e| format!("failed to parse {label}: {e}"))? {
plist::stream::Event::StartArray(_) | plist::stream::Event::StartDictionary(_) => {
depth += 1;
if depth > MAX_PLIST_DEPTH {
return Err(format!("{label}: plist nesting depth exceeds limit"));
}
}
plist::stream::Event::EndCollection => depth = depth.saturating_sub(1),
_ => {}
}
}
plist::Value::from_reader(Cursor::new(bytes))
.map_err(|e| format!("failed to parse {label}: {e}"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ShapePngError {
TooLarge { width: u32, height: u32 },
Corrupt(String),
}
impl std::fmt::Display for ShapePngError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ShapePngError::TooLarge { width, height } => write!(
f,
"Shape.png is {width}x{height}px; the maximum supported brush-tip dimension is {MAX_PNG_DIMENSION}px"
),
ShapePngError::Corrupt(msg) => f.write_str(msg),
}
}
}
impl std::error::Error for ShapePngError {}
pub fn decode_tip_png(bytes: &[u8]) -> Result<GrayscaleBitmap, ShapePngError> {
let mut reader = image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| ShapePngError::Corrupt(format!("failed to sniff Shape.png: {e}")))?;
let mut limits = image::Limits::default();
limits.max_image_width = Some(MAX_PNG_DIMENSION);
limits.max_image_height = Some(MAX_PNG_DIMENSION);
limits.max_alloc = Some(MAX_ENTRY_BYTES as u64);
reader.limits(limits);
let luma = reader
.decode()
.map_err(|e| match &e {
image::ImageError::Limits(l)
if matches!(l.kind(), image::error::LimitErrorKind::DimensionError) =>
{
match header_dimensions(bytes) {
Some((width, height)) => ShapePngError::TooLarge { width, height },
None => ShapePngError::Corrupt(format!("failed to decode Shape.png: {e}")),
}
}
_ => ShapePngError::Corrupt(format!("failed to decode Shape.png: {e}")),
})?
.to_luma8();
Ok(GrayscaleBitmap {
width: luma.width(),
height: luma.height(),
data: luma.into_raw(),
})
}
pub(crate) fn header_dimensions(bytes: &[u8]) -> Option<(u32, u32)> {
image::ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.ok()?
.into_dimensions()
.ok()
}
pub fn parse_brushset_plist(bytes: &[u8]) -> Result<(Option<String>, Vec<String>), String> {
let value = parse_plist_guarded(bytes, "brushset.plist")?;
let dict = value
.as_dictionary()
.ok_or("brushset.plist root is not a dictionary")?;
let name = dict
.get("name")
.and_then(|v| v.as_string())
.map(str::to_string);
let array = dict
.get("brushes")
.and_then(|v| v.as_array())
.ok_or("brushset.plist missing brushes array")?;
let uuids = array
.iter()
.enumerate()
.map(|(i, v)| {
v.as_string()
.map(|s| s.to_string())
.ok_or_else(|| format!("brushset.plist brushes[{i}] is not a string"))
})
.collect::<Result<Vec<_>, _>>()?;
Ok((name, uuids))
}
pub fn archive_objects_and_main(
value: &plist::Value,
) -> Result<(&[plist::Value], &plist::Dictionary), String> {
let root = value
.as_dictionary()
.ok_or("Brush.archive root is not a dictionary")?;
let objects = root
.get("$objects")
.and_then(|v| v.as_array())
.ok_or("Brush.archive missing $objects array")?;
let main_dict = objects
.get(1)
.and_then(|v| v.as_dictionary())
.ok_or("$objects[1] is not a dictionary")?;
Ok((objects, main_dict))
}
pub fn resolve_string(
objects: &[plist::Value],
main_dict: &plist::Dictionary,
key: &str,
) -> Option<String> {
main_dict
.get(key)
.and_then(|v| v.as_uid())
.and_then(|u| objects.get(u.get() as usize))
.and_then(|v| v.as_string())
.filter(|s| *s != "$null")
.map(str::to_string)
}
pub fn brush_name(archive_bytes: &[u8]) -> Result<Option<String>, String> {
let value = parse_plist_guarded(archive_bytes, "Brush.archive")?;
let (objects, main_dict) = archive_objects_and_main(&value)?;
Ok(resolve_string(objects, main_dict, "name"))
}
pub fn members_in_zip_order(zip: &mut zip::ZipArchive<Cursor<&[u8]>>) -> Vec<String> {
(0..zip.len())
.filter_map(|i| {
let dir = zip.name_for_index(i)?.strip_suffix("/Brush.archive")?;
(!dir.is_empty() && !dir.contains('/')).then(|| dir.to_string())
})
.collect()
}