f8dde16a7e
Delegates the refuse-gate to ledgerDegraded(), lifts its console message into a pure testable fold, fixes stale doc line citations and an inaccurate outcome-enum comment, and splits the rename counter into collision-vs-sanitize.
150 lines
6.1 KiB
C++
150 lines
6.1 KiB
C++
// import_landing.cpp — see import_landing.h for the contract. REAPER-free: standard
|
|
// filesystem only, so every property this file decides is unit-testable.
|
|
|
|
#include "shell/package/import_landing.h"
|
|
|
|
#include <cassert>
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <system_error>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "core/capture/wav_codec.h" // hashBytes — the digest the manifest records
|
|
#include "core/package/bank_package.h"
|
|
#include "shell/package/package_io.h"
|
|
#include "shell/package/package_path.h"
|
|
|
|
namespace reasampler {
|
|
|
|
namespace fs = std::filesystem;
|
|
using package::EntryAction;
|
|
using package::PackageEntrySpan;
|
|
|
|
namespace {
|
|
|
|
ImportLanding refusal(ImportOutcome outcome, const package::PackageHeader& header) {
|
|
ImportLanding out;
|
|
out.outcome = outcome;
|
|
out.header = header;
|
|
return out;
|
|
}
|
|
|
|
// Reads the package head incrementally: requiredPrefixSize may grow its answer as
|
|
// fields arrive, so ask, read to the count, ask again. False means these bytes can
|
|
// never frame a package, or the file is shorter than its own header claims.
|
|
bool readPrefix(PackageFileReader& reader, std::vector<std::uint8_t>& prefix) {
|
|
for (;;) {
|
|
const auto need = package::requiredPrefixSize(prefix);
|
|
if (!need) return false;
|
|
if (prefix.size() >= *need) return true;
|
|
PayloadBuffer head = reader.readRange(0, *need);
|
|
if (head.size() != *need) return false;
|
|
prefix.assign(head.data(), head.data() + head.size());
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ImportLanding landPackage(const std::string& packageAbsPath,
|
|
const std::string& projectDir,
|
|
const BankBook& destination,
|
|
const std::string& uniqueTag,
|
|
LandedFileJournal& journal) {
|
|
const package::PackageHeader noHeader;
|
|
if (projectDir.empty()) return refusal(ImportOutcome::NoProject, noHeader);
|
|
|
|
PackageFileReader reader(packageAbsPath);
|
|
if (!reader.ok()) return refusal(ImportOutcome::Unreadable, noHeader);
|
|
|
|
std::vector<std::uint8_t> prefix;
|
|
if (!readPrefix(reader, prefix)) return refusal(ImportOutcome::Malformed, noHeader);
|
|
|
|
const package::DecodedPackage dec = package::decodePackage(prefix, reader.fileSize());
|
|
if (dec.status == package::PackageReadability::TooNew)
|
|
return refusal(ImportOutcome::TooNew, dec.header);
|
|
if (dec.status != package::PackageReadability::Readable)
|
|
return refusal(ImportOutcome::Malformed, dec.header);
|
|
|
|
const std::string bankDir = package::bankFolderDir(projectDir);
|
|
|
|
ImportLanding out;
|
|
out.header = dec.header;
|
|
out.plan = package::planImport(dec.manifest, destination, projectDir,
|
|
listFolderFileNames(bankDir), uniqueTag);
|
|
|
|
// Integrity first, over EVERY declared entry — including one the plan collapses,
|
|
// since a package that fails its own digest is refused whole rather than partly
|
|
// trusted. Nothing is on disk yet, so a failure here needs no rollback.
|
|
for (std::size_t i = 0; i < dec.layout.size(); ++i) {
|
|
const PackageEntrySpan& span = dec.layout[i];
|
|
// The format refuses a zero-length entry on encode; one arriving anyway cannot
|
|
// be told from a failed read at this seam, so it is not well-formed input.
|
|
if (span.length == 0) return refusal(ImportOutcome::Malformed, dec.header);
|
|
|
|
PayloadBuffer payload = reader.readRange(span.offset, span.length);
|
|
if (payload.size() != span.length) {
|
|
out.outcome = ImportOutcome::IntegrityFailed;
|
|
out.failedEntryName = span.name;
|
|
return out;
|
|
}
|
|
if (capture::hashBytes(payload.data(), payload.size()) !=
|
|
dec.manifest.entries[i].byteHash) {
|
|
out.outcome = ImportOutcome::IntegrityFailed;
|
|
out.failedEntryName = span.name;
|
|
return out;
|
|
}
|
|
}
|
|
|
|
std::error_code ec;
|
|
fs::create_directories(utf8Path(bankDir), ec); // idempotent; the write reports failure
|
|
|
|
for (const package::PlannedEntry& e : out.plan.entries) {
|
|
if (e.action != EntryAction::Land) continue;
|
|
const PackageEntrySpan& span = dec.layout[e.manifestIndex];
|
|
PayloadBuffer payload = reader.readRange(span.offset, span.length);
|
|
if (payload.size() == span.length &&
|
|
journal.writeLandedFile(bankDir + "/" + e.destFileName, payload)) {
|
|
continue;
|
|
}
|
|
out.outcome = ImportOutcome::WriteFailed;
|
|
out.failedEntryName = e.destFileName;
|
|
out.rollback = journal.rollback();
|
|
return out;
|
|
}
|
|
|
|
out.outcome = ImportOutcome::Landed;
|
|
return out;
|
|
}
|
|
|
|
bool applyImportedBank(BankBook& book, const std::string& bankId,
|
|
const package::ImportPlan& plan, const RecordBirth& recordBirth) {
|
|
// An empty std::function throws std::bad_function_call on invoke; every real caller
|
|
// supplies one, so an empty one here is a caller bug, not a runtime condition to
|
|
// recover from — enforce the contract rather than let it surface as an uncaught
|
|
// exception out of an extension action.
|
|
assert(recordBirth && "applyImportedBank: RecordBirth must not be empty");
|
|
if (!book.createBank(bankId, plan.bankDisplayName)) return false;
|
|
|
|
BankModel* index = book.index(bankId);
|
|
for (const package::PlannedEntry& e : plan.entries) {
|
|
if (e.action != EntryAction::Land) continue;
|
|
const AddResult added = index->add(e.sample);
|
|
// planImport already deduped Land entries by hash against an empty destination
|
|
// bank (this same freshly-created one), so a Collapsed add here would mean the
|
|
// plan and the book disagree — that would silently undercount reportSuccess's
|
|
// landedCount rather than fail loudly.
|
|
assert(added == AddResult::Added && "planImport's Land entries must not collapse");
|
|
(void)added;
|
|
// Unconditional on the add's outcome: the file exists either way, and an
|
|
// unrecorded file is permanently unreclaimable.
|
|
recordBirth(e.sample);
|
|
}
|
|
|
|
book.bank(bankId)->slots = plan.slots;
|
|
book.reconcileSlots();
|
|
return true;
|
|
}
|
|
|
|
} // namespace reasampler
|