using System; using System.IO; using System.Text; using System.IO.Compression; using LZ4; using ZstdSharp; namespace RobloxFiles.BinaryFormat { /// /// BinaryRobloxFileChunk represents a generic LZ4-compressed chunk /// of data in Roblox's Binary File Format. /// public class BinaryRobloxFileChunk { public readonly string ChunkType; public readonly int Reserved; public readonly int CompressedSize; public readonly int Size; public readonly byte[] CompressedData; public readonly byte[] Data; public bool HasCompressedData => (CompressedSize > 0); public IBinaryFileChunk Handler { get; internal set; } public bool HasWriteBuffer { get; private set; } public byte[] WriteBuffer { get; private set; } public override string ToString() { string chunkType = ChunkType.Replace('\0', ' '); return $"'{chunkType}' Chunk ({Size} bytes) [{Handler?.ToString()}]"; } public BinaryRobloxFileChunk(BinaryRobloxFileReader reader) { byte[] rawChunkType = reader.ReadBytes(4); ChunkType = Encoding.ASCII.GetString(rawChunkType); CompressedSize = reader.ReadInt32(); Size = reader.ReadInt32(); Reserved = reader.ReadInt32(); if (HasCompressedData) { CompressedData = reader.ReadBytes(CompressedSize); Data = new byte[Size]; using (var compStream = new MemoryStream(CompressedData)) { Stream decompStream = null; if (CompressedData[0] >= 0xF0) { // Probably LZ4 decompStream = new LZ4Stream(compStream, CompressionMode.Decompress); } else if (CompressedData[0] == 0x78 || CompressedData[0] == 0x58) { // Probably zlib decompStream = new DeflateStream(compStream, CompressionMode.Decompress); } else if (BitConverter.ToString(CompressedData, 1, 3) == "B5-2F-FD") { // Probably zstd decompStream = new DecompressionStream(compStream); } if (decompStream == null) throw new Exception("Unsupported compression scheme!"); decompStream.Read(Data, 0, Size); decompStream.Dispose(); } } else { Data = reader.ReadBytes(Size); } } public BinaryRobloxFileChunk(BinaryRobloxFileWriter writer, bool compress = true) { if (!writer.WritingChunk) throw new Exception("BinaryRobloxFileChunk: Supplied writer must have WritingChunk set to true."); Stream stream = writer.BaseStream; using (BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, true)) { long length = (stream.Position - writer.ChunkStart); stream.Position = writer.ChunkStart; Size = (int)length; Data = reader.ReadBytes(Size); } CompressedData = LZ4Codec.Encode(Data, 0, Size); CompressedSize = CompressedData.Length; if (!compress || CompressedSize > Size) { CompressedSize = 0; CompressedData = Array.Empty(); } ChunkType = writer.ChunkType; Reserved = 0; } public void WriteChunk(BinaryRobloxFileWriter writer) { // Record where we are when we start writing. var stream = writer.BaseStream; long startPos = stream.Position; // Write the chunk's data. writer.WriteString(ChunkType, true); writer.Write(CompressedSize); writer.Write(Size); writer.Write(Reserved); if (CompressedSize > 0) writer.Write(CompressedData); else writer.Write(Data); // Capture the data we wrote into a byte[] array. long endPos = stream.Position; int length = (int)(endPos - startPos); using (MemoryStream buffer = new MemoryStream()) { stream.Position = startPos; stream.CopyTo(buffer, length); WriteBuffer = buffer.ToArray(); HasWriteBuffer = true; } } } }