wow_cdbc/
lib.rs

1//! # wow_cdbc
2//!
3//! A library for parsing World of Warcraft DBC (Database Client) files.
4//!
5//! ## Features
6//!
7//! - Parse DBC files from World of Warcraft
8//! - Support for different DBC versions (WDBC, WDB2, WDB5, etc.)
9//! - Schema-based parsing with validation
10//! - Export to JSON and CSV formats
11//! - Command-line interface for working with DBC files
12//!
13//! ## Example
14//!
15//! ```no_run
16//! use std::fs::File;
17//! use std::io::BufReader;
18//! use wow_cdbc::{DbcParser, FieldType, Schema, SchemaField, Value};
19//!
20//! fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     // Open a DBC file
22//!     let file = File::open("SpellItemEnchantment.dbc")?;
23//!     let mut reader = BufReader::new(file);
24//!
25//!     // Parse the DBC file
26//!     let parser = DbcParser::parse(&mut reader)?;
27//!
28//!     // Print header information
29//!     let header = parser.header();
30//!     println!("Record Count: {}", header.record_count);
31//!     println!("Field Count: {}", header.field_count);
32//!
33//!     // Define a schema for SpellItemEnchantment.dbc
34//!     let mut schema = Schema::new("SpellItemEnchantment");
35//!     schema.add_field(SchemaField::new("ID", FieldType::UInt32));
36//!     schema.add_field(SchemaField::new("Charges", FieldType::UInt32));
37//!     schema.add_field(SchemaField::new("Type1", FieldType::UInt32));
38//!     schema.add_field(SchemaField::new("Type2", FieldType::UInt32));
39//!     schema.add_field(SchemaField::new("Type3", FieldType::UInt32));
40//!     schema.add_field(SchemaField::new("Amount1", FieldType::Int32));
41//!     schema.add_field(SchemaField::new("Amount2", FieldType::Int32));
42//!     schema.add_field(SchemaField::new("Amount3", FieldType::Int32));
43//!     schema.add_field(SchemaField::new("ItemID", FieldType::UInt32));
44//!     schema.add_field(SchemaField::new("Description", FieldType::String));
45//!     schema.set_key_field("ID");
46//!
47//!     // Apply the schema and parse records
48//!     let parser = parser.with_schema(schema)?;
49//!     let record_set = parser.parse_records()?;
50//!
51//!     // Access records
52//!     if let Some(record) = record_set.get_record(0) {
53//!         if let Value::UInt32(id) = record.get_value_by_name("ID").unwrap() {
54//!             println!("ID: {}", id);
55//!         }
56//!
57//!         if let Value::StringRef(desc_ref) = record.get_value_by_name("Description").unwrap() {
58//!             let desc = record_set.get_string(*desc_ref)?;
59//!             println!("Description: {}", desc);
60//!         }
61//!     }
62//!
63//!     // Look up a record by key
64//!     if let Some(record) = record_set.get_record_by_key(123) {
65//!         // Work with the record...
66//!     }
67//!
68//!     Ok(())
69//! }
70//! ```
71
72mod error;
73#[cfg(any(feature = "serde", feature = "csv_export"))]
74mod export;
75mod field_parser;
76mod header;
77mod parser;
78mod schema;
79mod schema_discovery;
80mod schema_loader;
81mod stringblock;
82mod types;
83mod versions;
84mod writer;
85
86#[cfg(feature = "mmap")]
87mod mmap;
88
89mod lazy;
90
91#[cfg(feature = "parallel")]
92mod parallel;
93
94#[cfg(feature = "cli")]
95pub mod dbd;
96
97pub use error::Error;
98pub use header::DbcHeader;
99pub use lazy::{LazyDbcParser, LazyRecordIterator};
100pub use parser::{DbcParser, Record, RecordSet, Value};
101pub use schema::{FieldType, Schema, SchemaField};
102pub use schema_discovery::{Confidence, DiscoveredField, DiscoveredSchema, SchemaDiscoverer};
103pub use stringblock::{CachedStringBlock, StringBlock};
104pub use types::*;
105
106#[cfg(feature = "yaml")]
107pub use schema_loader::{SchemaDefinition, SchemaFieldDefinition};
108
109#[cfg(feature = "serde")]
110pub use export::export_to_json;
111
112#[cfg(feature = "csv_export")]
113pub use export::export_to_csv;
114
115#[cfg(feature = "mmap")]
116pub use mmap::MmapDbcFile;
117
118#[cfg(feature = "parallel")]
119pub use parallel::parse_records_parallel;
120
121pub use versions::{DbcVersion, Wdb2Header, Wdb5Header};
122pub use writer::DbcWriter;
123
124/// Result type used throughout the library
125pub type Result<T> = std::result::Result<T, Error>;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130    use std::io::Cursor;
131
132    fn create_test_dbc() -> Vec<u8> {
133        let mut data = Vec::new();
134
135        // Header
136        data.extend_from_slice(b"WDBC"); // Magic
137        data.extend_from_slice(&2u32.to_le_bytes()); // Record count
138        data.extend_from_slice(&3u32.to_le_bytes()); // Field count
139        data.extend_from_slice(&12u32.to_le_bytes()); // Record size
140        data.extend_from_slice(&19u32.to_le_bytes()); // String block size
141
142        // Records
143        // Record 1
144        data.extend_from_slice(&1u32.to_le_bytes()); // ID
145        data.extend_from_slice(&0u32.to_le_bytes()); // Name offset
146        data.extend_from_slice(&100u32.to_le_bytes()); // Value
147
148        // Record 2
149        data.extend_from_slice(&2u32.to_le_bytes()); // ID
150        data.extend_from_slice(&6u32.to_le_bytes()); // Name offset (Second starts at 6)
151        data.extend_from_slice(&200u32.to_le_bytes()); // Value
152
153        // String block
154        data.extend_from_slice(b"First\0Second\0Extra\0"); // String block
155
156        data
157    }
158
159    #[test]
160    fn test_header_parsing() {
161        let data = create_test_dbc();
162        let mut cursor = Cursor::new(&data);
163
164        let header = DbcHeader::parse(&mut cursor).unwrap();
165
166        assert_eq!(header.magic, *b"WDBC");
167        assert_eq!(header.record_count, 2);
168        assert_eq!(header.field_count, 3);
169        assert_eq!(header.record_size, 12);
170        assert_eq!(header.string_block_size, 19);
171    }
172
173    #[test]
174    fn test_schema_validation() {
175        let data = create_test_dbc();
176        let mut cursor = Cursor::new(&data);
177
178        let parser = DbcParser::parse(&mut cursor).unwrap();
179        let header = parser.header();
180
181        let mut schema = Schema::new("Test");
182        schema.add_field(SchemaField::new("ID", FieldType::UInt32));
183        schema.add_field(SchemaField::new("Name", FieldType::String));
184        schema.add_field(SchemaField::new("Value", FieldType::UInt32));
185        schema.set_key_field("ID");
186
187        // This should pass
188        assert!(
189            schema
190                .validate(header.field_count, header.record_size)
191                .is_ok()
192        );
193
194        // Now let's test some invalid schemas
195        let mut invalid_schema = Schema::new("Invalid");
196        invalid_schema.add_field(SchemaField::new("ID", FieldType::UInt32));
197        invalid_schema.add_field(SchemaField::new("Name", FieldType::String));
198        // Missing a field
199
200        assert!(
201            invalid_schema
202                .validate(header.field_count, header.record_size)
203                .is_err()
204        );
205    }
206
207    #[test]
208    fn test_value_display() {
209        use crate::Value;
210
211        // Test various value types
212        assert_eq!(format!("{}", Value::Int32(42)), "42");
213        assert_eq!(format!("{}", Value::UInt32(100)), "100");
214        assert_eq!(format!("{}", Value::Float32(3.5)), "3.5");
215        assert_eq!(format!("{}", Value::Bool(true)), "true");
216        assert_eq!(format!("{}", Value::Bool(false)), "false");
217        assert_eq!(format!("{}", Value::UInt8(255)), "255");
218        assert_eq!(format!("{}", Value::Int8(-128)), "-128");
219        assert_eq!(format!("{}", Value::UInt16(65535)), "65535");
220        assert_eq!(format!("{}", Value::Int16(-32768)), "-32768");
221        assert_eq!(
222            format!("{}", Value::StringRef(StringRef::new(42))),
223            "StringRef(42)"
224        );
225
226        // Test array display
227        let array = Value::Array(vec![Value::Int32(1), Value::Int32(2), Value::Int32(3)]);
228        assert_eq!(format!("{array}"), "[1, 2, 3]");
229
230        // Test empty array
231        let empty_array = Value::Array(vec![]);
232        assert_eq!(format!("{empty_array}"), "[]");
233    }
234
235    #[test]
236    fn test_record_parsing() {
237        let data = create_test_dbc();
238
239        let parser = DbcParser::parse_bytes(&data).unwrap();
240
241        let mut schema = Schema::new("Test");
242        schema.add_field(SchemaField::new("ID", FieldType::UInt32));
243        schema.add_field(SchemaField::new("Name", FieldType::String));
244        schema.add_field(SchemaField::new("Value", FieldType::UInt32));
245        schema.set_key_field("ID");
246
247        let parser = parser.with_schema(schema).unwrap();
248        let record_set = parser.parse_records().unwrap();
249
250        assert_eq!(record_set.len(), 2);
251
252        // Check first record
253        let record = record_set.get_record(0).unwrap();
254        if let Value::UInt32(id) = record.get_value_by_name("ID").unwrap() {
255            assert_eq!(*id, 1);
256        } else {
257            panic!("Expected UInt32 for ID");
258        }
259
260        if let Value::StringRef(name_ref) = record.get_value_by_name("Name").unwrap() {
261            let name = record_set.get_string(*name_ref).unwrap();
262            assert_eq!(name, "First");
263        } else {
264            panic!("Expected StringRef for Name");
265        }
266
267        if let Value::UInt32(value) = record.get_value_by_name("Value").unwrap() {
268            assert_eq!(*value, 100);
269        } else {
270            panic!("Expected UInt32 for Value");
271        }
272
273        // Check record by key
274        let record = record_set.get_record_by_key(2).unwrap();
275        if let Value::StringRef(name_ref) = record.get_value_by_name("Name").unwrap() {
276            let name = record_set.get_string(*name_ref).unwrap();
277            assert_eq!(name, "Second");
278        } else {
279            panic!("Expected StringRef for Name");
280        }
281    }
282}