0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

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.
This commit is contained in:
Luca Casonato 2021-04-08 15:05:08 +02:00 committed by GitHub
parent d2e500e1cf
commit c867c1aa47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 526 additions and 99 deletions

View file

@ -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 frs 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 frs state to "done".
this[state] = "done";
// 2. Let result be the result of package data given bytes, type, blobs 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 frs state to "done".
this[state] = "done";
// 2. Let result be the result of package data given bytes, type, blobs 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 frs 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 frs 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 frs 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 frs 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]) {

31
op_crates/web/00_infra.js Normal file
View file

@ -0,0 +1,31 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
"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);

View file

@ -0,0 +1,242 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
"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<string, string>} */
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);

View file

@ -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);

View file

@ -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<string, string>;
} | null;
};
declare var eventTarget: {
EventTarget: typeof EventTarget;
};

View file

@ -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"),

@ -1 +1 @@
Subproject commit f897da00871cf39366bc2f0ceec051c65bc75703
Subproject commit a522daf78a71c2252d10c978f09cf0575aceb794

View file

@ -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": {