Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
151 lines (120 loc) · 3.67 KB
/
Copy pathindex.js
File metadata and controls
151 lines (120 loc) · 3.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// Matches one or more consecutive percent-encoded bytes (e.g. `%C3%A5`).
const token = '%[a-f0-9]{2}';
const multiMatcher = new RegExp(`(${token})+`, 'gi');
const hexPair = /^[a-f\d]{2}$/i;
// Read a `%XX` sequence at `position`, returning the byte value and where to continue scanning.
function parsePercentByte(input, position) {
if (input.codePointAt(position) !== 37 || position + 3 > input.length) {
return;
}
const digits = input.slice(position + 1, position + 3);
if (!hexPair.test(digits)) {
return;
}
return {byte: Number.parseInt(digits, 16), next: position + 3};
}
/**
* Return how many bytes a UTF-8 code point needs based on its lead byte.
* Returns 0 for continuation bytes and other invalid lead bytes.
*/
function utf8SequenceLength(byte) {
if (byte <= 0x7F) {
return 1;
}
// Start at 0xC2 to exclude overlong 2-byte encodings (0xC0/0xC1).
if (byte >= 0xC2 && byte <= 0xDF) {
return 2;
}
if (byte >= 0xE0 && byte <= 0xEF) {
return 3;
}
if (byte >= 0xF0 && byte <= 0xF4) {
return 4;
}
return 0;
}
function isContinuationByte(byte) {
return byte >= 0x80 && byte <= 0xBF;
}
/**
* Decode as much of `input` as possible without throwing.
*Scans left-to-right in O(n), decoding valid UTF-8 runs and leaving the rest literal.
*/
function decode(input) {
try {
return decodeURIComponent(input);
} catch {
let output = '';
let position = 0;
while (position < input.length) {
if (input.codePointAt(position) !== 37) {
output += input.charAt(position);
position++;
continue;
}
const firstByte = parsePercentByte(input, position);
// `%` not followed by two hex digits (e.g. `%` or `%G`).
if (!firstByte) {
output += input.charAt(position);
position++;
continue;
}
const sequenceLength = utf8SequenceLength(firstByte.byte);
// Continuation byte or invalid lead byte — emit one `%XX` literally.
if (sequenceLength === 0) {
const pair = input.slice(position, position + 6);
// UTF-16 BOM byte pairs become two replacement characters.
if (pair === '%FE%FF' || pair === '%FF%FE') {
output += '\uFFFD\uFFFD';
position += 6;
continue;
}
output += input.slice(position, position + 3);
position += 3;
continue;
}
let end = firstByte.next;
let validSequence = true;
for (let index = 1; index < sequenceLength; index++) {
const nextByte = parsePercentByte(input, end);
if (!nextByte || !isContinuationByte(nextByte.byte)) {
validSequence = false;
break;
}
end = nextByte.next;
}
if (validSequence) {
const encodedSequence = input.slice(position, end);
try {
output += decodeURIComponent(encodedSequence);
position = end;
continue;
} catch {
// Invalid UTF-8 despite correct structure — emit the first byte literally.
}
}
// Missing continuation bytes or decode failure — emit the first byte literally.
const literal = input.slice(position, position + 3);
// A truncated `%C2` becomes a replacement character.
output += literal === '%C2' ? '\uFFFD' : literal;
position += 3;
}
return output;
}
}
function customDecodeURIComponent(input) {
// Decode each percent-encoded run in place in a single pass. Decoded output is never rescanned.
return input.replace(multiMatcher, run => decode(run));
}
export default function decodeUriComponent(encodedURI) {
if (typeof encodedURI !== 'string') {
throw new TypeError(`Expected \`encodedURI\` to be of type \`string\`, got \`${typeof encodedURI}\``);
}
try {
// Try the built in decoder first
return decodeURIComponent(encodedURI);
} catch {
// Fallback to a more advanced decoder
return customDecodeURIComponent(encodedURI);
}
}