import: remediate review findings — ledger gate, docs, message split

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.
This commit is contained in:
2026-08-02 14:02:05 -04:00
parent a927dad2f4
commit f8dde16a7e
20 changed files with 299 additions and 92 deletions
+4 -3
View File
@@ -3,6 +3,8 @@
#include <algorithm>
#include <unordered_set>
#include "core/util/ascii_ws.h"
// bank_book implementation — the registry RULES half: construction, pool
// privileges, bank lifecycle, active bank, sample movement/removal, slot order,
// and the reference queries. The JSON round-trip half lives in bank_book_json.cpp,
@@ -76,9 +78,8 @@ void BankBook::normalizeOrdinals() {
// one folding rule shared with bank_book_json.cpp's parse-time coalesce.
std::string BankBook::nameKey(const std::string& s) {
std::size_t b = 0, e = s.size();
auto isWs = [](char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; };
while (b < e && isWs(s[b])) ++b;
while (e > b && isWs(s[e - 1])) --e;
while (b < e && util::isAsciiWs(s[b])) ++b;
while (e > b && util::isAsciiWs(s[e - 1])) --e;
std::string out;
out.reserve(e - b);
for (std::size_t i = b; i < e; ++i) {
+16 -2
View File
@@ -76,8 +76,11 @@ landing after the format.
- `import_plan` — the pure import decision, and the reason the whole feature is
testable without a DAW: the destination bank's display name after
`BankBook`'s own fold, the reminted sample ids and remapped parents, and the
per-entry land / collapse / rename disposition. Also `importLedgerRefusal`,
the import's ledger gate.
per-entry land / collapse / rename disposition. Also `importLedgerRefusal` (the
import's ledger gate, delegating entirely to `tracking::ledgerDegraded`) and
`ledgerRefusalMessage` (the gate's console-block body, a pure
`(LedgerRefusal, namespace) -> string` fold the shell only supplies the
channel-correct namespace to).
- `bank_package` — framing and arithmetic composing the two above:
`encodePackage` (prefix bytes + layout + total size, stamping this build's
ladder pair and `version::stampVersion()`), `decodePackage` (prefix + observed
@@ -156,3 +159,14 @@ landing after the format.
not a real entry). `serializeManifest` refuses a zero-length `PackageEntry`
at encode so this layer never produces one; decode does not enforce it (a
hostile/older package declaring one is not this track's concern).
- **`import_plan`'s `spelledLikeABankFile` mints a fresh name even with NO
collision, and that third condition is a deliberate decision, not spec-derived.**
`docs/product/bank-package.md:447` ties the auto-rename mint to a *collision*
only; `spelledLikeABankFile` additionally mints whenever the package's own name
isn't spelled the way `deriveBankPaths` spells one (extension, sanitized stem).
Kept for two reasons: uniform folder spelling for every landed file regardless of
origin, and — the sharper one — a hostile entry name that isn't a legal Windows
filename or carries an unexpected extension (e.g. `evil.exe`) lands sanitized
(`evil_<tag>.wav`) rather than verbatim. `ImportPlan` counts this separately from a
genuine folder-name collision (`sanitizeRenameCount` vs `collisionRenameCount`) so
the summary line means what `bank-package.md:447` says it means.
+59 -8
View File
@@ -6,6 +6,7 @@
#include "core/capture/capture_paths.h"
#include "core/package/package_format.h"
#include "core/util/ascii_ws.h"
namespace reasampler::package {
@@ -14,10 +15,13 @@ namespace {
using capture::bankRelativeForName;
using capture::deriveBankPaths;
using capture::sanitizeStem;
using util::isAsciiWs;
// Shares BankBook::nameKey's whitespace set (core/util/ascii_ws.h) so a name nameKey
// would fold to empty is never treated as recorded here.
bool blankName(const std::string& s) {
for (char c : s)
if (c != ' ' && c != '\t' && c != '\n' && c != '\r') return false;
if (!isAsciiWs(c)) return false;
return true;
}
@@ -40,6 +44,14 @@ bool spelledLikeABankFile(const std::string& fileName) {
// The bank-folder names an import must not land on: what is there already, plus what
// this import has minted so far. Case-folded, because the two filesystems this tool
// ships on would treat "Kick.wav" and "kick.wav" as one file.
//
// `bankFolderFileNames` comes from `listFolderFileNames` (shell/package/package_io),
// which skips non-regular files — so a DIRECTORY sharing a bank file's name is
// invisible here. The plan then never mints around it, and the later exclusive-create
// land fails on that one entry (WriteFailed, rolled back). Safe direction ("never
// overwrite" still holds) but worth knowing before chasing a WriteFailed report that
// traces back to a same-named folder in the bank directory; test_import_landing's
// rollback suite deliberately exploits this to exercise the rollback path.
class NameSet {
public:
explicit NameSet(const std::vector<std::string>& present) {
@@ -71,13 +83,46 @@ std::string mintFileName(const std::string& projectDir, const std::string& packa
} // namespace
LedgerRefusal importLedgerRefusal(tracking::LedgerStatus status) {
switch (status) {
case tracking::LedgerStatus::Unreadable: return LedgerRefusal::Malformed;
case tracking::LedgerStatus::FutureVersion: return LedgerRefusal::FutureVersion;
case tracking::LedgerStatus::Fresh:
case tracking::LedgerStatus::Loaded: break;
// Delegates the refuse/proceed decision entirely to ledgerDegraded() rather than
// re-deriving it from the two named statuses, so a future degraded status added
// there is refused here too rather than silently falling through to None.
if (!tracking::ledgerDegraded(status)) return LedgerRefusal::None;
// Below this point status is known degraded; only the message variant is picked.
// Unreadable gets its own "corrupt, may be cleared" wording; every other degraded
// status (today only FutureVersion) gets the "written by a newer build" wording.
return status == tracking::LedgerStatus::Unreadable ? LedgerRefusal::Malformed
: LedgerRefusal::FutureVersion;
}
// Mirrors prune's abort block in structure and tone (shell/actions/prune_action.cpp),
// because a user who has hit that one should recognise this one. Every recovery line
// names THIS build's namespace: a beta user handed the stable spelling clears the wrong
// key and is still blocked.
std::string ledgerRefusalMessage(LedgerRefusal refusal, const std::string& extStateNamespace) {
if (refusal == LedgerRefusal::None) return {};
std::string msg =
"ReaSampler import: ABORTED -- the file-tracking ledger could not be read. "
"Nothing was imported.\n";
if (refusal == LedgerRefusal::Malformed) {
msg += "The stored file-tracking ledger is malformed. It has been left intact "
"rather than overwritten, so it can be repaired or cleared:\n"
" reaper.SetProjExtState(0, \"" + extStateNamespace + "\", \"owned_files\", \"\")\n"
"Clearing it makes every existing bank file un-reclaimable (they stop "
"being attributable to ReaSampler); no file is lost. Reopen the project "
"afterwards -- the block is held for the rest of this session.\n";
} else {
msg += "The stored file-tracking ledger was written by a NEWER version of "
"ReaSampler than this one, so its records cannot be read safely. It has "
"been left intact and will NOT be overwritten. Reopen the project with "
"that newer version -- do NOT clear this key from here, that would "
"discard tracking records this build cannot see. The block is held for "
"the rest of this session.\n";
}
return LedgerRefusal::None;
msg += "An import can land hundreds of files in one gesture. With no readable "
"ledger, none of them could be given a birth record, and every one would be "
"permanently unreclaimable.\n";
return msg;
}
std::string bankFolderDir(const std::string& projectDir) {
@@ -139,7 +184,13 @@ ImportPlan planImport(const PackageManifest& manifest,
idRemap[src.sample.id] = e.sample.id;
++plan.landCount;
if (e.renamed) ++plan.renameCount;
// A rename happens for one of two reasons: the package's own name was already
// taken (spelledLikeABankFile true but the mint's fast path lost the race to
// `taken`), or the name never qualified for that fast path at all (sanitize).
if (e.renamed) {
if (spelledLikeABankFile(src.fileName)) ++plan.collisionRenameCount;
else ++plan.sanitizeRenameCount;
}
plan.entries.push_back(std::move(e));
}
+13 -1
View File
@@ -32,6 +32,13 @@ enum class LedgerRefusal { None, Malformed, FutureVersion };
LedgerRefusal importLedgerRefusal(tracking::LedgerStatus status);
// The console-block body for a refusal — a pure (LedgerRefusal, namespace) -> string
// fold, so the wording is assertable without a DAW. `extStateNamespace` is the
// channel-correct namespace (`version::extStateNamespace()`) every recovery line must
// name, so a beta user is never handed the stable spelling. Empty string for None —
// callers only reach this once `importLedgerRefusal` has already returned a refusal.
std::string ledgerRefusalMessage(LedgerRefusal refusal, const std::string& extStateNamespace);
// What one manifest entry does when the import runs.
// - Land: write the payload under destFileName and add `sample`.
// - Collapse: an equal contentHash already lands in this same import, so the payload
@@ -56,7 +63,12 @@ struct ImportPlan {
model::SlotMap slots; // the package's slots over the reminted ids
int landCount = 0;
int collapseCount = 0;
int renameCount = 0;
// Two distinct triggers, counted separately (bank-package.md:447 defines the first
// as THE collision counter; conflating the second into it would misreport a mint
// that never collided as a collision).
int collisionRenameCount = 0; // the package's own name was already taken in the bank folder
int sanitizeRenameCount = 0; // the package's name was not spelled the way this tool spells
// a bank file (see spelledLikeABankFile, core/package/CLAUDE.md)
};
// The bank folder an import lands into — the same expression capture uses, so an
+3 -2
View File
@@ -3,8 +3,8 @@
## Scope
Tiny, dependency-free pure helpers linked by both artifacts: whole-file byte
loading, unit-interval clamping, the absolute-path rejection test, and the
per-segment envelope curve law.
loading, unit-interval clamping, the absolute-path rejection test, the
per-segment envelope curve law, and the ASCII-whitespace fold test.
## Modules
@@ -19,6 +19,7 @@ per-segment envelope curve law.
before curves existed play unchanged, and what the knob law's centre detent exists to keep
reachable from the dial.
- `relative_path` (`core/util`, header-only) — the ONE absolute-path rejection test behind the relative-paths-only invariant, shared by `bank_model` (`Sample.relativePath`) and `core/tracking/origin_ledger` (`OriginRecord.relativePath`). The two must reject identically or a path one accepts could be smuggled past the other; that is why it is one function and not two.
- `ascii_ws` (`core/util`, header-only) — the ONE ASCII-whitespace test (space/tab/CR/LF) behind `BankBook::nameKey`'s trim, shared by `core/package/import_plan`'s blank-bank-name fallback. Same rationale as `relative_path`: two independently-maintained copies could drift on what counts as blank.
## Gotchas
+10
View File
@@ -0,0 +1,10 @@
#pragma once
// ascii_ws — the ONE ASCII-whitespace test shared by every fold that must agree with
// BankBook::nameKey's trim (space/tab/CR/LF): a drifted second copy could accept a
// package bank name nameKey would treat as blank, or vice versa.
namespace reasampler::util {
inline bool isAsciiWs(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
} // namespace reasampler::util