diff --git a/core/src/main/java/eu/ostrzyciel/jelly/core/internal/NameDecoderImpl.java b/core/src/main/java/eu/ostrzyciel/jelly/core/internal/NameDecoderImpl.java index 03c2c5e14..28bac3a4d 100644 --- a/core/src/main/java/eu/ostrzyciel/jelly/core/internal/NameDecoderImpl.java +++ b/core/src/main/java/eu/ostrzyciel/jelly/core/internal/NameDecoderImpl.java @@ -62,7 +62,7 @@ public NameDecoderImpl(int prefixTableSize, int nameTableSize, Function> 31)) + id; - NameLookupEntry entry = nameLookup[lastNameIdSet]; - entry.name = nameEntry.value(); - // Enough to invalidate the last IRI – we don't have to touch the serial number. - entry.lastPrefixId = 0; - // Set to null is required to avoid a false positive in the decode method for cases without a prefix. - entry.lastIri = null; + try { + NameLookupEntry entry = nameLookup[lastNameIdSet]; + entry.name = nameEntry.value(); + // Enough to invalidate the last IRI – we don't have to touch the serial number. + entry.lastPrefixId = 0; + // Set to null is required to avoid a false positive in the decode method for cases without a prefix. + entry.lastIri = null; + } catch (ArrayIndexOutOfBoundsException | NullPointerException e) { + throw JellyExceptions.rdfProtoDeserializationError( + "Name entry with ID %d is out of bounds of the name lookup table.".formatted(id) + ); + } } /** * Update the prefix table with a new entry. * @param prefixEntry prefix row - * @throws ArrayIndexOutOfBoundsException if the identifier is out of bounds + * @throws RdfProtoDeserializationError if the identifier is out of bounds */ @Override public void updatePrefixes(RdfPrefixEntry prefixEntry) { int id = prefixEntry.id(); lastPrefixIdSet = ((lastPrefixIdSet + 1) & ((id - 1) >> 31)) + id; - PrefixLookupEntry entry = prefixLookup[lastPrefixIdSet]; - entry.prefix = prefixEntry.value(); - entry.serial++; + try { + PrefixLookupEntry entry = prefixLookup[lastPrefixIdSet]; + entry.prefix = prefixEntry.value(); + entry.serial++; + } catch (ArrayIndexOutOfBoundsException | NullPointerException e) { + throw JellyExceptions.rdfProtoDeserializationError( + "Prefix entry with ID %d is out of bounds of the prefix lookup table.".formatted(id) + ); + } } /** * Reconstruct an IRI from its prefix and name ids. * @param iri IRI row from the Jelly proto * @return full IRI combining the prefix and the name - * @throws ArrayIndexOutOfBoundsException if IRI had indices out of lookup table bounds * @throws RdfProtoDeserializationError if the IRI reference is invalid * @throws NullPointerException if the IRI reference is invalid */ @@ -107,8 +118,15 @@ public void updatePrefixes(RdfPrefixEntry prefixEntry) { public TIri decode(RdfIri iri) { int nameId = iri.nameId(); lastNameIdReference = ((lastNameIdReference + 1) & ((nameId - 1) >> 31)) + nameId; - NameLookupEntry nameEntry = nameLookup[lastNameIdReference]; - + NameLookupEntry nameEntry; + try { + nameEntry = nameLookup[lastNameIdReference]; + } catch (ArrayIndexOutOfBoundsException e) { + throw JellyExceptions.rdfProtoDeserializationError( + ("Encountered an invalid name table reference (out of bounds). " + + "Name ID: %d, Prefix ID: %d").formatted(nameId, iri.prefixId()) + ); + } int prefixId = iri.prefixId(); // Branchless way to update the prefix ID // Equivalent to: @@ -117,7 +135,15 @@ public TIri decode(RdfIri iri) { lastPrefixIdReference = prefixId = (((prefixId - 1) >> 31) & lastPrefixIdReference) + prefixId; if (prefixId != 0) { // Name and prefix - PrefixLookupEntry prefixEntry = prefixLookup[prefixId]; + PrefixLookupEntry prefixEntry; + try { + prefixEntry = prefixLookup[prefixId]; + } catch (ArrayIndexOutOfBoundsException e) { + throw JellyExceptions.rdfProtoDeserializationError( + ("Encountered an invalid prefix table reference (out of bounds). " + + "Prefix ID: %d, Name ID: %d").formatted(prefixId, nameId) + ); + } if (nameEntry.lastPrefixId != prefixId || nameEntry.lastPrefixSerial != prefixEntry.serial) { // Update the last prefix nameEntry.lastPrefixId = prefixId; @@ -128,13 +154,13 @@ public TIri decode(RdfIri iri) { } if (nameEntry.lastIri == null) { throw JellyExceptions.rdfProtoDeserializationError( - "Encountered an invalid IRI reference. " + "Prefix ID: " + iri.prefixId() + ", Name ID: " + nameId + "Encountered an invalid IRI reference. Prefix ID: %d, Name ID: %d".formatted(iri.prefixId(), nameId) ); } } else if (nameEntry.lastIri == null) { if (nameEntry.name == null) { throw JellyExceptions.rdfProtoDeserializationError( - "Encountered an invalid IRI reference. " + "No prefix, Name ID: " + nameId + "Encountered an invalid IRI reference. No prefix, Name ID: %d".formatted(nameId) ); } // Name only, no need to check the prefix lookup diff --git a/core/src/main/java/eu/ostrzyciel/jelly/core/internal/TranscoderLookup.java b/core/src/main/java/eu/ostrzyciel/jelly/core/internal/TranscoderLookup.java index 60f9b31d5..4436ae06b 100644 --- a/core/src/main/java/eu/ostrzyciel/jelly/core/internal/TranscoderLookup.java +++ b/core/src/main/java/eu/ostrzyciel/jelly/core/internal/TranscoderLookup.java @@ -1,5 +1,6 @@ package eu.ostrzyciel.jelly.core.internal; +import eu.ostrzyciel.jelly.core.JellyExceptions; import java.util.Arrays; /** @@ -101,7 +102,9 @@ int remap(int id) { */ void newInputStream(int size) { if (size > outputSize) { - throw new IllegalArgumentException("Input lookup size cannot be greater than the output lookup size"); + throw JellyExceptions.rdfProtoTranscodingError( + "Input lookup size cannot be greater than the output lookup size" + ); } if (table != null) { // Only set this for streams 2 and above (counting from 1) diff --git a/core/src/main/scala/eu/ostrzyciel/jelly/core/JellyExceptions.scala b/core/src/main/scala/eu/ostrzyciel/jelly/core/JellyExceptions.scala index 7861bb233..2e3af8bc0 100644 --- a/core/src/main/scala/eu/ostrzyciel/jelly/core/JellyExceptions.scala +++ b/core/src/main/scala/eu/ostrzyciel/jelly/core/JellyExceptions.scala @@ -30,4 +30,12 @@ private object JellyExceptions extends JellyExceptions: private[core] def rdfProtoSerializationError(msg: String): RdfProtoSerializationError = new RdfProtoSerializationError(msg) + /** + * Helper method to allow Java code to throw a [[RdfProtoTranscodingError]]. + * @param msg error message + * @return an instance of [[RdfProtoTranscodingError]] + */ + private[core] def rdfProtoTranscodingError(msg: String): RdfProtoTranscodingError = + new RdfProtoTranscodingError(msg) + export JellyExceptions.* diff --git a/core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoDecoderSpec.scala b/core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoDecoderSpec.scala index 9c7eacbdc..0723399e4 100644 --- a/core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoDecoderSpec.scala +++ b/core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoDecoderSpec.scala @@ -272,22 +272,26 @@ class ProtoDecoderSpec extends AnyWordSpec, Matchers: // The tests for this logic are in internal.NameDecoderSpec // Here we are just testing if the exceptions are rethrown correctly. - "throw exception on out-of-bounds references to lookups" in { + "throw exception on an invalid IRI term" in { val decoder = MockConverterFactory.triplesDecoder(None) val data = wrapEncodedFull(Seq( JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.TRIPLES), + RdfPrefixEntry(0, null), + RdfNameEntry(0, null), RdfTriple( RdfTerm.Bnode("1"), RdfTerm.Bnode("2"), - RdfIri(10000, 0), + RdfIri(1, 1), ), )) decoder.ingestRow(data.head) + decoder.ingestRow(data(1)) + decoder.ingestRow(data(2)) val error = intercept[RdfProtoDeserializationError] { - decoder.ingestRow(data(1)) + decoder.ingestRow(data(3)) } error.getMessage should include ("Error while decoding term") - error.getCause shouldBe a [ArrayIndexOutOfBoundsException] + error.getCause shouldBe a [NullPointerException] } } @@ -461,20 +465,28 @@ class ProtoDecoderSpec extends AnyWordSpec, Matchers: // The tests for this logic are in internal.NameDecoderSpec // Here we are just testing if the exceptions are rethrown correctly. - "throw exception on out-of-bounds references to lookups (graph term)" in { - val decoder = MockConverterFactory.graphsAsQuadsDecoder(None) + "throw exception on an invalid IRI term" in { + val decoder = MockConverterFactory.graphsAsQuadsDecoder() val data = wrapEncodedFull(Seq( JellyOptions.smallGeneralized.withPhysicalType(PhysicalStreamType.GRAPHS), - RdfGraphStart( - RdfIri(10000, 0), + RdfPrefixEntry(0, null), + RdfNameEntry(0, null), + RdfGraphStart(RdfDefaultGraph.defaultInstance), + RdfTriple( + RdfTerm.Bnode("1"), + RdfTerm.Bnode("2"), + RdfIri(1, 1), ), )) decoder.ingestRow(data.head) + decoder.ingestRow(data(1)) + decoder.ingestRow(data(2)) + decoder.ingestRow(data(3)) val error = intercept[RdfProtoDeserializationError] { - decoder.ingestRow(data(1)) + decoder.ingestRow(data(4)) } - error.getMessage should include ("Error while decoding graph term") - error.getCause shouldBe a [ArrayIndexOutOfBoundsException] + error.getMessage should include("Error while decoding term") + error.getCause shouldBe a[NullPointerException] } } diff --git a/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/NameDecoderSpec.scala b/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/NameDecoderSpec.scala index 230a254fa..a983df888 100644 --- a/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/NameDecoderSpec.scala +++ b/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/NameDecoderSpec.scala @@ -103,66 +103,75 @@ class NameDecoderSpec extends AnyWordSpec, Matchers: "not accept a new prefix ID larger than table size" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.updatePrefixes(RdfPrefixEntry(9, "https://test.org/")) } + e.getMessage should include ("Prefix entry with ID 9") } "not accept a new prefix ID lower than 0 (-1)" in { val dec = makeDecoder(smallOptions) - intercept[NullPointerException] { + val e = intercept[RdfProtoDeserializationError] { dec.updatePrefixes(RdfPrefixEntry(-1, "https://test.org/")) } + e.getMessage should include ("Prefix entry with ID -1") } "not accept a new prefix ID lower than 0 (-2)" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.updatePrefixes(RdfPrefixEntry(-2, "https://test.org/")) } + e.getMessage should include ("Prefix entry with ID -2") } "not retrieve a prefix ID larger than table size" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.decode(RdfIri(9, 0)) } + e.getMessage should include ("Prefix ID: 9") } "not accept a new name ID larger than table size" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.updateNames(RdfNameEntry(17, "Cake")) } + e.getMessage should include ("Name entry with ID 17") } "not accept a default ID going beyond the table size" in { val dec = makeDecoder(smallOptions) dec.updateNames(RdfNameEntry(16, "Cake")) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.updateNames(RdfNameEntry(0, "Cake 2")) } + e.getMessage should include ("Name entry with ID 0") } "not accept a new name ID lower than 0 (-1)" in { val dec = makeDecoder(smallOptions) - intercept[NullPointerException] { + val e = intercept[RdfProtoDeserializationError] { dec.updateNames(RdfNameEntry(-1, "Cake")) } + e.getMessage should include ("Name entry with ID -1") } "not accept a new name ID lower than 0 (-2)" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.updateNames(RdfNameEntry(-2, "Cake")) } + e.getMessage should include ("Name entry with ID -2") } "not retrieve a name ID larger than table size" in { val dec = makeDecoder(smallOptions) - intercept[ArrayIndexOutOfBoundsException] { + val e = intercept[RdfProtoDeserializationError] { dec.decode(RdfIri(0, 17)) } + e.getMessage should include ("Name ID: 17") } } } diff --git a/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/TranscoderLookupSpec.scala b/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/TranscoderLookupSpec.scala index 53293a1bd..33344e6bc 100644 --- a/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/TranscoderLookupSpec.scala +++ b/core/src/test/scala/eu/ostrzyciel/jelly/core/internal/TranscoderLookupSpec.scala @@ -1,5 +1,6 @@ package eu.ostrzyciel.jelly.core.internal +import eu.ostrzyciel.jelly.core.JellyExceptions.RdfProtoTranscodingError import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec @@ -11,7 +12,7 @@ class TranscoderLookupSpec extends AnyWordSpec, Matchers: "TranscoderLookup" should { "throw an exception when trying to set input lookup size greater than the output" in { val tl = TranscoderLookup(false, 100) - val ex = intercept[IllegalArgumentException] { + val ex = intercept[RdfProtoTranscodingError] { tl.newInputStream(120) } ex.getMessage should include ("Input lookup size cannot be greater than the output lookup size")