Skip to main content

nbt_rust/
mcstructure.rs

1use crate::config::ParseMode;
2use crate::error::{Error, Result};
3use crate::root::RootTag;
4use crate::tag::{CompoundTag, ListTag, Tag, TagType};
5
6#[derive(Debug, Clone, PartialEq, Eq, Default)]
7pub struct McStructureSemanticReport {
8    pub size: [usize; 3],
9    pub volume: usize,
10    pub layer_count: usize,
11    pub palette_len: usize,
12    pub has_default_palette: bool,
13    pub no_block_indices: usize,
14    pub out_of_range_indices: usize,
15    pub invalid_block_position_data_keys: usize,
16}
17
18pub fn validate_mcstructure_root(
19    root: &RootTag,
20    parse_mode: ParseMode,
21) -> Result<McStructureSemanticReport> {
22    validate_mcstructure_tag(&root.payload, parse_mode)
23}
24
25pub fn validate_mcstructure_tag(
26    payload: &Tag,
27    parse_mode: ParseMode,
28) -> Result<McStructureSemanticReport> {
29    let top = expect_compound(payload, "mcstructure_root_payload_type")?;
30    let _format_version = validate_format_version(top, parse_mode)?;
31    let size = parse_size(top)?;
32    validate_origin(top)?;
33    let volume = checked_volume(size)?;
34
35    let structure = expect_compound(
36        required(top, "structure", "mcstructure_structure_missing")?,
37        "mcstructure_structure_type",
38    )?;
39
40    let block_indices = expect_list(
41        required(
42            structure,
43            "block_indices",
44            "mcstructure_block_indices_missing",
45        )?,
46        "mcstructure_block_indices_type",
47    )?;
48    if block_indices.element_type != TagType::List {
49        return Err(Error::InvalidStructureShape {
50            detail: "mcstructure_block_indices_not_list_of_list",
51        });
52    }
53    if block_indices.elements.len() != 2 {
54        return Err(Error::InvalidStructureShape {
55            detail: "mcstructure_block_indices_layer_count_must_be_two",
56        });
57    }
58
59    let (palette_len, has_default_palette, block_position_data) =
60        resolve_palette_semantics(structure, parse_mode)?;
61
62    let mut report = McStructureSemanticReport {
63        size,
64        volume,
65        layer_count: block_indices.elements.len(),
66        palette_len,
67        has_default_palette,
68        ..McStructureSemanticReport::default()
69    };
70
71    for layer_tag in &block_indices.elements {
72        let layer = expect_list(layer_tag, "mcstructure_block_indices_layer_type")?;
73        if layer.element_type != TagType::Int && parse_mode == ParseMode::Strict {
74            return Err(Error::InvalidStructureShape {
75                detail: "mcstructure_block_indices_layer_not_int_list",
76            });
77        }
78        if layer.elements.len() != volume {
79            return Err(Error::InvalidStructureShape {
80                detail: "mcstructure_block_indices_length_mismatch",
81            });
82        }
83        validate_layer_indices(layer, palette_len, parse_mode, &mut report)?;
84    }
85
86    if let Some(position_data) = block_position_data {
87        validate_block_position_data_keys(position_data, volume, parse_mode, &mut report)?;
88    }
89
90    Ok(report)
91}
92
93pub fn zyx_flatten_index(size: [usize; 3], x: usize, y: usize, z: usize) -> Result<usize> {
94    if x >= size[0] || y >= size[1] || z >= size[2] {
95        return Err(Error::InvalidStructureShape {
96            detail: "mcstructure_coordinate_out_of_bounds",
97        });
98    }
99    let base = x
100        .checked_mul(size[1])
101        .and_then(|v| v.checked_add(y))
102        .ok_or(Error::LengthOverflow {
103            field: "mcstructure_flatten_index",
104            max: usize::MAX,
105            actual: usize::MAX,
106        })?;
107    let flat = base.checked_mul(size[2]).ok_or(Error::LengthOverflow {
108        field: "mcstructure_flatten_index",
109        max: usize::MAX,
110        actual: usize::MAX,
111    })?;
112    flat.checked_add(z).ok_or(Error::LengthOverflow {
113        field: "mcstructure_flatten_index",
114        max: usize::MAX,
115        actual: usize::MAX,
116    })
117}
118
119pub fn zyx_unflatten_index(size: [usize; 3], flat_index: usize) -> Result<(usize, usize, usize)> {
120    let volume = checked_volume(size)?;
121    if flat_index >= volume {
122        return Err(Error::InvalidStructureShape {
123            detail: "mcstructure_flat_index_out_of_bounds",
124        });
125    }
126    let yz_span = size[1] * size[2];
127    let x = flat_index / yz_span;
128    let rem = flat_index % yz_span;
129    let y = rem / size[2];
130    let z = rem % size[2];
131    Ok((x, y, z))
132}
133
134fn required<'a>(
135    compound: &'a CompoundTag,
136    key: &'static str,
137    detail: &'static str,
138) -> Result<&'a Tag> {
139    compound
140        .get(key)
141        .ok_or(Error::InvalidStructureShape { detail })
142}
143
144fn expect_compound<'a>(tag: &'a Tag, context: &'static str) -> Result<&'a CompoundTag> {
145    match tag {
146        Tag::Compound(value) => Ok(value),
147        other => Err(Error::UnexpectedType {
148            context,
149            expected_id: TagType::Compound.id(),
150            actual_id: other.tag_type().id(),
151        }),
152    }
153}
154
155fn expect_list<'a>(tag: &'a Tag, context: &'static str) -> Result<&'a ListTag> {
156    match tag {
157        Tag::List(value) => Ok(value),
158        other => Err(Error::UnexpectedType {
159            context,
160            expected_id: TagType::List.id(),
161            actual_id: other.tag_type().id(),
162        }),
163    }
164}
165
166fn expect_int(tag: &Tag, context: &'static str) -> Result<i32> {
167    match tag {
168        Tag::Int(value) => Ok(*value),
169        other => Err(Error::UnexpectedType {
170            context,
171            expected_id: TagType::Int.id(),
172            actual_id: other.tag_type().id(),
173        }),
174    }
175}
176
177fn parse_size(top: &CompoundTag) -> Result<[usize; 3]> {
178    let size = expect_list(
179        required(top, "size", "mcstructure_size_missing")?,
180        "mcstructure_size_type",
181    )?;
182    if size.element_type != TagType::Int || size.elements.len() != 3 {
183        return Err(Error::InvalidStructureShape {
184            detail: "mcstructure_size_must_be_int3",
185        });
186    }
187
188    let mut out = [0usize; 3];
189    for (index, value_tag) in size.elements.iter().enumerate() {
190        let value = expect_int(value_tag, "mcstructure_size_value_type")?;
191        if value < 0 {
192            return Err(Error::InvalidStructureShape {
193                detail: "mcstructure_size_negative_component",
194            });
195        }
196        out[index] = value as usize;
197    }
198    Ok(out)
199}
200
201fn validate_format_version(top: &CompoundTag, parse_mode: ParseMode) -> Result<i32> {
202    let format_version = required(top, "format_version", "mcstructure_format_version_missing")?;
203    let value = expect_int(format_version, "mcstructure_format_version_type")?;
204    if parse_mode == ParseMode::Strict && value != 1 {
205        return Err(Error::InvalidStructureShape {
206            detail: "mcstructure_format_version_must_be_one",
207        });
208    }
209    Ok(value)
210}
211
212fn validate_origin(top: &CompoundTag) -> Result<()> {
213    let origin = expect_list(
214        required(
215            top,
216            "structure_world_origin",
217            "mcstructure_world_origin_missing",
218        )?,
219        "mcstructure_world_origin_type",
220    )?;
221    if origin.element_type != TagType::Int || origin.elements.len() != 3 {
222        return Err(Error::InvalidStructureShape {
223            detail: "mcstructure_world_origin_must_be_int3",
224        });
225    }
226    for value in &origin.elements {
227        let _ = expect_int(value, "mcstructure_world_origin_value_type")?;
228    }
229    Ok(())
230}
231
232fn checked_volume(size: [usize; 3]) -> Result<usize> {
233    size[0]
234        .checked_mul(size[1])
235        .and_then(|value| value.checked_mul(size[2]))
236        .ok_or(Error::LengthOverflow {
237            field: "mcstructure_volume",
238            max: usize::MAX,
239            actual: usize::MAX,
240        })
241}
242
243fn resolve_palette_semantics(
244    structure: &CompoundTag,
245    parse_mode: ParseMode,
246) -> Result<(usize, bool, Option<&CompoundTag>)> {
247    let palette = expect_compound(
248        required(structure, "palette", "mcstructure_palette_missing")?,
249        "mcstructure_palette_type",
250    )?;
251    let Some(default_tag) = palette.get("default") else {
252        if parse_mode == ParseMode::Strict {
253            return Err(Error::InvalidStructureShape {
254                detail: "mcstructure_default_palette_missing",
255            });
256        }
257        return Ok((0, false, None));
258    };
259
260    let default = expect_compound(default_tag, "mcstructure_default_palette_type")?;
261    let block_palette = expect_list(
262        required(
263            default,
264            "block_palette",
265            "mcstructure_block_palette_missing",
266        )?,
267        "mcstructure_block_palette_type",
268    )?;
269    if block_palette.element_type != TagType::Compound {
270        return Err(Error::InvalidStructureShape {
271            detail: "mcstructure_block_palette_not_compound_list",
272        });
273    }
274
275    let block_position_data = match default.get("block_position_data") {
276        Some(tag) => Some(expect_compound(
277            tag,
278            "mcstructure_block_position_data_type",
279        )?),
280        None => None,
281    };
282
283    Ok((block_palette.elements.len(), true, block_position_data))
284}
285
286fn validate_layer_indices(
287    layer: &ListTag,
288    palette_len: usize,
289    parse_mode: ParseMode,
290    report: &mut McStructureSemanticReport,
291) -> Result<()> {
292    for index_tag in &layer.elements {
293        let index = match index_tag {
294            Tag::Int(value) => *value,
295            _ if parse_mode == ParseMode::Compatible => 0,
296            _ => {
297                return Err(Error::UnexpectedType {
298                    context: "mcstructure_block_index_value_type",
299                    expected_id: TagType::Int.id(),
300                    actual_id: index_tag.tag_type().id(),
301                })
302            }
303        };
304        if index == -1 {
305            report.no_block_indices += 1;
306            continue;
307        }
308        if index < -1 || (index as usize) >= palette_len {
309            if parse_mode == ParseMode::Strict {
310                return Err(Error::InvalidPaletteIndex { index, palette_len });
311            }
312            report.out_of_range_indices += 1;
313        }
314    }
315    Ok(())
316}
317
318fn validate_block_position_data_keys(
319    block_position_data: &CompoundTag,
320    volume: usize,
321    parse_mode: ParseMode,
322    report: &mut McStructureSemanticReport,
323) -> Result<()> {
324    for key in block_position_data.keys() {
325        let flat = match key.parse::<usize>() {
326            Ok(value) => value,
327            Err(_) => {
328                if parse_mode == ParseMode::Strict {
329                    return Err(Error::InvalidStructureShape {
330                        detail: "mcstructure_block_position_data_key_not_usize",
331                    });
332                }
333                report.invalid_block_position_data_keys += 1;
334                continue;
335            }
336        };
337        if flat >= volume {
338            if parse_mode == ParseMode::Strict {
339                return Err(Error::InvalidStructureShape {
340                    detail: "mcstructure_block_position_data_key_out_of_bounds",
341                });
342            }
343            report.invalid_block_position_data_keys += 1;
344            continue;
345        }
346
347        // Contract check: declared ZYX flatten/unflatten mapping must stay stable.
348        let (x, y, z) = zyx_unflatten_index(report.size, flat)?;
349        let roundtrip = zyx_flatten_index(report.size, x, y, z)?;
350        if roundtrip != flat {
351            return Err(Error::InvalidStructureShape {
352                detail: "mcstructure_zyx_roundtrip_mismatch",
353            });
354        }
355    }
356    Ok(())
357}
358
359#[cfg(test)]
360mod tests {
361    use indexmap::IndexMap;
362
363    use super::*;
364
365    fn build_valid_mcstructure_root() -> RootTag {
366        let mut root = IndexMap::new();
367        root.insert("format_version".to_string(), Tag::Int(1));
368        root.insert(
369            "size".to_string(),
370            Tag::List(
371                ListTag::new(TagType::Int, vec![Tag::Int(2), Tag::Int(1), Tag::Int(2)]).unwrap(),
372            ),
373        );
374
375        let mut structure = IndexMap::new();
376        let primary = Tag::List(
377            ListTag::new(
378                TagType::Int,
379                vec![Tag::Int(0), Tag::Int(1), Tag::Int(-1), Tag::Int(0)],
380            )
381            .unwrap(),
382        );
383        let secondary = Tag::List(
384            ListTag::new(
385                TagType::Int,
386                vec![Tag::Int(-1), Tag::Int(-1), Tag::Int(-1), Tag::Int(-1)],
387            )
388            .unwrap(),
389        );
390        structure.insert(
391            "block_indices".to_string(),
392            Tag::List(ListTag::new(TagType::List, vec![primary, secondary]).unwrap()),
393        );
394
395        let mut default = IndexMap::new();
396        default.insert(
397            "block_palette".to_string(),
398            Tag::List(
399                ListTag::new(
400                    TagType::Compound,
401                    vec![
402                        Tag::Compound(IndexMap::new()),
403                        Tag::Compound(IndexMap::new()),
404                    ],
405                )
406                .unwrap(),
407            ),
408        );
409        let mut palette = IndexMap::new();
410        palette.insert("default".to_string(), Tag::Compound(default));
411        structure.insert("palette".to_string(), Tag::Compound(palette));
412
413        root.insert("structure".to_string(), Tag::Compound(structure));
414        root.insert(
415            "structure_world_origin".to_string(),
416            Tag::List(
417                ListTag::new(TagType::Int, vec![Tag::Int(0), Tag::Int(64), Tag::Int(0)]).unwrap(),
418            ),
419        );
420        RootTag::new("", Tag::Compound(root))
421    }
422
423    #[test]
424    fn strict_validator_accepts_valid_fixture_shape() {
425        let root = build_valid_mcstructure_root();
426        let report = validate_mcstructure_root(&root, ParseMode::Strict).unwrap();
427        assert_eq!(report.size, [2, 1, 2]);
428        assert_eq!(report.volume, 4);
429        assert_eq!(report.layer_count, 2);
430        assert_eq!(report.palette_len, 2);
431        assert_eq!(report.no_block_indices, 5);
432        assert_eq!(report.out_of_range_indices, 0);
433    }
434
435    #[test]
436    fn strict_validator_requires_format_version() {
437        let mut root = build_valid_mcstructure_root();
438        let top = match &mut root.payload {
439            Tag::Compound(value) => value,
440            _ => unreachable!(),
441        };
442        top.shift_remove("format_version");
443
444        let err = validate_mcstructure_root(&root, ParseMode::Strict).unwrap_err();
445        assert!(matches!(
446            err,
447            Error::InvalidStructureShape {
448                detail: "mcstructure_format_version_missing"
449            }
450        ));
451    }
452
453    #[test]
454    fn strict_validator_rejects_non_one_format_version() {
455        let mut root = build_valid_mcstructure_root();
456        let top = match &mut root.payload {
457            Tag::Compound(value) => value,
458            _ => unreachable!(),
459        };
460        top.insert("format_version".to_string(), Tag::Int(2));
461
462        let err = validate_mcstructure_root(&root, ParseMode::Strict).unwrap_err();
463        assert!(matches!(
464            err,
465            Error::InvalidStructureShape {
466                detail: "mcstructure_format_version_must_be_one"
467            }
468        ));
469    }
470
471    #[test]
472    fn compatible_validator_accepts_non_one_format_version() {
473        let mut root = build_valid_mcstructure_root();
474        let top = match &mut root.payload {
475            Tag::Compound(value) => value,
476            _ => unreachable!(),
477        };
478        top.insert("format_version".to_string(), Tag::Int(2));
479
480        let report = validate_mcstructure_root(&root, ParseMode::Compatible).unwrap();
481        assert_eq!(report.volume, 4);
482    }
483
484    #[test]
485    fn strict_validator_rejects_missing_default_palette() {
486        let mut root = build_valid_mcstructure_root();
487        let top = match &mut root.payload {
488            Tag::Compound(value) => value,
489            _ => unreachable!(),
490        };
491        let structure = match top.get_mut("structure").unwrap() {
492            Tag::Compound(value) => value,
493            _ => unreachable!(),
494        };
495        let palette = match structure.get_mut("palette").unwrap() {
496            Tag::Compound(value) => value,
497            _ => unreachable!(),
498        };
499        palette.shift_remove("default");
500
501        let err = validate_mcstructure_root(&root, ParseMode::Strict).unwrap_err();
502        assert!(matches!(
503            err,
504            Error::InvalidStructureShape {
505                detail: "mcstructure_default_palette_missing"
506            }
507        ));
508    }
509
510    #[test]
511    fn compatible_validator_accepts_missing_default_palette() {
512        let mut root = build_valid_mcstructure_root();
513        let top = match &mut root.payload {
514            Tag::Compound(value) => value,
515            _ => unreachable!(),
516        };
517        let structure = match top.get_mut("structure").unwrap() {
518            Tag::Compound(value) => value,
519            _ => unreachable!(),
520        };
521        let palette = match structure.get_mut("palette").unwrap() {
522            Tag::Compound(value) => value,
523            _ => unreachable!(),
524        };
525        palette.shift_remove("default");
526
527        let report = validate_mcstructure_root(&root, ParseMode::Compatible).unwrap();
528        assert!(!report.has_default_palette);
529        assert_eq!(report.palette_len, 0);
530    }
531
532    #[test]
533    fn strict_validator_rejects_out_of_range_palette_index() {
534        let mut root = build_valid_mcstructure_root();
535        let top = match &mut root.payload {
536            Tag::Compound(value) => value,
537            _ => unreachable!(),
538        };
539        let structure = match top.get_mut("structure").unwrap() {
540            Tag::Compound(value) => value,
541            _ => unreachable!(),
542        };
543        let layers = match structure.get_mut("block_indices").unwrap() {
544            Tag::List(value) => value,
545            _ => unreachable!(),
546        };
547        let primary = match layers.elements.get_mut(0).unwrap() {
548            Tag::List(value) => value,
549            _ => unreachable!(),
550        };
551        primary.elements[0] = Tag::Int(99);
552
553        let err = validate_mcstructure_root(&root, ParseMode::Strict).unwrap_err();
554        assert!(matches!(
555            err,
556            Error::InvalidPaletteIndex {
557                index: 99,
558                palette_len: 2
559            }
560        ));
561    }
562
563    #[test]
564    fn compatible_validator_falls_back_for_non_int_layer_entries() {
565        let mut root = build_valid_mcstructure_root();
566        let top = match &mut root.payload {
567            Tag::Compound(value) => value,
568            _ => unreachable!(),
569        };
570        let structure = match top.get_mut("structure").unwrap() {
571            Tag::Compound(value) => value,
572            _ => unreachable!(),
573        };
574        let layers = match structure.get_mut("block_indices").unwrap() {
575            Tag::List(value) => value,
576            _ => unreachable!(),
577        };
578
579        let non_int_layer = Tag::List(
580            ListTag::new(
581                TagType::Byte,
582                vec![Tag::Byte(5), Tag::Byte(6), Tag::Byte(7), Tag::Byte(8)],
583            )
584            .unwrap(),
585        );
586        layers.elements[0] = non_int_layer;
587
588        let report = validate_mcstructure_root(&root, ParseMode::Compatible).unwrap();
589        assert_eq!(report.out_of_range_indices, 0);
590    }
591
592    #[test]
593    fn compatible_validator_counts_out_of_range_palette_indices() {
594        let mut root = build_valid_mcstructure_root();
595        let top = match &mut root.payload {
596            Tag::Compound(value) => value,
597            _ => unreachable!(),
598        };
599        let structure = match top.get_mut("structure").unwrap() {
600            Tag::Compound(value) => value,
601            _ => unreachable!(),
602        };
603        let layers = match structure.get_mut("block_indices").unwrap() {
604            Tag::List(value) => value,
605            _ => unreachable!(),
606        };
607        let primary = match layers.elements.get_mut(0).unwrap() {
608            Tag::List(value) => value,
609            _ => unreachable!(),
610        };
611        primary.elements[0] = Tag::Int(-2);
612        primary.elements[1] = Tag::Int(9);
613
614        let report = validate_mcstructure_root(&root, ParseMode::Compatible).unwrap();
615        assert_eq!(report.out_of_range_indices, 2);
616    }
617
618    #[test]
619    fn zyx_flatten_and_unflatten_roundtrip() {
620        let size = [2, 3, 4];
621        for x in 0..size[0] {
622            for y in 0..size[1] {
623                for z in 0..size[2] {
624                    let flat = zyx_flatten_index(size, x, y, z).unwrap();
625                    let (rx, ry, rz) = zyx_unflatten_index(size, flat).unwrap();
626                    assert_eq!((rx, ry, rz), (x, y, z));
627                }
628            }
629        }
630    }
631}