From 7b7052e1abb0735ca443ffd133b014a19b7dab3d Mon Sep 17 00:00:00 2001 From: Parsa Ghadimi Date: Fri, 14 Sep 2018 17:15:50 +0430 Subject: [PATCH] Implement Blob --- BUILD.gn | 1 + js/blob.ts | 113 ++++++++++++++++++++++++++++++++++++++++++++ js/blob_test.ts | 35 ++++++++++++++ js/fetch_types.d.ts | 3 ++ js/globals.ts | 4 ++ js/unit_tests.ts | 1 + js/util.ts | 9 ++++ 7 files changed, 166 insertions(+) create mode 100644 js/blob.ts create mode 100644 js/blob_test.ts diff --git a/BUILD.gn b/BUILD.gn index c48afcf83f..d2f4f83868 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -54,6 +54,7 @@ main_extern = [ ts_sources = [ "js/assets.ts", + "js/blob.ts", "js/compiler.ts", "js/console.ts", "js/deno.ts", diff --git a/js/blob.ts b/js/blob.ts new file mode 100644 index 0000000000..128146ecee --- /dev/null +++ b/js/blob.ts @@ -0,0 +1,113 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import { Blob, BlobPart, BlobPropertyBag } from "./fetch_types"; +import { containsOnlyASCII } from "./util"; + +const bytesSymbol = Symbol("bytes"); + +export class DenoBlob implements Blob { + private readonly [bytesSymbol]: Uint8Array; + readonly size: number = 0; + readonly type: string = ""; + + constructor(blobParts?: BlobPart[], options?: BlobPropertyBag) { + if (arguments.length === 0) { + this[bytesSymbol] = new Uint8Array(); + return; + } + + options = options || {}; + // Set ending property's default value to "tranparent". + if (!options.hasOwnProperty("ending")) { + options.ending = "tranparent"; + } + + if (options.type && !containsOnlyASCII(options.type)) { + const errMsg = "The 'type' property must consist of ASCII characters."; + throw new SyntaxError(errMsg); + } + + const bytes = processBlobParts(blobParts!, options); + // Normalize options.type. + let type = options.type ? options.type : ""; + if (type.length) { + for (let i = 0; i < type.length; ++i) { + const char = type[i]; + if (char < "\u0020" || char > "\u007E") { + type = ""; + break; + } + } + type = type.toLowerCase(); + } + // Set Blob object's properties. + this[bytesSymbol] = bytes; + this.size = bytes.byteLength; + this.type = type; + } + + slice(start?: number, end?: number, contentType?: string): DenoBlob { + return new DenoBlob([this[bytesSymbol].slice(start, end)], { + type: contentType || this.type + }); + } +} + +function processBlobParts( + blobParts: BlobPart[], + options: BlobPropertyBag +): 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 + .map(u8 => u8.byteLength) + .reduce((a, b) => a + b, 0); + 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; +} + +function toUint8Arrays( + blobParts: BlobPart[], + 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)); + } else if (element instanceof DenoBlob) { + ret.push(element[bytesSymbol]); + } else if (element instanceof Uint8Array) { + ret.push(element); + } 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); + } + } + return ret; +} + +function convertLineEndingsToNative(s: string): string { + // TODO(qti3e) Implement convertLineEndingsToNative. + // https://w3c.github.io/FileAPI/#convert-line-endings-to-native + return s; +} diff --git a/js/blob_test.ts b/js/blob_test.ts new file mode 100644 index 0000000000..293d475dde --- /dev/null +++ b/js/blob_test.ts @@ -0,0 +1,35 @@ +// Copyright 2018 the Deno authors. All rights reserved. MIT license. +import { test, assert, assertEqual } from "./test_util.ts"; + +test(async function blobString() { + const b1 = new Blob(["Hello World"]); + const str = "Test"; + const b2 = new Blob([b1, str]); + assertEqual(b2.size, b1.size + str.length); +}); + +test(async function blobBuffer() { + const buffer = new ArrayBuffer(12); + const u8 = new Uint8Array(buffer); + const f1 = new Float32Array(buffer); + const b1 = new Blob([buffer, u8]); + assertEqual(b1.size, 2 * u8.length); + const b2 = new Blob([b1, f1]); + assertEqual(b2.size, 3 * u8.length); +}); + +test(async function blobSlice() { + const blob = new Blob(["Deno", "Foo"]); + const b1 = blob.slice(0, 3, "Text/HTML"); + assert(b1 instanceof Blob); + assertEqual(b1.size, 3); + assertEqual(b1.type, "text/html"); + const b2 = blob.slice(-1, 3); + assertEqual(b2.size, 0); + const b3 = blob.slice(100, 3); + assertEqual(b3.size, 0); + const b4 = blob.slice(0, 10); + assertEqual(b4.size, blob.size); +}); + +// TODO(qti3e) Test the stored data in a Blob after implementing FileReader API. diff --git a/js/fetch_types.d.ts b/js/fetch_types.d.ts index 9d4082c359..5f49a88d15 100644 --- a/js/fetch_types.d.ts +++ b/js/fetch_types.d.ts @@ -43,8 +43,11 @@ interface HTMLFormElement { // TODO } +type EndingType = "tranparent" | "native"; + interface BlobPropertyBag { type?: string; + ending?: EndingType; } interface AbortSignalEventMap { diff --git a/js/globals.ts b/js/globals.ts index 3805f12f57..912af7dc4e 100644 --- a/js/globals.ts +++ b/js/globals.ts @@ -7,6 +7,7 @@ import * as fetch_ from "./fetch"; import { libdeno } from "./libdeno"; import { globalEval } from "./global-eval"; import { DenoHeaders } from "./fetch"; +import { DenoBlob } from "./blob"; declare global { interface Window { @@ -26,6 +27,7 @@ declare global { TextDecoder: typeof TextDecoder; Headers: typeof Headers; + Blob: typeof Blob; } const clearTimeout: typeof timers.clearTimer; @@ -42,6 +44,7 @@ declare global { const TextEncoder: typeof textEncoding.TextEncoder; const TextDecoder: typeof textEncoding.TextDecoder; const Headers: typeof DenoHeaders; + const Blob: typeof DenoBlob; // tslint:enable:variable-name } @@ -63,3 +66,4 @@ window.TextDecoder = textEncoding.TextDecoder; window.fetch = fetch_.fetch; window.Headers = DenoHeaders; +window.Blob = DenoBlob; diff --git a/js/unit_tests.ts b/js/unit_tests.ts index be947cd675..e1385e0116 100644 --- a/js/unit_tests.ts +++ b/js/unit_tests.ts @@ -11,3 +11,4 @@ import "./mkdir_test.ts"; import "./make_temp_dir_test.ts"; import "./stat_test.ts"; import "./rename_test.ts"; +import "./blob_test.ts"; diff --git a/js/util.ts b/js/util.ts index f65477ee5d..c4bada5890 100644 --- a/js/util.ts +++ b/js/util.ts @@ -85,6 +85,7 @@ export function unreachable(): never { throw new Error("Code not reachable"); } +// @internal export function hexdump(u8: Uint8Array): string { return Array.prototype.map .call(u8, (x: number) => { @@ -92,3 +93,11 @@ export function hexdump(u8: Uint8Array): string { }) .join(" "); } + +// @internal +export function containsOnlyASCII(str: string): boolean { + if (typeof str !== "string") { + return false; + } + return /^[\x00-\x7F]*$/.test(str); +}