index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. 'use strict'
  2. const assert = require('node:assert')
  3. const { utf8DecodeBytes } = require('../../encoding')
  4. /**
  5. * @param {(char: string) => boolean} condition
  6. * @param {string} input
  7. * @param {{ position: number }} position
  8. * @returns {string}
  9. *
  10. * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
  11. */
  12. function collectASequenceOfCodePoints (condition, input, position) {
  13. // 1. Let result be the empty string.
  14. let result = ''
  15. // 2. While position doesn’t point past the end of input and the
  16. // code point at position within input meets the condition condition:
  17. while (position.position < input.length && condition(input[position.position])) {
  18. // 1. Append that code point to the end of result.
  19. result += input[position.position]
  20. // 2. Advance position by 1.
  21. position.position++
  22. }
  23. // 3. Return result.
  24. return result
  25. }
  26. /**
  27. * A faster collectASequenceOfCodePoints that only works when comparing a single character.
  28. * @param {string} char
  29. * @param {string} input
  30. * @param {{ position: number }} position
  31. * @returns {string}
  32. *
  33. * @see https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
  34. */
  35. function collectASequenceOfCodePointsFast (char, input, position) {
  36. const idx = input.indexOf(char, position.position)
  37. const start = position.position
  38. if (idx === -1) {
  39. position.position = input.length
  40. return input.slice(start)
  41. }
  42. position.position = idx
  43. return input.slice(start, position.position)
  44. }
  45. const ASCII_WHITESPACE_REPLACE_REGEX = /[\u0009\u000A\u000C\u000D\u0020]/g // eslint-disable-line no-control-regex
  46. /**
  47. * @param {string} data
  48. * @returns {Uint8Array | 'failure'}
  49. *
  50. * @see https://infra.spec.whatwg.org/#forgiving-base64-decode
  51. */
  52. function forgivingBase64 (data) {
  53. // 1. Remove all ASCII whitespace from data.
  54. data = data.replace(ASCII_WHITESPACE_REPLACE_REGEX, '')
  55. let dataLength = data.length
  56. // 2. If data’s code point length divides by 4 leaving
  57. // no remainder, then:
  58. if (dataLength % 4 === 0) {
  59. // 1. If data ends with one or two U+003D (=) code points,
  60. // then remove them from data.
  61. if (data.charCodeAt(dataLength - 1) === 0x003D) {
  62. --dataLength
  63. if (data.charCodeAt(dataLength - 1) === 0x003D) {
  64. --dataLength
  65. }
  66. }
  67. }
  68. // 3. If data’s code point length divides by 4 leaving
  69. // a remainder of 1, then return failure.
  70. if (dataLength % 4 === 1) {
  71. return 'failure'
  72. }
  73. // 4. If data contains a code point that is not one of
  74. // U+002B (+)
  75. // U+002F (/)
  76. // ASCII alphanumeric
  77. // then return failure.
  78. if (/[^+/0-9A-Za-z]/.test(data.length === dataLength ? data : data.substring(0, dataLength))) {
  79. return 'failure'
  80. }
  81. const buffer = Buffer.from(data, 'base64')
  82. return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  83. }
  84. /**
  85. * @param {number} char
  86. * @returns {boolean}
  87. *
  88. * @see https://infra.spec.whatwg.org/#ascii-whitespace
  89. */
  90. function isASCIIWhitespace (char) {
  91. return (
  92. char === 0x09 || // \t
  93. char === 0x0a || // \n
  94. char === 0x0c || // \f
  95. char === 0x0d || // \r
  96. char === 0x20 // space
  97. )
  98. }
  99. /**
  100. * @param {Uint8Array} input
  101. * @returns {string}
  102. *
  103. * @see https://infra.spec.whatwg.org/#isomorphic-decode
  104. */
  105. function isomorphicDecode (input) {
  106. // 1. To isomorphic decode a byte sequence input, return a string whose code point
  107. // length is equal to input’s length and whose code points have the same values
  108. // as the values of input’s bytes, in the same order.
  109. const length = input.length
  110. if ((2 << 15) - 1 > length) {
  111. return String.fromCharCode.apply(null, input)
  112. }
  113. let result = ''
  114. let i = 0
  115. let addition = (2 << 15) - 1
  116. while (i < length) {
  117. if (i + addition > length) {
  118. addition = length - i
  119. }
  120. result += String.fromCharCode.apply(null, input.subarray(i, i += addition))
  121. }
  122. return result
  123. }
  124. const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line no-control-regex
  125. /**
  126. * @param {string} input
  127. * @returns {string}
  128. *
  129. * @see https://infra.spec.whatwg.org/#isomorphic-encode
  130. */
  131. function isomorphicEncode (input) {
  132. // 1. Assert: input contains no code points greater than U+00FF.
  133. assert(!invalidIsomorphicEncodeValueRegex.test(input))
  134. // 2. Return a byte sequence whose length is equal to input’s code
  135. // point length and whose bytes have the same values as the
  136. // values of input’s code points, in the same order
  137. return input
  138. }
  139. /**
  140. * @see https://infra.spec.whatwg.org/#parse-json-bytes-to-a-javascript-value
  141. * @param {Uint8Array} bytes
  142. */
  143. function parseJSONFromBytes (bytes) {
  144. return JSON.parse(utf8DecodeBytes(bytes))
  145. }
  146. /**
  147. * @param {string} str
  148. * @param {boolean} [leading=true]
  149. * @param {boolean} [trailing=true]
  150. * @returns {string}
  151. *
  152. * @see https://infra.spec.whatwg.org/#strip-leading-and-trailing-ascii-whitespace
  153. */
  154. function removeASCIIWhitespace (str, leading = true, trailing = true) {
  155. return removeChars(str, leading, trailing, isASCIIWhitespace)
  156. }
  157. /**
  158. * @param {string} str
  159. * @param {boolean} leading
  160. * @param {boolean} trailing
  161. * @param {(charCode: number) => boolean} predicate
  162. * @returns {string}
  163. */
  164. function removeChars (str, leading, trailing, predicate) {
  165. let lead = 0
  166. let trail = str.length - 1
  167. if (leading) {
  168. while (lead < str.length && predicate(str.charCodeAt(lead))) lead++
  169. }
  170. if (trailing) {
  171. while (trail > 0 && predicate(str.charCodeAt(trail))) trail--
  172. }
  173. return lead === 0 && trail === str.length - 1 ? str : str.slice(lead, trail + 1)
  174. }
  175. // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
  176. function serializeJavascriptValueToJSONString (value) {
  177. // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
  178. const result = JSON.stringify(value)
  179. // 2. If result is undefined, then throw a TypeError.
  180. if (result === undefined) {
  181. throw new TypeError('Value is not JSON serializable')
  182. }
  183. // 3. Assert: result is a string.
  184. assert(typeof result === 'string')
  185. // 4. Return result.
  186. return result
  187. }
  188. module.exports = {
  189. collectASequenceOfCodePoints,
  190. collectASequenceOfCodePointsFast,
  191. forgivingBase64,
  192. isASCIIWhitespace,
  193. isomorphicDecode,
  194. isomorphicEncode,
  195. parseJSONFromBytes,
  196. removeASCIIWhitespace,
  197. removeChars,
  198. serializeJavascriptValueToJSONString
  199. }