diff --git a/src/main/java/org/cyclonedx/parsers/BomParserFactory.java b/src/main/java/org/cyclonedx/parsers/BomParserFactory.java index 2936a039d..11494bab1 100644 --- a/src/main/java/org/cyclonedx/parsers/BomParserFactory.java +++ b/src/main/java/org/cyclonedx/parsers/BomParserFactory.java @@ -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) { @@ -42,21 +42,33 @@ 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"); @@ -64,7 +76,7 @@ public static Parser createParser(final byte[] bytes) throws ParseException { } 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("= 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 diff --git a/src/test/java/org/cyclonedx/BomParserFactoryTest.java b/src/test/java/org/cyclonedx/BomParserFactoryTest.java index 780402e64..896f90edf 100644 --- a/src/test/java/org/cyclonedx/BomParserFactoryTest.java +++ b/src/test/java/org/cyclonedx/BomParserFactoryTest.java @@ -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 + "") + .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(