Skip to content

Commit 10a0bdd

Browse files
committed
URL -> URI
Java 21 doesn't like our use of URL, so move to URI everywhere for future compatibility. Signed-off-by: Appu Goundan <[email protected]>
1 parent c125f83 commit 10a0bdd

File tree

9 files changed

+206
-53
lines changed

9 files changed

+206
-53
lines changed

sigstore-cli/src/main/java/dev/sigstore/cli/Sign.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
import dev.sigstore.oidc.client.TokenStringOidcClient;
2222
import dev.sigstore.tuf.RootProvider;
2323
import dev.sigstore.tuf.SigstoreTufClient;
24-
import java.net.URL;
24+
import java.net.URI;
2525
import java.nio.charset.StandardCharsets;
2626
import java.nio.file.Files;
2727
import java.nio.file.Path;
@@ -88,7 +88,7 @@ public Integer call() throws Exception {
8888
SigstoreTufClient.builder()
8989
.usePublicGoodInstance()
9090
.tufMirror(
91-
new URL(target.publicGoodWithTufUrlOverride),
91+
URI.create(target.publicGoodWithTufUrlOverride),
9292
RootProvider.fromResource(SigstoreTufClient.PUBLIC_GOOD_ROOT_RESOURCE));
9393
signerBuilder =
9494
KeylessSigner.builder()
@@ -99,7 +99,7 @@ public Integer call() throws Exception {
9999
SigstoreTufClient.builder()
100100
.useStagingInstance()
101101
.tufMirror(
102-
new URL(target.stagingWithTufUrlOverride),
102+
URI.create(target.stagingWithTufUrlOverride),
103103
RootProvider.fromResource(SigstoreTufClient.STAGING_ROOT_RESOURCE));
104104
signerBuilder =
105105
KeylessSigner.builder()

sigstore-cli/src/main/java/dev/sigstore/cli/Verify.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
import dev.sigstore.strings.StringMatcher;
2727
import dev.sigstore.tuf.RootProvider;
2828
import dev.sigstore.tuf.SigstoreTufClient;
29-
import java.net.URL;
29+
import java.net.URI;
3030
import java.nio.charset.StandardCharsets;
3131
import java.nio.file.Path;
3232
import java.util.concurrent.Callable;
@@ -141,7 +141,7 @@ public Integer call() throws Exception {
141141
SigstoreTufClient.builder()
142142
.usePublicGoodInstance()
143143
.tufMirror(
144-
new URL(target.publicGoodWithTufUrlOverride),
144+
URI.create(target.publicGoodWithTufUrlOverride),
145145
RootProvider.fromResource(SigstoreTufClient.PUBLIC_GOOD_ROOT_RESOURCE));
146146
verifier =
147147
KeylessVerifier.builder()
@@ -152,7 +152,7 @@ public Integer call() throws Exception {
152152
SigstoreTufClient.builder()
153153
.useStagingInstance()
154154
.tufMirror(
155-
new URL(target.stagingWithTufUrlOverride),
155+
URI.create(target.stagingWithTufUrlOverride),
156156
RootProvider.fromResource(SigstoreTufClient.STAGING_ROOT_RESOURCE));
157157
verifier =
158158
KeylessVerifier.builder()
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.http;
17+
18+
import java.net.URI;
19+
import java.net.URISyntaxException;
20+
21+
/**
22+
* A utility class for formatting URIs, providing predictable path appending.
23+
*
24+
* <p>This is preferable to {@link java.net.URI#resolve(String)} for simple path appending, as it
25+
* avoids {@code resolve()}'s specific handling of base paths without trailing slashes and appended
26+
* paths with leading slashes.
27+
*/
28+
public final class URIFormat {
29+
30+
private URIFormat() {}
31+
32+
/**
33+
* Ensures the given URI's path has a trailing slash. This method correctly handles URIs with
34+
* query parameters and fragments.
35+
*
36+
* @param input the URI to check.
37+
* @return a new URI with a trailing slash, or the original URI if it already had one.
38+
*/
39+
public static URI addTrailingSlash(URI input) {
40+
String path = input.getPath();
41+
if (path == null || path.isEmpty()) {
42+
path = "";
43+
} else if (path.endsWith("/")) {
44+
return input;
45+
}
46+
try {
47+
return new URI(
48+
input.getScheme(),
49+
input.getAuthority(),
50+
path + "/",
51+
input.getQuery(),
52+
input.getFragment());
53+
} catch (URISyntaxException e) {
54+
// This should be unreachable with a valid input URI
55+
throw new IllegalStateException("Could not append slash to invalid URI: " + input, e);
56+
}
57+
}
58+
59+
/**
60+
* Appends a path segment to a base URI, ensuring exactly one slash separates them. This method
61+
* will erase any query parameters or fragments
62+
*
63+
* @param base the base URI (e.g., "http://example.com/api?key=1").
64+
* @param path the path segment to append (e.g., "users" or "/users").
65+
* @return a new URI with the path appended (e.g., "http://example.com/api/users").
66+
*/
67+
public static URI appendPath(URI base, String path) {
68+
String relativePath = path.replaceAll("^/+", "");
69+
70+
// resolve has some goofy behavior unless we normalize everything before applying
71+
return addTrailingSlash(base).resolve(relativePath);
72+
}
73+
}

sigstore-java/src/main/java/dev/sigstore/tuf/HttpFetcher.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,27 @@
2020
import com.google.api.client.json.gson.GsonFactory;
2121
import dev.sigstore.http.HttpClients;
2222
import dev.sigstore.http.HttpParams;
23+
import dev.sigstore.http.URIFormat;
2324
import java.io.IOException;
24-
import java.net.URL;
25+
import java.net.URI;
2526
import java.util.Locale;
2627

2728
public class HttpFetcher implements Fetcher {
2829

29-
private final URL mirror;
30+
private final URI mirror;
3031
private final HttpRequestFactory requestFactory;
3132

32-
private HttpFetcher(URL mirror, HttpRequestFactory requestFactory) {
33+
private HttpFetcher(URI mirror, HttpRequestFactory requestFactory) {
3334
this.mirror = mirror;
3435
this.requestFactory = requestFactory;
3536
}
3637

37-
public static HttpFetcher newFetcher(URL mirror) throws IOException {
38+
public static HttpFetcher newFetcher(URI mirror) throws IOException {
3839
var requestFactory =
3940
HttpClients.newRequestFactory(
4041
HttpParams.builder().build(),
4142
GsonFactory.getDefaultInstance().createJsonObjectParser());
42-
if (mirror.toString().endsWith("/")) {
43-
return new HttpFetcher(mirror, requestFactory);
44-
}
45-
return new HttpFetcher(new URL(mirror.toExternalForm() + "/"), requestFactory);
43+
return new HttpFetcher(URIFormat.addTrailingSlash(mirror), requestFactory);
4644
}
4745

4846
@Override
@@ -53,7 +51,7 @@ public String getSource() {
5351
@Override
5452
public byte[] fetchResource(String filename, int maxLength)
5553
throws IOException, FileExceedsMaxLengthException {
56-
GenericUrl fileUrl = new GenericUrl(mirror + filename);
54+
GenericUrl fileUrl = new GenericUrl(URIFormat.appendPath(mirror, filename));
5755
var req = requestFactory.buildGetRequest(fileUrl);
5856
req.getHeaders().setAccept("application/json; api-version=2.0");
5957
req.getHeaders().setContentType("application/json");

sigstore-java/src/main/java/dev/sigstore/tuf/SigstoreTufClient.java

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@
1717

1818
import com.google.common.annotations.VisibleForTesting;
1919
import com.google.common.base.Preconditions;
20+
import dev.sigstore.http.URIFormat;
2021
import dev.sigstore.trustroot.SigstoreConfigurationException;
2122
import dev.sigstore.trustroot.SigstoreSigningConfig;
2223
import dev.sigstore.trustroot.SigstoreTrustedRoot;
2324
import java.io.IOException;
24-
import java.net.MalformedURLException;
25-
import java.net.URL;
25+
import java.net.URI;
2626
import java.nio.file.Files;
2727
import java.nio.file.Path;
2828
import java.security.InvalidKeyException;
@@ -65,21 +65,17 @@ public static class Builder {
6565
Path tufCacheLocation =
6666
Path.of(System.getProperty("user.home")).resolve(".sigstore-java").resolve("root");
6767

68-
private URL remoteMirror;
68+
private URI remoteMirror;
6969
private RootProvider trustedRoot;
7070

7171
public Builder usePublicGoodInstance() {
7272
if (remoteMirror != null || trustedRoot != null) {
7373
throw new IllegalStateException(
7474
"Using public good after configuring remoteMirror and trustedRoot");
7575
}
76-
try {
77-
tufMirror(
78-
new URL("https://tuf-repo-cdn.sigstore.dev/"),
79-
RootProvider.fromResource(PUBLIC_GOOD_ROOT_RESOURCE));
80-
} catch (MalformedURLException e) {
81-
throw new AssertionError(e);
82-
}
76+
tufMirror(
77+
URI.create("https://tuf-repo-cdn.sigstore.dev/"),
78+
RootProvider.fromResource(PUBLIC_GOOD_ROOT_RESOURCE));
8379
return this;
8480
}
8581

@@ -88,13 +84,9 @@ public Builder useStagingInstance() {
8884
throw new IllegalStateException(
8985
"Using staging after configuring remoteMirror and trustedRoot");
9086
}
91-
try {
92-
tufMirror(
93-
new URL("https://tuf-repo-cdn.sigstage.dev"),
94-
RootProvider.fromResource(STAGING_ROOT_RESOURCE));
95-
} catch (MalformedURLException e) {
96-
throw new AssertionError(e);
97-
}
87+
tufMirror(
88+
URI.create("https://tuf-repo-cdn.sigstage.dev"),
89+
RootProvider.fromResource(STAGING_ROOT_RESOURCE));
9890
tufCacheLocation =
9991
Path.of(System.getProperty("user.home"))
10092
.resolve(".sigstore-java")
@@ -103,7 +95,7 @@ public Builder useStagingInstance() {
10395
return this;
10496
}
10597

106-
public Builder tufMirror(URL mirror, RootProvider trustedRoot) {
98+
public Builder tufMirror(URI mirror, RootProvider trustedRoot) {
10799
this.remoteMirror = mirror;
108100
this.trustedRoot = trustedRoot;
109101
return this;
@@ -126,11 +118,7 @@ public SigstoreTufClient build() throws IOException {
126118
if (!Files.isDirectory(tufCacheLocation)) {
127119
Files.createDirectories(tufCacheLocation);
128120
}
129-
var normalizedRemoteMirror =
130-
remoteMirror.toString().endsWith("/")
131-
? remoteMirror
132-
: new URL(remoteMirror.toExternalForm() + "/");
133-
var remoteTargetsLocation = new URL(normalizedRemoteMirror.toExternalForm() + "targets");
121+
var remoteTargetsLocation = URIFormat.appendPath(remoteMirror, "targets");
134122
var filesystemTufStore = FileSystemTufStore.newFileSystemStore(tufCacheLocation);
135123
var tufUpdater =
136124
Updater.builder()
@@ -139,8 +127,7 @@ public SigstoreTufClient build() throws IOException {
139127
TrustedMetaStore.newTrustedMetaStore(
140128
PassthroughCacheMetaStore.newPassthroughMetaCache(filesystemTufStore)))
141129
.setTargetStore(filesystemTufStore)
142-
.setMetaFetcher(
143-
MetaFetcher.newFetcher(HttpFetcher.newFetcher(normalizedRemoteMirror)))
130+
.setMetaFetcher(MetaFetcher.newFetcher(HttpFetcher.newFetcher(remoteMirror)))
144131
.setTargetFetcher(HttpFetcher.newFetcher(remoteTargetsLocation))
145132
.build();
146133
return new SigstoreTufClient(tufUpdater, cacheValidity);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2025 The Sigstore Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package dev.sigstore.http;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
20+
import java.net.URI;
21+
import java.util.stream.Stream;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
26+
class URIFormatTest {
27+
28+
// Data provider for addTrailingSlash tests
29+
static Stream<Arguments> addTrailingSlashCases() {
30+
return Stream.of(
31+
Arguments.of(
32+
URI.create("https://example.com/path/"), URI.create("https://example.com/path/")),
33+
Arguments.of(
34+
URI.create("https://example.com/path"), URI.create("https://example.com/path/")),
35+
Arguments.of(URI.create("https://example.com"), URI.create("https://example.com/")),
36+
Arguments.of(
37+
URI.create("https://example.com/path?query=1"),
38+
URI.create("https://example.com/path/?query=1")),
39+
Arguments.of(
40+
URI.create("https://example.com/path#fragment"),
41+
URI.create("https://example.com/path/#fragment")),
42+
Arguments.of(
43+
URI.create("https://example.com/path?query=1#fragment"),
44+
URI.create("https://example.com/path/?query=1#fragment")));
45+
}
46+
47+
// Data provider for appendPath tests
48+
static Stream<Arguments> appendPathCases() {
49+
return Stream.of(
50+
Arguments.of(
51+
URI.create("https://example.com/api/"),
52+
"users",
53+
URI.create("https://example.com/api/users")),
54+
Arguments.of(
55+
URI.create("https://example.com/api"),
56+
"users",
57+
URI.create("https://example.com/api/users")),
58+
Arguments.of(
59+
URI.create("https://example.com/api/"),
60+
"/users",
61+
URI.create("https://example.com/api/users")),
62+
Arguments.of(
63+
URI.create("https://example.com/api"),
64+
"///users",
65+
URI.create("https://example.com/api/users")),
66+
Arguments.of(
67+
URI.create("https://example.com/api?key=123"),
68+
"users",
69+
URI.create("https://example.com/api/users")),
70+
Arguments.of(
71+
URI.create("https://example.com/api?key=123#section"),
72+
"users",
73+
URI.create("https://example.com/api/users")),
74+
Arguments.of(
75+
URI.create("https://example.com"),
76+
"/users/get",
77+
URI.create("https://example.com/users/get")));
78+
}
79+
80+
@ParameterizedTest
81+
@MethodSource("addTrailingSlashCases")
82+
void addTrailingSlash(URI input, URI expected) {
83+
URI result = URIFormat.addTrailingSlash(input);
84+
assertEquals(expected, result);
85+
}
86+
87+
@ParameterizedTest
88+
@MethodSource("appendPathCases")
89+
void appendPath(URI base, String path, URI expected) {
90+
URI result = URIFormat.appendPath(base, path);
91+
assertEquals(expected, result);
92+
}
93+
}

sigstore-java/src/test/java/dev/sigstore/tuf/HttpFetcherTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616
package dev.sigstore.tuf;
1717

18-
import java.net.URL;
18+
import java.io.IOException;
19+
import java.net.URI;
1920
import org.junit.jupiter.api.Assertions;
2021
import org.junit.jupiter.params.ParameterizedTest;
2122
import org.junit.jupiter.params.provider.CsvSource;
@@ -24,8 +25,8 @@ class HttpFetcherTest {
2425

2526
@ParameterizedTest
2627
@CsvSource({"http://example.com", "http://example.com/"})
27-
public void newFetcher_urlNoTrailingSlash(String url) throws Exception {
28-
var fetcher = HttpFetcher.newFetcher(new URL(url));
28+
public void newFetcher_urlNoTrailingSlash(String url) throws IOException {
29+
var fetcher = HttpFetcher.newFetcher(URI.create(url));
2930
Assertions.assertEquals("http://example.com/", fetcher.getSource());
3031
}
3132
}

0 commit comments

Comments
 (0)