Skip to content

Commit

Permalink
Merge pull request #106 from jridgewell/files
Browse files Browse the repository at this point in the history
Fix parsing of file: URLs
  • Loading branch information
jridgewell authored Apr 22, 2022
2 parents 1414cc3 + baaea2a commit 1c61454
Show file tree
Hide file tree
Showing 3 changed files with 10,325 additions and 241 deletions.
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

0 comments on commit 1c61454

Please sign in to comment.