diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java index c112f69e8..fb9f321d2 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParser.java @@ -16,8 +16,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -203,14 +201,34 @@ private static boolean forbiddenIriCharacter(int c) { } private void validateIri(String text) { - try { - URI uri = new URI(text); - if (!uri.isAbsolute()) { - throw error("N-Quads requires an absolute IRI"); + if (!isAbsoluteIri(text)) { + throw error("N-Quads requires an absolute IRI"); + } + } + + private static boolean isAbsoluteIri(String text) { + if (text == null || text.isEmpty() || !isAsciiAlpha(text.charAt(0))) { + return false; + } + int i = 1; + int length = text.length(); + while (i < length) { + char c = text.charAt(i); + if (isSchemeChar(c)) { + i++; + } else { + break; } - } catch (URISyntaxException e) { - throw new ParsingException(location("Invalid absolute IRI: " + e.getReason()), e); } + return i < length && text.charAt(i) == ':'; + } + + private static boolean isAsciiAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isSchemeChar(char c) { + return isAsciiAlpha(c) || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.'; } private BNode blankNode() throws IOException { diff --git a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java index cc0ae3a16..174819313 100644 --- a/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java +++ b/src/main/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParser.java @@ -16,8 +16,6 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.HashMap; @@ -182,14 +180,34 @@ private static boolean forbiddenIriCharacter(int c) { } private void validateIri(String text) { - try { - URI uri = new URI(text); - if (!uri.isAbsolute()) { - throw error("N-Triples requires an absolute IRI"); + if (!isAbsoluteIri(text)) { + throw error("N-Triples requires an absolute IRI"); + } + } + + private static boolean isAbsoluteIri(String text) { + if (text == null || text.isEmpty() || !isAsciiAlpha(text.charAt(0))) { + return false; + } + int i = 1; + int length = text.length(); + while (i < length) { + char c = text.charAt(i); + if (isSchemeChar(c)) { + i++; + } else { + break; } - } catch (URISyntaxException e) { - throw new ParsingException(location("Invalid absolute IRI: " + e.getReason()), e); } + return i < length && text.charAt(i) == ':'; + } + + private static boolean isAsciiAlpha(char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + private static boolean isSchemeChar(char c) { + return isAsciiAlpha(c) || (c >= '0' && c <= '9') || c == '+' || c == '-' || c == '.'; } private BNode blankNode() throws IOException { diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParserTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParserTest.java index 0bccbe03b..c3fa29be5 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParserTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/nquads/NQuadsParserTest.java @@ -15,6 +15,7 @@ import java.io.StringReader; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; /** @@ -195,4 +196,36 @@ void testParseUnicodeEscapeIRIUxInGraph() throws ParsingException { parser.parse(new StringReader(nquad)); verify(mockModel).add(mockSubjectIRI, mockPredicateIRI, mockObjectIRI, expectedGraphIRI); } + + @Test + @DisplayName("Test parsing a quad with non-ASCII Unicode characters in IRI (W3C test060 NBSP and math symbols)") + void testParseNonAsciiUnicodeInIRI() throws ParsingException { + String nquad = " ."; + IRI expectedSubject = mock(IRI.class); + IRI expectedPredicate = mock(IRI.class); + IRI expectedObject = mock(IRI.class); + IRI expectedGraph = mock(IRI.class); + + lenient().when(mockValueFactory.createIRI(eq("urn:ex:s:003"))).thenReturn(expectedSubject); + lenient().when(mockValueFactory.createIRI(eq("urn:ex:020"))).thenReturn(expectedPredicate); + lenient().when(mockValueFactory.createIRI(eq("urn:ex:\u00a0"))).thenReturn(expectedObject); + lenient().when(mockValueFactory.createIRI(eq("urn:ex:\u221e"))).thenReturn(expectedGraph); + + parser.parse(new StringReader(nquad)); + verify(mockModel).add(expectedSubject, expectedPredicate, expectedObject, expectedGraph); + } + + @Test + @DisplayName("Test parsing rejects relative IRIs in N-Quads") + void testParseRejectsRelativeIRI() { + String nquad = " ."; + assertThrows(ParsingException.class, () -> parser.parse(new StringReader(nquad))); + } + + @Test + @DisplayName("Test parsing rejects IRIs without valid scheme") + void testParseRejectsInvalidScheme() { + String nquad = "<:invalidScheme> ."; + assertThrows(ParsingException.class, () -> parser.parse(new StringReader(nquad))); + } } diff --git a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParserTest.java b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParserTest.java index 17b18aa4a..643ba2835 100644 --- a/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParserTest.java +++ b/src/test/java/fr/inria/corese/core/next/data/impl/io/parser/ntriples/NTriplesParserTest.java @@ -15,6 +15,7 @@ import java.io.StringReader; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; /** @@ -210,4 +211,34 @@ void testParseUnicodeEscapeIRIU() throws ParsingException { parser.parse(new StringReader(ntriple)); verify(mockModel).add(expectedSubjectIRI, mockPredicateIRI, mockObjectIRI); } + + @Test + @DisplayName("Test parsing a triple with non-ASCII Unicode characters in IRI (W3C test060 NBSP and math symbols)") + void testParseNonAsciiUnicodeInIRI() throws ParsingException { + String ntriple = " ."; + IRI expectedSubject = mock(IRI.class); + IRI expectedPredicate = mock(IRI.class); + IRI expectedObject = mock(IRI.class); + + lenient().when(mockValueFactory.createIRI(eq("urn:ex:s:003"))).thenReturn(expectedSubject); + lenient().when(mockValueFactory.createIRI(eq("urn:ex:020"))).thenReturn(expectedPredicate); + lenient().when(mockValueFactory.createIRI(eq("urn:ex:\u00a0"))).thenReturn(expectedObject); + + parser.parse(new StringReader(ntriple)); + verify(mockModel).add(expectedSubject, expectedPredicate, expectedObject); + } + + @Test + @DisplayName("Test parsing rejects relative IRIs in N-Triples") + void testParseRejectsRelativeIRI() { + String ntriple = " ."; + assertThrows(ParsingException.class, () -> parser.parse(new StringReader(ntriple))); + } + + @Test + @DisplayName("Test parsing rejects IRIs without valid scheme in N-Triples") + void testParseRejectsInvalidScheme() { + String ntriple = "<:invalidScheme> ."; + assertThrows(ParsingException.class, () -> parser.parse(new StringReader(ntriple))); + } }