diff --git a/src/main/java/org/cyclonedx/model/Metadata.java b/src/main/java/org/cyclonedx/model/Metadata.java index 391ffe738..91ef6602f 100644 --- a/src/main/java/org/cyclonedx/model/Metadata.java +++ b/src/main/java/org/cyclonedx/model/Metadata.java @@ -28,7 +28,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import org.cyclonedx.Version; -import org.cyclonedx.util.deserializer.LicenseDeserializer; +import org.cyclonedx.util.deserializer.LicenseChoiceDeserializer; import org.cyclonedx.util.deserializer.LifecycleDeserializer; import org.cyclonedx.util.deserializer.MetadataDeserializer; import org.cyclonedx.util.serializer.CustomDateSerializer; @@ -175,7 +175,7 @@ public void setLicenseChoice(LicenseChoice licenseChoice) { setLicenses(licenseChoice); } - @JsonDeserialize(using = LicenseDeserializer.class) + @JsonDeserialize(using = LicenseChoiceDeserializer.class) public LicenseChoice getLicenses() { return licenses; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java index ede96cde8..118b4838d 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java @@ -25,7 +25,7 @@ public class MetadataDeserializer private final LifecycleDeserializer lifecycleDeserializer = new LifecycleDeserializer(); private final PropertiesDeserializer propertiesDeserializer = new PropertiesDeserializer(); - private final LicenseDeserializer licenseDeserializer = new LicenseDeserializer(); + private final LicenseChoiceDeserializer licenseDeserializer = new LicenseChoiceDeserializer(); @Override public Metadata deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException { diff --git a/src/test/java/org/cyclonedx/parsers/MetadataLicensesTest.java b/src/test/java/org/cyclonedx/parsers/MetadataLicensesTest.java new file mode 100644 index 000000000..24b1c80b9 --- /dev/null +++ b/src/test/java/org/cyclonedx/parsers/MetadataLicensesTest.java @@ -0,0 +1,127 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.parsers; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; +import org.cyclonedx.Format; +import org.cyclonedx.Version; +import org.cyclonedx.generators.BomGeneratorFactory; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.License; +import org.cyclonedx.model.LicenseChoice; +import org.cyclonedx.model.LicenseItem; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.model.license.Expression; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class MetadataLicensesTest { + + static Stream licenseCases() { + return Stream.of(Format.JSON, Format.XML).flatMap(format -> Stream.of( + Arguments.of(format, Version.VERSION_16, "license", licenses(license("MIT"))), + Arguments.of(format, Version.VERSION_16, "licenses", licenses(license("MIT"), license("BSD-3-Clause"))), + Arguments.of(format, Version.VERSION_16, "expression", licenses(expression("MIT OR Apache-2.0"))), + Arguments.of(format, Version.VERSION_17, "licenses", licenses(license("MIT"), license("BSD-3-Clause"))), + Arguments.of(format, Version.VERSION_17, "expression", licenses(expression("MIT OR Apache-2.0"))), + Arguments.of(format, Version.VERSION_17, "expressions", licenses(expression("MIT"), expression("Apache-2.0"))), + Arguments.of(format, Version.VERSION_17, "license then expression", licenses(license("MIT"), expression("Apache-2.0 OR BSD-3-Clause"))), + Arguments.of(format, Version.VERSION_17, "expression then license", licenses(expression("Apache-2.0 OR BSD-3-Clause"), license("MIT"))))); + } + + @ParameterizedTest(name = "{0} {1}: {2}") + @MethodSource("licenseCases") + void preservesMetadataLicenses(Format format, Version version, String description, LicenseChoice licenses) throws Exception { + Bom original = new Bom(); + Metadata metadata = new Metadata(); + metadata.setLicenses(licenses); + original.setMetadata(metadata); + + Parser parser = format == Format.JSON ? new JsonParser() : new XmlParser(); + byte[] serialized = serialize(original, format, version); + assertThat(parser.validate(serialized, version)).isEmpty(); + + Bom parsed = parser.parse(serialized); + assertThat(parsed.getMetadata().getLicenses().getItems()) + .containsExactlyInAnyOrderElementsOf(licenses.getItems()); + + byte[] reserialized = serialize(parsed, format, version); + assertThat(parser.validate(reserialized, version)).isEmpty(); + assertThat(parser.parse(reserialized).getMetadata().getLicenses().getItems()) + .containsExactlyInAnyOrderElementsOf(licenses.getItems()); + } + + private static byte[] serialize(Bom bom, Format format, Version version) throws Exception { + String value = format == Format.JSON + ? BomGeneratorFactory.createJson(version, bom).toJsonString() + : BomGeneratorFactory.createXml(version, bom).toXmlString(); + return value.getBytes(StandardCharsets.UTF_8); + } + + @ParameterizedTest + @EnumSource(Format.class) + void rejectsMixedMetadataLicensesInSchema16(Format format) throws Exception { + Bom original = new Bom(); + Metadata metadata = new Metadata(); + metadata.setLicenses(licenses(license("MIT"), expression("Apache-2.0"))); + original.setMetadata(metadata); + + Parser parser = format == Format.JSON ? new JsonParser() : new XmlParser(); + String serialized = new String(serialize(original, format, Version.VERSION_17), StandardCharsets.UTF_8); + assertThat(parser.validate(serialized.getBytes(StandardCharsets.UTF_8), Version.VERSION_17)).isEmpty(); + byte[] invalidFor16 = serialized.replace("1.7", "1.6").getBytes(StandardCharsets.UTF_8); + assertThat(parser.validate(invalidFor16, Version.VERSION_16)).isNotEmpty(); + } + + @Test + void preservesInterleavedXmlLicenses() throws Exception { + byte[] serialized = ("" + + "MIT" + + "Apache-2.0BSD-3-Clause" + + "").getBytes(StandardCharsets.UTF_8); + Parser parser = new XmlParser(); + assertThat(parser.validate(serialized, Version.VERSION_17)).isEmpty(); + assertThat(parser.parse(serialized).getMetadata().getLicenses().getItems()) + .containsExactlyInAnyOrder(license("MIT"), expression("Apache-2.0"), license("BSD-3-Clause")); + } + + private static LicenseChoice licenses(LicenseItem... items) { + LicenseChoice choice = new LicenseChoice(); + for (LicenseItem item : items) { + choice.addItem(item); + } + return choice; + } + + private static LicenseItem license(String id) { + License license = new License(); + license.setId(id); + return LicenseItem.ofLicense(license); + } + + private static LicenseItem expression(String value) { + return LicenseItem.ofExpression(new Expression(value)); + } +}