Skip to main content

rakata_formats/tlk/
reader.rs

1//! TLK binary reader.
2
3use std::io::Read;
4
5use rakata_core::{
6    decode_text_strict, text_encoding_for_language, LanguageId, ResRef, TextEncoding,
7};
8
9use crate::binary;
10
11use super::{
12    Tlk, TlkBinaryError, TlkEntry, ENTRY_SIZE, FILE_HEADER_SIZE, TLK_MAGIC, TLK_VERSION_V3,
13};
14
15/// Reads a TLK from a reader.
16///
17/// The stream is consumed from its current position.
18///
19/// # Errors
20///
21/// [`TlkBinaryError::Io`] when the stream will not read to end, and whatever
22/// [`read_tlk_from_bytes`] reports for the bytes it collected.
23#[cfg_attr(
24    feature = "tracing",
25    tracing::instrument(level = "debug", skip(reader))
26)]
27pub fn read_tlk<R: Read>(reader: &mut R) -> Result<Tlk, TlkBinaryError> {
28    let mut bytes = Vec::new();
29    reader.read_to_end(&mut bytes)?;
30    read_tlk_from_bytes(&bytes)
31}
32
33/// Reads a TLK from in-memory bytes.
34///
35/// # Errors
36///
37/// [`TlkBinaryError::InvalidMagic`] when the signature is not `TLK `, and
38/// [`TlkBinaryError::InvalidVersion`] for a version other than `V3.0`.
39///
40/// [`TlkBinaryError::InvalidHeader`] when the header is truncated or an
41/// entry's string offset and length run past the end of `bytes`.
42/// [`TlkBinaryError::UnsupportedLanguageEncoding`] when the header's language
43/// id maps to no codepage, and [`TlkBinaryError::TextDecoding`] when an entry's
44/// text is not valid in that codepage.
45///
46/// [`TlkBinaryError::InvalidSoundResRef`] for an entry's sound resref that is
47/// not a valid one. The language id is read from the header, so one bad id
48/// fails the whole table rather than a single entry.
49#[cfg_attr(
50    feature = "tracing",
51    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
52)]
53pub fn read_tlk_from_bytes(bytes: &[u8]) -> Result<Tlk, TlkBinaryError> {
54    if bytes.len() < FILE_HEADER_SIZE {
55        return Err(TlkBinaryError::InvalidHeader(
56            "file smaller than TLK header".into(),
57        ));
58    }
59
60    let magic = binary::read_fourcc(bytes, 0)?;
61    binary::expect_fourcc(magic, TLK_MAGIC).map_err(TlkBinaryError::InvalidMagic)?;
62
63    let version = binary::read_fourcc(bytes, 4)?;
64    binary::expect_fourcc(version, TLK_VERSION_V3).map_err(TlkBinaryError::InvalidVersion)?;
65
66    let language_id = LanguageId::from_raw(binary::read_u32(bytes, 8)?);
67    let text_encoding = text_encoding_for_language(language_id)
68        .map_err(|err| TlkBinaryError::UnsupportedLanguageEncoding(err.language_id.raw()))?;
69    let entry_count = usize::try_from(binary::read_u32(bytes, 12)?)
70        .map_err(|_| TlkBinaryError::InvalidHeader("entry count does not fit usize".into()))?;
71    let entries_offset = usize::try_from(binary::read_u32(bytes, 16)?)
72        .map_err(|_| TlkBinaryError::InvalidHeader("entries offset does not fit usize".into()))?;
73
74    let entries_table_end = FILE_HEADER_SIZE
75        .checked_add(
76            entry_count
77                .checked_mul(ENTRY_SIZE)
78                .ok_or_else(|| TlkBinaryError::InvalidHeader("entry table size overflow".into()))?,
79        )
80        .ok_or_else(|| TlkBinaryError::InvalidHeader("entry table end overflow".into()))?;
81
82    if entries_offset < entries_table_end {
83        return Err(TlkBinaryError::InvalidHeader(
84            "string data offset overlaps entry headers".into(),
85        ));
86    }
87    if entries_offset > bytes.len() {
88        return Err(TlkBinaryError::InvalidHeader(
89            "string data offset beyond file size".into(),
90        ));
91    }
92
93    let mut tlk = Tlk::new(language_id);
94    tlk.entries.reserve(entry_count);
95
96    for index in 0..entry_count {
97        let base = FILE_HEADER_SIZE + index * ENTRY_SIZE;
98        let flags = binary::read_u32(bytes, base)?;
99
100        let sound_bytes = &bytes[base + 4..base + 20];
101        let sound_end = sound_bytes
102            .iter()
103            .position(|byte| *byte == 0)
104            .unwrap_or(sound_bytes.len());
105        let sound_name = decode_text_strict(&sound_bytes[..sound_end], TextEncoding::Windows1252)
106            .map_err(|source| TlkBinaryError::TextDecoding {
107            entry_index: index,
108            source,
109        })?;
110        let voiceover =
111            ResRef::new(&sound_name).map_err(|source| TlkBinaryError::InvalidSoundResRef {
112                entry_index: index,
113                value: sound_name,
114                source,
115            })?;
116
117        let volume_var = binary::read_u32(bytes, base + 20)?;
118        let pitch_var = binary::read_u32(bytes, base + 24)?;
119        let text_offset = usize::try_from(binary::read_u32(bytes, base + 28)?)
120            .map_err(|_| TlkBinaryError::InvalidHeader("text offset does not fit usize".into()))?;
121        let text_length = usize::try_from(binary::read_u32(bytes, base + 32)?)
122            .map_err(|_| TlkBinaryError::InvalidHeader("text length does not fit usize".into()))?;
123        let sound_length = binary::read_f32(bytes, base + 36)?;
124
125        let text_start = entries_offset
126            .checked_add(text_offset)
127            .ok_or_else(|| TlkBinaryError::InvalidHeader("text start overflow".into()))?;
128        let text_end = text_start
129            .checked_add(text_length)
130            .ok_or_else(|| TlkBinaryError::InvalidHeader("text end overflow".into()))?;
131        if text_end > bytes.len() {
132            return Err(TlkBinaryError::InvalidHeader(format!(
133                "text for entry {index} exceeds file length"
134            )));
135        }
136
137        let text =
138            decode_text_strict(&bytes[text_start..text_end], text_encoding).map_err(|source| {
139                TlkBinaryError::TextDecoding {
140                    entry_index: index,
141                    source,
142                }
143            })?;
144        tlk.entries.push(TlkEntry {
145            text,
146            voiceover,
147            text_present: (flags & 0x0001) != 0,
148            sound_present: (flags & 0x0002) != 0,
149            sound_length_present: (flags & 0x0004) != 0,
150            sound_length,
151            volume_var,
152            pitch_var,
153        });
154    }
155
156    Ok(tlk)
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use crate::tlk::write_tlk_to_vec;
163
164    #[test]
165    fn roundtrip_tlk_binary() {
166        let mut tlk = Tlk::new(0);
167        let mut first = TlkEntry::new(
168            "Hello \u{20ac} world",
169            ResRef::new("n_gendro").expect("valid"),
170        );
171        first.sound_length_present = true;
172        first.sound_length = 1.5;
173        tlk.entries.push(first);
174        tlk.entries.push(TlkEntry::new("Second", ResRef::blank()));
175
176        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
177        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
178
179        assert_eq!(parsed.language_id, LanguageId::from_raw(0));
180        assert_eq!(parsed.entries.len(), 2);
181        assert_eq!(parsed.entries[0].text, "Hello \u{20ac} world");
182        assert_eq!(parsed.entries[0].voiceover.as_bytes(), b"n_gendro");
183        assert!(parsed.entries[0].sound_length_present);
184        assert!((parsed.entries[0].sound_length - 1.5).abs() < f32::EPSILON);
185    }
186
187    #[test]
188    fn writer_is_deterministic_for_synthetic_tlk() {
189        let mut tlk = Tlk::new(0);
190        let mut first = TlkEntry::new(
191            "Hello \u{20ac} world",
192            ResRef::new("n_gendro").expect("valid"),
193        );
194        first.sound_length_present = true;
195        first.sound_length = 1.5;
196        tlk.entries.push(first);
197        tlk.entries.push(TlkEntry::new("Second", ResRef::blank()));
198
199        let first = write_tlk_to_vec(&tlk).expect("first write should succeed");
200        let second = write_tlk_to_vec(&tlk).expect("second write should succeed");
201        assert_eq!(first, second, "canonical TLK writer output drifted");
202    }
203
204    #[test]
205    fn rejects_truncated_header() {
206        let bytes = vec![0_u8; FILE_HEADER_SIZE - 1];
207        let err = read_tlk_from_bytes(&bytes).expect_err("must fail");
208        assert!(matches!(err, TlkBinaryError::InvalidHeader(_)));
209    }
210
211    #[test]
212    fn rejects_invalid_magic() {
213        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
214        bytes[0..4].copy_from_slice(b"NOPE");
215        bytes[4..8].copy_from_slice(&TLK_VERSION_V3);
216        let err = read_tlk_from_bytes(&bytes).expect_err("must fail");
217        assert!(matches!(err, TlkBinaryError::InvalidMagic(_)));
218    }
219
220    #[test]
221    fn writer_normalizes_inconsistent_entry_flags() {
222        let mut tlk = Tlk::new(0);
223        let mut inconsistent = TlkEntry::new("Line", ResRef::blank());
224        inconsistent.text_present = false;
225        inconsistent.sound_present = true;
226        inconsistent.sound_length_present = true;
227        inconsistent.sound_length = 2.0;
228        tlk.entries.push(inconsistent);
229
230        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
231        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
232        let entry = &parsed.entries[0];
233        assert!(entry.text_present);
234        assert!(!entry.sound_present);
235        assert!(!entry.sound_length_present);
236        assert_eq!(entry.sound_length, 0.0);
237    }
238
239    #[test]
240    fn writer_rejects_unencodable_text() {
241        let mut tlk = Tlk::new(0);
242        tlk.entries
243            .push(TlkEntry::new("Unsupported \u{1f600}", ResRef::blank()));
244
245        let err = write_tlk_to_vec(&tlk).expect_err("write must fail");
246        match err {
247            TlkBinaryError::TextEncoding {
248                entry_index,
249                source,
250            } => {
251                assert_eq!(entry_index, 0);
252                assert_eq!(source.character, '\u{1f600}');
253            }
254            other => panic!("unexpected error variant: {other}"),
255        }
256    }
257
258    #[test]
259    fn roundtrip_tlk_binary_polish_cp1250() {
260        let mut tlk = Tlk::new(5);
261        tlk.entries.push(TlkEntry::new(
262            "Za\u{17c}\u{f3}\u{142}\u{107} g\u{119}\u{15b}l\u{105} ja\u{17a}\u{144}",
263            ResRef::blank(),
264        ));
265
266        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
267        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
268        assert_eq!(parsed.language_id, LanguageId::from_raw(5));
269        assert_eq!(
270            parsed.entries[0].text,
271            "Za\u{17c}\u{f3}\u{142}\u{107} g\u{119}\u{15b}l\u{105} ja\u{17a}\u{144}"
272        );
273    }
274
275    #[test]
276    fn writer_rejects_unsupported_language_encoding() {
277        let mut tlk = Tlk::new(70);
278        tlk.entries.push(TlkEntry::new("abc", ResRef::blank()));
279
280        let err = write_tlk_to_vec(&tlk).expect_err("write must fail");
281        assert!(matches!(
282            err,
283            TlkBinaryError::UnsupportedLanguageEncoding(70)
284        ));
285    }
286
287    #[test]
288    fn roundtrip_tlk_binary_russian_cp1251() {
289        let mut tlk = Tlk::new(41);
290        tlk.entries.push(TlkEntry::new(
291            "\u{41f}\u{440}\u{438}\u{432}\u{435}\u{442}",
292            ResRef::blank(),
293        ));
294
295        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
296        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
297        assert_eq!(parsed.language_id, LanguageId::from_raw(41));
298        assert_eq!(
299            parsed.entries[0].text,
300            "\u{41f}\u{440}\u{438}\u{432}\u{435}\u{442}"
301        );
302    }
303
304    #[test]
305    fn roundtrip_tlk_binary_japanese_shift_jis() {
306        let mut tlk = Tlk::new(131);
307        tlk.entries
308            .push(TlkEntry::new("\u{30c6}\u{30b9}\u{30c8}", ResRef::blank()));
309
310        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
311        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
312        assert_eq!(parsed.language_id, LanguageId::from_raw(131));
313        assert_eq!(parsed.entries[0].text, "\u{30c6}\u{30b9}\u{30c8}");
314    }
315
316    #[test]
317    fn reader_rejects_unsupported_language_encoding() {
318        let mut bytes = vec![0_u8; FILE_HEADER_SIZE];
319        bytes[0..4].copy_from_slice(&TLK_MAGIC);
320        bytes[4..8].copy_from_slice(&TLK_VERSION_V3);
321        bytes[8..12].copy_from_slice(&70_u32.to_le_bytes());
322        bytes[12..16].copy_from_slice(&0_u32.to_le_bytes());
323        bytes[16..20].copy_from_slice(
324            &u32::try_from(FILE_HEADER_SIZE)
325                .expect("FILE_HEADER_SIZE fits in u32")
326                .to_le_bytes(),
327        );
328
329        let err = read_tlk_from_bytes(&bytes).expect_err("read must fail");
330        assert!(matches!(
331            err,
332            TlkBinaryError::UnsupportedLanguageEncoding(70)
333        ));
334    }
335
336    #[test]
337    fn reader_rejects_malformed_multibyte_text() {
338        let mut tlk = Tlk::new(131);
339        tlk.entries.push(TlkEntry::new("ok", ResRef::blank()));
340
341        let mut bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
342        let entry_base = FILE_HEADER_SIZE;
343        let text_offset = usize::try_from(u32::from_le_bytes(
344            bytes[entry_base + 28..entry_base + 32]
345                .try_into()
346                .expect("offset bytes"),
347        ))
348        .expect("offset fits");
349        let blob_start = FILE_HEADER_SIZE + ENTRY_SIZE;
350        bytes[blob_start + text_offset] = 0x81;
351        bytes[entry_base + 32..entry_base + 36].copy_from_slice(&1_u32.to_le_bytes());
352
353        let err = read_tlk_from_bytes(&bytes).expect_err("read must fail");
354        assert!(matches!(err, TlkBinaryError::TextDecoding { .. }));
355    }
356
357    #[test]
358    fn volume_var_and_pitch_var_survive_roundtrip() {
359        let mut tlk = Tlk::new(0);
360        let mut entry = TlkEntry::new("test", ResRef::blank());
361        entry.volume_var = 0xDEADBEEF;
362        entry.pitch_var = 0x12345678;
363        tlk.entries.push(entry);
364
365        let bytes = write_tlk_to_vec(&tlk).expect("write should succeed");
366        let parsed = read_tlk_from_bytes(&bytes).expect("read should succeed");
367
368        assert_eq!(parsed.entries[0].volume_var, 0xDEADBEEF);
369        assert_eq!(parsed.entries[0].pitch_var, 0x12345678);
370    }
371}