diff --git a/CSharp/DSDecmp/DSDecmp.csproj b/CSharp/DSDecmp/DSDecmp.csproj index 2e166b4..e7c8691 100644 --- a/CSharp/DSDecmp/DSDecmp.csproj +++ b/CSharp/DSDecmp/DSDecmp.csproj @@ -2,7 +2,7 @@ Debug AnyCPU - 8.0.50727 + 9.0.21022 2.0 {E6F419F9-D6B5-4BE7-99BB-97C48C927FF3} Exe @@ -40,6 +40,12 @@ + + + + + + diff --git a/CSharp/DSDecmp/Exceptions/NotEnoughDataException.cs b/CSharp/DSDecmp/Exceptions/NotEnoughDataException.cs new file mode 100644 index 0000000..e8a3dd6 --- /dev/null +++ b/CSharp/DSDecmp/Exceptions/NotEnoughDataException.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace DSDecmp +{ + /// + /// An exception that is thrown by the decompression functions when there + /// is not enough data available in order to properly decompress the input. + /// + public class NotEnoughDataException : IOException + { + private long currentOutSize; + private long totalOutSize; + /// + /// Gets the actual number of written bytes. + /// + public long WrittenLength { get { return this.currentOutSize; } } + /// + /// Gets the number of bytes that was supposed to be written. + /// + public long DesiredLength { get { return this.totalOutSize; } } + + /// + /// Creates a new NotEnoughDataException. + /// + /// The actual number of written bytes. + /// The desired number of written bytes. + 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; + } + } +} diff --git a/CSharp/DSDecmp/Exceptions/StreamTooShortException.cs b/CSharp/DSDecmp/Exceptions/StreamTooShortException.cs new file mode 100644 index 0000000..55d9d80 --- /dev/null +++ b/CSharp/DSDecmp/Exceptions/StreamTooShortException.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace DSDecmp +{ + /// + /// An exception thrown by the compression or decompression function, indicating that the + /// given input length was too large for the given input stream. + /// + public class StreamTooShortException : EndOfStreamException + { + public StreamTooShortException() + : base("The end of the stream was reached " + + "before the given amout of data was read.") + { } + } +} diff --git a/CSharp/DSDecmp/Formats/CompressionFormat.cs b/CSharp/DSDecmp/Formats/CompressionFormat.cs new file mode 100644 index 0000000..42a70b2 --- /dev/null +++ b/CSharp/DSDecmp/Formats/CompressionFormat.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace DSDecmp.Formats +{ + /// + /// Base class for compression formats. + /// + public abstract class CompressionFormat + { + /// + /// 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. + /// + /// The name of the file to check. + /// False if the file can certainly not be decompressed using this decompressor. + /// True if the file may potentially be decompressed using this decompressor. + 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); + } + } + + /// + /// 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. + /// + /// 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. + /// False if the data can certainly not be decompressed using this decompressor. + /// True if the data may potentially be decompressed using this decompressor. + public abstract bool Supports(Stream stream); + + /// + /// Decompresses the given file, writing the deocmpressed data to the given output file. + /// The output file will be overwritten if it already exists. + /// Assumes Supports(infile) returns true. + /// + /// The file to decompress. + /// The target location of the decompressed file. + 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); + } + } + + /// + /// Decompresses the given stream, writing the decompressed data to the given output stream. + /// Assumes Supports(instream) returns true. + /// After this call, the input stream will be positioned at the end of the compressed stream, + /// or at the initial position + inLength, whichever comes first. + /// + /// The stream to decompress. At the end of this method, the position + /// of this stream is directly after the compressed data. + /// 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. + /// The stream to write the decompressed data to. + /// When the given length of the input data + /// is not enough to properly decompress the input. + public abstract void Decompress(Stream instream, long inLength, Stream outstream); + + /// + /// Compresses the given input file, and writes the compressed data to the given + /// output file. + /// + /// The file to compress. + /// The file to write the compressed data to. + 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); + } + } + + /// + /// Compresses the next inLength bytes from the input stream, + /// and writes the compressed data to the given output stream. + /// + /// The stream to read plaintext data from. + /// The length of the plaintext data. + /// The stream to write the compressed data to. + public abstract void Compress(Stream instream, long inLength, Stream outstream); + } +} diff --git a/CSharp/DSDecmp/Formats/Nitro/LZ10.cs b/CSharp/DSDecmp/Formats/Nitro/LZ10.cs new file mode 100644 index 0000000..268cb1e --- /dev/null +++ b/CSharp/DSDecmp/Formats/Nitro/LZ10.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; + +namespace DSDecmp.Formats.Nitro +{ + /// + /// Decompressor for the LZ-0x10 format used in many of the games for the + /// newer Nintendo consoles and handhelds. + /// + 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(); + } + } +} diff --git a/CSharp/DSDecmp/Formats/Nitro/LZ11.cs b/CSharp/DSDecmp/Formats/Nitro/LZ11.cs new file mode 100644 index 0000000..daf07f0 --- /dev/null +++ b/CSharp/DSDecmp/Formats/Nitro/LZ11.cs @@ -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(); + } + } +} diff --git a/CSharp/DSDecmp/Formats/Nitro/NitroCFormat.cs b/CSharp/DSDecmp/Formats/Nitro/NitroCFormat.cs new file mode 100644 index 0000000..f420f5d --- /dev/null +++ b/CSharp/DSDecmp/Formats/Nitro/NitroCFormat.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace DSDecmp.Formats.Nitro +{ + /// + /// Base class for Nitro-based decompressors. Uses the 1-byte magic and 3-byte decompression + /// size format. + /// + public abstract class NitroCFormat : CompressionFormat + { + /// + /// If true, Nitro Decompressors will not decompress files that have a decompressed + /// size (plaintext size) larger than MaxPlaintextSize. + /// + public static bool SkipLargePlaintexts = true; + /// + /// The maximum allowed size of the decompressed file (plaintext size) allowed for Nitro + /// Decompressors. Only used when SkipLargePlaintexts = true. + /// + public static int MaxPlaintextSize = 0x180000; + + /// + /// The first byte of every file compressed with the format for this particular + /// Nitro Dcompressor instance. + /// + protected byte magicByte; + + public NitroCFormat(byte magicByte) + { + this.magicByte = magicByte; + } + + /// + /// 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. + /// + 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; + } + } + } +}