Skip to content

Commit e47071a

Browse files
committed
fix: add fileURLToPath polyfill
1 parent 2729fd4 commit e47071a

File tree

6 files changed

+296
-2
lines changed

6 files changed

+296
-2
lines changed
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const url = require('url')
2+
3+
const node = require('../node.js')
4+
const polyfill = require('./polyfill.js')
5+
6+
const useNative = node.satisfies('>=10.12.0')
7+
8+
const fileURLToPath = (path) => {
9+
// the polyfill is tested separately from this module, no need to hack
10+
// process.version to try to trigger it just for coverage
11+
// istanbul ignore next
12+
return useNative
13+
? url.fileURLToPath(path)
14+
: polyfill(path)
15+
}
16+
17+
module.exports = fileURLToPath
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
const { URL, domainToUnicode } = require('url')
2+
3+
const CHAR_LOWERCASE_A = 97
4+
const CHAR_LOWERCASE_Z = 122
5+
6+
const isWindows = process.platform === 'win32'
7+
8+
class ERR_INVALID_FILE_URL_HOST extends TypeError {
9+
constructor (platform) {
10+
super(`File URL host must be "localhost" or empty on ${platform}`)
11+
this.code = 'ERR_INVALID_FILE_URL_HOST'
12+
}
13+
14+
toString () {
15+
return `${this.name} [${this.code}]: ${this.message}`
16+
}
17+
}
18+
19+
class ERR_INVALID_FILE_URL_PATH extends TypeError {
20+
constructor (msg) {
21+
super(`File URL path ${msg}`)
22+
this.code = 'ERR_INVALID_FILE_URL_PATH'
23+
}
24+
25+
toString () {
26+
return `${this.name} [${this.code}]: ${this.message}`
27+
}
28+
}
29+
30+
class ERR_INVALID_ARG_TYPE extends TypeError {
31+
constructor (name, actual) {
32+
super(`The "${name}" argument must be one of type string or an instance of URL. Received type ${typeof actual} ${actual}`)
33+
this.code = 'ERR_INVALID_ARG_TYPE'
34+
}
35+
36+
toString () {
37+
return `${this.name} [${this.code}]: ${this.message}`
38+
}
39+
}
40+
41+
class ERR_INVALID_URL_SCHEME extends TypeError {
42+
constructor (expected) {
43+
super(`The URL must be of scheme ${expected}`)
44+
this.code = 'ERR_INVALID_URL_SCHEME'
45+
}
46+
47+
toString () {
48+
return `${this.name} [${this.code}]: ${this.message}`
49+
}
50+
}
51+
52+
const isURLInstance = (input) => {
53+
return input != null && input.href && input.origin
54+
}
55+
56+
const getPathFromURLWin32 = (url) => {
57+
const hostname = url.hostname
58+
let pathname = url.pathname
59+
for (let n = 0; n < pathname.length; n++) {
60+
if (pathname[n] === '%') {
61+
const third = pathname.codePointAt(n + 2) | 0x20
62+
if ((pathname[n + 1] === '2' && third === 102) ||
63+
(pathname[n + 1] === '5' && third === 99)) {
64+
throw new ERR_INVALID_FILE_URL_PATH('must not include encoded \\ or / characters')
65+
}
66+
}
67+
}
68+
69+
pathname = pathname.replace(/\//g, '\\')
70+
pathname = decodeURIComponent(pathname)
71+
if (hostname !== '') {
72+
return `\\\\${domainToUnicode(hostname)}${pathname}`
73+
}
74+
75+
const letter = pathname.codePointAt(1) | 0x20
76+
const sep = pathname[2]
77+
if (letter < CHAR_LOWERCASE_A || letter > CHAR_LOWERCASE_Z ||
78+
(sep !== ':')) {
79+
throw new ERR_INVALID_FILE_URL_PATH('must be absolute')
80+
}
81+
82+
return pathname.slice(1)
83+
}
84+
85+
const getPathFromURLPosix = (url) => {
86+
if (url.hostname !== '') {
87+
throw new ERR_INVALID_FILE_URL_HOST(process.platform)
88+
}
89+
90+
const pathname = url.pathname
91+
92+
for (let n = 0; n < pathname.length; n++) {
93+
if (pathname[n] === '%') {
94+
const third = pathname.codePointAt(n + 2) | 0x20
95+
if (pathname[n + 1] === '2' && third === 102) {
96+
throw new ERR_INVALID_FILE_URL_PATH('must not include encoded / characters')
97+
}
98+
}
99+
}
100+
101+
return decodeURIComponent(pathname)
102+
}
103+
104+
const fileURLToPath = (path) => {
105+
if (typeof path === 'string') {
106+
path = new URL(path)
107+
} else if (!isURLInstance(path)) {
108+
throw new ERR_INVALID_ARG_TYPE('path', ['string', 'URL'], path)
109+
}
110+
111+
if (path.protocol !== 'file:') {
112+
throw new ERR_INVALID_URL_SCHEME('file')
113+
}
114+
115+
return isWindows
116+
? getPathFromURLWin32(path)
117+
: getPathFromURLPosix(path)
118+
}
119+
120+
module.exports = fileURLToPath

lib/common/owner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { dirname, resolve } = require('path')
2-
const { fileURLToPath } = require('url')
32

3+
const fileURLToPath = require('./file-url-to-path/index.js')
44
const fs = require('../fs.js')
55

66
// given a path, find the owner of the nearest parent

lib/mkdir/polyfill.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const { dirname } = require('path')
2-
const { fileURLToPath } = require('url')
32

3+
const fileURLToPath = require('../common/file-url-to-path/index.js')
44
const fs = require('../fs.js')
55

66
const defaultOptions = {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const t = require('tap')
2+
3+
const fileURLToPath = require('../../../lib/common/file-url-to-path/index.js')
4+
5+
t.test('can convert a file url to a path', async (t) => {
6+
const url = process.platform === 'win32'
7+
? 'file://c:/some/path' // windows requires an absolute path, or hostname
8+
: 'file:///some/path' // posix cannot have a hostname
9+
const path = fileURLToPath(url)
10+
t.type(path, 'string', 'result is a string')
11+
})
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
const t = require('tap')
2+
3+
const fileURLToPath = require('../../../lib/common/file-url-to-path/polyfill.js')
4+
5+
// these two errors are the only shared code, everything else has platform
6+
// specific tests and assertions below
7+
t.test('invalid input throws ERR_INVALID_ARG_TYPE', async (t) => {
8+
t.throws(() => fileURLToPath({}), {
9+
code: 'ERR_INVALID_ARG_TYPE',
10+
message: /must be one of type string or an instance of URL/,
11+
}, 'got the right error')
12+
})
13+
14+
t.test('invalid protocol throws ERR_INVALID_URL_SCHEME', async (t) => {
15+
t.throws(() => fileURLToPath('https://npmjs.com'), {
16+
code: 'ERR_INVALID_URL_SCHEME',
17+
message: /URL must be of scheme file/,
18+
}, 'got the right error')
19+
})
20+
21+
t.test('posix', async (t) => {
22+
let fileURLToPath
23+
t.before(() => {
24+
t.context.platform = Object.getOwnPropertyDescriptor(process, 'platform')
25+
Object.defineProperty(process, 'platform', {
26+
...t.context.platform,
27+
value: 'linux',
28+
})
29+
fileURLToPath = t.mock('../../../lib/common/file-url-to-path/polyfill.js')
30+
})
31+
32+
t.teardown(() => {
33+
Object.defineProperty(process, 'platform', t.context.platform)
34+
})
35+
36+
t.test('can convert a file url to a path', async (t) => {
37+
const url = new URL('file:///some/path')
38+
const result = fileURLToPath(url)
39+
t.type(result, 'string', 'result is a string')
40+
t.equal(result, '/some/path', 'got the right path')
41+
})
42+
43+
t.test('allows string urls', async (t) => {
44+
const url = 'file:///some/path'
45+
const result = fileURLToPath(url)
46+
t.type(result, 'string', 'result is a string')
47+
t.equal(result, '/some/path', 'got the right path')
48+
})
49+
50+
t.test('allows url encoded characters', async (t) => {
51+
const url = 'file:///some%20path'
52+
const result = fileURLToPath(url)
53+
t.type(result, 'string', 'result is a string')
54+
t.equal(result, '/some path', 'got the right path')
55+
})
56+
57+
t.test('url encoded / throws ERR_INVALID_FILE_URL_PATH', async (t) => {
58+
t.throws(() => fileURLToPath('file:///some%2fpath'), {
59+
code: 'ERR_INVALID_FILE_URL_PATH',
60+
message: /must not include encoded/,
61+
}, '%2f encoded / throws')
62+
63+
t.throws(() => fileURLToPath('file:///some%2Fpath'), {
64+
code: 'ERR_INVALID_FILE_URL_PATH',
65+
message: /must not include encoded/,
66+
}, '%2F encoded / throws')
67+
})
68+
69+
t.test('urls with a hostname throw ERR_INVALID_FILE_URL_HOST', async (t) => {
70+
t.throws(() => fileURLToPath('file://host/some/path'), {
71+
code: 'ERR_INVALID_FILE_URL_HOST',
72+
message: /host must be "localhost" or empty/,
73+
}, 'hostname present throws')
74+
})
75+
})
76+
77+
t.test('windows', async (t) => {
78+
// t.mock instead of require so we flush the cache first
79+
let fileURLToPath
80+
t.before(() => {
81+
t.context.platform = Object.getOwnPropertyDescriptor(process, 'platform')
82+
Object.defineProperty(process, 'platform', {
83+
...t.context.platform,
84+
value: 'win32',
85+
})
86+
fileURLToPath = t.mock('../../../lib/common/file-url-to-path/polyfill.js')
87+
})
88+
89+
t.teardown(() => {
90+
Object.defineProperty(process, 'platform', t.context.platform)
91+
})
92+
93+
t.test('can convert a file url to a path', async (t) => {
94+
const url = new URL('file://C:\\some\\path')
95+
const result = fileURLToPath(url)
96+
t.type(result, 'string', 'result is a string')
97+
t.equal(result, 'C:\\some\\path', 'got the right path')
98+
})
99+
100+
t.test('allows string urls', async (t) => {
101+
const url = 'file://C:\\some\\path'
102+
const result = fileURLToPath(url)
103+
t.type(result, 'string', 'result is a string')
104+
t.equal(result, 'C:\\some\\path', 'got the right path')
105+
})
106+
107+
t.test('allows hostnames', async (t) => {
108+
const url = 'file://host/some/path'
109+
const result = fileURLToPath(url)
110+
t.type(result, 'string', 'result is a string')
111+
t.equal(result, '\\\\host\\some\\path', 'got the right path')
112+
})
113+
114+
t.test('allows url encoded characters', async (t) => {
115+
const url = 'file://host/some%20path'
116+
const result = fileURLToPath(url)
117+
t.type(result, 'string', 'result is a string')
118+
t.equal(result, '\\\\host\\some path', 'got the right path')
119+
})
120+
121+
t.test('non-absolute path throws ERR_INVALID_FILE_URL_PATH', async (t) => {
122+
t.throws(() => fileURLToPath('file://\\some\\path'), {
123+
code: 'ERR_INVALID_FILE_URL_PATH',
124+
message: /must be absolute/,
125+
}, 'path with no drive letter threw')
126+
})
127+
128+
t.test('encoded \\ or / characters throw ERR_INVALID_FILE_URL_PATH', async (t) => {
129+
t.throws(() => fileURLToPath('file://c:/some%5cpath'), {
130+
code: 'ERR_INVALID_FILE_URL_PATH',
131+
message: /must not include encoded/,
132+
}, '%5c encoded \\ threw')
133+
t.throws(() => fileURLToPath('file://c:/some%5Cpath'), {
134+
code: 'ERR_INVALID_FILE_URL_PATH',
135+
message: /must not include encoded/,
136+
}, '%5C encoded \\ threw')
137+
t.throws(() => fileURLToPath('file://c:/some%2fpath'), {
138+
code: 'ERR_INVALID_FILE_URL_PATH',
139+
message: /must not include encoded/,
140+
}, '%2f encoded / threw')
141+
t.throws(() => fileURLToPath('file://c:/some%2Fpath'), {
142+
code: 'ERR_INVALID_FILE_URL_PATH',
143+
message: /must not include encoded/,
144+
}, '%2F encoded / threw')
145+
})
146+
})

0 commit comments

Comments
 (0)