Skip to content
Open
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
18 changes: 17 additions & 1 deletion lib/immutable-url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,23 @@ export default class implements IURLExtended {
this.slashes += '/';
}
}
if (this.slashes.length >= 2) {

const slashesLen = this.slashes.length;
let authorityIncluded = slashesLen >= 2;
// file: is a "special" scheme as per spec
if (this._protocol == 'file:') {
// ansolute path. decrement index so the slash is included in pathname
if (slashesLen == 1) {
authorityIncluded = false;
index--;
} else if (slashesLen > 2) {
// keep all slashes after file://
index -= slashesLen - 2;
authorityIncluded = false;
}
// a file url with exactly two slashes denotes a file on a remote host: file://host/file
}
if (authorityIncluded) {
// Two slashes: Authority is included
index = this._extractHostname(index, end);
} else {
Expand Down
58 changes: 58 additions & 0 deletions test/url-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ describe('URL Spec', () => {
'https://example.com/?q=+33%201',
'https://example.com/?q=+33+%201',
'https://[::1]/',
'file:///test',
].forEach((urlString: string) => {
it(urlString, () => {
const expected = new URLSpec(urlString);
Expand Down Expand Up @@ -234,4 +235,61 @@ describe('Divergence from URL Spec', () => {
expect(encoded.href).toBe(expected.href);
expect(encoded.origin).toBe(expected.origin);
});

it('Does allow relative file paths', () => {
const urlString = 'file:test';
const expected = new URLSpec(urlString);
const actual = new URL(urlString);

expect(actual.hash).toBe(expected.hash);
expect(actual.password).toBe(expected.password);
expect(actual.protocol).toBe(expected.protocol);
expect(actual.search).toBe(expected.search);
expect(actual.username).toBe(expected.username);
expect(actual.host).toBe(expected.host);
expect(actual.hostname).toBe(expected.hostname);

// relative pathnames are not allowed per spec and are interpreted as absolute paths in the spec
expect(actual.pathname).toBe('test');
expect(expected.pathname).toBe('/test');
});

it('Does allow absolute pathnames without authority', () => {
const urlString = 'file:/test';
const expected = new URLSpec(urlString);
const actual = new URL(urlString);

expect(actual.hash).toBe(expected.hash);
expect(actual.password).toBe(expected.password);
expect(actual.protocol).toBe(expected.protocol);
expect(actual.search).toBe(expected.search);
expect(actual.username).toBe(expected.username);
expect(actual.host).toBe(expected.host);
expect(actual.hostname).toBe(expected.hostname);

// url reference implementation differs here
expect(actual.toString()).toBe('file:/test');
expect(expected.toString()).toBe('file:///test');
expect(actual.pathname).toBe('/test');
expect(expected.pathname).toBe('/test');
});

it('Does parse file URLs with authority but also creates an origin', () => {
const urlString = 'file://hostname/file';
const expected = new URLSpec(urlString);
const actual = new URL(urlString);

expect(actual.hash).toBe(expected.hash);
expect(actual.password).toBe(expected.password);
expect(expected.pathname).toBe(actual.pathname);
expect(actual.protocol).toBe(expected.protocol);
expect(actual.search).toBe(expected.search);
expect(actual.username).toBe(expected.username);
expect(actual.host).toBe(expected.host);
expect(actual.hostname).toBe(expected.hostname);

// spec recommends origin null
expect(expected.origin).toBe('null');
expect(actual.origin).toBe('file://hostname');
});
});