util.js 11 KB

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