rakata_formats/ltr/
reader.rs

1//! LTR binary reader.
2
3use std::io::Read;
4
5use crate::binary;
6
7use super::{
8    Ltr, LtrBinaryError, LtrProbabilityBlock, EXPECTED_FILE_SIZE, FILE_HEADER_SIZE,
9    FLOAT_SIZE_BYTES, LTR_CHARACTER_COUNT, LTR_MAGIC, LTR_VERSION_V10,
10};
11
12/// Reads an LTR file from a reader.
13///
14/// The stream is consumed from its current position.
15#[cfg_attr(
16    feature = "tracing",
17    tracing::instrument(level = "debug", skip(reader))
18)]
19pub fn read_ltr<R: Read>(reader: &mut R) -> Result<Ltr, LtrBinaryError> {
20    let mut bytes = Vec::new();
21    reader.read_to_end(&mut bytes)?;
22    read_ltr_from_bytes(&bytes)
23}
24
25/// Reads an LTR file directly from bytes.
26#[cfg_attr(
27    feature = "tracing",
28    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
29)]
30pub fn read_ltr_from_bytes(bytes: &[u8]) -> Result<Ltr, LtrBinaryError> {
31    if bytes.len() < FILE_HEADER_SIZE {
32        return Err(LtrBinaryError::InvalidHeader(
33            "file smaller than LTR header".into(),
34        ));
35    }
36
37    let magic = read_fourcc(bytes, 0)?;
38    if magic != LTR_MAGIC {
39        return Err(LtrBinaryError::InvalidMagic(magic));
40    }
41
42    let version = read_fourcc(bytes, 4)?;
43    if version != LTR_VERSION_V10 {
44        return Err(LtrBinaryError::InvalidVersion(version));
45    }
46
47    let letter_count = bytes[8];
48    if usize::from(letter_count) != LTR_CHARACTER_COUNT {
49        return Err(LtrBinaryError::UnsupportedLetterCount(letter_count));
50    }
51
52    if bytes.len() < EXPECTED_FILE_SIZE {
53        return Err(LtrBinaryError::InvalidHeader(format!(
54            "file smaller than expected LTR payload: expected at least {EXPECTED_FILE_SIZE} bytes, got {}",
55            bytes.len()
56        )));
57    }
58
59    let mut offset = FILE_HEADER_SIZE;
60    let mut ltr = Ltr::new();
61
62    ltr.singles = read_block(bytes, &mut offset)?;
63    for block in ltr.doubles.iter_mut() {
64        *block = read_block(bytes, &mut offset)?;
65    }
66    for row in ltr.triples.iter_mut() {
67        for block in row.iter_mut() {
68            *block = read_block(bytes, &mut offset)?;
69        }
70    }
71
72    Ok(ltr)
73}
74
75fn read_fourcc(bytes: &[u8], offset: usize) -> Result<[u8; 4], LtrBinaryError> {
76    binary::read_fourcc(bytes, offset).map_err(|err| LtrBinaryError::InvalidHeader(err.to_string()))
77}
78
79fn read_next_f32(bytes: &[u8], offset: &mut usize) -> Result<f32, LtrBinaryError> {
80    let value = binary::read_f32(bytes, *offset)
81        .map_err(|err| LtrBinaryError::InvalidData(err.to_string()))?;
82    *offset = offset
83        .checked_add(FLOAT_SIZE_BYTES)
84        .ok_or_else(|| LtrBinaryError::InvalidData("float offset overflow".into()))?;
85    Ok(value)
86}
87
88fn read_block(bytes: &[u8], offset: &mut usize) -> Result<LtrProbabilityBlock, LtrBinaryError> {
89    let mut block = LtrProbabilityBlock::new();
90
91    for chance in &mut block.start {
92        *chance = read_next_f32(bytes, offset)?;
93    }
94    for chance in &mut block.middle {
95        *chance = read_next_f32(bytes, offset)?;
96    }
97    for chance in &mut block.end {
98        *chance = read_next_f32(bytes, offset)?;
99    }
100
101    Ok(block)
102}
103
104#[cfg(test)]
105mod tests {
106    use std::io::Cursor;
107
108    use super::*;
109    use crate::ltr::{write_ltr, write_ltr_to_vec};
110
111    // TODO(ltr-fixture-parity): Add fixture-driven LTR parity tests
112    fn sample_ltr() -> Ltr {
113        let mut ltr = Ltr::new();
114        ltr.singles.start[0] = 0.95;
115        ltr.singles.middle[1] = 0.80;
116        ltr.singles.end[2] = 0.40;
117        ltr.doubles[3].start[4] = 0.65;
118        ltr.doubles[5].middle[6] = 0.50;
119        ltr.doubles[7].end[8] = 0.35;
120        ltr.triples[9][10].start[11] = 0.70;
121        ltr.triples[12][13].middle[14] = 0.55;
122        ltr.triples[15][16].end[17] = 0.45;
123        ltr
124    }
125
126    #[test]
127    fn roundtrip_synthetic_ltr() {
128        let ltr = sample_ltr();
129        let bytes = write_ltr_to_vec(&ltr).expect("write should succeed");
130        let parsed = read_ltr_from_bytes(&bytes).expect("read should succeed");
131        assert_eq!(parsed, ltr);
132    }
133
134    #[test]
135    fn writer_is_deterministic_for_synthetic_ltr() {
136        let ltr = sample_ltr();
137        let first = write_ltr_to_vec(&ltr).expect("first write should succeed");
138        let second = write_ltr_to_vec(&ltr).expect("second write should succeed");
139        assert_eq!(first, second);
140    }
141
142    #[test]
143    fn read_write_roundtrip_via_io_traits() {
144        let ltr = sample_ltr();
145        let mut out = Vec::new();
146        write_ltr(&mut out, &ltr).expect("write should succeed");
147        let parsed = read_ltr(&mut Cursor::new(out)).expect("read should succeed");
148        assert_eq!(parsed, ltr);
149    }
150
151    #[test]
152    fn writer_emits_expected_ltr_size() {
153        let ltr = Ltr::new();
154        let bytes = write_ltr_to_vec(&ltr).expect("write should succeed");
155        assert_eq!(bytes.len(), EXPECTED_FILE_SIZE);
156    }
157
158    #[test]
159    fn rejects_invalid_magic() {
160        let mut bytes = write_ltr_to_vec(&Ltr::new()).expect("write should succeed");
161        bytes[0] = b'X';
162        let err = read_ltr_from_bytes(&bytes).expect_err("must fail");
163        assert!(matches!(err, LtrBinaryError::InvalidMagic(_)));
164    }
165
166    #[test]
167    fn rejects_invalid_version() {
168        let mut bytes = write_ltr_to_vec(&Ltr::new()).expect("write should succeed");
169        bytes[4] = b'X';
170        let err = read_ltr_from_bytes(&bytes).expect_err("must fail");
171        assert!(matches!(err, LtrBinaryError::InvalidVersion(_)));
172    }
173
174    #[test]
175    fn rejects_unsupported_letter_count() {
176        let mut bytes = write_ltr_to_vec(&Ltr::new()).expect("write should succeed");
177        bytes[8] = 26;
178        let err = read_ltr_from_bytes(&bytes).expect_err("must fail");
179        assert!(matches!(err, LtrBinaryError::UnsupportedLetterCount(26)));
180    }
181
182    #[test]
183    fn rejects_truncated_payload() {
184        let mut bytes = write_ltr_to_vec(&Ltr::new()).expect("write should succeed");
185        bytes.truncate(bytes.len() - 1);
186        let err = read_ltr_from_bytes(&bytes).expect_err("must fail");
187        assert!(matches!(err, LtrBinaryError::InvalidHeader(_)));
188    }
189
190    #[test]
191    fn rejects_truncated_header() {
192        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
193        let err = read_ltr_from_bytes(&bytes).expect_err("must fail");
194        assert!(matches!(err, LtrBinaryError::InvalidHeader(_)));
195    }
196}