1 | /*!
|
---|
2 | * content-type
|
---|
3 | * Copyright(c) 2015 Douglas Christopher Wilson
|
---|
4 | * MIT Licensed
|
---|
5 | */
|
---|
6 |
|
---|
7 | 'use strict'
|
---|
8 |
|
---|
9 | /**
|
---|
10 | * RegExp to match *( ";" parameter ) in RFC 7231 sec 3.1.1.1
|
---|
11 | *
|
---|
12 | * parameter = token "=" ( token / quoted-string )
|
---|
13 | * token = 1*tchar
|
---|
14 | * tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
|
---|
15 | * / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
|
---|
16 | * / DIGIT / ALPHA
|
---|
17 | * ; any VCHAR, except delimiters
|
---|
18 | * quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
|
---|
19 | * qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text
|
---|
20 | * obs-text = %x80-FF
|
---|
21 | * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
---|
22 | */
|
---|
23 | var PARAM_REGEXP = /; *([!#$%&'*+.^_`|~0-9A-Za-z-]+) *= *("(?:[\u000b\u0020\u0021\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u000b\u0020-\u00ff])*"|[!#$%&'*+.^_`|~0-9A-Za-z-]+) */g // eslint-disable-line no-control-regex
|
---|
24 | var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/ // eslint-disable-line no-control-regex
|
---|
25 | var TOKEN_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
---|
26 |
|
---|
27 | /**
|
---|
28 | * RegExp to match quoted-pair in RFC 7230 sec 3.2.6
|
---|
29 | *
|
---|
30 | * quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
|
---|
31 | * obs-text = %x80-FF
|
---|
32 | */
|
---|
33 | var QESC_REGEXP = /\\([\u000b\u0020-\u00ff])/g // eslint-disable-line no-control-regex
|
---|
34 |
|
---|
35 | /**
|
---|
36 | * RegExp to match chars that must be quoted-pair in RFC 7230 sec 3.2.6
|
---|
37 | */
|
---|
38 | var QUOTE_REGEXP = /([\\"])/g
|
---|
39 |
|
---|
40 | /**
|
---|
41 | * RegExp to match type in RFC 7231 sec 3.1.1.1
|
---|
42 | *
|
---|
43 | * media-type = type "/" subtype
|
---|
44 | * type = token
|
---|
45 | * subtype = token
|
---|
46 | */
|
---|
47 | var TYPE_REGEXP = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+\/[!#$%&'*+.^_`|~0-9A-Za-z-]+$/
|
---|
48 |
|
---|
49 | /**
|
---|
50 | * Module exports.
|
---|
51 | * @public
|
---|
52 | */
|
---|
53 |
|
---|
54 | exports.format = format
|
---|
55 | exports.parse = parse
|
---|
56 |
|
---|
57 | /**
|
---|
58 | * Format object to media type.
|
---|
59 | *
|
---|
60 | * @param {object} obj
|
---|
61 | * @return {string}
|
---|
62 | * @public
|
---|
63 | */
|
---|
64 |
|
---|
65 | function format (obj) {
|
---|
66 | if (!obj || typeof obj !== 'object') {
|
---|
67 | throw new TypeError('argument obj is required')
|
---|
68 | }
|
---|
69 |
|
---|
70 | var parameters = obj.parameters
|
---|
71 | var type = obj.type
|
---|
72 |
|
---|
73 | if (!type || !TYPE_REGEXP.test(type)) {
|
---|
74 | throw new TypeError('invalid type')
|
---|
75 | }
|
---|
76 |
|
---|
77 | var string = type
|
---|
78 |
|
---|
79 | // append parameters
|
---|
80 | if (parameters && typeof parameters === 'object') {
|
---|
81 | var param
|
---|
82 | var params = Object.keys(parameters).sort()
|
---|
83 |
|
---|
84 | for (var i = 0; i < params.length; i++) {
|
---|
85 | param = params[i]
|
---|
86 |
|
---|
87 | if (!TOKEN_REGEXP.test(param)) {
|
---|
88 | throw new TypeError('invalid parameter name')
|
---|
89 | }
|
---|
90 |
|
---|
91 | string += '; ' + param + '=' + qstring(parameters[param])
|
---|
92 | }
|
---|
93 | }
|
---|
94 |
|
---|
95 | return string
|
---|
96 | }
|
---|
97 |
|
---|
98 | /**
|
---|
99 | * Parse media type to object.
|
---|
100 | *
|
---|
101 | * @param {string|object} string
|
---|
102 | * @return {Object}
|
---|
103 | * @public
|
---|
104 | */
|
---|
105 |
|
---|
106 | function parse (string) {
|
---|
107 | if (!string) {
|
---|
108 | throw new TypeError('argument string is required')
|
---|
109 | }
|
---|
110 |
|
---|
111 | // support req/res-like objects as argument
|
---|
112 | var header = typeof string === 'object'
|
---|
113 | ? getcontenttype(string)
|
---|
114 | : string
|
---|
115 |
|
---|
116 | if (typeof header !== 'string') {
|
---|
117 | throw new TypeError('argument string is required to be a string')
|
---|
118 | }
|
---|
119 |
|
---|
120 | var index = header.indexOf(';')
|
---|
121 | var type = index !== -1
|
---|
122 | ? header.slice(0, index).trim()
|
---|
123 | : header.trim()
|
---|
124 |
|
---|
125 | if (!TYPE_REGEXP.test(type)) {
|
---|
126 | throw new TypeError('invalid media type')
|
---|
127 | }
|
---|
128 |
|
---|
129 | var obj = new ContentType(type.toLowerCase())
|
---|
130 |
|
---|
131 | // parse parameters
|
---|
132 | if (index !== -1) {
|
---|
133 | var key
|
---|
134 | var match
|
---|
135 | var value
|
---|
136 |
|
---|
137 | PARAM_REGEXP.lastIndex = index
|
---|
138 |
|
---|
139 | while ((match = PARAM_REGEXP.exec(header))) {
|
---|
140 | if (match.index !== index) {
|
---|
141 | throw new TypeError('invalid parameter format')
|
---|
142 | }
|
---|
143 |
|
---|
144 | index += match[0].length
|
---|
145 | key = match[1].toLowerCase()
|
---|
146 | value = match[2]
|
---|
147 |
|
---|
148 | if (value.charCodeAt(0) === 0x22 /* " */) {
|
---|
149 | // remove quotes
|
---|
150 | value = value.slice(1, -1)
|
---|
151 |
|
---|
152 | // remove escapes
|
---|
153 | if (value.indexOf('\\') !== -1) {
|
---|
154 | value = value.replace(QESC_REGEXP, '$1')
|
---|
155 | }
|
---|
156 | }
|
---|
157 |
|
---|
158 | obj.parameters[key] = value
|
---|
159 | }
|
---|
160 |
|
---|
161 | if (index !== header.length) {
|
---|
162 | throw new TypeError('invalid parameter format')
|
---|
163 | }
|
---|
164 | }
|
---|
165 |
|
---|
166 | return obj
|
---|
167 | }
|
---|
168 |
|
---|
169 | /**
|
---|
170 | * Get content-type from req/res objects.
|
---|
171 | *
|
---|
172 | * @param {object}
|
---|
173 | * @return {Object}
|
---|
174 | * @private
|
---|
175 | */
|
---|
176 |
|
---|
177 | function getcontenttype (obj) {
|
---|
178 | var header
|
---|
179 |
|
---|
180 | if (typeof obj.getHeader === 'function') {
|
---|
181 | // res-like
|
---|
182 | header = obj.getHeader('content-type')
|
---|
183 | } else if (typeof obj.headers === 'object') {
|
---|
184 | // req-like
|
---|
185 | header = obj.headers && obj.headers['content-type']
|
---|
186 | }
|
---|
187 |
|
---|
188 | if (typeof header !== 'string') {
|
---|
189 | throw new TypeError('content-type header is missing from object')
|
---|
190 | }
|
---|
191 |
|
---|
192 | return header
|
---|
193 | }
|
---|
194 |
|
---|
195 | /**
|
---|
196 | * Quote a string if necessary.
|
---|
197 | *
|
---|
198 | * @param {string} val
|
---|
199 | * @return {string}
|
---|
200 | * @private
|
---|
201 | */
|
---|
202 |
|
---|
203 | function qstring (val) {
|
---|
204 | var str = String(val)
|
---|
205 |
|
---|
206 | // no need to quote tokens
|
---|
207 | if (TOKEN_REGEXP.test(str)) {
|
---|
208 | return str
|
---|
209 | }
|
---|
210 |
|
---|
211 | if (str.length > 0 && !TEXT_REGEXP.test(str)) {
|
---|
212 | throw new TypeError('invalid parameter value')
|
---|
213 | }
|
---|
214 |
|
---|
215 | return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
|
---|
216 | }
|
---|
217 |
|
---|
218 | /**
|
---|
219 | * Class to represent a content type.
|
---|
220 | * @private
|
---|
221 | */
|
---|
222 | function ContentType (type) {
|
---|
223 | this.parameters = Object.create(null)
|
---|
224 | this.type = type
|
---|
225 | }
|
---|