diff --git a/assembly/buffer/index.ts b/assembly/buffer/index.ts index 627a273..62e1987 100644 --- a/assembly/buffer/index.ts +++ b/assembly/buffer/index.ts @@ -1,10 +1,20 @@ import { BLOCK_MAXSIZE, BLOCK, BLOCK_OVERHEAD } from "rt/common"; import { E_INVALIDLENGTH, E_INDEXOUTOFRANGE } from "util/error"; import { Uint8Array } from "typedarray"; +import { ArrayBufferView } from "arraybuffer"; +import { Array } from "array"; + +// @ts-ignore: Decorator +@inline +function allocBuffer(arrayBuffer: usize, offset: usize, length: u32): Buffer { + let pointer = __alloc(offsetof(), idof()); + store(pointer, __retain(arrayBuffer), offsetof("buffer")); + store(pointer, arrayBuffer + offset, offsetof("dataStart")); + store(pointer, length, offsetof("byteLength")); + return changetype(pointer); +} export class Buffer extends Uint8Array { - [key: number]: u8; - constructor(size: i32) { super(size); } @@ -16,19 +26,89 @@ export class Buffer extends Uint8Array { @unsafe static allocUnsafe(size: i32): Buffer { // range must be valid if (size > BLOCK_MAXSIZE) throw new RangeError(E_INVALIDLENGTH); - let buffer = __alloc(size, idof()); - let result = __alloc(offsetof(), idof()); + return allocBuffer(__alloc(size, idof()), 0, size); + } + + public static fromArrayBuffer(buffer: ArrayBuffer, byteOffset: i32 = 0, length: i32 = -1): Buffer { + length = select(buffer.byteLength, length, length < 0); + if (i32(byteOffset < 0) | i32(byteOffset > buffer.byteLength - length)) throw new RangeError(E_INDEXOUTOFRANGE); + if (length == 0) return new Buffer(0); + + return allocBuffer(changetype(buffer), byteOffset, length); + } + + public static fromString(value: string, encoding: string = "utf8"): Buffer { + let buffer: ArrayBuffer; + if (encoding == "utf8" || encoding == "utf-8") { + buffer = String.UTF8.encode(value); + } else if (encoding == "utf16le") { + buffer = String.UTF16.encode(value); + } else if (encoding == "hex") { + buffer = Buffer.HEX.encode(value); + } else if (encoding == "ascii") { + buffer = Buffer.ASCII.encode(value); + } else { + throw new TypeError("Invalid string encoding."); + } + + // assemble the buffer + return allocBuffer(changetype(buffer), 0, buffer.byteLength); + } + + public static fromArray(value: T, offset: i32 = 0, length: i32 = -1): Buffer { + length = select(value.length, length, length < 0); + if (i32(offset < 0) | i32(offset > value.length)) throw new RangeError(E_INDEXOUTOFRANGE); + if (length > value.length - offset) throw new RangeError(E_INVALIDLENGTH); + if (length == 0) return new Buffer(0); + let arrayBuffer = __alloc(length, idof()); + if (value instanceof Array) { + for (let i = 0; i < length; i++) { + let index = i + offset; + let byteValue = parseFloat(unchecked(value[index])); + store(arrayBuffer + i, (isFinite(byteValue) ? byteValue : 0)); + } + } else { + for (let i = 0; i < length; i++) { + let index = i + offset; + let element = isDefined(unchecked(value[index])) + ? unchecked(value[index]) + : value[index]; + + if (isFloat(element)) { + store(arrayBuffer + i, (isFinite(element) ? element : 0)); + } else { + store(arrayBuffer + i, element); + } + } + } + + return allocBuffer(arrayBuffer, 0, length); + } - // set the properties - store(result, __retain(buffer), offsetof("buffer")); - store(result, buffer, offsetof("dataStart")); - store(result, size, offsetof("byteLength")); + public static fromBuffer(source: Buffer): Buffer { + let length = source.byteLength; + let data = __alloc(length, idof()); // retains + memory.copy(data, source.dataStart, length); + return allocBuffer(data, 0, length); + } - // return and retain - return changetype(result); + // @ts-ignore: Buffer returns on all valid branches + public static from(value: T): Buffer { + if (value instanceof ArrayBuffer) { + return allocBuffer(changetype(value), 0, value.byteLength); + } else if (value instanceof String) { + // @ts-ignore value not instance of `string` does changetype(value) work here? + let buffer = String.UTF8.encode(value); + return allocBuffer(changetype(buffer), 0, buffer.byteLength); + } else if (value instanceof Buffer) { + return Buffer.fromBuffer(value); + } else if (value instanceof ArrayBufferView) { + return Buffer.fromArray(value); + } + ERROR("Cannot call Buffer.from() where T is not a string, Buffer, ArrayBuffer, Array, or Array-like Object."); } - static isBuffer(value: T): bool { + public static isBuffer(value: T): bool { return value instanceof Buffer; } @@ -39,13 +119,7 @@ export class Buffer extends Uint8Array { end = end < 0 ? max(len + end, 0) : min(end, len); end = max(end, begin); - var out = __alloc(offsetof(), idof()); // retains - store(out, __retain(changetype(this.buffer)), offsetof("buffer")); - store(out, this.dataStart + begin, offsetof("dataStart")); - store(out, end - begin, offsetof("byteLength")); - - // retains - return changetype(out); + return allocBuffer(changetype(this.buffer), this.byteOffset + begin, end - begin); } readInt8(offset: i32 = 0): i8 { diff --git a/assembly/node.d.ts b/assembly/node.d.ts index a276e60..6c77fcd 100644 --- a/assembly/node.d.ts +++ b/assembly/node.d.ts @@ -3,6 +3,16 @@ declare class Buffer extends Uint8Array { static alloc(size: i32): Buffer; /** This method allocates a new Buffer of indicated size. This is unsafe because the data is not zeroed. */ static allocUnsafe(size: i32): Buffer; + /** This method creates a Buffer from the given reference. This method is naive and defaults to utf8 encoding for strings. */ + static from(value: T): Buffer; + /** This method creates a buffer from a given string. This method defaults to utf8 encoding. */ + public static fromString(value: string, encoding?: string): Buffer; + /** This method creates a buffer that uses the given ArrayBuffer as an underlying value. */ + public static fromArrayBuffer(buffer: ArrayBuffer, byteOffset?: i32 , length?: i32): Buffer; + /** This method creates a copy of the buffer using memory.copy(). */ + public static fromBuffer(source: Buffer): Buffer; + /** This method creates a new Buffer by copying the underlying values to a new ArrayBuffer and coercing each one to an 8 bit integer value. */ + public static fromArray>(value: T, offset?: i32, length?: i32): Buffer; /** This method asserts a value is a Buffer object via `value instanceof Buffer`. */ static isBuffer(value: T): bool; /** Reads a signed integer at the designated offset. */ diff --git a/tests/buffer.spec.ts b/tests/buffer.spec.ts index dcd4177..5259e85 100644 --- a/tests/buffer.spec.ts +++ b/tests/buffer.spec.ts @@ -36,7 +36,7 @@ describe("buffer", () => { expect(Buffer.alloc(10)).toBeTruthy(); expect(Buffer.alloc(10)).toHaveLength(10); let buff = Buffer.alloc(100); - for (let i = 0; i < buff.length; i++) expect(buff[i]).toBe(0); + for (let i = 0; i < buff.length; i++) expect(buff[i]).toBe(0); expect(buff.buffer).not.toBeNull(); expect(buff.byteLength).toBe(100); expect(() => { Buffer.alloc(-1); }).toThrow(); @@ -55,6 +55,108 @@ describe("buffer", () => { // expect(() => { Buffer.allocUnsafe(BLOCK_MAXSIZE + 1); }).toThrow(); }); + /** + * This specification is a tradeoff, because Buffer.from() takes _many_ parameters. + * Instead, the only common parameter is the first one, which results in Buffer.from + * acting in a very naive fashion. Perhaps an optional encoding parameter might be + * possible for strings, at least. However, this makes things more complicated. + * There are no good solutions. Only tradeoffs. Function overloading is the only + * way to fix this problem. + */ + test(".from", () => { + // Buffer.from uses the array buffer reference + let buff = new ArrayBuffer(100); + for (let i = 0; i < 100; i++) store(changetype(buff), u8(i)); + let abBuffer = Buffer.from(buff); + expect(abBuffer.buffer).toStrictEqual(buff); + expect(abBuffer.buffer).toBe(buff); + + // strings are utf8 encoded by default + let strBuffer = Buffer.from("Hello world!"); + let strBufferExpected = create([0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21]); + expect(strBuffer).toStrictEqual(strBufferExpected); + + // buffer returns a new reference view to a new ArrayBuffer + let buff2 = Buffer.from(abBuffer); + expect(buff2).not.toBe(abBuffer); + expect(buff2).toStrictEqual(abBuffer); + expect(buff2.buffer).not.toBe(abBuffer.buffer); + + // else if it extends ArrayBufferView simply converts all the values + let floats = create([1.1, 2.2, 3.3]); + let floatBuff = Buffer.from(floats); + let floatBuffExpected = create([1, 2, 3]); + expect(floatBuff).toStrictEqual(floatBuffExpected, "float values"); + + let strArrayExpected = create([1, 2, 3, 4, 5, 6, 7, 0, 0, 0]); + let stringValues = ["1.1", "2.2", "3.3", "4.4", "5.5", "6.6", "7.7", "Infinity", "NaN", "-Infinity"]; + let strArrayActual = Buffer.from(stringValues); + expect(strArrayActual).toStrictEqual(strArrayExpected, "Array Of Strings"); + }); + + test(".fromString", () => { + // public static fromString(value: string, encoding: string = "utf8"): Buffer { + // default encoding is utf8 + let expected = create([0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x74, 0xc3, 0xa9, 0x73, 0x74]) + expect(Buffer.from('this is a tést')) + .toStrictEqual(expected); + + expect(Buffer.fromString('7468697320697320612074c3a97374', 'hex')) + .toStrictEqual(expected); + }); + + test(".fromArrayBuffer", () => { + const arr = new Uint16Array(2); + + arr[0] = 5000; + arr[1] = 4000; + + // Shares memory with `arr`. + const buf = Buffer.fromArrayBuffer(arr.buffer); + + expect(buf).toStrictEqual(create([0x88, 0x13, 0xa0, 0x0f])); + + // Changing the original Uint16Array changes the Buffer also. + arr[1] = 6000; + expect(buf).toStrictEqual(create([0x88, 0x13, 0x70, 0x17])); + + // test optional parameters + expect(Buffer.fromArrayBuffer(arr.buffer, 1, 2)).toStrictEqual(create([0x13, 0x70])); + + // TODO: + // expectFn(() => { + // let value = create([5000, 4000]); // 4 bytes + // Buffer.fromArrayBuffer(value.buffer, 5); + // }).toThrow("offset out of bounds should throw"); + // expectFn(() => { + // let value = create([5000, 4000]); // 4 bytes + // Buffer.fromArrayBuffer(value.buffer, 2, 3); + // }).toThrow("length out of bounds should throw"); + }); + + test(".fromBuffer", () => { + let buff1 = create([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); + let buff2 = Buffer.fromBuffer(buff1); + + expect(buff1).not.toBe(buff2); + expect(buff1.buffer).not.toBe(buff2.buffer); + expect(buff1).toStrictEqual(buff2); + }); + + test(".fromArray", () => { + let buff1 = create([3, 6, 9, 12, 15, 18, 21]); + let buff2 = Buffer.fromArray(buff1, 2, 4); + let expected = create([9, 12, 15, 18]); + expect(buff2).toStrictEqual(expected); + + // test string values + buff2 = Buffer.fromArray(["9.2", "12.1", "15.3", "18.8"]); + expect(buff2).toStrictEqual(expected); + }); + + // todo: fromArray + // todo: fromBuffer + test("#isBuffer", () => { let a = ""; let b = new Uint8Array(0); @@ -252,7 +354,7 @@ describe("buffer", () => { expect(buff.writeInt32LE(-559038737)).toBe(4); expect(buff.writeInt32LE(283033613,4)).toBe(8); let result = create([0xEF,0xBE,0xAD,0xDE,0x0d,0xc0,0xde,0x10]); - expect(buff).toStrictEqual(result); + expect(buff).toStrictEqual(result); expect(() => { let newBuff = new Buffer(1); newBuff.writeInt32LE(0);