Spaces:
Runtime error
Runtime error
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 | |
} | |