package de.geolykt.starplane; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Properties; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; public class JarStripper { public static class MavenId { @NotNull private final String group, artifact, version; public MavenId(@NotNull String g, @NotNull String a, @NotNull String v) { this.group = g; this.artifact = a; this.version = v; } @Override public int hashCode() { return this.group.hashCode() ^ this.artifact.hashCode() ^ this.version.hashCode(); } @Override public boolean equals(Object obj) { if (obj instanceof MavenId) { MavenId other = (MavenId) obj; return this.group.equals(other.group) && this.artifact.equals(other.artifact) && this.version.equals(other.version); } return false; } @NotNull public String toGAVNotation() { return this.group + ":" + this.artifact + ":" + this.version; } } /** * Resolves the maven dependencies that are directly shaded into a jar. * This may include the jar itself. * This method is highly flawed by many aspects, especially because it relies on * files that are usually generated by maven to be present in the jar. * It also does not check whether the artifacts are actually included in the jar. * It may also include dependencies that are shaded in into another dependency that was ultimately * shaded in the jar. * * @param rawJarStream The {@link InputStream} which contains the jar's byte content. * @return The maven IDs that are directly shade-included in the jar. * @throws IOException If something unexpected occurs while reading the jar */ @NotNull @Contract(pure = true, mutates = "null -> fail; !null -> new") public Set getShadedDependencies(InputStream rawJarStream) throws IOException { Set out = new HashSet<>(); try (JarInputStream jarIn = new JarInputStream(rawJarStream, true)) { for (JarEntry entry = jarIn.getNextJarEntry(); entry != null; entry = jarIn.getNextJarEntry()) { if (!entry.getName().endsWith(".properties") || !entry.getName().startsWith("META-INF/maven")) { continue; } Properties properties = new Properties(); properties.load(jarIn); String groupId = properties.getProperty("groupId"); String artifactId = properties.getProperty("artifactId"); String version = properties.getProperty("version"); if (groupId == null || artifactId == null || version == null) { throw new IOException("File is incomplete: " + entry.getName()); } out.add(new MavenId(groupId, artifactId, version)); } } return out; } @NotNull public Set aggregate(@NotNull Path cache, @NotNull Set artifacts) throws IOException { Set blacklistedMembers = new HashSet<>(); blacklistedMembers.remove("META-INF/MANIFEST.MF"); for (MavenId dep : artifacts) { Path negativeCachePath = cache.resolve(dep.group.replace('.', '/')).resolve(dep.artifact).resolve(dep.artifact + "-" + dep.version + ".jar.error"); if (Files.exists(negativeCachePath)) { continue; } Path relativePath = Paths.get(dep.group.replace('.', '/')).resolve(dep.artifact).resolve(dep.artifact + "-" + dep.version + ".jar"); Path cachePath = cache.resolve(relativePath); if (Files.notExists(cachePath)) { Files.createDirectories(cachePath.getParent()); try { Files.copy(URI.create("https://repo1.maven.org/maven2/" + relativePath.toString()).toURL().openStream(), cachePath); } catch (IOException e) { Files.createFile(negativeCachePath); continue; } } try (ZipInputStream zipIn = new ZipInputStream(Files.newInputStream(cache))) { for (ZipEntry entry = zipIn.getNextEntry(); entry != null; entry = zipIn.getNextEntry()) { String name = entry.getName(); if (name.codePointAt(0) == '/') { name = name.substring(0); } blacklistedMembers.add(name); } } catch (IOException e) { throw new IOException("Cannot read dependency " + cache.toAbsolutePath().toString(), e); } } return blacklistedMembers; } public void createStrippedJar(@NotNull Path source, @NotNull Path target, @NotNull Collection removePaths) throws IOException { try (JarInputStream in = new JarInputStream(Files.newInputStream(source))) { try (OutputStream rawOut = Files.newOutputStream(target)) { Manifest man = in.getManifest(); try (JarOutputStream out = man == null ? new JarOutputStream(rawOut) : new JarOutputStream(rawOut, man)) { byte[] buffer = new byte[8096]; for (JarEntry entry = in.getNextJarEntry(); entry != null; entry = in.getNextJarEntry()) { if (removePaths.contains(entry.getName())) { continue; } out.putNextEntry(entry); for (int readBytes = in.read(buffer); readBytes != -1; readBytes = in.read(buffer)) { out.write(buffer, 0, readBytes); } } } } } } }