The attached task is based on the <uptodate> task but instead sets a property if the files are byte-by-byte different. In addition, the task is smart enough to try the files as java.util.zip.ZipFile's and byte-by-byte compare each of their entries.
We have found this task to be very useful, especially in the context of a nightly build.
Attachments: Diff.java - the task, under Apache license, with JavaDocs TestDiff.java - JUnit tests for Diff.java, under Apache license diff.html - some documentation _________________________________________________________________ Get your FREE download of MSN Explorer at http://explorer.msn.com
/* * The Apache Software License, Version 1.1 * * Copyright (c) 2000 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. */
package org.apache.tools.ant.taskdefs.optional.diff; import org.apache.tools.ant.DirectoryScanner; import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.taskdefs.MatchingTask; import org.apache.tools.ant.types.FileSet; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.InputStream; import java.io.IOException; import java.util.Enumeration; import java.util.Vector; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; /** * Will set the given property if the specified target is byte-to-byte * different from at least one the source files. * * Based on [EMAIL PROTECTED] org.apache.tools.ant.taskdefs.optional.UpToDate}. * * @author Chuck Burdick <a href="mailto:[EMAIL PROTECTED]">[EMAIL PROTECTED]</a> */ public class Diff extends MatchingTask { private String _property; private File _targetFile; private Vector sourceFileSets = new Vector(); private boolean _checkManifest = true; /** * The property to set if the target file is is byte-to-byte * different from at least one of the source files. * * @param property the name of the property to set if Target is up to date. */ public void setProperty(String property) { _property = property; } /** * The file which must be byte-to-byte different from at least one of the source files * if the property is to be set. * * @param file the file which we are checking against. */ public void setTargetFile(File file) { _targetFile = file; } /** * Sets whether to pay attention to the manifest file or not. * * @param checkManifest defaults to true */ public void setCheckManifest(boolean checkManifest) { _checkManifest = checkManifest; } /** * Nested <srcfiles> element. */ public void addSrcfiles(FileSet fs) { sourceFileSets.addElement(fs); } /** * Sets property to true if target file is byte-to-byte different from * at least one of the source files. */ public void execute() throws BuildException { if (sourceFileSets.size() == 0) { throw new BuildException("At least one <srcfiles> element must be set"); } if (_targetFile == null) { throw new BuildException("The targetfile attribute must be set"); } // if not there then it can't be equivalent if (!_targetFile.exists()) return; Enumeration enum = sourceFileSets.elements(); boolean different = false; while (!different && enum.hasMoreElements()) { FileSet fs = (FileSet)enum.nextElement(); DirectoryScanner ds = fs.getDirectoryScanner(project); for (int i = 0; i < ds.getIncludedFiles().length; i++) { String curFile = ds.getIncludedFiles()[i]; } different = dirHasDiffs(fs.getDir(project), _targetFile, ds.getIncludedFiles()); } if (different) { this.project.setProperty(_property, "true"); } } /** * Checks every file specified in the directory against the target file. * * @param srcDir The directory in which to look for files * @param destFile The file to compare against * @param files[] A list of filenames within the directory for comparison */ protected boolean dirHasDiffs(File srcDir, File destFile, String files[]) throws BuildException { boolean different = false; int i = 0; while (!different && i < files.length) { File srcFile = new File(srcDir, files[i]); log("Checking " + srcFile.toString() + " for differences against " + destFile.toString(), Project.MSG_VERBOSE); different = different(srcFile, destFile); i++; } return different; } /** * Checks to see if the given files are byte-to-byte different in a Zip-aware and Jar-aware way. */ protected boolean different(File srcFile, File destFile) throws BuildException { boolean different = true; try { different = differentZips(srcFile, destFile); } catch (ZipException e) { log("Neither file is a ZIP or JAR. Checking as regular files", Project.MSG_DEBUG); if (srcFile.length() == destFile.length()) { log("Files are same length. Further investigation needed.", Project.MSG_DEBUG); try { InputStream srcBytes = new BufferedInputStream(new FileInputStream(srcFile)); InputStream destBytes = new BufferedInputStream(new FileInputStream(destFile)); different = differentStreams(srcBytes, destBytes); } catch (FileNotFoundException ex) { throw new BuildException("The targetfile could not be found"); } catch (IOException ex) { throw new BuildException("IO exception while reading targetfile"); } } else { log(srcFile.toString() + " has length " + srcFile.length(), Project.MSG_DEBUG); log(destFile.toString() + " has length " + destFile.length(), Project.MSG_DEBUG); log("Files are different lengths", Project.MSG_DEBUG); } } catch (FileNotFoundException e) { throw new BuildException("The targetfile could not be found"); } catch (IOException e) { throw new BuildException("IO exception while reading targetfile"); } if (different) { log(srcFile.toString() + " and " + destFile.toString() + " are different", Project.MSG_VERBOSE); } else { log(srcFile.toString() + " and " + destFile.toString() + " are identical", Project.MSG_VERBOSE); } return different; } /** * Checks to see if the given files are byte-to-byte equivalent in a Zip-aware and Jar-aware way. * * @see different */ protected boolean same(File srcFile, File destFile) throws BuildException { return !different(srcFile, destFile); } /** * Checks to see if the given streams are byte-to-byte different */ protected boolean differentStreams(InputStream srcBytes, InputStream destBytes) throws IOException { log("Checking binary streams for differences", Project.MSG_DEBUG); boolean matchesSoFar = true; int src = -1; boolean started = false; while (matchesSoFar && ((src = srcBytes.read()) != -1)) { started = true; int dest = destBytes.read(); matchesSoFar = ((dest != -1) && (src == dest)); } if (!started) { // Source stream was empty if (destBytes.read() != -1) { // Dest stream not empty log("Source stream was empty, but dest stream was not", Project.MSG_DEBUG); matchesSoFar = false; } } srcBytes.close(); destBytes.close(); if (matchesSoFar) { log("Streams are identical", Project.MSG_DEBUG); } else { log("Streams are different", Project.MSG_DEBUG); } return !matchesSoFar; } /** * Checks to see if the given files are byte-to-byte different in a Zip-aware and Jar-aware way. * Throws a ZipException if neither file is a valid [EMAIL PROTECTED] ZipFile}. If only one of the files * is a valid [EMAIL PROTECTED] ZipFile} then indicates that the files are different. */ protected boolean differentZips(File srcFile, File destFile) throws IOException, BuildException { boolean different = false; File files[] = {srcFile, destFile}; ZipFile zipFiles[] = {null, null}; int filesAreZips = 0; for (int i = 0; i < 2; i++) { try { zipFiles[i] = new ZipFile(files[i]); filesAreZips = filesAreZips + (int)Math.pow(2, i); } catch (ZipException e) { // do nothing } } if (filesAreZips < 0 || filesAreZips > 3) { throw new BuildException("Error when checking if files are ZIP or JAR files"); } switch(filesAreZips) { case 0: throw new ZipException("Neither file is a JAR file"); case 1: different = true; break; case 2: different = true; break; case 3: different = differentZipContents(zipFiles[0], zipFiles[1]); break; } return different; } /** * Checks the contents of the given [EMAIL PROTECTED] ZipFile}s to see if they are byte-to-byte different. */ protected boolean differentZipContents(ZipFile srcZip, ZipFile destZip) throws IOException { boolean different = false; log("Checking ZipFile contents for differences", Project.MSG_DEBUG); if (srcZip.size() != destZip.size()) { log(srcZip.getName() + " has " + srcZip.size() + " entries", Project.MSG_DEBUG); log(destZip.getName() + " has " + destZip.size() + " entries", Project.MSG_DEBUG); different = true; } else { Enumeration entries = srcZip.entries(); while (!different && entries.hasMoreElements()) { ZipEntry curSrcEntry = (ZipEntry)entries.nextElement(); if (!_checkManifest && curSrcEntry.getName().equals(JarFile.MANIFEST_NAME)) { log("Ignoring manifest entry", Project.MSG_DEBUG); } else { ZipEntry curDestEntry = destZip.getEntry(curSrcEntry.getName()); different = differentZipEntries(srcZip, curSrcEntry, destZip, curDestEntry); } } } return different; } /** * Checks the contents of the given [EMAIL PROTECTED] ZipEntry}s to see if they are byte-to-byte different. */ protected boolean differentZipEntries(ZipFile srcFile, ZipEntry srcEntry, ZipFile destFile, ZipEntry destEntry) throws IOException { boolean different = false; if (srcEntry == null && destEntry != null) { log("Source file does not contain ZipEntry " + destEntry.getName(), Project.MSG_DEBUG); different = true; } else if (srcEntry != null && destEntry == null) { log("Destination file does not contain ZipEntry " + srcEntry.getName(), Project.MSG_DEBUG); different = true; } else { if (srcEntry.getSize() != destEntry.getSize()) { log("Size of ZipEntry " + srcEntry.getName() + " is " + srcEntry.getSize(), Project.MSG_DEBUG); log("Size of ZipEntry " + destEntry.getName() + " is " + destEntry.getSize(), Project.MSG_DEBUG); different = true; } else { log("Checking ZipEntry " + srcEntry.getName() + " for differences", Project.MSG_DEBUG); InputStream src = srcFile.getInputStream(srcEntry); InputStream dest = destFile.getInputStream(destEntry); different = differentStreams(src, dest); } } return different; } }
/* * The Apache Software License, Version 1.1 * * Copyright (c) 2000 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * * 3. The end-user documentation included with the redistribution, if * any, must include the following acknowlegement: * "This product includes software developed by the * Apache Software Foundation (http://www.apache.org/)." * Alternately, this acknowlegement may appear in the software itself, * if and wherever such third-party acknowlegements normally appear. * * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software * Foundation" must not be used to endorse or promote products derived * from this software without prior written permission. For written * permission, please contact [EMAIL PROTECTED] * * 5. Products derived from this software may not be called "Apache" * nor may "Apache" appear in their names without prior written * permission of the Apache Group. * * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF * SUCH DAMAGE. * ==================================================================== * * This software consists of voluntary contributions made by many * individuals on behalf of the Apache Software Foundation. For more * information on the Apache Software Foundation, please see * <http://www.apache.org/>. */ package org.apache.tools.ant.taskdefs.optional.diff; import junit.framework.*; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import org.apache.tools.ant.Project; /** * JUnit tests against Diff.java * * @author Chuck Burdick <a href="mailto:[EMAIL PROTECTED]">[EMAIL PROTECTED]</a> */ public class TestDiff extends TestCase { private int _counter = 0; private File _sysTempDir = new File(System.getProperty("java.io.tmpdir")); private Set _deleteFiles = null; private Diff _diffTask = null; private Project _project = null; public TestDiff(String testName) { super(testName); } public static Test suite() { return new TestSuite(TestDiff.class); } public static void main(String args[]) { String[] testCaseName = {TestDiff.class.getName()}; junit.textui.TestRunner.main(testCaseName); } public void setUp() { _project = new Project(); _project.setName("testProject"); _project.addTaskDefinition("diff", Diff.class); _diffTask = (Diff)_project.createTask("diff"); _counter = 0; _deleteFiles = new TreeSet(); } public void tearDown() { Set tryAgain = deleteFiles(_deleteFiles); /* if (!tryAgain.isEmpty()) { System.out.println("Failed to delete: " + tryAgain.toString()); } */ } // === TESTS === public void testTempDir() throws IOException { File dir = tempDir(); assert("Should exist and be a directory", dir.isDirectory()); assert("Should be readable", dir.canRead()); assert("Should be writable", dir.canWrite()); } public void testTempFile() throws IOException { File temp = getTempFile(); assert("Should exist and be a file", temp.isFile()); assert("Should be readable", temp.canRead()); assert("Should be writable", temp.canWrite()); } public void testTempFileSameName() throws IOException { File temp1 = getTempFile(); File temp2 = getTempFile(temp1.getName()); assertEquals("Files should have same name", temp1.getName(), temp2.getName()); } public void testShouldNotDiffCopy() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = copyFile(aFile); assert("Files should be equal", _diffTask.same(aFile, anotherFile)); assert("All files in parent directory should be equal", !_diffTask.dirHasDiffs(aFile.getParentFile(), anotherFile, aFile.getParentFile().list())); } public void testShouldNotDiffSame() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = dummyFile("This is a test"); assert("Files should be equal", _diffTask.same(aFile, anotherFile)); assert("All files in parent directory should be equal", !_diffTask.dirHasDiffs(aFile.getParentFile(), anotherFile, aFile.getParentFile().list())); } public void testShouldDiffLength() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = dummyFile("This is bogus text"); assert("Files should not be equal", _diffTask.different(aFile, anotherFile)); assert("At least one file in parent directory should be different", _diffTask.dirHasDiffs(aFile.getParentFile(), anotherFile, aFile.getParentFile().list())); } public void testShouldDiffContent() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = dummyFile("This is a text"); assert("Files should be same size", aFile.length() == anotherFile.length()); assert("Files should not be equal", _diffTask.different(aFile, anotherFile)); assert("At least one file in parent directory should be different", _diffTask.dirHasDiffs(aFile.getParentFile(), anotherFile, aFile.getParentFile().list())); } public void testShouldNotDiffZip() throws IOException { File aFile = dummyFile("This is a test"); File files[] = {aFile}; ZipFile aZip = dummyZipFile(files); ZipFile anotherZip = dummyZipFile(files); assert("Files should be equal", !_diffTask.differentZipContents(aZip, anotherZip)); } public void testShouldDiffZipNumEntries() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = dummyFile("This is another test"); File oneFile[] = {aFile}; File twoFiles[] = {aFile, anotherFile}; ZipFile aZip = dummyZipFile(oneFile); ZipFile anotherZip = dummyZipFile(twoFiles); assert("Files should be different", _diffTask.differentZipContents(aZip, anotherZip)); } public void testShouldDiffZipDiffBytes() throws IOException { File aFile = dummyFile("This is a test"); File anotherFile = dummyFile("This is a text", aFile.getName()); assertEquals("Files should have same name", aFile.getName(), anotherFile.getName()); assert("Files should have same size", aFile.length() == anotherFile.length()); File fileA[] = {aFile}; File fileB[] = {anotherFile}; ZipFile aZip = dummyZipFile(fileA); ZipFile anotherZip = dummyZipFile(fileB); assert("Files should be different", _diffTask.differentZipContents(aZip, anotherZip)); ZipEntry anEntry = aZip.getEntry(aFile.getName()); ZipEntry anotherEntry = anotherZip.getEntry(aFile.getName()); assert("Should be valid entries", anEntry != null); assert("Should be valid entries", anotherEntry != null); assert("Entries should be same size", anEntry.getSize() == anotherEntry.getSize()); assert("Should be different entries", _diffTask.differentZipEntries(aZip, anEntry, anotherZip, anotherEntry)); InputStream srcBytes = aZip.getInputStream(aZip.getEntry(aFile.getName())); InputStream destBytes = anotherZip.getInputStream(anotherZip.getEntry(aFile.getName())); assert("Streams should be different", _diffTask.differentStreams(srcBytes, destBytes)); } public void testSpecificFilesDiff() throws IOException { String fileNameA = System.getProperty("test.diff.file.src"); String fileNameB = System.getProperty("test.diff.file.dest"); if (fileNameA != null && fileNameB != null) { File fileA = new File(fileNameA); File fileB = new File(fileNameB); assert("Should be valid file", fileA.isFile()); assert("Should be readable", fileA.canRead()); assert("Should be valid file", fileB.isFile()); assert("Should be readable", fileB.canRead()); assert("Files should be different", _diffTask.different(fileA, fileB)); } } public void testSpecificFilesSame() throws IOException { String fileNameA = System.getProperty("test.same.file.src"); String fileNameB = System.getProperty("test.same.file.dest"); if (fileNameA != null && fileNameB != null) { File fileA = new File(fileNameA); File fileB = new File(fileNameB); assert("Should be valid file", fileA.isFile()); assert("Should be readable", fileA.canRead()); assert("Should be valid file", fileB.isFile()); assert("Should be readable", fileB.canRead()); assert("Files should be different", _diffTask.same(fileA, fileB)); } } // === PRIVATE METHODS === private File copyFile(File fileToCopy) throws IOException { File newFile = getTempFile(); InputStream in = new BufferedInputStream(new FileInputStream(fileToCopy)); OutputStream out = new BufferedOutputStream(new FileOutputStream(newFile)); sendStreams(in, out); return newFile; } private File dummyFile(String outputText) throws IOException { return dummyFile(outputText, null); } private File dummyFile(String outputText, String fileName) throws IOException { File newFile = getTempFile(fileName); InputStream in = new ByteArrayInputStream(outputText.getBytes()); OutputStream out = new BufferedOutputStream(new FileOutputStream(newFile)); sendStreams(in, out); return newFile; } private ZipFile dummyZipFile(File files[]) throws IOException { File newFile = getTempFile(); OutputStream out = new BufferedOutputStream(new FileOutputStream(newFile)); ZipOutputStream zipOut = new ZipOutputStream(out); for (int i = 0; i < files.length; i++) { File curFile = files[i]; ZipEntry curEntry = new ZipEntry(curFile.getName()); zipOut.putNextEntry(curEntry); sendStreams(new BufferedInputStream(new FileInputStream(curFile)), zipOut, false); zipOut.closeEntry(); } zipOut.close(); return new ZipFile(newFile); } private void sendStreams(InputStream in, OutputStream out) throws IOException { sendStreams(in, out, true); } private void sendStreams(InputStream in, OutputStream out, boolean closeOut) throws IOException { int curByte = -1; while ((curByte = in.read()) != -1) { out.write(curByte); } in.close(); if (closeOut) { out.close(); } } private File getTempFile() throws IOException { return getTempFile(null); } private File getTempFile(String name) throws IOException { File result = null; if (name == null) { result = File.createTempFile(tempName(), null, tempDir()); } else if (name != null) { result = new File(tempDir(), name); result.createNewFile(); } _deleteFiles.add(result); return result; } private String tempName() { return "TestDiff-test" + String.valueOf(_counter++); } private File tempDir() { File dir = new File(_sysTempDir, String.valueOf(_counter++)); dir.mkdir(); _deleteFiles.add(dir); return dir; } private Set deleteFiles(Set files) { Set sourceFiles = new TreeSet(files); int count = 0; while (!sourceFiles.isEmpty() && count < 3) { List fileList = new ArrayList(sourceFiles); Collections.reverse(fileList); Iterator i = fileList.iterator(); while (i.hasNext()) { File curFile = (File)i.next(); if (curFile.delete()) { sourceFiles.remove(curFile); } } count++; } return sourceFiles; } }Title: Ant User Manual
Diff
Description
Sets a property if a target file is byte-by-byte different from a set of Source files. In addition, if the source file(s) and target file are java.util.zip.ZipFile's (ZIPs or JARs), then each of the entries are checked byte-by-byte differences. This ensures that the timestamp and file ownership information is ignored, and just the file contents are compared.
Source files are specified by nested <srcfiles> elements, these are FileSets.
The value part of the property being set is true if the contents of the target files are different from the contents of every corresponding source file.
Normally, this task is used to set properties that are useful to avoid target execution depending on whether the specified files have different contents.
Parameters
| Attribute | Description | Required |
| property | the name of the property to set. | Yes |
| targetfile | the file for which we want to determine the status. | Yes |
| checkManifest | a boolean value that specifies whether to consider the manifest file when comparing JARs. | No, defaults to true. |
Examples
<diff property="jarBuild.notRequired" targetfile="${deploy}\myClasses.jar" >
<srcfiles dir= "${dest}/build" includes="myClasses2.jar"/>
</diff>
sets the property jarBuild.notRequired to the value "true"
if the entries in ${deploy}/jarClasses.jar are at all different from the entries in ${dest}/build/myClasses2.jar.
