# SPDX-License-Identifier: GPL-3.0-or-later
"""Hardened XML parsing for the one XML file the plugin reads/writes.

The vessel register (``vessel_names.xml``) is a file the plugin writes itself, so
it is first-party — but to keep every XML read defensive (and to satisfy static
analysers), all parsing goes through this helper, which:

* rejects any document carrying a DTD or entity declaration outright. stdlib
  ElementTree expands *internal* entities, so a crafted DTD could mount a
  "billion laughs" entity-expansion DoS; nothing the plugin reads legitimately
  uses a DTD, so a blanket reject is both safe and correct. (External entities
  are never resolved by ElementTree, so there is no XXE/file-disclosure vector.)
* parses the pre-screened bytes via the ElementTree *instance* API.

Pure stdlib — the QGIS Python environment does not ship defusedxml.
"""
import io
import xml.etree.ElementTree as ET  # nosec B405


def _screen(data):
    """Reject DTD/entity declarations anywhere in the document (case-blind,
    UTF-16 aware). Returns the raw bytes unchanged when clean."""
    probe = data
    if probe[:2] in (b"\xff\xfe", b"\xfe\xff"):          # UTF-16 BOM
        probe = probe.decode("utf-16", "replace").encode("utf-8", "replace")
    up = probe.upper()
    if b"<!DOCTYPE" in up or b"<!ENTITY" in up:
        raise ValueError(
            "XML document contains a DTD/entity declaration, which is not "
            "permitted (rejected to prevent entity-expansion attacks).")
    return data


def parse_root(source):
    """Root element of a defensively parsed XML file path or binary file object.

    Raises ``ValueError`` for documents carrying DTD/entity declarations;
    propagates ``FileNotFoundError`` / ``ParseError`` like the stdlib would.
    """
    if hasattr(source, "read"):
        data = source.read()
    else:
        with open(source, "rb") as fh:
            data = fh.read()
    if isinstance(data, str):
        data = data.encode("utf-8", "replace")
    tree = ET.ElementTree()
    tree.parse(io.BytesIO(_screen(data)))
    return tree.getroot()
