mirror of
https://github.com/MyLuxy/overdrive.git
synced 2026-10-22 11:53:05 -05:00
242 lines
10 KiB
C++
242 lines
10 KiB
C++
// Copyright (C) 2026 half144 and PS5 React contributors
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// rar-extract's 7z extractor. A 7z whose decoder needs more memory than the app's own heap can give
|
|
// (most often an LZMA2 dictionary over 64 MiB, which repacks set to 256 MiB) is extracted here with
|
|
// libarchive, in this process's memory, which goes back to the console when the process exits. The
|
|
// engine sends one only when the archive's headers declare no more than the budget it names.
|
|
//
|
|
// Request, after the "7ZX1" line: "memory <bytes>" (what the headers declare the decoder needs), "limit
|
|
// <bytes>", "flatten <extension>"..., "reserved <text>", "dest <directory>", "source <path>"... in volume
|
|
// order, then "end". The engine can then write "cancel" at any time. Answers as for a RAR set: "ready",
|
|
// "p <bytes written>", "k" every second, "f <sha256> <size> <path>" once a file is on disk and synced,
|
|
// "s" once every file is decoded, "t <key>=<value>...", then "ok", "cancelled" or "fail <reason>".
|
|
//
|
|
// A program's malloc on the console has no ceiling of its own: the system hands over memory it takes
|
|
// from everything else, slowly (a probe on a console took 8.5 s for 512 MiB and froze its interface
|
|
// beyond that), and keeps it with this process until it exits. So the decoder's memory is taken and
|
|
// every page touched before anything is decoded; when the console needs over a second to give it, the
|
|
// extraction stops there instead of running with the console short. The block is then freed into this
|
|
// process's heap, where libarchive's own allocation finds it.
|
|
#include "sevenzip.hpp"
|
|
#include "worker_io.hpp"
|
|
#include <archive.h>
|
|
#include <archive_entry.h>
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <new>
|
|
|
|
namespace sevenzip {
|
|
namespace {
|
|
using namespace worker;
|
|
const char* memory_busy = "The console has too little free memory to extract this archive now. Close other apps and retry.";
|
|
const char* unsafe = "Archive contains an unsafe path, link or special file.";
|
|
const char* too_large = "Archive exceeds the extraction size limit.";
|
|
// What the extraction holds beside the decoder: the writer's buffers and libarchive's read blocks.
|
|
constexpr std::uint64_t overhead = 32ULL << 20;
|
|
constexpr std::uint64_t claim_ms = 1000;
|
|
constexpr std::uint64_t page = 4096;
|
|
constexpr size_t read_block = 1 << 20;
|
|
|
|
struct Options {
|
|
std::uint64_t memory = 0, limit = 0;
|
|
std::vector<std::string> flatten, sources;
|
|
std::string reserved, destination;
|
|
};
|
|
|
|
bool cancelled = false; // Only the decoding thread reads and sets it.
|
|
|
|
bool read_options(Options& options) {
|
|
std::string line;
|
|
while (read_line(line)) {
|
|
const auto space = line.find(' ');
|
|
const std::string key = line.substr(0, space), value = space == std::string::npos ? "" : line.substr(space + 1);
|
|
if (key == "end") return !options.destination.empty() && !options.sources.empty() && options.limit && options.memory;
|
|
if (key == "memory") options.memory = strtoull(value.c_str(), nullptr, 10);
|
|
else if (key == "limit") options.limit = strtoull(value.c_str(), nullptr, 10);
|
|
else if (key == "flatten") options.flatten.push_back(value);
|
|
else if (key == "reserved") options.reserved = value;
|
|
else if (key == "dest") options.destination = value;
|
|
else if (key == "source") options.sources.push_back(value);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Takes `bytes` and writes a byte in every page so the console really hands them over, then frees them.
|
|
std::string claim(std::uint64_t bytes, std::uint64_t& took_ms) {
|
|
const auto started = now_ns();
|
|
auto* block = static_cast<volatile unsigned char*>(std::malloc(bytes));
|
|
if (!block) return too_much_memory;
|
|
for (std::uint64_t at = 0; at < bytes; at += page) block[at] = 1;
|
|
took_ms = (now_ns() - started) / 1000000;
|
|
std::free(const_cast<unsigned char*>(block));
|
|
return took_ms > claim_ms ? memory_busy : std::string();
|
|
}
|
|
|
|
struct Progress {
|
|
std::uint64_t written = 0, last_report = 0;
|
|
void beat() {
|
|
const auto now = now_ns();
|
|
if (now - last_report < progress_ns) return;
|
|
last_report = now;
|
|
say("p " + std::to_string(written));
|
|
if (stop_requested()) cancelled = true;
|
|
}
|
|
};
|
|
|
|
// libarchive names a failed allocation in its decoder only by liblzma's code; either way it is memory.
|
|
std::string archive_failure(archive* reader, const char* fallback) {
|
|
const char* text = archive_error_string(reader);
|
|
if (archive_errno(reader) == ENOMEM || (text && std::strstr(text, "memory"))) return too_much_memory;
|
|
return text && *text ? text : fallback;
|
|
}
|
|
|
|
std::string extract(const Options& options, Writer& writer, Progress& progress) {
|
|
archive* reader = archive_read_new();
|
|
if (!reader) return too_much_memory;
|
|
archive_read_support_format_7zip(reader);
|
|
std::vector<const char*> files;
|
|
for (const auto& path : options.sources) files.push_back(path.c_str());
|
|
files.push_back(nullptr);
|
|
std::string error;
|
|
if (archive_read_open_filenames(reader, files.data(), read_block) != ARCHIVE_OK) error = archive_failure(reader, "Unsupported or incomplete archive.");
|
|
archive_entry* entry = nullptr;
|
|
int result = ARCHIVE_OK;
|
|
std::uint64_t declared = 0;
|
|
// A warning is a header libarchive could only read in part, such as a name it could not convert.
|
|
while (error.empty() && !cancelled && ((result = archive_read_next_header(reader, &entry)) == ARCHIVE_OK || result == ARCHIVE_WARN)) {
|
|
progress.beat();
|
|
if (cancelled) break;
|
|
const char* raw = archive_entry_pathname_utf8(entry);
|
|
if (!raw) {
|
|
error = "Archive has a file name this console cannot read.";
|
|
break;
|
|
}
|
|
std::string name(raw);
|
|
while (!name.empty() && name.back() == '/') name.pop_back();
|
|
if (!name.empty() && name[0] != '/') name = without_dots(name);
|
|
const auto type = archive_entry_filetype(entry);
|
|
const bool link = archive_entry_symlink(entry) || archive_entry_hardlink(entry);
|
|
if (!safe(name) || link || (type != AE_IFREG && type != AE_IFDIR) ||
|
|
(!options.reserved.empty() && name.find(options.reserved) != std::string::npos)) {
|
|
error = unsafe;
|
|
break;
|
|
}
|
|
const auto size = archive_entry_size(entry);
|
|
if (type == AE_IFREG) {
|
|
if (size < 0 || std::uint64_t(size) > options.limit - declared) {
|
|
error = too_large;
|
|
break;
|
|
}
|
|
declared += std::uint64_t(size);
|
|
// ShadowMount's default scan reaches one subdirectory, so images are published at the set root.
|
|
if (flattened(options.flatten, name)) name = name.substr(name.rfind('/') + 1);
|
|
}
|
|
std::string target;
|
|
if (!make_parents(options.destination, name, target)) {
|
|
error = strerror(errno);
|
|
break;
|
|
}
|
|
if (type == AE_IFDIR) {
|
|
if (mkdir(target.c_str(), 0777) && (errno != EEXIST || !plain_directory(target))) error = strerror(errno);
|
|
continue;
|
|
}
|
|
const int fd = open(target.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0666);
|
|
if (fd < 0) {
|
|
error = strerror(errno);
|
|
break;
|
|
}
|
|
writer.begin(fd, name);
|
|
const void* data = nullptr;
|
|
size_t bytes = 0;
|
|
la_int64_t offset = 0;
|
|
std::uint64_t file_written = 0;
|
|
while (!cancelled && (result = archive_read_data_block(reader, &data, &bytes, &offset)) == ARCHIVE_OK) {
|
|
// 7z entries come in order; a gap would be a sparse file, which 7-Zip does not write.
|
|
if (offset < 0 || std::uint64_t(offset) != file_written) {
|
|
error = "Unsupported or damaged 7z archive.";
|
|
break;
|
|
}
|
|
if (bytes > options.limit - progress.written) {
|
|
error = too_large;
|
|
break;
|
|
}
|
|
if (!writer.append(static_cast<const unsigned char*>(data), bytes)) break;
|
|
file_written += bytes;
|
|
progress.written += bytes;
|
|
progress.beat();
|
|
}
|
|
if (error.empty() && !cancelled && result != ARCHIVE_EOF && result != ARCHIVE_OK) error = archive_failure(reader, "Unsupported or damaged 7z archive.");
|
|
if (error.empty()) error = writer.error();
|
|
if (!error.empty() || cancelled) break;
|
|
writer.end(std::uint64_t(size));
|
|
}
|
|
if (error.empty() && !cancelled && result != ARCHIVE_EOF) error = archive_failure(reader, "Incomplete archive headers.");
|
|
archive_read_free(reader);
|
|
return error;
|
|
}
|
|
}
|
|
|
|
int run() {
|
|
// Directories and files stay usable by the app, which may run as another user than this program.
|
|
umask(0);
|
|
Options options;
|
|
if (!read_options(options)) {
|
|
say("fail Invalid 7z worker request.");
|
|
return 1;
|
|
}
|
|
say("ready");
|
|
// "k" every second says this process is alive while the claim, a long fsync or a slow block holds it.
|
|
std::mutex alive_mutex;
|
|
std::condition_variable alive_wake;
|
|
bool done = false;
|
|
std::thread alive;
|
|
try {
|
|
alive = std::thread([&] {
|
|
std::unique_lock lock(alive_mutex);
|
|
while (!alive_wake.wait_for(lock, std::chrono::seconds(1), [&] { return done; })) say("k");
|
|
});
|
|
} catch (...) {
|
|
}
|
|
std::uint64_t claim_took = 0;
|
|
std::string error;
|
|
{
|
|
Progress progress;
|
|
std::unique_ptr<Writer> writer;
|
|
try {
|
|
error = claim(options.memory + overhead, claim_took);
|
|
if (error.empty()) {
|
|
writer = std::make_unique<Writer>();
|
|
error = extract(options, *writer, progress);
|
|
}
|
|
} catch (std::bad_alloc&) {
|
|
error = too_much_memory;
|
|
} catch (...) {
|
|
error = "7z extraction failed.";
|
|
}
|
|
if (writer) {
|
|
if (error.empty() && !cancelled) say("s");
|
|
const auto written = writer->finish();
|
|
if (error.empty()) error = written;
|
|
}
|
|
const auto ms = [](std::uint64_t ns) { return std::to_string(ns / 1000000) + "ms"; };
|
|
std::string timings = "t threads=1 cores=" + std::to_string(cores()) + " claim=" + std::to_string(claim_took) + "ms";
|
|
if (writer) {
|
|
timings += " write=" + ms(writer->write_ns) + "/" + std::to_string(writer->writes) + " fsync=" + ms(writer->sync_ns) + "/" +
|
|
std::to_string(writer->syncs) + " waited=" + ms(writer->wait_ns);
|
|
}
|
|
if (const std::uint64_t cpu = process_cpu_us()) timings += " cpu=" + ms(cpu * 1000);
|
|
say(timings);
|
|
}
|
|
{
|
|
std::lock_guard lock(alive_mutex);
|
|
done = true;
|
|
}
|
|
alive_wake.notify_all();
|
|
if (alive.joinable()) alive.join();
|
|
if (cancelled) say("cancelled");
|
|
else say(error.empty() ? "ok" : "fail " + error);
|
|
return error.empty() ? 0 : 1;
|
|
}
|
|
}
|