Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package software.amazon.smithy.build;

import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -37,6 +38,7 @@ public final class PluginContext implements ToSmithyBuilder<PluginContext> {
private final ClassLoader pluginClassLoader;
private final Set<Path> sources;
private final String artifactName;
private final List<String> sourceUris;
private Model nonTraitsModel;

private PluginContext(Builder builder) {
Expand All @@ -50,6 +52,15 @@ private PluginContext(Builder builder) {
settings = builder.settings;
pluginClassLoader = builder.pluginClassLoader;
sources = builder.sources.copy();
// Normalize the source paths into URI strings, which are URI encoded and use '/' separators,
// which is the format of paths in 'jar:file:/' filenames, so they can be compared in the
// 'isSource*' methods.
// This is done preemptively so we don't have to do the same work repeatedly, and the number
// of configured sources is typically very low.
sourceUris = new ArrayList<>(sources.size());
for (Path source : sources) {
sourceUris.add(source.toUri().toString());
}
}

/**
Expand Down Expand Up @@ -223,6 +234,21 @@ private boolean isSource(FromSourceLocation sourceLocation) {
String location = sourceLocation.getSourceLocation().getFilename();
int offsetFromStart = findOffsetFromStart(location);

if (offsetFromStart > 0) {
// Offset means the filename is a URI, so compare to the URI paths to account for encoding and windows.
for (String sourceUri : sourceUris) {
// Compare starting after the protocol (the source uri will always be "file", because we created it
// from a path).
int regionCompareLength = sourceUri.length() - 5;
if (location.regionMatches(offsetFromStart, sourceUri, 5, regionCompareLength)) {
return true;
}
}

return false;
}

// Filename is a regular path, so we can compare to sources directly.
for (Path path : sources) {
String pathString = path.toString();
int offsetFromStartInSource = findOffsetFromStart(pathString);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.build.MockManifest;
import software.amazon.smithy.build.PluginContext;
Expand All @@ -26,6 +33,19 @@
import software.amazon.smithy.utils.ListUtils;

public class SourcesPluginTest {

private Path tempDirectory;

@BeforeEach
public void before() throws IOException {
tempDirectory = Files.createTempDirectory(getClass().getSimpleName());
}

@AfterEach
public void after() throws IOException {
Files.walk(tempDirectory).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
}

@Test
public void copiesFilesForSourceProjection() throws URISyntaxException {
Model model = Model.assembler()
Expand Down Expand Up @@ -236,4 +256,69 @@ public void omitsUnsupportedFilesFromManifest() throws URISyntaxException {
assertThat(manifestString, containsString("a.smithy\n"));
assertThat(manifestString, not(containsString("foo.md")));
}

@Test
public void copiesModelFromJarWithEncodedPathWithSourceProjection()
throws IOException, URISyntaxException {
Path source = Paths.get(getClass().getResource("sources/jar-import.jar").toURI());
Path jarPath = tempDirectory.resolve(Paths.get("special%45path", "special.jar"));

Files.createDirectories(jarPath.getParent());
Files.copy(source, jarPath);

Model model = Model.assembler()
.addImport(jarPath)
.addImport(getClass().getResource("notsources/d.smithy"))
.assemble()
.unwrap();
MockManifest manifest = new MockManifest();
PluginContext context = PluginContext.builder()
.fileManifest(manifest)
.model(model)
.originalModel(model)
.sources(ListUtils.of(jarPath))
.build();
new SourcesPlugin().execute(context);
String manifestString = manifest.getFileString("manifest").get();

assertThat(manifestString, containsString("special/a.smithy\n"));
assertThat(manifestString, containsString("special/b/b.smithy\n"));
assertThat(manifestString, containsString("special/b/c/c.json\n"));
assertThat(manifestString, not(containsString("jar-import/d.json")));
assertThat(manifest.getFileString("special/a.smithy").get(), containsString("string A"));
assertThat(manifest.getFileString("special/b/b.smithy").get(), containsString("string B"));
assertThat(manifest.getFileString("special/b/c/c.json").get(), containsString("\"foo.baz#C\""));
}

@Test
public void copiesModelFromJarWithEncodedPathWithNonSourceProjection()
throws IOException, URISyntaxException {
Path source = Paths.get(getClass().getResource("sources/jar-import.jar").toURI());
Path jarPath = tempDirectory.resolve(Paths.get("special%45path", "special.jar"));

Files.createDirectories(jarPath.getParent());
Files.copy(source, jarPath);

Model model = Model.assembler()
.addImport(jarPath)
.assemble()
.unwrap();
MockManifest manifest = new MockManifest();
ProjectionConfig projection = ProjectionConfig.builder().build();
PluginContext context = PluginContext.builder()
.fileManifest(manifest)
.projection("foo", projection)
.model(model)
.originalModel(model)
.sources(ListUtils.of(jarPath))
.build();
new SourcesPlugin().execute(context);
String manifestString = manifest.getFileString("manifest").get();

assertThat(manifestString, containsString("model.json"));
assertThat(manifestString, not(containsString("special")));
assertThat(manifest.getFileString("model.json").get(), containsString("\"foo.baz#A\""));
assertThat(manifest.getFileString("model.json").get(), containsString("\"foo.baz#B\""));
assertThat(manifest.getFileString("model.json").get(), containsString("\"foo.baz#C\""));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -180,27 +182,36 @@ public static String getSmithyModelPathFromJarUrl(URL modelUrl) {
* to a file, (e.g., "/foo/baz.jar"), a file URL (e.g., "file:/baz.jar"),
* or a JAR URL (e.g., "jar:file:/baz.jar").
*
* <p>If {@code fileOrUrl} is a URL, it must be absolute and percent-encoded
* to ensure the manifest is read from the correct location.
*
* @param fileOrUrl Filename or URL that points to a JAR.
* @return Returns the computed URL.
*/
public static URL createSmithyJarManifestUrl(String fileOrUrl) {
try {
return new URL(getFilenameWithScheme(fileOrUrl) + "!/" + MANIFEST_PATH);
// URL expects callers to perform percent-encoding before construction,
// and for consumers to perform decoding. When given a URL-like string,
// (e.g. file:/foo.jar), we have to assume it is properly encoded because
// otherwise we risk double-encoding. For file paths, we have to encode
// to make sure that when the consumer of the URL performs decoding (i.e.
// to read from the file system), they will get back the original path.
String jarUrl;
if (fileOrUrl.startsWith("jar:")) {
jarUrl = fileOrUrl;
} else if (fileOrUrl.startsWith("file:")) {
jarUrl = "jar:" + fileOrUrl;
} else {
URI jarUri = Paths.get(fileOrUrl).toUri();
jarUrl = "jar:" + jarUri;
}

return new URL(jarUrl + "!/" + MANIFEST_PATH);
} catch (IOException e) {
throw new ModelImportException(e.getMessage(), e);
}
}

private static String getFilenameWithScheme(String filename) {
if (filename.startsWith("jar:")) {
return filename;
} else if (filename.startsWith("file:")) {
return "jar:" + filename;
} else {
return "jar:file:" + filename;
}
}

private static Set<String> parseManifest(URL location) throws IOException {
Set<String> models = new LinkedHashSet<>();
URLConnection connection = location.openConnection();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1459,4 +1459,57 @@ public void handlesSameModelWhenBuiltAndImported() throws Exception {
assertTrue(combinedModel.expectShape(ShapeId.from("smithy.example#MachineData$machineId"), MemberShape.class)
.hasTrait(RequiredTrait.ID));
}

@Test
public void importsJarFromPathWithEncodedChars() throws Exception {
Path source = Paths.get(getClass().getResource("assembler-valid-jar").toURI());
Path jarPath = outputDirectory.resolve(Paths.get("path%20with%45encoded%25chars", "test.jar"));

Files.createDirectories(jarPath.getParent());
Files.copy(JarUtils.createJarFromDir(source), jarPath);

Model model = Model.assembler()
.addImport(jarPath)
.assemble()
.unwrap();

assertTrue(model.getShape(ShapeId.from("smithy.example#ExampleString")).isPresent());
}

@Test
public void importsJarFromUrlWithEncodedChars() throws Exception {
Path source = Paths.get(getClass().getResource("assembler-valid-jar").toURI());
Path jarPath = outputDirectory.resolve(Paths.get("path%20with%45encoded%25chars", "test.jar"));

Files.createDirectories(jarPath.getParent());
Files.copy(JarUtils.createJarFromDir(source), jarPath);

Model model = Model.assembler()
.addImport(jarPath.toUri().toURL())
.assemble()
.unwrap();

assertTrue(model.getShape(ShapeId.from("smithy.example#ExampleString")).isPresent());
}

@Test
public void importedJarsWithEncodedCharsHaveCorrectFilename() throws Exception {
Path source = Paths.get(getClass().getResource("assembler-valid-jar").toURI());
Path jarPath = outputDirectory.resolve(Paths.get("path%20with%45encoded%25chars", "test.jar"));

Files.createDirectories(jarPath.getParent());
Files.copy(JarUtils.createJarFromDir(source), jarPath);

Model model = Model.assembler()
.addImport(jarPath)
.assemble()
.unwrap();

String filename = model.expectShape(ShapeId.from("smithy.example#ExampleString"))
.getSourceLocation()
.getFilename();
String fileContents = IoUtils.readUtf8Url(new URL(filename));

assertThat(fileContents, containsString("string ExampleString"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -132,8 +133,10 @@ public void requiresModelNameToBeValidWhenParsing() throws IOException {

@Test
public void createSmithyManifestUrlFromPath() throws IOException {
// Accounts for windows drive letters and path separators
String normalizedJarPath = Paths.get("/foo.jar").toUri().getPath();
assertThat(ModelDiscovery.createSmithyJarManifestUrl("/foo.jar"),
equalTo(new URL("jar:file:/foo.jar!/META-INF/smithy/manifest")));
equalTo(new URL("jar:file:" + normalizedJarPath + "!/META-INF/smithy/manifest")));
}

@Test
Expand All @@ -147,4 +150,19 @@ public void createSmithyManifestUrlFromJarUrl() throws IOException {
assertThat(ModelDiscovery.createSmithyJarManifestUrl("jar:file:/foo.jar"),
equalTo(new URL("jar:file:/foo.jar!/META-INF/smithy/manifest")));
}

@Test
public void encodesSmithyManifestUrlFromPath() throws IOException {
String normalizedAndEncodedJarPath = Paths.get("/foo bar.jar").toUri().getRawPath();
assertThat(ModelDiscovery.createSmithyJarManifestUrl("/foo bar.jar"),
equalTo(new URL("jar:file:" + normalizedAndEncodedJarPath + "!/META-INF/smithy/manifest")));
}

@Test
public void preservesEncodingOfSmithyManifestUrlFromUrl() throws IOException {
assertThat(ModelDiscovery.createSmithyJarManifestUrl("file:/foo%20bar.jar"),
equalTo(new URL("jar:file:/foo%20bar.jar!/META-INF/smithy/manifest")));
assertThat(ModelDiscovery.createSmithyJarManifestUrl("jar:file:/foo%20bar.jar"),
equalTo(new URL("jar:file:/foo%20bar.jar!/META-INF/smithy/manifest")));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Manifest-Version: 1.0
Created-By: 11.0.6 (Amazon.com Inc.)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
valid.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
$version: "2.0"

namespace smithy.example

string ExampleString
Loading