summaryrefslogtreecommitdiff
path: root/op_crates/file/02_filereader.js
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2021-04-08 15:05:08 +0200
committerGitHub <noreply@github.com>2021-04-08 15:05:08 +0200
commitc867c1aa476b3f00933be08cbc5f528973e37857 (patch)
tree4966a8a2e853db1c552ec39821a65bc13f927d5a /op_crates/file/02_filereader.js
parentd2e500e1cf7d27132fee92cc09238e1bc98897c6 (diff)
fix: enable FileReader wpt and align to spec (#10063)
This adds some algorithms from the whatwg mimesniff, whatwg infra, and whatwg encoding specs that FileReader needs to use internally.
Diffstat (limited to 'op_crates/file/02_filereader.js')
-rw-r--r--op_crates/file/02_filereader.js270
1 files changed, 173 insertions, 97 deletions
diff --git a/op_crates/file/02_filereader.js b/op_crates/file/02_filereader.js
index e398b23df..b32cbfce9 100644
--- a/op_crates/file/02_filereader.js
+++ b/op_crates/file/02_filereader.js
@@ -14,6 +14,8 @@
((window) => {
const webidl = window.__bootstrap.webidl;
+ const { decode } = window.__bootstrap.encoding;
+ const { parseMimeType } = window.__bootstrap.mimesniff;
const base64 = window.__bootstrap.base64;
const state = Symbol("[[state]]");
@@ -33,9 +35,9 @@
/**
* @param {Blob} blob
- * @param {{kind: "ArrayBuffer" | "Text" | "DataUrl", encoding?: string}} readtype
+ * @param {{kind: "ArrayBuffer" | "Text" | "DataUrl" | "BinaryString", encoding?: string}} readtype
*/
- #readOperation = async (blob, readtype) => {
+ #readOperation = (blob, readtype) => {
// 1. If fr’s state is "loading", throw an InvalidStateError DOMException.
if (this[state] === "loading") {
throw new DOMException(
@@ -67,119 +69,156 @@
let isFirstChunk = true;
// 10 in parallel while true
- while (!this[aborted]) {
- // 1. Wait for chunkPromise to be fulfilled or rejected.
- try {
- const chunk = await chunkPromise;
- if (this[aborted]) return;
-
- // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr.
- if (isFirstChunk) {
- queueMicrotask(() => {
- // fire a progress event for loadstart
- const ev = new ProgressEvent("loadstart", {});
- this.dispatchEvent(ev);
- });
- }
- // 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 (!chunk.done && chunk.value instanceof Uint8Array) {
- chunks.push(chunk.value);
-
- // TODO(bartlomieju): (only) If roughly 50ms have passed since last progress
- {
- const size = chunks.reduce((p, i) => p + i.byteLength, 0);
- const ev = new ProgressEvent("progress", {
- loaded: size,
+ (async () => {
+ while (!this[aborted]) {
+ // 1. Wait for chunkPromise to be fulfilled or rejected.
+ try {
+ const chunk = await chunkPromise;
+ if (this[aborted]) return;
+
+ // 2. If chunkPromise is fulfilled, and isFirstChunk is true, queue a task to fire a progress event called loadstart at fr.
+ if (isFirstChunk) {
+ // TODO(lucacasonato): this is wrong, should be HTML "queue a task"
+ queueMicrotask(() => {
+ if (this[aborted]) return;
+ // fire a progress event for loadstart
+ const ev = new ProgressEvent("loadstart", {});
+ this.dispatchEvent(ev);
});
- this.dispatchEvent(ev);
}
+ // 3. Set isFirstChunk to false.
+ isFirstChunk = false;
- chunkPromise = reader.read();
- } // 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:
- else if (chunk.done === true) {
- queueMicrotask(() => {
- // 1. Set fr’s state to "done".
- this[state] = "done";
- // 2. Let result be the result of package data given bytes, type, blob’s type, and encodingName.
- const size = chunks.reduce((p, i) => p + i.byteLength, 0);
- const bytes = new Uint8Array(size);
- let offs = 0;
- for (const chunk of chunks) {
- bytes.set(chunk, offs);
- offs += chunk.byteLength;
+ // 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 (!chunk.done && chunk.value instanceof Uint8Array) {
+ chunks.push(chunk.value);
+
+ // TODO(bartlomieju): (only) If roughly 50ms have passed since last progress
+ {
+ const size = chunks.reduce((p, i) => p + i.byteLength, 0);
+ const ev = new ProgressEvent("progress", {
+ loaded: size,
+ });
+ // TODO(lucacasonato): this is wrong, should be HTML "queue a task"
+ queueMicrotask(() => {
+ if (this[aborted]) return;
+ this.dispatchEvent(ev);
+ });
}
- switch (readtype.kind) {
- case "ArrayBuffer": {
- this[result] = bytes.buffer;
- break;
+
+ chunkPromise = reader.read();
+ } // 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:
+ else if (chunk.done === true) {
+ // TODO(lucacasonato): this is wrong, should be HTML "queue a task"
+ queueMicrotask(() => {
+ if (this[aborted]) return;
+ // 1. Set fr’s state to "done".
+ this[state] = "done";
+ // 2. Let result be the result of package data given bytes, type, blob’s type, and encodingName.
+ const size = chunks.reduce((p, i) => p + i.byteLength, 0);
+ const bytes = new Uint8Array(size);
+ let offs = 0;
+ for (const chunk of chunks) {
+ bytes.set(chunk, offs);
+ offs += chunk.byteLength;
}
- case "Text": {
- const decoder = new TextDecoder(readtype.encoding);
- this[result] = decoder.decode(bytes.buffer);
- break;
+ switch (readtype.kind) {
+ case "ArrayBuffer": {
+ this[result] = bytes.buffer;
+ break;
+ }
+ case "BinaryString":
+ this[result] = [...new Uint8Array(bytes.buffer)].map((v) =>
+ String.fromCodePoint(v)
+ ).join("");
+ break;
+ case "Text": {
+ let decoder = undefined;
+ if (readtype.encoding) {
+ try {
+ decoder = new TextDecoder(readtype.encoding);
+ } catch {
+ // don't care about the error
+ }
+ }
+ if (decoder === undefined) {
+ const mimeType = parseMimeType(blob.type);
+ if (mimeType) {
+ const charset = mimeType.parameters.get("charset");
+ if (charset) {
+ try {
+ decoder = new TextDecoder(charset);
+ } catch {
+ // don't care about the error
+ }
+ }
+ }
+ }
+ if (decoder === undefined) {
+ decoder = new TextDecoder();
+ }
+ this[result] = decode(bytes, decoder.encoding);
+ break;
+ }
+ case "DataUrl": {
+ const mediaType = blob.type || "application/octet-stream";
+ this[result] = `data:${mediaType};base64,${
+ base64.fromByteArray(bytes)
+ }`;
+ break;
+ }
}
- case "DataUrl": {
- this[result] = "data:application/octet-stream;base64," +
- base64.fromByteArray(bytes);
- break;
+ // 4.2 Fire a progress event called load at the fr.
+ {
+ const ev = new ProgressEvent("load", {
+ lengthComputable: true,
+ loaded: size,
+ total: size,
+ });
+ this.dispatchEvent(ev);
}
- }
- // 4.2 Fire a progress event called load at the fr.
+
+ // 5. If fr’s state is not "loading", fire a progress event called loadend at the fr.
+ //Note: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired.
+ if (this[state] !== "loading") {
+ const ev = new ProgressEvent("loadend", {
+ lengthComputable: true,
+ loaded: size,
+ total: size,
+ });
+ this.dispatchEvent(ev);
+ }
+ });
+ break;
+ }
+ } catch (err) {
+ // TODO(lucacasonato): this is wrong, should be HTML "queue a task"
+ queueMicrotask(() => {
+ if (this[aborted]) return;
+
+ // chunkPromise rejected
+ this[state] = "done";
+ this[error] = err;
+
{
- const ev = new ProgressEvent("load", {
- lengthComputable: true,
- loaded: size,
- total: size,
- });
+ const ev = new ProgressEvent("error", {});
this.dispatchEvent(ev);
}
- // 5. If fr’s state is not "loading", fire a progress event called loadend at the fr.
- //Note: Event handler for the load or error events could have started another load, if that happens the loadend event for this load is not fired.
+ //If fr’s state is not "loading", fire a progress event called loadend at fr.
+ //Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired.
if (this[state] !== "loading") {
- const ev = new ProgressEvent("loadend", {
- lengthComputable: true,
- loaded: size,
- total: size,
- });
+ const ev = new ProgressEvent("loadend", {});
this.dispatchEvent(ev);
}
});
-
break;
}
- } catch (err) {
- if (this[aborted]) return;
-
- // chunkPromise rejected
- this[state] = "done";
- this[error] = err;
-
- {
- const ev = new ProgressEvent("error", {});
- this.dispatchEvent(ev);
- }
-
- //If fr’s state is not "loading", fire a progress event called loadend at fr.
- //Note: Event handler for the error event could have started another load, if that happens the loadend event for this load is not fired.
- if (this[state] !== "loading") {
- const ev = new ProgressEvent("loadend", {});
- this.dispatchEvent(ev);
- }
-
- break;
}
- }
+ })();
};
- static EMPTY = 0;
- static LOADING = 1;
- static DONE = 2;
-
constructor() {
super();
this[webidl.brand] = webidl.brand;
@@ -254,7 +293,7 @@
const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'";
webidl.requiredArguments(arguments.length, 1, { prefix });
// alias for readAsArrayBuffer
- this.#readOperation(blob, { kind: "ArrayBuffer" });
+ this.#readOperation(blob, { kind: "BinaryString" });
}
/** @param {Blob} blob */
@@ -285,6 +324,43 @@
}
}
+ Object.defineProperty(FileReader, "EMPTY", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 0,
+ });
+ Object.defineProperty(FileReader, "LOADING", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 1,
+ });
+ Object.defineProperty(FileReader, "DONE", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 2,
+ });
+ Object.defineProperty(FileReader.prototype, "EMPTY", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 0,
+ });
+ Object.defineProperty(FileReader.prototype, "LOADING", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 1,
+ });
+ Object.defineProperty(FileReader.prototype, "DONE", {
+ writable: false,
+ enumerable: true,
+ configurable: false,
+ value: 2,
+ });
+
const handlerSymbol = Symbol("eventHandlers");
function makeWrappedHandler(handler) {
@@ -302,7 +378,7 @@
// HTML specification section 8.1.5.1
Object.defineProperty(emitter, `on${name}`, {
get() {
- return this[handlerSymbol]?.get(name)?.handler;
+ return this[handlerSymbol]?.get(name)?.handler ?? null;
},
set(value) {
if (!this[handlerSymbol]) {