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