~kiito/bare-js

ee85765f9277bc31165c0080b57e4ee1f862d2d0 — Emma 4 months ago
Initial commit, first usable state
4 files changed, 670 insertions(+), 0 deletions(-)

A README.md
A bare.js
A example.js
A lib-bare.js
A  => README.md +25 -0
@@ 1,25 @@
# bare-js

This is a work-in-progress JavaScript/Node.js implementation of [BARE](https://baremessages.org/).

The idea so far is, that the parser and converter will run with node.js, while the resulting JavaScript classes should be usable in any JavaScript environment.

Have a look at `examples.js` on how to create type definitions in code for now.  
Or peek inside the `lib-bare.js`, where all conversion classes are located.
These are to be used directly when defining your own type.
Whenever a type takes some sort of arguments, they are static variables right at the top,
just make sure to set them correctly since there are, as of now, no integrity checks on them. 

###What is still missing
 * The schema to js translator
 * Union and Enum types  
   These are difficult to translate to js, so I'm going to have to think about use cases and how to make them convenient to use
 * Conversion error handling and integrity verification
 * Unit tests, these will come last

###Known Issues
Javascript is a wonderful language, and as such it doesn't actually have an integer type.
Instead, every single number is stored as a double precision float.
This limits the usable range of integers to 53 bits, which means the maximum unsigned value is just over 9 quadrillion `(10^15)`
and about ±4.5e15 for signed integers, though that could technically be expanded to ±9e15 since the sign is technically stored outside of the 53 bits.  
In my use case this is not a problem, but for proper adherence to spec this should be addressed.
\ No newline at end of file

A  => bare.js +80 -0
@@ 1,80 @@
/**
 * This is the conversion script, to be run with node.js
 *
 * This script takes two optional arguments: [input, [output]]
 * if either one is omitted it will use stdout and stdin respectively.
 *
 * I am using node.js 14.5.0 but as far as I know, this should run in earlier versions as well,
 * as long as it supports promises and arrow syntax
 */

const fs = require('fs');

function readFile(fileName) {
	return new Promise((resolve, reject) => {
		fs.readFile(fileName, {encoding: 'utf-8'}, function(err, data) {
			if (err) {
				reject(err);
			} else {
				resolve(data);
			}
		});
	});
}

function readStdin() {
	return new Promise((resolve, reject) => {
		fs.read(/* stdin */0, function(err, bytesRead, buffer) {
			if (err) {
				reject(err);
			} else {
				resolve(buffer.toString("utf-8"));
			}
		});
	});
}

function writeFile(fileName, data) {
	return new Promise((resolve, reject) => {
		fs.writeFile(fileName, data, function(err) {
			if (err) {
				reject(err);
			} else {
				resolve();
			}
		})
	});
}

function writeStdout(data) {
	return new Promise((resolve, reject) => {
		fs.write(/* stdout */ 1, data, function(err, written, string) {
			if (err) {
				reject(err);
			} else {
				resolve();
			}
		});
	});
}

function parseSchema(schema) {
	// TODO lexer, parser, conversion to js classes
	return schema;
}

(async function() {
	let [input, output] = process.argv.slice(2, 4);
	let schema;
	if (input) {
		schema = await readFile(input);
	} else {
		schema = await readStdin();
	}
	let jsModule = parseSchema(schema);
	if (output) {
		await writeFile(jsModule);
	} else {
		await writeStdout(jsModule);
	}
})();
\ No newline at end of file

A  => example.js +54 -0
@@ 1,54 @@
let BARE = require("./lib-bare");

class Point extends BARE.ArrayFixed {
	static length = 3;
	static type = BARE.F32;
}

class Test1 extends BARE.Struct {
	static entries = [
		['str', BARE.String],
		['pos', Point],
		['verts', class extends BARE.Array {
			static type = Point;
		}],
		['test', BARE.F64],
		['flag', BARE.Bool],
	];
}

let test1 = {
	str: "Hello World",
	pos: [0, -3, 2.5],
	verts: [
		[-.5, 6900, 2e-6],
		[Math.PI, Infinity, NaN],
	],
	test: Math.PI,
	flag: false,
};

class Test2 extends BARE.Struct {
	static entries = [
		['vals', class extends BARE.Array {
			static type = BARE.Int;
		}],
		['uint', BARE.UInt],
	]
}

let test2 = {
	'vals': [420, -360, 0xFF_FFFF_FFFF],
	'uint': 365555,
};

(() => {

	console.log(tst);

	let res = Test2.pack(tst);
	console.log(res);

	let back = Test2.unpack(res);
	console.log(back);
})();
\ No newline at end of file

A  => lib-bare.js +511 -0
@@ 1,511 @@
"use strict";

/* UTILS */

function joinUint8Arrays(a, b) {
	let c = new Uint8Array(a.length + b.length);
	c.set(a, 0);
	c.set(b, a.length);
	return c;
}

function twoWayMap(pairs) {
	let map = {};
	for (let i = 0; i < pairs.length; i++) {
		let [a, b] = pairs[i];
		map[a] = b;
		map[b] = a;
	}
	return map;
}

/* PRIMITIVE TYPES */

class BareType {
	/**
	 * pack(obj):
	 *      <obj>: a js object compatible with the layout of the class this is called on
	 *      returns: the binary representation of the class in a Uint8Array with values inserted from <obj>
	 */
	static pack(obj) {}

	/**
	 * unpack(raw):
	 *      <raw>: a DataView on the Uint8Array of the message bytes, offset to the start of the unprocessed bytes
	 *      returns: [an object with its values set according to the class layout, the number of bytes consumed by this operation]
	 */
	static unpack(raw) {}
}

class BarePrimitive extends BareType {
	static pack(value) {}
}

class BareUInt extends BarePrimitive {
	static pack(value) {
		let bytes = [];
		while (value >= 0x80) {
			bytes.push((value & 0xFF) | 0x80);
			// shift 7 bits to the right (>> 7)
			// js caps the value to 32bit when using binary operators, so I use division
			value = Math.floor(value / 0x80);
		}
		bytes.push(value);
		return Uint8Array.from(bytes);
	}

	static unpack(raw) {
		let value = 0;
		for (let i = 0;; i++) {
			let byte = raw.getUint8(i);
			if (byte < 0x80) {
				value += byte * Math.pow(0x80, i);
				return [value, i + 1];
			}
			// shifted i*7 bits to the left, same story
			value += (byte & 0x7F) * Math.pow(0x80, i);
		}
	}
}
class BareInt extends BarePrimitive {
	static pack(value) {
		if (value < 0) {
			value = (2 * Math.abs(value)) - 1;
		} else {
			value = 2 * value;
		}
		return BareUInt.pack(value);
	}

	static unpack(raw) {
		let [value, length] = BareUInt.unpack(raw);
		let sign = value % 2;
		value = Math.floor(value / 2);
		if (sign) {
			value = -1 * (value + 1)
		}
		return [value, length];
	}
}

class BareU8 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(1);
		let view = new DataView(bin.buffer);
		view.setUint8(0, value);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getUint8(0);
		return [value, 1];
	}
}
class BareU16 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(2);
		let view = new DataView(bin.buffer);
		view.setUint16(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getUint16(0, true);
		return [value, 2];
	}
}
class BareU32 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(4);
		let view = new DataView(bin.buffer);
		view.setUint32(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getUint32(0, true);
		return [value, 4];
	}
}
class BareU64 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(8);
		let view = new DataView(bin.buffer);
		view.setBigUint64(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getBigUint64(0, true);
		return [value, 8];
	}
}

class BareI8 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(1);
		let view = new DataView(bin.buffer);
		view.setInt8(0, value);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getInt8(0);
		return [value, 1];
	}
}
class BareI16 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(2);
		let view = new DataView(bin.buffer);
		view.setInt16(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getInt16(0, true);
		return [value, 2];
	}
}
class BareI32 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(4);
		let view = new DataView(bin.buffer);
		view.setInt32(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getInt32(0, true);
		return [value, 4];
	}
}
class BareI64 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(8);
		let view = new DataView(bin.buffer);
		view.setBigInt64(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getBigInt64(0, true);
		return [value, 8];
	}
}

class BareF32 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(4);
		let view = new DataView(bin.buffer);
		view.setFloat32(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getFloat32(0, true);
		return [value, 4];
	}
}
class BareF64 extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(8);
		let view = new DataView(bin.buffer);
		view.setFloat64(0, value, true);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getFloat64(0, true);
		return [value, 8];
	}
}

class BareBool extends BarePrimitive {
	static pack(value) {
		let bin = new Uint8Array(1);
		let view = new DataView(bin.buffer);
		value = value ? 1 : 0;
		view.setUint8(0, value);
		return bin;
	}

	static unpack(raw) {
		let value = raw.getUint8(0);
		value = !!value;
		return [value, 1];
	}
}

// TODO how (and where) to represent/store possible values for an enum (since js doesn't have an enum type)
class BareEnum extends BarePrimitive {
	static values; // = twoWayMap([['name', n], ...])

	static pack(obj) {
		let num = this.values[obj];
		return BareUInt.pack(num);
	}

	static unpack(raw) {
		let [value, bytes] = BareUInt.unpack(raw);
		let name = this.values[value];
		return [name, bytes];
	}
}

const BareUTF8Encoder = new TextEncoder();
const BareUTF8Decoder = new TextDecoder();

class BareString extends BarePrimitive {
	static pack(value) {
		let bytes = BareUTF8Encoder.encode(value);
		let length = BareUInt.pack(bytes.length);
		return joinUint8Arrays(length, bytes);
	}

	static unpack(raw) {
		let [length, lenBytes] = BareUInt.unpack(raw);
		let bytes = new DataView(raw.buffer, raw.byteOffset + lenBytes, length);
		let value = BareUTF8Decoder.decode(bytes);
		return [value, length + lenBytes];
	}
}

class BareDataFixed extends BarePrimitive {
	static length;

	static pack(value) {
		return value;
	}

	static unpack(raw) {
		let value = raw.buffer.slice(raw.byteOffset, raw.byteOffset + this.length);
		return [value, this.length];
	}
}
class BareData extends BarePrimitive {
	static pack(value) {
		let length = BareUInt.pack(value.length);
		return joinUint8Arrays(length, value);
	}

	static unpack(raw) {
		let [length, lenBytes] = BareUInt.unpack(raw);
		let value = raw.buffer.slice(raw.byteOffset + lenBytes, raw.byteOffset + lenBytes + length);
		return [value, length + lenBytes];
	}
}

class BareVoid extends BarePrimitive {
	// INVARIANT: may only be used as a member of sets in a tagged union
	static pack(value) {
		return new Uint8Array(0);
	}

	static unpack(raw) {
		return [null, 0];
	}
}

/* AGGREGATE TYPES */

class BareOptional extends BareType {
	static type;

	static pack(obj) {
		if (obj === undefined) {
			return Uint8Array.of(0);
		} else {
			let bytes = this.type.pack(obj);
			let bin = new Uint8Array(bytes.length + 1);
			bin.set([1], 0);
			bin.set(bytes, 1);
			return bin;
		}
	}

	static unpack(raw) {
		let status = raw.getUint8(0);
		if (status === 0) {
			return [undefined, 1];
		} else {
			let [obj, bytes] = this.type.unpack(new DataView(raw.buffer, raw.byteOffset + 1));
			return [obj, bytes + 1];
		}
	}
}

class BareArrayFixed extends BareType {
	// INVARIANT: length is greater than 0
	static length;
	static type;

	static pack(obj) {
		let elements = new Uint8Array(0);
		for (let i = 0; i < this.length; i++) {
			let bytes = this.type.pack(obj[i]);
			elements = joinUint8Arrays(elements, bytes);
		}
		return elements;
	}

	static unpack(raw) {
		let obj = [];
		let length = 0;
		for (let i = 0; i < this.length; i++) {
			let view = new DataView(raw.buffer, raw.byteOffset + length);
			let [elem, bytes] = this.type.unpack(view);
			obj.push(elem);
			length += bytes;
		}
		return [obj, length];
	}
}
class BareArray extends BareType {
	static type;

	static pack(obj) {
		let bin = BareUInt.pack(obj.length);
		for (let i = 0; i < obj.length; i++) {
			let bytes = this.type.pack(obj[i]);
			bin = joinUint8Arrays(bin, bytes);
		}
		return bin;
	}

	static unpack(raw) {
		let obj = [];
		let [numElements, length] = BareUInt.unpack(raw);
		for (let i = 0; i < numElements; i++) {
			let view = new DataView(raw.buffer, raw.byteOffset + length);
			let [elem, bytes] = this.type.unpack(view);
			length += bytes;
			obj.push(elem);
		}
		return [obj, length];
	}
}

class BareMap extends BareType {
	// INVARIANT: map key is a primitive data type but not void, data or data<length>
	static keyType;
	static valueType;

	static pack(obj) {
		let keys = Object.keys(obj);
		let bin = BareUInt.pack(keys.length);
		for (let i = 0; i < keys.length; i++) {
			let keyBytes = this.keyType.pack(keys[i]);
			bin = joinUint8Arrays(bin, keyBytes);
			let valueBytes = this.valueType.pack(obj[keys[i]]);
			bin = joinUint8Arrays(bin, valueBytes);
		}
		return bin;
	}

	static unpack(raw) {
		let obj = {};
		let [numEntries, length] = BareUInt.unpack(raw);
		for (let i = 0; i < numEntries; i++) {
			let keyView = new DataView(raw.buffer, raw.byteOffset + length);
			let [key, keyBytes] = this.type.unpack(keyView);
			length += keyBytes;
			let valueView = new DataView(raw.buffer, raw.byteOffset + length);
			let [value, valueBytes] = this.valueType.unpack(valueView);
			length += valueBytes;
			obj[key] = value;
		}
		return [obj, length];
	}
}

// TODO how do I store/represent the BARE type of an object for unions???
class BareUnion extends BareType {
	// INVARIANT: has at least one type
	static types; // = twoWayMap([[class, i], ...])

	static pack(obj) {
		let unionIndex = this.types[obj.constructor];
		if (!unionIndex) {
			console.err("The union " + this + " does not support encoding the type " + obj)
		}

		let bin = obj.pack();
		let index = BareUInt.pack(unionIndex);
		return joinUint8Arrays(index, bin);
	}

	static unpack(raw) {
		let [index, length] = BareUInt.unpack(raw);
		let objType = this.types[index];
		let [obj, bytes] = objType.unpack(new DataView(raw.buffer, raw.byteOffset + length));
		return [obj, bytes + length];
	}
}

class BareStruct extends BareType {
	// INVARIANT: has at least one field
	static entries; // = [['key', type], ...]

	static pack(obj) {
		let bin = new Uint8Array();
		for (let i = 0; i < this.entries.length; i++) {
			let [key, type] = this.entries[i];
			let bytes = type.pack(obj[key]);
			bin = joinUint8Arrays(bin, bytes);
		}
		return bin;
	}

	static unpack(raw) {
		let obj = {};
		let length = 0;
		for (let i = 0; i < this.entries.length; i++) {
			let [key, type] = this.entries[i];
			let view = new DataView(raw.buffer, raw.byteOffset + length);
			let [value, bytes] = type.unpack(view);
			length += bytes;
			obj[key] = value;
		}
		return obj;
	}
}

module.exports = {
	UInt: BareUInt,
	Int: BareInt,
	U8: BareU8,
	U16: BareU16,
	U32: BareU32,
	U64: BareU64,
	I8: BareI8,
	I16: BareI16,
	I32: BareI32,
	I64: BareI64,
	F32: BareF32,
	F64: BareF64,
	Bool: BareBool,
	Enum: BareEnum, // TODO
	String: BareString,
	DataFixed: BareDataFixed,
	Data: BareData,
	Void: BareVoid,
	Optional: BareOptional,
	ArrayFixed: BareArrayFixed,
	Array: BareArray,
	Map: BareMap,
	Union: BareUnion, // TODO
	Struct: BareStruct,
}

/**
 * Things to be aware of:
 *
 * JavaScript represents all numbers as double precision floating point,
 * therefore the maximum precision for "integers" is clipped to 53-bit.
 * (This means the maximum value is 0x1F_FFFF_FFFF_FFFF or about 9 quadrillion (10^15))
 * This is an issue out of my scope, if you need that kind of precision,
 * there are some libraries which add a proper 64-bit long type.
 * The types UInt, Int, U64 and I64 will require some modification
 * to make use of such a library.
 */