Skip to content

Commit 5f1bba4

Browse files
committed
feat(stringify): add escape for quoted-pair
1 parent 4497340 commit 5f1bba4

File tree

5 files changed

+153
-6
lines changed

5 files changed

+153
-6
lines changed

auth_param.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
{
1818
"name": "dirty auth param",
1919
"header": "a=a,b=b, c=c , d=d , e=e ",
20-
"expected": { "a": "a", "b": "b", "c": "c", "d": "d", "e": "e" }
20+
"expected": { "a": "a", "b": "b", "c": "c", "d": "d", "e": "e" },
21+
"canonical": "a=a, b=b, c=c, d=d, e=e"
2122
},
2223
{
2324
"name": "all char",
@@ -34,7 +35,8 @@
3435
{
3536
"name": "quoted-pair",
3637
"header": "a=\"\\a\", b=\"\\\\\"",
37-
"expected": { "a": "\"a\"", "b": "\"\\\"" }
38+
"expected": { "a": "\"a\"", "b": "\"\\\"" },
39+
"canonical": "a=\"a\", b=\"\\\\\""
3840
},
3941
{
4042
"name": "duplicate key",

deno.lock

Lines changed: 1 addition & 0 deletions
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
@@ -6,3 +6,4 @@ export { trim } from "https://deno.land/x/[email protected]/trim.ts";
66
export { head } from "https://deno.land/x/[email protected]/head.ts";
77
export { isString } from "https://deno.land/x/[email protected]/is_string.ts";
88
export { isNullable } from "https://deno.land/x/[email protected]/is_nullable.ts";
9+
export { mapValues } from "https://deno.land/[email protected]/collections/map_values.ts";

stringify.ts

Lines changed: 29 additions & 2 deletions
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 { isNullable, isString, toLowerCase } from "./deps.ts";
4+
import { isNullable, isString, mapValues, toLowerCase } from "./deps.ts";
55
import { duplicate } from "./utils.ts";
66
import { Msg } from "./constants.ts";
77
import type { Authorization, AuthParams } from "./types.ts";
@@ -75,7 +75,7 @@ export function assertToken68(
7575
}
7676

7777
const reQuotedString =
78-
/^"(?:\t| |!|[ \x23-\x5B\x5D-\x7E]|[\x80-\xFF]|\\(?:\t| |[\x21-\x7E])[\x80-\xFF])*"$/;
78+
/^"(?:\t| |!|[ \x23-\x5B\x5D-\x7E]|[\x80-\xFF]|\\(?:\t| |[\x21-\x7E]|[\x80-\xFF]))*"$/;
7979

8080
export function isQuotedString(input: string): boolean {
8181
return reQuotedString.test(input);
@@ -98,6 +98,8 @@ function assertAuthParam(input: AuthParams): asserts input {
9898
}
9999

100100
export function stringifyAuthParams(input: AuthParams): string {
101+
input = mapValues(input, normalizeParameterValue);
102+
101103
assertAuthParam(input);
102104

103105
return Object
@@ -106,6 +108,31 @@ export function stringifyAuthParams(input: AuthParams): string {
106108
.join(", ");
107109
}
108110

111+
export function normalizeParameterValue(input: string): string {
112+
return isQuoted(input) ? `"${escapeOctet(trimChar(input))}"` : input;
113+
}
114+
115+
/** Escape DQuote and Backslash.
116+
* Skip escaped.
117+
* @see https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.4-5
118+
*/
119+
export function escapeOctet(input: string): string {
120+
// TODO(miyauci): dirty
121+
return input
122+
.replaceAll(`"`, `\\"`)
123+
.replaceAll("\\", "\\\\")
124+
.replaceAll("\\\\\\\\", "\\\\")
125+
.replaceAll(`\\\\"`, '\\"');
126+
}
127+
128+
export function isQuoted(input: string): input is `"${string}"` {
129+
return /^".*"$/.test(input);
130+
}
131+
132+
export function trimChar(input: string): string {
133+
return input.slice(1, -1);
134+
}
135+
109136
function joinEntry(
110137
entry: readonly [string, string],
111138
): string {

stringify_test.ts

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,35 @@
1-
import { AuthorizationLike, stringifyAuthorization } from "./stringify.ts";
1+
import {
2+
AuthorizationLike,
3+
escapeOctet,
4+
isQuoted,
5+
stringifyAuthorization,
6+
stringifyAuthParams,
7+
trimChar,
8+
} from "./stringify.ts";
29
import {
310
assertEquals,
411
assertThrows,
512
Authorization,
613
describe,
714
it,
815
} from "./_dev_deps.ts";
9-
1016
import authorization from "./authorization.json" assert { type: "json" };
17+
import authParam from "./auth_param.json" assert { type: "json" };
18+
19+
describe("stringifyAuthParams", () => {
20+
authParam.forEach((v) => {
21+
it(v.name, () => {
22+
if (!v.must_fail && v.expected) {
23+
const input = v.canonical ? v.canonical : v.header;
24+
25+
assertEquals(
26+
stringifyAuthParams(v.expected as Record<string, string>),
27+
input,
28+
);
29+
}
30+
});
31+
});
32+
});
1133

1234
describe("stringifyAuthorization", () => {
1335
it("should return string if the input is valid", () => {
@@ -61,3 +83,97 @@ describe("stringifyAuthorization", () => {
6183
});
6284
});
6385
});
86+
87+
describe("escapeOctet", () => {
88+
it("should escape double quote and backslash", () => {
89+
const table: [string, string][] = [
90+
["", ""],
91+
["a", "a"],
92+
[`\\"`, `\\"`],
93+
[`""`, `\\"\\"`],
94+
[`"\\"`, `\\"\\"`],
95+
[`\\"\\"`, `\\"\\"`],
96+
[`"""`, `\\"\\"\\"`],
97+
[`""""`, `\\"\\"\\"\\"`],
98+
[`"`, `\\"`],
99+
[`"a"`, `\\"a\\"`],
100+
[`a\\`, `a\\\\`],
101+
[`a"`, `a\\"`],
102+
[`a\\"`, `a\\"`],
103+
[`\\\\`, `\\\\`],
104+
[`\\\\\\`, `\\\\\\\\`],
105+
[`\\\\\\\\`, `\\\\\\\\`],
106+
[`\\`, `\\\\`],
107+
['"\\"', `\\"\\"`],
108+
['\\\\\\"', `\\\\\\"`],
109+
];
110+
111+
table.forEach(([input, expected]) => {
112+
assertEquals(escapeOctet(input), expected);
113+
});
114+
});
115+
});
116+
117+
describe("escapeOctet", () => {
118+
it("should escape double quote and backslash", () => {
119+
const table: [string, string][] = [
120+
["", ""],
121+
["a", "a"],
122+
[`\\"`, `\\"`],
123+
[`""`, `\\"\\"`],
124+
[`"\\"`, `\\"\\"`],
125+
[`\\"\\"`, `\\"\\"`],
126+
[`"""`, `\\"\\"\\"`],
127+
[`""""`, `\\"\\"\\"\\"`],
128+
[`"`, `\\"`],
129+
[`"a"`, `\\"a\\"`],
130+
[`a\\`, `a\\\\`],
131+
[`a"`, `a\\"`],
132+
[`a\\"`, `a\\"`],
133+
[`\\\\`, `\\\\`],
134+
[`\\\\\\`, `\\\\\\\\`],
135+
[`\\\\\\\\`, `\\\\\\\\`],
136+
[`\\`, `\\\\`],
137+
['"\\"', `\\"\\"`],
138+
['\\\\\\"', `\\\\\\"`],
139+
];
140+
141+
table.forEach(([input, expected]) => {
142+
assertEquals(escapeOctet(input), expected);
143+
});
144+
});
145+
});
146+
147+
describe("isQuoted", () => {
148+
it("should pass", () => {
149+
const table: [string, boolean][] = [
150+
["a", false],
151+
["ab", false],
152+
[`"`, false],
153+
[` ""`, false],
154+
[`""`, true],
155+
[`"a"`, true],
156+
[`""""`, true],
157+
[`"""`, true],
158+
];
159+
160+
table.forEach(([input, expected]) => {
161+
assertEquals(isQuoted(input), expected);
162+
});
163+
});
164+
});
165+
166+
describe("trimChar", () => {
167+
it("should pass", () => {
168+
const table: [string, string][] = [
169+
["a", ""],
170+
["ab", ""],
171+
["abc", "b"],
172+
["abcd", "bc"],
173+
];
174+
175+
table.forEach(([input, expected]) => {
176+
assertEquals(trimChar(input), expected);
177+
});
178+
});
179+
});

0 commit comments

Comments
 (0)