From 3c23adebe2c35906bb716ed54a610e367f0bc90d Mon Sep 17 00:00:00 2001 From: Merijn Hendriks Date: Sat, 9 Dec 2023 22:49:16 +0000 Subject: [PATCH] [enhancement] Simplify zlib (!43) ## Preface EFT has been reworking the `bsg.componentace.compression.libs.zlib` alot the past versions, including various enhancements and introduced bugs (zero-tail decompression infinite looping). This also includes working towards deprecating `SimpleZlib` and improving `ZOutputStream`'s performance. While working on Haru, @waffle.lord and I have been reworking the zlib code to be much simpler. These changes are compatible with Aki. ## The issue - The current code is complex to understand without experience with the zlib library in question - `Zlib.IsCompressed` has a bug when operating on < 3 bytes. The third statement is reached and thus out of bounds. ## Why fix this - Simplifying the code improves future readability. - Using `ZOutputStream` enables usage of further performance improvements made to `ZOutputStream` by BSG. - `Zlib.IsCompressed` will work again on `byte[2]` and below. ## Why was it like this in the first place? At the time of writing this code, there was poor understanding of how the zlib library worked. The implementation was a best-guess from decompiled code. ## What's affected? Only Zlib utility's internal code. No external libraries are affected. Co-authored-by: Merijn Hendriks Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Modules/pulls/43 Co-authored-by: Merijn Hendriks Co-committed-by: Merijn Hendriks --- project/Aki.Common/Utils/Zlib.cs | 175 ++++++++++++------------------- 1 file changed, 67 insertions(+), 108 deletions(-) diff --git a/project/Aki.Common/Utils/Zlib.cs b/project/Aki.Common/Utils/Zlib.cs index c48c21d..1f3dcd2 100644 --- a/project/Aki.Common/Utils/Zlib.cs +++ b/project/Aki.Common/Utils/Zlib.cs @@ -1,127 +1,86 @@ -using System; using System.IO; using ComponentAce.Compression.Libs.zlib; namespace Aki.Common.Utils { - public enum ZlibCompression - { - Store = 0, - Fastest = 1, - Fast = 3, - Normal = 5, - Ultra = 7, - Maximum = 9 - } + public enum ZlibCompression + { + Store = 0, + Fastest = 1, + Fast = 3, + Normal = 5, + Ultra = 7, + Maximum = 9 + } - public static class Zlib - { - // Level | CM/CI FLG - // ----- | --------- - // 1 | 78 01 - // 2 | 78 5E - // 3 | 78 5E - // 4 | 78 5E - // 5 | 78 5E - // 6 | 78 9C - // 7 | 78 DA - // 8 | 78 DA - // 9 | 78 DA + public static class Zlib + { + /// + /// Check if the file is ZLib compressed + /// + /// Data + /// If the file is Zlib compressed + public static bool IsCompressed(byte[] data) + { + if (data == null || data.Length < 3) + { + return false; + } - /// - /// Check if the file is ZLib compressed - /// - /// Data - /// If the file is Zlib compressed - public static bool IsCompressed(byte[] Data) - { - // We need the first two bytes; - // First byte: Info (CM/CINFO) Header, should always be 0x78 - // Second byte: Flags (FLG) Header, should define our compression level. + // data[0]: Info (CM/CINFO) Header; must be 0x78 + if (data[0] != 0x78) + { + return false; + } - if (Data == null || Data.Length < 3 || Data[0] != 0x78) - { - return false; - } + // data[1]: Flags (FLG) Header; compression level. + switch (data[1]) + { + case 0x01: // [0x78 0x01] level 0-2: fastest + case 0x5E: // [0x78 0x5E] level 3-4: low + case 0x9C: // [0x78 0x9C] level 5-6: normal + case 0xDA: // [0x78 0xDA] level 7-9: max + return true; + } - switch (Data[1]) - { - case 0x01: // fastest - case 0x5E: // low - case 0x9C: // normal - case 0xDA: // max - return true; - } + return false; + } - return false; - } + private static byte[] Run(byte[] data, ZlibCompression level) + { + // ZOutputStream.Close() flushes itself. + // ZOutputStream.Flush() flushes the target stream. + // It's fucking stupid, but whatever. + // -- Waffle.Lord, 2022-12-01 - /// - /// Deflate data. - /// - public static byte[] Compress(byte[] data, ZlibCompression level) - { - byte[] buffer = new byte[data.Length + 24]; + using (var ms = new MemoryStream()) + { + using (var zs = (level > ZlibCompression.Store) + ? new ZOutputStream(ms, (int)level) + : new ZOutputStream(ms)) + { + zs.Write(data, 0, data.Length); + } + // <-- zs flushes everything here - ZStream zs = new ZStream() - { - avail_in = data.Length, - next_in = data, - next_in_index = 0, - avail_out = buffer.Length, - next_out = buffer, - next_out_index = 0 - }; + return ms.ToArray(); + } + } - zs.deflateInit((int)level); - zs.deflate(zlibConst.Z_FINISH); - - data = new byte[zs.next_out_index]; - Array.Copy(zs.next_out, 0, data, 0, zs.next_out_index); - - return data; - } + /// + /// Deflate data. + /// + public static byte[] Compress(byte[] data, ZlibCompression level) + { + return Run(data, level); + } /// /// Inflate data. /// public static byte[] Decompress(byte[] data) - { - byte[] buffer = new byte[4096]; - - ZStream zs = new ZStream() - { - avail_in = data.Length, - next_in = data, - next_in_index = 0, - avail_out = buffer.Length, - next_out = buffer, - next_out_index = 0 - }; - - zs.inflateInit(); - - using (MemoryStream ms = new MemoryStream()) - { - do - { - zs.avail_out = buffer.Length; - zs.next_out = buffer; - zs.next_out_index = 0; - - int result = zs.inflate(0); - - if (result != 0 && result != 1) - { - break; - } - - ms.Write(zs.next_out, 0, zs.next_out_index); - } - while (zs.avail_in > 0 || zs.avail_out == 0); - - return ms.ToArray(); - } - } - } + { + return Run(data, ZlibCompression.Store); + } + } } \ No newline at end of file