Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public NameDecoderImpl(int prefixTableSize, int nameTableSize, Function<String,
/**
* Update the name table with a new entry.
* @param nameEntry name row
* @throws ArrayIndexOutOfBoundsException if the identifier is out of bounds
* @throws RdfProtoDeserializationError if the identifier is out of bounds
*/
@Override
public void updateNames(RdfNameEntry nameEntry) {
Expand All @@ -72,33 +72,44 @@ public void updateNames(RdfNameEntry nameEntry) {
// else lastNameIdSet = id;
// Same code is used in the methods below.
lastNameIdSet = ((lastNameIdSet + 1) & ((id - 1) >> 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 " + id + " is out of bounds of the name lookup table."
);
}
}

/**
* 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 " + id + " is out of bounds of the prefix lookup table."
);
}
}

/**
* 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
*/
Expand All @@ -107,8 +118,18 @@ 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: " +
nameId +
", Prefix ID: " +
iri.prefixId()
);
}
int prefixId = iri.prefixId();
// Branchless way to update the prefix ID
// Equivalent to:
Expand All @@ -117,7 +138,18 @@ 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: " +
prefixId +
", Name ID: " +
nameId
);
}
if (nameEntry.lastPrefixId != prefixId || nameEntry.lastPrefixSerial != prefixEntry.serial) {
// Update the last prefix
nameEntry.lastPrefixId = prefixId;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package eu.ostrzyciel.jelly.core.internal;

import eu.ostrzyciel.jelly.core.JellyExceptions;
import java.util.Arrays;

/**
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
34 changes: 23 additions & 11 deletions core/src/test/scala/eu/ostrzyciel/jelly/core/ProtoDecoderSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
}

Expand Down Expand Up @@ -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]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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")
Expand Down