freenet.support.io.FileUtil.java Source code

Java tutorial

Introduction

Here is the source code for freenet.support.io.FileUtil.java

Source

/* This code is part of Freenet. It is distributed under the GNU General
 * Public License, version 2 (or at your option any later version). See
 * http://www.gnu.org/ for further details of the GPL. */
package freenet.support.io;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Random;

import org.bouncycastle.crypto.BufferedBlockCipher;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.io.CipherInputStream;
import org.bouncycastle.crypto.modes.SICBlockCipher;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;

import freenet.client.DefaultMIMETypes;
import freenet.node.NodeStarter;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.SizeUtil;
import freenet.support.StringValidityChecker;
import freenet.support.Logger.LogLevel;

final public class FileUtil {

    public static final int BUFFER_SIZE = 32 * 1024;

    /**
     * Returns a line reading stream for the content of <code>logfile</code>. The stream will
     * contain at most <code>byteLimit</code> bytes. If <code>byteLimit</code> is less than the
     * size of <code>logfile</code>, the first part of the file will be skipped. If this leaves a
     * partial line at the beginning of the content to read, that partial line will also be
     * skipped.
     * @param logfile The file to open
     * @param byteLimit The maximum number of bytes to read
     * @return A line reader for the trailing portion of the file
     * @throws java.io.IOException if an I/O error occurs
     */
    public static LineReadingInputStream getLogTailReader(File logfile, long byteLimit) throws IOException {
        long length = logfile.length();
        long skip = 0;
        if (length > byteLimit) {
            skip = length - byteLimit;
        }

        FileInputStream fis = null;
        LineReadingInputStream lis = null;
        try {
            fis = new FileInputStream(logfile);
            lis = new LineReadingInputStream(fis);
            if (skip > 0) {
                lis.skip(skip);
                lis.readLine(100000, 200, true);
            }
        } catch (IOException e) {
            Closer.close(lis);
            Closer.close(fis);
            throw e;
        }
        return lis;
    }

    public static enum OperatingSystem {
        Unknown(false, false, false), // Special-cased in filename sanitising code.
        MacOS(false, true, true), // OS/X in that it can run scripts.
        Linux(false, false, true), FreeBSD(false, false, true), GenericUnix(false, false, true), Windows(true,
                false, false);

        public final boolean isWindows;
        public final boolean isMac;
        public final boolean isUnix;

        OperatingSystem(boolean win, boolean mac, boolean unix) {
            this.isWindows = win;
            this.isMac = mac;
            this.isUnix = unix;
        };
    };

    public static enum CPUArchitecture {
        Unknown, X86, X86_64, PPC_32, PPC_64, ARM, SPARC, IA64
    }

    public static final OperatingSystem detectedOS;

    /** Caveat: Sometimes this may not be entirely accurate, e.g. we may not be able to distinguish
     * 32-bit from 64-bit, we may be using the wrong JVM for the platform, we may be using an x86 
     * wrapper or JVM on an IA64 system etc. This *should* be the version the JVM is running. */
    public static final CPUArchitecture detectedArch;

    private static final Charset fileNameCharset;

    static {
        detectedOS = detectOperatingSystem();

        detectedArch = detectCPUArchitecture();

        // I did not find any way to detect the Charset of the file system so I'm using the file encoding charset.
        // On Windows and Linux this is set based on the users configured system language which is probably equal to the filename charset.
        // The worst thing which can happen if we misdetect the filename charset is that downloads fail because the filenames are invalid:
        // We disallow path and file separator characters anyway so its not possible to cause files to be stored in arbitrary places.
        fileNameCharset = getFileEncodingCharset();
    }

    private static volatile boolean logMINOR;
    static {
        Logger.registerLogThresholdCallback(new LogThresholdCallback() {
            @Override
            public void shouldUpdate() {
                logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
            }
        });
    }

    /**
     * Detects the operating system in which the JVM is running. Returns OperatingSystem.Unknown if the OS is unknown or an error occured.
     * Therefore this function should never throw.
     */
    private static OperatingSystem detectOperatingSystem() { // TODO: Move to the proper class
        try {
            final String name = System.getProperty("os.name").toLowerCase();

            // Order the if() by probability instead alphabetically to decrease the false-positive rate in case they decide to call it "Windows Mac" or whatever

            // Please adapt sanitizeFileName when adding new OS.

            if (name.indexOf("win") >= 0)
                return OperatingSystem.Windows;

            if (name.indexOf("mac") >= 0)
                return OperatingSystem.MacOS;

            if (name.indexOf("linux") >= 0)
                return OperatingSystem.Linux;

            if (name.indexOf("freebsd") >= 0)
                return OperatingSystem.FreeBSD;

            if (name.indexOf("unix") >= 0)
                return OperatingSystem.GenericUnix;
            else if (File.separatorChar == '/')
                return OperatingSystem.GenericUnix;
            else if (File.separatorChar == '\\')
                return OperatingSystem.Windows;

            Logger.error(FileUtil.class, "Unknown operating system:" + name);
        } catch (Throwable t) {
            Logger.error(FileUtil.class, "Operating system detection failed", t);
        }

        return OperatingSystem.Unknown;
    }

    private static CPUArchitecture detectCPUArchitecture() { // TODO Move to the proper class
        try {
            final String name = System.getProperty("os.arch").toLowerCase();
            if (name.equals("x86") || name.equals("i386") || name.matches("i[3-9]86"))
                return CPUArchitecture.X86;
            if (name.equals("amd64") || name.equals("x86-64") || name.equals("x86_64") || name.equals("x86")
                    || name.equals("em64t") || name.equals("x8664") || name.equals("8664"))
                return CPUArchitecture.X86_64;
            if (name.startsWith("arm"))
                return CPUArchitecture.ARM; // FIXME arm64 support?
            if (name.equals("ppc") || name.equals("powerpc"))
                return CPUArchitecture.PPC_32;
            if (name.equals("ppc64"))
                return CPUArchitecture.PPC_64;
            if (name.startsWith("ia64"))
                return CPUArchitecture.IA64;
        } catch (Throwable t) {
            Logger.error(FileUtil.class, "CPU architecture detection failed", t);
        }
        return CPUArchitecture.Unknown;
    }

    /**
     * Returns the Charset which is equal to the "file.encoding" property.
     * This property is set to the users configured system language on windows for example.
     *
     * If any error occurs, the default Charset is returned. Therefore this function should never throw.
     */
    public static Charset getFileEncodingCharset() {
        try {
            return Charset.forName(System.getProperty("file.encoding"));
        } catch (Throwable t) {
            return Charset.defaultCharset();
        }
    }

    /** Round up a value to the next multiple of a power of 2 */
    private static long roundup_2n(long val, int blocksize) {
        int mask = blocksize - 1;
        return (val + mask) & ~mask;
    }

    /**
     * Guesstimate real disk usage for a file with a given filename, of a given length.
     */
    public static long estimateUsage(File file, long flen) {
        /**
         * It's possible that none of these assumptions are accurate for any filesystem;
         * this is intended to be a plausible worst case.
         */
        // Assume 4kB clusters for calculating block usage (NTFS)
        long blockUsage = roundup_2n(flen, 4096);
        // Assume 512 byte filename entries, with 100 bytes overhead, for filename overhead (NTFS)
        String filename = file.getName();
        int nameLength;
        try {
            nameLength = Math.max(filename.getBytes("UTF-16").length, filename.getBytes("UTF-8").length) + 100;
        } catch (UnsupportedEncodingException e) {
            // Impossible.
            throw new RuntimeException("UTF-16 or UTF-8 charset not supported?!");
        }
        long filenameUsage = roundup_2n(nameLength, 512);
        // Assume 50 bytes per block tree overhead with 1kB blocks (reiser3 worst case)
        long extra = (roundup_2n(flen, 1024) / 1024) * 50;
        return blockUsage + filenameUsage + extra;
    }

    /**
     *  Is possParent a parent of filename?
     * Why doesn't java provide this? :(
     * */
    public static boolean isParent(File poss, File filename) {
        File canon = FileUtil.getCanonicalFile(poss);
        File canonFile = FileUtil.getCanonicalFile(filename);

        if (isParentInner(poss, filename))
            return true;
        if (isParentInner(poss, canonFile))
            return true;
        if (isParentInner(canon, filename))
            return true;
        if (isParentInner(canon, canonFile))
            return true;
        return false;
    }

    private static boolean isParentInner(File possParent, File filename) {
        while (true) {
            if (filename.equals(possParent))
                return true;
            filename = filename.getParentFile();
            if (filename == null)
                return false;
        }
    }

    public static File getCanonicalFile(File file) {
        // Having some problems storing File's in db4o ...
        // It would start up, and canonicalise a file with path "/var/lib/freenet-experimental/persistent-temp-24374"
        // to /var/lib/freenet-experimental/var/lib/freenet-experimental/persistent-temp-24374
        // (where /var/lib/freenet-experimental is the current working dir)
        // Regenerating from path worked. So do that here.
        // And yes, it's voodoo.
        String name = file.getPath();
        if (File.pathSeparatorChar == '\\') {
            name = name.toLowerCase();
        }
        file = new File(name);
        File result;
        try {
            result = file.getAbsoluteFile().getCanonicalFile();
        } catch (IOException e) {
            result = file.getAbsoluteFile();
        }
        return result;
    }

    /**
     * Reads the entire content of a file as UTF-8 and returns it.
     * @param file The file to read
     * @return The content of <code>file</code>
     * @throws FileNotFoundException if <code>file</code> cannot be opened
     * @throws IOException if an I/O error occurs
     */
    public static StringBuilder readUTF(File file) throws FileNotFoundException, IOException {
        return readUTF(file, 0);
    }

    /**
     * Reads the content of a file as UTF-8, starting at a specified offset, and returns it.
     * @param file The file to read
     * @param offset The point in <code>file</code> at which to start reading
     * @return The content of <code>file</code>, starting at <code>offset</code>
     * @throws FileNotFoundException if <code>file</code> cannot be opened
     * @throws IOException if an I/O error occurs
     */
    public static StringBuilder readUTF(File file, long offset) throws FileNotFoundException, IOException {
        StringBuilder result = new StringBuilder();
        FileInputStream fis = null;
        BufferedInputStream bis = null;
        InputStreamReader isr = null;

        try {
            fis = new FileInputStream(file);
            skipFully(fis, offset);
            bis = new BufferedInputStream(fis);
            isr = new InputStreamReader(bis, "UTF-8");

            char[] buf = new char[4096];
            int length = 0;

            while ((length = isr.read(buf)) > 0) {
                result.append(buf, 0, length);
            }

        } finally {
            Closer.close(isr);
            Closer.close(bis);
            Closer.close(fis);
        }
        return result;
    }

    /**
     * Reads the entire content of a stream as UTF-8 and returns it.
     * @param stream The stream to read
     * @return The content of <code>stream</code>
     * @throws IOException if an I/O error occurs
     */
    public static StringBuilder readUTF(InputStream stream) throws IOException {
        return readUTF(stream, 0);
    }

    /**
     * Reads the content of a stream as UTF-8, starting at a specified offset, and returns it.
     * @param stream The stream to read
     * @param offset The point in <code>stream</code> at which to start reading
     * @return The content of <code>stream</code>, starting at <code>offset</code>
     * @throws IOException if an I/O error occurs
     */
    public static StringBuilder readUTF(InputStream stream, long offset) throws IOException {
        StringBuilder result = new StringBuilder();
        skipFully(stream, offset);
        InputStreamReader reader = null;
        try {
            reader = new InputStreamReader(stream, "UTF-8");
            char[] buf = new char[4096];
            int length = 0;
            while ((length = reader.read(buf)) > 0) {
                result.append(buf, 0, length);
            }
        } finally {
            Closer.close(reader);
        }
        return result;
    }

    /**
     * Reliably skip a number of bytes or throw.
     */
    public static void skipFully(InputStream is, long skip) throws IOException {
        long skipped = 0;
        while (skipped < skip) {
            long x = is.skip(skip - skipped);
            if (x <= 0)
                throw new IOException("Unable to skip " + (skip - skipped) + " bytes");
            skipped += x;
        }
    }

    public static boolean writeTo(InputStream input, File target) throws FileNotFoundException, IOException {
        DataInputStream dis = null;
        FileOutputStream fos = null;
        File file = File.createTempFile("temp", ".tmp", target.getParentFile());
        if (logMINOR)
            Logger.minor(FileUtil.class, "Writing to " + file + " to be renamed to " + target);

        try {
            dis = new DataInputStream(input);
            fos = new FileOutputStream(file);

            int len = 0;
            byte[] buffer = new byte[4096];
            while ((len = dis.read(buffer)) > 0) {
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            throw e;
        } finally {
            if (dis != null)
                dis.close();
            if (fos != null)
                fos.close();
        }

        if (FileUtil.renameTo(file, target))
            return true;
        else {
            file.delete();
            return false;
        }
    }

    public static boolean renameTo(File orig, File dest) {
        // Try an atomic rename
        // Shall we prevent symlink-race-conditions here ?
        if (orig.equals(dest))
            throw new IllegalArgumentException("Huh? the two file descriptors are the same!");
        if (!orig.exists()) {
            throw new IllegalArgumentException("Original doesn't exist!");
        }
        if (!orig.renameTo(dest)) {
            // Not supported on some systems (Windows)
            if (!dest.delete()) {
                if (dest.exists()) {
                    Logger.error("FileUtil", "Could not delete " + dest + " - check permissions");
                    System.err.println("Could not delete " + dest + " - check permissions");
                }
            }
            if (!orig.renameTo(dest)) {
                String err = "Could not rename " + orig + " to " + dest + (dest.exists() ? " (target exists)" : "")
                        + (orig.exists() ? " (source exists)" : "") + " - check permissions";
                Logger.error(FileUtil.class, err);
                System.err.println(err);
                return false;
            }
        }
        return true;
    }

    /**
     * Like renameTo(), but can move across filesystems, by copying the data.
     * @param orig
     * @param dest
     * @param overwrite
     */
    public static boolean moveTo(File orig, File dest, boolean overwrite) {
        if (orig.equals(dest))
            throw new IllegalArgumentException("Huh? the two file descriptors are the same!");
        if (!orig.exists()) {
            throw new IllegalArgumentException("Original doesn't exist!");
        }
        if (dest.exists()) {
            if (overwrite)
                dest.delete();
            else {
                System.err.println("Not overwriting " + dest + " - already exists moving " + orig);
                return false;
            }
        }
        if (!orig.renameTo(dest))
            return copyFile(orig, dest);
        else
            return true;
    }

    /**
     * Sanitizes the given filename to be valid on the given operating system.
     * If OperatingSystem.Unknown is specified this function will generate a filename which fullfils the restrictions of all known OS, currently
     * this is MacOS, Unix and Windows.
     */
    public static String sanitizeFileName(final String fileName, OperatingSystem targetOS, String extraChars) {
        // Filter out any characters which do not exist in the charset.
        final CharBuffer buffer = fileNameCharset.decode(fileNameCharset.encode(fileName)); // Charset are thread-safe

        final StringBuilder sb = new StringBuilder(fileName.length() + 1);

        switch (targetOS) {
        case Unknown:
            break;
        case MacOS:
            break;
        case Linux:
            break;
        case FreeBSD:
            break;
        case GenericUnix:
            break;
        case Windows:
            break;
        default:
            Logger.error(FileUtil.class, "Unsupported operating system: " + targetOS);
            targetOS = OperatingSystem.Unknown;
            break;
        }

        char def = ' ';
        if (extraChars.indexOf(' ') != -1) {
            def = '_';
            if (extraChars.indexOf(def) != -1) {
                def = '-';
                if (extraChars.indexOf(def) != -1)
                    throw new IllegalArgumentException("What do you want me to use instead of spaces???");
            }
        }

        for (char c : buffer.array()) { // Note that this will add extra whitespace to the end, which we will trim later.

            if (extraChars.indexOf(c) != -1) {
                sb.append(def);
                continue;
            }

            // Control characters and whitespace are converted to space for all OS.
            // We do not check for the file separator character because it is included in each OS list of reserved characters.
            if (Character.getType(c) == Character.CONTROL || Character.isWhitespace(c)) {
                sb.append(def);
                continue;
            }

            if (targetOS == OperatingSystem.Unknown || targetOS.isWindows) {
                if (StringValidityChecker.isWindowsReservedPrintableFilenameCharacter(c)) {
                    sb.append(def);
                    continue;
                }
            }

            if (targetOS == OperatingSystem.Unknown || targetOS.isMac) {
                if (StringValidityChecker.isMacOSReservedPrintableFilenameCharacter(c)) {
                    sb.append(def);
                    continue;
                }
            }

            if (targetOS == OperatingSystem.Unknown || targetOS.isUnix) {
                if (StringValidityChecker.isUnixReservedPrintableFilenameCharacter(c)) {
                    sb.append(def);
                    continue;
                }
            }

            // Nothing did continue; so the character is okay
            sb.append(c);
        }

        // In windows, the last character of a filename may not be space or dot. We cut them off
        if (targetOS == OperatingSystem.Unknown || targetOS.isWindows) {
            int lastCharIndex = sb.length() - 1;
            while (lastCharIndex >= 0) {
                char lastChar = sb.charAt(lastCharIndex);
                if (lastChar == ' ' || lastChar == '.')
                    sb.deleteCharAt(lastCharIndex--);
                else
                    break;
            }
        }

        // Now the filename might be one of the reserved filenames in Windows (CON etc.) and we must replace it if it is...
        if (targetOS == OperatingSystem.Unknown || targetOS.isWindows) {
            if (StringValidityChecker.isWindowsReservedFilename(sb.toString()))
                sb.insert(0, '_');
        }

        if (sb.length() == 0) {
            sb.append("Invalid filename"); // TODO: L10n
        }

        return sb.toString().trim(); // Trim leading and trailing whitespace.
        // Some of the trailing whitespace may be from the CharBuffer.
    }

    public static String sanitize(String fileName) {
        return sanitizeFileName(fileName, detectedOS, "");
    }

    public static String sanitizeFileNameWithExtras(String fileName, String extraChars) {
        return sanitizeFileName(fileName, detectedOS, extraChars);
    }

    public static String sanitize(String filename, String mimeType) {
        filename = sanitize(filename);
        if (mimeType == null)
            return filename;
        return DefaultMIMETypes.forceExtension(filename, mimeType);
    }

    /**
     * Find the length of an input stream. This method will consume the complete
     * input stream until its {@link InputStream#read(byte[])} method returns
     * <code>-1</code>, thus signalling the end of the stream.
     *
     * @param source
     *            The input stream to find the length of
     * @return The numbe of bytes that can be read from the stream
     * @throws IOException
     *             if an I/O error occurs
     */
    public static long findLength(InputStream source) throws IOException {
        long length = 0;
        byte[] buffer = new byte[BUFFER_SIZE];
        int read = 0;
        while (read > -1) {
            read = source.read(buffer);
            if (read != -1) {
                length += read;
            }
        }
        return length;
    }

    /**
     * Copies <code>length</code> bytes from the source input stream to the
     * destination output stream. If <code>length</code> is <code>-1</code>
     * as much bytes as possible will be copied (i.e. until
     * {@link InputStream#read()} returns <code>-1</code> to signal the end of
     * the stream).
     *
     * @param source
     *            The input stream to read from
     * @param destination
     *            The output stream to write to
     * @param length
     *            The number of bytes to copy
     * @throws IOException
     *             if an I/O error occurs
     */
    public static void copy(InputStream source, OutputStream destination, long length) throws IOException {
        long remaining = length;
        byte[] buffer = new byte[BUFFER_SIZE];
        int read = 0;
        while ((remaining == -1) || (remaining > 0)) {
            read = source.read(buffer, 0,
                    ((remaining > BUFFER_SIZE) || (remaining == -1)) ? BUFFER_SIZE : (int) remaining);
            if (read == -1) {
                if (length == -1) {
                    return;
                }
                throw new EOFException("stream reached eof");
            }
            destination.write(buffer, 0, read);
            if (remaining > 0)
                remaining -= read;
        }
    }

    public static boolean secureDeleteAll(File wd) throws IOException {
        if (!wd.isDirectory()) {
            System.err.println("DELETING FILE " + wd);
            try {
                secureDelete(wd);
            } catch (IOException e) {
                Logger.error(FileUtil.class, "Could not delete file: " + wd, e);
                return false;
            }
        } else {
            for (File subfile : wd.listFiles()) {
                if (!removeAll(subfile))
                    return false;
            }
            if (!wd.delete()) {
                Logger.error(FileUtil.class, "Could not delete directory: " + wd);
            }
        }
        return true;
    }

    /** Delete everything in a directory. Only use this when we are *very sure* there is no
     * important data below it! */
    public static boolean removeAll(File wd) {
        if (!wd.isDirectory()) {
            System.err.println("DELETING FILE " + wd);
            if (!wd.delete() && wd.exists()) {
                Logger.error(FileUtil.class, "Could not delete file: " + wd);
                return false;
            }
        } else {
            for (File subfile : wd.listFiles()) {
                if (!removeAll(subfile))
                    return false;
            }
            if (!wd.delete()) {
                Logger.error(FileUtil.class, "Could not delete directory: " + wd);
            }
        }
        return true;
    }

    public static void secureDelete(File file) throws IOException {
        // FIXME somebody who understands these things should have a look at this...
        if (!file.exists())
            return;
        long size = file.length();
        if (size > 0) {
            RandomAccessFile raf = null;
            try {
                System.out.println("Securely deleting " + file + " which is of length " + size + " bytes...");
                raf = new RandomAccessFile(file, "rw");
                long count;
                // Random data first.
                raf.seek(0);
                fill(new RandomAccessFileOutputStream(raf), size);
                raf.getFD().sync();
                raf.close();
                raf = null;
            } finally {
                Closer.close(raf);
            }
        }
        if ((!file.delete()) && file.exists())
            throw new IOException("Unable to delete file " + file);
    }

    @Deprecated
    public static void secureDelete(File file, Random random) throws IOException {
        secureDelete(file);
    }

    /**
    ** Set owner-only RW on the given file.
    */
    public static boolean setOwnerRW(File f) {
        return setOwnerPerm(f, true, true, false);
    }

    /**
    ** Set owner-only RWX on the given file.
    */
    public static boolean setOwnerRWX(File f) {
        return setOwnerPerm(f, true, true, true);
    }

    /**
    ** Set owner-only permissions on the given file.
    */
    public static boolean setOwnerPerm(File f, boolean r, boolean w, boolean x) {
        /* JDK6 replace when we upgrade
        boolean b = f.setReadable(false, false);
        b &= f.setWritable(false, false);
        b &= f.setExecutable(false, false);
        b &= f.setReadable(r, true);
        b &= f.setWritable(w, true);
        b &= f.setExecutable(x, true);
        return b;
        */

        boolean success = true;
        try {

            String[] methods = { "setReadable", "setWritable", "setExecutable" };
            boolean[] perms = { r, w, x };

            for (int i = 0; i < methods.length; ++i) {
                Method m = File.class.getDeclaredMethod(methods[i], boolean.class, boolean.class);
                if (m != null) {
                    success &= (Boolean) m.invoke(f, false, false);
                    success &= (Boolean) m.invoke(f, perms[i], true);
                }
            }

        } catch (NoSuchMethodException e) {
            success = false;
        } catch (java.lang.reflect.InvocationTargetException e) {
            success = false;
        } catch (IllegalAccessException e) {
            success = false;
        } catch (ExceptionInInitializerError e) {
            success = false;
        } catch (RuntimeException e) {
            success = false;
        }
        return success;
    }

    public static boolean equals(File a, File b) {
        if (a == b)
            return true;
        if (a.equals(b))
            return true;
        a = getCanonicalFile(a);
        b = getCanonicalFile(b);
        return a.equals(b);
    }

    /** Create a temp file in a specific directory. Null = ".". 
     * @throws IOException */
    public static File createTempFile(String prefix, String suffix, File directory) throws IOException {
        if (directory == null)
            directory = new File(".");
        return File.createTempFile(prefix, suffix, directory);
    }

    public static boolean copyFile(File copyFrom, File copyTo) {
        copyTo.delete();
        boolean executable = copyFrom.canExecute();
        FileBucket outBucket = new FileBucket(copyTo, false, true, false, false);
        FileBucket inBucket = new FileBucket(copyFrom, true, false, false, false);
        try {
            BucketTools.copy(inBucket, outBucket);
            if (executable) {
                if (!(copyTo.setExecutable(true) || copyTo.canExecute())) {
                    System.err.println("Unable to preserve executable bit when copying " + copyFrom + " to "
                            + copyTo + " - you may need to make it executable!");
                    // return false; ??? FIXME debatable.
                }
            }
            return true;
        } catch (IOException e) {
            System.err.println("Unable to copy from " + copyFrom + " to " + copyTo);
            return false;
        }
    }

    private static CipherInputStream cis;
    private static ZeroInputStream zis = new ZeroInputStream();
    private static long cisCounter;

    /** Write hard to identify random data to the OutputStream. Does not drain the global secure 
     * random number generator, and is significantly faster than it.
     * @param os The stream to write to.
     * @param length The number of bytes to write.
     * @throws IOException If unable to write to the stream.
     */
    public static void fill(OutputStream os, long length) throws IOException {
        long remaining = length;
        byte[] buffer = new byte[BUFFER_SIZE];
        int read = 0;
        while ((remaining == -1) || (remaining > 0)) {
            synchronized (FileUtil.class) {
                if (cis == null || cisCounter > Long.MAX_VALUE / 2) {
                    // Reset it well before the birthday paradox (note this is actually counting bytes).
                    byte[] key = new byte[16];
                    byte[] iv = new byte[16];
                    SecureRandom rng = NodeStarter.getGlobalSecureRandom();
                    rng.nextBytes(key);
                    rng.nextBytes(iv);
                    AESFastEngine e = new AESFastEngine();
                    SICBlockCipher ctr = new SICBlockCipher(e);
                    ctr.init(true, new ParametersWithIV(new KeyParameter(key), iv));
                    cis = new CipherInputStream(zis, new BufferedBlockCipher(ctr));
                    cisCounter = 0;
                }
                read = cis.read(buffer, 0,
                        ((remaining > BUFFER_SIZE) || (remaining == -1)) ? BUFFER_SIZE : (int) remaining);
                cisCounter += read;
            }
            if (read == -1) {
                if (length == -1) {
                    return;
                }
                throw new EOFException("stream reached eof");
            }
            os.write(buffer, 0, read);
            if (remaining > 0)
                remaining -= read;
        }

    }

    /** @deprecated */
    public static void fill(OutputStream os, Random random, long length) throws IOException {
        long moved = 0;
        byte[] buf = new byte[BUFFER_SIZE];
        while (moved < length) {
            int toRead = (int) Math.min(BUFFER_SIZE, length - moved);
            random.nextBytes(buf);
            os.write(buf, 0, toRead);
            moved += toRead;
        }
    }

    public static boolean equalStreams(InputStream a, InputStream b, long size) throws IOException {
        byte[] aBuffer = new byte[BUFFER_SIZE];
        byte[] bBuffer = new byte[BUFFER_SIZE];
        DataInputStream aIn = new DataInputStream(a);
        DataInputStream bIn = new DataInputStream(b);
        long checked = 0;
        while (checked < size) {
            int toRead = (int) Math.min(BUFFER_SIZE, size - checked);
            aIn.readFully(aBuffer, 0, toRead);
            bIn.readFully(bBuffer, 0, toRead);
            if (!MessageDigest.isEqual(aBuffer, bBuffer))
                return false;
            checked += toRead;
        }
        return true;
    }

}