Skip to main content

validate_path_placeholder

Function validate_path_placeholder 

Source
pub fn validate_path_placeholder(
    param: &str,
    value: &str,
    rules: &PlaceholderRules<'_>,
) -> Result<(), PlaceholderRefusal>
Expand description

The D4 path-placeholder floor, re-exported from core pmcp.

There is exactly ONE implementation of these rules and it lives in pmcp::server::schema_validation. This is a pub use, never a second copy (Phase 128, Q2). Two reasons the home is core rather than here:

  1. The toolkit’s curated http build has no pmcp-code-mode edge and must not gain one (SC-1), so the shared rule cannot live in this crate.
  2. This repo has a documented three-way-drift incident from a security rule that existed in more than one copy, so a second denylist is a prohibited shape rather than a style preference.

The re-export exists because D-09 obliges the SDK to publish the helper under the name a third-party HttpExecutor implementor would look for. An implementor whose template syntax is not OpenAPI’s {key} can call pmcp_code_mode::validate_path_placeholder on each value it substitutes and pmcp_code_mode::validate_resolved_path on the composed result — or pmcp_code_mode::validate_resolved_target, which is that rule widened by the single author-written ? separator, and is what ResolvedPath::from_checked itself calls. Either reaches the same rule the SDK itself applies before calling HttpExecutor::execute_request. (Plain backticks, not an intra-doc link: executor is gated on js-runtime and the link would not resolve in a default-feature doc build.) Validate ONE path-placeholder value before substitution (D4).

This is the single copy of the D4 character floor in the SDK. It lives in core pmcp — not in pmcp-code-mode — because BOTH HTTP surfaces must reach it: the curated single-call build resolves to the toolkit’s http feature, whose dependency list carries no pmcp-code-mode edge (RESEARCH Finding 6), so a helper exported only from there would force either a new dependency edge that widens the curated graph or two copies of one security rule — the drift class this repo has already been bitten by. D-09’s published-helper obligation is met by a pub use re-export from pmcp-code-mode.

§The four steps, and why the order is load-bearing

  1. Unconditional floor, which no declared pattern can relax (D-10). It is implemented as DECODE ONCE, THEN DENY — never as an enumerated denylist of literal and pre-encoded spellings, because an enumeration is incomplete by construction: a mixed form such as .%2E or %2E. decodes to the parent-directory sequence while matching neither the literal nor the fully-encoded spelling. Enumerating more spellings does not converge; decoding does.
  2. Always-on cap at PLACEHOLDER_MAX_LENGTH code points (D-08).
  3. Declared pattern narrows — evaluated through cached_input_validator, so a placeholder pattern and an inputSchema pattern resolve \s through the identical engine AND a repeated pattern compiles once.
  4. Declared length narrows further; a declared length larger than the module constant never widens it.

Running the floor FIRST is empirically justified, not stylistic: a spec pattern of ^.*$ accepts every CR-01 payload (measured, RESEARCH Finding 5b), so pattern-supersedes-floor would have silently disabled the check.

This is a pure function holding no shared mutable state of its own, so two concurrent Code Mode calls on one executor cannot interleave placeholder state.

value is the value ALREADY RENDERED to a string by the caller (the toolkit’s render_scalar), so the floor and the cap see the rendered text rather than a JSON number or bool.

§Not sufficient on its own

Per-value checks cannot establish final-path safety. Every caller MUST also run validate_resolved_path on the composed path before dispatch.

§Errors

Err(PlaceholderRefusal) naming the DECLARED parameter and the DECLARED expectation, never the value.