fix(browser): preserve backslashes in POSIX file paths (#42257)

On macOS and Linux a backslash is an ordinary filename character, but
absolutePathToFileUrl converted every backslash into a path separator, so
typing /tmp/site\draft.html opened /tmp/site/draft.html instead.

Keep backslashes (encoded as %5C) for rooted paths when the local host is not
Windows. On Windows every path still has backslashes normalized to slashes,
so drive, UNC and rooted /foo\bar inputs behave as before.

Fixes #42256

Co-authored-by: Neil <neil@stably.ai>
This commit is contained in:
Henry Su
2026-11-13 20:17:16 -08:00
committed by GitHub
co-authored by Neil
parent 6b1738fad2
commit b2bb38873e
3 changed files with 30 additions and 3 deletions
+25 -1
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
isEligibleLocalCertificateHost,
@@ -43,6 +43,30 @@ describe('browser-url helpers', () => {
expect(normalizeExternalBrowserUrl('about:blank')).toBeNull()
})
describe('file path backslashes', () => {
afterEach(() => {
vi.unstubAllGlobals()
})
it('preserves backslashes as filename characters on POSIX hosts', () => {
vi.stubGlobal('navigator', { userAgent: 'Linux' })
expect(normalizeBrowserNavigationUrl('/tmp/site\\draft.html')).toBe(
'file:///tmp/site%5Cdraft.html'
)
})
it('normalizes backslashes to separators on Windows hosts', () => {
vi.stubGlobal('navigator', { userAgent: 'Windows' })
expect(normalizeBrowserNavigationUrl('/foo\\bar.html')).toBe('file:///foo/bar.html')
expect(normalizeBrowserNavigationUrl('C:\\site\\draft.html')).toBe(
'file:///C:/site/draft.html'
)
expect(normalizeBrowserNavigationUrl('\\\\server\\share\\draft.html')).toBe(
'file://server/share/draft.html'
)
})
})
// Refuse file URLs externally: a remote page must not drive Finder/Explorer to arbitrary paths.
it('rejects file:// for external opens even though it is allowed in-app', () => {
expect(normalizeExternalBrowserUrl('file:///etc/passwd')).toBeNull()
+4 -1
View File
@@ -1,5 +1,6 @@
import './constants'
import { ORCA_BROWSER_BLANK_URL } from './browser/browser-blank-page'
import { getCurrentLocalPlatform } from './execution-host'
const LOCAL_ADDRESS_PATTERN =
/^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[[0-9a-f:]+\])(?::\d+)?(?:[/?#].*)?$/i
@@ -245,7 +246,9 @@ export function isAbsoluteFilesystemPathInput(input: string): boolean {
}
function absolutePathToFileUrl(filePath: string): string {
const normalizedPath = filePath.replaceAll('\\', '/')
// Why: `\` is a filename char on POSIX but a separator on Windows, even in rooted paths like `/foo\bar`.
const isPosixPath = filePath.startsWith('/') && getCurrentLocalPlatform() !== 'win32'
const normalizedPath = isPosixPath ? filePath : filePath.replaceAll('\\', '/')
const segments = normalizedPath.split('/').map((segment, index) => {
if (index === 0 && /^[A-Za-z]:$/.test(segment)) {
return segment
+1 -1
View File
@@ -23,7 +23,7 @@ export type ParsedExecutionHost =
| { kind: 'ssh'; id: `ssh:${string}`; targetId: string }
| { kind: 'runtime'; id: `runtime:${string}`; environmentId: string }
function getCurrentLocalPlatform(): NodeJS.Platform | null {
export function getCurrentLocalPlatform(): NodeJS.Platform | null {
const globalNavigator = (globalThis as { navigator?: { userAgent?: string; platform?: string } })
.navigator
const userAgent = globalNavigator?.userAgent || globalNavigator?.platform || ''