diff --git a/src/pyipp/serializer.py b/src/pyipp/serializer.py index 3a9df1b6..abdce6af 100644 --- a/src/pyipp/serializer.py +++ b/src/pyipp/serializer.py @@ -12,6 +12,19 @@ _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) + +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.""" @@ -36,8 +49,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): @@ -50,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", + }, + }, + ]