// 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 #include #include #include #include #include #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& 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 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