Skip to content

Commit 36509a0

Browse files
committed
test fixes
1 parent 09f97a6 commit 36509a0

File tree

9 files changed

+289
-117
lines changed

9 files changed

+289
-117
lines changed

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/FetchPolicy.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ public record FetchPolicy(
1313
int maxDocuments,
1414
int maxDepth
1515
) {
16+
public static final String HTTPS = "https";
17+
public static final String HTTP = "http";
18+
1619
public FetchPolicy {
1720
Objects.requireNonNull(allowedSchemes, "allowedSchemes");
1821
Objects.requireNonNull(timeout, "timeout");
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package io.github.simbo1905.json.schema;
2+
3+
import jdk.sandbox.java.util.json.Json;
4+
import jdk.sandbox.java.util.json.JsonValue;
5+
6+
import java.io.IOException;
7+
import java.net.URI;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.Objects;
12+
import java.util.Optional;
13+
import java.util.logging.Level;
14+
15+
import static io.github.simbo1905.json.schema.JsonSchema.LOG;
16+
17+
/// Local file fetcher that enforces a mandatory jail root directory
18+
record FileFetcher(Path jailRoot) implements JsonSchema.RemoteFetcher {
19+
FileFetcher(Path jailRoot) {
20+
this.jailRoot = Objects.requireNonNull(jailRoot, "jailRoot").toAbsolutePath().normalize();
21+
LOG.info(() -> "FileFetcher jailRoot=" + this.jailRoot);
22+
}
23+
24+
@Override
25+
public String scheme() {
26+
return "file";
27+
}
28+
29+
@Override
30+
public FetchResult fetch(URI uri, FetchPolicy policy) {
31+
Objects.requireNonNull(uri, "uri");
32+
Objects.requireNonNull(policy, "policy");
33+
34+
if (!"file".equalsIgnoreCase(uri.getScheme())) {
35+
LOG.severe(() -> "ERROR: FileFetcher received non-file URI " + uri);
36+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED,
37+
"FileFetcher only handles file:// URIs");
38+
}
39+
40+
Path target = toPath(uri).normalize();
41+
if (!target.startsWith(jailRoot)) {
42+
LOG.fine(() -> "FETCH DENIED outside jail: uri=" + uri + " path=" + target + " jailRoot=" + jailRoot);
43+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED,
44+
"Outside jail: " + target);
45+
}
46+
47+
if (!Files.exists(target) || !Files.isRegularFile(target)) {
48+
LOG.finer(() -> "NOT_FOUND: " + target);
49+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.NOT_FOUND,
50+
"No such file: " + target);
51+
}
52+
53+
try {
54+
long size = Files.size(target);
55+
if (size > policy.maxDocumentBytes()) {
56+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.PAYLOAD_TOO_LARGE,
57+
"File exceeds maxDocumentBytes: " + size);
58+
}
59+
byte[] bytes = Files.readAllBytes(target);
60+
long actual = bytes.length;
61+
if (actual != size && actual > policy.maxDocumentBytes()) {
62+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.PAYLOAD_TOO_LARGE,
63+
"File exceeds maxDocumentBytes after read: " + actual);
64+
}
65+
JsonValue doc = Json.parse(new String(bytes, StandardCharsets.UTF_8));
66+
return new FetchResult(doc, actual, Optional.empty());
67+
} catch (IOException e) {
68+
LOG.log(Level.SEVERE, () -> "ERROR: IO reading file " + target);
69+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.NETWORK_ERROR,
70+
"IO reading file: " + e.getMessage());
71+
}
72+
}
73+
74+
private static Path toPath(URI uri) {
75+
// java.nio handles file URIs via Paths.get(URI)
76+
return Path.of(uri);
77+
}
78+
}

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/JsonSchema.java

Lines changed: 84 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,73 @@ CompileOptions withFetchPolicy(FetchPolicy policy) {
156156
Objects.requireNonNull(policy, "policy");
157157
return new CompileOptions(remoteFetcher, refRegistry, policy);
158158
}
159+
160+
/// Delegating fetcher selecting implementation per URI scheme
161+
static final class DelegatingRemoteFetcher implements RemoteFetcher {
162+
private final Map<String, RemoteFetcher> byScheme;
163+
164+
DelegatingRemoteFetcher(RemoteFetcher... fetchers) {
165+
Objects.requireNonNull(fetchers, "fetchers");
166+
if (fetchers.length == 0) {
167+
throw new IllegalArgumentException("At least one RemoteFetcher required");
168+
}
169+
Map<String, RemoteFetcher> map = new HashMap<>();
170+
for (RemoteFetcher fetcher : fetchers) {
171+
Objects.requireNonNull(fetcher, "fetcher");
172+
String scheme = Objects.requireNonNull(fetcher.scheme(), "fetcher.scheme()").toLowerCase(Locale.ROOT);
173+
if (scheme.isEmpty()) {
174+
throw new IllegalArgumentException("RemoteFetcher scheme must not be empty");
175+
}
176+
if (map.putIfAbsent(scheme, fetcher) != null) {
177+
throw new IllegalArgumentException("Duplicate RemoteFetcher for scheme: " + scheme);
178+
}
179+
}
180+
this.byScheme = Map.copyOf(map);
181+
}
182+
183+
@Override
184+
public String scheme() {
185+
return "delegating";
186+
}
187+
188+
@Override
189+
public FetchResult fetch(java.net.URI uri, FetchPolicy policy) {
190+
Objects.requireNonNull(uri, "uri");
191+
String scheme = Optional.ofNullable(uri.getScheme())
192+
.map(s -> s.toLowerCase(Locale.ROOT))
193+
.orElse("");
194+
RemoteFetcher fetcher = byScheme.get(scheme);
195+
if (fetcher == null) {
196+
LOG.severe(() -> "ERROR: FETCH: " + uri + " - unsupported scheme");
197+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED,
198+
"No RemoteFetcher registered for scheme: " + scheme);
199+
}
200+
return fetcher.fetch(uri, policy);
201+
}
202+
}
159203
}
160204

161205
/// Remote fetcher SPI for loading external schema documents
162206
interface RemoteFetcher {
207+
String scheme();
163208
FetchResult fetch(java.net.URI uri, FetchPolicy policy) throws RemoteResolutionException;
164209

165210
static RemoteFetcher disallowed() {
166-
return (uri, policy) -> {
167-
LOG.severe(() -> "ERROR: FETCH: " + uri + " - policy POLICY_DENIED");
168-
throw new RemoteResolutionException(
169-
Objects.requireNonNull(uri, "uri"),
170-
RemoteResolutionException.Reason.POLICY_DENIED,
171-
"Remote fetching is disabled"
172-
);
211+
return new RemoteFetcher() {
212+
@Override
213+
public String scheme() {
214+
return "<disabled>";
215+
}
216+
217+
@Override
218+
public FetchResult fetch(java.net.URI uri, FetchPolicy policy) {
219+
LOG.severe(() -> "ERROR: FETCH: " + uri + " - policy POLICY_DENIED");
220+
throw new RemoteResolutionException(
221+
Objects.requireNonNull(uri, "uri"),
222+
RemoteResolutionException.Reason.POLICY_DENIED,
223+
"Remote fetching is disabled"
224+
);
225+
}
173226
};
174227
}
175228

@@ -432,59 +485,30 @@ static JsonValue fetchIfNeeded(java.net.URI docUri,
432485

433486
// MVF: Fetch remote document using RemoteFetcher from compile options
434487
LOG.finer(() -> "fetchIfNeeded: fetching remote document: " + docUri);
435-
try {
436-
// Get the base URI without fragment for document fetching
437-
String fragment = docUri.getFragment();
438-
java.net.URI docUriWithoutFragment = fragment != null ?
439-
java.net.URI.create(docUri.toString().substring(0, docUri.toString().indexOf('#'))) :
440-
docUri;
441-
442-
LOG.finest(() -> "fetchIfNeeded: document URI without fragment: " + docUriWithoutFragment);
443-
444-
// Enforce allowed schemes
445-
String scheme = docUriWithoutFragment.getScheme();
446-
if (scheme == null || !compileOptions.fetchPolicy().allowedSchemes().contains(scheme)) {
447-
throw new RemoteResolutionException(
448-
docUriWithoutFragment,
449-
RemoteResolutionException.Reason.POLICY_DENIED,
450-
"Scheme not allowed by policy: " + scheme
451-
);
452-
}
453-
454-
// Prefer a local file mapping for tests when using file:// URIs
455-
java.net.URI fetchUri = docUriWithoutFragment;
456-
if ("file".equalsIgnoreCase(scheme)) {
457-
String base = System.getProperty("json.schema.test.resources", "src/test/resources");
458-
String path = fetchUri.getPath();
459-
if (path != null && path.startsWith("/")) path = path.substring(1);
460-
java.nio.file.Path abs = java.nio.file.Paths.get(base, path).toAbsolutePath();
461-
java.net.URI alt = abs.toUri();
462-
fetchUri = alt;
463-
LOG.fine(() -> "fetchIfNeeded: Using file mapping for fetch: " + alt + " (original=" + docUriWithoutFragment + ")");
464-
}
465-
466-
// Fetch via provided RemoteFetcher to ensure consistent policy/normalization
467-
RemoteFetcher.FetchResult fetchResult;
468-
try {
469-
fetchResult = compileOptions.remoteFetcher().fetch(fetchUri, compileOptions.fetchPolicy());
470-
} catch (RemoteResolutionException e1) {
471-
// On mapping miss, retry original URI once
472-
if (!fetchUri.equals(docUriWithoutFragment)) {
473-
fetchResult = compileOptions.remoteFetcher().fetch(docUriWithoutFragment, compileOptions.fetchPolicy());
474-
} else {
475-
throw e1;
476-
}
477-
}
478-
JsonValue fetchedDocument = fetchResult.document();
479-
480-
LOG.finer(() -> "fetchIfNeeded: successfully fetched remote document: " + docUriWithoutFragment + ", document type: " + fetchedDocument.getClass().getSimpleName());
481-
return fetchedDocument;
482-
483-
} catch (Exception e) {
484-
// Network failures are logged by the fetcher; suppress here to avoid duplication
485-
throw new RemoteResolutionException(docUri, RemoteResolutionException.Reason.NETWORK_ERROR,
486-
"Failed to fetch remote document: " + docUri, e);
487-
}
488+
// Get the base URI without fragment for document fetching
489+
String fragment = docUri.getFragment();
490+
java.net.URI docUriWithoutFragment = fragment != null ?
491+
java.net.URI.create(docUri.toString().substring(0, docUri.toString().indexOf('#'))) :
492+
docUri;
493+
494+
LOG.finest(() -> "fetchIfNeeded: document URI without fragment: " + docUriWithoutFragment);
495+
496+
// Enforce allowed schemes
497+
String scheme = docUriWithoutFragment.getScheme();
498+
if (scheme == null || !compileOptions.fetchPolicy().allowedSchemes().contains(scheme)) {
499+
throw new RemoteResolutionException(
500+
docUriWithoutFragment,
501+
RemoteResolutionException.Reason.POLICY_DENIED,
502+
"Scheme not allowed by policy: " + scheme
503+
);
504+
}
505+
506+
RemoteFetcher.FetchResult fetchResult =
507+
compileOptions.remoteFetcher().fetch(docUriWithoutFragment, compileOptions.fetchPolicy());
508+
JsonValue fetchedDocument = fetchResult.document();
509+
510+
LOG.finer(() -> "fetchIfNeeded: successfully fetched remote document: " + docUriWithoutFragment + ", document type: " + fetchedDocument.getClass().getSimpleName());
511+
return fetchedDocument;
488512
}
489513

490514

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/RemoteResolutionException.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,28 @@
55
/// Exception signalling remote resolution failures with typed reasons
66
public final class RemoteResolutionException extends RuntimeException {
77
private final java.net.URI uri;
8+
private final Reason reason;
89

910
RemoteResolutionException(java.net.URI uri, Reason reason, String message) {
1011
super(message);
1112
this.uri = Objects.requireNonNull(uri, "uri");
12-
Objects.requireNonNull(reason, "reason");
13+
this.reason = Objects.requireNonNull(reason, "reason");
1314
}
1415

1516
RemoteResolutionException(java.net.URI uri, Reason reason, String message, Throwable cause) {
1617
super(message, cause);
1718
this.uri = Objects.requireNonNull(uri, "uri");
18-
Objects.requireNonNull(reason, "reason");
19+
this.reason = Objects.requireNonNull(reason, "reason");
1920
}
2021

2122
public java.net.URI uri() {
2223
return uri;
2324
}
2425

26+
public Reason reason() {
27+
return reason;
28+
}
29+
2530
enum Reason {
2631
NETWORK_ERROR,
2732
POLICY_DENIED,

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/SchemaCompiler.java

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -293,17 +293,6 @@ static JsonSchema.CompilationBundle compileBundle(JsonValue schemaJson, JsonSche
293293
);
294294
}
295295

296-
URI first = docUri;
297-
if ("file".equalsIgnoreCase(scheme)) {
298-
String base = System.getProperty("json.schema.test.resources", "src/test/resources");
299-
String path = docUri.getPath();
300-
if (path.startsWith("/")) path = path.substring(1);
301-
java.nio.file.Path abs = java.nio.file.Paths.get(base, path).toAbsolutePath();
302-
URI alt = abs.toUri();
303-
first = alt;
304-
LOG.fine(() -> "compileBundle: Using file mapping for fetch: " + alt + " (original=" + docUri + ")");
305-
}
306-
307296
// Enforce global document count before fetching
308297
if (session.fetchedDocs + 1 > compileOptions.fetchPolicy().maxDocuments()) {
309298
throw new RemoteResolutionException(
@@ -313,16 +302,8 @@ static JsonSchema.CompilationBundle compileBundle(JsonValue schemaJson, JsonSche
313302
);
314303
}
315304

316-
JsonSchema.RemoteFetcher.FetchResult fetchResult;
317-
try {
318-
fetchResult = compileOptions.remoteFetcher().fetch(first, compileOptions.fetchPolicy());
319-
} catch (RemoteResolutionException e1) {
320-
if (!first.equals(docUri)) {
321-
fetchResult = compileOptions.remoteFetcher().fetch(docUri, compileOptions.fetchPolicy());
322-
} else {
323-
throw e1;
324-
}
325-
}
305+
JsonSchema.RemoteFetcher.FetchResult fetchResult =
306+
compileOptions.remoteFetcher().fetch(docUri, compileOptions.fetchPolicy());
326307

327308
if (fetchResult.byteSize() > compileOptions.fetchPolicy().maxDocumentBytes()) {
328309
throw new RemoteResolutionException(
@@ -352,8 +333,8 @@ static JsonSchema.CompilationBundle compileBundle(JsonValue schemaJson, JsonSche
352333

353334
documentToCompile = fetchResult.document();
354335
final String normType = documentToCompile.getClass().getSimpleName();
355-
final URI normUri = first;
356-
LOG.fine(() -> "compileBundle: Successfully fetched document (normalized): " + normUri + ", document type: " + normType);
336+
final URI normUri = docUri;
337+
LOG.fine(() -> "compileBundle: Successfully fetched document: " + normUri + ", document type: " + normType);
357338
}
358339

359340
// Compile the schema

json-java21-schema/src/main/java/io/github/simbo1905/json/schema/VirtualThreadHttpFetcher.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,22 +33,32 @@ final class VirtualThreadHttpFetcher implements JsonSchema.RemoteFetcher {
3333
private final ConcurrentMap<URI, FetchResult> cache = new ConcurrentHashMap<>();
3434
private final AtomicInteger documentCount = new AtomicInteger();
3535
private final AtomicLong totalBytes = new AtomicLong();
36+
private final String scheme;
3637

37-
VirtualThreadHttpFetcher() {
38-
this(HttpClient.newBuilder().build());
39-
// Centralized network logging banner
40-
LOG.config(() -> "http.fetcher init redirectPolicy=default timeout=" + 0 + "ms");
38+
VirtualThreadHttpFetcher(String scheme) {
39+
this(scheme, HttpClient.newBuilder().build());
40+
LOG.config(() -> "http.fetcher init scheme=" + this.scheme);
4141
}
4242

43-
VirtualThreadHttpFetcher(HttpClient client) {
43+
VirtualThreadHttpFetcher(String scheme, HttpClient client) {
44+
this.scheme = Objects.requireNonNull(scheme, "scheme").toLowerCase(Locale.ROOT);
4445
this.client = client;
4546
}
4647

48+
@Override
49+
public String scheme() {
50+
return scheme;
51+
}
52+
4753
@Override
4854
public FetchResult fetch(URI uri, FetchPolicy policy) {
4955
Objects.requireNonNull(uri, "uri");
5056
Objects.requireNonNull(policy, "policy");
51-
ensureSchemeAllowed(uri, policy.allowedSchemes());
57+
String uriScheme = ensureSchemeAllowed(uri, policy.allowedSchemes());
58+
if (!scheme.equals(uriScheme)) {
59+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED,
60+
"Fetcher configured for scheme " + scheme + " but received " + uriScheme);
61+
}
5262

5363
FetchResult cached = cache.get(uri);
5464
if (cached != null) {
@@ -142,11 +152,12 @@ private FetchResult performFetch(URI uri, FetchPolicy policy) {
142152
}
143153
}
144154

145-
private void ensureSchemeAllowed(URI uri, Set<String> allowedSchemes) {
146-
String scheme = uri.getScheme();
147-
if (scheme == null || !allowedSchemes.contains(scheme.toLowerCase(Locale.ROOT))) {
148-
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED, "Disallowed scheme: " + scheme);
155+
private String ensureSchemeAllowed(URI uri, Set<String> allowedSchemes) {
156+
String uriScheme = uri.getScheme();
157+
if (uriScheme == null || !allowedSchemes.contains(uriScheme.toLowerCase(Locale.ROOT))) {
158+
throw new RemoteResolutionException(uri, RemoteResolutionException.Reason.POLICY_DENIED, "Disallowed scheme: " + uriScheme);
149159
}
160+
return uriScheme.toLowerCase(Locale.ROOT);
150161
}
151162

152163
private void enforceDocumentLimits(URI uri, FetchPolicy policy) {

0 commit comments

Comments
 (0)