Skip to content

Commit 341ed01

Browse files
authored
Fix jar uri conversion (#233)
The language server has to convert to and from LSP's URIs and the Smithy model's source location filenames. The filenames used for files in jars are actually URIs in the form `jar:file/foo.jar!/bar.smithy` - obviously there isn't an actual file path to a file within a jar. Because LSP's URIs and these Jar URIs are URIs, they're percent-encoded. When we convert from a regular file URI -> filename, `Path.of(URI).toString()` makes sure the filename ends up properly decoded, regardless of how the client encoded the URI. However, when going from jar file URI -> jar file filename (as it appears in the model), we don't want to decode the URI (because the filename is encoded in the model). This should be easy, but since some clients encode the URI differently, the URI sent by the client might not be encoded the same way it is in the model filename. In particular, VSCode is quite aggresive in its encoding, and encodes the `!` in the jar URI. To handle this, we were decoding the LSP URI, but if the URI has other special characters, like spaces, those would also be decoded. I'm pretty sure this would always be an issue on windows too, since the `:` in `C:` would be encoded. The problem hasn't come up yet, because who puts special characters in file/directory names? However, when trying to autodownload our new standalone installations in the VSCode extension, I found that extensions' storage directories are under `/Application Support/` on Mac. So we need to fix this in order to autodownload the language server. To fix this, I updated the implementation of a few methods in LspAdapter. Most notable is the new `smithyJarUriToJarModelFilename` method which takes an LSP jar URI and turns it into a Java URI that can properly `toString()` into the model filename, or `toURL()` into a URL that can be used to read the contents of the file. The method has a comment that explains how it works - it's really a hack. We really shouldn't be using strings to represent URIs and paths at all, but at some point we still need to go back and forth between LSP URI and model filename, so I'm not sure if that would fix the problem, or just move it.
1 parent efe70d0 commit 341ed01

File tree

4 files changed

+113
-32
lines changed

4 files changed

+113
-32
lines changed

src/main/java/software/amazon/smithy/lsp/SmithyLanguageServer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ public CompletableFuture<String> jarFileContents(TextDocumentIdentifier textDocu
369369
return completedFuture(projectAndFile.file().document().copyText());
370370
} else {
371371
// Technically this can throw if the uri is invalid
372-
return completedFuture(IoUtils.readUtf8Url(LspAdapter.jarUrl(uri)));
372+
return completedFuture(IoUtils.readUtf8Url(LspAdapter.smithyJarUriToReadableUrl(uri)));
373373
}
374374
}
375375

src/main/java/software/amazon/smithy/lsp/project/ProjectLoader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ private static void findOrReadDocument(
211211
// the model stores jar paths as URIs
212212
if (LspAdapter.isSmithyJarFile(filePath) || LspAdapter.isJarFile(filePath)) {
213213
// Technically this can throw
214-
String text = IoUtils.readUtf8Url(LspAdapter.jarUrl(filePath));
214+
String text = IoUtils.readUtf8Url(LspAdapter.jarModelFilenameToReadableUrl(filePath));
215215
Document document = Document.of(text);
216216
consumer.accept(filePath, text, document);
217217
return;

src/main/java/software/amazon/smithy/lsp/protocol/LspAdapter.java

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55

66
package software.amazon.smithy.lsp.protocol;
77

8-
import java.io.IOException;
8+
import java.net.MalformedURLException;
99
import java.net.URI;
10+
import java.net.URISyntaxException;
1011
import java.net.URL;
11-
import java.net.URLDecoder;
12-
import java.nio.charset.StandardCharsets;
1312
import java.nio.file.Paths;
14-
import java.util.logging.Logger;
1513
import org.eclipse.lsp4j.Location;
1614
import org.eclipse.lsp4j.Position;
1715
import org.eclipse.lsp4j.Range;
@@ -28,8 +26,6 @@
2826
* 'smithyjar:' scheme we use.
2927
*/
3028
public final class LspAdapter {
31-
private static final Logger LOGGER = Logger.getLogger(LspAdapter.class.getName());
32-
3329
private LspAdapter() {
3430
}
3531

@@ -186,11 +182,11 @@ public static SourceLocation toSourceLocation(String path, Position position) {
186182
* @return A path representation of the {@code uri}, with the scheme removed
187183
*/
188184
public static String toPath(String uri) {
189-
if (uri.startsWith("file://")) {
185+
if (uri.startsWith("file:")) {
190186
return Paths.get(URI.create(uri)).toString();
191187
} else if (isSmithyJarFile(uri)) {
192-
String decoded = decode(uri);
193-
return fixJarScheme(decoded);
188+
URI jarUri = smithyJarUriToJarModelFilename(uri);
189+
return jarUri.toString();
194190
}
195191
return uri;
196192
}
@@ -231,36 +227,74 @@ public static boolean isJarFile(String uri) {
231227
}
232228

233229
/**
234-
* Get a {@link URL} for the Jar represented by the given URI or path.
230+
* Get a {@link URL} for the Jar represented by the given smithyjar URI.
235231
*
236-
* @param uriOrPath LSP URI or regular path
237-
* @return The {@link URL}, or throw if the uri/path cannot be decoded
232+
* @param uri The smithyjar URI
233+
* @return A URL which can be used to read the contents of the file
238234
*/
239-
public static URL jarUrl(String uriOrPath) {
235+
public static URL smithyJarUriToReadableUrl(String uri) {
240236
try {
241-
String decodedUri = decode(uriOrPath);
242-
return URI.create(fixJarScheme(decodedUri)).toURL();
243-
} catch (IOException e) {
237+
URI jarUri = smithyJarUriToJarModelFilename(uri);
238+
return jarUri.toURL();
239+
} catch (MalformedURLException e) {
244240
throw new RuntimeException(e);
245241
}
246242
}
247243

248-
private static String decode(String uriOrPath) {
249-
// Some clients encode parts of the jar, like !/
250-
return URLDecoder.decode(uriOrPath, StandardCharsets.UTF_8);
244+
/**
245+
* Get a {@link URL} for the jar file from the filename in the model's
246+
* source location.
247+
*
248+
* @param modelFilename The filename from the model's source location
249+
* @return A URL which can be used to read the contents of the file
250+
*/
251+
public static URL jarModelFilenameToReadableUrl(String modelFilename) {
252+
try {
253+
// No need to decode these, they're already encoded
254+
return URI.create(modelFilename).toURL();
255+
} catch (MalformedURLException e) {
256+
throw new RuntimeException(e);
257+
}
251258
}
252259

253-
private static String fixJarScheme(String uriOrPath) {
254-
if (uriOrPath.startsWith("smithyjar:")) {
255-
uriOrPath = uriOrPath.replaceFirst("smithyjar:", "");
256-
}
257-
if (uriOrPath.startsWith("jar:")) {
258-
return uriOrPath;
259-
} else if (uriOrPath.startsWith("file:")) {
260-
return "jar:" + uriOrPath;
261-
} else {
262-
return "jar:file:" + uriOrPath;
260+
/**
261+
* Converts a smithyjar uri that was sent from the client into a URI that
262+
* is equivalent to what appears in the Smithy model.
263+
*
264+
* @param smithyJarUri smithyjar uri received from the client
265+
* @return The converted URI
266+
*/
267+
private static URI smithyJarUriToJarModelFilename(String smithyJarUri) {
268+
// Clients encode URIs differently. VSCode is particularly aggressive with
269+
// its encoding, so the URIs it produces aren't equivalent to what we get
270+
// from source locations in the model.
271+
//
272+
// For example, given a jar that lives in some directory with special characters:
273+
// /path with spaces/foo.jar
274+
// The model will have a source location with a filename like:
275+
// jar:file:/path%20with%20spaces/foo.jar!/baz.smithy
276+
// When sending requests/notifications for this file, VSCode will encode the URI like:
277+
// smithyjar:/path%20with%20spaces/foo.jar%21/baz.smithy
278+
// Note the ! is encoded.
279+
//
280+
// If we just used URI.create().toString(), we will end up with the exact same
281+
// URI that VSCode sent, because URI.create() (and its equivalent ctor) keep
282+
// the original input string to use for the toString() call.
283+
//
284+
// Instead, we use getSchemeSpecificPart() to fully decode everything after the
285+
// smithyjar: part, to get:
286+
// /path with spaces/foo.jar!/baz.smithy
287+
// Then, we reconstruct the URI from parts, using a different ctor that performs
288+
// encoding. The resulting URI.toString() call will give us what we want:
289+
// jar:file:/path%20with%20spaces/foo.jar!/baz.smithy
290+
291+
URI encodedUri = URI.create(smithyJarUri);
292+
String decodedPath = encodedUri.getSchemeSpecificPart();
293+
294+
try {
295+
return new URI("jar", "file:" + decodedPath, null);
296+
} catch (URISyntaxException e) {
297+
throw new RuntimeException(e);
263298
}
264299
}
265-
266300
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.lsp.protocol;
6+
7+
import static org.hamcrest.MatcherAssert.assertThat;
8+
import static org.hamcrest.Matchers.equalTo;
9+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
public class LspAdapterTest {
14+
@Test
15+
public void jarModelFilenameRoundTrip() {
16+
String jarModelFilename = "jar:file:/path%20with%20spaces/foo.jar!/bar.smithy";
17+
String jarUri = LspAdapter.toUri(jarModelFilename);
18+
19+
assertThat(jarUri, equalTo("smithyjar:/path%20with%20spaces/foo.jar!/bar.smithy"));
20+
assertThat(LspAdapter.toPath(jarUri), equalTo(jarModelFilename));
21+
}
22+
23+
@Test
24+
public void smithyjarRoundTrip() {
25+
String jarUri = "smithyjar:/path%20with%20spaces/foo.jar!/bar.smithy";
26+
String jarModelFilename = LspAdapter.toPath(jarUri);
27+
28+
assertThat(jarModelFilename, equalTo("jar:file:/path%20with%20spaces/foo.jar!/bar.smithy"));
29+
assertThat(LspAdapter.toUri(jarModelFilename), equalTo(jarUri));
30+
}
31+
32+
@Test
33+
public void aggressivelyEncodedSmithyjarRoundTrip() {
34+
String encodedJarUri = "smithyjar:/path%20with%20spaces/foo.jar%21/bar.smithy";
35+
String jarModelFilename = LspAdapter.toPath(encodedJarUri);
36+
37+
assertThat(jarModelFilename, equalTo("jar:file:/path%20with%20spaces/foo.jar!/bar.smithy"));
38+
assertThat(LspAdapter.toUri(jarModelFilename), equalTo("smithyjar:/path%20with%20spaces/foo.jar!/bar.smithy"));
39+
}
40+
41+
@Test
42+
public void aggressivelyEncodedSmithyJarToUrl() {
43+
String encodedJarUri = "smithyjar:/path%20with%20spaces/foo.jar%21/bar.smithy";
44+
45+
assertDoesNotThrow(() -> LspAdapter.smithyJarUriToReadableUrl(encodedJarUri));
46+
}
47+
}

0 commit comments

Comments
 (0)