Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix parsing of file: URLs #106

Merged
merged 1 commit into from
Apr 22, 2022
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
54 changes: 40 additions & 14 deletions src/resolve-uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ const schemeRegex = /^[\w+.-]+:\/\//;
*/
const urlRegex = /^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?/;

/**
* File URLs are weird. They dont' need the regular `//` in the scheme, they may or may not start
* with a leading `/`, they can have a domain (but only if they don't start with a Windows drive).
*
* 1. Host, optional.
* 2. Path, which may inclue "/", guaranteed.
*/
const fileRegex = /^file:(?:\/\/((?![a-z]:)[^/]*)?)?(\/?.*)/i;

type Url = {
scheme: string;
user: string;
Expand All @@ -32,14 +41,28 @@ function isAbsolutePath(input: string): boolean {
return input.startsWith('/');
}

function isFileUrl(input: string): boolean {
return input.startsWith('file:');
}

function parseAbsoluteUrl(input: string): Url {
const match = urlRegex.exec(input)!;
return makeUrl(match[1], match[2] || '', match[3], match[4] || '', match[5] || '/');
}

function parseFileUrl(input: string): Url {
const match = fileRegex.exec(input)!;
const path = match[2];
return makeUrl('file:', '', match[1] || '', '', isAbsolutePath(path) ? path : '/' + path);
}

function makeUrl(scheme: string, user: string, host: string, port: string, path: string): Url {
return {
scheme: match[1],
user: match[2] || '',
host: match[3],
port: match[4] || '',
path: match[5] || '/',
scheme,
user,
host,
port,
path,
relativePath: false,
};
}
Expand All @@ -50,20 +73,23 @@ function parseUrl(input: string): Url {
url.scheme = '';
return url;
}

if (isAbsolutePath(input)) {
const url = parseAbsoluteUrl('http://foo.com' + input);
url.scheme = '';
url.host = '';
return url;
}
if (!isAbsoluteUrl(input)) {
const url = parseAbsoluteUrl('http://foo.com/' + input);
url.scheme = '';
url.host = '';
url.relativePath = true;
return url;
}
return parseAbsoluteUrl(input);

if (isFileUrl(input)) return parseFileUrl(input);

if (isAbsoluteUrl(input)) return parseAbsoluteUrl(input);

const url = parseAbsoluteUrl('http://foo.com/' + input);
url.scheme = '';
url.host = '';
url.relativePath = true;
return url;
}

function stripPathFilename(path: string): string {
Expand Down Expand Up @@ -173,7 +199,7 @@ export default function resolve(input: string, base: string | undefined): string
const baseUrl = parseUrl(base);
url.scheme = baseUrl.scheme;
// If there's no host, then we were just a path.
if (!url.host || baseUrl.scheme === 'file:') {
if (!url.host) {
// The host, user, and port are joined, you can't copy one without the others.
url.user = baseUrl.user;
url.host = baseUrl.host;
Expand Down
108 changes: 101 additions & 7 deletions test/generate-tests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/* eslint-env node */
/* eslint-disable @typescript-eslint/no-var-requires */

const { writeFileSync } = require('fs');
const { normalize } = require('path');
const prettier = require('prettier');
Expand All @@ -22,8 +24,10 @@ function describe(name, fn) {
function getOrigin(url) {
let index = 0;
if (!url) return '';
if (url.startsWith('file://')) {
index = 'file://'.length;
if (url.startsWith('file://') && !url.startsWith('file:///')) {
index = url.indexOf('/', 'file://'.length);
} else if (url.startsWith('file:')) {
return 'file://';
} else if (url.startsWith('https://')) {
index = url.indexOf('/', 'https://'.length);
} else if (url.startsWith('//')) {
Expand All @@ -37,15 +41,22 @@ function getOrigin(url) {

function getProtocol(url) {
if (!url) return '';
if (url.startsWith('file://')) return 'file:';
if (url.startsWith('file:')) return 'file:';
if (url.startsWith('https://')) return 'https:';
return '';
}

function getPath(base, input) {
let b = base;
const origin = getOrigin(b);
if (origin) b = b.slice(origin.length);
if (origin) {
if (b.startsWith(origin)) {
b = b.slice(origin.length);
} else {
// file:/foo or file:foo
b = b.replace(/file:\/*/, '');
}
}
b = normalize(b || '');
if (base?.endsWith('/..')) b += '/';
b = b.replace(/(^|\/)((?!\/|(?<=(^|\/))\.\.(?=(\/|$))).)*$/, '$1');
Expand All @@ -61,7 +72,7 @@ function getPath(base, input) {
}

function normalizeBase(base) {
if (base.startsWith('file://')) return new URL(base).href;
if (base.startsWith('file:')) return new URL(base).href;
if (base.startsWith('https://')) return new URL(base).href;
if (base.startsWith('//')) {
return new URL('https:' + base).href.slice('https:'.length);
Expand All @@ -73,7 +84,7 @@ function normalizeBase(base) {
}

function maybeDropHost(host, base) {
if (base?.startsWith('file://')) return '';
// if (base?.startsWith('file://')) return '';
return host;
}

Expand Down Expand Up @@ -124,12 +135,68 @@ function suite(base) {
assert.strictEqual(resolved, 'https://absolute.com/main.js.map');
});

it('normalizes file protocol', () => {
it('normalizes file protocol 1', () => {
const base = ${init};
const input = 'file:///root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///root/main.js.map');
});

it('normalizes file protocol 2', () => {
const base = ${init};
const input = 'file://root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file://root/main.js.map');
});

it('normalizes file protocol 2.5', () => {
const base = ${init};
const input = 'file://root';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file://root/');
});

it('normalizes file protocol 3', () => {
const base = ${init};
const input = 'file:/root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///root/main.js.map');
});

it('normalizes file protocol 4', () => {
const base = ${init};
const input = 'file:root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///root/main.js.map');
});

it('normalizes windows file 1', () => {
const base = ${init};
const input = 'file:///C:/root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///C:/root/main.js.map');
});

it('normalizes windows file 2', () => {
const base = ${init};
const input = 'file://C:/root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///C:/root/main.js.map');
});

it('normalizes windows file 3', () => {
const base = ${init};
const input = 'file:/C:/root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///C:/root/main.js.map');
});

it('normalizes windows file 4', () => {
const base = ${init};
const input = 'file:C:/root/main.js.map';
const resolved = resolve(input, base);
assert.strictEqual(resolved, 'file:///C:/root/main.js.map');
});
});

describe('with protocol relative input', () => {
Expand Down Expand Up @@ -323,6 +390,33 @@ describe('resolve', () => {
suite('file:///foo/..');
suite('file:///foo/../');
suite('file:///foo/dir/..');

suite('file://foo');
suite('file://foo/');
suite('file://foo/file');
suite('file://foo/dir/');
suite('file://foo/dir/file');
suite('file://foo/..');
suite('file://foo/../');
suite('file://foo/dir/..');

suite('file:/foo');
suite('file:/foo/');
suite('file:/foo/file');
suite('file:/foo/dir/');
suite('file:/foo/dir/file');
suite('file:/foo/..');
suite('file:/foo/../');
suite('file:/foo/dir/..');

suite('file:foo');
suite('file:foo/');
suite('file:foo/file');
suite('file:foo/dir/');
suite('file:foo/dir/file');
suite('file:foo/..');
suite('file:foo/../');
suite('file:foo/dir/..');
});

describe('with protocol relative base', () => {
Expand Down
Loading