mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 07:45:38 +00:00
Metafile: drop PNG ICC and text chunks before decoding; Folder batch lists aliased folders once
This commit is contained in:
@@ -716,6 +716,11 @@ fn collect_files(folder: &str, include_subdirs: bool, depth: usize) -> Result<Ve
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
// Names that are not valid text come back lossily, so different names can give one path: each
|
||||
// folder is listed once.
|
||||
files.dedup();
|
||||
subdirs.sort();
|
||||
subdirs.dedup();
|
||||
for sub in subdirs {
|
||||
let mut nested = collect_files(&sub, true, depth + 1)?;
|
||||
files.append(&mut nested);
|
||||
@@ -749,6 +754,22 @@ mod tests {
|
||||
d
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn folders_whose_names_alias_after_lossy_conversion_are_listed_once() {
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
let root = tmp("alias");
|
||||
// `a\u{FFFD}` is a real folder; two others with bytes that aren't text give the same name.
|
||||
let real = root.join("a\u{FFFD}");
|
||||
std::fs::create_dir_all(&real).unwrap();
|
||||
write_svg(&real, "x.svg", "red");
|
||||
for bad in [b"a\xFF", b"a\xFE"] {
|
||||
let _ = std::fs::create_dir_all(root.join(std::ffi::OsStr::from_bytes(bad)));
|
||||
}
|
||||
let files = collect_files(&root.to_string_lossy(), true, 0).unwrap();
|
||||
assert_eq!(files.len(), 1, "{files:?}");
|
||||
}
|
||||
|
||||
fn write_svg(dir: &std::path::Path, name: &str, fill: &str) -> std::path::PathBuf {
|
||||
let p = dir.join(name);
|
||||
std::fs::write(
|
||||
|
||||
@@ -141,6 +141,28 @@ fn channel(v: u32, mask: u32) -> u8 {
|
||||
((x * 255 + max / 2) / max.max(1)).min(255) as u8
|
||||
}
|
||||
|
||||
/// PNG `bytes` without its compressed or text ancillary chunks (`iCCP`, `zTXt`, `iTXt`, `tEXt`),
|
||||
/// which the pixels don't need and which a decoder expands when it is made. Other bytes, and a PNG
|
||||
/// whose chunks don't read to the end, are returned as they are.
|
||||
fn png_without_metadata(bytes: &[u8]) -> std::borrow::Cow<'_, [u8]> {
|
||||
const SIGNATURE: &[u8] = b"\x89PNG\r\n\x1a\n";
|
||||
if !bytes.starts_with(SIGNATURE) {
|
||||
return bytes.into();
|
||||
}
|
||||
let mut out = SIGNATURE.to_vec();
|
||||
let mut at = SIGNATURE.len();
|
||||
while at < bytes.len() {
|
||||
let (Some(len), Some(kind)) = (bytes.get(at..at + 4), bytes.get(at + 4..at + 8)) else { return bytes.into() };
|
||||
let len = u32::from_be_bytes([len[0], len[1], len[2], len[3]]) as usize;
|
||||
let Some(end) = at.checked_add(12).and_then(|n| n.checked_add(len)).filter(|end| *end <= bytes.len()) else { return bytes.into() };
|
||||
if !matches!(kind, b"iCCP" | b"zTXt" | b"iTXt" | b"tEXt") {
|
||||
out.extend_from_slice(&bytes[at..end]);
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
out.into()
|
||||
}
|
||||
|
||||
/// Take a bitmap of `w` × `h` from what is `left` of the file's budget, before it is decoded.
|
||||
fn charge(w: u32, h: u32, left: &mut u64) -> Result<(), String> {
|
||||
let n = u64::from(w) * u64::from(h);
|
||||
@@ -175,12 +197,15 @@ pub(crate) fn decode(bmi: &[u8], bits: &[u8], alpha: Alpha, left: &mut u64) -> R
|
||||
};
|
||||
if compression == BI_JPEG || compression == BI_PNG {
|
||||
// The size comes from the image header, so an over-limit bitmap is refused before any pixels are decoded.
|
||||
let (w, h) = image::ImageReader::new(Cursor::new(bits)).with_guessed_format().map_err(|_| bad())?.into_dimensions().map_err(|_| bad())?;
|
||||
// A PNG's compressed ICC profile and text chunks expand when its decoder is made, and
|
||||
// nothing here uses them: they are left out before the size is read and the pixels decoded.
|
||||
let bits = png_without_metadata(bits);
|
||||
let (w, h) = image::ImageReader::new(Cursor::new(&*bits)).with_guessed_format().map_err(|_| bad())?.into_dimensions().map_err(|_| bad())?;
|
||||
if w > MAX_READ_SIDE || h > MAX_READ_SIDE || u64::from(w) * u64::from(h) > MAX_READ_PIXELS {
|
||||
return Err("a bitmap is too large to read".into());
|
||||
}
|
||||
charge(w, h, left)?;
|
||||
let img = image::load_from_memory(bits).map_err(|_| bad())?.to_rgba8();
|
||||
let img = image::load_from_memory(&bits).map_err(|_| bad())?.to_rgba8();
|
||||
return Ok(Rgba { width: w, height: h, pixels: img.into_raw() });
|
||||
}
|
||||
if compression != BI_RGB && compression != BI_BITFIELDS {
|
||||
@@ -297,6 +322,23 @@ pub(crate) fn split(packed: &[u8]) -> Option<(&[u8], &[u8])> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_pngs_compressed_metadata_is_left_out_before_it_is_decoded() {
|
||||
use image::ImageEncoder;
|
||||
let mut png = vec![];
|
||||
let mut encoder = image::codecs::png::PngEncoder::new(&mut png);
|
||||
encoder.set_icc_profile(vec![0; 1 << 20]).unwrap();
|
||||
encoder.write_image(&[10, 20, 30], 1, 1, image::ExtendedColorType::Rgb8).unwrap();
|
||||
assert!(png.windows(4).any(|w| w == b"iCCP"));
|
||||
let stripped = png_without_metadata(&png);
|
||||
assert!(!stripped.windows(4).any(|w| w == b"iCCP") && stripped.len() < png.len());
|
||||
let img = image::load_from_memory(&stripped).unwrap().to_rgba8();
|
||||
assert_eq!(img.get_pixel(0, 0).0, [10, 20, 30, 255]);
|
||||
// Not a PNG, or a PNG cut short: unchanged.
|
||||
assert_eq!(&*png_without_metadata(b"BM...."), b"BM....");
|
||||
assert_eq!(&*png_without_metadata(&png[..png.len() - 3]), &png[..png.len() - 3]);
|
||||
}
|
||||
|
||||
fn sample() -> Rgba {
|
||||
// 3 × 2: red, green, blue / half-transparent black, transparent, white.
|
||||
let pixels = vec![255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 0, 0, 0, 128, 0, 0, 0, 0, 255, 255, 255, 255];
|
||||
|
||||
Reference in New Issue
Block a user