@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.
Files changed (57) hide show
  1. package/README.md +232 -21
  2. package/package.json +1 -1
  3. package/src/accept-encoding.js +1 -1
  4. package/src/accept-language.js +1 -1
  5. package/src/accept-util.js +1 -1
  6. package/src/accept.js +2 -5
  7. package/src/body.js +13 -15
  8. package/src/cache-control.js +6 -1
  9. package/src/clear-site-data.js +59 -0
  10. package/src/conditional.js +37 -42
  11. package/src/content-disposition.js +12 -7
  12. package/src/content-range.js +59 -0
  13. package/src/content-type.js +17 -14
  14. package/src/forwarded.js +1 -1
  15. package/src/index.js +6 -1
  16. package/src/multipart.js +81 -33
  17. package/src/preference.js +205 -0
  18. package/src/range.js +154 -0
  19. package/src/response/accepted.js +1 -0
  20. package/src/response/bytes.js +27 -0
  21. package/src/response/conflict.js +1 -0
  22. package/src/response/content-too-large.js +16 -0
  23. package/src/response/created.js +2 -1
  24. package/src/response/defs.js +15 -1
  25. package/src/response/error.js +1 -0
  26. package/src/response/forbidden.js +17 -0
  27. package/src/response/gone.js +16 -0
  28. package/src/response/header-util.js +2 -1
  29. package/src/response/im-a-teapot.js +16 -0
  30. package/src/response/index.js +17 -0
  31. package/src/response/insufficient-storage.js +16 -0
  32. package/src/response/json.js +6 -53
  33. package/src/response/moved-permanently.js +23 -0
  34. package/src/response/multiple-choices.js +21 -0
  35. package/src/response/no-content.js +2 -1
  36. package/src/response/not-acceptable.js +2 -1
  37. package/src/response/not-allowed.js +1 -0
  38. package/src/response/not-found.js +1 -0
  39. package/src/response/not-implemented.js +1 -0
  40. package/src/response/not-modified.js +3 -2
  41. package/src/response/partial-content.js +71 -0
  42. package/src/response/permanent-redirect.js +23 -0
  43. package/src/response/precondition-failed.js +1 -0
  44. package/src/response/preflight.js +21 -5
  45. package/src/response/range-not-satisfiable.js +28 -0
  46. package/src/response/response.js +26 -0
  47. package/src/response/see-other.js +23 -0
  48. package/src/response/send-util.js +137 -6
  49. package/src/response/sse.js +4 -3
  50. package/src/response/temporary-redirect.js +23 -0
  51. package/src/response/timeout.js +1 -0
  52. package/src/response/too-many-requests.js +1 -0
  53. package/src/response/trace.js +8 -4
  54. package/src/response/unauthorized.js +1 -0
  55. package/src/response/unavailable.js +1 -0
  56. package/src/response/unprocessable.js +1 -0
  57. package/src/response/unsupported-media.js +15 -4
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Set of utilities to aid in building from-scratch [node:http2](https://nodejs.org/docs/latest/api/http2.html) stream compatible services.
4
4
 
5
- - Header parsers
6
- - Stream Response methods
7
- - Body parser
5
+ - [Header parsers](#header-parsers)
6
+ - [Stream Response methods](#response)
7
+ - [Body parser](#body)
8
8
 
9
9
  ## Header Parsers
10
10
  ### From Client:
@@ -44,24 +44,37 @@ const bestMatchingType = Accept.select(acceptHeader, supportedType)
44
44
 
45
45
  All responders take in a `stream` as well as a metadata object to hint on servername and origin strings etc.
46
46
 
47
- - `sendAccepted`
48
- - `sendConflict`
49
- - `sendCreated`
50
- - `sendError` - 500
51
- - `sendJSON_Encoded` - Standard Ok response with encoding
52
- - `sendNoContent`
53
- - `sendNotAcceptable`
54
- - `sendNotAllowed` - Method not supported / allowed
55
- - `sendNotFound` - 404
56
- - `sendNotModified`
57
- - `sendPreflight` - Response to OPTIONS with CORS headers
58
- - `sendTimeout`
59
- - `sendTooManyRequests` - Rate limit response (429)
60
- - `sendTrace`
61
- - `sendUnauthorized` - Unauthorized
62
- - `sendUnprocessable`
63
- - `sendUnsupportedMediaType`
64
- - `sendSSE` - SSE header (leave the `stream` open)
47
+ - [`sendAccepted`](#responseaccepted)
48
+ - [`sendConflict`](#responseconflict)
49
+ - [`sendContentTooLarge`](#)
50
+ - [`sendCreated`](#responsecreated)
51
+ - [`sendError`](#responseerror) - 500
52
+ - [`sendGone`](#)
53
+ - [`sendImATeapot`](#)
54
+ - [`sendInsufficientStorage`](#)
55
+ - [`sendJSON_Encoded`](#responsejson) - Standard Ok response with encoding
56
+ - [`sendMovedPermanently`](#)
57
+ - [`sendMultipleChoices`](#)
58
+ - [`sendNoContent`](#responsenocontent)
59
+ - [`sendNotAcceptable`](#responsenotacceptable)
60
+ - [`sendNotAllowed`](#responsenotallowed) - Method not supported / allowed
61
+ - [`sendNotFound`](#responsenotfound) - 404
62
+ - [`sendNotImplemented`](#responsenotimplemented)
63
+ - [`sendNotModified`](#responsenotmodified)
64
+ - [`sendPartialContent`](#)
65
+ - [`sendPreconditionFailed`](#responsepreconditionfailed)
66
+ - [`sendPreflight`](#responsepreflight) - Response to OPTIONS with CORS headers
67
+ - [`sendRangeNotSatisfiable`](#)
68
+ - [`sendSeeOther`](#)
69
+ - [`sendTemporaryRedirect`](#)
70
+ - [`sendTimeout`](#responsetimeout)
71
+ - [`sendTooManyRequests`](#responsetoomanyrequests) - Rate limit response (429)
72
+ - [`sendTrace`](#responsetrace)
73
+ - [`sendUnauthorized`](#responseunauthorized) - Unauthorized
74
+ - [`sendUnavailable`](#responseunavailable)
75
+ - [`sendUnprocessable`](#responseunprocessable)
76
+ - [`sendUnsupportedMediaType`](#responseunsupportedmediatype)
77
+ - [`sendSSE`](#responsesse) - SSE header (leave the `stream` open)
65
78
 
66
79
  Responses allow for optional CORS headers as well as Server Timing meta data.
67
80
 
@@ -102,3 +115,201 @@ const futureBody = requestBody(stream, {
102
115
  const body = futureBody.json()
103
116
 
104
117
  ```
118
+
119
+ ## Response method API
120
+
121
+ All response methods take in the `stream` and a `meta` property.
122
+
123
+ Additional parameters are specific to each return type.
124
+
125
+ Each return type semantic may also expose header (in addition to the standard headers) to the calling code as needed.
126
+
127
+ Some common objects can be passed such as `EtagItem`, `CacheControlOptions` and `Metadata` have their own structure.
128
+
129
+ Many parameters accept `undefined` to skip/ignore the usage
130
+
131
+ ### Response.accepted
132
+
133
+ Parameters:
134
+ - stream
135
+ - meta
136
+
137
+ ### Response.conflict
138
+
139
+ Parameters:
140
+ - stream
141
+ - meta
142
+
143
+
144
+ ### Response.created
145
+
146
+ Parameters:
147
+ - stream
148
+ - location
149
+ - etag
150
+ - meta
151
+
152
+ Additional Exposed Headers:
153
+ - location
154
+
155
+ ### Response.error
156
+
157
+ Parameters:
158
+ - stream
159
+ - meta
160
+
161
+
162
+ ### Response.json
163
+
164
+ Parameters:
165
+ - stream
166
+ - object
167
+ - encoding - undefined | 'identity' | 'br' | 'gzip' | 'deflate' | 'zstd'
168
+ - etag
169
+ - age
170
+ - cacheControl
171
+ - supportedQueryTypes - undefined | Array of supported query types
172
+ - meta
173
+
174
+ Additional Exposed Headers
175
+ - age
176
+ - accept-query
177
+
178
+ ### Response.noContent
179
+
180
+ Parameters:
181
+ - stream
182
+ - etag
183
+ - meta
184
+
185
+ ### Response.notAcceptable
186
+
187
+ Parameters:
188
+ - stream
189
+ - supportedTypes
190
+ - meta
191
+
192
+ ### Response.notAllowed
193
+
194
+ Parameters:
195
+ - stream
196
+ - methods - Array of allowed methods
197
+ - meta
198
+
199
+ Additional Exposed Headers:
200
+ - allow
201
+
202
+ ### Response.notFound
203
+
204
+ Parameters:
205
+ - stream
206
+ - message
207
+ - meta
208
+
209
+ ### Response.notImplemented
210
+
211
+ Parameters:
212
+ - stream
213
+ - message
214
+ - meta
215
+
216
+ ### Response.notModified
217
+
218
+ Parameters:
219
+ - stream
220
+ - etag
221
+ - age
222
+ - cacheControl
223
+ - meta
224
+
225
+ Additional Exposed Headers:
226
+ - age
227
+
228
+ ### Response.preconditionFailed
229
+
230
+ Parameters:
231
+ - stream
232
+ - meta
233
+
234
+ ### Response.preflight
235
+
236
+ Parameters:
237
+ - stream
238
+ - methods
239
+ - supportedQueryTypes - undefined | Array of supported types
240
+ - meta
241
+
242
+ Additional Exposed Headers:
243
+ - accept-query
244
+
245
+ ### Response.sse
246
+
247
+ Parameters:
248
+ - stream
249
+ - meta
250
+
251
+
252
+ ### Response.timeout
253
+
254
+ Parameters:
255
+ - stream
256
+ - meta
257
+
258
+
259
+ ### Response.tooManyRequests
260
+
261
+ Parameters:
262
+ - stream
263
+ - limitInfo - `RateLimitInfo`
264
+ - policies - Array of `RateLimitPolicyInfo`
265
+ - meta
266
+
267
+ Additional Exposed Headers:
268
+ - retry-after
269
+ - rate-limit
270
+ - rate-limit-policy
271
+
272
+ ### Response.trace
273
+
274
+ Parameters:
275
+ - stream
276
+ - method
277
+ - url
278
+ - headers
279
+ - meta
280
+
281
+ ### Response.unauthorized
282
+
283
+ Parameters:
284
+ - stream
285
+ - meta
286
+
287
+
288
+ ### Response.unavailable
289
+
290
+ Parameters:
291
+ - stream
292
+ - message
293
+ - retryAfter
294
+ - meta
295
+
296
+ Additional Exposed Headers:
297
+ - retry-after
298
+
299
+ ### Response.unprocessable
300
+
301
+ Parameters:
302
+ - stream
303
+ - meta
304
+
305
+ ### Response.unsupportedMediaType
306
+
307
+ Parameters:
308
+ - stream
309
+ - acceptableMediaTypes
310
+ - supportedQueryTypes
311
+ - meta
312
+
313
+ Additional Exposed Headers:
314
+ - accept-query
315
+ - accept-post
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johntalton/http-util",
3
- "version": "4.1.1",
3
+ "version": "5.0.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,7 +21,7 @@ export class AcceptEncoding {
21
21
  */
22
22
  static select(acceptEncodingHeader, supportedTypes) {
23
23
  const accepts = AcceptEncoding.parse(acceptEncodingHeader)
24
- return this.selectFrom(accepts, supportedTypes)
24
+ return AcceptEncoding.selectFrom(accepts, supportedTypes)
25
25
  }
26
26
 
27
27
  /**
@@ -23,7 +23,7 @@ export class AcceptLanguage {
23
23
  */
24
24
  static select(acceptLanguageHeader, supportedTypes) {
25
25
  const accepts = AcceptLanguage.parse(acceptLanguageHeader)
26
- return this.selectFrom(accepts, supportedTypes)
26
+ return AcceptLanguage.selectFrom(accepts, supportedTypes)
27
27
  }
28
28
 
29
29
  /**
@@ -39,7 +39,7 @@ export function parseAcceptStyleHeader(header, wellKnown) {
39
39
  }))
40
40
 
41
41
  if(!parameters.has(QUALITY)) { parameters.set(QUALITY, DEFAULT_QUALITY_STRING) }
42
- const quality = parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
42
+ const quality = Number.parseFloat(parameters.get(QUALITY) ?? DEFAULT_QUALITY_STRING)
43
43
 
44
44
  return {
45
45
  name,
package/src/accept.js CHANGED
@@ -54,7 +54,6 @@ export class Accept {
54
54
  const qualityB = entryB.quality ?? 0
55
55
  const qualityA = entryA.quality ?? 0
56
56
  return qualityB - qualityA
57
- // return entryB.quality - entryA.quality
58
57
  })
59
58
  }
60
59
 
@@ -64,7 +63,7 @@ export class Accept {
64
63
  */
65
64
  static select(acceptHeader, supportedTypes) {
66
65
  const accepts = Accept.parse(acceptHeader)
67
- return this.selectFrom(accepts, supportedTypes)
66
+ return Accept.selectFrom(accepts, supportedTypes)
68
67
  }
69
68
 
70
69
  /**
@@ -84,9 +83,7 @@ export class Accept {
84
83
  quality
85
84
  }
86
85
  })
87
- .filter(best => {
88
- return best.supportedTypes.length > 0
89
- })
86
+ .filter(best => best.supportedTypes.length > 0)
90
87
 
91
88
  if(bests.length === 0) { return undefined }
92
89
  const [ first ] = bests
package/src/body.js CHANGED
@@ -1,15 +1,15 @@
1
- import { CHARSET_UTF8, MIME_TYPE_MULTIPART_FORM_DATA, MIME_TYPE_URL_FORM_DATA } from './content-type.js'
1
+ import {
2
+ CHARSET_UTF8,
3
+ MIME_TYPE_MULTIPART_FORM_DATA,
4
+ MIME_TYPE_URL_FORM_DATA
5
+ } from './content-type.js'
2
6
  import { Multipart } from './multipart.js'
3
7
 
4
- export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
8
+ export const BYTE_PER_K = 1024
9
+ export const DEFAULT_BYTE_LIMIT = BYTE_PER_K * BYTE_PER_K //
5
10
 
6
- /**
7
- * @import { Readable } from 'node:stream'
8
- */
9
-
10
- /**
11
- * @import { ContentType } from './content-type.js'
12
- */
11
+ /** @import { Readable } from 'node:stream' */
12
+ /** @import { ContentType } from './content-type.js' */
13
13
 
14
14
  /**
15
15
  * @typedef {Object} BodyOptions
@@ -32,7 +32,6 @@ export const DEFAULT_BYTE_LIMIT = 1024 * 1024 //
32
32
  * @property { () => Promise<any> } json
33
33
  */
34
34
 
35
-
36
35
  /**
37
36
  * @param {Readable} stream
38
37
  * @param {BodyOptions} [options]
@@ -45,7 +44,7 @@ export function requestBody(stream, options) {
45
44
  const charset = options?.contentType?.charset ?? CHARSET_UTF8
46
45
  const contentType = options?.contentType
47
46
 
48
- const invalidContentLength = (contentLength === undefined || isNaN(contentLength))
47
+ const invalidContentLength = (contentLength === undefined || Number.isNaN(contentLength))
49
48
  // if(contentLength > byteLimit) {
50
49
  // console.log(contentLength, invalidContentLength)
51
50
  // throw new Error('contentLength exceeds limit')
@@ -92,7 +91,6 @@ export function requestBody(stream, options) {
92
91
  // return
93
92
  }
94
93
 
95
-
96
94
  const listener = () => {
97
95
  stats.closed = true
98
96
  controller.error(new Error('Abort Signal'))
@@ -281,7 +279,7 @@ export async function bodyJSON(reader, charset) {
281
279
  * @param {ReadableStream} reader
282
280
  * @param {ContentType} contentType
283
281
  */
284
- async function _bodyFormData_Multipart(reader, contentType) {
282
+ export async function _bodyFormData_Multipart(reader, contentType) {
285
283
  const MULTIPART_FORM_DATA_BOUNDARY_PARAMETER = 'boundary'
286
284
 
287
285
  const text = await bodyText(reader, contentType.charset)
@@ -295,7 +293,7 @@ async function _bodyFormData_Multipart(reader, contentType) {
295
293
  * @param {ReadableStream} reader
296
294
  * @param {ContentType} contentType
297
295
  */
298
- async function _bodyFormData_URL(reader, contentType) {
296
+ export async function _bodyFormData_URL(reader, contentType) {
299
297
  const text = await bodyText(reader, contentType.charset)
300
298
  const sp = new URLSearchParams(text)
301
299
  const formData = new FormData()
@@ -323,4 +321,4 @@ export async function bodyFormData(reader, contentType) {
323
321
  }
324
322
 
325
323
  throw new TypeError('unknown mime type for form data')
326
- }
324
+ }
@@ -12,10 +12,12 @@
12
12
 
13
13
  export class CacheControl {
14
14
  /**
15
- * @param {CacheControlOptions} options
15
+ * @param {CacheControlOptions|undefined} options
16
16
  * @returns {string|undefined}
17
17
  */
18
18
  static encode(options) {
19
+ if(options === undefined) { return undefined }
20
+
19
21
  const {
20
22
  pub,
21
23
  priv,
@@ -46,6 +48,9 @@ export class CacheControl {
46
48
  result.push(`stale-if-error=${staleIfError}`)
47
49
  }
48
50
 
51
+ //
52
+ if(result.length === 0) { return undefined }
53
+
49
54
  return result.join(', ')
50
55
  }
51
56
  }
@@ -0,0 +1,59 @@
1
+
2
+ /** @type {'*'} */
3
+ export const WILDCARD = '*'
4
+
5
+ export const CSD_DIRECTIVE_SEPARATOR = ','
6
+ export const CSD_QUOTE = '"'
7
+
8
+ export const CSD_DIRECTIVE_CACHE = 'cache'
9
+ export const CSD_DIRECTIVE_CLIENT_HINTS = 'clientHints'
10
+ export const CSD_DIRECTIVE_COOKIES = 'cookies'
11
+ export const CSD_DIRECTIVE_EXECUTION_CONTEXTS = 'executionContexts'
12
+ export const CSD_DIRECTIVE_PREFETCH_CACHE = 'prefetchCache'
13
+ export const CSD_DIRECTIVE_PRERENDER_CACHE = 'prerenderCache'
14
+ export const CSD_DIRECTIVE_STORAGE = 'storage'
15
+
16
+ /**
17
+ * @typedef {Object} ClearOptions
18
+ * @property {boolean} cache
19
+ * @property {boolean} clientHints
20
+ * @property {boolean} cookies
21
+ * @property {boolean} executionContext
22
+ * @property {boolean} prefetchCache
23
+ * @property {boolean} prerenderCache
24
+ * @property {boolean} storage
25
+ */
26
+
27
+ export class SiteData {
28
+ /**
29
+ * @param {Partial<ClearOptions>|true|'*'|undefined} directives
30
+ */
31
+ static encode(directives) {
32
+ if(directives === undefined) { return undefined }
33
+ if(directives === true) { return WILDCARD }
34
+ if(directives === WILDCARD) { return WILDCARD }
35
+
36
+ const result = []
37
+ if(directives.cache === true) { result.push(CSD_DIRECTIVE_CACHE) }
38
+ if(directives.clientHints === true) { result.push(CSD_DIRECTIVE_CLIENT_HINTS) }
39
+ if(directives.cookies === true) { result.push(CSD_DIRECTIVE_COOKIES) }
40
+ if(directives.executionContext === true) { result.push(CSD_DIRECTIVE_EXECUTION_CONTEXTS) }
41
+ if(directives.prefetchCache === true) { result.push(CSD_DIRECTIVE_PREFETCH_CACHE) }
42
+ if(directives.prerenderCache === true) { result.push(CSD_DIRECTIVE_PRERENDER_CACHE) }
43
+ if(directives.storage === true) { result.push(CSD_DIRECTIVE_STORAGE) }
44
+
45
+ return result
46
+ .map(item => `${CSD_QUOTE}${item}${CSD_QUOTE}`)
47
+ .join(CSD_DIRECTIVE_SEPARATOR)
48
+ }
49
+ }
50
+
51
+ // console.log(SiteData.encode())
52
+ // console.log(SiteData.encode({}))
53
+ // console.log(SiteData.encode(false))
54
+ // console.log(SiteData.encode('true'))
55
+ // console.log(SiteData.encode(true))
56
+ // console.log(SiteData.encode('*'))
57
+ // console.log(SiteData.encode({ storage: false }))
58
+ // console.log(SiteData.encode({ storage: true }))
59
+ // console.log(SiteData.encode({ storage: true, cookies: true }))
@@ -49,6 +49,9 @@ export const DATE_SEPARATOR = ','
49
49
  export const DATE_TIME_SEPARATOR = ':'
50
50
  export const DATE_ZONE = 'GMT'
51
51
 
52
+ export const MINIMUM_YEAR = 1900
53
+ export const MAXIMUM_DAY = 31
54
+
52
55
  /**
53
56
  * @param {string} etag
54
57
  */
@@ -102,6 +105,31 @@ export class ETag {
102
105
  * @returns {AnyEtagItem}
103
106
  */
104
107
  static any() { return ANY_ETAG_ITEM }
108
+
109
+ /**
110
+ * @param {string|undefined} raw
111
+ * @returns {EtagItem|undefined}
112
+ */
113
+ static parse(raw) {
114
+ if(raw === undefined) { return undefined }
115
+
116
+ const rawEtag = raw.trim()
117
+ const weak = rawEtag.startsWith(CONDITION_ETAG_WEAK_PREFIX)
118
+ const quotedEtag = weak ? rawEtag.substring(CONDITION_ETAG_WEAK_PREFIX.length) : rawEtag
119
+
120
+ if(quotedEtag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
121
+
122
+ if(!isQuoted(quotedEtag)) { return undefined }
123
+ const etag = stripQuotes(quotedEtag)
124
+ if(!isValidEtag(etag)) { return undefined }
125
+ if(etag === CONDITION_ETAG_ANY) { return undefined }
126
+
127
+ return {
128
+ weak,
129
+ any: false,
130
+ etag
131
+ }
132
+ }
105
133
  }
106
134
 
107
135
  export class Conditional {
@@ -131,40 +159,7 @@ export class Conditional {
131
159
  if(matchHeader === undefined) { return [] }
132
160
 
133
161
  return matchHeader.split(CONDITION_ETAG_SEPARATOR)
134
- .map(etag => etag.trim())
135
- .map(etag => {
136
- if(etag.startsWith(CONDITION_ETAG_WEAK_PREFIX)) {
137
- // weak
138
- return {
139
- weak: true,
140
- etag: etag.substring(CONDITION_ETAG_WEAK_PREFIX.length)
141
- }
142
- }
143
-
144
- // strong
145
- return {
146
- weak: false,
147
- etag
148
- }
149
- })
150
- .map(item => {
151
- if(item.etag === CONDITION_ETAG_ANY) { return ANY_ETAG_ITEM }
152
-
153
- // validated quoted
154
- if(!isQuoted(item.etag)) { return undefined }
155
- const etag = stripQuotes(item.etag)
156
- if(!isValidEtag(etag)) { return undefined }
157
- if(etag === CONDITION_ETAG_ANY) { return undefined }
158
-
159
- /** @type {WeakEtagItem | NotWeakEtagItem} */
160
- const result = {
161
- weak: item.weak,
162
- any: false,
163
- etag
164
- }
165
-
166
- return result
167
- })
162
+ .map(ETag.parse)
168
163
  .filter(item => item !== undefined)
169
164
  }
170
165
 
@@ -209,7 +204,7 @@ export class Conditional {
209
204
  // <day-name>, <day> <month> <year> <hour>:<minute>:<second> GMT
210
205
  // day-name "," SP date1 SP time-of-day SP GMT
211
206
 
212
- if(matchHeader.length != 29) { return undefined }
207
+ if(matchHeader.length !== 29) { return undefined }
213
208
 
214
209
  //
215
210
  const spaces = [
@@ -238,12 +233,12 @@ export class Conditional {
238
233
 
239
234
  //
240
235
  const dayName = matchHeader.substring(0, 3)
241
- const day = parseInt(matchHeader.substring(5, 7))
236
+ const day = Number.parseInt(matchHeader.substring(5, 7))
242
237
  const month = matchHeader.substring(8, 11)
243
- const year = parseInt(matchHeader.substring(12, 16))
244
- const hour = parseInt(matchHeader.substring(17, 19))
245
- const minute = parseInt(matchHeader.substring(20, 22))
246
- const second = parseInt(matchHeader.substring(23, 25))
238
+ const year = Number.parseInt(matchHeader.substring(12, 16))
239
+ const hour = Number.parseInt(matchHeader.substring(17, 19))
240
+ const minute = Number.parseInt(matchHeader.substring(20, 22))
241
+ const second = Number.parseInt(matchHeader.substring(23, 25))
247
242
 
248
243
  //
249
244
  if(!DATE_DAYS.includes(dayName)) { return undefined }
@@ -255,8 +250,8 @@ export class Conditional {
255
250
  if(!Number.isInteger(second)) { return undefined }
256
251
 
257
252
  //
258
- if(day > 31 || day <= 0) { return undefined }
259
- if(year < 1900) { return undefined }
253
+ if(day > MAXIMUM_DAY || day <= 0) { return undefined }
254
+ if(year < MINIMUM_YEAR) { return undefined }
260
255
  if(hour > 24 || hour < 0) { return undefined }
261
256
  if(minute > 60 || minute < 0) { return undefined }
262
257
  if(second > 60 || second < 0) { return undefined }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * @typedef {Object} Disposition
3
3
  * @property {string} disposition
4
- * @property {Map<string, string>} parameters
5
- * @property {string} [name]
6
- * @property {string} [filename]
4
+ * @property {Map<string, string|undefined>} parameters
5
+ * @property {string|undefined} [name]
6
+ * @property {string|undefined} [filename]
7
7
  */
8
8
 
9
9
  export const DISPOSITION_SEPARATOR = {
@@ -15,17 +15,22 @@ export const DISPOSITION_PARAM_NAME = 'name'
15
15
  export const DISPOSITION_PARAM_FILENAME = 'filename'
16
16
 
17
17
  /**
18
- * @param {string} contentDispositionHeader
18
+ * @param {string|undefined} contentDispositionHeader
19
19
  * @returns {Disposition|undefined}
20
20
  */
21
21
  export function parseContentDisposition(contentDispositionHeader) {
22
22
  if(contentDispositionHeader === undefined) { return undefined }
23
23
 
24
24
  const [ disposition, ...parameterSet ] = contentDispositionHeader.trim().split(DISPOSITION_SEPARATOR.PARAMETER).map(entry => entry.trim())
25
+ if(disposition === undefined) { return undefined }
25
26
  const parameters = new Map(parameterSet.map(parameter => {
26
- const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
27
- return [ key, value ]
28
- }))
27
+ const [ key, value ] = parameter.split(DISPOSITION_SEPARATOR.KVP).map(p => p.trim())
28
+ if(key === undefined) { return undefined }
29
+ return { key, value }
30
+ })
31
+ .filter(item => item !== undefined)
32
+ .map(({ key, value }) => ([ key, value ])))
33
+
29
34
 
30
35
  const name = parameters.get(DISPOSITION_PARAM_NAME)
31
36
  const filename = parameters.get(DISPOSITION_PARAM_FILENAME)