From c21343800ee26ce9fbf514ecaaf7607d640cf291 Mon Sep 17 00:00:00 2001 From: Omar Saidahmed Date: Thu, 12 Nov 2026 00:26:39 +0000 Subject: [PATCH] craft-psd: shared descriptor lookups Descriptor::get_bool, get_number, get_text, get_enum and get_object, used by asl, grd and slices in place of their own copies. Ported from storytold/photocraft#9552 (its crates/psd part). --- crates/psd/src/asl.rs | 12 ++-- crates/psd/src/descriptor.rs | 2 + crates/psd/src/descriptor/lookup.rs | 93 +++++++++++++++++++++++++++++ crates/psd/src/grd.rs | 35 ++++------- crates/psd/src/slices.rs | 42 ++++++------- 5 files changed, 129 insertions(+), 55 deletions(-) create mode 100644 crates/psd/src/descriptor/lookup.rs diff --git a/crates/psd/src/asl.rs b/crates/psd/src/asl.rs index c24be52e6..4088bc1ba 100644 --- a/crates/psd/src/asl.rs +++ b/crates/psd/src/asl.rs @@ -65,28 +65,24 @@ impl<'a> Rd<'a> { let b = self.take(2)?; Ok(u16::from_be_bytes([b[0], b[1]])) } + /// The next big-endian `u32`. fn u32(&mut self) -> Result { let b = self.take(4)?; Ok(u32::from_be_bytes([b[0], b[1], b[2], b[3]])) } + /// Bytes left to read. fn remaining(&self) -> usize { self.b.len().saturating_sub(self.p) } } -fn text(d: &Descriptor, key: &str) -> String { - match d.get(key) { - Some(Value::Text(t)) => t.to_string_lossy(), - _ => String::new(), - } -} - +/// One style from an ASL body: its identity descriptor, then the style itself. fn read_style(body: &[u8]) -> Result { let (identity, used) = VersionedDescriptor::parse_prefix(body)?; let rest = body.get(used..).ok_or(PsdError::UnexpectedEof { offset: used, needed: 4 })?; let (style, _) = VersionedDescriptor::parse_prefix(rest)?; let id = &identity.descriptor; - Ok(AslStyle { name: text(id, "Nm "), id: text(id, "Idnt"), style: style.descriptor }) + Ok(AslStyle { name: id.get_text("Nm ").unwrap_or_default(), id: id.get_text("Idnt").unwrap_or_default(), style: style.descriptor }) } /// Parses an `.asl` style library. diff --git a/crates/psd/src/descriptor.rs b/crates/psd/src/descriptor.rs index a9142c1bd..72eecbf3f 100644 --- a/crates/psd/src/descriptor.rs +++ b/crates/psd/src/descriptor.rs @@ -7,6 +7,8 @@ use crate::error::{PsdError, Result}; use crate::io::{Reader, WriteExt, read_unicode_units, write_unicode_units}; +mod lookup; + /// Maximum nesting depth accepted when parsing (fuzz safety). pub const MAX_DEPTH: usize = 64; diff --git a/crates/psd/src/descriptor/lookup.rs b/crates/psd/src/descriptor/lookup.rs new file mode 100644 index 000000000..6e9eb1915 --- /dev/null +++ b/crates/psd/src/descriptor/lookup.rs @@ -0,0 +1,93 @@ +use super::{Descriptor, Value}; + +impl Descriptor { + /// The boolean at `key`. + pub fn get_bool(&self, key: &str) -> Option { + match self.get(key)? { + Value::Boolean(b) => Some(*b), + _ => None, + } + } + + /// The number at `key`, from any numeric value type. + pub fn get_number(&self, key: &str) -> Option { + match self.get(key)? { + Value::Double(v) => Some(*v), + Value::UnitFloat { value, .. } => Some(*value), + Value::Integer(v) => Some(f64::from(*v)), + Value::LargeInteger(v) => Some(*v as f64), + _ => None, + } + } + + /// The text at `key`. + pub fn get_text(&self, key: &str) -> Option { + match self.get(key)? { + Value::Text(t) => Some(t.to_string_lossy()), + _ => None, + } + } + + /// The enumerated value at `key`. + pub fn get_enum(&self, key: &str) -> Option { + match self.get(key)? { + Value::Enumerated { value, .. } => Some(String::from_utf8_lossy(value.as_bytes()).into_owned()), + _ => None, + } + } + + /// The object at `key`, plain or global. + pub fn get_object(&self, key: &str) -> Option<&Descriptor> { + match self.get(key)? { + Value::Descriptor(o) | Value::GlobalObject(o) => Some(o), + _ => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::super::{Id, UnicodeString}; + use super::*; + + fn sample() -> Descriptor { + Descriptor::new("null") + .with("enab", Value::Boolean(true)) + .with("doub", Value::Double(0.25)) + .with("Opct", Value::UnitFloat { unit: *b"#Prc", value: 75.5 }) + .with("long", Value::Integer(-5)) + .with("comp", Value::LargeInteger(1 << 40)) + .with("Nm ", Value::Text(UnicodeString::new("Café"))) + .with("Md ", Value::Enumerated { type_id: Id::new("BlnM"), value: Id::new("Nrml") }) + .with("Objc", Value::Descriptor(Descriptor::new("Clr ").with("Rd ", Value::Double(1.0)))) + .with("GlbO", Value::GlobalObject(Descriptor::new("Grsc"))) + } + + #[test] + fn each_lookup_reads_its_own_value_type() { + let d = sample(); + assert_eq!(d.get_bool("enab"), Some(true)); + assert_eq!(d.get_number("doub"), Some(0.25)); + assert_eq!(d.get_number("Opct"), Some(75.5)); + assert_eq!(d.get_number("long"), Some(-5.0)); + assert_eq!(d.get_number("comp"), Some((1u64 << 40) as f64)); + assert_eq!(d.get_text("Nm ").as_deref(), Some("Café")); + assert_eq!(d.get_enum("Md ").as_deref(), Some("Nrml")); + assert_eq!(d.get_object("Objc").and_then(|o| o.get_number("Rd ")), Some(1.0)); + assert!(d.get_object("GlbO").is_some()); + } + + #[test] + fn lookups_of_other_types_or_missing_keys_are_none() { + let d = sample(); + assert_eq!(d.get_bool("doub"), None); + assert_eq!(d.get_number("enab"), None); + assert_eq!(d.get_text("long"), None); + assert_eq!(d.get_enum("Nm "), None); + assert!(d.get_object("Md ").is_none()); + for key in ["none", " "] { + assert_eq!((d.get_bool(key), d.get_number(key), d.get_text(key), d.get_enum(key)), (None, None, None, None)); + assert!(d.get_object(key).is_none()); + } + } +} diff --git a/crates/psd/src/grd.rs b/crates/psd/src/grd.rs index bfa2cd91c..ada611303 100644 --- a/crates/psd/src/grd.rs +++ b/crates/psd/src/grd.rs @@ -58,18 +58,6 @@ pub struct GrdGradient { pub opacity: Vec<(f32, f32, f32)>, } -/// The raw number behind a key. It is deliberately *not* filtered: every caller passes the result -/// through `finite`, so filtering here as well would give two guards with two different answers to -/// the same input, and mutation testing showed the second one was unreachable. -fn num(d: &Descriptor, k: &str) -> Option { - match d.get(k)? { - Value::Double(v) => Some(*v), - Value::UnitFloat { value, .. } => Some(*value), - Value::Integer(v) => Some(f64::from(*v)), - Value::LargeInteger(v) => Some(*v as f64), - _ => None, - } -} /// The single guard every number passes through on its way to an `f32`. Only a NaN is truly /// without a value, so only a NaN returns `None`; the caller then falls back to the default its /// own field documents (opacity opaque, a midpoint centred, a channel zero). @@ -85,9 +73,11 @@ fn finite(v: f64) -> Option { } Some(v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32) } +/// The finite `f32` at `k`, or 0. fn f(d: &Descriptor, k: &str) -> f32 { - finite(num(d, k).unwrap_or(0.0)).unwrap_or(0.0) + finite(d.get_number(k).unwrap_or(0.0)).unwrap_or(0.0) } +/// `v` clamped to 0..=1. fn clamp01(v: f32) -> f32 { v.clamp(0.0, 1.0) } @@ -99,12 +89,7 @@ fn hue(h: f32) -> f32 { if (0.0..=360.0).contains(&h) { h } else { h.rem_euclid(360.0) } } -fn obj<'a>(d: &'a Descriptor, k: &str) -> Option<&'a Descriptor> { - match d.get(k)? { - Value::Descriptor(o) | Value::GlobalObject(o) => Some(o), - _ => None, - } -} +/// The list at `k`, empty when absent. fn list<'a>(d: &'a Descriptor, k: &str) -> &'a [Value] { match d.get(k) { Some(Value::List(v)) => v, @@ -115,6 +100,7 @@ fn enum_is(d: &Descriptor, k: &str, v: &str) -> bool { matches!(d.get(k), Some(Value::Enumerated { value, .. }) if value.is(v)) } +/// A gradient stop colour in its recorded colour model. fn color(c: &Descriptor) -> Option { let id = c.class_id.as_bytes(); Some(match id { @@ -135,13 +121,16 @@ fn color(c: &Descriptor) -> Option { }) } +/// A stop's location as a 0..1 fraction. fn location(d: &Descriptor) -> f32 { (f(d, "Lctn") / 4096.0).clamp(0.0, 1.0) } +/// A stop's midpoint as a 0..1 fraction, 0.5 when absent. fn midpoint(d: &Descriptor) -> f32 { - finite(num(d, "Mdpn").unwrap_or(50.0) / 100.0).unwrap_or(0.5).clamp(0.0, 1.0) + finite(d.get_number("Mdpn").unwrap_or(50.0) / 100.0).unwrap_or(0.5).clamp(0.0, 1.0) } +/// One gradient from its descriptor. fn gradient(g: &Descriptor) -> GrdGradient { let name = match g.get("Nm ") { Some(Value::Text(t)) => t.to_string_lossy(), @@ -155,7 +144,7 @@ fn gradient(g: &Descriptor) -> GrdGradient { } else if enum_is(s, "Type", "BckC") { Some(GrdColor::Background) } else { - obj(s, "Clr ").and_then(color) + s.get_object("Clr ").and_then(color) }; if let Some(color) = c { out.stops.push(GrdStop { location: location(s), midpoint: midpoint(s), color }); @@ -163,7 +152,7 @@ fn gradient(g: &Descriptor) -> GrdGradient { } for v in list(g, "Trns").iter().take(MAX_STOPS) { let Value::Descriptor(s) = v else { continue }; - out.opacity.push((location(s), finite(num(s, "Opct").unwrap_or(100.0) / 100.0).unwrap_or(1.0).clamp(0.0, 1.0), midpoint(s))); + out.opacity.push((location(s), finite(s.get_number("Opct").unwrap_or(100.0) / 100.0).unwrap_or(1.0).clamp(0.0, 1.0), midpoint(s))); } out } @@ -185,7 +174,7 @@ pub fn parse(data: &[u8]) -> Result> { .iter() .take(MAX_GRADIENTS) .filter_map(|v| match v { - Value::Descriptor(d) => obj(d, "Grad").map(gradient), + Value::Descriptor(d) => d.get_object("Grad").map(gradient), _ => None, }) .collect(); diff --git a/crates/psd/src/slices.rs b/crates/psd/src/slices.rs index 5d9cd50b3..85a72b967 100644 --- a/crates/psd/src/slices.rs +++ b/crates/psd/src/slices.rs @@ -108,20 +108,7 @@ fn d_u32(d: &Descriptor, key: &str) -> Option { } } -fn d_text(d: &Descriptor, key: &str) -> String { - match d.get(key) { - Some(Value::Text(t)) => t.to_string_lossy(), - _ => String::new(), - } -} - -fn d_enum(d: &Descriptor, key: &str) -> Option { - match d.get(key)? { - Value::Enumerated { value, .. } => Some(String::from_utf8_lossy(value.as_bytes()).into_owned()), - _ => None, - } -} - +/// Whether the boolean at `key` is true. fn d_bool(d: &Descriptor, key: &str) -> bool { matches!(d.get(key), Some(Value::Boolean(true))) } @@ -141,6 +128,7 @@ fn origin_from(s: &str) -> u32 { } } +/// The slice type code a slice type name stands for. fn kind_from(s: &str) -> u32 { match s { "noImage" => 0, @@ -149,9 +137,10 @@ fn kind_from(s: &str) -> u32 { } } +/// A slice record from its descriptor. fn slice_from_descriptor(d: &Descriptor) -> SliceRecord { let b = d_bounds(d, "bounds").unwrap_or_default(); - let color = match (d_enum(d, "bgColorType").as_deref(), d.get("bgColor")) { + let color = match (d.get_enum("bgColorType").as_deref(), d.get("bgColor")) { (Some("Clr "), Some(Value::Descriptor(c))) => { let g = |k: &str| d_int(c, k).unwrap_or(0).clamp(0, 255) as u8; let a = if c.get("alpha").is_some() { g("alpha") } else { 255 }; @@ -162,18 +151,18 @@ fn slice_from_descriptor(d: &Descriptor) -> SliceRecord { SliceRecord { id: d_u32(d, "sliceID").unwrap_or(0), group_id: d_u32(d, "groupID").unwrap_or(0), - origin: d_enum(d, "origin").map_or(2, |s| origin_from(&s)), + origin: d.get_enum("origin").map_or(2, |s| origin_from(&s)), layer_id: d_u32(d, "layerID"), - name: d_text(d, "Nm "), - kind: d_enum(d, "Type").map_or(1, |s| kind_from(&s)), + name: d.get_text("Nm ").unwrap_or_default(), + kind: d.get_enum("Type").map_or(1, |s| kind_from(&s)), // Descriptor bounds are top/left/bottom/right; records use left/top/right/bottom. rect: [b[1], b[0], b[3], b[2]], - url: d_text(d, "url"), - target: d_text(d, "null"), - message: d_text(d, "Msge"), - alt: d_text(d, "altTag"), + url: d.get_text("url").unwrap_or_default(), + target: d.get_text("null").unwrap_or_default(), + message: d.get_text("Msge").unwrap_or_default(), + alt: d.get_text("altTag").unwrap_or_default(), cell_text_is_html: d_bool(d, "cellTextIsHTML"), - cell_text: d_text(d, "cellText"), + cell_text: d.get_text("cellText").unwrap_or_default(), horizontal_align: 0, vertical_align: 0, color, @@ -220,7 +209,12 @@ impl SlicesResource { .collect(), _ => Vec::new(), }; - Ok(SlicesResource { version, bounds: d_bounds(d, "bounds").unwrap_or_default(), group_name: d_text(d, "baseName"), slices }) + Ok(SlicesResource { + version, + bounds: d_bounds(d, "bounds").unwrap_or_default(), + group_name: d.get_text("baseName").unwrap_or_default(), + slices, + }) } v => Err(PsdError::invalid(format!("unsupported slices resource version {v}"))), }