From ae223a4589fcccc2c3dc910455efd101c7bda57b Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 24 Apr 2024 00:47:31 -0400 Subject: [PATCH] Port Blast code from UnshieldSharp --- README.MD | 1 + SabreTools.Compression/Blast/Blast.cs | 270 ++++++++++++++++++++++ SabreTools.Compression/Blast/Constants.cs | 15 ++ SabreTools.Compression/Blast/Huffman.cs | 207 +++++++++++++++++ SabreTools.Compression/Blast/State.cs | 160 +++++++++++++ 5 files changed, 653 insertions(+) create mode 100644 SabreTools.Compression/Blast/Blast.cs create mode 100644 SabreTools.Compression/Blast/Constants.cs create mode 100644 SabreTools.Compression/Blast/Huffman.cs create mode 100644 SabreTools.Compression/Blast/State.cs diff --git a/README.MD b/README.MD index d36a013..4849174 100644 --- a/README.MD +++ b/README.MD @@ -8,6 +8,7 @@ Find the link to the Nuget package [here](https://www.nuget.org/packages/SabreTo | Compression Name | Decompress | Compress | | --- | --- | --- | +| Blast | Yes | No | | LZ | Yes | No | | MSZIP | Yes* | No | diff --git a/SabreTools.Compression/Blast/Blast.cs b/SabreTools.Compression/Blast/Blast.cs new file mode 100644 index 0000000..1c12080 --- /dev/null +++ b/SabreTools.Compression/Blast/Blast.cs @@ -0,0 +1,270 @@ +/* blast.c + * Copyright (C) 2003, 2012, 2013 Mark Adler + * For conditions of distribution and use, see copyright notice in blast.h + * version 1.3, 24 Aug 2013 + * + * blast.c decompresses data compressed by the PKWare Compression Library. + * This function provides functionality similar to the explode() function of + * the PKWare library, hence the name "blast". + * + * This decompressor is based on the excellent format description provided by + * Ben Rudiak-Gould in comp.compression on August 13, 2001. Interestingly, the + * example Ben provided in the post is incorrect. The distance 110001 should + * instead be 111000. When corrected, the example byte stream becomes: + * + * 00 04 82 24 25 8f 80 7f + * + * which decompresses to "AIAIAIAIAIAIA" (without the quotes). + */ + +/* + * Change history: + * + * 1.0 12 Feb 2003 - First version + * 1.1 16 Feb 2003 - Fixed distance check for > 4 GB uncompressed data + * 1.2 24 Oct 2012 - Add note about using binary mode in stdio + * - Fix comparisons of differently signed integers + * 1.3 24 Aug 2013 - Return unused input from blast() + * - Fix test code to correctly report unused input + * - Enable the provision of initial input to blast() + */ + +using System; +using System.Collections.Generic; +using static SabreTools.Compression.Blast.Constants; + +namespace SabreTools.Compression.Blast +{ + public unsafe static class BlastDecoder + { + #region Huffman Encoding + + /// + /// Literal code + /// + private static readonly Huffman litcode = new(MAXBITS + 1, 256); + + /// + /// Length code + /// + private static readonly Huffman lencode = new(MAXBITS + 1, 16); + + /// + /// Distance code + /// + private static readonly Huffman distcode = new(MAXBITS + 1, 64); + + /// + /// Base for length codes + /// + private static readonly short[] baseLength = + [ + 3, 2, 4, 5, 6, 7, 8, 9, 10, 12, 16, 24, 40, 72, 136, 264 + ]; + + /// + /// Extra bits for length codes + /// + private static readonly byte[] extra = + [ + 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8 + ]; + + #endregion + + /// + /// Static constructor + /// + static BlastDecoder() + { + // Repeated code lengths of literal codes + byte[] litlen = + [ + 11, 124, 8, 7, 28, 7, 188, 13, 76, 4, 10, 8, 12, 10, 12, 10, 8, 23, 8, + 9, 7, 6, 7, 8, 7, 6, 55, 8, 23, 24, 12, 11, 7, 9, 11, 12, 6, 7, 22, 5, + 7, 24, 6, 11, 9, 6, 7, 22, 7, 11, 38, 7, 9, 8, 25, 11, 8, 11, 9, 12, + 8, 12, 5, 38, 5, 38, 5, 11, 7, 5, 6, 21, 6, 10, 53, 8, 7, 24, 10, 27, + 44, 253, 253, 253, 252, 252, 252, 13, 12, 45, 12, 45, 12, 61, 12, 45, + 44, 173 + ]; + litcode.Initialize(litlen); + + // Repeated code lengths of length codes 0..15 + byte[] lenlen = + [ + 2, 35, 36, 53, 38, 23 + ]; + lencode.Initialize(lenlen); + + // Repeated code lengths of distance codes 0..63 + byte[] distlen = + [ + 2, 20, 53, 230, 247, 151, 248 + ]; + distcode.Initialize(distlen); + } + + /// + /// blast() decompresses the PKWare Data Compression Library (DCL) compressed + /// format. It provides the same functionality as the explode() function in + /// that library. (Note: PKWare overused the "implode" verb, and the format + /// used by their library implode() function is completely different and + /// incompatible with the implode compression method supported by PKZIP.) + /// + /// The binary mode for stdio functions should be used to assure that the + /// compressed data is not corrupted when read or written. For example: + /// fopen(..., "rb") and fopen(..., "wb"). + /// + public static int Blast(byte[] inhow, List outhow) + { + // Input/output state + var state = new State(inhow, outhow); + + // Attempt to decompress using the above state + int err; + try + { + err = Decomp(state); + } + catch (IndexOutOfRangeException) + { + // This was originally a jump, which is bad form for C# + err = 2; + } + + // Write any leftover output and update the error code if needed + if (err != 1 && state.Next != 0 && !state.ProcessOutput() && err == 0) + err = 1; + + return err; + } + + /// + /// Decode PKWare Compression Library stream. + /// + /// + /// First byte is 0 if literals are uncoded or 1 if they are coded. Second + /// byte is 4, 5, or 6 for the number of extra bits in the distance code. + /// This is the base-2 logarithm of the dictionary size minus six. + /// + /// Compressed data is a combination of literals and length/distance pairs + /// terminated by an end code. Literals are either Huffman coded or + /// uncoded bytes. A length/distance pair is a coded length followed by a + /// coded distance to represent a string that occurs earlier in the + /// uncompressed data that occurs again at the current location. + /// + /// A bit preceding a literal or length/distance pair indicates which comes + /// next, 0 for literals, 1 for length/distance. + /// + /// If literals are uncoded, then the next eight bits are the literal, in the + /// normal bit order in the stream, i.e. no bit-reversal is needed. Similarly, + /// no bit reversal is needed for either the length extra bits or the distance + /// extra bits. + /// + /// Literal bytes are simply written to the output. A length/distance pair is + /// an instruction to copy previously uncompressed bytes to the output. The + /// copy is from distance bytes back in the output stream, copying for length + /// bytes. + /// + /// Distances pointing before the beginning of the output data are not + /// permitted. + /// + /// Overlapped copies, where the length is greater than the distance, are + /// allowed and common. For example, a distance of one and a length of 518 + /// simply copies the last byte 518 times. A distance of four and a length of + /// twelve copies the last four bytes three times. A simple forward copy + /// ignoring whether the length is greater than the distance or not implements + /// this correctly. + /// + private static int Decomp(State state) + { + int symbol; // decoded symbol, extra bits for distance + int len; // length for copy + uint dist; // distance for copy + int copy; // copy counter + int from, to; // copy pointers + + // Read header + int lit = state.Bits(8); // true if literals are coded + if (lit > 1) + return -1; + + int dict = state.Bits(8); // log2(dictionary size) - 6 + if (dict < 4 || dict > 6) + return -2; + + // Decode literals and length/distance pairs + do + { + if (state.Bits(1) != 0) + { + // Get length + symbol = lencode.Decode(state); + len = baseLength[symbol] + state.Bits(extra[symbol]); + if (len == 519) + break; // end code + + // Get distance + symbol = len == 2 ? 2 : dict; + dist = (uint)(distcode.Decode(state) << symbol); + dist += (uint)state.Bits(symbol); + dist++; + if (state.First && dist > state.Next) + return -3; //distance too far back + + // Copy length bytes from distance bytes back + do + { + to = (int)(state.OutputPtr + state.Next); + from = (int)(to - dist); + copy = MAXWIN; + if (state.Next < dist) + { + from += copy; + copy = (int)dist; + } + + copy -= (int)state.Next; + if (copy > len) + copy = len; + + len -= copy; + state.Next += (uint)copy; + do + { + state.Output[to++] = state.Output[from++]; + } + while (--copy != 0); + + if (state.Next == MAXWIN) + { + if (!state.ProcessOutput()) + return 1; + + state.Next = 0; + state.First = false; + } + } + while (len != 0); + } + else + { + // Get literal and write it + symbol = lit != 0 ? litcode.Decode(state) : state.Bits(8); + state.Output[state.Next++] = (byte)symbol; + if (state.Next == MAXWIN) + { + if (!state.ProcessOutput()) + return 1; + + state.Next = 0; + state.First = false; + } + } + } + while (true); + + return 0; + } + } +} \ No newline at end of file diff --git a/SabreTools.Compression/Blast/Constants.cs b/SabreTools.Compression/Blast/Constants.cs new file mode 100644 index 0000000..2364153 --- /dev/null +++ b/SabreTools.Compression/Blast/Constants.cs @@ -0,0 +1,15 @@ +namespace SabreTools.Compression.Blast +{ + public static class Constants + { + /// + /// Maximum code length + /// + public const int MAXBITS = 13; + + /// + /// Maximum window size + /// + public const int MAXWIN = 4096; + } +} \ No newline at end of file diff --git a/SabreTools.Compression/Blast/Huffman.cs b/SabreTools.Compression/Blast/Huffman.cs new file mode 100644 index 0000000..70dd3a6 --- /dev/null +++ b/SabreTools.Compression/Blast/Huffman.cs @@ -0,0 +1,207 @@ +using System; +using static SabreTools.Compression.Blast.Constants; + +namespace SabreTools.Compression.Blast +{ + /// + /// Huffman code decoding tables. count[1..MAXBITS] is the number of symbols of + /// each length, which for a canonical code are stepped through in order. + /// symbol[] are the symbol values in canonical order, where the number of + /// entries is the sum of the counts in count[]. The decoding process can be + /// seen in the function decode() below. + /// + public class Huffman + { + /// + /// Number of symbols of each length + /// + public short[] Count { get; set; } + + /// + /// Pointer to number of symbols of each length + /// + public int CountPtr { get; set; } + + /// + /// Canonically ordered symbols + /// + public short[] Symbol { get; set; } + + /// + /// Constructor + /// + /// Length of the Count array + /// Length of the Symbol array + public Huffman(int countLength, int symbolLength) + { + Count = new short[countLength]; + Symbol = new short[symbolLength]; + } + + /// + /// Given a list of repeated code lengths rep[0..n-1], where each byte is a + /// count (high four bits + 1) and a code length (low four bits), generate the + /// list of code lengths. This compaction reduces the size of the object code. + /// Then given the list of code lengths length[0..n-1] representing a canonical + /// Huffman code for n symbols, construct the tables required to decode those + /// codes. Those tables are the number of codes of each length, and the symbols + /// sorted by length, retaining their original order within each length. The + /// return value is zero for a complete code set, negative for an over- + /// subscribed code set, and positive for an incomplete code set. The tables + /// can be used if the return value is zero or positive, but they cannot be used + /// if the return value is negative. If the return value is zero, it is not + /// possible for decode() using that table to return an error--any stream of + /// enough bits will resolve to a symbol. If the return value is positive, then + /// it is possible for decode() using that table to return an error for received + /// codes past the end of the incomplete lengths. + /// + /// Repeated code length array + public int Initialize(byte[] rep) + { + int n = rep.Length; // Length of the bit length array + short symbol = 0; // Current symbol when stepping through length[] + short len; // Current length when stepping through h.Count[] + int left; // Number of possible codes left of current length + short[] offs = new short[MAXBITS + 1]; // offsets in symbol table for each length + short[] length = new short[256]; // Code lengths + + // Convert compact repeat counts into symbol bit length list + int repPtr = 0; + do + { + len = rep[repPtr++]; + left = (len >> 4) + 1; + len &= 15; + do + { + length[symbol++] = len; + } + while (--left != 0); + } + while (--n != 0); + + n = symbol; + + // Count number of codes of each length + for (len = 0; len <= MAXBITS; len++) + { + Count[len] = 0; + } + + // Assumes lengths are within bounds + for (symbol = 0; symbol < n; symbol++) + { + (Count[length[symbol]])++; + } + + // No codes! Complete, but decode() will fail + if (Count[0] == n) + return 0; + + // Check for an over-subscribed or incomplete set of lengths + left = 1; // One possible code of zero length + for (len = 1; len <= MAXBITS; len++) + { + left <<= 1; // One more bit, double codes left + left -= Count[len]; // Deduct count from possible codes + if (left < 0) + return left; // over-subscribed--return negative + } + + // Generate offsets into symbol table for each length for sorting + offs[1] = 0; + for (len = 1; len < MAXBITS; len++) + { + offs[len + 1] = (short)(offs[len] + Count[len]); + } + + // Put symbols in table sorted by length, by symbol order within each length + for (symbol = 0; symbol < n; symbol++) + { + if (length[symbol] != 0) + Symbol[offs[length[symbol]]++] = symbol; + } + + // Return zero for complete set, positive for incomplete set + return left; + } + + /// + /// Decode a code from the stream s using huffman table h. Return the symbol or + /// a negative value if there is an error. If all of the lengths are zero, i.e. + /// an empty code, or if the code is incomplete and an invalid code is received, + /// then -9 is returned after reading MAXBITS bits. + /// + /// Current input/output state to process + /// + /// The codes as stored in the compressed data are bit-reversed relative to + /// a simple integer ordering of codes of the same lengths. Hence below the + /// bits are pulled from the compressed data one at a time and used to + /// build the code value reversed from what is in the stream in order to + /// permit simple integer comparisons for decoding. + /// + /// The first code for the shortest length is all ones. Subsequent codes of + /// the same length are simply integer decrements of the previous code. When + /// moving up a length, a one bit is appended to the code. For a complete + /// code, the last code of the longest length will be all zeros. To support + /// this ordering, the bits pulled during decoding are inverted to apply the + /// more "natural" ordering starting with all zeros and incrementing. + /// + public int Decode(State state) + { + int len = 1; // Current number of bits in code + int code = 0; // len bits being decoded + int first = 0; // First code of length len + int count; // Number of codes of length len + int index = 0; // Index of first code of length len in symbol table + int bitbuf = state.BitBuf; // Bits from stream + int left = state.BitCnt; // Bits left in next or left to process + int nextPtr = CountPtr + 1; // Next number of codes + + while (true) + { + while (left-- != 0) + { + // Invert code + code |= (bitbuf & 1) ^ 1; + bitbuf >>= 1; + count = Count[nextPtr++]; + + // If length len, return symbol + if (code < first + count) + { + state.BitBuf = bitbuf; + state.BitCnt = (state.BitCnt - len) & 7; + return Symbol[index + (code - first)]; + } + + // Else update for next length + index += count; + first += count; + first <<= 1; + code <<= 1; + len++; + } + + left = (MAXBITS + 1) - len; + if (left == 0) + break; + + if (state.Left == 0) + { + state.Left = state.ProcessInput(); + if (state.Left == 0) + throw new IndexOutOfRangeException(); + } + + bitbuf = state.Input[state.InputPtr++]; + state.Left--; + if (left > 8) + left = 8; + } + + // Ran out of codes + return -9; + } + }; +} \ No newline at end of file diff --git a/SabreTools.Compression/Blast/State.cs b/SabreTools.Compression/Blast/State.cs new file mode 100644 index 0000000..b946c7a --- /dev/null +++ b/SabreTools.Compression/Blast/State.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using static SabreTools.Compression.Blast.Constants; + +namespace SabreTools.Compression.Blast +{ + /// + /// Input and output state + /// + public class State + { + #region Input State + + /// + /// Opaque information passed to InputFunction() + /// + public byte[] InHow { get; set; } + + /// + /// Next input location + /// + public List Input { get; set; } + + /// + /// Pointer to the next input location + /// + public int InputPtr { get; set; } + + /// + /// Available input at in + /// + public uint Left { get; set; } + + /// + /// Bit buffer + /// + public int BitBuf { get; set; } + + /// + /// Number of bits in bit buffer + /// + public int BitCnt { get; set; } + + #endregion + + #region Output State + + /// + /// Opaque information passed to OutputFunction() + /// + public List OutHow { get; set; } + + /// + /// Index of next write location in out[] + /// + public uint Next { get; set; } + + /// + /// True to check distances (for first 4K) + /// + public bool First { get; set; } + + /// + /// Output buffer and sliding window + /// + public byte[] Output { get; set; } = new byte[MAXWIN]; + + /// + /// Pointer to the next output location + /// + public int OutputPtr { get; set; } + + #endregion + + /// + /// Constructor + /// + /// Input byte array + /// Output byte list + public State(byte[] inhow, List outhow) + { + InHow = inhow; + Input = new List(); + InputPtr = 0; + Left = 0; + BitBuf = 0; + BitCnt = 0; + + OutHow = outhow; + Next = 0; + First = true; + } + + /// + /// Return need bits from the input stream. This always leaves less than + /// eight bits in the buffer. bits() works properly for need == 0. + /// + /// Number of bits to read + /// + /// Bits are stored in bytes from the least significant bit to the most + /// significant bit. Therefore bits are dropped from the bottom of the bit + /// buffer, using shift right, and new bytes are appended to the top of the + /// bit buffer, using shift left. + /// + public int Bits(int need) + { + // Load at least need bits into val + int val = BitBuf; + while (BitCnt < need) + { + if (Left == 0) + { + Left = ProcessInput(); + if (Left == 0) + throw new IndexOutOfRangeException(); + } + + // Load eight bits + val |= (int)(Input[InputPtr++]) << BitCnt; + Left--; + BitCnt += 8; + } + + // Drop need bits and update buffer, always zero to seven bits left + BitBuf = val >> need; + BitCnt -= need; + + // Return need bits, zeroing the bits above that + return val & ((1 << need) - 1); + } + + /// + /// Process input for the current state + /// + /// Amount of data in Input + public uint ProcessInput() + { + Input = new List(InHow); + return (uint)Input.Count; + } + + /// + /// Process output for the current state + /// + /// True if the output could be added, false otherwise + public bool ProcessOutput() + { + try + { + OutHow.AddRange(Output.Take((int)Next)); + return true; + } + catch + { + return false; + } + } + } +} \ No newline at end of file