You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
duckstation/src/util/zip_helpers.cpp

451 lines
13 KiB
C++

// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "zip_helpers.h"
#include "dyn_libzip.h"
#include "common/error.h"
#include "common/file_system.h"
#include "common/log.h"
#include "common/progress_callback.h"
#include <fmt/format.h>
LOG_CHANNEL(Ungrouped);
void ZipHelpers::ZipFileDeleter::operator()(zip_file_t* zf)
{
if (!zf)
return;
g_dyn_libzip.zip_fclose(zf);
}
void ZipHelpers::ZipDeleter::operator()(zip_t* zf)
{
if (!zf)
return;
const int err = g_dyn_libzip.zip_close(zf);
if (err != 0)
{
ERROR_LOG("Failed to close zip file: {}", err);
g_dyn_libzip.zip_discard(zf);
}
}
void ZipHelpers::SetErrorObject(Error* error, std::string_view msg, zip_error_t* ze, bool finalize /*= true*/)
{
Error::SetStringFmt(error, "{}{}", msg, ze ? g_dyn_libzip.zip_error_strerror(ze) : "UNKNOWN");
if (finalize && ze)
g_dyn_libzip.zip_error_fini(ze);
}
ZipHelpers::ManagedZipT ZipHelpers::OpenManagedZipFile(const char* filename, int flags, Error* error /*= nullptr*/)
{
zip_error_t ze;
zip_source_t* zs = g_dyn_libzip.zip_source_file_create(filename, 0, 0, &ze);
zip_t* zip;
if (!zs)
{
SetErrorObject(error, "zip_source_file_create() failed: ", &ze);
zip = nullptr;
}
else
{
if (!(zip = g_dyn_libzip.zip_open_from_source(zs, flags, &ze)))
{
// have to clean up source
SetErrorObject(error, "zip_open_from_source() failed: {}", &ze);
g_dyn_libzip.zip_source_free(zs);
}
}
return ManagedZipT(zip);
}
namespace ZipHelpers {
static zip_int64_t CFileSourceCallback(void* userdata, void* data, zip_uint64_t len, zip_source_cmd_t cmd)
{
#define LOCAL_ZIP_SOURCE_GET_ARGS(type, data, len, error) \
((len) < sizeof(type) ? g_dyn_libzip.zip_error_set((error), ZIP_ER_INVAL, 0), (type*)NULL : (type*)(data))
std::FILE* fp = static_cast<std::FILE*>(userdata);
switch (cmd)
{
case ZIP_SOURCE_OPEN:
{
// file already open
return 0;
}
case ZIP_SOURCE_READ:
{
return std::fread(data, 1, static_cast<size_t>(len), fp);
}
case ZIP_SOURCE_CLOSE:
{
// file closing is caller's responsibility
return 0;
}
case ZIP_SOURCE_STAT:
{
FILESYSTEM_STAT_DATA st;
zip_stat_t* zst = LOCAL_ZIP_SOURCE_GET_ARGS(zip_stat_t, data, len, nullptr);
if (!zst || !FileSystem::StatFile(fp, &st))
return -1;
zst->size = st.Size;
zst->mtime = st.ModificationTime;
zst->valid = ZIP_STAT_SIZE | ZIP_STAT_MTIME;
return 0;
}
case ZIP_SOURCE_SEEK:
{
const zip_source_args_seek_t* args = LOCAL_ZIP_SOURCE_GET_ARGS(zip_source_args_seek_t, data, len, nullptr);
if (!args)
return -1;
return FileSystem::FSeek64(fp, args->offset, args->whence);
}
case ZIP_SOURCE_TELL:
{
return FileSystem::FTell64(fp);
}
case ZIP_SOURCE_FREE:
{
return 0;
}
case ZIP_SOURCE_SUPPORTS:
{
// we support all commands used by libzip for reading
return ZIP_SOURCE_SUPPORTS_SEEKABLE;
}
default:
return -1;
}
#undef LOCAL_ZIP_SOURCE_GET_ARGS
}
} // namespace ZipHelpers
ZipHelpers::ManagedZipT ZipHelpers::OpenManagedZipCFile(std::FILE* fp, int flags, Error* error /*= nullptr*/)
{
zip_error_t ze;
zip_t* zip;
zip_source_t* zs = g_dyn_libzip.zip_source_function_create(&CFileSourceCallback, fp, &ze);
if (!zs)
{
SetErrorObject(error, "zip_source_filep_create() failed: ", &ze);
std::fclose(fp);
zip = nullptr;
}
else
{
if (!(zip = g_dyn_libzip.zip_open_from_source(zs, flags, &ze)))
{
// have to clean up source
SetErrorObject(error, "zip_open_from_source() failed: {}", &ze);
g_dyn_libzip.zip_source_free(zs);
}
}
return ManagedZipT(zip);
}
ZipHelpers::ManagedZipT ZipHelpers::OpenManagedZipBuffer(const void* buffer, size_t size, int flags, bool free_buffer,
Error* error /*= nullptr*/)
{
zip_error_t ze;
zip_source_t* zs = g_dyn_libzip.zip_source_buffer_create(buffer, size, free_buffer, &ze);
zip_t* zip;
if (!zs)
{
SetErrorObject(error, "zip_source_buffer_create() failed: ", &ze);
if (free_buffer)
std::free(const_cast<void*>(buffer));
zip = nullptr;
}
else
{
if (!(zip = g_dyn_libzip.zip_open_from_source(zs, flags, &ze)))
{
// have to clean up source
SetErrorObject(error, "zip_open_from_source() failed: {}", &ze);
g_dyn_libzip.zip_source_free(zs);
}
}
return ManagedZipT(zip);
}
std::vector<std::string> ZipHelpers::ReadFileListInZip(zip_t* zip)
{
std::vector<std::string> ret;
zip_int64_t num_entries = g_dyn_libzip.zip_get_num_entries(zip, 0);
if constexpr (sizeof(size_t) < sizeof(zip_int64_t))
num_entries = std::min(num_entries, static_cast<zip_int64_t>(std::numeric_limits<size_t>::max()));
if (num_entries <= 0)
return ret;
ret.reserve(static_cast<size_t>(num_entries));
for (zip_uint64_t i = 0; i < static_cast<zip_uint64_t>(num_entries); i++)
{
const char* name = g_dyn_libzip.zip_get_name(zip, i, ZIP_FL_ENC_GUESS);
if (name)
ret.emplace_back(name);
}
return ret;
}
ZipHelpers::ManagedZipFileT ZipHelpers::OpenManagedFileInZip(zip_t* zip, const char* filename, u32 flags,
Error* error /*= nullptr*/)
{
zip_file_t* zf = g_dyn_libzip.zip_fopen(zip, filename, flags);
if (!zf)
SetErrorObject(error, "zip_fopen() failed: ", g_dyn_libzip.zip_get_error(zip), false);
return ManagedZipFileT(zf);
}
ZipHelpers::ManagedZipFileT ZipHelpers::OpenManagedFileIndexInZip(zip_t* zip, u64 index, u32 flags,
Error* error /*= nullptr*/)
{
zip_file_t* zf = g_dyn_libzip.zip_fopen_index(zip, index, flags);
if (!zf)
SetErrorObject(error, "zip_fopen_index() failed: ", g_dyn_libzip.zip_get_error(zip), false);
return ManagedZipFileT(zf);
}
std::optional<u64> ZipHelpers::GetFileSizeInZip(zip_t* zip, const char* name, bool case_sensitive /*= true*/,
Error* error /*= nullptr*/)
{
zip_stat_t st;
if (g_dyn_libzip.zip_stat(zip, name, 0, &st) != 0)
{
SetErrorObject(error, "zip_stat() failed: ", g_dyn_libzip.zip_get_error(zip));
return std::nullopt;
}
if (!(st.valid & ZIP_STAT_SIZE) || st.size < 0)
{
Error::SetStringView(error, "zip_stat() did not return valid size for file");
return std::nullopt;
}
return static_cast<u64>(st.size);
}
bool ZipHelpers::ExtractFileToDisk(zip_t* zip, const char* name, std::string disk_name, bool case_sensitive /*= true*/,
u32 chunk_size /*= DEFAULT_EXTRACT_CHUNK_SIZE*/,
ProgressCallback* progress /*= nullptr*/, Error* error /*= nullptr*/)
{
std::unique_ptr<u8[]> chunk = std::make_unique_for_overwrite<u8[]>(chunk_size);
return ExtractFileToDisk(zip, name, std::move(disk_name), std::span<u8>(chunk.get(), chunk_size), case_sensitive,
progress, error);
}
bool ZipHelpers::ExtractFileToDisk(zip_t* zip, const char* name, std::string disk_name, std::span<u8> chunk_buffer,
bool case_sensitive /*= true */, ProgressCallback* progress /*= nullptr*/,
Error* error /*= nullptr*/)
{
auto fp = FileSystem::CreateAtomicRenamedFile(std::move(disk_name), error);
if (!fp)
return false;
if (!ExtractFileToDisk(zip, name, fp.get(), chunk_buffer, case_sensitive, progress, error))
{
FileSystem::DiscardAtomicRenamedFile(fp);
return false;
}
return FileSystem::CommitAtomicRenamedFile(fp, error);
}
bool ZipHelpers::ExtractFileToDisk(zip_t* zip, const char* name, std::FILE* fp, std::span<u8> chunk_buffer,
bool case_sensitive /* = true */, ProgressCallback* progress /* = nullptr */,
Error* error /* = nullptr */)
{
const int flags = case_sensitive ? 0 : ZIP_FL_NOCASE;
const zip_int64_t file_index = g_dyn_libzip.zip_name_locate(zip, name, flags);
if (file_index < 0)
{
SetErrorObject(error, "zip_name_locate() failed: ", g_dyn_libzip.zip_get_error(zip), false);
return false;
}
zip_file_t* zf = g_dyn_libzip.zip_fopen_index(zip, file_index, flags);
if (!zf)
{
SetErrorObject(error, "zip_fopen_index() failed: ", g_dyn_libzip.zip_get_error(zip), false);
return false;
}
const u64 chunk_size = static_cast<u64>(chunk_buffer.size());
bool update_progress = (progress != nullptr);
if (update_progress)
{
zip_stat_t zst;
update_progress =
(g_dyn_libzip.zip_stat_index(zip, file_index, flags, &zst) == 0 && (zst.valid & ZIP_STAT_SIZE) && zst.size > 0);
if (update_progress)
{
progress->PushState();
progress->SetState(0, static_cast<u32>((zst.size + chunk_size - 1) / chunk_size));
}
}
for (;;)
{
const s64 read = g_dyn_libzip.zip_fread(zf, chunk_buffer.data(), chunk_size);
if (read < 0)
{
// read error
SetErrorObject(error, "zip_fread() failed: ", g_dyn_libzip.zip_get_error(zip), false);
g_dyn_libzip.zip_fclose(zf);
if (update_progress)
progress->PopState();
break;
}
if (std::fwrite(chunk_buffer.data(), static_cast<size_t>(read), 1, fp) != 1)
{
// write error
Error::SetErrno(error, "fwrite() failed: ", errno);
g_dyn_libzip.zip_fclose(zf);
if (update_progress)
progress->PopState();
break;
}
if (update_progress)
progress->IncrementProgressValue();
// if less than chunk size, we're EOF
if (read != static_cast<s64>(chunk_size))
break;
}
if (update_progress)
progress->PopState();
g_dyn_libzip.zip_fclose(zf);
if (std::fflush(fp) != 0)
{
Error::SetErrno(error, "fflush() failed: ", errno);
return false;
}
return true;
}
namespace ZipHelpers {
template<typename T>
static std::optional<T> ReadFileInZipToContainer(zip_t* zip, const char* name, bool case_sensitive = true,
Error* error = nullptr)
{
const int flags = case_sensitive ? 0 : ZIP_FL_NOCASE;
std::optional<T> ret;
const zip_int64_t file_index = g_dyn_libzip.zip_name_locate(zip, name, flags);
if (file_index >= 0)
{
zip_stat_t zst;
if (g_dyn_libzip.zip_stat_index(zip, file_index, flags, &zst) == 0)
{
zip_file_t* zf = g_dyn_libzip.zip_fopen_index(zip, file_index, flags);
if (zf)
{
ret = T();
ret->resize(static_cast<size_t>(zst.size));
if (g_dyn_libzip.zip_fread(zf, ret->data(), ret->size()) != static_cast<zip_int64_t>(ret->size()))
{
SetErrorObject(error, "zip_fread() failed: ", g_dyn_libzip.zip_get_error(zip), false);
ret.reset();
}
g_dyn_libzip.zip_fclose(zf);
}
}
else
{
SetErrorObject(error, "zip_stat_index() failed: ", g_dyn_libzip.zip_get_error(zip), false);
}
}
else
{
SetErrorObject(error, "zip_name_locate() failed: ", g_dyn_libzip.zip_get_error(zip), false);
}
return ret;
}
template<typename T>
static std::optional<T> ReadFileInZipToContainer(zip_file_t* file, u32 chunk_size = 4096, Error* error = nullptr)
{
std::optional<T> ret = T();
for (;;)
{
const size_t pos = ret->size();
ret->resize(pos + chunk_size);
const s64 read = g_dyn_libzip.zip_fread(file, ret->data() + pos, chunk_size);
if (read < 0)
{
// read error
Error::SetStringView(error, "zip_fread() failed");
break;
}
// if less than chunk size, we're EOF
if (read != static_cast<s64>(chunk_size))
{
ret->resize(pos + static_cast<size_t>(read));
break;
}
}
return ret;
}
} // namespace ZipHelpers
std::optional<std::string> ZipHelpers::ReadFileInZipToString(zip_t* zip, const char* name,
bool case_sensitive /*= true*/, Error* error /*= nullptr*/)
{
return ReadFileInZipToContainer<std::string>(zip, name, case_sensitive, error);
}
std::optional<std::vector<u8>> ZipHelpers::ReadBinaryFileInZip(zip_t* zip, const char* name,
bool case_sensitive /*= true*/,
Error* error /*= nullptr*/)
{
return ReadFileInZipToContainer<std::vector<u8>>(zip, name, case_sensitive, error);
}
std::optional<std::vector<u8>> ZipHelpers::ReadBinaryFileInZip(zip_file_t* file,
u32 chunk_size /*= DEFAULT_READ_CHUNK_SIZE*/,
Error* error /*= nullptr*/)
{
return ReadFileInZipToContainer<std::vector<u8>>(file, chunk_size, error);
}
std::optional<std::string> ZipHelpers::ReadFileInZipToString(zip_file_t* file,
u32 chunk_size /*= DEFAULT_READ_CHUNK_SIZE*/,
Error* error /*= nullptr*/)
{
return ReadFileInZipToContainer<std::string>(file, chunk_size, error);
}