From 25bed747f213784865f09db9e91e5d459278ea41 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 28 Dec 2022 17:24:30 -0800 Subject: [PATCH] First ported implementation of LZ --- BurnOutSharp.Compression/LZ.cs | 504 +++++++++++++++++- .../Compression/LZ/Constants.cs | 4 +- BurnOutSharp.Models/Compression/LZ/Enums.cs | 2 + .../Compression/LZ/FileHeader.cs | 2 +- 4 files changed, 509 insertions(+), 3 deletions(-) diff --git a/BurnOutSharp.Compression/LZ.cs b/BurnOutSharp.Compression/LZ.cs index 37c7e199..788fd23a 100644 --- a/BurnOutSharp.Compression/LZ.cs +++ b/BurnOutSharp.Compression/LZ.cs @@ -1,10 +1,512 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; using BurnOutSharp.Models.Compression.LZ; +using BurnOutSharp.Utilities; +using static BurnOutSharp.Models.Compression.LZ.Constants; namespace BurnOutSharp.Compression { /// public class LZ { - // TODO: Implement LZ decompression + /// + /// LZStart (KERNEL32.@) + /// + public LZERROR Start() => LZERROR.LZERROR_OK; + + /// + /// Initializes internal decompression buffers, returns lzfiledescriptor. + /// (return value the same as hfSrc, if hfSrc is not compressed) + /// on failure, returns error code <0 + /// lzfiledescriptors range from 0x400 to 0x410 (only 16 open files per process) + /// + /// since _llseek uses the same types as libc.lseek, we just use the macros of + /// libc + /// + public LZERROR Init(Stream hfSrc, out State lzs) + { + LZERROR ret = ReadHeader(hfSrc, out FileHeaader head); + if (ret != LZERROR.LZERROR_OK) + { + hfSrc.Seek(0, SeekOrigin.Begin); + if (ret == LZERROR.LZERROR_NOT_LZ) + { + lzs = new State { RealFD = hfSrc }; + return ret; + } + else + { + lzs = null; + return ret; + } + } + + lzs = new State(); + + lzs.RealFD = hfSrc; + lzs.LastChar = head.LastChar; + lzs.RealLength = head.RealLength; + + lzs.Get = new byte[GETLEN]; + lzs.GetLen = 0; + lzs.GetCur = 0; + + // Yes, preinitialize with spaces + lzs.Table = Enumerable.Repeat((byte)' ', LZ_TABLE_SIZE).ToArray(); + + // Yes, start 16 byte from the END of the table + lzs.CurTabEnt = 0xff0; + + return LZERROR.LZERROR_OK; + } + + /// + /// LZDone (KERNEL32.@) + /// + public void Done() { } + + /// + /// gets the full filename of the compressed file 'in' by opening it + /// and reading the header + /// + /// "file." is being translated to "file" + /// "file.bl_" (with lastchar 'a') is being translated to "file.bla" + /// "FILE.BL_" (with lastchar 'a') is being translated to "FILE.BLA" + /// + public LZERROR GetExpandedName(string input, out string output) + { + output = null; + + State state = OpenFile(input, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); + if (state.Get == null) + return LZERROR.LZERROR_NOT_LZ; + + string inputExtension = Path.GetExtension(input).TrimStart('.'); + if (string.IsNullOrWhiteSpace(inputExtension)) + { + output = Path.GetFileNameWithoutExtension(input); + return LZERROR.LZERROR_OK; + } + + if (inputExtension.Length == 1) + { + if (inputExtension == "_") + { + output = $"{Path.GetFileNameWithoutExtension(input)}.{char.ToLower(state.LastChar)}"; + return LZERROR.LZERROR_OK; + } + else + { + output = Path.GetFileNameWithoutExtension(input); + return LZERROR.LZERROR_OK; + } + } + + if (!inputExtension.EndsWith("_")) + { + output = Path.GetFileNameWithoutExtension(input); + return LZERROR.LZERROR_OK; + } + + bool isLowerCase = char.IsUpper(output[0]); + char replacementChar = isLowerCase ? char.ToLower(state.LastChar) : char.ToUpper(state.LastChar); + string outputExtension = inputExtension.Substring(0, inputExtension.Length - 1) + replacementChar; + output = $"{Path.GetFileNameWithoutExtension(input)}.{outputExtension}"; + return LZERROR.LZERROR_OK; + } + + /// + /// LZRead (KERNEL32.@) + /// + public int Read(State lzs, byte[] vbuf, int toread) + { + int howmuch; + byte b; + int buf = 0; // vbufPtr + + howmuch = toread; + + // If we have an uncompressed stream + if (lzs.Get == null) + return lzs.RealFD.Read(vbuf, 0, toread); + + // The decompressor itself is in a define, cause we need it twice + // in this function. (the decompressed byte will be in b) + // DECOMPRESS_ONE_BYTE + // { + // if (lzs.StringLen != 0) + // { + // b = lzs.Table[lzs.StringPos]; + // lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + // lzs.StringLen--; + // } + // else + // { + // if ((lzs.ByteType & 0x100) == 0) + // { + // if (Get(lzs, out b) != LZERROR.LZERROR_OK) + // return toread - howmuch; + + // lzs.ByteType = (ushort)(b | 0xFF00); + // } + // if ((lzs.ByteType & 1) != 0) + // { + // if (Get(lzs, out b) != LZERROR.LZERROR_OK) + // return toread - howmuch; + // } + // else + // { + // if (Get(lzs, out byte b1) != LZERROR.LZERROR_OK) + // return toread - howmuch; + // if (Get(lzs, out byte b2) != LZERROR.LZERROR_OK) + // return toread - howmuch; + + // // Format: + // // b1 b2 + // // AB CD + // // where CAB is the stringoffset in the table + // // and D+3 is the len of the string + // lzs.StringPos = (uint)(b1 | ((b2 & 0xf0) << 4)); + // lzs.StringLen = (byte)((b2 & 0xf) + 2); + + // // 3, but we use a byte already below... + // b = lzs.Table[lzs.StringPos]; + // lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + // } + + // lzs.ByteType >>= 1; + // } + + // // Store b in table + // lzs.Table[lzs.CurTabEnt++] = b; + // lzs.CurTabEnt &= 0xFFF; + // lzs.RealCurrent++; + // } + + // If someone has seeked, we have to bring the decompressor to that position + if (lzs.RealCurrent != lzs.RealWanted) + { + // If the wanted position is before the current position + // I see no easy way to unroll .. We have to restart at + // the beginning. *sigh* + if (lzs.RealCurrent > lzs.RealWanted) + { + // Flush decompressor state + lzs.RealFD.Seek(LZ_HEADER_LEN, SeekOrigin.Begin); + GetFlush(lzs); + lzs.RealCurrent = 0; + lzs.ByteType = 0; + lzs.StringLen = 0; + lzs.Table = Enumerable.Repeat((byte)' ', LZ_TABLE_SIZE).ToArray(); + lzs.CurTabEnt = 0xFF0; + } + + while (lzs.RealCurrent < lzs.RealWanted) + { + // DECOMPRESS_ONE_BYTE -- TODO: Make into helper method; original code is a DEFINE + if (lzs.StringLen != 0) + { + b = lzs.Table[lzs.StringPos]; + lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + lzs.StringLen--; + } + else + { + if ((lzs.ByteType & 0x100) == 0) + { + if (Get(lzs, out b) != LZERROR.LZERROR_OK) + return toread - howmuch; + + lzs.ByteType = (ushort)(b | 0xFF00); + } + if ((lzs.ByteType & 1) != 0) + { + if (Get(lzs, out b) != LZERROR.LZERROR_OK) + return toread - howmuch; + } + else + { + if (Get(lzs, out byte b1) != LZERROR.LZERROR_OK) + return toread - howmuch; + if (Get(lzs, out byte b2) != LZERROR.LZERROR_OK) + return toread - howmuch; + + // Format: + // b1 b2 + // AB CD + // where CAB is the stringoffset in the table + // and D+3 is the len of the string + lzs.StringPos = (uint)(b1 | ((b2 & 0xf0) << 4)); + lzs.StringLen = (byte)((b2 & 0xf) + 2); + + // 3, but we use a byte already below... + b = lzs.Table[lzs.StringPos]; + lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + } + + lzs.ByteType >>= 1; + } + + // Store b in table + lzs.Table[lzs.CurTabEnt++] = b; + lzs.CurTabEnt &= 0xFFF; + lzs.RealCurrent++; + } + } + + while (howmuch > 0) + { + // DECOMPRESS_ONE_BYTE -- TODO: Make into helper method; original code is a DEFINE + if (lzs.StringLen != 0) + { + b = lzs.Table[lzs.StringPos]; + lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + lzs.StringLen--; + } + else + { + if ((lzs.ByteType & 0x100) == 0) + { + if (Get(lzs, out b) != LZERROR.LZERROR_OK) + return toread - howmuch; + + lzs.ByteType = (ushort)(b | 0xFF00); + } + if ((lzs.ByteType & 1) != 0) + { + if (Get(lzs, out b) != LZERROR.LZERROR_OK) + return toread - howmuch; + } + else + { + if (Get(lzs, out byte b1) != LZERROR.LZERROR_OK) + return toread - howmuch; + if (Get(lzs, out byte b2) != LZERROR.LZERROR_OK) + return toread - howmuch; + + // Format: + // b1 b2 + // AB CD + // where CAB is the stringoffset in the table + // and D+3 is the len of the string + lzs.StringPos = (uint)(b1 | ((b2 & 0xf0) << 4)); + lzs.StringLen = (byte)((b2 & 0xf) + 2); + + // 3, but we use a byte already below... + b = lzs.Table[lzs.StringPos]; + lzs.StringPos = (lzs.StringPos + 1) & 0xFFF; + } + + lzs.ByteType >>= 1; + } + + // Store b in table + lzs.Table[lzs.CurTabEnt++] = b; + lzs.CurTabEnt &= 0xFFF; + lzs.RealCurrent++; + + lzs.RealWanted++; + vbuf[buf++] = b; + howmuch--; + } + + return toread; + } + + /// + /// LZSeek (KERNEL32.@) + /// + public long Seek(State lzs, long offset, SeekOrigin type) + { + // Not compressed? Just use normal Seek + if (lzs.Get == null) + return lzs.RealFD.Seek(offset, type); + + long newwanted = lzs.RealWanted; + switch (type) + { + case SeekOrigin.Current: + newwanted += offset; + break; + case SeekOrigin.End: + newwanted = lzs.RealLength - offset; + break; + default: + newwanted = offset; + break; + } + + if (newwanted > lzs.RealLength) + return (long)LZERROR.LZERROR_BADVALUE; + if (newwanted < 0) + return (long)LZERROR.LZERROR_BADVALUE; + + lzs.RealWanted = (uint)newwanted; + return newwanted; + } + + /// + /// Copies everything from src to dest + /// if src is a LZ compressed file, it will be uncompressed. + /// will return the number of bytes written to dest or errors. + /// + public long Copy(State src, State dest) + { + int usedlzinit = 0, ret, wret; + State oldsrc = src; + Stream srcfd; + DateTime filetime; + State lzs; + + const int BUFLEN = 1000; + byte[] buf = new byte[BUFLEN]; + + // we need that weird typedef, for i can't seem to get function pointer + // casts right. (Or they probably just do not like WINAPI in general) + //typedef UINT(WINAPI * _readfun)(HFILE, LPVOID, UINT); + + if (src.Get == null) + { + LZERROR err = Init(src.RealFD, out src); + if (err < LZERROR.LZERROR_NOT_LZ) + return (long)LZERROR.LZERROR_NOT_LZ; + + usedlzinit = 1; + } + + // Not compressed? just copy + if (src.Get == null) + { + src.RealFD.CopyTo(dest.RealFD); + return src.RealFD.Length; + } + + long len = 0; + while (true) + { + ret = Read(src, buf, BUFLEN); + if (ret <= 0) + { + if (ret == (int)LZERROR.LZERROR_NOT_LZ) + break; + if (ret == (int)LZERROR.LZERROR_BADINHANDLE) + return (long)LZERROR.LZERROR_READ; + return ret; + } + + len += ret; + dest.RealFD.Write(buf, 0, ret); + } + + // Maintain the timestamp of the source file to the destination file + // srcfd = (!(lzs = GET_LZ_STATE(src))) ? src : lzs->realfd; + // GetFileTime( LongToHandle(srcfd), NULL, NULL, &filetime ); + // SetFileTime( LongToHandle(dest), NULL, NULL, &filetime ); + + // Close handle + if (usedlzinit != 0) + Close(src); + + return len; + } + + /// + /// Opens a file. If not compressed, open it as a normal file. + /// + public State OpenFile(string fileName, FileMode mode, FileAccess access, FileShare fileShare = FileShare.ReadWrite) + { + try + { + Stream stream = File.Open(fileName, mode, access, fileShare); + LZERROR error = Init(stream, out State lzs); + if (error == LZERROR.LZERROR_OK || error == LZERROR.LZERROR_NOT_LZ) + return lzs; + } + catch { } + + return null; + } + + /// + /// Opens a file. If not compressed, open it as a normal file. + /// + public State OpenFile(Stream stream) + { + LZERROR error = Init(stream, out State lzs); + if (error == LZERROR.LZERROR_OK || error == LZERROR.LZERROR_NOT_LZ) + return lzs; + + return null; + } + + /// + /// Closes a state + /// + public void Close(State state) + { + // TODO: Figure out if this is effectively all + state?.RealFD?.Close(); + } + + /// + /// Reads one compressed byte, including buffering + /// + private LZERROR Get(State lzs, out byte b) + { + if (lzs.GetCur < lzs.GetLen) + { + b = lzs.Get[lzs.GetCur++]; + return LZERROR.LZERROR_OK; + } + else + { + int ret = lzs.RealFD.Read(lzs.Get, 0, GETLEN); + if (ret == 0) + { + b = 0; + return LZERROR.LZERROR_NOT_LZ; + } + + lzs.GetLen = (uint)ret; + lzs.GetCur = 1; + b = lzs.Get[0]; + return LZERROR.LZERROR_OK; + } + } + + private void GetFlush(State lzs) + { + lzs.GetCur = lzs.GetLen; + } + + /// + /// Internal function, reads lzheader + /// + private LZERROR ReadHeader(Stream fd, out FileHeaader head) + { + // Create the file header + head = new FileHeaader(); + + if (fd == null || !fd.CanSeek || !fd.CanRead) + return LZERROR.LZERROR_BADINHANDLE; + + // Ensure we're at the start of the stream + fd.Seek(0, SeekOrigin.Begin); + + byte[] magic = fd.ReadBytes(LZ_MAGIC_LEN); + head.Magic = Encoding.ASCII.GetString(magic); + if (head.Magic != MagicString) + return LZERROR.LZERROR_NOT_LZ; + + head.CompressionType = fd.ReadByteValue(); + if (head.CompressionType != (byte)'A') + return LZERROR.LZERROR_UNKNOWNALG; + + head.LastChar = (char)fd.ReadByteValue(); + head.RealLength = fd.ReadUInt32(); + return LZERROR.LZERROR_OK; + } } } \ No newline at end of file diff --git a/BurnOutSharp.Models/Compression/LZ/Constants.cs b/BurnOutSharp.Models/Compression/LZ/Constants.cs index b3322f9d..55a968ed 100644 --- a/BurnOutSharp.Models/Compression/LZ/Constants.cs +++ b/BurnOutSharp.Models/Compression/LZ/Constants.cs @@ -2,13 +2,15 @@ namespace BurnOutSharp.Models.Compression.LZ { public static class Constants { + public const int GETLEN = 2048; + public const int LZ_MAGIC_LEN = 8; public const int LZ_HEADER_LEN = 14; public static readonly byte[] MagicBytes = new byte[] { 0x53, 0x5a, 0x44, 0x44, 0x88, 0xf0, 0x27, 0x33 }; - public const string MagicString = "SZDD\u0088\u00f0\u0027\u0033"; + public static readonly string MagicString = System.Text.Encoding.ASCII.GetString(MagicBytes); public const ulong MagicUInt64 = 0x3327f08844445a53; diff --git a/BurnOutSharp.Models/Compression/LZ/Enums.cs b/BurnOutSharp.Models/Compression/LZ/Enums.cs index 43ac96af..8f5ed20f 100644 --- a/BurnOutSharp.Models/Compression/LZ/Enums.cs +++ b/BurnOutSharp.Models/Compression/LZ/Enums.cs @@ -3,6 +3,8 @@ namespace BurnOutSharp.Models.Compression.LZ /// public enum LZERROR { + LZERROR_OK = 1, + LZERROR_NOT_LZ = 0, LZERROR_BADINHANDLE = -1, LZERROR_BADOUTHANDLE = -2, LZERROR_READ = -3, diff --git a/BurnOutSharp.Models/Compression/LZ/FileHeader.cs b/BurnOutSharp.Models/Compression/LZ/FileHeader.cs index c6760343..fa519315 100644 --- a/BurnOutSharp.Models/Compression/LZ/FileHeader.cs +++ b/BurnOutSharp.Models/Compression/LZ/FileHeader.cs @@ -6,7 +6,7 @@ namespace BurnOutSharp.Models.Compression.LZ /// public sealed class FileHeaader { - public byte[] Magic; + public string Magic; public byte CompressionType;