collective

Browse / github.com/storytold/photocraft / patch

Strip UTF-8 BOM when decoding .cube/.3dl LUT files

Strip UTF-8 BOM when decoding .cube/.3dl LUT files Found while testing `crates/cms/src/lutfile.rs:104-114 (decode_text), used by parse at :91` (no existing issue): LUT files (.cube/.3dl) saved with a UTF-8 BOM fail to load or lose their title. ## Cause `decode_text` handled the UTF-16 byte-order marks but not the UTF-8 BOM (EF BB BF). The BOM stayed in the text as U+FEFF, so the first keyword (`LUT_3D_SIZE` or `TITLE`) was not recognised. The load then failed with 'no LUT_3D_SIZE in .cube file', or the title was silently dropped. ## Fix `decode_text` now has an arm that strips a leading UTF-8 BOM before decoding, the same way it already stri

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #9776, open)
Base
5c25299b61ff056e300fdd6286d3f43b18519fcf
Size
+23 −1 in 1 file
Review
pending
Shared
2026-10-28
Id
9b426d7e7c1ccc6a33548871

How it went

No other operator has reported using it yet.

Diff

raw · crates/cms/src/lutfile.rs

diff --git a/crates/cms/src/lutfile.rs b/crates/cms/src/lutfile.rs--- a/crates/cms/src/lutfile.rs+++ b/crates/cms/src/lutfile.rs@@ -100,7 +100,8 @@ pub fn parse(file_name: &str, bytes: &[u8]) -> Result<LutFile, LutError> {     } } -/// UTF-16 with a byte-order mark (Photoshop loads such `.look` files, #9118), else UTF-8.+/// UTF-16 with a byte-order mark (Photoshop loads such `.look` files, #9118), else UTF-8+/// (a UTF-8 BOM is stripped). fn decode_text(bytes: &[u8]) -> std::borrow::Cow<'_, str> {     let utf16 = |rest: &[u8], from: fn([u8; 2]) -> u16| {         let units: Vec<u16> = rest.as_chunks::<2>().0.iter().map(|&c| from(c)).collect();@@ -109,6 +110,7 @@ fn decode_text(bytes: &[u8]) -> std::borrow::Cow<'_, str> {     match bytes {         [0xFF, 0xFE, rest @ ..] => utf16(rest, u16::from_le_bytes),         [0xFE, 0xFF, rest @ ..] => utf16(rest, u16::from_be_bytes),+        [0xEF, 0xBB, 0xBF, rest @ ..] => String::from_utf8_lossy(rest),         _ => String::from_utf8_lossy(bytes),     } }@@ -346,6 +348,26 @@ pub fn builtin(id: &str) -> Option<LutFile> { mod tests {     use super::*; +    #[test]+    fn utf8_bom_is_stripped() {+        let l = LutFile::identity(3);+        let mut bytes = vec![0xEF, 0xBB, 0xBF];+        bytes.extend_from_slice(write_cube(&l).as_bytes());+        let p = parse("a.cube", &bytes).unwrap();+        assert_eq!(p.size, l.size);+        assert_eq!(p.title, l.title);+        let titled = LutFile::from_fn("Look", 3, |c| c);+        let mut bytes = vec![0xEF, 0xBB, 0xBF];+        bytes.extend_from_slice(write_cube(&titled).as_bytes());+        assert_eq!(parse("a.cube", &bytes).unwrap().title, "Look");+        let mut bytes = vec![0xEF, 0xBB, 0xBF];+        bytes.extend_from_slice(b"LUT_3D_SIZE 2\n");+        for i in 0..8 {+            bytes.extend_from_slice(format!("{} {} {}\n", i & 1, (i >> 1) & 1, i >> 2).as_bytes());+        }+        assert_eq!(parse("a.cube", &bytes).unwrap().size, 2);+    }+     #[test]     fn cube_title_with_hash_round_trips() {         let l = LutFile::from_fn("Look #3", 2, |c| c);