1mod reader;
32mod source;
33mod writer;
34
35pub use reader::{
36 read_twoda, read_twoda_from_bytes, read_twoda_from_bytes_with_options, read_twoda_with_options,
37};
38pub use source::TwoDaSource;
39pub use writer::{
40 write_twoda, write_twoda_to_vec, write_twoda_to_vec_with_options, write_twoda_with_options,
41};
42
43use std::io::Read;
44use thiserror::Error;
45
46#[cfg(feature = "serde")]
47use serde::{Deserialize, Serialize};
48
49use rakata_core::{DecodeTextError, EncodeTextError, TextEncoding};
50
51use crate::binary::{self, DecodeBinary, EncodeBinary};
52
53const TWODA_MAGIC: [u8; 4] = *b"2DA ";
55const TWODA_VERSION_V2B: [u8; 4] = *b"V2.b";
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct TwoDaBinaryOptions {
61 pub text_encoding: TextEncoding,
63}
64
65impl Default for TwoDaBinaryOptions {
66 fn default() -> Self {
67 Self {
68 text_encoding: TextEncoding::Windows1252,
69 }
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
78#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
79pub struct TwoDa {
80 pub headers: Vec<String>,
82 pub rows: Vec<TwoDaRow>,
84}
85
86impl TwoDa {
87 pub fn empty() -> Self {
89 Self {
90 headers: Vec::new(),
91 rows: Vec::new(),
92 }
93 }
94
95 pub fn new(headers: Vec<String>) -> Self {
97 Self {
98 headers,
99 rows: Vec::new(),
100 }
101 }
102
103 pub fn push_row(
105 &mut self,
106 label: impl Into<String>,
107 cells: Vec<String>,
108 ) -> Result<(), TwoDaBinaryError> {
109 if cells.len() != self.headers.len() {
110 return Err(TwoDaBinaryError::InvalidTable(format!(
111 "row width {} does not match header width {}",
112 cells.len(),
113 self.headers.len()
114 )));
115 }
116 self.rows.push(TwoDaRow {
117 label: label.into(),
118 cells,
119 });
120 Ok(())
121 }
122
123 pub fn row_count(&self) -> usize {
125 self.rows.len()
126 }
127
128 pub fn column_count(&self) -> usize {
130 self.headers.len()
131 }
132
133 pub fn cell(&self, row_index: usize, column_name: &str) -> Option<&str> {
135 let column_index = self
136 .headers
137 .iter()
138 .position(|header| header == column_name)?;
139 self.rows
140 .get(row_index)
141 .and_then(|row| row.cells.get(column_index))
142 .map(String::as_str)
143 }
144}
145
146impl DecodeBinary for TwoDa {
147 type Error = TwoDaBinaryError;
148
149 fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
150 read_twoda_from_bytes(bytes)
151 }
152}
153
154impl EncodeBinary for TwoDa {
155 type Error = TwoDaBinaryError;
156
157 fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
158 write_twoda_to_vec(self)
159 }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
164#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
165pub struct TwoDaRow {
166 pub label: String,
168 pub cells: Vec<String>,
170}
171
172#[derive(Debug, Error)]
174pub enum TwoDaBinaryError {
175 #[error(transparent)]
177 Io(#[from] std::io::Error),
178 #[error("invalid 2DA magic: {0:?}")]
180 InvalidMagic([u8; 4]),
181 #[error("invalid 2DA version: {0:?}")]
183 InvalidVersion([u8; 4]),
184 #[error("invalid 2DA header: {0}")]
186 InvalidHeader(String),
187 #[error("invalid 2DA table: {0}")]
189 InvalidTable(String),
190 #[error("value overflow while writing field `{0}`")]
192 ValueOverflow(&'static str),
193 #[error("2DA text encoding failed for {context}: {source}")]
195 TextEncoding {
196 context: String,
198 #[source]
200 source: EncodeTextError,
201 },
202 #[error("2DA text decoding failed for {context}: {source}")]
204 TextDecoding {
205 context: String,
207 #[source]
209 source: DecodeTextError,
210 },
211}
212
213impl From<binary::BinaryLayoutError> for TwoDaBinaryError {
214 fn from(error: binary::BinaryLayoutError) -> Self {
215 Self::InvalidHeader(error.to_string())
216 }
217}
218
219fn validate_twoda(twoda: &TwoDa) -> Result<(), TwoDaBinaryError> {
220 for (index, header) in twoda.headers.iter().enumerate() {
221 if header.contains('\0') || header.contains('\t') {
222 return Err(TwoDaBinaryError::InvalidTable(format!(
223 "header[{index}] contains reserved delimiter"
224 )));
225 }
226 }
227 for (row_index, row) in twoda.rows.iter().enumerate() {
228 if row.cells.len() != twoda.headers.len() {
229 return Err(TwoDaBinaryError::InvalidTable(format!(
230 "row[{row_index}] width {} does not match header width {}",
231 row.cells.len(),
232 twoda.headers.len()
233 )));
234 }
235 if row.label.contains('\0') || row.label.contains('\t') {
236 return Err(TwoDaBinaryError::InvalidTable(format!(
237 "row label[{row_index}] contains reserved delimiter"
238 )));
239 }
240 for (column_index, value) in row.cells.iter().enumerate() {
241 if value.contains('\0') {
242 return Err(TwoDaBinaryError::InvalidTable(format!(
243 "cell[{row_index}][{column_index}] contains NUL byte"
244 )));
245 }
246 }
247 }
248 Ok(())
249}
250
251#[cfg(feature = "serde")]
256mod serde_impl {
257 use super::*;
258
259 pub mod csv_impl {
260 use super::*;
261 use csv::{ReaderBuilder, StringRecord, WriterBuilder};
262 use std::io::{Cursor, Write};
263
264 pub fn read_twoda_from_csv<R: Read>(reader: &mut R) -> Result<TwoDa, TwoDaBinaryError> {
266 let mut csv_reader = ReaderBuilder::new()
267 .has_headers(true)
268 .flexible(true)
269 .trim(::csv::Trim::All)
270 .from_reader(reader);
271
272 let headers: Vec<String> = csv_reader
273 .headers()
274 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?
275 .iter()
276 .map(|s| s.to_string())
277 .collect();
278
279 let actual_headers = if headers.is_empty() {
284 Vec::new()
285 } else {
286 headers[1..].to_vec()
287 };
288
289 let mut twoda = TwoDa::new(actual_headers);
290
291 for result in csv_reader.records() {
292 let record = result.map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
293 if record.is_empty() {
294 continue;
295 }
296
297 let label = record.get(0).unwrap_or_default().to_string();
299 let cells: Vec<String> = record.iter().skip(1).map(|s| s.to_string()).collect();
301
302 if cells.len() != twoda.headers.len() {
304 return Err(TwoDaBinaryError::InvalidTable(format!(
305 "row width mismatch: expected {}, got {}",
306 twoda.headers.len(),
307 cells.len()
308 )));
309 }
310
311 twoda.push_row(label, cells)?;
312 }
313
314 Ok(twoda)
315 }
316
317 pub fn write_twoda_to_csv<W: Write>(
319 writer: &mut W,
320 twoda: &TwoDa,
321 ) -> Result<(), TwoDaBinaryError> {
322 validate_twoda(twoda)?;
323
324 let mut csv_writer = WriterBuilder::new().has_headers(true).from_writer(writer);
325
326 let mut header_record = StringRecord::new();
328 header_record.push_field("label");
329 for header in &twoda.headers {
330 header_record.push_field(header);
331 }
332 csv_writer
333 .write_record(&header_record)
334 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
335
336 for row in &twoda.rows {
338 let mut record = StringRecord::new();
339 record.push_field(&row.label);
340 for cell in &row.cells {
341 record.push_field(cell);
342 }
343 csv_writer
344 .write_record(&record)
345 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
346 }
347
348 csv_writer
349 .flush()
350 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
351
352 Ok(())
353 }
354
355 pub fn write_twoda_to_csv_vec(twoda: &TwoDa) -> Result<Vec<u8>, TwoDaBinaryError> {
357 let mut cursor = Cursor::new(Vec::new());
358 write_twoda_to_csv(&mut cursor, twoda)?;
359 Ok(cursor.into_inner())
360 }
361
362 pub fn read_twoda_from_csv_bytes(bytes: &[u8]) -> Result<TwoDa, TwoDaBinaryError> {
364 let mut cursor = Cursor::new(bytes);
365 read_twoda_from_csv(&mut cursor)
366 }
367 }
368
369 pub mod json_impl {
370 use super::*;
371 use serde_json::{from_slice, from_str, to_string_pretty, to_vec};
372
373 pub fn write_twoda_to_json(twoda: &TwoDa) -> Result<String, TwoDaBinaryError> {
375 to_string_pretty(twoda).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
376 }
377
378 pub fn write_twoda_to_json_vec(twoda: &TwoDa) -> Result<Vec<u8>, TwoDaBinaryError> {
380 to_vec(twoda).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
381 }
382
383 pub fn read_twoda_from_json(json: &str) -> Result<TwoDa, TwoDaBinaryError> {
385 from_str(json).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
386 }
387
388 pub fn read_twoda_from_json_bytes(bytes: &[u8]) -> Result<TwoDa, TwoDaBinaryError> {
390 from_slice(bytes).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
391 }
392 }
393}
394
395#[cfg(feature = "serde")]
396pub use serde_impl::csv_impl::{
397 read_twoda_from_csv, read_twoda_from_csv_bytes, write_twoda_to_csv, write_twoda_to_csv_vec,
398};
399
400#[cfg(feature = "serde")]
401pub use serde_impl::json_impl::{
402 read_twoda_from_json, read_twoda_from_json_bytes, write_twoda_to_json, write_twoda_to_json_vec,
403};
404
405#[cfg_attr(
412 feature = "tracing",
413 tracing::instrument(level = "debug", skip(reader))
414)]
415pub fn read_twoda_auto<R: Read>(
416 reader: &mut R,
417 name_or_extension: &str,
418) -> Result<TwoDa, TwoDaBinaryError> {
419 let ext = if let Some(idx) = name_or_extension.rfind('.') {
420 &name_or_extension[idx + 1..]
421 } else {
422 name_or_extension
423 };
424
425 match ext.to_lowercase().as_str() {
426 "csv" => {
427 #[cfg(feature = "serde")]
428 {
429 read_twoda_from_csv(reader)
430 }
431 #[cfg(not(feature = "serde"))]
432 Err(TwoDaBinaryError::InvalidTable(
433 "CSV format requires serde feature".into(),
434 ))
435 }
436 "json" => {
437 #[cfg(feature = "serde")]
438 {
439 let mut bytes = Vec::new();
441 reader.read_to_end(&mut bytes)?;
442 read_twoda_from_json_bytes(&bytes)
443 }
444 #[cfg(not(feature = "serde"))]
445 Err(TwoDaBinaryError::InvalidTable(
446 "JSON format requires serde feature".into(),
447 ))
448 }
449 _ => read_twoda(reader),
450 }
451}