using System.IO; using System.Linq; using System.Text; using BinaryObjectScanner.Models.Compression.LZ; using BinaryObjectScanner.Utilities; using static BinaryObjectScanner.Models.Compression.LZ.Constants; namespace BinaryObjectScanner.Compression { /// public class LZ { #region Constructors /// /// Constructor /// public LZ() { } #endregion #region Static Methods /// /// Decompress LZ-compressed data /// /// Byte array representing the compressed data /// Decompressed data as a byte array, null on error public static byte[] Decompress(byte[] compressed) { // If we have and invalid input if (compressed == null || compressed.Length == 0) return null; // Create a memory stream for the input and decompress that var compressedStream = new MemoryStream(compressed); return Decompress(compressedStream); } /// /// Decompress LZ-compressed data /// /// Stream representing the compressed data /// Decompressed data as a byte array, null on error public static byte[] Decompress(Stream compressed) { // If we have and invalid input if (compressed == null || compressed.Length == 0) return null; // Create a new LZ for decompression var lz = new LZ(); // Open the input data var sourceState = lz.Open(compressed, out _); if (sourceState?.Window == null) return null; // Create the output data and open it var decompressedStream = new MemoryStream(); var destState = lz.Open(decompressedStream, out _); if (destState == null) return null; // Decompress the data by copying long read = lz.CopyTo(sourceState, destState, out LZERROR error); // Copy the data to the buffer byte[] decompressed; if (read == 0 || (error != LZERROR.LZERROR_OK && error != LZERROR.LZERROR_NOT_LZ)) { decompressed = null; } else { int dataEnd = (int)decompressedStream.Position; decompressedStream.Seek(0, SeekOrigin.Begin); decompressed = decompressedStream.ReadBytes(dataEnd); } // Close the streams lz.Close(sourceState); lz.Close(destState); return decompressed; } /// /// Reconstructs the full filename of the compressed file /// public static string GetExpandedName(string input, out LZERROR error) { // Try to open the file as a compressed stream var fileStream = File.Open(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); var state = new LZ().Open(fileStream, out error); if (state?.Window == null) return null; // Get the extension for modification string inputExtension = Path.GetExtension(input).TrimStart('.'); // If we have no extension if (string.IsNullOrWhiteSpace(inputExtension)) return Path.GetFileNameWithoutExtension(input); // If we have an extension of length 1 if (inputExtension.Length == 1) { if (inputExtension == "_") return $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(state.LastChar)}"; else return Path.GetFileNameWithoutExtension(input); } // If we have an extension that doesn't end in an underscore if (!inputExtension.EndsWith("_")) return Path.GetFileNameWithoutExtension(input); // Build the new filename bool isLowerCase = char.IsUpper(input[0]); char replacementChar = isLowerCase ? char.ToLower(state.LastChar) : char.ToUpper(state.LastChar); string outputExtension = inputExtension.Substring(0, inputExtension.Length - 1) + replacementChar; return $"{Path.GetFileNameWithoutExtension(input)}.{outputExtension}"; } #endregion #region State Management /// /// Opens a stream and creates a state from it /// /// Source stream to create a state from /// Output representing the last error /// An initialized State, null on error /// Uncompressed streams are represented by a State with no buffer public State Open(Stream stream, out LZERROR error) { State lzs = Init(stream, out error); if (error == LZERROR.LZERROR_OK || error == LZERROR.LZERROR_NOT_LZ) return lzs; return null; } /// /// Closes a state by invalidating the source /// /// State object to close public void Close(State state) { try { state?.Source?.Close(); } catch { } } /// /// Initializes internal decompression buffers /// /// Input stream to create a state from /// Output representing the last error /// An initialized State, null on error /// Uncompressed streams are represented by a State with no buffer public State Init(Stream source, out LZERROR error) { // If we have an invalid source if (source == null) { error = LZERROR.LZERROR_BADVALUE; return null; } // Attempt to read the header var fileHeader = ParseFileHeader(source, out error); // If we had a valid but uncompressed stream if (error == LZERROR.LZERROR_NOT_LZ) { source.Seek(0, SeekOrigin.Begin); return new State { Source = source }; } // If we had any error else if (error != LZERROR.LZERROR_OK) { source.Seek(0, SeekOrigin.Begin); return null; } // Initialize the table with all spaces byte[] table = Enumerable.Repeat((byte)' ', LZ_TABLE_SIZE).ToArray(); // Build the state var state = new State { Source = source, LastChar = fileHeader.LastChar, RealLength = fileHeader.RealLength, Window = new byte[GETLEN], WindowLength = 0, WindowCurrent = 0, Table = table, CurrentTableEntry = 0xff0, }; // Return the state return state; } #endregion #region Stream Functionality /// /// Attempt to read the specified number of bytes from the State /// /// Source State to read from /// Byte buffer to read into /// Offset within the buffer to read /// Number of bytes to read /// Output representing the last error /// The number of bytes read, if possible /// /// If the source data is compressed, this will decompress the data. /// If the source data is uncompressed, it is copied directly /// public int Read(State source, byte[] buffer, int offset, int count, out LZERROR error) { // If we have an uncompressed input if (source.Window == null) { error = LZERROR.LZERROR_NOT_LZ; return source.Source.Read(buffer, offset, count); } // If seeking has occurred, we need to perform the seek if (source.RealCurrent != source.RealWanted) { // If the requested position is before the current, we need to reset if (source.RealCurrent > source.RealWanted) { // Reset the decompressor state source.Source.Seek(LZ_HEADER_LEN, SeekOrigin.Begin); FlushWindow(source); source.RealCurrent = 0; source.ByteType = 0; source.StringLength = 0; source.Table = Enumerable.Repeat((byte)' ', LZ_TABLE_SIZE).ToArray(); source.CurrentTableEntry = 0xFF0; } // While we are not at the right offset while (source.RealCurrent < source.RealWanted) { _ = DecompressByte(source, out error); if (error != LZERROR.LZERROR_OK) return 0; } } int bytesRemaining = count; while (bytesRemaining > 0) { byte b = DecompressByte(source, out error); if (error != LZERROR.LZERROR_OK) return count - bytesRemaining; source.RealWanted++; buffer[offset++] = b; bytesRemaining--; } error = LZERROR.LZERROR_OK; return count; } /// /// Perform a seek on the source data /// /// State to seek within /// Data offset to seek to /// SeekOrigin representing how to seek /// Output representing the last error /// The position that was seeked to, -1 on error public long Seek(State state, long offset, SeekOrigin seekOrigin, out LZERROR error) { // If we have an invalid state if (state == null) { error = LZERROR.LZERROR_BADVALUE; return -1; } // If we have an uncompressed input if (state.Window == null) { error = LZERROR.LZERROR_NOT_LZ; return state.Source.Seek(offset, seekOrigin); } // Otherwise, generate the new offset long newWanted = state.RealWanted; switch (seekOrigin) { case SeekOrigin.Current: newWanted += offset; break; case SeekOrigin.End: newWanted = state.RealLength - offset; break; default: newWanted = offset; break; } // If we have an invalid new offset if (newWanted < 0 && newWanted > state.RealLength) { error = LZERROR.LZERROR_BADVALUE; return -1; } error = LZERROR.LZERROR_OK; state.RealWanted = (uint)newWanted; return newWanted; } /// /// Copies all data from the source to the destination /// /// Source State to read from /// Destination state to write to /// Output representing the last error /// The number of bytes written, -1 on error /// /// If the source data is compressed, this will decompress the data. /// If the source data is uncompressed, it is copied directly /// public long CopyTo(State source, State dest, out LZERROR error) { error = LZERROR.LZERROR_OK; // If we have an uncompressed input if (source.Window == null) { source.Source.CopyTo(dest.Source); return source.Source.Length; } // Loop until we have read everything long length = 0; while (true) { // Read at most 1000 bytes byte[] buf = new byte[1000]; int read = Read(source, buf, 0, buf.Length, out error); // If we had an error if (read == 0) { if (error == LZERROR.LZERROR_NOT_LZ) { error = LZERROR.LZERROR_OK; break; } else if (error != LZERROR.LZERROR_OK) { error = LZERROR.LZERROR_READ; return 0; } } // Otherwise, append the length read and write the data length += read; dest.Source.Write(buf, 0, read); } return length; } /// /// Decompress a single byte of data from the source State /// /// Source State to read from /// Output representing the last error /// The read byte, if possible private byte DecompressByte(State source, out LZERROR error) { byte b; if (source.StringLength != 0) { b = source.Table[source.StringPosition]; source.StringPosition = (source.StringPosition + 1) & 0xFFF; source.StringLength--; } else { if ((source.ByteType & 0x100) == 0) { b = ReadByte(source, out error); if (error != LZERROR.LZERROR_OK) return 0; source.ByteType = (ushort)(b | 0xFF00); } if ((source.ByteType & 1) != 0) { b = ReadByte(source, out error); if (error != LZERROR.LZERROR_OK) return 0; } else { byte b1 = ReadByte(source, out error); if (error != LZERROR.LZERROR_OK) return 0; byte b2 = ReadByte(source, out error); if (error != LZERROR.LZERROR_OK) return 0; // Format: // b1 b2 // AB CD // where CAB is the stringoffset in the table // and D+3 is the len of the string source.StringPosition = (uint)(b1 | ((b2 & 0xf0) << 4)); source.StringLength = (byte)((b2 & 0xf) + 2); // 3, but we use a byte already below... b = source.Table[source.StringPosition]; source.StringPosition = (source.StringPosition + 1) & 0xFFF; } source.ByteType >>= 1; } // Store b in table source.Table[source.CurrentTableEntry++] = b; source.CurrentTableEntry &= 0xFFF; source.RealCurrent++; error = LZERROR.LZERROR_OK; return b; } /// /// Reads one compressed byte, including buffering /// /// State to read using /// Output representing the last error /// Byte value that was read, if possible private byte ReadByte(State state, out LZERROR error) { // If we have enough data in the buffer if (state.WindowCurrent < state.WindowLength) { error = LZERROR.LZERROR_OK; return state.Window[state.WindowCurrent++]; } // Otherwise, read from the source int ret = state.Source.Read(state.Window, 0, GETLEN); if (ret == 0) { error = LZERROR.LZERROR_NOT_LZ; return 0; } // Reset the window state state.WindowLength = (uint)ret; state.WindowCurrent = 1; error = LZERROR.LZERROR_OK; return state.Window[0]; } /// /// Reset the current window position to the length /// /// State to flush private void FlushWindow(State state) { state.WindowCurrent = state.WindowLength; } /// /// Parse a Stream into a file header /// /// Stream to parse /// Output representing the last error /// Filled file header on success, null on error private FileHeaader ParseFileHeader(Stream data, out LZERROR error) { error = LZERROR.LZERROR_OK; FileHeaader fileHeader = new FileHeaader(); byte[] magic = data.ReadBytes(LZ_MAGIC_LEN); fileHeader.Magic = Encoding.ASCII.GetString(magic); if (fileHeader.Magic != MagicString) { error = LZERROR.LZERROR_NOT_LZ; return null; } fileHeader.CompressionType = data.ReadByteValue(); if (fileHeader.CompressionType != (byte)'A') { error = LZERROR.LZERROR_UNKNOWNALG; return null; } fileHeader.LastChar = (char)data.ReadByteValue(); fileHeader.RealLength = data.ReadUInt32(); return fileHeader; } #endregion } }