# Source code for debsbom.sbom

    # Copyright (C) 2025 Siemens
    #
    # SPDX-License-Identifier: MIT

    from collections.abc import Set
    from dataclasses import dataclass
    from enum import Enum
    import re

    from .dpkg.package import BinaryPackage, Dependency, Package

    CDX_REF_PREFIX = "CDXRef-"
    CDX_PACKAGE_EXTREF_TYPE_WEBSITE = "website"

    SPDX_REF_PREFIX = "SPDXRef-"
    SPDX_REF_DOCUMENT = SPDX_REF_PREFIX + "DOCUMENT"
    SPDX_REFERENCE_TYPE_PURL = "purl"
    SPDX_REFERENCE_TYPE_VCS = "vcs"
    SPDX_REFERENCE_TYPE_DISTRIBUTION = "distribution"
    # SPDX IDs only allow alphanumeric, '.' and '-'
    SPDX_ID_RE = re.compile(r"[^A-Za-z0-9.\-]+")

    # cues for an organization in the maintainer name
    SPDX_SUPPLIER_ORG_CUE = [
        "maintainers",
        "group",
        "developers",
        "team",
        "project",
        "task force",
        "strike force",
        "packagers",
        "users",
    ]

    # pattern to match the common "John Doe <john@doe.com>"
    SUPPLIER_PATTERN = re.compile("(?P<supplier_name>^[^<]+)(\\<(?P<supplier_email>.+)\\>)?")

    class SBOMType(Enum):
        """Supported SBOM types"""

        CycloneDX = (0,)
        """CycloneDX"""
        SPDX = (1,)
        """SPDX"""

        @classmethod
        def from_str(cls, bomtype: str):
            match bomtype.lower():
                case "cdx":
                    return cls.CycloneDX
                case "spdx":
                    return cls.SPDX
                case _:
                    raise RuntimeError(f"Unknown SBOM type '{bomtype}'")

        def __str__(self):
            match self:
                case self.CycloneDX:
                    return "cdx"
                case self.SPDX:
                    return "spdx"
                case _:
                    assert False, "unreachable"

        def validate_dependency_availability(self) -> None:
            """
            Check if the required imports for the SBOM type are available.
            Raises RuntimeError if not available.
            """
            if self == SBOMType.CycloneDX:
                try:
                    import cyclonedx.model.component
                except ModuleNotFoundError as e:
                    raise RuntimeError(
                        "Missing dependency: cyclonedx-python-lib from package `python3-cyclonedx-lib`"
                    )
            elif self == SBOMType.SPDX:
                try:
                    import spdx_tools.spdx.model.package
                except ModuleNotFoundError as e:
                    raise RuntimeError(
                        "Missing dependency: spdx-tools from package `python3-spdx-tools`"
                    )

    class BOM_Standard(Enum):
        """Controls the data representation and added values in the SBOM"""

        DEFAULT = (0,)
        STANDARD_BOM = (1,)

    @dataclass
    class Reference:
        """Generic reference in a SBOM."""

        target: str
        is_source: bool = False

        def as_str(self, sbom_type: SBOMType) -> str:
            """Return a string representation for the given SBOM type."""
            if sbom_type == SBOMType.CycloneDX:
                s = CDX_REF_PREFIX + self.target
            elif sbom_type == SBOMType.SPDX:
                s = SPDX_REF_PREFIX + re.sub(SPDX_ID_RE, ".", self.target)

            if self.is_source:
                s += "-srcpkg"
            return s

        @classmethod
        def lookup(
            cls,
            pkg: BinaryPackage,
            dep: Dependency,
            sbom_type: SBOMType,
            dependency_refs: dict[str, Package],
            native_arch: str,
        ) -> str | None:
            """
            For imprecise references (without architecture), locate the matching
            reference id within the id passed as known_refs. Returns None if
            not found. To avoid downstream double caching of IDs in two formats, we pass the
            known_refs as a string set.
            """
            if dep.arch:
                ref = Reference.make_from_dep(dep).as_str(sbom_type)
                ref_pkg = dependency_refs.get(ref)
                if ref_pkg and ref_pkg.is_binary():
                    if ref_pkg.satisfies(dep):
                        return ref
                    else:
                        return None
                else:
                    # source package dependencies are not versioned
                    return ref
            candidates = map(
                lambda a: Reference.make_from_dep(dep, a).as_str(sbom_type),
                set([pkg.architecture, native_arch, "all"]),
            )
            for candidate in candidates:
                pkg = dependency_refs.get(candidate)
                if pkg and pkg.satisfies(dep):
                    return Reference.make_from_pkg(pkg).as_str(sbom_type)
            return None

        @staticmethod
        def make_from_pkg(pkg: Package) -> "Reference":
            """
            Return a reference to a package in the list of all packages.
            This representation must match the one returned by ``make_from_dep``.
            """
            if pkg.is_source():
                return Reference(target=f"{pkg.name}-{pkg.version}", is_source=True)
            elif pkg.is_binary():
                return Reference(target=f"{pkg.name}-{pkg.architecture}", is_source=False)
            raise NotImplementedError()

        @staticmethod
        def make_from_dep(dep: Dependency, to_arch: str | None = None) -> "Reference":
            """
            Return a reference to a package from a dependency. If the dependency does
            not specify an architecture, the caller is responsible for providing this
            in ``to_arch``.
            """
            if "source" in [dep.arch, to_arch]:
                return Reference(target=f"{dep.name}-{dep.version[1]}", is_source=True)
            else:
                to_arch = to_arch or dep.arch
                return Reference(target=f"{dep.name}-{to_arch}", is_source=False)

    class BomSpecific:
        """Mixin to denote that a class processes bom type specific data"""

        @classmethod
        def sbom_type(cls) -> SBOMType:
            """Type of SBOM this class can handle"""
            return cls._sbom_type

    class CDXType(BomSpecific):
        """CycloneDX type mixin"""

        _sbom_type = SBOMType.CycloneDX

    class SPDXType(BomSpecific):
        """SPDX type mixin"""

        _sbom_type = SBOMType.SPDX