Skip to content

Commit 93a633d

Browse files
authored
Major Update of Borsh (#65)
* indexed by class name instead of the class itself * serializer 1.0 * Implemented deserializer * Fixed indentation * Added schema validation * minor improvements * added more tests * added more tests * added more tests * updated readme * minor fix to examples * bump in version * minor update to README.md * minor update to README.md * trigger actions * Removed unnecesary packages + fixed lint * simplified buffer * added base encode/decode * implemented enums and removed deserializing of classes * better organized testing * exported schema * Added forgotten schemas to schema type * allowing numbers in BN * schema now leads serialization order * bump version * feat: allow strings in BN * feat: more tests & checkSchema flag * fix: made compatible to ES5 * updated readme * feat: building cjs & esm * feat: cjs & esm working versions * removed BN.js & bs58 * simplified tests * small change in bigint method * added compatibility with BN
1 parent 6db7476 commit 93a633d

File tree

89 files changed

+2408
-2007
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+2408
-2007
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const buildDir = './lib';
5+
function createEsmModulePackageJson() {
6+
fs.readdir(buildDir, function (err, dirs) {
7+
if (err) {
8+
throw err;
9+
}
10+
dirs.forEach(function (dir) {
11+
if (dir === 'esm') {
12+
var packageJsonFile = path.join(buildDir, dir, '/package.json');
13+
if (!fs.existsSync(packageJsonFile)) {
14+
fs.writeFile(
15+
packageJsonFile,
16+
new Uint8Array(Buffer.from('{"type": "module"}')),
17+
function (err) {
18+
if (err) {
19+
throw err;
20+
}
21+
}
22+
);
23+
}
24+
}
25+
});
26+
});
27+
}
28+
29+
createEsmModulePackageJson();

README.md

Lines changed: 65 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,76 @@ Borsh stands for _Binary Object Representation Serializer for Hashing_. It is me
1414
safety, speed, and comes with a strict specification.
1515

1616
## Examples
17-
### Serializing an object
17+
18+
### (De)serializing a Value
1819
```javascript
19-
const value = new Test({ x: 255, y: 20, z: '123', q: [1, 2, 3] });
20-
const schema = new Map([[Test, { kind: 'struct', fields: [['x', 'u8'], ['y', 'u64'], ['z', 'string'], ['q', [3]]] }]]);
21-
const buffer = borsh.serialize(schema, value);
20+
import * as borsh from 'borsh';
21+
22+
const encodedU16 = borsh.serialize('u16', 2);
23+
const decodedU16 = borsh.deserialize('u16', encodedU16);
24+
25+
const encodedStr = borsh.serialize('string', 'testing');
26+
const decodedStr = borsh.deserialize('string', encodedStr);
2227
```
2328

24-
### Deserializing an object
29+
### (De)serializing an Object
2530
```javascript
26-
const newValue = borsh.deserialize(schema, Test, buffer);
31+
import * as borsh from 'borsh';
32+
33+
const value = {x: 255, y: BigInt(20), z: '123', arr: [1, 2, 3]};
34+
const schema = { struct: { x: 'u8', y: 'u64', 'z': 'string', 'arr': { array: { type: 'u8' }}}};
35+
36+
const encoded = borsh.serialize(schema, value);
37+
const decoded = borsh.deserialize(schema, encoded);
2738
```
2839

29-
## Type Mappings
30-
31-
| Borsh | TypeScript |
32-
|-----------------------|----------------|
33-
| `u8` integer | `number` |
34-
| `u16` integer | `number` |
35-
| `u32` integer | `number` |
36-
| `u64` integer | `BN` |
37-
| `u128` integer | `BN` |
38-
| `u256` integer | `BN` |
39-
| `u512` integer | `BN` |
40-
| `f32` float | N/A |
41-
| `f64` float | N/A |
42-
| fixed-size byte array | `Uint8Array` |
43-
| UTF-8 string | `string` |
44-
| option | `null` or type |
45-
| map | N/A |
46-
| set | N/A |
47-
| structs | `any` |
40+
## API
41+
The package exposes the following functions:
42+
- `serialize(schema: Schema, obj: any): Uint8Array` - serializes an object `obj` according to the schema `schema`.
43+
- `deserialize(schema: Schema, buffer: Uint8Array, class?: Class): any` - deserializes an object according to the schema `schema` from the buffer `buffer`. If the optional parameter `class` is present, the deserialized object will be an of `class`.
44+
45+
## Schemas
46+
Schemas are used to describe the structure of the data being serialized or deserialized. They are used to
47+
validate the data and to determine the order of the fields in the serialized data.
48+
49+
> NOTE: You can find examples of valid in the [test](./borsh-ts/test/utils.test.js) folder.
50+
51+
### Basic Types
52+
Basic types are described by a string. The following types are supported:
53+
- `u8`, `u16`, `u32`, `u64`, `u128` - unsigned integers of 8, 16, 32, 64, and 128 bits respectively.
54+
- `i8`, `i16`, `i32`, `i64`, `i128` - signed integers of 8, 16, 32, 64, and 128 bits respectively.
55+
- `f32`, `f64` - IEEE 754 floating point numbers of 32 and 64 bits respectively.
56+
- `bool` - boolean value.
57+
- `string` - UTF-8 string.
58+
59+
### Arrays, Options, Maps, Sets, Enums, and Structs
60+
More complex objects are described by a JSON object. The following types are supported:
61+
- `{ array: { type: Schema, len?: number } }` - an array of objects of the same type. The type of the array elements is described by the `type` field. If the field `len` is present, the array is fixed-size and the length of the array is `len`. Otherwise, the array is dynamic-sized and the length of the array is serialized before the elements.
62+
- `{ option: Schema }` - an optional object. The type of the object is described by the `type` field.
63+
- `{ map: { key: Schema, value: Schema }}` - a map. The type of the keys and values are described by the `key` and `value` fields respectively.
64+
- `{ set: Schema }` - a set. The type of the elements is described by the `type` field.
65+
- `{ enum: [{ className1: { struct: {...} } }, { className2: { struct: {...} } }, ... ] }` - an enum. The variants of the enum are described by the `className1`, `className2`, etc. fields. The variants are structs.
66+
- `{ struct: { field1: Schema1, field2: Schema2, ... } }` - a struct. The fields of the struct are described by the `field1`, `field2`, etc. fields.
67+
68+
### Type Mappings
69+
70+
| Javascript | Borsh |
71+
|------------------|-----------------------------------|
72+
| `number` | `u8` `u16` `u32` `i8` `i16` `i32` |
73+
| `bigint` | `u64` `u128` `i64` `i128` |
74+
| `number` | `f32` `f64` |
75+
| `number` | `f32` `f64` |
76+
| `boolean` | `bool` |
77+
| `string` | UTF-8 string |
78+
| `type[]` | fixed-size byte array |
79+
| `type[]` | dynamic sized array |
80+
| `object` | enum |
81+
| `Map` | HashMap |
82+
| `Set` | HashSet |
83+
| `null` or `type` | Option |
84+
85+
86+
---
4887

4988
## Contributing
5089

@@ -80,4 +119,4 @@ When publishing to npm use [np](https://github.com/sindresorhus/np).
80119
This repository is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
81120
See [LICENSE-MIT](LICENSE-MIT.txt) and [LICENSE-APACHE](LICENSE-APACHE) for details.
82121

83-
[Borsh]: https://borsh.io
122+
[Borsh]: https://borsh.io

borsh-ts/.eslintrc.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ rules:
1111
'@typescript-eslint/no-explicit-any': 1
1212
'@typescript-eslint/ban-types': 1
1313
'@typescript-eslint/explicit-function-return-type': 1
14-
'@typescript-eslint/no-use-before-define': 1
1514

1615
parserOptions:
1716
ecmaVersion: 2018

borsh-ts/buffer.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { IntegerType } from './types.js';
2+
3+
export class EncodeBuffer {
4+
offset: number;
5+
buffer_size: number;
6+
buffer: ArrayBuffer;
7+
view: DataView;
8+
9+
constructor() {
10+
this.offset = 0;
11+
this.buffer_size = 256;
12+
this.buffer = new ArrayBuffer(this.buffer_size);
13+
this.view = new DataView(this.buffer);
14+
}
15+
16+
resize_if_necessary(needed_space: number): void {
17+
if (this.buffer_size - this.offset < needed_space) {
18+
this.buffer_size = Math.max(this.buffer_size * 2, this.buffer_size + needed_space);
19+
20+
const new_buffer = new ArrayBuffer(this.buffer_size);
21+
new Uint8Array(new_buffer).set(new Uint8Array(this.buffer));
22+
23+
this.buffer = new_buffer;
24+
this.view = new DataView(new_buffer);
25+
}
26+
}
27+
28+
get_used_buffer(): Uint8Array {
29+
return new Uint8Array(this.buffer).slice(0, this.offset);
30+
}
31+
32+
store_value(value: number, type: IntegerType): void {
33+
const bSize = type.substring(1);
34+
const size = parseInt(bSize) / 8;
35+
this.resize_if_necessary(size);
36+
37+
const toCall = type[0] === 'f'? `setFloat${bSize}`: type[0] === 'i'? `setInt${bSize}` : `setUint${bSize}`;
38+
this.view[toCall](this.offset, value, true);
39+
this.offset += size;
40+
}
41+
42+
store_bytes(from: Uint8Array): void {
43+
this.resize_if_necessary(from.length);
44+
new Uint8Array(this.buffer).set(new Uint8Array(from), this.offset);
45+
this.offset += from.length;
46+
}
47+
}
48+
49+
export class DecodeBuffer {
50+
offset: number;
51+
buffer_size: number;
52+
buffer: ArrayBuffer;
53+
view: DataView;
54+
55+
constructor(buf: Uint8Array) {
56+
this.offset = 0;
57+
this.buffer_size = buf.length;
58+
this.buffer = new ArrayBuffer(buf.length);
59+
new Uint8Array(this.buffer).set(buf);
60+
this.view = new DataView(this.buffer);
61+
}
62+
63+
assert_enough_buffer(size: number): void {
64+
if (this.offset + size > this.buffer.byteLength) {
65+
throw new Error('Error in schema, the buffer is smaller than expected');
66+
}
67+
}
68+
69+
consume_value(type: IntegerType): number {
70+
const bSize = type.substring(1);
71+
const size = parseInt(bSize) / 8;
72+
this.assert_enough_buffer(size);
73+
74+
const toCall = type[0] === 'f'? `getFloat${bSize}`: type[0] === 'i'? `getInt${bSize}` : `getUint${bSize}`;
75+
const ret = this.view[toCall](this.offset, true);
76+
77+
this.offset += size;
78+
return ret;
79+
}
80+
81+
consume_bytes(size: number): ArrayBuffer {
82+
this.assert_enough_buffer(size);
83+
const ret = this.buffer.slice(this.offset, this.offset + size);
84+
this.offset += size;
85+
return ret;
86+
}
87+
}

borsh-ts/deserialize.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { ArrayType, DecodeTypes, MapType, IntegerType, OptionType, Schema, SetType, StructType, integers, EnumType } from './types.js';
2+
import { DecodeBuffer } from './buffer.js';
3+
4+
export class BorshDeserializer {
5+
buffer: DecodeBuffer;
6+
7+
constructor(bufferArray: Uint8Array) {
8+
this.buffer = new DecodeBuffer(bufferArray);
9+
}
10+
11+
decode(schema: Schema): DecodeTypes {
12+
return this.decode_value(schema);
13+
}
14+
15+
decode_value(schema: Schema): DecodeTypes {
16+
if (typeof schema === 'string') {
17+
if (integers.includes(schema)) return this.decode_integer(schema);
18+
if (schema === 'string') return this.decode_string();
19+
if (schema === 'bool') return this.decode_boolean();
20+
}
21+
22+
if (typeof schema === 'object') {
23+
if ('option' in schema) return this.decode_option(schema as OptionType);
24+
if ('enum' in schema) return this.decode_enum(schema as EnumType);
25+
if ('array' in schema) return this.decode_array(schema as ArrayType);
26+
if ('set' in schema) return this.decode_set(schema as SetType);
27+
if ('map' in schema) return this.decode_map(schema as MapType);
28+
if ('struct' in schema) return this.decode_struct(schema as StructType);
29+
}
30+
31+
throw new Error(`Unsupported type: ${schema}`);
32+
}
33+
34+
decode_integer(schema: IntegerType): number | bigint {
35+
const size: number = parseInt(schema.substring(1));
36+
37+
if (size <= 32 || schema == 'f64') {
38+
return this.buffer.consume_value(schema);
39+
}
40+
return this.decode_bigint(size, schema.startsWith('i'));
41+
}
42+
43+
decode_bigint(size: number, signed = false): bigint {
44+
const buffer_len = size / 8;
45+
const buffer = new Uint8Array(this.buffer.consume_bytes(buffer_len));
46+
const bits = buffer.reduceRight((r, x) => r + x.toString(16).padStart(2, '0'), '');
47+
48+
if (signed && buffer[buffer_len - 1]) {
49+
return BigInt.asIntN(size, BigInt(`0x${bits}`));
50+
}
51+
return BigInt(`0x${bits}`);
52+
}
53+
54+
decode_string(): string {
55+
const len: number = this.decode_integer('u32') as number;
56+
const buffer = new Uint8Array(this.buffer.consume_bytes(len));
57+
return String.fromCharCode.apply(null, buffer);
58+
}
59+
60+
decode_boolean(): boolean {
61+
return this.buffer.consume_value('u8') > 0;
62+
}
63+
64+
decode_option(schema: OptionType): DecodeTypes {
65+
const option = this.buffer.consume_value('u8');
66+
if (option === 1) {
67+
return this.decode_value(schema.option);
68+
}
69+
if (option !== 0) {
70+
throw new Error(`Invalid option ${option}`);
71+
}
72+
return null;
73+
}
74+
75+
decode_enum(schema: EnumType): DecodeTypes {
76+
const valueIndex = this.buffer.consume_value('u8');
77+
78+
if (valueIndex > schema.enum.length) {
79+
throw new Error(`Enum option ${valueIndex} is not available`);
80+
}
81+
82+
const struct = schema.enum[valueIndex].struct;
83+
const key = Object.keys(struct)[0];
84+
return { [key]: this.decode_value(struct[key]) };
85+
}
86+
87+
decode_array(schema: ArrayType): Array<DecodeTypes> {
88+
const result = [];
89+
const len = schema.array.len ? schema.array.len : this.decode_integer('u32') as number;
90+
91+
for (let i = 0; i < len; ++i) {
92+
result.push(this.decode_value(schema.array.type));
93+
}
94+
95+
return result;
96+
}
97+
98+
decode_set(schema: SetType): Set<DecodeTypes> {
99+
const len = this.decode_integer('u32') as number;
100+
const result = new Set<DecodeTypes>();
101+
for (let i = 0; i < len; ++i) {
102+
result.add(this.decode_value(schema.set));
103+
}
104+
return result;
105+
}
106+
107+
decode_map(schema: MapType): Map<DecodeTypes, DecodeTypes> {
108+
const len = this.decode_integer('u32') as number;
109+
const result = new Map();
110+
for (let i = 0; i < len; ++i) {
111+
const key = this.decode_value(schema.map.key);
112+
const value = this.decode_value(schema.map.value);
113+
result.set(key, value);
114+
}
115+
return result;
116+
}
117+
118+
decode_struct(schema: StructType): object {
119+
const result = {};
120+
for (const key in schema.struct) {
121+
result[key] = this.decode_value(schema.struct[key]);
122+
}
123+
return result;
124+
}
125+
}

0 commit comments

Comments
 (0)