393 lines
11 KiB
JavaScript
393 lines
11 KiB
JavaScript
|
'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
|
|||
|
}
|