Land the package filesystem shell: streaming atomic package_io, journaled rollback carve-out, asymmetric platform pickers

This commit is contained in:
2026-08-02 07:36:25 -04:00
parent 09a9ef838f
commit 41a3016e63
11 changed files with 914 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
// shell/package/package_io — streaming filesystem seam for bank packages: append one
// payload at a time through a temp-file + atomic-rename writer, seek and read one
// payload at a time back out. Bytes only: what a package contains is core/package's
// business, never this seam's. Blocking I/O — UI-thread actions only, never the
// audio thread.
#pragma once
#include <cstdint>
#include <fstream>
#include <string>
#include <vector>
namespace reasampler {
// One entry's payload, and the seam counter that makes "never more than one entry in
// memory" assertable: alive() counts every buffer currently holding bytes, so the
// streaming claim is a test CHECK against this counter rather than a memory
// measurement. Move-only — copying a payload would silently double the held bytes.
class PayloadBuffer {
public:
PayloadBuffer() = default;
explicit PayloadBuffer(std::vector<std::uint8_t> bytes);
~PayloadBuffer();
PayloadBuffer(PayloadBuffer&& other) noexcept;
PayloadBuffer& operator=(PayloadBuffer&& other) noexcept;
PayloadBuffer(const PayloadBuffer&) = delete;
PayloadBuffer& operator=(const PayloadBuffer&) = delete;
const std::uint8_t* data() const { return bytes_.data(); }
std::size_t size() const { return bytes_.size(); }
bool empty() const { return bytes_.empty(); }
const std::vector<std::uint8_t>& bytes() const { return bytes_; }
// Buffers currently holding at least one byte, process-wide.
static int alive();
private:
void release();
std::vector<std::uint8_t> bytes_;
bool counted_ = false;
};
// Streaming atomic writer. Bytes accumulate in "<dest>.rsbanktmp" beside the
// destination (same directory, so the final rename never crosses a volume); the
// destination itself is touched only by commit()'s rename, so a failed, aborted, or
// abandoned write leaves it absent or holding its prior contents — never a partial
// file. Destruction without commit() aborts and removes the temp. commit() REPLACES
// an existing destination: the export save dialog's own overwrite confirm is the
// consent (the never-overwrite rule for bank-folder files lives in
// LandedFileJournal, upstream of this writer). Neither copyable nor movable, and
// append-only — there is deliberately no way to hand it a whole package at once.
class PackageFileWriter {
public:
explicit PackageFileWriter(std::string destAbsPath);
~PackageFileWriter();
PackageFileWriter(const PackageFileWriter&) = delete;
PackageFileWriter& operator=(const PackageFileWriter&) = delete;
bool ok() const { return ok_; }
// Framing/header bytes. False on a failed or already-finished writer.
bool appendRaw(const std::uint8_t* data, std::size_t len);
// One entry's bytes. Same contract as appendRaw.
bool appendPayload(const PayloadBuffer& payload);
// Flush, close, rename over the destination. False (and self-cleaning: the temp
// is removed, the destination untouched) on any failure or on a second call.
bool commit();
// Close and remove the temp; the destination is never touched. Idempotent.
void abort();
const std::string& destPath() const { return destPath_; }
const std::string& tempPath() const { return tempPath_; }
private:
std::string destPath_;
std::string tempPath_;
std::ofstream out_;
bool ok_ = false;
bool done_ = false;
};
// Seek-and-read reader: exactly one payload is materialized per readRange call, and
// there is deliberately no read-whole-file entry point. Empty buffer on ANY failure
// — unopenable file, zero length, out of range, short read — so the caller has one
// "nothing to work with" branch (file_bytes' contract).
class PackageFileReader {
public:
explicit PackageFileReader(const std::string& srcAbsPath);
bool ok() const { return ok_; }
std::uint64_t fileSize() const { return size_; }
// Bytes [offset, offset+length). Range-checked against the real file size, so a
// hostile layout can never demand an allocation past the file's end.
PayloadBuffer readRange(std::uint64_t offset, std::uint64_t length);
private:
std::ifstream in_;
std::uint64_t size_ = 0;
bool ok_ = false;
};
// One source file read whole as one entry's payload — a bank file IS the streaming
// unit, so whole-file here is one entry, released before the next is read. Empty on
// any failure, per readFileBytes.
PayloadBuffer readFilePayload(const std::string& absPath);
// Bare file names (regular files only, never a path) in dirAbsPath, sorted so
// callers see a deterministic order; empty on a missing or unreadable folder.
std::vector<std::string> listFolderFileNames(const std::string& dirAbsPath);
} // namespace reasampler