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