Skip to main content

ithmb_core/
profile_parser.rs

1// allow: SIZE_OK — hand-rolled JSON parser is an indivisible state machine
2
3//! Hand-rolled JSON parser for the profiles.json schema.
4//!
5//! Parses a fixed-format JSON array of profile objects into `Vec<Profile>`.
6//! Not a general-purpose JSON parser — understands only the field names and
7//! value types used by the profile database.
8
9use crate::error::DecodeError;
10use crate::profile::{Encoding, Profile};
11
12/// Parse a JSON array of profile objects from the given string input.
13///
14/// # Errors
15/// Returns `DecodeError::Profile` on invalid JSON, unknown encoding values,
16/// or numeric parse failures.
17pub fn parse_profiles_json(input: &str) -> Result<Vec<Profile>, DecodeError> {
18    let mut p = Parser {
19        bytes: input.as_bytes(),
20        pos: 0,
21    };
22    p.skip_ws();
23    p.parse_array()
24}
25
26// ---------------------------------------------------------------------------
27// Internal cursor-based parser
28// ---------------------------------------------------------------------------
29
30struct Parser<'a> {
31    bytes: &'a [u8],
32    pos: usize,
33}
34
35impl Parser<'_> {
36    // -- low-level helpers --
37
38    fn peek(&self) -> Option<u8> {
39        self.bytes.get(self.pos).copied()
40    }
41
42    fn advance(&mut self) -> Result<u8, DecodeError> {
43        let b = self
44            .bytes
45            .get(self.pos)
46            .copied()
47            .ok_or_else(|| DecodeError::Profile("unexpected end of input".into()))?;
48        self.pos += 1;
49        Ok(b)
50    }
51
52    fn expect(&mut self, want: u8) -> Result<(), DecodeError> {
53        let got = self.advance()?;
54        if got != want {
55            return Err(DecodeError::Profile(format!(
56                "expected '{}' (0x{:02X}), got '{}' (0x{:02X}) at offset {}",
57                want as char,
58                want,
59                got as char,
60                got,
61                self.pos - 1,
62            )));
63        }
64        Ok(())
65    }
66
67    fn skip_ws(&mut self) {
68        while let Some(b) = self.peek() {
69            if b != b' ' && b != b'\t' && b != b'\n' && b != b'\r' {
70                break;
71            }
72            self.pos += 1;
73        }
74    }
75
76    // -- value parsers --
77
78    fn parse_string(&mut self) -> Result<String, DecodeError> {
79        self.expect(b'"')?;
80        let mut s = String::new();
81        loop {
82            let b = self.advance()?;
83            match b {
84                b'"' => return Ok(s),
85                b'\\' => {
86                    let esc = self.advance()?;
87                    match esc {
88                        b'"' => s.push('"'),
89                        b'\\' => s.push('\\'),
90                        b'/' => s.push('/'),
91                        b'b' => s.push('\u{0008}'),
92                        b'f' => s.push('\u{000C}'),
93                        b'n' => s.push('\n'),
94                        b'r' => s.push('\r'),
95                        b't' => s.push('\t'),
96                        b'u' => {
97                            let code = self.parse_hex4()?;
98                            let c = char::from_u32(code).ok_or_else(|| {
99                                DecodeError::Profile(format!("invalid unicode escape: \\u{code:04X}"))
100                            })?;
101                            s.push(c);
102                        }
103                        _ => {
104                            return Err(DecodeError::Profile(format!(
105                                "invalid escape sequence: \\{}",
106                                esc as char
107                            )));
108                        }
109                    }
110                }
111                // JSON allows most characters inside strings, including UTF-8
112                0x20..=0x7E | 0x80..=0xFF => s.push(b as char),
113                _ => {
114                    return Err(DecodeError::Profile(format!("invalid character in string: 0x{b:02X}")));
115                }
116            }
117        }
118    }
119
120    fn parse_hex4(&mut self) -> Result<u32, DecodeError> {
121        let mut val: u32 = 0;
122        for _ in 0..4 {
123            let b = self.advance()?;
124            val <<= 4;
125            val += match b {
126                b'0'..=b'9' => u32::from(b - b'0'),
127                b'a'..=b'f' => u32::from(b - b'a' + 10),
128                b'A'..=b'F' => u32::from(b - b'A' + 10),
129                _ => {
130                    return Err(DecodeError::Profile(format!("invalid hex digit: '{}'", b as char)));
131                }
132            };
133        }
134        Ok(val)
135    }
136
137    fn parse_number_i32(&mut self) -> Result<i32, DecodeError> {
138        self.skip_ws();
139        let start = self.pos;
140        if self.pos >= self.bytes.len() {
141            return Err(DecodeError::Profile(format!("expected number at offset {}", self.pos)));
142        }
143        if self.peek() == Some(b'-') {
144            self.pos += 1;
145        }
146        if self.peek().is_none_or(|b| !b.is_ascii_digit()) {
147            return Err(DecodeError::Profile(format!("expected number at offset {start}")));
148        }
149        while self.peek().is_some_and(|b| b.is_ascii_digit()) {
150            self.pos += 1;
151        }
152        let s = std::str::from_utf8(&self.bytes[start..self.pos])
153            .map_err(|e| DecodeError::Profile(format!("invalid number encoding: {e}")))?;
154        s.parse::<i32>()
155            .map_err(|e| DecodeError::Profile(format!("invalid number '{s}': {e}")))
156    }
157
158    fn parse_bool(&mut self) -> Result<bool, DecodeError> {
159        self.skip_ws();
160        if self.pos >= self.bytes.len() {
161            return Err(DecodeError::Profile(format!("expected bool at offset {}", self.pos)));
162        }
163        if self.bytes[self.pos..].starts_with(b"true") {
164            self.pos += 4;
165            Ok(true)
166        } else if self.bytes[self.pos..].starts_with(b"false") {
167            self.pos += 5;
168            Ok(false)
169        } else {
170            Err(DecodeError::Profile(format!("expected bool at offset {}", self.pos)))
171        }
172    }
173
174    fn skip_value(&mut self) -> Result<(), DecodeError> {
175        self.skip_ws();
176        match self.peek() {
177            Some(b'"') => {
178                self.parse_string()?;
179            }
180            Some(b't' | b'f') => {
181                self.parse_bool()?;
182            }
183            Some(b'n') => {
184                self.parse_null()?;
185            }
186            Some(b'[') => {
187                self.skip_array()?;
188            }
189            Some(b'{') => {
190                self.skip_object()?;
191            }
192            Some(b'-' | b'0'..=b'9') => {
193                self.parse_number_i32()?;
194            }
195            Some(c) => {
196                return Err(DecodeError::Profile(format!(
197                    "unexpected character '{c}' at offset {}",
198                    self.pos
199                )));
200            }
201            None => {
202                return Err(DecodeError::Profile("unexpected end of input".into()));
203            }
204        }
205        Ok(())
206    }
207
208    fn parse_null(&mut self) -> Result<(), DecodeError> {
209        self.skip_ws();
210        if self.bytes[self.pos..].starts_with(b"null") {
211            self.pos += 4;
212            Ok(())
213        } else {
214            Err(DecodeError::Profile(format!("expected null at offset {}", self.pos)))
215        }
216    }
217
218    fn skip_array(&mut self) -> Result<(), DecodeError> {
219        self.expect(b'[')?;
220        loop {
221            self.skip_ws();
222            if self.peek() == Some(b']') {
223                self.pos += 1;
224                return Ok(());
225            }
226            if self.peek() != Some(b'[') {
227                self.skip_value()?;
228            }
229            self.skip_ws();
230            if self.peek() == Some(b',') {
231                self.pos += 1;
232            }
233        }
234    }
235
236    fn skip_object(&mut self) -> Result<(), DecodeError> {
237        self.expect(b'{')?;
238        loop {
239            self.skip_ws();
240            if self.peek() == Some(b'}') {
241                self.pos += 1;
242                return Ok(());
243            }
244            self.parse_string()?;
245            self.skip_ws();
246            self.expect(b':')?;
247            self.skip_value()?;
248            self.skip_ws();
249            if self.peek() == Some(b',') {
250                self.pos += 1;
251            }
252        }
253    }
254
255    // -- profile-specific parsers --
256
257    fn parse_array(&mut self) -> Result<Vec<Profile>, DecodeError> {
258        self.expect(b'[')?;
259        let mut profiles: Vec<Profile> = Vec::new();
260        loop {
261            self.skip_ws();
262            if self.peek() == Some(b']') {
263                self.pos += 1;
264                return Ok(profiles);
265            }
266            if !profiles.is_empty() {
267                self.expect(b',')?;
268                self.skip_ws();
269            }
270            profiles.push(self.parse_object()?);
271        }
272    }
273
274    fn parse_object(&mut self) -> Result<Profile, DecodeError> {
275        self.expect(b'{')?;
276        let mut profile = Profile::default();
277        loop {
278            self.skip_ws();
279            if self.peek() == Some(b'}') {
280                self.pos += 1;
281                return Ok(profile);
282            }
283            if self.peek() != Some(b'"') {
284                // Keys that are not objects — skip the entry
285                self.skip_value()?;
286                continue;
287            }
288            let key = self.parse_string()?;
289            self.skip_ws();
290            self.expect(b':')?;
291            self.set_field(&key, &mut profile)?;
292            self.skip_ws();
293            if self.peek() == Some(b',') {
294                self.pos += 1;
295            }
296        }
297    }
298
299    fn set_field(&mut self, key: &str, p: &mut Profile) -> Result<(), DecodeError> {
300        match key {
301            "prefix" => p.prefix = self.parse_number_i32()?,
302            "width" => p.width = self.parse_number_i32()?,
303            "height" => p.height = self.parse_number_i32()?,
304            "encoding" => p.encoding = self.parse_encoding()?,
305            "frame_byte_length" => p.frame_byte_length = self.parse_number_i32()?,
306            "swaps_dimensions" => p.swaps_dimensions = self.parse_bool()?,
307            "little_endian" => p.little_endian = self.parse_bool()?,
308            "is_padded" => p.is_padded = self.parse_bool()?,
309            "is_interlaced" => p.is_interlaced = self.parse_bool()?,
310            "clcl_chroma" => p.clcl_chroma = self.parse_bool()?,
311            "swap_chroma_planes" => p.swap_chroma_planes = self.parse_bool()?,
312            "cl_chroma" => p.cl_chroma = self.parse_bool()?,
313            "swap_rgb_channels" => p.swap_rgb_channels = self.parse_bool()?,
314            "rotation" => p.rotation = self.parse_number_i32()?,
315            "crop_x" => p.crop_x = self.parse_number_i32()?,
316            "crop_y" => p.crop_y = self.parse_number_i32()?,
317            "crop_width" => p.crop_width = self.parse_number_i32()?,
318            "crop_height" => p.crop_height = self.parse_number_i32()?,
319            "slot_size" => p.slot_size = self.parse_number_i32()?,
320            "use_mhni_dimensions" => p.use_mhni_dimensions = self.parse_bool()?,
321            "fallback_encodings" => {
322                if self.peek() == Some(b'n') {
323                    self.parse_null()?;
324                } else {
325                    p.fallback_encodings = Some(self.parse_encoding_array()?);
326                }
327            }
328            // Unknown field – skip its value to stay compatible
329            // with extended schemas.
330            _ => self.skip_value()?,
331        }
332        Ok(())
333    }
334
335    fn parse_encoding(&mut self) -> Result<Encoding, DecodeError> {
336        let s = self.parse_string()?;
337        match s.to_lowercase().as_str() {
338            "rgb565" => Ok(Encoding::Rgb565),
339            "rgb555" => Ok(Encoding::Rgb555),
340            "reorderedrgb555" => Ok(Encoding::ReorderedRgb555),
341            "yuv422" => Ok(Encoding::Yuv422),
342            "ycbcr420" => Ok(Encoding::Ycbcr420),
343            "jpeg" => Ok(Encoding::Jpeg),
344            _ => Err(DecodeError::Profile(format!("unknown encoding: '{s}'"))),
345        }
346    }
347
348    fn parse_encoding_array(&mut self) -> Result<Vec<Encoding>, DecodeError> {
349        self.expect(b'[')?;
350        let mut encodings: Vec<Encoding> = Vec::new();
351        loop {
352            self.skip_ws();
353            if self.peek() == Some(b']') {
354                self.pos += 1;
355                return Ok(encodings);
356            }
357            if !encodings.is_empty() {
358                self.expect(b',')?;
359                self.skip_ws();
360            }
361            encodings.push(self.parse_encoding()?);
362        }
363    }
364}
365
366// ---------------------------------------------------------------------------
367// Tests
368// ---------------------------------------------------------------------------
369
370#[cfg(test)]
371#[allow(clippy::unwrap_used)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn parse_full_profiles_json() {
377        let json = r#"[
378            {"prefix":1007,"width":480,"height":864,"encoding":"Rgb565","frame_byte_length":829440},
379            {"prefix":1019,"width":720,"height":480,"encoding":"Yuv422","frame_byte_length":691200,"is_interlaced":true}
380        ]"#;
381        let profiles = parse_profiles_json(json).unwrap();
382        assert_eq!(profiles.len(), 2);
383        assert_eq!(profiles[0].prefix, 1007);
384        assert_eq!(profiles[0].width, 480);
385        assert_eq!(profiles[0].height, 864);
386        assert_eq!(profiles[0].encoding, Encoding::Rgb565);
387        assert_eq!(profiles[0].frame_byte_length, 829_440);
388        assert!(!profiles[0].is_interlaced);
389
390        assert_eq!(profiles[1].prefix, 1019);
391        assert_eq!(profiles[1].encoding, Encoding::Yuv422);
392        assert!(profiles[1].is_interlaced);
393    }
394
395    #[test]
396    fn parse_with_fallback_encodings() {
397        let json = r#"[
398            {"prefix":1081,"width":640,"height":480,"encoding":"Rgb565","frame_byte_length":614400,"fallback_encodings":["Jpeg"]}
399        ]"#;
400        let profiles = parse_profiles_json(json).unwrap();
401        assert_eq!(profiles.len(), 1);
402        let fb = profiles[0].fallback_encodings.as_ref().unwrap();
403        assert_eq!(fb.len(), 1);
404        assert_eq!(fb[0], Encoding::Jpeg);
405    }
406
407    #[test]
408    fn parse_all_54_profiles() {
409        let json = include_str!("../data/profiles.json");
410        let profiles = parse_profiles_json(json).unwrap();
411        assert_eq!(profiles.len(), 54);
412        // Spot-check a few entries
413        let p1007 = profiles.iter().find(|p| p.prefix == 1007).unwrap();
414        assert_eq!(p1007.width, 480);
415        assert_eq!(p1007.height, 864);
416        assert_eq!(p1007.encoding, Encoding::Rgb565);
417
418        let p3004 = profiles.iter().find(|p| p.prefix == 3004).unwrap();
419        assert!(p3004.is_padded);
420        assert_eq!(p3004.slot_size, 8192);
421        assert_eq!(p3004.encoding, Encoding::Rgb555);
422
423        let p1042 = profiles.iter().find(|p| p.prefix == 1042).unwrap();
424        assert_eq!(p1042.width, 320);
425
426        // Fallback encodings
427        let p1081 = profiles.iter().find(|p| p.prefix == 1081).unwrap();
428        let fb = p1081.fallback_encodings.as_ref().unwrap();
429        assert_eq!(fb.as_slice(), &[Encoding::Jpeg]);
430
431        // Big-endian profiles
432        let p2002 = profiles.iter().find(|p| p.prefix == 2002).unwrap();
433        assert!(!p2002.little_endian);
434    }
435
436    #[test]
437    fn parse_error_on_bad_json() {
438        let result = parse_profiles_json("not json at all");
439        assert!(result.is_err());
440    }
441}