feat(npm): S11 validation helpers (Step 7)

What

Step 7 of the npm-local plan (docs/plans/2026-05-11-npm-local.md): the pure validation helpers in internal/format/npm/validate.go that back the publish, dist-tag, and download handlers (wired in later steps). Dependency-free pure functions:

  • ValidatePackageName / ParseScope - npm name rules (1-214 bytes, lowercase, leading-char, whitespace, URL-safe / encodeURIComponent, forbidden ~'!()*, reserved set); scope stored with the @ sigil.
  • ParseVersion / ValidateVersion / Version.String - semver 2.0.0, build metadata preserved byte-for-byte.
  • ValidateTagName - rejects empty / over-255-byte names, semver-shaped names (validRange), and NUL/control bytes.
  • ValidateFileName - {plain_name}-{version}.tgz shape + path-traversal reject.

Spec / acceptance

Implements the S11 "Validation rules" section. Covers AC 23 (tag_name_invalid), AC 39 unit half (package_name_invalid), AC 40 (version_invalid), and the AC 53 path-traversal unit foundation (integration half lands in Step 9).

Tests

Table-driven units with >=1 positive + negative case per rule, the build-metadata byte-for-byte round-trip, the uint64-overflow boundary, and NUL/control-byte cases. Adds FuzzParseVersion (no-panic + round-trip), run in CI via fuzz:npm.

Size note

~1385 LOC, above the 400-LOC target. It is one indivisible unit: the validators share helpers (splitScoped, isNumeric, ...) and a single table-driven test file, and ~250 of the impl lines are doc comments. Splitting per-validator would create artificial seams. History uses the test-first split (test(npm) skeleton -> feat(npm) impl) plus the review fix-ups.

Merge request reports

Loading
Loading