diff options
Diffstat (limited to 'op_crates/file')
-rw-r--r-- | op_crates/file/01_file.js | 344 | ||||
-rw-r--r-- | op_crates/file/02_filereader.js | 334 | ||||
-rw-r--r-- | op_crates/file/Cargo.toml | 17 | ||||
-rw-r--r-- | op_crates/file/README.md | 5 | ||||
-rw-r--r-- | op_crates/file/internal.d.ts | 18 | ||||
-rw-r--r-- | op_crates/file/lib.deno_file.d.ts | 40 | ||||
-rw-r--r-- | op_crates/file/lib.rs | 22 |
7 files changed, 780 insertions, 0 deletions
diff --git a/op_crates/file/01_file.js b/op_crates/file/01_file.js new file mode 100644 index 000000000..23886fbd5 --- /dev/null +++ b/op_crates/file/01_file.js @@ -0,0 +1,344 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference no-default-lib="true" /> +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../web/internal.d.ts" /> +/// <reference path="../web/lib.deno_web.d.ts" /> +/// <reference path="./internal.d.ts" /> +/// <reference path="./lib.deno_file.d.ts" /> +/// <reference lib="esnext" /> +"use strict"; + +((window) => { + // TODO(lucacasonato): this needs to not be hardcoded and instead depend on + // host os. + const isWindows = false; + + /** + * @param {string} input + * @param {number} position + * @returns {{result: string, position: number}} + */ + function collectCodepointsNotCRLF(input, position) { + // See https://w3c.github.io/FileAPI/#convert-line-endings-to-native and + // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points + const start = position; + for ( + let c = input.charAt(position); + position < input.length && !(c === "\r" || c === "\n"); + c = input.charAt(++position) + ); + return { result: input.slice(start, position), position }; + } + + /** + * @param {string} s + * @returns {string} + */ + function convertLineEndingsToNative(s) { + const nativeLineEnding = isWindows ? "\r\n" : "\n"; + + let { result, position } = collectCodepointsNotCRLF(s, 0); + + while (position < s.length) { + const codePoint = s.charAt(position); + if (codePoint === "\r") { + result += nativeLineEnding; + position++; + if (position < s.length && s.charAt(position) === "\n") { + position++; + } + } else if (codePoint === "\n") { + position++; + result += nativeLineEnding; + } + const { result: token, position: newPosition } = collectCodepointsNotCRLF( + s, + position, + ); + position = newPosition; + result += token; + } + + return result; + } + + /** + * @param {...Uint8Array} bytesArrays + * @returns {Uint8Array} + */ + function concatUint8Arrays(...bytesArrays) { + let byteLength = 0; + for (const bytes of bytesArrays) { + byteLength += bytes.byteLength; + } + const finalBytes = new Uint8Array(byteLength); + let current = 0; + for (const bytes of bytesArrays) { + finalBytes.set(bytes, current); + current += bytes.byteLength; + } + return finalBytes; + } + + const utf8Encoder = new TextEncoder(); + const utf8Decoder = new TextDecoder(); + + /** @typedef {BufferSource | Blob | string} BlobPart */ + + /** + * @param {BlobPart[]} parts + * @param {string} endings + * @returns {Uint8Array} + */ + function processBlobParts(parts, endings) { + /** @type {Uint8Array[]} */ + const bytesArrays = []; + for (const element of parts) { + if (element instanceof ArrayBuffer) { + bytesArrays.push(new Uint8Array(element.slice(0))); + } else if (ArrayBuffer.isView(element)) { + const buffer = element.buffer.slice( + element.byteOffset, + element.byteOffset + element.byteLength, + ); + bytesArrays.push(new Uint8Array(buffer)); + } else if (element instanceof Blob) { + bytesArrays.push( + new Uint8Array(element[_byteSequence].buffer.slice(0)), + ); + } else if (typeof element === "string") { + let s = element; + if (endings == "native") { + s = convertLineEndingsToNative(s); + } + bytesArrays.push(utf8Encoder.encode(s)); + } else { + throw new TypeError("Unreachable code (invalild element type)"); + } + } + return concatUint8Arrays(...bytesArrays); + } + + /** + * @param {string} str + * @returns {string} + */ + function normalizeType(str) { + let normalizedType = str; + if (!/^[\x20-\x7E]*$/.test(str)) { + normalizedType = ""; + } + return normalizedType.toLowerCase(); + } + + const _byteSequence = Symbol("[[ByteSequence]]"); + + class Blob { + /** @type {string} */ + #type; + + /** @type {Uint8Array} */ + [_byteSequence]; + + /** + * @param {BlobPart[]} [blobParts] + * @param {BlobPropertyBag} [options] + */ + constructor(blobParts, options) { + if (blobParts === undefined) { + blobParts = []; + } + if (typeof blobParts !== "object") { + throw new TypeError( + `Failed to construct 'Blob'. blobParts cannot be converted to a sequence.`, + ); + } + + const parts = []; + const iterator = blobParts[Symbol.iterator]?.(); + if (iterator === undefined) { + throw new TypeError( + "Failed to construct 'Blob'. The provided value cannot be converted to a sequence", + ); + } + while (true) { + const { value: element, done } = iterator.next(); + if (done) break; + if ( + ArrayBuffer.isView(element) || element instanceof ArrayBuffer || + element instanceof Blob + ) { + parts.push(element); + } else { + parts.push(String(element)); + } + } + + if (!options || typeof options === "function") { + options = {}; + } + if (typeof options !== "object") { + throw new TypeError( + `Failed to construct 'Blob'. options is not an object.`, + ); + } + const endings = options.endings?.toString() ?? "transparent"; + const type = options.type?.toString() ?? ""; + + /** @type {Uint8Array} */ + this[_byteSequence] = processBlobParts(parts, endings); + this.#type = normalizeType(type); + } + + /** @returns {number} */ + get size() { + return this[_byteSequence].byteLength; + } + + /** @returns {string} */ + get type() { + return this.#type; + } + + /** + * @param {number} [start] + * @param {number} [end] + * @param {string} [contentType] + * @returns {Blob} + */ + slice(start, end, contentType) { + const O = this; + /** @type {number} */ + let relativeStart; + if (start === undefined) { + relativeStart = 0; + } else { + start = Number(start); + if (start < 0) { + relativeStart = Math.max(O.size + start, 0); + } else { + relativeStart = Math.min(start, O.size); + } + } + /** @type {number} */ + let relativeEnd; + if (end === undefined) { + relativeEnd = O.size; + } else { + end = Number(end); + if (end < 0) { + relativeEnd = Math.max(O.size + end, 0); + } else { + relativeEnd = Math.min(end, O.size); + } + } + /** @type {string} */ + let relativeContentType; + if (contentType === undefined) { + relativeContentType = ""; + } else { + relativeContentType = normalizeType(String(contentType)); + } + return new Blob([ + O[_byteSequence].buffer.slice(relativeStart, relativeEnd), + ], { type: relativeContentType }); + } + + /** + * @returns {ReadableStream<Uint8Array>} + */ + stream() { + const bytes = this[_byteSequence]; + const stream = new ReadableStream({ + type: "bytes", + /** @param {ReadableByteStreamController} controller */ + start(controller) { + const chunk = new Uint8Array(bytes.buffer.slice(0)); + if (chunk.byteLength > 0) controller.enqueue(chunk); + controller.close(); + }, + }); + return stream; + } + + /** + * @returns {Promise<string>} + */ + async text() { + const buffer = await this.arrayBuffer(); + return utf8Decoder.decode(buffer); + } + + /** + * @returns {Promise<ArrayBuffer>} + */ + async arrayBuffer() { + const stream = this.stream(); + let bytes = new Uint8Array(); + for await (const chunk of stream) { + bytes = concatUint8Arrays(bytes, chunk); + } + return bytes.buffer; + } + + get [Symbol.toStringTag]() { + return "Blob"; + } + } + + const _Name = Symbol("[[Name]]"); + const _LastModfied = Symbol("[[LastModified]]"); + + class File extends Blob { + /** @type {string} */ + [_Name]; + /** @type {number} */ + [_LastModfied]; + + /** + * @param {BlobPart[]} fileBits + * @param {string} fileName + * @param {FilePropertyBag} [options] + */ + constructor(fileBits, fileName, options) { + if (fileBits === undefined) { + throw new TypeError( + "Failed to construct 'File'. 2 arguments required, but first not specified.", + ); + } + if (fileName === undefined) { + throw new TypeError( + "Failed to construct 'File'. 2 arguments required, but second not specified.", + ); + } + super(fileBits, { endings: options?.endings, type: options?.type }); + /** @type {string} */ + this[_Name] = String(fileName).replaceAll("/", ":"); + if (options?.lastModified === undefined) { + /** @type {number} */ + this[_LastModfied] = new Date().getTime(); + } else { + /** @type {number} */ + this[_LastModfied] = Number(options.lastModified); + } + } + + /** @returns {string} */ + get name() { + return this[_Name]; + } + + /** @returns {number} */ + get lastModified() { + return this[_LastModfied]; + } + } + + window.__bootstrap.file = { + Blob, + _byteSequence, + File, + }; +})(this); diff --git a/op_crates/file/02_filereader.js b/op_crates/file/02_filereader.js new file mode 100644 index 000000000..e398b23df --- /dev/null +++ b/op_crates/file/02_filereader.js @@ -0,0 +1,334 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// <reference no-default-lib="true" /> +/// <reference path="../../core/lib.deno_core.d.ts" /> +/// <reference path="../webidl/internal.d.ts" /> +/// <reference path="../web/internal.d.ts" /> +/// <reference path="../web/lib.deno_web.d.ts" /> +/// <reference path="./internal.d.ts" /> +/// <reference path="./lib.deno_file.d.ts" /> +/// <reference lib="esnext" /> + +"use strict"; + +((window) => { + const webidl = window.__bootstrap.webidl; + const base64 = window.__bootstrap.base64; + + const state = Symbol("[[state]]"); + const result = Symbol("[[result]]"); + const error = Symbol("[[error]]"); + const aborted = Symbol("[[aborted]]"); + + class FileReader extends EventTarget { + /** @type {"empty" | "loading" | "done"} */ + [state] = "empty"; + /** @type {null | string | ArrayBuffer} */ + [result] = null; + /** @type {null | DOMException} */ + [error] = null; + + [aborted] = false; + + /** + * @param {Blob} blob + * @param {{kind: "ArrayBuffer" | "Text" | "DataUrl", encoding?: string}} readtype + */ + #readOperation = async (blob, readtype) => { + // 1. If fr’s state is "loading", throw an InvalidStateError DOMException. + if (this[state] === "loading") { + throw new DOMException( + "Invalid FileReader state.", + "InvalidStateError", + ); + } + // 2. Set fr’s state to "loading". + this[state] = "loading"; + // 3. Set fr’s result to null. + this[result] = null; + // 4. Set fr’s error to null. + this[error] = null; + + // 5. Let stream be the result of calling get stream on blob. + const stream /*: ReadableStream<ArrayBufferView>*/ = 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 chunks = []; + + // 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 + 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, + }); + 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) { + 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. + { + 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) { + 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; + } + + /** @returns {number} */ + get readyState() { + webidl.assertBranded(this, FileReader); + switch (this[state]) { + case "empty": + return FileReader.EMPTY; + case "loading": + return FileReader.LOADING; + case "done": + return FileReader.DONE; + default: + throw new TypeError("Invalid state"); + } + } + + get result() { + webidl.assertBranded(this, FileReader); + return this[result]; + } + + get error() { + webidl.assertBranded(this, FileReader); + return this[error]; + } + + abort() { + webidl.assertBranded(this, FileReader); + // If context object's state is "empty" or if context object's state is "done" set context object's result to null and terminate this algorithm. + if ( + this[state] === "empty" || + this[state] === "done" + ) { + this[result] = null; + return; + } + // If context object's state is "loading" set context object's state to "done" and set context object's result to null. + if (this[state] === "loading") { + this[state] = "done"; + this[result] = null; + } + // If there are any tasks from the context object on the file reading task source in an affiliated task queue, then remove those tasks from that task queue. + // Terminate the algorithm for the read method being processed. + this[aborted] = true; + + // Fire a progress event called abort at the context object. + const ev = new ProgressEvent("abort", {}); + this.dispatchEvent(ev); + + // If context object's state is not "loading", fire a progress event called loadend at the context object. + if (this[state] !== "loading") { + const ev = new ProgressEvent("loadend", {}); + this.dispatchEvent(ev); + } + } + + /** @param {Blob} blob */ + readAsArrayBuffer(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsArrayBuffer' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + this.#readOperation(blob, { kind: "ArrayBuffer" }); + } + + /** @param {Blob} blob */ + readAsBinaryString(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "ArrayBuffer" }); + } + + /** @param {Blob} blob */ + readAsDataURL(blob) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "DataUrl" }); + } + + /** + * @param {Blob} blob + * @param {string} [encoding] + */ + readAsText(blob, encoding) { + webidl.assertBranded(this, FileReader); + const prefix = "Failed to execute 'readAsBinaryString' on 'FileReader'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + if (encoding !== undefined) { + encoding = webidl.converters["DOMString"](encoding, { + prefix, + context: "Argument 2", + }); + } + // alias for readAsArrayBuffer + this.#readOperation(blob, { kind: "Text", encoding }); + } + } + + const handlerSymbol = Symbol("eventHandlers"); + + function makeWrappedHandler(handler) { + function wrappedHandler(...args) { + if (typeof wrappedHandler.handler !== "function") { + return; + } + return wrappedHandler.handler.call(this, ...args); + } + wrappedHandler.handler = handler; + return wrappedHandler; + } + // TODO(benjamingr) reuse when we can reuse code between web crates + function defineEventHandler(emitter, name) { + // HTML specification section 8.1.5.1 + Object.defineProperty(emitter, `on${name}`, { + get() { + return this[handlerSymbol]?.get(name)?.handler; + }, + set(value) { + if (!this[handlerSymbol]) { + this[handlerSymbol] = new Map(); + } + let handlerWrapper = this[handlerSymbol]?.get(name); + if (handlerWrapper) { + handlerWrapper.handler = value; + } else { + handlerWrapper = makeWrappedHandler(value); + this.addEventListener(name, handlerWrapper); + } + this[handlerSymbol].set(name, handlerWrapper); + }, + configurable: true, + enumerable: true, + }); + } + defineEventHandler(FileReader.prototype, "error"); + defineEventHandler(FileReader.prototype, "loadstart"); + defineEventHandler(FileReader.prototype, "load"); + defineEventHandler(FileReader.prototype, "loadend"); + defineEventHandler(FileReader.prototype, "progress"); + defineEventHandler(FileReader.prototype, "abort"); + + window.__bootstrap.fileReader = { + FileReader, + }; +})(this); diff --git a/op_crates/file/Cargo.toml b/op_crates/file/Cargo.toml new file mode 100644 index 000000000..54476c783 --- /dev/null +++ b/op_crates/file/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_file" +version = "0.1.0" +edition = "2018" +description = "File API implementation for Deno" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.83.0", path = "../../core" } diff --git a/op_crates/file/README.md b/op_crates/file/README.md new file mode 100644 index 000000000..c421bf004 --- /dev/null +++ b/op_crates/file/README.md @@ -0,0 +1,5 @@ +# deno_file + +This crate implements the File API. + +Spec: https://w3c.github.io/FileAPI diff --git a/op_crates/file/internal.d.ts b/op_crates/file/internal.d.ts new file mode 100644 index 000000000..91a61d811 --- /dev/null +++ b/op_crates/file/internal.d.ts @@ -0,0 +1,18 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +/// <reference no-default-lib="true" /> +/// <reference lib="esnext" /> + +declare namespace globalThis { + declare namespace __bootstrap { + declare var file: { + Blob: typeof Blob & { + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; + }; + _byteSequence: unique symbol; + File: typeof File & { + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; + }; + }; + } +} diff --git a/op_crates/file/lib.deno_file.d.ts b/op_crates/file/lib.deno_file.d.ts new file mode 100644 index 000000000..a907c3f50 --- /dev/null +++ b/op_crates/file/lib.deno_file.d.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +/// <reference no-default-lib="true" /> +/// <reference lib="esnext" /> + +type BlobPart = BufferSource | Blob | string; + +interface BlobPropertyBag { + type?: string; + endings?: "transparent" | "native"; +} + +/** A file-like object of immutable, raw data. Blobs represent data that isn't necessarily in a JavaScript-native format. The File interface is based on Blob, inheriting blob functionality and expanding it to support files on the user's system. */ +declare class Blob { + constructor(blobParts?: BlobPart[], options?: BlobPropertyBag); + + readonly size: number; + readonly type: string; + arrayBuffer(): Promise<ArrayBuffer>; + slice(start?: number, end?: number, contentType?: string): Blob; + stream(): ReadableStream<Uint8Array>; + text(): Promise<string>; +} + +interface FilePropertyBag extends BlobPropertyBag { + lastModified?: number; +} + +/** Provides information about files and allows JavaScript in a web page to + * access their content. */ +declare class File extends Blob { + constructor( + fileBits: BlobPart[], + fileName: string, + options?: FilePropertyBag, + ); + + readonly lastModified: number; + readonly name: string; +} diff --git a/op_crates/file/lib.rs b/op_crates/file/lib.rs new file mode 100644 index 000000000..c7e690433 --- /dev/null +++ b/op_crates/file/lib.rs @@ -0,0 +1,22 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +use deno_core::JsRuntime; +use std::path::PathBuf; + +/// Load and execute the javascript code. +pub fn init(isolate: &mut JsRuntime) { + let files = vec![ + ("deno:op_crates/file/01_file.js", include_str!("01_file.js")), + ( + "deno:op_crates/file/02_filereader.js", + include_str!("02_filereader.js"), + ), + ]; + for (url, source_code) in files { + isolate.execute(url, source_code).unwrap(); + } +} + +pub fn get_declaration() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_file.d.ts") +} |