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 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}