uce/src/lib/zip.cpp
2026-06-15 10:28:23 +00:00

395 lines
13 KiB
C++

#include "zip.h"
#include <cstring>
extern "C" {
#include "../3rdparty/miniz/miniz.c"
#include "../3rdparty/miniz/miniz_tdef.c"
#include "../3rdparty/miniz/miniz_tinfl.c"
#include "../3rdparty/miniz/miniz_zip.c"
}
String zip_error(String api, String detail)
{
return(api + "(): " + detail);
}
void archive_check_size(String api, String label, u64 size, String config_key, u64 fallback)
{
u64 limit = config_u64(config_key, fallback);
if(limit > 0 && size > limit)
throw std::runtime_error(zip_error(api, label + " exceeds configured limit"));
}
bool zip_entry_name_safe(String name)
{
if(name == "")
return(false);
if(name[0] == '/' || name[0] == '\\')
return(false);
if(name.find(":") != String::npos)
return(false);
for(char c : name)
{
if(c == '\0' || (unsigned char)c < 0x20)
return(false);
}
auto parts = split(replace(name, "\\", "/"), "/");
for(auto part : parts)
{
if(part == "..")
return(false);
}
return(true);
}
String zip_normalize_entry_name(String name)
{
name = replace(name, "\\", "/");
while(name.find("//") != String::npos)
name = replace(name, "//", "/");
return(name);
}
bool zip_ensure_parent_directory(String path)
{
String parent = dirname(path);
if(parent == "" || parent == "." || parent == "/")
return(true);
if(file_exists(parent))
return(true);
return(mkdir(parent));
}
void zip_add_file_info(DValue& files, mz_zip_archive* archive, mz_uint index)
{
mz_zip_archive_file_stat stat;
std::memset(&stat, 0, sizeof(stat));
if(!mz_zip_reader_file_stat(archive, index, &stat))
throw std::runtime_error(zip_error("zip_list", "could not read file metadata at index " + std::to_string((u64)index)));
DValue item;
item["name"] = String(stat.m_filename);
item["index"] = (f64)index;
item["size"] = (f64)stat.m_uncomp_size;
item["compressed_size"] = (f64)stat.m_comp_size;
item["is_directory"].set_bool(mz_zip_reader_is_file_a_directory(archive, index));
item["method"] = (f64)stat.m_method;
files.push(item);
}
DValue zip_list(String zip_file_name)
{
mz_zip_archive archive;
std::memset(&archive, 0, sizeof(archive));
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
throw std::runtime_error(zip_error("zip_list", "could not open " + zip_file_name));
DValue result;
try
{
mz_uint count = mz_zip_reader_get_num_files(&archive);
archive_check_size("zip_list", "entry count", count, "ARCHIVE_MAX_ZIP_ENTRIES", 4096);
result["file"] = zip_file_name;
result["count"] = (f64)count;
result["entries"].set_array();
for(mz_uint i = 0; i < count; i++)
zip_add_file_info(result["entries"], &archive, i);
mz_zip_reader_end(&archive);
return(result);
}
catch(...)
{
mz_zip_reader_end(&archive);
throw;
}
}
String zip_read(String zip_file_name, String entry_name)
{
entry_name = zip_normalize_entry_name(entry_name);
if(!zip_entry_name_safe(entry_name))
throw std::runtime_error(zip_error("zip_read", "unsafe or empty entry name '" + entry_name + "'"));
mz_zip_archive archive;
std::memset(&archive, 0, sizeof(archive));
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
throw std::runtime_error(zip_error("zip_read", "could not open " + zip_file_name));
int file_index = mz_zip_reader_locate_file(&archive, entry_name.c_str(), NULL, 0);
if(file_index < 0)
{
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
mz_zip_archive_file_stat stat;
std::memset(&stat, 0, sizeof(stat));
if(!mz_zip_reader_file_stat(&archive, (mz_uint)file_index, &stat))
{
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
archive_check_size("zip_read", "uncompressed entry", stat.m_uncomp_size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
size_t size = 0;
void* data = mz_zip_reader_extract_file_to_heap(&archive, entry_name.c_str(), &size, 0);
if(!data)
{
mz_zip_reader_end(&archive);
throw std::runtime_error(zip_error("zip_read", "entry not found or not readable: " + entry_name));
}
String result((char*)data, size);
mz_free(data);
mz_zip_reader_end(&archive);
return(result);
}
String zip_entry_content(DValue item)
{
DValue* content = item.key("content");
if(content)
return(content->to_string());
DValue* file_name = item.key("file");
if(file_name)
return(file_get_contents(file_name->to_string()));
return(item.to_string());
}
String zip_entry_name(String key, DValue item)
{
DValue* name = item.key("name");
if(name)
return(name->to_string());
return(key);
}
void zip_add_entry(mz_zip_archive* archive, String name, String content)
{
archive_check_size("zip_create", "entry content", content.size(), "ARCHIVE_MAX_INPUT_BYTES", 64 * 1024 * 1024);
name = zip_normalize_entry_name(name);
if(!zip_entry_name_safe(name))
throw std::runtime_error(zip_error("zip_create", "unsafe or empty entry name '" + name + "'"));
if(!mz_zip_writer_add_mem(archive, name.c_str(), content.data(), content.size(), MZ_BEST_COMPRESSION))
throw std::runtime_error(zip_error("zip_create", "could not add entry " + name));
}
bool zip_create(String zip_file_name, DValue entries)
{
if(!zip_ensure_parent_directory(zip_file_name))
throw std::runtime_error(zip_error("zip_create", "could not create parent directory for " + zip_file_name));
mz_zip_archive archive;
std::memset(&archive, 0, sizeof(archive));
if(!mz_zip_writer_init_file(&archive, zip_file_name.c_str(), 0))
throw std::runtime_error(zip_error("zip_create", "could not open " + zip_file_name + " for writing"));
try
{
u64 entry_count = 0;
entries.each([&](DValue item, String key)
{
entry_count++;
archive_check_size("zip_create", "entry count", entry_count, "ARCHIVE_MAX_ZIP_ENTRIES", 4096);
String name = zip_entry_name(key, item);
String content = zip_entry_content(item);
zip_add_entry(&archive, name, content);
});
if(!mz_zip_writer_finalize_archive(&archive))
throw std::runtime_error(zip_error("zip_create", "could not finalize " + zip_file_name));
mz_zip_writer_end(&archive);
return(true);
}
catch(...)
{
mz_zip_writer_end(&archive);
throw;
}
}
void gz_append_u32_le(String& out, u32 value)
{
out.push_back((char)(value & 0xff));
out.push_back((char)((value >> 8) & 0xff));
out.push_back((char)((value >> 16) & 0xff));
out.push_back((char)((value >> 24) & 0xff));
}
u32 gz_read_u32_le(String src, size_t offset)
{
return(
((u32)(u8)src[offset]) |
((u32)(u8)src[offset + 1] << 8) |
((u32)(u8)src[offset + 2] << 16) |
((u32)(u8)src[offset + 3] << 24)
);
}
void gz_skip_zero_terminated(String compressed, size_t& offset)
{
while(offset < compressed.size() && compressed[offset] != '\0')
offset++;
if(offset >= compressed.size())
throw std::runtime_error("gz_uncompress(): truncated gzip header");
offset++;
}
size_t gz_deflate_offset(String compressed)
{
if(compressed.size() < 18)
throw std::runtime_error("gz_uncompress(): compressed data is too short");
if((u8)compressed[0] != 0x1f || (u8)compressed[1] != 0x8b)
throw std::runtime_error("gz_uncompress(): missing gzip header");
if((u8)compressed[2] != 8)
throw std::runtime_error("gz_uncompress(): unsupported gzip compression method");
u8 flags = (u8)compressed[3];
if(flags & 0xe0)
throw std::runtime_error("gz_uncompress(): gzip header uses reserved flags");
size_t offset = 10;
if(flags & 0x04)
{
if(offset + 2 > compressed.size())
throw std::runtime_error("gz_uncompress(): truncated gzip extra header");
u16 extra_len = ((u16)(u8)compressed[offset]) | ((u16)(u8)compressed[offset + 1] << 8);
offset += 2 + extra_len;
if(offset > compressed.size())
throw std::runtime_error("gz_uncompress(): truncated gzip extra data");
}
if(flags & 0x08)
gz_skip_zero_terminated(compressed, offset);
if(flags & 0x10)
gz_skip_zero_terminated(compressed, offset);
if(flags & 0x02)
{
offset += 2;
if(offset > compressed.size())
throw std::runtime_error("gz_uncompress(): truncated gzip header crc");
}
if(offset + 8 > compressed.size())
throw std::runtime_error("gz_uncompress(): missing gzip footer");
return(offset);
}
String gz_compress(String src)
{
archive_check_size("gz_compress", "input", src.size(), "ARCHIVE_MAX_INPUT_BYTES", 64 * 1024 * 1024);
mz_stream stream;
std::memset(&stream, 0, sizeof(stream));
int status = mz_deflateInit2(&stream, MZ_DEFAULT_COMPRESSION, MZ_DEFLATED, -MZ_DEFAULT_WINDOW_BITS, 9, MZ_DEFAULT_STRATEGY);
if(status != MZ_OK)
throw std::runtime_error("gz_compress(): could not initialize compressor");
mz_ulong bound = mz_deflateBound(&stream, src.size());
String deflated;
deflated.resize(bound);
stream.next_in = (const unsigned char*)src.data();
stream.avail_in = src.size();
stream.next_out = (unsigned char*)deflated.data();
stream.avail_out = deflated.size();
status = mz_deflate(&stream, MZ_FINISH);
if(status != MZ_STREAM_END)
{
mz_deflateEnd(&stream);
throw std::runtime_error("gz_compress(): compression failed");
}
deflated.resize(stream.total_out);
mz_deflateEnd(&stream);
String result;
result.reserve(10 + deflated.size() + 8);
result.push_back((char)0x1f);
result.push_back((char)0x8b);
result.push_back((char)0x08);
result.push_back((char)0x00);
gz_append_u32_le(result, 0);
result.push_back((char)0x00);
result.push_back((char)0xff);
result.append(deflated);
gz_append_u32_le(result, (u32)mz_crc32(MZ_CRC32_INIT, (const unsigned char*)src.data(), src.size()));
gz_append_u32_le(result, (u32)src.size());
return(result);
}
String gz_uncompress(String compressed)
{
archive_check_size("gz_uncompress", "input", compressed.size(), "ARCHIVE_MAX_INPUT_BYTES", 64 * 1024 * 1024);
size_t deflate_offset = gz_deflate_offset(compressed);
size_t footer_offset = compressed.size() - 8;
size_t deflate_size = footer_offset - deflate_offset;
u32 expected_size = gz_read_u32_le(compressed, footer_offset + 4);
archive_check_size("gz_uncompress", "declared output", expected_size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
size_t out_len = 0;
void* out = tinfl_decompress_mem_to_heap(compressed.data() + deflate_offset, deflate_size, &out_len, 0);
if(!out)
throw std::runtime_error("gz_uncompress(): decompression failed");
u64 output_limit = config_u64("ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
if(output_limit > 0 && out_len > output_limit)
{
mz_free(out);
throw std::runtime_error("gz_uncompress(): output exceeds configured limit");
}
String result((char*)out, out_len);
mz_free(out);
u32 expected_crc = gz_read_u32_le(compressed, footer_offset);
u32 actual_crc = (u32)mz_crc32(MZ_CRC32_INIT, (const unsigned char*)result.data(), result.size());
if(actual_crc != expected_crc)
throw std::runtime_error("gz_uncompress(): crc check failed");
if((u32)result.size() != expected_size)
throw std::runtime_error("gz_uncompress(): size check failed");
return(result);
}
bool zip_extract(String zip_file_name, String destination_directory)
{
if(destination_directory == "")
throw std::runtime_error(zip_error("zip_extract", "destination directory is empty"));
if(!file_exists(destination_directory) && !mkdir(destination_directory))
throw std::runtime_error(zip_error("zip_extract", "could not create destination directory " + destination_directory));
mz_zip_archive archive;
std::memset(&archive, 0, sizeof(archive));
if(!mz_zip_reader_init_file(&archive, zip_file_name.c_str(), 0))
throw std::runtime_error(zip_error("zip_extract", "could not open " + zip_file_name));
try
{
mz_uint count = mz_zip_reader_get_num_files(&archive);
archive_check_size("zip_extract", "entry count", count, "ARCHIVE_MAX_ZIP_ENTRIES", 4096);
for(mz_uint i = 0; i < count; i++)
{
mz_zip_archive_file_stat stat;
std::memset(&stat, 0, sizeof(stat));
if(!mz_zip_reader_file_stat(&archive, i, &stat))
throw std::runtime_error(zip_error("zip_extract", "could not read file metadata at index " + std::to_string((u64)i)));
archive_check_size("zip_extract", "uncompressed entry", stat.m_uncomp_size, "ARCHIVE_MAX_OUTPUT_BYTES", 64 * 1024 * 1024);
String name = zip_normalize_entry_name(stat.m_filename);
if(!zip_entry_name_safe(name))
throw std::runtime_error(zip_error("zip_extract", "refusing unsafe entry name '" + name + "'"));
String target = path_join(destination_directory, name);
if(mz_zip_reader_is_file_a_directory(&archive, i))
{
if(!file_exists(target) && !mkdir(target))
throw std::runtime_error(zip_error("zip_extract", "could not create directory " + target));
continue;
}
if(!zip_ensure_parent_directory(target))
throw std::runtime_error(zip_error("zip_extract", "could not create parent directory for " + target));
if(!mz_zip_reader_extract_to_file(&archive, i, target.c_str(), 0))
throw std::runtime_error(zip_error("zip_extract", "could not extract " + name));
}
mz_zip_reader_end(&archive);
return(true);
}
catch(...)
{
mz_zip_reader_end(&archive);
throw;
}
}