Skip to content

fix(cua-driver): percent-encode non-ASCII paths in launch_app urls - #1574

Closed
hippoley wants to merge 2 commits into
trycua:mainfrom
hippoley:fix/launch-app-cjk-url-encoding
Closed

fix(cua-driver): percent-encode non-ASCII paths in launch_app urls#1574
hippoley wants to merge 2 commits into
trycua:mainfrom
hippoley:fix/launch-app-cjk-url-encoding

Conversation

@hippoley

@hippoley hippoley commented May 19, 2026

Copy link
Copy Markdown
Contributor

Fixes #1519.

launch_app crashed the daemon when urls contained CJK / Cyrillic / accented characters (e.g. /Users/me/器材控/templates/file.key).

Root cause: resolveLaunchURL() used URL(string:) to detect the scheme, which returns nil for bare non-ASCII characters. The fallback URL(fileURLWithPath:) path was reached, but without .standardizedFileURL the resulting URL could carry unencoded bytes that LaunchServices rejected mid-request, disconnecting the daemon.

Fix:

  • Extract resolveLaunchURL into CuaDriverCore/Apps/URLResolver.swift as a public function so it can be unit-tested independently.
  • For plain file paths: use URL(fileURLWithPath:).standardizedFileURL, which correctly percent-encodes non-ASCII before handing to LaunchServices.
  • For file:// URLs: strip scheme, decode existing percent-encoding, then re-encode via fileURLWithPath for a clean round-trip.
  • For http/https URLs: addingPercentEncoding before URL(string:) so non-ASCII query params don't return nil.
  • LaunchAppTool.resolveLaunchURL now delegates to the core function.

Tests (URLResolverTests):

  • CJK absolute path (/Users/me/器材控/...)
  • Cyrillic path
  • Accented Latin path
  • Emoji path
  • ASCII path (regression guard)
  • Tilde expansion
  • file:// passthrough
  • file:// with percent-encoded CJK
  • http/https URLs
  • Non-ASCII in HTTPS query string
  • Empty string returns nil

Summary by CodeRabbit

  • New Features

    • Enhanced URL resolution for app launching with improved support for non-ASCII characters (CJK, Cyrillic, emoji, and accented characters).
    • Improved file path handling including tilde expansion for filesystem paths.
  • Refactor

    • Reorganized URL resolution logic into a dedicated utility for better maintainability.
  • Tests

    • Added comprehensive test coverage for URL resolution across various character sets and URL formats.

Review Change Stack

Fixes trycua#1519.

launch_app crashed the daemon when urls contained CJK / Cyrillic /
accented characters (e.g. /Users/me/器材控/templates/file.key).

Root cause: resolveLaunchURL() used URL(string:) to detect the scheme,
which returns nil for bare non-ASCII characters. The fallback
URL(fileURLWithPath:) path was reached, but without .standardizedFileURL
the resulting URL could carry unencoded bytes that LaunchServices
rejected mid-request, disconnecting the daemon.

Fix:
- Extract resolveLaunchURL into CuaDriverCore/Apps/URLResolver.swift
  as a public function so it can be unit-tested independently.
- For plain file paths: use URL(fileURLWithPath:).standardizedFileURL,
  which correctly percent-encodes non-ASCII before handing to
  LaunchServices.
- For file:// URLs: strip scheme, decode existing percent-encoding,
  then re-encode via fileURLWithPath for a clean round-trip.
- For http/https URLs: addingPercentEncoding before URL(string:) so
  non-ASCII query params don't return nil.
- LaunchAppTool.resolveLaunchURL now delegates to the core function.

Tests (URLResolverTests):
- CJK absolute path (/Users/me/器材控/...)
- Cyrillic path
- Accented Latin path
- Emoji path
- ASCII path (regression guard)
- Tilde expansion
- file:// passthrough
- file:// with percent-encoded CJK
- http/https URLs
- Non-ASCII in HTTPS query string
- Empty string returns nil
@vercel

vercel Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

@nishantpurohit04 is attempting to deploy a commit to the Cua Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c440240b-d3da-4f4e-baef-fa9b43f450c0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR fixes daemon crashes when launch_app receives non-ASCII (CJK) file paths by extracting URL resolution logic into a tested public utility function in CuaDriverCore that properly percent-encodes non-ASCII characters via URL(fileURLWithPath:), integrates it into LaunchAppTool, and adds comprehensive regression tests.

Changes

Non-ASCII URL Resolution for App Launching

Layer / File(s) Summary
URL resolution contract and implementation
libs/cua-driver/Sources/CuaDriverCore/Apps/URLResolver.swift, libs/cua-driver/Package.swift
New public resolveLaunchURL(_:) -> URL? function detects schemes via "://" and applies scheme-specific handling: percent-encodes non-ASCII for http/https, decodes and rebuilds file:// URLs via URL(fileURLWithPath:), and expands ~ then standardizes plain paths as file:// URLs.
LaunchAppTool integration
libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift
LaunchAppTool.resolveLaunchURL(_:) delegates to CuaDriverCore.resolveLaunchURL(_:), removing inline implementation and referencing the core function's encoding contract.
Comprehensive test coverage
libs/cua-driver/Tests/URLResolverTests/URLResolverTests.swift
Regression-focused test suite validates non-ASCII path handling (CJK percent-encoding and round-trip, Cyrillic, accented Latin, emoji), ASCII and tilde-expanded paths, file:// and http/https URL schemes, and edge cases (empty string returns nil, about:blank does not crash).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A crashy path with CJK chars,
Now encoded proper, near and far!
fileURLWithPath saves the day—
Non-ASCII files launch every way! 🌏

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: percent-encoding non-ASCII paths in launch_app URLs, which directly addresses the crash in issue #1519.
Linked Issues check ✅ Passed The PR comprehensively addresses issue #1519 by extracting resolveLaunchURL into a testable core function, implementing percent-encoding for file paths and URLs, and adding extensive test coverage for CJK, Cyrillic, accented, and emoji characters.
Out of Scope Changes check ✅ Passed All changes directly support the objective: extracting URL resolution logic to CuaDriverCore, updating LaunchAppTool to delegate to the core function, and adding comprehensive tests. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@libs/cua-driver/Sources/CuaDriverCore/Apps/URLResolver.swift`:
- Around line 31-55: The code currently only detects schemes via "://", so
inputs like "about:blank" fall through to the file-path branch; change scheme
detection to look for the first ':' before any '/' (e.g., find
raw.firstIndex(of: ":") and ensure it's before any "/" or not followed by "//")
and extract scheme = raw[..<colon].lowercased(); then handle schemes explicitly
(if scheme == "http" || "https" → percent-encode and return URL(string:), if
scheme == "file" → perform the existing fileURLWithPath flow) and for any other
detected scheme return URL(string: raw) (or percent-encode where appropriate)
instead of treating them as plain file paths; update uses of
schemeRange/scheme/pathPart/decoded to the new colon-based indices so non- "://"
schemes like "about:" are preserved.

In `@libs/cua-driver/Tests/URLResolverTests/URLResolverTests.swift`:
- Around line 91-98: Update the testAboutBlank to assert the resolved value
instead of ignoring it: call resolveLaunchURL("about:blank") and add assertions
that the returned URL is non-nil and that its scheme and/or absoluteString match
the expected "about" and "about:blank" respectively (use XCTAssertNotNil and
XCTAssertEqual on result and result?.scheme/result?.absoluteString). This will
verify resolveLaunchURL's behavior and catch regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99a6f263-609e-4615-a0dd-3f37628d551c

📥 Commits

Reviewing files that changed from the base of the PR and between 0f834c6 and 6b90eaa.

📒 Files selected for processing (4)
  • libs/cua-driver/Package.swift
  • libs/cua-driver/Sources/CuaDriverCore/Apps/URLResolver.swift
  • libs/cua-driver/Sources/CuaDriverServer/Tools/LaunchAppTool.swift
  • libs/cua-driver/Tests/URLResolverTests/URLResolverTests.swift

Comment on lines +31 to +55
if let schemeRange = raw.range(of: "://") {
let scheme = raw[raw.startIndex..<schemeRange.lowerBound].lowercased()

if scheme == "http" || scheme == "https" {
// Percent-encode non-ASCII so URL(string:) doesn't return nil.
if let encoded = raw.addingPercentEncoding(
withAllowedCharacters: .urlQueryAllowed),
let url = URL(string: encoded)
{
return url
}
// Already encoded by caller — try as-is.
return URL(string: raw)
}

if scheme == "file" {
// Strip "file://" prefix, decode any existing percent-encoding,
// then re-encode via fileURLWithPath for a clean round-trip.
let pathPart = String(raw[schemeRange.upperBound...])
let decoded = pathPart.removingPercentEncoding ?? pathPart
return URL(fileURLWithPath: decoded).standardizedFileURL
}
}

// Plain path (absolute or tilde-prefixed).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle non-:// schemes before file-path fallback.

At Line 31, scheme detection only matches ://. Inputs like about:blank skip URL-scheme handling and fall into the plain-path branch at Line 55, which changes them into file paths instead of preserving scheme semantics.

Suggested fix
 public func resolveLaunchURL(_ raw: String) -> URL? {
     guard !raw.isEmpty else { return nil }

+    // Detect generic URI schemes first (e.g. about:, mailto:, etc.).
+    if let colon = raw.firstIndex(of: ":") {
+        let schemeCandidate = String(raw[..<colon])
+        let isValidScheme = schemeCandidate.range(
+            of: #"^[A-Za-z][A-Za-z0-9+\-.]*$"#,
+            options: .regularExpression
+        ) != nil
+
+        if isValidScheme {
+            let scheme = schemeCandidate.lowercased()
+            if scheme == "http" || scheme == "https" {
+                if let encoded = raw.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed),
+                   let url = URL(string: encoded) {
+                    return url
+                }
+                return URL(string: raw)
+            }
+            if scheme == "file", let schemeRange = raw.range(of: "://") {
+                let pathPart = String(raw[schemeRange.upperBound...])
+                let decoded = pathPart.removingPercentEncoding ?? pathPart
+                return URL(fileURLWithPath: decoded).standardizedFileURL
+            }
+            return URL(string: raw)
+        }
+    }
+
-    // Detect explicit URL schemes (http / https / file).
-    if let schemeRange = raw.range(of: "://") {
+    // Backward-compatible handling for explicit URL schemes using ://
+    if let schemeRange = raw.range(of: "://") {
         let scheme = raw[raw.startIndex..<schemeRange.lowerBound].lowercased()
         ...
     }
🤖 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 `@libs/cua-driver/Sources/CuaDriverCore/Apps/URLResolver.swift` around lines 31
- 55, The code currently only detects schemes via "://", so inputs like
"about:blank" fall through to the file-path branch; change scheme detection to
look for the first ':' before any '/' (e.g., find raw.firstIndex(of: ":") and
ensure it's before any "/" or not followed by "//") and extract scheme =
raw[..<colon].lowercased(); then handle schemes explicitly (if scheme == "http"
|| "https" → percent-encode and return URL(string:), if scheme == "file" →
perform the existing fileURLWithPath flow) and for any other detected scheme
return URL(string: raw) (or percent-encode where appropriate) instead of
treating them as plain file paths; update uses of
schemeRange/scheme/pathPart/decoded to the new colon-based indices so non- "://"
schemes like "about:" are preserved.

Comment on lines +91 to +98
func testAboutBlank() throws {
// about:blank is used by browsers — not a file/http/https scheme,
// so it falls through to the plain-path branch. Verify it doesn't crash.
// (NSWorkspace handles about:blank natively for browsers.)
let result = resolveLaunchURL("about:blank")
// We don't assert a specific value — just that it doesn't crash/throw.
_ = result
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

testAboutBlank currently can’t catch regressions.

At Line 96, no behavioral assertion is made, so this test passes even if resolveLaunchURL("about:blank") resolves incorrectly. Please assert expected scheme/value.

Suggested test assertion update
-    func testAboutBlank() throws {
-        // about:blank is used by browsers — not a file/http/https scheme,
-        // so it falls through to the plain-path branch. Verify it doesn't crash.
-        // (NSWorkspace handles about:blank natively for browsers.)
-        let result = resolveLaunchURL("about:blank")
-        // We don't assert a specific value — just that it doesn't crash/throw.
-        _ = result
-    }
+    func testAboutBlank() throws {
+        let url = try XCTUnwrap(resolveLaunchURL("about:blank"))
+        XCTAssertEqual(url.scheme, "about")
+        XCTAssertEqual(url.absoluteString, "about:blank")
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func testAboutBlank() throws {
// about:blank is used by browsers — not a file/http/https scheme,
// so it falls through to the plain-path branch. Verify it doesn't crash.
// (NSWorkspace handles about:blank natively for browsers.)
let result = resolveLaunchURL("about:blank")
// We don't assert a specific value — just that it doesn't crash/throw.
_ = result
}
func testAboutBlank() throws {
let url = try XCTUnwrap(resolveLaunchURL("about:blank"))
XCTAssertEqual(url.scheme, "about")
XCTAssertEqual(url.absoluteString, "about:blank")
}
🤖 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 `@libs/cua-driver/Tests/URLResolverTests/URLResolverTests.swift` around lines
91 - 98, Update the testAboutBlank to assert the resolved value instead of
ignoring it: call resolveLaunchURL("about:blank") and add assertions that the
returned URL is non-nil and that its scheme and/or absoluteString match the
expected "about" and "about:blank" respectively (use XCTAssertNotNil and
XCTAssertEqual on result and result?.scheme/result?.absoluteString). This will
verify resolveLaunchURL's behavior and catch regressions.

Address CodeRabbit review comments on PR trycua#1574.

1. Scheme detection now uses firstIndex(of: ':') + RFC 3986 regex
   instead of range(of: '://'), so single-colon schemes like
   about:blank, mailto:, data: are correctly passed through via
   URL(string:) rather than falling into the file-path branch.

2. testAboutBlank now asserts url.scheme == 'about' and
   url.absoluteString == 'about:blank' instead of being a no-op.
   Added testMailtoScheme to cover the same code path.

No behaviour change for http/https/file/plain-path inputs.
@hippoley

Copy link
Copy Markdown
Contributor Author

Good catches, @coderabbitai — both fixed in the latest commit (3e61888).

1. Non-:// scheme handling: Replaced range(of: "://") with firstIndex(of: ":") + RFC 3986 regex validation. Now about:blank, mailto:, data: etc. are correctly passed through via URL(string:) instead of falling into the file-path branch.

2. testAboutBlank assertion: Now asserts url.scheme == "about" and url.absoluteString == "about:blank". Also added testMailtoScheme to cover the same code path.

@coderabbitai

coderabbitai Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me take a quick look at the updated files to verify the fixes.


🏁 Script executed:

#!/bin/bash
cat libs/cua-driver/Sources/CuaDriverCore/Apps/URLResolver.swift
echo "---"
cat libs/cua-driver/Tests/URLResolverTests/URLResolverTests.swift

Length of output: 7314


@hippoley Both fixes look great — the implementation and tests are clean and correct.

  1. Scheme detection — the RFC 3986 regex (^[A-Za-z][A-Za-z0-9+.\\-]*$) is spec-accurate and properly gates the scheme branch, so about:blank, mailto:, data: URIs all flow into URL(string:) as intended rather than the file-path branch. The file special-case is still correctly reached via scheme == "file".

  2. testAboutBlank + testMailtoScheme — asserting both url.scheme and url.absoluteString gives tight coverage of the passthrough semantics. The added testMailtoScheme rounds out the single-colon scheme path nicely.

🐇 ✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@injaneity

Copy link
Copy Markdown
Collaborator

closed as #2077 switched the rust backend to avoid the broken url(string:) behavior

@injaneity injaneity closed this Jul 20, 2026
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.

cua-driver daemon crash on launch_app when urls contains non-ASCII (CJK) path

2 participants