0
0
mirror of https://github.com/sp-tarkov/modules.git synced 2025-02-13 09:50:43 -05:00

[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 <merijn.d.hendriks@gmail.com>
Reviewed-on: SPT-AKI/Modules#43
Co-authored-by: Merijn Hendriks <senko-san@noreply.dev.sp-tarkov.com>
Co-committed-by: Merijn Hendriks <senko-san@noreply.dev.sp-tarkov.com>
This commit is contained in:
Merijn Hendriks 2023-12-09 22:49:16 +00:00 committed by chomp
parent 820619b0dc
commit 3c23adebe2

View File

@ -1,127 +1,86 @@
using System;
using System.IO; using System.IO;
using ComponentAce.Compression.Libs.zlib; using ComponentAce.Compression.Libs.zlib;
namespace Aki.Common.Utils namespace Aki.Common.Utils
{ {
public enum ZlibCompression public enum ZlibCompression
{ {
Store = 0, Store = 0,
Fastest = 1, Fastest = 1,
Fast = 3, Fast = 3,
Normal = 5, Normal = 5,
Ultra = 7, Ultra = 7,
Maximum = 9 Maximum = 9
} }
public static class Zlib public static class Zlib
{ {
// Level | CM/CI FLG /// <summary>
// ----- | --------- /// Check if the file is ZLib compressed
// 1 | 78 01 /// </summary>
// 2 | 78 5E /// <param name="data">Data</param>
// 3 | 78 5E /// <returns>If the file is Zlib compressed</returns>
// 4 | 78 5E public static bool IsCompressed(byte[] data)
// 5 | 78 5E {
// 6 | 78 9C if (data == null || data.Length < 3)
// 7 | 78 DA {
// 8 | 78 DA return false;
// 9 | 78 DA }
/// <summary> // data[0]: Info (CM/CINFO) Header; must be 0x78
/// Check if the file is ZLib compressed if (data[0] != 0x78)
/// </summary> {
/// <param name="Data">Data</param> return false;
/// <returns>If the file is Zlib compressed</returns> }
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.
if (Data == null || Data.Length < 3 || Data[0] != 0x78) // data[1]: Flags (FLG) Header; compression level.
{ switch (data[1])
return false; {
} 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]) return false;
{ }
case 0x01: // fastest
case 0x5E: // low
case 0x9C: // normal
case 0xDA: // max
return true;
}
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
/// <summary> using (var ms = new MemoryStream())
/// Deflate data. {
/// </summary> using (var zs = (level > ZlibCompression.Store)
public static byte[] Compress(byte[] data, ZlibCompression level) ? new ZOutputStream(ms, (int)level)
{ : new ZOutputStream(ms))
byte[] buffer = new byte[data.Length + 24]; {
zs.Write(data, 0, data.Length);
}
// <-- zs flushes everything here
ZStream zs = new ZStream() return ms.ToArray();
{ }
avail_in = data.Length, }
next_in = data,
next_in_index = 0,
avail_out = buffer.Length,
next_out = buffer,
next_out_index = 0
};
zs.deflateInit((int)level); /// <summary>
zs.deflate(zlibConst.Z_FINISH); /// Deflate data.
/// </summary>
data = new byte[zs.next_out_index]; public static byte[] Compress(byte[] data, ZlibCompression level)
Array.Copy(zs.next_out, 0, data, 0, zs.next_out_index); {
return Run(data, level);
return data; }
}
/// <summary> /// <summary>
/// Inflate data. /// Inflate data.
/// </summary> /// </summary>
public static byte[] Decompress(byte[] data) public static byte[] Decompress(byte[] data)
{ {
byte[] buffer = new byte[4096]; return Run(data, ZlibCompression.Store);
}
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();
}
}
}
} }