mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-24 10:09:01 -06:00
@@ -156,7 +156,15 @@ fn decode_chunk(data: &[u8], info: &ImageInfo, c: &Chunk, order: ByteOrder, pack
|
||||
return Err(RawError::Limit("chunk too large"));
|
||||
}
|
||||
match info.compression {
|
||||
comp::NONE => unpack_chunk(src, order, packing, cw, ch, cpp, bits, float, 1),
|
||||
comp::NONE => {
|
||||
// uncompressed: missing rows are filled with zeros, but a chunk holding a small fraction of what its
|
||||
// samples need is no picture: refused before the nominal chunk is allocated (issue #1512)
|
||||
let need = (n as u64).saturating_mul(u64::from(bits)) / 8;
|
||||
if (src.len() as u64).saturating_mul(64) < need {
|
||||
return Err(RawError::Corrupt(format!("an uncompressed chunk of {n} samples has only {} bytes", src.len())));
|
||||
}
|
||||
unpack_chunk(src, order, packing, cw, ch, cpp, bits, float, 1)
|
||||
}
|
||||
comp::JPEG => {
|
||||
let f = ljpeg::decode(src, n.saturating_mul(2).max(1 << 16))?;
|
||||
if f.data.len() < n {
|
||||
@@ -405,6 +413,31 @@ mod lossy_tests {
|
||||
assert!(read_image(&file, &past_end, ByteOrder::Little, Packing::Msb).is_err());
|
||||
}
|
||||
|
||||
/// A small image whose uncompressed tile claims to be enormous (16384 x 16384 nominal, a few bytes of data) is
|
||||
/// refused before that tile is allocated (issue #1512); an edge tile stored at its full nominal size decodes.
|
||||
#[test]
|
||||
fn a_huge_nominal_uncompressed_tile_with_little_data_is_refused() {
|
||||
let info = |tile: u32, bytes: u64| ImageInfo {
|
||||
width: 8,
|
||||
height: 8,
|
||||
bits_per_sample: vec![16],
|
||||
samples_per_pixel: 1,
|
||||
compression: comp::NONE,
|
||||
photometric: 32803,
|
||||
planar: 1,
|
||||
predictor: 1,
|
||||
sample_format: 1,
|
||||
new_subfile_type: 0,
|
||||
layout: Layout::Tiles { tile_width: tile, tile_height: tile },
|
||||
offsets: vec![0],
|
||||
byte_counts: vec![bytes],
|
||||
};
|
||||
let small = vec![1u8; 2 * 16 * 16];
|
||||
assert!(read_image(&small, &info(16, small.len() as u64), ByteOrder::Little, Packing::Msb).is_ok());
|
||||
let data = vec![1u8; 4096];
|
||||
assert!(matches!(read_image(&data, &info(16384, data.len() as u64), ByteOrder::Little, Packing::Msb), Err(RawError::Corrupt(_))));
|
||||
}
|
||||
|
||||
/// A lossless-JPEG tile may be coded only as tall as the part inside the image, never shorter.
|
||||
#[test]
|
||||
fn short_lossless_jpeg_tiles_are_errors() {
|
||||
|
||||
Vendored
+2
-2
@@ -524,10 +524,10 @@ pub(crate) fn decode(strip: &[u8], width: usize, height: usize, mode: crate::Mod
|
||||
}
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let decoded: Vec<Result<Vec<u16>>> = tiles.par_iter().enumerate().map(|(i, t)| decode_tile(bytes(i)?, t.width, t.height)).collect();
|
||||
// a tile that fails is reported before the mosaic is allocated (issue #1512)
|
||||
let decoded: Vec<Vec<u16>> = tiles.par_iter().enumerate().map(|(i, t)| decode_tile(bytes(i)?, t.width, t.height)).collect::<Result<_>>()?;
|
||||
let mut out = vec![0u16; width.checked_mul(height).ok_or_else(|| corrupt("image size"))?];
|
||||
for (t, d) in tiles.iter().zip(decoded) {
|
||||
let d = d?;
|
||||
for (r, row) in d.chunks_exact(t.width.max(1)).enumerate() {
|
||||
let start = (t.y + r).checked_mul(width).and_then(|a| a.checked_add(t.x)).ok_or_else(|| corrupt("tile placement"))?;
|
||||
let dst = start.checked_add(row.len()).and_then(|end| out.get_mut(start..end)).ok_or_else(|| corrupt("tile outside image"))?;
|
||||
|
||||
Vendored
+19
-1
@@ -143,7 +143,13 @@ pub(crate) fn table(b: &[u8], order: ByteOrder) -> Result<Huffman> {
|
||||
|
||||
/// Decode a `w × h` Huffman-coded image.
|
||||
pub(crate) fn decode_huffman(src: &[u8], h: &Huffman, w: usize, hgt: usize, bits_per: u32) -> Result<Vec<u16>> {
|
||||
let mut out = vec![0u16; w * hgt];
|
||||
// every sample takes at least one bit: a stream too short for the declared size is refused before its picture
|
||||
// is allocated, so a few bytes can't ask for gigabytes (issue #1512)
|
||||
let samples = w.checked_mul(hgt).ok_or(RawError::Limit("image too large"))?;
|
||||
if (src.len() as u64).saturating_mul(8) < samples as u64 {
|
||||
return Err(RawError::Corrupt(format!("PEF: {w} x {hgt} samples need more than the {} bytes of data there", src.len())));
|
||||
}
|
||||
let mut out = vec![0u16; samples];
|
||||
let mut bits = Bits::new(src);
|
||||
let mut vpred = [[0i32; 2]; 2];
|
||||
let max = (1i32 << bits_per.min(16)) - 1;
|
||||
@@ -358,6 +364,18 @@ mod tests {
|
||||
(b, codes)
|
||||
}
|
||||
|
||||
/// A compressed PEF declaring far more samples than its data could hold (one bit each at least) is refused
|
||||
/// before its picture is allocated (issue #1512).
|
||||
#[test]
|
||||
fn a_stream_too_short_for_its_size_is_refused_before_allocating() {
|
||||
let (tb, _) = test_table();
|
||||
let h = table(&tb, ByteOrder::Big).unwrap();
|
||||
let t = std::time::Instant::now();
|
||||
let r = decode_huffman(&[0x55; 4096], &h, 30_000, 30_000, 12);
|
||||
assert!(matches!(r, Err(RawError::Corrupt(ref m)) if m.contains("need more")), "{r:?}");
|
||||
assert!(t.elapsed() < std::time::Duration::from_secs(1));
|
||||
}
|
||||
|
||||
fn encode(px: &[u16], w: usize, codes: &[(u16, u8)]) -> Vec<u8> {
|
||||
let mut bits: Vec<bool> = Vec::new();
|
||||
let put = |v: u32, n: u8, bits: &mut Vec<bool>| (0..n).rev().for_each(|i| bits.push(v >> i & 1 == 1));
|
||||
|
||||
@@ -17,6 +17,12 @@
|
||||
stayed behind the edit. LightCraft now says once that the sidecars are waiting, tries again every few seconds once
|
||||
the files are reachable, and says when they're written. `library.retrySidecars` does it on request.
|
||||
|
||||
### Reliability
|
||||
- A damaged or crafted raw can no longer make LightCraft set aside gigabytes of memory before finding out the file is
|
||||
bad (issue #1512): a compressed PEF whose data is too short for the size it declares, an uncompressed TIFF-based
|
||||
raw whose tile claims far more samples than it holds, and an ARW6 with a broken tile are refused first. All 74
|
||||
files in the test corpus still decode.
|
||||
|
||||
### Hard-edged Heal and Clone spots
|
||||
- A spot with Feather at 0 no longer draws black pixels exactly on its radius. This also fixes spots at 0% Opacity
|
||||
changing pixels along their edge. The hard edge now leaves pixels on the radius unchanged.
|
||||
|
||||
Reference in New Issue
Block a user