changeset 1451:0cabe1192c8b

6854795: Miscellaneous improvements to "jar" Summary: cleanup of jar/Main.java (Initial patch by tobyr@google.com, additional review by jeremymanson@google.com, ulf.zibis@gmx.de) Reviewed-by: sherman, alanb
author martin
date Mon, 06 Jul 2009 11:30:40 -0700
parents fa488e4ff685
children 0a294c066e7a
files src/share/classes/sun/tools/jar/Main.java test/tools/jar/index/MetaInf.java
diffstat 2 files changed, 261 insertions(+), 159 deletions(-) [+]
line wrap: on
line diff
--- a/src/share/classes/sun/tools/jar/Main.java	Mon Jul 06 15:13:48 2009 +0200
+++ b/src/share/classes/sun/tools/jar/Main.java	Mon Jul 06 11:30:40 2009 -0700
@@ -26,12 +26,16 @@
 package sun.tools.jar;
 
 import java.io.*;
+import java.nio.file.Path;
 import java.util.*;
 import java.util.zip.*;
 import java.util.jar.*;
 import java.util.jar.Manifest;
 import java.text.MessageFormat;
 import sun.misc.JarIndex;
+import static sun.misc.JarIndex.INDEX_NAME;
+import static java.util.jar.JarFile.MANIFEST_NAME;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 
 /**
  * This class implements a simple utility for creating files in the JAR
@@ -58,7 +62,6 @@
     // Directories specified by "-C" operation.
     Set<String> paths = new HashSet<String>();
 
-    CRC32 crc32 = new CRC32();
     /*
      * cflag: create
      * uflag: update
@@ -71,10 +74,8 @@
      */
     boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag;
 
-    static final String MANIFEST = JarFile.MANIFEST_NAME;
     static final String MANIFEST_DIR = "META-INF/";
     static final String VERSION = "1.0";
-    static final String INDEX = JarIndex.INDEX_NAME;
 
     private static ResourceBundle rsrc;
 
@@ -126,9 +127,21 @@
         this.program = program;
     }
 
+    /**
+     * Creates a new empty temporary file in the same directory as the
+     * specified file.  A variant of File.createTempFile.
+     */
+    private static File createTempFileInSameDirectoryAs(File file)
+        throws IOException {
+        File dir = file.getParentFile();
+        if (dir == null)
+            dir = new File(".");
+        return File.createTempFile("jartmp", null, dir);
+    }
+
     private boolean ok;
 
-    /*
+    /**
      * Starts main program with the specified arguments.
      */
     public synchronized boolean run(String args[]) {
@@ -161,7 +174,7 @@
                     }
                     addVersion(manifest);
                     addCreatedBy(manifest);
-                    if (isAmbigousMainClass(manifest)) {
+                    if (isAmbiguousMainClass(manifest)) {
                         if (in != null) {
                             in.close();
                         }
@@ -195,9 +208,7 @@
                 FileOutputStream out;
                 if (fname != null) {
                     inputFile = new File(fname);
-                    String path = inputFile.getParent();
-                    tmpFile = File.createTempFile("tmp", null,
-                              new File((path == null) ? "." : path));
+                    tmpFile = createTempFileInSameDirectoryAs(inputFile);
                     in = new FileInputStream(inputFile);
                     out = new FileOutputStream(tmpFile);
                 } else {
@@ -208,7 +219,8 @@
                 InputStream manifest = (!Mflag && (mname != null)) ?
                     (new FileInputStream(mname)) : null;
                 expand(null, files, true);
-                boolean updateOk = update(in, new BufferedOutputStream(out), manifest, null);
+                boolean updateOk = update(in, new BufferedOutputStream(out),
+                                          manifest, null);
                 if (ok) {
                     ok = updateOk;
                 }
@@ -270,8 +282,8 @@
         return ok;
     }
 
-    /*
-     * Parse command line arguments.
+    /**
+     * Parses command line arguments.
      */
     boolean parseArgs(String args[]) {
         /* Preprocess and expand @file arguments */
@@ -405,7 +417,7 @@
         return true;
     }
 
-    /*
+    /**
      * Expands list of files to process into full list of all files that
      * can be found by recursively descending directories.
      */
@@ -442,7 +454,7 @@
         }
     }
 
-    /*
+    /**
      * Creates a new JAR file.
      */
     void create(OutputStream out, Manifest manifest)
@@ -461,7 +473,7 @@
             e.setSize(0);
             e.setCrc(0);
             zos.putNextEntry(e);
-            e = new ZipEntry(MANIFEST);
+            e = new ZipEntry(MANIFEST_NAME);
             e.setTime(System.currentTimeMillis());
             if (flag0) {
                 crc32Manifest(e, manifest);
@@ -476,8 +488,32 @@
         zos.close();
     }
 
-    /*
-     * update an existing jar file.
+    private char toUpperCaseASCII(char c) {
+        return (c < 'a' || c > 'z') ? c : (char) (c + 'A' - 'a');
+    }
+
+    /**
+     * Compares two strings for equality, ignoring case.  The second
+     * argument must contain only upper-case ASCII characters.
+     * We don't want case comparison to be locale-dependent (else we
+     * have the notorious "turkish i bug").
+     */
+    private boolean equalsIgnoreCase(String s, String upper) {
+        assert upper.toUpperCase(java.util.Locale.ENGLISH).equals(upper);
+        int len;
+        if ((len = s.length()) != upper.length())
+            return false;
+        for (int i = 0; i < len; i++) {
+            char c1 = s.charAt(i);
+            char c2 = upper.charAt(i);
+            if (c1 != c2 && toUpperCaseASCII(c1) != c2)
+                return false;
+        }
+        return true;
+    }
+
+    /**
+     * Updates an existing jar file.
      */
     boolean update(InputStream in, OutputStream out,
                    InputStream newManifest,
@@ -487,8 +523,6 @@
         ZipOutputStream zos = new JarOutputStream(out);
         ZipEntry e = null;
         boolean foundManifest = false;
-        byte[] buf = new byte[1024];
-        int n = 0;
         boolean updateOk = true;
 
         if (jarIndex != null) {
@@ -499,10 +533,9 @@
         while ((e = zis.getNextEntry()) != null) {
             String name = e.getName();
 
-            boolean isManifestEntry = name.toUpperCase(
-                                            java.util.Locale.ENGLISH).
-                                        equals(MANIFEST);
-            if ((name.toUpperCase().equals(INDEX) && jarIndex != null)
+            boolean isManifestEntry = equalsIgnoreCase(name, MANIFEST_NAME);
+
+            if ((jarIndex != null && equalsIgnoreCase(name, INDEX_NAME))
                 || (Mflag && isManifestEntry)) {
                 continue;
             } else if (isManifestEntry && ((newManifest != null) ||
@@ -513,9 +546,9 @@
                     // might need it below, and we can't re-read the same data
                     // twice.
                     FileInputStream fis = new FileInputStream(mname);
-                    boolean ambigous = isAmbigousMainClass(new Manifest(fis));
+                    boolean ambiguous = isAmbiguousMainClass(new Manifest(fis));
                     fis.close();
-                    if (ambigous) {
+                    if (ambiguous) {
                         return false;
                     }
                 }
@@ -539,9 +572,7 @@
                         e2.setCrc(e.getCrc());
                     }
                     zos.putNextEntry(e2);
-                    while ((n = zis.read(buf, 0, buf.length)) != -1) {
-                        zos.write(buf, 0, n);
-                    }
+                    copy(zis, zos);
                 } else { // replace with the new files
                     File f = entryMap.get(name);
                     addFile(zos, f);
@@ -558,7 +589,7 @@
         if (!foundManifest) {
             if (newManifest != null) {
                 Manifest m = new Manifest(newManifest);
-                updateOk = !isAmbigousMainClass(m);
+                updateOk = !isAmbiguousMainClass(m);
                 if (updateOk) {
                     updateManifest(m, zos);
                 }
@@ -575,23 +606,16 @@
     private void addIndex(JarIndex index, ZipOutputStream zos)
         throws IOException
     {
-        ZipEntry e = new ZipEntry(INDEX);
+        ZipEntry e = new ZipEntry(INDEX_NAME);
         e.setTime(System.currentTimeMillis());
         if (flag0) {
-            e.setMethod(ZipEntry.STORED);
-            File ifile = File.createTempFile("index", null, new File("."));
-            BufferedOutputStream bos = new BufferedOutputStream
-                (new FileOutputStream(ifile));
-            index.write(bos);
-            crc32File(e, ifile);
-            bos.close();
-            ifile.delete();
+            CRC32OutputStream os = new CRC32OutputStream();
+            index.write(os);
+            os.updateEntry(e);
         }
         zos.putNextEntry(e);
         index.write(zos);
-        if (vflag) {
-            // output(getMsg("out.update.manifest"));
-        }
+        zos.closeEntry();
     }
 
     private void updateManifest(Manifest m, ZipOutputStream zos)
@@ -602,10 +626,9 @@
         if (ename != null) {
             addMainClass(m, ename);
         }
-        ZipEntry e = new ZipEntry(MANIFEST);
+        ZipEntry e = new ZipEntry(MANIFEST_NAME);
         e.setTime(System.currentTimeMillis());
         if (flag0) {
-            e.setMethod(ZipEntry.STORED);
             crc32Manifest(e, m);
         }
         zos.putNextEntry(e);
@@ -620,7 +643,8 @@
         name = name.replace(File.separatorChar, '/');
         String matchPath = "";
         for (String path : paths) {
-            if (name.startsWith(path) && (path.length() > matchPath.length())) {
+            if (name.startsWith(path)
+                && (path.length() > matchPath.length())) {
                 matchPath = path;
             }
         }
@@ -658,7 +682,7 @@
         global.put(Attributes.Name.MAIN_CLASS, mainApp);
     }
 
-    private boolean isAmbigousMainClass(Manifest m) {
+    private boolean isAmbiguousMainClass(Manifest m) {
         if (ename != null) {
             Attributes global = m.getMainAttributes();
             if ((global.get(Attributes.Name.MAIN_CLASS) != null)) {
@@ -670,7 +694,7 @@
         return false;
     }
 
-    /*
+    /**
      * Adds a new file entry to the ZIP output stream.
      */
     void addFile(ZipOutputStream zos, File file) throws IOException {
@@ -684,7 +708,7 @@
 
         if (name.equals("") || name.equals(".") || name.equals(zname)) {
             return;
-        } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST))
+        } else if ((name.equals(MANIFEST_DIR) || name.equals(MANIFEST_NAME))
                    && !Mflag) {
             if (vflag) {
                 output(formatMsg("out.ignore.entry", name));
@@ -704,19 +728,11 @@
             e.setSize(0);
             e.setCrc(0);
         } else if (flag0) {
-            e.setSize(size);
-            e.setMethod(ZipEntry.STORED);
             crc32File(e, file);
         }
         zos.putNextEntry(e);
         if (!isDir) {
-            byte[] buf = new byte[8192];
-            int len;
-            InputStream is = new BufferedInputStream(new FileInputStream(file));
-            while ((len = is.read(buf, 0, buf.length)) != -1) {
-                zos.write(buf, 0, len);
-            }
-            is.close();
+            copy(file, zos);
         }
         zos.closeEntry();
         /* report how much compression occurred. */
@@ -737,39 +753,83 @@
         }
     }
 
-    /*
-     * compute the crc32 of a file.  This is necessary when the ZipOutputStream
-     * is in STORED mode.
+    /**
+     * A buffer for use only by copy(InputStream, OutputStream).
+     * Not as clean as allocating a new buffer as needed by copy,
+     * but significantly more efficient.
+     */
+    private byte[] copyBuf = new byte[8192];
+
+    /**
+     * Copies all bytes from the input stream to the output stream.
+     * Does not close or flush either stream.
+     *
+     * @param from the input stream to read from
+     * @param to the output stream to write to
+     * @throws IOException if an I/O error occurs
+     */
+    private void copy(InputStream from, OutputStream to) throws IOException {
+        int n;
+        while ((n = from.read(copyBuf)) != -1)
+            to.write(copyBuf, 0, n);
+    }
+
+    /**
+     * Copies all bytes from the input file to the output stream.
+     * Does not close or flush the output stream.
+     *
+     * @param from the input file to read from
+     * @param to the output stream to write to
+     * @throws IOException if an I/O error occurs
+     */
+    private void copy(File from, OutputStream to) throws IOException {
+        InputStream in = new FileInputStream(from);
+        try {
+            copy(in, to);
+        } finally {
+            in.close();
+        }
+    }
+
+    /**
+     * Copies all bytes from the input stream to the output file.
+     * Does not close the input stream.
+     *
+     * @param from the input stream to read from
+     * @param to the output file to write to
+     * @throws IOException if an I/O error occurs
+     */
+    private void copy(InputStream from, File to) throws IOException {
+        OutputStream out = new FileOutputStream(to);
+        try {
+            copy(from, out);
+        } finally {
+            out.close();
+        }
+    }
+
+    /**
+     * Computes the crc32 of a Manifest.  This is necessary when the
+     * ZipOutputStream is in STORED mode.
      */
     private void crc32Manifest(ZipEntry e, Manifest m) throws IOException {
-        crc32.reset();
-        CRC32OutputStream os = new CRC32OutputStream(crc32);
+        CRC32OutputStream os = new CRC32OutputStream();
         m.write(os);
-        e.setSize((long) os.n);
-        e.setCrc(crc32.getValue());
+        os.updateEntry(e);
     }
 
-    /*
-     * compute the crc32 of a file.  This is necessary when the ZipOutputStream
-     * is in STORED mode.
+    /**
+     * Computes the crc32 of a File.  This is necessary when the
+     * ZipOutputStream is in STORED mode.
      */
     private void crc32File(ZipEntry e, File f) throws IOException {
-        InputStream is = new BufferedInputStream(new FileInputStream(f));
-        byte[] buf = new byte[8192];
-        crc32.reset();
-        int r = 0;
-        int nread = 0;
-        long len = f.length();
-        while ((r = is.read(buf)) != -1) {
-            nread += r;
-            crc32.update(buf, 0, r);
-        }
-        is.close();
-        if (nread != (int) len) {
+        CRC32OutputStream os = new CRC32OutputStream();
+        copy(f, os);
+        if (os.n != f.length()) {
             throw new JarException(formatMsg(
                         "error.incorrect.length", f.getPath()));
         }
-        e.setCrc(crc32.getValue());
+        os.updateEntry(e);
     }
 
     void replaceFSC(String files[]) {
@@ -780,6 +840,7 @@
         }
     }
 
+    @SuppressWarnings("serial")
     Set<ZipEntry> newDirSet() {
         return new HashSet<ZipEntry>() {
             public boolean add(ZipEntry e) {
@@ -797,7 +858,7 @@
         }
     }
 
-    /*
+    /**
      * Extracts specified entries from JAR file.
      */
     void extract(InputStream in, String files[]) throws IOException {
@@ -827,7 +888,7 @@
         updateLastModifiedTime(dirs);
     }
 
-    /*
+    /**
      * Extracts specified entries from JAR file, via ZipFile.
      */
     void extract(String fname, String files[]) throws IOException {
@@ -853,7 +914,7 @@
         updateLastModifiedTime(dirs);
     }
 
-    /*
+    /**
      * Extracts next entry from JAR file, creating directories as needed.  If
      * the entry is for a directory which doesn't exist prior to this
      * invocation, returns that entry, otherwise returns null.
@@ -888,19 +949,13 @@
                         "error.create.dir", d.getPath()));
                 }
             }
-            OutputStream os = new FileOutputStream(f);
-            byte[] b = new byte[8192];
-            int len;
             try {
-                while ((len = is.read(b, 0, b.length)) != -1) {
-                    os.write(b, 0, len);
-                }
+                copy(is, f);
             } finally {
                 if (is instanceof ZipInputStream)
                     ((ZipInputStream)is).closeEntry();
                 else
                     is.close();
-                os.close();
             }
             if (vflag) {
                 if (e.getMethod() == ZipEntry.DEFLATED) {
@@ -919,7 +974,7 @@
         return rc;
     }
 
-    /*
+    /**
      * Lists contents of JAR file.
      */
     void list(InputStream in, String files[]) throws IOException {
@@ -937,7 +992,7 @@
         }
     }
 
-    /*
+    /**
      * Lists contents of JAR file, via ZipFile.
      */
     void list(String fname, String files[]) throws IOException {
@@ -950,32 +1005,38 @@
     }
 
     /**
-     * Output the class index table to the INDEX.LIST file of the
+     * Outputs the class index table to the INDEX.LIST file of the
      * root jar file.
      */
     void dumpIndex(String rootjar, JarIndex index) throws IOException {
-        File scratchFile = File.createTempFile("scratch", null, new File("."));
         File jarFile = new File(rootjar);
-        boolean updateOk = update(new FileInputStream(jarFile),
-                                  new FileOutputStream(scratchFile),
-                                  null, index);
-        jarFile.delete();
-        if (!scratchFile.renameTo(jarFile)) {
-            scratchFile.delete();
-            throw new IOException(getMsg("error.write.file"));
+        Path jarPath = jarFile.toPath();
+        Path tmpPath = createTempFileInSameDirectoryAs(jarFile).toPath();
+        try {
+            if (update(jarPath.newInputStream(),
+                       tmpPath.newOutputStream(),
+                       null, index)) {
+                try {
+                    tmpPath.moveTo(jarPath, REPLACE_EXISTING);
+                } catch (IOException e) {
+                    throw new IOException(getMsg("error.write.file"), e);
+                }
+            }
+        } finally {
+            tmpPath.deleteIfExists();
         }
-        scratchFile.delete();
     }
 
-    private Hashtable jarTable = new Hashtable();
-    /*
-     * Generate the transitive closure of the Class-Path attribute for
+    private HashSet<String> jarPaths = new HashSet<String>();
+
+    /**
+     * Generates the transitive closure of the Class-Path attribute for
      * the specified jar file.
      */
-    Vector getJarPath(String jar) throws IOException {
-        Vector files = new Vector();
+    List<String> getJarPath(String jar) throws IOException {
+        List<String> files = new ArrayList<String>();
         files.add(jar);
-        jarTable.put(jar, jar);
+        jarPaths.add(jar);
 
         // take out the current path
         String path = jar.substring(0, Math.max(0, jar.lastIndexOf('/') + 1));
@@ -998,7 +1059,7 @@
                             if (!ajar.endsWith("/")) {  // it is a jar file
                                 ajar = path.concat(ajar);
                                 /* check on cyclic dependency */
-                                if (jarTable.get(ajar) == null) {
+                                if (! jarPaths.contains(ajar)) {
                                     files.addAll(getJarPath(ajar));
                                 }
                             }
@@ -1012,10 +1073,10 @@
     }
 
     /**
-     * Generate class index file for the specified root jar file.
+     * Generates class index file for the specified root jar file.
      */
     void genIndex(String rootjar, String[] files) throws IOException {
-        Vector jars = getJarPath(rootjar);
+        List<String> jars = getJarPath(rootjar);
         int njars = jars.size();
         String[] jarfiles;
 
@@ -1027,12 +1088,12 @@
             }
             njars = jars.size();
         }
-        jarfiles = (String[])jars.toArray(new String[njars]);
+        jarfiles = jars.toArray(new String[njars]);
         JarIndex index = new JarIndex(jarfiles);
         dumpIndex(rootjar, index);
     }
 
-    /*
+    /**
      * Prints entry information, if requested.
      */
     void printEntry(ZipEntry e, String[] files) throws IOException {
@@ -1049,7 +1110,7 @@
         }
     }
 
-    /*
+    /**
      * Prints entry information.
      */
     void printEntry(ZipEntry e) throws IOException {
@@ -1067,21 +1128,21 @@
         }
     }
 
-    /*
-     * Print usage message and die.
+    /**
+     * Prints usage message.
      */
     void usageError() {
         error(getMsg("usage"));
     }
 
-    /*
+    /**
      * A fatal exception has been caught.  No recovery possible
      */
     void fatalError(Exception e) {
         e.printStackTrace();
     }
 
-    /*
+    /**
      * A fatal condition has been detected; message is "s".
      * No recovery possible
      */
@@ -1103,39 +1164,43 @@
         err.println(s);
     }
 
-    /*
+    /**
      * Main routine to start program.
      */
     public static void main(String args[]) {
         Main jartool = new Main(System.out, System.err, "jar");
         System.exit(jartool.run(args) ? 0 : 1);
     }
-}
 
-/*
- * an OutputStream that doesn't send its output anywhere, (but could).
- * It's here to find the CRC32 of a manifest, necessary for STORED only
- * mode in ZIP.
- */
-final class CRC32OutputStream extends java.io.OutputStream {
-    CRC32 crc;
-    int n = 0;
-    CRC32OutputStream(CRC32 crc) {
-        this.crc = crc;
-    }
+    /**
+     * An OutputStream that doesn't send its output anywhere, (but could).
+     * It's here to find the CRC32 of an input file, necessary for STORED
+     * mode in ZIP.
+     */
+    private static class CRC32OutputStream extends java.io.OutputStream {
+        final CRC32 crc = new CRC32();
+        long n = 0;
 
-    public void write(int r) throws IOException {
-        crc.update(r);
-        n++;
-    }
+        CRC32OutputStream() {}
 
-    public void write(byte[] b) throws IOException {
-        crc.update(b, 0, b.length);
-        n += b.length;
-    }
+        public void write(int r) throws IOException {
+            crc.update(r);
+            n++;
+        }
 
-    public void write(byte[] b, int off, int len) throws IOException {
-        crc.update(b, off, len);
-        n += len - off;
+        public void write(byte[] b, int off, int len) throws IOException {
+            crc.update(b, off, len);
+            n += len;
+        }
+
+        /**
+         * Updates a ZipEntry which describes the data read by this
+         * output stream, in STORED mode.
+         */
+        public void updateEntry(ZipEntry e) {
+            e.setMethod(ZipEntry.STORED);
+            e.setSize(n);
+            e.setCrc(crc.getValue());
+        }
     }
 }
--- a/test/tools/jar/index/MetaInf.java	Mon Jul 06 15:13:48 2009 +0200
+++ b/test/tools/jar/index/MetaInf.java	Mon Jul 06 11:30:40 2009 -0700
@@ -23,13 +23,15 @@
 
 /*
  * @test
- * @bug 4408526
+ * @bug 4408526 6854795
  * @summary Index the non-meta files in META-INF, such as META-INF/services.
  */
 
 import java.io.*;
+import java.util.Arrays;
 import java.util.jar.*;
 import sun.tools.jar.Main;
+import java.util.zip.ZipFile;
 
 public class MetaInf {
 
@@ -39,29 +41,51 @@
     static String contents =
         System.getProperty("test.src") + File.separatorChar + "jarcontents";
 
-    // Options passed to "jar" command.
-    static String[] jarArgs1 = new String[] {
-        "cf", jarName, "-C", contents, SERVICES
-    };
-    static String[] jarArgs2 = new String[] {
-        "i", jarName
-    };
+    static void run(String ... args) {
+        if (! new Main(System.out, System.err, "jar").run(args))
+            throw new Error("jar failed: args=" + Arrays.toString(args));
+    }
 
-    public static void main(String[] args) throws IOException {
+    static void copy(File from, File to) throws IOException {
+        FileInputStream in = new FileInputStream(from);
+        FileOutputStream out = new FileOutputStream(to);
+        try {
+            byte[] buf = new byte[8192];
+            int n;
+            while ((n = in.read(buf)) != -1)
+                out.write(buf, 0, n);
+        } finally {
+            in.close();
+            out.close();
+        }
+    }
+
+    static boolean contains(File jarFile, String entryName)
+        throws IOException {
+        return new ZipFile(jarFile).getEntry(entryName) != null;
+    }
+
+    static void checkContains(File jarFile, String entryName)
+        throws IOException {
+        if (! contains(jarFile, entryName))
+            throw new Error(String.format("expected jar %s to contain %s",
+                                          jarFile, entryName));
+    }
+
+    static void testIndex(String jarName) throws IOException {
+        System.err.printf("jarName=%s%n", jarName);
+
+        File jar = new File(jarName);
 
         // Create a jar to be indexed.
-        Main jarTool = new Main(System.out, System.err, "jar");
-        if (!jarTool.run(jarArgs1)) {
-            throw new Error("Could not create jar file.");
+        run("cf", jarName, "-C", contents, SERVICES);
+
+        for (int i = 0; i < 2; i++) {
+            run("i", jarName);
+            checkContains(jar, INDEX);
+            checkContains(jar, SERVICES);
         }
 
-        // Index the jar.
-        jarTool = new Main(System.out, System.err, "jar");
-        if (!jarTool.run(jarArgs2)) {
-            throw new Error("Could not index jar file.");
-        }
-
-        // Read the index.  Verify that META-INF/services is indexed.
         JarFile f = new JarFile(jarName);
         BufferedReader index =
             new BufferedReader(
@@ -75,4 +99,17 @@
         }
         throw new Error(SERVICES + " not indexed.");
     }
+
+    public static void main(String[] args) throws IOException {
+        testIndex("a.jar");             // a path with parent == null
+        testIndex("./a.zip");           // a path with parent != null
+
+        // Try indexing a jar in the default temp directory.
+        File tmpFile = File.createTempFile("MetaInf", null, null);
+        try {
+            testIndex(tmpFile.getPath());
+        } finally {
+            tmpFile.delete();
+        }
+    }
 }