Source code for cadrumo.adapters.inbound.einvoice.xml
"""Hardened XML read boundary for externally-authored e-invoice documents.Every byte parsed here arrives from outside the host: an operator's attachment,a supplier's emailed invoice, a payload embedded in a PDF. The document istherefore treated as hostile input, and the parse is bounded on all four axesan XML reader can be attacked on.* **Entity resolution and external DTD loading are disabled**, which is what ``defusedxml`` is for and why this module exists rather than each parser reaching for ``ElementTree`` directly. An XXE probe cannot read a local file or open a network connection.* **Payload size is bounded** before parsing begins, so a multi-gigabyte document is refused rather than buffered.* **Nesting depth is bounded** after parsing, which is the residual billion-laughs-adjacent risk once entity expansion is already off: a deeply nested document can still exhaust the recursion budget of a naive walker.``defusedxml`` is already a declared ``[project.dependencies]`` entry, so exactstructured reading costs no new dependency, no model, no GPU and no extra --which is what lets it live in the deterministic core rather than behind theinference boundary."""from__future__importannotationsfromxml.etree.ElementTreeimportElementfromdefusedxml.ElementTreeimportParseErroras_DefusedParseErrorfromdefusedxml.ElementTreeimportfromstringas_defused_fromstringfrom....core.errors.hierarchyimportCadrumoError
[docs]deflocal_tag_name(tag:str)->str:"""Return an XML tag without its namespace prefix. ElementTree reports a namespaced tag as ``{uri}local``. Three parsers each carried this strip, which meant three places decided what a tag's local name is. """returntag.rsplit("}",1)[-1]if"}"intagelsetag
__all__=["MAX_XML_DEPTH","MAX_XML_PAYLOAD_BYTES","EInvoiceXmlParseError","local_tag_name","parse_hardened_xml"]MAX_XML_PAYLOAD_BYTES=32*1024*1024"""Largest e-invoice payload accepted. Two orders of magnitude above any realEN16931 or Facturae document, so it refuses abuse without refusing a genuineinvoice carrying embedded attachments."""MAX_XML_DEPTH=100"""Deepest element nesting accepted. EN16931 and Facturae both bottom out wellunder 20; 100 leaves generous headroom while still bounding a walker."""
[docs]classEInvoiceXmlParseError(CadrumoError):"""Raised when an e-invoice XML payload is refused at the read boundary. Deliberately a refusal rather than a partial result. A structured reader that returned half a record on malformed input would be worse than a model: it would look exact while being wrong. :class:`~cadrumo.core.errors.hierarchy.CadrumoError` binds the refusal to the error registry, so an operator receives a stable code and translated message. XML parsing is not a Pydantic callback, so the adapter raises this registered type directly rather than leaking a builtin validation base. """
def_depth(element:Element)->int:"""Return the maximum nesting depth under ``element``, iteratively. Iterative on purpose: a recursive measurement of a hostile document would hit the interpreter's recursion limit before it could report the very condition it exists to detect. """deepest=1stack:list[tuple[Element,int]]=[(element,1)]whilestack:node,level=stack.pop()deepest=max(deepest,level)iflevel>MAX_XML_DEPTH:returnlevelstack.extend((child,level+1)forchildinnode)returndeepest
[docs]defparse_hardened_xml(payload:bytes)->Element:"""Parse ``payload`` with entity resolution and external DTDs disabled. Args: payload: Raw document bytes from outside the host. Returns: The parsed root :class:`~xml.etree.ElementTree.Element`. Raises: EInvoiceXmlParseError: If the payload is empty, exceeds :data:`MAX_XML_PAYLOAD_BYTES`, is not well-formed XML, resolves a forbidden entity, or nests deeper than :data:`MAX_XML_DEPTH`. """ifnotpayload:raiseEInvoiceXmlParseError("empty XML payload")iflen(payload)>MAX_XML_PAYLOAD_BYTES:raiseEInvoiceXmlParseError(f"XML payload of {len(payload)} bytes exceeds the {MAX_XML_PAYLOAD_BYTES}-byte read limit",)try:root=_defused_fromstring(payload)except_DefusedParseErrorasexc:raiseEInvoiceXmlParseError(f"malformed XML: {exc}")fromexcexceptExceptionasexc:# defusedxml raises its own EntityDeclared / DTDForbidden family for a# hostile document. Every one of them is a refusal, and none may be# allowed to surface as a partially-read record.raiseEInvoiceXmlParseError(f"refused XML payload: {type(exc).__name__}: {exc}")fromexcdepth=_depth(root)ifdepth>MAX_XML_DEPTH:raiseEInvoiceXmlParseError(f"XML nesting depth {depth} exceeds the limit of {MAX_XML_DEPTH}")returnroot