diff --git a/op_crates/file/02_filereader.js b/op_crates/file/02_filereader.js
index e398b23df3..b32cbfce93 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;
+ (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) {
- 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,
+ // 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;
- }
- switch (readtype.kind) {
- case "ArrayBuffer": {
- this[result] = bytes.buffer;
- break;
- }
- case "Text": {
- const decoder = new TextDecoder(readtype.encoding);
- this[result] = decoder.decode(bytes.buffer);
- 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.
+ // 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 ev = new ProgressEvent("load", {
- lengthComputable: true,
+ const size = chunks.reduce((p, i) => p + i.byteLength, 0);
+ const ev = new ProgressEvent("progress", {
loaded: size,
- total: size,
});
+ // TODO(lucacasonato): this is wrong, should be HTML "queue a task"
+ queueMicrotask(() => {
+ if (this[aborted]) return;
+ this.dispatchEvent(ev);
+ });
+ }
+
+ 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;
+ }
+ 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;
+ }
+ }
+ // 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);
+ }
+
+ // 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("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]) {
diff --git a/op_crates/web/00_infra.js b/op_crates/web/00_infra.js
new file mode 100644
index 0000000000..0590ffd035
--- /dev/null
+++ b/op_crates/web/00_infra.js
@@ -0,0 +1,31 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+///
+///
+///
+
+"use strict";
+
+((window) => {
+ /**
+ * https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
+ * @param {string} input
+ * @param {number} position
+ * @param {(char: string) => boolean} condition
+ * @returns {{result: string, position: number}}
+ */
+ function collectSequenceOfCodepoints(input, position, condition) {
+ const start = position;
+ for (
+ let c = input.charAt(position);
+ position < input.length && condition(c);
+ c = input.charAt(++position)
+ );
+ return { result: input.slice(start, position), position };
+ }
+
+ window.__bootstrap.infra = {
+ collectSequenceOfCodepoints,
+ };
+})(globalThis);
diff --git a/op_crates/web/01_mimesniff.js b/op_crates/web/01_mimesniff.js
new file mode 100644
index 0000000000..918343f2cf
--- /dev/null
+++ b/op_crates/web/01_mimesniff.js
@@ -0,0 +1,242 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+///
+///
+///
+
+"use strict";
+
+((window) => {
+ const { collectSequenceOfCodepoints } = window.__bootstrap.infra;
+
+ const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
+ const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
+
+ const ASCII_DIGIT = ["\u0030-\u0039"];
+ const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
+ const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
+ const ASCII_ALPHA = [...ASCII_UPPER_ALPHA, ...ASCII_LOWER_ALPHA];
+ const ASCII_ALPHANUMERIC = [...ASCII_DIGIT, ...ASCII_ALPHA];
+ const HTTP_TOKEN_CODE_POINT = [
+ "\u0021",
+ "\u0023",
+ "\u0025",
+ "\u0026",
+ "\u0027",
+ "\u002A",
+ "\u002B",
+ "\u002D",
+ "\u002E",
+ "\u005E",
+ "\u005F",
+ "\u0060",
+ "\u007C",
+ "\u007E",
+ ...ASCII_ALPHANUMERIC,
+ ];
+ const HTTP_TOKEN_CODE_POINT_RE = new RegExp(`^[${HTTP_TOKEN_CODE_POINT}]+$`);
+ const HTTP_QUOTED_STRING_TOKEN_POINT = [
+ "\u0009",
+ "\u0020-\u007E",
+ "\u0080-\u00FF",
+ ];
+ const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
+ `^[${HTTP_QUOTED_STRING_TOKEN_POINT}]+$`,
+ );
+
+ /**
+ * https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+ * @param {string} input
+ * @param {number} position
+ * @param {boolean} extractValue
+ * @returns {{result: string, position: number}}
+ */
+ function collectHttpQuotedString(input, position, extractValue) {
+ // 1.
+ const positionStart = position;
+ // 2.
+ let value = "";
+ // 3.
+ if (input[position] !== "\u0022") throw new Error('must be "');
+ // 4.
+ position++;
+ // 5.
+ while (true) {
+ // 5.1.
+ const res = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c !== "\u0022" && c !== "\u005C",
+ );
+ value += res.result;
+ position = res.position;
+ // 5.2.
+ if (position >= input.length) break;
+ // 5.3.
+ const quoteOrBackslash = input[position];
+ // 5.4.
+ position++;
+ // 5.5.
+ if (quoteOrBackslash === "\u005C") {
+ // 5.5.1.
+ if (position >= input.length) {
+ value += "\u005C";
+ break;
+ }
+ // 5.5.2.
+ value += input[position];
+ // 5.5.3.
+ position++;
+ } else { // 5.6.
+ // 5.6.1
+ if (input[position] !== "\u0022") throw new Error('must be "');
+ // 5.6.2
+ break;
+ }
+ }
+ // 6.
+ if (extractValue) return { result: value, position };
+ // 7.
+ return { result: input.substring(positionStart, position + 1), position };
+ }
+
+ /**
+ * @param {string} input
+ */
+ function parseMimeType(input) {
+ // 1.
+ input = input.replaceAll(new RegExp(`^[${HTTP_WHITESPACE}]+`, "g"), "");
+ input = input.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
+
+ // 2.
+ let position = 0;
+ const endOfInput = input.length;
+
+ // 3.
+ const res1 = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c != "\u002F",
+ );
+ const type = res1.result;
+ position = res1.position;
+
+ // 4.
+ if (type === "" || !HTTP_TOKEN_CODE_POINT_RE.test(type)) {
+ return null;
+ }
+
+ // 5.
+ if (position >= endOfInput) return null;
+
+ // 6.
+ position++;
+
+ // 7.
+ const res2 = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c != "\u003B",
+ );
+ let subtype = res2.result;
+ position = res2.position;
+
+ // 8.
+ subtype = subtype.replaceAll(new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"), "");
+
+ // 9.
+ if (subtype === "" || !HTTP_TOKEN_CODE_POINT_RE.test(subtype)) {
+ return null;
+ }
+
+ // 10.
+ const mimeType = {
+ type: type.toLowerCase(),
+ subtype: subtype.toLowerCase(),
+ /** @type {Map} */
+ parameters: new Map(),
+ };
+
+ // 11.
+ while (position < endOfInput) {
+ // 11.1.
+ position++;
+
+ // 11.2.
+ const res1 = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => HTTP_WHITESPACE.includes(c),
+ );
+ position = res1.position;
+
+ // 11.3.
+ const res2 = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c !== "\u003B" && c !== "\u003D",
+ );
+ let parameterName = res2.result;
+ position = res2.position;
+
+ // 11.4.
+ parameterName = parameterName.toLowerCase();
+
+ // 11.5.
+ if (position < endOfInput) {
+ if (input[position] == "\u003B") continue;
+ position++;
+ }
+
+ // 11.6.
+ if (position >= endOfInput) break;
+
+ // 11.7.
+ let parameterValue = null;
+
+ // 11.8.
+ if (input[position] == "\u0022") {
+ // 11.8.1.
+ const res = collectHttpQuotedString(input, position, true);
+ parameterValue = res.result;
+ position = res.position;
+
+ // 11.8.2.
+ position++;
+ } else { // 11.9.
+ // 11.9.1.
+ const res = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c !== "\u003B",
+ );
+ parameterValue = res.result;
+ position = res.position;
+
+ // 11.9.2.
+ parameterValue = parameterValue.replaceAll(
+ new RegExp(`[${HTTP_WHITESPACE}]+$`, "g"),
+ "",
+ );
+
+ // 11.9.3.
+ if (parameterValue === "") continue;
+ }
+
+ // 11.9.
+ if (
+ parameterName !== "" && HTTP_TOKEN_CODE_POINT_RE.test(parameterName) &&
+ HTTP_QUOTED_STRING_TOKEN_POINT_RE.test(parameterValue) &&
+ !mimeType.parameters.has(parameterName)
+ ) {
+ mimeType.parameters.set(parameterName, parameterValue);
+ }
+ }
+
+ // 12.
+ return mimeType;
+ }
+
+ window.__bootstrap.mimesniff = { parseMimeType };
+})(this);
diff --git a/op_crates/web/08_text_encoding.js b/op_crates/web/08_text_encoding.js
index 73cb38311c..1fda1a8167 100644
--- a/op_crates/web/08_text_encoding.js
+++ b/op_crates/web/08_text_encoding.js
@@ -4545,10 +4545,38 @@
fromByteArray,
};
+ /**
+ * @param {Uint8Array} bytes
+ */
+ function decode(bytes, encoding) {
+ const BOMEncoding = BOMSniff(bytes);
+ let start = 0;
+ if (BOMEncoding !== null) {
+ encoding = BOMEncoding;
+ if (BOMEncoding === "UTF-8") start = 3;
+ else start = 2;
+ }
+ return new TextDecoder(encoding).decode(bytes.slice(start));
+ }
+
+ /**
+ * @param {Uint8Array} bytes
+ */
+ function BOMSniff(bytes) {
+ const BOM = bytes.subarray(0, 3);
+ if (BOM[0] === 0xEF && BOM[1] === 0xBB && BOM[2] === 0xBF) {
+ return "UTF-8";
+ }
+ if (BOM[0] === 0xFE && BOM[1] === 0xFF) return "UTF-16BE";
+ if (BOM[0] === 0xFF && BOM[1] === 0xFE) return "UTF-16LE";
+ return null;
+ }
+
window.TextEncoder = TextEncoder;
window.TextDecoder = TextDecoder;
window.atob = atob;
window.btoa = btoa;
window.__bootstrap = window.__bootstrap || {};
+ window.__bootstrap.encoding = { decode };
window.__bootstrap.base64 = base64;
})(this);
diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts
index 521563810b..18220b08e6 100644
--- a/op_crates/web/internal.d.ts
+++ b/op_crates/web/internal.d.ts
@@ -5,6 +5,25 @@
declare namespace globalThis {
declare namespace __bootstrap {
+ declare var infra: {
+ collectSequenceOfCodepoints(
+ input: string,
+ position: number,
+ condition: (char: string) => boolean,
+ ): {
+ result: string;
+ position: number;
+ };
+ };
+
+ declare var mimesniff: {
+ parseMimeType(input: string): {
+ type: string;
+ subtype: string;
+ parameters: Map;
+ } | null;
+ };
+
declare var eventTarget: {
EventTarget: typeof EventTarget;
};
diff --git a/op_crates/web/lib.rs b/op_crates/web/lib.rs
index 8ee944d74c..a609dc4cd3 100644
--- a/op_crates/web/lib.rs
+++ b/op_crates/web/lib.rs
@@ -6,10 +6,18 @@ use std::path::PathBuf;
/// Load and execute the javascript code.
pub fn init(isolate: &mut JsRuntime) {
let files = vec![
+ (
+ "deno:op_crates/web/00_infra.js",
+ include_str!("00_infra.js"),
+ ),
(
"deno:op_crates/web/01_dom_exception.js",
include_str!("01_dom_exception.js"),
),
+ (
+ "deno:op_crates/web/01_mimesniff.js",
+ include_str!("01_mimesniff.js"),
+ ),
(
"deno:op_crates/web/02_event.js",
include_str!("02_event.js"),
diff --git a/test_util/wpt b/test_util/wpt
index f897da0087..a522daf78a 160000
--- a/test_util/wpt
+++ b/test_util/wpt
@@ -1 +1 @@
-Subproject commit f897da00871cf39366bc2f0ceec051c65bc75703
+Subproject commit a522daf78a71c2252d10c978f09cf0575aceb794
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 4f04213aa3..632af480a5 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -787,6 +787,29 @@
"fileReader.any.js": true,
"url": {
"url-format.any.js": true
+ },
+ "reading-data-section": {
+ "Determining-Encoding.any.js": true,
+ "FileReader-event-handler-attributes.any.js": true,
+ "FileReader-multiple-reads.any.js": true,
+ "filereader_abort.any.js": true,
+ "filereader_error.any.js": true,
+ "filereader_events.any.js": false,
+ "filereader_readAsArrayBuffer.any.js": true,
+ "filereader_readAsBinaryString.any.js": true,
+ "filereader_readAsDataURL.any.js": true,
+ "filereader_readAsText.any.js": true,
+ "filereader_readystate.any.js": true,
+ "filereader_result.any.js": [
+ "result is null during \"loadstart\" event for readAsText",
+ "result is null during \"loadstart\" event for readAsDataURL",
+ "result is null during \"loadstart\" event for readAsArrayBuffer",
+ "result is null during \"loadstart\" event for readAsBinaryString",
+ "result is null during \"progress\" event for readAsText",
+ "result is null during \"progress\" event for readAsDataURL",
+ "result is null during \"progress\" event for readAsArrayBuffer",
+ "result is null during \"progress\" event for readAsBinaryString"
+ ]
}
},
"html": {