From 4092fe79c94c3eed9ba66f8f82bc362f6eb560a6 Mon Sep 17 00:00:00 2001 From: Simone Date: Fri, 23 Oct 2026 23:40:16 +0200 Subject: [PATCH] fix(cms): reject malformed hex data in .look LUTs (#8657) Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com> --- crates/cms/src/lutfile.rs | 41 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/crates/cms/src/lutfile.rs b/crates/cms/src/lutfile.rs index 240aac36f8..dbe6281d8f 100644 --- a/crates/cms/src/lutfile.rs +++ b/crates/cms/src/lutfile.rs @@ -221,7 +221,14 @@ pub fn parse_look(text: &str) -> Result { }; let size: usize = tag("size").and_then(|s| s.parse().ok()).ok_or_else(|| LutError("no in .look".into()))?; let (entries, _) = checked_data_len(size)?; - let hex: Vec = tag("data").ok_or_else(|| LutError("no in .look".into()))?.bytes().filter(u8::is_ascii_hexdigit).collect(); + let hex: Vec = tag("data").ok_or_else(|| LutError("no in .look".into()))?.bytes().filter(|b| !b.is_ascii_whitespace()).collect(); + if hex.iter().any(|b| !b.is_ascii_hexdigit()) { + return err("invalid hexadecimal in .look"); + } + // Each float needs eight hex digits; as_chunks would silently discard a partial value. + if !hex.len().is_multiple_of(8) { + return err("incomplete float in .look "); + } let nib = |c: u8| (c as char).to_digit(16).unwrap_or(0) as u8; let bytes: Vec = hex.as_chunks::<2>().0.iter().map(|p| nib(p[0]) << 4 | nib(p[1])).collect(); let floats: Vec = bytes.as_chunks::<4>().0.iter().map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect(); @@ -456,6 +463,38 @@ DOMAIN_MAX 0.5 1 1 assert_eq!(parse("a.look", xml.as_bytes()).unwrap().data, id.data); } + #[test] + fn look_rgb_and_rgba_allow_whitespace_in_hex_data() { + let id = LutFile::identity(2); + for channels in [3, 4] { + let mut hex = String::new(); + for rgb in id.data.as_chunks::<3>().0 { + for value in rgb.iter().copied().chain((channels == 4).then_some(1.0)) { + for byte in value.to_le_bytes() { + hex.push_str(&format!("{byte:02x} \t\r\n")); + } + } + } + let xml = format!("2\"{hex}\""); + assert_eq!(parse("valid.look", xml.as_bytes()).unwrap().data, id.data); + } + } + + #[test] + fn look_rejects_partial_floats_and_invalid_hex() { + let id = LutFile::identity(2); + let hex: String = id.data.iter().flat_map(|v| v.to_le_bytes()).map(|b| format!("{b:02X}")).collect(); + for suffix in ["0", "00", "0000", "000000", "G", "é"] { + let xml = format!("2{hex}{suffix}"); + assert!(parse("bad.look", xml.as_bytes()).is_err(), "accepted malformed suffix {suffix:?}"); + } + let invalid = hex.replacen('0', "G", 1); + let xml = format!("2{invalid}"); + assert!(parse("bad.look", xml.as_bytes()).unwrap_err().0.contains("invalid hexadecimal")); + let xml = format!("2{hex}00"); + assert!(parse("bad.look", xml.as_bytes()).unwrap_err().0.contains("incomplete float")); + } + #[test] fn look_3d_size_exponent_overflow_is_rejected() { let text = format!("{}00", usize::MAX);