Source code for debsbom.apt.copyright

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

from collections.abc import Iterable
from functools import cached_property
from debian.copyright import (
    Copyright as DebCopyright,
    License,
    MachineReadableFormatError,
    NotMachineReadableError,
)
from license_expression import LicenseExpression, Licensing, get_spdx_licensing
import logging
from pathlib import Path
import re

logger = logging.getLogger(__name__)

# well-known list of expressions conversions, see
# https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-specification
WELL_KNOWN_EXPRESSIONS = {
    "AGPL-1": "AGPL-1.0-only",
    "AGPL-1+": "AGPL-1.0-or-later",
    "AGPL-3": "AGPL-3.0-only",
    "AGPL-3+": "AGPL-3.0-or-later",
    "Apache-1": "Apache-1.0",
    "Apache-2": "Apache-2.0",
    "Artistic": "Artistic-1.0",
    "Artistic-1": "Artistic-1.0",
    "Artistic-2": "Artistic-2.0",
    "BSD-2": "BSD-2-Clause",
    "BSD-3": "BSD-3-Clause",
    "BSD-4": "BSD-4-Clause",
    "BSL": "BSL-1.0",
    "BSL-1": "BSL-1.0",
    "CC-BY": "CC-BY-1.0",
    "CC-BY-1": "CC-BY-1.0",
    "CC-BY-2": "CC-BY-2.0",
    "CC-BY-3": "CC-BY-3.0",
    "CC-BY-4": "CC-BY-4.0",
    "CC-BY-NC": "CC-BY-NC-1.0",
    "CC-BY-NC-1": "CC-BY-NC-1.0",
    "CC-BY-NC-2": "CC-BY-NC-2.0",
    "CC-BY-NC-3": "CC-BY-NC-3.0",
    "CC-BY-NC-4": "CC-BY-NC-4.0",
    "CC-BY-NC-ND": "CC-BY-NC-ND-1.0",
    "CC-BY-NC-ND-1": "CC-BY-NC-ND-1.0",
    "CC-BY-NC-ND-2": "CC-BY-NC-ND-2.0",
    "CC-BY-NC-ND-3": "CC-BY-NC-ND-3.0",
    "CC-BY-NC-ND-4": "CC-BY-NC-ND-4.0",
    "CC-BY-NC-SA": "CC-BY-NC-SA-1.0",
    "CC-BY-NC-SA-1": "CC-BY-NC-SA-1.0",
    "CC-BY-NC-SA-2": "CC-BY-NC-SA-2.0",
    "CC-BY-NC-SA-3": "CC-BY-NC-SA-3.0",
    "CC-BY-NC-SA-4": "CC-BY-NC-SA-4.0",
    "CC-BY-SA": "CC-BY-SA-1.0",
    "CC-BY-SA-1": "CC-BY-SA-1.0",
    "CC-BY-SA-2": "CC-BY-SA-2.0",
    "CC-BY-SA-3": "CC-BY-SA-3.0",
    "CC-BY-SA-4": "CC-BY-SA-4.0",
    "CC0": "CC0-1.0",
    "CC0-1": "CC0-1.0",
    "CDDL": "CDDL-1.0",
    "CDDL-1": "CDDL-1.0",
    "CPL": "CPL-1.0",
    "CPL-1": "CPL-1.0",
    "EFL": "EFL-1.0",
    "EFL-1": "EFL-1.0",
    "EFL-2": "EFL-2.0",
    "EPL": "EPL-1.0",
    "EPL-1": "EPL-1.0",
    "EPL-2": "EPL-2.0",
    "Expat": "MIT",
    "GFDL": "GFDL-1.1-only",
    "GFDL+": "GFDL-1.1-or-later",
    "GFDL-1.1": "GFDL-1.1-only",
    "GFDL-1.1+": "GFDL-1.1-or-later",
    "GFDL-1.2": "GFDL-1.2-only",
    "GFDL-1.2+": "GFDL-1.2-or-later",
    "GFDL-1.3": "GFDL-1.3-only",
    "GFDL-1.3+": "GFDL-1.3-or-later",
    "GFDL-NIV": "GFDL-1.1-no-invariants-only",
    "GFDL-NIV+": "GFDL-1.1-no-invariants-or-later",
    "GFDL-NIV-1.1": "GFDL-1.1-no-invariants-only",
    "GFDL-NIV-1.1+": "GFDL-1.1-no-invariants-or-later",
    "GFDL-NIV-1.2": "GFDL-1.2-no-invariants-only",
    "GFDL-NIV-1.2+": "GFDL-1.2-no-invariants-or-later",
    "GFDL-NIV-1.3": "GFDL-1.3-no-invariants-only",
    "GFDL-NIV-1.3+": "GFDL-1.3-no-invariants-or-later",
    "GPL": "GPL-1.0-only",
    "GPL+": "GPL-1.0-or-later",
    "GPL-1": "GPL-1.0-only",
    "GPL-1+": "GPL-1.0-or-later",
    "GPL-2": "GPL-2.0-only",
    "GPL-2+": "GPL-2.0-or-later",
    "GPL-3": "GPL-3.0-only",
    "GPL-3+": "GPL-3.0-or-later",
    "GPL3+": "GPL-3.0-or-later",
    "LGPL": "LGPL-1.0-only",
    "LGPL+": "LGPL-1.0-or-later",
    "LGPL-1": "LGPL-1.0-only",
    "LGPL-1+": "LGPL-1.0-or-later",
    "LGPL-2": "LGPL-2.0-only",
    "LGPL-2+": "LGPL-2.0-or-later",
    "LGPL-3": "LGPL-3.0-only",
    "LGPL-3+": "LGPL-3.0-or-later",
    "LPGL-2.1": "LGPL-2.1-only",
    "LPGL-2.1+": "LGPL-2.1-or-later",
    "LPPL": "LPPL-1.0",
    "LPPL-1": "LPPL-1.0",
    "MPL": "MPL-1.0",
    "MPL-1": "MPL-1.0",
    "MPL-2": "MPL-2.0",
    "PSF-2": "PSF-2.0",
    "Perl": "Artistic-1.0-Perl",
    "Python": "Python-2.0",
    "QPL": "QPL-1.0",
    "QPL-1": "QPL-1.0",
    "RubyLicense": "Ruby",
    "SIL-1": "OFL-1.0",
    "SIL-1.1": "OFL-1.1",
    "SIL-OFL-1": "OFL-1.0",
    "SIL-OFL-1.1": "OFL-1.1",
    "Zope": "ZPL-1.0",
    "Zope-1": "ZPL-1.0",
    "Zope-2": "ZPL-2.0",
}

# Debian exception names are free-form. Only map complete license names with
# exceptions whose license and exception texts have been verified.
WELL_KNOWN_EXCEPTIONS = {
    "GPL-3+ with Bison exception": "GPL-3.0-or-later WITH Bison-exception-2.2",
    "GPL-3+ with Bison-2.2 exception": "GPL-3.0-or-later WITH Bison-exception-2.2",
    "GPL-2 with Linux-syscall-note exception": "GPL-2.0-only WITH Linux-syscall-note",
    "GPL-3+ with texinfo exception": "GPL-3.0-or-later WITH Texinfo-exception",
    "GPL-3.0-or-later-WITH-Autoconf-exception-macro": (
        "GPL-3.0-or-later WITH Autoconf-exception-macro"
    ),
    "BSD-3-clause-Cambridge with BINARY LIBRARY-LIKE PACKAGES exception": (
        "BSD-3-Clause WITH PCRE2-exception"
    ),
    "GPL-3+-WITH-BISON-EXCEPTION": "GPL-3.0-or-later WITH Bison-exception-2.2",
}

_EXCEPTIONS = {name.lower(): expression for name, expression in WELL_KNOWN_EXCEPTIONS.items()}
_EXCEPTIONS_RE = re.compile(
    r"(?<![\w+.-])(?:{})(?![\w+.-])".format(
        "|".join(re.escape(name) for name in sorted(WELL_KNOWN_EXCEPTIONS, key=len, reverse=True))
    ),
    re.IGNORECASE,
)


[docs] class UnknownLicenseError(Exception): """License is unknown to the SPDX standard.""" pass
[docs] class CopyrightDirectory: """Directory of Debian copyright information.""" def __init__(self, path: Path): self.copyright_root = path @classmethod def for_rootdir(cls, root: Path) -> "CopyrightDirectory": return CopyrightDirectory(root / "usr/share/doc")
[docs] def copyright(self, bin_pkg) -> Copyright: """Get the copyright information for a binary package.""" copyright_path = self.copyright_root / bin_pkg.name / "copyright" return Copyright(copyright_path)