Skip to content

Commit 9a772a2

Browse files
authored
Merge pull request #885 from nscuro/tolerate-byte-order-mark
Tolerate UTF8 byte order marker in file parsing
2 parents 52e52bb + 3d58a1a commit 9a772a2

7 files changed

Lines changed: 86 additions & 20 deletions

File tree

src/main/java/org/cyclonedx/parsers/BomParserFactory.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,44 +20,46 @@
2020

2121
import org.apache.commons.io.IOUtils;
2222
import org.cyclonedx.exception.ParseException;
23-
import org.cyclonedx.parsers.JsonParser;
24-
import org.cyclonedx.parsers.Parser;
25-
import org.cyclonedx.parsers.XmlParser;
23+
2624
import java.io.File;
2725
import java.io.IOException;
2826
import java.io.InputStream;
2927
import java.nio.charset.StandardCharsets;
3028
import java.nio.file.Files;
29+
import java.util.Arrays;
3130

3231
public class BomParserFactory {
3332

3433
private BomParserFactory() {}
3534

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

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

50-
if (bytes[0] == 123) {
51+
if (bytes[offset] == (byte) '{') {
5152
return new JsonParser();
52-
} else if (bytes[0] == 60) {
53+
} else if (bytes[offset] == (byte) '<') {
5354
return new XmlParser();
5455
} else {
5556
throw new ParseException("The specified BOM is not in a supported format. Supported formats are XML and JSON");
5657
}
5758
}
5859

5960
public static boolean looksLikeCycloneDX(final byte[] bytes) {
60-
final String bomString = new String(bytes, StandardCharsets.UTF_8);
61+
final int offset = hasUtf8ByteOrderMark(bytes) ? 3 : 0;
62+
final String bomString = new String(bytes, offset, bytes.length - offset, StandardCharsets.UTF_8);
6163
if (bomString.startsWith("<?xml") && bomString.contains("<bom") &&
6264
bomString.contains("http://cyclonedx.org/schema/bom")) {
6365
return true;
@@ -66,4 +68,12 @@ public static boolean looksLikeCycloneDX(final byte[] bytes) {
6668
return bomString.startsWith("{") && bomString.contains("bomFormat") && bomString.contains("CycloneDX");
6769
}
6870
}
71+
72+
private static boolean hasUtf8ByteOrderMark(byte[] bytes) {
73+
return bytes.length >= 3
74+
&& bytes[0] == (byte) 0xEF
75+
&& bytes[1] == (byte) 0xBB
76+
&& bytes[2] == (byte) 0xBF;
77+
}
78+
6979
}

src/main/java/org/cyclonedx/parsers/JsonParser.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public List<ParseException> validate(final InputStream inputStream, final Versio
160160
* @return a list of exceptions encountered during validation
161161
*/
162162
public List<ParseException> validate(final String bomString, final Version schemaVersion) throws IOException {
163-
return validate(mapper.readTree(bomString), schemaVersion);
163+
return validate(mapper.readTree(skipUtf8Bom(bomString)), schemaVersion);
164164
}
165165

166166
/**
@@ -244,4 +244,12 @@ public boolean isValid(final InputStream inputStream) throws IOException {
244244
public boolean isValid(final InputStream inputStream, final Version schemaVersion) throws IOException {
245245
return validate(inputStream, schemaVersion).isEmpty();
246246
}
247+
248+
private static String skipUtf8Bom(String string) {
249+
// NB: Jackson does not strip a UTF-8 BOM when reading from a String,
250+
// but it DOES do that for byte-based input.
251+
return string.startsWith("\uFEFF")
252+
? string.substring(1)
253+
: string;
254+
}
247255
}

src/test/java/org/cyclonedx/BomParserFactoryTest.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,11 @@
2424
import org.cyclonedx.parsers.Parser;
2525
import org.cyclonedx.parsers.XmlParser;
2626
import org.junit.jupiter.api.Test;
27+
2728
import java.io.File;
28-
import java.util.Objects;
29+
import java.nio.file.Files;
2930

31+
import static java.util.Objects.requireNonNull;
3032
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
3133
import static org.junit.jupiter.api.Assertions.assertThrows;
3234

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

4244
@Test
4345
public void testJSONFactory() throws Exception {
4446
Parser parser = BomParserFactory.createParser(new File(
45-
Objects.requireNonNull(BomParserFactory.class.getResource("/bom-1.2.json")).getFile()));
47+
requireNonNull(BomParserFactory.class.getResource("/bom-1.2.json")).getFile()));
48+
assertInstanceOf(JsonParser.class, parser);
49+
}
50+
51+
@Test
52+
public void testXMLFactoryWithUtf8ByteOrderMarker() throws Exception {
53+
Parser parser = BomParserFactory.createParser(
54+
new File(requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.xml")).getFile()));
55+
assertInstanceOf(XmlParser.class, parser);
56+
}
57+
58+
@Test
59+
public void testJSONFactoryWithUtf8ByteOrderMarker() throws Exception {
60+
Parser parser = BomParserFactory.createParser(new File(
61+
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.json")).getFile()));
4662
assertInstanceOf(JsonParser.class, parser);
4763
}
4864

65+
@Test
66+
public void testFactoryFromByteArrayWithUtf8ByteOrderMarker() throws Exception {
67+
byte[] xml = Files.readAllBytes(new File(
68+
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.xml")).getFile()).toPath());
69+
byte[] json = Files.readAllBytes(new File(
70+
requireNonNull(BomParserFactory.class.getResource("/bom-1.2-utf8bom.json")).getFile()).toPath());
71+
assertInstanceOf(XmlParser.class, BomParserFactory.createParser(xml));
72+
assertInstanceOf(JsonParser.class, BomParserFactory.createParser(json));
73+
}
74+
4975
@Test()
5076
public void testFactoryThrowsParseExceptionWithEmptyData() {
5177
byte[] emptyData = new byte[]{};

src/test/java/org/cyclonedx/parsers/JsonParserTest.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@
2020

2121
import org.cyclonedx.Version;
2222
import org.cyclonedx.model.Bom;
23+
import org.cyclonedx.model.Citation;
2324
import org.cyclonedx.model.Component;
2425
import org.cyclonedx.model.Component.Type;
2526
import org.cyclonedx.model.Dependency;
2627
import org.cyclonedx.model.ExternalReference;
27-
import org.cyclonedx.model.TlpClassification;
2828
import org.cyclonedx.model.License;
2929
import org.cyclonedx.model.LicenseChoice;
3030
import org.cyclonedx.model.OrganizationalEntity;
31+
import org.cyclonedx.model.Patent;
32+
import org.cyclonedx.model.PatentAssertion;
33+
import org.cyclonedx.model.PatentFamily;
34+
import org.cyclonedx.model.PatentItem;
35+
import org.cyclonedx.model.TlpClassification;
3136
import org.cyclonedx.model.attestation.Assessor;
3237
import org.cyclonedx.model.attestation.Attestation;
3338
import org.cyclonedx.model.attestation.AttestationMap;
@@ -64,14 +69,10 @@
6469
import org.cyclonedx.model.definition.Standard;
6570
import org.cyclonedx.model.license.Acknowledgement;
6671
import org.cyclonedx.model.license.Expression;
67-
import org.cyclonedx.model.license.ExpressionDetailed;
6872
import org.cyclonedx.model.license.ExpressionDetail;
69-
import org.cyclonedx.model.Citation;
70-
import org.cyclonedx.model.Patent;
71-
import org.cyclonedx.model.PatentFamily;
72-
import org.cyclonedx.model.PatentAssertion;
73-
import org.cyclonedx.model.PatentItem;
73+
import org.cyclonedx.model.license.ExpressionDetailed;
7474
import org.junit.jupiter.api.Test;
75+
7576
import java.io.File;
7677
import java.util.ArrayList;
7778
import java.util.Arrays;
@@ -98,6 +99,15 @@ public void testValid12Bom() throws Exception {
9899
System.out.println(bom.getSerialNumber());
99100
}
100101

102+
@Test
103+
public void testValid12BomWithUtf8ByteOrderMarker() throws Exception {
104+
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.2-utf8bom.json")).getFile());
105+
final JsonParser parser = new JsonParser();
106+
assertNotNull(parser.parse(file));
107+
assertNotNull(getJsonBom("bom-1.2-utf8bom.json"));
108+
assertTrue(parser.isValid(file, Version.VERSION_12));
109+
}
110+
101111
@Test
102112
public void testParsedObjects12Bom() throws Exception {
103113
final Bom bom = getJsonBom("bom-1.2.json");

src/test/java/org/cyclonedx/parsers/XmlParserTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,15 @@ public void testValid12Bom() throws Exception {
136136
assertTrue(valid);
137137
}
138138

139+
@Test
140+
public void testValid12BomWithUtf8ByteOrderMarker() throws Exception {
141+
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.2-utf8bom.xml")).getFile());
142+
final XmlParser parser = new XmlParser();
143+
assertNotNull(parser.parse(file));
144+
assertNotNull(getXmlBom("bom-1.2-utf8bom.xml"));
145+
assertTrue(parser.isValid(file, Version.VERSION_12));
146+
}
147+
139148
@Test
140149
public void testValidBomLink() throws Exception {
141150
final File file = new File(Objects.requireNonNull(this.getClass().getResource("/bom-1.4-bomlink.xml")).getFile());
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"bomFormat":"CycloneDX","specVersion":"1.2","version":1}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1"/>

0 commit comments

Comments
 (0)