Files
WPinternals/DiscUtils/Ntfs/LZNT1.cs
T
2018-10-25 22:35:49 +02:00

315 lines
13 KiB
C#

//
// Copyright (c) 2008-2011, Kenneth Bell
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//
// Contributions by bsobel:
// - Compression algorithm distantly derived from Puyo tools (BSD license)*
// - Decompression adjusted to support variety of block sizes
//
// (*) Puyo tools implements a different LZ-style algorithm
//
namespace DiscUtils.Ntfs
{
using System;
using DiscUtils.Compression;
/// <summary>
/// Implementation of the LZNT1 algorithm used for compressing NTFS files.
/// </summary>
/// <remarks>
/// Due to apparent bugs in Window's LZNT1 decompressor, it is <b>strongly</b> recommended that
/// only the block size of 4096 is used. Other block sizes corrupt data on decompression.
/// </remarks>
internal sealed class LZNT1 : BlockCompressor
{
private const ushort SubBlockIsCompressedFlag = 0x8000;
private const ushort SubBlockSizeMask = 0x0fff;
// LZNT1 appears to ignore the actual block size requested, most likely due to
// a bug in the decompressor, which assumes 4KB block size. To be bug-compatible,
// we assume each block is 4KB on decode also.
private const int FixedBlockSize = 0x1000;
private static byte[] s_compressionBits = CalcCompressionBits();
public LZNT1()
{
BlockSize = 4096;
}
public override CompressionResult Compress(byte[] source, int sourceOffset, int sourceLength, byte[] compressed, int compressedOffset, ref int compressedLength)
{
uint sourcePointer = 0;
uint sourceCurrentBlock = 0;
uint destPointer = 0;
// Set up the Lz Compression Dictionary
LzWindowDictionary lzDictionary = new LzWindowDictionary();
bool nonZeroDataFound = false;
for (int subBlock = 0; subBlock < sourceLength; subBlock += BlockSize)
{
lzDictionary.MinMatchAmount = 3;
sourceCurrentBlock = sourcePointer;
uint decompressedSize = (uint)Math.Min(sourceLength - subBlock, BlockSize);
uint compressedSize = 0;
// Start compression
uint headerPosition = destPointer;
compressed[compressedOffset + destPointer] = compressed[compressedOffset + destPointer + 1] = 0;
destPointer += 2;
while (sourcePointer - subBlock < decompressedSize)
{
if (destPointer + 1 >= compressedLength)
{
return CompressionResult.Incompressible;
}
byte bitFlag = 0x0;
uint flagPosition = destPointer;
compressed[compressedOffset + destPointer] = bitFlag; // It will be filled in later
compressedSize++;
destPointer++;
for (int i = 0; i < 8; i++)
{
int lengthBits = 16 - s_compressionBits[sourcePointer - subBlock];
ushort lengthMask = (ushort)((1 << s_compressionBits[sourcePointer - subBlock]) - 1);
lzDictionary.MaxMatchAmount = Math.Min(1 << lengthBits, BlockSize - 1);
int[] lzSearchMatch = lzDictionary.Search(source, sourceOffset + subBlock, (uint)(sourcePointer - subBlock), decompressedSize);
if (lzSearchMatch[1] > 0)
{
// There is a compression match
if (destPointer + 2 >= compressedLength)
{
return CompressionResult.Incompressible;
}
bitFlag |= (byte)(1 << i);
int rawOffset = lzSearchMatch[0];
int rawLength = lzSearchMatch[1];
int convertedOffset = (rawOffset - 1) << lengthBits;
int convertedSize = (rawLength - 3) & ((1 << lengthMask) - 1);
ushort convertedData = (ushort)(convertedOffset | convertedSize);
Utilities.WriteBytesLittleEndian(convertedData, compressed, compressedOffset + (int)destPointer);
lzDictionary.AddEntryRange(source, sourceOffset + subBlock, (int)(sourcePointer - subBlock), lzSearchMatch[1]);
sourcePointer += (uint)lzSearchMatch[1];
destPointer += 2;
compressedSize += 2;
}
else
{
// There wasn't a match
if (destPointer + 1 >= compressedLength)
{
return CompressionResult.Incompressible;
}
bitFlag |= (byte)(0 << i);
if (source[sourceOffset + sourcePointer] != 0)
{
nonZeroDataFound = true;
}
compressed[compressedOffset + destPointer] = source[sourceOffset + sourcePointer];
lzDictionary.AddEntry(source, sourceOffset + subBlock, (int)(sourcePointer - subBlock));
sourcePointer++;
destPointer++;
compressedSize++;
}
// Check for out of bounds
if (sourcePointer - subBlock >= decompressedSize)
{
break;
}
}
// Write the real flag.
compressed[compressedOffset + flagPosition] = bitFlag;
}
// If compressed size >= block size just store block
if (compressedSize >= BlockSize)
{
// Set the header to indicate non-compressed block
Utilities.WriteBytesLittleEndian((ushort)(0x3000 | (BlockSize - 1)), compressed, compressedOffset + (int)headerPosition);
Array.Copy(source, (int)sourceOffset + sourceCurrentBlock, compressed, compressedOffset + headerPosition + 2, BlockSize);
destPointer = (uint)(headerPosition + 2 + BlockSize);
// Make sure decompression stops by setting the next two bytes to null, prevents us from having to
// clear the rest of the array.
compressed[destPointer] = 0;
compressed[destPointer + 1] = 0;
}
else
{
// Set the header to indicate compressed and the right length
Utilities.WriteBytesLittleEndian((ushort)(0xb000 | (compressedSize - 1)), compressed, compressedOffset + (int)headerPosition);
}
lzDictionary.Reset();
}
if (destPointer >= sourceLength)
{
compressedLength = 0;
return CompressionResult.Incompressible;
}
else if (nonZeroDataFound)
{
compressedLength = (int)destPointer;
return CompressionResult.Compressed;
}
else
{
compressedLength = 0;
return CompressionResult.AllZeros;
}
}
public override int Decompress(byte[] source, int sourceOffset, int sourceLength, byte[] decompressed, int decompressedOffset)
{
int sourceIdx = 0;
int destIdx = 0;
while (sourceIdx < sourceLength)
{
ushort header = Utilities.ToUInt16LittleEndian(source, sourceOffset + sourceIdx);
sourceIdx += 2;
// Look for null-terminating sub-block header
if (header == 0)
{
break;
}
if ((header & SubBlockIsCompressedFlag) == 0)
{
int blockSize = (header & SubBlockSizeMask) + 1;
Array.Copy(source, sourceOffset + sourceIdx, decompressed, decompressedOffset + destIdx, blockSize);
sourceIdx += blockSize;
destIdx += blockSize;
}
else
{
// compressed
int destSubBlockStart = destIdx;
int srcSubBlockEnd = sourceIdx + (header & SubBlockSizeMask) + 1;
while (sourceIdx < srcSubBlockEnd)
{
byte tag = source[sourceOffset + sourceIdx];
++sourceIdx;
for (int token = 0; token < 8; ++token)
{
// We might have hit the end of the sub block whilst still working though
// a tag - abort if we have...
if (sourceIdx >= srcSubBlockEnd)
{
break;
}
if ((tag & 1) == 0)
{
if (decompressedOffset + destIdx >= decompressed.Length)
{
return destIdx;
}
decompressed[decompressedOffset + destIdx] = source[sourceOffset + sourceIdx];
++destIdx;
++sourceIdx;
}
else
{
ushort lengthBits = (ushort)(16 - s_compressionBits[destIdx - destSubBlockStart]);
ushort lengthMask = (ushort)((1 << lengthBits) - 1);
ushort phraseToken = Utilities.ToUInt16LittleEndian(source, sourceOffset + sourceIdx);
sourceIdx += 2;
int destBackAddr = destIdx - (phraseToken >> lengthBits) - 1;
int length = (phraseToken & lengthMask) + 3;
for (int i = 0; i < length; ++i)
{
decompressed[decompressedOffset + destIdx++] = decompressed[decompressedOffset + destBackAddr++];
}
}
tag >>= 1;
}
}
// Bug-compatible - if we decompressed less than 4KB, jump to next 4KB boundary. If
// that would leave less than a 4KB remaining, abort with data decompressed so far.
if (decompressedOffset + destIdx + FixedBlockSize > decompressed.Length)
{
return destIdx;
}
else if (destIdx < destSubBlockStart + FixedBlockSize)
{
int skip = (destSubBlockStart + FixedBlockSize) - destIdx;
Array.Clear(decompressed, decompressedOffset + destIdx, skip);
destIdx += skip;
}
}
}
return destIdx;
}
private static byte[] CalcCompressionBits()
{
byte[] result = new byte[4096];
byte offsetBits = 0;
int y = 0x10;
for (int x = 0; x < result.Length; x++)
{
result[x] = (byte)(4 + offsetBits);
if (x == y)
{
y <<= 1;
offsetBits++;
}
}
return result;
}
}
}