Skip to content

perf: single-scan fast-path guard for resolveDotSegments#1458

Merged
pi0 merged 6 commits into
mainfrom
perf/resolve-dot-segments-fast-path
Jul 14, 2026
Merged

perf: single-scan fast-path guard for resolveDotSegments#1458
pi0 merged 6 commits into
mainfrom
perf/resolve-dot-segments-fast-path

Conversation

@pi0x

@pi0x pi0x commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

What

Collapses resolveDotSegments' fast-path guard — previously 2 regex tests + 2 includes scans (\\, ENCODED_SEP_RE, //, DOT_SEGMENT_RE) — into a single scan of one of four mode-indexed trigger regexes ((decodeSlashes?1:0) | (mergeSlashes?2:0)).

  • The per-mode regexes are built from shared source strings (DOT_SEGMENT_SRC, ENCODED_SEP_SRC), so each trigger pattern still lives in exactly one place.
  • Each mode's regex contains exactly its own triggers, so a non-match remains an exact no-op guarantee (no cross-mode false positives, no behavior change).
  • On the (rare) slow path, the hasEncodedSep pre-test becomes an opt-gated global replace — a no-op when nothing matches.

Why

resolveDotSegments runs per request in serveStatic and downstream security consumers (h3-rules calls it on every matched request for dual-path rule matching), and the overwhelmingly common input is an already-canonical path — the guard is the hot path.

Benchmarks (mitata, node 24, x64):

case before after
plain path, no opts 37 ns 29 ns
plain path, { decodeSlashes, mergeSlashes } 89 ns 38 ns (-58%)
/.well-known/security.txt, both opts 84 ns 34 ns
traversal (slow path), both opts 878 ns 849 ns (noise)

Verification

  • Differential fuzz vs the previous implementation: 966,736 comparisons across all four option modes over adversarial fragment combinations (%252e, %25%32%66, \, //, ..., dot-prefixed names) — zero divergence.
  • New directed tests pin the boundary-aware edge the combined regexes must keep: /.well-known/…, /a/..b/c, /a%2eb/c, /a/... stay untouched in every mode.
  • Full suite: 1437 tests + typecheck + lint pass.

Follow-up

Planned next: export the guard as a public isCanonicalPath(path, opts) predicate (pinned by a property test against resolveDotSegments identity) so security-adjacent callers like h3-rules can skip canonicalization calls entirely without duplicating decode knowledge.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Exposed a new isCanonicalPath(path, opts?) utility to let you check whether path resolution would leave the input unchanged.
  • Bug Fixes
    • Improved path normalization/canonicality handling so dot-prefixed segment names (including encoded dot-leading forms) reliably remain unchanged across supported processing modes.
    • Preserved correct ./.. resolution, slash decoding, and optional slash merging behavior.
  • Documentation
    • Expanded Path security documentation with precise “canonical” conditions for isCanonicalPath.
    • Added “v2 TODOs” for request-body size safeguards and removal of a legacy unseal fallback.
  • Tests
    • Added targeted unit coverage and fuzzing to validate canonical-path behavior.

pi0 and others added 3 commits July 14, 2026 07:30
Collapse the fast-path guard (2 regex tests + 2 includes scans) into a
single scan of one of four mode-indexed trigger regexes, built from
shared source strings so each trigger pattern still lives in one place.
Each mode's regex contains exactly its own triggers, so a non-match
remains an exact no-op guarantee.

Fast path with { decodeSlashes, mergeSlashes }: 89ns -> 38ns (-58%);
plain mode 37ns -> 29ns; slow path unchanged. Differential fuzz vs the
previous implementation: 966k comparisons across all four modes, no
divergence.

Co-Authored-By: Claude Fable 5 <[email protected]>
@pi0x
pi0x requested a review from pi0 as a code owner July 14, 2026 08:59
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Path normalization

Layer / File(s) Summary
Trigger detection and normalization
src/utils/path.ts, test/unit/path.test.ts
Adds mode-aware canonical-path detection, preserves dot-prefixed non-special segments, and tests resolver equivalence across option modes.
Public path API and documentation
src/index.ts, docs/2.utils/4.security.md, test/unit/package.test.ts, test/unit/path.test.ts
Exports and documents isCanonicalPath, and updates public-export and behavior coverage.

v2 planning

Layer / File(s) Summary
v2 TODO entries
v2.md
Adds TODOs for request-body size limits and removal of the legacy PBKDF2 fallback.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • h3js/h3#1428: Introduces the related resolveDotSegments utility and options.
  • h3js/h3#1430: Modifies overlapping dot-segment and encoded-separator handling.

Suggested reviewers: pi0

Poem

I hop through dots, both near and far,
Keeping ... just as they are.
One regex guards each path’s bright trail,
While future TODOs gently sail.
Squeak—canonical routes prevail!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main performance change to resolveDotSegments and the single-scan fast-path guard.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/resolve-dot-segments-fast-path

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
v2.md (2)

4-4: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Tie fallback removal to concrete migration criteria.

“After a migration window” should be tied to the maximum legacy-cookie/session lifetime and fallback-use telemetry. Otherwise, removing it can invalidate active sessions, while retaining it unnecessarily prolongs weak-KDF compatibility and extra CPU work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v2.md` at line 4, Update the migration guidance for removing the iterations:
1 fallback in unsealSession and the legacySealFallback option to require both
expiration of the maximum legacy-cookie/session lifetime and confirmation from
fallback-use telemetry that no legacy sessions remain active. State these
concrete criteria instead of using only an unspecified migration window.

3-3: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Define the cap override contract before implementation.

readValidatedBody and defineJsonRpcHandler currently do not share a maxSize option. Also, Infinity would provide an easy way to reintroduce unbounded buffering. Specify how the option propagates and restrict unlimited reads to an explicit trusted/unsafe path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v2.md` at line 3, Define the request-body limit contract before implementing
the cap: add and consistently propagate a maxSize option through readBody,
readValidatedBody, and defineJsonRpcHandler, defaulting to DEFAULT_BODY_LIMIT.
Do not allow Infinity through the general option; expose unlimited buffering
only via an explicitly named trusted/unsafe API or path, and ensure that path is
intentional and clearly distinguishable.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@v2.md`:
- Line 4: Update the migration guidance for removing the iterations: 1 fallback
in unsealSession and the legacySealFallback option to require both expiration of
the maximum legacy-cookie/session lifetime and confirmation from fallback-use
telemetry that no legacy sessions remain active. State these concrete criteria
instead of using only an unspecified migration window.
- Line 3: Define the request-body limit contract before implementing the cap:
add and consistently propagate a maxSize option through readBody,
readValidatedBody, and defineJsonRpcHandler, defaulting to DEFAULT_BODY_LIMIT.
Do not allow Infinity through the general option; expose unlimited buffering
only via an explicitly named trusted/unsafe API or path, and ensure that path is
intentional and clearly distinguishable.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 56ab266a-ef25-4cb5-897b-34abac063f05

📥 Commits

Reviewing files that changed from the base of the PR and between ca7de07 and 1378d0c.

📒 Files selected for processing (3)
  • src/utils/path.ts
  • test/unit/path.test.ts
  • v2.md

@pi0 pi0 changed the title perf(resolveDotSegments): single-scan fast-path guard perf: single-scan fast-path guard for resolveDotSegments Jul 14, 2026
@pi0 pi0 changed the title perf: single-scan fast-path guard for resolveDotSegments perf: single-scan fast-path guard for resolveDotSegments Jul 14, 2026
@pi0
pi0 merged commit ee885c3 into main Jul 14, 2026
9 checks passed
@pi0
pi0 deleted the perf/resolve-dot-segments-fast-path branch July 14, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants