mirror of
https://github.com/MyLuxy/overdrive.git
synced 2026-10-22 03:42:44 -05:00
526 lines
23 KiB
C++
526 lines
23 KiB
C++
// Copyright (C) 2026 half144
|
|
// SPDX-License-Identifier: MIT
|
|
//
|
|
// rar-extract: extracts one RAR set with RarLab's UnRAR library, in a process of its own. UnRAR's license
|
|
// does not let it be linked into the GPL engine, so the engine talks to this program over a stream
|
|
// (the PS5 payload loader's socket, or a pipe on the desktop) and never loads its code. Its own
|
|
// crashes and C++ exceptions stay in this process too. A request that starts with "7ZX1" is a 7z whose
|
|
// decoder needs more memory than the app's heap can give, extracted with libarchive (sevenzip.cpp).
|
|
//
|
|
// Request, one item per line: "RARX1", then "threads <n>" (0 picks from the cores), "window <bytes>",
|
|
// "limit <bytes>", "flatten <extension>"..., "reserved <text>", "password <hex>", "dest <directory>",
|
|
// "source <path>"... in volume order, "wait 1" when later volumes are still downloading (each is read once
|
|
// a file has its name), then "end". The engine can then write "cancel" at any time.
|
|
// Answers: "ready", "p <bytes written>" a few times a second, "k" every second, "f <sha256> <size>
|
|
// <path>" once a file is on disk and synced, "d <bytes>" first with the unpacked size of every file when
|
|
// all volumes are there and their headers read, "s" once every file is decoded and only writes and fsyncs are
|
|
// left, "t <key>=<value>..." with where the time went (an engine that does not know a line or a key ignores
|
|
// it), then "ok", "cancelled" or "fail <reason>". A set whose last volume is not the last fails before any
|
|
// decoding with "fail Incomplete RAR archive or missing volume.", one whose first is not the first with
|
|
// "fail Missing first RAR volume."; the app reads these two exactly.
|
|
#include "rar.hpp"
|
|
#include "sevenzip.hpp"
|
|
#include "worker_io.hpp"
|
|
#ifdef __APPLE__
|
|
#include <CommonCrypto/CommonDigest.h>
|
|
#else
|
|
#include <openssl/evp.h>
|
|
#endif
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <deque>
|
|
#include <fcntl.h>
|
|
#include <mutex>
|
|
#include <poll.h>
|
|
#include <signal.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/stat.h>
|
|
#include <thread>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
namespace {
|
|
using namespace worker;
|
|
const char* missing_first = "Missing first RAR volume.";
|
|
|
|
struct Options {
|
|
unsigned threads = 0;
|
|
uint64 window = 0, limit = 0;
|
|
bool wait = false;
|
|
std::vector<std::string> flatten, sources;
|
|
std::string reserved, password, destination;
|
|
};
|
|
|
|
bool cancelled = false; // Only the decoding thread reads and sets it.
|
|
|
|
bool from_hex(const std::string& hex, std::string& out) {
|
|
if (hex.size() % 2) return false;
|
|
for (size_t i = 0; i < hex.size(); i += 2) {
|
|
int value = 0;
|
|
for (char c : hex.substr(i, 2)) {
|
|
const int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
|
|
if (digit < 0) return false;
|
|
value = value * 16 + digit;
|
|
}
|
|
out += char(value);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The lines after "RARX1".
|
|
bool read_request(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.window;
|
|
if (key == "threads") options.threads = unsigned(strtoul(value.c_str(), nullptr, 10));
|
|
else if (key == "window") options.window = 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 == "password" && !from_hex(value, options.password)) return false;
|
|
else if (key == "dest") options.destination = value;
|
|
else if (key == "wait") options.wait = value == "1";
|
|
else if (key == "source") options.sources.push_back(value);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
struct Session {
|
|
explicit Session(const Options& options) : options(options) {}
|
|
const Options& options;
|
|
Writer writer;
|
|
// `reading` is the index of the volume UnRAR is reading, the count of them once it wants one past the last.
|
|
size_t next_volume = 1, reading = 0;
|
|
bool writing = false, missing_volume = false, missing_password = false, last_checked = false, last_cut = false;
|
|
const Archive* archive = nullptr;
|
|
uint64 written = 0, last_report = 0;
|
|
std::string error;
|
|
};
|
|
|
|
void check_last(Session& session);
|
|
|
|
// Progress, a look for "cancel" and, while a set downloads, for its last volume, a few times a second.
|
|
void heartbeat(Session& session) {
|
|
const uint64 now = now_ns();
|
|
if (now - session.last_report < progress_ns) return;
|
|
session.last_report = now;
|
|
say("p " + std::to_string(session.written));
|
|
if (stop_requested()) cancelled = true;
|
|
check_last(session);
|
|
}
|
|
|
|
// The app renames a volume to its final name once it is complete; until then, progress and a look for
|
|
// "cancel" go on as while decoding. Without hard links (the console, exFAT) the download reserves the final
|
|
// name with an empty file just before renaming the volume over it: an empty file is not the volume yet.
|
|
bool await_volume(Session& session, const std::string& path) {
|
|
for (;;) {
|
|
struct stat st;
|
|
if (!lstat(path.c_str(), &st)) {
|
|
if (!S_ISREG(st.st_mode)) return false;
|
|
if (st.st_size) return true;
|
|
} else if (errno != ENOENT) {
|
|
return false;
|
|
}
|
|
heartbeat(session);
|
|
if (cancelled || !session.error.empty()) return false;
|
|
usleep(250000);
|
|
}
|
|
}
|
|
|
|
int CALLBACK callback(UINT message, LPARAM user, LPARAM p1, LPARAM p2) {
|
|
Session& session = *reinterpret_cast<Session*>(user);
|
|
switch (message) {
|
|
case UCM_PROCESSDATA: {
|
|
// Service data (NTFS streams and such) after a file is read and dropped.
|
|
if (!session.writing) return 1;
|
|
const size_t size = size_t(p2);
|
|
if (size > session.options.limit - session.written) {
|
|
session.error = "Archive exceeds the extraction size limit.";
|
|
return -1;
|
|
}
|
|
session.written += size;
|
|
if (!session.writer.append(reinterpret_cast<const byte*>(p1), size)) return -1;
|
|
heartbeat(session);
|
|
return cancelled || !session.error.empty() ? -1 : 1;
|
|
}
|
|
case UCM_NEXTVOLUMEW: {
|
|
// Volumes are exactly the engine's list, in order, whatever their names.
|
|
if (session.next_volume >= session.options.sources.size()) {
|
|
session.reading = session.next_volume;
|
|
session.missing_volume = true;
|
|
return -1;
|
|
}
|
|
session.reading = session.next_volume;
|
|
const std::string& path = session.options.sources[session.next_volume++];
|
|
if (session.options.wait && !await_volume(session, path)) {
|
|
if (!cancelled) session.missing_volume = true;
|
|
return -1;
|
|
}
|
|
UtfToWide(path.c_str(), reinterpret_cast<wchar*>(p1), size_t(p2));
|
|
return 1;
|
|
}
|
|
case UCM_CHANGEVOLUME:
|
|
return p2 == RAR_VOL_NOTIFY ? 1 : -1;
|
|
case UCM_CHANGEVOLUMEW:
|
|
// A RAR5 volume names its place in the set; one out of place means a volume between is missing.
|
|
if (p2 == RAR_VOL_NOTIFY && session.archive->Format == RARFMT50 && session.archive->VolNumber &&
|
|
session.archive->VolNumber != session.next_volume - 1)
|
|
session.missing_volume = true;
|
|
return p2 == RAR_VOL_NOTIFY && !session.missing_volume ? 1 : -1;
|
|
case UCM_NEEDPASSWORD:
|
|
case UCM_NEEDPASSWORDW: // Asked only when none was given.
|
|
session.missing_password = true;
|
|
return -1;
|
|
default: // No dictionary beyond the limit.
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
std::string describe(int code) {
|
|
switch (code) {
|
|
case ERAR_EOPEN: return "Incomplete RAR archive or missing volume.";
|
|
case ERAR_BAD_DATA: return "RAR checksum mismatch; the archive is damaged.";
|
|
case ERAR_EREAD: return "Cannot read the RAR archive; it may be truncated.";
|
|
case ERAR_MISSING_PASSWORD: return "RAR archive password is missing.";
|
|
case ERAR_BAD_PASSWORD: return "Incorrect RAR archive password.";
|
|
case ERAR_NO_MEMORY:
|
|
case ERAR_LARGE_DICT: return too_much_memory;
|
|
case ERAR_BAD_ARCHIVE:
|
|
case ERAR_UNKNOWN_FORMAT: return "Unsupported or damaged RAR archive.";
|
|
default: return "RAR extraction failed.";
|
|
}
|
|
}
|
|
|
|
int dll_error(CommandData& command) {
|
|
if (command.DllError) return command.DllError;
|
|
switch (ErrHandler.GetErrorCode()) {
|
|
case RARX_SUCCESS: return ERAR_SUCCESS;
|
|
case RARX_FATAL:
|
|
case RARX_READ: return ERAR_EREAD;
|
|
case RARX_CRC: return ERAR_BAD_DATA;
|
|
case RARX_OPEN: return ERAR_EOPEN;
|
|
case RARX_MEMORY: return ERAR_NO_MEMORY;
|
|
case RARX_BADPWD: return ERAR_BAD_PASSWORD;
|
|
case RARX_BADARC: return ERAR_BAD_ARCHIVE;
|
|
default: return ERAR_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
// The engine asks for fewer while downloads run. One core always stays with the app's interface; when the
|
|
// count is unknown, the cap alone applies.
|
|
unsigned pick_threads(unsigned requested) {
|
|
const long known = cores();
|
|
if (!requested) return unsigned(std::clamp(known - 2, 1L, 4L));
|
|
return unsigned(std::min<long>(requested, known > 0 ? std::clamp(known - 1, 1L, 6L) : 6));
|
|
}
|
|
|
|
// Throws RAR_EXIT or std::bad_alloc from UnRAR; returns the reason extraction stopped, empty on success.
|
|
std::string extract(Session& session, CommandData& command, Archive& archive, CmdExtract& extractor) {
|
|
const Options& options = session.options;
|
|
uint64 declared = 0;
|
|
while (true) {
|
|
heartbeat(session);
|
|
if (cancelled) return {};
|
|
if (!session.error.empty()) return session.error;
|
|
const size_t header_size = archive.SearchBlock(HEAD_FILE);
|
|
if (header_size == 0) {
|
|
if (archive.Volume && archive.GetHeaderType() == HEAD_ENDARC && archive.EndArcHead.NextVolume) {
|
|
if (!MergeArchive(archive, nullptr, false, 'L')) return describe(ERAR_EOPEN);
|
|
archive.Seek(archive.CurBlockPos, SEEK_SET);
|
|
continue;
|
|
}
|
|
if (archive.BrokenHeader) return describe(ERAR_BAD_DATA);
|
|
if (archive.FailedHeaderDecryption) return describe(ERAR_BAD_PASSWORD);
|
|
if (const int code = dll_error(command)) return describe(code);
|
|
return {};
|
|
}
|
|
const FileHeader& header = archive.FileHead;
|
|
if (header.SplitBefore) return describe(ERAR_EOPEN);
|
|
std::string name;
|
|
WideToUtf(header.FileName, name);
|
|
while (!name.empty() && name.back() == '/') name.pop_back();
|
|
if (!name.empty() && name[0] != '/') name = without_dots(name);
|
|
if (!safe(name) || header.RedirType != FSREDIR_NONE || (!options.reserved.empty() && name.find(options.reserved) != std::string::npos))
|
|
return "Archive contains an unsafe path, link or special file.";
|
|
if (header.Method != 0 && header.WinSize > options.window) return too_much_memory;
|
|
if (!header.Dir) {
|
|
if (header.UnpSize < 0 || uint64(header.UnpSize) > options.limit - declared) return "Archive exceeds the extraction size limit.";
|
|
declared += header.UnpSize;
|
|
}
|
|
// ShadowMount's default scan reaches one subdirectory, so images are published at the set root.
|
|
if (!header.Dir && flattened(options.flatten, name)) name = name.substr(name.rfind('/') + 1);
|
|
std::string target;
|
|
if (!make_parents(options.destination, name, target)) return strerror(errno);
|
|
if (header.Dir) {
|
|
if (mkdir(target.c_str(), 0777) && (errno != EEXIST || !plain_directory(target))) return strerror(errno);
|
|
} else {
|
|
const int fd = open(target.c_str(), O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0666);
|
|
if (fd < 0) return strerror(errno);
|
|
session.writer.begin(fd, name);
|
|
session.writing = true;
|
|
}
|
|
// Reading the service headers below may load the next file's header.
|
|
const uint64 size = uint64(header.UnpSize);
|
|
const bool directory = header.Dir;
|
|
command.DllError = 0;
|
|
command.DllOpMode = RAR_TEST;
|
|
command.Command = L"T";
|
|
command.Test = true;
|
|
bool repeat = false;
|
|
extractor.ExtractCurrentFile(archive, header_size, repeat);
|
|
session.writing = false;
|
|
// As unrar.dll does: the file's service headers are handled in the same step.
|
|
while (archive.IsOpened() && archive.ReadHeader() != 0 && archive.GetHeaderType() == HEAD_SERVICE) {
|
|
extractor.ExtractCurrentFile(archive, header_size, repeat);
|
|
archive.SeekToNext();
|
|
}
|
|
archive.Seek(archive.CurBlockPos, SEEK_SET);
|
|
if (!session.error.empty()) return session.error;
|
|
if (const auto error = session.writer.error(); !error.empty()) return error;
|
|
if (cancelled) return {};
|
|
if (const int code = dll_error(command)) return describe(code);
|
|
if (!directory) session.writer.end(size);
|
|
}
|
|
}
|
|
|
|
void use_password(CommandData& command, const Options& options) {
|
|
if (options.password.empty()) return;
|
|
std::wstring password;
|
|
UtfToWide(options.password.c_str(), password);
|
|
command.Password.Set(password.c_str());
|
|
}
|
|
|
|
// The unpacked size of every file in the set, from each volume's headers; 0 while volumes are still
|
|
// downloading or when one cannot be read, and the engine keeps its own estimate.
|
|
uint64 declared_total(const Options& options) {
|
|
if (options.wait) return 0;
|
|
CommandData command;
|
|
use_password(command, options);
|
|
uint64 total = 0;
|
|
try {
|
|
for (const auto& path : options.sources) {
|
|
std::wstring name;
|
|
UtfToWide(path.c_str(), name);
|
|
Archive archive(&command);
|
|
if (!archive.Open(name) || !archive.IsArchive(false)) return 0;
|
|
while (archive.ReadHeader() > 0 && archive.GetHeaderType() != HEAD_ENDARC) {
|
|
const FileHeader& header = archive.FileHead;
|
|
// A file split across volumes declares its whole size in each part; it counts once.
|
|
if (archive.GetHeaderType() == HEAD_FILE && !header.Dir && !header.SplitBefore) {
|
|
if (header.UnknownUnpSize || header.UnpSize < 0) return 0;
|
|
total += uint64(header.UnpSize);
|
|
}
|
|
archive.SeekToNext();
|
|
}
|
|
if (archive.BrokenHeader || archive.FailedHeaderDecryption) return 0;
|
|
}
|
|
} catch (...) {
|
|
total = 0;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
// The volume to blame for a set that ended early or failed its checksum, 1-based, or 0 when none stands
|
|
// out: the last if its headers run past its end; else, as a set's volumes are all one size but the last, a
|
|
// shorter one, cut off; otherwise, for an early end, the volume UnRAR was reading.
|
|
size_t suspect_volume(const Session& session, bool early_end) {
|
|
const auto& sources = session.options.sources;
|
|
if (session.last_cut) return sources.size();
|
|
std::vector<uint64> sizes;
|
|
for (const auto& path : sources) {
|
|
struct stat st;
|
|
sizes.push_back(lstat(path.c_str(), &st) ? 0 : uint64(st.st_size));
|
|
}
|
|
const uint64 full = sizes.size() > 1 ? *std::max_element(sizes.begin(), sizes.end() - 1) : 0;
|
|
for (size_t i = 0; i + 1 < sizes.size(); ++i)
|
|
if (sizes[i] < full) return i + 1;
|
|
return early_end && session.reading < sources.size() ? session.reading + 1 : 0;
|
|
}
|
|
|
|
// What a volume's own headers say about its place in the set. Not known when they cannot be read for a
|
|
// reason the extraction names better: no password, a wrong one, not a RAR volume. `whole` once every
|
|
// header and entry fits in the file: a volume cut short says nothing about what follows it. `cut` when an
|
|
// entry or the header after it runs past the file's end; a header damaged within the file is not a cut.
|
|
struct Place {
|
|
bool known = false, whole = false, cut = false, first = true, last = true;
|
|
};
|
|
|
|
Place place_of(const Options& options, const std::string& path) {
|
|
// A streamed set is checked while UnRAR decodes; its error state stays the decoder's.
|
|
const ErrorHandler saved = ErrHandler;
|
|
Place place;
|
|
try {
|
|
CommandData command;
|
|
use_password(command, options);
|
|
std::wstring name;
|
|
UtfToWide(path.c_str(), name);
|
|
Archive archive(&command);
|
|
if (archive.Open(name) && archive.IsArchive(false) && archive.Format != RARFMT14) {
|
|
bool entries = false, split_before = false, split_after = false, ended = false;
|
|
int64 entry_end = 0;
|
|
while (archive.ReadHeader() > 0) {
|
|
const HEADER_TYPE type = archive.GetHeaderType();
|
|
if (type == HEAD_ENDARC) {
|
|
ended = true;
|
|
break;
|
|
}
|
|
if (type == HEAD_FILE || type == HEAD_SERVICE) {
|
|
const FileHeader& header = type == HEAD_FILE ? archive.FileHead : archive.SubHead;
|
|
if (!entries) split_before = header.SplitBefore;
|
|
entries = true;
|
|
split_after = header.SplitAfter;
|
|
entry_end = archive.NextBlockPos;
|
|
}
|
|
archive.SeekToNext();
|
|
}
|
|
// RAR4 has no password check: a header cut short fails to decrypt as with a wrong password, but only
|
|
// after the right one decrypted the entries before it.
|
|
if (!archive.FailedHeaderDecryption || entries) {
|
|
place.known = true;
|
|
// Reading stops at the end block, or where no whole header is left: the file's end if nothing is cut.
|
|
const int64 size = archive.FileLength();
|
|
place.whole = !archive.BrokenHeader && entry_end <= size && (ended || archive.CurBlockPos >= size);
|
|
place.cut = entry_end > size || (!ended && archive.CurBlockPos < size && archive.Tell() >= size);
|
|
if (archive.Format == RARFMT50) {
|
|
place.first = !archive.Volume || archive.VolNumber == 0;
|
|
place.last = !archive.Volume || (ended && !archive.EndArcHead.NextVolume);
|
|
} else {
|
|
// RAR 2.x volumes have no first volume flag; RAR 3 and later ones number themselves in their end block.
|
|
const bool numbered = ended && archive.EndArcHead.StoreVolNumber && archive.EndArcHead.VolNumber;
|
|
place.first = !archive.Volume || (archive.MainHead.Flags & MHD_FIRSTVOLUME) || !(split_before || numbered);
|
|
// Without an end block (RAR 2.x, or "-en"), the last entry tells.
|
|
place.last = ended ? !archive.EndArcHead.NextVolume : entries && !split_after;
|
|
}
|
|
}
|
|
}
|
|
} catch (...) {
|
|
}
|
|
ErrHandler = saved;
|
|
return place;
|
|
}
|
|
|
|
// A set that lacks its last volume would otherwise fail only once every other volume is decoded, hours later
|
|
// on the console. A last volume cut short fails as before, named, so that one is downloaded again. A
|
|
// streamed set's last volume is checked once it is there.
|
|
void check_last(Session& session) {
|
|
const std::string& path = session.options.sources.back();
|
|
struct stat st;
|
|
if (session.last_checked || (session.options.wait && (lstat(path.c_str(), &st) || !S_ISREG(st.st_mode) || !st.st_size))) return;
|
|
session.last_checked = true;
|
|
const Place place = place_of(session.options, path);
|
|
session.last_cut = place.known && place.cut;
|
|
if (place.known && place.whole && !place.last) session.error = describe(ERAR_EOPEN);
|
|
}
|
|
|
|
void check_first(Session& session) {
|
|
const Place place = place_of(session.options, session.options.sources.front());
|
|
if (place.known && !place.first) session.error = missing_first;
|
|
}
|
|
|
|
std::string run(Session& session) {
|
|
const Options& options = session.options;
|
|
CommandData command;
|
|
command.FileArgs.AddString(L"*");
|
|
command.Overwrite = OVERWRITE_ALL;
|
|
command.VersionControl = 1;
|
|
command.Callback = callback;
|
|
command.UserData = LPARAM(&session);
|
|
command.Threads = pick_threads(options.threads);
|
|
command.WinSizeLimit = options.window;
|
|
use_password(command, options);
|
|
std::wstring name;
|
|
UtfToWide(options.sources.front().c_str(), name);
|
|
command.AddArcName(name);
|
|
Archive archive(&command);
|
|
session.archive = &archive;
|
|
if (!archive.Open(name)) return describe(ERAR_EOPEN);
|
|
if (!archive.IsArchive(true)) return describe(command.DllError ? command.DllError : dll_error(command) ? dll_error(command) : ERAR_BAD_ARCHIVE);
|
|
CmdExtract extractor(&command);
|
|
extractor.ExtractArchiveInit(archive);
|
|
return extract(session, command, archive, extractor);
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
signal(SIGPIPE, SIG_IGN);
|
|
// Directories and files stay usable by the app, which may run as another user than this program.
|
|
umask(0);
|
|
// "7ZX1" asks for a 7z (sevenzip.cpp); "RARX1" for a RAR set.
|
|
std::string magic;
|
|
if (read_line(magic) && magic == "7ZX1") return sevenzip::run();
|
|
Options options;
|
|
if (magic != "RARX1" || !read_request(options)) {
|
|
say("fail Invalid RAR worker request.");
|
|
return 1;
|
|
}
|
|
say("ready");
|
|
// Progress comes from the decoder's callback only, so one long fsync on a slow drive would look like a
|
|
// dead worker to the engine. "k" every second says this process is still alive whatever it is doing.
|
|
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::string error;
|
|
{
|
|
Session session{options};
|
|
try {
|
|
check_last(session);
|
|
if (session.error.empty()) check_first(session);
|
|
if (session.error.empty()) {
|
|
if (const uint64 total = declared_total(options)) say("d " + std::to_string(total));
|
|
ErrHandler.Clean();
|
|
error = run(session);
|
|
} else {
|
|
error = session.error;
|
|
}
|
|
} catch (RAR_EXIT code) {
|
|
error = !session.error.empty() ? session.error : session.writer.error();
|
|
if (error.empty() && !cancelled) error = code == RARX_MEMORY ? too_much_memory : describe(ERAR_UNKNOWN);
|
|
} catch (std::bad_alloc&) {
|
|
error = too_much_memory;
|
|
} catch (...) {
|
|
error = describe(ERAR_UNKNOWN);
|
|
}
|
|
if (error.empty() && !cancelled) say("s");
|
|
const auto written = session.writer.finish();
|
|
if (error.empty()) error = written;
|
|
const Writer& w = session.writer;
|
|
const auto ms = [](uint64 ns) { return std::to_string(ns / 1000000) + "ms"; };
|
|
std::string timings = "t threads=" + std::to_string(pick_threads(options.threads)) + " cores=" + std::to_string(cores())
|
|
+ " write=" + ms(w.write_ns) + "/" + std::to_string(w.writes) + " fsync=" + ms(w.sync_ns) + "/" + std::to_string(w.syncs)
|
|
+ " waited=" + ms(w.wait_ns);
|
|
if (const uint64 cpu = process_cpu_us()) timings += " cpu=" + ms(cpu * 1000);
|
|
say(timings);
|
|
// The precise cause, rather than the checksum or header failure it leads to.
|
|
if (!error.empty() && session.error.empty() && !cancelled) {
|
|
if (session.missing_password) error = describe(ERAR_MISSING_PASSWORD);
|
|
else if (session.missing_volume) error = describe(ERAR_EOPEN);
|
|
}
|
|
// Which volume, so that one alone is downloaded again: "... Volume 6 of 12 looks incomplete." A volume
|
|
// check's reason stands as it is. A last volume cut in its end block fails with no more precise reason.
|
|
const bool early_end = error == describe(ERAR_EOPEN) || error == describe(ERAR_EREAD) ||
|
|
(session.last_cut && error == describe(ERAR_UNKNOWN));
|
|
if (!cancelled && session.error.empty() && session.options.sources.size() > 1 && (early_end || error == describe(ERAR_BAD_DATA)))
|
|
if (const size_t volume = suspect_volume(session, early_end))
|
|
error += " Volume " + std::to_string(volume) + " of " + std::to_string(session.options.sources.size()) + " looks incomplete.";
|
|
}
|
|
{
|
|
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;
|
|
}
|