Skip to content

fix(core): encode reserved path chars in file URIs#151

Open
dergachoff wants to merge 2 commits into
bug-ops:mainfrom
dergachoff:fix/file-uri-reserved-path-chars
Open

fix(core): encode reserved path chars in file URIs#151
dergachoff wants to merge 2 commits into
bug-ops:mainfrom
dergachoff:fix/file-uri-reserved-path-chars

Conversation

@dergachoff

@dergachoff dergachoff commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #150.

path_to_uri formatted file URIs as file://{path}. Valid filesystem paths containing URI-reserved characters, such as bracketed route filenames, produced invalid raw URI paths. lsp_types::Uri::from_str then returned UnexpectedChar; because this path used expect, diagnostics requests could panic and abort the MCP stdio server.

This builds platform-aware file URLs with Url::from_file_path, preserves the existing Windows-rooted synthetic test-path behavior, encodes RFC3986-invalid path characters that WHATWG URL leaves raw in the path ([, ], ^, |), and adds a regression test for a route-style filename with uri_to_path round-trip coverage.

Validation

  • cargo +nightly fmt --check
  • cargo +1.95 test -p mcpls-core path_to_uri
  • cargo +1.95 test -p mcpls-core
  • cargo +1.95 clippy -p mcpls-core --all-targets --all-features -- -D warnings
  • rustup target add --toolchain 1.95 x86_64-pc-windows-msvc && cargo +1.95 check -p mcpls-core --target x86_64-pc-windows-msvc --all-targets --all-features
  • Manual MCP stdio probe: before the patch, get_diagnostics on a bracketed route path aborted with failed to create URI from path: ParseError { ... UnexpectedChar }; after the patch, the same call returned a normal tool response.

@github-actions github-actions Bot added rust Rust code changes mcpls-core mcpls-core crate changes labels May 8, 2026
@dergachoff dergachoff force-pushed the fix/file-uri-reserved-path-chars branch from aceae78 to 07196a4 Compare May 8, 2026 19:03

@bug-ops bug-ops left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Review: changes requested

Critical (C1): is the enum discriminant , not the path byte offset. The test only passes because the path is long — fails silently for short paths like .

Minor (M1): Test covers only a long path; a short-path case would catch the bug.

Minor (M2): and backtick are RFC 3986 §2.2 reserved chars omitted from encoding. Unlikely to matter with WHATWG consumers, but worth tracking.

Comment thread crates/mcpls-core/src/bridge/state.rs Outdated
}

fn encode_rfc3986_path_chars(uri: &str) -> String {
let path_start = url::Position::BeforePath as usize;

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[C1 — critical] url::Position::BeforePath is an enum discriminant (10), not a byte offset in the URI string. For file:///[a].ts the [ sits at byte 8 — it falls in the "prefix" slice and is never encoded. The stated motivation for this fix (Next.js [slug] files) silently fails.

Use the Index trait that the url crate actually provides for this:

fn encode_rfc3986_path_chars(uri: &str) -> String {
    let url = Url::parse(uri).expect("encode called with invalid URI");
    let prefix = url[..url::Position::BeforePath].to_owned();
    let encoded = url[url::Position::BeforePath..]
        .replace('[', "%5B")
        .replace(']', "%5D")
        .replace('^', "%5E")
        .replace('|', "%7C");
    format!("{prefix}{encoded}")
}

#[cfg(windows)]
let path = Path::new(r"C:\home\user\routes\api\[...]^|.ts");
#[cfg(not(windows))]
let path = Path::new("/home/user/routes/api/[...]^|.ts");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[M1 — minor] Only a long path is tested; the split_at(10) bug does not surface here. Add a short case:

let path = Path::new("/[a].ts");
let uri = path_to_uri(path);
assert!(uri.as_str().ends_with("%5Ba%5D.ts"));

Comment thread crates/mcpls-core/src/bridge/state.rs Outdated
fn encode_rfc3986_path_chars(uri: &str) -> String {
let path_start = url::Position::BeforePath as usize;
let (prefix, path) = uri.split_at(path_start);
let encoded_path = path

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

[M2 — minor] {, }, and backtick are also RFC 3986 §2.2 reserved characters omitted here. Low risk for WHATWG-compliant consumers (rust-analyzer, pyright), but silently wrong for strict RFC 3986 clients. Tracking in a follow-up issue.

Address review: url::Position::BeforePath as usize is the enum
discriminant, not a byte offset, so reserved chars in short paths
escaped encoding. Slice the parsed Url with the Index<Range<Position>>
impl instead, and add a short-path regression test.
@dergachoff dergachoff force-pushed the fix/file-uri-reserved-path-chars branch from 07196a4 to a9a9c35 Compare July 6, 2026 08:15
@dergachoff

Copy link
Copy Markdown
Contributor Author

Thanks — confirmed and fixed in a9a9c35 (branch also rebased onto current main).

  • C1: Applied your suggested implementation — slice the parsed Url via the Index<Position> impls instead of as usize. I verified the new short-path test fails against the previous code with exactly the panic you predicted.
  • M1: Added test_path_to_uri_percent_encodes_reserved_chars_in_short_path covering /[a].ts (C:\[a].ts on Windows) with a round-trip assertion.
  • M2: After the C1 fix this is moot in practice: the function now re-parses through Url::parse, whose WHATWG path percent-encode set already encodes `, {, and } (PATH set in url 2.5.8 parser.rs). Verified: Url::parse("file:///a`b{c}d.ts") serializes as file:///a%60b%7Bc%7Dd.ts, so only [ ] ^ | need the manual pass. Happy to add explicit replacements anyway if you'd prefer not to depend on the url crate's encode set.

Local validation: cargo +nightly fmt --check, clippy --all-targets --all-features -D warnings, cargo nextest run (403 passed), cargo check --target x86_64-pc-windows-msvc --all-targets.

@dergachoff

Copy link
Copy Markdown
Contributor Author

Btw, heads up on broken CodeQL checks. f5d044b bumped actions/checkout to v7, which now refuses to check out fork PR code inside pull_request_target workflows unless the step sets allow-unsafe-pr-checkout: true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mcpls-core mcpls-core crate changes rust Rust code changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

get_diagnostics can abort stdio server for file paths containing URI-reserved chars

2 participants