Skip to content

Commit

Permalink
bsppp: move valve LZMA handling out to common library
Browse files Browse the repository at this point in the history
  • Loading branch information
craftablescience committed Jan 20, 2025
1 parent ca2b924 commit 26199b9
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 112 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ include_subdirectory(ext)

# Shared code
include_subdirectory(src/sourcepp)
include_subdirectory(src/sourcepp/compression)
include_subdirectory(src/sourcepp/crypto)
include_subdirectory(src/sourcepp/parser)

Expand Down
8 changes: 3 additions & 5 deletions ext/_ext.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,10 @@ if(NOT TARGET miniz)
endif()


# minizip-ng (guard this because it's a big dependency!)
if((SOURCEPP_USE_BSPPP OR SOURCEPP_USE_VPKPP OR SOURCEPP_USE_VTFPP) AND NOT TARGET MINIZIP::minizip)
# minizip-ng
if(NOT TARGET MINIZIP::minizip)
set(MZ_COMPAT OFF CACHE INTERNAL "")
if(SOURCEPP_USE_BSPPP)
set(MZ_LZMA ON CACHE INTERNAL "" FORCE)
endif()
set(MZ_LZMA ON CACHE INTERNAL "" FORCE)
if(SOURCEPP_USE_VTFPP OR SOURCEPP_VPKPP_SUPPORT_VPK_V54)
set(MZ_ZSTD ON CACHE INTERNAL "" FORCE)
endif()
Expand Down
5 changes: 0 additions & 5 deletions include/bsppp/BSP.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
namespace bsppp {

constexpr auto BSP_SIGNATURE = sourcepp::parser::binary::makeFourCC("VBSP");
constexpr auto LZMA_VALVE_SIGNATURE = sourcepp::parser::binary::makeFourCC("LZMA");

enum class BSPLump : int32_t {
UNKNOWN = -1,
Expand Down Expand Up @@ -250,10 +249,6 @@ class BSP {

[[nodiscard]] std::vector<BSPGameLump> parseGameLumps(bool decompress) const;

[[nodiscard]] static std::optional<std::vector<std::byte>> compressLumpData(const std::span<const std::byte> data, uint8_t compressLevel = 6);

[[nodiscard]] static std::optional<std::vector<std::byte>> decompressLumpData(const std::span<const std::byte> data);

std::string path;
Header header{};

Expand Down
14 changes: 14 additions & 0 deletions include/sourcepp/compression/LZMA.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#pragma once

#include <cstddef>
#include <optional>
#include <span>
#include <vector>

namespace sourcepp::compression {

[[nodiscard]] std::optional<std::vector<std::byte>> compressValveLZMA(std::span<const std::byte> data, uint8_t compressLevel = 6);

[[nodiscard]] std::optional<std::vector<std::byte>> decompressValveLZMA(std::span<const std::byte> data);

} // namespace sourcepp::compression
105 changes: 6 additions & 99 deletions src/bsppp/BSP.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#include <bsppp/BSP.h>

#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <type_traits>
#include <utility>

#include <FileStream.h>
#include <lzma.h>
#include <sourcepp/compression/LZMA.h>

using namespace bsppp;
using namespace sourcepp;
Expand Down Expand Up @@ -152,7 +151,7 @@ std::optional<std::vector<std::byte>> BSP::getLumpData(BSPLump lumpIndex, bool n
}

if (!noDecompression && this->isLumpCompressed(lumpIndex)) {
return decompressLumpData(lumpBytes);
return compression::decompressValveLZMA(lumpBytes);
}
return lumpBytes;
}
Expand All @@ -173,7 +172,7 @@ bool BSP::setLump(BSPLump lumpIndex, uint32_t version, std::span<const std::byte

const auto lump = static_cast<std::underlying_type_t<BSPLump>>(lumpIndex);
if (compressLevel > 0) {
auto compressedData = compressLumpData(data, compressLevel);
auto compressedData = compression::compressValveLZMA(data, compressLevel);
if (!compressedData) {
return false;
}
Expand Down Expand Up @@ -226,7 +225,7 @@ std::optional<std::vector<std::byte>> BSP::getGameLumpData(BSPGameLump::Signatur
for (const auto& gameLump : this->stagedGameLumps) {
if (gameLump.signature == signature) {
if (gameLump.isCompressed) {
return decompressLumpData(gameLump.data);
return compression::decompressValveLZMA(gameLump.data);
}
return gameLump.data;
}
Expand All @@ -244,7 +243,7 @@ bool BSP::setGameLump(BSPGameLump::Signature signature, uint16_t version, std::s
};

if (compressLevel) {
const auto compressedData = compressLumpData(data, compressLevel);
const auto compressedData = compression::compressValveLZMA(data, compressLevel);
if (!compressedData) {
return false;
}
Expand Down Expand Up @@ -723,7 +722,7 @@ std::vector<BSPGameLump> BSP::parseGameLumps(bool decompress) const {
if (!decompress) {
lumps[i].data = stream.read_bytes(nextOffset - lumps[i].offset);
} else {
auto uncompressedData = decompressLumpData(stream.read_bytes(nextOffset - lumps[i].offset));
auto uncompressedData = compression::decompressValveLZMA(stream.read_bytes(nextOffset - lumps[i].offset));
if (uncompressedData) {
lumps[i].data = *uncompressedData;
}
Expand All @@ -736,95 +735,3 @@ std::vector<BSPGameLump> BSP::parseGameLumps(bool decompress) const {
}
return lumps;
}

std::optional<std::vector<std::byte>> BSP::compressLumpData(std::span<const std::byte> data, uint8_t compressLevel) {
// Preallocate extra 4 bytes for Valve LZMA header signature
std::vector<std::byte> compressedData(sizeof(uint32_t));
std::array<std::byte, 8192> compressedChunk{};

lzma_stream stream{
.next_in = reinterpret_cast<const uint8_t*>(data.data()),
.avail_in = data.size(),
.next_out = reinterpret_cast<uint8_t*>(compressedChunk.data()),
.avail_out = compressedChunk.size(),
};

lzma_options_lzma options{};
lzma_lzma_preset(&options, std::clamp<uint8_t>(compressLevel, 0, 9));

if (lzma_alone_encoder(&stream, &options) != LZMA_OK) {
lzma_end(&stream);
return std::nullopt;
}

lzma_ret ret;
do {
stream.next_out = reinterpret_cast<uint8_t*>(compressedChunk.data());
stream.avail_out = compressedChunk.size();

ret = lzma_code(&stream, LZMA_RUN);
compressedData.insert(compressedData.end(), compressedChunk.begin(), compressedChunk.begin() + compressedChunk.size() - stream.avail_out);
} while (ret == LZMA_OK);

if (auto code = lzma_code(&stream, LZMA_FINISH); code != LZMA_OK && code != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
lzma_end(&stream);

{
// Switch out normal header with Valve one
BufferStream compressedStream{compressedData};
compressedStream << LZMA_VALVE_SIGNATURE;
const auto properties = compressedStream.read<std::array<uint8_t, 5>>();
compressedStream
.seek_u(sizeof(uint32_t))
.write<uint32_t>(data.size())
.write(compressedData.size() - (sizeof(uint32_t) * 3) + (sizeof(uint8_t) * 5))
.write(properties);
}
return compressedData;
}

std::optional<std::vector<std::byte>> BSP::decompressLumpData(std::span<const std::byte> data) {
std::vector<std::byte> compressedData{data.begin(), data.end()};
std::vector<std::byte> uncompressedData;
{
// Switch out Valve header with normal one
BufferStreamReadOnly in{data.data(), data.size()};
if (in.read<uint32_t>() != LZMA_VALVE_SIGNATURE) {
return std::nullopt;
}
const auto uncompressedLength = in.read<uint32_t>();
in.skip<uint32_t>();
const auto properties = in.read<std::array<uint8_t, 5>>();
BufferStream out{compressedData};
out
.write(properties)
.write<uint64_t>(uncompressedLength);
uncompressedData.resize(uncompressedLength);
}

lzma_stream stream{
.next_in = reinterpret_cast<const uint8_t*>(compressedData.data()),
.avail_in = compressedData.size(),
.next_out = reinterpret_cast<uint8_t*>(uncompressedData.data()),
.avail_out = uncompressedData.size(),
};

if (lzma_alone_decoder(&stream, UINT64_MAX) != LZMA_OK) {
lzma_end(&stream);
return std::nullopt;
}
if (auto ret = lzma_code(&stream, LZMA_RUN); ret != LZMA_OK && ret != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
if (auto ret = lzma_code(&stream, LZMA_FINISH); ret != LZMA_OK && ret != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
lzma_end(&stream);

return uncompressedData;
}
4 changes: 1 addition & 3 deletions src/bsppp/_bsppp.cmake
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
add_pretty_parser(bsppp
DEPS liblzma MINIZIP::minizip sourcepp_parser
DEPS MINIZIP::minizip sourcepp_compression sourcepp_parser
DEPS_PUBLIC sourcepp::vpkpp
PRECOMPILED_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/bsppp/BSP.h"
Expand All @@ -11,5 +11,3 @@ add_pretty_parser(bsppp
"${CMAKE_CURRENT_LIST_DIR}/BSP.cpp"
"${CMAKE_CURRENT_LIST_DIR}/EntityLump.cpp"
"${CMAKE_CURRENT_LIST_DIR}/PakLump.cpp")

target_include_directories(bsppp PRIVATE "${xz_SOURCE_DIR}/src/liblzma/api")
105 changes: 105 additions & 0 deletions src/sourcepp/compression/LZMA.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#include <sourcepp/compression/LZMA.h>

#include <BufferStream.h>
#include <lzma.h>
#include <sourcepp/parser/Binary.h>

using namespace sourcepp;

namespace {

constexpr auto LZMA_VALVE_SIGNATURE = parser::binary::makeFourCC("LZMA");

} // namespace

std::optional<std::vector<std::byte>> compression::compressValveLZMA(std::span<const std::byte> data, uint8_t compressLevel) {
// Preallocate extra 4 bytes for Valve LZMA header signature
std::vector<std::byte> compressedData(sizeof(uint32_t));
std::array<std::byte, 8192> compressedChunk{};

lzma_stream stream{
.next_in = reinterpret_cast<const uint8_t*>(data.data()),
.avail_in = data.size(),
.next_out = reinterpret_cast<uint8_t*>(compressedChunk.data()),
.avail_out = compressedChunk.size(),
};

lzma_options_lzma options{};
lzma_lzma_preset(&options, std::clamp<uint8_t>(compressLevel, 0, 9));

if (lzma_alone_encoder(&stream, &options) != LZMA_OK) {
lzma_end(&stream);
return std::nullopt;
}

lzma_ret ret;
do {
stream.next_out = reinterpret_cast<uint8_t*>(compressedChunk.data());
stream.avail_out = compressedChunk.size();

ret = lzma_code(&stream, LZMA_RUN);
compressedData.insert(compressedData.end(), compressedChunk.begin(), compressedChunk.begin() + compressedChunk.size() - static_cast<std::ptrdiff_t>(stream.avail_out));
} while (ret == LZMA_OK);

if (auto code = lzma_code(&stream, LZMA_FINISH); code != LZMA_OK && code != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
lzma_end(&stream);

{
// Switch out normal header with Valve one
BufferStream compressedStream{compressedData};
compressedStream << LZMA_VALVE_SIGNATURE;
const auto properties = compressedStream.read<std::array<uint8_t, 5>>();
compressedStream
.seek_u(sizeof(uint32_t))
.write<uint32_t>(data.size())
.write(compressedData.size() - (sizeof(uint32_t) * 3) + (sizeof(uint8_t) * 5))
.write(properties);
}
return compressedData;
}

std::optional<std::vector<std::byte>> compression::decompressValveLZMA(std::span<const std::byte> data) {
std::vector<std::byte> compressedData{data.begin(), data.end()};
std::vector<std::byte> uncompressedData;
{
// Switch out Valve header with normal one
BufferStreamReadOnly in{data.data(), data.size()};
if (in.read<uint32_t>() != LZMA_VALVE_SIGNATURE) {
return std::nullopt;
}
const auto uncompressedLength = in.read<uint32_t>();
in.skip<uint32_t>();
const auto properties = in.read<std::array<uint8_t, 5>>();
BufferStream out{compressedData};
out
.write(properties)
.write<uint64_t>(uncompressedLength);
uncompressedData.resize(uncompressedLength);
}

lzma_stream stream{
.next_in = reinterpret_cast<const uint8_t*>(compressedData.data()),
.avail_in = compressedData.size(),
.next_out = reinterpret_cast<uint8_t*>(uncompressedData.data()),
.avail_out = uncompressedData.size(),
};

if (lzma_alone_decoder(&stream, UINT64_MAX) != LZMA_OK) {
lzma_end(&stream);
return std::nullopt;
}
if (auto ret = lzma_code(&stream, LZMA_RUN); ret != LZMA_OK && ret != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
if (auto ret = lzma_code(&stream, LZMA_FINISH); ret != LZMA_OK && ret != LZMA_STREAM_END) {
lzma_end(&stream);
return std::nullopt;
}
lzma_end(&stream);

return uncompressedData;
}
13 changes: 13 additions & 0 deletions src/sourcepp/compression/_compression.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
list(APPEND ${PROJECT_NAME}_compression_HEADERS
"${CMAKE_CURRENT_SOURCE_DIR}/include/sourcepp/compression/LZMA.h")

add_library(${PROJECT_NAME}_compression STATIC
${${PROJECT_NAME}_compression_HEADERS}
"${CMAKE_CURRENT_LIST_DIR}/LZMA.cpp")

target_precompile_headers(${PROJECT_NAME}_compression PUBLIC ${${PROJECT_NAME}_compression_HEADERS})

target_link_libraries(${PROJECT_NAME}_compression PUBLIC ${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME}_compression PRIVATE liblzma ${PROJECT_NAME}_parser)

target_include_directories(${PROJECT_NAME}_compression PRIVATE "${xz_SOURCE_DIR}/src/liblzma/api")

0 comments on commit 26199b9

Please sign in to comment.