The start of an attempt to make this program structured better, as well as for easier addition of more formats AND compression for supported formats.

This commit is contained in:
barubary 2011-03-21 19:18:07 +00:00
parent 052b9c0677
commit 8c777e2c02
7 changed files with 440 additions and 1 deletions

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>8.0.50727</ProductVersion>
<ProductVersion>9.0.21022</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{E6F419F9-D6B5-4BE7-99BB-97C48C927FF3}</ProjectGuid>
<OutputType>Exe</OutputType>
@ -40,6 +40,12 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Exceptions\StreamTooShortException.cs" />
<Compile Include="Formats\CompressionFormat.cs" />
<Compile Include="Formats\Nitro\LZ10.cs" />
<Compile Include="Formats\Nitro\LZ11.cs" />
<Compile Include="Formats\Nitro\NitroCFormat.cs" />
<Compile Include="Exceptions\NotEnoughDataException.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>

View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace DSDecmp
{
/// <summary>
/// An exception that is thrown by the decompression functions when there
/// is not enough data available in order to properly decompress the input.
/// </summary>
public class NotEnoughDataException : IOException
{
private long currentOutSize;
private long totalOutSize;
/// <summary>
/// Gets the actual number of written bytes.
/// </summary>
public long WrittenLength { get { return this.currentOutSize; } }
/// <summary>
/// Gets the number of bytes that was supposed to be written.
/// </summary>
public long DesiredLength { get { return this.totalOutSize; } }
/// <summary>
/// Creates a new NotEnoughDataException.
/// </summary>
/// <param name="currentOutSize">The actual number of written bytes.</param>
/// <param name="totalOutSize">The desired number of written bytes.</param>
public NotEnoughDataException(long currentOutSize, long totalOutSize)
: base("Not enough data availble; 0x" + currentOutSize.ToString("X")
+ " of " + (totalOutSize < 0 ? "???" : ("0x" + totalOutSize.ToString("X")))
+ " bytes written.")
{
this.currentOutSize = currentOutSize;
this.totalOutSize = totalOutSize;
}
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace DSDecmp
{
/// <summary>
/// An exception thrown by the compression or decompression function, indicating that the
/// given input length was too large for the given input stream.
/// </summary>
public class StreamTooShortException : EndOfStreamException
{
public StreamTooShortException()
: base("The end of the stream was reached "
+ "before the given amout of data was read.")
{ }
}
}

View File

@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace DSDecmp.Formats
{
/// <summary>
/// Base class for compression formats.
/// </summary>
public abstract class CompressionFormat
{
/// <summary>
/// Checks if the decompressor for this format supports the given file. Assumes the
/// file exists. Returns false when it is certain that the given file is not supported.
/// False positives may occur, as this method should not do any decompression, and
/// may mis-interpret a similar file format as compressed.
/// </summary>
/// <param name="file">The name of the file to check.</param>
/// <returns>False if the file can certainly not be decompressed using this decompressor.
/// True if the file may potentially be decompressed using this decompressor.</returns>
public bool Supports(string file)
{
// open the file, and delegate to the decompressor-specific code.
using (FileStream fstr = new FileStream(file, FileMode.Open))
{
return this.Supports(fstr);
}
}
/// <summary>
/// Checks if the decompressor for this format supports the data from the given stream.
/// Returns false when it is certain that the given data is not supported.
/// False positives may occur, as this method should not do any decompression, and may
/// mis-interpret a similar data format as compressed.
/// </summary>
/// <param name="stream">The stream that may or may not contain compressed data. The
/// position of this stream may change during this call, but will be returned to its
/// original position when the method returns.</param>
/// <returns>False if the data can certainly not be decompressed using this decompressor.
/// True if the data may potentially be decompressed using this decompressor.</returns>
public abstract bool Supports(Stream stream);
/// <summary>
/// Decompresses the given file, writing the deocmpressed data to the given output file.
/// The output file will be overwritten if it already exists.
/// Assumes <code>Supports(infile)</code> returns <code>true</code>.
/// </summary>
/// <param name="infile">The file to decompress.</param>
/// <param name="outfile">The target location of the decompressed file.</param>
public void Decompress(string infile, string outfile)
{
// open the two given files, and delegate to the format-specific code.
using (FileStream inStream = new FileStream(infile, FileMode.Open),
outStream = new FileStream(outfile, FileMode.Create))
{
this.Decompress(inStream, inStream.Length, outStream);
}
}
/// <summary>
/// Decompresses the given stream, writing the decompressed data to the given output stream.
/// Assumes <code>Supports(instream)</code> returns <code>true</code>.
/// After this call, the input stream will be positioned at the end of the compressed stream,
/// or at the initial position + <code>inLength</code>, whichever comes first.
/// </summary>
/// <param name="instream">The stream to decompress. At the end of this method, the position
/// of this stream is directly after the compressed data.</param>
/// <param name="inLength">The length of the input data. Not necessarily all of the
/// input data may be read (if there is padding, for example), however never more than
/// this number of bytes is read from the input stream.</param>
/// <param name="outstream">The stream to write the decompressed data to.</param>
/// <exception cref="NotEnoughDataException">When the given length of the input data
/// is not enough to properly decompress the input.</exception>
public abstract void Decompress(Stream instream, long inLength, Stream outstream);
/// <summary>
/// Compresses the given input file, and writes the compressed data to the given
/// output file.
/// </summary>
/// <param name="infile">The file to compress.</param>
/// <param name="outfile">The file to write the compressed data to.</param>
public void Compress(string infile, string outfile)
{
// open the proper Streams, and delegate to the format-specific code.
using (FileStream inStream = File.Open(infile, FileMode.Open),
outStream = File.Create(outfile))
{
this.Compress(inStream, inStream.Length, outStream);
}
}
/// <summary>
/// Compresses the next <code>inLength</code> bytes from the input stream,
/// and writes the compressed data to the given output stream.
/// </summary>
/// <param name="instream">The stream to read plaintext data from.</param>
/// <param name="inLength">The length of the plaintext data.</param>
/// <param name="outstream">The stream to write the compressed data to.</param>
public abstract void Compress(Stream instream, long inLength, Stream outstream);
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace DSDecmp.Formats.Nitro
{
/// <summary>
/// Decompressor for the LZ-0x10 format used in many of the games for the
/// newer Nintendo consoles and handhelds.
/// </summary>
public class LZ10 : NitroCFormat
{
public LZ10() : base(0x10) { }
public override void Decompress(Stream instream, long inLength,
Stream outstream)
{
/* Data header (32bit)
Bit 0-3 Reserved
Bit 4-7 Compressed type (must be 1 for LZ77)
Bit 8-31 Size of decompressed data
Repeat below. Each Flag Byte followed by eight Blocks.
Flag data (8bit)
Bit 0-7 Type Flags for next 8 Blocks, MSB first
Block Type 0 - Uncompressed - Copy 1 Byte from Source to Dest
Bit 0-7 One data byte to be copied to dest
Block Type 1 - Compressed - Copy N+3 Bytes from Dest-Disp-1 to Dest
Bit 0-3 Disp MSBs
Bit 4-7 Number of bytes to copy (minus 3)
Bit 8-15 Disp LSBs
*/
long readBytes = 0;
byte type = (byte)instream.ReadByte();
if (type != base.magicByte)
throw new InvalidDataException("The provided stream is not a valid LZ-0x10 "
+ "compressed stream (invalid type 0x" + type.ToString("X") + ")");
byte[] sizeBytes = new byte[3];
instream.Read(sizeBytes, 0, 3);
int decompressedSize = base.Bytes2Size(sizeBytes);
readBytes += 4;
if (decompressedSize == 0)
{
sizeBytes = new byte[4];
instream.Read(sizeBytes, 0, 4);
decompressedSize = base.Bytes2Size(sizeBytes);
readBytes += 4;
}
// the maximum 'DISP' is 0xFFF.
int bufferLength = 0x1000;
byte[] buffer = new byte[bufferLength];
int bufferOffset = 0;
int currentOutSize = 0;
int flags = 0, mask = 0;
while (currentOutSize < decompressedSize)
{
// (throws when requested new flags byte is not available)
#region Update the mask. If all flag bits have been read, get a new set.
// the current mask is the mask used in the previous run. So of it masks the
// last flag bit, get a new flags byte.
if (mask == 1)
{
if (readBytes >= inLength)
throw new NotEnoughDataException(currentOutSize, decompressedSize);
flags = instream.ReadByte(); readBytes++;
if (flags < 0)
throw new StreamTooShortException();
mask = 0x80;
}
else
{
mask >>= 1;
}
#endregion
// bit = 1 <=> compressed.
if ((flags & mask) > 0)
{
// (throws when < 2 bytes are available)
#region Get length and displacement('disp') values from next 2 bytes
// there are < 2 bytes available when the end is at most 1 byte away
if (readBytes + 1 >= inLength)
{
// make sure the stream is at the end
if (readBytes < inLength)
instream.ReadByte();
throw new NotEnoughDataException(currentOutSize, decompressedSize);
}
int byte1 = instream.ReadByte(); readBytes++;
int byte2 = instream.ReadByte(); readBytes++;
if (byte2 < 0)
throw new StreamTooShortException();
// the number of bytes to copy
int length = byte1 >> 4;
length += 3;
// from where the bytes should be copied (relatively)
int disp = ((byte1 & 0x0F) << 8) | byte2;
disp += 1;
if (disp > currentOutSize)
throw new InvalidDataException("Cannot go back more than already written. "
+ "DISP = " + disp + ", #written bytes = 0x" + currentOutSize.ToString("X")
+ " at 0x" + instream.Position.ToString("X"));
#endregion
int bufIdx = bufferOffset + bufferLength - disp;
for (int i = 0; i < length; i++)
{
byte next = buffer[bufIdx % bufferLength];
bufIdx++;
outstream.WriteByte(next);
buffer[bufferOffset] = next;
bufferOffset = (bufferOffset + 1) % bufferLength;
}
}
else
{
if (readBytes >= inLength)
throw new NotEnoughDataException(currentOutSize, decompressedSize);
int next = instream.ReadByte(); readBytes++;
if (next < 0)
throw new StreamTooShortException();
currentOutSize++;
outstream.WriteByte((byte)next);
buffer[bufferOffset] = (byte)next;
bufferOffset = (bufferOffset + 1) % bufferLength;
}
}
}
public override void Compress(Stream instream, long inLength, Stream outstream)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace DSDecmp.Formats.Nitro
{
public class LZ11 : NitroCFormat
{
public LZ11() : base(0x11) { }
public override void Decompress(Stream instream, long inLength, Stream outstream)
{
/* Data header (32bit)
Bit 0-3 Reserved
Bit 4-7 Compressed type (must be 1 for LZ77)
Bit 8-31 Size of decompressed data. if 0, the next 4 bytes are decompressed length
Repeat below. Each Flag Byte followed by eight Blocks.
Flag data (8bit)
Bit 0-7 Type Flags for next 8 Blocks, MSB first
Block Type 0 - Uncompressed - Copy 1 Byte from Source to Dest
Bit 0-7 One data byte to be copied to dest
Block Type 1 - Compressed - Copy LEN Bytes from Dest-Disp-1 to Dest
If Reserved is 0: - Default
Bit 0-3 Disp MSBs
Bit 4-7 LEN - 3
Bit 8-15 Disp LSBs
If Reserved is 1: - Higher compression rates for files with (lots of) long repetitions
Bit 4-7 Indicator
If Indicator > 1:
Bit 0-3 Disp MSBs
Bit 4-7 LEN - 1 (same bits as Indicator)
Bit 8-15 Disp LSBs
If Indicator is 1:
Bit 0-3 and 8-19 LEN - 0x111
Bit 20-31 Disp
If Indicator is 0:
Bit 0-3 and 8-11 LEN - 0x11
Bit 12-23 Disp
*/
throw new NotImplementedException();
}
public override void Compress(Stream instream, long inLength, Stream outstream)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace DSDecmp.Formats.Nitro
{
/// <summary>
/// Base class for Nitro-based decompressors. Uses the 1-byte magic and 3-byte decompression
/// size format.
/// </summary>
public abstract class NitroCFormat : CompressionFormat
{
/// <summary>
/// If true, Nitro Decompressors will not decompress files that have a decompressed
/// size (plaintext size) larger than MaxPlaintextSize.
/// </summary>
public static bool SkipLargePlaintexts = true;
/// <summary>
/// The maximum allowed size of the decompressed file (plaintext size) allowed for Nitro
/// Decompressors. Only used when SkipLargePlaintexts = true.
/// </summary>
public static int MaxPlaintextSize = 0x180000;
/// <summary>
/// The first byte of every file compressed with the format for this particular
/// Nitro Dcompressor instance.
/// </summary>
protected byte magicByte;
public NitroCFormat(byte magicByte)
{
this.magicByte = magicByte;
}
/// <summary>
/// Converts an array of (at least) 3 bytes into an integer, using the format used
/// in Nitro compression formats to store the decompressed size.
/// If the size is not 3, the fourth byte will also be included.
/// </summary>
protected int Bytes2Size(byte[] bytes)
{
if (bytes.Length == 3)
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16);
else
return bytes[0] | (bytes[1] << 8) | (bytes[2] << 16) | (bytes[3] << 24);
}
public override bool Supports(System.IO.Stream stream)
{
long startPosition = stream.Position;
try
{
int firstByte = stream.ReadByte();
if (firstByte != this.magicByte)
return false;
// no need to read the size info as well if it's used anyway.
if (!SkipLargePlaintexts)
return true;
byte[] sizeBytes = new byte[3];
stream.Read(sizeBytes, 0, 3);
int outSize = this.Bytes2Size(sizeBytes);
if (outSize == 0)
{
sizeBytes = new byte[4];
stream.Read(sizeBytes, 0, 4);
outSize = this.Bytes2Size(sizeBytes);
}
return outSize <= MaxPlaintextSize;
}
finally
{
stream.Position = startPosition;
}
}
}
}