second
[josuexyz/.git] / node_modules / content-type / index.js
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
24 var TEXT_REGEXP = /^[\u000b\u0020-\u007e\u0080-\u00ff]+$/
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
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.substr(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[0] === '"') {
149         // remove quotes and escapes
150         value = value
151           .substr(1, value.length - 2)
152           .replace(QESC_REGEXP, '$1')
153       }
154
155       obj.parameters[key] = value
156     }
157
158     if (index !== header.length) {
159       throw new TypeError('invalid parameter format')
160     }
161   }
162
163   return obj
164 }
165
166 /**
167  * Get content-type from req/res objects.
168  *
169  * @param {object}
170  * @return {Object}
171  * @private
172  */
173
174 function getcontenttype (obj) {
175   var header
176
177   if (typeof obj.getHeader === 'function') {
178     // res-like
179     header = obj.getHeader('content-type')
180   } else if (typeof obj.headers === 'object') {
181     // req-like
182     header = obj.headers && obj.headers['content-type']
183   }
184
185   if (typeof header !== 'string') {
186     throw new TypeError('content-type header is missing from object')
187   }
188
189   return header
190 }
191
192 /**
193  * Quote a string if necessary.
194  *
195  * @param {string} val
196  * @return {string}
197  * @private
198  */
199
200 function qstring (val) {
201   var str = String(val)
202
203   // no need to quote tokens
204   if (TOKEN_REGEXP.test(str)) {
205     return str
206   }
207
208   if (str.length > 0 && !TEXT_REGEXP.test(str)) {
209     throw new TypeError('invalid parameter value')
210   }
211
212   return '"' + str.replace(QUOTE_REGEXP, '\\$1') + '"'
213 }
214
215 /**
216  * Class to represent a content type.
217  * @private
218  */
219 function ContentType (type) {
220   this.parameters = Object.create(null)
221   this.type = type
222 }