Merge Ε-W2: bank export and bank import, both verbs and both panel rows
Union of two parallel tracks. Both action rows, both menu rows, both link edges survive; the two package CLAUDE.md files now describe the post-merge reality rather than either side's pre-merge scope.
This commit is contained in:
+32
-10
@@ -7,7 +7,7 @@ unit-tested outside the DAW): the format contract and version ladder, the JSON
|
||||
manifest, and the framing/layout codec. No filesystem — the shell
|
||||
(`src/shell/package`) streams bytes against the layouts produced here. The
|
||||
export/import *decisions* (`export_plan` / `import_plan`) are separate modules;
|
||||
`export_plan` has landed, `import_plan` has not.
|
||||
both have landed.
|
||||
|
||||
## Invariants
|
||||
|
||||
@@ -79,6 +79,14 @@ export/import *decisions* (`export_plan` / `import_plan`) are separate modules;
|
||||
(missing / unreadable / an index record the format cannot represent). Owns the
|
||||
name repair the codec's refusal backstops, and normalizes each shipping record's
|
||||
`relativePath` to the bare package name — see the transport-name gotcha below.
|
||||
- `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, 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
|
||||
@@ -97,6 +105,11 @@ export/import *decisions* (`export_plan` / `import_plan`) are separate modules;
|
||||
deliberately NOT enforced by the codec — they are `import_plan` decisions. The
|
||||
codec rejects only what makes the container itself incoherent (duplicate
|
||||
entry names, invalid names, a non-single-sample nested index).
|
||||
- **`import_plan` consults no other bank's hashes, and that is the ruling, not
|
||||
an omission.** An import always creates a NEW bank, so "already present in the
|
||||
destination bank by content" is exactly "already landed by this same plan".
|
||||
Cross-bank dedup is not enforced anywhere (`core/model/CLAUDE.md`), so a hash
|
||||
the pool already holds still lands its own file here.
|
||||
- `requiredPrefixSize` trusts fields beyond the frozen region only when the
|
||||
version pair classifies `Readable`; for `TooNew` it stops at the semver —
|
||||
don't "fix" it to read the manifest length there, a future structural format
|
||||
@@ -155,18 +168,27 @@ export/import *decisions* (`export_plan` / `import_plan`) are separate modules;
|
||||
collision class as the ASCII case fold, which `sameEntryName` does catch. A
|
||||
table-free fix does not exist, and restricting names to ASCII would be
|
||||
genuinely over-strict for non-English users. Left open knowingly.
|
||||
- **`duplicateName` is O(n²) over `entries` on the decode path** — pre-existing
|
||||
shape (the double loop is unchanged since `af35fc5`; only the comparator
|
||||
changed). Under the `kMaxManifestBytes` cap (64 MB) a minimal entry is
|
||||
~100 bytes, so a hostile package can declare ~670k entries — ~2×10¹¹ pair
|
||||
comparisons, a multi-minute hang on import. It signals an error rather than
|
||||
UB, so the hostile-input invariant above still holds, but it sits against
|
||||
this module's "a forged header cannot demand gigabytes" posture. Forward
|
||||
obligation for `import_plan`: fold this into a sorted vector or hash set
|
||||
when that track lands; not changed here.
|
||||
- **`duplicateName` folds through a hash set, not a pairwise scan.** Under the
|
||||
`kMaxManifestBytes` cap (64 MB) a minimal entry is ~100 bytes, so a hostile
|
||||
package can declare ~670k entries; the former double loop was ~2×10¹¹ pair
|
||||
comparisons — a multi-minute hang on the decode path an import drives. The
|
||||
set is keyed on `entryNameKey`, which is `sameEntryName`'s ASCII-case fold
|
||||
made explicit, so the equivalence rule still has one home (`lowerAscii`).
|
||||
Do not reintroduce the pairwise scan.
|
||||
- **Cross-module contract with `src/shell/package`:** a genuinely zero-length
|
||||
entry cannot round-trip through the filesystem seam there (`appendPayload`
|
||||
refuses an empty payload — an empty buffer signals an upstream read failure,
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user