Browse Source
Move various write methods to a class `BufferWriter`. This allows increased code reuse for libraries that want to implement different serialization formats. Also de-duplicates some code paths and increases test coverage. Original concept by idea by https://github.com/argjv: https://github.com/BitGo/bitgo-utxo-lib/blob/master/src/bufferWriter.jspsbt-tx-getters
6 changed files with 432 additions and 181 deletions
@ -0,0 +1,44 @@ |
|||
'use strict'; |
|||
Object.defineProperty(exports, '__esModule', { value: true }); |
|||
const bufferutils = require('./bufferutils'); |
|||
const types = require('./types'); |
|||
const typeforce = require('typeforce'); |
|||
const varuint = require('varuint-bitcoin'); |
|||
/** |
|||
* Helper class for serialization of bitcoin data types into a pre-allocated buffer. |
|||
*/ |
|||
class BufferWriter { |
|||
constructor(buffer, offset = 0) { |
|||
typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); |
|||
this.buffer = buffer; |
|||
this.offset = offset; |
|||
} |
|||
writeUInt8(i) { |
|||
this.offset = this.buffer.writeUInt8(i, this.offset); |
|||
} |
|||
writeInt32(i) { |
|||
this.offset = this.buffer.writeInt32LE(i, this.offset); |
|||
} |
|||
writeUInt32(i) { |
|||
this.offset = this.buffer.writeUInt32LE(i, this.offset); |
|||
} |
|||
writeUInt64(i) { |
|||
this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); |
|||
} |
|||
writeVarInt(i) { |
|||
varuint.encode(i, this.buffer, this.offset); |
|||
this.offset += varuint.encode.bytes; |
|||
} |
|||
writeSlice(slice) { |
|||
this.offset += slice.copy(this.buffer, this.offset); |
|||
} |
|||
writeVarSlice(slice) { |
|||
this.writeVarInt(slice.length); |
|||
this.writeSlice(slice); |
|||
} |
|||
writeVector(vector) { |
|||
this.writeVarInt(vector.length); |
|||
vector.forEach(buf => this.writeVarSlice(buf)); |
|||
} |
|||
} |
|||
exports.BufferWriter = BufferWriter; |
@ -0,0 +1,231 @@ |
|||
import * as assert from 'assert'; |
|||
import { describe, it } from 'mocha'; |
|||
import { BufferWriter } from '../src/buffer_writer'; |
|||
|
|||
const varuint = require('varuint-bitcoin'); |
|||
|
|||
describe('BufferWriter', () => { |
|||
function concatToBuffer(values: number[][]): Buffer { |
|||
return Buffer.concat(values.map(data => Buffer.from(data))); |
|||
} |
|||
|
|||
function testBuffer( |
|||
bufferWriter: BufferWriter, |
|||
expectedBuffer: Buffer, |
|||
expectedOffset: number = expectedBuffer.length, |
|||
): void { |
|||
assert.strictEqual(bufferWriter.offset, expectedOffset); |
|||
assert.deepStrictEqual( |
|||
bufferWriter.buffer.slice(0, expectedOffset), |
|||
expectedBuffer.slice(0, expectedOffset), |
|||
); |
|||
} |
|||
|
|||
it('writeUint8', () => { |
|||
const values = [0, 1, 254, 255]; |
|||
const expectedBuffer = Buffer.from([0, 1, 0xfe, 0xff]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((v: number) => { |
|||
const expectedOffset = bufferWriter.offset + 1; |
|||
bufferWriter.writeUInt8(v); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeInt32', () => { |
|||
const values = [ |
|||
0, |
|||
1, |
|||
Math.pow(2, 31) - 2, |
|||
Math.pow(2, 31) - 1, |
|||
-1, |
|||
-Math.pow(2, 31), |
|||
]; |
|||
const expectedBuffer = concatToBuffer([ |
|||
[0, 0, 0, 0], |
|||
[1, 0, 0, 0], |
|||
[0xfe, 0xff, 0xff, 0x7f], |
|||
[0xff, 0xff, 0xff, 0x7f], |
|||
[0xff, 0xff, 0xff, 0xff], |
|||
[0x00, 0x00, 0x00, 0x80], |
|||
]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: number) => { |
|||
const expectedOffset = bufferWriter.offset + 4; |
|||
bufferWriter.writeInt32(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeUInt32', () => { |
|||
const maxUInt32 = Math.pow(2, 32) - 1; |
|||
const values = [0, 1, Math.pow(2, 16), maxUInt32]; |
|||
const expectedBuffer = concatToBuffer([ |
|||
[0, 0, 0, 0], |
|||
[1, 0, 0, 0], |
|||
[0, 0, 1, 0], |
|||
[0xff, 0xff, 0xff, 0xff], |
|||
]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: number) => { |
|||
const expectedOffset = bufferWriter.offset + 4; |
|||
bufferWriter.writeUInt32(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeUInt64', () => { |
|||
const values = [ |
|||
0, |
|||
1, |
|||
Math.pow(2, 32), |
|||
Number.MAX_SAFE_INTEGER /* 2^53 - 1 */, |
|||
]; |
|||
const expectedBuffer = concatToBuffer([ |
|||
[0, 0, 0, 0, 0, 0, 0, 0], |
|||
[1, 0, 0, 0, 0, 0, 0, 0], |
|||
[0, 0, 0, 0, 1, 0, 0, 0], |
|||
[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], |
|||
]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: number) => { |
|||
const expectedOffset = bufferWriter.offset + 8; |
|||
bufferWriter.writeUInt64(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeVarInt', () => { |
|||
const values = [ |
|||
0, |
|||
1, |
|||
252, |
|||
253, |
|||
254, |
|||
255, |
|||
256, |
|||
Math.pow(2, 16) - 2, |
|||
Math.pow(2, 16) - 1, |
|||
Math.pow(2, 16), |
|||
Math.pow(2, 32) - 2, |
|||
Math.pow(2, 32) - 1, |
|||
Math.pow(2, 32), |
|||
Number.MAX_SAFE_INTEGER, |
|||
]; |
|||
const expectedBuffer = concatToBuffer([ |
|||
[0x00], |
|||
[0x01], |
|||
[0xfc], |
|||
[0xfd, 0xfd, 0x00], |
|||
[0xfd, 0xfe, 0x00], |
|||
[0xfd, 0xff, 0x00], |
|||
[0xfd, 0x00, 0x01], |
|||
[0xfd, 0xfe, 0xff], |
|||
[0xfd, 0xff, 0xff], |
|||
[0xfe, 0x00, 0x00, 0x01, 0x00], |
|||
[0xfe, 0xfe, 0xff, 0xff, 0xff], |
|||
[0xfe, 0xff, 0xff, 0xff, 0xff], |
|||
[0xff, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00], |
|||
[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1f, 0x00], |
|||
]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: number) => { |
|||
const expectedOffset = |
|||
bufferWriter.offset + varuint.encodingLength(value); |
|||
bufferWriter.writeVarInt(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeSlice', () => { |
|||
const values = [[], [1], [1, 2, 3, 4], [254, 255]]; |
|||
const expectedBuffer = concatToBuffer(values); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((v: number[]) => { |
|||
const expectedOffset = bufferWriter.offset + v.length; |
|||
bufferWriter.writeSlice(Buffer.from(v)); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeVarSlice', () => { |
|||
const values = [ |
|||
Buffer.alloc(1, 1), |
|||
Buffer.alloc(252, 2), |
|||
Buffer.alloc(253, 3), |
|||
]; |
|||
const expectedBuffer = Buffer.concat([ |
|||
Buffer.from([0x01, 0x01]), |
|||
Buffer.from([0xfc]), |
|||
Buffer.alloc(252, 0x02), |
|||
Buffer.from([0xfd, 0xfd, 0x00]), |
|||
Buffer.alloc(253, 0x03), |
|||
]); |
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: Buffer) => { |
|||
const expectedOffset = |
|||
bufferWriter.offset + |
|||
varuint.encodingLength(value.length) + |
|||
value.length; |
|||
bufferWriter.writeVarSlice(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
|
|||
it('writeVector', () => { |
|||
const values = [ |
|||
[Buffer.alloc(1, 4), Buffer.alloc(253, 5)], |
|||
Array(253).fill(Buffer.alloc(1, 6)), |
|||
]; |
|||
const expectedBuffer = Buffer.concat([ |
|||
Buffer.from([0x02]), |
|||
Buffer.from([0x01, 0x04]), |
|||
Buffer.from([0xfd, 0xfd, 0x00]), |
|||
Buffer.alloc(253, 5), |
|||
|
|||
Buffer.from([0xfd, 0xfd, 0x00]), |
|||
Buffer.concat( |
|||
Array(253) |
|||
.fill(0) |
|||
.map(() => Buffer.from([0x01, 0x06])), |
|||
), |
|||
]); |
|||
|
|||
const bufferWriter = new BufferWriter( |
|||
Buffer.allocUnsafe(expectedBuffer.length), |
|||
); |
|||
values.forEach((value: Buffer[]) => { |
|||
const expectedOffset = |
|||
bufferWriter.offset + |
|||
varuint.encodingLength(value.length) + |
|||
value.reduce( |
|||
(sum: number, v) => sum + varuint.encodingLength(v.length) + v.length, |
|||
0, |
|||
); |
|||
bufferWriter.writeVector(value); |
|||
testBuffer(bufferWriter, expectedBuffer, expectedOffset); |
|||
}); |
|||
testBuffer(bufferWriter, expectedBuffer); |
|||
}); |
|||
}); |
@ -0,0 +1,54 @@ |
|||
import * as bufferutils from './bufferutils'; |
|||
import * as types from './types'; |
|||
|
|||
const typeforce = require('typeforce'); |
|||
const varuint = require('varuint-bitcoin'); |
|||
|
|||
/** |
|||
* Helper class for serialization of bitcoin data types into a pre-allocated buffer. |
|||
*/ |
|||
export class BufferWriter { |
|||
buffer: Buffer; |
|||
offset: number; |
|||
|
|||
constructor(buffer: Buffer, offset: number = 0) { |
|||
typeforce(types.tuple(types.Buffer, types.UInt32), [buffer, offset]); |
|||
this.buffer = buffer; |
|||
this.offset = offset; |
|||
} |
|||
|
|||
writeUInt8(i: number): void { |
|||
this.offset = this.buffer.writeUInt8(i, this.offset); |
|||
} |
|||
|
|||
writeInt32(i: number): void { |
|||
this.offset = this.buffer.writeInt32LE(i, this.offset); |
|||
} |
|||
|
|||
writeUInt32(i: number): void { |
|||
this.offset = this.buffer.writeUInt32LE(i, this.offset); |
|||
} |
|||
|
|||
writeUInt64(i: number): void { |
|||
this.offset = bufferutils.writeUInt64LE(this.buffer, i, this.offset); |
|||
} |
|||
|
|||
writeVarInt(i: number): void { |
|||
varuint.encode(i, this.buffer, this.offset); |
|||
this.offset += varuint.encode.bytes; |
|||
} |
|||
|
|||
writeSlice(slice: Buffer): void { |
|||
this.offset += slice.copy(this.buffer, this.offset); |
|||
} |
|||
|
|||
writeVarSlice(slice: Buffer): void { |
|||
this.writeVarInt(slice.length); |
|||
this.writeSlice(slice); |
|||
} |
|||
|
|||
writeVector(vector: Buffer[]): void { |
|||
this.writeVarInt(vector.length); |
|||
vector.forEach((buf: Buffer) => this.writeVarSlice(buf)); |
|||
} |
|||
} |
@ -0,0 +1,16 @@ |
|||
/** |
|||
* Helper class for serialization of bitcoin data types into a pre-allocated buffer. |
|||
*/ |
|||
export declare class BufferWriter { |
|||
buffer: Buffer; |
|||
offset: number; |
|||
constructor(buffer: Buffer, offset?: number); |
|||
writeUInt8(i: number): void; |
|||
writeInt32(i: number): void; |
|||
writeUInt32(i: number): void; |
|||
writeUInt64(i: number): void; |
|||
writeVarInt(i: number): void; |
|||
writeSlice(slice: Buffer): void; |
|||
writeVarSlice(slice: Buffer): void; |
|||
writeVector(vector: Buffer[]): void; |
|||
} |
Loading…
Reference in new issue