Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Implement] Naive Buffer.from, and friends #6

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 92 additions & 18 deletions assembly/buffer/index.ts
Original file line number Diff line number Diff line change
@@ -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 assembleBuffer(arrayBuffer: usize, offset: usize, length: u32): Buffer {
let pointer = __alloc(offsetof<Buffer>(), idof<Buffer>());
store<usize>(pointer, __retain(arrayBuffer), offsetof<Buffer>("buffer"));
store<usize>(pointer, arrayBuffer + offset, offsetof<Buffer>("dataStart"));
store<u32>(pointer, length, offsetof<Buffer>("byteLength"));
return changetype<Buffer>(pointer);
}

export class Buffer extends Uint8Array {
[key: number]: u8;

constructor(size: i32) {
super(size);
}
Expand All @@ -16,19 +26,89 @@ export class Buffer extends Uint8Array {
@unsafe static allocUnsafe(size: i32): Buffer {
// range must be valid
if (<usize>size > BLOCK_MAXSIZE) throw new RangeError(E_INVALIDLENGTH);
let buffer = __alloc(size, idof<ArrayBuffer>());
let result = __alloc(offsetof<Buffer>(), idof<Buffer>());
return assembleBuffer(__alloc(size, idof<ArrayBuffer>()), 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 assembleBuffer(changetype<usize>(buffer), <usize>byteOffset, <u32>length);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MaxGraey should this be min(length, buffer.byteLength - byteOffset)?

}

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 assembleBuffer(changetype<usize>(buffer), 0, buffer.byteLength);
}

public static fromArray<T extends ArrayBufferView>(value: T, offset: i32 = 0, length: i32 = -1): Buffer {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dcodeIO what is going to happen to this extends clause? Are we going to detach Array from ArrayBufferView?

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<ArrayBuffer>());
if (value instanceof Array<string>) {
for (let i = 0; i < length; i++) {
let index = i + offset;
let byteValue = parseFloat(unchecked(value[index]));
store<u8>(arrayBuffer + <usize>i, <u8>(isFinite(byteValue) ? <u8>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<u8>(arrayBuffer + <usize>i, <u8>(isFinite(element) ? <u8>element : 0));
} else {
store<u8>(arrayBuffer + <usize>i, <u8>element);
}
}
}

return assembleBuffer(arrayBuffer, 0, length);
}

// set the properties
store<usize>(result, __retain(buffer), offsetof<Buffer>("buffer"));
store<usize>(result, buffer, offsetof<Buffer>("dataStart"));
store<i32>(result, size, offsetof<Buffer>("byteLength"));
public static fromBuffer(source: Buffer): Buffer {
let length = source.byteLength;
let data = __alloc(length, idof<ArrayBuffer>()); // retains
memory.copy(data, source.dataStart, length);
return assembleBuffer(data, 0, length);
}

// return and retain
return changetype<Buffer>(result);
// @ts-ignore: Buffer returns on all valid branches
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can suppress this error by returning changetype<Buffer>(null) at the bottom here after the ERROR. What is the right way to do this?

public static from<T>(value: T): Buffer {
if (value instanceof ArrayBuffer) {
return assembleBuffer(changetype<usize>(value), 0, value.byteLength);
} else if (value instanceof String) {
// @ts-ignore value not instance of `string` does changetype<string>(value) work here?
let buffer = String.UTF8.encode(value);
return assembleBuffer(changetype<usize>(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<T>() where T is not a string, Buffer, ArrayBuffer, Array, or Array-like Object.");
}

static isBuffer<T>(value: T): bool {
public static isBuffer<T>(value: T): bool {
return value instanceof Buffer;
}

Expand All @@ -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<Buffer>(), idof<Buffer>()); // retains
store<usize>(out, __retain(changetype<usize>(this.buffer)), offsetof<Buffer>("buffer"));
store<usize>(out, this.dataStart + <usize>begin, offsetof<Buffer>("dataStart"));
store<i32>(out, end - begin, offsetof<Buffer>("byteLength"));

// retains
return changetype<Buffer>(out);
return assembleBuffer(changetype<usize>(this.buffer), <usize>this.byteOffset + <usize>begin, <u32>end - <u32>begin);
}

readInt8(offset: i32 = 0): i8 {
Expand Down
10 changes: 10 additions & 0 deletions assembly/node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(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<T extends ArrayBufferView<number | string>>(value: T, offset?: i32, length?: i32): Buffer;
/** This method asserts a value is a Buffer object via `value instanceof Buffer`. */
static isBuffer<T>(value: T): bool;
/** Reads a signed integer at the designated offset. */
Expand Down
106 changes: 104 additions & 2 deletions tests/buffer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<u8>(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();
Expand All @@ -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<u8>(changetype<usize>(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<Buffer>([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<Float32Array>([1.1, 2.2, 3.3]);
let floatBuff = Buffer.from(floats);
let floatBuffExpected = create<Buffer>([1, 2, 3]);
expect(floatBuff).toStrictEqual(floatBuffExpected, "float values");

let strArrayExpected = create<Buffer>([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<Buffer>([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<Buffer>([0x88, 0x13, 0xa0, 0x0f]));

// Changing the original Uint16Array changes the Buffer also.
arr[1] = 6000;
expect(buf).toStrictEqual(create<Buffer>([0x88, 0x13, 0x70, 0x17]));

// test optional parameters
expect(Buffer.fromArrayBuffer(arr.buffer, 1, 2)).toStrictEqual(create<Buffer>([0x13, 0x70]));

// TODO:
// expectFn(() => {
// let value = create<Uint16Array>([5000, 4000]); // 4 bytes
// Buffer.fromArrayBuffer(value.buffer, 5);
// }).toThrow("offset out of bounds should throw");
// expectFn(() => {
// let value = create<Uint16Array>([5000, 4000]); // 4 bytes
// Buffer.fromArrayBuffer(value.buffer, 2, 3);
// }).toThrow("length out of bounds should throw");
});

test(".fromBuffer", () => {
let buff1 = create<Buffer>([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<Uint16Array>([3, 6, 9, 12, 15, 18, 21]);
let buff2 = Buffer.fromArray(buff1, 2, 4);
let expected = create<Buffer>([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);
Expand Down Expand Up @@ -252,7 +354,7 @@ describe("buffer", () => {
expect(buff.writeInt32LE(-559038737)).toBe(4);
expect(buff.writeInt32LE(283033613,4)).toBe(8);
let result = create<Buffer>([0xEF,0xBE,0xAD,0xDE,0x0d,0xc0,0xde,0x10]);
expect<Buffer>(buff).toStrictEqual(result);
expect(buff).toStrictEqual(result);
expect(() => {
let newBuff = new Buffer(1);
newBuff.writeInt32LE(0);
Expand Down