package net.i2p.util; import java.io.BufferedReader; 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.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; // WARNING // Some methods called from install.jar (Windows installer utils) // or InstallUpdate (i2pupdate.zip installer), // where most external classes are not available, including DataHelper! // Use caution when adding dependencies. import net.i2p.data.DataHelper; // Pack200 now loaded dynamically in unpack() below // // For Sun, OpenJDK, IcedTea, etc, use this //import java.util.jar.Pack200; // // For Apache Harmony or if you put its pack200.jar in your library directory use this //import org.apache.harmony.unpack200.Archive; /** * General helper methods for messing with files * * These are static methods that do NOT convert arguments * to absolute paths for a particular context and directory. * * Callers should ALWAYS provide absolute paths as arguments, * and should NEVER assume files are in the current working directory. * */ public class FileUtil { /** * Delete the path as well as any files or directories underneath it. * * @param path path to the directory being deleted * @param failIfNotEmpty if true, do not delete anything if the directory * is not empty (and return false) * @return true if the path no longer exists (aka was removed), * false if it remains */ public static final boolean rmdir(String path, boolean failIfNotEmpty) { return rmdir(new File(path), failIfNotEmpty); } /** * Delete the path as well as any files or directories underneath it. * * @param target the file or directory being deleted * @param failIfNotEmpty if true, do not delete anything if the directory * is not empty (and return false) * @return true if the path no longer exists (aka was removed), * false if it remains */ public static final boolean rmdir(File target, boolean failIfNotEmpty) { if (!target.exists()) { //System.out.println("info: target does not exist [" + target.getPath() + "]"); return true; } if (!target.isDirectory()) { //System.out.println("info: target is not a directory [" + target.getPath() + "]"); return target.delete(); } else { File children[] = target.listFiles(); if (children == null) { //System.out.println("info: target null children [" + target.getPath() + "]"); return false; } if ( (failIfNotEmpty) && (children.length > 0) ) { //System.out.println("info: target is not emtpy[" + target.getPath() + "]"); return false; } for (int i = 0; i < children.length; i++) { if (!rmdir(children[i], failIfNotEmpty)) return false; //System.out.println("info: target removed recursively [" + children[i].getPath() + "]"); } return target.delete(); } } /** * As of release 0.7.12, any files inside the zip that have a .jar.pack or .war.pack suffix * are transparently unpacked to a .jar or .war file using unpack200. * Logs at WARN level to wrapper.log */ public static boolean extractZip(File zipfile, File targetDir) { return extractZip(zipfile, targetDir, Log.WARN); } /** * Warning - do not call any new classes from here, or * update will crash the JVM. * * @param logLevel Log.WARN, etc. * @return true if it was copied successfully * @since 0.9.7 */ public static boolean extractZip(File zipfile, File targetDir, int logLevel) { int files = 0; ZipFile zip = null; try { final byte buf[] = new byte[8192]; zip = new ZipFile(zipfile); Enumeration<? extends ZipEntry> entries = zip.entries(); while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry)entries.nextElement(); if (entry.getName().contains("..")) { System.err.println("ERROR: Refusing to extract a zip entry with '..' in it [" + entry.getName() + "]"); return false; } if (entry.getName().indexOf(0) >= 0) { System.err.println("ERROR: Refusing to extract a zip entry with null in it [" + entry.getName() + "]"); return false; } File target = new File(targetDir, entry.getName()); File parent = target.getParentFile(); if ( (parent != null) && (!parent.exists()) ) { boolean parentsOk = parent.mkdirs(); if (!parentsOk) { if (logLevel <= Log.ERROR) System.err.println("ERROR: Unable to create the parent dir for " + entry.getName() + ": [" + parent.getAbsolutePath() + "]"); return false; } } if (entry.isDirectory()) { if (!target.exists()) { boolean created = target.mkdirs(); if (!created) { if (logLevel <= Log.ERROR) System.err.println("ERROR: Unable to create the directory [" + entry.getName() + "]"); return false; } else if (logLevel <= Log.INFO) { System.err.println("INFO: Creating directory [" + entry.getName() + "]"); } } } else { InputStream in = null; FileOutputStream fos = null; JarOutputStream jos = null; try { in = zip.getInputStream(entry); if (entry.getName().endsWith(".jar.pack") || entry.getName().endsWith(".war.pack")) { target = new File(targetDir, entry.getName().substring(0, entry.getName().length() - ".pack".length())); jos = new JarOutputStream(new FileOutputStream(target)); unpack(in, jos); if (logLevel <= Log.INFO) System.err.println("INFO: File [" + entry.getName() + "] extracted and unpacked"); } else { fos = new FileOutputStream(target); // We do NOT use DataHelper.copy() because it loads new classes // and causes the update to crash. //DataHelper.copy(in, fos); int read; while ((read = in.read(buf)) != -1) { fos.write(buf, 0, read); } if (logLevel <= Log.INFO) System.err.println("INFO: File [" + entry.getName() + "] extracted"); } files++; } catch (IOException ioe) { if (logLevel <= Log.ERROR) { System.err.println("ERROR: Error extracting the zip entry (" + entry.getName() + ')'); if (ioe.getMessage() != null && ioe.getMessage().indexOf("CAFED00D") >= 0) System.err.println("This may be caused by a packed library that requires Java 1.6, your Java version is: " + System.getProperty("java.version")); ioe.printStackTrace(); } return false; } catch (Exception e) { // Oracle unpack() should throw an IOE but other problems can happen, e.g: // java.lang.reflect.InvocationTargetException // Caused by: java.util.zip.ZipException: duplicate entry: xxxxx if (logLevel <= Log.ERROR) { System.err.println("ERROR: Error extracting the zip entry (" + entry.getName() + ')'); e.printStackTrace(); } return false; } finally { try { if (in != null) in.close(); } catch (IOException ioe) {} try { if (fos != null) fos.close(); } catch (IOException ioe) {} try { if (jos != null) jos.close(); } catch (IOException ioe) {} } } } return true; } catch (IOException ioe) { if (logLevel <= Log.ERROR) { System.err.println("ERROR: Unable to extract the zip file"); ioe.printStackTrace(); } return false; } finally { if (zip != null) { try { zip.close(); } catch (IOException ioe) {} } if (files > 0 && logLevel <= Log.WARN) System.err.println("INFO: " + files + " files extracted to " + targetDir); } } /** * Verify the integrity of a zipfile. * There doesn't seem to be any library function to do this, * so we basically go through all the motions of extractZip() above, * unzipping everything but throwing away the data. * * Todo: verify zip header? Although this would break the undocumented * practice of renaming the i2pupdate.sud file to i2pupdate.zip and * letting the unzip method skip over the leading 56 bytes of * "junk" (sig and version) * * @return true if ok */ public static boolean verifyZip(File zipfile) { ZipFile zip = null; try { byte buf[] = new byte[16*1024]; zip = new ZipFile(zipfile); Enumeration<? extends ZipEntry> entries = zip.entries(); boolean p200TestRequired = true; while (entries.hasMoreElements()) { ZipEntry entry = (ZipEntry)entries.nextElement(); if (entry.getName().indexOf("..") != -1) { //System.err.println("ERROR: Refusing to extract a zip entry with '..' in it [" + entry.getName() + "]"); return false; } if (entry.isDirectory()) { // noop } else { if (p200TestRequired && (entry.getName().endsWith(".jar.pack") || entry.getName().endsWith(".war.pack"))) { if (!isPack200Supported()) { System.err.println("ERROR: Zip verify failed, your JVM does not support unpack200"); return false; } p200TestRequired = false; } try { InputStream in = zip.getInputStream(entry); while ( (in.read(buf)) != -1) { // throw the data away } //System.err.println("INFO: File [" + entry.getName() + "] extracted"); in.close(); } catch (IOException ioe) { //System.err.println("ERROR: Error extracting the zip entry (" + entry.getName() + "]"); //ioe.printStackTrace(); return false; } } } return true; } catch (IOException ioe) { //System.err.println("ERROR: Unable to extract the zip file"); //ioe.printStackTrace(); return false; } finally { if (zip != null) { try { zip.close(); } catch (IOException ioe) {} } } } /** * Public since 0.8.3 * @since 0.8.1 */ public static boolean isPack200Supported() { try { Class.forName("java.util.jar.Pack200", false, ClassLoader.getSystemClassLoader()); return true; } catch (Exception e) {} try { Class.forName("org.apache.harmony.unpack200.Archive", false, ClassLoader.getSystemClassLoader()); return true; } catch (Exception e) {} return false; } private static boolean _failedOracle; private static boolean _failedApache; /** * Unpack using either Oracle or Apache's unpack200 library, * with the classes discovered at runtime so neither is required at compile time. * * Caller must close streams * @throws IOException on unpack error or if neither library is available. * Will not throw ClassNotFoundException. * @throws org.apache.harmony.pack200.Pack200Exception which is not an IOException * @throws java.lang.reflect.InvocationTargetException on duplicate zip entries in the packed jar * @since 0.8.1 */ private static void unpack(InputStream in, JarOutputStream out) throws Exception { // For Sun, OpenJDK, IcedTea, etc, use this //Pack200.newUnpacker().unpack(in, out); if (!_failedOracle) { try { Class<?> p200 = Class.forName("java.util.jar.Pack200", true, ClassLoader.getSystemClassLoader()); Method newUnpacker = p200.getMethod("newUnpacker"); Object unpacker = newUnpacker.invoke(null,(Object[]) null); Method unpack = unpacker.getClass().getMethod("unpack", InputStream.class, JarOutputStream.class); // throws IOException unpack.invoke(unpacker, new Object[] {in, out}); return; } catch (ClassNotFoundException e) { _failedOracle = true; //e.printStackTrace(); } catch (NoSuchMethodException e) { _failedOracle = true; //e.printStackTrace(); } } // ------------------ // For Apache Harmony or if you put its pack200.jar in your library directory use this //(new Archive(in, out)).unpack(); if (!_failedApache) { try { Class<?> p200 = Class.forName("org.apache.harmony.unpack200.Archive", true, ClassLoader.getSystemClassLoader()); Constructor<?> newUnpacker = p200.getConstructor(InputStream.class, JarOutputStream.class); Object unpacker = newUnpacker.newInstance(in, out); Method unpack = unpacker.getClass().getMethod("unpack"); // throws IOException or Pack200Exception unpack.invoke(unpacker, (Object[]) null); return; } catch (ClassNotFoundException e) { _failedApache = true; //e.printStackTrace(); } catch (NoSuchMethodException e) { _failedApache = true; //e.printStackTrace(); } } // ------------------ // For gcj, gij, etc., use this throw new IOException("Unpack200 not supported"); } /** * Read in the last few lines of a (newline delimited) textfile, or null if * the file doesn't exist. * * Warning - this inefficiently allocates a StringBuilder of size maxNumLines*80, * so don't make it too big. * Warning - converts \r\n to \n * * @param startAtBeginning if true, read the first maxNumLines, otherwise read * the last maxNumLines * @param maxNumLines max number of lines (or -1 for unlimited) * @return string or null; does not throw IOException. * */ public static String readTextFile(String filename, int maxNumLines, boolean startAtBeginning) { File f = new File(filename); if (!f.exists()) return null; FileInputStream fis = null; BufferedReader in = null; try { fis = new FileInputStream(f); in = new BufferedReader(new InputStreamReader(fis, "UTF-8")); List<String> lines = new ArrayList<String>(maxNumLines > 0 ? maxNumLines : 64); String line = null; while ( (line = in.readLine()) != null) { lines.add(line); if ( (maxNumLines > 0) && (lines.size() >= maxNumLines) ) { if (startAtBeginning) break; else lines.remove(0); } } StringBuilder buf = new StringBuilder(lines.size() * 80); for (int i = 0; i < lines.size(); i++) { buf.append(lines.get(i)).append('\n'); } return buf.toString(); } catch (IOException ioe) { return null; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } } /** * Dump the contents of the given path (relative to the root) to the output * stream. The path must not go above the root, either - if it does, it will * throw a FileNotFoundException * * Closes the OutputStream out on successful completion * but leaves it open when throwing IOE. */ public static void readFile(String path, String root, OutputStream out) throws IOException { File rootDir = new File(root); while (path.startsWith("/") && (path.length() > 0) ) path = path.substring(1); if (path.length() <= 0) throw new FileNotFoundException("Not serving up the root dir"); File target = new File(rootDir, path); if (!target.exists()) throw new FileNotFoundException("Requested file does not exist: " + path); String targetStr = target.getCanonicalPath(); String rootDirStr = rootDir.getCanonicalPath(); if (!targetStr.startsWith(rootDirStr)) throw new FileNotFoundException("Requested file is outside the root dir: " + path); FileInputStream in = null; try { in = new FileInputStream(target); DataHelper.copy(in, out); try { out.close(); } catch (IOException ioe) {} } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} } } /** * @return true if it was copied successfully */ public static boolean copy(String source, String dest, boolean overwriteExisting) { return copy(source, dest, overwriteExisting, false); } /** * @param quiet don't log fails to wrapper log if true * @return true if it was copied successfully */ public static boolean copy(String source, String dest, boolean overwriteExisting, boolean quiet) { File src = new File(source); File dst = new File(dest); return copy(src, dst, overwriteExisting, quiet); } /** * @param quiet don't log fails to wrapper log if true * @return true if it was copied successfully * @since 0.8.8 */ public static boolean copy(File src, File dst, boolean overwriteExisting, boolean quiet) { if (dst.exists() && dst.isDirectory()) dst = new File(dst, src.getName()); if (!src.exists()) return false; if (dst.exists() && !overwriteExisting) return false; InputStream in = null; OutputStream out = null; try { in = new FileInputStream(src); out = new FileOutputStream(dst); // We do NOT use DataHelper.copy() because it's used in installer.jar // which does not contain DataHelper //DataHelper.copy(in, out); int read; byte buf[] = new byte[4096]; while ((read = in.read(buf)) != -1) { out.write(buf, 0, read); } return true; } catch (IOException ioe) { if (!quiet) ioe.printStackTrace(); return false; } finally { try { if (in != null) in.close(); } catch (IOException ioe) {} try { if (out != null) out.close(); } catch (IOException ioe) {} } } /** * Try to rename, if it doesn't work then copy and delete the old. * Always overwrites any existing "to" file. * Method moved from SingleFileNamingService. * * @return true if it was renamed / copied successfully * @since 0.8.8 */ public static boolean rename(File from, File to) { if (!from.exists()) return false; boolean success = false; boolean isWindows = SystemVersion.isWindows(); // overwrite fails on windows boolean exists = to.exists(); if (!isWindows || !exists) success = from.renameTo(to); if (!success) { if (exists && to.delete()) success = from.renameTo(to); if (!success) { // hard way success = copy(from, to, true, true); if (success) from.delete(); } } return success; } /** * Usage: FileUtil (delete path | copy source dest | rename from to | unzip path.zip) * */ public static void main(String args[]) { if ( (args == null) || (args.length < 2) ) { System.err.println("Usage: delete path | copy source dest | rename from to | unzip path.zip"); //testRmdir(); } else if ("delete".equals(args[0])) { boolean deleted = FileUtil.rmdir(args[1], false); if (!deleted) System.err.println("Error deleting [" + args[1] + "]"); } else if ("copy".equals(args[0])) { boolean copied = FileUtil.copy(args[1], args[2], false); if (!copied) System.err.println("Error copying [" + args[1] + "] to [" + args[2] + "]"); } else if ("unzip".equals(args[0])) { File f = new File(args[1]); File to = new File("tmp"); to.mkdir(); boolean copied = verifyZip(f); if (!copied) System.err.println("Error verifying " + args[1]); copied = extractZip(f, to); if (copied) System.err.println("Unzipped [" + args[1] + "] to [" + to + "]"); else System.err.println("Error unzipping [" + args[1] + "] to [" + to + "]"); } else if ("rename".equals(args[0])) { boolean success = rename(new File(args[1]), new File(args[2])); if (!success) System.err.println("Error renaming [" + args[1] + "] to [" + args[2] + "]"); } else { System.err.println("Usage: delete path | copy source dest | rename from to | unzip path.zip"); } } /***** private static void testRmdir() { File t = new File("rmdirTest/test/subdir/blah"); boolean created = t.mkdirs(); if (!t.exists()) throw new RuntimeException("Unable to create test"); boolean deleted = FileUtil.rmdir("rmdirTest", false); if (!deleted) System.err.println("FAIL: unable to delete rmdirTest"); else System.out.println("PASS: rmdirTest deleted"); } *****/ }