Add ZIP64 support using libzip

Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
This commit is contained in:
Ralph Sennhauser 2025-09-30 20:19:08 +02:00
parent 50e1f51755
commit 02504863e8
No known key found for this signature in database
16 changed files with 290 additions and 1428 deletions

1
.gitignore vendored
View file

@ -17,6 +17,7 @@ libraries/win64
libraries/source/cpp-httplib/*
libraries/source/cxxtest-4.4/*
libraries/source/fcollada/*
libraries/source/libzip/*
libraries/source/nvtt/*
libraries/source/premake-core/*
libraries/source/spidermonkey/*

View file

@ -68,6 +68,10 @@ if os.istarget("macosx") then
pkgconfig.set_static_link_libs(true)
end
if not _OPTIONS["with-system-libzip"] then
pkgconfig.add_pkg_config_path(libraries_source_dir .. "libzip/lib/pkgconfig/")
end
local function add_delayload(name, suffix, def)
if def["no_delayload"] then
@ -532,6 +536,25 @@ extern_lib_defs = {
end
end,
},
libzip = {
compile_settings = function()
if os.istarget("windows") then
add_default_include_paths("libzip")
else
pkgconfig.add_includes("libzip")
end
end,
link_settings = function()
if os.istarget("windows") then
add_default_lib_paths("libzip")
add_default_links({
win_names = { "zip" },
})
else
pkgconfig.add_links("libzip")
end
end,
},
miniupnpc = {
compile_settings = function()
if os.istarget("windows") then

View file

@ -52,6 +52,7 @@ newoption { category = "Pyrogenesis", trigger = "with-system-cpp-httplib", descr
newoption { category = "Pyrogenesis", trigger = "with-system-cxxtest", description = "Search standard paths for cxxtest, instead of using bundled copy" }
newoption { category = "Pyrogenesis", trigger = "with-lto", description = "Enable Link Time Optimization (LTO)" }
newoption { category = "Pyrogenesis", trigger = "with-system-mozjs", description = "Search standard paths for libmozjs128, instead of using bundled copy" }
newoption { category = "Pyrogenesis", trigger = "with-system-libzip", description = "Search standard paths for libzip, instead of using bundled copy" }
newoption { category = "Pyrogenesis", trigger = "with-system-nvtt", description = "Search standard paths for nvidia-texture-tools library, instead of using bundled copy" }
newoption { category = "Pyrogenesis", trigger = "with-valgrind", description = "Enable Valgrind support (non-Windows only)" }
newoption { category = "Pyrogenesis", trigger = "without-audio", description = "Disable use of OpenAL/Ogg/Vorbis APIs" }
@ -994,6 +995,7 @@ function setup_all_libs ()
"valgrind",
"cxxtest",
"fmt",
"libzip",
}
-- CPU architecture-specific
@ -1090,6 +1092,7 @@ used_extern_libs = {
"libpng",
"zlib",
"libzip",
"spidermonkey",
"libxml2",

View file

@ -1248,6 +1248,11 @@ export ARCH CXXFLAGS CFLAGS LDFLAGS CMAKE_FLAGS JOBS
# shellcheck disable=SC2086
./../source/fcollada/build.sh $build_sh_options || die "FCollada build failed"
# --------------------------------------------------------------
# shellcheck disable=SC2086
./../source/libzip/build.sh $build_sh_options || die "libzip build failed"
cp ./../source/libzip/lib/pkgconfig/* "$PC_PATH"
# --------------------------------------------------------------
# shellcheck disable=SC2086
./../source/nvtt/build.sh $build_sh_options || die "NVTT build failed"

View file

@ -33,6 +33,7 @@ options:
--force-rebuild - rebuild all
--without-nvtt - don't build nvtt
--with-system-cxxtest - don't build cxxtest
--with-system-libzip - don't build libzip
--with-system-nvtt - don't build nvtt
--with-system-mozjs - don't build spidermonkey
--with-system-premake - don't build premake
@ -44,6 +45,7 @@ EOF
without_nvtt=false
with_system_cpp_httplib=false
with_system_cxxtest=false
with_system_libzip=false
with_system_nvtt=false
with_system_mozjs=false
with_system_premake=false
@ -62,6 +64,7 @@ while [ "$#" -gt 0 ]; do
--without-nvtt) without_nvtt=true ;;
--with-system-cpp-httplib) with_system_cpp_httplib=true ;;
--with-system-cxxtest) with_system_cxxtest=true ;;
--with-system-libzip) with_system_libzip=true ;;
--with-system-nvtt) with_system_nvtt=true ;;
--with-system-mozjs) with_system_mozjs=true ;;
--with-system-premake) with_system_premake=true ;;
@ -102,6 +105,11 @@ if [ "$with_system_cxxtest" = "false" ]; then
fi
# shellcheck disable=SC2086
./source/fcollada/build.sh $build_sh_options || die "FCollada build failed"
if [ "$with_system_libzip" = "false" ]; then
# shellcheck disable=SC2086
./source/libzip/build.sh $build_sh_options || die "libzip build failed"
cp source/libzip/lib/*so* ../binaries/system/
fi
if [ "$with_system_nvtt" = "false" ] && [ "$without_nvtt" = "false" ]; then
# shellcheck disable=SC2086
./source/nvtt/build.sh $build_sh_options || die "NVTT build failed"

View file

@ -22,7 +22,7 @@ rem **Copy dependencies' binaries to binaries/system/**
rem static libs: boost fmt
rem wxwidgets isn't provided and needs to be built manually
set DIR_LIST=cpp-httplib enet fcollada freetype gloox iconv icu libcurl libpng libsodium libxml2 microsoft miniupnpc nvtt openal sdl2 spidermonkey vorbis zlib
set DIR_LIST=cpp-httplib enet fcollada freetype gloox iconv icu libcurl libpng libsodium libxml2 libzip microsoft miniupnpc nvtt openal sdl2 spidermonkey vorbis zlib
for %%d in (%DIR_LIST%) do (
copy /y %LIBS_PATH%\%%d\bin\* ..\binaries\system\ || exit /b 1
)

View file

@ -0,0 +1,80 @@
#!/bin/sh
set -e
: "${OS:=$(uname -s)}"
: "${TAR:=tar}"
cd "$(dirname "$0")"
PV=1.11.4
LIB_VERSION=${PV}+wfg0
fetch()
{
curl -fLo "libzip-${PV}.tar.xz" \
"https://libzip.org/download/libzip-${PV}.tar.xz"
}
echo "Building libzip..."
while [ "$#" -gt 0 ]; do
case "$1" in
--fetch-only)
fetch
exit
;;
--force-rebuild) rm -f .already-built ;;
*)
echo "Unknown option: $1"
exit 1
;;
esac
shift
done
if [ -e .already-built ] && [ "$(cat .already-built || true)" = "${LIB_VERSION}" ]; then
echo "Skipping - already built (use --force-rebuild to override)"
exit
fi
# fetch
if [ ! -e "libzip-${PV}.tar.xz" ]; then
fetch
fi
# unpack
rm -Rf "libzip-${PV}"
"${TAR}" xf "libzip-${PV}.tar.xz"
# configure
rm -rf build
shared_libs=ON
if [ "${OS}" = "Darwin" ]; then
shared_libs=OFF
fi
cmake -B build -S "libzip-${PV}" \
-DCMAKE_INSTALL_PREFIX="$(realpath . || true)" \
-DCMAKE_INSTALL_LIBDIR=lib \
-DBUILD_SHARED_LIBS=${shared_libs} \
-DENABLE_COMMONCRYPTO=OFF \
-DENABLE_GNUTLS=OFF \
-DENABLE_MBEDTLS=OFF \
-DENABLE_OPENSSL=OFF \
-DENABLE_WINDOWS_CRYPTO=OFF \
-DENABLE_BZIP2=OFF \
-DENABLE_LZMA=OFF \
-DENABLE_ZSTD=OFF \
-DENABLE_FDOPEN=OFF \
-DBUILD_TOOLS=OFF \
-DBUILD_REGRESS=OFF \
-DBUILD_OSSFUZZ=OFF \
-DBUILD_EXAMPLES=OFF \
-DBUILD_DOC=OFF
# build
cmake --build build
# install
rm -Rf bin include lib
cmake --install build
echo "${LIB_VERSION}" >.already-built

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -28,296 +28,20 @@
#include "archive_zip.h"
#include "lib/alignment.h"
#include "lib/allocators/dynarray.h"
#include "lib/allocators/pool.h"
#include "lib/bits.h"
#include "lib/byte_order.h"
#include "lib/code_annotation.h"
#include "lib/debug.h"
#include "lib/file/archive/archive.h"
#include "lib/file/archive/codec.h"
#include "lib/file/archive/codec_zlib.h"
#include "lib/file/archive/stream.h"
#include "lib/file/file.h"
#include "lib/file/file_system.h"
#include "lib/file/io/io.h"
#include "lib/lib.h"
#include "lib/path.h"
#include "lib/posix/posix_types.h"
#include "lib/os_path.h"
#include "lib/status.h"
#include "lib/types.h"
#include "lib/utf8.h"
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <ctime>
#include <exception>
#include <fcntl.h>
#include <memory>
#include <string>
#include <cinttypes>
#include <zip.h>
//-----------------------------------------------------------------------------
// timestamp conversion: DOS FAT <-> Unix time_t
//-----------------------------------------------------------------------------
static time_t time_t_from_FAT(u32 fat_timedate)
{
const u32 fat_time = bits(fat_timedate, 0, 15);
const u32 fat_date = bits(fat_timedate, 16, 31);
struct tm t; // struct tm format:
t.tm_sec = bits(fat_time, 0,4) * 2; // [0,59]
t.tm_min = bits(fat_time, 5,10); // [0,59]
t.tm_hour = bits(fat_time, 11,15); // [0,23]
t.tm_mday = bits(fat_date, 0,4); // [1,31]
t.tm_mon = bits(fat_date, 5,8) - 1; // [0,11]
t.tm_year = bits(fat_date, 9,15) + 80; // since 1900
t.tm_isdst = -1; // unknown - let libc determine
// otherwise: totally bogus, and at the limit of 32-bit time_t
ENSURE(t.tm_year < 138);
time_t ret = mktime(&t);
ENSURE(ret != (time_t)-1); // mktime shouldn't fail
return ret;
}
static u32 FAT_from_time_t(time_t time)
{
// (values are adjusted for DST)
struct tm* t = localtime(&time);
const u16 fat_time = u16(
(t->tm_sec/2) | // 5
(u16(t->tm_min) << 5) | // 6
(u16(t->tm_hour) << 11) // 5
);
const u16 fat_date = u16(
(t->tm_mday) | // 5
(u16(t->tm_mon+1) << 5) | // 4
(u16(t->tm_year-80) << 9) // 7
);
u32 fat_timedate = u32_from_u16(fat_date, fat_time);
return fat_timedate;
}
//-----------------------------------------------------------------------------
// Zip archive definitions
//-----------------------------------------------------------------------------
static const u32 cdfh_magic = FOURCC_LE('P','K','\1','\2');
static const u32 lfh_magic = FOURCC_LE('P','K','\3','\4');
static const u32 ecdr_magic = FOURCC_LE('P','K','\5','\6');
enum ZipMethod
{
ZIP_METHOD_NONE = 0,
ZIP_METHOD_DEFLATE = 8
};
#pragma pack(push, 1)
class LFH
class ArchiveFile_Zip : public IArchiveFile
{
public:
void Init(const CFileInfo& fileInfo, off_t csize, ZipMethod method, u32 checksum, const Path& pathname)
{
const std::string pathnameUTF8 = utf8_from_wstring(pathname.string());
const size_t pathnameSize = pathnameUTF8.length();
m_magic = lfh_magic;
m_x1 = to_le16(0);
m_flags = to_le16(0);
m_method = to_le16(u16_from_larger(method));
m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime()));
m_crc = to_le32(checksum);
m_csize = to_le32(u32_from_larger(csize));
m_usize = to_le32(u32_from_larger(fileInfo.Size()));
m_fn_len = to_le16(u16_from_larger(pathnameSize));
m_e_len = to_le16(0);
memcpy((char*)this + sizeof(LFH), pathnameUTF8.c_str(), pathnameSize);
}
size_t Size() const
{
ENSURE(m_magic == lfh_magic);
size_t size = sizeof(LFH);
size += read_le16(&m_fn_len);
size += read_le16(&m_e_len);
// note: LFH doesn't have a comment field!
return size;
}
private:
u32 m_magic;
u16 m_x1; // version needed
u16 m_flags;
u16 m_method;
u32 m_fat_mtime; // last modified time (DOS FAT format)
u32 m_crc;
u32 m_csize;
u32 m_usize;
u16 m_fn_len;
u16 m_e_len;
};
cassert(sizeof(LFH) == 30);
class CDFH
{
public:
void Init(const CFileInfo& fileInfo, off_t ofs, off_t csize, ZipMethod method, u32 checksum, const Path& pathname, size_t slack)
{
const std::string pathnameUTF8 = utf8_from_wstring(pathname.string());
const size_t pathnameLength = pathnameUTF8.length();
m_magic = cdfh_magic;
m_x1 = to_le32(0);
m_flags = to_le16(0);
m_method = to_le16(u16_from_larger(method));
m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime()));
m_crc = to_le32(checksum);
m_csize = to_le32(u32_from_larger(csize));
m_usize = to_le32(u32_from_larger(fileInfo.Size()));
m_fn_len = to_le16(u16_from_larger(pathnameLength));
m_e_len = to_le16(0);
m_c_len = to_le16(u16_from_larger((size_t)slack));
m_x2 = to_le32(0);
m_x3 = to_le32(0);
m_lfh_ofs = to_le32(u32_from_larger(ofs));
memcpy((char*)this + sizeof(CDFH), pathnameUTF8.c_str(), pathnameLength);
}
Path Pathname() const
{
const size_t length = (size_t)read_le16(&m_fn_len);
const char* pathname = (const char*)this + sizeof(CDFH); // not 0-terminated!
return Path(std::string(pathname, length));
}
off_t HeaderOffset() const
{
return read_le32(&m_lfh_ofs);
}
off_t USize() const
{
return (off_t)read_le32(&m_usize);
}
off_t CSize() const
{
return (off_t)read_le32(&m_csize);
}
ZipMethod Method() const
{
return (ZipMethod)read_le16(&m_method);
}
u32 Checksum() const
{
return read_le32(&m_crc);
}
time_t MTime() const
{
const u32 fat_mtime = read_le32(&m_fat_mtime);
return time_t_from_FAT(fat_mtime);
}
size_t Size() const
{
size_t size = sizeof(CDFH);
size += read_le16(&m_fn_len);
size += read_le16(&m_e_len);
size += read_le16(&m_c_len);
return size;
}
private:
u32 m_magic;
u32 m_x1; // versions
u16 m_flags;
u16 m_method;
u32 m_fat_mtime; // last modified time (DOS FAT format)
u32 m_crc;
u32 m_csize;
u32 m_usize;
u16 m_fn_len;
u16 m_e_len;
u16 m_c_len;
u32 m_x2; // spanning
u32 m_x3; // attributes
u32 m_lfh_ofs;
};
cassert(sizeof(CDFH) == 46);
class ECDR
{
public:
void Init(size_t cd_numEntries, off_t cd_ofs, size_t cd_size)
{
m_magic = ecdr_magic;
m_diskNum = to_le16(0);
m_cd_diskNum = to_le16(0);
m_cd_numEntriesOnDisk = to_le16(u16_from_larger(cd_numEntries));
m_cd_numEntries = m_cd_numEntriesOnDisk;
m_cd_size = to_le32(u32_from_larger(cd_size));
m_cd_ofs = to_le32(u32_from_larger(cd_ofs));
m_comment_len = to_le16(0);
}
void Decompose(size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size) const
{
cd_numEntries = (size_t)read_le16(&m_cd_numEntries);
cd_ofs = (off_t)read_le32(&m_cd_ofs);
cd_size = (size_t)read_le32(&m_cd_size);
}
off_t GetCommentLength() const
{
return static_cast<off_t>(read_le16(&m_comment_len));
}
private:
u32 m_magic;
u16 m_diskNum;
u16 m_cd_diskNum;
u16 m_cd_numEntriesOnDisk;
u16 m_cd_numEntries;
u32 m_cd_size;
u32 m_cd_ofs;
u16 m_comment_len;
};
cassert(sizeof(ECDR) == 22);
#pragma pack(pop)
//-----------------------------------------------------------------------------
// ArchiveFile_Zip
//-----------------------------------------------------------------------------
class ArchiveFile_Zip final : public IArchiveFile
{
public:
ArchiveFile_Zip(const PFile& file, off_t ofs, off_t csize, u32 checksum, ZipMethod method)
: m_file(file), m_ofs(ofs)
, m_csize(csize), m_checksum(checksum), m_method((u16)method)
, m_flags(NeedsFixup)
ArchiveFile_Zip(std::shared_ptr<zip_t> zip, zip_uint64_t index, OsPath path)
: m_Zip(std::move(zip)), m_Index(index), m_Path(path)
{
}
@ -333,286 +57,179 @@ public:
const OsPath& Path() const override
{
return m_file->Pathname();
return m_Path;
}
Status Load(const OsPath& /*name*/, const std::shared_ptr<u8>& buf, size_t size) const override
{
AdjustOffset();
PICodec codec;
switch(m_method)
zip_file_t* zipFile = zip_fopen_index(m_Zip.get(), m_Index, 0);
if (zipFile == nullptr)
{
case ZIP_METHOD_NONE:
codec = CreateCodec_ZLibNone();
break;
case ZIP_METHOD_DEFLATE:
codec = CreateDecompressor_ZLibDeflate();
break;
default:
WARN_RETURN(ERR::ARCHIVE_UNKNOWN_METHOD);
debug_printf("Failed to open file at zip index '%" PRIu64 "'\n", m_Index);
return ERR::FAIL;
}
Stream stream(codec);
stream.SetOutputBuffer(buf.get(), size);
io::Operation op(*m_file.get(), 0, m_csize, m_ofs);
StreamFeeder streamFeeder(stream);
RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
RETURN_STATUS_IF_ERR(stream.Finish());
#if CODEC_COMPUTE_CHECKSUM
ENSURE(m_checksum == stream.Checksum());
#endif
if (zip_fread(zipFile, buf.get(), size) < 0)
{
debug_printf("Failed to read file at zip index '%" PRIu64 "'\n", m_Index);
zip_fclose(zipFile);
return ERR::FAIL;
}
if (zip_fclose(zipFile) != 0)
{
debug_printf("Failed to close file at zip index '%" PRIu64 "'\n", m_Index);
return ERR::FAIL;
}
return INFO::OK;
}
private:
enum Flags
{
// indicates m_ofs points to a "local file header" instead of
// the file data. a fixup routine is called when reading the file;
// it skips past the LFH and clears this flag.
// this is somewhat of a hack, but vital to archive open performance.
// without it, we'd have to scan through the entire archive file,
// which can take *seconds*.
// (we cannot use the information in CDFH, because its 'extra' field
// has been observed to differ from that of the LFH)
// since we read the LFH right before the rest of the file, the block
// cache will absorb the IO cost.
NeedsFixup = 1
};
struct LFH_Copier
{
LFH_Copier(u8* lfh_dst, size_t lfh_bytes_remaining)
: lfh_dst(lfh_dst), lfh_bytes_remaining(lfh_bytes_remaining)
{
}
// this code grabs an LFH struct from file block(s) that are
// passed to the callback. usually, one call copies the whole thing,
// but the LFH may straddle a block boundary.
//
// rationale: this allows using temp buffers for zip_fixup_lfh,
// which avoids involving the file buffer manager and thus
// avoids cluttering the trace and cache contents.
Status operator()(const u8* block, size_t size) const
{
ENSURE(size <= lfh_bytes_remaining);
memcpy(lfh_dst, block, size);
lfh_dst += size;
lfh_bytes_remaining -= size;
return INFO::OK;
}
mutable u8* lfh_dst;
mutable size_t lfh_bytes_remaining;
};
/**
* fix up m_ofs (adjust it to point to cdata instead of the LFH).
*
* note: we cannot use CDFH filename and extra field lengths to skip
* past LFH since that may not mirror CDFH (has happened).
*
* this is called at file-open time instead of while mounting to
* reduce seeks: since reading the file will typically follow, the
* block cache entirely absorbs the IO cost.
**/
void AdjustOffset() const
{
if(!(m_flags & NeedsFixup))
return;
m_flags &= ~NeedsFixup;
// performance note: this ends up reading one file block, which is
// only in the block cache if the file starts in the same block as a
// previously read file (i.e. both are small).
LFH lfh;
io::Operation op(*m_file.get(), 0, sizeof(LFH), m_ofs);
if(io::Run(op, io::Parameters(), LFH_Copier((u8*)&lfh, sizeof(LFH))) == INFO::OK)
m_ofs += (off_t)lfh.Size();
}
PFile m_file;
// all relevant LFH/CDFH fields not covered by CFileInfo
mutable off_t m_ofs;
off_t m_csize;
u32 m_checksum;
u16 m_method;
mutable u16 m_flags;
std::shared_ptr<zip_t> m_Zip;
zip_uint64_t m_Index;
OsPath m_Path;
};
//-----------------------------------------------------------------------------
// ArchiveReader_Zip
//-----------------------------------------------------------------------------
struct ZipArchiveDeleter
{
void operator()(zip_t* zip) const noexcept
{
if (zip_close(zip) < 0)
{
debug_printf("archive-deleter: cannot close archive : %s\n", zip_strerror(zip));
zip_discard(zip);
}
}
};
class ArchiveReader_Zip : public IArchiveReader
{
public:
ArchiveReader_Zip(const OsPath& pathname)
: m_file(new File(pathname, O_RDONLY))
ArchiveReader_Zip(const OsPath& archivePath)
{
CFileInfo fileInfo;
GetFileInfo(pathname, &fileInfo);
m_fileSize = fileInfo.Size();
const size_t minFileSize = sizeof(LFH)+sizeof(CDFH)+sizeof(ECDR);
ENSURE(m_fileSize >= off_t(minFileSize));
int err;
zip_t* zip = zip_open(archivePath.string8().c_str(), 0, &err);
if (zip == nullptr)
{
zip_error_t error;
zip_error_init_with_code(&error, err);
std::runtime_error exception{"archive-reader: cannot open input archive " + archivePath.string8() + ": " + zip_error_strerror(&error)};
zip_error_fini(&error);
throw exception;
}
m_Zip = std::shared_ptr<zip_t>(zip, ZipArchiveDeleter());
}
virtual Status ReadEntries(ArchiveEntryCallback cb, uintptr_t cbData)
Status ReadEntries(ArchiveEntryCallback cb, uintptr_t cbData) override
{
// locate and read Central Directory
off_t cd_ofs = 0;
size_t cd_numEntries = 0;
size_t cd_size = 0;
RETURN_STATUS_IF_ERR(LocateCentralDirectory(m_file, m_fileSize, cd_ofs, cd_numEntries, cd_size));
io::BufferPtr buf(io::Allocate(cd_size));
Status returnStatus = INFO::OK;
io::Operation op(*m_file.get(), buf.get(), cd_size, cd_ofs);
RETURN_STATUS_IF_ERR(io::Run(op));
// iterate over Central Directory
const u8* pos = buf.get();
for(size_t i = 0; i < cd_numEntries; i++)
const zip_int64_t numEntries{zip_get_num_entries(m_Zip.get(), 0)};
if (numEntries < 0)
{
// scan for next CDFH
CDFH* cdfh = (CDFH*)FindRecord(buf.get(), cd_size, pos, cdfh_magic, sizeof(CDFH));
if(!cdfh)
WARN_RETURN(ERR::CORRUPTED);
debug_printf("Can't get entries count for null zip");
}
const Path relativePathname(cdfh->Pathname());
const zip_uint64_t indices{static_cast<std::make_unsigned_t<zip_int64_t>>(numEntries)};
for (zip_uint64_t index = 0; index < indices; ++index)
{
zip_stat_t zipStat;
if (zip_stat_index(m_Zip.get(), index, 0, &zipStat) < 0)
{
debug_printf("Can't get zip stat for index '%" PRIu64 "'", index);
returnStatus = ERR::FAIL;
continue;
}
const Path relativePathname(zipStat.name);
if(!relativePathname.IsDirectory())
{
const OsPath name = relativePathname.Filename();
CFileInfo fileInfo(name, cdfh->USize(), cdfh->MTime());
std::shared_ptr<ArchiveFile_Zip> archiveFile = std::make_shared<ArchiveFile_Zip>(m_file, cdfh->HeaderOffset(), cdfh->CSize(), cdfh->Checksum(), cdfh->Method());
CFileInfo fileInfo(name, zipStat.size, zipStat.mtime);
std::shared_ptr<ArchiveFile_Zip> archiveFile = std::make_shared<ArchiveFile_Zip>(m_Zip, index, name);
cb(relativePathname, fileInfo, archiveFile, cbData);
}
pos += cdfh->Size();
}
return INFO::OK;
return returnStatus;
}
private:
/**
* Scan buffer for a Zip file record.
*
* @param buf
* @param size
* @param start position within buffer
* @param magic signature of record
* @param recordSize size of record (including signature)
* @return pointer to record within buffer or 0 if not found.
**/
static const u8* FindRecord(const u8* buf, size_t size, const u8* start, u32 magic, size_t recordSize)
{
// (don't use <start> as the counter - otherwise we can't tell if
// scanning within the buffer was necessary.)
for(const u8* p = start; p <= buf+size-recordSize; p++)
{
// found it
if(*(u32*)p == magic)
{
ENSURE(p == start); // otherwise, the archive is a bit broken
return p;
}
}
std::shared_ptr<zip_t> m_Zip;
};
// passed EOF, didn't find it.
// note: do not warn - this happens in the initial ECDR search at
// EOF if the archive contains a comment field.
return 0;
class ArchiveWriter_Zip : public IArchiveWriter
{
public:
ArchiveWriter_Zip(const OsPath& archivePath, bool noDeflate)
: m_NoDeflate(noDeflate)
{
int err;
zip_t* zip = zip_open(archivePath.string8().c_str(), ZIP_CREATE | ZIP_EXCL, &err);
if (zip == nullptr)
{
zip_error_t error;
zip_error_init_with_code(&error, err);
std::runtime_error exception{"archive-writer: cannot open output archive " + archivePath.string8() + ": " + zip_error_strerror(&error)};
zip_error_fini(&error);
throw exception;
}
m_Zip = std::unique_ptr<zip_t, ZipArchiveDeleter>(zip);
}
// search for ECDR in the last <maxScanSize> bytes of the file.
// if found, fill <dst_ecdr> with a copy of the (little-endian) ECDR and
// return INFO::OK, otherwise IO error or ERR::CORRUPTED.
static Status ScanForEcdr(const PFile& file, off_t fileSize, u8* buf, size_t maxScanSize, size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size)
Status AddFile(const OsPath& pathname, const OsPath& pathnameInArchive) override
{
// don't scan more than the entire file
const size_t scanSize = std::min(maxScanSize, size_t(fileSize));
// read desired chunk of file into memory
const off_t ofs = fileSize - off_t(scanSize);
io::Operation op(*file.get(), buf, scanSize, ofs);
RETURN_STATUS_IF_ERR(io::Run(op));
// Scanning for ECDR first assumes no comment exists
// (standard case), so ECDR structure exists right at
// end of file
off_t offsetInBlock = scanSize - sizeof(ECDR);
const ECDR* ecdr = nullptr;
for (off_t commentSize = 0; commentSize <= offsetInBlock && !ecdr; ++commentSize)
zip_error_t error;
zip_source_t* zipSource = zip_source_file_create(pathname.string8().c_str(), 0, ZIP_LENGTH_TO_END, &error);
if (zipSource == nullptr)
{
const u8 *pECDRTest = buf + offsetInBlock - commentSize;
if (*reinterpret_cast<const u32*>(pECDRTest) == ecdr_magic)
{
// Signature matches, test whether comment
// fills up the whole space following the
// ECDR
ecdr = reinterpret_cast<const ECDR*>(pECDRTest);
if (commentSize != ecdr->GetCommentLength())
{
// Signature matches but there is some other data between
// header, comment and EOF. There are three possibilities
// for this:
// 1) Header file format and size differ from what we expect
// 2) File has been truncated
// 3) The magic id occurs inside a zip comment
ecdr = nullptr;
}
else
{
// Seems like a valid archive header before an archive-level
// comment
break;
}
}
debug_printf("Can't open zip source file '%s' : %s\n", pathname.string8().c_str(), zip_error_strerror(&error));
zip_error_fini(&error);
return ERR::FAIL;
}
return AddZipSource(zipSource, pathnameInArchive);
}
if(!ecdr)
return INFO::CANNOT_HANDLE;
Status AddMemory(const u8* data, size_t size, time_t /*mtime*/, const OsPath& pathnameInArchive) override
{
zip_error_t error;
zip_source_t* zipSource = zip_source_buffer_create(data, size, 0, &error);
if (zipSource == nullptr)
{
debug_printf("Can't open zip source buffer : %s\n", zip_error_strerror(&error));
zip_error_fini(&error);
return ERR::FAIL;
}
return AddZipSource(zipSource, pathnameInArchive);
}
ecdr->Decompose(cd_numEntries, cd_ofs, cd_size);
private:
std::unique_ptr<zip_t, ZipArchiveDeleter> m_Zip;
bool m_NoDeflate;
Status AddZipSource(zip_source_t* zipSource, const OsPath& pathnameInArchive)
{
const zip_int64_t index{zip_file_add(m_Zip.get(), pathnameInArchive.string8().c_str(), zipSource, ZIP_FL_ENC_UTF_8)};
if (index < 0)
{
debug_printf("Failed to add zip source as '%s'\n", pathnameInArchive.string8().c_str());
zip_source_free(zipSource);
return ERR::FAIL;
}
const zip_int32_t comp{m_NoDeflate ? ZIP_CM_STORE : ZIP_CM_DEFLATE};
if (zip_set_file_compression(m_Zip.get(), index, comp, 0) < 0)
{
debug_printf("Failed to set compression for '%s'\n", pathnameInArchive.string8().c_str());
return ERR::FAIL;
}
return INFO::OK;
}
static Status LocateCentralDirectory(const PFile& file, off_t fileSize, off_t& cd_ofs, size_t& cd_numEntries, size_t& cd_size)
{
const size_t maxScanSize = 66000u; // see below
io::BufferPtr buf(io::Allocate(maxScanSize));
Status ret = ScanForEcdr(file, fileSize, static_cast<u8*>(buf.get()), maxScanSize, cd_numEntries, cd_ofs, cd_size);
if(ret == INFO::OK)
return INFO::OK;
io::Operation op(*file.get(), buf.get(), sizeof(LFH));
RETURN_STATUS_IF_ERR(io::Run(op));
// the Zip file has an LFH but lacks an ECDR. this can happen if
// the user hard-exits while an archive is being written.
// notes:
// - return ERR::CORRUPTED so VFS will not include this file.
// - we could work around this by scanning all LFHs, but won't bother
// because it'd be slow.
// - do not warn - the corrupt archive will be deleted on next
// successful archive builder run anyway.
if(FindRecord(buf.get(), sizeof(LFH), buf.get(), lfh_magic, sizeof(LFH)))
return ERR::CORRUPTED; // NOWARN
// totally bogus
else
WARN_RETURN(ERR::ARCHIVE_UNKNOWN_FORMAT);
}
PFile m_file;
off_t m_fileSize;
};
PIArchiveReader CreateArchiveReader_Zip(const OsPath& archivePathname)
{
try
@ -625,170 +242,6 @@ PIArchiveReader CreateArchiveReader_Zip(const OsPath& archivePathname)
}
}
//-----------------------------------------------------------------------------
// ArchiveWriter_Zip
//-----------------------------------------------------------------------------
class ArchiveWriter_Zip : public IArchiveWriter
{
public:
ArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate)
: m_file(new File(archivePathname, O_WRONLY)), m_fileSize(0)
, m_numEntries(0), m_noDeflate(noDeflate)
{
THROW_STATUS_IF_ERR(pool_create(&m_cdfhPool, 10*MiB, 0));
}
~ArchiveWriter_Zip()
{
// append an ECDR to the CDFH list (this allows us to
// write out both to the archive file in one burst)
const size_t cd_size = m_cdfhPool.da.pos;
ECDR* ecdr = (ECDR*)pool_alloc(&m_cdfhPool, sizeof(ECDR));
if(!ecdr)
std::terminate();
const off_t cd_ofs = m_fileSize;
ecdr->Init(m_numEntries, cd_ofs, cd_size);
if(write(m_file->Descriptor(), m_cdfhPool.da.base, cd_size+sizeof(ECDR)) < 0)
DEBUG_WARN_ERR(ERR::IO); // no way to return error code
(void)pool_destroy(&m_cdfhPool);
}
Status AddFile(const OsPath& pathname, const OsPath& pathnameInArchive)
{
CFileInfo fileInfo;
RETURN_STATUS_IF_ERR(GetFileInfo(pathname, &fileInfo));
PFile file(new File);
RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY));
return AddFileOrMemory(fileInfo, pathnameInArchive, file, NULL);
}
Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive)
{
CFileInfo fileInfo(pathnameInArchive, size, mtime);
return AddFileOrMemory(fileInfo, pathnameInArchive, PFile(), data);
}
Status AddFileOrMemory(const CFileInfo& fileInfo, const OsPath& pathnameInArchive, const PFile& file, const u8* data)
{
ENSURE((file && !data) || (data && !file));
const off_t usize = fileInfo.Size();
// skip 0-length files.
// rationale: zip.cpp needs to determine whether a CDFH entry is
// a file or directory (the latter are written by some programs but
// not needed - they'd only pollute the file table).
// it looks like checking for usize=csize=0 is the safest way -
// relying on file attributes (which are system-dependent!) is
// even less safe.
// we thus skip 0-length files to avoid confusing them with directories.
if(!usize)
return INFO::SKIPPED;
const size_t pathnameLength = pathnameInArchive.string().length();
// choose method and the corresponding codec
ZipMethod method;
PICodec codec;
if(m_noDeflate || IsFileTypeIncompressible(pathnameInArchive))
{
method = ZIP_METHOD_NONE;
codec = CreateCodec_ZLibNone();
}
else
{
method = ZIP_METHOD_DEFLATE;
codec = CreateCompressor_ZLibDeflate();
}
// allocate memory
const size_t csizeMax = codec->MaxOutputSize(size_t(usize));
io::BufferPtr buf(io::Allocate(sizeof(LFH) + pathnameLength + csizeMax));
// read and compress file contents
size_t csize; u32 checksum;
{
u8* cdata = buf.get() + sizeof(LFH) + pathnameLength;
Stream stream(codec);
stream.SetOutputBuffer(cdata, csizeMax);
StreamFeeder streamFeeder(stream);
if(file)
{
io::Operation op(*file.get(), 0, usize);
RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder));
}
else
{
RETURN_STATUS_IF_ERR(streamFeeder(data, usize));
}
RETURN_STATUS_IF_ERR(stream.Finish());
csize = stream.OutSize();
checksum = stream.Checksum();
}
// build LFH
{
LFH* lfh = reinterpret_cast<LFH*>(buf.get());
lfh->Init(fileInfo, (off_t)csize, method, checksum, pathnameInArchive);
}
// append a CDFH to the central directory (in memory)
const off_t ofs = m_fileSize;
const size_t prev_pos = m_cdfhPool.da.pos; // (required to determine padding size)
const size_t cdfhSize = sizeof(CDFH) + pathnameLength;
CDFH* cdfh = (CDFH*)pool_alloc(&m_cdfhPool, cdfhSize);
if(!cdfh)
WARN_RETURN(ERR::NO_MEM);
const size_t slack = m_cdfhPool.da.pos - prev_pos - cdfhSize;
cdfh->Init(fileInfo, ofs, (off_t)csize, method, checksum, pathnameInArchive, slack);
m_numEntries++;
// write LFH, pathname and cdata to file
const size_t packageSize = sizeof(LFH) + pathnameLength + csize;
if(write(m_file->Descriptor(), buf.get(), packageSize) < 0)
WARN_RETURN(ERR::IO);
m_fileSize += (off_t)packageSize;
return INFO::OK;
}
private:
static bool IsFileTypeIncompressible(const OsPath& pathname)
{
const OsPath extension = pathname.Extension();
// file extensions that we don't want to compress
static const wchar_t* incompressibleExtensions[] =
{
L".zip", L".rar",
L".jpg", L".jpeg", L".png",
L".ogg", L".mp3"
};
for(size_t i = 0; i < ARRAY_SIZE(incompressibleExtensions); i++)
{
if(extension == incompressibleExtensions[i])
return true;
}
return false;
}
PFile m_file;
off_t m_fileSize;
Pool m_cdfhPool;
size_t m_numEntries;
bool m_noDeflate;
};
PIArchiveWriter CreateArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate)
{
try

View file

@ -1,28 +0,0 @@
/* Copyright (C) 2010 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "lib/file/archive/codec.h"
ICodec::~ICodec()
{
}

View file

@ -1,100 +0,0 @@
/* Copyright (C) 2025 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* this layer allows for other compression methods/libraries
* besides ZLib. it also simplifies the interface for user code and
* does error checking, etc.
*/
#ifndef INCLUDED_CODEC
#define INCLUDED_CODEC
#include "lib/status.h"
#include "lib/types.h"
#include <cstddef>
#include <memory>
#define CODEC_COMPUTE_CHECKSUM 1
struct ICodec
{
public:
/**
* note: the implementation should not check whether any data remains -
* codecs are sometimes destroyed without completing a transfer.
**/
virtual ~ICodec();
/**
* @return an upper bound on the output size for the given amount of input.
* this is used when allocating a single buffer for the whole operation.
**/
virtual size_t MaxOutputSize(size_t inSize) const = 0;
/**
* clear all previous state and prepare for reuse.
*
* this is as if the object were destroyed and re-created, but more
* efficient since it avoids reallocating a considerable amount of
* memory (about 200KB for LZ).
**/
virtual Status Reset() = 0;
/**
* process (i.e. compress or decompress) data.
*
* @param in
* @param inSize
* @param out
* @param outSize Bytes remaining in the output buffer; shall not be zero.
* @param inConsumed,outProduced How many bytes in the input and
* output buffers were used. either or both of these can be zero if
* the input size is small or there's not enough output space.
**/
virtual Status Process(const u8* in, size_t inSize, u8* out, size_t outSize, size_t& inConsumed, size_t& outProduced) = 0;
/**
* Flush buffers and make sure all output has been produced.
*
* @param checksum Checksum over all input data.
* @param outProduced
* @return error status for the entire operation.
**/
virtual Status Finish(u32& checksum, size_t& outProduced) = 0;
/**
* update a checksum to reflect the contents of a buffer.
*
* @param checksum the initial value (must be 0 on first call)
* @param in
* @param inSize
* @return the new checksum. note: after all data has been seen, this is
* identical to the what Finish would return.
**/
virtual u32 UpdateChecksum(u32 checksum, const u8* in, size_t inSize) const = 0;
};
typedef std::shared_ptr<ICodec> PICodec;
#endif // #ifndef INCLUDED_CODEC

View file

@ -1,308 +0,0 @@
/* Copyright (C) 2025 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "codec_zlib.h"
#include "lib/alignment.h"
#include "lib/debug.h"
#include "lib/external_libraries/zlib.h"
#include "lib/file/archive/codec.h"
#include "lib/status.h"
#include "lib/types.h"
#include <algorithm>
#include <cstring>
class Codec_ZLib : public ICodec
{
public:
u32 UpdateChecksum([[maybe_unused]] u32 checksum, [[maybe_unused]] const u8* in,
[[maybe_unused]] size_t inSize) const
{
#if CODEC_COMPUTE_CHECKSUM
return (u32)crc32(checksum, in, (uInt)inSize);
#else
return 0;
#endif
}
protected:
u32 InitializeChecksum()
{
#if CODEC_COMPUTE_CHECKSUM
return crc32(0, 0, 0);
#else
return 0;
#endif
}
};
//-----------------------------------------------------------------------------
class Codec_ZLibNone : public Codec_ZLib
{
public:
Codec_ZLibNone()
{
Reset();
}
virtual ~Codec_ZLibNone()
{
}
virtual size_t MaxOutputSize(size_t inSize) const
{
return inSize;
}
virtual Status Reset()
{
m_checksum = InitializeChecksum();
return INFO::OK;
}
virtual Status Process(const u8* in, size_t inSize, u8* out, size_t outSize, size_t& inConsumed, size_t& outProduced)
{
const size_t transferSize = std::min(inSize, outSize);
memcpy(out, in, transferSize);
inConsumed = outProduced = transferSize;
m_checksum = UpdateChecksum(m_checksum, out, outProduced);
return INFO::OK;
}
virtual Status Finish(u32& checksum, size_t& outProduced)
{
outProduced = 0;
checksum = m_checksum;
return INFO::OK;
}
private:
u32 m_checksum;
};
//-----------------------------------------------------------------------------
class CodecZLibStream : public Codec_ZLib
{
protected:
CodecZLibStream()
{
memset(&m_zs, 0, sizeof(m_zs));
m_checksum = InitializeChecksum();
}
static Status LibError_from_zlib(int zlib_ret)
{
switch(zlib_ret)
{
case Z_OK:
return INFO::OK;
case Z_STREAM_END:
WARN_RETURN(ERR::FAIL);
case Z_MEM_ERROR:
WARN_RETURN(ERR::NO_MEM);
case Z_DATA_ERROR:
WARN_RETURN(ERR::CORRUPTED);
case Z_STREAM_ERROR:
WARN_RETURN(ERR::INVALID_PARAM);
default:
WARN_RETURN(ERR::FAIL);
}
}
static void WarnIfZLibError(int zlib_ret)
{
(void)LibError_from_zlib(zlib_ret);
}
typedef int ZEXPORT (*ZLibFunc)(z_streamp strm, int flush);
Status CallStreamFunc(ZLibFunc func, int flush, const u8* in, const size_t inSize, u8* out, const size_t outSize, size_t& inConsumed, size_t& outProduced)
{
m_zs.next_in = (Byte*)in;
m_zs.avail_in = (uInt)inSize;
m_zs.next_out = (Byte*)out;
m_zs.avail_out = (uInt)outSize;
int ret = func(&m_zs, flush);
// sanity check: if ZLib reports end of stream, all input data
// must have been consumed.
if(ret == Z_STREAM_END)
{
ENSURE(m_zs.avail_in == 0);
ret = Z_OK;
}
ENSURE(inSize >= m_zs.avail_in && outSize >= m_zs.avail_out);
inConsumed = inSize - m_zs.avail_in;
outProduced = outSize - m_zs.avail_out;
return LibError_from_zlib(ret);
}
mutable z_stream m_zs;
// note: z_stream does contain an 'adler' checksum field, but that's
// not updated in streams lacking a gzip header, so we'll have to
// calculate a checksum ourselves.
// adler32 is somewhat weaker than CRC32, but a more important argument
// is that we should use the latter for compatibility with Zip archives.
mutable u32 m_checksum;
};
//-----------------------------------------------------------------------------
class Compressor_ZLib : public CodecZLibStream
{
public:
Compressor_ZLib()
{
// note: with Z_BEST_COMPRESSION, 78% percent of
// archive builder CPU time is spent in ZLib, even though
// that is interleaved with IO; everything else is negligible.
// we prefer faster speed at the cost of 1.5% larger archives.
const int level = Z_BEST_SPEED;
const int windowBits = -MAX_WBITS; // max window size; omit ZLib header
const int memLevel = 9; // max speed; total mem ~= 384KiB
const int strategy = Z_DEFAULT_STRATEGY; // normal data - not RLE
const int ret = deflateInit2(&m_zs, level, Z_DEFLATED, windowBits, memLevel, strategy);
ENSURE(ret == Z_OK);
}
virtual ~Compressor_ZLib()
{
const int ret = deflateEnd(&m_zs);
WarnIfZLibError(ret);
}
virtual size_t MaxOutputSize(size_t inSize) const
{
return (size_t)deflateBound(&m_zs, (uLong)inSize);
}
virtual Status Reset()
{
m_checksum = InitializeChecksum();
const int ret = deflateReset(&m_zs);
return LibError_from_zlib(ret);
}
virtual Status Process(const u8* in, size_t inSize, u8* out, size_t outSize, size_t& inConsumed, size_t& outProduced)
{
m_checksum = UpdateChecksum(m_checksum, in, inSize);
return CodecZLibStream::CallStreamFunc(deflate, 0, in, inSize, out, outSize, inConsumed, outProduced);
}
virtual Status Finish(u32& checksum, size_t& outProduced)
{
const uInt availOut = m_zs.avail_out;
// notify zlib that no more data is forthcoming and have it flush output.
// our output buffer has enough space due to use of deflateBound;
// therefore, deflate must return Z_STREAM_END.
const int ret = deflate(&m_zs, Z_FINISH);
ENSURE(ret == Z_STREAM_END);
outProduced = size_t(availOut - m_zs.avail_out);
checksum = m_checksum;
return INFO::OK;
}
};
//-----------------------------------------------------------------------------
class Decompressor_ZLib : public CodecZLibStream
{
public:
Decompressor_ZLib()
{
const int windowBits = -MAX_WBITS; // max window size; omit ZLib header
const int ret = inflateInit2(&m_zs, windowBits);
ENSURE(ret == Z_OK);
}
virtual ~Decompressor_ZLib()
{
const int ret = inflateEnd(&m_zs);
WarnIfZLibError(ret);
}
virtual size_t MaxOutputSize(size_t inSize) const
{
// relying on an upper bound for the output is a really bad idea for
// large files. archive formats store the uncompressed file sizes,
// so callers should use that when allocating the output buffer.
ENSURE(inSize < 1*MiB);
return inSize*1032; // see http://www.zlib.org/zlib_tech.html
}
virtual Status Reset()
{
m_checksum = InitializeChecksum();
const int ret = inflateReset(&m_zs);
return LibError_from_zlib(ret);
}
virtual Status Process(const u8* in, size_t inSize, u8* out, size_t outSize, size_t& inConsumed, size_t& outProduced)
{
const Status ret = CodecZLibStream::CallStreamFunc(inflate, Z_SYNC_FLUSH, in, inSize, out, outSize, inConsumed, outProduced);
m_checksum = UpdateChecksum(m_checksum, out, outProduced);
return ret;
}
virtual Status Finish(u32& checksum, size_t& outProduced)
{
// no action needed - decompression always flushes immediately.
outProduced = 0;
checksum = m_checksum;
return INFO::OK;
}
};
//-----------------------------------------------------------------------------
PICodec CreateCodec_ZLibNone()
{
return PICodec(new Codec_ZLibNone);
}
PICodec CreateCompressor_ZLibDeflate()
{
return PICodec(new Compressor_ZLib);
}
PICodec CreateDecompressor_ZLibDeflate()
{
return PICodec (new Decompressor_ZLib);
}

View file

@ -1,32 +0,0 @@
/* Copyright (C) 2010 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef INCLUDED_CODEC_ZLIB
#define INCLUDED_CODEC_ZLIB
#include "lib/file/archive/codec.h"
extern PICodec CreateCodec_ZLibNone();
extern PICodec CreateCompressor_ZLibDeflate();
extern PICodec CreateDecompressor_ZLibDeflate();
#endif // INCLUDED_CODEC_ZLIB

View file

@ -1,139 +0,0 @@
/* Copyright (C) 2025 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "precompiled.h"
#include "stream.h"
#include "lib/allocators/shared_ptr.h"
#include "lib/debug.h"
#include "lib/file/archive/codec.h"
//#include "lib/timer.h"
//TIMER_ADD_CLIENT(tc_stream);
OutputBufferManager::OutputBufferManager()
{
Reset();
}
void OutputBufferManager::Reset()
{
m_buffer = 0;
m_size = 0;
m_capacity = 0;
}
void OutputBufferManager::SetBuffer(u8* buffer, size_t size)
{
ENSURE(IsAllowableBuffer(buffer, size));
m_buffer = buffer;
m_size = size;
}
void OutputBufferManager::AllocateBuffer(size_t size)
{
// notes:
// - this implementation allows reusing previous buffers if they
// are big enough, which reduces the number of allocations.
// - no further attempts to reduce allocations (e.g. by doubling
// the current size) are made; this strategy is enough.
// - Pool etc. cannot be used because files may be huge (larger
// than the address space of 32-bit systems).
// no buffer or the previous one wasn't big enough: reallocate
if(!m_mem || m_capacity < size)
{
AllocateAligned(m_mem, size);
m_capacity = size;
}
SetBuffer(m_mem.get(), size);
}
bool OutputBufferManager::IsAllowableBuffer(u8* buffer, size_t size)
{
// none yet established
if(m_buffer == 0 && m_size == 0)
return true;
// same as last time (happens with temp buffers)
if(m_buffer == buffer && m_size == size)
return true;
// located after the last buffer (note: not necessarily after
// the entire buffer; a lack of input can cause the output buffer
// to only partially be used before the next call.)
if((unsigned)(buffer - m_buffer) <= m_size)
return true;
return false;
}
//-----------------------------------------------------------------------------
Stream::Stream(const PICodec& codec)
: m_codec(codec)
, m_inConsumed(0), m_outProduced(0)
{
}
void Stream::AllocateOutputBuffer(size_t outSizeMax)
{
m_outputBufferManager.AllocateBuffer(outSizeMax);
}
void Stream::SetOutputBuffer(u8* out, size_t outSize)
{
m_outputBufferManager.SetBuffer(out, outSize);
}
Status Stream::Feed(const u8* in, size_t inSize)
{
if(m_outProduced == m_outputBufferManager.Size()) // output buffer full; must not call Process
return INFO::ALL_COMPLETE;
size_t inConsumed, outProduced;
u8* const out = m_outputBufferManager.Buffer() + m_outProduced;
const size_t outSize = m_outputBufferManager.Size() - m_outProduced;
RETURN_STATUS_IF_ERR(m_codec->Process(in, inSize, out, outSize, inConsumed, outProduced));
m_inConsumed += inConsumed;
m_outProduced += outProduced;
return INFO::OK;
}
Status Stream::Finish()
{
size_t outProduced;
RETURN_STATUS_IF_ERR(m_codec->Finish(m_checksum, outProduced));
m_outProduced += outProduced;
return INFO::OK;
}

View file

@ -1,139 +0,0 @@
/* Copyright (C) 2025 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* output buffer and 'stream' layered on top of a compression codec
*/
#ifndef INCLUDED_STREAM
#define INCLUDED_STREAM
#include "lib/code_annotation.h"
#include "lib/file/archive/codec.h"
#include "lib/status.h"
#include "lib/types.h"
#include <cstddef>
#include <memory>
// note: this is similar in function to std::vector, but we don't need
// iterators etc. and would prefer to avoid initializing each byte.
class OutputBufferManager
{
public:
OutputBufferManager();
void Reset();
void SetBuffer(u8* buffer, size_t size);
/**
* allocate a new output buffer.
*
* @param size [bytes] to allocate.
*
* notes:
* - if a buffer had previously been allocated and is large enough,
* it is reused (this reduces the number of allocations).
* - this class manages the lifetime of the buffer.
**/
void AllocateBuffer(size_t size);
u8* Buffer() const
{
return m_buffer;
}
size_t Size() const
{
return m_size;
}
private:
bool IsAllowableBuffer(u8* buffer, size_t size);
u8* m_buffer;
size_t m_size;
std::shared_ptr<u8> m_mem;
// size of m_mem. allows reusing previously allocated buffers
// (user-specified buffers can't be reused because we have no control
// over their lifetime)
size_t m_capacity;
};
class Stream
{
public:
Stream(const PICodec& codec);
void SetOutputBuffer(u8* out, size_t outSize);
void AllocateOutputBuffer(size_t outSizeMax);
/**
* 'feed' the codec with a data block.
**/
Status Feed(const u8* in, size_t inSize);
Status Finish();
size_t OutSize() const
{
return m_outProduced;
}
u32 Checksum() const
{
return m_checksum;
}
private:
PICodec m_codec;
OutputBufferManager m_outputBufferManager;
size_t m_inConsumed;
size_t m_outProduced;
u32 m_checksum;
};
// avoids the need for std::bind (not supported on all compilers) and boost::bind (can't be
// used at work)
struct StreamFeeder
{
NONCOPYABLE(StreamFeeder);
public:
StreamFeeder(Stream& stream)
: stream(stream)
{
}
Status operator()(const u8* data, size_t size) const
{
return stream.Feed(data, size);
}
private:
Stream& stream;
};
#endif // #ifndef INCLUDED_STREAM

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
@ -33,6 +33,7 @@
#include <cstdint>
#include <iterator>
#include <memory>
#include <string>
namespace
@ -108,10 +109,44 @@ public:
TS_ASSERT_EQUALS("buildzipwithcomment.sh", g_ResultBuffer);
}
void test_round_trip()
{
OsPath testDir = MOD_PATH / "file" / "archive";
OsPath testPath = testDir / "test.zip";
TS_ASSERT_EQUALS(INFO::OK, CreateDirectories(testDir, 0700, false));
{
u8 buf[5] = { 'h', 'e', 'l', 'l', 'o' };
PIArchiveWriter zWriter = CreateArchiveWriter_Zip(testPath, false);
zWriter->AddMemory(buf, std::size(buf), 0, "world/hello");
}
{
PIArchiveReader zReader = CreateArchiveReader_Zip(testPath);
TS_ASSERT_DIFFERS(nullptr, zReader);
TS_ASSERT_EQUALS(INFO::OK, zReader->ReadEntries(TestArchiveZip::ReadWorldCallback, 0));
TS_ASSERT_EQUALS("hello", g_ResultBuffer);
}
}
private:
static void ArchiveEntryCallback(const VfsPath& path, const CFileInfo&, PIArchiveFile,
uintptr_t /*cbData*/)
{
g_ResultBuffer = path.string8();
}
static void ReadWorldCallback(const VfsPath& path, const CFileInfo&, PIArchiveFile file,
uintptr_t /*cbData*/)
{
if (path.string8() == "world/hello")
{
std::shared_ptr<u8> buf = std::make_shared<u8>(5);
file->Load("", buf, 5);
g_ResultBuffer = std::string(buf.get(), buf.get() + 5);
}
}
};

View file

@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games.
/* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D.
*
* 0 A.D. is free software: you can redistribute it and/or modify