feat(npm): streampub envelope bounds + attachment shape (part 4/8)
📦 What this MR does
Part 4/8 of the stack splitting the streampub streaming publish-envelope library (S11 Step 13). See Part 1/8 (!432) for the stack overview and the byte-identity contract against the reference, !420 (closed).
- Targets
10io/npm-local-step-13-part3of8(Part 3/8). GitLab retargets tomainas the parent merges. - Depends on Part 3/8 (set as a merge-request dependency).
This part threads the five envelope memory-budget bounds back through the tokenizer and adds the attachment structural walk and the cross-field shape validation. The streaming attachment pipeline (multi-hash + gzip/tar inspector) is the only remaining major piece.
Bounds — all enforced incrementally during tokenization, so the offending structure is never fully materialized:
token.go—readTokennow enforces the depth cap (MaxEnvelopeDepth) and the per-object key cap (enforceObjectKeyCap/MaxEnvelopeObjectKeys) on every token.versions.go—readVersionValueenforces the per-version byte-span cap (MaxEnvelopePackageJSONSize), aborting on the first byte past it (S11 AC 43).walker.go—readDistTagsenforces the dist-tags count cap (MaxEnvelopeDistTags). The walker gainslim+ thesaw*flags;newWalkertakesLimits.
Attachment shape + validation:
attachment.go(new) —readAttachments/readAttachmentObject/attachmentFileName.readAttachmentObjectskips thedatavalue structurally here; the streaming hand-off replaces that one line in a later part.walker.go—validateShape(name present + valid, exactly one version, one attachment whose key matches{plain_name}-{version}.tgz), called after the walk;dispatchTopLevelroutes_attachments.error.go—errEnvelopeBound+ the four bound codes, referenced for the first time here.
🧪 Tests
bounds_test.go(new): the five bounds —EnvelopePackageJSONTooLarge(AC 43, with the abort-offset assertion),EnvelopeTooDeep(AC 44),EnvelopeTooManyKeys(AC 45),EnvelopeDistTagsTooMany(AC 46),TwoVersionsAbortsEarly(AC 47) — plusFailsClosedOnInvalidLimits.TestProcess_EnvelopeShapeInvalid(AC 38, unit half): missing name / versions / attachments and attachment-key mismatch.- The trailing-field depth-bomb test (
TestProcess_TrailingFieldBoundBypass) needs the streaming tail scanner (consumeToEnvelopeEnd) and lands with it in a later part.
This closes the five "deferred bound" items flagged on Parts 2–3: the bounds land here with the real configurable caps rather than as interim ceilings.
golangci-lint(repo-pinned v2.12) → 0 issuesGOEXPERIMENT=jsonv2 go test -race -count=1 ./internal/format/npm/streampub/...→ pass (10 tests)- Diff is 489 added / 462 net lines — within the 500-LOC reviewability guideline.
🚩 Deviations from the reference (!420 (closed))
Carries forward the Part 1/8 error.go API improvements (type ProcessError, unexported code / wrapped), the Part 2/8 six-cap validate, and the Part 3/8 test-label fix. token.go and versions.go reach their reference form here (modulo the *ProcessError rename in abortErr/mapDecodeErr).
New deviations in this part (from review):
- Attachment structural walk —
readAttachmentObjectconsumes the whole attachment object andreadAttachmentsconsumes the_attachmentsclosing brace, rejects a second entry (mirroringreadVersions' single-entry guard), and setsAttachmentKey/sawAttachmentsonly after the object parses. The reference instead handsdataoff to the streaming pipeline, which consumes the envelope remainder itself; that is a later part. So this part'sattachment.goand thekeyDataconstant are transient and are replaced when the streaming hand-off lands — the final tree matches the reference. - Stale comment fix —
TestProcess_FailsClosedOnInvalidLimits's "this test FAILS today" note (a test-author-phase artifact) is corrected;Processalready validatesLimitsup front, so the test passes. - Added test coverage (
94168d2): an "empty attachments object" case inTestProcess_EnvelopeShapeInvalidassertspublish_envelope_invalidfor"_attachments":{}, exercising thereadAttachmentsempty-guard. That guard is byte-identical in the reference but untested there (the input appears only as aFuzzProcessseed, which asserts no-panic, not the spec code). Test-only; no production change.
🔗 References
- Reference MR (read-only): !420 (closed)
- Related work item: #131 (closed) (not closed by this MR)