aboutsummaryrefslogtreecommitdiff
path: root/src/zencore/compress.cpp
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2023-05-02 10:01:47 +0200
committerGitHub <[email protected]>2023-05-02 10:01:47 +0200
commit075d17f8ada47e990fe94606c3d21df409223465 (patch)
treee50549b766a2f3c354798a54ff73404217b4c9af /src/zencore/compress.cpp
parentfix: bundle shouldn't append content zip to zen (diff)
downloadzen-075d17f8ada47e990fe94606c3d21df409223465.tar.xz
zen-075d17f8ada47e990fe94606c3d21df409223465.zip
moved source directories into `/src` (#264)
* moved source directories into `/src` * updated bundle.lua for new `src` path * moved some docs, icon * removed old test trees
Diffstat (limited to 'src/zencore/compress.cpp')
-rw-r--r--src/zencore/compress.cpp1353
1 files changed, 1353 insertions, 0 deletions
diff --git a/src/zencore/compress.cpp b/src/zencore/compress.cpp
new file mode 100644
index 000000000..632e0e8f3
--- /dev/null
+++ b/src/zencore/compress.cpp
@@ -0,0 +1,1353 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#include <zencore/compress.h>
+
+#include <zencore/blake3.h>
+#include <zencore/compositebuffer.h>
+#include <zencore/crc32.h>
+#include <zencore/endian.h>
+#include <zencore/iohash.h>
+#include <zencore/testing.h>
+
+#include "../../thirdparty/Oodle/include/oodle2.h"
+#if ZEN_PLATFORM_WINDOWS
+# pragma comment(lib, "oo2core_win64.lib")
+#endif
+
+#include <lz4.h>
+#include <functional>
+#include <limits>
+
+namespace zen::detail {
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+static constexpr uint64_t DefaultBlockSize = 256 * 1024;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/** Method used to compress the data in a compressed buffer. */
+enum class CompressionMethod : uint8_t
+{
+ /** Header is followed by one uncompressed block. */
+ None = 0,
+ /** Header is followed by an array of compressed block sizes then the compressed blocks. */
+ Oodle = 3,
+ /** Header is followed by an array of compressed block sizes then the compressed blocks. */
+ LZ4 = 4,
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+/** Header used on every compressed buffer. Always stored in big-endian format. */
+struct BufferHeader
+{
+ static constexpr uint32_t ExpectedMagic = 0xb7756362; // <dot>ucb
+
+ uint32_t Magic = ExpectedMagic; // A magic number to identify a compressed buffer. Always 0xb7756362.
+ uint32_t Crc32 = 0; // A CRC-32 used to check integrity of the buffer. Uses the polynomial 0x04c11db7
+ CompressionMethod Method =
+ CompressionMethod::None; // The method used to compress the buffer. Affects layout of data following the header
+ uint8_t Compressor = 0; // The method-specific compressor used to compress the buffer.
+ uint8_t CompressionLevel = 0; // The method-specific compression level used to compress the buffer.
+ uint8_t BlockSizeExponent = 0; // The power of two size of every uncompressed block except the last. Size is 1 << BlockSizeExponent
+ uint32_t BlockCount = 0; // The number of blocks that follow the header
+ uint64_t TotalRawSize = 0; // The total size of the uncompressed data
+ uint64_t TotalCompressedSize = 0; // The total size of the compressed data including the header
+ BLAKE3 RawHash; // The hash of the uncompressed data
+
+ /** Checks validity of the buffer based on the magic number, method, and CRC-32. */
+ static bool IsValid(const CompositeBuffer& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize);
+ static bool IsValid(const SharedBuffer& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+ {
+ return IsValid(CompositeBuffer(CompressedData), OutRawHash, OutRawSize);
+ }
+
+ /** Read a header from a buffer that is at least sizeof(BufferHeader) without any validation. */
+ static BufferHeader Read(const CompositeBuffer& CompressedData)
+ {
+ BufferHeader Header;
+ if (sizeof(BufferHeader) <= CompressedData.GetSize())
+ {
+ // if (CompressedData.GetSegments()[0].AsIoBuffer().IsWholeFile())
+ // {
+ // ZEN_ASSERT(true);
+ // }
+ CompositeBuffer::Iterator It;
+ CompressedData.CopyTo(MakeMutableMemoryView(&Header, &Header + 1), It);
+ Header.ByteSwap();
+ }
+ return Header;
+ }
+
+ /**
+ * Write a header to a memory view that is at least sizeof(BufferHeader).
+ *
+ * @param HeaderView View of the header to write, including any method-specific header data.
+ */
+ void Write(MutableMemoryView HeaderView) const
+ {
+ BufferHeader Header = *this;
+ Header.ByteSwap();
+ HeaderView.CopyFrom(MakeMemoryView(&Header, &Header + 1));
+ Header.ByteSwap();
+ Header.Crc32 = CalculateCrc32(HeaderView);
+ Header.ByteSwap();
+ HeaderView.CopyFrom(MakeMemoryView(&Header, &Header + 1));
+ }
+
+ void ByteSwap()
+ {
+ Magic = zen::ByteSwap(Magic);
+ Crc32 = zen::ByteSwap(Crc32);
+ BlockCount = zen::ByteSwap(BlockCount);
+ TotalRawSize = zen::ByteSwap(TotalRawSize);
+ TotalCompressedSize = zen::ByteSwap(TotalCompressedSize);
+ }
+
+ /** Calculate the CRC-32 from a view of a header including any method-specific header data. */
+ static uint32_t CalculateCrc32(MemoryView HeaderView)
+ {
+ uint32_t Crc32 = 0;
+ constexpr uint64_t MethodOffset = offsetof(BufferHeader, Method);
+ for (MemoryView View = HeaderView + MethodOffset; const uint64_t ViewSize = View.GetSize();)
+ {
+ const int32_t Size = static_cast<int32_t>(zen::Min<uint64_t>(ViewSize, /* INT_MAX */ 2147483647u));
+ Crc32 = zen::MemCrc32(View.GetData(), Size, Crc32);
+ View += Size;
+ }
+ return Crc32;
+ }
+};
+
+static_assert(sizeof(BufferHeader) == 64, "BufferHeader is the wrong size.");
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class BaseEncoder
+{
+public:
+ virtual CompositeBuffer Compress(const CompositeBuffer& RawData, uint64_t BlockSize = DefaultBlockSize) const = 0;
+};
+
+class BaseDecoder
+{
+public:
+ virtual CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const = 0;
+ virtual bool TryDecompressTo(const BufferHeader& Header,
+ const CompositeBuffer& CompressedData,
+ MutableMemoryView RawView,
+ uint64_t RawOffset) const = 0;
+ virtual uint64_t GetHeaderSize(const BufferHeader& Header) const = 0;
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class NoneEncoder final : public BaseEncoder
+{
+public:
+ [[nodiscard]] CompositeBuffer Compress(const CompositeBuffer& RawData, uint64_t /* BlockSize */) const final
+ {
+ BufferHeader Header;
+ Header.Method = CompressionMethod::None;
+ Header.BlockCount = 1;
+ Header.TotalRawSize = RawData.GetSize();
+ Header.TotalCompressedSize = Header.TotalRawSize + sizeof(BufferHeader);
+ Header.RawHash = BLAKE3::HashBuffer(RawData);
+
+ UniqueBuffer HeaderData = UniqueBuffer::Alloc(sizeof(BufferHeader));
+ Header.Write(HeaderData);
+ return CompositeBuffer(HeaderData.MoveToShared(), RawData.MakeOwned());
+ }
+};
+
+class NoneDecoder final : public BaseDecoder
+{
+public:
+ [[nodiscard]] CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final
+ {
+ if (Header.Method == CompressionMethod::None && Header.TotalCompressedSize == CompressedData.GetSize() &&
+ Header.TotalCompressedSize == Header.TotalRawSize + sizeof(BufferHeader))
+ {
+ return CompressedData.Mid(sizeof(BufferHeader), Header.TotalRawSize).MakeOwned();
+ }
+ return CompositeBuffer();
+ }
+
+ [[nodiscard]] bool TryDecompressTo(const BufferHeader& Header,
+ const CompositeBuffer& CompressedData,
+ MutableMemoryView RawView,
+ uint64_t RawOffset) const final
+ {
+ if (Header.Method == CompressionMethod::None && RawOffset + RawView.GetSize() <= Header.TotalRawSize &&
+ Header.TotalCompressedSize == CompressedData.GetSize() &&
+ Header.TotalCompressedSize == Header.TotalRawSize + sizeof(BufferHeader))
+ {
+ CompressedData.CopyTo(RawView, sizeof(BufferHeader) + RawOffset);
+ return true;
+ }
+ return false;
+ }
+
+ [[nodiscard]] uint64_t GetHeaderSize(const BufferHeader&) const final { return sizeof(BufferHeader); }
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+class BlockEncoder : public BaseEncoder
+{
+public:
+ CompositeBuffer Compress(const CompositeBuffer& RawData, uint64_t BlockSize = DefaultBlockSize) const final;
+
+protected:
+ virtual CompressionMethod GetMethod() const = 0;
+ virtual uint8_t GetCompressor() const = 0;
+ virtual uint8_t GetCompressionLevel() const = 0;
+ virtual uint64_t CompressBlockBound(uint64_t RawSize) const = 0;
+ virtual bool CompressBlock(MutableMemoryView& CompressedData, MemoryView RawData) const = 0;
+
+private:
+ uint64_t GetCompressedBlocksBound(uint64_t BlockCount, uint64_t BlockSize, uint64_t RawSize) const
+ {
+ switch (BlockCount)
+ {
+ case 0:
+ return 0;
+ case 1:
+ return CompressBlockBound(RawSize);
+ default:
+ return CompressBlockBound(BlockSize) - BlockSize + RawSize;
+ }
+ }
+};
+
+CompositeBuffer
+BlockEncoder::Compress(const CompositeBuffer& RawData, const uint64_t BlockSize) const
+{
+ ZEN_ASSERT(IsPow2(BlockSize) && (BlockSize <= (1u << 31)));
+
+ const uint64_t RawSize = RawData.GetSize();
+ BLAKE3Stream RawHash;
+
+ const uint64_t BlockCount = RoundUp(RawSize, BlockSize) / BlockSize;
+ ZEN_ASSERT(BlockCount <= ~uint32_t(0));
+
+ // Allocate the buffer for the header, metadata, and compressed blocks.
+ const uint64_t MetaSize = BlockCount * sizeof(uint32_t);
+ const uint64_t CompressedDataSize = sizeof(BufferHeader) + MetaSize + GetCompressedBlocksBound(BlockCount, BlockSize, RawSize);
+ UniqueBuffer CompressedData = UniqueBuffer::Alloc(CompressedDataSize);
+
+ // Compress the raw data in blocks and store the raw data for incompressible blocks.
+ std::vector<uint32_t> CompressedBlockSizes;
+ CompressedBlockSizes.reserve(BlockCount);
+ uint64_t CompressedSize = 0;
+ {
+ UniqueBuffer RawBlockCopy;
+ MutableMemoryView CompressedBlocksView = CompressedData.GetMutableView() + sizeof(BufferHeader) + MetaSize;
+
+ CompositeBuffer::Iterator It = RawData.GetIterator(0);
+
+ for (uint64_t RawOffset = 0; RawOffset < RawSize;)
+ {
+ const uint64_t RawBlockSize = zen::Min(RawSize - RawOffset, BlockSize);
+ const MemoryView RawBlock = RawData.ViewOrCopyRange(It, RawBlockSize, RawBlockCopy);
+ RawHash.Append(RawBlock);
+
+ MutableMemoryView CompressedBlock = CompressedBlocksView;
+ if (!CompressBlock(CompressedBlock, RawBlock))
+ {
+ return CompositeBuffer();
+ }
+
+ uint64_t CompressedBlockSize = CompressedBlock.GetSize();
+ if (RawBlockSize <= CompressedBlockSize)
+ {
+ CompressedBlockSize = RawBlockSize;
+ CompressedBlocksView = CompressedBlocksView.CopyFrom(RawBlock);
+ }
+ else
+ {
+ CompressedBlocksView += CompressedBlockSize;
+ }
+
+ CompressedBlockSizes.push_back(static_cast<uint32_t>(CompressedBlockSize));
+ CompressedSize += CompressedBlockSize;
+ RawOffset += RawBlockSize;
+ }
+ }
+
+ // Return an uncompressed buffer if the compressed data is larger than the raw data.
+ if (RawSize <= MetaSize + CompressedSize)
+ {
+ CompressedData.Reset();
+ return NoneEncoder().Compress(RawData, BlockSize);
+ }
+
+ // Write the header and calculate the CRC-32.
+ for (uint32_t& Size : CompressedBlockSizes)
+ {
+ Size = ByteSwap(Size);
+ }
+ CompressedData.GetMutableView().Mid(sizeof(BufferHeader), MetaSize).CopyFrom(MakeMemoryView(CompressedBlockSizes));
+
+ BufferHeader Header;
+ Header.Method = GetMethod();
+ Header.Compressor = GetCompressor();
+ Header.CompressionLevel = GetCompressionLevel();
+ Header.BlockSizeExponent = static_cast<uint8_t>(zen::FloorLog2_64(BlockSize));
+ Header.BlockCount = static_cast<uint32_t>(BlockCount);
+ Header.TotalRawSize = RawSize;
+ Header.TotalCompressedSize = sizeof(BufferHeader) + MetaSize + CompressedSize;
+ Header.RawHash = RawHash.GetHash();
+ Header.Write(CompressedData.GetMutableView().Left(sizeof(BufferHeader) + MetaSize));
+
+ const MemoryView CompositeView = CompressedData.GetView().Left(Header.TotalCompressedSize);
+ return CompositeBuffer(SharedBuffer::MakeView(CompositeView, CompressedData.MoveToShared()));
+}
+
+class BlockDecoder : public BaseDecoder
+{
+public:
+ CompositeBuffer Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const final;
+ [[nodiscard]] bool TryDecompressTo(const BufferHeader& Header,
+ const CompositeBuffer& CompressedData,
+ MutableMemoryView RawView,
+ uint64_t RawOffset) const final;
+ [[nodiscard]] uint64_t GetHeaderSize(const BufferHeader& Header) const final
+ {
+ return sizeof(BufferHeader) + sizeof(uint32_t) * uint64_t(Header.BlockCount);
+ }
+
+protected:
+ virtual bool DecompressBlock(MutableMemoryView RawData, MemoryView CompressedData) const = 0;
+};
+
+CompositeBuffer
+BlockDecoder::Decompress(const BufferHeader& Header, const CompositeBuffer& CompressedData) const
+{
+ if (Header.BlockCount == 0 || Header.TotalCompressedSize != CompressedData.GetSize())
+ {
+ return CompositeBuffer();
+ }
+
+ // The raw data cannot reference the compressed data unless it is owned.
+ // An empty raw buffer requires an empty segment, which this path creates.
+ if (!CompressedData.IsOwned() || Header.TotalRawSize == 0)
+ {
+ UniqueBuffer Buffer = UniqueBuffer::Alloc(Header.TotalRawSize);
+ return TryDecompressTo(Header, CompressedData, Buffer, 0) ? CompositeBuffer(Buffer.MoveToShared()) : CompositeBuffer();
+ }
+
+ std::vector<uint32_t> CompressedBlockSizes;
+ CompressedBlockSizes.resize(Header.BlockCount);
+ CompressedData.CopyTo(MakeMutableMemoryView(CompressedBlockSizes), sizeof(BufferHeader));
+
+ for (uint32_t& Size : CompressedBlockSizes)
+ {
+ Size = ByteSwap(Size);
+ }
+
+ // Allocate the buffer for the raw blocks that were compressed.
+ SharedBuffer RawData;
+ MutableMemoryView RawDataView;
+ const uint64_t BlockSize = uint64_t(1) << Header.BlockSizeExponent;
+ {
+ uint64_t RawDataSize = 0;
+ uint64_t RemainingRawSize = Header.TotalRawSize;
+ for (const uint32_t CompressedBlockSize : CompressedBlockSizes)
+ {
+ const uint64_t RawBlockSize = zen::Min(RemainingRawSize, BlockSize);
+ if (CompressedBlockSize < BlockSize)
+ {
+ RawDataSize += RawBlockSize;
+ }
+ RemainingRawSize -= RawBlockSize;
+ }
+ UniqueBuffer RawDataBuffer = UniqueBuffer::Alloc(RawDataSize);
+ RawDataView = RawDataBuffer;
+ RawData = RawDataBuffer.MoveToShared();
+ }
+
+ // Decompress the compressed data in blocks and reference the uncompressed blocks.
+ uint64_t PendingCompressedSegmentOffset = sizeof(BufferHeader) + uint64_t(Header.BlockCount) * sizeof(uint32_t);
+ uint64_t PendingCompressedSegmentSize = 0;
+ uint64_t PendingRawSegmentOffset = 0;
+ uint64_t PendingRawSegmentSize = 0;
+ std::vector<SharedBuffer> Segments;
+
+ const auto CommitPendingCompressedSegment =
+ [&PendingCompressedSegmentOffset, &PendingCompressedSegmentSize, &CompressedData, &Segments] {
+ if (PendingCompressedSegmentSize)
+ {
+ CompressedData.IterateRange(PendingCompressedSegmentOffset,
+ PendingCompressedSegmentSize,
+ [&Segments](MemoryView View, const SharedBuffer& ViewOuter) {
+ Segments.push_back(SharedBuffer::MakeView(View, ViewOuter));
+ });
+ PendingCompressedSegmentOffset += PendingCompressedSegmentSize;
+ PendingCompressedSegmentSize = 0;
+ }
+ };
+
+ const auto CommitPendingRawSegment = [&PendingRawSegmentOffset, &PendingRawSegmentSize, &RawData, &Segments] {
+ if (PendingRawSegmentSize)
+ {
+ const MemoryView PendingSegment = RawData.GetView().Mid(PendingRawSegmentOffset, PendingRawSegmentSize);
+ Segments.push_back(SharedBuffer::MakeView(PendingSegment, RawData));
+ PendingRawSegmentOffset += PendingRawSegmentSize;
+ PendingRawSegmentSize = 0;
+ }
+ };
+
+ UniqueBuffer CompressedBlockCopy;
+ uint64_t RemainingRawSize = Header.TotalRawSize;
+ uint64_t RemainingCompressedSize = CompressedData.GetSize();
+ for (const uint32_t CompressedBlockSize : CompressedBlockSizes)
+ {
+ if (RemainingCompressedSize < CompressedBlockSize)
+ {
+ return CompositeBuffer();
+ }
+
+ const uint64_t RawBlockSize = zen::Min(RemainingRawSize, BlockSize);
+ if (RawBlockSize == CompressedBlockSize)
+ {
+ CommitPendingRawSegment();
+ PendingCompressedSegmentSize += RawBlockSize;
+ }
+ else
+ {
+ CommitPendingCompressedSegment();
+ const MemoryView CompressedBlock =
+ CompressedData.ViewOrCopyRange(PendingCompressedSegmentOffset, CompressedBlockSize, CompressedBlockCopy);
+ if (!DecompressBlock(RawDataView.Left(RawBlockSize), CompressedBlock))
+ {
+ return CompositeBuffer();
+ }
+ PendingCompressedSegmentOffset += CompressedBlockSize;
+ PendingRawSegmentSize += RawBlockSize;
+ RawDataView += RawBlockSize;
+ }
+
+ RemainingCompressedSize -= CompressedBlockSize;
+ RemainingRawSize -= RawBlockSize;
+ }
+
+ CommitPendingCompressedSegment();
+ CommitPendingRawSegment();
+
+ return CompositeBuffer(std::move(Segments));
+}
+
+bool
+BlockDecoder::TryDecompressTo(const BufferHeader& Header,
+ const CompositeBuffer& CompressedData,
+ MutableMemoryView RawView,
+ uint64_t RawOffset) const
+{
+ if (Header.TotalRawSize < RawOffset + RawView.GetSize() || Header.TotalCompressedSize != CompressedData.GetSize())
+ {
+ return false;
+ }
+
+ const uint64_t BlockSize = uint64_t(1) << Header.BlockSizeExponent;
+
+ UniqueBuffer BlockSizeBuffer;
+ MemoryView BlockSizeView = CompressedData.ViewOrCopyRange(sizeof(BufferHeader), Header.BlockCount * sizeof(uint32_t), BlockSizeBuffer);
+ std::span<uint32_t const> CompressedBlockSizes(reinterpret_cast<const uint32_t*>(BlockSizeView.GetData()), Header.BlockCount);
+
+ UniqueBuffer CompressedBlockCopy;
+ UniqueBuffer UncompressedBlockCopy;
+
+ const size_t FirstBlockIndex = uint64_t(RawOffset / BlockSize);
+ const size_t LastBlockIndex = uint64_t((RawOffset + RawView.GetSize() - 1) / BlockSize);
+ const uint64_t LastBlockSize = BlockSize - ((Header.BlockCount * BlockSize) - Header.TotalRawSize);
+ uint64_t OffsetInFirstBlock = RawOffset % BlockSize;
+ uint64_t CompressedOffset = sizeof(BufferHeader) + uint64_t(Header.BlockCount) * sizeof(uint32_t);
+ uint64_t RemainingRawSize = RawView.GetSize();
+
+ for (size_t BlockIndex = 0; BlockIndex < FirstBlockIndex; BlockIndex++)
+ {
+ const uint32_t CompressedBlockSize = ByteSwap(CompressedBlockSizes[BlockIndex]);
+ CompressedOffset += CompressedBlockSize;
+ }
+
+ for (size_t BlockIndex = FirstBlockIndex; BlockIndex <= LastBlockIndex; BlockIndex++)
+ {
+ const uint64_t UncompressedBlockSize = BlockIndex == Header.BlockCount - 1 ? LastBlockSize : BlockSize;
+ const uint32_t CompressedBlockSize = ByteSwap(CompressedBlockSizes[BlockIndex]);
+ const bool IsCompressed = CompressedBlockSize < UncompressedBlockSize;
+
+ const uint64_t BytesToUncompress = OffsetInFirstBlock > 0 ? zen::Min(RawView.GetSize(), UncompressedBlockSize - OffsetInFirstBlock)
+ : zen::Min(RemainingRawSize, BlockSize);
+
+ MemoryView CompressedBlock = CompressedData.ViewOrCopyRange(CompressedOffset, CompressedBlockSize, CompressedBlockCopy);
+
+ if (IsCompressed)
+ {
+ MutableMemoryView UncompressedBlock = RawView.Left(BytesToUncompress);
+
+ const bool IsAligned = BytesToUncompress == UncompressedBlockSize;
+ if (!IsAligned)
+ {
+ // Decompress to a temporary buffer when the first or the last block reads are not aligned with the block boundaries.
+ if (UncompressedBlockCopy.IsNull())
+ {
+ UncompressedBlockCopy = UniqueBuffer::Alloc(BlockSize);
+ }
+ UncompressedBlock = UncompressedBlockCopy.GetMutableView().Mid(0, UncompressedBlockSize);
+ }
+
+ if (!DecompressBlock(UncompressedBlock, CompressedBlock))
+ {
+ return false;
+ }
+
+ if (!IsAligned)
+ {
+ RawView.CopyFrom(UncompressedBlock.Mid(OffsetInFirstBlock, BytesToUncompress));
+ }
+ }
+ else
+ {
+ RawView.CopyFrom(CompressedBlock.Mid(OffsetInFirstBlock, BytesToUncompress));
+ }
+
+ OffsetInFirstBlock = 0;
+ RemainingRawSize -= BytesToUncompress;
+ CompressedOffset += CompressedBlockSize;
+ RawView += BytesToUncompress;
+ }
+
+ return RemainingRawSize == 0;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+struct OodleInit
+{
+ OodleInit()
+ {
+ OodleConfigValues Config;
+ Oodle_GetConfigValues(&Config);
+ // Always read/write Oodle v9 binary data.
+ Config.m_OodleLZ_BackwardsCompatible_MajorVersion = 9;
+ Oodle_SetConfigValues(&Config);
+ }
+};
+
+OodleInit InitOodle;
+
+class OodleEncoder final : public BlockEncoder
+{
+public:
+ OodleEncoder(OodleCompressor InCompressor, OodleCompressionLevel InCompressionLevel)
+ : Compressor(InCompressor)
+ , CompressionLevel(InCompressionLevel)
+ {
+ }
+
+protected:
+ CompressionMethod GetMethod() const final { return CompressionMethod::Oodle; }
+ uint8_t GetCompressor() const final { return static_cast<uint8_t>(Compressor); }
+ uint8_t GetCompressionLevel() const final { return static_cast<uint8_t>(CompressionLevel); }
+
+ uint64_t CompressBlockBound(uint64_t RawSize) const final
+ {
+ return static_cast<uint64_t>(OodleLZ_GetCompressedBufferSizeNeeded(OodleLZ_Compressor_Kraken, static_cast<OO_SINTa>(RawSize)));
+ }
+
+ bool CompressBlock(MutableMemoryView& CompressedData, MemoryView RawData) const final
+ {
+ const OodleLZ_Compressor LZCompressor = GetOodleLZCompressor(Compressor);
+ const OodleLZ_CompressionLevel LZCompressionLevel = GetOodleLZCompressionLevel(CompressionLevel);
+ if (LZCompressor == OodleLZ_Compressor_Invalid || LZCompressionLevel == OodleLZ_CompressionLevel_Invalid ||
+ LZCompressionLevel == OodleLZ_CompressionLevel_None)
+ {
+ return false;
+ }
+
+ const OO_SINTa RawSize = static_cast<OO_SINTa>(RawData.GetSize());
+ if (static_cast<OO_SINTa>(CompressedData.GetSize()) < OodleLZ_GetCompressedBufferSizeNeeded(LZCompressor, RawSize))
+ {
+ return false;
+ }
+
+ const OO_SINTa Size = OodleLZ_Compress(LZCompressor, RawData.GetData(), RawSize, CompressedData.GetData(), LZCompressionLevel);
+ CompressedData.LeftInline(static_cast<uint64_t>(Size));
+ return Size > 0;
+ }
+
+ static OodleLZ_Compressor GetOodleLZCompressor(OodleCompressor Compressor)
+ {
+ switch (Compressor)
+ {
+ case OodleCompressor::Selkie:
+ return OodleLZ_Compressor_Selkie;
+ case OodleCompressor::Mermaid:
+ return OodleLZ_Compressor_Mermaid;
+ case OodleCompressor::Kraken:
+ return OodleLZ_Compressor_Kraken;
+ case OodleCompressor::Leviathan:
+ return OodleLZ_Compressor_Leviathan;
+ case OodleCompressor::NotSet:
+ default:
+ return OodleLZ_Compressor_Invalid;
+ }
+ }
+
+ static OodleLZ_CompressionLevel GetOodleLZCompressionLevel(OodleCompressionLevel Level)
+ {
+ const int IntLevel = (int)Level;
+ if (IntLevel < (int)OodleLZ_CompressionLevel_Min || IntLevel > (int)OodleLZ_CompressionLevel_Max)
+ {
+ return OodleLZ_CompressionLevel_Invalid;
+ }
+ return OodleLZ_CompressionLevel(IntLevel);
+ }
+
+private:
+ const OodleCompressor Compressor;
+ const OodleCompressionLevel CompressionLevel;
+};
+
+class OodleDecoder final : public BlockDecoder
+{
+protected:
+ bool DecompressBlock(MutableMemoryView RawData, MemoryView CompressedData) const final
+ {
+ const OO_SINTa RawSize = static_cast<OO_SINTa>(RawData.GetSize());
+ const OO_SINTa Size = OodleLZ_Decompress(CompressedData.GetData(),
+ static_cast<OO_SINTa>(CompressedData.GetSize()),
+ RawData.GetData(),
+ RawSize,
+ OodleLZ_FuzzSafe_Yes,
+ OodleLZ_CheckCRC_Yes,
+ OodleLZ_Verbosity_None);
+ return Size == RawSize;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+
+class LZ4Decoder final : public BlockDecoder
+{
+protected:
+ bool DecompressBlock(MutableMemoryView RawData, MemoryView CompressedData) const final
+ {
+ if (CompressedData.GetSize() <= std::numeric_limits<int>::max())
+ {
+ const int Size = LZ4_decompress_safe(static_cast<const char*>(CompressedData.GetData()),
+ static_cast<char*>(RawData.GetData()),
+ static_cast<int>(CompressedData.GetSize()),
+ static_cast<int>(zen::Min<uint64_t>(RawData.GetSize(), uint64_t(LZ4_MAX_INPUT_SIZE))));
+ return static_cast<uint64_t>(Size) == RawData.GetSize();
+ }
+ return false;
+ }
+};
+
+//////////////////////////////////////////////////////////////////////////
+
+static const BaseDecoder*
+GetDecoder(CompressionMethod Method)
+{
+ static NoneDecoder None;
+ static OodleDecoder Oodle;
+ static LZ4Decoder LZ4;
+
+ switch (Method)
+ {
+ default:
+ return nullptr;
+ case CompressionMethod::None:
+ return &None;
+ case CompressionMethod::Oodle:
+ return &Oodle;
+ case CompressionMethod::LZ4:
+ return &LZ4;
+ }
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+bool
+BufferHeader::IsValid(const CompositeBuffer& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ uint64_t Size = CompressedData.GetSize();
+ if (Size < sizeof(BufferHeader))
+ {
+ return false;
+ }
+ const size_t StackBufferSize = 256;
+ uint8_t StackBuffer[StackBufferSize];
+ uint64_t ReadSize = Min(Size, StackBufferSize);
+ BufferHeader* Header = reinterpret_cast<BufferHeader*>(StackBuffer);
+ {
+ CompositeBuffer::Iterator It;
+ CompressedData.CopyTo(MutableMemoryView(StackBuffer, StackBuffer + StackBufferSize), It);
+ }
+ Header->ByteSwap();
+ if (Header->Magic != BufferHeader::ExpectedMagic)
+ {
+ return false;
+ }
+ const BaseDecoder* const Decoder = GetDecoder(Header->Method);
+ if (!Decoder)
+ {
+ return false;
+ }
+ uint32_t Crc32 = Header->Crc32;
+ OutRawHash = IoHash::FromBLAKE3(Header->RawHash);
+ OutRawSize = Header->TotalRawSize;
+ uint64_t HeaderSize = Decoder->GetHeaderSize(*Header);
+ Header->ByteSwap();
+
+ if (HeaderSize > ReadSize)
+ {
+ // 0.004% of cases on a Fortnite hot cache cook
+ UniqueBuffer HeaderCopy = UniqueBuffer::Alloc(HeaderSize);
+ CompositeBuffer::Iterator It;
+ CompressedData.CopyTo(HeaderCopy.GetMutableView(), It);
+ const MemoryView HeaderView = HeaderCopy.GetView();
+ if (Crc32 != BufferHeader::CalculateCrc32(HeaderView))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ MemoryView FullHeaderView(StackBuffer, StackBuffer + HeaderSize);
+ if (Crc32 != BufferHeader::CalculateCrc32(FullHeaderView))
+ {
+ return false;
+ }
+ }
+ return true;
+}
+
+//////////////////////////////////////////////////////////////////////////
+
+template<typename BufferType>
+inline CompositeBuffer
+ValidBufferOrEmpty(BufferType&& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ return BufferHeader::IsValid(CompressedData, OutRawHash, OutRawSize) ? CompositeBuffer(std::forward<BufferType>(CompressedData))
+ : CompositeBuffer();
+}
+
+CompositeBuffer
+CopyCompressedRange(const BufferHeader& Header, const CompositeBuffer& CompressedData, uint64_t RawOffset, uint64_t RawSize)
+{
+ if (Header.TotalRawSize < RawOffset + RawSize)
+ {
+ return CompositeBuffer();
+ }
+
+ if (Header.Method == CompressionMethod::None)
+ {
+ UniqueBuffer NewCompressedData = UniqueBuffer::Alloc(RawSize);
+ CompressedData.CopyTo(NewCompressedData.GetMutableView(), sizeof(Header) + RawOffset);
+
+ BufferHeader NewHeader = Header;
+ NewHeader.Crc32 = 0;
+ NewHeader.TotalRawSize = RawSize;
+ NewHeader.TotalCompressedSize = NewHeader.TotalRawSize + sizeof(BufferHeader);
+ NewHeader.RawHash = BLAKE3();
+
+ UniqueBuffer HeaderData = UniqueBuffer::Alloc(sizeof(BufferHeader));
+ NewHeader.Write(HeaderData);
+
+ return CompositeBuffer(HeaderData.MoveToShared(), NewCompressedData.MoveToShared());
+ }
+ else
+ {
+ UniqueBuffer BlockSizeBuffer;
+ MemoryView BlockSizeView =
+ CompressedData.ViewOrCopyRange(sizeof(BufferHeader), Header.BlockCount * sizeof(uint32_t), BlockSizeBuffer);
+ std::span<uint32_t const> CompressedBlockSizes(reinterpret_cast<const uint32_t*>(BlockSizeView.GetData()), Header.BlockCount);
+
+ const uint64_t BlockSize = uint64_t(1) << Header.BlockSizeExponent;
+ const uint64_t LastBlockSize = BlockSize - ((Header.BlockCount * BlockSize) - Header.TotalRawSize);
+ const size_t FirstBlock = uint64_t(RawOffset / BlockSize);
+ const size_t LastBlock = uint64_t((RawOffset + RawSize - 1) / BlockSize);
+ uint64_t CompressedOffset = sizeof(BufferHeader) + uint64_t(Header.BlockCount) * sizeof(uint32_t);
+
+ const uint64_t NewBlockCount = LastBlock - FirstBlock + 1;
+ const uint64_t NewMetaSize = NewBlockCount * sizeof(uint32_t);
+ uint64_t NewCompressedSize = 0;
+ uint64_t NewTotalRawSize = 0;
+ std::vector<uint32_t> NewCompressedBlockSizes;
+
+ NewCompressedBlockSizes.reserve(NewBlockCount);
+ for (size_t BlockIndex = FirstBlock; BlockIndex <= LastBlock; ++BlockIndex)
+ {
+ const uint64_t UncompressedBlockSize = (BlockIndex == Header.BlockCount - 1) ? LastBlockSize : BlockSize;
+ NewTotalRawSize += UncompressedBlockSize;
+
+ const uint32_t CompressedBlockSize = CompressedBlockSizes[BlockIndex];
+ NewCompressedBlockSizes.push_back(CompressedBlockSize);
+ NewCompressedSize += ByteSwap(CompressedBlockSize);
+ }
+
+ const uint64_t NewTotalCompressedSize = sizeof(BufferHeader) + NewBlockCount * sizeof(uint32_t) + NewCompressedSize;
+ UniqueBuffer NewCompressedData = UniqueBuffer::Alloc(NewTotalCompressedSize);
+ MutableMemoryView NewCompressedBlocks = NewCompressedData.GetMutableView() + sizeof(BufferHeader) + NewMetaSize;
+
+ // Seek to first compressed block
+ for (size_t BlockIndex = 0; BlockIndex < FirstBlock; ++BlockIndex)
+ {
+ const uint64_t CompressedBlockSize = ByteSwap(CompressedBlockSizes[BlockIndex]);
+ CompressedOffset += CompressedBlockSize;
+ }
+
+ // Copy blocks
+ UniqueBuffer CompressedBlockCopy;
+ const MemoryView CompressedRange = CompressedData.ViewOrCopyRange(CompressedOffset, NewCompressedSize, CompressedBlockCopy);
+ NewCompressedBlocks.CopyFrom(CompressedRange);
+
+ // Copy block sizes
+ NewCompressedData.GetMutableView().Mid(sizeof(BufferHeader), NewMetaSize).CopyFrom(MakeMemoryView(NewCompressedBlockSizes));
+
+ BufferHeader NewHeader;
+ NewHeader.Crc32 = 0;
+ NewHeader.Method = Header.Method;
+ NewHeader.Compressor = Header.Compressor;
+ NewHeader.CompressionLevel = Header.CompressionLevel;
+ NewHeader.BlockSizeExponent = Header.BlockSizeExponent;
+ NewHeader.BlockCount = static_cast<uint32_t>(NewBlockCount);
+ NewHeader.TotalRawSize = NewTotalRawSize;
+ NewHeader.TotalCompressedSize = NewTotalCompressedSize;
+ NewHeader.RawHash = BLAKE3();
+ NewHeader.Write(NewCompressedData.GetMutableView().Left(sizeof(BufferHeader) + NewMetaSize));
+
+ return CompositeBuffer(NewCompressedData.MoveToShared());
+ }
+}
+
+} // namespace zen::detail
+
+namespace zen {
+
+const CompressedBuffer CompressedBuffer::Null;
+
+CompressedBuffer
+CompressedBuffer::Compress(const CompositeBuffer& RawData,
+ OodleCompressor Compressor,
+ OodleCompressionLevel CompressionLevel,
+ uint64_t BlockSize)
+{
+ using namespace detail;
+
+ if (BlockSize == 0)
+ {
+ BlockSize = DefaultBlockSize;
+ }
+
+ CompressedBuffer Local;
+ if (CompressionLevel == OodleCompressionLevel::None)
+ {
+ Local.CompressedData = NoneEncoder().Compress(RawData, BlockSize);
+ }
+ else
+ {
+ Local.CompressedData = OodleEncoder(Compressor, CompressionLevel).Compress(RawData, BlockSize);
+ }
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::Compress(const SharedBuffer& RawData,
+ OodleCompressor Compressor,
+ OodleCompressionLevel CompressionLevel,
+ uint64_t BlockSize)
+{
+ return Compress(CompositeBuffer(RawData), Compressor, CompressionLevel, BlockSize);
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressed(const CompositeBuffer& InCompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ CompressedBuffer Local;
+ Local.CompressedData = detail::ValidBufferOrEmpty(InCompressedData, OutRawHash, OutRawSize);
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressed(CompositeBuffer&& InCompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ CompressedBuffer Local;
+ Local.CompressedData = detail::ValidBufferOrEmpty(std::move(InCompressedData), OutRawHash, OutRawSize);
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressed(const SharedBuffer& InCompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ CompressedBuffer Local;
+ Local.CompressedData = detail::ValidBufferOrEmpty(InCompressedData, OutRawHash, OutRawSize);
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressed(SharedBuffer&& InCompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ CompressedBuffer Local;
+ Local.CompressedData = detail::ValidBufferOrEmpty(std::move(InCompressedData), OutRawHash, OutRawSize);
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressedNoValidate(IoBuffer&& InCompressedData)
+{
+ if (InCompressedData.GetSize() <= sizeof(detail::BufferHeader))
+ {
+ return CompressedBuffer();
+ }
+ CompressedBuffer Local;
+ Local.CompressedData = CompositeBuffer(SharedBuffer(std::move(InCompressedData)));
+ return Local;
+}
+
+CompressedBuffer
+CompressedBuffer::FromCompressedNoValidate(CompositeBuffer&& InCompressedData)
+{
+ if (InCompressedData.GetSize() <= sizeof(detail::BufferHeader))
+ {
+ return CompressedBuffer();
+ }
+ CompressedBuffer Local;
+ Local.CompressedData = std::move(InCompressedData);
+ return Local;
+}
+
+bool
+CompressedBuffer::ValidateCompressedHeader(IoBuffer&& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ return detail::BufferHeader::IsValid(SharedBuffer(std::move(CompressedData)), OutRawHash, OutRawSize);
+}
+
+bool
+CompressedBuffer::ValidateCompressedHeader(const IoBuffer& CompressedData, IoHash& OutRawHash, uint64_t& OutRawSize)
+{
+ return detail::BufferHeader::IsValid(SharedBuffer(CompressedData), OutRawHash, OutRawSize);
+}
+
+uint64_t
+CompressedBuffer::DecodeRawSize() const
+{
+ return CompressedData ? detail::BufferHeader::Read(CompressedData).TotalRawSize : 0;
+}
+
+IoHash
+CompressedBuffer::DecodeRawHash() const
+{
+ return CompressedData ? IoHash::FromBLAKE3(detail::BufferHeader::Read(CompressedData).RawHash) : IoHash();
+}
+
+CompressedBuffer
+CompressedBuffer::CopyRange(uint64_t RawOffset, uint64_t RawSize) const
+{
+ using namespace detail;
+ const BufferHeader Header = BufferHeader::Read(CompressedData);
+ const uint64_t TotalRawSize = RawSize < ~uint64_t(0) ? RawSize : Header.TotalRawSize - RawOffset;
+
+ CompressedBuffer Range;
+ Range.CompressedData = CopyCompressedRange(Header, CompressedData, RawOffset, TotalRawSize);
+
+ return Range;
+}
+
+bool
+CompressedBuffer::TryDecompressTo(MutableMemoryView RawView, uint64_t RawOffset) const
+{
+ using namespace detail;
+ if (CompressedData)
+ {
+ const BufferHeader Header = BufferHeader::Read(CompressedData);
+ if (Header.Magic == BufferHeader::ExpectedMagic)
+ {
+ if (const BaseDecoder* const Decoder = GetDecoder(Header.Method))
+ {
+ return Decoder->TryDecompressTo(Header, CompressedData, RawView, RawOffset);
+ }
+ }
+ }
+ return false;
+}
+
+SharedBuffer
+CompressedBuffer::Decompress(uint64_t RawOffset, uint64_t RawSize) const
+{
+ using namespace detail;
+ if (CompressedData && RawSize > 0)
+ {
+ const BufferHeader Header = BufferHeader::Read(CompressedData);
+ if (Header.Magic == BufferHeader::ExpectedMagic)
+ {
+ if (const BaseDecoder* const Decoder = GetDecoder(Header.Method))
+ {
+ const uint64_t TotalRawSize = RawSize < ~uint64_t(0) ? RawSize : Header.TotalRawSize - RawOffset;
+ UniqueBuffer RawData = UniqueBuffer::Alloc(TotalRawSize);
+ if (Decoder->TryDecompressTo(Header, CompressedData, RawData, RawOffset))
+ {
+ return RawData.MoveToShared();
+ }
+ }
+ }
+ }
+ return SharedBuffer();
+}
+
+CompositeBuffer
+CompressedBuffer::DecompressToComposite() const
+{
+ using namespace detail;
+ if (CompressedData)
+ {
+ const BufferHeader Header = BufferHeader::Read(CompressedData);
+ if (Header.Magic == BufferHeader::ExpectedMagic)
+ {
+ if (const BaseDecoder* const Decoder = GetDecoder(Header.Method))
+ {
+ return Decoder->Decompress(Header, CompressedData);
+ }
+ }
+ }
+ return CompositeBuffer();
+}
+
+bool
+CompressedBuffer::TryGetCompressParameters(OodleCompressor& OutCompressor,
+ OodleCompressionLevel& OutCompressionLevel,
+ uint64_t& OutBlockSize) const
+{
+ using namespace detail;
+ if (CompressedData)
+ {
+ switch (const BufferHeader Header = BufferHeader::Read(CompressedData); Header.Method)
+ {
+ case CompressionMethod::None:
+ OutCompressor = OodleCompressor::NotSet;
+ OutCompressionLevel = OodleCompressionLevel::None;
+ OutBlockSize = 0;
+ return true;
+ case CompressionMethod::Oodle:
+ OutCompressor = OodleCompressor(Header.Compressor);
+ OutCompressionLevel = OodleCompressionLevel(Header.CompressionLevel);
+ OutBlockSize = uint64_t(1) << Header.BlockSizeExponent;
+ return true;
+ default:
+ break;
+ }
+ }
+ return false;
+}
+
+/**
+ ______________________ _____________________________
+ \__ ___/\_ _____// _____/\__ ___/ _____/
+ | | | __)_ \_____ \ | | \_____ \
+ | | | \/ \ | | / \
+ |____| /_______ /_______ / |____| /_______ /
+ \/ \/ \/
+ */
+
+#if ZEN_WITH_TESTS
+
+TEST_CASE("CompressedBuffer")
+{
+ uint8_t Zeroes[1024]{};
+ uint8_t Ones[1024];
+ memset(Ones, 1, sizeof Ones);
+
+ {
+ CompressedBuffer Buffer = CompressedBuffer::Compress(CompositeBuffer(SharedBuffer::MakeView(MakeMemoryView(Zeroes))),
+ OodleCompressor::NotSet,
+ OodleCompressionLevel::None);
+
+ CHECK(Buffer.DecodeRawSize() == sizeof(Zeroes));
+ CHECK(Buffer.GetCompressedSize() == (sizeof(Zeroes) + sizeof(detail::BufferHeader)));
+
+ CompositeBuffer Compressed = Buffer.GetCompressed();
+ IoHash DecodedHash;
+ uint64_t DecodedRawSize;
+ CompressedBuffer BufferD = CompressedBuffer::FromCompressed(Compressed, DecodedHash, DecodedRawSize);
+
+ CHECK(BufferD.IsNull() == false);
+
+ CompositeBuffer Decomp = BufferD.DecompressToComposite();
+
+ CHECK(Decomp.GetSize() == DecodedRawSize);
+ CHECK(IoHash::HashBuffer(Decomp) == DecodedHash);
+ }
+
+ {
+ CompressedBuffer Buffer = CompressedBuffer::Compress(
+ CompositeBuffer(SharedBuffer::MakeView(MakeMemoryView(Zeroes)), SharedBuffer::MakeView(MakeMemoryView(Ones))),
+ OodleCompressor::NotSet,
+ OodleCompressionLevel::None);
+
+ CHECK(Buffer.DecodeRawSize() == (sizeof(Zeroes) + sizeof(Ones)));
+ CHECK(Buffer.GetCompressedSize() == (sizeof(Zeroes) + sizeof(Ones) + sizeof(detail::BufferHeader)));
+
+ CompositeBuffer Compressed = Buffer.GetCompressed();
+ IoHash DecodedHash;
+ uint64_t DecodedRawSize;
+ CompressedBuffer BufferD = CompressedBuffer::FromCompressed(Compressed, DecodedHash, DecodedRawSize);
+
+ CHECK(BufferD.IsNull() == false);
+
+ CompositeBuffer Decomp = BufferD.DecompressToComposite();
+
+ CHECK(Decomp.GetSize() == DecodedRawSize);
+ CHECK(IoHash::HashBuffer(Decomp) == DecodedHash);
+ }
+
+ {
+ CompressedBuffer Buffer = CompressedBuffer::Compress(CompositeBuffer(SharedBuffer::MakeView(MakeMemoryView(Zeroes))));
+
+ CHECK(Buffer.DecodeRawSize() == sizeof(Zeroes));
+ CHECK(Buffer.GetCompressedSize() < sizeof(Zeroes));
+
+ CompositeBuffer Compressed = Buffer.GetCompressed();
+ IoHash DecodedHash;
+ uint64_t DecodedRawSize;
+ CompressedBuffer BufferD = CompressedBuffer::FromCompressed(Compressed, DecodedHash, DecodedRawSize);
+
+ CHECK(BufferD.IsNull() == false);
+
+ CompositeBuffer Decomp = BufferD.DecompressToComposite();
+
+ CHECK(Decomp.GetSize() == DecodedRawSize);
+ CHECK(IoHash::HashBuffer(Decomp) == DecodedHash);
+ }
+
+ {
+ CompressedBuffer Buffer = CompressedBuffer::Compress(
+ CompositeBuffer(SharedBuffer::MakeView(MakeMemoryView(Zeroes)), SharedBuffer::MakeView(MakeMemoryView(Ones))));
+
+ CHECK(Buffer.DecodeRawSize() == (sizeof(Zeroes) + sizeof(Ones)));
+ CHECK(Buffer.GetCompressedSize() < (sizeof(Zeroes) + sizeof(Ones)));
+
+ CompositeBuffer Compressed = Buffer.GetCompressed();
+ IoHash DecodedHash;
+ uint64_t DecodedRawSize;
+ CompressedBuffer BufferD = CompressedBuffer::FromCompressed(Compressed, DecodedHash, DecodedRawSize);
+
+ CHECK(BufferD.IsNull() == false);
+
+ CompositeBuffer Decomp = BufferD.DecompressToComposite();
+
+ CHECK(Decomp.GetSize() == DecodedRawSize);
+ CHECK(IoHash::HashBuffer(Decomp) == DecodedHash);
+ }
+
+ auto GenerateData = [](uint64_t N) -> std::vector<uint64_t> {
+ std::vector<uint64_t> Data;
+ Data.resize(N);
+ for (size_t Idx = 0; Idx < Data.size(); ++Idx)
+ {
+ Data[Idx] = Idx;
+ }
+ return Data;
+ };
+
+ auto ValidateData = [](std::span<uint64_t const> Values, std::span<uint64_t const> ExpectedValues, uint64_t Offset) {
+ for (size_t Idx = Offset; uint64_t Value : Values)
+ {
+ const uint64_t ExpectedValue = ExpectedValues[Idx++];
+ CHECK(Value == ExpectedValue);
+ }
+ };
+
+ SUBCASE("decompress with offset and size")
+ {
+ auto UncompressAndValidate = [&ValidateData](CompressedBuffer Compressed,
+ uint64_t OffsetCount,
+ uint64_t Count,
+ const std::vector<uint64_t>& ExpectedValues) {
+ SharedBuffer Uncompressed = Compressed.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t));
+ CHECK(Uncompressed.GetSize() == Count * sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ };
+
+ const uint64_t BlockSize = 64 * sizeof(uint64_t);
+ const uint64_t N = 5000;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::Mermaid,
+ OodleCompressionLevel::Optimal4,
+ BlockSize);
+ UncompressAndValidate(Compressed, 0, N, ExpectedValues);
+ UncompressAndValidate(Compressed, 1, N - 1, ExpectedValues);
+ UncompressAndValidate(Compressed, N - 1, 1, ExpectedValues);
+ UncompressAndValidate(Compressed, 0, 1, ExpectedValues);
+ UncompressAndValidate(Compressed, 2, 4, ExpectedValues);
+ UncompressAndValidate(Compressed, 0, 512, ExpectedValues);
+ UncompressAndValidate(Compressed, 3, 514, ExpectedValues);
+ UncompressAndValidate(Compressed, 256, 512, ExpectedValues);
+ UncompressAndValidate(Compressed, 512, 512, ExpectedValues);
+ }
+
+ SUBCASE("decompress with offset only")
+ {
+ const uint64_t BlockSize = 64 * sizeof(uint64_t);
+ const uint64_t N = 1000;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::Mermaid,
+ OodleCompressionLevel::Optimal4,
+ BlockSize);
+ const uint64_t OffsetCount = 150;
+ SharedBuffer Uncompressed = Compressed.Decompress(OffsetCount * sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ SUBCASE("decompress buffer with one block")
+ {
+ const uint64_t BlockSize = 256 * sizeof(uint64_t);
+ const uint64_t N = 100;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::Mermaid,
+ OodleCompressionLevel::Optimal4,
+ BlockSize);
+ const uint64_t OffsetCount = 2;
+ const uint64_t Count = 50;
+ SharedBuffer Uncompressed = Compressed.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ SUBCASE("decompress uncompressed buffer")
+ {
+ const uint64_t N = 4242;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::NotSet,
+ OodleCompressionLevel::None);
+ {
+ const uint64_t OffsetCount = 0;
+ const uint64_t Count = N;
+ SharedBuffer Uncompressed = Compressed.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 21;
+ const uint64_t Count = 999;
+ SharedBuffer Uncompressed = Compressed.Decompress(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+ }
+
+ SUBCASE("copy range")
+ {
+ const uint64_t BlockSize = 64 * sizeof(uint64_t);
+ const uint64_t N = 1000;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::Mermaid,
+ OodleCompressionLevel::Optimal4,
+ BlockSize);
+
+ {
+ const uint64_t OffsetCount = 0;
+ const uint64_t Count = N;
+ SharedBuffer Uncompressed = Compressed.CopyRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress();
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 64;
+ const uint64_t Count = N - 64;
+ SharedBuffer Uncompressed = Compressed.CopyRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress();
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 64 * 2 + 32;
+ const uint64_t Count = N - OffsetCount;
+ const uint64_t RawOffset = OffsetCount * sizeof(uint64_t);
+ const uint64_t RawSize = Count * sizeof(uint64_t);
+ uint64_t FirstBlockOffset = RawOffset % BlockSize;
+
+ SharedBuffer Uncompressed = Compressed.CopyRange(RawOffset, RawSize).Decompress();
+ std::span<uint64_t const> AllValues((const uint64_t*)Uncompressed.GetData(), RawSize / sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)(((const uint8_t*)(Uncompressed.GetData()) + FirstBlockOffset)),
+ RawSize / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 64 * 2 + 63;
+ const uint64_t Count = N - OffsetCount - 5;
+ const uint64_t RawOffset = OffsetCount * sizeof(uint64_t);
+ const uint64_t RawSize = Count * sizeof(uint64_t);
+ uint64_t FirstBlockOffset = RawOffset % BlockSize;
+
+ SharedBuffer Uncompressed = Compressed.CopyRange(RawOffset, RawSize).Decompress();
+ std::span<uint64_t const> AllValues((const uint64_t*)Uncompressed.GetData(), RawSize / sizeof(uint64_t));
+ std::span<uint64_t const> Values((const uint64_t*)(((const uint8_t*)(Uncompressed.GetData()) + FirstBlockOffset)),
+ RawSize / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+ }
+
+ SUBCASE("copy uncompressed range")
+ {
+ const uint64_t N = 1000;
+ std::vector<uint64_t> ExpectedValues = GenerateData(N);
+
+ CompressedBuffer Compressed = CompressedBuffer::Compress(SharedBuffer::MakeView(MakeMemoryView(ExpectedValues)),
+ OodleCompressor::NotSet,
+ OodleCompressionLevel::None);
+
+ {
+ const uint64_t OffsetCount = 0;
+ const uint64_t Count = N;
+ SharedBuffer Uncompressed = Compressed.CopyRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress();
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 1;
+ const uint64_t Count = N - OffsetCount;
+ SharedBuffer Uncompressed = Compressed.CopyRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress();
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+
+ {
+ const uint64_t OffsetCount = 42;
+ const uint64_t Count = 100;
+ SharedBuffer Uncompressed = Compressed.CopyRange(OffsetCount * sizeof(uint64_t), Count * sizeof(uint64_t)).Decompress();
+ std::span<uint64_t const> Values((const uint64_t*)Uncompressed.GetData(), Uncompressed.GetSize() / sizeof(uint64_t));
+ CHECK(Values.size() == Count);
+ ValidateData(Values, ExpectedValues, OffsetCount);
+ }
+ }
+}
+
+void
+compress_forcelink()
+{
+}
+#endif
+
+} // namespace zen