7021927: javac: regression in performance
authorksrini
Mon, 07 Mar 2011 17:39:42 -0800
changeset 8837 141b22c7e7b2
parent 8637 be1b564b50aa
child 8838 91e08c1bcd5c
7021927: javac: regression in performance Reviewed-by: jjg
langtools/src/share/classes/com/sun/tools/javac/file/JavacFileManager.java
langtools/src/share/classes/com/sun/tools/javac/file/ZipFileIndex.java
langtools/src/share/classes/com/sun/tools/javac/util/Options.java
langtools/test/tools/javac/6508981/TestInferBinaryName.java
langtools/test/tools/javac/api/6411310/Test.java
langtools/test/tools/javac/api/T6838467.java
langtools/test/tools/javac/api/T6877206.java
langtools/test/tools/javac/file/zip/T6836682.java
langtools/test/tools/javac/file/zip/T6865530.java
langtools/test/tools/javac/file/zip/Utils.java
--- a/langtools/src/share/classes/com/sun/tools/javac/file/JavacFileManager.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/src/share/classes/com/sun/tools/javac/file/JavacFileManager.java	Mon Mar 07 17:39:42 2011 -0800
@@ -89,7 +89,7 @@
 
     private FSInfo fsInfo;
 
-    private boolean useZipFileIndex;
+    private boolean contextUseOptimizedZip;
     private ZipFileIndexCache zipFileIndexCache;
 
     private final File uninited = new File("U N I N I T E D");
@@ -164,8 +164,8 @@
 
         fsInfo = FSInfo.instance(context);
 
-        useZipFileIndex = options.isSet("useOptimizedZip");
-        if (useZipFileIndex)
+        contextUseOptimizedZip = options.getBoolean("useOptimizedZip", true);
+        if (contextUseOptimizedZip)
             zipFileIndexCache = ZipFileIndexCache.getSharedInstance();
 
         mmappedIO = options.isSet("mmappedIO");
@@ -471,9 +471,27 @@
     private static final RelativeDirectory symbolFilePrefix
             = new RelativeDirectory("META-INF/sym/rt.jar/");
 
+    /*
+     * This method looks for a ZipFormatException and takes appropriate
+     * evasive action. If there is a failure in the fast mode then we
+     * fail over to the platform zip, and allow it to deal with a potentially
+     * non compliant zip file.
+     */
+    protected Archive openArchive(File zipFilename) throws IOException {
+        try {
+            return openArchive(zipFilename, contextUseOptimizedZip);
+        } catch (IOException ioe) {
+            if (ioe instanceof ZipFileIndex.ZipFormatException) {
+                return openArchive(zipFilename, false);
+            } else {
+                throw ioe;
+            }
+        }
+    }
+
     /** Open a new zip file directory, and cache it.
      */
-    protected Archive openArchive(File zipFileName) throws IOException {
+    private Archive openArchive(File zipFileName, boolean useOptimizedZip) throws IOException {
         File origZipFileName = zipFileName;
         if (!ignoreSymbolFile && paths.isDefaultBootClassPathRtJar(zipFileName)) {
             File file = zipFileName.getParentFile().getParentFile(); // ${java.home}
@@ -495,7 +513,7 @@
             boolean usePreindexedCache = false;
             String preindexCacheLocation = null;
 
-            if (!useZipFileIndex) {
+            if (!useOptimizedZip) {
                 zdir = new ZipFile(zipFileName);
             } else {
                 usePreindexedCache = options.isSet("usezipindex");
@@ -524,23 +542,22 @@
             }
 
             if (origZipFileName == zipFileName) {
-                if (!useZipFileIndex) {
+                if (!useOptimizedZip) {
                     archive = new ZipArchive(this, zdir);
                 } else {
                     archive = new ZipFileIndexArchive(this,
-                                zipFileIndexCache.getZipFileIndex(zipFileName,
+                                    zipFileIndexCache.getZipFileIndex(zipFileName,
                                     null,
                                     usePreindexedCache,
                                     preindexCacheLocation,
                                     options.isSet("writezipindexfiles")));
                 }
             } else {
-                if (!useZipFileIndex) {
+                if (!useOptimizedZip) {
                     archive = new SymbolArchive(this, origZipFileName, zdir, symbolFilePrefix);
-                }
-                else {
+                } else {
                     archive = new ZipFileIndexArchive(this,
-                                zipFileIndexCache.getZipFileIndex(zipFileName,
+                                    zipFileIndexCache.getZipFileIndex(zipFileName,
                                     symbolFilePrefix,
                                     usePreindexedCache,
                                     preindexCacheLocation,
@@ -549,6 +566,8 @@
             }
         } catch (FileNotFoundException ex) {
             archive = new MissingArchive(zipFileName);
+        } catch (ZipFileIndex.ZipFormatException zfe) {
+            throw zfe;
         } catch (IOException ex) {
             if (zipFileName.exists())
                 log.error("error.reading.file", zipFileName, getMessage(ex));
--- a/langtools/src/share/classes/com/sun/tools/javac/file/ZipFileIndex.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/src/share/classes/com/sun/tools/javac/file/ZipFileIndex.java	Mon Mar 07 17:39:42 2011 -0800
@@ -492,8 +492,30 @@
         public ZipDirectory(RandomAccessFile zipRandomFile, long start, long end, ZipFileIndex index) throws IOException {
             this.zipRandomFile = zipRandomFile;
             this.zipFileIndex = index;
+            hasValidHeader();
+            findCENRecord(start, end);
+        }
 
-            findCENRecord(start, end);
+        /*
+         * the zip entry signature should be at offset 0, otherwise allow the
+         * calling logic to take evasive action by throwing ZipFormatException.
+         */
+        private boolean hasValidHeader() throws IOException {
+            final long pos = zipRandomFile.getFilePointer();
+            try {
+                if (zipRandomFile.read() == 'P') {
+                    if (zipRandomFile.read() == 'K') {
+                        if (zipRandomFile.read() == 0x03) {
+                            if (zipRandomFile.read() == 0x04) {
+                                return true;
+                            }
+                        }
+                    }
+                }
+            } finally {
+                zipRandomFile.seek(pos);
+            }
+            throw new ZipFormatException("invalid zip magic");
         }
 
         /*
@@ -529,7 +551,13 @@
                     zipDir = new byte[get4ByteLittleEndian(endbuf, i + 12) + 2];
                     zipDir[0] = endbuf[i + 10];
                     zipDir[1] = endbuf[i + 11];
-                    zipRandomFile.seek(start + get4ByteLittleEndian(endbuf, i + 16));
+                    int sz = get4ByteLittleEndian(endbuf, i + 16);
+                    // a negative offset or the entries field indicates a
+                    // potential zip64 archive
+                    if (sz < 0 || get2ByteLittleEndian(zipDir, 0) == 0xffff) {
+                        throw new ZipFormatException("detected a zip64 archive");
+                    }
+                    zipRandomFile.seek(start + sz);
                     zipRandomFile.readFully(zipDir, 2, zipDir.length - 2);
                     return;
                 } else {
@@ -1127,4 +1155,18 @@
         }
     }
 
+    /*
+     * Exception primarily used to implement a failover, used exclusively here.
+     */
+
+    static final class ZipFormatException extends IOException {
+        private static final long serialVersionUID = 8000196834066748623L;
+        protected ZipFormatException(String message) {
+            super(message);
+        }
+
+        protected ZipFormatException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
 }
--- a/langtools/src/share/classes/com/sun/tools/javac/util/Options.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/src/share/classes/com/sun/tools/javac/util/Options.java	Mon Mar 07 17:39:42 2011 -0800
@@ -76,6 +76,22 @@
     }
 
     /**
+     * Get the boolean value for an option, patterned after Boolean.getBoolean,
+     * essentially will return true, iff the value exists and is set to "true".
+     */
+    public boolean getBoolean(String name) {
+        return getBoolean(name, false);
+    }
+
+    /**
+     * Get the boolean with a default value if the option is not set.
+     */
+    public boolean getBoolean(String name, boolean defaultValue) {
+        String value = get(name);
+        return (value == null) ? defaultValue : Boolean.parseBoolean(value);
+    }
+
+    /**
      * Check if the value for an undocumented option has been set.
      */
     public boolean isSet(String name) {
--- a/langtools/test/tools/javac/6508981/TestInferBinaryName.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/test/tools/javac/6508981/TestInferBinaryName.java	Mon Mar 07 17:39:42 2011 -0800
@@ -139,9 +139,8 @@
             throws IOException {
         Context ctx = new Context();
         Options options = Options.instance(ctx);
-        // uugh, ugly back door, should be cleaned up, someday
-        if (zipFileIndexKind == USE_ZIP_FILE_INDEX)
-            options.put("useOptimizedZip", "true");
+        options.put("useOptimizedZip",
+                Boolean.toString(zipFileIndexKind == USE_ZIP_FILE_INDEX));
 
         if (symFileKind == IGNORE_SYMBOL_FILE)
             options.put("ignore.symbol.file", "true");
--- a/langtools/test/tools/javac/api/6411310/Test.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/test/tools/javac/api/6411310/Test.java	Mon Mar 07 17:39:42 2011 -0800
@@ -153,14 +153,12 @@
         Context c = new Context();
         Options options = Options.instance(c);
 
-            if (useOptimizedZip) {
-                options.put("useOptimizedZip", "true");
-            }
+        options.put("useOptimizedZip", Boolean.toString(useOptimizedZip));
 
-            if (!useSymbolFile) {
-                options.put("ignore.symbol.file", "true");
-            }
-            return new JavacFileManager(c, false, null);
+        if (!useSymbolFile) {
+            options.put("ignore.symbol.file", "true");
+        }
+        return new JavacFileManager(c, false, null);
     }
 
     File createDir(String name, String... entries) throws Exception {
--- a/langtools/test/tools/javac/api/T6838467.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/test/tools/javac/api/T6838467.java	Mon Mar 07 17:39:42 2011 -0800
@@ -178,12 +178,10 @@
         return fm;
     }
 
-    JavacFileManager createFileManager(boolean useOptimedZipIndex) {
+    JavacFileManager createFileManager(boolean useOptimizedZip) {
         Context ctx = new Context();
-        if (useOptimedZipIndex) {
-            Options options = Options.instance(ctx);
-            options.put("useOptimizedZip", "true");
-        }
+        Options options = Options.instance(ctx);
+        options.put("useOptimizedZip", Boolean.toString(useOptimizedZip));
         return new JavacFileManager(ctx, false, null);
     }
 
--- a/langtools/test/tools/javac/api/T6877206.java	Mon Mar 07 13:45:06 2011 -0800
+++ b/langtools/test/tools/javac/api/T6877206.java	Mon Mar 07 17:39:42 2011 -0800
@@ -168,9 +168,7 @@
     JavacFileManager createFileManager(boolean useOptimizedZip, boolean useSymbolFile) {
         Context ctx = new Context();
         Options options = Options.instance(ctx);
-        if (useOptimizedZip) {
-            options.put("useOptimizedZip", "true");
-        }
+        options.put("useOptimizedZip", Boolean.toString(useOptimizedZip));
         if (!useSymbolFile) {
             options.put("ignore.symbol.file", "true");
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/tools/javac/file/zip/T6836682.java	Mon Mar 07 17:39:42 2011 -0800
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 6836682
+ * @summary JavacFileManager handles zip64 archives (64K+ entries and large file support)
+ * @compile  -XDignore.symbol.file T6836682.java Utils.java
+ * @run main T6836682
+ */
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.jar.JarOutputStream;
+import java.util.zip.ZipEntry;
+
+public class T6836682 {
+
+    private static final long GIGA = 1024 * 1024 * 1024;
+
+    static void createLargeFile(File outFile, long minlength) throws IOException {
+        FileOutputStream fos = null;
+        BufferedOutputStream bos = null;
+        byte[] buffer = new byte[Short.MAX_VALUE * 2];
+        try {
+            fos = new FileOutputStream(outFile);
+            bos = new BufferedOutputStream(fos);
+            long count = minlength / ( Short.MAX_VALUE * 2)  + 1;
+            for (long i = 0 ; i < count ; i++) {
+                bos.write(buffer);
+            }
+        } finally {
+            Utils.close(bos);
+            Utils.close(fos);
+        }
+        if (outFile.length() < minlength) {
+            throw new RuntimeException("could not create large file " + outFile.getAbsolutePath());
+        }
+    }
+
+    static void createJarWithLargeFile(File jarFile, File javaFile,
+            long minlength) throws IOException {
+        Utils.createClassFile(javaFile, null, true);
+        File largeFile = new File("large.data");
+        createLargeFile(largeFile, minlength);
+        String[] jarArgs = {
+            "0cvf",
+            jarFile.getAbsolutePath(),
+            largeFile.getName(),
+            Utils.getClassFileName(javaFile)
+        };
+        Utils.jarTool.run(jarArgs);
+        // deleted to prevent accidental linkage
+        new File(Utils.getClassFileName(javaFile)).delete();
+    }
+
+    static void createLargeJar(File jarFile, File javaFile) throws IOException {
+        File classFile = new File(Utils.getClassFileName(javaFile));
+        Utils.createClassFile(javaFile, null, true);
+        JarOutputStream jos = null;
+        FileInputStream fis = null;
+        try {
+            jos = new JarOutputStream(new FileOutputStream(jarFile));
+
+            for (int i = 0; i < Short.MAX_VALUE * 2 + 10; i++) {
+                jos.putNextEntry(new ZipEntry("X" + i + ".txt"));
+            }
+            jos.putNextEntry(new ZipEntry(classFile.getName()));
+            fis = new FileInputStream(classFile);
+            Utils.copyStream(fis, jos);
+        } finally {
+            Utils.close(jos);
+            Utils.close(fis);
+        }
+        // deleted to prevent accidental linkage
+        new File(Utils.getClassFileName(javaFile)).delete();
+    }
+
+    // a jar with entries exceeding 64k + a class file for the existential test
+    public static void testLargeJar(String... args) throws IOException {
+        File largeJar = new File("large.jar");
+        File javaFile = new File("Foo.java");
+        createLargeJar(largeJar, javaFile);
+
+        File testFile = new File("Bar.java");
+        try {
+            Utils.createJavaFile(testFile, javaFile);
+            if (!Utils.compile("-doe", "-verbose", "-cp",
+                    largeJar.getAbsolutePath(), testFile.getAbsolutePath())) {
+                throw new IOException("test failed");
+            }
+        } finally {
+            Utils.deleteFile(largeJar);
+        }
+    }
+
+    // a jar with an enormous file + a class file for the existential test
+    public static void testHugeJar(String... args) throws IOException {
+        final File largeJar = new File("huge.jar");
+        final File javaFile = new File("Foo.java");
+
+        final Path path = largeJar.getAbsoluteFile().getParentFile().toPath();
+        final long available = Files.getFileStore(path).getUsableSpace();
+        final long MAX_VALUE = 0xFFFF_FFFFL;
+
+        final long absolute  = MAX_VALUE + 1L;
+        final long required  = (long)(absolute * 1.1); // pad for sundries
+        System.out.println("\tavailable: " + available / GIGA + " GB");
+        System.out.println("\required: " + required / GIGA + " GB");
+
+        if (available > required) {
+            createJarWithLargeFile(largeJar, javaFile, absolute);
+            File testFile = new File("Bar.java");
+            Utils.createJavaFile(testFile, javaFile);
+            try {
+                if (!Utils.compile("-doe", "-verbose", "-cp",
+                        largeJar.getAbsolutePath(), testFile.getAbsolutePath())) {
+                    throw new IOException("test failed");
+                }
+            } finally {
+                Utils.deleteFile(largeJar);
+            }
+        } else {
+            System.out.println("Warning: test passes vacuously, requirements exceeds available space");
+        }
+    }
+
+    public static void main(String... args) throws IOException {
+        testLargeJar();
+        testHugeJar();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/tools/javac/file/zip/T6865530.java	Mon Mar 07 17:39:42 2011 -0800
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * @test
+ * @bug 6865530
+ * @summary ensure JavacFileManager handles non-standard zipfiles.
+ * @compile  -XDignore.symbol.file T6865530.java
+ * @run main T6865530
+ */
+
+
+import java.io.File;
+
+
+public class T6865530 {
+
+    public static void main(String... args) throws Exception {
+        File badFile = new File("bad.exe");
+        File testJar = new File("test.jar");
+        File fooJava = new File("Foo.java");
+        File barJava = new File("Bar.java");
+
+        // create a jar by compiling a file, and append the jar to some
+        // arbitrary data to offset the start of the zip/jar archive
+        Utils.createJavaFile(fooJava);
+        Utils.compile("-doe", "-verbose", fooJava.getName());
+        String[] jarArgs = {
+            "cvf", testJar.getAbsolutePath(), "Foo.class"
+        };
+        Utils.jarTool.run(jarArgs);
+        Utils.cat(badFile, fooJava, testJar);
+
+        // create test file and use the above file as a classpath
+        Utils.createJavaFile(barJava);
+        try {
+            if (!Utils.compile("-doe", "-verbose", "-cp", badFile.getAbsolutePath(), "Bar.java")) {
+                throw new RuntimeException("test fails javac did not compile");
+            }
+        } finally {
+            Utils.deleteFile(badFile);
+            Utils.deleteFile(testJar);
+        }
+    }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/langtools/test/tools/javac/file/zip/Utils.java	Mon Mar 07 17:39:42 2011 -0800
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.Closeable;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+
+public class Utils {
+
+    static final sun.tools.jar.Main jarTool =
+            new sun.tools.jar.Main(System.out, System.err, "jar-tool");
+
+    static final com.sun.tools.javac.Main javac =
+            new com.sun.tools.javac.Main();
+
+    private Utils(){}
+
+    public static boolean compile(String... args) {
+        return javac.compile(args) == 0;
+    }
+
+    public static void createClassFile(File javaFile, File superClass,
+            boolean delete) throws IOException {
+        createJavaFile(javaFile, superClass);
+        if (!compile(javaFile.getName())) {
+            throw new RuntimeException("compile failed unexpectedly");
+        }
+        if (delete) javaFile.delete();
+    }
+
+    public static void createJavaFile(File outFile) throws IOException {
+        createJavaFile(outFile, null);
+    }
+
+    public static void createJavaFile(File outFile, File superClass) throws IOException {
+        PrintStream ps = null;
+        String srcStr = "public class " + getSimpleName(outFile) + " ";
+        if (superClass != null) {
+            srcStr = srcStr.concat("extends " + getSimpleName(superClass) + " ");
+        }
+        srcStr = srcStr.concat("{}");
+        try {
+            FileOutputStream fos = new FileOutputStream(outFile);
+            ps = new PrintStream(fos);
+            ps.println(srcStr);
+        } finally {
+            close(ps);
+        }
+    }
+
+    static String getClassFileName(File javaFile) {
+        return javaFile.getName().endsWith(".java")
+                ? javaFile.getName().replace(".java", ".class")
+                : null;
+    }
+
+    static String getSimpleName(File inFile) {
+        String fname = inFile.getName();
+        return fname.substring(0, fname.indexOf("."));
+    }
+
+    public static void copyStream(InputStream in, OutputStream out) throws IOException {
+        byte[] buf = new byte[8192];
+        int n = in.read(buf);
+        while (n > 0) {
+            out.write(buf, 0, n);
+            n = in.read(buf);
+        }
+    }
+
+    public static void close(Closeable c) {
+        if (c != null) {
+            try {
+                c.close();
+            } catch (IOException ignore) {}
+        }
+    }
+
+    public static void deleteFile(File f) {
+        if (!f.delete()) {
+            throw new RuntimeException("could not delete file: " + f.getAbsolutePath());
+        }
+    }
+
+    public static void cat(File output, File... files) throws IOException {
+        BufferedInputStream bis = null;
+        BufferedOutputStream bos = null;
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(output);
+            bos = new BufferedOutputStream(fos);
+            for (File x : files) {
+                FileInputStream fis = new FileInputStream(x);
+                bis = new BufferedInputStream(fis);
+                copyStream(bis, bos);
+                Utils.close(bis);
+            }
+        } finally {
+            Utils.close(bis);
+            Utils.close(bos);
+            Utils.close(fos);
+        }
+    }
+}