Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ For a detailed view of what has changed, refer to the {uri-repo}/commits/main[co

== Unreleased

Bug Fixes::

* Fix `PathResolver#partitionPath()`/`#expandPath()` dropping a slash from `file:///` (and other triple-slash) URIs, turning `file:///Users/guillaume/foo.png` into `file://Users/guillaume/foo.png` — a malformed URL whose "host" (`Users`) browsers reject as `Not allowed to load local resource`. `UriSniffRx` only ever matches up to 2 slashes after the scheme, so for a triple-slash URI the 3rd slash is left in the remainder to partition into segments; `partitionPath()` unconditionally filtered out *all* empty segments (including that leading one), silently discarding the information needed to reconstruct the slash on `joinPath()`. It now mirrors Ruby's `String#split('/')` semantics and drops only trailing empty segments, keeping leading ones intact

== v4.0.6 (2026-07-26)

Bug Fixes::
Expand Down
15 changes: 10 additions & 5 deletions packages/core/src/path_resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,16 @@ export class PathResolver {
}

const relative = root ? posixPath.slice(root.length) : posixPath
let segments = relative.split(SLASH).filter((s) => s !== DOT && s !== '')
// Re-add non-empty-string DOT segments removal is as above; preserve empty for UNC
segments = relative.split(SLASH).filter((s) => s !== DOT)
// Remove any empty segments (trailing slash artifacts) except retain intent
segments = segments.filter((s) => s !== '')
// Mirror Ruby's String#split('/'), which drops only *trailing* empty
// strings. A leading empty segment must be kept: it's what reconstructs
// the missing slash for URI roots that under-consume it, e.g. root
// "file://" + relative "/Users/foo" (from "file:///Users/foo") needs
// that leading '' so joinPath() rebuilds "file:///Users/foo" and not
// "file://Users/foo".
const segments = relative.split(SLASH).filter((s) => s !== DOT)
while (segments.length && segments[segments.length - 1] === '') {
segments.pop()
}

const result = [segments, root]
cache[path] = result
Expand Down
30 changes: 30 additions & 0 deletions packages/core/test/path_resolver.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,22 @@ describe('PathResolver#partitionPath()', () => {
assert.equal(root, '//')
assert.deepEqual(segments, ['server', 'share', 'docs'])
})

test('file:// URI with triple slash: root keeps only 2 slashes, segments keep the leading "" placeholder', () => {
const [segments, root] = posix().partitionPath(
'file:///Users/guillaume/foo.png'
)
assert.equal(root, 'file://')
assert.deepEqual(segments, ['', 'Users', 'guillaume', 'foo.png'])
})

test('http:// URI (authority present) has no leading empty segment', () => {
const [segments, root] = posix().partitionPath(
'http://example.org/path/to/file.png'
)
assert.equal(root, 'http://')
assert.deepEqual(segments, ['example.org', 'path', 'to', 'file.png'])
})
})

// ── joinPath() ────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -204,6 +220,20 @@ describe('PathResolver#expandPath()', () => {
'//server/share/docs'
)
})

test('roundtrips a file:// URI with triple slash without dropping a slash', () => {
assert.equal(
posix().expandPath('file:///Users/guillaume/foo.png'),
'file:///Users/guillaume/foo.png'
)
})

test('resolves ".." in a file:// URI with triple slash', () => {
assert.equal(
posix().expandPath('file:///Users/guillaume/../foo.png'),
'file:///Users/foo.png'
)
})
})

// ── descendsFrom() ────────────────────────────────────────────────────────────
Expand Down