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
40 changes: 32 additions & 8 deletions src/main/java/org/cyclonedx/parsers/BomParserFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@

import org.cyclonedx.exception.ParseException;

import java.io.BufferedInputStream;
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())) {
try (final InputStream fis = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
final byte[] prefix = new byte[4]; // potential 3-byte UTF-8 byte-order mark + 1 content byte
int actualPrefixLength = 0;
while (actualPrefixLength < prefix.length) {
Expand All @@ -42,29 +42,41 @@ public static Parser createParser(final File file) throws ParseException {
}
actualPrefixLength += read;
}
return createParser(Arrays.copyOf(prefix, actualPrefixLength));
final int offset = firstContentOffset(prefix, actualPrefixLength);
if (offset < actualPrefixLength) {
return createParserForContentByte(prefix[offset] & 0xFF);
}
int contentByte;
do {
contentByte = fis.read();
} while (isWhitespace(contentByte));
return createParserForContentByte(contentByte);
} catch (IOException e) {
throw new ParseException("An error occurred creating parser from file", e);
}
}

public static Parser createParser(final byte[] bytes) throws ParseException {
final int offset = hasUtf8ByteOrderMark(bytes) ? 3 : 0;
if (bytes.length - offset < 1) {
final int offset = firstContentOffset(bytes, bytes.length);
return createParserForContentByte(offset < bytes.length ? bytes[offset] & 0xFF : -1);
}

private static Parser createParserForContentByte(final int contentByte) throws ParseException {
if (contentByte == -1) {
throw new ParseException("Cannot create parser from empty byte array.");
}

if (bytes[offset] == (byte) '{') {
if (contentByte == '{') {
return new JsonParser();
} else if (bytes[offset] == (byte) '<') {
} else if (contentByte == '<') {
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 int offset = hasUtf8ByteOrderMark(bytes) ? 3 : 0;
final int offset = firstContentOffset(bytes, bytes.length);
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")) {
Expand All @@ -75,6 +87,18 @@ public static boolean looksLikeCycloneDX(final byte[] bytes) {
}
}

private static int firstContentOffset(final byte[] bytes, final int length) {
int offset = length >= 3 && hasUtf8ByteOrderMark(bytes) ? 3 : 0;
while (offset < length && isWhitespace(bytes[offset])) {
offset++;
}
return offset;
}

private static boolean isWhitespace(final int value) {
return value == ' ' || value == '\t' || value == '\r' || value == '\n';
}

private static boolean hasUtf8ByteOrderMark(byte[] bytes) {
return bytes.length >= 3
&& bytes[0] == (byte) 0xEF
Expand Down
52 changes: 52 additions & 0 deletions src/test/java/org/cyclonedx/BomParserFactoryTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,68 @@
import org.cyclonedx.parsers.Parser;
import org.cyclonedx.parsers.XmlParser;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;

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

public class BomParserFactoryTest {

@TempDir
Path tempDir;

@ParameterizedTest
@ValueSource(strings = {"", "\n", " \t\r\n", "\uFEFF", "\uFEFF \r\n"})
void testJsonWithLeadingWhitespace(String prefix) throws Exception {
byte[] bytes = (prefix + "{\"bomFormat\":\"CycloneDX\",\"specVersion\":\"1.6\",\"version\":1}")
.getBytes(StandardCharsets.UTF_8);
Parser parser = BomParserFactory.createParser(bytes);
assertInstanceOf(JsonParser.class, parser);
assertEquals(1, parser.parse(bytes).getVersion());
assertTrue(BomParserFactory.looksLikeCycloneDX(bytes));

File file = Files.write(tempDir.resolve("bom.json"), bytes).toFile();
Parser fileParser = BomParserFactory.createParser(file);
assertInstanceOf(JsonParser.class, fileParser);
assertEquals(1, fileParser.parse(file).getVersion());
}

@ParameterizedTest
@ValueSource(strings = {"", "\n", " \t\r\n", "\uFEFF", "\uFEFF \r\n"})
void testXmlWithLeadingWhitespace(String prefix) throws Exception {
byte[] bytes = (prefix + "<bom xmlns=\"http://cyclonedx.org/schema/bom/1.6\" version=\"1\"/>")
.getBytes(StandardCharsets.UTF_8);
Parser parser = BomParserFactory.createParser(bytes);
assertInstanceOf(XmlParser.class, parser);
assertEquals(1, parser.parse(bytes).getVersion());

File file = Files.write(tempDir.resolve("bom.xml"), bytes).toFile();
Parser fileParser = BomParserFactory.createParser(file);
assertInstanceOf(XmlParser.class, fileParser);
assertEquals(1, fileParser.parse(file).getVersion());
}

@ParameterizedTest
@ValueSource(strings = {"", " \t\r\n", "\uFEFF", "\uFEFF \n", "x{}", " \u000B{}"})
void testRejectsEmptyOrUnsupportedContent(String content) throws Exception {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
assertThrows(ParseException.class, () -> BomParserFactory.createParser(bytes));
assertFalse(BomParserFactory.looksLikeCycloneDX(bytes));
File file = Files.write(tempDir.resolve("unsupported"), bytes).toFile();
assertThrows(ParseException.class, () -> BomParserFactory.createParser(file));
}

@Test
public void testXMLFactory() throws Exception {
Parser parser = BomParserFactory.createParser(
Expand Down
Loading