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 // 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  }