util.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. 'use strict'
  2. const {
  3. kState,
  4. kError,
  5. kResult,
  6. kAborted,
  7. kLastProgressEventFired
  8. } = require('./symbols')
  9. const { ProgressEvent } = require('./progressevent')
  10. const { getEncoding } = require('./encoding')
  11. const { serializeAMimeType, parseMIMEType } = require('../fetch/data-url')
  12. const { types } = require('node:util')
  13. const { StringDecoder } = require('string_decoder')
  14. const { btoa } = require('node:buffer')
  15. /** @type {PropertyDescriptor} */
  16. const staticPropertyDescriptors = {
  17. enumerable: true,
  18. writable: false,
  19. configurable: false
  20. }
  21. /**
  22. * @see https://w3c.github.io/FileAPI/#readOperation
  23. * @param {import('./filereader').FileReader} fr
  24. * @param {import('buffer').Blob} blob
  25. * @param {string} type
  26. * @param {string?} encodingName
  27. */
  28. function readOperation (fr, blob, type, encodingName) {
  29. // 1. If fr’s state is "loading", throw an InvalidStateError
  30. // DOMException.
  31. if (fr[kState] === 'loading') {
  32. throw new DOMException('Invalid state', 'InvalidStateError')
  33. }
  34. // 2. Set fr’s state to "loading".
  35. fr[kState] = 'loading'
  36. // 3. Set fr’s result to null.
  37. fr[kResult] = null
  38. // 4. Set fr’s error to null.
  39. fr[kError] = null
  40. // 5. Let stream be the result of calling get stream on blob.
  41. /** @type {import('stream/web').ReadableStream} */
  42. const stream = blob.stream()
  43. // 6. Let reader be the result of getting a reader from stream.
  44. const reader = stream.getReader()
  45. // 7. Let bytes be an empty byte sequence.
  46. /** @type {Uint8Array[]} */
  47. const bytes = []
  48. // 8. Let chunkPromise be the result of reading a chunk from
  49. // stream with reader.
  50. let chunkPromise = reader.read()
  51. // 9. Let isFirstChunk be true.
  52. let isFirstChunk = true
  53. // 10. In parallel, while true:
  54. // Note: "In parallel" just means non-blocking
  55. // Note 2: readOperation itself cannot be async as double
  56. // reading the body would then reject the promise, instead
  57. // of throwing an error.
  58. ;(async () => {
  59. while (!fr[kAborted]) {
  60. // 1. Wait for chunkPromise to be fulfilled or rejected.
  61. try {
  62. const { done, value } = await chunkPromise
  63. // 2. If chunkPromise is fulfilled, and isFirstChunk is
  64. // true, queue a task to fire a progress event called
  65. // loadstart at fr.
  66. if (isFirstChunk && !fr[kAborted]) {
  67. queueMicrotask(() => {
  68. fireAProgressEvent('loadstart', fr)
  69. })
  70. }
  71. // 3. Set isFirstChunk to false.
  72. isFirstChunk = false
  73. // 4. If chunkPromise is fulfilled with an object whose
  74. // done property is false and whose value property is
  75. // a Uint8Array object, run these steps:
  76. if (!done && types.isUint8Array(value)) {
  77. // 1. Let bs be the byte sequence represented by the
  78. // Uint8Array object.
  79. // 2. Append bs to bytes.
  80. bytes.push(value)
  81. // 3. If roughly 50ms have passed since these steps
  82. // were last invoked, queue a task to fire a
  83. // progress event called progress at fr.
  84. if (
  85. (
  86. fr[kLastProgressEventFired] === undefined ||
  87. Date.now() - fr[kLastProgressEventFired] >= 50
  88. ) &&
  89. !fr[kAborted]
  90. ) {
  91. fr[kLastProgressEventFired] = Date.now()
  92. queueMicrotask(() => {
  93. fireAProgressEvent('progress', fr)
  94. })
  95. }
  96. // 4. Set chunkPromise to the result of reading a
  97. // chunk from stream with reader.
  98. chunkPromise = reader.read()
  99. } else if (done) {
  100. // 5. Otherwise, if chunkPromise is fulfilled with an
  101. // object whose done property is true, queue a task
  102. // to run the following steps and abort this algorithm:
  103. queueMicrotask(() => {
  104. // 1. Set fr’s state to "done".
  105. fr[kState] = 'done'
  106. // 2. Let result be the result of package data given
  107. // bytes, type, blob’s type, and encodingName.
  108. try {
  109. const result = packageData(bytes, type, blob.type, encodingName)
  110. // 4. Else:
  111. if (fr[kAborted]) {
  112. return
  113. }
  114. // 1. Set fr’s result to result.
  115. fr[kResult] = result
  116. // 2. Fire a progress event called load at the fr.
  117. fireAProgressEvent('load', fr)
  118. } catch (error) {
  119. // 3. If package data threw an exception error:
  120. // 1. Set fr’s error to error.
  121. fr[kError] = error
  122. // 2. Fire a progress event called error at fr.
  123. fireAProgressEvent('error', fr)
  124. }
  125. // 5. If fr’s state is not "loading", fire a progress
  126. // event called loadend at the fr.
  127. if (fr[kState] !== 'loading') {
  128. fireAProgressEvent('loadend', fr)
  129. }
  130. })
  131. break
  132. }
  133. } catch (error) {
  134. if (fr[kAborted]) {
  135. return
  136. }
  137. // 6. Otherwise, if chunkPromise is rejected with an
  138. // error error, queue a task to run the following
  139. // steps and abort this algorithm:
  140. queueMicrotask(() => {
  141. // 1. Set fr’s state to "done".
  142. fr[kState] = 'done'
  143. // 2. Set fr’s error to error.
  144. fr[kError] = error
  145. // 3. Fire a progress event called error at fr.
  146. fireAProgressEvent('error', fr)
  147. // 4. If fr’s state is not "loading", fire a progress
  148. // event called loadend at fr.
  149. if (fr[kState] !== 'loading') {
  150. fireAProgressEvent('loadend', fr)
  151. }
  152. })
  153. break
  154. }
  155. }
  156. })()
  157. }
  158. /**
  159. * @see https://w3c.github.io/FileAPI/#fire-a-progress-event
  160. * @see https://dom.spec.whatwg.org/#concept-event-fire
  161. * @param {string} e The name of the event
  162. * @param {import('./filereader').FileReader} reader
  163. */
  164. function fireAProgressEvent (e, reader) {
  165. // The progress event e does not bubble. e.bubbles must be false
  166. // The progress event e is NOT cancelable. e.cancelable must be false
  167. const event = new ProgressEvent(e, {
  168. bubbles: false,
  169. cancelable: false
  170. })
  171. reader.dispatchEvent(event)
  172. }
  173. /**
  174. * @see https://w3c.github.io/FileAPI/#blob-package-data
  175. * @param {Uint8Array[]} bytes
  176. * @param {string} type
  177. * @param {string?} mimeType
  178. * @param {string?} encodingName
  179. */
  180. function packageData (bytes, type, mimeType, encodingName) {
  181. // 1. A Blob has an associated package data algorithm, given
  182. // bytes, a type, a optional mimeType, and a optional
  183. // encodingName, which switches on type and runs the
  184. // associated steps:
  185. switch (type) {
  186. case 'DataURL': {
  187. // 1. Return bytes as a DataURL [RFC2397] subject to
  188. // the considerations below:
  189. // * Use mimeType as part of the Data URL if it is
  190. // available in keeping with the Data URL
  191. // specification [RFC2397].
  192. // * If mimeType is not available return a Data URL
  193. // without a media-type. [RFC2397].
  194. // https://datatracker.ietf.org/doc/html/rfc2397#section-3
  195. // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data
  196. // mediatype := [ type "/" subtype ] *( ";" parameter )
  197. // data := *urlchar
  198. // parameter := attribute "=" value
  199. let dataURL = 'data:'
  200. const parsed = parseMIMEType(mimeType || 'application/octet-stream')
  201. if (parsed !== 'failure') {
  202. dataURL += serializeAMimeType(parsed)
  203. }
  204. dataURL += ';base64,'
  205. const decoder = new StringDecoder('latin1')
  206. for (const chunk of bytes) {
  207. dataURL += btoa(decoder.write(chunk))
  208. }
  209. dataURL += btoa(decoder.end())
  210. return dataURL
  211. }
  212. case 'Text': {
  213. // 1. Let encoding be failure
  214. let encoding = 'failure'
  215. // 2. If the encodingName is present, set encoding to the
  216. // result of getting an encoding from encodingName.
  217. if (encodingName) {
  218. encoding = getEncoding(encodingName)
  219. }
  220. // 3. If encoding is failure, and mimeType is present:
  221. if (encoding === 'failure' && mimeType) {
  222. // 1. Let type be the result of parse a MIME type
  223. // given mimeType.
  224. const type = parseMIMEType(mimeType)
  225. // 2. If type is not failure, set encoding to the result
  226. // of getting an encoding from type’s parameters["charset"].
  227. if (type !== 'failure') {
  228. encoding = getEncoding(type.parameters.get('charset'))
  229. }
  230. }
  231. // 4. If encoding is failure, then set encoding to UTF-8.
  232. if (encoding === 'failure') {
  233. encoding = 'UTF-8'
  234. }
  235. // 5. Decode bytes using fallback encoding encoding, and
  236. // return the result.
  237. return decode(bytes, encoding)
  238. }
  239. case 'ArrayBuffer': {
  240. // Return a new ArrayBuffer whose contents are bytes.
  241. const sequence = combineByteSequences(bytes)
  242. return sequence.buffer
  243. }
  244. case 'BinaryString': {
  245. // Return bytes as a binary string, in which every byte
  246. // is represented by a code unit of equal value [0..255].
  247. let binaryString = ''
  248. const decoder = new StringDecoder('latin1')
  249. for (const chunk of bytes) {
  250. binaryString += decoder.write(chunk)
  251. }
  252. binaryString += decoder.end()
  253. return binaryString
  254. }
  255. }
  256. }
  257. /**
  258. * @see https://encoding.spec.whatwg.org/#decode
  259. * @param {Uint8Array[]} ioQueue
  260. * @param {string} encoding
  261. */
  262. function decode (ioQueue, encoding) {
  263. const bytes = combineByteSequences(ioQueue)
  264. // 1. Let BOMEncoding be the result of BOM sniffing ioQueue.
  265. const BOMEncoding = BOMSniffing(bytes)
  266. let slice = 0
  267. // 2. If BOMEncoding is non-null:
  268. if (BOMEncoding !== null) {
  269. // 1. Set encoding to BOMEncoding.
  270. encoding = BOMEncoding
  271. // 2. Read three bytes from ioQueue, if BOMEncoding is
  272. // UTF-8; otherwise read two bytes.
  273. // (Do nothing with those bytes.)
  274. slice = BOMEncoding === 'UTF-8' ? 3 : 2
  275. }
  276. // 3. Process a queue with an instance of encoding’s
  277. // decoder, ioQueue, output, and "replacement".
  278. // 4. Return output.
  279. const sliced = bytes.slice(slice)
  280. return new TextDecoder(encoding).decode(sliced)
  281. }
  282. /**
  283. * @see https://encoding.spec.whatwg.org/#bom-sniff
  284. * @param {Uint8Array} ioQueue
  285. */
  286. function BOMSniffing (ioQueue) {
  287. // 1. Let BOM be the result of peeking 3 bytes from ioQueue,
  288. // converted to a byte sequence.
  289. const [a, b, c] = ioQueue
  290. // 2. For each of the rows in the table below, starting with
  291. // the first one and going down, if BOM starts with the
  292. // bytes given in the first column, then return the
  293. // encoding given in the cell in the second column of that
  294. // row. Otherwise, return null.
  295. if (a === 0xEF && b === 0xBB && c === 0xBF) {
  296. return 'UTF-8'
  297. } else if (a === 0xFE && b === 0xFF) {
  298. return 'UTF-16BE'
  299. } else if (a === 0xFF && b === 0xFE) {
  300. return 'UTF-16LE'
  301. }
  302. return null
  303. }
  304. /**
  305. * @param {Uint8Array[]} sequences
  306. */
  307. function combineByteSequences (sequences) {
  308. const size = sequences.reduce((a, b) => {
  309. return a + b.byteLength
  310. }, 0)
  311. let offset = 0
  312. return sequences.reduce((a, b) => {
  313. a.set(b, offset)
  314. offset += b.byteLength
  315. return a
  316. }, new Uint8Array(size))
  317. }
  318. module.exports = {
  319. staticPropertyDescriptors,
  320. readOperation,
  321. fireAProgressEvent
  322. }