package org.jnode.fs.hfsplus.compression; import java.io.IOException; import java.nio.ByteBuffer; import org.apache.log4j.Logger; import org.jnode.fs.hfsplus.HfsPlusFile; import org.jnode.fs.hfsplus.HfsPlusFileSystem; import org.jnode.fs.hfsplus.attributes.AttributeData; import org.jnode.fs.util.FSUtils; import org.jnode.util.BigEndian; import org.jnode.util.LittleEndian; /** * LZVN compressed data stored off in the file's resource fork. * * Adapted from: https://github.com/Piker-Alpha/LZVN/blob/master/C/lzvn_decode.c * * @author Luke Quinane */ public class LzvnForkCompression implements HfsPlusCompression { /** * The logger for this class. */ private static final Logger log = Logger.getLogger(LzvnForkCompression.class); /** * The LZVN fork compression chunk size. */ private static final int LZVN_FORK_CHUNK_SIZE = 0x10000; /** * The LZVN fork compression chunk workspace size. */ private static final int LZVN_FORK_WORKSPACE_SIZE = 0x80000; /** * The case table lookup values. */ private static final short[] CASE_TABLE = { 1, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 1, 1, 4, 3, 1, 1, 1, 1, 1, 1, 4, 3, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 1, 5, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 1, 1, 1, 1, 1, 1, 0, 3, 1, 1, 1, 1, 1, 1, 0, 3, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 9, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10 }; private static final int LZVN_0 = 0; private static final int LZVN_1 = 1; private static final int LZVN_2 = 2; private static final int LZVN_3 = 3; private static final int LZVN_4 = 4; private static final int LZVN_5 = 5; private static final int LZVN_6 = 6; private static final int LZVN_7 = 7; private static final int LZVN_8 = 8; private static final int LZVN_9 = 9; private static final int LZVN_10 = 10; private static final int LZVN_11 = 11; private static final int LZVN_CASE_TABLE = 127; /** * The HFS+ file. */ private final HfsPlusFile file; /** * The detail of the fork compression if it is being used. */ private LzvnForkCompressionDetails lzvnForkCompressionDetails; /** * Creates a new decompressor. * * @param file the file to read from. */ public LzvnForkCompression(HfsPlusFile file) { this.file = file; } @Override public void read(HfsPlusFileSystem fs, long fileOffset, ByteBuffer dest) throws IOException { if (lzvnForkCompressionDetails == null) { lzvnForkCompressionDetails = new LzvnForkCompressionDetails(fs, file.getCatalogFile().getResources()); } while (dest.remaining() > 0) { int chunk = (int) (fileOffset / LZVN_FORK_CHUNK_SIZE); long chunkOffset = lzvnForkCompressionDetails.getChunkOffset(chunk); long nextChunkOffset = lzvnForkCompressionDetails.getChunkOffset(chunk + 1); long chunkLength = nextChunkOffset - chunkOffset; // Read in the compressed chunk ByteBuffer compressed = ByteBuffer.allocate((int) chunkLength); file.getCatalogFile().getResources().read(fs, chunkOffset, compressed); // Decompress the chunk ByteBuffer uncompressed = ByteBuffer.allocate(LZVN_FORK_WORKSPACE_SIZE); int decodedLength = lzvnDecode(compressed, uncompressed); // Copy the data into the destination buffer uncompressed.position((int) fileOffset % LZVN_FORK_CHUNK_SIZE); int copySize = Math.min(dest.remaining(), decodedLength); uncompressed.limit(Math.min(uncompressed.capacity(), uncompressed.position() + copySize)); dest.put(uncompressed); fileOffset += copySize; } } /** * Decodes the data in the compressed buffer into the uncompressed buffer. * * @param compressedByteBuffer the buffer to read from. * @param decompressedByteBuffer the buffer to write to. * @return the number of bytes written to the decompressed buffer. */ public static int lzvnDecode(ByteBuffer compressedByteBuffer, ByteBuffer decompressedByteBuffer) { long destOffset = 0; byte[] uncompressedBuffer = decompressedByteBuffer.array(); byte[] compressedBuffer = compressedByteBuffer.array(); long caseTableIndex; long byteCount = 0; long currentLength = 0; long negativeOffset = 0; long address; int jmpTo = LZVN_CASE_TABLE; int decompressedSize = decompressedByteBuffer.remaining(); decompressedSize -= 8; if (decompressedSize < 8) { return 0; } long sourceOffset = 0; long sourceValue = LittleEndian.getInt64(compressedBuffer, 0); caseTableIndex = compressedBuffer[0] & 255; do { switch (jmpTo) { case LZVN_CASE_TABLE: log.debug(String .format("caseTable[%d]", LzvnForkCompression.CASE_TABLE[FSUtils.checkedCast(caseTableIndex)])); switch (LzvnForkCompression.CASE_TABLE[FSUtils.checkedCast(caseTableIndex)]) { case 0: caseTableIndex >>= 6; sourceOffset = sourceOffset + caseTableIndex + 1; byteCount = 56; byteCount &= sourceValue; sourceValue >>>= 8; byteCount >>>= 3; byteCount += 3; jmpTo = LZVN_10; break; case 1: caseTableIndex >>>= 6; sourceOffset = sourceOffset + caseTableIndex + 2; negativeOffset = sourceValue; negativeOffset = ReverseInt64(negativeOffset); byteCount = negativeOffset; negativeOffset <<= 5; byteCount <<= 2; negativeOffset >>>= 53; byteCount >>>= 61; sourceValue >>>= 16; byteCount += 3; jmpTo = LZVN_10; break; case 2: return (int) destOffset; case 3: caseTableIndex >>>= 6; sourceOffset = sourceOffset + caseTableIndex + 3; byteCount = 56; negativeOffset = 65535; byteCount &= sourceValue; sourceValue >>>= 8; byteCount >>>= 3; negativeOffset &= sourceValue; sourceValue >>>= 16; byteCount += 3; jmpTo = LZVN_10; break; case 4: sourceOffset++; sourceValue = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(sourceOffset)); caseTableIndex = (sourceValue & 255); jmpTo = LZVN_CASE_TABLE; break; case 5: return 0; case 6: caseTableIndex >>>= 3; caseTableIndex &= 3; sourceOffset = sourceOffset + caseTableIndex + 3; byteCount = sourceValue; byteCount &= 775; sourceValue >>>= 10; negativeOffset = (byteCount & 255); byteCount >>>= 8; negativeOffset <<= 2; byteCount |= negativeOffset; negativeOffset = 16383; byteCount += 3; negativeOffset &= sourceValue; sourceValue >>>= 14; jmpTo = LZVN_10; break; case 7: sourceValue >>>= 8; sourceValue &= 255; sourceValue += 16; sourceOffset = sourceOffset + sourceValue + 2; jmpTo = LZVN_0; break; case 8: sourceValue &= 15; sourceOffset = sourceOffset + sourceValue + 1; jmpTo = LZVN_0; break; case 9: sourceOffset += 2; byteCount = sourceValue; byteCount >>>= 8; byteCount &= 255; byteCount += 16; jmpTo = LZVN_11; break; case 10: sourceOffset++; byteCount = sourceValue; byteCount &= 15; jmpTo = LZVN_11; break; default: throw new IllegalStateException("Invalid case table value"); } break; case LZVN_0: log.debug("jmpTable(0)"); currentLength = destOffset + sourceValue; sourceValue = -sourceValue; if (currentLength > decompressedSize) { jmpTo = LZVN_2; break; } case LZVN_1: log.debug("jmpTable(1)"); do { address = sourceOffset + sourceValue; caseTableIndex = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(address)); address = currentLength + sourceValue; LittleEndian.setInt64(uncompressedBuffer, FSUtils.checkedCast(address), caseTableIndex); sourceValue += 8; } while ((0xffffffffffffffffL - (sourceValue - 8)) >= 8); destOffset = currentLength; sourceValue = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(sourceOffset)); caseTableIndex = (sourceValue & 255); jmpTo = LZVN_CASE_TABLE; break; case LZVN_2: log.debug("jmpTable(2)"); currentLength = (decompressedSize + 8); case LZVN_3: log.debug("jmpTable(3)"); do { address = sourceOffset + sourceValue; caseTableIndex = compressedBuffer[FSUtils.checkedCast(address)] & 255; uncompressedBuffer[FSUtils.checkedCast(destOffset)] = (byte) caseTableIndex; destOffset++; if (currentLength == destOffset) { return (int) destOffset; } sourceValue++; } while (sourceValue != 0); sourceValue = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(sourceOffset)); caseTableIndex = (sourceValue & 255); jmpTo = LZVN_CASE_TABLE; break; case LZVN_4: log.debug("jmpTable(4)"); currentLength = (decompressedSize + 8); jmpTo = LZVN_9; break; case LZVN_5: log.debug("jmpTable(5)"); do { address = sourceValue; caseTableIndex = LittleEndian.getInt64(uncompressedBuffer, FSUtils.checkedCast(address)); sourceValue += 8; LittleEndian.setInt64(uncompressedBuffer, FSUtils.checkedCast(destOffset), caseTableIndex); destOffset += 8; byteCount -= 8; } while ((byteCount + 8) > 8); destOffset += byteCount; sourceValue = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(sourceOffset)); caseTableIndex = (sourceValue & 255); jmpTo = LZVN_CASE_TABLE; break; case LZVN_6: log.debug("jmpTable(6)"); do { uncompressedBuffer[FSUtils.checkedCast(destOffset)] = (byte) (sourceValue & 0xff); destOffset++; if (destOffset == currentLength) { return (int) destOffset; } sourceValue >>>= 8; caseTableIndex--; } while (caseTableIndex != 1); case LZVN_7: log.debug("jmpTable(7)"); sourceValue = destOffset; sourceValue -= negativeOffset; if (sourceValue < negativeOffset) { return 0; } jmpTo = LZVN_4; break; case LZVN_8: log.debug("jmpTable(8)"); if (caseTableIndex == 0) { jmpTo = LZVN_7; break; } currentLength = (decompressedSize + 8); jmpTo = LZVN_6; break; case LZVN_9: log.debug("jmpTable(9)"); do { address = sourceValue; caseTableIndex = uncompressedBuffer[FSUtils.checkedCast(address)] & 255; sourceValue++; uncompressedBuffer[FSUtils.checkedCast(destOffset)] = (byte) caseTableIndex; destOffset++; if (destOffset == currentLength) { return (int) destOffset; } byteCount--; } while (byteCount != 0); sourceValue = LittleEndian.getInt64(compressedBuffer, FSUtils.checkedCast(sourceOffset)); caseTableIndex = (sourceValue & 255); jmpTo = LZVN_CASE_TABLE; break; case LZVN_10: log.debug("jmpTable(10)"); currentLength = (destOffset + caseTableIndex); currentLength += byteCount; if (currentLength < decompressedSize) { LittleEndian.setInt64(uncompressedBuffer, FSUtils.checkedCast(destOffset), sourceValue); destOffset += caseTableIndex; sourceValue = destOffset; if (sourceValue < negativeOffset) { return 0; } sourceValue -= negativeOffset; if (negativeOffset < 8) { jmpTo = LZVN_4; break; } jmpTo = LZVN_5; break; } jmpTo = LZVN_8; break; case LZVN_11: log.debug("jmpTable(11)"); sourceValue = destOffset; sourceValue -= negativeOffset; currentLength = destOffset + byteCount; if (currentLength < decompressedSize) { if (negativeOffset >= 8) { jmpTo = LZVN_5; break; } } jmpTo = LZVN_4; break; } } while (true); } /** * Reverses the byte order of a 64-bit integer value. * * @param value the value to switch. * @return the switched value. */ private static long ReverseInt64(long value) { byte[] swapBuffer = new byte[8]; LittleEndian.setInt64(swapBuffer, 0, value); return BigEndian.getInt64(swapBuffer, 0); } /** * The factory for this compression type. */ public static class Factory implements HfsPlusCompressionFactory { @Override public HfsPlusCompression createDecompressor(HfsPlusFile file, AttributeData attributeData, DecmpfsDiskHeader decmpfsDiskHeader) { return new LzvnForkCompression(file); } } }