1pub mod cli;
2pub mod counter;
3pub mod nbt_utils;
4pub mod tree;
5pub mod view;
6
7use std::{
8 collections::HashMap,
9 io::{Cursor, Read},
10 path::{Path, PathBuf},
11};
12
13use cli::{CliArgs, ItemFilter};
14use counter::{Counter, CounterMap};
15use flate2::read::GzDecoder;
16use mca::RegionReader;
17use nbt_utils::{convert_simdnbt_to_valence_nbt, get_entity_pos_string};
18use ptree::print_tree;
19use serde::{Deserialize, Serialize};
20use tree::ItemSummaryNode;
21use valence_nbt::Value;
22
23const CHUNK_PER_REGION_SIDE: usize = 32;
24
25#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct Scope {
27 pub dimension: String,
28 pub data_type: DataType,
29}
30
31#[derive(
32 Debug,
33 Clone,
34 Copy,
35 PartialEq,
36 Eq,
37 Hash,
38 PartialOrd,
39 Ord,
40 Serialize,
41 Deserialize,
42 strum::Display,
43 strum::EnumString,
44 strum::EnumIter,
45)]
46pub enum DataType {
47 #[strum(to_string = "Block Entity")]
48 BlockEntity,
49 #[strum(to_string = "Entity")]
50 Entity,
51 #[strum(to_string = "Player Data")]
52 Player,
53}
54
55pub struct ScanTask {
56 pub path: PathBuf,
57 pub scope: Scope,
58}
59
60pub fn list_mca_files(dir: &Path) -> Result<Vec<PathBuf>, String> {
61 let entries = std::fs::read_dir(dir)
62 .map_err(|e| format!("Error: failed to read directory '{}': {e}", dir.display()))?;
63
64 let mut mca_files = Vec::new();
65 for entry_res in entries {
66 match entry_res {
67 Ok(de) => {
68 let path = de.path();
69 if path.extension().and_then(|e| e.to_str()) == Some("mca") {
70 mca_files.push(path);
71 }
72 }
73 Err(e) => {
74 eprintln!(
75 "Warning: failed to read an entry in '{}': {}",
76 dir.display(),
77 e
78 );
79 }
80 }
81 }
82 Ok(mca_files)
83}
84
85pub fn process_task(
86 task: ScanTask,
87 queries: &[ItemFilter],
88 args: &CliArgs,
89 user_cache: &HashMap<String, String>,
90) -> CounterMap {
91 let mut counter = Counter::new();
92 match task.scope.data_type {
93 DataType::BlockEntity => process_region_file(&task, queries, args, &mut counter),
94 DataType::Entity => process_entities_file(&task, queries, args, &mut counter),
95 DataType::Player => process_player_file(&task, queries, args, &mut counter, user_cache),
96 }
97 let mut map = CounterMap::new();
98 map.merge_scope(task.scope, &counter);
99 map
100}
101
102fn process_any_region_file<F>(
105 task: &ScanTask,
106 item_queries: &[ItemFilter],
107 cli_args: &CliArgs,
108 counter: &mut Counter,
109 process_chunk_fn: F,
110) where
111 F: Fn(&mca::RawChunk, usize, usize, &ScanTask, &[ItemFilter], &CliArgs, &mut Counter),
112{
113 let region_file_path = &task.path;
114 let data = match std::fs::read(region_file_path) {
115 Ok(d) => d,
116 Err(e) => {
117 if cli_args.verbose {
118 eprintln!("Failed to read file {}: {e}", region_file_path.display());
119 }
120 return;
121 }
122 };
123
124 let region_reader = match RegionReader::new(&data) {
125 Ok(r) => r,
126 Err(e) => {
127 if cli_args.verbose {
128 eprintln!(
129 "Failed to parse region file {}: {e}",
130 region_file_path.display()
131 );
132 }
133 return;
134 }
135 };
136
137 for cy in 0..CHUNK_PER_REGION_SIDE {
138 for cx in 0..CHUNK_PER_REGION_SIDE {
139 let chunk_data = match region_reader.get_chunk(cx, cy) {
140 Ok(Some(c)) => c,
141 Ok(None) => continue, Err(e) => {
143 if cli_args.verbose {
144 eprintln!(
145 "Failed to get chunk ({cx}, {cy}) from {}: {e}",
146 region_file_path.display()
147 );
148 }
149 continue;
150 }
151 };
152 process_chunk_fn(&chunk_data, cx, cy, task, item_queries, cli_args, counter);
153 }
154 }
155}
156
157pub fn process_region_file(
159 task: &ScanTask,
160 item_queries: &[ItemFilter],
161 cli_args: &CliArgs,
162 counter: &mut Counter,
163) {
164 process_any_region_file(
165 task,
166 item_queries,
167 cli_args,
168 counter,
169 process_chunk_for_block_entities,
170 );
171}
172
173pub fn process_entities_file(
176 task: &ScanTask,
177 item_queries: &[ItemFilter],
178 cli_args: &CliArgs,
179 counter: &mut Counter,
180) {
181 process_any_region_file(
182 task,
183 item_queries,
184 cli_args,
185 counter,
186 process_chunk_for_entities,
187 );
188}
189
190#[allow(clippy::too_many_arguments)] fn process_chunk_nbt_list<F>(
193 chunk_data: &mca::RawChunk,
194 cy: usize,
195 cx: usize,
196 task: &ScanTask,
197 item_queries: &[ItemFilter],
198 cli_args: &CliArgs,
199 counter: &mut Counter,
200 nbt_list_name: &str,
201 process_nbt_compound_fn: F,
202) where
203 F: Fn(simdnbt::borrow::NbtCompound, &ScanTask, &[ItemFilter], &CliArgs, &mut Counter),
204{
205 let region_file_path = &task.path;
206 let decompressed_data = match chunk_data.decompress() {
207 Ok(d) => d,
208 Err(e) => {
209 if cli_args.verbose {
210 eprintln!(
211 "Failed to decompress chunk ({cx}, {cy}) in {}: {e}",
212 region_file_path.display()
213 );
214 }
215 return;
216 }
217 };
218 let mut cursor = Cursor::new(decompressed_data.as_slice());
219 let nbt_root = match simdnbt::borrow::read(&mut cursor) {
220 Ok(simdnbt::borrow::Nbt::Some(nbt)) => nbt,
221 Ok(simdnbt::borrow::Nbt::None) => {
222 if cli_args.verbose {
223 eprintln!(
224 "No NBT data found in chunk ({cx}, {cy}) in {}",
225 region_file_path.display()
226 );
227 }
228 return;
229 }
230 Err(e) => {
231 if cli_args.verbose {
232 eprintln!(
233 "Failed to read NBT data for chunk ({cx}, {cy}) in {}: {e}",
234 region_file_path.display()
235 );
236 }
237 return;
238 }
239 };
240
241 let Some(compounds_list) = nbt_root.list(nbt_list_name).and_then(|l| l.compounds()) else {
242 return;
244 };
245
246 for nbt_compound in compounds_list {
247 process_nbt_compound_fn(nbt_compound, task, item_queries, cli_args, counter);
248 }
249}
250
251fn process_chunk_for_block_entities(
253 chunk_data: &mca::RawChunk,
254 cx: usize,
255 cy: usize,
256 task: &ScanTask,
257 item_queries: &[ItemFilter],
258 cli_args: &CliArgs,
259 counter: &mut Counter,
260) {
261 process_chunk_nbt_list(
262 chunk_data,
263 cx,
264 cy,
265 task,
266 item_queries,
267 cli_args,
268 counter,
269 "block_entities", process_block_entity,
271 );
272}
273
274fn process_chunk_for_entities(
276 chunk_data: &mca::RawChunk,
277 cx: usize,
278 cy: usize,
279 task: &ScanTask,
280 item_queries: &[ItemFilter],
281 cli_args: &CliArgs,
282 counter: &mut Counter,
283) {
284 process_chunk_nbt_list(
285 chunk_data,
286 cx,
287 cy,
288 task,
289 item_queries,
290 cli_args,
291 counter,
292 "Entities", process_single_entity,
294 );
295}
296
297fn process_player_file(
299 task: &ScanTask,
300 queries: &[ItemFilter],
301 cli_args: &CliArgs,
302 counter: &mut Counter,
303 user_cache: &HashMap<String, String>,
304) {
305 let file_path = &task.path;
306 let file_data = match std::fs::read(file_path) {
307 Ok(d) => d,
308 Err(e) => {
309 if cli_args.verbose {
310 eprintln!("Failed to read player file {}: {e}", file_path.display());
311 }
312 return;
313 }
314 };
315
316 let mut decompressor = GzDecoder::new(file_data.as_slice());
317 let mut decompressed_data = Vec::new();
318 if let Err(e) = decompressor.read_to_end(&mut decompressed_data) {
319 if cli_args.verbose {
320 eprintln!(
321 "Failed to decompress player file {}: {e}",
322 file_path.display(),
323 );
324 }
325 return;
326 }
327
328 let mut cursor = Cursor::new(decompressed_data.as_slice());
329 let nbt_root_container = match simdnbt::borrow::read(&mut cursor) {
330 Ok(nbt) => nbt,
331 Err(e) => {
332 if cli_args.verbose {
333 eprintln!(
334 "Failed to read NBT for player file {}: {e}",
335 file_path.display(),
336 );
337 }
338 return;
339 }
340 };
341
342 let nbt_root = match nbt_root_container {
343 simdnbt::borrow::Nbt::Some(nbt) => nbt,
344 simdnbt::borrow::Nbt::None => {
345 if cli_args.verbose {
346 eprintln!("No NBT data found in player file {}", file_path.display());
347 }
348 return;
349 }
350 };
351
352 let (player_nbt_compound_opt, source_id, base_location_str): (
353 Option<simdnbt::borrow::NbtCompound>,
354 String,
355 String,
356 ) = if file_path
357 .file_name()
358 .is_some_and(|name| name == "level.dat")
359 {
360 nbt_root
362 .compound(nbt_utils::NBT_KEY_PLAYER_DATA)
363 .and_then(|data_compound| data_compound.compound(nbt_utils::NBT_KEY_PLAYER))
364 .map_or_else(
365 || {
366 if cli_args.verbose {
367 eprintln!(
368 "Player data not found in level.dat: {}",
369 file_path.display()
370 );
371 }
372 (None, "".to_string(), "".to_string())
373 },
374 |player_data| {
375 (
376 Some(player_data),
377 "Player (level.dat)".to_string(),
378 "level.dat".to_string(),
379 )
380 },
381 )
382 } else {
383 let player_uuid = file_path
385 .file_stem()
386 .and_then(|s| s.to_str())
387 .unwrap_or("UnknownPlayer")
388 .to_string();
389
390 let display_name = uuid::Uuid::parse_str(&player_uuid)
394 .ok()
395 .and_then(|u| user_cache.get(&u.to_string())) .map_or_else(
397 || player_uuid.clone(),
398 |name| format!("{name} ({player_uuid})"),
399 );
400
401 (
402 Some(nbt_root.as_compound()),
403 display_name,
404 file_path
405 .file_name()
406 .unwrap_or_default()
407 .to_string_lossy()
408 .into_owned(),
409 )
410 };
411
412 if let Some(player_nbt) = player_nbt_compound_opt {
413 let location_str = get_entity_pos_string(&player_nbt).unwrap_or(base_location_str); process_player_nbt_compound(
416 player_nbt,
417 task,
418 queries,
419 cli_args,
420 counter,
421 &source_id,
422 &location_str,
423 );
424 }
425}
426
427pub fn extract_single_player_uuid_from_level_dat(
429 level_dat_path: &Path,
430 cli_args: &CliArgs,
431) -> Option<String> {
432 if let Ok(file_data) = std::fs::read(level_dat_path) {
433 let mut decompressor = GzDecoder::new(file_data.as_slice());
434 let mut decompressed_data = Vec::new();
435 if decompressor.read_to_end(&mut decompressed_data).is_ok() {
436 let mut cursor = Cursor::new(decompressed_data.as_slice());
437 if let Ok(simdnbt::borrow::Nbt::Some(nbt_root)) = simdnbt::borrow::read(&mut cursor) {
440 if let Some(player_compound) = nbt_root
441 .compound(nbt_utils::NBT_KEY_PLAYER_DATA)
442 .and_then(|data_compound| data_compound.compound(nbt_utils::NBT_KEY_PLAYER))
443 {
444 return nbt_utils::get_uuid_from_nbt(&player_compound);
445 } else if cli_args.verbose {
446 eprintln!(
447 "Player NBT compound (Data.Player) not found in {}.",
448 level_dat_path.display()
449 );
450 }
451 }
452 }
453 }
454 None
455}
456
457fn process_player_nbt_compound(
459 player_nbt: simdnbt::borrow::NbtCompound,
460 task: &ScanTask,
461 item_queries: &[ItemFilter],
462 cli_args: &CliArgs,
463 counter: &mut Counter,
464 source_id: &str,
465 location_str: &str,
466) {
467 let mut summary_nodes = Vec::new();
468
469 if let Some(item_list) = player_nbt
470 .list(nbt_utils::NBT_KEY_INVENTORY)
471 .and_then(|l| l.compounds())
472 {
473 for item_compound in item_list {
474 collect_summary_node(
475 &item_compound,
476 cli_args,
477 item_queries,
478 &mut summary_nodes,
479 counter,
480 );
481 }
482 }
483
484 if let Some(item_list) = player_nbt
485 .list(nbt_utils::NBT_KEY_ENDER_ITEMS)
486 .and_then(|l| l.compounds())
487 {
488 for item_compound in item_list {
489 collect_summary_node(
490 &item_compound,
491 cli_args,
492 item_queries,
493 &mut summary_nodes,
494 counter,
495 );
496 }
497 }
498
499 if let Some(holder_compound) = player_nbt.compound(nbt_utils::NBT_KEY_EQUIPMENT) {
500 for (_key_in_holder, value_nbt) in holder_compound.iter() {
501 if let Some(actual_item_compound) = value_nbt.compound() {
502 collect_summary_node(
503 &actual_item_compound,
504 cli_args,
505 item_queries,
506 &mut summary_nodes,
507 counter,
508 );
509 }
510 }
511 }
512
513 print_per_source_summary_if_enabled(
514 cli_args,
515 &task.scope.dimension,
516 source_id,
517 location_str,
518 summary_nodes,
519 );
520}
521
522fn print_per_source_summary_if_enabled(
524 cli_args: &CliArgs,
525 dimension: &str,
526 source_id: &str,
527 source_location: &str,
528 summary_nodes: Vec<ItemSummaryNode>, ) {
530 if cli_args.per_source_summary && !summary_nodes.is_empty() {
531 let root_label = format!("[{dimension}] {source_id} @ {source_location}");
532 let mut root = ItemSummaryNode::new_root(root_label, summary_nodes);
533 root.collapse_leaves_recursive();
534 if let Err(e) = print_tree(&root) {
535 eprintln!("Error printing tree summary for {source_id}: {e}");
537 }
538 }
539}
540
541fn process_single_entity(
543 entity_nbt: simdnbt::borrow::NbtCompound,
544 task: &ScanTask,
545 queries: &[ItemFilter],
546 cli_args: &CliArgs,
547 counter: &mut Counter,
548) {
549 let Some(id_str) = entity_nbt.string(nbt_utils::NBT_KEY_ID) else {
550 return;
551 };
552 let id = id_str.to_string();
553 let pos_str =
554 get_entity_pos_string(&entity_nbt).unwrap_or_else(|| "Unknown Position".to_string());
555
556 let mut summary_nodes = Vec::new();
557 for list_field_name in &[nbt_utils::NBT_KEY_ITEMS, nbt_utils::NBT_KEY_INVENTORY] {
558 if let Some(item_list) = entity_nbt.list(list_field_name).and_then(|l| l.compounds()) {
559 for item_compound in item_list {
560 collect_summary_node(
561 &item_compound,
562 cli_args,
563 queries,
564 &mut summary_nodes,
565 counter,
566 );
567 }
568 }
569 }
570
571 if let Some(item_compound) = entity_nbt.compound(nbt_utils::NBT_KEY_ITEM) {
572 collect_summary_node(
573 &item_compound,
574 cli_args,
575 queries,
576 &mut summary_nodes,
577 counter,
578 );
579 }
580
581 if let Some(holder_compound) = entity_nbt.compound(nbt_utils::NBT_KEY_EQUIPMENT) {
582 for (_key_in_holder, value_nbt) in holder_compound.iter() {
583 if let Some(actual_item_compound) = value_nbt.compound() {
584 collect_summary_node(
585 &actual_item_compound,
586 cli_args,
587 queries,
588 &mut summary_nodes,
589 counter,
590 );
591 }
592 }
593 }
594
595 if let Some(passengers_list) = entity_nbt
596 .list(nbt_utils::NBT_KEY_PASSENGERS)
597 .and_then(|l| l.compounds())
598 {
599 for passenger_nbt in passengers_list {
600 process_single_entity(passenger_nbt, task, queries, cli_args, counter);
605 }
606 }
607
608 print_per_source_summary_if_enabled(
609 cli_args,
610 &task.scope.dimension,
611 &id,
612 &pos_str,
613 summary_nodes,
614 );
615}
616
617fn process_block_entity(
618 block_entity: simdnbt::borrow::NbtCompound,
619 task: &ScanTask,
620 item_queries: &[ItemFilter],
621 cli_args: &CliArgs,
622 counter: &mut Counter,
623) {
624 let id = block_entity
625 .string(nbt_utils::NBT_KEY_ID)
626 .unwrap()
627 .to_string();
628 let x = block_entity.int("x").unwrap();
629 let y = block_entity.int("y").unwrap();
630 let z = block_entity.int("z").unwrap();
631
632 let mut summary_nodes = Vec::new();
633 if let Some(items) = block_entity
634 .list(nbt_utils::NBT_KEY_ITEMS)
635 .and_then(|l| l.compounds())
636 {
637 for item in items {
638 collect_summary_node(&item, cli_args, item_queries, &mut summary_nodes, counter);
639 }
640 }
641
642 for single_item_field in &["item", "RecordItem", "Book"] {
643 if let Some(item) = block_entity.compound(single_item_field) {
644 collect_summary_node(&item, cli_args, item_queries, &mut summary_nodes, counter);
645 }
646 }
647
648 let location_str = format!("{x} {y} {z}");
649 print_per_source_summary_if_enabled(
650 cli_args,
651 &task.scope.dimension,
652 &id,
653 &location_str,
654 summary_nodes,
655 );
656}
657
658fn collect_summary_node(
661 item_nbt: &simdnbt::borrow::NbtCompound,
662 cli_args: &CliArgs,
663 queries: &[ItemFilter],
664 out_nodes: &mut Vec<ItemSummaryNode>,
665 global_counter: &mut Counter,
666) {
667 let id = item_nbt.string(nbt_utils::NBT_KEY_ID).unwrap().to_string();
668 let count = item_nbt.int(nbt_utils::NBT_KEY_COUNT).unwrap_or(1) as u64;
669
670 let matches_filter = if queries.is_empty() {
671 true
672 } else {
673 let valence_nbt = convert_simdnbt_to_valence_nbt(item_nbt);
674 queries.iter().any(|q| {
675 let id_ok = q.id.as_ref().is_none_or(|qid| qid == &id);
676 let nbt_ok = q
677 .required_nbt
678 .as_ref()
679 .is_none_or(|req| nbt_is_subset(&valence_nbt, req));
680 id_ok && nbt_ok
681 })
682 };
683
684 let mut children = Vec::new();
685
686 if let Some(components) = item_nbt.compound(nbt_utils::NBT_KEY_COMPONENTS) {
687 if let Some(nested_list) = components
688 .list(nbt_utils::NBT_KEY_MINECRAFT_CONTAINER)
689 .and_then(|l| l.compounds())
690 {
691 for nested_entry in nested_list {
692 if let Some(nested_item) = nested_entry.compound("item") {
693 collect_summary_node(
694 &nested_item,
695 cli_args,
696 queries,
697 &mut children,
698 global_counter,
699 );
700 }
701 }
702 }
703
704 if let Some(nested_list) = components
705 .list(nbt_utils::NBT_KEY_MINECRAFT_BUNDLE_CONTENTS)
706 .and_then(|l| l.compounds())
707 {
708 for nested_entry in nested_list {
709 collect_summary_node(
710 &nested_entry,
711 cli_args,
712 queries,
713 &mut children,
714 global_counter,
715 );
716 }
717 }
718 }
719
720 if matches_filter {
721 let nbt_components = item_nbt
722 .compound(nbt_utils::NBT_KEY_COMPONENTS)
723 .as_ref()
724 .map(convert_simdnbt_to_valence_nbt);
725
726 global_counter.add(id.clone(), nbt_components.as_ref(), count);
727
728 let snbt = if cli_args.show_nbt {
729 nbt_components
730 .map(|c| valence_nbt::snbt::to_snbt_string(&c))
731 .as_deref()
732 .map(escape_nbt_string)
733 } else {
734 None
735 };
736
737 let node = ItemSummaryNode::new_item(id.clone(), count, snbt, children);
738 out_nodes.push(node);
739 } else if !children.is_empty() {
740 out_nodes.extend(children);
741 }
742}
743
744pub fn nbt_is_subset(superset: &Value, subset: &Value) -> bool {
748 match (superset, subset) {
749 (Value::Compound(sup_map), Value::Compound(sub_map)) => {
751 sub_map.iter().all(|(field, sub_value)| {
752 sup_map
753 .get(field)
754 .is_some_and(|sup_value| nbt_is_subset(sup_value, sub_value))
755 })
756 }
757
758 (Value::List(superset_list), Value::List(subset_list)) => {
761 let mut used = vec![false; superset_list.len()];
763
764 subset_list.iter().all(|sub_element| {
765 if let Some((idx, _)) = superset_list.iter().enumerate().find(|(i, sup_element)| {
767 !used[*i] && nbt_is_subset(&sup_element.to_value(), &sub_element.to_value())
768 }) {
769 used[idx] = true;
770 true
771 } else {
772 false
773 }
774 })
775 }
776
777 _ => superset == subset,
778 }
779}
780
781pub fn escape_nbt_string(s: &str) -> String {
783 s.chars()
784 .flat_map(|c| match c {
785 '\\' => Some("\\\\".to_string()),
786 '\n' => Some("\\n".to_string()),
787 '\r' => Some("\\r".to_string()),
788 '\t' => Some("\\t".to_string()),
789 c if c.is_control() => Some(format!("\\u{:04x}", c as u32)),
790 _ => Some(c.to_string()),
791 })
792 .collect::<String>()
793}
794
795#[cfg(test)]
796mod tests {
797 use super::nbt_is_subset;
798 use valence_nbt::Value;
799 use valence_nbt::snbt::from_snbt_str;
800
801 fn parse(s: &str) -> Value {
802 from_snbt_str(s).expect("Failed to parse SNBT")
803 }
804
805 #[test]
806 fn simple_compound_subset() {
807 let sup = parse("{a:1, b:2, c:3}");
808 let sub = parse("{a:1, c:3}");
809 assert!(nbt_is_subset(&sup, &sub));
810 }
811
812 #[test]
813 fn compound_missing_key_should_fail() {
814 let sup = parse("{a:1, b:2}");
815 let sub = parse("{a:1, c:3}");
816 assert!(!nbt_is_subset(&sup, &sub));
817 }
818
819 #[test]
820 fn unordered_list_subset() {
821 let sup = parse("[1, 2, 3, 4]");
822 let sub = parse("[4, 2]");
823 assert!(nbt_is_subset(&sup, &sub));
824 }
825
826 #[test]
827 fn list_insufficient_elements_should_fail() {
828 let sup = parse("[1, 2, 2]");
829 let sub = parse("[2, 2, 2]");
830 assert!(!nbt_is_subset(&sup, &sub));
831 }
832
833 #[test]
834 fn nested_structures_subset() {
835 let sup = parse("{x:{y:[{z:1}, {z:2}]}, w:5}");
836 let sub = parse("{x:{y:[{z:2}]}}");
837 assert!(nbt_is_subset(&sup, &sub));
838 }
839
840 #[test]
841 fn primitive_equality_match_and_mismatch() {
842 let sup = parse("123");
843 let sub = parse("123");
844 assert!(nbt_is_subset(&sup, &sub));
845
846 let sup2 = parse("123");
847 let sub2 = parse("456");
848 assert!(!nbt_is_subset(², &sub2));
849 }
850
851 #[test]
852 fn mismatched_types_should_fail() {
853 let sup = parse("{a:1}");
854 let sub = parse("[1]");
855 assert!(!nbt_is_subset(&sup, &sub));
856 }
857
858 #[test]
859 fn empty_list_subset() {
860 let sup = parse("[1,2,3]");
861 let sub = parse("[]");
862 assert!(nbt_is_subset(&sup, &sub));
863 }
864
865 #[test]
866 fn non_empty_list_on_empty_should_fail() {
867 let sup = parse("[]");
868 let sub = parse("[1]");
869 assert!(!nbt_is_subset(&sup, &sub));
870 }
871
872 #[test]
873 fn empty_compound_subset() {
874 let sup = parse("{a:1}");
875 let sub = parse("{}");
876 assert!(nbt_is_subset(&sup, &sub));
877 }
878
879 #[test]
880 fn byte_array_exact_match() {
881 let sup = parse("[I;1,2,3]");
882 let sub = parse("[I;1,2,3]");
883 assert!(nbt_is_subset(&sup, &sub));
884 }
885
886 #[test]
887 fn byte_array_partial_should_fail() {
888 let sup = parse("[I;1,2,3]");
889 let sub = parse("[I;2,3]");
890 assert!(!nbt_is_subset(&sup, &sub));
891 }
892
893 #[test]
894 fn byte_array_missing_element_should_fail() {
895 let sup = parse("[I;1,2]");
896 let sub = parse("[I;1,2,3]");
897 assert!(!nbt_is_subset(&sup, &sub));
898 }
899
900 #[test]
901 fn mixed_list_types_should_fail_to_parse() {
902 let res = valence_nbt::snbt::from_snbt_str("[1, \"a\"]");
903 assert!(res.is_err(), "Mixed-type list unexpectedly parsed");
904 }
905
906 #[test]
907 fn int_array_vs_byte_array_should_fail() {
908 let sup = parse("[I;1,2,3]");
909 let sub = parse("[B;1b,2b,3b]");
910 assert!(!nbt_is_subset(&sup, &sub));
911 }
912
913 #[test]
914 fn nested_empty_compound() {
915 let sup = parse("{a:{b:{}}}");
916 let sub = parse("{a:{b:{}}}");
917 assert!(nbt_is_subset(&sup, &sub));
918 }
919
920 #[test]
921 fn deeply_nested_empty_list() {
922 let sup = parse("{a:{b:[[],[1]]}}");
923 let sub = parse("{a:{b:[[]]}}");
924 assert!(nbt_is_subset(&sup, &sub));
925 }
926
927 #[test]
928 fn numeric_type_coercion_should_fail() {
929 let res = valence_nbt::snbt::from_snbt_str("[1b, 2, 3s]");
930 assert!(
931 res.is_err(),
932 "Parser unexpectedly accepted mixed numeric types"
933 );
934 }
935
936 #[test]
937 fn long_array_partial_should_fail() {
938 let sup = parse("[L;9223372036854775807l,0l]");
939 let sub = parse("[L;0l]");
940 assert!(!nbt_is_subset(&sup, &sub));
941 }
942
943 #[test]
944 fn empty_string_vs_non_empty_should_fail() {
945 let sup = parse("{text:\"\"}");
946 let sub = parse("{text:\"something\"}");
947 assert!(!nbt_is_subset(&sup, &sub));
948 }
949
950 #[test]
951 fn float_vs_double_zero_should_fail() {
952 let sup = parse("{val:0.0f}");
953 let sub = parse("{val:0.0d}");
954 assert!(!nbt_is_subset(&sup, &sub));
955 }
956
957 #[test]
958 fn compound_with_empty_list_and_nested_empty_compound() {
959 let sup = parse("{data:{items:[], meta:{}}}");
960 let sub = parse("{data:{items:[]}}");
961 assert!(nbt_is_subset(&sup, &sub));
962 }
963
964 #[test]
965 fn unicode_string_match() {
966 let sup = parse("{msg:\"こんにちは\"}");
967 let sub = parse("{msg:\"こんにちは\"}");
968 assert!(nbt_is_subset(&sup, &sub));
969 }
970}