2018-01-25 05:13:30 -05:00
|
|
|
import * as XRegExp from 'xregexp';
|
|
|
|
|
2019-04-24 08:27:24 -04:00
|
|
|
// The `XRegExp` typings are not accurate.
|
|
|
|
interface XRegExp extends RegExp {
|
|
|
|
xregexp: { captureNames?: string[] };
|
|
|
|
}
|
|
|
|
|
2021-04-27 15:50:11 -04:00
|
|
|
export class FirebaseRedirectSource {
|
|
|
|
regex = XRegExp(this.pattern) as XRegExp;
|
|
|
|
namedGroups: string[] = [];
|
|
|
|
|
|
|
|
private constructor(public pattern: string, public restNamedGroups: string[] = []) {
|
|
|
|
const restNamedGroupsSet = new Set(restNamedGroups);
|
|
|
|
pattern.replace(/\(\?<([^>]+)>/g, (_, name) => {
|
|
|
|
if (!restNamedGroupsSet.has(name)) {
|
|
|
|
this.namedGroups.push(name);
|
|
|
|
}
|
|
|
|
return '';
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static fromGlobPattern(glob: string): FirebaseRedirectSource {
|
|
|
|
const dot = /\./g;
|
|
|
|
const star = /\*/g;
|
|
|
|
const doubleStar = /(^|\/)\*\*($|\/)/g; // e.g. a/**/b or **/b or a/** but not a**b
|
|
|
|
const modifiedPatterns = /(.)\(([^)]+)\)/g; // e.g. `@(a|b)
|
|
|
|
const restParam = /\/:([A-Za-z]+)\*/g; // e.g. `:rest*`
|
|
|
|
const namedParam = /\/:([A-Za-z]+)/g; // e.g. `:api`
|
|
|
|
const possiblyEmptyInitialSegments = /^\.🐷\//g; // e.g. `**/a` can also match `a`
|
|
|
|
const possiblyEmptySegments = /\/\.🐷\//g; // e.g. `a/**/b` can also match `a/b`
|
|
|
|
const willBeStar = /🐷/g; // e.g. `a**b` not matched by previous rule
|
2018-01-25 05:13:30 -05:00
|
|
|
|
|
|
|
try {
|
2021-04-27 15:50:11 -04:00
|
|
|
const restNamedGroups: string[] = [];
|
2018-01-25 05:13:30 -05:00
|
|
|
const pattern = glob
|
|
|
|
.replace(dot, '\\.')
|
|
|
|
.replace(modifiedPatterns, replaceModifiedPattern)
|
|
|
|
.replace(restParam, (_, param) => {
|
|
|
|
// capture the rest of the string
|
2021-04-27 15:50:11 -04:00
|
|
|
restNamedGroups.push(param);
|
2018-01-25 05:13:30 -05:00
|
|
|
return `(?:/(?<${param}>.🐷))?`;
|
|
|
|
})
|
2021-04-27 15:50:11 -04:00
|
|
|
.replace(namedParam, `/(?<$1>[^/]+)`)
|
2018-01-25 05:13:30 -05:00
|
|
|
.replace(doubleStar, '$1.🐷$2') // use the pig to avoid replacing ** in next rule
|
|
|
|
.replace(star, '[^/]*') // match a single segment
|
|
|
|
.replace(possiblyEmptyInitialSegments, '(?:.*)')// deal with **/ special cases
|
|
|
|
.replace(possiblyEmptySegments, '(?:/|/.*/)') // deal with /**/ special cases
|
|
|
|
.replace(willBeStar, '*'); // other ** matches
|
2021-04-27 15:50:11 -04:00
|
|
|
|
|
|
|
return new FirebaseRedirectSource(`^${pattern}$`, restNamedGroups);
|
|
|
|
} catch (err) {
|
|
|
|
throw new Error(`Error in FirebaseRedirectSource: "${glob}" - ${err.message}`);
|
2018-01-25 05:13:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-27 15:50:12 -04:00
|
|
|
static fromRegexPattern(regex: string): FirebaseRedirectSource {
|
|
|
|
try {
|
|
|
|
// NOTE:
|
|
|
|
// Firebase redirect regexes use the [RE2 library](https://github.com/google/re2/wiki/Syntax),
|
|
|
|
// which requires named capture groups to begin with `?P`. See
|
|
|
|
// https://firebase.google.com/docs/hosting/full-config#capture-url-segments-for-redirects.
|
|
|
|
|
|
|
|
if (/\(\?<[^>]+>/.test(regex)) {
|
|
|
|
// Throw if the regex contains a non-RE2 named capture group.
|
|
|
|
throw new Error(
|
|
|
|
'The regular expression pattern contains a named capture group of the format ' +
|
|
|
|
'`(?<name>...)`, which is not compatible with the RE2 library. Use `(?P<name>...)` ' +
|
|
|
|
'instead.');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Replace `(?P<...>` with just `(?<...>` to convert to `XRegExp`-compatible syntax for named
|
|
|
|
// capture groups.
|
|
|
|
return new FirebaseRedirectSource(regex.replace(/(\(\?)P(<[^>]+>)/g, '$1$2'));
|
|
|
|
} catch (err) {
|
|
|
|
throw new Error(`Error in FirebaseRedirectSource: "${regex}" - ${err.message}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-03 05:46:38 -04:00
|
|
|
test(url: string): boolean {
|
2018-01-25 05:13:30 -05:00
|
|
|
return XRegExp.test(url, this.regex);
|
|
|
|
}
|
|
|
|
|
2019-10-03 05:46:38 -04:00
|
|
|
match(url: string): { [key: string]: string } | undefined {
|
2021-04-27 15:50:11 -04:00
|
|
|
const match = XRegExp.exec(url, this.regex) as ReturnType<typeof XRegExp.exec>;
|
2019-10-03 05:46:38 -04:00
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
return undefined;
|
2018-01-25 05:13:30 -05:00
|
|
|
}
|
2019-10-03 05:46:38 -04:00
|
|
|
|
|
|
|
const result: { [key: string]: string } = {};
|
|
|
|
const names = this.regex.xregexp.captureNames || [];
|
2021-04-13 09:49:43 -04:00
|
|
|
names.forEach(name => result[name] = match.groups![name]);
|
2019-10-03 05:46:38 -04:00
|
|
|
return result;
|
2018-01-25 05:13:30 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-24 08:27:24 -04:00
|
|
|
function replaceModifiedPattern(_: string, modifier: string, pattern: string) {
|
2018-01-25 05:13:30 -05:00
|
|
|
switch (modifier) {
|
|
|
|
case '!':
|
|
|
|
throw new Error(`"not" expansions are not supported: "${_}"`);
|
|
|
|
case '?':
|
|
|
|
case '+':
|
|
|
|
return `(${pattern})${modifier}`;
|
|
|
|
case '*':
|
|
|
|
return `(${pattern})🐷`; // it will become a star
|
|
|
|
case '@':
|
|
|
|
return `(${pattern})`;
|
|
|
|
default:
|
|
|
|
throw new Error(`unknown expansion type: "${modifier}" in "${_}"`);
|
|
|
|
}
|
|
|
|
}
|