/* * $Id$ * * Copyright (C) 2003-2015 JNode.org * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published * by the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library 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 Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library; If not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ package org.jnode.fs.ntfs; import java.io.IOException; import java.util.Arrays; import org.apache.log4j.Logger; import org.jnode.util.LittleEndian; /** * @author Daniel Noll (daniel@noll.id.au) */ public final class CompressedDataRun implements DataRunInterface { /** * Size of a compressed block in NTFS. This is always the same even if the cluster size * is not 4k. */ private static final int BLOCK_SIZE = 0x1000; /** * Logger. */ private static final Logger log = Logger.getLogger(CompressedDataRun.class); /** * The underlying data run containing the compressed data. */ private final DataRun compressedRun; /** * The number of clusters which make up a compression unit. */ private final int compressionUnitSize; /** * Constructs a compressed run which when read, will decrypt data found * in the provided data run. * * @param compressedRun the compressed data run. * @param compressionUnitSize the number of clusters which make up a compression unit. */ public CompressedDataRun(DataRun compressedRun, int compressionUnitSize) { this.compressedRun = compressedRun; this.compressionUnitSize = compressionUnitSize; } /** * Gets the length of the data run in clusters. * * @return the length of the run in clusters. */ public int getLength() { return compressionUnitSize; } /** * Reads clusters from this datarun. * * @param vcn the VCN to read, offset from the start of the entire file. * @param dst destination buffer. * @param dstOffset offset into destination buffer. * @param nrClusters number of clusters to read. * @param clusterSize size of each cluster. * @param volume reference to the NTFS volume structure. * @return the number of clusters read. * @throws IOException if an error occurs reading. */ public int readClusters(long vcn, byte[] dst, int dstOffset, int nrClusters, int clusterSize, NTFSVolume volume) throws IOException { // Logic to determine whether we own the VCN which has been requested. // XXX: Lifted from DataRun. Consider moving to some good common location. final long myFirstVcn = compressedRun.getFirstVcn(); final long myLastVcn = getLastVcn(); final long reqLastVcn = vcn + nrClusters - 1; log.debug("me:" + myFirstVcn + "-" + myLastVcn + ", req:" + vcn + "-" + reqLastVcn); if ((vcn > myLastVcn) || (myFirstVcn > reqLastVcn)) { // Not my region return 0; } // Now we know it's in our data run, here's the actual fragment to read. final long actFirstVcn = Math.max(myFirstVcn, vcn); final int actLength = (int) (Math.min(myLastVcn, reqLastVcn) - actFirstVcn + 1); // This is the actual number of stored clusters after compression. // If the number of stored clusters is the same as the compression unit size, // then the data can be read directly without decompressing it. final int compClusters = compressedRun.getLength(); if (compClusters == compressionUnitSize) { return compressedRun.readClusters(vcn, dst, dstOffset, compClusters, clusterSize, volume); } // Now we know the data is compressed. Read in the compressed block... final int vcnOffsetWithinUnit = (int) (actFirstVcn % compressionUnitSize); final byte[] tempCompressed = new byte[compressionUnitSize * clusterSize]; final int read = compressedRun.readClusters(myFirstVcn, tempCompressed, 0, compClusters, clusterSize, volume); if (read != compClusters) { throw new IOException("Needed " + compClusters + " clusters but could " + "only read " + read); } // Uncompress it, and copy into the destination. final byte[] tempUncompressed = new byte[compressionUnitSize * clusterSize]; // XXX: We could potentially reduce the overhead by modifying the compression // routine such that it's capable of skipping chunks that aren't needed. unCompressUnit(tempCompressed, tempUncompressed); int copySource = vcnOffsetWithinUnit * clusterSize; int copyDest = dstOffset + (int) (actFirstVcn - vcn) * clusterSize; int copyLength = actLength * clusterSize; if (copyDest + copyLength > dst.length) { throw new ArrayIndexOutOfBoundsException( String .format("Copy dest %d length %d is too big for destination %d", copyDest, copyLength, dst.length)); } if (copySource + copyLength > tempUncompressed.length) { throw new ArrayIndexOutOfBoundsException( String.format("Copy source %d length %d is too big for source %d", copySource, copyLength, tempUncompressed.length)); } System.arraycopy(tempUncompressed, copySource, dst, copyDest, copyLength); return actLength; } /** * Uncompresses a single unit of multiple compressed blocks. * * @param compressed the compressed data (in.) * @param uncompressed the uncompressed data (out.) * @throws IOException if the decompression fails. */ private static void unCompressUnit(final byte[] compressed, final byte[] uncompressed) throws IOException { // This is just a convenient way to simulate the original code's pointer arithmetic. // I tried using buffers but positions in those are always from the beginning and // I had to also maintain a position from the start of the current block. final OffsetByteArray compressedData = new OffsetByteArray(compressed); final OffsetByteArray uncompressedData = new OffsetByteArray(uncompressed); for (int i = 0; i * BLOCK_SIZE < uncompressed.length; i++) { final int consumed = uncompressBlock(compressedData, uncompressedData); // Apple's code had this as an error but to me it looks like this simply // terminates the sequence of compressed blocks. if (consumed == 0) { // At the current point in time this is already zero but if the code // changes in the future to reuse the temp buffer, this is a good idea. uncompressedData.zero(0, uncompressed.length - uncompressedData.offset); break; } compressedData.offset += consumed; uncompressedData.offset += BLOCK_SIZE; } } /** * Uncompresses a single block. * * @param compressed the compressed buffer (in.) * @param uncompressed the uncompressed buffer (out.) * @return the number of bytes consumed from the compressed buffer. */ private static int uncompressBlock(final OffsetByteArray compressed, final OffsetByteArray uncompressed) { int pos = 0, cpos = 0; final int rawLen = compressed.getShort(cpos); cpos += 2; final int len = rawLen & 0xFFF; log.debug("ntfs_uncompblock: block length: " + len + " + 3, 0x" + Integer.toHexString(len) + ",0x" + Integer.toHexString(rawLen)); if (rawLen == 0) { // End of sequence, rest is zero. For some reason there is nothing // of the sort documented in the Linux kernel's description of compression. return 0; } if ((rawLen & 0x8000) == 0) { // Uncompressed chunks store length as 0xFFF always. if ((len + 1) != BLOCK_SIZE) { log.debug("ntfs_uncompblock: len: " + len + " instead of 0xfff"); } // Copies the entire compression block as-is, need to skip the compression flag, // no idea why they even stored it given that it isn't used. // Darwin's version I was referring to doesn't skip this, which seems be a bug. uncompressed.copyFrom(compressed, cpos, 0, len + 1); uncompressed.zero(len + 1, BLOCK_SIZE - 1 - len); cpos++; return len + 3; } while (cpos < len + 3 && pos < BLOCK_SIZE) { byte ctag = compressed.get(cpos++); for (int i = 0; i < 8 && pos < BLOCK_SIZE; i++) { if ((ctag & 1) != 0) { int j, lmask, dshift; for (j = pos - 1, lmask = 0xFFF, dshift = 12; j >= 0x10; j >>= 1) { dshift--; lmask >>= 1; } final int tmp = compressed.getShort(cpos); cpos += 2; final int boff = -1 - (tmp >> dshift); final int blen = Math.min(3 + (tmp & lmask), BLOCK_SIZE - pos); // Note that boff is negative. uncompressed.copyFrom(uncompressed, pos + boff, pos, blen); pos += blen; } else { uncompressed.put(pos++, compressed.get(cpos++)); } ctag >>= 1; } } return len + 3; } @Override public long getFirstVcn() { return compressedRun.getFirstVcn(); } @Override public long getLastVcn() { return getFirstVcn() + getLength() - 1; } /** * Gets the number of clusters which make up a compression unit. * * @return the number of clusters. */ public int getCompressionUnitSize() { return compressionUnitSize; } /** * Gets the underlying data run containing the compressed data. * * @return the data run. */ public DataRun getCompressedRun() { return compressedRun; } /** * Convenience class wrapping an array with its offset. An alternative to pointer * arithmetic without going to the level of using an NIO buffer. */ private static class OffsetByteArray { /** * The contained array. */ private final byte[] array; /** * The current offset. */ private int offset; /** * Constructs the offset byte array. The offset begins at zero. * * @param array the contained array. */ private OffsetByteArray(final byte[] array) { this.array = array; } /** * Gets a single byte from the array. * * @param offset the offset from the contained offset. * @return the byte. */ private byte get(int offset) { return array[this.offset + offset]; } /** * Puts a single byte into the array. * * @param offset the offset from the contained offset. * @param value the byte. */ private void put(int offset, byte value) { array[this.offset + offset] = value; } /** * Gets a 16-bit little-endian value from the array. * * @param offset the offset from the contained offset. * @return the short. */ private int getShort(int offset) { return LittleEndian.getUInt16(array, this.offset + offset); } /** * Copies a slice from the provided array into our own array. Uses {@code System.arraycopy} * where possible; if the slices overlap, copies one byte at a time to avoid a problem with * using {@code System.arraycopy} in this situation. * * @param src the source offset byte array. * @param srcOffset offset from the source array's offset. * @param destOffset offset from our own offset. * @param length the number of bytes to copy. */ private void copyFrom(OffsetByteArray src, int srcOffset, int destOffset, int length) { int realSrcOffset = src.offset + srcOffset; int realDestOffset = offset + destOffset; byte[] srcArray = src.array; byte[] destArray = array; // If the arrays are the same and the slices overlap we can't use the optimisation // because System.arraycopy effectively copies to a temp area. :-( if (srcArray == destArray && (realSrcOffset < realDestOffset && realSrcOffset + length > realDestOffset || realDestOffset < realSrcOffset && realDestOffset + length > realSrcOffset)) { // Don't change to System.arraycopy (see above) for (int i = 0; i < length; i++) { destArray[realDestOffset + i] = srcArray[realSrcOffset + i]; } return; } System.arraycopy(srcArray, realSrcOffset, destArray, realDestOffset, length); } /** * Zeroes out elements of the array. * * @param offset the offset from the contained offset. * @param length the number of sequential bytes to zero out. */ private void zero(int offset, int length) { Arrays.fill(array, this.offset + offset, this.offset + offset + length, (byte) 0); } } @Override public String toString() { return String.format("[compressed-run vcn:%d-%d %s]", getFirstVcn(), getLastVcn(), compressedRun); } }