package com.esotericsoftware.scar; import static com.esotericsoftware.minlog.Log.*; import static com.esotericsoftware.scar.Scar.*; import com.esotericsoftware.wildcard.Paths; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStreamWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashSet; import java.util.List; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.Deflater; import java.util.zip.ZipEntry; public class Jar { static private final String manifestFileName = "META-INF" + File.separator + "MANIFEST.MF"; static public void jar (String outputFile, String inputDir) throws IOException { jar(outputFile, new Paths(inputDir), null, null); } static public void jar (String outputFile, String inputDir, String mainClass, Paths classpath) throws IOException { jar(outputFile, new Paths(inputDir), mainClass, classpath); } static public void jar (String outputFile, Paths inputPaths) throws IOException { jar(outputFile, inputPaths, null, null); } /** @param mainClass May be null. * @param classpath May be null if mainClass is null. */ static public void jar (String outputFile, Paths inputPaths, String mainClass, Paths classpath) throws IOException { if (outputFile == null) throw new IllegalArgumentException("jarFile cannot be null."); if (inputPaths == null) throw new IllegalArgumentException("inputPaths cannot be null."); inputPaths = inputPaths.filesOnly(); if (inputPaths.isEmpty()) { if (WARN) warn("scar", "No files to JAR."); return; } List<String> fullPaths = inputPaths.getPaths(); List<String> relativePaths = inputPaths.getRelativePaths(); int manifestIndex = relativePaths.indexOf(manifestFileName); if (manifestIndex > 0) { // Ensure MANIFEST.MF is first. relativePaths.remove(manifestIndex); relativePaths.add(0, manifestFileName); String manifestFullPath = fullPaths.get(manifestIndex); fullPaths.remove(manifestIndex); fullPaths.add(0, manifestFullPath); } else if (mainClass != null) { if (DEBUG) debug("scar", "Generating JAR manifest."); String manifestFile = tempFile("manifest"); relativePaths.add(0, manifestFileName); fullPaths.add(0, manifestFile); Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); attributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0"); if (DEBUG) debug("scar", "Main class: " + mainClass); attributes.putValue(Attributes.Name.MAIN_CLASS.toString(), mainClass); StringBuilder buffer = new StringBuilder(512); buffer.append(fileName(outputFile)); buffer.append(" ."); for (String name : classpath.getRelativePaths()) { buffer.append(' '); buffer.append(name); } attributes.putValue(Attributes.Name.CLASS_PATH.toString(), buffer.toString()); FileOutputStream output = new FileOutputStream(manifestFile); try { manifest.write(output); } finally { try { output.close(); } catch (Exception ignored) { } } } if (DEBUG) debug("scar", "Creating JAR (" + inputPaths.count() + " entries): " + outputFile); mkdir(new File(outputFile).getParent()); JarOutputStream output = new JarOutputStream(new FileOutputStream(outputFile)); output.setLevel(Deflater.BEST_COMPRESSION); try { for (int i = 0, n = fullPaths.size(); i < n; i++) { JarEntry jarEntry = new JarEntry(relativePaths.get(i).replace('\\', '/')); output.putNextEntry(jarEntry); FileInputStream input = new FileInputStream(fullPaths.get(i)); try { byte[] buffer = new byte[4096]; while (true) { int length = input.read(buffer); if (length == -1) break; output.write(buffer, 0, length); } } finally { try { input.close(); } catch (Exception ignored) { } } } } finally { try { output.close(); } catch (Exception ignored) { } } } static public void oneJAR (String inputDir, String outputFile, String mainClass, Paths classpath) throws IOException { oneJAR(paths(inputDir, "*.jar"), outputFile, mainClass, classpath); } static public void oneJAR (Paths jars, String outputFile, String mainClass, Paths classpath) throws IOException { if (jars == null) throw new IllegalArgumentException("jars cannot be null."); String tempDir = tempDirectory("oneJAR"); ArrayList<String> processedJARs = new ArrayList(); for (String jarFile : jars) { unzip(jarFile, tempDir); processedJARs.add(jarFile); } if (mainClass != null) new File(tempDir, manifestFileName).delete(); mkdir(parent(outputFile)); jar(outputFile, tempDir, mainClass, classpath); delete(tempDir); } /** Removes any code signatures on the specified JAR. Removes any signature files in the META-INF directory and removes any * signature entries from the JAR's manifest. * @return The path to the JAR file. */ static public String unsign (String jarFile) throws IOException { if (jarFile == null) throw new IllegalArgumentException("jarFile cannot be null."); if (DEBUG) debug("scar", "Removing signature from JAR: " + jarFile); File tempFile = File.createTempFile("scar", "removejarsig"); JarOutputStream jarOutput = null; JarInputStream jarInput = null; try { jarOutput = new JarOutputStream(new FileOutputStream(tempFile)); jarOutput.setLevel(Deflater.BEST_COMPRESSION); jarInput = new JarInputStream(new FileInputStream(jarFile)); Manifest manifest = jarInput.getManifest(); if (manifest != null) { // Remove manifest file entries. manifest.getEntries().clear(); jarOutput.putNextEntry(new JarEntry(manifestFileName)); manifest.write(jarOutput); } byte[] buffer = new byte[4096]; while (true) { JarEntry entry = jarInput.getNextJarEntry(); if (entry == null) break; String name = entry.getName(); // Skip signature files. if (name.startsWith("META-INF") && (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) continue; jarOutput.putNextEntry(new JarEntry(name)); while (true) { int length = jarInput.read(buffer); if (length == -1) break; jarOutput.write(buffer, 0, length); } } jarInput.close(); jarOutput.close(); copyFile(tempFile.getAbsolutePath(), jarFile); } catch (IOException ex) { throw new IOException("Error unsigning JAR file: " + jarFile, ex); } finally { try { if (jarInput != null) jarInput.close(); } catch (Exception ignored) { } try { if (jarOutput != null) jarOutput.close(); } catch (Exception ignored) { } tempFile.delete(); } return jarFile; } /** Creates a new keystore for signing JARs. If the keystore file already exists, no action will be taken. * @return The path to the keystore file. */ static public String keystore (String keystoreFile, String alias, String password, String company, String title) throws IOException { if (keystoreFile == null) throw new IllegalArgumentException("keystoreFile cannot be null."); if (fileExists(keystoreFile)) return keystoreFile; if (alias == null) throw new IllegalArgumentException("alias cannot be null."); if (password == null) throw new IllegalArgumentException("password cannot be null."); if (password.length() < 6) throw new IllegalArgumentException("password must be 6 or more characters."); if (company == null) throw new IllegalArgumentException("company cannot be null."); if (title == null) throw new IllegalArgumentException("title cannot be null."); if (DEBUG) debug("scar", "Creating keystore (" + alias + ":" + password + ", " + company + ", " + title + "): " + keystoreFile); File file = new File(keystoreFile); file.delete(); Process process = Runtime.getRuntime() .exec(new String[] {resolvePath("keytool"), "-genkey", "-keystore", keystoreFile, "-alias", alias}); OutputStreamWriter writer = new OutputStreamWriter(process.getOutputStream()); writer.write(password + "\n"); // Enter keystore password: writer.write(password + "\n"); // Re-enter new password: writer.write(company + "\n"); // What is your first and last name? writer.write(title + "\n"); // What is the name of your organizational unit? writer.write(title + "\n"); // What is the name of your organization? writer.write("\n"); // What is the name of your City or Locality? [Unknown] writer.write("\n"); // What is the name of your State or Province? [Unknown] writer.write("\n"); // What is the two-letter country code for this unit? [Unknown] writer.write("yes\n"); // Correct? writer.write("\n"); // Return if same alias key password as keystore. writer.flush(); process.getOutputStream().close(); process.getInputStream().close(); process.getErrorStream().close(); try { process.waitFor(); } catch (InterruptedException ignored) { } if (!file.exists()) throw new RuntimeException("Error creating keystore."); return keystoreFile; } /** Signs the specified JAR. * @return The path to the JAR. */ static public String sign (String jarFile, String keystoreFile, String alias, String password) throws IOException { if (jarFile == null) throw new IllegalArgumentException("jarFile cannot be null."); if (keystoreFile == null) throw new IllegalArgumentException("keystoreFile cannot be null."); if (alias == null) throw new IllegalArgumentException("alias cannot be null."); if (password == null) throw new IllegalArgumentException("password cannot be null."); if (password.length() < 6) throw new IllegalArgumentException("password must be 6 or more characters."); if (DEBUG) debug("scar", "Signing JAR (" + keystoreFile + ", " + alias + ":" + password + "): " + jarFile); shell("jarsigner", "-keystore", keystoreFile, "-storepass", password, "-keypass", password, jarFile, alias); return jarFile; } /** Encodes the specified file with pack200. The resulting filename is the filename plus ".pack". The file is deleted after * encoding. * @return The path to the encoded file. */ static public String pack200 (String jarFile) throws IOException { String packedFile = pack200(jarFile, jarFile + ".pack"); delete(jarFile); return packedFile; } /** Encodes the specified file with pack200. * @return The path to the encoded file. */ static public String pack200 (String jarFile, String packedFile) throws IOException { if (jarFile == null) throw new IllegalArgumentException("jarFile cannot be null."); if (packedFile == null) throw new IllegalArgumentException("packedFile cannot be null."); if (DEBUG) debug("scar", "Packing JAR: " + jarFile + " -> " + packedFile); shell("pack200", "--no-gzip", "--segment-limit=-1", "--no-keep-file-order", "--effort=7", "--modification-time=latest", packedFile, jarFile); return packedFile; } /** Decodes the specified file with pack200. The filename must end in ".pack" and the resulting filename has this stripped. The * encoded file is deleted after decoding. * @return The path to the decoded file. */ static public String unpack200 (String packedFile) throws IOException { if (packedFile == null) throw new IllegalArgumentException("packedFile cannot be null."); if (!packedFile.endsWith(".pack")) throw new IllegalArgumentException("packedFile must end with .pack: " + packedFile); String jarFile = unpack200(packedFile, substring(packedFile, 0, -5)); delete(packedFile); return jarFile; } /** Decodes the specified file with pack200. * @return The path to the decoded file. */ static public String unpack200 (String packedFile, String jarFile) throws IOException { if (packedFile == null) throw new IllegalArgumentException("packedFile cannot be null."); if (jarFile == null) throw new IllegalArgumentException("jarFile cannot be null."); if (DEBUG) debug("scar", "Unpacking JAR: " + packedFile + " -> " + jarFile); shell("unpack200", packedFile, jarFile); return jarFile; } static public ArrayList<String> entryNames (String jar) throws IOException { JarFile jarFile = new JarFile(jar); ArrayList<String> names = new ArrayList(); for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) names.add(entries.nextElement().getName().replace('\\', '/')); jarFile.close(); return names; } /** Combines the JARs into one. If both JARs have the same entry, the entry from the first JAR is used. */ static public void mergeJars (String firstJar, String secondJar, String outJar) throws IOException { if (DEBUG) debug("scar", "Merging JARs: " + firstJar + " + " + secondJar + " -> " + outJar); JarFile firstJarFile = new JarFile(firstJar); JarFile secondJarFile = new JarFile(secondJar); HashSet<String> names = new HashSet(); for (Enumeration<JarEntry> entries = firstJarFile.entries(); entries.hasMoreElements();) names.add(entries.nextElement().getName().replace('\\', '/')); for (Enumeration<JarEntry> entries = secondJarFile.entries(); entries.hasMoreElements();) names.add(entries.nextElement().getName().replace('\\', '/')); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); for (String name : names) { InputStream input; ZipEntry entry = firstJarFile.getEntry(name); if (entry != null) input = firstJarFile.getInputStream(entry); else { entry = firstJarFile.getEntry(name.replace('/', '\\')); if (entry != null) input = firstJarFile.getInputStream(entry); else { entry = secondJarFile.getEntry(name); input = secondJarFile.getInputStream(entry != null ? entry : secondJarFile.getEntry(name.replace('/', '\\'))); } } outJarStream.putNextEntry(new JarEntry(name)); copyStream(input, outJarStream); outJarStream.closeEntry(); } firstJarFile.close(); secondJarFile.close(); outJarStream.close(); } static public void copyFromJAR (String inJar, String outJar, String... regexs) throws IOException { if (DEBUG) debug("scar", "Copying from JAR: " + inJar + " -> " + outJar + ", " + Arrays.asList(regexs)); JarFile inJarFile = new JarFile(inJar); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); for (Enumeration<JarEntry> entries = inJarFile.entries(); entries.hasMoreElements();) { JarEntry inEntry = entries.nextElement(); String name = inEntry.getName(); boolean matches = false; for (String regex : regexs) { if (name.matches(regex)) { matches = true; break; } } if (!matches) continue; JarEntry outEntry = new JarEntry(name); outJarStream.putNextEntry(outEntry); copyStream(inJarFile.getInputStream(inEntry), outJarStream); outJarStream.closeEntry(); } outJarStream.close(); inJarFile.close(); } static public void removeFromJAR (String inJar, String outJar, String... regexs) throws IOException { if (DEBUG) debug("scar", "Removing from JAR: " + inJar + " -> " + outJar + ", " + Arrays.asList(regexs)); JarFile inJarFile = new JarFile(inJar); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); outer: for (Enumeration<JarEntry> entries = inJarFile.entries(); entries.hasMoreElements();) { JarEntry inEntry = entries.nextElement(); String name = inEntry.getName(); for (String regex : regexs) if (name.matches(regex)) continue outer; JarEntry outEntry = new JarEntry(name); outEntry.setTime(1370273339); // Reset time. outJarStream.putNextEntry(outEntry); copyStream(inJarFile.getInputStream(inEntry), outJarStream); outJarStream.closeEntry(); } outJarStream.close(); inJarFile.close(); } static public void addToJAR (String inJar, String outJar, String addName, byte[] bytes, boolean overwrite) throws IOException { if (DEBUG) debug("scar", "Adding to JAR: " + inJar + " -> " + outJar + ", " + addName); JarFile inJarFile = new JarFile(inJar); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); ArrayList<String> names = new ArrayList(); for (Enumeration<JarEntry> entries = inJarFile.entries(); entries.hasMoreElements();) names.add(entries.nextElement().getName()); addName = addName.replace('\\', '/'); boolean exists = names.contains(addName) || names.contains(addName.replace('/', '\\')); if (!overwrite && exists) throw new RuntimeException("JAR already has entry: " + addName); if (!exists) { names.add(addName); Collections.sort(names); } if (names.remove("META-INF/MANIFEST.MF") || names.remove("META-INF\\MANIFEST.MF")) names.add(0, "META-INF/MANIFEST.MF"); for (String name : names) { outJarStream.putNextEntry(new JarEntry(name.replace('\\', '/'))); if (name.replace('\\', '/').equals(addName)) outJarStream.write(bytes); else copyStream(inJarFile.getInputStream(inJarFile.getEntry(name)), outJarStream); outJarStream.closeEntry(); } outJarStream.close(); inJarFile.close(); } static public void setEntryTime (String inJar, String outJar, long time) throws IOException { if (DEBUG) debug("scar", "Setting entry to for JAR: " + inJar + " -> " + outJar + ", " + time); JarFile inJarFile = new JarFile(inJar); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); for (Enumeration<JarEntry> entries = inJarFile.entries(); entries.hasMoreElements();) { JarEntry inEntry = entries.nextElement(); String name = inEntry.getName(); JarEntry outEntry = new JarEntry(name); outEntry.setTime(time); outJarStream.putNextEntry(outEntry); copyStream(inJarFile.getInputStream(inEntry), outJarStream); outJarStream.closeEntry(); } outJarStream.close(); inJarFile.close(); } static public void setClassVersions (String inJar, String outJar, int max, int min) throws IOException { if (DEBUG) debug("scar", "Setting class versions for JAR: " + inJar + " -> " + outJar + ", " + max + "." + min); JarFile inJarFile = new JarFile(inJar); mkdir(parent(outJar)); JarOutputStream outJarStream = new JarOutputStream(new FileOutputStream(outJar)); outJarStream.setLevel(Deflater.BEST_COMPRESSION); for (Enumeration<JarEntry> entries = inJarFile.entries(); entries.hasMoreElements();) { String name = entries.nextElement().getName(); outJarStream.putNextEntry(new JarEntry(name)); InputStream input = inJarFile.getInputStream(inJarFile.getEntry(name)); if (name.endsWith(".class")) input = new ClassVersionStream(input, name, max, min); copyStream(input, outJarStream); outJarStream.closeEntry(); } outJarStream.close(); inJarFile.close(); } static class ClassVersionStream extends FilterInputStream { private boolean first = true; private final int max, min; private String name; public ClassVersionStream (InputStream in, String name, int max, int min) { super(in); this.name = name; this.max = max; this.min = min; } public int read (byte[] b, int off, int len) throws IOException { int count = super.read(b, off, len); if (first) { first = false; if (count < 8) throw new RuntimeException("Too few bytes: " + count); int oldMin = (b[off + 4] << 8) | b[off + 5]; int oldMax = ((b[off + 6] & 0xff) << 8) | (b[off + 7] & 0xff); b[off + 4] = (byte)(min >> 8); b[off + 5] = (byte)min; b[off + 6] = (byte)(max >> 8); b[off + 7] = (byte)max; if (DEBUG && (oldMax != max || oldMin != min)) debug(oldMax + "." + oldMin + " to " + max + "." + min + ": " + name); } return count; } } }