Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions src/main/java/org/cyclonedx/parsers/BomParserFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,44 +20,46 @@

import org.apache.commons.io.IOUtils;
import org.cyclonedx.exception.ParseException;
import org.cyclonedx.parsers.JsonParser;
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Arrays;

public class BomParserFactory {

private BomParserFactory() {}

public static Parser createParser(final File file) throws ParseException {
try (final InputStream fis = Files.newInputStream(file.toPath())) {
final byte[] bytes = IOUtils.toByteArray(fis, 1);
return createParser(bytes);
final byte[] prefix = new byte[4]; // potential 3-byte UTF-8 byte-order mark + 1 content byte
final int actualPrefixLength = IOUtils.read(fis, prefix);
return createParser(Arrays.copyOf(prefix, actualPrefixLength));
} catch (IOException e) {
throw new ParseException("An error occurred creating parser from file", e);
}
}

public static Parser createParser(final byte[] bytes) throws ParseException {
if(bytes.length < 1) {
final int offset = hasUtf8ByteOrderMark(bytes) ? 3 : 0;
if (bytes.length - offset < 1) {
throw new ParseException("Cannot create parser from empty byte array.");
}

if (bytes[0] == 123) {
if (bytes[offset] == (byte) '{') {
return new JsonParser();
} else if (bytes[0] == 60) {
} else if (bytes[offset] == (byte) '<') {
return new XmlParser();
} else {
throw new ParseException("The specified BOM is not in a supported format. Supported formats are XML and JSON");
}
}

public static boolean looksLikeCycloneDX(final byte[] bytes) {
final String bomString = new String(bytes, StandardCharsets.UTF_8);
final int offset = hasUtf8ByteOrderMark(bytes) ? 3 : 0;
final String bomString = new String(bytes, offset, bytes.length - offset, StandardCharsets.UTF_8);
if (bomString.startsWith("<?xml") && bomString.contains("<bom") &&
bomString.contains("http://cyclonedx.org/schema/bom")) {
return true;
Expand All @@ -66,4 +68,12 @@ public static boolean looksLikeCycloneDX(final byte[] bytes) {
return bomString.startsWith("{") && bomString.contains("bomFormat") && bomString.contains("CycloneDX");
}
}

private static boolean hasUtf8ByteOrderMark(byte[] bytes) {
return bytes.length >= 3
&& bytes[0] == (byte) 0xEF
&& bytes[1] == (byte) 0xBB
&& bytes[2] == (byte) 0xBF;
}

}
10 changes: 9 additions & 1 deletion src/main/java/org/cyclonedx/parsers/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ public List<ParseException> validate(final InputStream inputStream, final Versio
* @return a list of exceptions encountered during validation
*/
public List<ParseException> validate(final String bomString, final Version schemaVersion) throws IOException {
return validate(mapper.readTree(bomString), schemaVersion);
return validate(mapper.readTree(skipUtf8Bom(bomString)), schemaVersion);
}

/**
Expand Down Expand Up @@ -244,4 +244,12 @@ public boolean isValid(final InputStream inputStream) throws IOException {
public boolean isValid(final InputStream inputStream, final Version schemaVersion) throws IOException {
return validate(inputStream, schemaVersion).isEmpty();
}

private static String skipUtf8Bom(String string) {
// NB: Jackson does not strip a UTF-8 BOM when reading from a String,
// but it DOES do that for byte-based input.
return string.startsWith("\uFEFF")
? string.substring(1)
: string;
}
}
32 changes: 29 additions & 3 deletions src/test/java/org/cyclonedx/BomParserFactoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.util.Objects;
import java.nio.file.Files;

import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;

Expand All @@ -35,17 +37,41 @@ public class BomParserFactoryTest {
@Test
public void testXMLFactory() throws Exception {
Parser parser = BomParserFactory.createParser(
new File(Objects.requireNonNull(BomParserFactory.class.getResource("/bom-1.2.xml")).getFile()));
new File(requireNonNull(BomParserFactory.class.getResource("/bom-1.2.xml")).getFile()));
assertInstanceOf(XmlParser.class, parser);
}

@Test
public void testJSONFactory() throws Exception {
Parser parser = BomParserFactory.createParser(new File(
Objects.requireNonNull(BomParserFactory.class.getResource("/bom-1.2.json")).getFile()));
requireNonNull(BomParserFactory.class.getResource("/bom-1.2.json")).getFile()));
assertInstanceOf(JsonParser.class, parser);
}

@Test
public void testXMLFactoryWithUtf8ByteOrderMarker() throws Exception {
Parser parser = BomParserFactory.createParser(
new File(requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.xml")).getFile()));
assertInstanceOf(XmlParser.class, parser);
}

@Test
public void testJSONFactoryWithUtf8ByteOrderMarker() throws Exception {
Parser parser = BomParserFactory.createParser(new File(
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.json")).getFile()));
assertInstanceOf(JsonParser.class, parser);
}

@Test
public void testFactoryFromByteArrayWithUtf8ByteOrderMarker() throws Exception {
byte[] xml = Files.readAllBytes(new File(
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.xml")).getFile()).toPath());
byte[] json = Files.readAllBytes(new File(
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.json")).getFile()).toPath());
assertInstanceOf(XmlParser.class, BomParserFactory.createParser(xml));
assertInstanceOf(JsonParser.class, BomParserFactory.createParser(json));
}

@Test()
public void testFactoryThrowsParseExceptionWithEmptyData() {
byte[] emptyData = new byte[]{};
Expand Down
24 changes: 17 additions & 7 deletions src/test/java/org/cyclonedx/parsers/JsonParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,19 @@

import org.cyclonedx.Version;
import org.cyclonedx.model.Bom;
import org.cyclonedx.model.Citation;
import org.cyclonedx.model.Component;
import org.cyclonedx.model.Component.Type;
import org.cyclonedx.model.Dependency;
import org.cyclonedx.model.ExternalReference;
import org.cyclonedx.model.TlpClassification;
import org.cyclonedx.model.License;
import org.cyclonedx.model.LicenseChoice;
import org.cyclonedx.model.OrganizationalEntity;
import org.cyclonedx.model.Patent;
import org.cyclonedx.model.PatentAssertion;
import org.cyclonedx.model.PatentFamily;
import org.cyclonedx.model.PatentItem;
import org.cyclonedx.model.TlpClassification;
import org.cyclonedx.model.attestation.Assessor;
import org.cyclonedx.model.attestation.Attestation;
import org.cyclonedx.model.attestation.AttestationMap;
Expand Down Expand Up @@ -64,14 +69,10 @@
import org.cyclonedx.model.definition.Standard;
import org.cyclonedx.model.license.Acknowledgement;
import org.cyclonedx.model.license.Expression;
import org.cyclonedx.model.license.ExpressionDetailed;
import org.cyclonedx.model.license.ExpressionDetail;
import org.cyclonedx.model.Citation;
import org.cyclonedx.model.Patent;
import org.cyclonedx.model.PatentFamily;
import org.cyclonedx.model.PatentAssertion;
import org.cyclonedx.model.PatentItem;
import org.cyclonedx.model.license.ExpressionDetailed;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
Expand All @@ -98,6 +99,15 @@ public void testValid12Bom() throws Exception {
System.out.println(bom.getSerialNumber());
}

@Test
public void testValid12BomWithUtf8ByteOrderMarker() throws Exception {
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.2-utf8bom.json")).getFile());
final JsonParser parser = new JsonParser();
assertNotNull(parser.parse(file));
assertNotNull(getJsonBom("bom-1.2-utf8bom.json"));
assertTrue(parser.isValid(file, Version.VERSION_12));
}

@Test
public void testParsedObjects12Bom() throws Exception {
final Bom bom = getJsonBom("bom-1.2.json");
Expand Down
9 changes: 9 additions & 0 deletions src/test/java/org/cyclonedx/parsers/XmlParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ public void testValid12Bom() throws Exception {
assertTrue(valid);
}

@Test
public void testValid12BomWithUtf8ByteOrderMarker() throws Exception {
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.2-utf8bom.xml")).getFile());
final XmlParser parser = new XmlParser();
assertNotNull(parser.parse(file));
assertNotNull(getXmlBom("bom-1.2-utf8bom.xml"));
assertTrue(parser.isValid(file, Version.VERSION_12));
}

@Test
public void testValidBomLink() throws Exception {
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.4-bomlink.xml")).getFile());
Expand Down
1 change: 1 addition & 0 deletions src/test/resources/bom-1.2-utf8bom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"bomFormat":"CycloneDX","specVersion":"1.2","version":1}
2 changes: 2 additions & 0 deletions src/test/resources/bom-1.2-utf8bom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1"/>
Loading