1use 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#[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#[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}