JavaBuilder/src/dorkbox/build/util/jar/JarUtil.java

1817 lines
70 KiB
Java

/*
* Copyright 2012 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package dorkbox.build.util.jar;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.RandomAccessFile;
import java.io.Writer;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
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.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.bouncycastle.crypto.digests.SHA512Digest;
import com.esotericsoftware.wildcard.Paths;
import com.ice.tar.TarEntry;
import com.ice.tar.TarInputStream;
import dorkbox.Build;
import dorkbox.BuildOptions;
import dorkbox.license.License;
import dorkbox.util.Base64Fast;
import dorkbox.util.FileUtil;
import dorkbox.util.LZMA;
import dorkbox.util.OS;
import dorkbox.util.OsType;
import dorkbox.util.Sys;
public class JarUtil {
public static int JAR_COMPRESSION_LEVEL = 9;
public static byte[] ZIP_HEADER = { 80, 75, 3, 4 }; // PK34
public static final String metaInfName = "META-INF/";
public static final String configFile = "config.ini";
/**
* @return true if the file is a zip/jar file
*/
public static boolean isZipFile(File file) {
boolean isZip = true;
byte[] buffer = new byte[ZIP_HEADER.length];
RandomAccessFile raf = null;
try {
raf = new RandomAccessFile(file, "r");
raf.readFully(buffer);
for (int i = 0; i < ZIP_HEADER.length; i++) {
if (buffer[i] != ZIP_HEADER[i]) {
isZip = false;
break;
}
}
} catch (Exception e) {
isZip = false;
} finally {
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return isZip;
}
/**
* @return true if the file is a zip/jar stream
*/
public static boolean isZipStream(ByteArrayInputStream input) {
boolean isZip = true;
int length = ZIP_HEADER.length;
try {
input.mark(length+1);
for (int i = 0; i < length; i++) {
byte read = (byte) input.read();
if (read != ZIP_HEADER[i]) {
isZip = false;
break;
}
}
input.reset();
} catch (Exception e) {
isZip = false;
}
return isZip;
}
/**
* retrieve the manifest from a jar file -- this will either load a
* pre-existing META-INF/MANIFEST.MF, or return null if none
*/
public static final Manifest getManifestFile(JarFile jarFile) throws IOException {
JarEntry je = jarFile.getJarEntry(JarFile.MANIFEST_NAME);
// verify that it really exists.
if (je != null) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
je = jarEntries.nextElement();
if (JarFile.MANIFEST_NAME.equals(je.getName())) {
break;
} else {
je = null;
}
}
// create the manifest object
Manifest manifest = new Manifest();
InputStream inputStream = jarFile.getInputStream(je);
manifest.read(inputStream);
Sys.close(inputStream);
return manifest;
} else {
return null;
}
}
/**
* a helper function that can take entries from one jar file and write it to
* another jar stream
*
* Will close the output stream automatically.
*/
public static final void writeZipEntry(ZipEntry entry, ZipFile zipInputFile, ZipOutputStream zipOutputStream) throws IOException {
// create a new entry to avoid ZipException: invalid entry compressed size
ZipEntry newEntry = new ZipEntry(entry.getName());
newEntry.setTime(entry.getTime());
newEntry.setComment(entry.getComment());
newEntry.setExtra(entry.getExtra());
zipOutputStream.putNextEntry(newEntry);
if (!entry.isDirectory()) {
InputStream is = zipInputFile.getInputStream(entry);
Sys.copyStream(is, zipOutputStream);
Sys.close(is);
zipOutputStream.flush();
}
zipOutputStream.closeEntry();
}
/**
* a helper function that can take entries from one jar file and write it to
* another jar stream
*
* Does NOT close any streams!
*/
public static void writeZipEntry(ZipEntry entry, ZipInputStream zipInputStream, ZipOutputStream zipOutputStream) throws IOException {
ZipEntry newEntry = new ZipEntry(entry.getName());
newEntry.setTime(entry.getTime());
newEntry.setComment(entry.getComment());
newEntry.setExtra(entry.getExtra());
zipOutputStream.putNextEntry(newEntry);
if (!entry.isDirectory()) {
Sys.copyStream(zipInputStream, zipOutputStream);
zipOutputStream.flush();
}
zipInputStream.closeEntry();
zipOutputStream.closeEntry();
}
public static final String updateDigest(InputStream inputStream, MessageDigest digest) throws IOException {
byte[] buffer = new byte[2048];
int read = 0;
digest.reset();
while ((read = inputStream.read(buffer)) > 0) {
digest.update(buffer, 0, read);
}
Sys.close(inputStream);
byte[] digestBytes = digest.digest();
/*
* Do not insert a default newline at the end of the output line, as
* java.util.jar does its own line management (see
* Manifest.make72Safe()). Inserting additional new lines will cause
* line-wrapping problems.
*/
return Base64Fast.encodeToString(digestBytes, false);
}
public static final ByteArrayOutputStream createNewJar(JarFile jar, String name, byte[] manifestBytes,
byte[] signatureFileManifestBytes, byte[] signatureBlockBytes) throws IOException {
name = name.toUpperCase();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
// cannot use the jarInputStream technique here, since i'm reordering
// the contents of the jar.
// MANIFEST ENTRIES MUST BE FIRST
// write out the manifest to the output jar stream
JarEntry manifestFile = new JarEntry(JarFile.MANIFEST_NAME);
jarOutputStream.putNextEntry(manifestFile);
jarOutputStream.write(manifestBytes, 0, manifestBytes.length);
jarOutputStream.closeEntry();
String signatureAlias = metaInfName + name;
// write out the signature file
String signatureFileName = signatureAlias + ".SF";
JarEntry signatureFileEntry = new JarEntry(signatureFileName);
jarOutputStream.putNextEntry(signatureFileEntry);
jarOutputStream.write(signatureFileManifestBytes, 0, signatureFileManifestBytes.length);
jarOutputStream.closeEntry();
// write out the signature block file
String signatureBlockName = signatureAlias + ".DSA"; // forced DSA
JarEntry signatureBlockEntry = new JarEntry(signatureBlockName);
jarOutputStream.putNextEntry(signatureBlockEntry);
jarOutputStream.write(signatureBlockBytes, 0, signatureBlockBytes.length);
jarOutputStream.closeEntry();
// commit the rest of the original entries in the
// META-INF directory. if any of their names conflict
// with one that we created for the signed JAR file, then
// we simply ignore it
Enumeration<JarEntry> metaEntries = jar.entries();
while (metaEntries.hasMoreElements()) {
JarEntry metaEntry = metaEntries.nextElement();
String entryName = metaEntry.getName();
if (entryName.startsWith(metaInfName)
&& !(JarFile.MANIFEST_NAME.equalsIgnoreCase(entryName) || signatureFileName.equalsIgnoreCase(entryName) || signatureBlockName.equalsIgnoreCase(entryName))) {
JarUtil.writeZipEntry(metaEntry, jar, jarOutputStream);
}
}
// now write out the rest of the files to the stream
Enumeration<JarEntry> allEntries = jar.entries();
while (allEntries.hasMoreElements()) {
JarEntry entry = allEntries.nextElement();
if (!entry.getName().startsWith(metaInfName)) {
JarUtil.writeZipEntry(entry, jar, jarOutputStream);
}
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
jar.close();
return byteArrayOutputStream;
}
/**
* removes all of the (META-INF, OSGI-INF, etc) information (removes the entire directory), AND ALSO removes all comments from the files
*/
public static InputStream removeManifestCommentsAndFiles(String fileName, InputStream inputStream,
String[] pathToRemove, String[] pathToKeep) throws IOException {
// shortcut out -- nothing to do
if (pathToRemove == null || pathToRemove.length == 0) {
return inputStream;
}
Set<String> stripped = new HashSet<String>();
// by default, this will not have access to the manifest! (not that we care...)
// we will ALSO lose entry comments!
JarInputStream jarInputStream = new JarInputStream(inputStream, false);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
JarEntry entry;
JAR_PROCESSING:
while ((entry = jarInputStream.getNextJarEntry()) != null) {
String name = entry.getName();
boolean preserveEntry = false;
if (pathToKeep != null) {
for (String dir : pathToKeep) {
if (name.startsWith(dir)) {
preserveEntry = true;
break;
}
}
}
if (!preserveEntry) {
for (String dir : pathToRemove) {
if (name.startsWith(dir)) {
if (!stripped.contains(fileName)) {
System.err.println("Removing " + dir + " directory & signatures... (" + fileName + ")");
stripped.add(fileName);
}
continue JAR_PROCESSING;
}
}
}
// create a new entry to avoid ZipException: invalid entry compressed size
// we want to COPY this over. hashes should remain the same between builds!
writeZipEntry(entry, jarInputStream, jarOutputStream);
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
Sys.close(jarInputStream);
Sys.close(inputStream);
// return the regular stream if we didn't strip anything!
// convert the output stream to an input stream
int length = byteArrayOutputStream.size();
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray(), 0, length);
}
/**
* removes all of the (META-INF, OSGI-INF, etc) information (removes the entire directory), AND ALSO removes all comments from the files
*/
public static InputStream extractLibraries(File parentLocation, InputStream inputStream, String libraryNameOverride) throws IOException {
// these are really the file extensions that we want to extract
// from the JAR to the DIR this file is in
String[] libraryExtensions = new String[] {".so", ".dll", ".dylib", ".jnilib"};
List<String> pathsToRemove = new ArrayList<String>();
// by default, this will not have access to the manifest! (not that we care...)
// we will ALSO lose entry comments!
JarInputStream jarInputStream = new JarInputStream(inputStream, false);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
JarEntry entry;
JAR_PROCESSING:
while ((entry = jarInputStream.getNextJarEntry()) != null) {
String name = entry.getName();
boolean moveEntry = !entry.isDirectory();
if (moveEntry) {
moveEntry = false;
for (String extension : libraryExtensions) {
if (name.endsWith(extension)) {
moveEntry = true;
break;
}
}
}
if (moveEntry) {
// extract file+path to parent name location
int index = name.lastIndexOf('/');
String nameOnly = name;
String directParent = "";
String fullParentPath = "";
if (index > 0) {
nameOnly = name.substring(index+1);
fullParentPath = name.substring(0, name.length()-nameOnly.length()-1);
directParent = fullParentPath;
index = fullParentPath.lastIndexOf('/');
if (index > 0) {
directParent = fullParentPath.substring(index+1);
}
// have to put the '/' back on the path
fullParentPath = fullParentPath + '/';
pathsToRemove.add(fullParentPath);
// now we have to override the library name
index = name.lastIndexOf('.');
String extension = name.substring(index);
nameOnly = libraryNameOverride + extension;
}
// now, we have a parent file that needs to be TRANSLATED to our OS specific (so we can nicely load it later)
if (directParent == null) {
throw new RuntimeException("DirectParent");
}
if (directParent.equals("darwin")) {
// JNA includes a multi-arch binary for x86, x86-64, and ppc.
directParent = OsType.MacOsX32.getName();
System.err.println("\t - Moving " + directParent + " library...");
// now we copy the file to disk
File parentFile = new File(parentLocation, directParent);
parentFile.mkdir();
File file = new File(parentFile, nameOnly);
FileOutputStream fileOutputStream = new FileOutputStream(file);
Sys.copyStream(jarInputStream, fileOutputStream);
fileOutputStream.flush();
jarInputStream.closeEntry();
// 64 bit is the same lib
directParent = OsType.MacOsX64.getName();
System.err.println("\t - Moving " + directParent + " library...");
// now we copy the file to disk
parentFile = new File(parentLocation, directParent);
parentFile.mkdir();
file = new File(parentFile, nameOnly);
fileOutputStream = new FileOutputStream(file);
Sys.copyStream(jarInputStream, fileOutputStream);
fileOutputStream.flush();
jarInputStream.closeEntry();
continue JAR_PROCESSING;
} else if (directParent.startsWith("freebsd")) {
directParent = null;
} else if (directParent.equals("linux-arm")) {
directParent = OsType.LinuxArm.getName();
} else if (directParent.equals("linux-x86")) {
directParent = OsType.Linux32.getName();
} else if (directParent.equals("linux-x86-64")) {
directParent = OsType.Linux64.getName();
} else if (directParent.startsWith("openbsd")) {
directParent = null;
} else if (directParent.startsWith("freebsd")) {
directParent = null;
} else if (directParent.startsWith("sunos")) {
directParent = null;
} else if (directParent.startsWith("w32ce")) {
directParent = null;
} else if (directParent.equals("win32-x86")) {
directParent = OsType.Windows32.getName();
} else if (directParent.equals("win32-x86-64")) {
directParent = OsType.Windows64.getName();
}
// only do something if we have a target specified
if (directParent != null) {
System.err.println("\t - Moving " + directParent + " library...");
// now we copy the file to disk
File parentFile = new File(parentLocation, directParent);
parentFile.mkdir();
File file = new File(parentFile, nameOnly);
FileOutputStream fileOutputStream = new FileOutputStream(file);
Sys.copyStream(jarInputStream, fileOutputStream);
fileOutputStream.flush();
jarInputStream.closeEntry();
}
}
else {
// create a new entry to avoid ZipException: invalid entry compressed size
// we want to COPY this over. hashes should remain the same between builds!
writeZipEntry(entry, jarInputStream, jarOutputStream);
}
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
Sys.close(jarInputStream);
Sys.close(inputStream);
// NOW -- we load it again, and REMOVE the empty dirs from the jar
System.err.println("\t - Cleaning library directories in jar...");
int length = byteArrayOutputStream.size();
inputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray(), 0, length);
// by default, this will not have access to the manifest! (not that we care...)
// we will ALSO lose entry comments!
jarInputStream = new JarInputStream(inputStream, false);
byteArrayOutputStream = new ByteArrayOutputStream();
jarOutputStream = new JarOutputStream(byteArrayOutputStream);
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
JAR_PROCESSING:
while ((entry = jarInputStream.getNextJarEntry()) != null) {
String name = entry.getName();
boolean moveEntry = entry.isDirectory();
if (moveEntry) {
moveEntry = false;
for (String path : pathsToRemove) {
if (name.startsWith(path)) {
continue JAR_PROCESSING;
}
}
}
// create a new entry to avoid ZipException: invalid entry compressed size
// we want to COPY this over. hashes should remain the same between builds!
writeZipEntry(entry, jarInputStream, jarOutputStream);
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
Sys.close(jarInputStream);
Sys.close(inputStream);
// return the regular stream if we didn't strip anything!
// convert the output stream to an input stream
length = byteArrayOutputStream.size();
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray(), 0, length);
}
/**
* This will ALSO normalize (pack+unpack) the jar
*
* Note about JarOutputStream:
* The JAR_MAGIC "0xCAFE" in the extra field data of the first JAR entry from our JarOutputStream implementation is
* not required by JAR specification. It's an "internal implementation detail" to support "executable" jar on Solaris
* platform. see#4138619. It would be incorrect to reject a JAR file that does not have this extra field data, from
* specification point of view.
*
* (basically, if you use a JarOutputStream, it adds in extra crap we don't want)
*/
public static void jar(JarOptions options) throws IOException {
if (options.outputFile == null) {
throw new IllegalArgumentException("jarFile cannot be null.");
}
if (options.inputPaths == null) {
throw new IllegalArgumentException("inputPaths cannot be null.");
}
options.inputPaths = options.inputPaths.filesOnly();
if (options.inputPaths.isEmpty()) {
System.err.println("No files to JAR.");
return;
}
List<String> fullPaths = options.inputPaths.getPaths();
List<String> relativePaths = options.inputPaths.getRelativePaths();
String manifestFile = null;
String manifestName = JarFile.MANIFEST_NAME;
int manifestIndex = relativePaths.indexOf(manifestName);
// manage the MANIFEST
if (manifestIndex > 0) {
// Ensure MANIFEST.MF is first.
relativePaths.remove(manifestIndex);
relativePaths.add(0, manifestName);
String manifestFullPath = fullPaths.get(manifestIndex);
fullPaths.remove(manifestIndex);
fullPaths.add(0, manifestFullPath);
} else
if (options.mainClass != null) {
manifestFile = FileUtil.tempFile("manifest").getAbsolutePath();
relativePaths.add(0, manifestName);
fullPaths.add(0, manifestFile);
Manifest manifest = new Manifest();
Attributes attributes = manifest.getMainAttributes();
attributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
attributes.putValue(Attributes.Name.MAIN_CLASS.toString(), options.mainClass);
if (options.otherManifestAttributes != null) {
for (Entry<String, String> entry : options.otherManifestAttributes.entrySet()) {
attributes.putValue(entry.getKey(), entry.getValue());
}
}
StringBuilder buffer = new StringBuilder(512);
buffer.append(".");
if (options.classpath != null) {
for (String name : options.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 {
Sys.close(output);
}
}
Build.log().title(" Creating JAR").message("(" + options.inputPaths.count() + " entries)",
FileUtil.normalizeAsFile(options.outputFile));
// CLEANUP DIRECTORIES
Set<String> directories = figureOutDirectories(fullPaths, relativePaths);
// NOW WE ACTUALLY MAKE THE JAR
FileUtil.mkdir(new File(options.outputFile).getParent());
ByteArrayOutputStream jarOutputStream = new ByteArrayOutputStream();
JarOutputStream output = new JarOutputStream(jarOutputStream);
output.setLevel(JAR_COMPRESSION_LEVEL);
try {
// quirks & zip standards.
// - Directory names must end with a slash '/'
// - All paths must use '/' style slashes, not '\'
// - JarEntry names should NOT begin with '/'
InputStream input = null;
boolean foundManifest = false;
// MANIFEST FIRST! There is only the manifest, as we are creating
// the jar from scratch.
// this means that there won't be any other "META-INF" files.
if (manifestIndex >= 0) {
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i).replace('\\', '/');
if (fileName.equals(manifestName)) {
File file = new File(fullPaths.get(i));
JarEntry jarEntry = new JarEntry(fileName);
jarEntry.setTime(file.lastModified());
output.putNextEntry(jarEntry);
input = new BufferedInputStream(new FileInputStream(file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
FileUtil.delete(manifestFile);
foundManifest = true;
}
if (foundManifest) {
fullPaths.remove(i);
relativePaths.remove(i);
break;
}
}
}
// there won't be any OTHER manifest files, since we haven't signed
// the jar yet...
// NEXT all directories
List<String> sortList = new ArrayList<String>(directories.size());
for (String dirName : directories) {
if (!dirName.endsWith("/")) {
dirName += "/";
}
sortList.add(dirName);
}
// sort them
Collections.sort(sortList);
for (String dirName : sortList) {
JarEntry jarEntry = new JarEntry(dirName);
output.putNextEntry(jarEntry);
output.closeEntry();
}
class SortedFiles implements Comparable<SortedFiles> {
public String fileName;
public File file;
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (this.fileName == null ? 0 : this.fileName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
SortedFiles other = (SortedFiles) obj;
if (this.fileName == null) {
if (other.fileName != null) {
return false;
}
} else if (!this.fileName.equals(other.fileName)) {
return false;
}
return true;
}
@Override
public int compareTo(SortedFiles o) {
return this.fileName.compareTo(o.fileName);
}
}
///////////////////////////////////////////////
// THEN all CLASS files. Skip the MANIFEST
///////////////////////////////////////////////
List<SortedFiles> sortList2 = new ArrayList<SortedFiles>(fullPaths.size());
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i).replace('\\', '/');
if (!fileName.equals(manifestName) && fileName.endsWith(".class")) {
SortedFiles file = new SortedFiles();
file.file = new File(fullPaths.get(i));
file.fileName = fileName;
sortList2.add(file);
// mucking with the backing array so our indexing still works
fullPaths.remove(i);
relativePaths.remove(i);
i--;
n--;
}
}
//sort them
Collections.sort(sortList2);
for (SortedFiles cf : sortList2) {
JarEntry jarEntry = new JarEntry(cf.fileName);
jarEntry.setTime(cf.file.lastModified());
output.putNextEntry(jarEntry);
// else just copy the file over
input = new BufferedInputStream(new FileInputStream(cf.file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
}
///////////////////////////////////////////////
// files other than class files.
///////////////////////////////////////////////
sortList2 = new ArrayList<SortedFiles>(fullPaths.size());
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i).replace('\\', '/');
SortedFiles file = new SortedFiles();
file.file = new File(fullPaths.get(i));
file.fileName = fileName;
sortList2.add(file);
}
// sort them
Collections.sort(sortList2);
for (SortedFiles cf : sortList2) {
//System.err.println('\t' + fullPaths.get(i));
JarEntry jarEntry = new JarEntry(cf.fileName);
jarEntry.setTime(cf.file.lastModified());
output.putNextEntry(jarEntry);
// else just copy the file over
input = new BufferedInputStream(new FileInputStream(cf.file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
}
///////////////////////////////////////////////
// NOW we do the EXTRA files.
// These files will MATCH the path hierarchy in the jar
///////////////////////////////////////////////
if (options.extraPaths != null) {
sortList2 = new ArrayList<SortedFiles>(options.extraPaths.count());
Build.log().message(" Adding extras");
fullPaths = options.extraPaths.getPaths();
relativePaths = options.extraPaths.getRelativePaths();
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName;
fileName = relativePaths.get(i).replace('\\', '/');
Build.log().message("\t" + fileName);
SortedFiles file = new SortedFiles();
file.file = new File(fullPaths.get(i));
file.fileName = fileName;
sortList2.add(file);
}
// sort them
Collections.sort(sortList2);
for (SortedFiles cf : sortList2) {
JarEntry jarEntry = new JarEntry(cf.fileName);
jarEntry.setTime(cf.file.lastModified());
output.putNextEntry(jarEntry);
// else just copy the file over
input = new BufferedInputStream(new FileInputStream(cf.file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
}
}
///////////////////////////////////////////////
// include the source code if possible
///////////////////////////////////////////////
if (options.sourcePaths != null && !options.sourcePaths.isEmpty()) {
sortList2 = new ArrayList<SortedFiles>(options.sourcePaths.count());
Build.log().message(" Adding sources (" + options.sourcePaths.count() + " entries)...");
fullPaths = options.sourcePaths.getPaths();
relativePaths = options.sourcePaths.getRelativePaths();
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i).replace('\\', '/');
// System.err.println("\t\t: " + fileName);
SortedFiles file = new SortedFiles();
file.file = new File(fullPaths.get(i));
file.fileName = fileName;
sortList2.add(file);
}
// sort them
Collections.sort(sortList2);
for (SortedFiles cf : sortList2) {
JarEntry jarEntry = new JarEntry(cf.fileName);
jarEntry.setTime(cf.file.lastModified());
output.putNextEntry(jarEntry);
// else just copy the file over
input = new BufferedInputStream(new FileInputStream(cf.file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
}
}
///////////////////////////////////////////////
// now include the license, if possible
///////////////////////////////////////////////
if (options.licenses != null) {
Build.log().message(" Adding license");
License.install(output, options.licenses);
}
} finally {
output.finish();
Sys.close(output);
}
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(jarOutputStream.toByteArray());
// now we normalize the JAR.
ByteArrayOutputStream repacked = Pack200Util.Java.repackJar(byteArrayInputStream);
byteArrayInputStream = new ByteArrayInputStream(repacked.toByteArray());
FileOutputStream fileOutputStream = new FileOutputStream(options.outputFile);
Sys.copyStream(byteArrayInputStream, fileOutputStream);
Sys.close(fileOutputStream);
}
/**
* Figures out what are going to be directories that should be created in the war.
*/
private static Set<String> figureOutDirectories(List<String> fullPaths, List<String> relativePaths) {
Set<String> directories = new HashSet<String>();
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i);
String pathName = fullPaths.get(i);
// determine if we have a directory or not.
if (fileName.indexOf("/") > -1 || fileName.indexOf("\\") > -1) {
int indexOf = pathName.indexOf(fileName);
// if our filename is a part of the path (this is when loading classes)
if (indexOf > -1) {
String dir = fileName.replace('\\', '/');
// keep the trailing slash! (needed later on)
int lastIndex = dir.lastIndexOf('/');
dir = dir.substring(0, lastIndex);
// now we add ourself, then recursively add our parent dirs
// always add back in the slash!
directories.add(dir + "/");
lastIndex = dir.lastIndexOf('/');
while (lastIndex > 0) {
dir = dir.substring(0, lastIndex);
lastIndex = dir.lastIndexOf('/');
// now we add ourself, then recursively add our parent dirs
directories.add(dir + "/");
}
}
// when loading up jars and other resources.
else {
// have to fetch the directory.
String dirName = fileName.substring(0, fileName.lastIndexOf("/"));
directories.add(dirName + "/"); // set the same in this instance!
}
}
}
return directories;
}
/**
* Similar to 'jar', however this is for war files instead.
*/
public static void war(String warFilePath, List<String> fullPaths, List<String> relativePaths) throws FileNotFoundException, IOException {
// CLEANUP DIRECTORIES
Set<String> directories = figureOutDirectories(fullPaths, relativePaths);
// NOW WE ACTUALLY MAKE THE JAR
FileUtil.mkdir(new File(warFilePath).getParent());
JarOutputStream output = new JarOutputStream(new BufferedOutputStream(new FileOutputStream(warFilePath)));
output.setLevel(JAR_COMPRESSION_LEVEL);
try {
// quirks & zip standards.
// - Directory names must end with a slash '/'
// - All paths must use '/' style slashes, not '\'
// - JarEntry names should NOT begin with '/'
BufferedInputStream input = null;
// FIRST all directories
for (String dirName : directories) {
if (!dirName.endsWith("/")) {
dirName += "/";
}
JarEntry jarEntry = new JarEntry(dirName);
output.putNextEntry(jarEntry);
output.closeEntry();
}
// regular files
for (int i = 0, n = fullPaths.size(); i < n; i++) {
String fileName = relativePaths.get(i).replace('\\', '/');
File file = new File(fullPaths.get(i));
JarEntry jarEntry = new JarEntry(fileName);
jarEntry.setTime(file.lastModified());
output.putNextEntry(jarEntry);
input = new BufferedInputStream(new FileInputStream(file));
Sys.copyStream(input, output);
Sys.close(input);
output.closeEntry();
}
} finally {
output.finish();
Sys.close(output);
}
}
public static void removeArchiveCommentFromJar(String jarName) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
// cannot use the jarInputStream technique here, since i'm reordering
// the contents of the jar.
JarFile jarFile = new JarFile(jarName);
// MANIFEST ENTRIES MUST BE FIRST
Enumeration<JarEntry> metaEntries = jarFile.entries();
while (metaEntries.hasMoreElements()) {
JarEntry metaEntry = metaEntries.nextElement();
String name = metaEntry.getName();
if (name.startsWith(metaInfName) && !metaEntry.isDirectory()) {
JarUtil.writeZipEntry(metaEntry, jarFile, jarOutputStream);
} else {
// since this is already a valid jar, the META-INF data is
// already first.
break;
}
}
// now guarantee that directories are NEXT
Enumeration<JarEntry> directoryEntries = jarFile.entries();
while (directoryEntries.hasMoreElements()) {
JarEntry entry = directoryEntries.nextElement();
if (entry.isDirectory()) {
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
}
}
// now write out the rest of the files to the stream
Enumeration<JarEntry> allEntries = jarFile.entries();
while (allEntries.hasMoreElements()) {
JarEntry entry = allEntries.nextElement();
if (!entry.isDirectory() && !entry.getName().startsWith(metaInfName)) {
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
}
}
// don't add the archive comment
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
jarFile.close();
OutputStream outputStream = new FileOutputStream(jarName, false);
byteArrayOutputStream.writeTo(outputStream);
Sys.close(outputStream);
}
public static long addTimeStampToJar(String jarName) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
// cannot use the jarInputStream technique here, since i'm reordering
// the contents of the jar.
JarFile jarFile = new JarFile(jarName);
// MANIFEST ENTRIES MUST BE FIRST
Enumeration<JarEntry> metaEntries = jarFile.entries();
while (metaEntries.hasMoreElements()) {
JarEntry metaEntry = metaEntries.nextElement();
String name = metaEntry.getName();
if (name.startsWith(metaInfName) && !metaEntry.isDirectory()) {
JarUtil.writeZipEntry(metaEntry, jarFile, jarOutputStream);
} else {
// since this is already a valid jar, the META-INF data is
// already first.
break;
}
}
// now add our TIMESTAMP.
// It will ALWAYS calculate the timestamp from the BUILD SYSTEM, not the
// LOCAL/REMOTE SYSTEM (which can exist with incorrect/different clocks)
long timeStamp = System.currentTimeMillis();
JarEntry jarEntry = new JarEntry(metaInfName + "___" + Long.toString(timeStamp));
jarOutputStream.putNextEntry(jarEntry);
jarOutputStream.closeEntry();
// now guarantee that directories are NEXT
Enumeration<JarEntry> directoryEntries = jarFile.entries();
while (directoryEntries.hasMoreElements()) {
JarEntry entry = directoryEntries.nextElement();
if (entry.isDirectory()) {
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
}
}
// now write out the rest of the files to the stream
Enumeration<JarEntry> allEntries = jarFile.entries();
while (allEntries.hasMoreElements()) {
JarEntry entry = allEntries.nextElement();
if (!entry.isDirectory() && !entry.getName().startsWith(metaInfName)) {
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
}
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
jarFile.close();
OutputStream outputStream = new FileOutputStream(jarName, false);
byteArrayOutputStream.writeTo(outputStream);
Sys.close(outputStream);
return timeStamp;
}
/**
* Adds args (launcher or VM args) to the ini file.
* @throws IOException
*/
public static void addArgsToIniInJar(String jarName, String... args) throws IOException {
Build.log().message("Modifying config.ini file in jar...");
for (String arg : args) {
Build.log().message("\t" + arg);
}
// we have to use a JarFile, so we preserve the comments that might already be in the file.
JarFile origJarFile = new JarFile(jarName);
JarEntry entry;
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
boolean foundConfigFile = false;
// now write out the rest of the files to the stream
// THIS DOES NOT MESS WITH THE ORDER OF THE FILES IN THE JAR!
Enumeration<JarEntry> metaEntries = origJarFile.entries();
while (metaEntries.hasMoreElements()) {
entry = metaEntries.nextElement();
String name = entry.getName();
if (!name.equals(configFile)) {
JarUtil.writeZipEntry(entry, origJarFile, jarOutputStream);
} else {
foundConfigFile = true;
addArgsToIniContents(entry, origJarFile.getInputStream(entry), jarOutputStream, args);
}
}
if (!foundConfigFile) {
addArgsToIniContents(null, jarOutputStream, args);
}
origJarFile.close();
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
OutputStream outputStream = new FileOutputStream(jarName, false);
byteArrayOutputStream.writeTo(outputStream);
Sys.close(outputStream);
}
public static void addArgsToIniFile(String iniFile, String... args) throws IOException {
InputStream inFile = new BufferedInputStream(new FileInputStream(iniFile));
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
addArgsToIniContents(inFile, outStream, args);
FileOutputStream outFile = new FileOutputStream(iniFile);
outStream.writeTo(outFile);
outFile.flush();
Sys.close(outFile);
}
/**
* Fixes up the ini file inside the jar.
*/
private static void addArgsToIniContents(InputStream inputStream, OutputStream outputStream, String... args) throws IOException {
addArgsToIniContents(null, inputStream, outputStream, args);
}
/**
* Fixes up the ini file inside the jar.
*/
private static void addArgsToIniContents(JarEntry entry, InputStream inputStream, OutputStream outputStream,
String... args) throws IOException {
ByteArrayOutputStream outputStreamCopy = new ByteArrayOutputStream();
if (inputStream != null) {
Sys.copyStream(inputStream, outputStreamCopy);
}
ByteArrayInputStream inputStreamCopy = new ByteArrayInputStream(outputStreamCopy.toByteArray());
List<String> iniFileArgs = new ArrayList<String>(16);
BufferedReader input = null;
try {
input = new BufferedReader(new InputStreamReader(inputStreamCopy));
//FileReader always assumes default encoding is OK!
String line = null;
/*
* returns the content of a line MINUS the newline.
* returns null only for the END of the stream.
* returns an empty String if two newlines appear in a row.
*/
while (( line = input.readLine()) != null) {
iniFileArgs.add(line);
}
}
catch (IOException e) { }
finally {
Sys.close(input);
}
// now we write the args.
outputStreamCopy = new ByteArrayOutputStream();
Writer output = null;
try {
output = new BufferedWriter(new OutputStreamWriter(outputStreamCopy));
// FileWriter always assumes default encoding is OK
// write all of the original args
for (String arg : iniFileArgs) {
output.write(arg);
output.write(OS.LINE_SEPARATOR);
}
// write our new args
for (String arg : args) {
output.write(arg);
output.write(OS.LINE_SEPARATOR);
}
} catch (IOException e) {
} finally {
Sys.close(output);
}
inputStreamCopy = new ByteArrayInputStream(outputStreamCopy.toByteArray());
if (outputStream instanceof JarOutputStream) {
JarOutputStream jarOutputStream = (JarOutputStream) outputStream;
JarEntry entry2 = new JarEntry(configFile);
entry2.setComment(entry.getComment());
entry2.setExtra(entry.getExtra());
jarOutputStream.putNextEntry(entry2);
Sys.copyStream(inputStreamCopy, outputStream);
jarOutputStream.closeEntry();
Sys.close(inputStreamCopy);
} else {
Sys.copyStream(inputStreamCopy, outputStream);
outputStream.flush();
Sys.close(outputStream);
Sys.close(inputStreamCopy);
}
}
/**
* Adds the specified files AS REGULAR FILES to the jar.
*
* @param filesToAdd
* a PAIR of strings. First in pair is SOURCE, second in pair is
* DEST
*/
public static void addFilesToJar(String jarName, BuildOptions options, ExtraDataInterface extraDataWriter, Pack... filesToAdd) throws IOException {
addFilesToJar(jarName, options, null, extraDataWriter, filesToAdd);
}
/**
* Adds the specified files AS REGULAR FILES to the jar. Will ALSO let us REPLACE files in the jar
*
* @param filesToAdd
* a PAIR of strings. First in pair is SOURCE, second in pair is DEST
*/
public static void addFilesToJar(String jarName, BuildOptions properties, EncryptInterface encryption, ExtraDataInterface extraDataWriter,
Pack... filesToAdd) throws IOException {
PackAction[] actionsToRemove;
if (properties.compiler.enableDebugSpeedImprovement) {
actionsToRemove = new PackAction[] {PackAction.Pack, PackAction.Lzma, PackAction.Encrypt};
} else {
actionsToRemove = new PackAction[] {PackAction.Encrypt};
}
boolean addDebug = properties.compiler.debugEnabled;
boolean release = properties.compiler.release;
Build.log().message("Adding files to jar: '" + jarName + "'");
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
JarOutputStream jarOutputStream = new JarOutputStream(new BufferedOutputStream(byteArrayOutputStream));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
// we have to use a JarFile, so we preserve the comments that might already be in the file.
JarFile jarFile = new JarFile(jarName);
JarEntry entry;
// THIS DOES NOT MESS WITH THE ORDER OF THE FILES IN THE JAR!
// This will also let us replace a jar with a different pack-action (ie, change something to LGPL, LoadAction, etc)
Enumeration<JarEntry> metaEntries = jarFile.entries();
while (metaEntries.hasMoreElements()) {
entry = metaEntries.nextElement();
String name = entry.getName();
boolean canAdd = true;
for (Pack pack : filesToAdd) {
String destPath = pack.getDestPath();
if (name.equals(destPath)) {
Build.log().message(" Replacing '" + destPath + "'");
canAdd = false;
break;
}
}
if (canAdd) {
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
}
}
// now add the files that we want to add.
for (Pack pack : filesToAdd) {
if (!release) {
pack.remove(actionsToRemove);
}
String sourcePath = pack.getSourcePath();
String destPath = pack.getDestPath();
Build.log().message(" '" + sourcePath + "' -> '" + destPath + "'");
InputStream inputStream;
int length = 0;
long time = 0L;
entry = new JarEntry(destPath);
if (sourcePath != null) {
File fileToAdd = new File(sourcePath);
time = fileToAdd.lastModified();
entry.setTime(time);
inputStream = new FileInputStream(fileToAdd);
length = (int) fileToAdd.length(); // yea, yea, yea, the length truncates...
} else {
inputStream = new ByteArrayInputStream(new byte[0]);
}
/////////////
// now load the entry to the jar
/////////////
PackTask task = new PackTask(pack, inputStream);
task.time = time;
task.debug = addDebug;
task.length = length; // have to do this, because of how FileInputStream works.
task.encryption = encryption;
if (pack.canDo(PackAction.Extract)) {
// we do this here, so that the unpack will copy over/duplicate our custom extra data field
if (extraDataWriter != null) {
extraDataWriter.write(entry, null);
}
unpackEntry(task, extraDataWriter, jarOutputStream);
} else {
packEntry(task);
if (extraDataWriter != null) {
extraDataWriter.write(entry, task);
}
jarOutputStream.putNextEntry(entry);
Sys.copyStream(task.inputStream, jarOutputStream);
jarOutputStream.closeEntry();
Sys.close(task.inputStream);
}
}
// finish the stream that we have been writing to
jarOutputStream.finish();
Sys.close(jarOutputStream);
OutputStream outputStream = new FileOutputStream(jarName, false);
byteArrayOutputStream.writeTo(outputStream);
Sys.close(outputStream);
jarFile.close();
}
/**
* Repackages the JAR, compressing/etc based on specific rules.
*
* Also makes sure to have our custom header (in 'extra data') written for each entry
*
* This is how we get the JAR file size down.
* @return
*/
public static void packageJar(String jarName, BuildOptions properties,
EncryptInterface encryption, ExtraDataInterface extraDataWriter,
List<String> fileExtensionToHandle,
Repack... specialActions) throws IOException {
PackAction[] actionsToRemove;
if (properties.compiler.enableDebugSpeedImprovement) {
actionsToRemove = new PackAction[] {PackAction.Pack, PackAction.Lzma, PackAction.Encrypt};
} else {
actionsToRemove = new PackAction[] {PackAction.Encrypt};
}
boolean release = properties.compiler.release;
String tempJarName = jarName+".tmp";
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tempJarName, false));
jarOutputStream.setLevel(JAR_COMPRESSION_LEVEL);
JarFile jarFile = new JarFile(jarName);
Enumeration<JarEntry> entries = jarFile.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
long time = entry.getTime();
// DO NOT handle manifest dir, subdirs or directories!
if (JarFile.MANIFEST_NAME.equals(name) || entry.isDirectory() || name.indexOf('/') > -1) {
// abusing the system -- but by doing this, we will have our extra data copied over
if (extraDataWriter != null) {
extraDataWriter.write(entry, null);
}
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
} else {
// only handle if we match one of our extensions!
boolean handle = false;
for (String fileExtension : fileExtensionToHandle) {
if (name.endsWith(fileExtension)) {
handle = true;
break;
}
}
if (!handle) {
// abusing the system -- but by doing this, we will have our extra data copied over
if (extraDataWriter != null) {
extraDataWriter.write(entry, null);
}
JarUtil.writeZipEntry(entry, jarFile, jarOutputStream);
} else {
Repack repack = null;
for (Repack specialRepack : specialActions) {
if (name.equals(specialRepack.getName())) {
repack = specialRepack;
break;
}
}
// default is all actions.
if (repack == null) {
repack = new Repack(name, PackAction.Package);
}
// undo PACK, LZMA, GZIP, and encrypt so debug/testing is faster
if (!release) {
repack.remove(actionsToRemove);
}
System.err.print(".");
// load the entry into memory
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
InputStream is = jarFile.getInputStream(entry);
Sys.copyStream(is, baos);
Sys.close(is);
// read the bytes into a buffer.
byte[] entryAsBytes = baos.toByteArray();
PackTask task = new PackTask(repack, entryAsBytes);
task.time = time;
task.debug = properties.compiler.debugEnabled;
task.encryption = encryption;
if (repack.canDo(PackAction.Extract)) {
// this also writes out (and overrides) our custom extra data header
unpackEntry(task, extraDataWriter, jarOutputStream);
} else {
packEntry(task);
// now write (single entry) to the outputStream
// figure out the ACTION file name extension
JarEntry destEntry = new JarEntry(name);
destEntry.setTime(entry.getTime());
if (extraDataWriter != null) {
extraDataWriter.write(destEntry, task);
}
jarOutputStream.putNextEntry(destEntry);
Sys.copyStream(task.inputStream, jarOutputStream);
jarOutputStream.flush();
jarOutputStream.closeEntry();
}
}
}
}
jarOutputStream.finish();
Sys.close(jarOutputStream);
jarFile.close();
System.err.println(".");
FileUtil.moveFile(tempJarName, jarName);
}
private static void unpackEntry(PackTask task, ExtraDataInterface extraDataWriter, JarOutputStream jarOutputStream) {
InputStream inputStream = task.inputStream;
Repack repack = task.pack;
// sometimes we want to extract the contents of a compressed file to the root of our 'box' file!
// supports tar, tar.gz, gzip, zip
String name = repack.getName();
String extension = repack.getExtension();
if (extension.endsWith("tar")) {
TarInputStream newInputStream = null;
newInputStream = new TarInputStream(inputStream);
try {
TarEntry entry;
while ((entry = newInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String name2 = entry.getName();
// now write (the inside entry) to the outputStream
// figure out the ACTION file name extension
JarEntry destEntry = new JarEntry(name2);
destEntry.setTime(entry.getModTime().getTime());
if (extraDataWriter != null) {
extraDataWriter.write(destEntry, task);
}
jarOutputStream.putNextEntry(destEntry);
Sys.copyStream(newInputStream, jarOutputStream);
jarOutputStream.flush();
jarOutputStream.closeEntry();
}
} catch (Exception e) {
System.err.println("Unable to extract contents of tar file!");
}
} else {
if (extension.equals("gz") || extension.equals("gzip")) {
TarInputStream newInputStream = null;
GZIPInputStream gzipInputStream = null;
try {
if (name.endsWith("tar.gz") || name.endsWith("tar.gzip")) {
// ungzip AND untar
gzipInputStream = new GZIPInputStream(inputStream);
newInputStream = new TarInputStream(gzipInputStream);
TarEntry entry;
while ((entry = newInputStream.getNextEntry()) != null) {
if (entry.isDirectory()) {
continue;
}
String name2 = entry.getName();
// now write (the inside entry) to the outputStream
// figure out the ACTION file name extension
JarEntry destEntry = new JarEntry(name2);
destEntry.setTime(entry.getModTime().getTime());
if (extraDataWriter != null) {
extraDataWriter.write(destEntry, task);
}
jarOutputStream.putNextEntry(destEntry);
Sys.copyStream(newInputStream, jarOutputStream);
jarOutputStream.flush();
jarOutputStream.closeEntry();
}
} else {
// ONLY ungzip (gzip only works on ONE file)
// this is a regular file (such as a txt file, etc)
// now write (the inside entry) to the outputStream
// figure out the ACTION file name extension
JarEntry destEntry = new JarEntry(name);
destEntry.setTime(task.time); // set the time to whatever the compressed entry time was
if (extraDataWriter != null) {
extraDataWriter.write(destEntry, task);
}
jarOutputStream.putNextEntry(destEntry);
gzipInputStream = new GZIPInputStream(inputStream);
byte[] buffer = new byte[8192];
int read = 0;
while ((read = gzipInputStream.read(buffer)) > 0) {
jarOutputStream.write(buffer, 0, read);
}
jarOutputStream.flush();
jarOutputStream.closeEntry();
}
} catch (Exception e) {
System.err.println("Unable to extract contents of compressed file!");
}
Sys.close(newInputStream);
Sys.close(gzipInputStream);
} else {
// input stream can be fileInputStream (if it was a file)
// or a bytearrayinput stream if it was a stream from another file
boolean isZip = false;
if (inputStream instanceof FileInputStream) {
File file = new File(name);
isZip = isZipFile(file);
} else {
ByteArrayInputStream s = (ByteArrayInputStream) inputStream;
isZip = isZipStream(s);
}
// can be zip (ie: jar)
if (isZip) {
ZipInputStream zipInputStream = null;
try {
zipInputStream = new ZipInputStream(inputStream);
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
// create a new entry to avoid ZipException: invalid entry compressed size
// we want to COPY this over. hashes should remain the same between builds!
if (extraDataWriter != null) {
extraDataWriter.write(entry, task);
}
writeZipEntry(entry, zipInputStream, jarOutputStream);
}
} catch (Exception e) {
System.err.println("Unable to extract contents of compressed file!");
}
Sys.close(zipInputStream);
} else {
System.err.println("Unable to extract contents of compressed file!");
}
}
}
}
private static void packEntry(PackTask task) throws IOException {
InputStream inputStream = task.inputStream;
int length = task.length;
Repack repack = task.pack;
// now handle pack/compress/encrypt
if (Pack200Util.canPack200(repack, task.inputStream)) {
// Create the Packer object
ByteArrayOutputStream outputPackStream = Pack200Util.Java.pack200(inputStream, task.debug);
// convert the output stream to an input stream
inputStream = new ByteArrayInputStream(outputPackStream.toByteArray());
length = inputStream.available();
}
// we RELY on the the jar ALREADY being NORMALIZED (PACK+UNPACK). pack200 -repack DOES NOT WORK! You must EXPLICITY
// use the programmatic safePack200 and safeUnpack200 so the jar will be consistent between pack/unpack cycles.
if (repack.canDo(PackAction.LoadLibray)) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(8192);
Sys.copyStream(inputStream, baos);
// we make it NOT pack, and since we ARE NOT modifying the input stream, it's safe to read it directly
byte[] unpackBuffer = baos.toByteArray();
int unpackLength = unpackBuffer.length;
SHA512Digest digest = new SHA512Digest();
// now run the hash on it!
byte[] hashBytes = new byte[digest.getDigestSize()];
digest.update(unpackBuffer, 0, unpackLength);
digest.doFinal(hashBytes, 0);
task.extraData = hashBytes;
// since we can only read the input stream once, make sure to make it again.
inputStream = new ByteArrayInputStream(unpackBuffer);
}
if (repack.canDo(PackAction.Lzma)) {
ByteArrayOutputStream packedOutputStream = new ByteArrayOutputStream(length); // will be size or smaller.
LZMA.encode(length, inputStream, packedOutputStream);
Sys.close(inputStream);
// convert the output stream to an input stream
inputStream = new ByteArrayInputStream(packedOutputStream.toByteArray());
length = inputStream.available();
}
// we cannot do BOTH encrypt + LGPL. They are mutually exclusive.
// LGPL will also not be hashed in the signature generation
if (repack.canDo(PackAction.Encrypt) && !repack.canDo(PackAction.LGPL)) {
if (task.encryption != null) {
ByteArrayOutputStream encryptedOutputStream = task.encryption.encrypt(inputStream, length);
// convert the output stream to an input stream
inputStream = new ByteArrayInputStream(encryptedOutputStream.toByteArray());
length = inputStream.available();
} else {
throw new RuntimeException("** Unable to encrypt data when AES information is null!!");
}
}
task.inputStream = inputStream;
}
/**
* Merge the specified files into the primaryFile
*
* @param primaryFile This is the file that will contain all of the other files.
* @param files Array of files (zips/jars) to be added into the primary file
* @throws IOException
* @throws FileNotFoundException
*/
public static void merge(File primaryFile, File... files) throws FileNotFoundException, IOException {
String[] fileNames = new String[files.length];
for (int i=0; i<files.length; i++) {
fileNames[i] = files[i].getAbsolutePath();
}
merge(primaryFile.getAbsolutePath(), fileNames);
}
/**
* Merge the specified files into the primaryFile
*
* @param primaryFile This is the file that will contain all of the other files.
* @param files Array of files (zips/jars) to be added into the primary file
* @throws IOException
* @throws FileNotFoundException
*/
public static void merge(String primaryFile, String... files) throws FileNotFoundException, IOException {
Build.log().message("Merging files into single jar/zip: '" + primaryFile + "'");
// write everything to staging dir, then jar it up.
String tempDirectory = FileUtil.tempDirectory("mergeTemp");
File mergeLocation = new File(tempDirectory);
FileUtil.unzipJar(primaryFile, mergeLocation.getAbsolutePath(), true);
for (String fileName : files) {
File file = new File(fileName);
// is this a zip?
if (FileUtil.isZipFile(file)) {
FileUtil.unzipJar(file, mergeLocation, false);
} else {
// just copy it over
String relativeToDir = FileUtil.getChildRelativeToDir(file, "src");
FileUtil.copyFile(file, new File(mergeLocation, relativeToDir));
}
}
JarOptions options = new JarOptions();
options.outputFile = primaryFile;
options.inputPaths = new Paths(tempDirectory);
options.mainClass = null;
options.otherManifestAttributes = null;
options.classpath = null;
JarUtil.jar(options);
// cleanup
FileUtil.delete(mergeLocation);
}
}