Skip to content

Commit 7bbaad3

Browse files
committed
refactor algorithm
1 parent 126463b commit 7bbaad3

File tree

2 files changed

+71
-20
lines changed

2 files changed

+71
-20
lines changed

lib/content-type.js

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,31 @@
11
'use strict'
22

33
/**
4-
* tokensReg is used to parse the media-type and media-subtype fields.
4+
* keyValuePairsReg is used to split the parameters list into associated
5+
* key value pairings.
6+
*
7+
* @see https://httpwg.org/specs/rfc9110.html#parameter
8+
* @type {RegExp}
9+
*/
10+
const keyValuePairsReg = /([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
11+
12+
/**
13+
* typeNameReg is used to validate that the first part of the media-type
14+
* does not use disallowed characters.
515
*
616
* @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
717
* @type {RegExp}
818
*/
9-
const tokensReg = /^([\w!#$%&'*+.^`|~-]+)\/([\w!#$%&'*+.^`|~-]+)\s*(;.*)?/
19+
const typeNameReg = /^[\w!#$%&'*+.^`|~-]+$/
1020

1121
/**
12-
* keyValuePairsReg is used to split the parameters list into associated
13-
* key value pairings.
22+
* subtypeNameReg is used to validate that the second part of the media-type
23+
* does not use disallowed characters.
1424
*
15-
* @see https://httpwg.org/specs/rfc9110.html#parameter
25+
* @see https://httpwg.org/specs/rfc9110.html#rule.token.separators
1626
* @type {RegExp}
1727
*/
18-
const keyValuePairsReg = /([\w!#$%&'*+.^`|~-]+)=([^;]*)/gm
28+
const subtypeNameReg = /^[\w!#$%&'*+.^`|~-]+\s*/
1929

2030
/**
2131
* ContentType parses and represents the value of the content-type header.
@@ -36,30 +46,64 @@ class ContentType {
3646
return
3747
}
3848

39-
const hv = headerValue.trim()
40-
let matches = tokensReg.exec(hv)
41-
if (matches === null) {
49+
let sepIdx = headerValue.indexOf(';')
50+
if (sepIdx === -1) {
51+
// The value is the simplest `type/subtype` variant.
52+
sepIdx = headerValue.indexOf('/')
53+
if (sepIdx === -1) {
54+
// Got a string without the correct `type/subtype` format.
55+
return
56+
}
57+
58+
const type = headerValue.slice(0, sepIdx).trimStart().toLowerCase()
59+
const subtype = headerValue.slice(sepIdx + 1).trimEnd().toLowerCase()
60+
61+
if (
62+
typeNameReg.test(type) === true &&
63+
subtypeNameReg.test(subtype) === true
64+
) {
65+
this.#valid = true
66+
this.#empty = false
67+
this.#type = type
68+
this.#subtype = subtype
69+
}
70+
4271
return
4372
}
44-
this.#type = matches[1].toLowerCase()
45-
this.#subtype = matches[2].toLowerCase()
4673

47-
this.#valid = true
48-
this.#empty = false
49-
if (!matches[3]) {
50-
// We don't need to parse the parameters because none were supplied.
74+
// We have a `type/subtype; params=list...` header value.
75+
const mediaType = headerValue.slice(0, sepIdx).toLowerCase()
76+
const paramsList = headerValue.slice(sepIdx + 1).trim()
77+
78+
sepIdx = mediaType.indexOf('/')
79+
if (sepIdx === -1) {
80+
// We got an invalid string like `something; params=list...`.
81+
return
82+
}
83+
const type = mediaType.slice(0, sepIdx).trimStart()
84+
const subtype = mediaType.slice(sepIdx + 1).trimEnd()
85+
86+
if (
87+
typeNameReg.test(type) === false ||
88+
subtypeNameReg.test(subtype) === false
89+
) {
90+
// Some portion of the media-type is using invalid characters. Therefore,
91+
// the content-type header is invalid.
5192
return
5293
}
94+
this.#type = type
95+
this.#subtype = subtype
96+
this.#valid = true
97+
this.#empty = false
5398

54-
const paramsString = matches[3]
55-
matches = keyValuePairsReg.exec(paramsString)
99+
let matches = keyValuePairsReg.exec(paramsList)
56100
while (matches) {
57101
const key = matches[1]
58102
const value = matches[2]
59103
if (value[0] === '"') {
60104
if (value.at(-1) !== '"') {
61105
this.#parameters.set(key, 'invalid quoted string')
62-
matches = keyValuePairsReg.exec(paramsString)
106+
matches = keyValuePairsReg.exec(paramsList)
63107
continue
64108
}
65109
// We should probably verify the value matches a quoted string
@@ -70,7 +114,7 @@ class ContentType {
70114
} else {
71115
this.#parameters.set(key, value)
72116
}
73-
matches = keyValuePairsReg.exec(paramsString)
117+
matches = keyValuePairsReg.exec(paramsList)
74118
}
75119
}
76120

test/content-type.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,18 @@ describe('ContentType class', () => {
6666
found = new ContentType('foo/ bar')
6767
t.assert.equal(found.isEmpty, true)
6868
t.assert.equal(found.isValid, false)
69+
70+
found = new ContentType('foo; param=1')
71+
t.assert.equal(found.isEmpty, true)
72+
t.assert.equal(found.isValid, false)
73+
74+
found = new ContentType('foo/π; param=1')
75+
t.assert.equal(found.isEmpty, true)
76+
t.assert.equal(found.isValid, false)
6977
})
7078

7179
test('returns a plain media type instance', (t) => {
7280
const found = new ContentType('Application/JSON')
73-
t.assert.equal(found.isEmpty, false)
7481
t.assert.equal(found.mediaType, 'application/json')
7582
t.assert.equal(found.type, 'application')
7683
t.assert.equal(found.subtype, 'json')

0 commit comments

Comments
 (0)