diff --git a/.github/workflows/install_and_test.yml b/.github/workflows/install_and_test.yml index 77078c5f6..de867e1cb 100644 --- a/.github/workflows/install_and_test.yml +++ b/.github/workflows/install_and_test.yml @@ -23,6 +23,7 @@ jobs: python -m build -nwx . python -m pip install --upgrade ./dist/*.whl python -m pip install pytest + python -m pip install rdflib shell: bash - name: Run tests run: pytest diff --git a/README.md b/README.md index e281354ac..cb8e9d3c2 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,12 @@ instead of `bin`. # Dependencies -* PLY: https://pypi.python.org/pypi/ply/ used for parsing. -* rdflib: https://pypi.python.org/pypi/rdflib/ for handling RDF. * PyYAML: https://pypi.org/project/PyYAML/ for handling YAML. * xmltodict: https://pypi.org/project/xmltodict/ for handling XML. * click: https://pypi.org/project/click/ for creating the CLI interface. +* rdflib: https://pypi.python.org/pypi/rdflib/ for handling RDF. +* typeguard: https://pypi.org/project/typeguard/ for using typehints. +* uritools: https://pypi.org/project/uritools/ for validation of URIs. # Support diff --git a/pyproject.toml b/pyproject.toml index b7f176add..97a5f851a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "spdx-tools" -authors = [{name = "Ahmed H. Ismail", email = "ahm3d.hisham@gmail.com"}] +authors = [{ name = "Ahmed H. Ismail", email = "ahm3d.hisham@gmail.com" }] maintainers = [ - {name = "Philippe Ombredanne", email = "pombredanne@gmail.com"}, - {name = "SPDX group at the Linux Foundation and others"}, + { name = "Philippe Ombredanne", email = "pombredanne@gmail.com" }, + { name = "SPDX group at the Linux Foundation and others" }, ] -license = {text = "Apache-2.0"} +license = { text = "Apache-2.0" } description = "SPDX parser and tools." readme = "README.md" classifiers = [ @@ -22,13 +22,14 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", ] -urls = {Homepage = "https://github.com/spdx/tools-python"} +urls = { Homepage = "https://github.com/spdx/tools-python" } requires-python = ">=3.7" -dependencies = ["ply", "rdflib", "click", "pyyaml", "xmltodict", "typeguard", "uritools", "license_expression"] +dependencies = ["click", "pyyaml", "xmltodict", "rdflib", "typeguard", "uritools", "license_expression"] dynamic = ["version"] [project.optional-dependencies] -test = ["pytest"] +test = ["pytest", "rdflib"] +rdf = ["rdflib"] [project.scripts] pyspdxtools = "spdx.clitools.pyspdxtools:main" @@ -40,8 +41,10 @@ include-package-data = true [tool.setuptools.packages.find] where = ["src"] +# the default git describe resolves to the tag `python3.6` because the current release tag is not on main +# by adding "v" the command resolves to the alpha release of 0.7.0 which leads to the desired name spdx-tools-0.7.0 [tool.setuptools_scm] -git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"] # `python3.6` tag falsely matches to the default one, clrearly a bug in setuptools_scm +git_describe_command = ["git", "describe", "--dirty", "--tags", "--long", "--match", "v[0-9]*"] [tool.aliases] release = "clean --all sdist --formats=gztar bdist_wheel" diff --git a/src/spdx/model/actor.py b/src/spdx/model/actor.py index 4dc3ac3fe..022954f3a 100644 --- a/src/spdx/model/actor.py +++ b/src/spdx/model/actor.py @@ -36,3 +36,6 @@ def to_serialized_string(self) -> str: """ optional_email = f" ({self.email})" if self.email else "" return "".join([f"{self.actor_type.name.title()}:", f" {self.name}", optional_email]) + + def __str__(self): + return self.to_serialized_string() diff --git a/src/spdx/model/package.py b/src/spdx/model/package.py index 50bd802b1..7f4e32296 100644 --- a/src/spdx/model/package.py +++ b/src/spdx/model/package.py @@ -57,7 +57,8 @@ class ExternalPackageRefCategory(Enum): CATEGORY_TO_EXTERNAL_PACKAGE_REF_TYPES: Dict[ExternalPackageRefCategory, List[str]] = { ExternalPackageRefCategory.SECURITY : ["cpe22Type", "cpe23Type", "advisory", "fix", "url", "swid"], ExternalPackageRefCategory.PACKAGE_MANAGER : ["maven-central", "npm", "nuget", "bower", "purl"], - ExternalPackageRefCategory.PERSISTENT_ID : ["swh", "gitoid"] + ExternalPackageRefCategory.PERSISTENT_ID : ["swh", "gitoid"], + ExternalPackageRefCategory.OTHER: [] } diff --git a/src/spdx/rdfschema/__init__.py b/src/spdx/rdfschema/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/spdx/rdfschema/namespace.py b/src/spdx/rdfschema/namespace.py new file mode 100644 index 000000000..b29f55ad5 --- /dev/null +++ b/src/spdx/rdfschema/namespace.py @@ -0,0 +1,16 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Namespace + +SPDX_NAMESPACE = Namespace("http://spdx.org/rdf/terms#") +POINTER_NAMESPACE = Namespace("http://www.w3.org/2009/pointers#") +REFERENCE_NAMESPACE = Namespace("http://spdx.org/rdf/references/") +LICENSE_NAMESPACE = Namespace("http://spdx.org/licenses/") diff --git a/src/spdx/writer/rdf/__init__.py b/src/spdx/writer/rdf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/spdx/writer/rdf/annotation_writer.py b/src/spdx/writer/rdf/annotation_writer.py new file mode 100644 index 000000000..56b272329 --- /dev/null +++ b/src/spdx/writer/rdf/annotation_writer.py @@ -0,0 +1,34 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict + +from rdflib import Graph, Literal, RDFS, URIRef, RDF, BNode + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.model.annotation import Annotation +from spdx.writer.casing_tools import snake_case_to_camel_case +from spdx.writer.rdf.writer_utils import add_namespace_to_spdx_id +from spdx.rdfschema.namespace import SPDX_NAMESPACE + + +def add_annotation_to_graph(annotation: Annotation, graph: Graph, doc_namespace: str, + external_doc_ref_to_namespace: Dict[str, str]): + annotation_resource = URIRef(add_namespace_to_spdx_id(annotation.spdx_id, doc_namespace, external_doc_ref_to_namespace)) + annotation_node = BNode() + graph.add((annotation_node, RDF.type, SPDX_NAMESPACE.Annotation)) + graph.add((annotation_node, SPDX_NAMESPACE.annotationType, + SPDX_NAMESPACE[f"annotationType_{snake_case_to_camel_case(annotation.annotation_type.name)}"])) + graph.add((annotation_node, SPDX_NAMESPACE.annotator, Literal(annotation.annotator.to_serialized_string()))) + graph.add( + (annotation_node, SPDX_NAMESPACE.annotationDate, Literal(datetime_to_iso_string(annotation.annotation_date)))) + graph.add((annotation_node, RDFS.comment, Literal(annotation.annotation_comment))) + + graph.add((annotation_resource, SPDX_NAMESPACE.annotation, annotation_node)) diff --git a/src/spdx/writer/rdf/checksum_writer.py b/src/spdx/writer/rdf/checksum_writer.py new file mode 100644 index 000000000..b9b2fe84f --- /dev/null +++ b/src/spdx/writer/rdf/checksum_writer.py @@ -0,0 +1,31 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef, BNode, RDF, Literal + +from spdx.model.checksum import Checksum, ChecksumAlgorithm +from spdx.rdfschema.namespace import SPDX_NAMESPACE + + +def add_checksum_to_graph(checksum: Checksum, graph: Graph, parent: URIRef): + checksum_node = BNode() + graph.add((checksum_node, RDF.type, SPDX_NAMESPACE.Checksum)) + graph.add((checksum_node, SPDX_NAMESPACE.algorithm, algorithm_to_rdf_string(checksum.algorithm))) + graph.add((checksum_node, SPDX_NAMESPACE.checksumValue, Literal(checksum.value))) + + graph.add((parent, SPDX_NAMESPACE.checksum, checksum_node)) + +def algorithm_to_rdf_string(algorithm: ChecksumAlgorithm) -> URIRef: + if "BLAKE2B" in algorithm.name: + algorithm_rdf_string = algorithm.name.replace("_","").lower() + else: + algorithm_rdf_string = algorithm.name.lower() + + return SPDX_NAMESPACE[f"checksumAlgorithm_{algorithm_rdf_string}"] diff --git a/src/spdx/writer/rdf/creation_info_writer.py b/src/spdx/writer/rdf/creation_info_writer.py new file mode 100644 index 000000000..1fe1330c5 --- /dev/null +++ b/src/spdx/writer/rdf/creation_info_writer.py @@ -0,0 +1,45 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, BNode, RDF, Literal, RDFS, URIRef + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.model.document import CreationInfo +from spdx.writer.rdf.external_document_ref_writer import add_external_document_ref_to_graph +from spdx.writer.rdf.writer_utils import add_optional_literal +from spdx.rdfschema.namespace import SPDX_NAMESPACE, LICENSE_NAMESPACE + + +def add_creation_info_to_graph(creation_info: CreationInfo, graph: Graph): + doc_node = URIRef(f"{creation_info.document_namespace}#{creation_info.spdx_id}") + graph.add((doc_node, RDF.type, SPDX_NAMESPACE.SpdxDocument)) + graph.add((doc_node, SPDX_NAMESPACE.specVersion, Literal(creation_info.spdx_version))) + graph.add((doc_node, SPDX_NAMESPACE.dataLicense, LICENSE_NAMESPACE[creation_info.data_license])) + graph.add((doc_node, SPDX_NAMESPACE.name, Literal(creation_info.name))) + add_optional_literal(creation_info.document_comment, graph, doc_node, RDFS.comment) + + creation_info_node = BNode() + graph.add((creation_info_node, RDF.type, SPDX_NAMESPACE.CreationInfo)) + + graph.add((creation_info_node, SPDX_NAMESPACE.created, Literal(datetime_to_iso_string(creation_info.created)))) + + for creator in creation_info.creators: + graph.add((creation_info_node, SPDX_NAMESPACE.creator, Literal(creator.to_serialized_string()))) + + add_optional_literal(creation_info.license_list_version, graph, creation_info_node, + SPDX_NAMESPACE.licenseListVersion) + add_optional_literal(creation_info.creator_comment, graph, creation_info_node, RDFS.comment) + + graph.add((doc_node, SPDX_NAMESPACE.creationInfo, creation_info_node)) + + for external_document_ref in creation_info.external_document_refs: + add_external_document_ref_to_graph(external_document_ref, graph, doc_node, creation_info.document_namespace) + + return doc_node diff --git a/src/spdx/writer/rdf/external_document_ref_writer.py b/src/spdx/writer/rdf/external_document_ref_writer.py new file mode 100644 index 000000000..7521bd338 --- /dev/null +++ b/src/spdx/writer/rdf/external_document_ref_writer.py @@ -0,0 +1,25 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef, RDF + +from spdx.model.external_document_ref import ExternalDocumentRef +from spdx.writer.rdf.checksum_writer import add_checksum_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE + + +def add_external_document_ref_to_graph(external_document_ref: ExternalDocumentRef, graph: Graph, doc_node: URIRef, + doc_namespace: str): + external_document_ref_resource = URIRef(f"{doc_namespace}#{external_document_ref.document_ref_id}") + graph.add((external_document_ref_resource, RDF.type, SPDX_NAMESPACE.ExternalDocumentRef)) + graph.add((external_document_ref_resource, SPDX_NAMESPACE.spdxDocument, URIRef(external_document_ref.document_uri))) + add_checksum_to_graph(external_document_ref.checksum, graph, external_document_ref_resource) + + graph.add((doc_node, SPDX_NAMESPACE.externalDocumentRef, external_document_ref_resource)) diff --git a/src/spdx/writer/rdf/extracted_licensing_info_writer.py b/src/spdx/writer/rdf/extracted_licensing_info_writer.py new file mode 100644 index 000000000..4d0f4b674 --- /dev/null +++ b/src/spdx/writer/rdf/extracted_licensing_info_writer.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef, RDF, BNode, RDFS, Literal +from spdx.writer.rdf.writer_utils import add_optional_literal, add_literal_or_no_assertion +from spdx.rdfschema.namespace import SPDX_NAMESPACE + +from spdx.model.extracted_licensing_info import ExtractedLicensingInfo + + +def add_extracted_licensing_info_to_graph(extracted_licensing_info: ExtractedLicensingInfo, graph: Graph, doc_node, + doc_namespace: str): + if extracted_licensing_info.license_id: + extracted_licensing_info_resource = URIRef(f"{doc_namespace}#{extracted_licensing_info.license_id}") + graph.add((extracted_licensing_info_resource, RDF.type, SPDX_NAMESPACE.ExtractedLicensingInfo)) + else: + extracted_licensing_info_resource = BNode() + add_optional_literal(extracted_licensing_info.license_id, graph, extracted_licensing_info_resource, + SPDX_NAMESPACE.licenseId) + add_optional_literal(extracted_licensing_info.extracted_text, graph, extracted_licensing_info_resource, + SPDX_NAMESPACE.extractedText) + add_literal_or_no_assertion(extracted_licensing_info.license_name, graph, extracted_licensing_info_resource, + SPDX_NAMESPACE.name) + for cross_reference in extracted_licensing_info.cross_references: + graph.add((extracted_licensing_info_resource, RDFS.seeAlso, Literal(cross_reference))) + add_optional_literal(extracted_licensing_info.comment, graph, extracted_licensing_info_resource, RDFS.comment) + + graph.add((doc_node, SPDX_NAMESPACE.hasExtractedLicensingInfo, extracted_licensing_info_resource)) diff --git a/src/spdx/writer/rdf/file_writer.py b/src/spdx/writer/rdf/file_writer.py new file mode 100644 index 000000000..76d6209f4 --- /dev/null +++ b/src/spdx/writer/rdf/file_writer.py @@ -0,0 +1,47 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict + +from rdflib import Graph, URIRef, Literal, RDF, RDFS + +from spdx.model.file import File +from spdx.writer.casing_tools import snake_case_to_camel_case +from spdx.writer.rdf.checksum_writer import add_checksum_to_graph +from spdx.writer.rdf.license_expression_writer import add_license_expression_or_none_or_no_assertion +from spdx.writer.rdf.writer_utils import add_optional_literal, add_namespace_to_spdx_id +from spdx.rdfschema.namespace import SPDX_NAMESPACE + + +def add_file_to_graph(file: File, graph: Graph, doc_namespace: str, + external_doc_ref_to_namespace: Dict[str, str]): + file_resource = URIRef(add_namespace_to_spdx_id(file.spdx_id, doc_namespace, external_doc_ref_to_namespace)) + graph.add((file_resource, RDF.type, SPDX_NAMESPACE.File)) + graph.add((file_resource, SPDX_NAMESPACE.fileName, Literal(file.name))) + for file_type in file.file_type: + graph.add((file_resource, SPDX_NAMESPACE.fileType, + SPDX_NAMESPACE[f"fileType_{snake_case_to_camel_case(file_type.name)}"])) + + for checksum in file.checksums: + add_checksum_to_graph(checksum, graph, file_resource) + + add_license_expression_or_none_or_no_assertion(file.license_concluded, graph, file_resource, + SPDX_NAMESPACE.licenseConcluded, doc_namespace) + add_license_expression_or_none_or_no_assertion(file.license_info_in_file, graph, file_resource, + SPDX_NAMESPACE.licenseInfoInFile, doc_namespace) + + add_optional_literal(file.license_comment, graph, file_resource, SPDX_NAMESPACE.licenseComments) + add_optional_literal(file.copyright_text, graph, file_resource, SPDX_NAMESPACE.copyrightText) + add_optional_literal(file.comment, graph, file_resource, RDFS.comment) + add_optional_literal(file.notice, graph, file_resource, SPDX_NAMESPACE.noticeText) + for contributor in file.contributors: + graph.add((file_resource, SPDX_NAMESPACE.fileContributor, Literal(contributor))) + for attribution_text in file.attribution_texts: + graph.add((file_resource, SPDX_NAMESPACE.attributionText, Literal(attribution_text))) diff --git a/src/spdx/writer/rdf/license_expression_writer.py b/src/spdx/writer/rdf/license_expression_writer.py new file mode 100644 index 000000000..aa21ba740 --- /dev/null +++ b/src/spdx/writer/rdf/license_expression_writer.py @@ -0,0 +1,87 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Union, List + +from boolean import Expression +from rdflib import Graph, URIRef, BNode, RDF +from license_expression import AND, OR, LicenseWithExceptionSymbol, LicenseSymbol, get_spdx_licensing, ExpressionInfo, \ + LicenseExpression +from rdflib.term import Node, Literal + +from spdx.model.spdx_no_assertion import SpdxNoAssertion +from spdx.model.spdx_none import SpdxNone + +from spdx.rdfschema.namespace import SPDX_NAMESPACE, LICENSE_NAMESPACE + + +def add_license_expression_or_none_or_no_assertion(value: Union[ + List[LicenseExpression], LicenseExpression, SpdxNoAssertion, SpdxNone], graph: Graph, parent: Node, predicate: Node, + doc_namespace: str): + if isinstance(value, SpdxNoAssertion): + graph.add((parent, predicate, SPDX_NAMESPACE.noassertion)) + return + if isinstance(value, SpdxNone): + graph.add((parent, predicate, SPDX_NAMESPACE.none)) + return + if isinstance(value, list): + for license_expression in value: + add_license_expression_to_graph(license_expression, graph, parent, predicate, doc_namespace) + if isinstance(value, LicenseExpression): + add_license_expression_to_graph(value, graph, parent, predicate, doc_namespace) + + +def add_license_expression_to_graph(license_expression: Expression, graph: Graph, parent: Node, predicate: Node, + doc_namespace: str): + if isinstance(license_expression, AND): + member_node = BNode() + graph.add((member_node, RDF.type, SPDX_NAMESPACE.ConjunctiveLicenseSet)) + graph.add((parent, predicate, member_node)) + for arg in license_expression.args: + add_license_expression_to_graph(arg, graph, member_node, SPDX_NAMESPACE.member, doc_namespace) + if isinstance(license_expression, OR): + member_node = BNode() + graph.add((member_node, RDF.type, SPDX_NAMESPACE.DisjunctiveLicenseSet)) + graph.add((parent, predicate, member_node)) + for arg in license_expression.args: + add_license_expression_to_graph(arg, graph, member_node, SPDX_NAMESPACE.member, doc_namespace) + if isinstance(license_expression, LicenseWithExceptionSymbol): + member_node = BNode() + graph.add((member_node, RDF.type, SPDX_NAMESPACE.WithExceptionOperator)) + graph.add((parent, predicate, member_node)) + + add_license_expression_to_graph(license_expression.license_symbol, graph, member_node, SPDX_NAMESPACE.member, + doc_namespace) + add_license_exception_to_graph(license_expression.exception_symbol, graph, member_node) + + if isinstance(license_expression, LicenseSymbol): + if license_or_exception_is_on_spdx_licensing_list(license_expression): + graph.add( + (parent, predicate, LICENSE_NAMESPACE[str(license_expression)])) + else: + # assuming that the license expression is a LicenseRef to an instance of ExtractedLicensingInfo + graph.add((parent, predicate, URIRef(f"{doc_namespace}#{license_expression}"))) + + +def license_or_exception_is_on_spdx_licensing_list(license_symbol: LicenseSymbol) -> bool: + symbol_info: ExpressionInfo = get_spdx_licensing().validate(license_symbol) + return not symbol_info.errors + + +def add_license_exception_to_graph(license_exception: LicenseSymbol, graph: Graph, parent: Node): + if license_or_exception_is_on_spdx_licensing_list(license_exception): + exception_node = LICENSE_NAMESPACE[str(license_exception)] + graph.add((parent, SPDX_NAMESPACE.licenseException, exception_node)) + else: + exception_node = BNode() + graph.add((exception_node, SPDX_NAMESPACE.licenseExceptionId, Literal(license_exception))) + graph.add((parent, SPDX_NAMESPACE.licenseException, exception_node)) + + graph.add((exception_node, RDF.type, SPDX_NAMESPACE.LicenseException)) diff --git a/src/spdx/writer/rdf/package_writer.py b/src/spdx/writer/rdf/package_writer.py new file mode 100644 index 000000000..37f963f91 --- /dev/null +++ b/src/spdx/writer/rdf/package_writer.py @@ -0,0 +1,100 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict + +from rdflib import Graph, URIRef, RDF, Literal, XSD, BNode, DOAP, RDFS +from spdx.writer.rdf.license_expression_writer import add_license_expression_or_none_or_no_assertion + +from spdx.writer.casing_tools import snake_case_to_camel_case +from spdx.writer.rdf.checksum_writer import add_checksum_to_graph + +from spdx.model.package import Package, PackageVerificationCode, ExternalPackageRef, \ + CATEGORY_TO_EXTERNAL_PACKAGE_REF_TYPES +from spdx.writer.rdf.writer_utils import add_optional_literal, add_literal_or_no_assertion_or_none, \ + add_datetime_to_graph, add_namespace_to_spdx_id +from spdx.rdfschema.namespace import SPDX_NAMESPACE, REFERENCE_NAMESPACE + + +def add_package_to_graph(package: Package, graph: Graph, doc_namespace: str, + external_doc_ref_to_namespace: Dict[str, str]): + package_resource = URIRef(add_namespace_to_spdx_id(package.spdx_id, doc_namespace, external_doc_ref_to_namespace)) + graph.add((package_resource, RDF.type, SPDX_NAMESPACE.Package)) + + graph.add((package_resource, SPDX_NAMESPACE.name, Literal(package.name))) + add_optional_literal(package.version, graph, package_resource, SPDX_NAMESPACE.versionInfo) + add_optional_literal(package.file_name, graph, package_resource, SPDX_NAMESPACE.packageFileName) + add_optional_literal(package.supplier, graph, package_resource, SPDX_NAMESPACE.supplier) + add_optional_literal(package.originator, graph, package_resource, SPDX_NAMESPACE.originator) + add_literal_or_no_assertion_or_none(package.download_location, graph, package_resource, + SPDX_NAMESPACE.downloadLocation) + graph.add((package_resource, SPDX_NAMESPACE.filesAnalyzed, Literal(package.files_analyzed, datatype=XSD.boolean))) + add_package_verification_code_to_graph(package.verification_code, graph, package_resource) + for checksum in package.checksums: + add_checksum_to_graph(checksum, graph, package_resource) + + add_optional_literal(package.homepage, graph, package_resource, DOAP.homepage) + add_optional_literal(package.source_info, graph, package_resource, SPDX_NAMESPACE.sourceInfo) + add_license_expression_or_none_or_no_assertion(package.license_concluded, graph, package_resource, + SPDX_NAMESPACE.licenseConcluded, doc_namespace) + add_license_expression_or_none_or_no_assertion(package.license_info_from_files, graph, package_resource, + SPDX_NAMESPACE.licenseInfoFromFiles, doc_namespace) + add_license_expression_or_none_or_no_assertion(package.license_declared, graph, package_resource, + SPDX_NAMESPACE.licenseDeclared, doc_namespace) + add_optional_literal(package.license_comment, graph, package_resource, SPDX_NAMESPACE.licenseComments) + add_optional_literal(package.copyright_text, graph, package_resource, SPDX_NAMESPACE.copyrightText) + add_optional_literal(package.summary, graph, package_resource, SPDX_NAMESPACE.summary) + add_optional_literal(package.description, graph, package_resource, SPDX_NAMESPACE.description) + add_optional_literal(package.comment, graph, package_resource, RDFS.comment) + for external_reference in package.external_references: + add_external_package_ref_to_graph(external_reference, graph, package_resource) + for attribution_text in package.attribution_texts: + add_optional_literal(attribution_text, graph, package_resource, SPDX_NAMESPACE.attributionText) + if package.primary_package_purpose: + graph.add((package_resource, SPDX_NAMESPACE.primaryPackagePurpose, + SPDX_NAMESPACE[f"purpose_{snake_case_to_camel_case(package.primary_package_purpose.name)}"])) + + add_datetime_to_graph(package.release_date, graph, package_resource, SPDX_NAMESPACE.releaseDate) + add_datetime_to_graph(package.built_date, graph, package_resource, SPDX_NAMESPACE.builtDate) + add_datetime_to_graph(package.valid_until_date, graph, package_resource, SPDX_NAMESPACE.validUntilDate) + + +def add_package_verification_code_to_graph(package_verification_code: PackageVerificationCode, graph: Graph, + package_node: URIRef): + if not package_verification_code: + return + package_verification_code_node = BNode() + graph.add((package_verification_code_node, RDF.type, SPDX_NAMESPACE.PackageVerificationCode)) + graph.add((package_verification_code_node, SPDX_NAMESPACE.packageVerificationCodeValue, + Literal(package_verification_code.value))) + for excluded_file in package_verification_code.excluded_files: + graph.add((package_verification_code_node, SPDX_NAMESPACE.packageVerificationCodeExcludedFile, + Literal(excluded_file))) + + graph.add((package_node, SPDX_NAMESPACE.packageVerificationCode, package_verification_code_node)) + + +def add_external_package_ref_to_graph(external_package_ref: ExternalPackageRef, graph: Graph, package_node: URIRef): + external_package_ref_node = BNode() + graph.add((external_package_ref_node, RDF.type, SPDX_NAMESPACE.ExternalRef)) + graph.add((external_package_ref_node, SPDX_NAMESPACE.referenceCategory, + SPDX_NAMESPACE[f"referenceCategory_{snake_case_to_camel_case(external_package_ref.category.name)}"])) + + if external_package_ref.reference_type in CATEGORY_TO_EXTERNAL_PACKAGE_REF_TYPES[external_package_ref.category]: + graph.add((external_package_ref_node, SPDX_NAMESPACE.referenceType, + REFERENCE_NAMESPACE[external_package_ref.reference_type])) + else: + graph.add((external_package_ref_node, SPDX_NAMESPACE.referenceType, + URIRef(external_package_ref.reference_type))) + graph.add((external_package_ref_node, SPDX_NAMESPACE.referenceLocator, Literal(external_package_ref.locator))) + if external_package_ref.comment: + graph.add((external_package_ref_node, RDFS.comment, Literal(external_package_ref.comment))) + + graph.add((package_node, SPDX_NAMESPACE.externalRef, external_package_ref_node)) diff --git a/src/spdx/writer/rdf/rdf_writer.py b/src/spdx/writer/rdf/rdf_writer.py new file mode 100644 index 000000000..9251f57a8 --- /dev/null +++ b/src/spdx/writer/rdf/rdf_writer.py @@ -0,0 +1,63 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, List + +from rdflib import Graph, DOAP +from rdflib.compare import to_isomorphic + +from spdx.model.document import Document +from spdx.validation.document_validator import validate_full_spdx_document +from spdx.validation.validation_message import ValidationMessage +from spdx.writer.rdf.annotation_writer import add_annotation_to_graph +from spdx.writer.rdf.creation_info_writer import add_creation_info_to_graph +from spdx.writer.rdf.extracted_licensing_info_writer import add_extracted_licensing_info_to_graph +from spdx.writer.rdf.file_writer import add_file_to_graph +from spdx.writer.rdf.package_writer import add_package_to_graph +from spdx.writer.rdf.relationship_writer import add_relationship_to_graph +from spdx.writer.rdf.snippet_writer import add_snippet_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE, POINTER_NAMESPACE + + +def write_document_to_file(document: Document, file_name: str, validate: bool): + if validate: + validation_messages: List[ValidationMessage] = validate_full_spdx_document(document, + document.creation_info.spdx_version) + if validation_messages: + raise ValueError(f"Document is not valid. The following errors were detected: {validation_messages}") + + graph = Graph() + doc_namespace = document.creation_info.document_namespace + external_doc_ref_to_namespace: Dict[str, str] = {external_doc_ref.document_ref_id: external_doc_ref.document_uri for + external_doc_ref in document.creation_info.external_document_refs} + doc_node = add_creation_info_to_graph(document.creation_info, graph) + for annotation in document.annotations: + add_annotation_to_graph(annotation, graph, doc_namespace, external_doc_ref_to_namespace) + + for file in document.files: + add_file_to_graph(file, graph, doc_namespace, external_doc_ref_to_namespace) + + for package in document.packages: + add_package_to_graph(package, graph, doc_namespace, external_doc_ref_to_namespace) + + for relationship in document.relationships: + add_relationship_to_graph(relationship, graph, doc_namespace, external_doc_ref_to_namespace) + + for snippet in document.snippets: + add_snippet_to_graph(snippet, graph, doc_namespace, external_doc_ref_to_namespace) + + for extracted_licensing_info in document.extracted_licensing_info: + add_extracted_licensing_info_to_graph(extracted_licensing_info, graph, doc_node, doc_namespace) + + graph = to_isomorphic(graph) + graph.bind("spdx", SPDX_NAMESPACE) + graph.bind("doap", DOAP) + graph.bind("ptr", POINTER_NAMESPACE) + graph.serialize(file_name, "pretty-xml", encoding="UTF-8", max_depth=100) diff --git a/src/spdx/writer/rdf/relationship_writer.py b/src/spdx/writer/rdf/relationship_writer.py new file mode 100644 index 000000000..fcef9648f --- /dev/null +++ b/src/spdx/writer/rdf/relationship_writer.py @@ -0,0 +1,40 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict + +from rdflib import Graph, BNode, RDF, URIRef + +from spdx.model.relationship import Relationship +from spdx.model.spdx_no_assertion import SpdxNoAssertion +from spdx.model.spdx_none import SpdxNone +from spdx.writer.casing_tools import snake_case_to_camel_case +from spdx.writer.rdf.writer_utils import add_namespace_to_spdx_id +from spdx.rdfschema.namespace import SPDX_NAMESPACE + + +def add_relationship_to_graph(relationship: Relationship, graph: Graph, doc_namespace: str, + external_doc_ref_to_namespace: Dict[str, str]): + relationship_node = BNode() + graph.add((relationship_node, RDF.type, SPDX_NAMESPACE.Relationship)) + graph.add((relationship_node, SPDX_NAMESPACE.relationshipType, + SPDX_NAMESPACE[f"relationshipType_{snake_case_to_camel_case(relationship.relationship_type.name)}"])) + if isinstance(relationship.related_spdx_element_id, SpdxNone): + graph.add((relationship_node, SPDX_NAMESPACE.relatedSpdxElement, SPDX_NAMESPACE.none)) + elif isinstance(relationship.related_spdx_element_id, SpdxNoAssertion): + graph.add((relationship_node, SPDX_NAMESPACE.relatedSpdxElement, SPDX_NAMESPACE.noassertion)) + else: + graph.add((relationship_node, SPDX_NAMESPACE.relatedSpdxElement, + URIRef(add_namespace_to_spdx_id(relationship.related_spdx_element_id, doc_namespace, + external_doc_ref_to_namespace)))) + + relationship_resource = URIRef( + add_namespace_to_spdx_id(relationship.spdx_element_id, doc_namespace, external_doc_ref_to_namespace)) + graph.add((relationship_resource, SPDX_NAMESPACE.relationship, relationship_node)) diff --git a/src/spdx/writer/rdf/snippet_writer.py b/src/spdx/writer/rdf/snippet_writer.py new file mode 100644 index 000000000..b7ab6ce2a --- /dev/null +++ b/src/spdx/writer/rdf/snippet_writer.py @@ -0,0 +1,62 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Tuple, Optional, Dict + +from rdflib import Graph, URIRef, RDF, RDFS, Literal, BNode + +from spdx.writer.rdf.license_expression_writer import add_license_expression_or_none_or_no_assertion +from spdx.writer.rdf.writer_utils import add_optional_literal, add_namespace_to_spdx_id +from spdx.rdfschema.namespace import SPDX_NAMESPACE, POINTER_NAMESPACE + +from spdx.model.snippet import Snippet + + +def add_snippet_to_graph(snippet: Snippet, graph: Graph, doc_namespace: str, + external_doc_ref_to_namespace: Dict[str, str]): + snippet_resource = URIRef(add_namespace_to_spdx_id(snippet.spdx_id, doc_namespace, external_doc_ref_to_namespace)) + graph.add((snippet_resource, RDF.type, SPDX_NAMESPACE.Snippet)) + + snippet_from_file_ref = URIRef( + add_namespace_to_spdx_id(snippet.file_spdx_id, doc_namespace, external_doc_ref_to_namespace)) + graph.add((snippet_resource, SPDX_NAMESPACE.snippetFromFile, + snippet_from_file_ref)) + add_range_to_graph(snippet.byte_range, graph, snippet_resource, snippet_from_file_ref, + POINTER_NAMESPACE.ByteOffsetPointer) + add_range_to_graph(snippet.line_range, graph, snippet_resource, snippet_from_file_ref, + POINTER_NAMESPACE.LineCharPointer) + add_license_expression_or_none_or_no_assertion(snippet.license_concluded, graph, snippet_resource, + SPDX_NAMESPACE.licenseConcluded, doc_namespace) + add_license_expression_or_none_or_no_assertion(snippet.license_info_in_snippet, graph, snippet_resource, + SPDX_NAMESPACE.licenseInfoInSnippet, doc_namespace) + add_optional_literal(snippet.license_comment, graph, snippet_resource, SPDX_NAMESPACE.licenseComments) + add_optional_literal(snippet.copyright_text, graph, snippet_resource, SPDX_NAMESPACE.copyrightText) + add_optional_literal(snippet.comment, graph, snippet_resource, RDFS.comment) + add_optional_literal(snippet.name, graph, snippet_resource, SPDX_NAMESPACE.name) + for attribution_text in snippet.attribution_texts: + graph.add((snippet_resource, SPDX_NAMESPACE.attributionText, Literal(attribution_text))) + + +def add_range_to_graph(range_information: Optional[Tuple[int, int]], graph: Graph, snippet_node: URIRef, + snippet_from_file_ref: URIRef, pointer_class: URIRef): + start_end_pointer = BNode() + graph.add((start_end_pointer, RDF.type, POINTER_NAMESPACE.StartEndPointer)) + for (predicate, value) in [(POINTER_NAMESPACE.startPointer, range_information[0]), + (POINTER_NAMESPACE.endPointer, range_information[1])]: + pointer_node = BNode() + graph.add((pointer_node, RDF.type, pointer_class)) + graph.add((start_end_pointer, predicate, pointer_node)) + graph.add((pointer_node, POINTER_NAMESPACE.reference, snippet_from_file_ref)) + if pointer_class == POINTER_NAMESPACE.ByteOffsetPointer: + graph.add((pointer_node, POINTER_NAMESPACE.offset, Literal(value))) + else: + graph.add((pointer_node, POINTER_NAMESPACE.lineNumber, Literal(value))) + + graph.add((snippet_node, SPDX_NAMESPACE.range, start_end_pointer)) diff --git a/src/spdx/writer/rdf/writer_utils.py b/src/spdx/writer/rdf/writer_utils.py new file mode 100644 index 000000000..772f80f6e --- /dev/null +++ b/src/spdx/writer/rdf/writer_utils.py @@ -0,0 +1,70 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from datetime import datetime +from typing import Any, Optional, Dict + +from rdflib import Graph, Literal +from rdflib.term import Node + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.model.spdx_no_assertion import SpdxNoAssertion +from spdx.model.spdx_none import SpdxNone +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from spdx.validation.spdx_id_validators import is_valid_internal_spdx_id + + +def add_optional_literal(value: Any, graph: Graph, parent: Node, predicate: Node): + if value is None: + return + if isinstance(value, list): + for element in value: + graph.add((parent, predicate, Literal(str(element)))) + return + graph.add((parent, predicate, Literal(str(value)))) + + +def add_literal_or_no_assertion_or_none(value: Any, graph: Graph, parent: Node, predicate: Node): + if value is None: + return + if isinstance(value, SpdxNone): + graph.add((parent, predicate, SPDX_NAMESPACE.none)) + return + add_literal_or_no_assertion(value, graph, parent, predicate) + + +def add_literal_or_no_assertion(value: Any, graph: Graph, parent: Node, predicate: Node): + if value is None: + return + if isinstance(value, SpdxNoAssertion): + graph.add((parent, predicate, SPDX_NAMESPACE.noassertion)) + return + add_optional_literal(value, graph, parent, predicate) + + +def add_datetime_to_graph(value: Optional[datetime], graph: Graph, parent: Node, predicate: Node): + if value: + graph.add((parent, predicate, Literal(datetime_to_iso_string(value)))) + + +def add_namespace_to_spdx_id(spdx_id: str, doc_namespace: str, external_doc_namespaces: Dict[str, str]) -> str: + if ":" in spdx_id: + external_doc_ref_id = spdx_id.split(":")[0] + if external_doc_ref_id not in external_doc_namespaces.keys(): + print(f"No namespace for external document reference with id {external_doc_ref_id} provided.", + file=sys.stderr) + return spdx_id + return f"{external_doc_namespaces[external_doc_ref_id]}#{spdx_id.split(':')[1]}" + + if is_valid_internal_spdx_id(spdx_id): + return f"{doc_namespace}#{spdx_id}" + + return spdx_id diff --git a/src/spdx/writer/write_anything.py b/src/spdx/writer/write_anything.py index 43f786639..14506a007 100644 --- a/src/spdx/writer/write_anything.py +++ b/src/spdx/writer/write_anything.py @@ -8,6 +8,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +from spdx.writer.rdf import rdf_writer + from spdx.formats import file_name_to_format, FileFormat from spdx.model.document import Document from spdx.writer.json import json_writer @@ -19,12 +21,12 @@ def write_file(document: Document, file_name: str, validate: bool = True): output_format = file_name_to_format(file_name) if output_format == FileFormat.JSON: - json_writer.write_document(document, file_name, validate=False) + json_writer.write_document(document, file_name, validate) elif output_format == FileFormat.YAML: - yaml_writer.write_document_to_file(document, file_name, validate=False) + yaml_writer.write_document_to_file(document, file_name, validate) elif output_format == FileFormat.XML: - xml_writer.write_document_to_file(document, file_name, validate=False) + xml_writer.write_document_to_file(document, file_name, validate) elif output_format == FileFormat.TAG_VALUE: tagvalue_writer.write_document_to_file(document, file_name) elif output_format == FileFormat.RDF_XML: - raise NotImplementedError("Currently, the rdf writer is not implemented") + rdf_writer.write_document_to_file(document, file_name, validate) diff --git a/tests/spdx/fixtures.py b/tests/spdx/fixtures.py index 094347db7..9c99f17a9 100644 --- a/tests/spdx/fixtures.py +++ b/tests/spdx/fixtures.py @@ -10,7 +10,7 @@ # limitations under the License. from datetime import datetime -from license_expression import Licensing +from license_expression import get_spdx_licensing from spdx.model.actor import Actor, ActorType from spdx.model.annotation import Annotation, AnnotationType @@ -56,13 +56,13 @@ def creation_info_fixture(spdx_version="SPDX-2.3", spdx_id="SPDXRef-DOCUMENT", n def file_fixture(name="./fileName.py", spdx_id="SPDXRef-File", checksums=None, file_type=None, - license_concluded=Licensing().parse("MIT and GPL-2.0"), license_info_in_file=None, + license_concluded=get_spdx_licensing().parse("MIT and GPL-2.0"), license_info_in_file=None, license_comment="licenseComment", copyright_text="copyrightText", comment="fileComment", notice="fileNotice", contributors=None, attribution_texts=None) -> File: checksums = [checksum_fixture()] if checksums is None else checksums file_type = [FileType.TEXT] if file_type is None else file_type - license_info_in_file = [Licensing().parse("MIT"), - Licensing().parse("GPL-2.0")] if license_info_in_file is None else license_info_in_file + license_info_in_file = [get_spdx_licensing().parse("MIT"), + get_spdx_licensing().parse("GPL-2.0")] if license_info_in_file is None else license_info_in_file contributors = ["fileContributor"] if contributors is None else contributors attribution_texts = ["fileAttributionText"] if attribution_texts is None else attribution_texts return File(name=name, spdx_id=spdx_id, checksums=checksums, file_type=file_type, @@ -76,15 +76,15 @@ def package_fixture(spdx_id="SPDXRef-Package", name="packageName", download_loca supplier=actor_fixture(name="supplierName"), originator=actor_fixture(name="originatorName"), files_analyzed=True, verification_code=package_verification_code_fixture(), checksums=None, homepage="https://homepage.com", source_info="sourceInfo", - license_concluded=Licensing().parse("MIT and GPL-2.0"), license_info_from_files=None, - license_declared=Licensing().parse("MIT and GPL-2.0"), + license_concluded=get_spdx_licensing().parse("MIT and GPL-2.0"), license_info_from_files=None, + license_declared=get_spdx_licensing().parse("MIT and GPL-2.0"), license_comment="packageLicenseComment", copyright_text="packageCopyrightText", summary="packageSummary", description="packageDescription", comment="packageComment", external_references=None, attribution_texts=None, primary_package_purpose=PackagePurpose.SOURCE, release_date=datetime(2022, 12, 1), built_date=datetime(2022, 12, 2), valid_until_date=datetime(2022, 12, 3)) -> Package: checksums = [checksum_fixture()] if checksums is None else checksums - license_info_from_files = [Licensing().parse("MIT"), Licensing().parse( + license_info_from_files = [get_spdx_licensing().parse("MIT"), get_spdx_licensing().parse( "GPL-2.0")] if license_info_from_files is None else license_info_from_files external_references = [external_package_ref_fixture()] if external_references is None else external_references attribution_texts = ["packageAttributionText"] if attribution_texts is None else attribution_texts @@ -111,11 +111,11 @@ def external_package_ref_fixture(category=ExternalPackageRefCategory.PACKAGE_MAN def snippet_fixture(spdx_id="SPDXRef-Snippet", file_spdx_id="SPDXRef-File", byte_range=(1, 2), - line_range=(3, 4), license_concluded=Licensing().parse("MIT and GPL-2.0"), + line_range=(3, 4), license_concluded=get_spdx_licensing().parse("MIT and GPL-2.0"), license_info_in_snippet=None, license_comment="snippetLicenseComment", copyright_text="licenseCopyrightText", comment="snippetComment", name="snippetName", attribution_texts=None) -> Snippet: - license_info_in_snippet = [Licensing().parse("MIT"), Licensing().parse( + license_info_in_snippet = [get_spdx_licensing().parse("MIT"), get_spdx_licensing().parse( "GPL-2.0")] if license_info_in_snippet is None else license_info_in_snippet attribution_texts = ["snippetAttributionText"] if attribution_texts is None else attribution_texts return Snippet(spdx_id=spdx_id, file_spdx_id=file_spdx_id, byte_range=byte_range, line_range=line_range, diff --git a/tests/spdx/jsonschema/test_file_converter.py b/tests/spdx/jsonschema/test_file_converter.py index b169a9d44..d1a08282c 100644 --- a/tests/spdx/jsonschema/test_file_converter.py +++ b/tests/spdx/jsonschema/test_file_converter.py @@ -23,7 +23,7 @@ from spdx.model.checksum import Checksum, ChecksumAlgorithm from spdx.model.document import Document from spdx.model.file import File, FileType -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING from spdx.model.spdx_none import SpdxNone, SPDX_NONE_STRING from tests.spdx.fixtures import creation_info_fixture, file_fixture, annotation_fixture, document_fixture diff --git a/tests/spdx/jsonschema/test_package_converter.py b/tests/spdx/jsonschema/test_package_converter.py index fae81f6db..eb88da3eb 100644 --- a/tests/spdx/jsonschema/test_package_converter.py +++ b/tests/spdx/jsonschema/test_package_converter.py @@ -22,7 +22,7 @@ from spdx.model.annotation import Annotation, AnnotationType from spdx.model.checksum import Checksum, ChecksumAlgorithm from spdx.model.document import Document -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.model.package import Package, PackageVerificationCode, PackagePurpose from spdx.model.relationship import RelationshipType from spdx.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING diff --git a/tests/spdx/jsonschema/test_snippet_converter.py b/tests/spdx/jsonschema/test_snippet_converter.py index 082cd1a90..9a12b34f4 100644 --- a/tests/spdx/jsonschema/test_snippet_converter.py +++ b/tests/spdx/jsonschema/test_snippet_converter.py @@ -21,7 +21,7 @@ from spdx.model.actor import Actor, ActorType from spdx.model.annotation import Annotation, AnnotationType from spdx.model.document import Document -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.model.snippet import Snippet from spdx.model.spdx_no_assertion import SpdxNoAssertion, SPDX_NO_ASSERTION_STRING from spdx.model.spdx_none import SpdxNone, SPDX_NONE_STRING diff --git a/tests/spdx/parser/jsonlikedict/test_file_parser.py b/tests/spdx/parser/jsonlikedict/test_file_parser.py index 3050dcaec..a4faa3ae8 100644 --- a/tests/spdx/parser/jsonlikedict/test_file_parser.py +++ b/tests/spdx/parser/jsonlikedict/test_file_parser.py @@ -14,7 +14,7 @@ from spdx.model.checksum import Checksum, ChecksumAlgorithm from spdx.model.file import FileType -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.parser.error import SPDXParsingError from spdx.parser.jsonlikedict.dict_parsing_functions import parse_list_of_elements from spdx.parser.jsonlikedict.file_parser import FileParser @@ -126,4 +126,5 @@ def test_parse_invalid_file_types(): with pytest.raises(SPDXParsingError) as err: file_parser.parse_file_types(file_types_list) - TestCase().assertCountEqual(err.value.get_messages(), ["Error while parsing FileType: ['Invalid FileType: APPLICAON']"]) + TestCase().assertCountEqual(err.value.get_messages(), + ["Error while parsing FileType: ['Invalid FileType: APPLICAON']"]) diff --git a/tests/spdx/parser/jsonlikedict/test_package_parser.py b/tests/spdx/parser/jsonlikedict/test_package_parser.py index fa9408dd7..a9813f2fc 100644 --- a/tests/spdx/parser/jsonlikedict/test_package_parser.py +++ b/tests/spdx/parser/jsonlikedict/test_package_parser.py @@ -15,7 +15,7 @@ from spdx.model.actor import Actor, ActorType from spdx.model.checksum import Checksum, ChecksumAlgorithm -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.model.package import PackageVerificationCode, ExternalPackageRef, ExternalPackageRefCategory, PackagePurpose from spdx.parser.error import SPDXParsingError from spdx.parser.jsonlikedict.dict_parsing_functions import parse_list_of_elements diff --git a/tests/spdx/parser/jsonlikedict/test_snippet_parser.py b/tests/spdx/parser/jsonlikedict/test_snippet_parser.py index 9bbe71a26..2663b6e61 100644 --- a/tests/spdx/parser/jsonlikedict/test_snippet_parser.py +++ b/tests/spdx/parser/jsonlikedict/test_snippet_parser.py @@ -12,7 +12,7 @@ import pytest -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.parser.error import SPDXParsingError from spdx.parser.jsonlikedict.snippet_parser import SnippetParser diff --git a/tests/spdx/validation/test_license_expression_validator.py b/tests/spdx/validation/test_license_expression_validator.py index 6167fd50a..458a98acc 100644 --- a/tests/spdx/validation/test_license_expression_validator.py +++ b/tests/spdx/validation/test_license_expression_validator.py @@ -11,7 +11,7 @@ from typing import List -from license_expression import LicenseExpression, Licensing +from license_expression import Licensing from spdx.validation.license_expression_validator import validate_license_expression from spdx.validation.validation_message import ValidationMessage diff --git a/tests/spdx/writer/json/expected_results/expected.json b/tests/spdx/writer/json/expected_results/expected.json index 9e9a9c45a..c441e917d 100644 --- a/tests/spdx/writer/json/expected_results/expected.json +++ b/tests/spdx/writer/json/expected_results/expected.json @@ -54,8 +54,8 @@ "TEXT" ], "licenseComments": "licenseComment", - "licenseConcluded": "MIT AND GPL-2.0", - "licenseInfoInFiles": ["MIT", "GPL-2.0"], + "licenseConcluded": "MIT AND GPL-2.0-only", + "licenseInfoInFiles": ["MIT", "GPL-2.0-only"], "noticeText": "fileNotice" } ], @@ -99,9 +99,9 @@ "filesAnalyzed": true, "homepage": "https://homepage.com", "licenseComments": "packageLicenseComment", - "licenseConcluded": "MIT AND GPL-2.0", - "licenseDeclared": "MIT AND GPL-2.0", - "licenseInfoFromFiles": ["MIT", "GPL-2.0"], + "licenseConcluded": "MIT AND GPL-2.0-only", + "licenseDeclared": "MIT AND GPL-2.0-only", + "licenseInfoFromFiles": ["MIT", "GPL-2.0-only"], "name": "packageName", "originator": "Person: originatorName (some@mail.com)", "packageFileName": "./packageFileName", @@ -137,8 +137,8 @@ "comment": "snippetComment", "copyrightText": "licenseCopyrightText", "licenseComments": "snippetLicenseComment", - "licenseConcluded": "MIT AND GPL-2.0", - "licenseInfoInSnippets": ["MIT", "GPL-2.0"], + "licenseConcluded": "MIT AND GPL-2.0-only", + "licenseInfoInSnippets": ["MIT", "GPL-2.0-only"], "name": "snippetName", "ranges": [ { diff --git a/tests/spdx/writer/rdf/__init__.py b/tests/spdx/writer/rdf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/spdx/writer/rdf/test_annotation_writer.py b/tests/spdx/writer/rdf/test_annotation_writer.py new file mode 100644 index 000000000..435599b85 --- /dev/null +++ b/tests/spdx/writer/rdf/test_annotation_writer.py @@ -0,0 +1,30 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, Literal, RDFS, URIRef, RDF + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.writer.rdf.annotation_writer import add_annotation_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import annotation_fixture + + +def test_add_annotation_to_graph(): + graph = Graph() + annotation = annotation_fixture() + + add_annotation_to_graph(annotation, graph, "docNamespace", {}) + + assert (URIRef("docNamespace#SPDXRef-File"), SPDX_NAMESPACE.annotation, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.Annotation) in graph + assert (None, SPDX_NAMESPACE.annotationType, SPDX_NAMESPACE.annotationType_review) in graph + assert (None, SPDX_NAMESPACE.annotationDate, Literal(datetime_to_iso_string(annotation.annotation_date))) in graph + assert (None, SPDX_NAMESPACE.annotator, Literal(annotation.annotator.to_serialized_string())) in graph + assert (None, RDFS.comment, Literal(annotation.annotation_comment)) in graph diff --git a/tests/spdx/writer/rdf/test_checksum_writer.py b/tests/spdx/writer/rdf/test_checksum_writer.py new file mode 100644 index 000000000..535418dff --- /dev/null +++ b/tests/spdx/writer/rdf/test_checksum_writer.py @@ -0,0 +1,56 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +from rdflib import Graph, URIRef, Literal, RDF + +from spdx.model.checksum import ChecksumAlgorithm +from spdx.writer.rdf.checksum_writer import add_checksum_to_graph, algorithm_to_rdf_string +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import checksum_fixture + + +def test_add_checksum_to_graph(): + graph = Graph() + checksum = checksum_fixture() + + add_checksum_to_graph(checksum, graph, URIRef("parentNode")) + + assert (URIRef("parentNode"), SPDX_NAMESPACE.checksum, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.Checksum) in graph + assert (None, SPDX_NAMESPACE.algorithm, SPDX_NAMESPACE.checksumAlgorithm_sha1) in graph + assert (None, SPDX_NAMESPACE.checksumValue, Literal(checksum.value)) in graph + + +@pytest.mark.parametrize("algorithm,expected", [(ChecksumAlgorithm.SHA1, SPDX_NAMESPACE.checksumAlgorithm_sha1), + (ChecksumAlgorithm.SHA224, SPDX_NAMESPACE.checksumAlgorithm_sha224), + (ChecksumAlgorithm.SHA256, SPDX_NAMESPACE.checksumAlgorithm_sha256), + (ChecksumAlgorithm.SHA384, SPDX_NAMESPACE.checksumAlgorithm_sha384), + (ChecksumAlgorithm.SHA512, SPDX_NAMESPACE.checksumAlgorithm_sha512), + (ChecksumAlgorithm.SHA3_256, SPDX_NAMESPACE.checksumAlgorithm_sha3_256), + (ChecksumAlgorithm.SHA3_384, SPDX_NAMESPACE.checksumAlgorithm_sha3_384), + (ChecksumAlgorithm.SHA3_512, SPDX_NAMESPACE.checksumAlgorithm_sha3_512), + (ChecksumAlgorithm.BLAKE2B_256, + SPDX_NAMESPACE.checksumAlgorithm_blake2b256), + (ChecksumAlgorithm.BLAKE2B_384, + SPDX_NAMESPACE.checksumAlgorithm_blake2b384), + (ChecksumAlgorithm.BLAKE2B_512, + SPDX_NAMESPACE.checksumAlgorithm_blake2b512), + (ChecksumAlgorithm.BLAKE3, SPDX_NAMESPACE.checksumAlgorithm_blake3), + (ChecksumAlgorithm.MD2, SPDX_NAMESPACE.checksumAlgorithm_md2), + (ChecksumAlgorithm.MD4, SPDX_NAMESPACE.checksumAlgorithm_md4), + (ChecksumAlgorithm.MD5, SPDX_NAMESPACE.checksumAlgorithm_md5), + (ChecksumAlgorithm.MD6, SPDX_NAMESPACE.checksumAlgorithm_md6), + (ChecksumAlgorithm.ADLER32, SPDX_NAMESPACE.checksumAlgorithm_adler32) + ]) +def test_algorithm_to_rdf_string(algorithm, expected): + rdf_element = algorithm_to_rdf_string(algorithm) + + assert rdf_element == expected diff --git a/tests/spdx/writer/rdf/test_creation_info_writer.py b/tests/spdx/writer/rdf/test_creation_info_writer.py new file mode 100644 index 000000000..b7592555c --- /dev/null +++ b/tests/spdx/writer/rdf/test_creation_info_writer.py @@ -0,0 +1,36 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, Literal, RDFS, URIRef, RDF + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.writer.rdf.creation_info_writer import add_creation_info_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import creation_info_fixture + + +def test_add_creation_info_to_graph(): + graph = Graph() + creation_info = creation_info_fixture() + + add_creation_info_to_graph(creation_info, graph) + + assert (None, RDF.type, SPDX_NAMESPACE.SpdxDocument) in graph + assert (URIRef(f"{creation_info.document_namespace}#{creation_info.spdx_id}"), None, None) in graph + assert (None, SPDX_NAMESPACE.dataLicense, URIRef(f"https://spdx.org/licenses/{creation_info.data_license}")) + assert (None, SPDX_NAMESPACE.name, Literal(creation_info.name)) in graph + assert (None, SPDX_NAMESPACE.specVersion, Literal(creation_info.spdx_version)) in graph + assert (None, SPDX_NAMESPACE.creationInfo, None) in graph + + assert (None, RDF.type, SPDX_NAMESPACE.CreationInfo) in graph + assert (None, SPDX_NAMESPACE.created, Literal(datetime_to_iso_string(creation_info.created))) in graph + assert (None, RDFS.comment, Literal(creation_info.creator_comment)) in graph + assert (None, SPDX_NAMESPACE.licenseListVersion, Literal(creation_info.license_list_version)) in graph + assert (None, SPDX_NAMESPACE.creator, Literal(creation_info.creators[0].to_serialized_string())) in graph diff --git a/tests/spdx/writer/rdf/test_external_document_ref_writer.py b/tests/spdx/writer/rdf/test_external_document_ref_writer.py new file mode 100644 index 000000000..a8c206df1 --- /dev/null +++ b/tests/spdx/writer/rdf/test_external_document_ref_writer.py @@ -0,0 +1,31 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef, RDF +from spdx.rdfschema.namespace import SPDX_NAMESPACE + +from spdx.writer.rdf.external_document_ref_writer import add_external_document_ref_to_graph +from tests.spdx.fixtures import external_document_ref_fixture + + +def test_add_external_document_ref_to_graph(): + graph = Graph() + external_document_ref = external_document_ref_fixture() + + add_external_document_ref_to_graph(external_document_ref, graph, URIRef("docNode"), "docNamespace") + + assert (URIRef("docNode"), SPDX_NAMESPACE.externalDocumentRef, URIRef("docNamespace#DocumentRef-external")) in graph + assert (None, RDF.type, SPDX_NAMESPACE.ExternalDocumentRef) in graph + assert (None, SPDX_NAMESPACE.checksum, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.Checksum) in graph + assert (None, SPDX_NAMESPACE.spdxDocument, URIRef(external_document_ref.document_uri)) in graph + + + diff --git a/tests/spdx/writer/rdf/test_extracted_licensing_info_writer.py b/tests/spdx/writer/rdf/test_extracted_licensing_info_writer.py new file mode 100644 index 000000000..55669dae5 --- /dev/null +++ b/tests/spdx/writer/rdf/test_extracted_licensing_info_writer.py @@ -0,0 +1,30 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, Literal, RDFS, URIRef, RDF +from spdx.rdfschema.namespace import SPDX_NAMESPACE + +from spdx.writer.rdf.extracted_licensing_info_writer import add_extracted_licensing_info_to_graph +from tests.spdx.fixtures import extracted_licensing_info_fixture + + +def test_add_extracted_licensing_info_to_graph(): + graph = Graph() + extracted_licensing_info = extracted_licensing_info_fixture() + + add_extracted_licensing_info_to_graph(extracted_licensing_info, graph, URIRef("docNode"), "docNamespace") + + assert (URIRef("docNode"), SPDX_NAMESPACE.hasExtractedLicensingInfo, None) in graph + assert (URIRef("docNamespace#LicenseRef-1"), RDF.type, SPDX_NAMESPACE.ExtractedLicensingInfo) in graph + assert (None, SPDX_NAMESPACE.licenseId, Literal(extracted_licensing_info.license_id)) in graph + assert (None, SPDX_NAMESPACE.extractedText, Literal(extracted_licensing_info.extracted_text)) in graph + assert (None, RDFS.seeAlso, Literal(extracted_licensing_info.cross_references[0])) in graph + assert (None, SPDX_NAMESPACE.name, Literal(extracted_licensing_info.license_name)) in graph + assert (None, RDFS.comment, Literal(extracted_licensing_info.comment)) in graph diff --git a/tests/spdx/writer/rdf/test_file_writer.py b/tests/spdx/writer/rdf/test_file_writer.py new file mode 100644 index 000000000..0fb67a884 --- /dev/null +++ b/tests/spdx/writer/rdf/test_file_writer.py @@ -0,0 +1,35 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, Literal, RDFS, RDF, URIRef + +from spdx.writer.rdf.file_writer import add_file_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import file_fixture + + +def test_add_file_to_graph(): + graph = Graph() + file = file_fixture() + + add_file_to_graph(file, graph, "docNamespace", {}) + + assert (URIRef("docNamespace#SPDXRef-File"), RDF.type, SPDX_NAMESPACE.File) in graph + assert (None, SPDX_NAMESPACE.fileName, Literal(file.name)) in graph + assert (None, SPDX_NAMESPACE.fileType, SPDX_NAMESPACE.fileType_text) in graph + assert (None, SPDX_NAMESPACE.licenseComments, Literal(file.license_comment)) in graph + assert (None, SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, SPDX_NAMESPACE.licenseInfoInFile, None) in graph + assert (None, SPDX_NAMESPACE.copyrightText, Literal(file.copyright_text)) in graph + assert (None, RDFS.comment, Literal(file.comment)) in graph + assert (None, SPDX_NAMESPACE.noticeText, Literal(file.notice)) in graph + assert (None, SPDX_NAMESPACE.fileContributor, Literal(file.contributors[0])) in graph + assert (None, SPDX_NAMESPACE.checksum, None) in graph + assert (None, SPDX_NAMESPACE.attributionText, Literal(file.attribution_texts[0])) in graph diff --git a/tests/spdx/writer/rdf/test_license_expression_writer.py b/tests/spdx/writer/rdf/test_license_expression_writer.py new file mode 100644 index 000000000..8f7d9ff5c --- /dev/null +++ b/tests/spdx/writer/rdf/test_license_expression_writer.py @@ -0,0 +1,62 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +from license_expression import get_spdx_licensing +from rdflib import Graph, URIRef, RDF, Literal +from spdx.rdfschema.namespace import SPDX_NAMESPACE + +from spdx.writer.rdf.license_expression_writer import add_license_expression_to_graph + + +def test_add_conjunctive_license_set_to_graph(): + graph = Graph() + license_expression = get_spdx_licensing().parse("MIT AND GPL-2.0") + + add_license_expression_to_graph(license_expression, graph, URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, + "https://namespace") + + assert (URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.ConjunctiveLicenseSet) in graph + assert (None, SPDX_NAMESPACE.member, URIRef("http://spdx.org/licenses/MIT")) in graph + assert (None, SPDX_NAMESPACE.member, URIRef("http://spdx.org/licenses/GPL-2.0-only")) in graph + + +def test_add_disjunctive_license_set_to_graph(): + graph = Graph() + license_expression = get_spdx_licensing().parse("MIT OR GPL-2.0") + + add_license_expression_to_graph(license_expression, graph, URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, + "https://namespace") + + assert (URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.DisjunctiveLicenseSet) in graph + assert (None, SPDX_NAMESPACE.member, URIRef("http://spdx.org/licenses/MIT")) in graph + assert (None, SPDX_NAMESPACE.member, URIRef("http://spdx.org/licenses/GPL-2.0-only")) in graph + + +@pytest.mark.parametrize("license_with_exception," + "expected_triple", [("MIT WITH openvpn-openssl-exception", + (URIRef("http://spdx.org/licenses/openvpn-openssl-exception"), RDF.type, + SPDX_NAMESPACE.LicenseException)), + ("MIT WITH unknown-exception", + (None, SPDX_NAMESPACE.licenseExceptionId, Literal("unknown-exception")))]) +def test_license_exception_to_graph(license_with_exception, expected_triple): + graph = Graph() + license_expression = get_spdx_licensing().parse(license_with_exception) + + add_license_expression_to_graph(license_expression, graph, URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, + "https://namespace") + + assert (URIRef("parentNode"), SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, RDF.type, SPDX_NAMESPACE.WithExceptionOperator) in graph + assert (None, SPDX_NAMESPACE.member, URIRef("http://spdx.org/licenses/MIT")) in graph + assert (None, SPDX_NAMESPACE.licenseException, None) in graph + assert expected_triple in graph diff --git a/tests/spdx/writer/rdf/test_package_writer.py b/tests/spdx/writer/rdf/test_package_writer.py new file mode 100644 index 000000000..7d2bd20df --- /dev/null +++ b/tests/spdx/writer/rdf/test_package_writer.py @@ -0,0 +1,76 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef, RDF, Literal, XSD, RDFS, DOAP + +from spdx.datetime_conversions import datetime_to_iso_string +from spdx.writer.rdf.package_writer import add_package_to_graph, add_external_package_ref_to_graph, \ + add_package_verification_code_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import package_fixture, external_package_ref_fixture, package_verification_code_fixture + + +def test_add_package_to_graph(): + graph = Graph() + package = package_fixture() + + add_package_to_graph(package, graph, "docNamespace", {}) + + assert (URIRef("docNamespace#SPDXRef-Package"), RDF.type, SPDX_NAMESPACE.Package) in graph + assert (None, SPDX_NAMESPACE.name, Literal(package.name)) in graph + assert (None, SPDX_NAMESPACE.versionInfo, Literal(package.version)) in graph + assert (None, SPDX_NAMESPACE.packageFileName, Literal(package.file_name)) in graph + assert (None, SPDX_NAMESPACE.supplier, Literal(package.supplier.to_serialized_string())) in graph + assert (None, SPDX_NAMESPACE.originator, Literal(package.originator.to_serialized_string())) in graph + assert (None, SPDX_NAMESPACE.downloadLocation, Literal(package.download_location)) in graph + assert (None, SPDX_NAMESPACE.filesAnalyzed, Literal(package.files_analyzed, datatype=XSD.boolean)) in graph + assert (URIRef("docNamespace#SPDXRef-Package"), SPDX_NAMESPACE.packageVerificationCode, None) in graph + assert (URIRef("docNamespace#SPDXRef-Package"), SPDX_NAMESPACE.checksum, None) in graph + assert (None, DOAP.homepage, Literal(package.homepage)) in graph + assert (None, SPDX_NAMESPACE.sourceInfo, Literal(package.source_info)) in graph + assert (None, SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, SPDX_NAMESPACE.licenseInfoFromFiles, None) in graph + assert (None, SPDX_NAMESPACE.licenseDeclared, None) in graph + assert (None, SPDX_NAMESPACE.licenseComments, Literal(package.license_comment)) in graph + assert (None, SPDX_NAMESPACE.copyrightText, Literal(package.copyright_text)) in graph + assert (None, SPDX_NAMESPACE.summary, Literal(package.summary)) in graph + assert (None, SPDX_NAMESPACE.description, Literal(package.description)) in graph + assert (None, RDFS.comment, Literal(package.comment)) in graph + assert (URIRef("docNamespace#SPDXRef-Package"), SPDX_NAMESPACE.externalRef, None) in graph + assert (None, SPDX_NAMESPACE.attributionText, Literal(package.attribution_texts[0])) in graph + assert (None, SPDX_NAMESPACE.primaryPackagePurpose, SPDX_NAMESPACE.purpose_source) in graph + assert (None, SPDX_NAMESPACE.releaseDate, Literal(datetime_to_iso_string(package.release_date))) in graph + assert (None, SPDX_NAMESPACE.builtDate, Literal(datetime_to_iso_string(package.built_date))) in graph + assert (None, SPDX_NAMESPACE.validUntilDate, Literal(datetime_to_iso_string(package.valid_until_date))) in graph + + +def test_add_package_verification_code_to_graph(): + graph = Graph() + verification_code = package_verification_code_fixture() + + add_package_verification_code_to_graph(verification_code, graph, URIRef("docNamespace")) + + assert (None, RDF.type, SPDX_NAMESPACE.PackageVerificationCode) in graph + assert (None, SPDX_NAMESPACE.packageVerificationCodeValue, + Literal("85ed0817af83a24ad8da68c2b5094de69833983c")) in graph + assert (None, SPDX_NAMESPACE.packageVerificationCodeExcludedFile, Literal("./exclude.py")) in graph + + +def test_external_package_ref_to_graph(): + graph = Graph() + external_reference = external_package_ref_fixture() + + add_external_package_ref_to_graph(external_reference, graph, URIRef("docNamespace")) + + assert (None, RDF.type, SPDX_NAMESPACE.ExternalRef) in graph + assert (None, SPDX_NAMESPACE.referenceCategory, SPDX_NAMESPACE.referenceCategory_packageManager) in graph + assert (None, SPDX_NAMESPACE.referenceType, URIRef("http://spdx.org/rdf/references/maven-central")) in graph + assert (None, SPDX_NAMESPACE.referenceLocator, Literal("org.apache.tomcat:tomcat:9.0.0.M4")) in graph + assert (None, RDFS.comment, Literal("externalPackageRefComment")) in graph diff --git a/tests/spdx/writer/rdf/test_rdf_writer.py b/tests/spdx/writer/rdf/test_rdf_writer.py new file mode 100644 index 000000000..38e4f5871 --- /dev/null +++ b/tests/spdx/writer/rdf/test_rdf_writer.py @@ -0,0 +1,31 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os + +import pytest + +from tests.spdx.fixtures import document_fixture + +from spdx.model.document import Document +from spdx.writer.rdf.rdf_writer import write_document_to_file + + +@pytest.fixture +def temporary_file_path() -> str: + temporary_file_path = "temp_test_rdf_writer_output.rdf.xml" + yield temporary_file_path + os.remove(temporary_file_path) + + +def test_write_document_to_file(temporary_file_path: str): + document: Document = document_fixture() + + write_document_to_file(document, temporary_file_path, False) diff --git a/tests/spdx/writer/rdf/test_relationship_writer.py b/tests/spdx/writer/rdf/test_relationship_writer.py new file mode 100644 index 000000000..998da1db8 --- /dev/null +++ b/tests/spdx/writer/rdf/test_relationship_writer.py @@ -0,0 +1,25 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from rdflib import Graph, URIRef + +from spdx.writer.rdf.relationship_writer import add_relationship_to_graph +from spdx.rdfschema.namespace import SPDX_NAMESPACE +from tests.spdx.fixtures import relationship_fixture + + +def test_add_relationship_to_graph(): + relationship = relationship_fixture() + graph = Graph() + add_relationship_to_graph(relationship, graph, "docNamespace", {}) + + assert(URIRef("docNamespace#SPDXRef-DOCUMENT"), SPDX_NAMESPACE.relationship, None) in graph + assert (None, SPDX_NAMESPACE.relationshipType, SPDX_NAMESPACE.relationshipType_describes) in graph + assert (None, SPDX_NAMESPACE.relatedSpdxElement, URIRef("docNamespace#SPDXRef-File")) in graph diff --git a/tests/spdx/writer/rdf/test_snippet_writer.py b/tests/spdx/writer/rdf/test_snippet_writer.py new file mode 100644 index 000000000..03591c2ff --- /dev/null +++ b/tests/spdx/writer/rdf/test_snippet_writer.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +from rdflib import Graph, URIRef, RDF, Literal, RDFS +from spdx.rdfschema.namespace import SPDX_NAMESPACE, POINTER_NAMESPACE + +from spdx.writer.rdf.snippet_writer import add_snippet_to_graph, add_range_to_graph +from tests.spdx.fixtures import snippet_fixture + + +def test_add_snippet_to_graph(): + graph = Graph() + snippet = snippet_fixture() + + add_snippet_to_graph(snippet, graph, "docNamespace", {}) + + assert (URIRef("docNamespace#SPDXRef-Snippet"), RDF.type, SPDX_NAMESPACE.Snippet) in graph + assert (None, SPDX_NAMESPACE.snippetFromFile, URIRef(f"docNamespace#{snippet.file_spdx_id}")) in graph + assert (None, SPDX_NAMESPACE.licenseConcluded, None) in graph + assert (None, SPDX_NAMESPACE.licenseInfoInSnippet, None) in graph + assert (None, SPDX_NAMESPACE.licenseComments, Literal(snippet.license_comment)) in graph + assert (None, SPDX_NAMESPACE.copyrightText, Literal(snippet.copyright_text)) in graph + assert (None, SPDX_NAMESPACE.name, Literal(snippet.name)) in graph + assert (None, SPDX_NAMESPACE.attributionText, Literal(snippet.attribution_texts[0])) in graph + assert (None, RDFS.comment, Literal(snippet.comment)) in graph + + +@pytest.mark.parametrize("range,pointer,predicate", + [((5, 190), POINTER_NAMESPACE.ByteOffsetPointer, POINTER_NAMESPACE.offset), + ((1, 3), POINTER_NAMESPACE.LineCharPointer, POINTER_NAMESPACE.lineNumber)]) +def test_add_ranges_to_graph(range, pointer, predicate): + graph = Graph() + add_range_to_graph(range, graph, URIRef("snippetNode"), URIRef("docNamespace#SPDXRef-File"), pointer) + + assert (URIRef("snippetNode"), SPDX_NAMESPACE.range, None) in graph + assert (None, POINTER_NAMESPACE.startPointer, None) in graph + assert (None, POINTER_NAMESPACE.endPointer, None) in graph + assert (None, POINTER_NAMESPACE.reference, URIRef("docNamespace#SPDXRef-File")) in graph + assert (None, predicate, Literal(range[0])) in graph + assert (None, predicate, Literal(range[1])) in graph diff --git a/tests/spdx/writer/rdf/test_writer_utils.py b/tests/spdx/writer/rdf/test_writer_utils.py new file mode 100644 index 000000000..258fa3788 --- /dev/null +++ b/tests/spdx/writer/rdf/test_writer_utils.py @@ -0,0 +1,25 @@ +# Copyright (c) 2023 spdx contributors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from spdx.writer.rdf.writer_utils import add_namespace_to_spdx_id + + +@pytest.mark.parametrize("spdx_id,namespace,external_namespaces,expected", + [("SPDXRef-File", "docNamespace", {}, "docNamespace#SPDXRef-File"), + ("externalDoc:SPDXRef-File", "docNamespace", {"externalDoc": "externalNamespace"}, + "externalNamespace#SPDXRef-File"), + ("externalDoc#A-Ref", "", {}, "externalDoc#A-Ref"), + ("externalDoc:A-Ref", "", {}, "externalDoc:A-Ref")]) +def test_add_namespace_to_spdx_id(spdx_id, namespace, expected, external_namespaces): + extended_spdx_id = add_namespace_to_spdx_id(spdx_id, namespace, external_namespaces) + + assert extended_spdx_id == expected diff --git a/tests/spdx/writer/tagvalue/test_package_writer.py b/tests/spdx/writer/tagvalue/test_package_writer.py index c7d9ec0c0..feb2608f3 100644 --- a/tests/spdx/writer/tagvalue/test_package_writer.py +++ b/tests/spdx/writer/tagvalue/test_package_writer.py @@ -38,10 +38,10 @@ def test_package_writer(): call('PackageChecksum: SHA1: 71c4025dd9897b364f3ebbb42c484ff43d00791c\n'), call('PackageHomePage: https://homepage.com\n'), call('PackageSourceInfo: sourceInfo\n'), - call('PackageLicenseConcluded: MIT AND GPL-2.0\n'), + call('PackageLicenseConcluded: MIT AND GPL-2.0-only\n'), call('PackageLicenseInfoFromFiles: MIT\n'), - call('PackageLicenseInfoFromFiles: GPL-2.0\n'), - call('PackageLicenseDeclared: MIT AND GPL-2.0\n'), + call('PackageLicenseInfoFromFiles: GPL-2.0-only\n'), + call('PackageLicenseDeclared: MIT AND GPL-2.0-only\n'), call('PackageLicenseComments: packageLicenseComment\n'), call('PackageCopyrightText: packageCopyrightText\n'), call('PackageSummary: packageSummary\n'),