Start with assemblyscript-bson as skeleton

This commit is contained in:
Vladimir Grichina 2019-01-05 20:54:13 -08:00
commit cdc035253f
18 changed files with 6073 additions and 0 deletions

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2016 Marco Paland
Copyright (c) 2018 NEAR Protocol
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

108
README.md Normal file
View File

@ -0,0 +1,108 @@
# assemblyscript-bson
BSON encoder / decoder for AssemblyScript somewhat based on https://github.com/mpaland/bsonfy.
Special thanks to https://github.com/MaxGraey/bignum.wasm for basic unit testing infra for AssemblyScript.
# Limitations
This is developed for use in smart contracts written in AssemblyScript for https://github.com/nearprotocol/nearcore.
This imposes such limitations:
- Only limited data types are supported:
- arrays
- objects
- 32-bit integers
- strings
- booleans
- null
- `Uint8Array`
- We assume that memory never needs to be deallocated (cause these contracts are short-lived).
Note that this mostly just defines the way it's currently implemented. Contributors are welcome to fix limitations.
# Usage
## Encoding BSON
```ts
// Make sure memory allocator is available
import "allocator/arena";
// Import encoder
import { BSONEncoder } from "path/to/module";
// Create encoder
let encoder = new BSONEncoder();
// Construct necessary object
encoder.pushObject("obj");
encoder.setInteger("int", 10);
encoder.setString("str", "");
encoder.popObject();
// Get serialized data
let bson: Uint8Array = encoder.serialize();
```
## Parsing BSON
```ts
// Make sure memory allocator is available
import "allocator/arena";
// Import decoder
import { BSONDecoder, BSONHandler } from "path/to/module";
// Events need to be received by custom object extending BSONHandler.
// NOTE: All methods are optional to implement.
class MyBSONEventsHandler extends BSONHandler {
setString(name: string, value: string): void {
// Handle field
}
setBoolean(name: string, value: bool): void {
// Handle field
}
setNull(name: string): void {
// Handle field
}
setInteger(name: string, value: i32): void {
// Handle field
}
setUint8Array(name: string, value: Uint8Array): void {
// Handle field
}
pushArray(name: string): bool {
// Handle array start
return true; // true means that nested object needs to be traversed, false otherwise
}
popArray(): void {
// Handle array end
}
pushObject(name: string): bool {
// Handle object start
return true; // true means that nested object needs to be traversed, false otherwise
}
popObject(): void {
// Handle object end
}
}
// Create decoder
let decoder = new BSONDecoder<MyBSONEventsHandler>(new MyBSONEventsHandler());
// Let's assume BSON data is available in this variable
let bson: Uint8Array = ...;
// Parse BSON
decoder.deserialize(bson); // This will send events to MyBSONEventsHandler
```

188
assembly/decoder.ts Normal file
View File

@ -0,0 +1,188 @@
/**
* Extend from this class to handle events from parser.
* Default implementation traverses whole object tree and does nothing.
*/
export abstract class BSONHandler {
setString(name: string, value: string): void {
}
setBoolean(name: string, value: bool): void {
}
setNull(name: string): void {
}
setInteger(name: string, value: i32): void {
}
setUint8Array(name: string, value: Uint8Array): void {
}
pushArray(name: string): bool {
return true;
}
popArray(): void {
}
pushObject(name: string): bool {
return true;
}
popObject(): void {
}
}
/**
* Extend from this class to handle events from parser.
* This implementation crashes on every unimplemented set/push method
* to allow easier validation of input.
*/
export class ThrowingBSONHandler extends BSONHandler {
setString(name: string, value: string): void {
assert(false, 'Unexpected string field ' + name + ' : "' + value + '"');
}
setBoolean(name: string, value: bool): void {
assert(false, 'Unexpected boolean field ' + name + ' : ' + (value ? 'true' : 'false'));
}
setNull(name: string): void {
assert(false, 'Unexpected null field ' + name);
}
setInteger(name: string, value: i32): void {
let arr: Array<i32> = [value];
assert(false, 'Unexpected integer field ' + name + ' : ' + arr.toString());
}
setUint8Array(name: string, value: Uint8Array): void {
assert(false, 'Unexpected byte array field ' + name + ' : ' + bin2str(value));
}
pushArray(name: string): bool {
assert(false, 'Unexpected array field' + name);
return true;
}
pushObject(name: string): bool {
assert(false, 'Unexpected object field ' + name);
return true;
}
}
export class BSONDecoder<BSONHandlerT extends BSONHandler> {
handler: BSONHandlerT;
readIndex: i32 = 0;
constructor(handler: BSONHandlerT) {
this.handler = handler;
}
deserialize(buffer: Uint8Array, startIndex: i32 = 0): void {
this.readIndex = startIndex;
assert(buffer.length >= 5, "Document error: Size < 5 bytes");
let size : i32 = buffer[this.readIndex++] | i32(buffer[this.readIndex++]) << 8 | i32(buffer[this.readIndex++]) << 16 | i32(buffer[this.readIndex++]) << 24;
assert(size <= buffer.length, "Document error: Size mismatch");
assert(buffer[buffer.length - 1] == 0x00, "Document error: Missing termination");
for (; ;) {
// get element type
let elementType = buffer[this.readIndex++]; // read type
if (elementType === 0) break; // zero means last byte, exit
// get element name
let end = this.readIndex;
for (; buffer[end] !== 0x00 && end < buffer.length; end++);
assert(end < buffer.length - 1, "Document error: Illegal key name");
let name = bin2str(buffer.subarray(this.readIndex, end));
this.readIndex = ++end; // skip terminating zero
switch (elementType) {
case 0x02: // BSON type: String
size = buffer[this.readIndex++] | i32(buffer[this.readIndex++]) << 8 | i32(buffer[this.readIndex++]) << 16 | i32(buffer[this.readIndex++]) << 24;
this.handler.setString(name, bin2str(buffer.subarray(this.readIndex, this.readIndex += size - 1)));
this.readIndex++;
break;
case 0x03: // BSON type: Document (Object)
size = buffer[this.readIndex] | i32(buffer[this.readIndex + 1]) << 8 | i32(buffer[this.readIndex + 2]) << 16 | i32(buffer[this.readIndex + 3]) << 24;
if (this.handler.pushObject(name)) {
this.deserialize(buffer, this.readIndex);
} else {
this.readIndex += size;
}
this.handler.popObject();
break;
case 0x04: // BSON type: Array
size = buffer[this.readIndex] | i32(buffer[this.readIndex + 1]) << 8 | i32(buffer[this.readIndex + 2]) << 16 | i32(buffer[this.readIndex + 3]) << 24; // NO 'i' increment since the size bytes are reread during the recursion
if (this.handler.pushArray(name)) {
this.deserialize(buffer, this.readIndex);
} else {
this.readIndex += size;
}
this.handler.popArray();
break;
case 0x05: // BSON type: Binary data
size = buffer[this.readIndex++] | i32(buffer[this.readIndex++]) << 8 | i32(buffer[this.readIndex++]) << 16 | i32(buffer[this.readIndex++]) << 24;
if (buffer[this.readIndex++] === 0x04) {
// BSON subtype: UUID (not supported)
return
}
this.handler.setUint8Array(name, buffer.subarray(this.readIndex, this.readIndex += size)); // use slice() here to get a new array
break;
case 0x08: // BSON type: Boolean
this.handler.setBoolean(name, buffer[this.readIndex++] === 1);
break;
case 0x0A: // BSON type: Null
this.handler.setNull(name);
break;
case 0x10: // BSON type: 32-bit integer
this.handler.setInteger(name, buffer[this.readIndex++] | i32(buffer[this.readIndex++]) << 8 | i32(buffer[this.readIndex++]) << 16 | i32(buffer[this.readIndex++]) << 24);
break;
default:
assert(false, "Parsing error: Unknown element");
}
}
}
}
/*
* Parse byte array as an UTF-8 string
* @param {Uint8Array} bin UTF-8 text given as array of bytes
* @return {String} UTF-8 Text string
*/
export function bin2str(bin: Uint8Array): string {
let str = '', len = bin.length, i = 0;
let c: i32, c2: i32, c3: i32;
while (i < len) {
c = bin[i];
if (c < 128) {
str += String.fromCharCode(c);
i++;
}
else if ((c > 191) && (c < 224)) {
c2 = bin[i + 1];
str += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = bin[i + 1];
c3 = bin[i + 2];
str += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return str;
}

131
assembly/encoder.ts Normal file
View File

@ -0,0 +1,131 @@
const START_SIZE = 32;
// Growth should be aggressive as we don't free old buffer
const GROWTH_MULT = 2;
declare function logStr(str: string): void;
declare function logF64(val: f64): void;
export class BSONEncoder {
offsets: Array<i32> = new Array<i32>();
buffer: Uint8Array = new Uint8Array(START_SIZE)
writeIndex: i32 = 4 // Make place for total size
serialize(): Uint8Array {
this.writeByte(0);
this.int32(this.writeIndex, 0);
return this.buffer.subarray(0, this.writeIndex);
}
setString(name: string, value: string): void {
this.writeByte(0x02); // BSON type: String
this.cstring(name);
let startOffset = this.writeIndex;
this.writeIndex += 4;
this.cstring(value);
this.int32(this.writeIndex - startOffset - 4, startOffset);
}
setBoolean(name: string, value: bool): void {
this.writeByte(0x08); // BSON type: Boolean
this.cstring(name);
this.writeByte(value ? 1 : 0);
}
setNull(name: string): void {
this.writeByte(0x0A); // BSON type: Null
this.cstring(name);
}
setInteger(name: string, value: i32): void {
this.writeByte(0x10); // BSON type: int32
this.cstring(name);
this.int32(value);
}
setUint8Array(name: string, value: Uint8Array): void {
this.writeByte(0x05); // BSON type: Binary data
this.cstring(name);
this.int32(value.length);
this.writeByte(0); // use generic binary subtype 0
for (let i = 0; i < value.length; i++) {
this.writeByte(value[i]);
}
}
pushArray(name: string): void {
this.writeByte(0x04); // BSON type: Array
this.cstring(name);
this.offsets.push(this.writeIndex);
this.writeIndex += 4;
}
popArray(): void {
this.writeByte(0);
let startOffset = this.offsets.pop();
this.int32(this.writeIndex - startOffset, startOffset);
}
pushObject(name: string): void {
this.writeByte(0x03); // BSON type: Document
this.cstring(name);
this.offsets.push(this.writeIndex);
this.writeIndex += 4;
}
popObject(): void {
this.writeByte(0);
let startOffset = this.offsets.pop();
this.int32(this.writeIndex - startOffset, startOffset);
}
private cstring(str: string): void {
// TODO: Handle newlines properly
// str = str.replace(/\r\n/g, '\n');
// TODO: Maybe use AssemblyScript std Unicode conversion?
for (let i = 0, len = str.length; i < len; i++) {
let c = str.charCodeAt(i);
if (c < 128) {
this.writeByte(c);
} else if (c < 2048) {
this.writeByte((c >>> 6) | 192);
this.writeByte((c & 63) | 128);
} else {
this.writeByte((c >>> 12) | 224);
this.writeByte(((c >>> 6) & 63) | 128);
this.writeByte((c & 63) | 128);
}
}
this.writeByte(0);
}
private int32(num: i32, offset: i32 = -1): void {
if (offset == -1) {
this.growIfNeeded(4);
offset = this.writeIndex;
this.writeIndex += 4;
}
this.buffer[offset] = (num) & 0xff;
this.buffer[offset + 1] = (num >>> 8) & 0xff;
this.buffer[offset + 2] = (num >>> 16) & 0xff;
this.buffer[offset + 3] = (num >>> 24) & 0xff;
}
private writeByte(b: u32): void {
this.growIfNeeded(1);
this.buffer[this.writeIndex++] = b;
}
private growIfNeeded(numBytes: i32): void {
if (this.buffer.length >= this.writeIndex + numBytes) {
return;
}
let oldBuffer = this.buffer;
this.buffer = new Uint8Array(this.buffer.length * GROWTH_MULT);
for (let i = 0; i < oldBuffer.length; i++) {
this.buffer[i] = oldBuffer[i];
}
}
}

4
assembly/index.ts Normal file
View File

@ -0,0 +1,4 @@
import { BSONDecoder } from './decoder';
import { BSONEncoder } from './encoder';
export { BSONDecoder, BSONEncoder };

6
assembly/tsconfig.json Normal file
View File

@ -0,0 +1,6 @@
{
"extends": "../../../n/lib/node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

8
index.js Normal file
View File

@ -0,0 +1,8 @@
const fs = require("fs");
const compiled = new WebAssembly.Module(fs.readFileSync(__dirname + "/build/optimized.wasm"));
const imports = {
//log:
};
Object.defineProperty(module, "exports", {
get: () => new WebAssembly.Instance(compiled, imports).exports
});

4859
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

32
package.json Normal file
View File

@ -0,0 +1,32 @@
{
"scripts": {
"asbuild:untouched": "npx asc assembly/index.ts -b build/untouched.wasm -t build/untouched.wat --sourceMap --validate --debug",
"asbuild:optimized": "npx asc assembly/index.ts -b build/optimized.wasm -t build/optimized.wat --sourceMap --validate --optimize",
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
"asbuild:test": "npm run asbuild:test:encoder && npm run asbuild:test:decoder",
"asbuild:test:encoder": "npx asc tests/assembly/encoder.spec.as.ts -b tests/build/encoder.wasm -t tests/build/encoder.wat --validate --sourceMap --importMemory --debug",
"asbuild:test:decoder": "npx asc tests/assembly/decoder.spec.as.ts -b tests/build/decoder.wasm -t tests/build/decoder.wat --validate --sourceMap --importMemory --debug",
"test": "npm run asbuild:test && ava -v --serial",
"test:ci": "npm run asbuild:test && ava --fail-fast --serial"
},
"devDependencies": {
"@types/node": "^10.12.3",
"assemblyscript": "AssemblyScript/assemblyscript",
"ava": "1.0.0-rc.1",
"ts-node": "^7.0.1",
"typedoc": "^0.13.0",
"typescript": "^3.1.6"
},
"ava": {
"compileEnhancements": true,
"extensions": [
"ts"
],
"require": [
"ts-node/register/transpile-only"
],
"files": [
"tests/**/*.spec.ts"
]
}
}

View File

@ -0,0 +1,272 @@
import "allocator/arena";
import { BSONDecoder, BSONHandler } from "../../assembly/decoder";
declare function logStr(str: string): void;
declare function logF64(val: f64): void;
/*
let deserialize_vector = [
{
obj: { "BSON": ["awesome", 5.05, 1986] },
bson: "310000000442534f4e002600000002300008000000617765736f6d65000131003333333333331440103200c20700000000",
},
{
obj: { arr: ["foo", "bar", 100, 1000], ta: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), obj: { int32: 10, int64: 1125899906842624, flo: 3.141592653 } },
bson: "7500000004617272002900000002300004000000666f6f00023100040000006261720010320064000000103300e8030000000574610008000000000102030405060708036f626a002c00000010696e743332000a00000012696e74363400000000000000040001666c6f0038e92f54fb2109400000"
},
{
obj: { id: 123456, sk: new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]), pk: new Uint8Array([255, 254, 253, 252, 251, 250, 249, 248]) },
bson: "2f0000001069640040e2010005736b000800000000010203040506070805706b000800000000fffefdfcfbfaf9f800"
},
];
*/
enum EventType {
String = 1,
Bytes = 2,
Int = 3,
Bool = 4,
Null = 5,
PushArray = 6,
PopArray = 7,
PushObject = 8,
PopObject = 9
}
class BSONEvent {
constructor(public type: EventType, public name: string, public valuePtr: usize) { }
getValue<T>() : T {
return changetype<T>(this.valuePtr);
}
toString(): string {
switch (this.type) {
case EventType.String:
return this.name + ": " + "'" + this.getValue<string>() + "'";
case EventType.Int:
// TODO: Should be some easy way to convert int to string
let intArray = new Array<i32>();
intArray.push(this.getValue<i32>());
return this.name + ": " + intArray.toString();
case EventType.Bool:
let value = this.getValue<bool>();
return this.name + ": " + (value ? "true" : "false");
case EventType.Null:
return this.name + ": null";
case EventType.PushArray:
return this.name + ": [";
case EventType.PopArray:
return "]";
case EventType.PushObject:
return this.name + ": {";
case EventType.PopObject:
return "}";
case EventType.Bytes:
return this.name + ": " + bytes2array(this.getValue<Uint8Array>()).toString();
default:
return "<Invalid BSONEvent>";
}
}
}
class BSONTestHandler extends BSONHandler {
events: Array<BSONEvent> = new Array<BSONEvent>();
setString(name: string, value: string): void {
this.events.push(new BSONEvent(EventType.String, name, changetype<usize>(value)));
}
setBoolean(name: string, value: bool): void {
this.events.push(new BSONEvent(EventType.Bool, name, changetype<usize>(value)));
}
setNull(name: string): void {
this.events.push(new BSONEvent(EventType.Null, name, 0));
}
setInteger(name: string, value: i32): void {
this.events.push(new BSONEvent(EventType.Int, name, changetype<usize>(value)));
}
setUint8Array(name: string, value: Uint8Array): void {
this.events.push(new BSONEvent(EventType.Bytes, name, changetype<usize>(value)));
}
pushArray(name: string): bool {
this.events.push(new BSONEvent(EventType.PushArray, name, 0));
return true;
}
popArray(): void {
this.events.push(new BSONEvent(EventType.PopArray, "", 0));
}
pushObject(name: string): bool {
this.events.push(new BSONEvent(EventType.PushObject, name, 0));
return true;
}
popObject(): void {
this.events.push(new BSONEvent(EventType.PopObject, "", 0));
}
}
let handler : BSONTestHandler = new BSONTestHandler();
export class StringConversionTests {
static setUp(): void {
handler.events = new Array<BSONEvent>();
}
static createDecoder(): BSONDecoder<BSONTestHandler> {
return new BSONDecoder(handler);
}
static shouldHandleEmptyObject(): bool {
this.createDecoder().deserialize(hex2bin("0500000000"));
return handler.events.length == 0
}
static shouldHandleInt32(): bool {
this.createDecoder().deserialize(hex2bin("0e00000010696e74003412000000"));
return handler.events.length == 1 &&
handler.events[0].toString() == "int: 4660"; // 0x1234
}
static shouldHandleNegativeInt32(): bool {
this.createDecoder().deserialize(hex2bin("0e00000010696e7400f6ffffff00"));
return handler.events.length == 1 &&
handler.events[0].toString() == "int: -10";
}
static shouldHandleString(): bool {
this.createDecoder().deserialize(hex2bin("1a00000002737472000c00000048656c6c6f20576f726c640000"));
return handler.events.length == 1 &&
handler.events[0].toString() == "str: 'Hello World'";
}
static shouldHandleUTF8String() : bool {
this.createDecoder().deserialize(hex2bin("17000000027374720009000000c384c396c39cc39f0000"));
return handler.events.length == 1 &&
handler.events[0].toString() == "str: '" + "\u00C4\u00D6\u00DC\u00DF" + "'";
}
static shouldHandleBooleanFalse(): bool {
this.createDecoder().deserialize(hex2bin("0c00000008626f6f6c000000"));
return handler.events.length == 1 &&
handler.events[0].toString() == "bool: false";
}
static shouldHandleBooleanTrue(): bool {
this.createDecoder().deserialize(hex2bin("0c00000008626f6f6c000100"));
return handler.events.length == 1 &&
handler.events[0].toString() == "bool: true";
}
static shouldHandleNull(): bool {
this.createDecoder().deserialize(hex2bin("0a0000000a6e756c0000"));
return handler.events.length == 1 &&
handler.events[0].toString() == "nul: null";
}
static shouldHandleBytes(): bool {
this.createDecoder().deserialize(hex2bin("190000000562696e000a00000000010203040506070809ff00"));
return handler.events.length == 1 &&
handler.events[0].toString() == "bin: 1,2,3,4,5,6,7,8,9,255";
};
static shouldHandleArray(): bool {
this.createDecoder().deserialize(hex2bin("2b000000046172720021000000103000fa000000103100fb000000103200fc000000103300fd0000000000"));
return handler.events.length == 6 &&
handler.events[0].toString() == "arr: [" &&
handler.events[1].toString() == "0: 250" && // 0xFA
handler.events[2].toString() == "1: 251" && // 0XFB
handler.events[3].toString() == "2: 252" && // 0xFC
handler.events[4].toString() == "3: 253" && // 0xFD
handler.events[5].toString() == "]";
};
static shouldHandleNestedArray(): bool {
this.createDecoder().deserialize(hex2bin("4f000000046172720045000000043000210000001030001000000010310011000000103200120000001033001300000000103100fa000000103200fb000000103300fc000000103400fd0000000000"));
return handler.events.length == 12 &&
handler.events[0].toString() == "arr: [" &&
handler.events[1].toString() == "0: [" &&
handler.events[2].toString() == "0: 16" && // 0x10
handler.events[3].toString() == "1: 17" && // 0X11
handler.events[4].toString() == "2: 18" && // 0x12
handler.events[5].toString() == "3: 19" && // 0x13
handler.events[6].toString() == "]" &&
handler.events[7].toString() == "1: 250" && // 0xFA
handler.events[8].toString() == "2: 251" && // 0XFB
handler.events[9].toString() == "3: 252" && // 0xFC
handler.events[10].toString() == "4: 253" && // 0xFD
handler.events[11].toString() == "]";
}
static shouldHandleObjects(): bool {
this.createDecoder().deserialize(hex2bin("22000000036f626a001800000010696e74000a000000027374720001000000000000"));
return handler.events.length == 4 &&
handler.events[0].toString() == "obj: {" &&
handler.events[1].toString() == "int: 10" &&
handler.events[2].toString() == "str: ''" &&
handler.events[3].toString() == "}";
}
/*
TODO: Enable when serializer is ready
static shouldHandleComplexObjects(): bool {
for (let i = 0; i < deserialize_vector.length; i++) {
let bson = BSON.serialize(deserialize_vector[i].obj);
this.createDecoder().deserialize(hex2bin(deserialize_vector[i].bson), true);
expect(obj).to.deep.equal(deserialize_vector[i].obj);
}
}
*/
static shouldAbortDocumentTooSmall(): void {
this.createDecoder().deserialize(hex2bin("04000000"));
}
static shouldAbortDocumentTermination1(): void {
this.createDecoder().deserialize(hex2bin("0c00000008626f6f6c000001"));
}
static shouldAbortDocumentTermination2(): void {
this.createDecoder().deserialize(hex2bin("0c00000008626f6f6c0000"));
}
static shouldAbortDocumentSizeMismatch(): void {
this.createDecoder().deserialize(hex2bin("0d00000008626f6f6c000000"));
}
static shouldAbortIllegalKeyname(): void {
this.createDecoder().deserialize(hex2bin("0c00000008626f6f6c010100"));
}
static shouldAbortUnknownElement(): void {
this.createDecoder().deserialize(hex2bin("0c00000018626f6f6c000000"));
}
}
function logEvents(): void {
for (let i = 0; i < handler.events.length; i++) {
logStr("events:" + handler.events[i].toString());
}
}
function bytes2array(typedArr: Uint8Array): Array<u8> {
let arr = new Array<u8>();
for (let i = 0; i < typedArr.length; i++) {
arr.push(typedArr[i]);
}
return arr;
}
function hex2bin(hex: string): Uint8Array {
let bin = new Uint8Array(hex.length >>> 1);
for (let i = 0, len = hex.length >>> 1; i < len; i++) {
bin[i] = u32(parseInt(hex.substr(i << 1, 2), 16));
}
return bin;
}

View File

@ -0,0 +1,127 @@
import "allocator/arena";
import { BSONEncoder } from "../../assembly/encoder";
declare function logStr(str: string): void;
declare function logF64(val: f64): void;
let encoder : BSONEncoder;
export class StringConversionTests {
static setUp(): void {
encoder = new BSONEncoder();
}
static shouldHandleEmptyObject(): bool {
return encodedMatches("0500000000");
}
static shouldHandleInt32(): bool {
encoder.setInteger("int", 0x1234);
return encodedMatches("0e00000010696e74003412000000");
}
static shouldHandleNegativeInt32(): bool {
encoder.setInteger("int", -10);
return encodedMatches("0e00000010696e7400f6ffffff00");
}
static shouldHandleString(): bool {
encoder.setString("str", "Hello World");
return encodedMatches("1a00000002737472000c00000048656c6c6f20576f726c640000");
}
static shouldHandleUnicodeString(): bool {
encoder.setString("str", "\u00C4\u00D6\u00DC\u00DF");
return encodedMatches("17000000027374720009000000c384c396c39cc39f0000");
}
static shouldHandleBoolFalse(): bool {
encoder.setBoolean("bool", false);
return encodedMatches("0c00000008626f6f6c000000");
}
static shouldHandleBoolTrue(): bool {
encoder.setBoolean("bool", true);
return encodedMatches("0c00000008626f6f6c000100");
}
static shouldHandleNull(): bool {
encoder.setNull("nul");
return encodedMatches("0a0000000a6e756c0000");
}
static shouldHandleBinary(): bool {
encoder.setUint8Array("bin", array2bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 0xFF]));
return encodedMatches("190000000562696e000a00000000010203040506070809ff00");
}
static shouldHandleArray(): bool {
let arr : Array<i32> = [0xFA, 0xFB, 0xFC, 0xFD];
encoder.pushArray("arr");
for (let i = 0; i < arr.length; i++) {
encoder.setInteger(itoa(i), arr[i]);
}
encoder.popArray();
return encodedMatches("2b000000046172720021000000103000fa000000103100fb000000103200fc000000103300fd0000000000");
}
static shouldHandleNestedArray(): bool {
encoder.pushArray("arr");
encoder.pushArray("0");
let innerArray : Array<i32> = [0x10, 0x11, 0x12, 0x13];
for (let i = 0; i < innerArray.length; i++) {
encoder.setInteger(itoa(i), innerArray[i]);
}
encoder.popArray();
let outerArray : Array<i32> = [0xFA, 0xFB, 0xFC, 0xFD];
for (let i = 0; i < outerArray.length; i++) {
encoder.setInteger(itoa(i + 1), outerArray[i]);
}
encoder.popArray();
return encodedMatches("4f000000046172720045000000043000210000001030001000000010310011000000103200120000001033001300000000103100fa000000103200fb000000103300fc000000103400fd0000000000");
}
static shouldHandleObject(): bool {
encoder.pushObject("obj");
encoder.setInteger("int", 10);
encoder.setString("str", "");
encoder.popObject();
return encodedMatches("22000000036f626a001800000010696e74000a000000027374720001000000000000");
}
}
// TODO: Expose this from std instead of this ugly hack
function itoa(i: i32): string {
let arr: Array<i32> = [i];
return arr.toString();
}
function encodedMatches(hexStr: String): bool {
let bson = encoder.serialize();
let asHex = bin2hex(bson);
let result = asHex == hexStr;
if (!result) {
logStr("expected: " + hexStr);
logStr("actual: " + asHex);
}
return result;
}
function bin2hex(bin: Uint8Array, uppercase: boolean = false): string {
let hex = uppercase ? "0123456789ABCDEF" : "0123456789abcdef";
let str = "";
for (let i = 0, len = bin.length; i < len; i++) {
str += hex.charAt((bin[i] >>> 4) & 0x0f) + hex.charAt(bin[i] & 0x0f);
}
return str;
}
function array2bytes(arr: Array<u8>): Uint8Array {
let bytes = new Uint8Array(arr.length);
for (let i: i32 = 0; i < arr.length; i++) {
bytes[i] = arr[i];
}
return bytes;
}

View File

@ -0,0 +1,63 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
// "lib": [], /* Specify library files to be included in the compilation. */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "./", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true, /* Report errors on unused locals. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
},
"extends": "../../node_modules/assemblyscript/std/assembly.json",
"include": [
"./**/*.ts","*.ts"
]
}

3
tests/decoder.spec.ts Normal file
View File

@ -0,0 +1,3 @@
import { defineTestsFromModule } from './utils/spec';
defineTestsFromModule('decoder');

3
tests/encoder.spec.ts Normal file
View File

@ -0,0 +1,3 @@
import { defineTestsFromModule } from './utils/spec';
defineTestsFromModule('encoder');

96
tests/types/webassembly/index.d.ts vendored Normal file
View File

@ -0,0 +1,96 @@
/**
* WebAssembly v1 (MVP) declaration file for TypeScript
* Definitions by: 01alchemist (https://twitter.com/01alchemist)
*/
declare namespace WebAssembly {
/**
* WebAssembly.Module
**/
class Module {
constructor(bufferSource: ArrayBuffer | ArrayBufferView<number>);
static customSections(module: Module, sectionName: string): ArrayBuffer[];
static exports(module: Module): { name: string, kind: string }[];
static imports(module: Module): { module: string, name: string, kind: string }[];
}
/**
* WebAssembly.Instance
**/
class Instance {
readonly exports: any;
constructor(module: Module, importObject?: any);
}
/**
* WebAssembly.Memory
* Note: A WebAssembly page has a constant size of 65,536 bytes, i.e., 64KiB.
**/
interface MemoryDescriptor {
initial: number;
maximum?: number;
}
class Memory {
readonly buffer: ArrayBuffer;
constructor(memoryDescriptor: MemoryDescriptor);
grow(numPages: number): number;
}
/**
* WebAssembly.Table
**/
interface TableDescriptor {
element: "anyfunc",
initial: number;
maximum?: number;
}
class Table {
readonly length: number;
constructor(tableDescriptor: TableDescriptor);
get(index: number): Function;
grow(numElements: number): number;
set(index: number, value: Function): void;
}
/**
* Errors
*/
class CompileError extends Error {
readonly fileName: string;
readonly lineNumber: string;
readonly columnNumber: string;
constructor(message?: string, fileName?: string, lineNumber?: number);
toString(): string;
}
class LinkError extends Error {
readonly fileName: string;
readonly lineNumber: string;
readonly columnNumber: string;
constructor(message?: string, fileName?: string, lineNumber?: number);
toString(): string;
}
class RuntimeError extends Error {
readonly fileName: string;
readonly lineNumber: string;
readonly columnNumber: string;
constructor(message?: string, fileName?: string, lineNumber?: number);
toString(): string;
}
function compile(bufferSource: ArrayBuffer | ArrayBufferView<number>): Promise<Module>;
interface ResultObject {
module: Module;
instance: Instance;
}
function instantiateStreaming(bufferSource: ArrayBuffer | ArrayBufferView<number>, importObject?: any): Promise<ResultObject>;
function instantiate(bufferSource: ArrayBuffer | ArrayBufferView<number>, importObject?: any): Promise<ResultObject>;
function instantiate(module: Module, importObject?: any): Promise<Instance>;
function validate(bufferSource: ArrayBuffer | ArrayBufferView<number>): boolean;
}

View File

@ -0,0 +1,3 @@
{
"types": "index.d.ts"
}

114
tests/utils/helpers.ts Normal file
View File

@ -0,0 +1,114 @@
/// <reference path="../types/webassembly/index.d.ts" />
import * as fs from 'fs';
import * as path from 'path';
import * as util from 'util';
import { demangle } from 'assemblyscript/lib/loader';
const DIGITALS_REGEXP = /([0-9]{1,})/g;
const UPPER_ALPHAS_REGEXP = /([A-Z]{1,})/g;
export type ImportEntries = { [key: string]: object };
export type ExportedEntry = { [key: string]: Function };
export type ExportedEntries = { [key: string]: ExportedEntry };
const readFile = util.promisify(fs.readFile);
const F64 = new Float64Array(1);
const U64 = new Uint32Array(F64.buffer);
export function decamelize(str: string): string {
const t = str
.replace(DIGITALS_REGEXP, ' $1')
.replace(UPPER_ALPHAS_REGEXP, m => ' ' + (m.length === 1 ? m.toLowerCase() : m));
return t.charAt(0).toUpperCase() + t.slice(1);
}
export async function setup(testFileName: string): Promise<ExportedEntries> {
const pathName = path.resolve(__dirname, `../build/${ testFileName }.wasm`);
const file = await readFile(pathName, null);
if (!WebAssembly.validate(file)) {
throw new Error(`WebAssembly binary "${ pathName }" file not valid!`);
}
const imports = buildImports(`${ testFileName }.spec.as`, new WebAssembly.Memory({ initial: 2 }));
const result = await WebAssembly.instantiate(file, imports);
return demangle<ExportedEntries>(result.instance.exports);
}
function unpackToString64(value: number): string {
F64[0] = value;
return U64[1].toString(16) + U64[0].toString(16);
}
function unpackToString128(lo: number, hi: number): string {
return `0x${ (unpackToString64(hi) + unpackToString64(lo)).padStart(32, '0') }`;
}
function getString(ptr: number, buffer: ArrayBuffer): string {
var U16 = new Uint16Array(buffer);
var U32 = new Uint32Array(buffer);
var dataLength = U32[ptr >>> 2];
var dataOffset = (ptr + 4) >>> 1;
var dataRemain = dataLength;
var parts = [];
const chunkSize = 1024;
while (dataRemain > chunkSize) {
let last = U16[dataOffset + chunkSize - 1];
let size = last >= 0xD800 && last < 0xDC00 ? chunkSize - 1 : chunkSize;
let part = U16.subarray(dataOffset, dataOffset += size);
parts.push(String.fromCharCode.apply(String, part));
dataRemain -= size;
}
return parts.join('') + String.fromCharCode.apply(String, U16.subarray(dataOffset, dataOffset + dataRemain));
}
function buildImports(name: string, memory: WebAssembly.Memory): ImportEntries {
const buffer = memory.buffer;
return {
env: {
memory,
abort(msgPtr: number, filePtr: number, line: number, column: number) {
if (msgPtr) {
throw new Error(
`Abort called by reason "${ getString(msgPtr, buffer) }" at ${ getString(filePtr, buffer) } [${ line }:${ column }]`
);
} else {
throw new Error(`Abort called at ${ getString(filePtr, buffer) } [${ line }:${ column }]`);
}
},
},
// TODO: Don't hardcode support for encoder/decoder
decoder: {
logStr(msgPtr: number) {
if (msgPtr) console.log(`[str]: ${ getString(msgPtr, buffer) }`);
},
logF64(value: number) {
console.log(`[f64]: ${ value }`);
},
},
encoder: {
logStr(msgPtr: number) {
if (msgPtr) console.log(`[str]: ${ getString(msgPtr, buffer) }`);
},
logF64(value: number) {
console.log(`[f64]: ${ value }`);
},
},
[name]: {
logF64(value: number) {
console.log(`[f64]: ${ value }`);
},
logStr(msgPtr: number) {
if (msgPtr) console.log(`[str]: ${ getString(msgPtr, buffer) }`);
},
logU128Packed(msgPtr: number, lo: number, hi: number) {
if (msgPtr) {
console.log(`[u128] ${ getString(msgPtr, buffer) }: ${ unpackToString128(lo, hi) }`);
} else {
console.log(`[u128]: ${ unpackToString128(lo, hi) }`);
}
}
}
};
}

34
tests/utils/spec.ts Normal file
View File

@ -0,0 +1,34 @@
import test from 'ava';
import { setup, decamelize } from './helpers';
export async function defineTestsFromModule(moduleName: string) {
try {
const instance = await setup(moduleName);
// TODO: Refactor into proper testing framework for AssemblyScript
for (const tests in instance) {
const testsInstance = instance[tests];
if (testsInstance.setUp) {
test.beforeEach(() => {
testsInstance.setUp();
});
}
if (testsInstance.tearDown) {
test.afterEach(() => {
testsInstance.tearDown();
});
}
for (const testName of Object.keys(testsInstance).filter(it => !(["setUp", "tearDown"].indexOf(it) != -1))) {
if (testName.startsWith("shouldAbort")) {
test(decamelize(testName), t => { t.throws(() => testsInstance[testName]()) });
} else {
test(decamelize(testName), t => t.truthy(testsInstance[testName]()));
}
}
}
} catch (e) {
console.log("Error loading WebAssembly module:", e);
throw e;
}
};