1use std::io::Read;
4
5use rakata_core::decode_text_strict;
6
7use super::{
8 binary, TwoDa, TwoDaBinaryError, TwoDaBinaryOptions, TwoDaRow, TWODA_MAGIC, TWODA_VERSION_V2B,
9};
10
11#[cfg_attr(
20 feature = "tracing",
21 tracing::instrument(level = "debug", skip(reader))
22)]
23pub fn read_twoda<R: Read>(reader: &mut R) -> Result<TwoDa, TwoDaBinaryError> {
24 read_twoda_with_options(reader, TwoDaBinaryOptions::default())
25}
26
27#[cfg_attr(
36 feature = "tracing",
37 tracing::instrument(level = "debug", skip(reader, options))
38)]
39pub fn read_twoda_with_options<R: Read>(
40 reader: &mut R,
41 options: TwoDaBinaryOptions,
42) -> Result<TwoDa, TwoDaBinaryError> {
43 let mut bytes = Vec::new();
44 reader.read_to_end(&mut bytes)?;
45 read_twoda_from_bytes_with_options(&bytes, options)
46}
47
48#[cfg_attr(
55 feature = "tracing",
56 tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
57)]
58pub fn read_twoda_from_bytes(bytes: &[u8]) -> Result<TwoDa, TwoDaBinaryError> {
59 read_twoda_from_bytes_with_options(bytes, TwoDaBinaryOptions::default())
60}
61
62#[cfg_attr(
79 feature = "tracing",
80 tracing::instrument(level = "debug", skip(bytes, options), fields(bytes_len = bytes.len()))
81)]
82pub fn read_twoda_from_bytes_with_options(
83 bytes: &[u8],
84 options: TwoDaBinaryOptions,
85) -> Result<TwoDa, TwoDaBinaryError> {
86 if bytes.len() < 9 {
87 return Err(TwoDaBinaryError::InvalidHeader(
88 "file smaller than 2DA header".into(),
89 ));
90 }
91
92 let magic = binary::read_fourcc(bytes, 0)?;
93 if magic != TWODA_MAGIC {
94 return Err(TwoDaBinaryError::InvalidMagic(magic));
95 }
96
97 let version = binary::read_fourcc(bytes, 4)?;
98 if version != TWODA_VERSION_V2B {
99 return Err(TwoDaBinaryError::InvalidVersion(version));
100 }
101
102 if bytes[8] != b'\n' {
103 return Err(TwoDaBinaryError::InvalidHeader(
104 "missing newline after 2DA version".into(),
105 ));
106 }
107
108 let mut cursor = 9;
109 let headers_end = find_byte(bytes, cursor, b'\0')
110 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("missing header terminator".into()))?;
111 let mut headers = split_tabbed(&bytes[cursor..headers_end])
112 .into_iter()
113 .enumerate()
114 .map(|(index, token)| {
115 decode_text_strict(token, options.text_encoding).map_err(|source| {
116 TwoDaBinaryError::TextDecoding {
117 context: format!("header[{index}]"),
118 source,
119 }
120 })
121 })
122 .collect::<Result<Vec<_>, _>>()?;
123 if headers.last().is_some_and(String::is_empty) {
124 headers.pop();
125 }
126 cursor = headers_end + 1;
127
128 let row_count = usize::try_from(binary::read_u32(bytes, cursor)?)
129 .map_err(|_| TwoDaBinaryError::InvalidHeader("row count does not fit usize".into()))?;
130 cursor = cursor
131 .checked_add(4)
132 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("row count cursor overflow".into()))?;
133
134 let mut rows = Vec::with_capacity(row_count);
135 for row_index in 0..row_count {
136 let label_end = find_byte(bytes, cursor, b'\t').ok_or_else(|| {
137 TwoDaBinaryError::InvalidHeader(format!(
138 "missing tab terminator for row label {row_index}"
139 ))
140 })?;
141 let label = decode_text_strict(&bytes[cursor..label_end], options.text_encoding).map_err(
142 |source| TwoDaBinaryError::TextDecoding {
143 context: format!("row label[{row_index}]"),
144 source,
145 },
146 )?;
147 rows.push(TwoDaRow {
148 label,
149 cells: vec![String::new(); headers.len()],
150 });
151 cursor = label_end
152 .checked_add(1)
153 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("row label cursor overflow".into()))?;
154 }
155
156 let cell_count = row_count
157 .checked_mul(headers.len())
158 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("cell count overflow".into()))?;
159 let offsets_bytes = cell_count
160 .checked_mul(2)
161 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("offset table size overflow".into()))?;
162 if cursor
163 .checked_add(offsets_bytes + 2)
164 .is_none_or(|end| end > bytes.len())
165 {
166 return Err(TwoDaBinaryError::InvalidHeader(
167 "offset table exceeds file length".into(),
168 ));
169 }
170
171 let mut cell_offsets = Vec::with_capacity(cell_count);
172 for _ in 0..cell_count {
173 cell_offsets.push(binary::read_u16(bytes, cursor)?);
174 cursor = cursor
175 .checked_add(2)
176 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("offset cursor overflow".into()))?;
177 }
178
179 let cell_data_size = usize::from(binary::read_u16(bytes, cursor)?);
180 cursor = cursor
181 .checked_add(2)
182 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("cell data cursor overflow".into()))?;
183 let cell_data_end = cursor
184 .checked_add(cell_data_size)
185 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("cell data end overflow".into()))?;
186 if cell_data_end > bytes.len() {
187 return Err(TwoDaBinaryError::InvalidHeader(
188 "cell string table exceeds file length".into(),
189 ));
190 }
191 let cell_data = &bytes[cursor..cell_data_end];
192
193 if !headers.is_empty() {
194 for (cell_index, offset) in cell_offsets.iter().enumerate() {
195 let offset = usize::from(*offset);
196 if offset >= cell_data_size {
197 return Err(TwoDaBinaryError::InvalidHeader(format!(
198 "cell offset {offset} out of range for cell index {cell_index}"
199 )));
200 }
201
202 let value_end = find_byte(cell_data, offset, b'\0').ok_or_else(|| {
203 TwoDaBinaryError::InvalidHeader(format!(
204 "missing NUL terminator for cell index {cell_index}"
205 ))
206 })?;
207 let row_index = cell_index / headers.len();
208 let column_index = cell_index % headers.len();
209 let value = decode_text_strict(&cell_data[offset..value_end], options.text_encoding)
210 .map_err(|source| TwoDaBinaryError::TextDecoding {
211 context: format!("cell[{row_index}][{column_index}]"),
212 source,
213 })?;
214 rows[row_index].cells[column_index] = value;
215 }
216 }
217
218 Ok(TwoDa { headers, rows })
219}
220
221fn find_byte(bytes: &[u8], start: usize, needle: u8) -> Option<usize> {
222 bytes
223 .get(start..)?
224 .iter()
225 .position(|byte| *byte == needle)
226 .map(|relative| start + relative)
227}
228
229fn split_tabbed(bytes: &[u8]) -> Vec<&[u8]> {
230 if bytes.is_empty() {
231 return Vec::new();
232 }
233 let mut out = Vec::new();
234 let mut start = 0;
235 for (index, byte) in bytes.iter().enumerate() {
236 if *byte == b'\t' {
237 out.push(&bytes[start..index]);
238 start = index + 1;
239 }
240 }
241 out.push(&bytes[start..]);
242 out
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::twoda::{
249 write_twoda_to_vec, write_twoda_to_vec_with_options, TwoDaBinaryOptions, TWODA_MAGIC,
250 TWODA_VERSION_V2B,
251 };
252 use rakata_core::TextEncoding;
253
254 #[test]
255 fn roundtrip_twoda_binary() {
256 let mut table = TwoDa::new(vec!["col3".into(), "col2".into(), "col1".into()]);
257 table
258 .push_row("10", vec!["ghi".into(), "def".into(), "abc".into()])
259 .expect("valid row");
260 table
261 .push_row("1", vec!["123".into(), "ghi".into(), "def".into()])
262 .expect("valid row");
263 table
264 .push_row("2", vec!["abc".into(), "".into(), "123".into()])
265 .expect("valid row");
266
267 let bytes = write_twoda_to_vec(&table).expect("write should succeed");
268 let parsed = read_twoda_from_bytes(&bytes).expect("read should succeed");
269
270 assert_eq!(parsed, table);
271 assert_eq!(parsed.cell(0, "col1"), Some("abc"));
272 assert_eq!(parsed.cell(2, "col2"), Some(""));
273 }
274
275 #[test]
276 fn writer_is_deterministic_for_synthetic_twoda() {
277 let mut table = TwoDa::new(vec!["col3".into(), "col2".into(), "col1".into()]);
278 table
279 .push_row("10", vec!["ghi".into(), "def".into(), "abc".into()])
280 .expect("valid row");
281 table
282 .push_row("1", vec!["123".into(), "ghi".into(), "def".into()])
283 .expect("valid row");
284 table
285 .push_row("2", vec!["abc".into(), "".into(), "123".into()])
286 .expect("valid row");
287
288 let first = write_twoda_to_vec(&table).expect("first write should succeed");
289 let second = write_twoda_to_vec(&table).expect("second write should succeed");
290 assert_eq!(first, second, "canonical 2DA writer output drifted");
291 }
292
293 #[test]
294 fn rejects_truncated_header() {
295 let bytes = vec![0_u8; 8];
296 let err = read_twoda_from_bytes(&bytes).expect_err("must fail");
297 assert!(matches!(err, TwoDaBinaryError::InvalidHeader(_)));
298 }
299
300 #[test]
301 fn rejects_invalid_magic() {
302 let mut bytes = vec![0_u8; 9];
303 bytes[0..4].copy_from_slice(b"BAD!");
304 bytes[4..8].copy_from_slice(&TWODA_VERSION_V2B);
305 bytes[8] = b'\n';
306
307 let err = read_twoda_from_bytes(&bytes).expect_err("must fail");
308 assert!(matches!(err, TwoDaBinaryError::InvalidMagic(_)));
309 }
310
311 #[test]
312 fn rejects_invalid_version() {
313 let mut bytes = vec![0_u8; 9];
314 bytes[0..4].copy_from_slice(&TWODA_MAGIC);
315 bytes[4..8].copy_from_slice(b"V2.0");
316 bytes[8] = b'\n';
317
318 let err = read_twoda_from_bytes(&bytes).expect_err("must fail");
319 assert!(matches!(err, TwoDaBinaryError::InvalidVersion(_)));
320 }
321
322 #[test]
323 fn writer_rejects_unencodable_text() {
324 let mut table = TwoDa::new(vec!["col".into()]);
325 table
326 .push_row("0", vec!["emoji \u{1f600}".into()])
327 .expect("valid table shape");
328
329 let err = write_twoda_to_vec(&table).expect_err("must fail");
330 match err {
331 TwoDaBinaryError::TextEncoding { context, source } => {
332 assert!(context.contains("cell[0][0]"));
333 assert_eq!(source.character, '\u{1f600}');
334 }
335 other => panic!("unexpected error variant: {other}"),
336 }
337 }
338
339 #[test]
340 fn writer_rejects_row_width_mismatch() {
341 let mut table = TwoDa::new(vec!["col1".into(), "col2".into()]);
342 table.rows.push(TwoDaRow {
343 label: "0".into(),
344 cells: vec!["only_one".into()],
345 });
346
347 let err = write_twoda_to_vec(&table).expect_err("must fail");
348 assert!(matches!(err, TwoDaBinaryError::InvalidTable(_)));
349 }
350
351 #[test]
352 fn writer_deduplicates_identical_cell_strings() {
353 let mut table = TwoDa::new(vec!["a".into(), "b".into()]);
354 table
355 .push_row("0", vec!["same".into(), "x".into()])
356 .expect("valid row");
357 table
358 .push_row("1", vec!["same".into(), "y".into()])
359 .expect("valid row");
360
361 let bytes = write_twoda_to_vec(&table).expect("write should succeed");
362 let offsets = extract_cell_offsets(&bytes).expect("must parse offsets");
363 assert_eq!(offsets.len(), 4);
364 assert_eq!(offsets[0], offsets[2]);
365 }
366
367 #[test]
368 fn configurable_encoding_supports_cp1250_data() {
369 let mut table = TwoDa::new(vec!["text".into()]);
370 table
371 .push_row("0", vec!["Za\u{017c}\u{00f3}\u{0142}\u{0107} g\u{0119}\u{015b}l\u{0105} ja\u{017a}\u{0144}".into()])
372 .expect("valid row");
373 let options = TwoDaBinaryOptions {
374 text_encoding: TextEncoding::Windows1250,
375 };
376
377 let bytes = write_twoda_to_vec_with_options(&table, options).expect("write should succeed");
378 let parsed =
379 read_twoda_from_bytes_with_options(&bytes, options).expect("read should succeed");
380 assert_eq!(
381 parsed.cell(0, "text"),
382 Some(
383 "Za\u{017c}\u{00f3}\u{0142}\u{0107} g\u{0119}\u{015b}l\u{0105} ja\u{017a}\u{0144}"
384 )
385 );
386 }
387
388 #[test]
389 fn default_encoding_rejects_cp1250_specific_characters() {
390 let mut table = TwoDa::new(vec!["text".into()]);
391 table
392 .push_row("0", vec!["Za\u{017c}\u{00f3}\u{0142}\u{0107} g\u{0119}\u{015b}l\u{0105} ja\u{017a}\u{0144}".into()])
393 .expect("valid row");
394
395 let err = write_twoda_to_vec(&table).expect_err("must fail");
396 assert!(matches!(err, TwoDaBinaryError::TextEncoding { .. }));
397 }
398
399 fn extract_cell_offsets(bytes: &[u8]) -> Result<Vec<u16>, TwoDaBinaryError> {
400 let mut cursor = 9;
401 let headers_end = find_byte(bytes, cursor, b'\0')
402 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("missing header terminator".into()))?;
403 cursor = headers_end + 1;
404
405 let row_count = usize::try_from(binary::read_u32(bytes, cursor)?)
406 .map_err(|_| TwoDaBinaryError::InvalidHeader("row count conversion failed".into()))?;
407 cursor += 4;
408 for _ in 0..row_count {
409 let label_end = find_byte(bytes, cursor, b'\t')
410 .ok_or_else(|| TwoDaBinaryError::InvalidHeader("missing row label tab".into()))?;
411 cursor = label_end + 1;
412 }
413
414 let cell_count = row_count * 2;
416 let mut offsets = Vec::with_capacity(cell_count);
417 for _ in 0..cell_count {
418 offsets.push(binary::read_u16(bytes, cursor)?);
419 cursor += 2;
420 }
421 Ok(offsets)
422 }
423}