fix(map): reject duplicate YAML keys via a SafeLoader duplicate-check

This commit is contained in:
2026-09-20 18:00:27 +02:00
parent 53c6dc0b43
commit ee22f7548a
2 changed files with 55 additions and 1 deletions
+34 -1
View File
@@ -12,10 +12,14 @@ from typing import TYPE_CHECKING
import yaml
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
from yaml.nodes import ScalarNode
if TYPE_CHECKING:
from collections.abc import Hashable
from pathlib import Path
from yaml.nodes import MappingNode
Pos = tuple[int, int] # (row, col), 0-indexed
_ELEVATION_CHARS = frozenset("0123456789.") # digit = height, '.' = ground level
@@ -38,6 +42,29 @@ class MapValidationError(Exception):
self.reason = reason
class _NoDuplicateLoader(yaml.SafeLoader):
"""SafeLoader that rejects duplicate mapping keys (PyYAML is last-wins)."""
def _construct_mapping(
loader: _NoDuplicateLoader, node: MappingNode, *, deep: bool = False
) -> dict[Hashable, object]:
seen: set[str] = set()
for key_node, _ in node.value:
if isinstance(key_node, ScalarNode):
key = loader.construct_scalar(key_node)
if key in seen:
msg = f"duplicate mapping key: {key!r}"
raise yaml.constructor.ConstructorError(None, None, msg, key_node.start_mark)
seen.add(key)
return yaml.constructor.SafeConstructor.construct_mapping(loader, node, deep)
_NoDuplicateLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping
)
class TerrainType(BaseModel):
"""What one square means for movement and sight lines."""
@@ -177,7 +204,13 @@ def load_map(path: Path) -> MapSpec:
msg = f"cannot read file: {exc}"
raise MapValidationError(path, msg) from exc
try:
data: object = yaml.safe_load(text)
# _NoDuplicateLoader subclasses SafeLoader: no arbitrary object
# instantiation, it only adds duplicate-key rejection (S506 is a
# false positive for this loader).
data: object = yaml.load(
text,
Loader=_NoDuplicateLoader, # noqa: S506
)
except yaml.YAMLError as exc:
msg = f"invalid YAML: {exc}"
raise MapValidationError(path, msg) from exc