diff --git a/code/cstream.cpp b/code/cstream.cpp index f0429c6c..e0cd4003 100644 --- a/code/cstream.cpp +++ b/code/cstream.cpp @@ -28,8 +28,9 @@ CStreamClass::CStreamClass(void) : IsWriting(false), CurOffset(0), DataBuffer(new unsigned char[BUFFER_SIZE]), - StreamBuffer(new unsigned char[BUFFER_SIZE]), - LZODictionary(new unsigned char[BUFFER_SIZE]) + StreamBuffer(new unsigned char[COMP_BUFFER_SIZE]), + // The dictionary holds pointer-wide entries, so its byte size follows the target. + LZODictionary(new unsigned char[LZO1X_MEM_COMPRESS]) { BlockHead.CompSize = BUFFER_SIZE - 1; } @@ -490,7 +491,9 @@ HRESULT CStreamClass::Compress(void *in_buffer, ULONG length) HRESULT hr; unsigned int out_len = length; lzo1x_1_compress((lzo_byte *)in_buffer, length, (lzo_byte *)StreamBuffer, &out_len, (lzo_byte *)LZODictionary); - BlockHead.UncompSize = BUFFER_SIZE; + // The final block of a stream is usually partial, and the reader takes this + // header as the number of bytes the block expands to. + BlockHead.UncompSize = length; length = 0; BlockHead.CompSize = out_len; diff --git a/code/cstream.h b/code/cstream.h index 33e1b48b..b3adccab 100644 --- a/code/cstream.h +++ b/code/cstream.h @@ -46,6 +46,10 @@ class CStreamClass : public IStream, public ILinkStream enum { BUFFER_SIZE = 64*1024, + + // A block that will not compress comes out bigger than it went in, up to + // the LZO worst case, and the compressed side has to hold that. + COMP_BUFFER_SIZE = BUFFER_SIZE + BUFFER_SIZE / 16 + 64 + 3, }; private: diff --git a/code/lzopipe.cpp b/code/lzopipe.cpp index 283ee4b3..191e877d 100644 --- a/code/lzopipe.cpp +++ b/code/lzopipe.cpp @@ -196,7 +196,7 @@ int LZOPipe::Put(void const * source, int slen) if (Counter == BlockSize) { unsigned int len = sizeof (Buffer2); - char *dictionary = new char [64*1024]; + char *dictionary = new char [LZO1X_MEM_COMPRESS]; lzo1x_1_compress ((unsigned char*)Buffer, BlockSize, (unsigned char*)Buffer2, &len, dictionary); delete [] dictionary; BlockHeader.CompCount = (unsigned short)len; @@ -213,7 +213,7 @@ int LZOPipe::Put(void const * source, int slen) */ while (slen >= BlockSize) { unsigned int len = 0;//sizeof (Buffer2); - char *dictionary = new char [64*1024]; + char *dictionary = new char [LZO1X_MEM_COMPRESS]; lzo1x_1_compress ((unsigned char*)source, BlockSize, (unsigned char*)Buffer2, &len, dictionary); delete [] dictionary; source = ((char *)source) + BlockSize; @@ -300,7 +300,7 @@ int LZOPipe::Flush(void) ** compress the partial block and output normally. */ unsigned int len = 0;//sizeof (Buffer2); - char *dictionary = new char [64*1024]; + char *dictionary = new char [LZO1X_MEM_COMPRESS]; lzo1x_1_compress ((unsigned char*)Buffer, Counter, (unsigned char *)Buffer2, &len, dictionary); delete [] dictionary; BlockHeader.CompCount = (unsigned short)len; diff --git a/code/lzostraw.cpp b/code/lzostraw.cpp index e2490b52..bdaea314 100644 --- a/code/lzostraw.cpp +++ b/code/lzostraw.cpp @@ -171,7 +171,7 @@ int LZOStraw::Get(void * destbuf, int slen) } else { BlockHeader.UncompCount = (unsigned short)BASECLASS::Get(Buffer, BlockSize); if (BlockHeader.UncompCount == 0) break; - char *dictionary = new char [64*1024]; + char *dictionary = new char [LZO1X_MEM_COMPRESS]; unsigned int length = sizeof (Buffer2) - sizeof (BlockHeader); lzo1x_1_compress ((unsigned char*)Buffer, BlockHeader.UncompCount, (unsigned char*)(&Buffer2[sizeof(BlockHeader)]), &length, dictionary); BlockHeader.CompCount = (unsigned short)length; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5de329d7..74c398c4 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,4 @@ add_subdirectory(gamedirs) add_subdirectory(logstress) add_subdirectory(cpudetect) +add_subdirectory(lzocomp) diff --git a/tests/lzocomp/CMakeLists.txt b/tests/lzocomp/CMakeLists.txt new file mode 100644 index 00000000..fcac6cf6 --- /dev/null +++ b/tests/lzocomp/CMakeLists.txt @@ -0,0 +1,33 @@ +# The compression sources are compiled straight into the harness. They live outside code/ +# so that the recursive glob building OpenTS cannot pick this target's entry point up. +add_executable(LzoComp + "${CMAKE_CURRENT_SOURCE_DIR}/lzocomp.cpp" + "${CMAKE_SOURCE_DIR}/code/lzo1x_c.cpp" + "${CMAKE_SOURCE_DIR}/code/lzo1x_d.cpp" + "${CMAKE_SOURCE_DIR}/code/lzopipe.cpp" + "${CMAKE_SOURCE_DIR}/code/lzostraw.cpp" + "${CMAKE_SOURCE_DIR}/code/pipe.cpp" + "${CMAKE_SOURCE_DIR}/code/straw.cpp" +) + +target_compile_features(LzoComp PRIVATE cxx_std_20) + +target_include_directories(LzoComp PRIVATE + "${CMAKE_SOURCE_DIR}/code" + "${CMAKE_CURRENT_SOURCE_DIR}" +) + +target_compile_definitions(LzoComp PRIVATE WIN32 _WINDOWS _MBCS NOMINMAX) + +target_compile_options(LzoComp PRIVATE + $<$:/MTd /EHsc /Zc:__cplusplus> + $<$:/MT /EHsc /Zc:__cplusplus> +) + +target_link_libraries(LzoComp PRIVATE kernel32 user32 shell32) + +set_target_properties(LzoComp PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +add_test(NAME lzocomp COMMAND LzoComp) diff --git a/tests/lzocomp/lzocomp.cpp b/tests/lzocomp/lzocomp.cpp new file mode 100644 index 00000000..ec4e0f40 --- /dev/null +++ b/tests/lzocomp/lzocomp.cpp @@ -0,0 +1,159 @@ +/******************************************************************************* + * O P E N T S + ******************************************************************************* + * SPDX-License-Identifier: GPL-3.0-or-later + * Copyright 2026 OpenTS contributors + * + * See LICENSE.md for applicable additional terms and warranty disclaimers. + ******************************************************************************/ + +// Round-trips data through LZOPipe and LZOStraw, the compression path save games +// travel. The compressor's dictionary holds pointers, so its byte size differs +// between 32-bit and LP64 targets; an undersized allocation corrupts the stream +// this test would then fail to expand. Needs no game data. + +#include +#include +#include + +#include "lzopipe.h" +#include "lzostraw.h" + +namespace { + +int Failures = 0; + +unsigned int Seed = 0; + +unsigned int Next_Random(void) +{ + Seed = Seed * 1103515245u + 12345u; + return(Seed >> 8); +} + + +// Collects everything a pipe chain emits. +class CapturePipe : public Pipe +{ + public: + std::vector Data; + + virtual int Put(void const * source, int slen) override + { + unsigned char const * bytes = (unsigned char const *)source; + Data.insert(Data.end(), bytes, bytes + slen); + return(slen); + } +}; + + +// Serves a fixed buffer to a straw chain. +class MemoryStraw : public Straw +{ + public: + MemoryStraw(unsigned char const * data, int length) : Data(data), Length(length), Offset(0) {} + + virtual int Get(void * buffer, int slen) override + { + int count = (slen < Length - Offset) ? slen : (Length - Offset); + if (count > 0) { + memcpy(buffer, Data + Offset, count); + Offset += count; + } + return(count); + } + + private: + unsigned char const * Data; + int Length; + int Offset; +}; + + +void Fill_Source(std::vector & source, int shape, int size, unsigned int seed) +{ + Seed = seed; + source.resize((size_t)size); + + for (int i = 0; i < size; i++) { + switch (shape) { + case 0: + source[(size_t)i] = (unsigned char)Next_Random(); + break; + + case 1: + source[(size_t)i] = (unsigned char)(i & 0x0F); + break; + + case 2: + source[(size_t)i] = (unsigned char)((Next_Random() % 8 == 0) ? Next_Random() : 0x55); + break; + + default: + source[(size_t)i] = 0; + break; + } + } +} + + +void Check_Roundtrip(int shape, int size, unsigned int seed) +{ + std::vector source; + Fill_Source(source, shape, size, seed); + + CapturePipe captured; + { + LZOPipe compressor(LZOPipe::COMPRESS); + compressor.Put_To(captured); + int offset = 0; + while (offset < size) { + int chunk = (size - offset < 977) ? (size - offset) : 977; + compressor.Put(&source[(size_t)offset], chunk); + offset += chunk; + } + compressor.End(); + } + + MemoryStraw stored(captured.Data.data(), (int)captured.Data.size()); + LZOStraw expander(LZOStraw::DECOMPRESS); + expander.Get_From(stored); + + std::vector expanded((size_t)size, 0); + int got = 0; + while (got < size) { + int step = expander.Get(&expanded[(size_t)got], size - got); + if (step <= 0) { + break; + } + got += step; + } + + if (got != size || memcmp(source.data(), expanded.data(), (size_t)size) != 0) { + printf("FAIL shape %d size %d: %d of %d bytes back, %s\n", shape, size, got, size, + (got == size) ? "content differs" : "stream ended short"); + Failures++; + } +} + +} // namespace + + +int main(void) +{ + int const sizes[] = { 1, 100, 8191, 8192, 8193, 65536, 250000 }; + + for (int shape = 0; shape < 3; shape++) { + for (int size : sizes) { + Check_Roundtrip(shape, size, 0x1234u + (unsigned int)shape); + } + } + + if (Failures > 0) { + printf("lzocomp: %d failures\n", Failures); + return(1); + } + + printf("lzocomp: all round trips match\n"); + return(0); +}