2020-01-02 15:13:47 -05:00
|
|
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
2020-07-06 21:45:39 -04:00
|
|
|
|
2020-04-03 14:55:23 -04:00
|
|
|
import { TextDecoder, TextEncoder } from "./text_encoding.ts";
|
2020-03-05 07:05:41 -05:00
|
|
|
import { build } from "../build.ts";
|
2020-04-22 10:06:51 -04:00
|
|
|
import { ReadableStreamImpl } from "./streams/readable_stream.ts";
|
2018-09-14 08:45:50 -04:00
|
|
|
|
2019-01-03 06:41:20 -05:00
|
|
|
export const bytesSymbol = Symbol("bytes");
|
2018-09-14 08:45:50 -04:00
|
|
|
|
2020-03-05 07:05:41 -05:00
|
|
|
export function containsOnlyASCII(str: string): boolean {
|
|
|
|
if (typeof str !== "string") {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return /^[\x00-\x7F]*$/.test(str);
|
|
|
|
}
|
|
|
|
|
2019-03-09 12:30:38 -05:00
|
|
|
function convertLineEndingsToNative(s: string): string {
|
2020-04-28 12:35:23 -04:00
|
|
|
const nativeLineEnd = build.os == "windows" ? "\r\n" : "\n";
|
2019-08-01 10:04:39 -04:00
|
|
|
|
|
|
|
let position = 0;
|
|
|
|
|
|
|
|
let collectionResult = collectSequenceNotCRLF(s, position);
|
|
|
|
|
|
|
|
let token = collectionResult.collected;
|
|
|
|
position = collectionResult.newPosition;
|
|
|
|
|
|
|
|
let result = token;
|
|
|
|
|
|
|
|
while (position < s.length) {
|
2019-09-07 12:27:18 -04:00
|
|
|
const c = s.charAt(position);
|
2019-08-01 10:04:39 -04:00
|
|
|
if (c == "\r") {
|
|
|
|
result += nativeLineEnd;
|
|
|
|
position++;
|
|
|
|
if (position < s.length && s.charAt(position) == "\n") {
|
|
|
|
position++;
|
|
|
|
}
|
|
|
|
} else if (c == "\n") {
|
|
|
|
position++;
|
|
|
|
result += nativeLineEnd;
|
|
|
|
}
|
|
|
|
|
|
|
|
collectionResult = collectSequenceNotCRLF(s, position);
|
|
|
|
|
|
|
|
token = collectionResult.collected;
|
|
|
|
position = collectionResult.newPosition;
|
|
|
|
|
|
|
|
result += token;
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
function collectSequenceNotCRLF(
|
|
|
|
s: string,
|
|
|
|
position: number
|
|
|
|
): { collected: string; newPosition: number } {
|
|
|
|
const start = position;
|
|
|
|
for (
|
|
|
|
let c = s.charAt(position);
|
|
|
|
position < s.length && !(c == "\r" || c == "\n");
|
|
|
|
c = s.charAt(++position)
|
|
|
|
);
|
|
|
|
return { collected: s.slice(start, position), newPosition: position };
|
2019-03-09 12:30:38 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function toUint8Arrays(
|
2020-04-11 16:25:31 -04:00
|
|
|
blobParts: BlobPart[],
|
2019-03-09 12:30:38 -05:00
|
|
|
doNormalizeLineEndingsToNative: boolean
|
|
|
|
): Uint8Array[] {
|
|
|
|
const ret: Uint8Array[] = [];
|
|
|
|
const enc = new TextEncoder();
|
|
|
|
for (const element of blobParts) {
|
|
|
|
if (typeof element === "string") {
|
|
|
|
let str = element;
|
|
|
|
if (doNormalizeLineEndingsToNative) {
|
|
|
|
str = convertLineEndingsToNative(element);
|
|
|
|
}
|
|
|
|
ret.push(enc.encode(str));
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
|
|
} else if (element instanceof DenoBlob) {
|
|
|
|
ret.push(element[bytesSymbol]);
|
|
|
|
} else if (element instanceof Uint8Array) {
|
|
|
|
ret.push(element);
|
|
|
|
} else if (element instanceof Uint16Array) {
|
|
|
|
const uint8 = new Uint8Array(element.buffer);
|
|
|
|
ret.push(uint8);
|
|
|
|
} else if (element instanceof Uint32Array) {
|
|
|
|
const uint8 = new Uint8Array(element.buffer);
|
|
|
|
ret.push(uint8);
|
|
|
|
} else if (ArrayBuffer.isView(element)) {
|
|
|
|
// Convert view to Uint8Array.
|
|
|
|
const uint8 = new Uint8Array(element.buffer);
|
|
|
|
ret.push(uint8);
|
|
|
|
} else if (element instanceof ArrayBuffer) {
|
|
|
|
// Create a new Uint8Array view for the given ArrayBuffer.
|
|
|
|
const uint8 = new Uint8Array(element);
|
|
|
|
ret.push(uint8);
|
|
|
|
} else {
|
|
|
|
ret.push(enc.encode(String(element)));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
function processBlobParts(
|
2020-04-11 16:25:31 -04:00
|
|
|
blobParts: BlobPart[],
|
2020-04-14 09:23:07 -04:00
|
|
|
options: BlobPropertyBag
|
2019-03-09 12:30:38 -05:00
|
|
|
): Uint8Array {
|
|
|
|
const normalizeLineEndingsToNative = options.ending === "native";
|
|
|
|
// ArrayBuffer.transfer is not yet implemented in V8, so we just have to
|
|
|
|
// pre compute size of the array buffer and do some sort of static allocation
|
|
|
|
// instead of dynamic allocation.
|
|
|
|
const uint8Arrays = toUint8Arrays(blobParts, normalizeLineEndingsToNative);
|
|
|
|
const byteLength = uint8Arrays
|
2019-04-21 16:40:10 -04:00
|
|
|
.map((u8): number => u8.byteLength)
|
|
|
|
.reduce((a, b): number => a + b, 0);
|
2019-03-09 12:30:38 -05:00
|
|
|
const ab = new ArrayBuffer(byteLength);
|
|
|
|
const bytes = new Uint8Array(ab);
|
|
|
|
let courser = 0;
|
|
|
|
for (const u8 of uint8Arrays) {
|
|
|
|
bytes.set(u8, courser);
|
|
|
|
courser += u8.byteLength;
|
|
|
|
}
|
|
|
|
|
|
|
|
return bytes;
|
|
|
|
}
|
|
|
|
|
2020-04-22 10:06:51 -04:00
|
|
|
function getStream(blobBytes: Uint8Array): ReadableStream<ArrayBufferView> {
|
|
|
|
// TODO: Align to spec https://fetch.spec.whatwg.org/#concept-construct-readablestream
|
|
|
|
return new ReadableStreamImpl({
|
|
|
|
type: "bytes",
|
|
|
|
start: (controller: ReadableByteStreamController): void => {
|
2020-04-03 14:55:23 -04:00
|
|
|
controller.enqueue(blobBytes);
|
|
|
|
controller.close();
|
|
|
|
},
|
2020-04-22 10:06:51 -04:00
|
|
|
});
|
2020-04-03 14:55:23 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
async function readBytes(
|
2020-04-22 10:06:51 -04:00
|
|
|
reader: ReadableStreamReader<ArrayBufferView>
|
2020-04-03 14:55:23 -04:00
|
|
|
): Promise<ArrayBuffer> {
|
|
|
|
const chunks: Uint8Array[] = [];
|
|
|
|
while (true) {
|
2020-04-22 10:06:51 -04:00
|
|
|
const { done, value } = await reader.read();
|
|
|
|
if (!done && value instanceof Uint8Array) {
|
|
|
|
chunks.push(value);
|
|
|
|
} else if (done) {
|
|
|
|
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;
|
2020-04-03 14:55:23 -04:00
|
|
|
}
|
2020-04-22 10:06:51 -04:00
|
|
|
return bytes;
|
|
|
|
} else {
|
|
|
|
throw new TypeError("Invalid reader result.");
|
2020-04-03 14:55:23 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// A WeakMap holding blob to byte array mapping.
|
|
|
|
// Ensures it does not impact garbage collection.
|
2020-04-11 16:25:31 -04:00
|
|
|
export const blobBytesWeakMap = new WeakMap<Blob, Uint8Array>();
|
2020-04-03 14:55:23 -04:00
|
|
|
|
2020-05-29 02:27:57 -04:00
|
|
|
class DenoBlob implements Blob {
|
2020-03-28 13:03:49 -04:00
|
|
|
[bytesSymbol]: Uint8Array;
|
2018-09-14 08:45:50 -04:00
|
|
|
readonly size: number = 0;
|
|
|
|
readonly type: string = "";
|
|
|
|
|
2020-04-14 09:23:07 -04:00
|
|
|
constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) {
|
2018-09-14 08:45:50 -04:00
|
|
|
if (arguments.length === 0) {
|
|
|
|
this[bytesSymbol] = new Uint8Array();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2020-03-10 12:08:58 -04:00
|
|
|
const { ending = "transparent", type = "" } = options ?? {};
|
2018-09-14 08:45:50 -04:00
|
|
|
// Normalize options.type.
|
2020-03-10 12:08:58 -04:00
|
|
|
let normalizedType = type;
|
2020-03-31 14:42:18 -04:00
|
|
|
if (!containsOnlyASCII(type)) {
|
|
|
|
normalizedType = "";
|
|
|
|
} else {
|
|
|
|
if (type.length) {
|
|
|
|
for (let i = 0; i < type.length; ++i) {
|
|
|
|
const char = type[i];
|
|
|
|
if (char < "\u0020" || char > "\u007E") {
|
|
|
|
normalizedType = "";
|
|
|
|
break;
|
|
|
|
}
|
2018-09-14 08:45:50 -04:00
|
|
|
}
|
2020-03-31 14:42:18 -04:00
|
|
|
normalizedType = type.toLowerCase();
|
2018-09-14 08:45:50 -04:00
|
|
|
}
|
|
|
|
}
|
2020-03-31 14:42:18 -04:00
|
|
|
const bytes = processBlobParts(blobParts!, { ending, type });
|
2018-09-14 08:45:50 -04:00
|
|
|
// Set Blob object's properties.
|
|
|
|
this[bytesSymbol] = bytes;
|
|
|
|
this.size = bytes.byteLength;
|
2020-03-10 12:08:58 -04:00
|
|
|
this.type = normalizedType;
|
2018-09-14 08:45:50 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
slice(start?: number, end?: number, contentType?: string): DenoBlob {
|
|
|
|
return new DenoBlob([this[bytesSymbol].slice(start, end)], {
|
2020-03-28 13:03:49 -04:00
|
|
|
type: contentType || this.type,
|
2018-09-14 08:45:50 -04:00
|
|
|
});
|
|
|
|
}
|
2020-04-03 14:55:23 -04:00
|
|
|
|
2020-04-22 10:06:51 -04:00
|
|
|
stream(): ReadableStream<ArrayBufferView> {
|
2020-04-03 14:55:23 -04:00
|
|
|
return getStream(this[bytesSymbol]);
|
|
|
|
}
|
|
|
|
|
|
|
|
async text(): Promise<string> {
|
|
|
|
const reader = getStream(this[bytesSymbol]).getReader();
|
|
|
|
const decoder = new TextDecoder();
|
|
|
|
return decoder.decode(await readBytes(reader));
|
|
|
|
}
|
|
|
|
|
|
|
|
arrayBuffer(): Promise<ArrayBuffer> {
|
|
|
|
return readBytes(getStream(this[bytesSymbol]).getReader());
|
|
|
|
}
|
2018-09-14 08:45:50 -04:00
|
|
|
}
|
2020-05-29 02:27:57 -04:00
|
|
|
|
|
|
|
// we want the Base class name to be the name of the class.
|
|
|
|
Object.defineProperty(DenoBlob, "name", {
|
|
|
|
value: "Blob",
|
|
|
|
configurable: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
export { DenoBlob };
|