From 5542e4b54855136b25f0e85424df251b9e6884e3 Mon Sep 17 00:00:00 2001 From: Khafra <42794878+KhafraDev@users.noreply.github.com> Date: Tue, 18 Oct 2022 05:51:32 -0400 Subject: [PATCH] feat: implement FileReader (#1690) * feat: implement FileReader * fix: implement correct text decoding * fix: make readOperation sync * feat: implement abort * test: event order fails * fix: readAsArrayBuffer incorrect byteLength * fix: base64 data url & default mime type * test: add most remaining tests * fix: update index-fetch.js * fix: correct types --- index-fetch.js | 4 +- index.d.ts | 1 + index.js | 5 +- lib/fetch/file.js | 6 + lib/fetch/index.js | 3 +- lib/fetch/webidl.js | 10 + lib/fileapi/encoding.js | 286 +++++++++++++ lib/fileapi/filereader.js | 368 ++++++++++++++++ lib/fileapi/progressevent.js | 84 ++++ lib/fileapi/symbols.js | 10 + lib/fileapi/util.js | 398 ++++++++++++++++++ package.json | 3 +- test/wpt/runner/runner/worker.mjs | 7 +- test/wpt/server/server.mjs | 2 + test/wpt/status/FileAPI.status.json | 26 ++ test/wpt/tests/FileAPI/fileReader.any.js | 59 +++ test/wpt/tests/FileAPI/idlharness.any.js | 19 + .../Determining-Encoding.any.js | 81 ++++ ...FileReader-event-handler-attributes.any.js | 17 + .../FileReader-multiple-reads.any.js | 81 ++++ .../filereader_abort.any.js | 38 ++ .../filereader_error.any.js | 19 + .../filereader_events.any.js | 19 + .../filereader_readAsArrayBuffer.any.js | 23 + .../filereader_readAsBinaryString.any.js | 23 + .../filereader_readAsDataURL.any.js | 42 ++ .../filereader_readAsText.any.js | 36 ++ .../filereader_readystate.any.js | 19 + test/wpt/tests/interfaces/FileAPI.idl | 100 +++++ test/wpt/tests/interfaces/url.idl | 42 ++ types/filereader.d.ts | 49 +++ 31 files changed, 1872 insertions(+), 8 deletions(-) create mode 100644 lib/fileapi/encoding.js create mode 100644 lib/fileapi/filereader.js create mode 100644 lib/fileapi/progressevent.js create mode 100644 lib/fileapi/symbols.js create mode 100644 lib/fileapi/util.js create mode 100644 test/wpt/tests/FileAPI/fileReader.any.js create mode 100644 test/wpt/tests/FileAPI/idlharness.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js create mode 100644 test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js create mode 100644 test/wpt/tests/interfaces/FileAPI.idl create mode 100644 test/wpt/tests/interfaces/url.idl create mode 100644 types/filereader.d.ts diff --git a/index-fetch.js b/index-fetch.js index 27a44732f44..0d59d254f7d 100644 --- a/index-fetch.js +++ b/index-fetch.js @@ -1,12 +1,10 @@ 'use strict' -const { getGlobalDispatcher } = require('./lib/global') const fetchImpl = require('./lib/fetch').fetch module.exports.fetch = async function fetch (resource) { - const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher() try { - return await fetchImpl.apply(dispatcher, arguments) + return await fetchImpl(...arguments) } catch (err) { Error.captureStackTrace(err, this) throw err diff --git a/index.d.ts b/index.d.ts index 1c8c6e2d03a..1aaf14d38b2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -18,6 +18,7 @@ import { request, pipeline, stream, connect, upgrade } from './types/api' export * from './types/fetch' export * from './types/file' +export * from './types/filereader' export * from './types/formdata' export * from './types/diagnostics-channel' export { Interceptable } from './types/mock-interceptor' diff --git a/index.js b/index.js index 9cde34aed6f..ad01a46920d 100644 --- a/index.js +++ b/index.js @@ -98,9 +98,9 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { if (!fetchImpl) { fetchImpl = require('./lib/fetch').fetch } - const dispatcher = (arguments[1] && arguments[1].dispatcher) || getGlobalDispatcher() + try { - return await fetchImpl.apply(dispatcher, arguments) + return await fetchImpl(...arguments) } catch (err) { Error.captureStackTrace(err, this) throw err @@ -111,6 +111,7 @@ if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 8)) { module.exports.Request = require('./lib/fetch/request').Request module.exports.FormData = require('./lib/fetch/formdata').FormData module.exports.File = require('./lib/fetch/file').File + module.exports.FileReader = require('./lib/fileapi/filereader').FileReader const { setGlobalOrigin, getGlobalOrigin } = require('./lib/fetch/global') diff --git a/lib/fetch/file.js b/lib/fetch/file.js index cf17e8b674f..d0d8c68d793 100644 --- a/lib/fetch/file.js +++ b/lib/fetch/file.js @@ -6,6 +6,7 @@ const { kState } = require('./symbols') const { isBlobLike } = require('./util') const { webidl } = require('./webidl') const { parseMIMEType, serializeAMimeType } = require('./dataURL') +const { kEnumerableProperty } = require('../core/util') class File extends Blob { constructor (fileBits, fileName, options = {}) { @@ -220,6 +221,11 @@ class FileLike { } } +Object.defineProperties(File.prototype, { + name: kEnumerableProperty, + lastModified: kEnumerableProperty +}) + webidl.converters.Blob = webidl.interfaceConverter(Blob) webidl.converters.BlobPart = function (V, opts) { diff --git a/lib/fetch/index.js b/lib/fetch/index.js index 38492c50e8f..53751c41ae6 100644 --- a/lib/fetch/index.js +++ b/lib/fetch/index.js @@ -54,6 +54,7 @@ const { Readable, pipeline } = require('stream') const { isErrored, isReadable } = require('../core/util') const { dataURLProcessor, serializeAMimeType } = require('./dataURL') const { TransformStream } = require('stream/web') +const { getGlobalDispatcher } = require('../../index') /** @type {import('buffer').resolveObjectURL} */ let resolveObjectURL @@ -241,7 +242,7 @@ async function fetch (input, init = {}) { request, processResponseEndOfBody: handleFetchDone, processResponse, - dispatcher: this // undici + dispatcher: init.dispatcher ?? getGlobalDispatcher() // undici }) // 14. Return p. diff --git a/lib/fetch/webidl.js b/lib/fetch/webidl.js index 7c6c147e277..cd42de2ac8a 100644 --- a/lib/fetch/webidl.js +++ b/lib/fetch/webidl.js @@ -478,6 +478,16 @@ webidl.converters['long long'] = function (V, opts) { return x } +// https://webidl.spec.whatwg.org/#es-unsigned-long-long +webidl.converters['unsigned long long'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 64, "unsigned"). + const x = webidl.util.ConvertToInt(V, 64, 'unsigned') + + // 2. Return the IDL unsigned long long value that + // represents the same numeric value as x. + return x +} + // https://webidl.spec.whatwg.org/#es-unsigned-short webidl.converters['unsigned short'] = function (V) { // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). diff --git a/lib/fileapi/encoding.js b/lib/fileapi/encoding.js new file mode 100644 index 00000000000..4aac20cce13 --- /dev/null +++ b/lib/fileapi/encoding.js @@ -0,0 +1,286 @@ +'use strict' + +/** + * @see https://encoding.spec.whatwg.org/#concept-encoding-get + * @param {string} label + */ +function getEncoding (label) { + // 1. Remove any leading and trailing ASCII whitespace from label. + // 2. If label is an ASCII case-insensitive match for any of the + // labels listed in the table below, then return the + // corresponding encoding; otherwise return failure. + switch (label.trim().toLowerCase()) { + case 'unicode-1-1-utf-8': + case 'unicode11utf8': + case 'unicode20utf8': + case 'utf-8': + case 'utf8': + case 'x-unicode20utf8': + return 'UTF-8' + case '866': + case 'cp866': + case 'csibm866': + case 'ibm866': + return 'IBM866' + case 'csisolatin2': + case 'iso-8859-2': + case 'iso-ir-101': + case 'iso8859-2': + case 'iso88592': + case 'iso_8859-2': + case 'iso_8859-2:1987': + case 'l2': + case 'latin2': + return 'ISO-8859-2' + case 'csisolatin3': + case 'iso-8859-3': + case 'iso-ir-109': + case 'iso8859-3': + case 'iso88593': + case 'iso_8859-3': + case 'iso_8859-3:1988': + case 'l3': + case 'latin3': + return 'ISO-8859-3' + case 'csisolatin4': + case 'iso-8859-4': + case 'iso-ir-110': + case 'iso8859-4': + case 'iso88594': + case 'iso_8859-4': + case 'iso_8859-4:1988': + case 'l4': + case 'latin4': + return 'ISO-8859-4' + case 'csisolatincyrillic': + case 'cyrillic': + case 'iso-8859-5': + case 'iso-ir-144': + case 'iso8859-5': + case 'iso88595': + case 'iso_8859-5': + case 'iso_8859-5:1988': + return 'ISO-8859-5' + case 'arabic': + case 'asmo-708': + case 'csiso88596e': + case 'csiso88596i': + case 'csisolatinarabic': + case 'ecma-114': + case 'iso-8859-6': + case 'iso-8859-6-e': + case 'iso-8859-6-i': + case 'iso-ir-127': + case 'iso8859-6': + case 'iso88596': + case 'iso_8859-6': + case 'iso_8859-6:1987': + return 'ISO-8859-6' + case 'csisolatingreek': + case 'ecma-118': + case 'elot_928': + case 'greek': + case 'greek8': + case 'iso-8859-7': + case 'iso-ir-126': + case 'iso8859-7': + case 'iso88597': + case 'iso_8859-7': + case 'iso_8859-7:1987': + case 'sun_eu_greek': + return 'ISO-8859-7' + case 'csiso88598e': + case 'csisolatinhebrew': + case 'hebrew': + case 'iso-8859-8': + case 'iso-8859-8-e': + case 'iso-ir-138': + case 'iso8859-8': + case 'iso88598': + case 'iso_8859-8': + case 'iso_8859-8:1988': + case 'visual': + return 'ISO-8859-8' + case 'csiso88598i': + case 'iso-8859-8-i': + case 'logical': + return 'ISO-8859-8-I' + case 'csisolatin6': + case 'iso-8859-10': + case 'iso-ir-157': + case 'iso8859-10': + case 'iso885910': + case 'l6': + case 'latin6': + return 'ISO-8859-10' + case 'iso-8859-13': + case 'iso8859-13': + case 'iso885913': + return 'ISO-8859-13' + case 'iso-8859-14': + case 'iso8859-14': + case 'iso885914': + return 'ISO-8859-14' + case 'csisolatin9': + case 'iso-8859-15': + case 'iso8859-15': + case 'iso885915': + case 'iso_8859-15': + case 'l9': + return 'ISO-8859-15' + case 'iso-8859-16': + return 'ISO-8859-16' + case 'cskoi8r': + case 'koi': + case 'koi8': + case 'koi8-r': + case 'koi8_r': + return 'KOI8-R' + case 'koi8-ru': + case 'koi8-u': + return 'KOI8-U' + case 'csmacintosh': + case 'mac': + case 'macintosh': + case 'x-mac-roman': + return 'macintosh' + case 'iso-8859-11': + case 'iso8859-11': + case 'iso885911': + case 'tis-620': + case 'windows-874': + return 'windows-874' + case 'cp1250': + case 'windows-1250': + case 'x-cp1250': + return 'windows-1250' + case 'cp1251': + case 'windows-1251': + case 'x-cp1251': + return 'windows-1251' + case 'ansi_x3.4-1968': + case 'ascii': + case 'cp1252': + case 'cp819': + case 'csisolatin1': + case 'ibm819': + case 'iso-8859-1': + case 'iso-ir-100': + case 'iso8859-1': + case 'iso88591': + case 'iso_8859-1': + case 'iso_8859-1:1987': + case 'l1': + case 'latin1': + case 'us-ascii': + case 'windows-1252': + case 'x-cp1252': + return 'windows-1252' + case 'cp1253': + case 'windows-1253': + case 'x-cp1253': + return 'windows-1253' + case 'cp1254': + case 'csisolatin5': + case 'iso-8859-9': + case 'iso-ir-148': + case 'iso8859-9': + case 'iso88599': + case 'iso_8859-9': + case 'iso_8859-9:1989': + case 'l5': + case 'latin5': + case 'windows-1254': + case 'x-cp1254': + return 'windows-1254' + case 'cp1255': + case 'windows-1255': + case 'x-cp1255': + return 'windows-1255' + case 'cp1256': + case 'windows-1256': + case 'x-cp1256': + return 'windows-1256' + case 'cp1257': + case 'windows-1257': + case 'x-cp1257': + return 'windows-1257' + case 'cp1258': + case 'windows-1258': + case 'x-cp1258': + return 'windows-1258' + case 'x-mac-cyrillic': + case 'x-mac-ukrainian': + return 'x-mac-cyrillic' + case 'chinese': + case 'csgb2312': + case 'csiso58gb231280': + case 'gb2312': + case 'gb_2312': + case 'gb_2312-80': + case 'gbk': + case 'iso-ir-58': + case 'x-gbk': + return 'GBK' + case 'gb18030': + return 'gb18030' + case 'big5': + case 'big5-hkscs': + case 'cn-big5': + case 'csbig5': + case 'x-x-big5': + return 'Big5' + case 'cseucpkdfmtjapanese': + case 'euc-jp': + case 'x-euc-jp': + return 'EUC-JP' + case 'csiso2022jp': + case 'iso-2022-jp': + return 'ISO-2022-JP' + case 'csshiftjis': + case 'ms932': + case 'ms_kanji': + case 'shift-jis': + case 'shift_jis': + case 'sjis': + case 'windows-31j': + case 'x-sjis': + return 'Shift_JIS' + case 'cseuckr': + case 'csksc56011987': + case 'euc-kr': + case 'iso-ir-149': + case 'korean': + case 'ks_c_5601-1987': + case 'ks_c_5601-1989': + case 'ksc5601': + case 'ksc_5601': + case 'windows-949': + return 'EUC-KR' + case 'csiso2022kr': + case 'hz-gb-2312': + case 'iso-2022-cn': + case 'iso-2022-cn-ext': + case 'iso-2022-kr': + case 'replacement': + return 'replacement' + case 'unicodefffe': + case 'utf-16be': + return 'UTF-16BE' + case 'csunicode': + case 'iso-10646-ucs-2': + case 'ucs-2': + case 'unicode': + case 'unicodefeff': + case 'utf-16': + case 'utf-16le': + return 'UTF-16LE' + case 'x-user-defined': + return 'x-user-defined' + default: return 'failure' + } +} + +module.exports = { + getEncoding +} diff --git a/lib/fileapi/filereader.js b/lib/fileapi/filereader.js new file mode 100644 index 00000000000..fa18e8eb593 --- /dev/null +++ b/lib/fileapi/filereader.js @@ -0,0 +1,368 @@ +'use strict' + +const { + staticPropertyDescriptors, + readOperation, + fireAProgressEvent +} = require('./util') +const { + kState, + kError, + kResult, + kEvents, + kAborted +} = require('./symbols') +const { webidl } = require('../fetch/webidl') +const { kEnumerableProperty } = require('../core/util') + +class FileReader extends EventTarget { + constructor () { + super() + + this[kState] = 'empty' + this[kResult] = null + this[kError] = null + this[kEvents] = { + loadend: null, + error: null, + abort: null, + load: null, + progress: null, + loadstart: null + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsArrayBuffer + * @param {import('buffer').Blob} blob + */ + readAsArrayBuffer (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsArrayBuffer\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsArrayBuffer(blob) method, when invoked, + // must initiate a read operation for blob with ArrayBuffer. + readOperation(this, blob, 'ArrayBuffer') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsBinaryString + * @param {import('buffer').Blob} blob + */ + readAsBinaryString (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsBinaryString\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsBinaryString(blob) method, when invoked, + // must initiate a read operation for blob with BinaryString. + readOperation(this, blob, 'BinaryString') + } + + /** + * @see https://w3c.github.io/FileAPI/#readAsDataText + * @param {import('buffer').Blob} blob + * @param {string?} encoding + */ + readAsText (blob, encoding = undefined) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsText\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + if (encoding !== undefined) { + encoding = webidl.converters.DOMString(encoding) + } + + // The readAsText(blob, encoding) method, when invoked, + // must initiate a read operation for blob with Text and encoding. + readOperation(this, blob, 'Text', encoding) + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-readAsDataURL + * @param {import('buffer').Blob} blob + */ + readAsDataURL (blob) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (arguments.length === 0) { + throw new TypeError( + 'Failed to execute \'readAsDataURL\' on \'FileReader\': 1 argument required, but 0 present.' + ) + } + + blob = webidl.converters.Blob(blob, { strict: false }) + + // The readAsDataURL(blob) method, when invoked, must + // initiate a read operation for blob with DataURL. + readOperation(this, blob, 'DataURL') + } + + /** + * @see https://w3c.github.io/FileAPI/#dfn-abort + */ + abort () { + // 1. If this's state is "empty" or if this's state is + // "done" set this's result to null and terminate + // this algorithm. + if (this[kState] === 'empty' || this[kState] === 'done') { + this[kResult] = null + return + } + + // 2. If this's state is "loading" set this's state to + // "done" and set this's result to null. + if (this[kState] === 'loading') { + this[kState] = 'done' + this[kResult] = null + } + + // 3. If there are any tasks from this on the file reading + // task source in an affiliated task queue, then remove + // those tasks from that task queue. + this[kAborted] = true + + // 4. Terminate the algorithm for the read method being processed. + // TODO + + // 5. Fire a progress event called abort at this. + fireAProgressEvent('abort', this) + + // 6. If this's state is not "loading", fire a progress + // event called loadend at this. + if (this[kState] !== 'loading') { + fireAProgressEvent('loadend', this) + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-readystate + */ + get readyState () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + switch (this[kState]) { + case 'empty': return this.EMPTY + case 'loading': return this.LOADING + case 'done': return this.DONE + } + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-result + */ + get result () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + // The result attribute’s getter, when invoked, must return + // this's result. + return this[kResult] + } + + /** + * @see https://w3c.github.io/FileAPI/#dom-filereader-error + */ + get error () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + // The error attribute’s getter, when invoked, must return + // this's error. + return this[kError] + } + + get onloadend () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].loadend + } + + set onloadend (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].loadend = fn + } else { + this[kEvents].loadend = null + } + } + + get onerror () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].error + } + + set onerror (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].error = fn + } else { + this[kEvents].error = null + } + } + + get onloadstart () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].loadstart + } + + set onloadstart (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].loadstart = fn + } else { + this[kEvents].loadstart = null + } + } + + get onprogress () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].progress + } + + set onprogress (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].progress = fn + } else { + this[kEvents].progress = null + } + } + + get onload () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].load + } + + set onload (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].load = fn + } else { + this[kEvents].load = null + } + } + + get onabort () { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + return this[kEvents].abort + } + + set onabort (fn) { + if (!(this instanceof FileReader)) { + throw new TypeError('Illegal invocation') + } + + if (typeof fn === 'function') { + this[kEvents].abort = fn + } else { + this[kEvents].abort = null + } + } +} + +// https://w3c.github.io/FileAPI/#dom-filereader-empty +FileReader.EMPTY = FileReader.prototype.EMPTY = 0 +// https://w3c.github.io/FileAPI/#dom-filereader-loading +FileReader.LOADING = FileReader.prototype.LOADING = 1 +// https://w3c.github.io/FileAPI/#dom-filereader-done +FileReader.DONE = FileReader.prototype.DONE = 2 + +Object.defineProperties(FileReader.prototype, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors, + readAsArrayBuffer: kEnumerableProperty, + readAsBinaryString: kEnumerableProperty, + readAsText: kEnumerableProperty, + readAsDataURL: kEnumerableProperty, + abort: kEnumerableProperty, + readyState: kEnumerableProperty, + result: kEnumerableProperty, + error: kEnumerableProperty, + onloadstart: kEnumerableProperty, + onprogress: kEnumerableProperty, + onload: kEnumerableProperty, + onabort: kEnumerableProperty, + onerror: kEnumerableProperty, + onloadend: kEnumerableProperty, + [Symbol.toStringTag]: { + value: 'FileReader', + writable: false, + enumerable: false, + configurable: true + } +}) + +Object.defineProperties(FileReader, { + EMPTY: staticPropertyDescriptors, + LOADING: staticPropertyDescriptors, + DONE: staticPropertyDescriptors +}) + +module.exports = { + FileReader +} diff --git a/lib/fileapi/progressevent.js b/lib/fileapi/progressevent.js new file mode 100644 index 00000000000..ab3c6191e2d --- /dev/null +++ b/lib/fileapi/progressevent.js @@ -0,0 +1,84 @@ +'use strict' + +const { webidl } = require('../fetch/webidl') + +const kState = Symbol('ProgressEvent state') + +/** + * @see https://xhr.spec.whatwg.org/#progressevent + */ +class ProgressEvent extends Event { + constructor (type, eventInitDict = {}) { + type = webidl.converters.DOMString(type) + eventInitDict = webidl.converters.ProgressEventInit(eventInitDict ?? {}) + + super(type, eventInitDict) + + this[kState] = { + lengthComputable: eventInitDict.lengthComputable, + loaded: eventInitDict.loaded, + total: eventInitDict.total + } + } + + get lengthComputable () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].lengthComputable + } + + get loaded () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].loaded + } + + get total () { + if (!(this instanceof ProgressEvent)) { + throw new TypeError('Illegal invocation') + } + + return this[kState].total + } +} + +webidl.converters.ProgressEventInit = webidl.dictionaryConverter([ + { + key: 'lengthComputable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'loaded', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'total', + converter: webidl.converters['unsigned long long'], + defaultValue: 0 + }, + { + key: 'bubbles', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'cancelable', + converter: webidl.converters.boolean, + defaultValue: false + }, + { + key: 'composed', + converter: webidl.converters.boolean, + defaultValue: false + } +]) + +module.exports = { + ProgressEvent +} diff --git a/lib/fileapi/symbols.js b/lib/fileapi/symbols.js new file mode 100644 index 00000000000..dd11746de38 --- /dev/null +++ b/lib/fileapi/symbols.js @@ -0,0 +1,10 @@ +'use strict' + +module.exports = { + kState: Symbol('FileReader state'), + kResult: Symbol('FileReader result'), + kError: Symbol('FileReader error'), + kLastProgressEventFired: Symbol('FileReader last progress event fired timestamp'), + kEvents: Symbol('FileReader events'), + kAborted: Symbol('FileReader aborted') +} diff --git a/lib/fileapi/util.js b/lib/fileapi/util.js new file mode 100644 index 00000000000..b37e0dd2b51 --- /dev/null +++ b/lib/fileapi/util.js @@ -0,0 +1,398 @@ +'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 + * @param {string} e The name of the event + * @param {import('./filereader').FileReader} reader + */ +function fireAProgressEvent (e, reader) { + const event = new ProgressEvent(e, { + bubbles: false, + cancelable: false + }) + + reader.dispatchEvent(event) + try { + // eslint-disable-next-line no-useless-call + reader[`on${e}`]?.call(reader, event) + } catch (err) { + // Prevent the error from being swallowed + queueMicrotask(() => { + throw err + }) + } +} + +/** + * @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 +} diff --git a/package.json b/package.json index 009c1ec55fb..50ea900af14 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,8 @@ "compilerOptions": { "esModuleInterop": true, "lib": [ - "esnext" + "esnext", + "DOM" ] } }, diff --git a/test/wpt/runner/runner/worker.mjs b/test/wpt/runner/runner/worker.mjs index cea71058bd8..f5ff9a1cc95 100644 --- a/test/wpt/runner/runner/worker.mjs +++ b/test/wpt/runner/runner/worker.mjs @@ -9,7 +9,8 @@ import { fetch, FormData, File, - Headers + Headers, + FileReader } from '../../../../index.js' const { initScripts, meta, test, url, path } = workerData @@ -48,6 +49,10 @@ Object.defineProperties(globalThis, { Response: { ...globalPropertyDescriptors, value: Response + }, + FileReader: { + ...globalPropertyDescriptors, + value: FileReader } }) diff --git a/test/wpt/server/server.mjs b/test/wpt/server/server.mjs index 20950de5ee4..7aa9dd0b8ab 100644 --- a/test/wpt/server/server.mjs +++ b/test/wpt/server/server.mjs @@ -37,8 +37,10 @@ const server = createServer(async (req, res) => { case '/mimesniff/mime-types/resources/generated-mime-types.json': case '/mimesniff/mime-types/resources/mime-types.json': case '/interfaces/dom.idl': + case '/interfaces/url.idl': case '/interfaces/html.idl': case '/interfaces/fetch.idl': + case '/interfaces/FileAPI.idl': case '/interfaces/referrer-policy.idl': case '/xhr/resources/utf16-bom.json': case '/fetch/data-urls/resources/base64.json': diff --git a/test/wpt/status/FileAPI.status.json b/test/wpt/status/FileAPI.status.json index 2131af65e55..b4f740decc1 100644 --- a/test/wpt/status/FileAPI.status.json +++ b/test/wpt/status/FileAPI.status.json @@ -3,5 +3,31 @@ "fail": [ "Using type in File constructor: nonparsable" ] + }, + "idlharness.any.js": { + "fail": [ + "URL interface: operation revokeObjectURL(DOMString)", + "URL interface: operation createObjectURL((Blob or MediaSource))", + "Blob interface: attribute size", + "Blob interface: attribute type", + "Blob interface: operation slice(optional long long, optional long long, optional DOMString)", + "Blob interface: operation stream()", + "Blob interface: operation text()", + "Blob interface: operation arrayBuffer()", + "FileList interface: existence and properties of interface object", + "FileList interface object length", + "FileList interface object name", + "FileList interface: existence and properties of interface prototype object", + "FileList interface: existence and properties of interface prototype object's \"constructor\" property", + "FileList interface: existence and properties of interface prototype object's @@unscopables property", + "FileList interface: operation item(unsigned long)", + "FileList interface: attribute length" + ] + }, + "filereader_events.any.js": { + "fail": [ + "events are dispatched in the correct order for an empty blob", + "events are dispatched in the correct order for a non-empty blob" + ] } } \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/fileReader.any.js b/test/wpt/tests/FileAPI/fileReader.any.js new file mode 100644 index 00000000000..2876dcb4ce3 --- /dev/null +++ b/test/wpt/tests/FileAPI/fileReader.any.js @@ -0,0 +1,59 @@ +// META: title=FileReader States + +'use strict'; + +test(function () { + assert_true( + "FileReader" in globalThis, + "globalThis should have a FileReader property.", + ); +}, "FileReader interface object"); + +test(function () { + var fileReader = new FileReader(); + assert_true(fileReader instanceof FileReader); +}, "no-argument FileReader constructor"); + +var t_abort = async_test("FileReader States -- abort"); +t_abort.step(function () { + var fileReader = new FileReader(); + assert_equals(fileReader.readyState, 0); + assert_equals(fileReader.readyState, FileReader.EMPTY); + + var blob = new Blob(); + fileReader.readAsArrayBuffer(blob); + assert_equals(fileReader.readyState, 1); + assert_equals(fileReader.readyState, FileReader.LOADING); + + fileReader.onabort = this.step_func(function (e) { + assert_equals(fileReader.readyState, 2); + assert_equals(fileReader.readyState, FileReader.DONE); + t_abort.done(); + }); + fileReader.abort(); + fileReader.onabort = this.unreached_func("abort event should fire sync"); +}); + +var t_event = async_test("FileReader States -- events"); +t_event.step(function () { + var fileReader = new FileReader(); + + var blob = new Blob(); + fileReader.readAsArrayBuffer(blob); + + fileReader.onloadstart = this.step_func(function (e) { + assert_equals(fileReader.readyState, 1); + assert_equals(fileReader.readyState, FileReader.LOADING); + }); + + fileReader.onprogress = this.step_func(function (e) { + assert_equals(fileReader.readyState, 1); + assert_equals(fileReader.readyState, FileReader.LOADING); + }); + + fileReader.onloadend = this.step_func(function (e) { + assert_equals(fileReader.readyState, 2); + assert_equals(fileReader.readyState, FileReader.DONE); + t_event.done(); + }); +}); diff --git a/test/wpt/tests/FileAPI/idlharness.any.js b/test/wpt/tests/FileAPI/idlharness.any.js new file mode 100644 index 00000000000..1744242b9f3 --- /dev/null +++ b/test/wpt/tests/FileAPI/idlharness.any.js @@ -0,0 +1,19 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js +// META: timeout=long + +'use strict'; + +// https://w3c.github.io/FileAPI/ + +idl_test( + ['FileAPI'], + ['dom', 'html', 'url'], + idl_array => { + idl_array.add_objects({ + Blob: ['new Blob(["TEST"])'], + File: ['new File(["myFileBits"], "myFileName")'], + FileReader: ['new FileReader()'] + }); + } +); diff --git a/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js b/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js new file mode 100644 index 00000000000..5b69f7ed982 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/Determining-Encoding.any.js @@ -0,0 +1,81 @@ +// META: title=FileAPI Test: Blob Determining Encoding + +var t = async_test("Blob Determing Encoding with encoding argument"); +t.step(function() { + // string 'hello' + var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F]; + var blob = new Blob([new Uint8Array(data)]); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.") + }, reader); + + reader.readAsText(blob, "UTF-16BE"); +}); + +var t = async_test("Blob Determing Encoding with type attribute"); +t.step(function() { + var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F]; + var blob = new Blob([new Uint8Array(data)], {type:"text/plain;charset=UTF-16BE"}); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.") + }, reader); + + reader.readAsText(blob); +}); + + +var t = async_test("Blob Determing Encoding with UTF-8 BOM"); +t.step(function() { + var data = [0xEF,0xBB,0xBF,0x68,0x65,0x6C,0x6C,0xC3,0xB6]; + var blob = new Blob([new Uint8Array(data)]); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hellö", "The FileReader should read the blob with UTF-8."); + }, reader); + + reader.readAsText(blob); +}); + +var t = async_test("Blob Determing Encoding without anything implying charset."); +t.step(function() { + var data = [0x68,0x65,0x6C,0x6C,0xC3,0xB6]; + var blob = new Blob([new Uint8Array(data)]); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hellö", "The FileReader should read the blob by default with UTF-8."); + }, reader); + + reader.readAsText(blob); +}); + +var t = async_test("Blob Determing Encoding with UTF-16BE BOM"); +t.step(function() { + var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F]; + var blob = new Blob([new Uint8Array(data)]); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE."); + }, reader); + + reader.readAsText(blob); +}); + +var t = async_test("Blob Determing Encoding with UTF-16LE BOM"); +t.step(function() { + var data = [0xFF,0xFE,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F,0x00]; + var blob = new Blob([new Uint8Array(data)]); + var reader = new FileReader(); + + reader.onloadend = t.step_func_done (function(event) { + assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16LE."); + }, reader); + + reader.readAsText(blob); +}); diff --git a/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js b/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js new file mode 100644 index 00000000000..fc71c643481 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js @@ -0,0 +1,17 @@ +// META: title=FileReader event handler attributes + +var attributes = [ + "onloadstart", + "onprogress", + "onload", + "onabort", + "onerror", + "onloadend", +]; +attributes.forEach(function(a) { + test(function() { + var reader = new FileReader(); + assert_equals(reader[a], null, + "event handler attribute should initially be null"); + }, "FileReader." + a + ": initial value"); +}); diff --git a/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js b/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js new file mode 100644 index 00000000000..4b19c69b425 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/FileReader-multiple-reads.any.js @@ -0,0 +1,81 @@ +// META: title=FileReader: starting new reads while one is in progress + +test(function() { + var blob_1 = new Blob(['TEST000000001']) + var blob_2 = new Blob(['TEST000000002']) + var reader = new FileReader(); + reader.readAsText(blob_1) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") + assert_throws_dom("InvalidStateError", function () { + reader.readAsText(blob_2) + }) +}, 'test FileReader InvalidStateError exception for readAsText'); + +test(function() { + var blob_1 = new Blob(['TEST000000001']) + var blob_2 = new Blob(['TEST000000002']) + var reader = new FileReader(); + reader.readAsDataURL(blob_1) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") + assert_throws_dom("InvalidStateError", function () { + reader.readAsDataURL(blob_2) + }) +}, 'test FileReader InvalidStateError exception for readAsDataURL'); + +test(function() { + var blob_1 = new Blob(['TEST000000001']) + var blob_2 = new Blob(['TEST000000002']) + var reader = new FileReader(); + reader.readAsArrayBuffer(blob_1) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") + assert_throws_dom("InvalidStateError", function () { + reader.readAsArrayBuffer(blob_2) + }) +}, 'test FileReader InvalidStateError exception for readAsArrayBuffer'); + +async_test(function() { + var blob_1 = new Blob(['TEST000000001']) + var blob_2 = new Blob(['TEST000000002']) + var reader = new FileReader(); + var triggered = false; + reader.onloadstart = this.step_func_done(function() { + assert_false(triggered, "Only one loadstart event should be dispatched"); + triggered = true; + assert_equals(reader.readyState, FileReader.LOADING, + "readyState must be LOADING") + assert_throws_dom("InvalidStateError", function () { + reader.readAsArrayBuffer(blob_2) + }) + }); + reader.readAsArrayBuffer(blob_1) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") +}, 'test FileReader InvalidStateError exception in onloadstart event for readAsArrayBuffer'); + +async_test(function() { + var blob_1 = new Blob(['TEST000000001']) + var blob_2 = new Blob(['TEST000000002']) + var reader = new FileReader(); + reader.onloadend = this.step_func_done(function() { + assert_equals(reader.readyState, FileReader.DONE, + "readyState must be DONE") + reader.readAsArrayBuffer(blob_2) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") + }); + reader.readAsArrayBuffer(blob_1) + assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING") +}, 'test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer'); + +async_test(function() { + var blob_1 = new Blob([new Uint8Array(0x414141)]); + var blob_2 = new Blob(['TEST000000002']); + var reader = new FileReader(); + reader.onloadstart = this.step_func(function() { + reader.abort(); + reader.onloadstart = null; + reader.onloadend = this.step_func_done(function() { + assert_equals('TEST000000002', reader.result); + }); + reader.readAsText(blob_2); + }); + reader.readAsText(blob_1); +}, 'test abort and restart in onloadstart event for readAsText'); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js new file mode 100644 index 00000000000..c778ae55bb5 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_abort.any.js @@ -0,0 +1,38 @@ +// META: title=FileAPI Test: filereader_abort + + test(function() { + var readerNoRead = new FileReader(); + readerNoRead.abort(); + assert_equals(readerNoRead.readyState, readerNoRead.EMPTY); + assert_equals(readerNoRead.result, null); + }, "Aborting before read"); + + promise_test(t => { + var blob = new Blob(["TEST THE ABORT METHOD"]); + var readerAbort = new FileReader(); + + var eventWatcher = new EventWatcher(t, readerAbort, + ['abort', 'loadstart', 'loadend', 'error', 'load']); + + // EventWatcher doesn't let us inspect the state after the abort event, + // so add an extra event handler for that. + readerAbort.addEventListener('abort', t.step_func(e => { + assert_equals(readerAbort.readyState, readerAbort.DONE); + })); + + readerAbort.readAsText(blob); + return eventWatcher.wait_for('loadstart') + .then(() => { + assert_equals(readerAbort.readyState, readerAbort.LOADING); + // 'abort' and 'loadend' events are dispatched synchronously, so + // call wait_for before calling abort. + var nextEvent = eventWatcher.wait_for(['abort', 'loadend']); + readerAbort.abort(); + return nextEvent; + }) + .then(() => { + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=24401 + assert_equals(readerAbort.result, null); + assert_equals(readerAbort.readyState, readerAbort.DONE); + }); + }, "Aborting after read"); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js new file mode 100644 index 00000000000..98459620901 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_error.any.js @@ -0,0 +1,19 @@ +// META: title=FileAPI Test: filereader_error + + async_test(function() { + var blob = new Blob(["TEST THE ERROR ATTRIBUTE AND ERROR EVENT"]); + var reader = new FileReader(); + assert_equals(reader.error, null, "The error is null when no error occurred"); + + reader.onload = this.step_func(function(evt) { + assert_unreached("Should not dispatch the load event"); + }); + + reader.onloadend = this.step_func(function(evt) { + assert_equals(reader.result, null, "The result is null"); + this.done(); + }); + + reader.readAsText(blob); + reader.abort(); + }); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js new file mode 100644 index 00000000000..ac692907d11 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_events.any.js @@ -0,0 +1,19 @@ +promise_test(async t => { + var reader = new FileReader(); + var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); + reader.readAsText(new Blob([])); + await eventWatcher.wait_for('loadstart'); + // No progress event for an empty blob, as no data is loaded. + await eventWatcher.wait_for('load'); + await eventWatcher.wait_for('loadend'); +}, 'events are dispatched in the correct order for an empty blob'); + +promise_test(async t => { + var reader = new FileReader(); + var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); + reader.readAsText(new Blob(['a'])); + await eventWatcher.wait_for('loadstart'); + await eventWatcher.wait_for('progress'); + await eventWatcher.wait_for('load'); + await eventWatcher.wait_for('loadend'); +}, 'events are dispatched in the correct order for a non-empty blob'); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js new file mode 100644 index 00000000000..d06e3170782 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js @@ -0,0 +1,23 @@ +// META: title=FileAPI Test: filereader_readAsArrayBuffer + + async_test(function() { + var blob = new Blob(["TEST"]); + var reader = new FileReader(); + + reader.onload = this.step_func(function(evt) { + assert_equals(reader.result.byteLength, 4, "The byteLength is 4"); + assert_true(reader.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer"); + assert_equals(reader.readyState, reader.DONE); + this.done(); + }); + + reader.onloadstart = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.onprogress = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.readAsArrayBuffer(blob); + }); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js new file mode 100644 index 00000000000..e69ff15e75b --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js @@ -0,0 +1,23 @@ +// META: title=FileAPI Test: filereader_readAsBinaryString + +async_test(t => { + const blob = new Blob(["σ"]); + const reader = new FileReader(); + + reader.onload = t.step_func_done(() => { + assert_equals(typeof reader.result, "string", "The result is string"); + assert_equals(reader.result.length, 2, "The result length is 2"); + assert_equals(reader.result, "\xcf\x83", "The result is \xcf\x83"); + assert_equals(reader.readyState, reader.DONE); + }); + + reader.onloadstart = t.step_func(() => { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.onprogress = t.step_func(() => { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.readAsBinaryString(blob); +}); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js new file mode 100644 index 00000000000..d6812121295 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsDataURL.any.js @@ -0,0 +1,42 @@ +// META: title=FileAPI Test: FileReader.readAsDataURL + +async_test(function(testCase) { + var blob = new Blob(["TEST"]); + var reader = new FileReader(); + + reader.onload = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.DONE); + testCase.done(); + }); + reader.onloadstart = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + reader.onprogress = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.readAsDataURL(blob); +}, 'FileReader readyState during readAsDataURL'); + +async_test(function(testCase) { + var blob = new Blob(["TEST"], { type: 'text/plain' }); + var reader = new FileReader(); + + reader.onload = this.step_func(function() { + assert_equals(reader.result, "data:text/plain;base64,VEVTVA=="); + testCase.done(); + }); + reader.readAsDataURL(blob); +}, 'readAsDataURL result for Blob with specified MIME type'); + +async_test(function(testCase) { + var blob = new Blob(["TEST"]); + var reader = new FileReader(); + + reader.onload = this.step_func(function() { + assert_equals(reader.result, + "data:application/octet-stream;base64,VEVTVA=="); + testCase.done(); + }); + reader.readAsDataURL(blob); +}, 'readAsDataURL result for Blob with unspecified MIME type'); \ No newline at end of file diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js new file mode 100644 index 00000000000..4d0fa113931 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readAsText.any.js @@ -0,0 +1,36 @@ +// META: title=FileAPI Test: filereader_readAsText + + async_test(function() { + var blob = new Blob(["TEST"]); + var reader = new FileReader(); + + reader.onload = this.step_func(function(evt) { + assert_equals(typeof reader.result, "string", "The result is typeof string"); + assert_equals(reader.result, "TEST", "The result is TEST"); + this.done(); + }); + + reader.onloadstart = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING, "The readyState"); + }); + + reader.onprogress = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.readAsText(blob); + }, "readAsText should correctly read UTF-8."); + + async_test(function() { + var blob = new Blob(["TEST"]); + var reader = new FileReader(); + var reader_UTF16 = new FileReader(); + reader_UTF16.onload = this.step_func(function(evt) { + // "TEST" in UTF-8 is 0x54 0x45 0x53 0x54. + // Decoded as utf-16 (little-endian), we get 0x4554 0x5453. + assert_equals(reader_UTF16.readyState, reader.DONE, "The readyState"); + assert_equals(reader_UTF16.result, "\u4554\u5453", "The result is not TEST"); + this.done(); + }); + reader_UTF16.readAsText(blob, "UTF-16"); + }, "readAsText should correctly read UTF-16."); diff --git a/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js b/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js new file mode 100644 index 00000000000..3cb36ab9996 --- /dev/null +++ b/test/wpt/tests/FileAPI/reading-data-section/filereader_readystate.any.js @@ -0,0 +1,19 @@ +// META: title=FileAPI Test: filereader_readystate + + async_test(function() { + var blob = new Blob(["THIS TEST THE READYSTATE WHEN READ BLOB"]); + var reader = new FileReader(); + + assert_equals(reader.readyState, reader.EMPTY); + + reader.onloadstart = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.LOADING); + }); + + reader.onloadend = this.step_func(function(evt) { + assert_equals(reader.readyState, reader.DONE); + this.done(); + }); + + reader.readAsDataURL(blob); + }); diff --git a/test/wpt/tests/interfaces/FileAPI.idl b/test/wpt/tests/interfaces/FileAPI.idl new file mode 100644 index 00000000000..aee0e65dcae --- /dev/null +++ b/test/wpt/tests/interfaces/FileAPI.idl @@ -0,0 +1,100 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: File API (https://w3c.github.io/FileAPI/) + +[Exposed=(Window,Worker), Serializable] +interface Blob { + constructor(optional sequence blobParts, + optional BlobPropertyBag options = {}); + + readonly attribute unsigned long long size; + readonly attribute DOMString type; + + // slice Blob into byte-ranged chunks + Blob slice(optional [Clamp] long long start, + optional [Clamp] long long end, + optional DOMString contentType); + + // read from the Blob. + [NewObject] ReadableStream stream(); + [NewObject] Promise text(); + [NewObject] Promise arrayBuffer(); +}; + +enum EndingType { "transparent", "native" }; + +dictionary BlobPropertyBag { + DOMString type = ""; + EndingType endings = "transparent"; +}; + +typedef (BufferSource or Blob or USVString) BlobPart; + +[Exposed=(Window,Worker), Serializable] +interface File : Blob { + constructor(sequence fileBits, + USVString fileName, + optional FilePropertyBag options = {}); + readonly attribute DOMString name; + readonly attribute long long lastModified; +}; + +dictionary FilePropertyBag : BlobPropertyBag { + long long lastModified; +}; + +[Exposed=(Window,Worker), Serializable] +interface FileList { + getter File? item(unsigned long index); + readonly attribute unsigned long length; +}; + +[Exposed=(Window,Worker)] +interface FileReader: EventTarget { + constructor(); + // async read methods + undefined readAsArrayBuffer(Blob blob); + undefined readAsBinaryString(Blob blob); + undefined readAsText(Blob blob, optional DOMString encoding); + undefined readAsDataURL(Blob blob); + + undefined abort(); + + // states + const unsigned short EMPTY = 0; + const unsigned short LOADING = 1; + const unsigned short DONE = 2; + + readonly attribute unsigned short readyState; + + // File or Blob data + readonly attribute (DOMString or ArrayBuffer)? result; + + readonly attribute DOMException? error; + + // event handler content attributes + attribute EventHandler onloadstart; + attribute EventHandler onprogress; + attribute EventHandler onload; + attribute EventHandler onabort; + attribute EventHandler onerror; + attribute EventHandler onloadend; +}; + +[Exposed=(DedicatedWorker,SharedWorker)] +interface FileReaderSync { + constructor(); + // Synchronously return strings + + ArrayBuffer readAsArrayBuffer(Blob blob); + DOMString readAsBinaryString(Blob blob); + DOMString readAsText(Blob blob, optional DOMString encoding); + DOMString readAsDataURL(Blob blob); +}; + +[Exposed=(Window,DedicatedWorker,SharedWorker)] +partial interface URL { + static DOMString createObjectURL((Blob or MediaSource) obj); + static undefined revokeObjectURL(DOMString url); +}; diff --git a/test/wpt/tests/interfaces/url.idl b/test/wpt/tests/interfaces/url.idl new file mode 100644 index 00000000000..360c9adcfa1 --- /dev/null +++ b/test/wpt/tests/interfaces/url.idl @@ -0,0 +1,42 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: URL Standard (https://url.spec.whatwg.org/) + +[Exposed=*, + LegacyWindowAlias=webkitURL] +interface URL { + constructor(USVString url, optional USVString base); + + stringifier attribute USVString href; + readonly attribute USVString origin; + attribute USVString protocol; + attribute USVString username; + attribute USVString password; + attribute USVString host; + attribute USVString hostname; + attribute USVString port; + attribute USVString pathname; + attribute USVString search; + [SameObject] readonly attribute URLSearchParams searchParams; + attribute USVString hash; + + USVString toJSON(); +}; + +[Exposed=*] +interface URLSearchParams { + constructor(optional (sequence> or record or USVString) init = ""); + + undefined append(USVString name, USVString value); + undefined delete(USVString name); + USVString? get(USVString name); + sequence getAll(USVString name); + boolean has(USVString name); + undefined set(USVString name, USVString value); + + undefined sort(); + + iterable; + stringifier; +}; diff --git a/types/filereader.d.ts b/types/filereader.d.ts new file mode 100644 index 00000000000..8bd7e15c242 --- /dev/null +++ b/types/filereader.d.ts @@ -0,0 +1,49 @@ +/// + +import { Blob } from 'buffer' + +export declare class FileReader extends EventTarget { + constructor () + + readAsArrayBuffer (blob: Blob): void + readAsBinaryString (blob: Blob): void + readAsText (blob: Blob, encoding?: string): void + readAsDataURL (blob: Blob): void + + abort (): void + + static readonly EMPTY = 0 + static readonly LOADING = 1 + static readonly DONE = 2 + + readonly EMPTY = 0 + readonly LOADING = 1 + readonly DONE = 2 + + readonly readyState: number + + readonly result: string | ArrayBuffer | null + + readonly error: DOMException | null + + onloadstart: null | ((this: FileReader, event: ProgressEvent) => void) + onprogress: null | ((this: FileReader, event: ProgressEvent) => void) + onload: null | ((this: FileReader, event: ProgressEvent) => void) + onabort: null | ((this: FileReader, event: ProgressEvent) => void) + onerror: null | ((this: FileReader, event: ProgressEvent) => void) + onloadend: null | ((this: FileReader, event: ProgressEvent) => void) +} + +export interface ProgressEventInit extends EventInit { + lengthComputable?: boolean + loaded?: number + total?: number +} + +export declare class ProgressEvent extends Event { + constructor (type: string, eventInitDict?: ProgressEventInit) + + readonly lengthComputable: boolean + readonly loaded: number + readonly total: number +} \ No newline at end of file