diff --git a/SabreTools.Compression/LZ/Decompressor.cs b/SabreTools.Compression/LZ/Decompressor.cs deleted file mode 100644 index afcbda8..0000000 --- a/SabreTools.Compression/LZ/Decompressor.cs +++ /dev/null @@ -1,567 +0,0 @@ -using System; -using System.IO; -using System.Text; -using SabreTools.IO.Extensions; -using SabreTools.Models.Compression.LZ; -using static SabreTools.Models.Compression.LZ.Constants; - -namespace SabreTools.Compression.LZ -{ - /// - public class Decompressor - { - #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 Decompressor(); - - // 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 Decompressor().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.IsNullOrEmpty(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) - { - var 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 (fileHeader == null || error != LZERROR.LZERROR_OK) - { - source.Seek(0, SeekOrigin.Begin); - return null; - } - - // Initialize the table with all spaces - byte[] table = new byte[LZ_TABLE_SIZE]; - table = Array.ConvertAll(table, b => (byte)' '); - - // 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 state, byte[] buffer, int offset, int count, out LZERROR error) - { - // If the source is invalid - if (state.Source == null) - { - error = LZERROR.LZERROR_BADVALUE; - return 0; - } - - // If we have an uncompressed input - if (state.Window == null) - { - error = LZERROR.LZERROR_NOT_LZ; - return state.Source.Read(buffer, offset, count); - } - - // If seeking has occurred, we need to perform the seek - if (state.RealCurrent != state.RealWanted) - { - // If the requested position is before the current, we need to reset - if (state.RealCurrent > state.RealWanted) - { - // Reset the decompressor state - state.Source.Seek(LZ_HEADER_LEN, SeekOrigin.Begin); - FlushWindow(state); - state.RealCurrent = 0; - state.ByteType = 0; - state.StringLength = 0; - state.Table = new byte[LZ_TABLE_SIZE]; - state.Table = Array.ConvertAll(state.Table, b => (byte)' '); - state.CurrentTableEntry = 0xFF0; - } - - // While we are not at the right offset - while (state.RealCurrent < state.RealWanted) - { - _ = DecompressByte(state, out error); - if (error != LZERROR.LZERROR_OK) - return 0; - } - } - - int bytesRemaining = count; - while (bytesRemaining > 0) - { - byte b = DecompressByte(state, out error); - if (error != LZERROR.LZERROR_OK) - return count - bytesRemaining; - - state.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 the source is invalid - if (state.Source == null) - { - error = LZERROR.LZERROR_BADVALUE; - return -1; - } - - // 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 state, State dest, out LZERROR error) - { - error = LZERROR.LZERROR_OK; - - // If the sources are invalid - if (state.Source == null || dest.Source == null) - { - error = LZERROR.LZERROR_BADVALUE; - return 0; - } - - // If we have an uncompressed input - if (state.Window == null) - { - state.Source.CopyTo(dest.Source); - return state.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(state, 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 state, out LZERROR error) - { - // If the table is invalid - if (state.Table == null) - { - error = LZERROR.LZERROR_BADVALUE; - return 0; - } - - byte b; - - if (state.StringLength != 0) - { - b = state.Table[state.StringPosition]; - state.StringPosition = (state.StringPosition + 1) & 0xFFF; - state.StringLength--; - } - else - { - if ((state.ByteType & 0x100) == 0) - { - b = ReadByte(state, out error); - if (error != LZERROR.LZERROR_OK) - return 0; - - state.ByteType = (ushort)(b | 0xFF00); - } - if ((state.ByteType & 1) != 0) - { - b = ReadByte(state, out error); - if (error != LZERROR.LZERROR_OK) - return 0; - } - else - { - byte b1 = ReadByte(state, out error); - if (error != LZERROR.LZERROR_OK) - return 0; - - byte b2 = ReadByte(state, 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 - state.StringPosition = (uint)(b1 | ((b2 & 0xf0) << 4)); - state.StringLength = (byte)((b2 & 0xf) + 2); - - // 3, but we use a byte already below... - b = state.Table[state.StringPosition]; - state.StringPosition = (state.StringPosition + 1) & 0xFFF; - } - - state.ByteType >>= 1; - } - - // Store b in table - state.Table[state.CurrentTableEntry++] = b; - state.CurrentTableEntry &= 0xFFF; - state.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 the source or window is invalid - if (state.Source == null || state.Window == null) - { - error = LZERROR.LZERROR_BADVALUE; - return 0; - } - - // 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) - { - try - { - error = LZERROR.LZERROR_OK; - var fileHeader = new FileHeaader(); - - var magic = data.ReadBytes(LZ_MAGIC_LEN); - if (magic == null) - { - error = LZERROR.LZERROR_BADINHANDLE; - return null; - } - - 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; - } - catch - { - error = LZERROR.LZERROR_NOT_LZ; - return null; - } - } - - #endregion - } -} \ No newline at end of file diff --git a/SabreTools.Compression/SZDD/Decompressor.cs b/SabreTools.Compression/SZDD/Decompressor.cs new file mode 100644 index 0000000..3c43c89 --- /dev/null +++ b/SabreTools.Compression/SZDD/Decompressor.cs @@ -0,0 +1,245 @@ +using System; +using System.IO; +using System.Text; +using SabreTools.IO.Extensions; + +namespace SabreTools.Compression.SZDD +{ + /// + public class Decompressor + { + /// + /// Window to deflate data into + /// + private readonly byte[] _window = new byte[4096]; + + /// + /// Source stream for the decompressor + /// + private readonly BufferedStream _source; + + /// + /// Offset within the window + /// + private int _offset; + + #region Constructors + + /// + /// Create a SZDD decompressor + /// + private Decompressor(Stream source, int offset) + { + // Validate the inputs + if (source.Length == 0) + throw new ArgumentOutOfRangeException(nameof(source)); + if (!source.CanRead) + throw new InvalidOperationException(nameof(source)); + if (offset < 0 || offset > 4096) + throw new ArgumentOutOfRangeException(nameof(offset)); + + // Initialize the window with space characters + _window = Array.ConvertAll(_window, b => (byte)0x20); + _source = new BufferedStream(source); + _offset = 4096 - offset; + } + + /// + /// Create a QBasic 4.5 installer SZDD decompressor + /// + public static Decompressor CreateQBasic(byte[] source) + => CreateQBasic(new MemoryStream(source)); + + /// + /// Create a QBasic 4.5 installer SZDD decompressor + /// + /// TODO: Replace validation when Models is updated + public static Decompressor CreateQBasic(Stream source) + { + // Create the decompressor + var decompressor = new Decompressor(source, 18); + + // Validate the header + byte[] magic = source.ReadBytes(8); + if (Encoding.ASCII.GetString(magic) != Encoding.ASCII.GetString([0x53, 0x5A, 0x20, 0x88, 0xF0, 0x27, 0x33, 0xD1])) + throw new InvalidDataException(nameof(source)); + + // Skip the rest of the header + _ = source.ReadUInt32(); // RealLength + return decompressor; + } + + /// + /// Create a standard SZDD decompressor + /// + public static Decompressor CreateSZDD(byte[] source) + => CreateSZDD(new MemoryStream(source)); + + /// + /// Create a standard SZDD decompressor + /// + /// TODO: Replace validation when Models is updated + public static Decompressor CreateSZDD(Stream source) + { + // Create the decompressor + var decompressor = new Decompressor(source, 16); + + // Validate the header + byte[] magic = source.ReadBytes(8); + if (Encoding.ASCII.GetString(magic) != Encoding.ASCII.GetString([0x53, 0x5A, 0x44, 0x44, 0x88, 0xF0, 0x27, 0x33])) + throw new InvalidDataException(nameof(source)); + byte compressionType = source.ReadByteValue(); + if (compressionType != 0x41) + throw new InvalidDataException(nameof(source)); + + // Skip the rest of the header + _ = source.ReadByteValue(); // LastChar + _ = source.ReadUInt32(); // RealLength + return decompressor; + } + + #endregion + + /// + /// Decompress source data to an output stream + /// + public bool CopyTo(Stream dest) + { + // Ignore unwritable streams + if (!dest.CanWrite) + return false; + + // Loop and decompress + while (true) + { + // Get the control byte + byte? control = _source.ReadNextByte(); + if (control == null) + break; + + for (int cbit = 0x01; (cbit & 0xFF) != 0; cbit <<= 1) + { + // Literal value + if ((control & cbit) != 0) + { + // Read the literal byte + byte? literal = _source.ReadNextByte(); + if (literal == null) + break; + + // Store the data in the window and write + _window[_offset] = literal.Value; + dest.WriteByte(_window[_offset]); + + // Set the next offset value + _offset++; + _offset &= 4095; + continue; + } + + // Read the match position + int? matchpos = _source.ReadNextByte(); + if (matchpos == null) + break; + + // Read the match length + int? matchlen = _source.ReadNextByte(); + if (matchlen == null) + break; + + // Adjust the position and length + matchpos |= (matchlen & 0xF0) << 4; + matchlen = (matchlen & 0x0F) + 3; + + // Loop over the match length + while (matchlen-- > 0) + { + // Copy the window value and write + _window[_offset] = _window[matchpos.Value]; + dest.WriteByte(_window[_offset]); + + // Set the next offset value + _offset++; matchpos++; + _offset &= 4095; matchpos &= 4095; + } + } + } + + // Flush and return + dest.Flush(); + return true; + } + + /// + /// Buffered stream that reads in blocks + /// + private class BufferedStream + { + /// + /// Source stream for populating the buffer + /// + private readonly Stream _source; + + /// + /// Internal buffer to read + /// + private readonly byte[] _buffer = new byte[2048]; + + /// + /// Current pointer into the buffer + /// + private int _bufferPtr = 0; + + /// + /// Represents the number of available bytes + /// + private int _available = -1; + + /// + /// Create a new buffered stream + /// + public BufferedStream(Stream source) + { + _source = source; + } + + /// + /// Read the next byte from the buffer, if possible + /// + public byte? ReadNextByte() + { + // Ensure the buffer first + if (!EnsureBuffer()) + return null; + + // Return the next available value + return _buffer[_bufferPtr++]; + } + + /// + /// Ensure the buffer has data to read + /// + private bool EnsureBuffer() + { + // Force an update if in the initial state + if (_available == -1) + { + _available = _source.Read(_buffer, 0, _buffer.Length); + _bufferPtr = 0; + return _available != 0; + } + + // If the pointer is out of range + if (_bufferPtr >= _available) + { + _available = _source.Read(_buffer, 0, _buffer.Length); + _bufferPtr = 0; + return _available != 0; + } + + // Otherwise, assume data is available + return true; + } + } + } +} \ No newline at end of file