| 'use strict' |
| |
| const { |
| kState, |
| kError, |
| kResult, |
| kAborted, |
| kLastProgressEventFired |
| } = require('./symbols') |
| const { ProgressEvent } = require('./progressevent') |
| const { getEncoding } = require('./encoding') |
| const { DOMException } = require('../fetch/constants') |
| const { serializeAMimeType, parseMIMEType } = require('../fetch/dataURL') |
| const { types } = require('util') |
| const { StringDecoder } = require('string_decoder') |
| const { btoa } = require('buffer') |
| |
| /** @type {PropertyDescriptor} */ |
| const staticPropertyDescriptors = { |
| enumerable: true, |
| writable: false, |
| configurable: false |
| } |
| |
| /** |
| * @see https://w3c.github.io/FileAPI/#readOperation |
| * @param {import('./filereader').FileReader} fr |
| * @param {import('buffer').Blob} blob |
| * @param {string} type |
| * @param {string?} encodingName |
| */ |
| function readOperation (fr, blob, type, encodingName) { |
| // 1. If fr’s state is "loading", throw an InvalidStateError |
| // DOMException. |
| if (fr[kState] === 'loading') { |
| throw new DOMException('Invalid state', 'InvalidStateError') |
| } |
| |
| // 2. Set fr’s state to "loading". |
| fr[kState] = 'loading' |
| |
| // 3. Set fr’s result to null. |
| fr[kResult] = null |
| |
| // 4. Set fr’s error to null. |
| fr[kError] = null |
| |
| // 5. Let stream be the result of calling get stream on blob. |
| /** @type {import('stream/web').ReadableStream} */ |
| const stream = blob.stream() |
| |
| // 6. Let reader be the result of getting a reader from stream. |
| const reader = stream.getReader() |
| |
| // 7. Let bytes be an empty byte sequence. |
| /** @type {Uint8Array[]} */ |
| const bytes = [] |
| |
| // 8. Let chunkPromise be the result of reading a chunk from |
| // stream with reader. |
| let chunkPromise = reader.read() |
| |
| // 9. Let isFirstChunk be true. |
| let isFirstChunk = true |
| |
| // 10. In parallel, while true: |
| // Note: "In parallel" just means non-blocking |
| // Note 2: readOperation itself cannot be async as double |
| // reading the body would then reject the promise, instead |
| // of throwing an error. |
| ;(async () => { |
| while (!fr[kAborted]) { |
| // 1. Wait for chunkPromise to be fulfilled or rejected. |
| try { |
| const { done, value } = await chunkPromise |
| |
| // 2. If chunkPromise is fulfilled, and isFirstChunk is |
| // true, queue a task to fire a progress event called |
| // loadstart at fr. |
| if (isFirstChunk && !fr[kAborted]) { |
| queueMicrotask(() => { |
| fireAProgressEvent('loadstart', fr) |
| }) |
| } |
| |
| // 3. Set isFirstChunk to false. |
| isFirstChunk = false |
| |
| // 4. If chunkPromise is fulfilled with an object whose |
| // done property is false and whose value property is |
| // a Uint8Array object, run these steps: |
| if (!done && types.isUint8Array(value)) { |
| // 1. Let bs be the byte sequence represented by the |
| // Uint8Array object. |
| |
| // 2. Append bs to bytes. |
| bytes.push(value) |
| |
| // 3. If roughly 50ms have passed since these steps |
| // were last invoked, queue a task to fire a |
| // progress event called progress at fr. |
| if ( |
| ( |
| fr[kLastProgressEventFired] === undefined || |
| Date.now() - fr[kLastProgressEventFired] >= 50 |
| ) && |
| !fr[kAborted] |
| ) { |
| fr[kLastProgressEventFired] = Date.now() |
| queueMicrotask(() => { |
| fireAProgressEvent('progress', fr) |
| }) |
| } |
| |
| // 4. Set chunkPromise to the result of reading a |
| // chunk from stream with reader. |
| chunkPromise = reader.read() |
| } else if (done) { |
| // 5. Otherwise, if chunkPromise is fulfilled with an |
| // object whose done property is true, queue a task |
| // to run the following steps and abort this algorithm: |
| queueMicrotask(() => { |
| // 1. Set fr’s state to "done". |
| fr[kState] = 'done' |
| |
| // 2. Let result be the result of package data given |
| // bytes, type, blob’s type, and encodingName. |
| try { |
| const result = packageData(bytes, type, blob.type, encodingName) |
| |
| // 4. Else: |
| |
| if (fr[kAborted]) { |
| return |
| } |
| |
| // 1. Set fr’s result to result. |
| fr[kResult] = result |
| |
| // 2. Fire a progress event called load at the fr. |
| fireAProgressEvent('load', fr) |
| } catch (error) { |
| // 3. If package data threw an exception error: |
| |
| // 1. Set fr’s error to error. |
| fr[kError] = error |
| |
| // 2. Fire a progress event called error at fr. |
| fireAProgressEvent('error', fr) |
| } |
| |
| // 5. If fr’s state is not "loading", fire a progress |
| // event called loadend at the fr. |
| if (fr[kState] !== 'loading') { |
| fireAProgressEvent('loadend', fr) |
| } |
| }) |
| |
| break |
| } |
| } catch (error) { |
| if (fr[kAborted]) { |
| return |
| } |
| |
| // 6. Otherwise, if chunkPromise is rejected with an |
| // error error, queue a task to run the following |
| // steps and abort this algorithm: |
| queueMicrotask(() => { |
| // 1. Set fr’s state to "done". |
| fr[kState] = 'done' |
| |
| // 2. Set fr’s error to error. |
| fr[kError] = error |
| |
| // 3. Fire a progress event called error at fr. |
| fireAProgressEvent('error', fr) |
| |
| // 4. If fr’s state is not "loading", fire a progress |
| // event called loadend at fr. |
| if (fr[kState] !== 'loading') { |
| fireAProgressEvent('loadend', fr) |
| } |
| }) |
| |
| break |
| } |
| } |
| })() |
| } |
| |
| /** |
| * @see https://w3c.github.io/FileAPI/#fire-a-progress-event |
| * @see https://dom.spec.whatwg.org/#concept-event-fire |
| * @param {string} e The name of the event |
| * @param {import('./filereader').FileReader} reader |
| */ |
| function fireAProgressEvent (e, reader) { |
| // The progress event e does not bubble. e.bubbles must be false |
| // The progress event e is NOT cancelable. e.cancelable must be false |
| const event = new ProgressEvent(e, { |
| bubbles: false, |
| cancelable: false |
| }) |
| |
| reader.dispatchEvent(event) |
| } |
| |
| /** |
| * @see https://w3c.github.io/FileAPI/#blob-package-data |
| * @param {Uint8Array[]} bytes |
| * @param {string} type |
| * @param {string?} mimeType |
| * @param {string?} encodingName |
| */ |
| function packageData (bytes, type, mimeType, encodingName) { |
| // 1. A Blob has an associated package data algorithm, given |
| // bytes, a type, a optional mimeType, and a optional |
| // encodingName, which switches on type and runs the |
| // associated steps: |
| |
| switch (type) { |
| case 'DataURL': { |
| // 1. Return bytes as a DataURL [RFC2397] subject to |
| // the considerations below: |
| // * Use mimeType as part of the Data URL if it is |
| // available in keeping with the Data URL |
| // specification [RFC2397]. |
| // * If mimeType is not available return a Data URL |
| // without a media-type. [RFC2397]. |
| |
| // https://datatracker.ietf.org/doc/html/rfc2397#section-3 |
| // dataurl := "data:" [ mediatype ] [ ";base64" ] "," data |
| // mediatype := [ type "/" subtype ] *( ";" parameter ) |
| // data := *urlchar |
| // parameter := attribute "=" value |
| let dataURL = 'data:' |
| |
| const parsed = parseMIMEType(mimeType || 'application/octet-stream') |
| |
| if (parsed !== 'failure') { |
| dataURL += serializeAMimeType(parsed) |
| } |
| |
| dataURL += ';base64,' |
| |
| const decoder = new StringDecoder('latin1') |
| |
| for (const chunk of bytes) { |
| dataURL += btoa(decoder.write(chunk)) |
| } |
| |
| dataURL += btoa(decoder.end()) |
| |
| return dataURL |
| } |
| case 'Text': { |
| // 1. Let encoding be failure |
| let encoding = 'failure' |
| |
| // 2. If the encodingName is present, set encoding to the |
| // result of getting an encoding from encodingName. |
| if (encodingName) { |
| encoding = getEncoding(encodingName) |
| } |
| |
| // 3. If encoding is failure, and mimeType is present: |
| if (encoding === 'failure' && mimeType) { |
| // 1. Let type be the result of parse a MIME type |
| // given mimeType. |
| const type = parseMIMEType(mimeType) |
| |
| // 2. If type is not failure, set encoding to the result |
| // of getting an encoding from type’s parameters["charset"]. |
| if (type !== 'failure') { |
| encoding = getEncoding(type.parameters.get('charset')) |
| } |
| } |
| |
| // 4. If encoding is failure, then set encoding to UTF-8. |
| if (encoding === 'failure') { |
| encoding = 'UTF-8' |
| } |
| |
| // 5. Decode bytes using fallback encoding encoding, and |
| // return the result. |
| return decode(bytes, encoding) |
| } |
| case 'ArrayBuffer': { |
| // Return a new ArrayBuffer whose contents are bytes. |
| const sequence = combineByteSequences(bytes) |
| |
| return sequence.buffer |
| } |
| case 'BinaryString': { |
| // Return bytes as a binary string, in which every byte |
| // is represented by a code unit of equal value [0..255]. |
| let binaryString = '' |
| |
| const decoder = new StringDecoder('latin1') |
| |
| for (const chunk of bytes) { |
| binaryString += decoder.write(chunk) |
| } |
| |
| binaryString += decoder.end() |
| |
| return binaryString |
| } |
| } |
| } |
| |
| /** |
| * @see https://encoding.spec.whatwg.org/#decode |
| * @param {Uint8Array[]} ioQueue |
| * @param {string} encoding |
| */ |
| function decode (ioQueue, encoding) { |
| const bytes = combineByteSequences(ioQueue) |
| |
| // 1. Let BOMEncoding be the result of BOM sniffing ioQueue. |
| const BOMEncoding = BOMSniffing(bytes) |
| |
| let slice = 0 |
| |
| // 2. If BOMEncoding is non-null: |
| if (BOMEncoding !== null) { |
| // 1. Set encoding to BOMEncoding. |
| encoding = BOMEncoding |
| |
| // 2. Read three bytes from ioQueue, if BOMEncoding is |
| // UTF-8; otherwise read two bytes. |
| // (Do nothing with those bytes.) |
| slice = BOMEncoding === 'UTF-8' ? 3 : 2 |
| } |
| |
| // 3. Process a queue with an instance of encoding’s |
| // decoder, ioQueue, output, and "replacement". |
| |
| // 4. Return output. |
| |
| const sliced = bytes.slice(slice) |
| return new TextDecoder(encoding).decode(sliced) |
| } |
| |
| /** |
| * @see https://encoding.spec.whatwg.org/#bom-sniff |
| * @param {Uint8Array} ioQueue |
| */ |
| function BOMSniffing (ioQueue) { |
| // 1. Let BOM be the result of peeking 3 bytes from ioQueue, |
| // converted to a byte sequence. |
| const [a, b, c] = ioQueue |
| |
| // 2. For each of the rows in the table below, starting with |
| // the first one and going down, if BOM starts with the |
| // bytes given in the first column, then return the |
| // encoding given in the cell in the second column of that |
| // row. Otherwise, return null. |
| if (a === 0xEF && b === 0xBB && c === 0xBF) { |
| return 'UTF-8' |
| } else if (a === 0xFE && b === 0xFF) { |
| return 'UTF-16BE' |
| } else if (a === 0xFF && b === 0xFE) { |
| return 'UTF-16LE' |
| } |
| |
| return null |
| } |
| |
| /** |
| * @param {Uint8Array[]} sequences |
| */ |
| function combineByteSequences (sequences) { |
| const size = sequences.reduce((a, b) => { |
| return a + b.byteLength |
| }, 0) |
| |
| let offset = 0 |
| |
| return sequences.reduce((a, b) => { |
| a.set(b, offset) |
| offset += b.byteLength |
| return a |
| }, new Uint8Array(size)) |
| } |
| |
| module.exports = { |
| staticPropertyDescriptors, |
| readOperation, |
| fireAProgressEvent |
| } |