From 25b35be50dd59d00e126591dd24d06824e2c50cf Mon Sep 17 00:00:00 2001 From: Luca Casonato Date: Thu, 4 Feb 2021 15:05:36 +0100 Subject: refactor: rewrite File implementation (#9334) --- op_crates/fetch/21_blob.js | 294 ------------------------------------ op_crates/fetch/21_file.js | 343 ++++++++++++++++++++++++++++++++++++++++++ op_crates/fetch/26_fetch.js | 36 +---- op_crates/fetch/internal.d.ts | 7 +- op_crates/fetch/lib.rs | 4 +- 5 files changed, 356 insertions(+), 328 deletions(-) delete mode 100644 op_crates/fetch/21_blob.js create mode 100644 op_crates/fetch/21_file.js (limited to 'op_crates') diff --git a/op_crates/fetch/21_blob.js b/op_crates/fetch/21_blob.js deleted file mode 100644 index 552441b21..000000000 --- a/op_crates/fetch/21_blob.js +++ /dev/null @@ -1,294 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. - -// @ts-check -/// -/// -/// -/// -/// -/// -/// - -((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} - */ - 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} - */ - async text() { - const buffer = await this.arrayBuffer(); - return utf8Decoder.decode(buffer); - } - - /** - * @returns {Promise} - */ - 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"; - } - } - - window.__bootstrap.blob = { - Blob, - _byteSequence, - }; -})(this); diff --git a/op_crates/fetch/21_file.js b/op_crates/fetch/21_file.js new file mode 100644 index 000000000..d5160ece2 --- /dev/null +++ b/op_crates/fetch/21_file.js @@ -0,0 +1,343 @@ +// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. + +// @ts-check +/// +/// +/// +/// +/// +/// +/// + +((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} + */ + 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} + */ + async text() { + const buffer = await this.arrayBuffer(); + return utf8Decoder.decode(buffer); + } + + /** + * @returns {Promise} + */ + 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/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js index 47d701f3c..52ae91e83 100644 --- a/op_crates/fetch/26_fetch.js +++ b/op_crates/fetch/26_fetch.js @@ -21,7 +21,7 @@ window.__bootstrap.streams; const { DomIterableMixin } = window.__bootstrap.domIterable; const { Headers } = window.__bootstrap.headers; - const { Blob, _byteSequence } = window.__bootstrap.blob; + const { Blob, _byteSequence, File } = window.__bootstrap.file; const MAX_SIZE = 2 ** 32 - 2; @@ -226,42 +226,19 @@ const dataSymbol = Symbol("data"); - class DomFile extends Blob { - /** - * @param {globalThis.BlobPart[]} fileBits - * @param {string} fileName - * @param {FilePropertyBag | undefined} options - */ - constructor( - fileBits, - fileName, - options, - ) { - const { lastModified = Date.now(), ...blobPropertyBag } = options ?? {}; - super(fileBits, blobPropertyBag); - - // 4.1.2.1 Replace any "/" character (U+002F SOLIDUS) - // with a ":" (U + 003A COLON) - this.name = String(fileName).replace(/\u002F/g, "\u003A"); - // 4.1.3.3 If lastModified is not provided, set lastModified to the current - // date and time represented in number of milliseconds since the Unix Epoch. - this.lastModified = lastModified; - } - } - /** * @param {Blob | string} value * @param {string | undefined} filename * @returns {FormDataEntryValue} */ function parseFormDataValue(value, filename) { - if (value instanceof DomFile) { - return new DomFile([value], filename || value.name, { + if (value instanceof File) { + return new File([value], filename || value.name, { type: value.type, lastModified: value.lastModified, }); } else if (value instanceof Blob) { - return new DomFile([value], filename || "blob", { + return new File([value], filename || "blob", { type: value.type, }); } else { @@ -408,7 +385,7 @@ */ getBody() { for (const [fieldName, fieldValue] of this.formData.entries()) { - if (fieldValue instanceof DomFile) { + if (fieldValue instanceof File) { this.#writeFile(fieldName, fieldValue); } else this.#writeField(fieldName, fieldValue); } @@ -487,7 +464,7 @@ /** * @param {string} field - * @param {DomFile} value + * @param {File} value * @returns {void} */ #writeFile = (field, value) => { @@ -1493,7 +1470,6 @@ } window.__bootstrap.fetch = { - File: DomFile, FormData, setBaseUrl, fetch, diff --git a/op_crates/fetch/internal.d.ts b/op_crates/fetch/internal.d.ts index 5fb30f503..a474d499c 100644 --- a/op_crates/fetch/internal.d.ts +++ b/op_crates/fetch/internal.d.ts @@ -19,11 +19,14 @@ declare namespace globalThis { Headers: typeof Headers; }; - declare var blob: { + declare var file: { Blob: typeof Blob & { - [globalThis.__bootstrap.blob._byteSequence]: Uint8Array; + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; }; _byteSequence: unique symbol; + File: typeof File & { + [globalThis.__bootstrap.file._byteSequence]: Uint8Array; + }; }; declare var streams: { diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs index 23f356a96..157ce2fb2 100644 --- a/op_crates/fetch/lib.rs +++ b/op_crates/fetch/lib.rs @@ -67,8 +67,8 @@ pub fn init(isolate: &mut JsRuntime) { include_str!("20_headers.js"), ), ( - "deno:op_crates/fetch/21_blob.js", - include_str!("21_blob.js"), + "deno:op_crates/fetch/21_file.js", + include_str!("21_file.js"), ), ( "deno:op_crates/fetch/26_fetch.js", -- cgit v1.2.3