mirror of
https://github.com/denoland/deno.git
synced 2024-12-14 19:37:59 -05:00
640 lines
17 KiB
JavaScript
640 lines
17 KiB
JavaScript
// Copyright 2018-2023 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="../../core/internal.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 lib="esnext" />
|
|
|
|
const core = globalThis.Deno.core;
|
|
const ops = core.ops;
|
|
import * as webidl from "ext:deno_webidl/00_webidl.js";
|
|
import { ReadableStream } from "ext:deno_web/06_streams.js";
|
|
const primordials = globalThis.__bootstrap.primordials;
|
|
const {
|
|
ArrayBufferPrototype,
|
|
ArrayBufferPrototypeSlice,
|
|
ArrayBufferIsView,
|
|
ArrayPrototypePush,
|
|
AsyncGeneratorPrototypeNext,
|
|
Date,
|
|
DatePrototypeGetTime,
|
|
FinalizationRegistry,
|
|
MathMax,
|
|
MathMin,
|
|
ObjectPrototypeIsPrototypeOf,
|
|
RegExpPrototypeTest,
|
|
// TODO(lucacasonato): add SharedArrayBuffer to primordials
|
|
// SharedArrayBufferPrototype
|
|
SafeRegExp,
|
|
StringPrototypeCharAt,
|
|
StringPrototypeToLowerCase,
|
|
StringPrototypeSlice,
|
|
Symbol,
|
|
SymbolFor,
|
|
TypedArrayPrototypeSet,
|
|
TypeError,
|
|
Uint8Array,
|
|
} = primordials;
|
|
import { createFilteredInspectProxy } from "ext:deno_console/02_console.js";
|
|
|
|
// 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 = StringPrototypeCharAt(input, position);
|
|
position < input.length && !(c === "\r" || c === "\n");
|
|
c = StringPrototypeCharAt(input, ++position)
|
|
);
|
|
return { result: StringPrototypeSlice(input, 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 = StringPrototypeCharAt(s, position);
|
|
if (codePoint === "\r") {
|
|
result += nativeLineEnding;
|
|
position++;
|
|
if (
|
|
position < s.length && StringPrototypeCharAt(s, 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 {(BlobReference | Blob)[]} parts */
|
|
async function* toIterator(parts) {
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
yield* parts[i].stream();
|
|
}
|
|
}
|
|
|
|
/** @typedef {BufferSource | Blob | string} BlobPart */
|
|
|
|
/**
|
|
* @param {BlobPart[]} parts
|
|
* @param {string} endings
|
|
* @returns {{ parts: (BlobReference|Blob)[], size: number }}
|
|
*/
|
|
function processBlobParts(parts, endings) {
|
|
/** @type {(BlobReference|Blob)[]} */
|
|
const processedParts = [];
|
|
let size = 0;
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
const element = parts[i];
|
|
if (ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, element)) {
|
|
const chunk = new Uint8Array(ArrayBufferPrototypeSlice(element, 0));
|
|
ArrayPrototypePush(processedParts, BlobReference.fromUint8Array(chunk));
|
|
size += element.byteLength;
|
|
} else if (ArrayBufferIsView(element)) {
|
|
const chunk = new Uint8Array(
|
|
element.buffer,
|
|
element.byteOffset,
|
|
element.byteLength,
|
|
);
|
|
size += element.byteLength;
|
|
ArrayPrototypePush(processedParts, BlobReference.fromUint8Array(chunk));
|
|
} else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, element)) {
|
|
ArrayPrototypePush(processedParts, element);
|
|
size += element.size;
|
|
} else if (typeof element === "string") {
|
|
const chunk = core.encode(
|
|
endings == "native" ? convertLineEndingsToNative(element) : element,
|
|
);
|
|
size += chunk.byteLength;
|
|
ArrayPrototypePush(processedParts, BlobReference.fromUint8Array(chunk));
|
|
} else {
|
|
throw new TypeError("Unreachable code (invalid element type)");
|
|
}
|
|
}
|
|
return { parts: processedParts, size };
|
|
}
|
|
|
|
const NORMALIZE_PATTERN = new SafeRegExp(/^[\x20-\x7E]*$/);
|
|
|
|
/**
|
|
* @param {string} str
|
|
* @returns {string}
|
|
*/
|
|
function normalizeType(str) {
|
|
let normalizedType = str;
|
|
if (!RegExpPrototypeTest(NORMALIZE_PATTERN, str)) {
|
|
normalizedType = "";
|
|
}
|
|
return StringPrototypeToLowerCase(normalizedType);
|
|
}
|
|
|
|
/**
|
|
* Get all Parts as a flat array containing all references
|
|
* @param {Blob} blob
|
|
* @param {string[]} bag
|
|
* @returns {string[]}
|
|
*/
|
|
function getParts(blob, bag = []) {
|
|
const parts = blob[_parts];
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
const part = parts[i];
|
|
if (ObjectPrototypeIsPrototypeOf(BlobPrototype, part)) {
|
|
getParts(part, bag);
|
|
} else {
|
|
ArrayPrototypePush(bag, part._id);
|
|
}
|
|
}
|
|
return bag;
|
|
}
|
|
|
|
const _type = Symbol("Type");
|
|
const _size = Symbol("Size");
|
|
const _parts = Symbol("Parts");
|
|
|
|
class Blob {
|
|
[_type] = "";
|
|
[_size] = 0;
|
|
[_parts];
|
|
|
|
/**
|
|
* @param {BlobPart[]} blobParts
|
|
* @param {BlobPropertyBag} options
|
|
*/
|
|
constructor(blobParts = [], options = {}) {
|
|
const prefix = "Failed to construct 'Blob'";
|
|
blobParts = webidl.converters["sequence<BlobPart>"](blobParts, {
|
|
context: "Argument 1",
|
|
prefix,
|
|
});
|
|
options = webidl.converters["BlobPropertyBag"](options, {
|
|
context: "Argument 2",
|
|
prefix,
|
|
});
|
|
|
|
this[webidl.brand] = webidl.brand;
|
|
|
|
const { parts, size } = processBlobParts(
|
|
blobParts,
|
|
options.endings,
|
|
);
|
|
|
|
this[_parts] = parts;
|
|
this[_size] = size;
|
|
this[_type] = normalizeType(options.type);
|
|
}
|
|
|
|
/** @returns {number} */
|
|
get size() {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
return this[_size];
|
|
}
|
|
|
|
/** @returns {string} */
|
|
get type() {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
return this[_type];
|
|
}
|
|
|
|
/**
|
|
* @param {number} [start]
|
|
* @param {number} [end]
|
|
* @param {string} [contentType]
|
|
* @returns {Blob}
|
|
*/
|
|
slice(start = undefined, end = undefined, contentType = undefined) {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
const prefix = "Failed to execute 'slice' on 'Blob'";
|
|
if (start !== undefined) {
|
|
start = webidl.converters["long long"](start, {
|
|
clamp: true,
|
|
context: "Argument 1",
|
|
prefix,
|
|
});
|
|
}
|
|
if (end !== undefined) {
|
|
end = webidl.converters["long long"](end, {
|
|
clamp: true,
|
|
context: "Argument 2",
|
|
prefix,
|
|
});
|
|
}
|
|
if (contentType !== undefined) {
|
|
contentType = webidl.converters["DOMString"](contentType, {
|
|
context: "Argument 3",
|
|
prefix,
|
|
});
|
|
}
|
|
|
|
// deno-lint-ignore no-this-alias
|
|
const O = this;
|
|
/** @type {number} */
|
|
let relativeStart;
|
|
if (start === undefined) {
|
|
relativeStart = 0;
|
|
} else {
|
|
if (start < 0) {
|
|
relativeStart = MathMax(O.size + start, 0);
|
|
} else {
|
|
relativeStart = MathMin(start, O.size);
|
|
}
|
|
}
|
|
/** @type {number} */
|
|
let relativeEnd;
|
|
if (end === undefined) {
|
|
relativeEnd = O.size;
|
|
} else {
|
|
if (end < 0) {
|
|
relativeEnd = MathMax(O.size + end, 0);
|
|
} else {
|
|
relativeEnd = MathMin(end, O.size);
|
|
}
|
|
}
|
|
|
|
const span = MathMax(relativeEnd - relativeStart, 0);
|
|
const blobParts = [];
|
|
let added = 0;
|
|
|
|
const parts = this[_parts];
|
|
for (let i = 0; i < parts.length; ++i) {
|
|
const part = parts[i];
|
|
// don't add the overflow to new blobParts
|
|
if (added >= span) {
|
|
// Could maybe be possible to remove variable `added`
|
|
// and only use relativeEnd?
|
|
break;
|
|
}
|
|
const size = part.size;
|
|
if (relativeStart && size <= relativeStart) {
|
|
// Skip the beginning and change the relative
|
|
// start & end position as we skip the unwanted parts
|
|
relativeStart -= size;
|
|
relativeEnd -= size;
|
|
} else {
|
|
const chunk = part.slice(
|
|
relativeStart,
|
|
MathMin(part.size, relativeEnd),
|
|
);
|
|
added += chunk.size;
|
|
relativeEnd -= part.size;
|
|
ArrayPrototypePush(blobParts, chunk);
|
|
relativeStart = 0; // All next sequential parts should start at 0
|
|
}
|
|
}
|
|
|
|
/** @type {string} */
|
|
let relativeContentType;
|
|
if (contentType === undefined) {
|
|
relativeContentType = "";
|
|
} else {
|
|
relativeContentType = normalizeType(contentType);
|
|
}
|
|
|
|
const blob = new Blob([], { type: relativeContentType });
|
|
blob[_parts] = blobParts;
|
|
blob[_size] = span;
|
|
return blob;
|
|
}
|
|
|
|
/**
|
|
* @returns {ReadableStream<Uint8Array>}
|
|
*/
|
|
stream() {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
const partIterator = toIterator(this[_parts]);
|
|
const stream = new ReadableStream({
|
|
type: "bytes",
|
|
/** @param {ReadableByteStreamController} controller */
|
|
async pull(controller) {
|
|
while (true) {
|
|
const { value, done } = await AsyncGeneratorPrototypeNext(
|
|
partIterator,
|
|
);
|
|
if (done) return controller.close();
|
|
if (value.byteLength > 0) {
|
|
return controller.enqueue(value);
|
|
}
|
|
}
|
|
},
|
|
});
|
|
return stream;
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<string>}
|
|
*/
|
|
async text() {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
const buffer = await this.#u8Array(this.size);
|
|
return core.decode(buffer);
|
|
}
|
|
|
|
async #u8Array(size) {
|
|
const bytes = new Uint8Array(size);
|
|
const partIterator = toIterator(this[_parts]);
|
|
let offset = 0;
|
|
while (true) {
|
|
const { value, done } = await AsyncGeneratorPrototypeNext(
|
|
partIterator,
|
|
);
|
|
if (done) break;
|
|
const byteLength = value.byteLength;
|
|
if (byteLength > 0) {
|
|
TypedArrayPrototypeSet(bytes, value, offset);
|
|
offset += byteLength;
|
|
}
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
/**
|
|
* @returns {Promise<ArrayBuffer>}
|
|
*/
|
|
async arrayBuffer() {
|
|
webidl.assertBranded(this, BlobPrototype);
|
|
const buf = await this.#u8Array(this.size);
|
|
return buf.buffer;
|
|
}
|
|
|
|
[SymbolFor("Deno.customInspect")](inspect) {
|
|
return inspect(createFilteredInspectProxy({
|
|
object: this,
|
|
evaluate: ObjectPrototypeIsPrototypeOf(BlobPrototype, this),
|
|
keys: [
|
|
"size",
|
|
"type",
|
|
],
|
|
}));
|
|
}
|
|
}
|
|
|
|
webidl.configurePrototype(Blob);
|
|
const BlobPrototype = Blob.prototype;
|
|
|
|
webidl.converters["Blob"] = webidl.createInterfaceConverter(
|
|
"Blob",
|
|
Blob.prototype,
|
|
);
|
|
webidl.converters["BlobPart"] = (V, opts) => {
|
|
// Union for ((ArrayBuffer or ArrayBufferView) or Blob or USVString)
|
|
if (typeof V == "object") {
|
|
if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) {
|
|
return webidl.converters["Blob"](V, opts);
|
|
}
|
|
if (
|
|
ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V) ||
|
|
// deno-lint-ignore prefer-primordials
|
|
ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V)
|
|
) {
|
|
return webidl.converters["ArrayBuffer"](V, opts);
|
|
}
|
|
if (ArrayBufferIsView(V)) {
|
|
return webidl.converters["ArrayBufferView"](V, opts);
|
|
}
|
|
}
|
|
// BlobPart is passed to processBlobParts after conversion, which calls core.encode()
|
|
// on the string.
|
|
// core.encode() is equivalent to USVString normalization.
|
|
return webidl.converters["DOMString"](V, opts);
|
|
};
|
|
webidl.converters["sequence<BlobPart>"] = webidl.createSequenceConverter(
|
|
webidl.converters["BlobPart"],
|
|
);
|
|
webidl.converters["EndingType"] = webidl.createEnumConverter("EndingType", [
|
|
"transparent",
|
|
"native",
|
|
]);
|
|
const blobPropertyBagDictionary = [
|
|
{
|
|
key: "type",
|
|
converter: webidl.converters["DOMString"],
|
|
defaultValue: "",
|
|
},
|
|
{
|
|
key: "endings",
|
|
converter: webidl.converters["EndingType"],
|
|
defaultValue: "transparent",
|
|
},
|
|
];
|
|
webidl.converters["BlobPropertyBag"] = webidl.createDictionaryConverter(
|
|
"BlobPropertyBag",
|
|
blobPropertyBagDictionary,
|
|
);
|
|
|
|
const _Name = Symbol("[[Name]]");
|
|
const _LastModified = Symbol("[[LastModified]]");
|
|
|
|
class File extends Blob {
|
|
/** @type {string} */
|
|
[_Name];
|
|
/** @type {number} */
|
|
[_LastModified];
|
|
|
|
/**
|
|
* @param {BlobPart[]} fileBits
|
|
* @param {string} fileName
|
|
* @param {FilePropertyBag} options
|
|
*/
|
|
constructor(fileBits, fileName, options = {}) {
|
|
const prefix = "Failed to construct 'File'";
|
|
webidl.requiredArguments(arguments.length, 2, { prefix });
|
|
|
|
fileBits = webidl.converters["sequence<BlobPart>"](fileBits, {
|
|
context: "Argument 1",
|
|
prefix,
|
|
});
|
|
fileName = webidl.converters["USVString"](fileName, {
|
|
context: "Argument 2",
|
|
prefix,
|
|
});
|
|
options = webidl.converters["FilePropertyBag"](options, {
|
|
context: "Argument 3",
|
|
prefix,
|
|
});
|
|
|
|
super(fileBits, options);
|
|
|
|
/** @type {string} */
|
|
this[_Name] = fileName;
|
|
if (options.lastModified === undefined) {
|
|
/** @type {number} */
|
|
this[_LastModified] = DatePrototypeGetTime(new Date());
|
|
} else {
|
|
/** @type {number} */
|
|
this[_LastModified] = options.lastModified;
|
|
}
|
|
}
|
|
|
|
/** @returns {string} */
|
|
get name() {
|
|
webidl.assertBranded(this, FilePrototype);
|
|
return this[_Name];
|
|
}
|
|
|
|
/** @returns {number} */
|
|
get lastModified() {
|
|
webidl.assertBranded(this, FilePrototype);
|
|
return this[_LastModified];
|
|
}
|
|
}
|
|
|
|
webidl.configurePrototype(File);
|
|
const FilePrototype = File.prototype;
|
|
|
|
webidl.converters["FilePropertyBag"] = webidl.createDictionaryConverter(
|
|
"FilePropertyBag",
|
|
blobPropertyBagDictionary,
|
|
[
|
|
{
|
|
key: "lastModified",
|
|
converter: webidl.converters["long long"],
|
|
},
|
|
],
|
|
);
|
|
|
|
// A finalization registry to deallocate a blob part when its JS reference is
|
|
// garbage collected.
|
|
const registry = new FinalizationRegistry((uuid) => {
|
|
ops.op_blob_remove_part(uuid);
|
|
});
|
|
|
|
// TODO(lucacasonato): get a better stream from Rust in BlobReference#stream
|
|
|
|
/**
|
|
* An opaque reference to a blob part in Rust. This could be backed by a file,
|
|
* in memory storage, or something else.
|
|
*/
|
|
class BlobReference {
|
|
/**
|
|
* Don't use directly. Use `BlobReference.fromUint8Array`.
|
|
* @param {string} id
|
|
* @param {number} size
|
|
*/
|
|
constructor(id, size) {
|
|
this._id = id;
|
|
this.size = size;
|
|
registry.register(this, id);
|
|
}
|
|
|
|
/**
|
|
* Create a new blob part from a Uint8Array.
|
|
*
|
|
* @param {Uint8Array} data
|
|
* @returns {BlobReference}
|
|
*/
|
|
static fromUint8Array(data) {
|
|
const id = ops.op_blob_create_part(data);
|
|
return new BlobReference(id, data.byteLength);
|
|
}
|
|
|
|
/**
|
|
* Create a new BlobReference by slicing this BlobReference. This is a copy
|
|
* free operation - the sliced reference will still reference the original
|
|
* underlying bytes.
|
|
*
|
|
* @param {number} start
|
|
* @param {number} end
|
|
* @returns {BlobReference}
|
|
*/
|
|
slice(start, end) {
|
|
const size = end - start;
|
|
const id = ops.op_blob_slice_part(this._id, {
|
|
start,
|
|
len: size,
|
|
});
|
|
return new BlobReference(id, size);
|
|
}
|
|
|
|
/**
|
|
* Read the entire contents of the reference blob.
|
|
* @returns {AsyncGenerator<Uint8Array>}
|
|
*/
|
|
async *stream() {
|
|
yield core.opAsync("op_blob_read_part", this._id);
|
|
|
|
// let position = 0;
|
|
// const end = this.size;
|
|
// while (position !== end) {
|
|
// const size = MathMin(end - position, 65536);
|
|
// const chunk = this.slice(position, position + size);
|
|
// position += chunk.size;
|
|
// yield core.opAsync("op_blob_read_part", chunk._id);
|
|
// }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Construct a new Blob object from an object URL.
|
|
*
|
|
* This new object will not duplicate data in memory with the original Blob
|
|
* object from which this URL was created or with other Blob objects created
|
|
* from the same URL, but they will be different objects.
|
|
*
|
|
* The object returned from this function will not be a File object, even if
|
|
* the original object from which the object URL was constructed was one. This
|
|
* means that the `name` and `lastModified` properties are lost.
|
|
*
|
|
* @param {string} url
|
|
* @returns {Blob | null}
|
|
*/
|
|
function blobFromObjectUrl(url) {
|
|
const blobData = ops.op_blob_from_object_url(url);
|
|
if (blobData === null) {
|
|
return null;
|
|
}
|
|
|
|
/** @type {BlobReference[]} */
|
|
const parts = [];
|
|
let totalSize = 0;
|
|
|
|
for (let i = 0; i < blobData.parts.length; ++i) {
|
|
const { uuid, size } = blobData.parts[i];
|
|
ArrayPrototypePush(parts, new BlobReference(uuid, size));
|
|
totalSize += size;
|
|
}
|
|
|
|
const blob = webidl.createBranded(Blob);
|
|
blob[_type] = blobData.media_type;
|
|
blob[_size] = totalSize;
|
|
blob[_parts] = parts;
|
|
return blob;
|
|
}
|
|
|
|
export {
|
|
Blob,
|
|
blobFromObjectUrl,
|
|
BlobPrototype,
|
|
File,
|
|
FilePrototype,
|
|
getParts,
|
|
};
|