Skip to content

Commit a79a6af

Browse files
committed
JCL-431: Improve containment validation (#653)
1 parent 237a264 commit a79a6af

File tree

4 files changed

+84
-23
lines changed

4 files changed

+84
-23
lines changed

solid/src/main/java/com/inrupt/client/solid/SolidContainer.java

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ public SolidContainer(final URI identifier, final Dataset dataset, final Metadat
8282
* @return the contained resources
8383
*/
8484
public Set<SolidResource> getResources() {
85-
final String container = normalize(getIdentifier());
8685
// As defined by the Solid Protocol, containers always end with a slash.
87-
if (container.endsWith("/")) {
88-
final Node node = new Node(rdf.createIRI(getIdentifier().toString()), getGraph());
86+
final URI base = getIdentifier().normalize();
87+
if (isContainer(base)) {
88+
final String container = normalize(base);
89+
final Node node = new Node(rdf.createIRI(base.toString()), getGraph());
8990
try (final Stream<Node.TypedNode> stream = node.getResources()) {
9091
return stream.filter(child -> verifyContainmentIri(container, child)).map(child -> {
9192
final Metadata.Builder builder = Metadata.newBuilder();
@@ -100,14 +101,15 @@ public Set<SolidResource> getResources() {
100101

101102
@Override
102103
public ValidationResult validate() {
103-
// Get the normalized container URI
104-
final String container = normalize(getIdentifier());
105104
final List<String> messages = new ArrayList<>();
106105
// Verify that the container URI path ends with a slash
107-
if (!container.endsWith("/")) {
106+
final URI base = getIdentifier().normalize();
107+
if (!isContainer(base)) {
108108
messages.add("Container URI does not end in a slash");
109109
}
110110

111+
// Get the normalized container URI
112+
final String container = normalize(base);
111113
// Verify that all ldp:contains triples align with Solid expectations
112114
getGraph().stream(null, rdf.createIRI(LDP.contains.toString()), null)
113115
.collect(Collectors.partitioningBy(verifyContainmentTriple(container)))
@@ -121,8 +123,8 @@ public ValidationResult validate() {
121123
return new ValidationResult(false, messages);
122124
}
123125

124-
static String normalize(final IRI iri) {
125-
return normalize(URI.create(iri.getIRIString()));
126+
static boolean isContainer(final URI uri) {
127+
return uri.normalize().getPath().endsWith("/");
126128
}
127129

128130
static String normalize(final URI uri) {
@@ -145,22 +147,44 @@ static Predicate<Triple> verifyContainmentTriple(final String container) {
145147
}
146148

147149
static boolean verifyContainmentIri(final String container, final IRI object) {
148-
if (!object.getIRIString().startsWith(container)) {
149-
// Out-of-domain containment triple object
150+
151+
// URI Structure Tests
152+
final URI base = URI.create(container).normalize();
153+
final URI normalized = URI.create(object.getIRIString()).normalize();
154+
155+
// Query strings are not allowed in subject or object URI
156+
if (base.getQuery() != null || normalized.getQuery() != null) {
157+
return false;
158+
}
159+
160+
// URI fragments are not allowed in subject or object URI
161+
if (base.getFragment() != null || normalized.getFragment() != null) {
150162
return false;
151-
} else {
152-
final String relativePath = object.getIRIString().substring(container.length());
153-
final String normalizedPath = relativePath.endsWith("/") ?
154-
relativePath.substring(0, relativePath.length() - 1) : relativePath;
155-
if (normalizedPath.isEmpty()) {
156-
// Containment triple subject and object cannot be the same
157-
return false;
158-
}
159-
if (normalizedPath.contains("/")) {
160-
// Containment cannot skip intermediate nodes
161-
return false;
162-
}
163163
}
164+
165+
// Base URI cannot equal the object URI
166+
if (base.getScheme().equals(normalized.getScheme()) &&
167+
base.getSchemeSpecificPart().equals(normalized.getSchemeSpecificPart())) {
168+
return false;
169+
}
170+
171+
// Relative path tests
172+
final URI relative = base.relativize(normalized);
173+
174+
// Object URI must be relative to (contained in) the base URI
175+
if (relative.isAbsolute()) {
176+
return false;
177+
}
178+
179+
final String relativePath = relative.getPath();
180+
final String normalizedPath = relativePath.endsWith("/") ?
181+
relativePath.substring(0, relativePath.length() - 1) : relativePath;
182+
183+
// Containment cannot skip intermediate nodes
184+
if (normalizedPath.contains("/")) {
185+
return false;
186+
}
187+
164188
return true;
165189
}
166190

solid/src/test/java/com/inrupt/client/solid/SolidClientTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,8 @@ void testLowLevelSolidContainer() {
231231
expected.add(URIBuilder.newBuilder(uri).path("newContainer/").build());
232232
expected.add(URIBuilder.newBuilder(uri).path("test.txt").build());
233233
expected.add(URIBuilder.newBuilder(uri).path("test2.txt").build());
234+
expected.add(URIBuilder.newBuilder(uri).path("test3").build());
235+
expected.add(URIBuilder.newBuilder(uri).path("test4").build());
234236

235237
client.send(Request.newBuilder(uri).build(), SolidResourceHandlers.ofSolidContainer())
236238
.thenAccept(response -> {

solid/src/test/java/com/inrupt/client/solid/SolidRDFSourceTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,25 @@ void testEmptyContentType() {
194194
}
195195
}
196196

197+
@Test
198+
void testNonContainer() {
199+
final URI resource = URI.create(config.get("solid_resource_uri") + "/recipe");
200+
final Request request = Request.newBuilder()
201+
.uri(resource)
202+
.header("Accept", "text/turtle")
203+
.GET()
204+
.build();
205+
206+
final Response<SolidContainer> response = client.send(request, SolidResourceHandlers.ofSolidContainer())
207+
.toCompletableFuture().join();
208+
209+
try (final SolidContainer container = response.body()) {
210+
assertEquals(resource, container.getIdentifier());
211+
assertTrue(container.getResources().isEmpty());
212+
assertFalse(container.validate().isValid());
213+
}
214+
}
215+
197216
@Test
198217
void testInvalidRdf() {
199218
final URI resource = URI.create(config.get("solid_resource_uri") + "/nonRDF");

solid/src/test/resources/__files/container.ttl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
a ldp:BasicContainer ;
88
dct:modified "2022-11-25T10:36:36Z"^^xsd:dateTime;
99
ldp:contains <newContainer/>, <test.txt>, <test2.txt> .
10+
<intermediate/..>
11+
a ldp:BasicContainer ;
12+
dct:modified "2022-11-25T10:38:12Z"^^xsd:dateTime ;
13+
ldp:contains <test3> .
14+
<intermediate/../>
15+
a ldp:BasicContainer ;
16+
dct:modified "2022-11-25T10:38:47Z"^^xsd:dateTime ;
17+
ldp:contains <test4> .
1018
<newContainer/>
1119
a ldp:BasicContainer ;
1220
dct:modified "2022-11-25T10:36:36Z"^^xsd:dateTime .
@@ -16,10 +24,18 @@
1624
<test2.txt>
1725
a pl:Resource, ldp:NonRDFSource;
1826
dct:modified "2022-11-25T10:37:06Z"^^xsd:dateTime .
27+
<test3>
28+
a ldp:RDFSource ;
29+
dct:modified "2022-11-25T10:37:31Z"^^xsd:dateTime .
30+
<test4>
31+
a ldp:RDFSource ;
32+
dct:modified "2022-11-25T10:39:22Z"^^xsd:dateTime .
1933

2034
# These containment triples should not be included in a getResources response
2135
<>
22-
ldp:contains <https://example.com/other> , <newContainer/child> , <> , <./> .
36+
ldp:contains <https://example.com/other> , <newContainer/child> , <newContainer%2Fchild2> , <> , <./> ,
37+
<?foo> , <#bar> , <?foo#bar> , <./?foo> , <./#bar> , <./?foo#bar> ,
38+
<../?foo> , <../#bar> , <../?foo#bar> , <child?foo> , <child#bar> , <child?foo#bar> .
2339
<https://example.test/container/>
2440
a ldp:BasicContainer ;
2541
ldp:contains <https://example.test/container/external> .

0 commit comments

Comments
 (0)