1use anyhow::{Context, Result};
4use clap::Subcommand;
5use std::fs::File;
6use std::io::{self, BufReader, BufWriter};
7use std::path::{Path, PathBuf};
8use std::time::Instant;
9use wow_cdbc::{
10 DbcParser, RecordSet, SchemaDefinition, SchemaDiscoverer, Value, export_to_csv, export_to_json,
11};
12
13#[cfg(feature = "yaml")]
14use wow_cdbc::FieldType;
15
16#[derive(Subcommand)]
17pub enum DbcCommands {
18 Info {
20 file: PathBuf,
22 },
23
24 Validate {
26 file: PathBuf,
28
29 #[arg(short, long)]
31 schema: PathBuf,
32 },
33
34 List {
36 file: PathBuf,
38
39 #[arg(short, long)]
41 schema: Option<PathBuf>,
42
43 #[arg(short, long, default_value_t = 10)]
45 limit: usize,
46 },
47
48 Export {
50 file: PathBuf,
52
53 #[arg(short, long)]
55 schema: PathBuf,
56
57 #[arg(short, long, value_enum, default_value = "json")]
59 format: ExportFormat,
60
61 #[arg(short, long)]
63 output: Option<PathBuf>,
64 },
65
66 Analyze {
68 file: PathBuf,
70
71 #[arg(short, long)]
73 schema: Option<PathBuf>,
74
75 #[arg(long)]
77 mmap: bool,
78
79 #[arg(long)]
81 lazy: bool,
82
83 #[arg(long)]
85 cache_strings: bool,
86
87 #[arg(long)]
89 sorted_keys: bool,
90 },
91
92 Discover {
94 file: PathBuf,
96
97 #[arg(short, long, default_value_t = 100)]
99 max_records: u32,
100
101 #[arg(long, default_value_t = true)]
103 validate_strings: bool,
104
105 #[arg(long, default_value_t = true)]
107 detect_arrays: bool,
108
109 #[arg(long, default_value_t = true)]
111 detect_key: bool,
112
113 #[arg(short, long)]
115 yaml: bool,
116
117 #[arg(short, long)]
119 output: Option<PathBuf>,
120 },
121}
122
123#[derive(Clone, Copy, Debug, clap::ValueEnum)]
124pub enum ExportFormat {
125 Json,
126 Csv,
127}
128
129pub fn execute(command: DbcCommands) -> Result<()> {
130 match command {
131 DbcCommands::Info { file } => info_command(&file),
132 DbcCommands::List {
133 file,
134 schema,
135 limit,
136 } => list_command(&file, schema.as_deref(), limit),
137 DbcCommands::Export {
138 file,
139 schema,
140 format,
141 output,
142 } => export_command(&file, &schema, format, output.as_deref()),
143 DbcCommands::Analyze {
144 file,
145 schema,
146 mmap,
147 lazy,
148 cache_strings,
149 sorted_keys,
150 } => analyze_command(
151 &file,
152 schema.as_deref(),
153 mmap,
154 lazy,
155 cache_strings,
156 sorted_keys,
157 ),
158 DbcCommands::Validate { file, schema } => validate_command(&file, &schema),
159 DbcCommands::Discover {
160 file,
161 max_records,
162 validate_strings,
163 detect_arrays,
164 detect_key,
165 yaml,
166 output,
167 } => discover_command(
168 &file,
169 max_records,
170 validate_strings,
171 detect_arrays,
172 detect_key,
173 yaml,
174 output.as_deref(),
175 ),
176 }
177}
178
179fn info_command(file: &Path) -> Result<()> {
181 let dbc_file =
182 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
183 let mut reader = BufReader::new(dbc_file);
184
185 let parser = DbcParser::parse(&mut reader)
186 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
187 let header = parser.header();
188
189 println!("DBC File Information");
190 println!("===================");
191 println!();
192 println!("File: {}", file.display());
193 println!(
194 "Magic: {} ({})",
195 std::str::from_utf8(&header.magic).unwrap_or("Invalid"),
196 header
197 .magic
198 .iter()
199 .map(|b| format!("{b:02X}"))
200 .collect::<Vec<_>>()
201 .join(" ")
202 );
203 println!("Version: {:?}", parser.version());
204 println!("Record Count: {}", header.record_count);
205 println!("Field Count: {}", header.field_count);
206 println!("Record Size: {} bytes", header.record_size);
207 println!("String Block Size: {} bytes", header.string_block_size);
208 println!("Total File Size: {} bytes", header.total_size());
209
210 let record_set = parser.parse_records().context("Failed to parse records")?;
212
213 println!();
214 println!("String Block Stats:");
215 println!(" Offset: {}", header.string_block_offset());
216 println!(" Size: {}", header.string_block_size);
217
218 if let Some(record) = record_set.get_record(0) {
220 println!();
221 println!("Sample Record (First Record - Raw Values):");
222 println!("=========================================");
223 for (i, value) in record.values().iter().enumerate() {
224 match value {
225 Value::UInt32(v) => println!(" Field {i:2}: {v:10} (UInt32)"),
226 Value::Int32(v) => println!(" Field {i:2}: {v:10} (Int32)"),
227 Value::Float32(v) => println!(" Field {i:2}: {v:10.4} (Float32)"),
228 Value::StringRef(v) => match record_set.get_string(*v) {
229 Ok(s) => println!(" Field {i:2}: \"{s}\" (String)"),
230 Err(_) => println!(
231 " Field {:2}: <Invalid string ref: {}> (String)",
232 i,
233 v.offset()
234 ),
235 },
236 Value::Bool(v) => println!(" Field {i:2}: {v:10} (Bool)"),
237 Value::UInt8(v) => println!(" Field {i:2}: {v:10} (UInt8)"),
238 Value::Int8(v) => println!(" Field {i:2}: {v:10} (Int8)"),
239 Value::UInt16(v) => println!(" Field {i:2}: {v:10} (UInt16)"),
240 Value::Int16(v) => println!(" Field {i:2}: {v:10} (Int16)"),
241 Value::Array(vals) => {
242 println!(" Field {:2}: Array[{}]", i, vals.len());
243 for (j, val) in vals.iter().enumerate().take(3) {
244 println!(" [{j}]: {val:?}");
245 }
246 if vals.len() > 3 {
247 println!(" ... {} more items", vals.len() - 3);
248 }
249 }
250 }
251 }
252 }
253
254 Ok(())
255}
256
257fn list_command(file: &Path, schema_path: Option<&Path>, limit: usize) -> Result<()> {
259 let dbc_file =
260 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
261 let mut reader = BufReader::new(dbc_file);
262
263 let mut parser = DbcParser::parse(&mut reader)
264 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
265
266 if let Some(schema_path) = schema_path {
268 let schema_def = SchemaDefinition::from_yaml(schema_path).map_err(|e| {
269 anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e)
270 })?;
271 let schema = schema_def
272 .to_schema()
273 .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
274 parser = parser
275 .with_schema(schema)
276 .context("Failed to apply schema")?;
277 }
278
279 let record_set = parser.parse_records().context("Failed to parse records")?;
280
281 println!("DBC Records");
282 println!("===========");
283 println!("Total records: {}", record_set.len());
284 println!("Showing first {} records:", limit.min(record_set.len()));
285 println!();
286
287 for i in 0..limit.min(record_set.len()) {
288 if let Some(record) = record_set.get_record(i) {
289 println!("Record {i}:");
290 if let Some(schema) = record.schema() {
291 for (field_idx, field) in schema.fields.iter().enumerate() {
293 if let Some(value) = record.get_value(field_idx) {
294 print!(" {}: ", field.name);
295 print_value(value, &record_set)?;
296 }
297 }
298 } else {
299 for (field_idx, value) in record.values().iter().enumerate() {
301 print!(" Field {field_idx}: ");
302 print_value(value, &record_set)?;
303 }
304 }
305 println!();
306 }
307 }
308
309 if record_set.len() > limit {
310 println!("... {} more records", record_set.len() - limit);
311 }
312
313 Ok(())
314}
315
316fn export_command(
318 file: &Path,
319 schema_path: &Path,
320 format: ExportFormat,
321 output_path: Option<&Path>,
322) -> Result<()> {
323 let dbc_file =
324 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
325 let mut reader = BufReader::new(dbc_file);
326
327 let schema_def = SchemaDefinition::from_yaml(schema_path)
329 .map_err(|e| anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e))?;
330 let schema = schema_def
331 .to_schema()
332 .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
333
334 let parser = DbcParser::parse(&mut reader)
336 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
337 let parser = parser
338 .with_schema(schema)
339 .context("Failed to apply schema")?;
340 let record_set = parser.parse_records().context("Failed to parse records")?;
341
342 match output_path {
344 Some(path) => {
345 let output_file = File::create(path)
346 .with_context(|| format!("Failed to create output file: {}", path.display()))?;
347 let writer = BufWriter::new(output_file);
348
349 match format {
350 ExportFormat::Json => {
351 export_to_json(&record_set, writer).context("Failed to export to JSON")?;
352 }
353 ExportFormat::Csv => {
354 export_to_csv(&record_set, writer).context("Failed to export to CSV")?;
355 }
356 }
357
358 println!(
359 "Exported {} records to {}: {}",
360 record_set.len(),
361 match format {
362 ExportFormat::Json => "JSON",
363 ExportFormat::Csv => "CSV",
364 },
365 path.display()
366 );
367 }
368 None => {
369 let stdout = io::stdout();
371 let writer = stdout.lock();
372
373 match format {
374 ExportFormat::Json => {
375 export_to_json(&record_set, writer).context("Failed to export to JSON")?;
376 }
377 ExportFormat::Csv => {
378 export_to_csv(&record_set, writer).context("Failed to export to CSV")?;
379 }
380 }
381 }
382 }
383
384 Ok(())
385}
386
387fn analyze_command(
389 file: &Path,
390 schema_path: Option<&Path>,
391 mmap: bool,
392 lazy: bool,
393 cache_strings: bool,
394 sorted_keys: bool,
395) -> Result<()> {
396 println!("Analyzing DBC file: {}", file.display());
397 println!();
398
399 let start = Instant::now();
400
401 if mmap || lazy {
402 anyhow::bail!(
403 "Memory-mapped file support is not available in the current build.\n\
404 The --mmap and --lazy flags require additional build configuration."
405 );
406 }
407
408 let dbc_file =
409 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
410 let mut reader = BufReader::new(dbc_file);
411
412 let parser = DbcParser::parse(&mut reader)
413 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
414 println!(" Header parsed in: {:?}", start.elapsed());
415
416 let schema_obj = if let Some(schema_path) = schema_path {
417 println!("Loading schema: {}", schema_path.display());
418 let schema_def = SchemaDefinition::from_yaml(schema_path).map_err(|e| {
419 anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e)
420 })?;
421 Some(
422 schema_def
423 .to_schema()
424 .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?,
425 )
426 } else {
427 None
428 };
429
430 if let Some(schema_obj) = schema_obj {
431 let parser = parser
432 .with_schema(schema_obj)
433 .context("Failed to apply schema")?;
434 println!(" Schema applied in: {:?}", start.elapsed());
435
436 let mut record_set = parser.parse_records().context("Failed to parse records")?;
437 println!(" Records parsed in: {:?}", start.elapsed());
438
439 if cache_strings {
440 println!();
441 println!("Enabling string caching");
442 record_set.enable_string_caching();
443 println!(" String cache built in: {:?}", start.elapsed());
444 }
445
446 if sorted_keys {
447 println!();
448 println!("Creating sorted key map");
449 record_set
450 .create_sorted_key_map()
451 .context("Failed to create sorted key map")?;
452 println!(" Sorted key map created in: {:?}", start.elapsed());
453 }
454
455 println!();
456 println!("Total records: {}", record_set.len());
457 } else {
458 let record_set = parser.parse_records().context("Failed to parse records")?;
459 println!(" Records parsed in: {:?}", start.elapsed());
460 println!();
461 println!("Total records: {}", record_set.len());
462 }
463
464 println!();
465 println!("Total time: {:?}", start.elapsed());
466
467 Ok(())
468}
469
470fn validate_command(file: &Path, schema_path: &Path) -> Result<()> {
472 let dbc_file =
473 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
474 let mut reader = BufReader::new(dbc_file);
475
476 let schema_def = SchemaDefinition::from_yaml(schema_path)
478 .map_err(|e| anyhow::anyhow!("Failed to load schema {}: {}", schema_path.display(), e))?;
479 let schema = schema_def
480 .to_schema()
481 .map_err(|e| anyhow::anyhow!("Failed to convert schema definition: {}", e))?;
482
483 let parser = DbcParser::parse(&mut reader)
485 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
486
487 println!("Validating DBC file against schema");
488 println!("==================================");
489 println!();
490 println!("File: {}", file.display());
491 println!("Schema: {}", schema_path.display());
492 println!();
493
494 match parser.with_schema(schema) {
496 Ok(parser) => {
497 println!("✓ Schema validation passed");
498 println!();
499
500 println!("Parsing records...");
502 match parser.parse_records() {
503 Ok(record_set) => {
504 println!("✓ Successfully parsed {} records", record_set.len());
505
506 let mut invalid_strings = 0;
508 for i in 0..record_set.len() {
509 if let Some(record) = record_set.get_record(i) {
510 for value in record.values() {
511 if let Value::StringRef(str_ref) = value
512 && record_set.get_string(*str_ref).is_err()
513 {
514 invalid_strings += 1;
515 }
516 }
517 }
518 }
519
520 if invalid_strings > 0 {
521 println!("⚠ Found {invalid_strings} invalid string references");
522 } else {
523 println!("✓ All string references are valid");
524 }
525 }
526 Err(e) => {
527 println!("✗ Failed to parse records: {e}");
528 return Err(e.into());
529 }
530 }
531 }
532 Err(e) => {
533 println!("✗ Schema validation failed: {e}");
534 return Err(anyhow::anyhow!("Schema validation failed: {}", e));
535 }
536 }
537
538 println!();
539 println!("Validation complete!");
540
541 Ok(())
542}
543
544fn print_value(value: &Value, record_set: &RecordSet) -> Result<()> {
546 match value {
547 Value::UInt32(v) => println!("{v}"),
548 Value::Int32(v) => println!("{v}"),
549 Value::Float32(v) => println!("{v:.4}"),
550 Value::StringRef(v) => match record_set.get_string(*v) {
551 Ok(s) => println!("\"{s}\""),
552 Err(_) => println!("<Invalid string ref: {}>", v.offset()),
553 },
554 Value::Bool(v) => println!("{v}"),
555 Value::UInt8(v) => println!("{v}"),
556 Value::Int8(v) => println!("{v}"),
557 Value::UInt16(v) => println!("{v}"),
558 Value::Int16(v) => println!("{v}"),
559 Value::Array(vals) => {
560 print!("[");
561 for (i, val) in vals.iter().enumerate() {
562 if i > 0 {
563 print!(", ");
564 }
565 match val {
566 Value::UInt32(v) => print!("{v}"),
567 Value::Int32(v) => print!("{v}"),
568 Value::Float32(v) => print!("{v:.4}"),
569 _ => print!("{val:?}"),
570 }
571 }
572 println!("]");
573 }
574 }
575 Ok(())
576}
577
578fn discover_command(
580 file: &Path,
581 max_records: u32,
582 validate_strings: bool,
583 detect_arrays: bool,
584 detect_key: bool,
585 yaml: bool,
586 output_path: Option<&Path>,
587) -> Result<()> {
588 println!("Discovering schema for: {}", file.display());
589 println!();
590
591 let start = Instant::now();
593 let dbc_file =
594 File::open(file).with_context(|| format!("Failed to open DBC file: {}", file.display()))?;
595 let mut reader = BufReader::new(dbc_file);
596
597 let parser = DbcParser::parse(&mut reader)
598 .with_context(|| format!("Failed to parse DBC file: {}", file.display()))?;
599 println!("File parsed in: {:?}", start.elapsed());
600
601 let header = parser.header();
603 println!("DBC Header Information:");
604 println!("======================");
605 println!(
606 "Magic: {} ({:02X?})",
607 std::str::from_utf8(&header.magic).unwrap_or("Invalid"),
608 header.magic
609 );
610 println!("Version: {:?}", parser.version());
611 println!("Record Count: {}", header.record_count);
612 println!("Field Count: {}", header.field_count);
613 println!("Record Size: {} bytes", header.record_size);
614 println!("String Block Size: {} bytes", header.string_block_size);
615 println!();
616
617 let record_set = parser.parse_records().context("Failed to parse records")?;
619 println!("Records parsed in: {:?}", start.elapsed());
620
621 let discoverer = SchemaDiscoverer::new(header, parser.data(), record_set.string_block())
623 .with_max_records(max_records)
624 .with_validate_strings(validate_strings)
625 .with_detect_arrays(detect_arrays)
626 .with_detect_key(detect_key);
627
628 let start = Instant::now();
630 let discovered = discoverer.discover().context("Failed to discover schema")?;
631 println!("Schema discovered in: {:?}", start.elapsed());
632 println!();
633
634 println!("Schema Validation:");
636 println!("==================");
637 if discovered.is_valid {
638 println!("✓ Schema is valid!");
639 } else {
640 println!(
641 "✗ Schema validation failed: {}",
642 discovered
643 .validation_message
644 .unwrap_or_else(|| "Unknown validation error".to_string())
645 );
646 }
647 println!();
648
649 println!("Discovered Fields:");
651 println!("==================");
652 for (i, field) in discovered.fields.iter().enumerate() {
653 println!(
654 "Field {:2}: {:8} (confidence: {:?})",
655 i,
656 format!("{:?}", field.field_type),
657 field.confidence
658 );
659
660 if field.is_array {
661 println!(" Array size: {}", field.array_size.unwrap_or(0));
662 }
663
664 if field.is_key_candidate {
665 println!(" ⚷ Key candidate");
666 }
667
668 if field.is_locstring {
669 let locale_names = [
670 "enUS", "koKR", "frFR", "deDE", "zhCN", "zhTW", "esES", "esMX", "flags",
671 ];
672 if let Some(idx) = field.locstring_index {
673 let name = locale_names.get(idx as usize).unwrap_or(&"?");
674 println!(" 🌐 Locstring ({})", name);
675 }
676 }
677
678 let samples: Vec<String> = field
680 .sample_values
681 .iter()
682 .take(5)
683 .map(|v| v.to_string())
684 .collect();
685 println!(" Sample values: [{}]", samples.join(", "));
686 }
687 println!();
688
689 if let Some(key_index) = discovered.key_field_index {
691 println!(
692 "Key Field: Field {} ({})",
693 key_index,
694 if key_index < discovered.fields.len() {
695 format!("{:?}", discovered.fields[key_index].field_type)
696 } else {
697 "Unknown".to_string()
698 }
699 );
700 } else {
701 println!("Key Field: None detected");
702 }
703 println!();
704
705 let file_stem = file.file_stem().unwrap_or_default().to_string_lossy();
707 let schema = discoverer
708 .generate_schema(&file_stem)
709 .context("Failed to generate schema")?;
710
711 if yaml {
713 #[cfg(feature = "yaml")]
714 {
715 use serde::{Deserialize, Serialize};
716
717 #[derive(Serialize, Deserialize)]
719 struct YamlSchemaField {
720 name: String,
721 type_name: String,
722 #[serde(skip_serializing_if = "Option::is_none")]
723 is_array: Option<bool>,
724 #[serde(skip_serializing_if = "Option::is_none")]
725 array_size: Option<usize>,
726 }
727
728 #[derive(Serialize, Deserialize)]
729 struct YamlSchema {
730 name: String,
731 #[serde(skip_serializing_if = "Option::is_none")]
732 key_field: Option<String>,
733 fields: Vec<YamlSchemaField>,
734 }
735
736 let mut fields = Vec::new();
737 for field in schema.fields.iter() {
738 let type_name = match field.field_type {
739 FieldType::Int32 => "Int32",
740 FieldType::UInt32 => "UInt32",
741 FieldType::Float32 => "Float32",
742 FieldType::String => "String",
743 FieldType::Bool => "Bool",
744 FieldType::UInt8 => "UInt8",
745 FieldType::Int8 => "Int8",
746 FieldType::UInt16 => "UInt16",
747 FieldType::Int16 => "Int16",
748 };
749
750 fields.push(YamlSchemaField {
751 name: field.name.clone(),
752 type_name: type_name.to_string(),
753 is_array: if field.is_array { Some(true) } else { None },
754 array_size: field.array_size,
755 });
756 }
757
758 let key_field = if let Some(key_index) = schema.key_field_index {
759 if key_index < schema.fields.len() {
760 Some(schema.fields[key_index].name.clone())
761 } else {
762 None
763 }
764 } else {
765 None
766 };
767
768 let yaml_schema = YamlSchema {
769 name: schema.name.clone(),
770 key_field,
771 fields,
772 };
773
774 let yaml_content = serde_yaml_ng::to_string(&yaml_schema)
775 .context("Failed to serialize schema to YAML")?;
776
777 if let Some(output_path) = output_path {
779 std::fs::write(output_path, yaml_content).with_context(|| {
780 format!("Failed to write schema to: {}", output_path.display())
781 })?;
782 println!("Schema written to: {}", output_path.display());
783 } else {
784 println!("Generated Schema (YAML):");
785 println!("========================");
786 println!("{yaml_content}");
787 }
788 }
789
790 #[cfg(not(feature = "yaml"))]
791 {
792 anyhow::bail!(
793 "YAML output requested but yaml feature is not enabled. Rebuild with --features yaml to enable YAML support."
794 );
795 }
796 } else {
797 println!("Generated Schema:");
799 println!("=================");
800 println!("Name: {}", schema.name);
801
802 if let Some(key_index) = schema.key_field_index
803 && key_index < schema.fields.len()
804 {
805 println!("Key Field: {}", schema.fields[key_index].name);
806 }
807
808 println!("Fields:");
809
810 for (i, field) in schema.fields.iter().enumerate() {
811 let field_desc = if field.is_array {
812 format!(
813 "{}: {:?}[{}]",
814 field.name,
815 field.field_type,
816 field.array_size.unwrap_or(0)
817 )
818 } else {
819 format!("{}: {:?}", field.name, field.field_type)
820 };
821
822 if Some(i) == schema.key_field_index {
823 println!(" {field_desc} (Key field)");
824 } else {
825 println!(" {field_desc}");
826 }
827 }
828
829 if let Some(output_path) = output_path {
831 let mut contents = format!("Schema: {}\n", schema.name);
832
833 if let Some(key_index) = schema.key_field_index
834 && key_index < schema.fields.len()
835 {
836 contents.push_str(&format!("Key Field: {}\n", schema.fields[key_index].name));
837 }
838
839 contents.push_str("Fields:\n");
840
841 for (i, field) in schema.fields.iter().enumerate() {
842 let field_desc = if field.is_array {
843 format!(
844 " {}: {:?}[{}]\n",
845 field.name,
846 field.field_type,
847 field.array_size.unwrap_or(0)
848 )
849 } else {
850 format!(" {}: {:?}\n", field.name, field.field_type)
851 };
852
853 contents.push_str(&field_desc);
854
855 if Some(i) == schema.key_field_index {
856 contents.push_str(" (Key field)\n");
857 }
858 }
859
860 std::fs::write(output_path, contents)
861 .with_context(|| format!("Failed to write schema to: {}", output_path.display()))?;
862 println!("\nSchema written to: {}", output_path.display());
863 }
864 }
865
866 Ok(())
867}