mirror of
https://github.com/ReneLergner/WPinternals.git
synced 2026-06-16 20:30:12 +10:00
315 lines
13 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|