@johntalton/http-util 4.1.1 → 5.0.1
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/README.md +232 -21
- package/package.json +1 -1
- package/src/accept-encoding.js +1 -1
- package/src/accept-language.js +1 -1
- package/src/accept-util.js +1 -1
- package/src/accept.js +2 -5
- package/src/body.js +13 -15
- package/src/cache-control.js +6 -1
- package/src/clear-site-data.js +59 -0
- package/src/conditional.js +37 -42
- package/src/content-disposition.js +12 -7
- package/src/content-range.js +59 -0
- package/src/content-type.js +17 -14
- package/src/forwarded.js +1 -1
- package/src/index.js +6 -1
- package/src/multipart.js +81 -33
- package/src/preference.js +205 -0
- package/src/range.js +154 -0
- package/src/response/accepted.js +1 -0
- package/src/response/bytes.js +27 -0
- package/src/response/conflict.js +1 -0
- package/src/response/content-too-large.js +16 -0
- package/src/response/created.js +2 -1
- package/src/response/defs.js +15 -1
- package/src/response/error.js +1 -0
- package/src/response/forbidden.js +17 -0
- package/src/response/gone.js +16 -0
- package/src/response/header-util.js +2 -1
- package/src/response/im-a-teapot.js +16 -0
- package/src/response/index.js +17 -0
- package/src/response/insufficient-storage.js +16 -0
- package/src/response/json.js +6 -53
- package/src/response/moved-permanently.js +23 -0
- package/src/response/multiple-choices.js +21 -0
- package/src/response/no-content.js +2 -1
- package/src/response/not-acceptable.js +2 -1
- package/src/response/not-allowed.js +1 -0
- package/src/response/not-found.js +1 -0
- package/src/response/not-implemented.js +1 -0
- package/src/response/not-modified.js +3 -2
- package/src/response/partial-content.js +71 -0
- package/src/response/permanent-redirect.js +23 -0
- package/src/response/precondition-failed.js +1 -0
- package/src/response/preflight.js +21 -5
- package/src/response/range-not-satisfiable.js +28 -0
- package/src/response/response.js +26 -0
- package/src/response/see-other.js +23 -0
- package/src/response/send-util.js +137 -6
- package/src/response/sse.js +4 -3
- package/src/response/temporary-redirect.js +23 -0
- package/src/response/timeout.js +1 -0
- package/src/response/too-many-requests.js +1 -0
- package/src/response/trace.js +8 -4
- package/src/response/unauthorized.js +1 -0
- package/src/response/unavailable.js +1 -0
- package/src/response/unprocessable.js +1 -0
- package/src/response/unsupported-media.js +15 -4
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { RANGE_UNITS_BYTES } from "./response/defs.js"
|
|
2
|
+
|
|
3
|
+
export const CONTENT_RANGE_UNKNOWN = '*'
|
|
4
|
+
export const CONTENT_RANGE_SEPARATOR = '-'
|
|
5
|
+
export const CONTENT_RANGE_SIZE_SEPARATOR = '/'
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_CONTENT_RANGE_DIRECTIVE = {
|
|
8
|
+
units: RANGE_UNITS_BYTES,
|
|
9
|
+
range: CONTENT_RANGE_UNKNOWN,
|
|
10
|
+
size: CONTENT_RANGE_UNKNOWN
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* @typedef {Object} ContentRangeDirective
|
|
14
|
+
* @property {RANGE_UNITS_BYTES|undefined} [units]
|
|
15
|
+
* @property {{ start: number, end: number }|CONTENT_RANGE_UNKNOWN|undefined} [range]
|
|
16
|
+
* @property {number|CONTENT_RANGE_UNKNOWN|undefined} [size]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export class ContentRange {
|
|
20
|
+
/**
|
|
21
|
+
* @param {ContentRangeDirective|undefined} rangeDirective
|
|
22
|
+
* @returns {string|undefined}
|
|
23
|
+
*/
|
|
24
|
+
static encode(rangeDirective) {
|
|
25
|
+
if(rangeDirective === undefined) { return undefined }
|
|
26
|
+
|
|
27
|
+
const units = rangeDirective.units ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.units
|
|
28
|
+
const size = rangeDirective.size ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.size
|
|
29
|
+
const range = rangeDirective.range ?? DEFAULT_CONTENT_RANGE_DIRECTIVE.range
|
|
30
|
+
|
|
31
|
+
if(units !== RANGE_UNITS_BYTES) { return undefined }
|
|
32
|
+
if(size !== CONTENT_RANGE_UNKNOWN && !Number.isInteger(size)) { return undefined }
|
|
33
|
+
|
|
34
|
+
if((typeof range === 'string')) {
|
|
35
|
+
if(range !== CONTENT_RANGE_UNKNOWN) { return undefined }
|
|
36
|
+
return `${units} ${CONTENT_RANGE_UNKNOWN}${CONTENT_RANGE_SIZE_SEPARATOR}${size}`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rangeStr = `${range.start}${CONTENT_RANGE_SEPARATOR}${range.end}`
|
|
40
|
+
return `${units} ${rangeStr}${CONTENT_RANGE_SIZE_SEPARATOR}${size}`
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// console.log(ContentRange.encode({}))
|
|
45
|
+
// console.log(ContentRange.encode({ size: '*' }))
|
|
46
|
+
// console.log(ContentRange.encode({ units: 'bytes' }))
|
|
47
|
+
// console.log(ContentRange.encode({ range: '*' }))
|
|
48
|
+
// console.log(ContentRange.encode({ range: '*', size: '*' }))
|
|
49
|
+
|
|
50
|
+
// console.log()
|
|
51
|
+
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 } }))
|
|
52
|
+
// console.log(ContentRange.encode({ range: { start: 0, end: 1024 }, size: 1024 }))
|
|
53
|
+
// console.log(ContentRange.encode({ range: '*', size: 1024 }))
|
|
54
|
+
// console.log(ContentRange.encode({ size: 1024 }))
|
|
55
|
+
|
|
56
|
+
// console.log()
|
|
57
|
+
// console.log(ContentRange.encode({ units: 'bob' }))
|
|
58
|
+
// console.log(ContentRange.encode({ range: 'bob' }))
|
|
59
|
+
// console.log(ContentRange.encode({ size: 'bob' }))
|
package/src/content-type.js
CHANGED
|
@@ -5,9 +5,11 @@ export const MIME_TYPE_EVENT_STREAM = 'text/event-stream'
|
|
|
5
5
|
export const MIME_TYPE_XML = 'application/xml'
|
|
6
6
|
export const MIME_TYPE_URL_FORM_DATA = 'application/x-www-form-urlencoded'
|
|
7
7
|
export const MIME_TYPE_MULTIPART_FORM_DATA = 'multipart/form-data'
|
|
8
|
+
export const MIME_TYPE_MULTIPART_RANGE = 'multipart/byteranges'
|
|
8
9
|
export const MIME_TYPE_OCTET_STREAM = 'application/octet-stream'
|
|
9
10
|
export const MIME_TYPE_MESSAGE_HTTP = 'message/http'
|
|
10
11
|
|
|
12
|
+
|
|
11
13
|
export const KNOWN_CONTENT_TYPES = [
|
|
12
14
|
'application', 'audio', 'image', 'message',
|
|
13
15
|
'multipart','text', 'video'
|
|
@@ -27,10 +29,12 @@ export const SPECIAL_CHARS = [
|
|
|
27
29
|
'\n', '\r', '\t'
|
|
28
30
|
]
|
|
29
31
|
|
|
32
|
+
export const WHITESPACE_REGEX = /\s/
|
|
33
|
+
|
|
30
34
|
/**
|
|
31
35
|
* @param {string} c
|
|
32
36
|
*/
|
|
33
|
-
export function isWhitespace(c){ return
|
|
37
|
+
export function isWhitespace(c){ return WHITESPACE_REGEX.test(c) }
|
|
34
38
|
|
|
35
39
|
/**
|
|
36
40
|
* @param {string|undefined} value
|
|
@@ -114,22 +118,21 @@ export function parseContentType(contentTypeHeader) {
|
|
|
114
118
|
|
|
115
119
|
const parameters = new Map()
|
|
116
120
|
|
|
117
|
-
parameterSet
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if(hasSpecialChar(key)) { return }
|
|
121
|
+
for(const parameter of parameterSet) {
|
|
122
|
+
const [ key, value ] = parameter.split(CONTENT_TYPE_SEPARATOR.KVP)
|
|
123
|
+
if(key === undefined || key === '') { return }
|
|
124
|
+
if(value === undefined || value === '') { return }
|
|
125
|
+
if(hasSpecialChar(key)) { return }
|
|
123
126
|
|
|
124
|
-
|
|
127
|
+
const actualKey = key?.trim().toLowerCase()
|
|
125
128
|
|
|
126
|
-
|
|
127
|
-
|
|
129
|
+
const quoted = (value.at(0) === '"' && value.at(-1) === '"')
|
|
130
|
+
const actualValue = quoted ? value.substring(1, value.length - 1) : value
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
132
|
+
if(!parameters.has(actualKey)) {
|
|
133
|
+
parameters.set(actualKey, actualValue)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
133
136
|
|
|
134
137
|
const charset = parameters.get(CHARSET)
|
|
135
138
|
|
package/src/forwarded.js
CHANGED
package/src/index.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
/** biome-ignore-all lint/performance/noBarrelFile: entry point */
|
|
2
|
+
/** biome-ignore-all lint/performance/noReExportAll: entry point */
|
|
3
|
+
export * from './accept.js'
|
|
1
4
|
export * from './accept-encoding.js'
|
|
2
5
|
export * from './accept-language.js'
|
|
3
6
|
export * from './accept-util.js'
|
|
4
|
-
export * from './
|
|
7
|
+
export * from './cache-control.js'
|
|
5
8
|
export * from './conditional.js'
|
|
6
9
|
export * from './content-disposition.js'
|
|
10
|
+
export * from './content-range.js'
|
|
7
11
|
export * from './content-type.js'
|
|
8
12
|
export * from './forwarded.js'
|
|
9
13
|
export * from './multipart.js'
|
|
14
|
+
export * from './range.js'
|
|
10
15
|
export * from './rate-limit.js'
|
|
11
16
|
export * from './server-timing.js'
|
package/src/multipart.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { ReadableStream } from 'node:stream/web'
|
|
2
|
+
|
|
1
3
|
import { parseContentDisposition } from './content-disposition.js'
|
|
4
|
+
import { ContentRange } from './content-range.js'
|
|
2
5
|
import { parseContentType } from './content-type.js'
|
|
3
6
|
|
|
7
|
+
/** @import { ContentRangeDirective } from './content-range.js' */
|
|
8
|
+
/** @import { SendBody } from './response/send-util.js' */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} MultipartBytePart
|
|
12
|
+
* @property {SendBody} obj
|
|
13
|
+
* @property {ContentRangeDirective} range
|
|
14
|
+
*/
|
|
15
|
+
|
|
4
16
|
export const DISPOSITION_FORM_DATA = 'form-data'
|
|
5
17
|
|
|
6
18
|
export const BOUNDARY_MARK = '--'
|
|
@@ -12,7 +24,8 @@ export const EMPTY = ''
|
|
|
12
24
|
|
|
13
25
|
export const MULTIPART_HEADER = {
|
|
14
26
|
CONTENT_DISPOSITION: 'content-disposition',
|
|
15
|
-
CONTENT_TYPE: 'content-type'
|
|
27
|
+
CONTENT_TYPE: 'content-type',
|
|
28
|
+
CONTENT_RANGE: 'content-range'
|
|
16
29
|
}
|
|
17
30
|
|
|
18
31
|
export const MULTIPART_STATE = {
|
|
@@ -29,6 +42,15 @@ export class Multipart {
|
|
|
29
42
|
* @param {string} [charset='utf8']
|
|
30
43
|
*/
|
|
31
44
|
static parse(text, boundary, charset = 'utf8') {
|
|
45
|
+
return Multipart.parse_FormData(text, boundary, charset)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} text
|
|
50
|
+
* @param {string} boundary
|
|
51
|
+
* @param {string} [_charset='utf8']
|
|
52
|
+
*/
|
|
53
|
+
static parse_FormData(text, boundary, _charset = 'utf8') {
|
|
32
54
|
// console.log({ boundary, text })
|
|
33
55
|
const formData = new FormData()
|
|
34
56
|
|
|
@@ -69,10 +91,10 @@ export class Multipart {
|
|
|
69
91
|
if(line === EMPTY) { state = MULTIPART_STATE.VALUE }
|
|
70
92
|
else {
|
|
71
93
|
const [ rawName, value ] = line.split(HEADER_SEPARATOR)
|
|
72
|
-
const name = rawName
|
|
94
|
+
const name = rawName?.toLowerCase()
|
|
73
95
|
// console.log('header', name, value)
|
|
74
96
|
if(name === MULTIPART_HEADER.CONTENT_TYPE) {
|
|
75
|
-
const
|
|
97
|
+
const _contentType = parseContentType(value)
|
|
76
98
|
// console.log({ contentType })
|
|
77
99
|
}
|
|
78
100
|
else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
|
|
@@ -114,36 +136,62 @@ export class Multipart {
|
|
|
114
136
|
|
|
115
137
|
return formData
|
|
116
138
|
}
|
|
117
|
-
}
|
|
118
139
|
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} contentType
|
|
142
|
+
* @param {Array<MultipartBytePart>} parts
|
|
143
|
+
* @param {number|undefined} contentLength
|
|
144
|
+
* @param {string} boundary
|
|
145
|
+
* @returns {ReadableStream<Uint8Array>}
|
|
146
|
+
*/
|
|
147
|
+
static encode_Bytes(contentType, parts, contentLength, boundary) {
|
|
148
|
+
const boundaryBegin = `${BOUNDARY_MARK}${boundary}`
|
|
149
|
+
const boundaryEnd = `${BOUNDARY_MARK}${boundary}${BOUNDARY_MARK}`
|
|
119
150
|
|
|
151
|
+
return new ReadableStream({
|
|
152
|
+
type: 'bytes',
|
|
153
|
+
async start(controller) {
|
|
154
|
+
const encoder = new TextEncoder()
|
|
155
|
+
|
|
156
|
+
for (const part of parts) {
|
|
157
|
+
controller.enqueue(encoder.encode(`${boundaryBegin}${MULTIPART_SEPARATOR}`))
|
|
158
|
+
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
|
|
159
|
+
controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
|
|
160
|
+
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
161
|
+
// controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
162
|
+
|
|
163
|
+
if(part.obj instanceof ReadableStream) {
|
|
164
|
+
for await (const chunk of part.obj) {
|
|
165
|
+
if(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
|
|
166
|
+
controller.enqueue(chunk)
|
|
167
|
+
}
|
|
168
|
+
else if(typeof chunk === 'string'){
|
|
169
|
+
controller.enqueue(encoder.encode(chunk))
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// console.log('chunk type', typeof chunk)
|
|
173
|
+
controller.enqueue(Uint8Array.from([ chunk ]))
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if(part.obj instanceof ArrayBuffer || ArrayBuffer.isView(part.obj)) {
|
|
178
|
+
controller.enqueue(part.obj)
|
|
179
|
+
}
|
|
180
|
+
else if(typeof part.obj === 'string'){
|
|
181
|
+
controller.enqueue(encoder.encode(part.obj))
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
// console.log('error', typeof part.obj, part.obj)
|
|
185
|
+
throw new Error('unknown part type')
|
|
186
|
+
}
|
|
120
187
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// console.log(result)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// const test = [
|
|
135
|
-
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
|
|
136
|
-
// 'Content-Disposition: form-data; '
|
|
137
|
-
// + 'name="upload_file_0"; filename="テスト.dat"',
|
|
138
|
-
// 'Content-Type: application/octet-stream',
|
|
139
|
-
// '',
|
|
140
|
-
// 'A'.repeat(1023),
|
|
141
|
-
// '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
|
|
142
|
-
// ].join('\r\n')
|
|
143
|
-
// const result = Multipart.parse(test, '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k')
|
|
144
|
-
// console.log(result)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// const test = '--X-INSOMNIA-BOUNDARY--\r\n'
|
|
148
|
-
// const result = Multipart.parse(test, 'X-INSOMNIA-BOUNDARY')
|
|
149
|
-
// console.log(result)
|
|
188
|
+
controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
controller.enqueue(encoder.encode(boundaryEnd))
|
|
192
|
+
|
|
193
|
+
controller.close()
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// https://datatracker.ietf.org/doc/html/rfc7240
|
|
2
|
+
// https://www.rfc-editor.org/rfc/rfc7240#section-3
|
|
3
|
+
|
|
4
|
+
export const SEPARATOR = {
|
|
5
|
+
PREFERENCE: ',',
|
|
6
|
+
PARAMS: ';',
|
|
7
|
+
PARAM_KVP: '=',
|
|
8
|
+
KVP: '='
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const QUOTE = '"'
|
|
12
|
+
|
|
13
|
+
export const DIRECTIVE_RESPOND_ASYNC = 'respond-async'
|
|
14
|
+
export const DIRECTIVE_WAIT = 'wait'
|
|
15
|
+
export const DIRECTIVE_HANDLING = 'handling'
|
|
16
|
+
export const DIRECTIVE_REPRESENTATION = 'return'
|
|
17
|
+
export const DIRECTIVE_TIMEZONE = 'timezone'
|
|
18
|
+
|
|
19
|
+
export const DIRECTIVE_HANDLING_STRICT = 'strict'
|
|
20
|
+
export const DIRECTIVE_HANDLING_LENIENT = 'lenient'
|
|
21
|
+
|
|
22
|
+
export const DIRECTIVE_REPRESENTATION_MINIMAL = 'minimal'
|
|
23
|
+
export const DIRECTIVE_REPRESENTATION_HEADERS_ONLY = 'headers-only'
|
|
24
|
+
export const DIRECTIVE_REPRESENTATION_FULL = 'representation'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string|undefined} value
|
|
29
|
+
*/
|
|
30
|
+
export function stripQuotes(value) {
|
|
31
|
+
if(value === undefined) { return undefined }
|
|
32
|
+
return value.substring(1, value.length - 1)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string|undefined} value
|
|
37
|
+
*/
|
|
38
|
+
export function isQuoted(value) {
|
|
39
|
+
if(value === undefined) { return false }
|
|
40
|
+
if(value.length < 2) { return false }
|
|
41
|
+
if(!value.startsWith(QUOTE)) { return false }
|
|
42
|
+
if(!value.endsWith(QUOTE)) { return false }
|
|
43
|
+
return true
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} Preference
|
|
48
|
+
* @property {string|undefined} value
|
|
49
|
+
* @property {Map<string, string|undefined>} parameters
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @template P
|
|
54
|
+
* @typedef {Object} RequestPreferencesBase
|
|
55
|
+
* @property {boolean|undefined} [asynchronous]
|
|
56
|
+
* @property {string|undefined} [representation]
|
|
57
|
+
* @property {string|undefined} [handling]
|
|
58
|
+
* @property {number|undefined} [wait]
|
|
59
|
+
* @property {string|undefined} [timezone]
|
|
60
|
+
* @property {Map<string, P>} preferences
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/** @typedef {RequestPreferencesBase<Preference>} RequestPreferences */
|
|
64
|
+
/** @typedef {RequestPreferencesBase<Partial<Omit<Preference,'parameters'>>|undefined>} AppliedRequestPreferences */
|
|
65
|
+
|
|
66
|
+
export class Preferences {
|
|
67
|
+
/**
|
|
68
|
+
* @param {string|undefined} header
|
|
69
|
+
* @returns {RequestPreferences|undefined}
|
|
70
|
+
*/
|
|
71
|
+
static parse(header) {
|
|
72
|
+
if(header === undefined) { return undefined }
|
|
73
|
+
|
|
74
|
+
const preferences = new Map(header.split(SEPARATOR.PREFERENCE)
|
|
75
|
+
.map(pref => {
|
|
76
|
+
const [ kvp, ...params ] = pref.trim().split(SEPARATOR.PARAMS)
|
|
77
|
+
const [ key, rawValue ] = kvp?.split(SEPARATOR.KVP) ?? []
|
|
78
|
+
|
|
79
|
+
if(key === undefined) { return {} }
|
|
80
|
+
const valueOrEmpty = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
|
|
81
|
+
const value = (valueOrEmpty !== '') ? valueOrEmpty : undefined
|
|
82
|
+
|
|
83
|
+
const parameters = new Map(params
|
|
84
|
+
.map(param => {
|
|
85
|
+
const [ pKey, rawPValue ] = param.split(SEPARATOR.PARAM_KVP)
|
|
86
|
+
if(pKey === undefined) { return {} }
|
|
87
|
+
const trimmedRawPValue = rawPValue?.trim()
|
|
88
|
+
const pValueOrEmpty = isQuoted(trimmedRawPValue) ? stripQuotes(trimmedRawPValue) : trimmedRawPValue
|
|
89
|
+
const pValue = (pValueOrEmpty !== '') ? pValueOrEmpty : undefined
|
|
90
|
+
return { key: pKey.trim(), value: pValue }
|
|
91
|
+
})
|
|
92
|
+
.filter(item => item.key !== undefined)
|
|
93
|
+
.map(item => ([ item.key, item.value ]))
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return { key, value, parameters }
|
|
97
|
+
})
|
|
98
|
+
.filter(item => item.key !== undefined)
|
|
99
|
+
.map(item => ([
|
|
100
|
+
item.key,
|
|
101
|
+
{ value: item.value, parameters: item.parameters }
|
|
102
|
+
]))
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
//
|
|
106
|
+
const asynchronous = preferences.get(DIRECTIVE_RESPOND_ASYNC) !== undefined
|
|
107
|
+
const representation = preferences.get(DIRECTIVE_REPRESENTATION)?.value
|
|
108
|
+
const handling = preferences.get(DIRECTIVE_HANDLING)?.value
|
|
109
|
+
const wait = Number.parseInt(preferences.get(DIRECTIVE_WAIT)?.value ?? '')
|
|
110
|
+
const timezone = preferences.get(DIRECTIVE_TIMEZONE)?.value
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
asynchronous,
|
|
114
|
+
representation,
|
|
115
|
+
handling,
|
|
116
|
+
wait: Number.isFinite(wait) ? wait : undefined,
|
|
117
|
+
timezone,
|
|
118
|
+
preferences
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class AppliedPreferences {
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {Map<string, string|undefined>} preferences
|
|
127
|
+
*/
|
|
128
|
+
static #encode_Map(preferences) {
|
|
129
|
+
return [ ...preferences.entries()
|
|
130
|
+
.map(([ key, value ]) => {
|
|
131
|
+
// todo check if value should be quoted
|
|
132
|
+
if(value !== undefined) { return `${key}${SEPARATOR.KVP}${value}` }
|
|
133
|
+
return key
|
|
134
|
+
}) ]
|
|
135
|
+
.join(SEPARATOR.PREFERENCE)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {Partial<AppliedRequestPreferences>|Map<string, string|undefined>|undefined} preferences
|
|
140
|
+
*/
|
|
141
|
+
static encode(preferences) {
|
|
142
|
+
if(preferences === undefined) { return undefined}
|
|
143
|
+
if(preferences instanceof Map) {
|
|
144
|
+
return AppliedPreferences.#encode_Map(preferences)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const applied = new Map()
|
|
148
|
+
|
|
149
|
+
for(const [ key, pref ] of preferences.preferences?.entries() ?? []) {
|
|
150
|
+
applied.set(key, pref?.value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if(preferences.asynchronous === true) { applied.set(DIRECTIVE_RESPOND_ASYNC, undefined) }
|
|
154
|
+
if(preferences.asynchronous === false) { applied.delete(DIRECTIVE_RESPOND_ASYNC) }
|
|
155
|
+
if(preferences.representation !== undefined) { applied.set(DIRECTIVE_REPRESENTATION, preferences.representation) }
|
|
156
|
+
if(preferences.handling !== undefined) { applied.set(DIRECTIVE_HANDLING, preferences.handling) }
|
|
157
|
+
if(preferences.wait !== undefined) { applied.set(DIRECTIVE_WAIT, preferences.wait) }
|
|
158
|
+
if(preferences.timezone !== undefined) { applied.set(DIRECTIVE_TIMEZONE, preferences.timezone) }
|
|
159
|
+
|
|
160
|
+
return AppliedPreferences.#encode_Map(applied)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
// console.log(AppliedPreferences.encode(undefined))
|
|
166
|
+
// console.log(AppliedPreferences.encode({ }))
|
|
167
|
+
// console.log(AppliedPreferences.encode({ wait: 10 }))
|
|
168
|
+
// console.log(AppliedPreferences.encode({ asynchronous: undefined }))
|
|
169
|
+
// console.log(AppliedPreferences.encode({ asynchronous: false }))
|
|
170
|
+
// console.log(AppliedPreferences.encode({ asynchronous: true }))
|
|
171
|
+
// console.log(AppliedPreferences.encode({ preferences: new Map([
|
|
172
|
+
// [ 'respond-async', { value: undefined } ]
|
|
173
|
+
// ]) }))
|
|
174
|
+
// console.log(AppliedPreferences.encode({
|
|
175
|
+
// asynchronous: false,
|
|
176
|
+
// preferences: new Map([
|
|
177
|
+
// [ 'respond-async', { value: 'fake' } ]
|
|
178
|
+
// ]) }))
|
|
179
|
+
// console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
|
|
180
|
+
// console.log(AppliedPreferences.encode({
|
|
181
|
+
// representation: DIRECTIVE_REPRESENTATION_MINIMAL,
|
|
182
|
+
// preferences: new Map([
|
|
183
|
+
// ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
|
|
184
|
+
// [ 'fake', undefined ]
|
|
185
|
+
// ])
|
|
186
|
+
// }))
|
|
187
|
+
|
|
188
|
+
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
189
|
+
// console.log(Preferences.parse(' foo; bar')?.preferences)
|
|
190
|
+
// console.log(Preferences.parse(' foo; bar=""')?.preferences)
|
|
191
|
+
// console.log(Preferences.parse(' foo=""; bar')?.preferences)
|
|
192
|
+
// console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
|
|
193
|
+
// console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
|
|
194
|
+
|
|
195
|
+
// console.log(Preferences.parse('timezone=America/Los_Angeles'))
|
|
196
|
+
// console.log(Preferences.parse('return=headers-only'))
|
|
197
|
+
// console.log(Preferences.parse('return=minimal'))
|
|
198
|
+
// console.log(Preferences.parse('return=representation'))
|
|
199
|
+
// console.log(Preferences.parse('respond-async, wait=10`'))
|
|
200
|
+
// console.log(Preferences.parse('priority=5'))
|
|
201
|
+
// console.log(Preferences.parse('foo; bar'))
|
|
202
|
+
// console.log(Preferences.parse('foo; bar=""'))
|
|
203
|
+
// console.log(Preferences.parse('foo=""; bar'))
|
|
204
|
+
// console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
|
|
205
|
+
// console.log(Preferences.parse('return=minimal; foo="some parameter"'))
|
package/src/range.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { RANGE_UNITS_BYTES } from "./response/defs.js"
|
|
2
|
+
|
|
3
|
+
export const RANGE_EQUAL = '='
|
|
4
|
+
export const RANGE_SEPARATOR = '-'
|
|
5
|
+
export const RANGE_LIST_SEPARATOR = ','
|
|
6
|
+
|
|
7
|
+
/** @type {''} */
|
|
8
|
+
export const RANGE_EMPTY = ''
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} RangeValueFixed
|
|
12
|
+
* @property {number} start
|
|
13
|
+
* @property {number} end
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} RangeValueOpenEnded
|
|
18
|
+
* @property {number} start
|
|
19
|
+
* @property {''} end
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} RangeValueFromEnd
|
|
24
|
+
* @property {''} start
|
|
25
|
+
* @property {number} end
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** @typedef {RangeValueFixed | RangeValueOpenEnded | RangeValueFromEnd} RangeValue */
|
|
29
|
+
|
|
30
|
+
/** @typedef {RangeValueFixed} NormalizedRangeValue */
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @template RV
|
|
34
|
+
* @typedef {Object} RangeDirective
|
|
35
|
+
* @property {'bytes'|'none'|undefined} units
|
|
36
|
+
* @property {Array<RV>} ranges
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} RangeDirectiveInfo
|
|
41
|
+
* @property {boolean} exceeds
|
|
42
|
+
* @property {boolean} overlap
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
export class Range {
|
|
47
|
+
/**
|
|
48
|
+
* @param {string|undefined} rangeHeader
|
|
49
|
+
* @returns {RangeDirective<RangeValue>|undefined}
|
|
50
|
+
*/
|
|
51
|
+
static parse(rangeHeader) {
|
|
52
|
+
if(rangeHeader === undefined) { return undefined }
|
|
53
|
+
if(!rangeHeader.startsWith(RANGE_UNITS_BYTES)) { return undefined }
|
|
54
|
+
if(!(rangeHeader.substring(RANGE_UNITS_BYTES.length, RANGE_UNITS_BYTES.length + 1) === RANGE_EQUAL)) { return undefined }
|
|
55
|
+
const rangeStr = rangeHeader.substring(RANGE_UNITS_BYTES.length + RANGE_EQUAL.length).trim()
|
|
56
|
+
if(rangeStr === '') { return undefined }
|
|
57
|
+
|
|
58
|
+
const ranges = rangeStr.split(RANGE_LIST_SEPARATOR)
|
|
59
|
+
.map(range => range.trim())
|
|
60
|
+
.map(range => {
|
|
61
|
+
const [ startStr, endStr ] = range.split(RANGE_SEPARATOR)
|
|
62
|
+
if(startStr === undefined) { return undefined }
|
|
63
|
+
if(endStr === undefined) { return undefined }
|
|
64
|
+
if(startStr === RANGE_EMPTY && endStr === RANGE_EMPTY) { return undefined }
|
|
65
|
+
|
|
66
|
+
const start = Number.parseInt(startStr, 10)
|
|
67
|
+
const end = Number.parseInt(endStr, 10)
|
|
68
|
+
|
|
69
|
+
if(startStr === RANGE_EMPTY) {
|
|
70
|
+
if(!Number.isInteger(end)) { return undefined }
|
|
71
|
+
if(end === 0) { return undefined }
|
|
72
|
+
return { start: RANGE_EMPTY, end }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if(endStr === RANGE_EMPTY) {
|
|
76
|
+
if(!Number.isInteger(start)) { return undefined }
|
|
77
|
+
return { start, end: RANGE_EMPTY }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if(!(Number.isInteger(start) && Number.isInteger(end))) { return undefined }
|
|
81
|
+
return { start, end }
|
|
82
|
+
})
|
|
83
|
+
.filter(range => range !== undefined)
|
|
84
|
+
|
|
85
|
+
if(ranges.length === 0) { return undefined }
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
units: RANGE_UNITS_BYTES,
|
|
89
|
+
ranges
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @param {RangeDirective<RangeValue>|undefined} directive
|
|
95
|
+
* @param {number} contentLength
|
|
96
|
+
* @returns {RangeDirectiveInfo & RangeDirective<NormalizedRangeValue>|undefined}
|
|
97
|
+
*/
|
|
98
|
+
static normalize(directive, contentLength) {
|
|
99
|
+
if(directive === undefined) { return undefined }
|
|
100
|
+
|
|
101
|
+
/** @type {Array<NormalizedRangeValue>} */
|
|
102
|
+
const normalizedRanges = directive.ranges.map(({ start, end }) => {
|
|
103
|
+
if(end === RANGE_EMPTY) { return { start, end: contentLength - 1 } }
|
|
104
|
+
if(start === RANGE_EMPTY) { return { start: contentLength - end, end: contentLength - 1 } }
|
|
105
|
+
return { start, end }
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
const exceeds = normalizedRanges.reduce((acc, value) => (acc || (value.start >= contentLength) || (value.end >= contentLength)), false)
|
|
109
|
+
|
|
110
|
+
const { overlap } = normalizedRanges
|
|
111
|
+
.toSorted((a, b) => a.start - b.start)
|
|
112
|
+
.reduce((acc, item) => ({
|
|
113
|
+
overlap: acc.overlap || acc.end > item.start,
|
|
114
|
+
end: item.end
|
|
115
|
+
}), { overlap: false, end: 0 })
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
units: directive.units,
|
|
119
|
+
overlap,
|
|
120
|
+
exceeds,
|
|
121
|
+
ranges: normalizedRanges
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// console.log(Range.parse(''))
|
|
127
|
+
// console.log(Range.parse('='))
|
|
128
|
+
// console.log(Range.parse('foo'))
|
|
129
|
+
// console.log(Range.parse('bytes'))
|
|
130
|
+
// console.log(Range.parse('bytes='))
|
|
131
|
+
// console.log(Range.parse('bytes=-'))
|
|
132
|
+
// console.log(Range.parse('bytes=foo'))
|
|
133
|
+
// console.log(Range.parse('bytes=0-foo'))
|
|
134
|
+
// console.log(Range.parse('bytes=0-0xff'))
|
|
135
|
+
// console.log()
|
|
136
|
+
// console.log(Range.parse('bytes=1024-'))
|
|
137
|
+
// console.log(Range.parse('bytes=-1024'))
|
|
138
|
+
// console.log(Range.parse('bytes=0-1024'))
|
|
139
|
+
// console.log()
|
|
140
|
+
// console.log(Range.parse('bytes=0-0,-1'))
|
|
141
|
+
// console.log(Range.parse('bytes=0-1024, -1024'))
|
|
142
|
+
// console.log(Range.parse('bytes= 0-999, 4500-5499, -1000'))
|
|
143
|
+
// console.log(Range.parse('bytes=500-600,601-999'))
|
|
144
|
+
|
|
145
|
+
// console.log('------')
|
|
146
|
+
// console.log(Range.normalize(Range.parse('bytes=1024-'), 5000))
|
|
147
|
+
// console.log(Range.normalize(Range.parse('bytes=-1024'), 5000))
|
|
148
|
+
// console.log(Range.normalize(Range.parse('bytes=0-1024'), 5000))
|
|
149
|
+
// console.log(Range.normalize(Range.parse('bytes=0-0,-1'), 10000)) // 0 and 9999
|
|
150
|
+
// console.log(Range.normalize(Range.parse('bytes=0-1024, -1024'), 5000))
|
|
151
|
+
// console.log(Range.normalize(Range.parse('bytes= 0-999, 4500-5499, -1000'), 5000))
|
|
152
|
+
// console.log(Range.normalize(Range.parse('bytes=500-600,601-999'), 5000))
|
|
153
|
+
|
|
154
|
+
// console.log(Range.normalize(Range.parse('bytes=-500'), 10000)) // 9500-9999
|