mirror of
https://github.com/storytold/photocraft.git
synced 2026-11-02 18:13:39 +00:00
Merge main into integration-batch-7
This commit is contained in:
+40
-9
@@ -435,6 +435,7 @@ fn parse_v6_within(r: &mut Rd, out: &mut AbrFile, budget: &mut Budget) -> Result
|
||||
return Err(PsdError::Unsupported(format!("brush file subversion {}", out.subversion)));
|
||||
}
|
||||
let mut hierarchy = None;
|
||||
let mut patterns = PatternBudget::default();
|
||||
while r.left() >= 12 {
|
||||
let sig = r.arr::<4>()?;
|
||||
if &sig != b"8BIM" {
|
||||
@@ -458,16 +459,17 @@ fn parse_v6_within(r: &mut Rd, out: &mut AbrFile, budget: &mut Budget) -> Result
|
||||
match &key {
|
||||
b"samp" => read_samp(data, out.subversion, out, budget)?,
|
||||
b"patt" => {
|
||||
// Patterns share the samples' budget, so neither can claim it twice.
|
||||
// Patterns share the samples' budget and one padding budget across every `patt`
|
||||
// section, so neither can be claimed twice and failed blocks still deduct their
|
||||
// decoded charges.
|
||||
let limit = u64::try_from(budget.0).unwrap_or(u64::MAX);
|
||||
let mut patterns = PatternBudget::with_decoded_limit(limit);
|
||||
patterns.cap_decoded_to(limit);
|
||||
let left = patterns.decoded_left();
|
||||
match parse_pattern_block_with(data, &mut patterns) {
|
||||
Ok(p) => {
|
||||
let spent = usize::try_from(left.saturating_sub(patterns.decoded_left())).unwrap_or(usize::MAX);
|
||||
budget.spend(spent)?;
|
||||
out.patterns.extend(p);
|
||||
}
|
||||
let res = parse_pattern_block_with(data, &mut patterns);
|
||||
let spent = usize::try_from(left.saturating_sub(patterns.decoded_left())).unwrap_or(usize::MAX);
|
||||
budget.spend(spent)?;
|
||||
match res {
|
||||
Ok(p) => out.patterns.extend(p),
|
||||
Err(e) => out.warnings.push(format!("embedded patterns unreadable: {e}")),
|
||||
}
|
||||
}
|
||||
@@ -706,10 +708,39 @@ mod tests {
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 2, 0));
|
||||
// One byte short: the patterns no longer fit after the sample (they used not to count).
|
||||
let (out, left) = parse_within(189);
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 0, 89));
|
||||
assert_eq!((out.samples.len(), out.patterns.len(), left), (1, 0, 14));
|
||||
assert!(out.warnings.iter().any(|w| w.contains("embedded patterns unreadable")), "{:?}", out.warnings);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_section_does_not_refund_budget_to_later_sections() {
|
||||
let p = PsdPattern {
|
||||
mode: 3,
|
||||
width: 5,
|
||||
height: 3,
|
||||
name: "p".into(),
|
||||
id: "id".into(),
|
||||
palette: None,
|
||||
depth: 8,
|
||||
channels: vec![vec![1; 15], vec![2; 15], vec![3; 15]],
|
||||
alpha: None,
|
||||
};
|
||||
let valid = write_pattern_block(std::slice::from_ref(&p)).unwrap();
|
||||
let mut malformed = valid.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
|
||||
let mut body = 2u16.to_be_bytes().to_vec();
|
||||
section(&mut body, b"patt", &malformed);
|
||||
section(&mut body, b"patt", &valid);
|
||||
let mut out = AbrFile::default();
|
||||
let mut budget = Budget(60);
|
||||
parse_v6_within(&mut Rd::new(&body), &mut out, &mut budget).unwrap();
|
||||
|
||||
assert!(out.patterns.is_empty(), "a failed block is discarded atomically");
|
||||
assert_eq!(budget.0, 0, "the failed block's first 45 bytes and later partial 15-byte charge are retained");
|
||||
assert_eq!(out.warnings.iter().filter(|w| w.contains("embedded patterns unreadable")).count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1_v2_round_trip_raw_and_rle() {
|
||||
for version in [1u16, 2] {
|
||||
|
||||
@@ -56,6 +56,12 @@ impl PatternBudget {
|
||||
PatternBudget { decoded_left: d.decoded_left.min(decoded), padded_left: d.padded_left.min(decoded) }
|
||||
}
|
||||
|
||||
/// Cap both decoded and padded bytes to the amount still available to a containing file.
|
||||
pub fn cap_decoded_to(&mut self, decoded: u64) {
|
||||
self.decoded_left = self.decoded_left.min(decoded);
|
||||
self.padded_left = self.padded_left.min(decoded);
|
||||
}
|
||||
|
||||
/// Decoded bytes still available.
|
||||
pub fn decoded_left(&self) -> u64 {
|
||||
self.decoded_left
|
||||
@@ -425,9 +431,8 @@ pub fn parse_pattern_block(data: &[u8]) -> Result<Vec<PsdPattern>> {
|
||||
}
|
||||
|
||||
/// [`parse_pattern_block`] charging `budget`, so several blocks of one document share one limit.
|
||||
/// The budget is charged only for patterns that are returned.
|
||||
/// Charges are monotonic even when a later record in the block fails.
|
||||
pub fn parse_pattern_block_with(data: &[u8], budget: &mut PatternBudget) -> Result<Vec<PsdPattern>> {
|
||||
let mut left = budget.clone();
|
||||
let mut r = Rd { b: data, p: 0 };
|
||||
let mut out = Vec::new();
|
||||
while r.p + 4 <= data.len() {
|
||||
@@ -436,11 +441,10 @@ pub fn parse_pattern_block_with(data: &[u8], budget: &mut PatternBudget) -> Resu
|
||||
break;
|
||||
}
|
||||
let body = r.take(len)?;
|
||||
out.push(read_pattern(&mut Rd { b: body, p: 0 }, &mut left)?);
|
||||
out.push(read_pattern(&mut Rd { b: body, p: 0 }, budget)?);
|
||||
let pad = (4 - len % 4) % 4;
|
||||
r.p = (r.p + pad).min(data.len());
|
||||
}
|
||||
*budget = left;
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -729,6 +733,18 @@ mod tests {
|
||||
block
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_pattern_block_keeps_partial_charge_for_the_next_block() {
|
||||
let one = padded_gray_block(100);
|
||||
let mut malformed = one.clone();
|
||||
malformed.extend_from_slice(&100u32.to_be_bytes());
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(parse_pattern_block_with(&malformed, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
assert!(parse_pattern_block_with(&one, &mut budget).is_err());
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn padding_budget_is_shared_by_every_pattern_of_a_parse() {
|
||||
// Each pattern pads 100x100 = 10000 bytes; a 15000-byte budget fits one, not two. Before
|
||||
@@ -741,8 +757,9 @@ mod tests {
|
||||
let two = [one.clone(), one.clone()].concat();
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
assert!(matches!(parse_pattern_block_with(&two, &mut budget), Err(PsdError::LimitExceeded(_))));
|
||||
// A failed parse keeps nothing, so it charges nothing.
|
||||
assert_eq!(budget.decoded_left(), 15_000);
|
||||
// Charges are monotonic: the first pattern's 10000 bytes stay charged even when the second
|
||||
// record fails.
|
||||
assert_eq!(budget.decoded_left(), 5_000);
|
||||
|
||||
// Two blocks of one document share a budget too.
|
||||
let mut budget = PatternBudget::with_decoded_limit(15_000);
|
||||
|
||||
@@ -245,12 +245,14 @@ impl<'a> Layer<'a> {
|
||||
let rect = self.record.rect;
|
||||
let (w, h) = rect.size()?;
|
||||
let n = w.checked_mul(h).ok_or(PsdError::LimitExceeded("layer size overflow"))?;
|
||||
if n.checked_mul(4).is_none_or(|len| len as u64 > crate::compression::MAX_DECODED_BYTES) {
|
||||
return Err(PsdError::LimitExceeded("RGBA output exceeds MAX_DECODED_BYTES"));
|
||||
}
|
||||
let depth = self.header.depth;
|
||||
let mode = self.header.color_mode;
|
||||
let cc = color_channel_count(mode)?;
|
||||
// The planes (one per colour channel, filled in for channels the layer lacks, and alpha)
|
||||
// coexist with the RGBA output, so the cap covers all of them, not the output alone.
|
||||
if n.checked_mul(cc + 2 + 4).is_none_or(|len| len as u64 > crate::compression::MAX_DECODED_BYTES) {
|
||||
return Err(PsdError::LimitExceeded("RGBA output exceeds MAX_DECODED_BYTES"));
|
||||
}
|
||||
let mut color = Vec::with_capacity(cc);
|
||||
for id in 0..cc as i16 {
|
||||
let plane = if self.record.channel(id).is_some() {
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
//! version reads. Unknown fields are not kept here; callers that need a byte-exact round trip
|
||||
//! keep the raw resource while the slices are unchanged.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::descriptor::{Descriptor, Id, UnicodeString, Value, VersionedDescriptor};
|
||||
use crate::error::{PsdError, Result};
|
||||
use crate::io::{Reader, WriteExt};
|
||||
@@ -274,13 +276,22 @@ impl SlicesResource {
|
||||
&& let Ok(vd) = VersionedDescriptor::read(r)
|
||||
&& let Some(Value::List(items)) = vd.descriptor.get("slices")
|
||||
{
|
||||
// Index each slice ID to its first occurrence once, so matching trailing
|
||||
// descriptors against slices is linear rather than quadratic. Both the slice
|
||||
// count and the descriptor-ID list are file-controlled, so a plain
|
||||
// `find()` per descriptor is O(n*m) and lets a crafted resource burn CPU.
|
||||
// `or_insert` preserves the original first-match behavior for duplicate IDs.
|
||||
let mut id_to_index: HashMap<u32, usize> = HashMap::with_capacity(slices.len());
|
||||
for (i, s) in slices.iter().enumerate() {
|
||||
id_to_index.entry(s.id).or_insert(i);
|
||||
}
|
||||
for v in items {
|
||||
let Value::Descriptor(d) = v else { continue };
|
||||
// An entry without a usable id belongs to no slice (not to slice 0).
|
||||
let Some(id) = d_u32(d, "sliceID") else { continue };
|
||||
let extra = slice_from_descriptor(d);
|
||||
if let Some(s) = slices.iter_mut().find(|s| s.id == id) {
|
||||
s.outsets = extra.outsets;
|
||||
if let Some(&i) = id_to_index.get(&id) {
|
||||
slices[i].outsets = extra.outsets;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -493,4 +504,28 @@ mod tests {
|
||||
assert!(SlicesResource::from_bytes(&huge).is_err());
|
||||
assert!(SlicesResource::from_bytes(&9u32.to_be_bytes()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_descriptor_applies_outsets_to_first_matching_slice() {
|
||||
// Two slices share id 1; the trailing descriptor must update the first one only,
|
||||
// matching the pre-index behavior of `iter_mut().find()`.
|
||||
let mut plain = SlicesResource {
|
||||
version: 6,
|
||||
bounds: [0, 0, 80, 100],
|
||||
group_name: "site".into(),
|
||||
slices: vec![
|
||||
SliceRecord { id: 1, origin: 0, kind: 1, rect: [0, 0, 10, 10], ..Default::default() },
|
||||
SliceRecord { id: 1, origin: 0, kind: 1, rect: [10, 10, 20, 20], ..Default::default() },
|
||||
],
|
||||
};
|
||||
for s in &mut plain.slices {
|
||||
s.outsets = [0; 4];
|
||||
}
|
||||
let mut bytes = plain.to_bytes();
|
||||
let extra = Descriptor::new("slice").with("sliceID", Value::Integer(1)).with("topOutset", Value::Integer(7));
|
||||
VersionedDescriptor::new(Descriptor::new("null").with("slices", Value::List(vec![Value::Descriptor(extra)]))).write(&mut bytes);
|
||||
let back = SlicesResource::from_bytes(&bytes).unwrap();
|
||||
assert_eq!(back.slices[0].outsets, [7, 0, 0, 0], "first slice receives the outset");
|
||||
assert_eq!(back.slices[1].outsets, [0, 0, 0, 0], "later duplicate is untouched");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user