Skip to content

Commit

Permalink
Enforce valid resource location names
Browse files Browse the repository at this point in the history
  • Loading branch information
SmylerMC committed Aug 10, 2024
1 parent 3fa321d commit 2a361fe
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 4 deletions.
47 changes: 44 additions & 3 deletions litemapy/minecraft.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(self, block_id: str, **properties: str) -> None:
:param block_id: the identifier of the block (e.g. *minecraft:stone*)
:param properties: the properties of the block state as keyword parameters (e.g. *facing="north"*)
"""
self.__block_id = block_id
self.__block_id = assert_valid_identifier(block_id)
self.__properties = DiscriminatingDictionary(self.__validate, properties)
self.__identifier_cache = None

Expand All @@ -53,7 +53,7 @@ def from_nbt(nbt: Compound) -> 'BlockState':
"""
Reads a :class:`BlockState` from an nbt tag.
"""
block_id = str(nbt["Name"])
block_id = assert_valid_identifier(str(nbt["Name"]))
if "Properties" in nbt:
properties: dict[str, str] = {str(k): str(v) for k, v in nbt["Properties"].items()}
else:
Expand All @@ -80,6 +80,7 @@ def with_id(self, block_id: str) -> 'BlockState':
:param block_id: the block id for the new :class:`BlockState`
"""
assert_valid_identifier(block_id)
return BlockState(block_id, **self.__properties)

def with_properties(self, **properties: Optional[str]) -> 'BlockState':
Expand Down Expand Up @@ -163,6 +164,7 @@ class Entity:
(e.g. a sheep has a tag for its color and one indicating whether it has been sheared).
"""

_id: str
_data: Compound
_position: EntityPosition
_rotation: EntityRotation
Expand Down Expand Up @@ -192,7 +194,7 @@ def __init__(self, str_or_nbt: Union[str, Compound]) -> None:
if 'Motion' not in keys:
self._data['Motion'] = List[Double]([Double(0.), Double(0.), Double(0.)])

self._id = self._data['id']
self._id = assert_valid_identifier(self._data['id'])
position = [float(coord) for coord in self._data['Pos']]
self._position = (position[0], position[1], position[2])
rotation = [float(coord) for coord in self._data['Rotation']]
Expand Down Expand Up @@ -389,6 +391,45 @@ def position(self, position: BlockPosition):
self._data[coord] = Int(self._position[index])


def is_valid_identifier(identifier: str) -> bool:
"""
Checks if a string is a valid identifier (aka. ResourceLocation in Mojmap).
"""
# Check taken from Minecraft 1.20.1 ResourceLocation
allowed_chars = "_-abcdefghijklmnopqrstuvwxyz0123456789.:"
separator = False
for char in identifier:
if char not in allowed_chars:
return False
if char == ":":
# Now parsing the path part
separator = True
allowed_chars = "_-abcdefghijklmnopqrstuvwxyz0123456789./"
return separator


class InvalidIdentifier(ValueError):
identifier: str

def __init__(self, identifier: str) -> None:
super().__init__(f'Invalid identifier "{identifier}"')


def assert_valid_identifier(identifier: str) -> str:
"""
Checks whether a string is a valid identifier (aka ResourceLocation in Mojmap),
and raises InvalidIdentifierError if it is not.
The name "identifier" is from Yarn mappings but makes more sens in this context.
:returns: the identifier
:raises CorruptedSchematicError: if provided string is not a valid identifier
"""

if not is_valid_identifier(identifier):
raise InvalidIdentifier(identifier)
return identifier


class RequiredKeyMissingException(Exception):

def __init__(self, key: str, message: str = 'The required key is missing in the (Tile)Entity\'s NBT Compound'):
Expand Down
33 changes: 32 additions & 1 deletion tests/test_minecraft.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import pytest

from litemapy import BlockState
from litemapy.minecraft import is_valid_identifier
from litemapy.minecraft import InvalidIdentifier
from litemapy.schematic import AIR


def test_blockstate_initialization():
# TODO Split into multiple smaller tests
prop = {"test1": "testval", "test2": "testval2"}
b = BlockState("minecraft:stone", **prop)
assert len(prop) == len(b)
for k, v in prop.items():
assert b[k] == v


def test_cannot_create_blockstate_with_invalid_id():
ids = (
"",
"minecraft stone",
"stone",
"minecraft:stone[property=value]",
)
for id_ in ids:
with pytest.raises(InvalidIdentifier):
BlockState(id_, prop="val")
with pytest.raises(InvalidIdentifier):
AIR.with_id(id_)


def test_blockstate_nbt_is_identity():
prop = {"test1": "testval", "test2": "testval2"}
blockstate_1 = BlockState("minecraft:stone", **prop)
Expand All @@ -34,3 +52,16 @@ def test_blockstate_is_hashable():
assert state1 == state2
assert hash(state1) == hash(state2)
assert hash(state1) == hash(state1)


def test_is_valid_identifier():
assert is_valid_identifier("minecraft:air")
assert is_valid_identifier("minecraft:stone_cutter")
assert is_valid_identifier("terramap:path/is/allowed_slashes.png")
assert is_valid_identifier("weird.mod-id:both_are_allowed-dashes-and_underscores.and.dots")

assert not is_valid_identifier("")
assert not is_valid_identifier(" ")
assert not is_valid_identifier("minecraft:minecraft:stone")
assert not is_valid_identifier("minecraft:oak_stairs[facing=north]")
assert not is_valid_identifier("minecraft")

0 comments on commit 2a361fe

Please sign in to comment.