index.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. /*!
  2. * media-typer
  3. * Copyright(c) 2014 Douglas Christopher Wilson
  4. * MIT Licensed
  5. */
  6. /**
  7. * RegExp to match *( ";" parameter ) in RFC 2616 sec 3.7
  8. *
  9. * parameter = token "=" ( token | quoted-string )
  10. * token = 1*<any CHAR except CTLs or separators>
  11. * separators = "(" | ")" | "<" | ">" | "@"
  12. * | "," | ";" | ":" | "\" | <">
  13. * | "/" | "[" | "]" | "?" | "="
  14. * | "{" | "}" | SP | HT
  15. * quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
  16. * qdtext = <any TEXT except <">>
  17. * quoted-pair = "\" CHAR
  18. * CHAR = <any US-ASCII character (octets 0 - 127)>
  19. * TEXT = <any OCTET except CTLs, but including LWS>
  20. * LWS = [CRLF] 1*( SP | HT )
  21. * CRLF = CR LF
  22. * CR = <US-ASCII CR, carriage return (13)>
  23. * LF = <US-ASCII LF, linefeed (10)>
  24. * SP = <US-ASCII SP, space (32)>
  25. * SHT = <US-ASCII HT, horizontal-tab (9)>
  26. * CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
  27. * OCTET = <any 8-bit sequence of data>
  28. */
  29. var paramRegExp = /; *([!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) *= *("(?:[ !\u0023-\u005b\u005d-\u007e\u0080-\u00ff]|\\[\u0020-\u007e])*"|[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+) */g;
  30. var textRegExp = /^[\u0020-\u007e\u0080-\u00ff]+$/
  31. var tokenRegExp = /^[!#$%&'\*\+\-\.0-9A-Z\^_`a-z\|~]+$/
  32. /**
  33. * RegExp to match quoted-pair in RFC 2616
  34. *
  35. * quoted-pair = "\" CHAR
  36. * CHAR = <any US-ASCII character (octets 0 - 127)>
  37. */
  38. var qescRegExp = /\\([\u0000-\u007f])/g;
  39. /**
  40. * RegExp to match chars that must be quoted-pair in RFC 2616
  41. */
  42. var quoteRegExp = /([\\"])/g;
  43. /**
  44. * RegExp to match type in RFC 6838
  45. *
  46. * type-name = restricted-name
  47. * subtype-name = restricted-name
  48. * restricted-name = restricted-name-first *126restricted-name-chars
  49. * restricted-name-first = ALPHA / DIGIT
  50. * restricted-name-chars = ALPHA / DIGIT / "!" / "#" /
  51. * "$" / "&" / "-" / "^" / "_"
  52. * restricted-name-chars =/ "." ; Characters before first dot always
  53. * ; specify a facet name
  54. * restricted-name-chars =/ "+" ; Characters after last plus always
  55. * ; specify a structured syntax suffix
  56. * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
  57. * DIGIT = %x30-39 ; 0-9
  58. */
  59. var subtypeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.-]{0,126}$/
  60. var typeNameRegExp = /^[A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126}$/
  61. var typeRegExp = /^ *([A-Za-z0-9][A-Za-z0-9!#$&^_-]{0,126})\/([A-Za-z0-9][A-Za-z0-9!#$&^_.+-]{0,126}) *$/;
  62. /**
  63. * Module exports.
  64. */
  65. exports.format = format
  66. exports.parse = parse
  67. /**
  68. * Format object to media type.
  69. *
  70. * @param {object} obj
  71. * @return {string}
  72. * @api public
  73. */
  74. function format(obj) {
  75. if (!obj || typeof obj !== 'object') {
  76. throw new TypeError('argument obj is required')
  77. }
  78. var parameters = obj.parameters
  79. var subtype = obj.subtype
  80. var suffix = obj.suffix
  81. var type = obj.type
  82. if (!type || !typeNameRegExp.test(type)) {
  83. throw new TypeError('invalid type')
  84. }
  85. if (!subtype || !subtypeNameRegExp.test(subtype)) {
  86. throw new TypeError('invalid subtype')
  87. }
  88. // format as type/subtype
  89. var string = type + '/' + subtype
  90. // append +suffix
  91. if (suffix) {
  92. if (!typeNameRegExp.test(suffix)) {
  93. throw new TypeError('invalid suffix')
  94. }
  95. string += '+' + suffix
  96. }
  97. // append parameters
  98. if (parameters && typeof parameters === 'object') {
  99. var param
  100. var params = Object.keys(parameters).sort()
  101. for (var i = 0; i < params.length; i++) {
  102. param = params[i]
  103. if (!tokenRegExp.test(param)) {
  104. throw new TypeError('invalid parameter name')
  105. }
  106. string += '; ' + param + '=' + qstring(parameters[param])
  107. }
  108. }
  109. return string
  110. }
  111. /**
  112. * Parse media type to object.
  113. *
  114. * @param {string|object} string
  115. * @return {Object}
  116. * @api public
  117. */
  118. function parse(string) {
  119. if (!string) {
  120. throw new TypeError('argument string is required')
  121. }
  122. // support req/res-like objects as argument
  123. if (typeof string === 'object') {
  124. string = getcontenttype(string)
  125. }
  126. if (typeof string !== 'string') {
  127. throw new TypeError('argument string is required to be a string')
  128. }
  129. var index = string.indexOf(';')
  130. var type = index !== -1
  131. ? string.substr(0, index)
  132. : string
  133. var key
  134. var match
  135. var obj = splitType(type)
  136. var params = {}
  137. var value
  138. paramRegExp.lastIndex = index
  139. while (match = paramRegExp.exec(string)) {
  140. key = match[1].toLowerCase()
  141. value = match[2]
  142. if (value[0] === '"') {
  143. // remove quotes and escapes
  144. value = value
  145. .substr(1, value.length - 2)
  146. .replace(qescRegExp, '$1')
  147. }
  148. params[key] = value
  149. }
  150. obj.parameters = params
  151. return obj
  152. }
  153. /**
  154. * Get content-type from req/res objects.
  155. *
  156. * @param {object}
  157. * @return {Object}
  158. * @api private
  159. */
  160. function getcontenttype(obj) {
  161. if (typeof obj.getHeader === 'function') {
  162. // res-like
  163. return obj.getHeader('content-type')
  164. }
  165. if (typeof obj.headers === 'object') {
  166. // req-like
  167. return obj.headers && obj.headers['content-type']
  168. }
  169. }
  170. /**
  171. * Quote a string if necessary.
  172. *
  173. * @param {string} val
  174. * @return {string}
  175. * @api private
  176. */
  177. function qstring(val) {
  178. var str = String(val)
  179. // no need to quote tokens
  180. if (tokenRegExp.test(str)) {
  181. return str
  182. }
  183. if (str.length > 0 && !textRegExp.test(str)) {
  184. throw new TypeError('invalid parameter value')
  185. }
  186. return '"' + str.replace(quoteRegExp, '\\$1') + '"'
  187. }
  188. /**
  189. * Simply "type/subtype+siffx" into parts.
  190. *
  191. * @param {string} string
  192. * @return {Object}
  193. * @api private
  194. */
  195. function splitType(string) {
  196. var match = typeRegExp.exec(string.toLowerCase())
  197. if (!match) {
  198. throw new TypeError('invalid media type')
  199. }
  200. var type = match[1]
  201. var subtype = match[2]
  202. var suffix
  203. // suffix after last +
  204. var index = subtype.lastIndexOf('+')
  205. if (index !== -1) {
  206. suffix = subtype.substr(index + 1)
  207. subtype = subtype.substr(0, index)
  208. }
  209. var obj = {
  210. type: type,
  211. subtype: subtype,
  212. suffix: suffix
  213. }
  214. return obj
  215. }