Last active
January 29, 2024 22:18
-
-
Save redheadgektor/4d6a7c568470c46c5679c9c7bddd4e65 to your computer and use it in GitHub Desktop.
Bit-packing tool
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Runtime.CompilerServices; | |
using System.Runtime.InteropServices; | |
using System.Text; | |
using UnityEngine.UIElements; | |
/* Bits & Buffers */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
private byte[] buffer; | |
private int position; | |
private int bitPosition; | |
public int Size { get; private set; } | |
public bool AutoResize = true; | |
public int AutoResizeCount = 1024; | |
public bool BitAligned | |
{ | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
get => (bitPosition & 7) == 0; | |
} | |
public BitStream(int capacity) | |
{ | |
if (capacity <= 0) | |
{ | |
capacity = 1; | |
} | |
buffer = new byte[capacity]; | |
Reset(true); | |
} | |
public BitStream(byte[] data) | |
{ | |
SetData(data); | |
} | |
public byte this[int index] | |
{ | |
get | |
{ | |
if (index < 0 || index >= buffer.Length) | |
throw new ArgumentException("Invalid byte position."); | |
return buffer[index]; | |
} | |
} | |
public void SetBitPosition(int newPosition) | |
{ | |
if (newPosition < 0) | |
{ | |
throw new ArgumentOutOfRangeException("Bit position cannot be negative."); | |
} | |
int newBytePosition = newPosition / 8; | |
int newBitPosition = newPosition % 8; | |
position = newBytePosition; | |
bitPosition = newBitPosition; | |
} | |
public int GetBitPosition() | |
{ | |
return (position * 8) + bitPosition; | |
} | |
public int GetSizeFromBitPosition() | |
{ | |
return (GetBitPosition() + 7) / 8; | |
} | |
public int GetPosition() | |
{ | |
return position; | |
} | |
public byte[] Data | |
{ | |
get { return buffer; } | |
} | |
public byte* GetDataPointer(int offset = 0) | |
{ | |
fixed (byte* ptr = &buffer[offset]) | |
{ | |
return ptr; | |
} | |
} | |
public byte* GetDataPointerAtPosition(int offset = 0) | |
{ | |
fixed (byte* ptr = &buffer[position + offset]) | |
{ | |
return ptr; | |
} | |
} | |
public void SetData(byte[] data) | |
{ | |
buffer = data; | |
Size = data.Length; | |
} | |
public void SetData(byte* data, int size) | |
{ | |
SetBitPosition(0); | |
CheckAndResize(size); | |
fixed (byte* ptr = &buffer[0]) | |
{ | |
Buffer.MemoryCopy(data, ptr, size, size); | |
} | |
Size = size; | |
} | |
public int AvailableBits => Size * 8; | |
public int RemainBits => AvailableBits - GetBitPosition(); | |
public int RemainBytes => RemainBits / 8; | |
private void CheckAndResize(int size) | |
{ | |
var target = position + size; | |
var need = target - Size; | |
if (need > 0) | |
{ | |
Size += need; | |
} | |
if (Size > buffer.Length) | |
{ | |
if (!AutoResize) | |
{ | |
Size = buffer.Length; | |
throw new OutOfMemoryException("AutoResize disabled!"); | |
} | |
Array.Resize(ref buffer, Size + AutoResizeCount); | |
} | |
} | |
public static bool IsLittleEndian() | |
{ | |
unsafe | |
{ | |
int testValue = 1; | |
byte* testBytes = (byte*)&testValue; | |
return (*testBytes == 1); | |
} | |
} | |
public void SkipBits(int bitCount) | |
{ | |
int bitsToSkip = bitCount; | |
int newPosition = GetBitPosition() + bitsToSkip; | |
SetBitPosition(newPosition); | |
} | |
public void SkipBytes(int byteCount) | |
{ | |
int bitsToSkip = byteCount * 8; | |
SkipBits(bitsToSkip); | |
} | |
public void Skip<T>() | |
where T : unmanaged | |
{ | |
SkipBytes(sizeof(T)); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public void WriteBit(bool bit) | |
{ | |
if (bitPosition == 8) | |
{ | |
bitPosition = 0; | |
position++; | |
CheckAndResize(1); | |
} | |
int offset = bitPosition & 7; | |
int pos = position; | |
++bitPosition; | |
buffer[pos] = (byte)( | |
bit ? (buffer[pos] & ~(1 << offset)) | (1 << offset) : (buffer[pos] & ~(1 << offset)) | |
); | |
} | |
[StructLayout(LayoutKind.Explicit)] | |
private struct FloatUnion | |
{ | |
[FieldOffset(0)] | |
public float fValue; | |
[FieldOffset(0)] | |
public ulong lValue; | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public void WriteBits(float value, int bitCount = 32) | |
{ | |
var union = new FloatUnion() { fValue = value }; | |
WriteBits(union.lValue, bitCount); | |
} | |
[MethodImpl(MethodImplOptions.AggressiveInlining)] | |
public void WriteBits(ulong value, int bitCount = 32) | |
{ | |
if (bitCount < 1 || bitCount > 64) | |
{ | |
throw new ArgumentException("Bit count must be between 1 and 64."); | |
} | |
for (int i = 0; i < bitCount; i++) | |
{ | |
WriteBit((value & (1UL << i)) != 0); | |
} | |
} | |
public void WriteBits(long value, int bitCount = 32) => WriteBits((ulong)value, bitCount); | |
public float ReadBitsFloat(int bitCount = 32) | |
{ | |
var union = new FloatUnion() { lValue = ReadBits(bitCount) }; | |
return union.fValue; | |
} | |
public ulong ReadBits(int bitCount = 32) | |
{ | |
if (bitCount < 1 || bitCount > 64) | |
{ | |
throw new ArgumentException("Bit count must be between 1 and 64."); | |
} | |
ulong result = 0; | |
for (int i = 0; i < bitCount; i++) | |
{ | |
var bit = ReadBit(); | |
result |= (bit ? 1UL : 0UL) << i; | |
} | |
return result; | |
} | |
public bool ReadBit() | |
{ | |
if (bitPosition == 8) | |
{ | |
bitPosition = 0; | |
position++; | |
} | |
if (position >= buffer.Length) | |
{ | |
throw new EndOfStreamException("BitStream buffer is end"); | |
} | |
var offset = bitPosition & 7; | |
var bit = (buffer[position] & (1 << offset)) != 0; | |
bitPosition++; | |
return bit; | |
} | |
public void Reset(bool resetSize = false, bool resetBuffer = false) | |
{ | |
position = 0; | |
bitPosition = 0; | |
if (resetSize) | |
{ | |
Size = 0; | |
} | |
if (resetBuffer) | |
{ | |
Size = 0; | |
Array.Resize(ref buffer, 0); | |
} | |
} | |
} | |
/* Dump to file */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
public enum Compressor | |
{ | |
GZip, | |
Deflate | |
} | |
public bool DumpToFile( | |
string path, | |
bool compress = false, | |
Compressor compressor = Compressor.GZip, | |
bool allBuffer = false | |
) | |
{ | |
bool result = false; | |
try | |
{ | |
var dir = Path.GetDirectoryName(path); | |
if (!Directory.Exists(dir)) | |
{ | |
Directory.CreateDirectory(dir); | |
} | |
using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write)) | |
{ | |
if (compress) | |
{ | |
if (compressor == Compressor.GZip) | |
{ | |
fs.WriteByte((byte)'G'); | |
fs.WriteByte((byte)'z'); | |
using (var cstream = new GZipStream(fs, CompressionMode.Compress)) | |
{ | |
cstream.Write(buffer, 0, Size); | |
cstream.Close(); | |
result = true; | |
return true; | |
} | |
} | |
else | |
{ | |
fs.WriteByte((byte)'D'); | |
fs.WriteByte((byte)'e'); | |
fs.WriteByte((byte)'f'); | |
using (var cstream = new DeflateStream(fs, CompressionMode.Compress)) | |
{ | |
cstream.Write(buffer, 0, Size); | |
cstream.Close(); | |
result = true; | |
return true; | |
} | |
} | |
} | |
fs.Write(buffer, 0, Size); | |
fs.Close(); | |
return true; | |
} | |
} | |
catch | |
{ | |
result = false; | |
} | |
return result; | |
} | |
public bool ReadFromFile( | |
string path, | |
bool compressed = false, | |
Compressor compressor = Compressor.GZip | |
) | |
{ | |
bool result = false; | |
try | |
{ | |
if (File.Exists(path)) | |
{ | |
using ( | |
var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read) | |
) | |
{ | |
//force decompress if has compression header (see DumpToFile method) | |
if (!compressed) | |
{ | |
if (fs.Length >= 3) | |
{ | |
if (fs.ReadByte() == (byte)'G' && fs.ReadByte() == (byte)'z') | |
{ | |
compressor = Compressor.GZip; | |
compressed = true; | |
} | |
fs.Position = 0; | |
if ( | |
fs.ReadByte() == (byte)'D' | |
&& fs.ReadByte() == (byte)'e' | |
&& fs.ReadByte() == (byte)'f' | |
) | |
{ | |
compressor = Compressor.Deflate; | |
compressed = true; | |
} | |
fs.Position = 0; | |
} | |
} | |
if (compressed) | |
{ | |
if (compressor == Compressor.GZip) | |
{ | |
using (var dstream = new GZipStream(fs, CompressionMode.Decompress)) | |
{ | |
CheckAndResize((int)fs.Length); | |
Size = dstream.Read(buffer, 0, (int)fs.Length); | |
return true; | |
} | |
} | |
else | |
{ | |
using (var dstream = new DeflateStream(fs, CompressionMode.Decompress)) | |
{ | |
CheckAndResize((int)fs.Length); | |
Size = dstream.Read(buffer, 0, (int)fs.Length); | |
return true; | |
} | |
} | |
} | |
CheckAndResize((int)fs.Length); | |
fs.Read(buffer, 0, (int)fs.Length); | |
} | |
result = true; | |
} | |
} | |
catch | |
{ | |
result = false; | |
} | |
return result; | |
} | |
} | |
/* Pool */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
private static Stack<BitStream> Pool = new Stack<BitStream>(); | |
public bool IsPooled { get; private set; } = false; | |
public static BitStream Get(int size = 1) | |
{ | |
BitStream bs = Pool.Count > 0 ? Pool.Pop() : null; | |
if (bs == null) | |
{ | |
bs = new BitStream(size); | |
bs.IsPooled = true; | |
} | |
bs.Reset(true); | |
bs.CheckAndResize(size); | |
return bs; | |
} | |
public static BitStream Get(byte[] data) | |
{ | |
BitStream bs = Pool.Count > 0 ? Pool.Pop() : null; | |
if (bs == null) | |
{ | |
bs = new BitStream(data); | |
bs.IsPooled = true; | |
} | |
bs.Reset(); | |
bs.SetData(data); | |
return bs; | |
} | |
public static BitStream Get(byte* data, int size) | |
{ | |
BitStream bs = Pool.Count > 0 ? Pool.Pop() : null; | |
if (bs == null) | |
{ | |
bs = new BitStream(size); | |
bs.IsPooled = true; | |
} | |
bs.Reset(); | |
bs.SetData(data, size); | |
return bs; | |
} | |
void IDisposable.Dispose() | |
{ | |
if (IsPooled) | |
{ | |
Pool.Push(this); | |
} | |
} | |
} | |
/* Unmanaged */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
public void Write<T>(T value) | |
where T : unmanaged | |
{ | |
unsafe | |
{ | |
var ptr = (byte*)(&value); | |
for (int i = 0; i < sizeof(T); i++) | |
{ | |
WriteBits(ptr[i], 8); | |
} | |
} | |
} | |
public T Read<T>() | |
where T : unmanaged | |
{ | |
unsafe | |
{ | |
T value = default(T); | |
var ptr = (byte*)(&value); | |
for (int i = 0; i < sizeof(T); i++) | |
{ | |
ptr[i] = (byte)ReadBits(8); | |
} | |
return value; | |
} | |
} | |
} | |
/* Arrays */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
public void Write<T>(T[] value) | |
where T : unmanaged | |
{ | |
Write((ushort)value.Length); | |
for (int i = 0; i < value.Length; i++) | |
{ | |
Write(value[i]); | |
} | |
} | |
public T[] ReadArray<T>() | |
where T : unmanaged | |
{ | |
var sz = Read<ushort>(); | |
var array = new T[sz]; | |
for (int i = 0; i < sz; i++) | |
{ | |
array[i] = Read<T>(); | |
} | |
return array; | |
} | |
public void Write<T>(T[] value, int size) | |
where T : unmanaged | |
{ | |
for (int i = 0; i < size; i++) | |
{ | |
Write(value[i]); | |
} | |
} | |
public T[] ReadArray<T>(int size) | |
where T : unmanaged | |
{ | |
var array = new T[size]; | |
for (int i = 0; i < array.Length; i++) | |
{ | |
array[i] = Read<T>(); | |
} | |
return array; | |
} | |
} | |
/* Strings */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
UTF8Encoding encoding; | |
private UTF8Encoding GetOrCreateUTF8Encoder() | |
{ | |
if (encoding == null) | |
{ | |
encoding = new UTF8Encoding(false, false); | |
} | |
return encoding; | |
} | |
/* UTF8 */ | |
public void WriteStringU(string str) | |
{ | |
var bytes = GetOrCreateUTF8Encoder().GetBytes(str); | |
Write(bytes, bytes.Length); | |
Write((byte)0); | |
} | |
public string ReadStringU() | |
{ | |
var bytes = new List<byte>(); | |
byte b; | |
while ((b = Read<byte>()) != 0) | |
{ | |
bytes.Add(b); | |
} | |
string str = GetOrCreateUTF8Encoder().GetString(bytes.ToArray()); | |
return str; | |
} | |
/* ANSI */ | |
public void WriteStringA(string str) | |
{ | |
foreach (char c in str) | |
{ | |
Write((byte)c); | |
} | |
Write((byte)'\0'); | |
} | |
public string ReadStringA() | |
{ | |
var chars = new List<char>(); | |
char c; | |
while ((c = (char)Read<byte>()) != '\0') | |
{ | |
chars.Add(c); | |
} | |
return new string(chars.ToArray()); | |
} | |
} | |
/* Pointers */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
public void Write(byte* ptr, int size) | |
{ | |
for (int i = 0; i < size; i++) | |
{ | |
WriteBits(ptr[i], 8); | |
} | |
} | |
} | |
/* Scramble */ | |
public unsafe partial class BitStream : IDisposable | |
{ | |
int scrambleStartPosition = -1; | |
public void ScrambleStart() | |
{ | |
scrambleStartPosition = GetPosition(); | |
} | |
public void ScrambleEnd(int seed = 0) | |
{ | |
if (scrambleStartPosition == -1) | |
{ | |
throw new InvalidOperationException($"{nameof(ScrambleStart)} not called!"); | |
} | |
var pos = GetPosition(); | |
for (int i = scrambleStartPosition; i < pos; i++) | |
{ | |
buffer[i] ^= (byte)(i ^ seed); | |
} | |
scrambleStartPosition = -1; | |
} | |
public void ScrambleBuffer(int seed = 0) | |
{ | |
for (int i = 0; i < buffer.Length; i++) | |
{ | |
buffer[i] ^= (byte)(i ^ seed); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment