@johntalton/http-util 5.1.6 → 6.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.
Files changed (84) hide show
  1. package/README.md +54 -7
  2. package/package.json +26 -6
  3. package/src/body.js +8 -8
  4. package/src/{response/defs.js → defs.js} +35 -1
  5. package/src/{accept-encoding.js → headers/accept-encoding.js} +11 -14
  6. package/src/{accept-language.js → headers/accept-language.js} +14 -9
  7. package/src/headers/accept.js +86 -0
  8. package/src/{cache-control.js → headers/cache-control.js} +0 -6
  9. package/src/{clear-site-data.js → headers/clear-site-data.js} +4 -10
  10. package/src/headers/client-hints.js +88 -0
  11. package/src/{conditional.js → headers/conditional.js} +190 -117
  12. package/src/headers/content-disposition.js +44 -0
  13. package/src/{content-range.js → headers/content-range.js} +1 -18
  14. package/src/headers/content-type.js +101 -0
  15. package/src/{forwarded.js → headers/forwarded.js} +8 -56
  16. package/src/{index.js → headers/index.js} +4 -2
  17. package/src/headers/link.js +34 -0
  18. package/src/{multipart.js → headers/multipart.js} +22 -13
  19. package/src/{preference.js → headers/preference.js} +3 -58
  20. package/src/{range.js → headers/range.js} +4 -32
  21. package/src/{rate-limit.js → headers/rate-limit.js} +6 -1
  22. package/src/{server-timing.js → headers/server-timing.js} +3 -16
  23. package/src/headers/strict-transport-security.js +39 -0
  24. package/src/{accept-util.js → headers/util/accept-util.js} +8 -14
  25. package/src/headers/util/index.js +7 -0
  26. package/src/headers/util/kvp.js +79 -0
  27. package/src/headers/util/mime.js +77 -0
  28. package/src/headers/util/whitespace.js +8 -0
  29. package/src/{www-authenticate.js → headers/www-authenticate.js} +1 -1
  30. package/src/response/{accepted.js → 2xx/accepted.js} +2 -2
  31. package/src/response/2xx/bytes.js +62 -0
  32. package/src/response/2xx/created.js +49 -0
  33. package/src/response/2xx/json.js +60 -0
  34. package/src/response/2xx/no-content.js +45 -0
  35. package/src/response/2xx/partial-content.js +101 -0
  36. package/src/response/{preflight.js → 2xx/preflight.js} +29 -10
  37. package/src/response/{sse.js → 2xx/sse.js} +2 -2
  38. package/src/response/{trace.js → 2xx/trace.js} +3 -3
  39. package/src/response/3xx/found.js +23 -0
  40. package/src/response/{moved-permanently.js → 3xx/moved-permanently.js} +2 -2
  41. package/src/response/{multiple-choices.js → 3xx/multiple-choices.js} +2 -3
  42. package/src/response/3xx/not-modified.js +59 -0
  43. package/src/response/{permanent-redirect.js → 3xx/permanent-redirect.js} +2 -2
  44. package/src/response/{see-other.js → 3xx/see-other.js} +2 -2
  45. package/src/response/{temporary-redirect.js → 3xx/temporary-redirect.js} +2 -2
  46. package/src/response/4xx/bad-request.js +19 -0
  47. package/src/response/{conflict.js → 4xx/conflict.js} +2 -2
  48. package/src/response/{content-too-large.js → 4xx/content-too-large.js} +2 -2
  49. package/src/response/{forbidden.js → 4xx/forbidden.js} +3 -2
  50. package/src/response/{gone.js → 4xx/gone.js} +2 -2
  51. package/src/response/{im-a-teapot.js → 4xx/im-a-teapot.js} +2 -2
  52. package/src/response/{not-acceptable.js → 4xx/not-acceptable.js} +14 -3
  53. package/src/response/4xx/not-allowed.js +34 -0
  54. package/src/response/{not-found.js → 4xx/not-found.js} +3 -3
  55. package/src/response/4xx/payment-required.js +17 -0
  56. package/src/response/4xx/precondition-failed.js +45 -0
  57. package/src/response/{range-not-satisfiable.js → 4xx/range-not-satisfiable.js} +15 -4
  58. package/src/response/{timeout.js → 4xx/timeout.js} +2 -2
  59. package/src/response/{too-many-requests.js → 4xx/too-many-requests.js} +22 -5
  60. package/src/response/{unauthorized.js → 4xx/unauthorized.js} +5 -5
  61. package/src/response/{unprocessable.js → 4xx/unprocessable.js} +2 -2
  62. package/src/response/{unsupported-media.js → 4xx/unsupported-media.js} +21 -4
  63. package/src/response/{error.js → 5xx/error.js} +3 -3
  64. package/src/response/{insufficient-storage.js → 5xx/insufficient-storage.js} +2 -2
  65. package/src/response/{not-implemented.js → 5xx/not-implemented.js} +4 -4
  66. package/src/response/{unavailable.js → 5xx/unavailable.js} +16 -4
  67. package/src/response/header-util.js +2 -2
  68. package/src/response/index.js +39 -35
  69. package/src/response/response.js +40 -34
  70. package/src/response/send-util.js +32 -21
  71. package/src/accept.js +0 -122
  72. package/src/content-disposition.js +0 -57
  73. package/src/content-type.js +0 -148
  74. package/src/link.js +0 -35
  75. package/src/response/bytes.js +0 -27
  76. package/src/response/created.js +0 -28
  77. package/src/response/json.js +0 -28
  78. package/src/response/no-content.js +0 -25
  79. package/src/response/not-allowed.js +0 -23
  80. package/src/response/not-modified.js +0 -35
  81. package/src/response/partial-content.js +0 -71
  82. package/src/response/precondition-failed.js +0 -16
  83. /package/src/{fetch-metadata.js → headers/fetch-metadata.js} +0 -0
  84. /package/src/{quote.js → headers/util/quote.js} +0 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @typedef {Object} LinkItem
3
+ * @property {URL|string} url
4
+ * @property {string|undefined} [relation]
5
+ * @property {Map<string, string>|undefined} [parameters]
6
+ */
7
+
8
+ export class Link {
9
+ /**
10
+ * @param {LinkItem} link
11
+ */
12
+ static *#encode(link) {
13
+ const encodedUri = (link.url instanceof URL) ? link.url : encodeURI(link.url)
14
+
15
+ yield `<${encodedUri}>`
16
+ if(link.relation !== undefined) { yield `rel="${link.relation}"` }
17
+ if(link.parameters === undefined) { return }
18
+ for(const [ key, value ] of link.parameters) {
19
+ yield `${key}="${value}"`
20
+ }
21
+ }
22
+
23
+ /**
24
+ * @param {Array<LinkItem>|LinkItem|undefined} links
25
+ */
26
+ static encode(links) {
27
+ if(links === undefined) { return undefined }
28
+ const linkAry = Array.isArray(links) ? links : [ links ]
29
+ if(linkAry.length === 0) { return undefined }
30
+ return linkAry
31
+ .map(link => [ ...Link.#encode(link) ].join('; '))
32
+ .join(', ')
33
+ }
34
+ }
@@ -1,11 +1,12 @@
1
1
  import { ReadableStream } from 'node:stream/web'
2
2
 
3
- import { parseContentDisposition } from './content-disposition.js'
3
+ import { isQuoted, stripQuotes } from '../headers/util/quote.js'
4
+ import { ContentDisposition } from './content-disposition.js'
4
5
  import { ContentRange } from './content-range.js'
5
- import { parseContentType } from './content-type.js'
6
+ import { ContentType } from './content-type.js'
6
7
 
7
8
  /** @import { ContentRangeDirective } from './content-range.js' */
8
- /** @import { SendBody } from './response/send-util.js' */
9
+ /** @import { SendBody } from '../defs.js' */
9
10
 
10
11
  /**
11
12
  * @typedef {Object} MultipartBytePart
@@ -49,11 +50,14 @@ export class Multipart {
49
50
  * @param {string} text
50
51
  * @param {string} boundary
51
52
  * @param {string} [_charset='utf8']
53
+ * @returns {FormData}
52
54
  */
53
55
  static parse_FormData(text, boundary, _charset = 'utf8') {
54
- // console.log({ boundary, text })
55
56
  const formData = new FormData()
56
57
 
58
+ if(text === undefined) { return formData }
59
+ if(boundary === undefined) { return formData }
60
+
57
61
  if(text === '') {
58
62
  // empty body
59
63
  return formData
@@ -94,17 +98,16 @@ export class Multipart {
94
98
  const name = rawName?.toLowerCase()
95
99
  // console.log('header', name, value)
96
100
  if(name === MULTIPART_HEADER.CONTENT_TYPE) {
97
- const _contentType = parseContentType(value)
98
- // console.log({ contentType })
101
+ const contentType = ContentType.parse(value)
102
+ //console.log({ contentType })
99
103
  }
100
104
  else if(name === MULTIPART_HEADER.CONTENT_DISPOSITION) {
101
- const disposition = parseContentDisposition(value)
105
+ const disposition = ContentDisposition.parse(value)
102
106
  if(disposition?.disposition !== DISPOSITION_FORM_DATA) {
103
107
  throw new Error('disposition not form-data')
104
108
  }
105
109
 
106
- // todo: are names always quoted?
107
- partName = disposition.name?.slice(1, -1)
110
+ partName = isQuoted(disposition.name) ? stripQuotes(disposition.name) : disposition.name
108
111
  }
109
112
  else {
110
113
  // unsupported part header - ignore
@@ -150,6 +153,7 @@ export class Multipart {
150
153
 
151
154
  return new ReadableStream({
152
155
  type: 'bytes',
156
+ // async pull(controller) {},
153
157
  async start(controller) {
154
158
  const encoder = new TextEncoder()
155
159
 
@@ -158,9 +162,9 @@ export class Multipart {
158
162
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_TYPE}: ${contentType}${MULTIPART_SEPARATOR}`))
159
163
  controller.enqueue(encoder.encode(`${MULTIPART_HEADER.CONTENT_RANGE}: ${ContentRange.encode({ ...part.range, size: contentLength })}${MULTIPART_SEPARATOR}`))
160
164
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
161
- // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
162
165
 
163
166
  if(part.obj instanceof ReadableStream) {
167
+ // biome-ignore lint/performance/noAwaitInLoops: readable
164
168
  for await (const chunk of part.obj) {
165
169
  if(chunk instanceof ArrayBuffer || ArrayBuffer.isView(chunk)) {
166
170
  controller.enqueue(chunk)
@@ -174,7 +178,10 @@ export class Multipart {
174
178
  }
175
179
  }
176
180
  }
177
- else if(part.obj instanceof ArrayBuffer || ArrayBuffer.isView(part.obj)) {
181
+ else if(part.obj instanceof ArrayBuffer) {
182
+ controller.enqueue(new Uint8Array(part.obj))
183
+ }
184
+ else if(ArrayBuffer.isView(part.obj)) {
178
185
  controller.enqueue(part.obj)
179
186
  }
180
187
  else if(typeof part.obj === 'string'){
@@ -182,7 +189,7 @@ export class Multipart {
182
189
  }
183
190
  else {
184
191
  // console.log('error', typeof part.obj, part.obj)
185
- throw new Error('unknown part type')
192
+ controller.error(new Error('unknown part type'))
186
193
  }
187
194
 
188
195
  controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
@@ -190,7 +197,9 @@ export class Multipart {
190
197
 
191
198
  controller.enqueue(encoder.encode(boundaryEnd))
192
199
 
193
- controller.close()
200
+ // controller.enqueue(encoder.encode(MULTIPART_SEPARATOR))
201
+
202
+ controller.close()
194
203
  }
195
204
  })
196
205
  }
@@ -1,7 +1,8 @@
1
1
  // https://datatracker.ietf.org/doc/html/rfc7240
2
2
  // https://www.rfc-editor.org/rfc/rfc7240#section-3
3
3
 
4
- import { isQuoted, stripQuotes } from './quote.js'
4
+ import { KVP } from './util/kvp.js'
5
+ import { isQuoted, stripQuotes } from './util/quote.js'
5
6
 
6
7
  export const PREFERENCE_SEPARATOR = {
7
8
  PREFERENCE: ',',
@@ -53,26 +54,13 @@ export class Preferences {
53
54
 
54
55
  const preferences = new Map(header.split(PREFERENCE_SEPARATOR.PREFERENCE)
55
56
  .map(pref => {
56
- const [ kvp, ...params ] = pref.trim().split(PREFERENCE_SEPARATOR.PARAMS)
57
+ const { name: kvp, parameters } = KVP.parse(pref) ?? { parameters: new Map() }
57
58
  const [ key, rawValue ] = kvp?.split(PREFERENCE_SEPARATOR.KVP) ?? []
58
59
 
59
60
  if(key === undefined) { return {} }
60
61
  const valueOrEmpty = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
61
62
  const value = (valueOrEmpty !== '') ? valueOrEmpty : undefined
62
63
 
63
- const parameters = new Map(params
64
- .map(param => {
65
- const [ pKey, rawPValue ] = param.split(PREFERENCE_SEPARATOR.PARAM_KVP)
66
- if(pKey === undefined) { return {} }
67
- const trimmedRawPValue = rawPValue?.trim()
68
- const pValueOrEmpty = isQuoted(trimmedRawPValue) ? stripQuotes(trimmedRawPValue) : trimmedRawPValue
69
- const pValue = (pValueOrEmpty !== '') ? pValueOrEmpty : undefined
70
- return { key: pKey.trim(), value: pValue }
71
- })
72
- .filter(item => item.key !== undefined)
73
- .map(item => ([ item.key, item.value ]))
74
- )
75
-
76
64
  return { key, value, parameters }
77
65
  })
78
66
  .filter(item => item.key !== undefined)
@@ -140,46 +128,3 @@ export class AppliedPreferences {
140
128
  return AppliedPreferences.#encode_Map(applied)
141
129
  }
142
130
  }
143
-
144
-
145
- // console.log(AppliedPreferences.encode(undefined))
146
- // console.log(AppliedPreferences.encode({ }))
147
- // console.log(AppliedPreferences.encode({ wait: 10 }))
148
- // console.log(AppliedPreferences.encode({ asynchronous: undefined }))
149
- // console.log(AppliedPreferences.encode({ asynchronous: false }))
150
- // console.log(AppliedPreferences.encode({ asynchronous: true }))
151
- // console.log(AppliedPreferences.encode({ preferences: new Map([
152
- // [ 'respond-async', { value: undefined } ]
153
- // ]) }))
154
- // console.log(AppliedPreferences.encode({
155
- // asynchronous: false,
156
- // preferences: new Map([
157
- // [ 'respond-async', { value: 'fake' } ]
158
- // ]) }))
159
- // console.log(AppliedPreferences.encode({ asynchronous: true, wait: 100 }))
160
- // console.log(AppliedPreferences.encode({
161
- // representation: DIRECTIVE_REPRESENTATION_MINIMAL,
162
- // preferences: new Map([
163
- // ['foo', { value: 'bar', parameters: new Map([ [ 'biz', 'bang' ] ]) } ],
164
- // [ 'fake', undefined ]
165
- // ])
166
- // }))
167
-
168
- // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
169
- // console.log(Preferences.parse(' foo; bar')?.preferences)
170
- // console.log(Preferences.parse(' foo; bar=""')?.preferences)
171
- // console.log(Preferences.parse(' foo=""; bar')?.preferences)
172
- // console.log(Preferences.parse(' foo =""; bar;biz; bang ')?.preferences)
173
- // console.log(Preferences.parse('return=minimal; foo="some parameter"')?.preferences)
174
-
175
- // console.log(Preferences.parse('timezone=America/Los_Angeles'))
176
- // console.log(Preferences.parse('return=headers-only'))
177
- // console.log(Preferences.parse('return=minimal'))
178
- // console.log(Preferences.parse('return=representation'))
179
- // console.log(Preferences.parse('respond-async, wait=10`'))
180
- // console.log(Preferences.parse('priority=5'))
181
- // console.log(Preferences.parse('foo; bar'))
182
- // console.log(Preferences.parse('foo; bar=""'))
183
- // console.log(Preferences.parse('foo=""; bar'))
184
- // console.log(Preferences.parse('handling=lenient, wait=100, respond-async'))
185
- // console.log(Preferences.parse('return=minimal; foo="some parameter"'))
@@ -1,4 +1,6 @@
1
- import { RANGE_UNITS_BYTES } from "./response/defs.js"
1
+ import { RANGE_UNITS_BYTES } from '../defs.js'
2
+
3
+ /** @import { AcceptRangeUnits } from '../defs.js' */
2
4
 
3
5
  export const RANGE_EQUAL = '='
4
6
  export const RANGE_SEPARATOR = '-'
@@ -32,7 +34,7 @@ export const RANGE_EMPTY = ''
32
34
  /**
33
35
  * @template RV
34
36
  * @typedef {Object} RangeDirective
35
- * @property {'bytes'|'none'|undefined} units
37
+ * @property {AcceptRangeUnits|undefined} units
36
38
  * @property {Array<RV>} ranges
37
39
  */
38
40
 
@@ -122,33 +124,3 @@ export class Range {
122
124
  }
123
125
  }
124
126
  }
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
@@ -45,6 +45,7 @@ export class RateLimit {
45
45
  * @param {RateLimitInfo} limitInfo
46
46
  */
47
47
  static from(limitInfo) {
48
+ if(limitInfo === undefined) { return undefined }
48
49
  const { name, remaining, resetSeconds, partitionKey } = limitInfo
49
50
 
50
51
  if(name === undefined || remaining === undefined) { return undefined }
@@ -61,8 +62,12 @@ export class RateLimitPolicy {
61
62
  */
62
63
  static from(...policies) {
63
64
  if(policies === undefined) { return undefined }
65
+ if(policies.length === 0) { return undefined }
64
66
 
65
- return policies
67
+ const remainingPolicies = policies.filter(pol => pol !== undefined)
68
+ if(remainingPolicies.length === 0) { return undefined }
69
+
70
+ return remainingPolicies
66
71
  .filter(policy => policy.name !== undefined && policy.quota !== undefined)
67
72
  .map(policy => {
68
73
  const {
@@ -19,7 +19,7 @@ export const SERVER_TIMING_SEPARATOR = {
19
19
 
20
20
  export class ServerTiming {
21
21
  /**
22
- * @param {Array<TimingsInfo>} timings
22
+ * @param {Array<TimingsInfo>|undefined} timings
23
23
  */
24
24
  static encode(timings) {
25
25
  if(timings === undefined) { return undefined }
@@ -28,24 +28,11 @@ export class ServerTiming {
28
28
  return timings
29
29
  .map(({ name, duration, description }) => [
30
30
  `${name}`,
31
- description !== undefined ? `${SERVER_TIMING_KEY_DESCRIPTION}${SERVER_TIMING_SEPARATOR.KVP}"${description}"` : undefined,
32
- duration !== undefined ? `${SERVER_TIMING_KEY_DURATION}${SERVER_TIMING_SEPARATOR.KVP}${Math.trunc(duration * 10) / 10}` : undefined
31
+ description === undefined ? undefined : `${SERVER_TIMING_KEY_DESCRIPTION}${SERVER_TIMING_SEPARATOR.KVP}"${description}"`,
32
+ duration === undefined ? undefined : `${SERVER_TIMING_KEY_DURATION}${SERVER_TIMING_SEPARATOR.KVP}${Math.trunc(duration * 10) / 10}`
33
33
  ]
34
34
  .filter(item => item !== undefined)
35
35
  .join(SERVER_TIMING_SEPARATOR.PARAMETER))
36
36
  .join(SERVER_TIMING_SEPARATOR.METRIC)
37
37
  }
38
38
  }
39
-
40
-
41
- // console.log(ServerTiming.encode([{ name: 'missedCache' }]))
42
- // console.log(ServerTiming.encode([{ name: 'cpu', duration: 2.4 }]))
43
-
44
- // // cache;desc="Cache Read";dur=23.2
45
- // console.log(ServerTiming.encode([{ name: 'cache', duration: 23.2, description: "Cache Read" }]))
46
-
47
- // // db;dur=53, app;dur=47.2
48
- // console.log(ServerTiming.encode([
49
- // { name: 'db', duration: 54 },
50
- // { name: 'app', duration: 47.2 }
51
- // ]))
@@ -0,0 +1,39 @@
1
+
2
+
3
+ export const STS_MAX_AGE = 'max-age'
4
+ export const STS_INCLUDE_SUBDOMAIN = 'includeSubDomains'
5
+ export const STS_PRELOAD = 'preload'
6
+
7
+ export const STS_MIN_AGE_FOR_PRELOAD_SECS = 60 * 60 * 24 * 365 // 31536000
8
+
9
+
10
+ /**
11
+ * @typedef {Object} StrictTransportSecurityOptions
12
+ * @property {number} maxAge
13
+ * @property {boolean|undefined} [includeSubDomains]
14
+ * @property {boolean|undefined} [preload]
15
+ */
16
+
17
+ export class StrictTransportSecurity {
18
+ /**
19
+ * @param {StrictTransportSecurityOptions} sts
20
+ */
21
+ static *#encode(sts) {
22
+ if(!Number.isFinite(sts.maxAge)) {
23
+ throw new Error('invalid max-age')
24
+ }
25
+
26
+ const maxAge = sts.preload ? Math.max(STS_MIN_AGE_FOR_PRELOAD_SECS, sts.maxAge) : sts.maxAge
27
+ yield `${STS_MAX_AGE}=${maxAge}`
28
+ if(sts.includeSubDomains) { yield STS_INCLUDE_SUBDOMAIN }
29
+ if(sts.preload) { yield STS_PRELOAD }
30
+ }
31
+
32
+ /**
33
+ * @param {StrictTransportSecurityOptions} sts
34
+ */
35
+ static encode(sts) {
36
+ if(sts === undefined) { return undefined }
37
+ return [ ...StrictTransportSecurity.#encode(sts) ].join('; ')
38
+ }
39
+ }
@@ -1,8 +1,8 @@
1
+ import { KVP } from './kvp.js'
2
+
1
3
  export const QUALITY = 'q'
2
4
  export const SEPARATOR = {
3
- MEDIA_RANGE: ',',
4
- PARAMETER: ';',
5
- KVP: '='
5
+ MEDIA_RANGE: ','
6
6
  }
7
7
 
8
8
  export const DEFAULT_QUALITY_STRING = '1'
@@ -29,16 +29,10 @@ export function parseAcceptStyleHeader(header, wellKnown) {
29
29
  .trim()
30
30
  .split(SEPARATOR.MEDIA_RANGE)
31
31
  .map(mediaRange => {
32
- const [ name, ...parametersSet ] = mediaRange
33
- .trim()
34
- .split(SEPARATOR.PARAMETER)
35
-
36
- const parameters = new Map(parametersSet.map(parameter => {
37
- const [ key, value ] = parameter.split(SEPARATOR.KVP).map(p => p.trim())
38
- return [ key, value ]
39
- }))
32
+ const { name, parameters } = KVP.parse(mediaRange) ?? { parameters: new Map() }
33
+ if(name === undefined) { return undefined }
34
+ if(name === '') { return undefined }
40
35
 
41
- if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
42
36
  const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
43
37
 
44
38
  return {
@@ -47,9 +41,9 @@ export function parseAcceptStyleHeader(header, wellKnown) {
47
41
  parameters
48
42
  }
49
43
  })
50
- .filter(entry => entry.name !== undefined && entry.name !== '')
44
+ .filter(entry => entry !== undefined)
51
45
  .sort((entryA, entryB) => {
52
46
  // B - A descending order
53
47
  return entryB.quality - entryA.quality
54
48
  })
55
- }
49
+ }
@@ -0,0 +1,7 @@
1
+ /** biome-ignore-all lint/performance/noBarrelFile: entry point */
2
+ /** biome-ignore-all lint/performance/noReExportAll: entry point */
3
+ export * from './accept-util.js'
4
+ export * from './kvp.js'
5
+ export * from './mime.js'
6
+ export * from './quote.js'
7
+ export * from './whitespace.js'
@@ -0,0 +1,79 @@
1
+ import { hasSpecialChar } from './mime.js'
2
+ import { isQuoted, stripQuotes } from './quote.js'
3
+
4
+ export const DEFAULT_DELIMITER = ';'
5
+ export const KVP_DELIMITER = '='
6
+ export const KVP_EMPTY = ''
7
+
8
+
9
+ export class KVP {
10
+ /**
11
+ * @param {Array<string>|undefined} params
12
+ * @param {Array<string>|undefined} [acceptableKeys=undefined]
13
+ */
14
+ static #parse(params, acceptableKeys = undefined) {
15
+ const parameters = new Map()
16
+
17
+ if(params === undefined) { return parameters }
18
+
19
+ for(const kvp of params) {
20
+ const [ rawKey, rawValue ] = kvp
21
+ .split(KVP_DELIMITER)
22
+ .map(p => p.trim())
23
+
24
+ if(rawKey === undefined) { continue }
25
+ if(rawKey === KVP_EMPTY) { continue }
26
+ const key = rawKey.toLowerCase()
27
+ if(hasSpecialChar(key)) { continue }
28
+
29
+ if(acceptableKeys !== undefined && !acceptableKeys.includes(key)) { continue }
30
+
31
+ const unquotedValue = isQuoted(rawValue) ? stripQuotes(rawValue) : rawValue
32
+ const value = unquotedValue === KVP_EMPTY ? undefined : unquotedValue
33
+
34
+ if(!parameters.has(key)) {
35
+ parameters.set(key, value)
36
+ }
37
+ }
38
+
39
+ return parameters
40
+ }
41
+
42
+ /**
43
+ * @param {string|undefined} str
44
+ * @param {Array<string>|undefined} [acceptableKeys=undefined]
45
+ */
46
+ static parse(str, acceptableKeys = undefined, delimiter = DEFAULT_DELIMITER) {
47
+ if(str === undefined) { return undefined }
48
+ if(str === KVP_EMPTY) { return undefined }
49
+
50
+ const [ name, ...params ] = str
51
+ .trim()
52
+ .split(delimiter)
53
+ .map(p => p.trim())
54
+
55
+ const parameters = KVP.#parse(params, acceptableKeys)
56
+
57
+ return { name, parameters }
58
+ }
59
+
60
+ /**
61
+ * @param {string|undefined} str
62
+ * @param {Array<string>|undefined} [acceptableKeys=undefined]
63
+ */
64
+ static parseParameters(str, acceptableKeys = undefined, delimiter = DEFAULT_DELIMITER) {
65
+ if(str === undefined) { return undefined }
66
+ if(str === KVP_EMPTY) { return undefined }
67
+
68
+ const params = str
69
+ .trim()
70
+ .split(delimiter)
71
+ .map(p => p.trim())
72
+
73
+ const parameters = KVP.#parse(params, acceptableKeys)
74
+
75
+ return { parameters }
76
+ }
77
+ }
78
+
79
+
@@ -0,0 +1,77 @@
1
+
2
+ /**
3
+ * @typedef {Object} MimeItem
4
+ * @property {string} mimetype
5
+ * @property {string} type
6
+ * @property {string} subtype
7
+ */
8
+
9
+ export const MIME_SEPARATOR = { SUBTYPE: '/' }
10
+
11
+ export const MIME_ANY = '*'
12
+
13
+ //
14
+ export const SPECIAL_CHARS = [
15
+ // special
16
+ '(', ')',
17
+ '<', '>',
18
+ '[', ']',
19
+ '{', '}',
20
+ '@', ',', ';', ':',
21
+ '\\', '"', '/', '?', '=', // '.',
22
+ // '%', // '!', '$', '&', // # ^ * | ~ `
23
+ // space
24
+ ' ', '\u000B', '\u000C',
25
+ // control
26
+ '\n', '\r', '\t'
27
+ ]
28
+
29
+ /**
30
+ * @param {string|undefined} value
31
+ */
32
+ export function hasSpecialChar(value) {
33
+ if(value === undefined) { return false }
34
+ for(const special of SPECIAL_CHARS) {
35
+ if(value.includes(special)) { return true }
36
+ }
37
+
38
+ return false
39
+ }
40
+
41
+ export class Mime {
42
+ /**
43
+ * @param {string|undefined} name
44
+ * @returns {MimeItem|undefined}
45
+ */
46
+ static parse(name) {
47
+ // console.log('mime::parse', name)
48
+ if(name === undefined) { return undefined }
49
+ if(name === '') { return undefined }
50
+
51
+ const parts = name
52
+ .trim() // leading space of type and trailing of subtype
53
+ .split(MIME_SEPARATOR.SUBTYPE)
54
+ .map(t => t.toLowerCase()) // all type/subtypes should be lower
55
+
56
+ // protect against multiple slashes
57
+ if(parts.length > 2) { return undefined }
58
+
59
+ const [ type, candidateSubtype ] = parts
60
+
61
+ if(type === undefined) { return undefined }
62
+ if(type === '') { return undefined }
63
+ if(hasSpecialChar(type)) { return undefined }
64
+
65
+ // if(candidateSubtype === undefined) { return undefined }
66
+ // if(candidateSubtype === '') { return undefined }
67
+ if(hasSpecialChar(candidateSubtype)) { return undefined }
68
+
69
+ const subtype = (candidateSubtype === '') ? MIME_ANY : (candidateSubtype ?? MIME_ANY)
70
+
71
+ return {
72
+ mimetype: `${type}${MIME_SEPARATOR.SUBTYPE}${subtype}`,
73
+ type,
74
+ subtype
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,8 @@
1
+ export const WHITESPACE_REGEX = /\s/
2
+
3
+ /**
4
+ * @param {string} c
5
+ */
6
+ export function isWhitespace(c){
7
+ return WHITESPACE_REGEX.test(c)
8
+ }
@@ -99,7 +99,7 @@ export class Challenge {
99
99
  return `${key}=${value}`
100
100
  })
101
101
 
102
- const params = parameters !== undefined ? [ ...parameters ].join(',') : ''
102
+ const params = parameters === undefined ? '' : [ ...parameters ].join(',')
103
103
 
104
104
  return `${challenge.scheme} ${params}`
105
105
  }
@@ -1,9 +1,9 @@
1
1
  import http2 from 'node:http2'
2
2
 
3
- import { send } from './send-util.js'
3
+ import { send } from '../send-util.js'
4
4
 
5
5
  /** @import { ServerHttp2Stream } from 'node:http2' */
6
- /** @import { Metadata } from './defs.js' */
6
+ /** @import { Metadata } from '../../defs.js' */
7
7
 
8
8
  const { HTTP_STATUS_ACCEPTED } = http2.constants
9
9
 
@@ -0,0 +1,62 @@
1
+ import http2 from 'node:http2'
2
+
3
+ import { send_bytes } from '../send-util.js'
4
+
5
+ /** @import { ServerHttp2Stream } from 'node:http2' */
6
+ /** @import { AcceptRangeUnits, SendContent, SendInfo, Metadata, SendBody } from '../../defs.js' */
7
+ /** @import { EtagItem, IMFFixDateInput } from '../../headers/conditional.js' */
8
+ /** @import { CacheControlOptions } from '../../headers/cache-control.js' */
9
+
10
+ const { HTTP_STATUS_OK } = http2.constants
11
+
12
+ /**
13
+ * @param {ServerHttp2Stream} stream
14
+ * @param {SendBody|undefined} obj
15
+ * @param {string|undefined} contentType
16
+ * @param {number|undefined} contentLength
17
+ * @param {string|undefined} encoding
18
+ * @param {EtagItem|undefined} etag
19
+ * @param {IMFFixDateInput|string|undefined} lastModified
20
+ * @param {number|undefined} age
21
+ * @param {CacheControlOptions} cacheControl
22
+ * @param {AcceptRangeUnits|undefined} acceptRanges
23
+ * @param {Metadata} meta
24
+ */
25
+ export function sendBytes(stream, contentType, obj, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, meta) {
26
+ _sendBytes(stream, obj, {
27
+ contentType,
28
+ contentLength,
29
+ encoding,
30
+ etag,
31
+ lastModified,
32
+ age,
33
+ cacheControl
34
+ }, {
35
+ acceptRanges
36
+ }, meta)
37
+ }
38
+
39
+ /**
40
+ * @param {ServerHttp2Stream} stream
41
+ * @param {SendBody|undefined} obj
42
+ * @param {Omit<SendContent, 'rangeDirective'>} content
43
+ * @param {Pick<SendInfo, 'acceptRanges'>} info
44
+ * @param {Metadata} meta
45
+ */
46
+ export function _sendBytes(stream, obj, content, info, meta) {
47
+ const {
48
+ contentType,
49
+ contentLength,
50
+ encoding,
51
+ etag,
52
+ lastModified,
53
+ age,
54
+ cacheControl,
55
+ } = content
56
+
57
+ const { acceptRanges } = info
58
+
59
+ const supportedQueryType = undefined
60
+ const range = undefined
61
+ send_bytes(stream, HTTP_STATUS_OK, contentType, obj, range, contentLength, encoding, etag, lastModified, age, cacheControl, acceptRanges, supportedQueryType, meta)
62
+ }