All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.archivepatcher.generator.bsdiff.RandomAccessObject Maven / Gradle / Ivy

The newest version!
// Copyright 2016 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.archivepatcher.generator.bsdiff;

import java.io.Closeable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

// TODO(andrewhayden): clean up the implementations, we only really need two and they can be in
// separate files.

/**
 * Interface which offers random access interface. This class exists to allow BsDiff to use either
 * a RandomAccessFile for disk-based io, or a byte[] for fully in-memory operation.
 */
public interface RandomAccessObject extends DataInput, DataOutput, Closeable {

  /**
   * Returns the length of the file or byte array associated with the RandomAccessObject.
   *
   * @return the length of the file or byte array associated with the RandomAccessObject
   * @throws IOException if unable to determine the length of the file, when backed by a file
   */
  public long length() throws IOException;

  /**
   * Seeks to a specified position, in bytes, into the file or byte array.
   *
   * @param pos the position to seek to
   * @throws IOException if seeking fails
   */
  public void seek(long pos) throws IOException;

  /**
   * Seek to a specified integer-aligned position in the associated file or byte array. For example,
   * seekToIntAligned(5) will seek to the beginning of the 5th integer, or in other words the 20th
   * byte. In general, seekToIntAligned(n) will seek to byte 4n.
   *
   * @param pos the position to seek to
   * @throws IOException if seeking fails
   */
  public void seekToIntAligned(long pos) throws IOException;

  /**
   * A {@link RandomAccessFile}-based implementation of {@link RandomAccessObject} which just
   * delegates all operations to the equivalents in {@link RandomAccessFile}. Slower than the
   * {@link RandomAccessMmapObject} implementation.
   */
  public static final class RandomAccessFileObject extends RandomAccessFile
      implements RandomAccessObject {
    private final boolean mShouldDeleteFileOnClose;
    private final File mFile;

    /**
     * This constructor takes in a temporary file. This constructor does not take ownership of the
     * file, and the file will not be deleted on {@link #close()}.
     *
     * @param tempFile the file backing this object
     * @param mode the mode to use, e.g. "r" or "w" for read or write
     * @throws IOException if unable to open the file for the specified mode
     */
    public RandomAccessFileObject(final File tempFile, final String mode) throws IOException {
      this(tempFile, mode, false);
    }

    /**
     * This constructor takes in a temporary file. If deleteFileOnClose is true, the constructor
     * takes ownership of that file, and this file is deleted on close().
     *
     * @param tempFile the file backing this object
     * @param mode the mode to use, e.g. "r" or "w" for read or write
     * @param deleteFileOnClose if true the constructor takes ownership of that file, and this file
     *     is deleted on close().
     * @throws IOException if unable to open the file for the specified mode
     * @throws IllegalArgumentException if the size of the file is too great
     */
    // TODO(hartmanng): rethink the handling of these temp files. It's confusing and shouldn't
    // really be the responsibility of RandomAccessObject.
    public RandomAccessFileObject(final File tempFile, final String mode, boolean deleteFileOnClose)
        throws IOException {
      super(tempFile, mode);
      mShouldDeleteFileOnClose = deleteFileOnClose;
      mFile = tempFile;
      if (mShouldDeleteFileOnClose) {
        mFile.deleteOnExit();
      }
    }

    @Override
    public void seekToIntAligned(long pos) throws IOException {
      seek(pos * 4);
    }

    /**
     * Close the associated file. Also delete the associated temp file if specified in the
     * constructor. This should be called on every RandomAccessObject when it is no longer needed.
     */
    // TODO(hartmanng): rethink the handling of these temp files. It's confusing and shouldn't
    // really be the responsibility of RandomAccessObject.
    @Override
    public void close() throws IOException {
      super.close();
      if (mShouldDeleteFileOnClose) {
        mFile.delete();
      }
    }
  }

  /**
   * An array-based implementation of {@link RandomAccessObject} for entirely in-memory operations.
   */
  public static class RandomAccessByteArrayObject implements RandomAccessObject {
    protected ByteBuffer mByteBuffer;

    /**
     * The passed-in byte array will be treated as big-endian when dealing with ints.
     *
     * @param byteArray the byte array to wrap
     */
    public RandomAccessByteArrayObject(final byte[] byteArray) {
      mByteBuffer = ByteBuffer.wrap(byteArray);
    }

    /**
     * Allocate a new ByteBuffer of given length. This will be treated as big-endian.
     *
     * @param length the length of the buffer to allocate
     */
    public RandomAccessByteArrayObject(final int length) {
      mByteBuffer = ByteBuffer.allocate(length);
    }

    protected RandomAccessByteArrayObject() {
      // No-op, this is just used by the extending class RandomAccessMmapObject.
    }

    @Override
    public long length() {
      return mByteBuffer.capacity();
    }

    @Override
    public byte readByte() {
      return mByteBuffer.get();
    }

    /**
     * Reads an integer from the underlying data array in big-endian order.
     */
    @Override
    public int readInt() {
      return mByteBuffer.getInt();
    }

    public void writeByte(byte b) {
      mByteBuffer.put(b);
    }

    @Override
    public void writeInt(int i) {
      mByteBuffer.putInt(i);
    }

    /**
     * RandomAccessByteArrayObject.seek() only supports addresses up to 2^31-1, due to the fact
     * that it needs to support byte[] backend (and in Java, arrays are indexable only by int).
     * This means that it can only seek up to a 2GiB byte[].
     */
    @Override
    public void seek(long pos) {
      if (pos > Integer.MAX_VALUE) {
        throw new IllegalArgumentException(
            "RandomAccessByteArrayObject can only handle seek() "
                + "addresses up to Integer.MAX_VALUE.");
      }

      mByteBuffer.position((int) pos);
    }

    @Override
    public void seekToIntAligned(long pos) {
      seek(pos * 4);
    }

    @Override
    public void close() throws IOException {
      // Nothing necessary.
    }

    @Override
    public boolean readBoolean() {
      return readByte() != 0;
    }

    @Override
    public char readChar() {
      return mByteBuffer.getChar();
    }

    @Override
    public double readDouble() {
      return mByteBuffer.getDouble();
    }

    @Override
    public float readFloat() {
      return mByteBuffer.getFloat();
    }

    @Override
    public void readFully(byte[] b) {
      mByteBuffer.get(b);
    }

    @Override
    public void readFully(byte[] b, int off, int len) {
      mByteBuffer.get(b, off, len);
    }

    @Override
    public String readLine() {
      throw new UnsupportedOperationException();
    }

    @Override
    public long readLong() {
      return mByteBuffer.getLong();
    }

    @Override
    public short readShort() {
      return mByteBuffer.getShort();
    }

    @Override
    public int readUnsignedByte() {
      return mByteBuffer.get() & 0xff;
    }

    @Override
    public int readUnsignedShort() {
      throw new UnsupportedOperationException();
    }

    @Override
    public String readUTF() {
      throw new UnsupportedOperationException();
    }

    @Override
    public int skipBytes(int n) {
      mByteBuffer.position(mByteBuffer.position() + n);
      return n;
    }

    @Override
    public void write(byte[] b) {
      mByteBuffer.put(b);
    }

    @Override
    public void write(byte[] b, int off, int len) {
      mByteBuffer.put(b, off, len);
    }

    @Override
    public void write(int b) {
      writeByte((byte) b);
    }

    @Override
    public void writeBoolean(boolean v) {
      writeByte(v ? (byte) 1 : (byte) 0);
    }

    @Override
    public void writeByte(int v) {
      writeByte((byte) v);
    }

    @Override
    public void writeBytes(String s) {
      for (int x = 0; x < s.length(); x++) {
        writeByte((byte) s.charAt(x));
      }
    }

    @Override
    public void writeChar(int v) {
      mByteBuffer.putChar((char) v);
    }

    @Override
    public void writeChars(String s) {
      for (int x = 0; x < s.length(); x++) {
        writeChar(s.charAt(x));
      }
    }

    @Override
    public void writeDouble(double v) {
      mByteBuffer.putDouble(v);
    }

    @Override
    public void writeFloat(float v) {
      mByteBuffer.putFloat(v);
    }

    @Override
    public void writeLong(long v) {
      mByteBuffer.putLong(v);
    }

    @Override
    public void writeShort(int v) {
      mByteBuffer.putShort((short) v);
    }

    @Override
    public void writeUTF(String s) {
      throw new UnsupportedOperationException();
    }
  }

  /**
   * A {@link ByteBuffer}-based implementation of {@link RandomAccessObject} that uses files on
   * disk, but is significantly faster than the RandomAccessFile implementation.
   */
  public static final class RandomAccessMmapObject extends RandomAccessByteArrayObject {
    private final boolean mShouldDeleteFileOnRelease;
    private final File mFile;
    private final FileChannel mFileChannel;

    public RandomAccessMmapObject(final RandomAccessFile randomAccessFile, String mode)
        throws IOException, IllegalArgumentException {
      if (randomAccessFile.length() > Integer.MAX_VALUE) {
        throw new IllegalArgumentException("Only files up to 2GiB in size are supported.");
      }

      FileChannel.MapMode mapMode;
      if (mode.equals("r")) {
        mapMode = FileChannel.MapMode.READ_ONLY;
      } else {
        mapMode = FileChannel.MapMode.READ_WRITE;
      }

      mFileChannel = randomAccessFile.getChannel();
      mByteBuffer = mFileChannel.map(mapMode, 0, randomAccessFile.length());
      mByteBuffer.position(0);
      mShouldDeleteFileOnRelease = false;
      mFile = null;
    }

    /**
     * This constructor creates a temporary file. This file is deleted on close(), so be sure to
     * call it when you're done, otherwise it'll leave stray files.
     *
     * @param tempFileName the path to the file backing this object
     * @param mode the mode to use, e.g. "r" or "w" for read or write
     * @param length the size of the file to be read or written
     * @throws IOException if unable to open the file for the specified mode
     * @throws IllegalArgumentException if the size of the file is too great
     */
    // TODO(hartmanng): rethink the handling of these temp files. It's confusing and shouldn't
    // really be the responsibility of RandomAccessObject.
    @SuppressWarnings("resource") // RandomAccessFile deliberately left open
    public RandomAccessMmapObject(final String tempFileName, final String mode, long length)
        throws IOException, IllegalArgumentException {
      if (length > Integer.MAX_VALUE) {
        throw new IllegalArgumentException(
            "RandomAccessMmapObject only supports file sizes up to " + "Integer.MAX_VALUE.");
      }

      mFile = File.createTempFile(tempFileName, "temp");
      mFile.deleteOnExit();
      mShouldDeleteFileOnRelease = true;

      FileChannel.MapMode mapMode;
      if (mode.equals("r")) {
        mapMode = FileChannel.MapMode.READ_ONLY;
      } else {
        mapMode = FileChannel.MapMode.READ_WRITE;
      }

      RandomAccessFile file = null;
      try {
        file = new RandomAccessFile(mFile, mode);
        mFileChannel = file.getChannel();
        mByteBuffer = mFileChannel.map(mapMode, 0, (int) length);
        mByteBuffer.position(0);
      } catch (IOException e) {
        if (file != null) {
          try {
            file.close();
          } catch (Exception ignored) {
            // Nothing more can be done
          }
        }
        close();
        throw new IOException("Unable to open file", e);
      }
    }

    /**
     * This constructor takes in a temporary file, and takes ownership of that file. This file is
     * deleted on close() OR IF THE CONSTRUCTOR FAILS. The main purpose of this constructor is to
     * test close() on the passed-in file.
     *
     * @param tempFile the the file backing this object
     * @param mode the mode to use, e.g. "r" or "w" for read or write
     * @throws IOException if unable to open the file for the specified mode
     * @throws IllegalArgumentException if the size of the file is too great
     */
    // TODO(hartmanng): rethink the handling of these temp files. It's confusing and shouldn't
    // really be the responsibility of RandomAccessObject.
    @SuppressWarnings("resource") // RandomAccessFile deliberately left open
    public RandomAccessMmapObject(final File tempFile, final String mode)
        throws IOException, IllegalArgumentException {
      if (tempFile.length() > Integer.MAX_VALUE) {
        throw new IllegalArgumentException("Only files up to 2GiB in size are supported.");
      }

      mFile = tempFile;
      mFile.deleteOnExit();
      mShouldDeleteFileOnRelease = true;

      FileChannel.MapMode mapMode;
      if (mode.equals("r")) {
        mapMode = FileChannel.MapMode.READ_ONLY;
      } else {
        mapMode = FileChannel.MapMode.READ_WRITE;
      }

      RandomAccessFile file = null;
      try {
        file = new RandomAccessFile(mFile, mode);
        mFileChannel = file.getChannel();
        mByteBuffer = mFileChannel.map(mapMode, 0, tempFile.length());
        mByteBuffer.position(0);
      } catch (IOException e) {
        if (file != null) {
          try {
            file.close();
          } catch (Exception ignored) {
            // Nothing more can be done
          }
        }
        close();
        throw new IOException("Unable to open file", e);
      }
    }

    @Override
    public void close() throws IOException {
      if (mFileChannel != null) {
        mFileChannel.close();
      }

      // There is a long-standing bug with memory mapped objects in Java that requires the JVM to
      // finalize the MappedByteBuffer reference before the unmap operation is performed. This leaks
      // file handles and fills the virtual address space. Worse, on some systems (Windows for one)
      // the active mmap prevents the temp file from being deleted - even if File.deleteOnExit() is
      // used. The only safe way to ensure that file handles and actual files are not leaked by this
      // class is to force an explicit full gc after explicitly nulling the MappedByteBuffer
      // reference. This has to be done before attempting file deletion.
      //
      // See https://github.com/andrewhayden/archive-patcher/issues/5 for more information.
      mByteBuffer = null;
      System.gc();

      if (mShouldDeleteFileOnRelease && mFile != null) {
        mFile.delete();
      }

    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy