From 86eaf9e20697dc1ca3abb5493282fd002d178f0c Mon Sep 17 00:00:00 2001 From: Teemu R Date: Fri, 27 Jan 2023 00:08:57 +0100 Subject: [PATCH] Do not crash on extranous urn components (#1693) Store the unexpected components inside 'unexpected', e.g., urn:miot-spec-v2:service:device-information:00007801:yeelink-sw1:1:0000C809 spotted for `yeelink.switch.sw1`. --- miio/devtools/simulators/miotsimulator.py | 16 +++++++++++++++- miio/miot_models.py | 15 ++++++++++++--- miio/tests/test_miot_models.py | 22 ++++++++++++++++++++-- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/miio/devtools/simulators/miotsimulator.py b/miio/devtools/simulators/miotsimulator.py index b6e4800a8..6d2ed3d97 100644 --- a/miio/devtools/simulators/miotsimulator.py +++ b/miio/devtools/simulators/miotsimulator.py @@ -1,4 +1,5 @@ import asyncio +import json import logging import random from collections import defaultdict @@ -271,7 +272,20 @@ def miot_simulator(file, model): dev = SimulatedDeviceModel.parse_raw(data) else: cloud = MiotCloud() - dev = SimulatedDeviceModel.parse_obj(cloud.get_model_schema(model)) + try: + schema = cloud.get_model_schema(model) + except Exception as ex: + _LOGGER.error("Unable to get schema: %s" % ex) + return + try: + dev = SimulatedDeviceModel.parse_obj(schema) + except Exception as ex: + # this is far from optimal, but considering this is a developer tool it can be fixed later + fn = f"/tmp/pythonmiio_unparseable_{model}.json" # nosec + with open(fn, "w") as f: + json.dump(schema, f, indent=4) + _LOGGER.error("Unable to parse the schema, see %s: %s", fn, ex) + return loop = asyncio.get_event_loop() random.seed(1) # nosec diff --git a/miio/miot_models.py b/miio/miot_models.py index 995d633f3..19409636d 100644 --- a/miio/miot_models.py +++ b/miio/miot_models.py @@ -18,7 +18,11 @@ class URN(BaseModel): - """Parsed type URN.""" + """Parsed type URN. + + The expected format is urn::::::. + All extraneous parts are stored inside *unexpected*. + """ namespace: str type: str @@ -26,6 +30,7 @@ class URN(BaseModel): internal_id: str model: str version: int + unexpected: Optional[List[str]] parent_urn: Optional["URN"] = Field(None, repr=False) @@ -38,7 +43,7 @@ def validate(cls, v): if not isinstance(v, str) or ":" not in v: raise TypeError("invalid type") - _, namespace, type, name, id_, model, version = v.split(":") + _, namespace, type, name, id_, model, version, *unexpected = v.split(":") return cls( namespace=namespace, @@ -47,12 +52,16 @@ def validate(cls, v): internal_id=id_, model=model, version=version, + unexpected=unexpected if unexpected else None, ) @property def urn_string(self) -> str: """Return string presentation of the URN.""" - return f"urn:{self.namespace}:{self.type}:{self.name}:{self.internal_id}:{self.model}:{self.version}" + urn = f"urn:{self.namespace}:{self.type}:{self.name}:{self.internal_id}:{self.model}:{self.version}" + if self.unexpected is not None: + urn = f"{urn}:{':'.join(self.unexpected)}" + return urn def __repr__(self): return f"" diff --git a/miio/tests/test_miot_models.py b/miio/tests/test_miot_models.py index f5d29ff1f..314a4f384 100644 --- a/miio/tests/test_miot_models.py +++ b/miio/tests/test_miot_models.py @@ -116,9 +116,26 @@ def test_action(): assert act.plain_name == "dummy-action" -def test_urn(): +@pytest.mark.parametrize( + ("urn_string", "unexpected"), + [ + pytest.param( + "urn:namespace:type:name:41414141:dummy.model:1", None, id="regular_urn" + ), + pytest.param( + "urn:namespace:type:name:41414141:dummy.model:1:unexpected", + ["unexpected"], + id="unexpected_component", + ), + pytest.param( + "urn:namespace:type:name:41414141:dummy.model:1:unexpected:unexpected2", + ["unexpected", "unexpected2"], + id="multiple_unexpected_components", + ), + ], +) +def test_urn(urn_string, unexpected): """Test the parsing of URN strings.""" - urn_string = "urn:namespace:type:name:41414141:dummy.model:1" example_urn = f'{{"urn": "{urn_string}"}}' # noqa: B028 class Wrapper(BaseModel): @@ -134,6 +151,7 @@ class Wrapper(BaseModel): assert urn.internal_id == "41414141" assert urn.model == "dummy.model" assert urn.version == 1 + assert urn.unexpected == unexpected # Check that the serialization works assert urn.urn_string == urn_string