@irvinebroque/http-rfc-utils 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/auth.d.ts +139 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +991 -0
- package/dist/auth.js.map +1 -0
- package/dist/cache-status.d.ts +15 -0
- package/dist/cache-status.d.ts.map +1 -0
- package/dist/cache-status.js +152 -0
- package/dist/cache-status.js.map +1 -0
- package/dist/cache.d.ts +94 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +244 -0
- package/dist/cache.js.map +1 -0
- package/dist/client-hints.d.ts +23 -0
- package/dist/client-hints.d.ts.map +1 -0
- package/dist/client-hints.js +81 -0
- package/dist/client-hints.js.map +1 -0
- package/dist/conditional.d.ts +97 -0
- package/dist/conditional.d.ts.map +1 -0
- package/dist/conditional.js +300 -0
- package/dist/conditional.js.map +1 -0
- package/dist/content-disposition.d.ts +23 -0
- package/dist/content-disposition.d.ts.map +1 -0
- package/dist/content-disposition.js +122 -0
- package/dist/content-disposition.js.map +1 -0
- package/dist/cookie.d.ts +43 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +472 -0
- package/dist/cookie.js.map +1 -0
- package/dist/cors.d.ts +53 -0
- package/dist/cors.d.ts.map +1 -0
- package/dist/cors.js +170 -0
- package/dist/cors.js.map +1 -0
- package/dist/datetime.d.ts +53 -0
- package/dist/datetime.d.ts.map +1 -0
- package/dist/datetime.js +205 -0
- package/dist/datetime.js.map +1 -0
- package/dist/digest.d.ts +220 -0
- package/dist/digest.d.ts.map +1 -0
- package/dist/digest.js +355 -0
- package/dist/digest.js.map +1 -0
- package/dist/encoding.d.ts +14 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +86 -0
- package/dist/encoding.js.map +1 -0
- package/dist/etag.d.ts +55 -0
- package/dist/etag.d.ts.map +1 -0
- package/dist/etag.js +182 -0
- package/dist/etag.js.map +1 -0
- package/dist/ext-value.d.ts +40 -0
- package/dist/ext-value.d.ts.map +1 -0
- package/dist/ext-value.js +119 -0
- package/dist/ext-value.js.map +1 -0
- package/dist/forwarded.d.ts +14 -0
- package/dist/forwarded.d.ts.map +1 -0
- package/dist/forwarded.js +93 -0
- package/dist/forwarded.js.map +1 -0
- package/dist/header-utils.d.ts +71 -0
- package/dist/header-utils.d.ts.map +1 -0
- package/dist/header-utils.js +143 -0
- package/dist/header-utils.js.map +1 -0
- package/dist/headers.d.ts +71 -0
- package/dist/headers.d.ts.map +1 -0
- package/dist/headers.js +134 -0
- package/dist/headers.js.map +1 -0
- package/dist/hsts.d.ts +15 -0
- package/dist/hsts.d.ts.map +1 -0
- package/dist/hsts.js +106 -0
- package/dist/hsts.js.map +1 -0
- package/dist/http-signatures.d.ts +202 -0
- package/dist/http-signatures.d.ts.map +1 -0
- package/dist/http-signatures.js +720 -0
- package/dist/http-signatures.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/json-pointer.d.ts +97 -0
- package/dist/json-pointer.d.ts.map +1 -0
- package/dist/json-pointer.js +278 -0
- package/dist/json-pointer.js.map +1 -0
- package/dist/jsonpath.d.ts +98 -0
- package/dist/jsonpath.d.ts.map +1 -0
- package/dist/jsonpath.js +1470 -0
- package/dist/jsonpath.js.map +1 -0
- package/dist/language.d.ts +14 -0
- package/dist/language.d.ts.map +1 -0
- package/dist/language.js +95 -0
- package/dist/language.js.map +1 -0
- package/dist/link.d.ts +102 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +437 -0
- package/dist/link.js.map +1 -0
- package/dist/linkset.d.ts +111 -0
- package/dist/linkset.d.ts.map +1 -0
- package/dist/linkset.js +501 -0
- package/dist/linkset.js.map +1 -0
- package/dist/negotiate.d.ts +71 -0
- package/dist/negotiate.d.ts.map +1 -0
- package/dist/negotiate.js +357 -0
- package/dist/negotiate.js.map +1 -0
- package/dist/pagination.d.ts +80 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +188 -0
- package/dist/pagination.js.map +1 -0
- package/dist/prefer.d.ts +18 -0
- package/dist/prefer.d.ts.map +1 -0
- package/dist/prefer.js +93 -0
- package/dist/prefer.js.map +1 -0
- package/dist/problem.d.ts +54 -0
- package/dist/problem.d.ts.map +1 -0
- package/dist/problem.js +104 -0
- package/dist/problem.js.map +1 -0
- package/dist/proxy-status.d.ts +28 -0
- package/dist/proxy-status.d.ts.map +1 -0
- package/dist/proxy-status.js +220 -0
- package/dist/proxy-status.js.map +1 -0
- package/dist/range.d.ts +28 -0
- package/dist/range.d.ts.map +1 -0
- package/dist/range.js +243 -0
- package/dist/range.js.map +1 -0
- package/dist/response.d.ts +101 -0
- package/dist/response.d.ts.map +1 -0
- package/dist/response.js +200 -0
- package/dist/response.js.map +1 -0
- package/dist/sorting.d.ts +66 -0
- package/dist/sorting.d.ts.map +1 -0
- package/dist/sorting.js +168 -0
- package/dist/sorting.js.map +1 -0
- package/dist/structured-fields.d.ts +30 -0
- package/dist/structured-fields.d.ts.map +1 -0
- package/dist/structured-fields.js +468 -0
- package/dist/structured-fields.js.map +1 -0
- package/dist/types.d.ts +772 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/uri-template.d.ts +48 -0
- package/dist/uri-template.d.ts.map +1 -0
- package/dist/uri-template.js +483 -0
- package/dist/uri-template.js.map +1 -0
- package/dist/uri.d.ts +80 -0
- package/dist/uri.d.ts.map +1 -0
- package/dist/uri.js +423 -0
- package/dist/uri.js.map +1 -0
- package/package.json +66 -0
package/dist/sorting.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a sort string into field/direction pairs.
|
|
3
|
+
*
|
|
4
|
+
* Format: "field1,-field2,field3"
|
|
5
|
+
* - Prefix with '-' for descending
|
|
6
|
+
* - No prefix for ascending
|
|
7
|
+
* - Multiple fields separated by comma
|
|
8
|
+
*
|
|
9
|
+
* @param sort - The sort string
|
|
10
|
+
* @returns Array of SortField objects
|
|
11
|
+
*/
|
|
12
|
+
export function parseSortString(sort) {
|
|
13
|
+
if (!sort || !sort.trim()) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
return sort
|
|
17
|
+
.split(',')
|
|
18
|
+
.map(field => field.trim())
|
|
19
|
+
.filter(field => field.length > 0)
|
|
20
|
+
.map(field => {
|
|
21
|
+
if (field.startsWith('-')) {
|
|
22
|
+
return {
|
|
23
|
+
field: field.slice(1),
|
|
24
|
+
direction: 'desc'
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
field,
|
|
29
|
+
direction: 'asc'
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get a nested value from an object using dot notation.
|
|
35
|
+
*
|
|
36
|
+
* @param obj - The object to access
|
|
37
|
+
* @param path - Dot-separated path (e.g., "user.name")
|
|
38
|
+
* @returns The value at the path, or undefined if not found
|
|
39
|
+
*/
|
|
40
|
+
function getNestedValue(obj, path) {
|
|
41
|
+
const parts = path.split('.');
|
|
42
|
+
let current = obj;
|
|
43
|
+
for (const part of parts) {
|
|
44
|
+
if (current === null || current === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
if (typeof current !== 'object') {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
current = current[part];
|
|
51
|
+
}
|
|
52
|
+
return current;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Compare two values for sorting.
|
|
56
|
+
* Internal helper exposed for custom comparators.
|
|
57
|
+
*
|
|
58
|
+
* @param a - First value
|
|
59
|
+
* @param b - Second value
|
|
60
|
+
* @param direction - Sort direction
|
|
61
|
+
* @returns Comparison result (-1, 0, 1)
|
|
62
|
+
*/
|
|
63
|
+
export function compareValues(a, b, direction) {
|
|
64
|
+
const multiplier = direction === 'desc' ? -1 : 1;
|
|
65
|
+
// Handle null/undefined - always sort last
|
|
66
|
+
const aIsNullish = a === null || a === undefined;
|
|
67
|
+
const bIsNullish = b === null || b === undefined;
|
|
68
|
+
if (aIsNullish && bIsNullish) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
if (aIsNullish) {
|
|
72
|
+
return 1; // a sorts last regardless of direction
|
|
73
|
+
}
|
|
74
|
+
if (bIsNullish) {
|
|
75
|
+
return -1; // b sorts last regardless of direction
|
|
76
|
+
}
|
|
77
|
+
// Handle dates
|
|
78
|
+
if (a instanceof Date && b instanceof Date) {
|
|
79
|
+
return multiplier * (a.getTime() - b.getTime());
|
|
80
|
+
}
|
|
81
|
+
// Handle booleans (false < true)
|
|
82
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
83
|
+
if (a === b)
|
|
84
|
+
return 0;
|
|
85
|
+
return multiplier * (a ? 1 : -1);
|
|
86
|
+
}
|
|
87
|
+
// Handle numbers
|
|
88
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
89
|
+
return multiplier * (a - b);
|
|
90
|
+
}
|
|
91
|
+
// Handle strings (case-insensitive)
|
|
92
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
93
|
+
const comparison = a.toLowerCase().localeCompare(b.toLowerCase());
|
|
94
|
+
return multiplier * comparison;
|
|
95
|
+
}
|
|
96
|
+
// Mixed types - convert to strings for comparison
|
|
97
|
+
const aStr = String(a).toLowerCase();
|
|
98
|
+
const bStr = String(b).toLowerCase();
|
|
99
|
+
return multiplier * aStr.localeCompare(bStr);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Apply sorting to an array of objects.
|
|
103
|
+
*
|
|
104
|
+
* @param data - Array to sort (not mutated, returns new array)
|
|
105
|
+
* @param sort - Sort string (e.g., "-date,title")
|
|
106
|
+
* @returns Sorted array
|
|
107
|
+
*
|
|
108
|
+
* Sorting rules:
|
|
109
|
+
* - undefined/null values sort last
|
|
110
|
+
* - Strings are compared case-insensitively
|
|
111
|
+
* - Numbers compared numerically
|
|
112
|
+
* - Dates compared by timestamp
|
|
113
|
+
* - Booleans: false < true
|
|
114
|
+
* - Multiple sort fields applied in order
|
|
115
|
+
*/
|
|
116
|
+
export function applySorting(data, sort) {
|
|
117
|
+
if (!sort || !sort.trim()) {
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
const sortFields = parseSortString(sort);
|
|
121
|
+
if (sortFields.length === 0) {
|
|
122
|
+
return data;
|
|
123
|
+
}
|
|
124
|
+
// Create shallow copy to avoid mutating input
|
|
125
|
+
const result = [...data];
|
|
126
|
+
result.sort((a, b) => {
|
|
127
|
+
for (const { field, direction } of sortFields) {
|
|
128
|
+
const aValue = getNestedValue(a, field);
|
|
129
|
+
const bValue = getNestedValue(b, field);
|
|
130
|
+
const comparison = compareValues(aValue, bValue, direction);
|
|
131
|
+
if (comparison !== 0) {
|
|
132
|
+
return comparison;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Validate that sort fields exist in the data.
|
|
141
|
+
*
|
|
142
|
+
* @param sort - Sort string
|
|
143
|
+
* @param allowedFields - Array of allowed field names
|
|
144
|
+
* @returns true if all fields are allowed, false otherwise
|
|
145
|
+
*/
|
|
146
|
+
export function validateSortFields(sort, allowedFields) {
|
|
147
|
+
const sortFields = parseSortString(sort);
|
|
148
|
+
if (sortFields.length === 0) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const allowedSet = new Set(allowedFields);
|
|
152
|
+
return sortFields.every(({ field }) => allowedSet.has(field));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build a sort string from field/direction pairs.
|
|
156
|
+
* Inverse of parseSortString.
|
|
157
|
+
*
|
|
158
|
+
* @param fields - Array of SortField objects
|
|
159
|
+
* @returns Sort string
|
|
160
|
+
*/
|
|
161
|
+
export function buildSortString(fields) {
|
|
162
|
+
return fields
|
|
163
|
+
.map(({ field, direction }) => {
|
|
164
|
+
return direction === 'desc' ? `-${field}` : field;
|
|
165
|
+
})
|
|
166
|
+
.join(',');
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=sorting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sorting.js","sourceRoot":"","sources":["../src/sorting.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Field Values per RFC 8941.
|
|
3
|
+
* RFC 8941 §3, §4.
|
|
4
|
+
*/
|
|
5
|
+
import type { SfItem, SfList, SfDictionary } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse a Structured Field List.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseSfList(value: string): SfList | null;
|
|
10
|
+
/**
|
|
11
|
+
* Parse a Structured Field Dictionary.
|
|
12
|
+
*/
|
|
13
|
+
export declare function parseSfDict(value: string): SfDictionary | null;
|
|
14
|
+
/**
|
|
15
|
+
* Parse a Structured Field Item.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseSfItem(value: string): SfItem | null;
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a Structured Field List.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serializeSfList(list: SfList): string;
|
|
22
|
+
/**
|
|
23
|
+
* Serialize a Structured Field Dictionary.
|
|
24
|
+
*/
|
|
25
|
+
export declare function serializeSfDict(dict: SfDictionary): string;
|
|
26
|
+
/**
|
|
27
|
+
* Serialize a Structured Field Item.
|
|
28
|
+
*/
|
|
29
|
+
export declare function serializeSfItem(item: SfItem): string;
|
|
30
|
+
//# sourceMappingURL=structured-fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"structured-fields.d.ts","sourceRoot":"","sources":["../src/structured-fields.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAc,MAAM,EAAe,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAiXxF;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxD;AAED;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAU9D;AAED;;GAEG;AAEH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUxD;AAqED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOpD;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAoB1D;AAED;;GAEG;AAEH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD"}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured Field Values per RFC 8941.
|
|
3
|
+
* RFC 8941 §3, §4.
|
|
4
|
+
*/
|
|
5
|
+
import { Buffer } from 'node:buffer';
|
|
6
|
+
// RFC 8941 §3.3.4: sf-token allows ALPHA (case-insensitive), digits, and tchar plus : and /
|
|
7
|
+
const TOKEN_RE = /^[A-Za-z*][A-Za-z0-9!#$%&'*+\-.^_`|~:\/]*$/;
|
|
8
|
+
const BASE64_RE = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
9
|
+
const KEY_RE = /^[a-z*][a-z0-9_\-\.\*]*$/;
|
|
10
|
+
class Parser {
|
|
11
|
+
input;
|
|
12
|
+
index;
|
|
13
|
+
constructor(input) {
|
|
14
|
+
this.input = input;
|
|
15
|
+
this.index = 0;
|
|
16
|
+
}
|
|
17
|
+
eof() {
|
|
18
|
+
return this.index >= this.input.length;
|
|
19
|
+
}
|
|
20
|
+
peek() {
|
|
21
|
+
return this.input[this.index] ?? '';
|
|
22
|
+
}
|
|
23
|
+
consume() {
|
|
24
|
+
return this.input[this.index++] ?? '';
|
|
25
|
+
}
|
|
26
|
+
skipOWS() {
|
|
27
|
+
while (!this.eof()) {
|
|
28
|
+
const char = this.peek();
|
|
29
|
+
if (char === ' ' || char === '\t') {
|
|
30
|
+
this.consume();
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
isAtEnd() {
|
|
38
|
+
this.skipOWS();
|
|
39
|
+
return this.eof();
|
|
40
|
+
}
|
|
41
|
+
parseItem() {
|
|
42
|
+
const value = this.parseBareItem();
|
|
43
|
+
if (value === null) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
const params = this.parseParameters();
|
|
47
|
+
return params ? { value, params } : { value };
|
|
48
|
+
}
|
|
49
|
+
parseInnerList() {
|
|
50
|
+
if (this.peek() !== '(') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
this.consume();
|
|
54
|
+
const items = [];
|
|
55
|
+
this.skipOWS();
|
|
56
|
+
while (!this.eof() && this.peek() !== ')') {
|
|
57
|
+
const item = this.parseItem();
|
|
58
|
+
if (!item) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
items.push(item);
|
|
62
|
+
this.skipOWS();
|
|
63
|
+
}
|
|
64
|
+
if (this.peek() !== ')') {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
this.consume();
|
|
68
|
+
const params = this.parseParameters();
|
|
69
|
+
return params ? { items, params } : { items };
|
|
70
|
+
}
|
|
71
|
+
parseList() {
|
|
72
|
+
const list = [];
|
|
73
|
+
while (true) {
|
|
74
|
+
this.skipOWS();
|
|
75
|
+
if (this.eof()) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
const member = this.peek() === '(' ? this.parseInnerList() : this.parseItem();
|
|
79
|
+
if (!member) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
list.push(member);
|
|
83
|
+
this.skipOWS();
|
|
84
|
+
if (this.eof()) {
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (this.peek() === ',') {
|
|
88
|
+
this.consume();
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return list;
|
|
94
|
+
}
|
|
95
|
+
parseDictionary() {
|
|
96
|
+
const dict = {};
|
|
97
|
+
while (true) {
|
|
98
|
+
this.skipOWS();
|
|
99
|
+
if (this.eof()) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const key = this.parseKey();
|
|
103
|
+
if (!key) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
this.skipOWS();
|
|
107
|
+
let value;
|
|
108
|
+
if (this.peek() === '=') {
|
|
109
|
+
this.consume();
|
|
110
|
+
this.skipOWS();
|
|
111
|
+
if (this.peek() === '(') {
|
|
112
|
+
const list = this.parseInnerList();
|
|
113
|
+
if (!list) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
value = list;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const item = this.parseItem();
|
|
120
|
+
if (!item) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
value = item;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const params = this.parseParameters();
|
|
128
|
+
value = params ? { value: true, params } : { value: true };
|
|
129
|
+
}
|
|
130
|
+
dict[key] = value;
|
|
131
|
+
this.skipOWS();
|
|
132
|
+
if (this.eof()) {
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
if (this.peek() === ',') {
|
|
136
|
+
this.consume();
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return dict;
|
|
142
|
+
}
|
|
143
|
+
parseParameters() {
|
|
144
|
+
const params = {};
|
|
145
|
+
while (true) {
|
|
146
|
+
this.skipOWS();
|
|
147
|
+
if (this.peek() !== ';') {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
this.consume();
|
|
151
|
+
this.skipOWS();
|
|
152
|
+
const key = this.parseKey();
|
|
153
|
+
if (!key) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
let value = true;
|
|
157
|
+
this.skipOWS();
|
|
158
|
+
if (this.peek() === '=') {
|
|
159
|
+
this.consume();
|
|
160
|
+
this.skipOWS();
|
|
161
|
+
const bare = this.parseBareItem();
|
|
162
|
+
if (bare === null) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
value = bare;
|
|
166
|
+
}
|
|
167
|
+
params[key] = value;
|
|
168
|
+
}
|
|
169
|
+
return Object.keys(params).length > 0 ? params : null;
|
|
170
|
+
}
|
|
171
|
+
parseBareItem() {
|
|
172
|
+
if (this.eof()) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const char = this.peek();
|
|
176
|
+
if (char === '"') {
|
|
177
|
+
return this.parseString();
|
|
178
|
+
}
|
|
179
|
+
if (char === '?') {
|
|
180
|
+
return this.parseBoolean();
|
|
181
|
+
}
|
|
182
|
+
if (char === ':') {
|
|
183
|
+
return this.parseByteSequence();
|
|
184
|
+
}
|
|
185
|
+
if (char === '-' || (char >= '0' && char <= '9')) {
|
|
186
|
+
return this.parseNumber();
|
|
187
|
+
}
|
|
188
|
+
return this.parseToken();
|
|
189
|
+
}
|
|
190
|
+
parseKey() {
|
|
191
|
+
let key = '';
|
|
192
|
+
while (!this.eof()) {
|
|
193
|
+
const char = this.peek();
|
|
194
|
+
if (!/[a-z0-9_\-\.\*]/.test(char)) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
key += char;
|
|
198
|
+
this.consume();
|
|
199
|
+
}
|
|
200
|
+
if (!key || !KEY_RE.test(key)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return key;
|
|
204
|
+
}
|
|
205
|
+
// RFC 8941 §3.3.3: String bare item.
|
|
206
|
+
parseString() {
|
|
207
|
+
if (this.consume() !== '"') {
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
let result = '';
|
|
211
|
+
while (!this.eof()) {
|
|
212
|
+
const char = this.consume();
|
|
213
|
+
if (char === '"') {
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
if (char === '\\') {
|
|
217
|
+
if (this.eof()) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const escapedChar = this.consume();
|
|
221
|
+
// RFC 8941 §3.3.3: Only \" and \\ are valid escape sequences.
|
|
222
|
+
if (escapedChar !== '"' && escapedChar !== '\\') {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
result += escapedChar;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
// RFC 8941 §3.3.3: Validate unescaped chars are printable ASCII (excluding " and \).
|
|
229
|
+
const code = char.charCodeAt(0);
|
|
230
|
+
if (code < 0x20 || code > 0x7E || code === 0x22 || code === 0x5C) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
result += char;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
// RFC 8941 §3.3.6: Boolean bare item.
|
|
239
|
+
parseBoolean() {
|
|
240
|
+
if (this.consume() !== '?') {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const char = this.consume();
|
|
244
|
+
if (char === '1') {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
if (char === '0') {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
// RFC 8941 §3.3.5: Byte sequence bare item.
|
|
253
|
+
parseByteSequence() {
|
|
254
|
+
if (this.consume() !== ':') {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
let base64 = '';
|
|
258
|
+
while (!this.eof()) {
|
|
259
|
+
const char = this.consume();
|
|
260
|
+
if (char === ':') {
|
|
261
|
+
try {
|
|
262
|
+
if (base64.includes('\n') || base64.includes('\r')) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (!BASE64_RE.test(base64)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (base64.length % 4 !== 0) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
272
|
+
return new Uint8Array(buffer);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
base64 += char;
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
// RFC 8941 §3.3.1: Numeric bare item.
|
|
283
|
+
parseNumber() {
|
|
284
|
+
const remaining = this.input.slice(this.index);
|
|
285
|
+
const match = remaining.match(/^(-?)(\d+)(?:\.(\d{1,3}))?/);
|
|
286
|
+
if (!match) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const integerPart = match[2] ?? '';
|
|
290
|
+
const fractionalPart = match[3];
|
|
291
|
+
if (fractionalPart !== undefined) {
|
|
292
|
+
if (integerPart.length > 12) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
if (fractionalPart.length > 3) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else if (integerPart.length > 15) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
this.index += match[0].length;
|
|
303
|
+
const value = Number(match[0]);
|
|
304
|
+
if (!Number.isFinite(value)) {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
return value;
|
|
308
|
+
}
|
|
309
|
+
// RFC 8941 §3.3.4: Token parsing.
|
|
310
|
+
parseToken() {
|
|
311
|
+
let token = '';
|
|
312
|
+
while (!this.eof()) {
|
|
313
|
+
const char = this.peek();
|
|
314
|
+
// RFC 8941 §3.3.4: tchar plus ":" and "/"
|
|
315
|
+
if (!/[A-Za-z0-9!#$%&'*+\-.^_`|~:\/]/.test(char)) {
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
token += char;
|
|
319
|
+
this.consume();
|
|
320
|
+
}
|
|
321
|
+
if (!token || !TOKEN_RE.test(token)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
return token;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Parse a Structured Field List.
|
|
329
|
+
*/
|
|
330
|
+
// RFC 8941 §3.1: Structured Field List.
|
|
331
|
+
export function parseSfList(value) {
|
|
332
|
+
const parser = new Parser(value);
|
|
333
|
+
const list = parser.parseList();
|
|
334
|
+
if (!list) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (!parser.isAtEnd()) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return list;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Parse a Structured Field Dictionary.
|
|
344
|
+
*/
|
|
345
|
+
// RFC 8941 §3.2: Structured Field Dictionary.
|
|
346
|
+
export function parseSfDict(value) {
|
|
347
|
+
const parser = new Parser(value);
|
|
348
|
+
const dict = parser.parseDictionary();
|
|
349
|
+
if (!dict) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (!parser.isAtEnd()) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
return dict;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Parse a Structured Field Item.
|
|
359
|
+
*/
|
|
360
|
+
// RFC 8941 §3.3: Structured Field Item.
|
|
361
|
+
export function parseSfItem(value) {
|
|
362
|
+
const parser = new Parser(value.trim());
|
|
363
|
+
const item = parser.parseItem();
|
|
364
|
+
if (!item) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
if (!parser.isAtEnd()) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
return item;
|
|
371
|
+
}
|
|
372
|
+
// RFC 8941 §4: Structured Field serialization.
|
|
373
|
+
function serializeBareItem(value) {
|
|
374
|
+
if (typeof value === 'boolean') {
|
|
375
|
+
return value ? '?1' : '?0';
|
|
376
|
+
}
|
|
377
|
+
if (typeof value === 'number') {
|
|
378
|
+
if (!Number.isFinite(value)) {
|
|
379
|
+
throw new Error('Invalid numeric structured field value');
|
|
380
|
+
}
|
|
381
|
+
if (Number.isInteger(value)) {
|
|
382
|
+
// RFC 8941 §4.1.4: Integer range check.
|
|
383
|
+
if (value < -999_999_999_999_999 || value > 999_999_999_999_999) {
|
|
384
|
+
throw new Error('Integer out of range for structured field');
|
|
385
|
+
}
|
|
386
|
+
return String(value);
|
|
387
|
+
}
|
|
388
|
+
let encoded = value.toFixed(3);
|
|
389
|
+
encoded = encoded.replace(/0+$/, '');
|
|
390
|
+
if (encoded.endsWith('.')) {
|
|
391
|
+
encoded = encoded.slice(0, -1);
|
|
392
|
+
}
|
|
393
|
+
return encoded;
|
|
394
|
+
}
|
|
395
|
+
if (value instanceof Uint8Array) {
|
|
396
|
+
const base64 = Buffer.from(value).toString('base64');
|
|
397
|
+
return `:${base64}:`;
|
|
398
|
+
}
|
|
399
|
+
if (TOKEN_RE.test(value)) {
|
|
400
|
+
return value;
|
|
401
|
+
}
|
|
402
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
403
|
+
return `"${escaped}"`;
|
|
404
|
+
}
|
|
405
|
+
function serializeParams(params) {
|
|
406
|
+
if (!params) {
|
|
407
|
+
return '';
|
|
408
|
+
}
|
|
409
|
+
const parts = [];
|
|
410
|
+
for (const [key, value] of Object.entries(params)) {
|
|
411
|
+
if (value === true) {
|
|
412
|
+
parts.push(`;${key}`);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
parts.push(`;${key}=${serializeBareItem(value)}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return parts.join('');
|
|
419
|
+
}
|
|
420
|
+
function serializeItem(item) {
|
|
421
|
+
const base = serializeBareItem(item.value);
|
|
422
|
+
return base + serializeParams(item.params);
|
|
423
|
+
}
|
|
424
|
+
function serializeInnerList(list) {
|
|
425
|
+
const items = list.items.map(serializeItem).join(' ');
|
|
426
|
+
return `(${items})${serializeParams(list.params)}`;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Serialize a Structured Field List.
|
|
430
|
+
*/
|
|
431
|
+
// RFC 8941 §4: Structured Field List serialization.
|
|
432
|
+
export function serializeSfList(list) {
|
|
433
|
+
return list.map(member => {
|
|
434
|
+
if ('items' in member) {
|
|
435
|
+
return serializeInnerList(member);
|
|
436
|
+
}
|
|
437
|
+
return serializeItem(member);
|
|
438
|
+
}).join(', ');
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Serialize a Structured Field Dictionary.
|
|
442
|
+
*/
|
|
443
|
+
// RFC 8941 §4: Structured Field Dictionary serialization.
|
|
444
|
+
export function serializeSfDict(dict) {
|
|
445
|
+
const parts = [];
|
|
446
|
+
for (const [key, value] of Object.entries(dict)) {
|
|
447
|
+
if ('items' in value) {
|
|
448
|
+
parts.push(`${key}=${serializeInnerList(value)}`);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
const item = value;
|
|
452
|
+
const isBareTrue = item.value === true && !item.params;
|
|
453
|
+
if (isBareTrue) {
|
|
454
|
+
parts.push(key);
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
parts.push(`${key}=${serializeItem(item)}`);
|
|
458
|
+
}
|
|
459
|
+
return parts.join(', ');
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Serialize a Structured Field Item.
|
|
463
|
+
*/
|
|
464
|
+
// RFC 8941 §4: Structured Field Item serialization.
|
|
465
|
+
export function serializeSfItem(item) {
|
|
466
|
+
return serializeItem(item);
|
|
467
|
+
}
|
|
468
|
+
//# sourceMappingURL=structured-fields.js.map
|