import: a .rsbank lands as a new bank, whole or not at all
Four collisions answered explicitly: ids reminted, names never overwritten, content deduped before the write, bank name auto-suffixed. Degraded ledger refuses before the picker.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// 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 <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) {
|
||||
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;
|
||||
index->add(e.sample);
|
||||
// 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
|
||||
Reference in New Issue
Block a user