# Source code for debsbom.dpkg.package

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

    from abc import ABC, abstractmethod
    from dataclasses import dataclass
    from collections.abc import Iterable
    from enum import Enum
    import io
    import itertools
    from pathlib import Path
    from debian.deb822 import Packages, PkgRelation
    from debian.debian_support import Version
    import logging
    import re
    from packageurl import PackageURL

    from ..apt.copyright import Copyright
    from ..util.checksum import ChecksumAlgo, checksums_from_dsc, checksums_from_package
    from .. import HAS_PYTHON_APT

    logger = logging.getLogger(__name__)

    # Debian policy 5.6.13, item 1
    DESC_REGEX_P1 = re.compile(r"^\s(\w.*)$")

    class DpkgStatus(Enum):
        NOT_INSTALLED = "n"
        CONFIG_FILES = "c"
        HALF_INSTALLED = "H"
        UNPACKED = "U"
        HALF_CONFIGURED = "F"
        TRIGGERS_AWAITING = "W"
        TRIGGERS_PENDING = "t"
        INSTALLED = "i"
        DEBSBOM_UNKNOWN = "0"

        @classmethod
        def from_dpkg(cls, status: str) -> "DpkgStatus":
            if len(status) > 1:
                status = status.lower()
            for checkStatus in DpkgStatus:
                if checkStatus == DpkgStatus.DEBSBOM_UNKNOWN:
                    continue
                if status in [checkStatus.value, checkStatus.name.lower().replace("_", "-")]:
                    return checkStatus
            raise ValueError(f"Unknown dpkg status '{status}'")

    class PkgListType(Enum):
        """Type of package list data (e.g. PURL or dpkg status file)"""

        STATUS_FILE = (0,)
        PKG_LIST = (1,)
        PURL_LIST = (2,)
        ISAR_MANIFEST = (3,)

    class PkgListStream:
        """
        Wrapper around a packages iterator that takes care of closing the attached
        stream (either on StopIteration or via context manager). It further allows
        to return the source type of data from which the packages are created.
        """

        def __init__(self, stream: io.IOBase, kind: PkgListType, pkgs: Iterable["Package"]):
            self._stream = stream
            self.kind = kind
            self._pkgs = pkgs

        def __iter__(self) -> "PkgListStream":
            return self

        def __next__(self) -> "Package":
            """return the next package, close the stream if no further elements."""
            try:
                return next(self._pkgs)
            except StopIteration:
                self.close()
                raise

        def __enter__(self):
            return self

        def __exit__(self, exc_type, exc_value, traceback):
            self.close()

        def close(self):
            """explicitly close the stream"""
            self._stream.close()

    def filter_sources(pkgs: Iterable["Package"]) -> Iterable["SourcePackage"]:
        """Free function to only return source packages"""
        return filter(lambda p: p.is_source(), pkgs)

    def filter_binaries(pkgs: Iterable["Package"]) -> Iterable["BinaryPackage"]:
        """Free function to only return binary packages"""
        return filter(lambda p: p.is_binary(), pkgs)

    def filter_installed(pkgs: Iterable["BinaryPackage"]) -> Iterable["BinaryPackage"]:
        """Free function to only return installed binary packages."""
        return filter(lambda p: p.status in (DpkgStatus.INSTALLED, DpkgStatus.DEBSBOM_UNKNOWN), pkgs)

    @dataclass
    class Dependency:
        """Representation of a dependency for a package."""

        name: str
        archqual: str | None = None
        version: tuple[str, Version] | None = None
        arch: str | None = None
        restrictions: str | None = None

        @classmethod
        def from_pkg_relations(cls, relations: list[list[dict]], is_source=False) -> list["Dependency"]:
            dependencies = []
            for relation in relations:
                for dep in relation:
                    if dep.get("version"):
                        # make a proper Version out of it
                        dep["version"] = (dep["version"][0], Version(dep["version"][1]))
                    if is_source:
                        dep["arch"] = "source"
                    dependencies.append(Dependency(**dep))
            return dependencies

        @classmethod
        def parse_depends_line(cls, line: str) -> list["Dependency"]:
            return Dependency.from_pkg_relations(PkgRelation.parse_relations(line))

        def is_satisfying_version(self, other: Version) -> bool:
            """Returns True if the passed version satisfies the dependencies version constraint."""

            # as defined in https://www.debian.org/doc/debian-policy/ch-relationships.html#syntax-of-relationship-fields
            operator = self.version[0]
            ours = self.version[1]
            if operator == "=":
                return other == ours
            elif operator == "<<":
                return other < ours
            elif operator == "<=":
                return other <= ours
            elif operator == ">>":
                return other > ours
            elif operator == ">=":
                return other >= ours
            else:
                raise ValueError(f"invalid operator '{operator}'")

    class DebianPriority(Enum):
        REQUIRED = "required"
        IMPORTANT = "important"
        STANDARD = "standard"
        OPTIONAL = "optional"
        EXTRA = "extra"

    @dataclass
    class VirtualPackage:
        """Virtual Package, as declared in the `Provides` field."""

        name: str
        version: Version | None = None

        @classmethod
        def from_pkg_relations(cls, relations: list[list[dict]]) -> list["VirtualPackage"]:
            dependencies = []
            for relation in relations:
                for dep in relation:
                    if dep.get("version"):
                        # make a proper Version out of it
                        dep["version"] = Version(dep["version"][1])
                    dependencies.append(VirtualPackage(dep["name"], dep["version"]))
            return dependencies

        def satisfies(self, dep: Dependency) -> bool:
            """Returns True if this virtual package satisfies the dependency."""
            if self.name != dep.name:
                return False

            if dep.version is None:
                return True

            # the debian policy states that versioned dependencies
            # must have a versioned virtual package provides
            if not self.version and dep.version:
                return False

            return dep.is_satisfying_version(self.version)

        @classmethod
        def best_match(
            cls, candidates: list[tuple["VirtualPackage", "BinaryPackage"]], dependency: Dependency
        ) -> type["BinaryPackage"] | None:
            """Return the best matching virtual package that satisifes the dependency."""
            candidates.sort(key=lambda c: c[0].version or 0, reverse=True)
            for provides, candidate in candidates:
                if provides.satisfies(dependency):
                    return candidate
            return None

    @dataclass(init=False)
    class Package(ABC):
        """Base class for binary and source packages."""

        name: str
        version: Version
        maintainer: str | None = None
        homepage: str | None = None
        checksums: dict[ChecksumAlgo, str]

        def __init__(self, name: str, version: str | Version):
            self.name = name
            self.version = Version(version)

        @classmethod
        def parse_status_file(cls, status_file: Path) -> PkgListStream:
            """
            Parse a dpkg status file and returns packages with their relations.
            """
            logger.info(f"Parsing status file '{status_file}'...")
            deb822_stream = open(status_file, "r")
            pkgs_it = cls.inject_src_packages(filter_installed(cls._parse_dpkg_status(deb822_stream)))
            return PkgListStream(deb822_stream, PkgListType.STATUS_FILE, pkgs_it)

        @classmethod
        def _parse_dpkg_status(
            cls, stream: io.IOBase, force_no_apt: bool = False
        ) -> Iterable["BinaryPackage"]:
            """
            Parse a dpkg status file and returns binary packages with their relations.
            The relations might contain links to packages that are not known yet (e.g.
            all source packages). These need to be resolved in a second pass.

            The force_no_apt flag can be used to explicitly disable the use of python-apt.
            Due to bugs in some python-apt versions, this might be required if a non-file
            input stream is used.
            """
            use_apt = HAS_PYTHON_APT and not force_no_apt
            for package in Packages.iter_paragraphs(stream, use_apt_pkg=use_apt):
                bpkg = BinaryPackage.from_deb822(package)
                logger.debug(f"Found binary package: '{bpkg.name}'")
                yield bpkg

        @classmethod
        def parse_pkglist_stream(cls, stream: io.IOBase) -> PkgListStream:
            """
            Parses a stream of space separated tuples describing packages
            (name, version, arch), PURLs, isar manifest data
            (<src name>|<src version>|<bin name>:<bin arch>|<bin version>) or a
            dpkg-status file. If not passing a dpkg-status file, each line describes
            one package. Example:
            gcc 15.0-1 amd64
            g++ 15.0-1 amd64
            """
            DPKG_STATUS_MAGIC = "Package: ".encode()
            PURL_MAGIC = "pkg:deb/".encode()

            if isinstance(stream, io.BufferedReader):
                bstream = stream
            elif isinstance(stream, io.TextIOBase):
                bstream = io.BufferedReader(stream.buffer)
            else:
                bstream = io.BufferedReader(stream)

            if bstream.peek(len(DPKG_STATUS_MAGIC)).startswith(DPKG_STATUS_MAGIC):
                pkgs_it = cls.inject_src_packages(cls._parse_dpkg_status(bstream, force_no_apt=True))
                return PkgListStream(bstream, PkgListType.STATUS_FILE, pkgs_it)
            elif bstream.peek(len(PURL_MAGIC)).startswith(PURL_MAGIC):
                return PkgListStream(
                    stream, PkgListType.PURL_LIST, map(lambda l: Package.from_purl(l.decode()), bstream)
                )
            elif sum(c == b"|"[0] for c in bstream.peek(128).split(b"\n")[0]) == 3:
                return PkgListStream(
                    bstream, PkgListType.ISAR_MANIFEST, cls._parse_manifest_line_stream(bstream)
                )
            return PkgListStream(bstream, PkgListType.PKG_LIST, cls._parse_pkglist_line_stream(bstream))

        @classmethod
        def _parse_pkglist_line_stream(cls, stream: io.IOBase) -> Iterable["Package"]:
            for line in stream:
                name, version, arch = line.decode().strip().split()
                if arch == "source":
                    yield SourcePackage(
                        name=name,
                        version=version,
                    )
                else:
                    yield BinaryPackage(
                        name=name,
                        architecture=arch,
                        version=version,
                    )

        @classmethod
        def _parse_manifest_line_stream(cls, stream: io.IOBase) -> Iterable["Package"]:
            """
            Parse isar manifest file entries. The format is:
            <src name>|<src version>|<bin name>:<bin arch>|<bin version>
            Example:
            json-c|0.16-2|libjson-c5:amd64|0.16-2
            """
            for line in stream:
                src, srcv, bin, binv = line.decode().strip().split("|")
                yield SourcePackage(name=src, version=srcv)
                bin_parts = bin.split(":")
                if len(bin_parts) != 2:
                    logger.error(f"Binary package '{bin}' misses the architecture specifier")
                yield BinaryPackage(
                    name=bin_parts[0],
                    architecture=bin_parts[1] if len(bin_parts) == 2 else "unknown",
                    version=binv,
                )

        @classmethod
        def from_purl(cls, purl: str) -> "Package":
            """
            Create a package from a PURL. Note, that the package only encodes
            information that can be derived from the PURL.
            """
            purl = PackageURL.from_string(purl)
            if not purl.type == "deb":
                raise RuntimeError("Not a debian purl", purl)
            if purl.qualifiers.get("arch") == "source":
                return SourcePackage(purl.name, purl.version)
            else:
                return BinaryPackage(
                    name=purl.name,
                    architecture=purl.qualifiers.get("arch"),
                    version=purl.version,
                )

        @classmethod
        def inject_src_packages(cls, binpkgs: Iterable["BinaryPackage"]) -> Iterable["Package"]:
            """Create and inject referenced source packages"""
            return cls._unique_everseen(
                itertools.chain.from_iterable(map(lambda p: cls._resolve_sources(p, True), binpkgs))
            )

        @classmethod
        def referenced_src_packages(
            cls, binpkgs: Iterable["BinaryPackage"]
        ) -> Iterable["SourcePackage"]:
            """Create and return referenced source packages"""
            return cls._unique_everseen(
                itertools.chain.from_iterable(map(lambda p: cls._resolve_sources(p, False), binpkgs))
            )

        @classmethod
        def _unique_everseen(cls, iterable: Iterable[object], key=None):
            """
            Yield unique elements, preserving order. Remember all elements ever seen.
            """
            seen = set()
            if key is None:
                for element in itertools.filterfalse(seen.__contains__, iterable):
                    seen.add(element)
                    yield element
            else:
                for element in iterable:
                    k = key(element)
                    if k not in seen:
                        seen.add(k)
                        yield element

        def merge_with(self, other: "Package"):
            """
            Copy the corresponding values of the other package for each unset field.
            """
            if not self.maintainer:
                self.maintainer = other.maintainer
            if not self.homepage:
                self.homepage = other.homepage
            self.checksums |= other.checksums

        @classmethod
        def _resolve_sources(cls, pkg: "BinaryPackage", add_pkg=False) -> Iterable["Package"]:
            """
            Returns an iterator to resolve the source package of a binary package.
            If add_pkg=True is set, the passed binary package is returned as well.
            """
            src_pkg = pkg.source_package()
            if src_pkg:
                logger.debug(f"Found source package: '{pkg.source.name}'")
                yield src_pkg
            for bu in pkg.built_using:
                # Add partial source package. This can later be enhanced by merging it with
                # a more complete source package we discovered via other mechanisms (e.g. apt cache).
                logger.debug(f"Found built-using source package: '{bu.name}@{bu.version[1]}'")
                yield SourcePackage(bu.name, bu.version[1])
            for sbu in pkg.static_built_using:
                logger.debug(f"Found static-built-using source package: '{sbu.name}@{sbu.version[1]}'")
                yield SourcePackage(sbu.name, sbu.version[1])
            if add_pkg:
                yield pkg

        def is_binary(self) -> bool:
            """Helper to check if the package is a binary package"""
            if isinstance(self, BinaryPackage):
                return True
            return False

        def is_source(self) -> bool:
            """Helper to check if the package is a source package"""
            if isinstance(self, SourcePackage):
                return True
            return False

        @abstractmethod
        def purl(self) -> PackageURL:
            raise NotImplementedError

        @property
        @abstractmethod
        def locator(self) -> str:
            raise NotImplementedError()

        @property
        def filename(self) -> str:
            """Return the filename part from the locator of a package."""
            return self.locator.split("/")[-1]

        def __str__(self) -> str:
            return f"{self.name}@{self.version}"

    class VcsType(Enum):
        Arch = "Arch"
        Bzr = "Bazaar"
        Cvs = "CVS"
        Darcs = "Darcs"
        Git = "Git"
        Hg = "Mercurial"
        Mtn = "Monotone"
        Svn = "Subversion"

    @dataclass
    class VcsInfo:
        """Internal representation of the Vcs-<type> information for a source package."""

        type: VcsType
        locator: str

    @dataclass(init=False)
    class SourcePackage(Package):
        """
        Representation of a Debian Source package.
        The checksums refer to the .dsc file.
        """

        binaries: list[str] | None = None
        vcs: VcsInfo | None = None
        copyright: Copyright | None = None
        _locator: str | None = None

        def __init__(
            self,
            name: str,
            version: str | Version,
            maintainer: str | None = None,
            binaries: list[str] | None = None,
            homepage: str | None = None,
            vcs: VcsInfo | None = None,
            checksums: dict[ChecksumAlgo, str] | None = None,
            copyright: Copyright | None = None,
        ):
            self.name = name
            self.version = Version(version)
            self.maintainer = maintainer
            self.binaries = binaries or []
            self.homepage = homepage
            self.vcs = vcs
            self.checksums = checksums or {}
            self.copyright = copyright

        def __hash__(self):
            return hash(self.purl())

        def __eq__(self, other):
            # For compatibility reasons
            if other.is_source():
                return self.purl() == other.purl()
            return NotImplemented

        def purl(self, vendor="debian") -> PackageURL:
            """Return the PURL of the package."""
            return PackageURL.from_string(
                "pkg:deb/{}/{}@{}?arch=source".format(vendor, self.name, self.version)
            )

        @property
        def locator(self) -> str:
            """Path to file if set or name of .dsc file"""
            return self._locator or self.dscfile()

        @locator.setter
        def locator(self, loc) -> None:
            self._locator = loc

        def dscfile(self) -> str:
            """Return the name of the .dsc file"""
            # TODO: find where this filename format is specified
            if self.version.debian_revision:
                version_wo_epoch = f"{self.version.upstream_version}-{self.version.debian_revision}"
            else:
                version_wo_epoch = self.version.upstream_version
            return f"{self.name}_{version_wo_epoch}.dsc"

        def merge_with(self, other: "SourcePackage"):
            """Copy properties from other which are unset on our side. Merge lists."""
            super().merge_with(other)
            if not self.vcs:
                self.vcs = other.vcs
            # add binaries from other
            binaries = list(self.binaries)
            binaries.extend(x for x in other.binaries if x not in binaries)
            self.binaries = binaries

        @staticmethod
        def from_deb822(package) -> "SourcePackage":
            """
            Create a package from a deb822 representation. If the deb822 input
            is a .dsc file, the name is read from the source property.
            """
            name = package.get("Source") or package["Package"]
            version = Version(package.get("Version"))
            maintainer = package.get("Maintainer")
            binary_entry = package.get("Binary")
            if binary_entry is not None:
                binaries = [b.strip() for b in binary_entry.split(",")]
            else:
                binaries = []
            homepage = package.get("Homepage")
            vcs = None
            for type in VcsType:
                locator = package.get(f"Vcs-{type.name}")
                if locator:
                    if vcs:
                        logger.warning(
                            f"Multiple VCS types found for package {name}: {vcs.type} and {type}"
                        )
                    # remove additional details (e.g., branch, path)
                    locator = locator.split()[0]
                    vcs = VcsInfo(type, locator)

            # Checksums according to Debian policy 5.6.24
            pkg_chksums = {}
            chksums = checksums_from_dsc(package)
            for c in filter(lambda c_: c_.endswith(".dsc"), chksums):
                pkg_chksums = chksums[c]

            return SourcePackage(
                name=name,
                version=version,
                maintainer=maintainer,
                binaries=binaries,
                homepage=homepage,
                vcs=vcs,
                checksums=pkg_chksums,
            )

    @dataclass(init=False)
    class BinaryPackage(Package):
        """Incomplete representation of a Debian binary package."""

        section: str | None
        architecture: str | None
        source: Dependency | None
        depends: list[Dependency]
        pre_depends: list[Dependency]
        provides: list[VirtualPackage]
        recommends: list[Dependency]
        suggests: list[Dependency]
        built_using: list[Dependency]
        static_built_using: list[Dependency]
        description: str | None
        essential: bool
        priority: DebianPriority | None
        manually_installed: bool
        status: DpkgStatus
        _locator: str | None = None

        def __init__(
            self,
            name: str,
            version: str | Version,
            section: str | None = None,
            maintainer: str | None = None,
            architecture: str | None = None,
            source: Dependency | None = None,
            depends: list[Dependency] | None = None,
            pre_depends: list[Dependency] | None = None,
            provides: list[VirtualPackage] | None = None,
            recommends: list[Dependency] | None = None,
            suggests: list[Dependency] | None = None,
            built_using: list[Dependency] | None = None,
            static_built_using: list[Dependency] | None = None,
            description: str | None = None,
            essential: bool = False,
            priority: DebianPriority | None = None,
            homepage: str | None = None,
            checksums: dict[ChecksumAlgo, str] | None = None,
            manually_installed: bool = True,
            status: DpkgStatus = DpkgStatus.DEBSBOM_UNKNOWN,
        ):
            self.name = name
            self.section = section
            self.maintainer = maintainer
            self.architecture = architecture
            self.source = source
            self.version = Version(version)
            self.depends = depends or []
            self.pre_depends = pre_depends or []
            self.provides = provides or []
            self.recommends = recommends or []
            self.suggests = suggests or []
            self.built_using = built_using or []
            self.static_built_using = static_built_using or []
            self.description = description
            self.essential = essential
            self.priority = priority
            self.homepage = homepage
            self.checksums = checksums or {}
            self.manually_installed = manually_installed
            self.status = status

        def __hash__(self):
            return hash(self.purl())

        def __eq__(self, other):
            if other.is_binary():
                return self.purl() == other.purl()
            return NotImplemented

        def purl(self, vendor="debian") -> PackageURL:
            """Return the PURL of the package."""
            purl = "pkg:deb/{}/{}@{}".format(vendor, self.name, self.version)
            if self.architecture:
                purl = purl + "?arch={}".format(self.architecture)
            return PackageURL.from_string(purl)

        def source_package(self) -> SourcePackage | None:
            """Construct a source package from the referenced source dependency."""
            if self.source:
                return SourcePackage(self.source.name, self.source.version[1], self.maintainer)
            else:
                return None

        @property
        def all_depends(self) -> Iterable[Dependency]:
            """Returns an iterator containing both "Depends" and "Pre-Depends."""
            return itertools.chain(self.depends, self.pre_depends)

        def _unique_deps_for(self, dependencies: Iterable[Dependency]) -> list[Dependency]:
            """
            Returns the unique dependencies of a dependency list without version.
            """
            seen = set()
            unique = []
            for dep in dependencies:
                key = (dep.name, dep.arch)
                if key not in seen:
                    seen.add(key)
                    unique.append(dep)
            return unique

        @property
        def unique_depends(self) -> list[Dependency]:
            """
            Returns the unique dependencies without version.
            The raw dependencies can include version specifiers, but as only a single
            version can be installed at a time, we ignore them.
            """
            return self._unique_deps_for(self.all_depends)

        @property
        def unique_recommends(self) -> list[Dependency]:
            """
            Returns the unique recommendations without version.
            The raw dependencies can include version specifiers, but as only a single
            version can be installed at a time, we ignore them.
            """
            return self._unique_deps_for(self.recommends)

        @property
        def unique_suggests(self) -> list[Dependency]:
            """
            Returns the unique suggestions without version.
            The raw dependencies can include version specifiers, but as only a single
            version can be installed at a time, we ignore them.
            """
            return self._unique_deps_for(self.suggests)

        def merge_with(self, other: "BinaryPackage"):
            """Copy properties from other which are unset on our side. Merge lists and dicts. Or booleans."""
            super().merge_with(other)
            if not self.section:
                self.section = other.section
            if not self.architecture:
                self.architecture = other.architecture
            if not self.source:
                self.source = other.source
            if not self.description:
                self.description = other.description
            self.essential |= other.essential
            if not self.priority:
                self.priority = other.priority
            self.manually_installed |= other.manually_installed
            # we cannot merge the status, but if the other package is
            # marked as installed, consider all as installed.
            if self.status == DpkgStatus.DEBSBOM_UNKNOWN:
                self.status = other.status
            elif other.status != DpkgStatus.DEBSBOM_UNKNOWN and self.status != other.status:
                # this indicates an internal error
                logger.warning(f"package statuses are inconsistent: {self.status} != {other.status}")

            def _merge_lists(list1: list, list2: list) -> list:
                list1.extend(x for x in list2 if x not in list1)
                return list1

            self.depends = _merge_lists(list(self.depends), other.depends)

            self.pre_depends = _merge_lists(list(self.pre_depends), other.pre_depends)

            self.recommends = _merge_lists(list(self.recommends), other.recommends)

            self.suggests = _merge_lists(list(self.suggests), other.suggests)

            self.built_using = _merge_lists(list(self.built_using), other.built_using)

            self.static_built_using = _merge_lists(
                list(self.static_built_using), other.static_built_using
            )

        @property
        def locator(self) -> str:
            """Return the name (and path if available) of the .deb file"""
            if self._locator:
                return self._locator
            # TODO: find where this filename format is specified
            if self.version.debian_revision:
                version_wo_epoch = f"{self.version.upstream_version}-{self.version.debian_revision}"
            else:
                version_wo_epoch = self.version.upstream_version
            return f"{self.name}_{version_wo_epoch}_{self.architecture}.deb"

        @locator.setter
        def locator(self, loc) -> None:
            self._locator = loc

        @staticmethod
        def _cleanup_description(desc: str | None) -> str | None:
            """
            Parse description according to Debian policy 5.6.13
            """
            if desc is None:
                return None
            buffer = str()
            in_paragraph = False
            for line in desc.split("\n"):
                matches = DESC_REGEX_P1.match(line)
                if matches:
                    buffer += (" " if in_paragraph else "") + matches[1]
                    in_paragraph = True
                    continue

                if len(line) == 0:
                    pass
                elif line == " .":
                    buffer += "\n"
                elif line[0] != " ":
                    # First line
                    buffer += line + "\n"
                else:
                    buffer += "\n" + line[1:]
                in_paragraph = False
            return buffer.strip()

        @classmethod
        def from_deb822(cls, package) -> "BinaryPackage":
            """
            Create a ``BinaryPackage`` from a deb822 representation.
            """
            try:
                if package.source:
                    srcdep = Dependency(
                        package.source, None, ("=", package.source_version), arch="source"
                    )
                else:
                    srcdep = None
            except KeyError:
                name = package.get("Package")
                logger.warning(f"Package {name} has incorrect source package relation")
                srcdep = None

            pdepends = package.relations["depends"] or []
            dependencies = Dependency.from_pkg_relations(pdepends)

            pre_pdepends = package.relations["pre-depends"] or []
            pre_dependencies = Dependency.from_pkg_relations(pre_pdepends)

            provides = VirtualPackage.from_pkg_relations(package.relations["provides"] or [])

            recommends = package.relations["recommends"] or []
            recommends = Dependency.from_pkg_relations(recommends)

            suggests = package.relations["suggests"] or []
            suggests = Dependency.from_pkg_relations(suggests)

            # Built-Using relationships are primarily used for license purposes
            built_using = package.relations["built-using"] or []
            budepends = Dependency.from_pkg_relations(built_using, is_source=True)

            # statically linked dependencies are modeled with Static-Built-Using
            #
            # unlike all other relationships this is not yet included in the relations field as
            # of python-debian 1.1.0 so we have to parse it by hand
            s_built_using = package.get("Static-Built-Using")
            if s_built_using:
                sbudepends = Dependency.from_pkg_relations(
                    PkgRelation.parse_relations(s_built_using), is_source=True
                )
            else:
                sbudepends = []

            status_raw = package.get("Status")
            if status_raw:
                _, _, pkg_status = status_raw.split(" ")
                status = DpkgStatus.from_dpkg(pkg_status)
            else:
                status = DpkgStatus.DEBSBOM_UNKNOWN

            raw_priority = package.get("Priority")
            priority = None
            if raw_priority:
                try:
                    priority = DebianPriority(raw_priority)
                except ValueError:
                    pkg_name = package.get("Package")
                    logger.warning(f"{pkg_name}: invalid Priority: '{raw_priority}'")

            return BinaryPackage(
                name=package.get("Package"),
                section=package.get("Section"),
                maintainer=package.get("Maintainer"),
                architecture=package.get("Architecture"),
                source=srcdep,
                version=package.get("Version"),
                depends=dependencies,
                pre_depends=pre_dependencies,
                provides=provides,
                recommends=recommends,
                suggests=suggests,
                built_using=budepends,
                static_built_using=sbudepends,
                description=cls._cleanup_description(package.get("Description")),
                essential=package.get("Essential") == "yes",
                priority=priority,
                homepage=package.get("Homepage"),
                checksums=checksums_from_package(package),
                status=status,
            )

        def satisfies(self, dependency: Dependency) -> bool:
            """Returns True if the binary package satisfies the dependency."""
            if self.name != dependency.name:
                return False

            if dependency.version is None:
                return True

            return dependency.is_satisfying_version(self.version)