Skip to content

Commit dbed45b

Browse files
committed
perf(parse): remove complex regex pattern
1 parent 0eeb39e commit dbed45b

File tree

7 files changed

+117
-43
lines changed

7 files changed

+117
-43
lines changed

_abnf.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,7 @@ import { optimize } from "https://esm.sh/regexp-tree";
1010

1111
const tchar = /[!#$%&'*+.^_`|~\dA-Za-z-]/;
1212
const token = atomic(suffix("+", tchar));
13-
const authScheme = namedCapture("authScheme", token);
1413
const SP = / /;
15-
const ALPHA = /[A-Za-z]/;
16-
const DIGIT = /\d/;
17-
const token68 = sequence(
18-
atomic(suffix("+", either(ALPHA, DIGIT, /[-._~+/]/))),
19-
atomic(suffix("*", "=")),
20-
);
21-
22-
const challenge = sequence(
23-
authScheme,
24-
maybe(
25-
atomic(suffix("+", SP)),
26-
either(
27-
namedCapture("token68", token68),
28-
namedCapture("authParam", atomic(/.*/)),
29-
),
30-
),
31-
);
3214

3315
const OWS = /[ \t]*/;
3416
const BWS = OWS;
@@ -64,7 +46,6 @@ const authParam = sequence(
6446
);
6547

6648
if (import.meta.main) {
67-
console.log("challenge:", optimize(challenge).toRegExp());
6849
console.log("element: ", element);
6950
console.log("authParam: ", optimize(authParam).toRegExp());
7051
}

_tools/meta.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,9 @@ export const makeOptions = (version: string): BuildOptions => ({
7474
version: "1.2.0",
7575
subPath: "list.js",
7676
},
77+
"https://esm.sh/[email protected]?pin=v118": {
78+
name: "escape-string-regexp",
79+
version: "5.0.0",
80+
},
7781
},
7882
});

deno.lock

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export { mapValues } from "https://deno.land/[email protected]/collections/map_values.
99
export { isQuotedString } from "https://deno.land/x/[email protected]/quoted_string.ts";
1010
export { isToken } from "https://deno.land/x/[email protected]/token.ts";
1111
export { parseListFields } from "https://deno.land/x/[email protected]/list.ts";
12+
export { default as escapeStringRegExp } from "https://esm.sh/[email protected]?pin=v118";

parse.ts

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
22
// This module is browser compatible.
33

4-
import { duplicate } from "./utils.ts";
5-
import { head, isString, parseListFields, toLowerCase } from "./deps.ts";
4+
import { divideWhile, duplicate, isToken68, trimStartBy } from "./utils.ts";
5+
import {
6+
head,
7+
isString,
8+
isToken,
9+
parseListFields,
10+
toLowerCase,
11+
} from "./deps.ts";
612
import { Msg } from "./constants.ts";
713
import type { Authorization, AuthParams } from "./types.ts";
814

9-
/** Generate from _abnf.ts. */
10-
const reAuthorization =
11-
/^(?<authScheme>(?=([\w!#$%&'*+.^`|~-]+))\2)(?:(?=( +))\3(?:(?<token68>(?=((?:[A-Za-z]|\d|[+./_~-])+))\5(?=(=*))\6)|(?<authParam>(?=(.*))\8)))?$/;
12-
1315
/** Parse string into {@link Authorization}.
1416
*
1517
* @example
@@ -39,24 +41,28 @@ const reAuthorization =
3941
* @throws {Error} If the auth param key is duplicated.
4042
*/
4143
export function parseAuthorization(input: string): Authorization {
42-
const result = reAuthorization.exec(input);
44+
const result = divideWhile(input, isToken);
4345

44-
if (!result || !result.groups) throw SyntaxError(Msg.InvalidSyntax);
46+
if (!result) throw new SyntaxError(Msg.InvalidSyntax);
4547

46-
const groups = result.groups as ParsedGroups;
47-
const { authScheme } = groups;
48-
const params = isString(groups.authParam)
49-
? parseAuthParams(groups.authParam)
50-
: groups.token68;
48+
const [authScheme, rest] = result;
5149

52-
return { authScheme, params: params ?? null };
53-
}
50+
// challenge = auth-scheme
51+
if (!rest) return { authScheme, params: null };
52+
if (!rest.startsWith(" ")) throw new SyntaxError(Msg.InvalidSyntax);
53+
54+
const maybeToken68OrAuthParam = trimStartBy(rest, " ");
5455

55-
type ParsedGroups = {
56-
readonly authScheme: string;
57-
readonly token68: string | undefined;
58-
readonly authParam: string | undefined;
59-
};
56+
// challenge = auth-scheme [ 1*SP ( token68 ) ]
57+
if (isToken68(maybeToken68OrAuthParam)) {
58+
return { authScheme, params: maybeToken68OrAuthParam };
59+
}
60+
61+
// challenge = auth-scheme [ 1*SP ( #auth-param ) ]
62+
const params = parseAuthParams(maybeToken68OrAuthParam);
63+
64+
return { authScheme, params };
65+
}
6066

6167
/** Generate from _abnf.ts. */
6268
const reAuthParam =
@@ -79,7 +85,7 @@ export function parseAuthParams(input: string): AuthParams {
7985
const entries = list.map((el) => {
8086
const result = reAuthParam.exec(el);
8187

82-
if (!result || !result.groups) throw SyntaxError(Msg.InvalidSyntax);
88+
if (!result || !result.groups) throw new SyntaxError(Msg.InvalidSyntax);
8389

8490
const groups = result.groups as AuthParamGroups;
8591
const value = isString(groups.token)

utils.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright 2023-latest the httpland authors. All rights reserved. MIT license.
22
// This module is browser compatible.
33

4-
import { isToken } from "./deps.ts";
4+
import { escapeStringRegExp, isToken } from "./deps.ts";
55

66
export function duplicate<T>(list: readonly T[]): T[] {
77
const duplicates = new Set<T>();
@@ -45,3 +45,28 @@ export function assertToken(
4545
): asserts input {
4646
if (!isToken(input)) throw new constructor(msg);
4747
}
48+
49+
/** Divide two string. The first string is matched, second string is rest. */
50+
export function divideWhile(
51+
input: string,
52+
match: (input: string) => boolean,
53+
): [matched: string, rest: string] | null {
54+
let matched = "";
55+
56+
for (const str of input) {
57+
if (!match(str)) break;
58+
59+
matched += str;
60+
}
61+
62+
if (!matched) return null;
63+
64+
const rest = input.slice(matched.length);
65+
66+
return [matched, rest];
67+
}
68+
69+
/** Trim start by something. */
70+
export function trimStartBy(input: string, separator: string): string {
71+
return input.replace(new RegExp(`^${escapeStringRegExp(separator)}+`), "");
72+
}

utils_test.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1-
import { assertToken, assertToken68, isToken68 } from "./utils.ts";
1+
import {
2+
assertToken,
3+
assertToken68,
4+
divideWhile,
5+
isToken68,
6+
trimStartBy,
7+
} from "./utils.ts";
28
import {
39
assert,
10+
assertEquals,
411
assertFalse,
512
assertThrows,
613
describe,
@@ -68,3 +75,45 @@ describe("assertToken", () => {
6875
assertThrows(() => assertToken("a=="));
6976
});
7077
});
78+
79+
describe("divideWhile", () => {
80+
it("should return null if the input does not match", () => {
81+
assertEquals(divideWhile("", () => false), null);
82+
assertEquals(divideWhile("a", () => false), null);
83+
assertEquals(divideWhile("abc", (str) => str === "b"), null);
84+
});
85+
86+
it("should return null if the input does not match", () => {
87+
assertEquals(divideWhile("abc", (str) => str === "a"), ["a", "bc"]);
88+
assertEquals(divideWhile("abcCBA", (str) => /[a-z]/.test(str)), [
89+
"abc",
90+
"CBA",
91+
]);
92+
assertEquals(
93+
divideWhile("あabc亜", (str) => /[a-z]/.test(str) || str === "あ"),
94+
[
95+
"あabc",
96+
"亜",
97+
],
98+
);
99+
assertEquals(divideWhile("abcCBA", (str) => /.*/.test(str)), [
100+
"abcCBA",
101+
"",
102+
]);
103+
});
104+
});
105+
106+
describe("trimStartBy", () => {
107+
it("should return trimmed string", () => {
108+
const table: [string, string, string][] = [
109+
["abc", "a", "bc"],
110+
["abcdefg", "abc", "defg"],
111+
["abcdefg", "aaa", "abcdefg"],
112+
["\\\\abc", "\\", "abc"],
113+
];
114+
115+
table.forEach(([input, separator, expected]) => {
116+
assertEquals(trimStartBy(input, separator), expected);
117+
});
118+
});
119+
});

0 commit comments

Comments
 (0)