From d933cd987ccc13f4caa9a51cd4398039f8236430 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jun 2024 07:40:14 +0200 Subject: [PATCH 1/2] Throw errors on invalid messages --- src/pyipp/serializer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pyipp/serializer.py b/src/pyipp/serializer.py index 3a9df1b6..72e10e2d 100644 --- a/src/pyipp/serializer.py +++ b/src/pyipp/serializer.py @@ -12,6 +12,12 @@ _LOGGER = logging.getLogger(__name__) +class UnsupportedAttributeError(RuntimeError): + """Some attribute name in a message is unsupported.""" + + def __init__(self, name: str) -> None: + """Initialize Exception with name of unsupported attribute.""" + super(Exception, self).__init__(name) def construct_attribute_values(tag: IppTag, value: Any) -> bytes: """Serialize the attribute values into IPP format.""" @@ -36,8 +42,7 @@ def construct_attribute(name: str, value: Any, tag: IppTag | None = None) -> byt byte_str = b"" if not tag and not (tag := ATTRIBUTE_TAG_MAP.get(name, None)): - _LOGGER.debug("Unknown IppTag for %s", name) - return byte_str + raise UnsupportedAttributeError(name) if isinstance(value, (list, tuple, set)): for index, list_value in enumerate(value): From 960fbf22d1160f793744a14b9b4016c9b2898e43 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jun 2024 08:17:12 +0200 Subject: [PATCH 2/2] Add support for Collections --- src/pyipp/serializer.py | 26 +++++++++++++++++++++++ src/pyipp/tags.py | 10 +++++++++ tests/test_serializer.py | 45 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/src/pyipp/serializer.py b/src/pyipp/serializer.py index 72e10e2d..abdce6af 100644 --- a/src/pyipp/serializer.py +++ b/src/pyipp/serializer.py @@ -19,6 +19,13 @@ def __init__(self, name: str) -> None: """Initialize Exception with name of unsupported attribute.""" super(Exception, self).__init__(name) +class DatatypeMismatchError(RuntimeError): + """Some attribute value has an unexpected data type.""" + + def __init__(self, msg: str) -> None: + """Initialize Exception with message.""" + super(Exception, self).__init__(msg) + def construct_attribute_values(tag: IppTag, value: Any) -> bytes: """Serialize the attribute values into IPP format.""" byte_str = b"" @@ -55,6 +62,25 @@ def construct_attribute(name: str, value: Any, tag: IppTag | None = None) -> byt byte_str += struct.pack(">h", 0) byte_str += construct_attribute_values(tag, list_value) + elif isinstance(value, dict): + if tag != IppTag.BEGIN_COLLECTION: + msg = (f"Attribute {name} has data of type dict, but " + f"its tag is not a collection but a {tag}") + raise DatatypeMismatchError(msg) + byte_str += struct.pack(">b", tag.value) # value-tag + byte_str += struct.pack(">h", len(name)) # name-length + byte_str += name.encode("utf-8") # name + byte_str += struct.pack(">h", 0) # value-length + for k, v in value.items(): + byte_str += struct.pack(">b", IppTag.MEMBER_NAME.value) # value-tag + byte_str += struct.pack(">h", 0) # name-length + byte_str += struct.pack(">h", len(k)) # value-length + byte_str += k.encode("utf-8") # value (member-name) + byte_str += construct_attribute(k, v) + byte_str += struct.pack(">b", IppTag.END_COLLECTION.value) # end-value-tag + byte_str += struct.pack(">h", 0) # end-name-length + byte_str += struct.pack(">h", 0) # end-value-length + else: byte_str = struct.pack(">b", tag.value) diff --git a/src/pyipp/tags.py b/src/pyipp/tags.py index 9d03fba1..93dfd5da 100644 --- a/src/pyipp/tags.py +++ b/src/pyipp/tags.py @@ -31,6 +31,16 @@ "job-uuid": IppTag.URI, "requested-attributes": IppTag.KEYWORD, "member-uris": IppTag.URI, + "media-col": IppTag.BEGIN_COLLECTION, + "media-size": IppTag.BEGIN_COLLECTION, + "media-bottom-margin": IppTag.INTEGER, + "media-left-margin": IppTag.INTEGER, + "media-right-margin": IppTag.INTEGER, + "x-dimension": IppTag.INTEGER, + "y-dimension": IppTag.INTEGER, + "media-source": IppTag.KEYWORD, + "media-top-margin": IppTag.INTEGER, + "media-type": IppTag.KEYWORD, "operations-supported": IppTag.ENUM, "ppd-name": IppTag.NAME, "printer-state-reason": IppTag.KEYWORD, diff --git a/tests/test_serializer.py b/tests/test_serializer.py index 410f8de8..51db1a76 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -1,5 +1,6 @@ """Tests for Serializer.""" from pyipp import serializer +from pyipp import parser from pyipp.const import DEFAULT_CHARSET, DEFAULT_CHARSET_LANGUAGE, DEFAULT_PROTO_VERSION from pyipp.enums import IppOperation, IppTag @@ -74,3 +75,47 @@ def test_encode_dict() -> None: assert result == load_fixture_binary( "serializer/get-printer-attributes-request-000.bin", ) + +def test_encode_collections() -> None: + """Test encoding collections.""" + message = { + "version": DEFAULT_PROTO_VERSION, + "operation": IppOperation.VALIDATE_JOB, + "request-id": 1, + "operation-attributes-tag": { + "attributes-charset": DEFAULT_CHARSET, + "attributes-natural-language": DEFAULT_CHARSET_LANGUAGE, + "requesting-user-name": "PythonIPP", + "printer-uri": "ipp://printer.example.com:361/ipp/print", + }, + "job-attributes-tag": { + "media-col": { + "media-bottom-margin": 0, + "media-left-margin": 0, + "media-right-margin": 0, + "media-size": { + "x-dimension": 10000, + "y-dimension": 14800, + }, + "media-source": "photo", + "media-top-margin": 0, + "media-type": "photographic", + }, + }, + } + encoded = serializer.encode_dict(message) + parsed = parser.parse(encoded) + assert parsed["jobs"] == [ + { + "ipp-attribute-fidelity": True, + "media-col": { + "media-bottom-margin": 0, + "media-left-margin": 0, + "media-right-margin": 0, + "media-size": {"x-dimension": 10000, "y-dimension": 14800}, + "media-source": "photo", + "media-top-margin": 0, + "media-type": "photographic", + }, + }, + ]