Skip to main content

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