1mod reader;
24mod writer;
25
26pub use reader::{
27 read_twoda, read_twoda_from_bytes, read_twoda_from_bytes_with_options, read_twoda_with_options,
28};
29pub use writer::{
30 write_twoda, write_twoda_to_vec, write_twoda_to_vec_with_options, write_twoda_with_options,
31};
32
33use std::io::Read;
34use thiserror::Error;
35
36#[cfg(feature = "serde")]
37use serde::{Deserialize, Serialize};
38
39use rakata_core::{DecodeTextError, EncodeTextError, TextEncoding};
40
41use crate::binary::{self, DecodeBinary, EncodeBinary};
42
43const TWODA_MAGIC: [u8; 4] = *b"2DA ";
45const TWODA_VERSION_V2B: [u8; 4] = *b"V2.b";
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct TwoDaBinaryOptions {
51 pub text_encoding: TextEncoding,
53}
54
55impl Default for TwoDaBinaryOptions {
56 fn default() -> Self {
57 Self {
58 text_encoding: TextEncoding::Windows1252,
59 }
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
68#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
69pub struct TwoDa {
70 pub headers: Vec<String>,
72 pub rows: Vec<TwoDaRow>,
74}
75
76impl TwoDa {
77 pub fn empty() -> Self {
79 Self {
80 headers: Vec::new(),
81 rows: Vec::new(),
82 }
83 }
84
85 pub fn new(headers: Vec<String>) -> Self {
87 Self {
88 headers,
89 rows: Vec::new(),
90 }
91 }
92
93 pub fn push_row(
102 &mut self,
103 label: impl Into<String>,
104 cells: Vec<String>,
105 ) -> Result<(), TwoDaBinaryError> {
106 if cells.len() != self.headers.len() {
107 return Err(TwoDaBinaryError::InvalidTable(format!(
108 "row width {} does not match header width {}",
109 cells.len(),
110 self.headers.len()
111 )));
112 }
113 self.rows.push(TwoDaRow {
114 label: label.into(),
115 cells,
116 });
117 Ok(())
118 }
119
120 pub fn row_count(&self) -> usize {
122 self.rows.len()
123 }
124
125 pub fn column_count(&self) -> usize {
127 self.headers.len()
128 }
129
130 pub fn cell(&self, row_index: usize, column_name: &str) -> Option<&str> {
132 let column_index = self
133 .headers
134 .iter()
135 .position(|header| header == column_name)?;
136 self.rows
137 .get(row_index)
138 .and_then(|row| row.cells.get(column_index))
139 .map(String::as_str)
140 }
141}
142
143impl DecodeBinary for TwoDa {
144 type Error = TwoDaBinaryError;
145
146 fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
147 read_twoda_from_bytes(bytes)
148 }
149}
150
151impl EncodeBinary for TwoDa {
152 type Error = TwoDaBinaryError;
153
154 fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
155 write_twoda_to_vec(self)
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq)]
161#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
162pub struct TwoDaRow {
163 pub label: String,
165 pub cells: Vec<String>,
167}
168
169#[derive(Debug, Error)]
171pub enum TwoDaBinaryError {
172 #[error(transparent)]
174 Io(#[from] std::io::Error),
175 #[error("invalid 2DA magic: {0:?}")]
177 InvalidMagic([u8; 4]),
178 #[error("invalid 2DA version: {0:?}")]
180 InvalidVersion([u8; 4]),
181 #[error("invalid 2DA header: {0}")]
183 InvalidHeader(String),
184 #[error("invalid 2DA table: {0}")]
186 InvalidTable(String),
187 #[error("value overflow while writing field `{0}`")]
189 ValueOverflow(&'static str),
190 #[error("2DA text encoding failed for {context}: {source}")]
192 TextEncoding {
193 context: String,
195 #[source]
197 source: EncodeTextError,
198 },
199 #[error("2DA text decoding failed for {context}: {source}")]
201 TextDecoding {
202 context: String,
204 #[source]
206 source: DecodeTextError,
207 },
208}
209
210impl From<binary::BinaryLayoutError> for TwoDaBinaryError {
211 fn from(error: binary::BinaryLayoutError) -> Self {
212 Self::InvalidHeader(error.to_string())
213 }
214}
215
216fn validate_twoda(twoda: &TwoDa) -> Result<(), TwoDaBinaryError> {
217 for (index, header) in twoda.headers.iter().enumerate() {
218 if header.contains('\0') || header.contains('\t') {
219 return Err(TwoDaBinaryError::InvalidTable(format!(
220 "header[{index}] contains reserved delimiter"
221 )));
222 }
223 }
224 for (row_index, row) in twoda.rows.iter().enumerate() {
225 if row.cells.len() != twoda.headers.len() {
226 return Err(TwoDaBinaryError::InvalidTable(format!(
227 "row[{row_index}] width {} does not match header width {}",
228 row.cells.len(),
229 twoda.headers.len()
230 )));
231 }
232 if row.label.contains('\0') || row.label.contains('\t') {
233 return Err(TwoDaBinaryError::InvalidTable(format!(
234 "row label[{row_index}] contains reserved delimiter"
235 )));
236 }
237 for (column_index, value) in row.cells.iter().enumerate() {
238 if value.contains('\0') {
239 return Err(TwoDaBinaryError::InvalidTable(format!(
240 "cell[{row_index}][{column_index}] contains NUL byte"
241 )));
242 }
243 }
244 }
245 Ok(())
246}
247
248#[cfg(feature = "serde")]
253mod serde_impl {
254 use super::*;
255
256 pub mod csv_impl {
257 use super::*;
258 use csv::{ReaderBuilder, StringRecord, WriterBuilder};
259 use std::io::{Cursor, Write};
260
261 pub fn read_twoda_from_csv<R: Read>(reader: &mut R) -> Result<TwoDa, TwoDaBinaryError> {
271 let mut csv_reader = ReaderBuilder::new()
272 .has_headers(true)
273 .flexible(true)
274 .trim(::csv::Trim::All)
275 .from_reader(reader);
276
277 let headers: Vec<String> = csv_reader
278 .headers()
279 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?
280 .iter()
281 .map(|s| s.to_string())
282 .collect();
283
284 let actual_headers = if headers.is_empty() {
289 Vec::new()
290 } else {
291 headers[1..].to_vec()
292 };
293
294 let mut twoda = TwoDa::new(actual_headers);
295
296 for result in csv_reader.records() {
297 let record = result.map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
298 if record.is_empty() {
299 continue;
300 }
301
302 let label = record.get(0).unwrap_or_default().to_string();
304 let cells: Vec<String> = record.iter().skip(1).map(|s| s.to_string()).collect();
306
307 if cells.len() != twoda.headers.len() {
309 return Err(TwoDaBinaryError::InvalidTable(format!(
310 "row width mismatch: expected {}, got {}",
311 twoda.headers.len(),
312 cells.len()
313 )));
314 }
315
316 twoda.push_row(label, cells)?;
317 }
318
319 Ok(twoda)
320 }
321
322 pub fn write_twoda_to_csv<W: Write>(
332 writer: &mut W,
333 twoda: &TwoDa,
334 ) -> Result<(), TwoDaBinaryError> {
335 validate_twoda(twoda)?;
336
337 let mut csv_writer = WriterBuilder::new().has_headers(true).from_writer(writer);
338
339 let mut header_record = StringRecord::new();
341 header_record.push_field("label");
342 for header in &twoda.headers {
343 header_record.push_field(header);
344 }
345 csv_writer
346 .write_record(&header_record)
347 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
348
349 for row in &twoda.rows {
351 let mut record = StringRecord::new();
352 record.push_field(&row.label);
353 for cell in &row.cells {
354 record.push_field(cell);
355 }
356 csv_writer
357 .write_record(&record)
358 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
359 }
360
361 csv_writer
362 .flush()
363 .map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))?;
364
365 Ok(())
366 }
367
368 pub fn write_twoda_to_csv_vec(twoda: &TwoDa) -> Result<Vec<u8>, TwoDaBinaryError> {
374 let mut cursor = Cursor::new(Vec::new());
375 write_twoda_to_csv(&mut cursor, twoda)?;
376 Ok(cursor.into_inner())
377 }
378
379 pub fn read_twoda_from_csv_bytes(bytes: &[u8]) -> Result<TwoDa, TwoDaBinaryError> {
385 let mut cursor = Cursor::new(bytes);
386 read_twoda_from_csv(&mut cursor)
387 }
388 }
389
390 pub mod json_impl {
391 use super::*;
392 use serde_json::{from_slice, from_str, to_string_pretty, to_vec};
393
394 pub fn write_twoda_to_json(twoda: &TwoDa) -> Result<String, TwoDaBinaryError> {
402 to_string_pretty(twoda).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
403 }
404
405 pub fn write_twoda_to_json_vec(twoda: &TwoDa) -> Result<Vec<u8>, TwoDaBinaryError> {
411 to_vec(twoda).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
412 }
413
414 pub fn read_twoda_from_json(json: &str) -> Result<TwoDa, TwoDaBinaryError> {
423 from_str(json).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
424 }
425
426 pub fn read_twoda_from_json_bytes(bytes: &[u8]) -> Result<TwoDa, TwoDaBinaryError> {
432 from_slice(bytes).map_err(|e| TwoDaBinaryError::InvalidTable(e.to_string()))
433 }
434 }
435}
436
437#[cfg(feature = "serde")]
438pub use serde_impl::csv_impl::{
439 read_twoda_from_csv, read_twoda_from_csv_bytes, write_twoda_to_csv, write_twoda_to_csv_vec,
440};
441
442#[cfg(feature = "serde")]
443pub use serde_impl::json_impl::{
444 read_twoda_from_json, read_twoda_from_json_bytes, write_twoda_to_json, write_twoda_to_json_vec,
445};
446
447#[cfg_attr(
463 feature = "tracing",
464 tracing::instrument(level = "debug", skip(reader))
465)]
466pub fn read_twoda_auto<R: Read>(
467 reader: &mut R,
468 name_or_extension: &str,
469) -> Result<TwoDa, TwoDaBinaryError> {
470 let ext = if let Some(idx) = name_or_extension.rfind('.') {
471 &name_or_extension[idx + 1..]
472 } else {
473 name_or_extension
474 };
475
476 match ext.to_lowercase().as_str() {
477 "csv" => {
478 #[cfg(feature = "serde")]
479 {
480 read_twoda_from_csv(reader)
481 }
482 #[cfg(not(feature = "serde"))]
483 Err(TwoDaBinaryError::InvalidTable(
484 "CSV format requires serde feature".into(),
485 ))
486 }
487 "json" => {
488 #[cfg(feature = "serde")]
489 {
490 let mut bytes = Vec::new();
492 reader.read_to_end(&mut bytes)?;
493 read_twoda_from_json_bytes(&bytes)
494 }
495 #[cfg(not(feature = "serde"))]
496 Err(TwoDaBinaryError::InvalidTable(
497 "JSON format requires serde feature".into(),
498 ))
499 }
500 _ => read_twoda(reader),
501 }
502}