diff options
| author | Stefan Boberg <[email protected]> | 2023-05-02 10:01:47 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2023-05-02 10:01:47 +0200 |
| commit | 075d17f8ada47e990fe94606c3d21df409223465 (patch) | |
| tree | e50549b766a2f3c354798a54ff73404217b4c9af /src/zencore/compress.cpp | |
| parent | fix: bundle shouldn't append content zip to zen (diff) | |
| download | zen-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.cpp | 1353 |
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 |