index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. if (match.index !== index) {
  141. throw new TypeError('invalid parameter format')
  142. }
  143. index += match[0].length
  144. key = match[1].toLowerCase()
  145. value = match[2]
  146. if (value[0] === '"') {
  147. // remove quotes and escapes
  148. value = value
  149. .substr(1, value.length - 2)
  150. .replace(qescRegExp, '$1')
  151. }
  152. params[key] = value
  153. }
  154. if (index !== -1 && index !== string.length) {
  155. throw new TypeError('invalid parameter format')
  156. }
  157. obj.parameters = params
  158. return obj
  159. }
  160. /**
  161. * Get content-type from req/res objects.
  162. *
  163. * @param {object}
  164. * @return {Object}
  165. * @api private
  166. */
  167. function getcontenttype(obj) {
  168. if (typeof obj.getHeader === 'function') {
  169. // res-like
  170. return obj.getHeader('content-type')
  171. }
  172. if (typeof obj.headers === 'object') {
  173. // req-like
  174. return obj.headers && obj.headers['content-type']
  175. }
  176. }
  177. /**
  178. * Quote a string if necessary.
  179. *
  180. * @param {string} val
  181. * @return {string}
  182. * @api private
  183. */
  184. function qstring(val) {
  185. var str = String(val)
  186. // no need to quote tokens
  187. if (tokenRegExp.test(str)) {
  188. return str
  189. }
  190. if (str.length > 0 && !textRegExp.test(str)) {
  191. throw new TypeError('invalid parameter value')
  192. }
  193. return '"' + str.replace(quoteRegExp, '\\$1') + '"'
  194. }
  195. /**
  196. * Simply "type/subtype+siffx" into parts.
  197. *
  198. * @param {string} string
  199. * @return {Object}
  200. * @api private
  201. */
  202. function splitType(string) {
  203. var match = typeRegExp.exec(string.toLowerCase())
  204. if (!match) {
  205. throw new TypeError('invalid media type')
  206. }
  207. var type = match[1]
  208. var subtype = match[2]
  209. var suffix
  210. // suffix after last +
  211. var index = subtype.lastIndexOf('+')
  212. if (index !== -1) {
  213. suffix = subtype.substr(index + 1)
  214. subtype = subtype.substr(0, index)
  215. }
  216. var obj = {
  217. type: type,
  218. subtype: subtype,
  219. suffix: suffix
  220. }
  221. return obj
  222. }