Files
ECAP_Server/module/Local_Server/server/libarchive.cpp
T
2026-06-25 15:44:25 +08:00

690 lines
23 KiB
C++

#include "libarchive.h"
#include <archive.h>
#include <archive_entry.h>
#include <algorithm>
#include <cctype>
#include <chrono>
#include <cstring>
#include <fstream>
#include <memory>
#include <sstream>
#include <system_error>
#include <unordered_map>
#include <unordered_set>
namespace fs = std::filesystem;
namespace {
constexpr std::size_t kArchiveBlockSize = 64 * 1024;
[[noreturn]] void throw_invalid_archive(const std::string &reason) {
throw Invalid_Archive_Error("invalid upgrade archive: " + reason);
}
std::string archive_error_text(archive *a, const std::string &stage) {
const char *err = archive_error_string(a);
if (err == nullptr || *err == '\0') {
return stage;
}
return stage + ": " + err;
}
void require_archive_ok(archive *a, int code, const std::string &stage) {
if (code != ARCHIVE_OK) {
throw std::runtime_error(archive_error_text(a, stage));
}
}
std::string path_to_utf8(const fs::path &path) {
const auto u8 = path.generic_u8string();
return std::string(u8.begin(), u8.end());
}
bool path_is_within(const fs::path &base, const fs::path &target) {
const fs::path canonical_base = fs::weakly_canonical(base);
const fs::path canonical_target = fs::weakly_canonical(target);
auto base_it = canonical_base.begin();
auto target_it = canonical_target.begin();
for (; base_it != canonical_base.end(); ++base_it, ++target_it) {
if (target_it == canonical_target.end() || *base_it != *target_it) {
return false;
}
}
return true;
}
std::string normalize_archive_path(const std::string &raw,
bool allow_empty = false) {
if (raw.empty()) {
if (allow_empty)
return {};
throw_invalid_archive("empty entry path");
}
if (raw.front() == '/' || raw.find('\\') != std::string::npos ||
raw.find(':') != std::string::npos) {
throw_invalid_archive("unsafe entry path: " + raw);
}
std::string normalized;
std::size_t start = 0;
while (start < raw.size()) {
const std::size_t slash = raw.find('/', start);
const std::size_t end = slash == std::string::npos ? raw.size() : slash;
const std::string component = raw.substr(start, end - start);
if (component.empty() || component == "." || component == "..") {
throw_invalid_archive("unsafe entry path: " + raw);
}
if (!normalized.empty())
normalized.push_back('/');
normalized += component;
if (slash == std::string::npos)
break;
start = slash + 1;
if (start == raw.size())
break;
}
return normalized;
}
std::string entry_path(archive_entry *entry) {
const char *name = archive_entry_pathname_utf8(entry);
if (name == nullptr) {
name = archive_entry_pathname(entry);
}
if (name == nullptr) {
throw_invalid_archive("entry path is unavailable");
}
return normalize_archive_path(name);
}
bool is_directory_entry(archive_entry *entry) {
return archive_entry_filetype(entry) == AE_IFDIR;
}
bool is_regular_entry(archive_entry *entry) {
const auto type = archive_entry_filetype(entry);
return type == AE_IFREG || type == 0;
}
void reject_unsupported_entry_type(archive_entry *entry,
const std::string &path) {
const auto type = archive_entry_filetype(entry);
if (type == AE_IFLNK) {
throw_invalid_archive("symbolic link is not allowed: " + path);
}
if (type != AE_IFDIR && type != AE_IFREG && type != 0) {
throw_invalid_archive("special file is not allowed: " + path);
}
}
void setup_archive_reader(archive *a) {
require_archive_ok(a, archive_read_support_filter_all(a),
"archive_read_support_filter_all failed");
require_archive_ok(a, archive_read_support_format_all(a),
"archive_read_support_format_all failed");
}
void consume_entry_data_for_validation(archive *a, const std::string &path,
const Archive_Layout_Rules &rules,
std::uint64_t &total_uncompressed_size) {
std::uint64_t entry_size = 0;
const void *block = nullptr;
std::size_t block_size = 0;
la_int64_t offset = 0;
while (true) {
const int result = archive_read_data_block(a, &block, &block_size, &offset);
if (result == ARCHIVE_EOF) {
break;
}
if (result != ARCHIVE_OK) {
throw_invalid_archive(
archive_error_text(a, "cannot read entry: " + path));
}
if (block_size > rules.max_entry_uncompressed_size ||
entry_size > rules.max_entry_uncompressed_size - block_size) {
throw_invalid_archive("entry size exceeds limit: " + path);
}
if (block_size > rules.max_total_uncompressed_size ||
total_uncompressed_size >
rules.max_total_uncompressed_size - block_size) {
throw_invalid_archive("uncompressed size exceeds limit");
}
entry_size += block_size;
total_uncompressed_size += block_size;
}
}
void consume_entry_data_for_validation(archive *a, archive_entry *entry,
const std::string &path,
const Archive_Layout_Rules &rules,
std::uint64_t &total_uncompressed_size) {
if (archive_entry_size_is_set(entry)) {
const auto expected_size = archive_entry_size(entry);
if (expected_size < 0) {
throw_invalid_archive("negative entry size: " + path);
}
const auto size = static_cast<std::uint64_t>(expected_size);
if (size > rules.max_entry_uncompressed_size ||
size > rules.max_total_uncompressed_size) {
throw_invalid_archive("entry size exceeds limit: " + path);
}
}
consume_entry_data_for_validation(a, path, rules, total_uncompressed_size);
}
std::unordered_set<std::string>
make_allowed_entries(const std::vector<std::string> &required,
const std::vector<std::string> &optional) {
std::unordered_set<std::string> result;
auto add = [&result](const std::vector<std::string> &entries) {
for (const auto &entry : entries) {
if (entry.empty() || entry.find('/') != std::string::npos ||
entry.find('\\') != std::string::npos) {
throw_invalid_archive("invalid layout rule entry: " + entry);
}
result.insert(entry);
}
};
add(required);
add(optional);
return result;
}
struct Validated_Archive_Entry {
std::string path;
bool is_directory = false;
};
struct Archive_Read_Deleter {
void operator()(archive *a) const noexcept {
if (a != nullptr) {
archive_read_free(a);
}
}
};
struct Archive_Write_Deleter {
void operator()(archive *a) const noexcept {
if (a != nullptr) {
archive_write_free(a);
}
}
};
using Read_Archive_Ptr = std::unique_ptr<archive, Archive_Read_Deleter>;
using Write_Archive_Ptr = std::unique_ptr<archive, Archive_Write_Deleter>;
Read_Archive_Ptr make_archive_reader() {
Read_Archive_Ptr reader(archive_read_new());
if (!reader) {
throw std::runtime_error("archive_read_new failed");
}
setup_archive_reader(reader.get());
return reader;
}
bool is_safe_archive_name_token(const std::string &value) {
if (value.empty()) {
return false;
}
return std::ranges::all_of(value, [](unsigned char ch) {
return std::isalnum(ch) != 0 || ch == '_' || ch == '-' || ch == '.';
});
}
void validate_archive_write_options(const Archive_Write_Options &options) {
if (!is_safe_archive_name_token(options.format)) {
throw std::invalid_argument("invalid archive format: " + options.format);
}
if (!options.filter.empty() && options.filter != "none" &&
!is_safe_archive_name_token(options.filter)) {
throw std::invalid_argument("invalid archive filter: " + options.filter);
}
if (!is_safe_archive_name_token(options.extension) ||
options.extension.find('/') != std::string::npos) {
throw std::invalid_argument("invalid archive extension: " +
options.extension);
}
}
void configure_archive_writer(archive *writer,
const Archive_Write_Options &options) {
validate_archive_write_options(options);
if (!options.filter.empty() && options.filter != "none") {
require_archive_ok(
writer,
archive_write_add_filter_by_name(writer, options.filter.c_str()),
"archive_write_add_filter_by_name failed: " + options.filter);
}
require_archive_ok(
writer, archive_write_set_format_by_name(writer, options.format.c_str()),
"archive_write_set_format_by_name failed: " + options.format);
if (options.format == "zip") {
require_archive_ok(writer,
archive_write_zip_set_compression_deflate(writer),
"archive_write_zip_set_compression_deflate failed");
}
}
Write_Archive_Ptr make_archive_writer(const Archive_Write_Options &options) {
Write_Archive_Ptr writer(archive_write_new());
if (!writer) {
throw std::runtime_error("archive_write_new failed");
}
configure_archive_writer(writer.get(), options);
return writer;
}
void extract_archive(archive *a, const fs::path &out_dir) {
fs::create_directories(out_dir);
const fs::path base = fs::weakly_canonical(out_dir);
archive_entry *entry = nullptr;
while (true) {
const int result = archive_read_next_header(a, &entry);
if (result == ARCHIVE_EOF) {
break;
}
if (result != ARCHIVE_OK) {
throw std::runtime_error(
archive_error_text(a, "archive_read_next_header failed"));
}
const std::string normalized = entry_path(entry);
reject_unsupported_entry_type(entry, normalized);
const fs::path out_path = (base / fs::path(normalized)).lexically_normal();
if (!path_is_within(base, out_path)) {
throw std::runtime_error("archive-slip detected: " + normalized);
}
if (is_directory_entry(entry)) {
fs::create_directories(out_path);
continue;
}
if (!is_regular_entry(entry)) {
throw std::runtime_error("unsupported archive entry type: " + normalized);
}
fs::create_directories(out_path.parent_path());
std::ofstream out(out_path, std::ios::binary);
if (!out) {
throw std::runtime_error("open output failed: " + path_to_utf8(out_path));
}
const void *block = nullptr;
std::size_t block_size = 0;
la_int64_t offset = 0;
while (true) {
const int read_result =
archive_read_data_block(a, &block, &block_size, &offset);
if (read_result == ARCHIVE_EOF) {
break;
}
if (read_result != ARCHIVE_OK) {
throw std::runtime_error(
archive_error_text(a, "archive_read_data_block failed"));
}
out.write(static_cast<const char *>(block),
static_cast<std::streamsize>(block_size));
if (!out) {
throw std::runtime_error("write output failed: " +
path_to_utf8(out_path));
}
}
}
}
struct Ignore_Rules {
std::vector<fs::path> directories;
std::unordered_set<std::string> files;
std::unordered_set<std::string> extensions;
};
Ignore_Rules
parse_ignore_rules(const std::vector<std::string> &ignore_patterns) {
Ignore_Rules rules;
for (const auto &pattern : ignore_patterns) {
if (pattern.empty())
continue;
if (pattern.ends_with('/')) {
rules.directories.emplace_back(
fs::path(pattern.substr(0, pattern.size() - 1)).lexically_normal());
} else if (pattern[0] == '.') {
rules.extensions.insert(pattern);
} else {
rules.files.insert(pattern);
}
}
return rules;
}
bool should_skip_directory(const fs::path &rel_path,
const Ignore_Rules &rules) {
for (const auto &ignored : rules.directories) {
const auto rel_native = rel_path.native();
const auto ignored_native = ignored.native();
if (rel_path == ignored ||
rel_native.starts_with(ignored_native +
fs::path::preferred_separator)) {
return true;
}
}
return false;
}
void add_file_to_archive(archive *writer, const fs::path &base,
const fs::path &abs_path, const fs::path &rel_path) {
const std::uintmax_t file_size = fs::file_size(abs_path);
const std::string archive_name = path_to_utf8(rel_path);
if (archive_name.empty() || archive_name == ".." ||
archive_name.starts_with("../") ||
archive_name.find("/../") != std::string::npos ||
archive_name.ends_with("/..")) {
throw std::runtime_error("invalid path in archive: " + archive_name);
}
std::unique_ptr<archive_entry, decltype(&archive_entry_free)> entry(
archive_entry_new(), archive_entry_free);
if (!entry) {
throw std::runtime_error("archive_entry_new failed");
}
archive_entry_set_pathname_utf8(entry.get(), archive_name.c_str());
archive_entry_set_filetype(entry.get(), AE_IFREG);
archive_entry_set_perm(entry.get(), 0644);
archive_entry_set_size(entry.get(), static_cast<la_int64_t>(file_size));
const auto now =
std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
archive_entry_set_mtime(entry.get(), now, 0);
require_archive_ok(writer, archive_write_header(writer, entry.get()),
"archive_write_header failed");
std::ifstream in(abs_path, std::ios::binary);
if (!in) {
throw std::runtime_error("failed to open input file: " +
path_to_utf8(abs_path));
}
char buffer[kArchiveBlockSize];
while (in) {
in.read(buffer, sizeof(buffer));
const auto count = in.gcount();
if (count <= 0) {
break;
}
const la_ssize_t written =
archive_write_data(writer, buffer, static_cast<std::size_t>(count));
if (written < 0 || written != count) {
throw std::runtime_error(
archive_error_text(writer, "archive_write_data failed"));
}
}
if (!in.eof()) {
throw std::runtime_error("failed to read input file: " +
path_to_utf8(abs_path));
}
require_archive_ok(writer, archive_write_finish_entry(writer),
"archive_write_finish_entry failed");
(void)base;
}
void write_directory_to_archive(
archive *writer, const fs::path &dir_path,
const std::vector<std::string> &ignore_patterns) {
if (!fs::exists(dir_path)) {
throw std::runtime_error("source path does not exist: " +
path_to_utf8(dir_path));
}
if (!fs::is_directory(dir_path)) {
throw std::runtime_error("source path exists but is not a directory: " +
path_to_utf8(dir_path));
}
const fs::path base = fs::weakly_canonical(dir_path);
const Ignore_Rules ignore_rules = parse_ignore_rules(ignore_patterns);
fs::recursive_directory_iterator it(base);
const fs::recursive_directory_iterator end;
for (; it != end; ++it) {
const fs::path abs_path = it->path();
const fs::path rel_path = fs::relative(abs_path, base);
if (it->is_directory()) {
if (should_skip_directory(rel_path, ignore_rules)) {
it.disable_recursion_pending();
}
continue;
}
if (!it->is_regular_file()) {
continue;
}
const std::string filename = abs_path.filename().string();
const std::string extension = abs_path.extension().string();
if (ignore_rules.files.contains(filename) ||
(!extension.empty() && ignore_rules.extensions.contains(extension))) {
continue;
}
add_file_to_archive(writer, base, abs_path, rel_path);
}
}
la_ssize_t archive_string_write_callback(archive *, void *client_data,
const void *buffer,
std::size_t length) {
auto *out = static_cast<std::string *>(client_data);
out->append(static_cast<const char *>(buffer), length);
return static_cast<la_ssize_t>(length);
}
int archive_noop_open_close_callback(archive *, void *) { return ARCHIVE_OK; }
void validate_archive_layout_from_memory_or_throw(
const char *data, std::size_t size, const Archive_Layout_Rules &rules) {
if (data == nullptr || size == 0) {
throw_invalid_archive("empty archive");
}
if (size > rules.max_archive_size) {
throw_invalid_archive("archive size exceeds limit");
}
auto reader = make_archive_reader();
require_archive_ok(reader.get(),
archive_read_open_memory(reader.get(), data, size),
"archive_read_open_memory failed");
std::vector<Validated_Archive_Entry> entries;
std::unordered_set<std::string> unique_paths;
std::uint64_t total_uncompressed_size = 0;
std::size_t entry_count = 0;
archive_entry *entry = nullptr;
while (true) {
const int result = archive_read_next_header(reader.get(), &entry);
if (result == ARCHIVE_EOF) {
break;
}
if (result != ARCHIVE_OK) {
throw_invalid_archive(
archive_error_text(reader.get(), "cannot read archive header"));
}
++entry_count;
if (entry_count > rules.max_entry_count) {
throw_invalid_archive("entry count exceeds limit");
}
const std::string path = entry_path(entry);
if (!unique_paths.insert(path).second) {
throw_invalid_archive("duplicate entry path: " + path);
}
if (archive_entry_is_encrypted(entry)) {
throw_invalid_archive("encrypted entry is not allowed: " + path);
}
reject_unsupported_entry_type(entry, path);
const bool directory = is_directory_entry(entry);
if (!directory) {
if (!is_regular_entry(entry)) {
throw_invalid_archive("special file is not allowed: " + path);
}
consume_entry_data_for_validation(reader.get(), entry, path, rules,
total_uncompressed_size);
}
entries.push_back({path, directory});
}
for (const auto &rule : rules.directories) {
const std::string rule_directory =
normalize_archive_path(rule.directory, true);
const auto allowed_files =
make_allowed_entries(rule.required_files, rule.optional_files);
const auto allowed_directories = make_allowed_entries(
rule.required_directories, rule.optional_directories);
std::unordered_map<std::string, bool> direct_children;
for (const auto &entry : entries) {
std::string relative;
if (rule_directory.empty()) {
relative = entry.path;
} else {
const std::string prefix = rule_directory + "/";
if (!entry.path.starts_with(prefix))
continue;
relative = entry.path.substr(prefix.size());
}
const std::size_t slash = relative.find('/');
const std::string child = relative.substr(0, slash);
const bool is_directory =
slash != std::string::npos || entry.is_directory;
const auto [it, inserted] = direct_children.emplace(child, is_directory);
if (!inserted && it->second != is_directory) {
throw_invalid_archive("entry is both file and directory: " + child);
}
}
for (const auto &[child, is_directory] : direct_children) {
const auto &allowed = is_directory ? allowed_directories : allowed_files;
if (!allowed.contains(child)) {
throw_invalid_archive("unexpected entry under " + rule_directory +
": " + child);
}
}
for (const auto &required : rule.required_files) {
const auto it = direct_children.find(required);
if (it == direct_children.end() || it->second) {
throw_invalid_archive("missing required file under " + rule_directory +
": " + required);
}
}
for (const auto &required : rule.required_directories) {
const auto it = direct_children.find(required);
if (it == direct_children.end() || !it->second) {
throw_invalid_archive("missing required directory under " +
rule_directory + ": " + required);
}
}
}
}
} // namespace
Psc::expected<void, std::string>
validate_archive_layout_from_memory(const char *data, std::size_t size,
const Archive_Layout_Rules &rules) {
try {
validate_archive_layout_from_memory_or_throw(data, size, rules);
return {};
} catch (const Invalid_Archive_Error &e) {
return Psc::unexpected<std::string>(e.what());
}
}
void unarchive_file(const fs::path &zipPath, const fs::path &outDir) {
auto reader = make_archive_reader();
#ifdef _WIN32
require_archive_ok(reader.get(),
archive_read_open_filename_w(reader.get(),
zipPath.wstring().c_str(),
kArchiveBlockSize),
"archive_read_open_filename_w failed");
#else
require_archive_ok(reader.get(),
archive_read_open_filename(reader.get(), zipPath.c_str(),
kArchiveBlockSize),
"archive_read_open_filename failed");
#endif
extract_archive(reader.get(), outDir);
}
void unarchive_from_memory(const char *data, size_t size,
const fs::path &outDir) {
if (data == nullptr || size == 0) {
throw std::runtime_error("empty archive");
}
auto reader = make_archive_reader();
require_archive_ok(reader.get(),
archive_read_open_memory(reader.get(), data, size),
"archive_read_open_memory failed");
extract_archive(reader.get(), outDir);
}
void archive_directory(const fs::path &dirPath, const fs::path &archive_path,
const Archive_Write_Options &options) {
auto writer = make_archive_writer(options);
#ifdef _WIN32
require_archive_ok(writer.get(),
archive_write_open_filename_w(
writer.get(), archive_path.wstring().c_str()),
"archive_write_open_filename_w failed");
#else
require_archive_ok(
writer.get(),
archive_write_open_filename(writer.get(), archive_path.c_str()),
"archive_write_open_filename failed");
#endif
write_directory_to_archive(writer.get(), dirPath, {});
require_archive_ok(writer.get(), archive_write_close(writer.get()),
"archive_write_close failed");
}
std::string
archive_directory_to_memory(const fs::path &dirPath,
const std::vector<std::string> &ignorePatterns,
const Archive_Write_Options &options) {
std::string out;
auto writer = make_archive_writer(options);
require_archive_ok(writer.get(),
archive_write_open(writer.get(), &out,
archive_noop_open_close_callback,
archive_string_write_callback,
archive_noop_open_close_callback),
"archive_write_open failed");
write_directory_to_archive(writer.get(), dirPath, ignorePatterns);
require_archive_ok(writer.get(), archive_write_close(writer.get()),
"archive_write_close failed");
return out;
}