diff --git a/packages/types/src/Bytes.ts b/packages/types/src/Bytes.ts index 50e929efc5..f493a4a40a 100644 --- a/packages/types/src/Bytes.ts +++ b/packages/types/src/Bytes.ts @@ -2,56 +2,37 @@ // This software may be modified and distributed under the terms // of the ISC license. See the LICENSE file for details. -import { AnyU8a } from './types'; - import u8aConcat from '@polkadot/util/u8a/concat'; -import Length from './codec/Length'; +import Compact from './codec/Compact'; import U8a from './codec/U8a'; // A Bytes. The significant difference between this and a normal Uint8Array is that // this version allows for length-encoding. (i.e. it is a variable-item codec, the same -// as what is found in String and Array) +// as what is found in Text and Vector) export default class Bytes extends U8a { - protected _length: Length; - - constructor (value?: AnyU8a) { - super(value); - - this._length = new Length(this.raw.length); - } - get length (): number { - return this._length.toNumber(); + return this.raw.length; } byteLength (): number { - return this._length.byteLength() + this.length; - } - - fromJSON (input: any): Bytes { - super.fromJSON(input); - - this._length.setValue(this.raw.length); - - return this; + return this.length + Compact.encode(this.length).length; } fromU8a (input: Uint8Array): Bytes { - this._length.fromU8a(input); + const [offset, length] = Compact.decode(input); - const length = this._length.toNumber(); - const offset = this._length.byteLength(); - - super.fromU8a(input.subarray(offset, offset + length)); + super.fromU8a(input.subarray(offset, offset + length.toNumber())); return this; } toU8a (isBare?: boolean): Uint8Array { - return u8aConcat( - this._length.toU8a(isBare), - this.raw - ); + return isBare + ? this.raw + : u8aConcat( + Compact.encode(this.length), + this.raw + ); } } diff --git a/packages/types/src/Text.ts b/packages/types/src/Text.ts index baa5a640f4..e1b150f991 100644 --- a/packages/types/src/Text.ts +++ b/packages/types/src/Text.ts @@ -7,7 +7,7 @@ import u8aToUtf8 from '@polkadot/util/u8a/toUtf8'; import u8aConcat from '@polkadot/util/u8a/concat'; import Base from './codec/Base'; -import Length from './codec/Length'; +import Compact from './codec/Compact'; // This is a string wrapper, along with the length. It is used both for strings as well // as stuff like documentation. @@ -18,16 +18,12 @@ import Length from './codec/Length'; // wraps the `Balance`, `T::AccountId`, etc. The reasoning - with a "TypeString" // we can nicely strip types down like "T::AcountId" -> "AccountId" export default class Text extends Base { - protected _length: Length; - constructor (value: Text | string = '') { super( value instanceof Text ? value.raw : value.trim() ); - - this._length = new Length(value.length); } get length (): number { @@ -35,25 +31,19 @@ export default class Text extends Base { } byteLength (): number { - // NOTE String gets trimmed in the fromU8a, so get the original length from the length - // placeholder and the associtated byteLength from that encoding - return this._length.toNumber() + - this._length.byteLength(); + return this.length + Compact.encode(this.length).length; } - fromJSON (input: any): Text { - this.raw = `${input || ''}`; + fromU8a (input: Uint8Array): Text { + const [offset, length] = Compact.decode(input); + + this.raw = u8aToUtf8(input.subarray(offset, offset + length.toNumber())); return this; } - fromU8a (input: Uint8Array): Text { - this._length.fromU8a(input); - - const length = this._length.toNumber(); - const offset = this._length.byteLength(); - - this.raw = u8aToUtf8(input.subarray(offset, offset + length)).trim(); + fromJSON (input: any): Text { + this.raw = `${input || ''}`; return this; } @@ -67,14 +57,13 @@ export default class Text extends Base { } toU8a (isBare?: boolean): Uint8Array { - // NOTE Since we trim in the fromU8a, here we re-calculate the actual length const encoded = u8aFromUtf8(this.raw); - this._length.setValue(encoded.length); - - return u8aConcat( - this._length.toU8a(isBare), - encoded - ); + return isBare + ? encoded + : u8aConcat( + Compact.encode(this.length), + encoded + ); } } diff --git a/packages/types/src/Type.spec.js b/packages/types/src/Type.spec.js index 9ffa87b65b..b7230e314a 100644 --- a/packages/types/src/Type.spec.js +++ b/packages/types/src/Type.spec.js @@ -34,7 +34,7 @@ describe('Type', () => { expect( new Type( new Text(' Box ') - ).length - ).toEqual('Proposal'.length); // eslint-disable-line + ).toString() + ).toEqual('Proposal'); // eslint-disable-line }); }); diff --git a/packages/types/src/Type.ts b/packages/types/src/Type.ts index 86f50cd855..200b41ea3a 100644 --- a/packages/types/src/Type.ts +++ b/packages/types/src/Type.ts @@ -12,12 +12,21 @@ const ALLOWED_BOXES = ['Vec', 'Option']; // what string provides us, however we also "tweak" the types received from the runtime, i.e. // we remove the `T::` prefixes found in some types for consistency accross implementation. export default class Type extends Text { + private _originalLength: number = 0; + constructor (value?: Text | string) { super(value); this._cleanupTypes(); } + // NOTE Length is used in the decoding calculations, so return the original (pre-cleanup) + // length of the data. Since toU8a is disabled, this does not affect encoding, but rather + // only the decoding leg, allowing the decoders to work with original pointers + get length (): number { + return this._originalLength; + } + fromJSON (input: any): Type { super.fromJSON(input); @@ -55,6 +64,7 @@ export default class Type extends Text { // `PropIndex` -> `ProposalIndex` (implementation looks the same, however meant as diff) ]; + this._originalLength = this.raw.length; this.raw = mappings.reduce((result, fn) => { return fn(result); }, this.raw); diff --git a/packages/types/src/codec/Compact.spec.js b/packages/types/src/codec/Compact.spec.js new file mode 100644 index 0000000000..5d10e6c718 --- /dev/null +++ b/packages/types/src/codec/Compact.spec.js @@ -0,0 +1,86 @@ +// Copyright 2017-2018 @polkadot/types authors & contributors +// This software may be modified and distributed under the terms +// of the ISC license. See the LICENSE file for details. + +import BN from 'bn.js'; + +import Compact from './Compact'; +import UInt from './UInt'; + +describe('Compact', () => { + describe('encode', () => { + it('encodes short u8', () => { + expect( + Compact.encode(18) + ).toEqual( + new Uint8Array([18 << 2]) + ); + }); + + it('encodes max u8 values', () => { + expect( + Compact.encode(new UInt(63)) + ).toEqual( + new Uint8Array([0b11111100]) + ); + }); + + it('encodes basic u16 value', () => { + expect( + Compact.encode(511) + ).toEqual( + new Uint8Array([0b11111101, 0b00000111]) + ); + }); + + it('encodes basic ua6 (not at edge)', () => { + expect( + Compact.encode(111) + ).toEqual( + new Uint8Array([0xbd, 0x01]) + ); + }); + + it('encodes basic u32 values (short)', () => { + expect( + Compact.encode(0xffff) + ).toEqual( + new Uint8Array([254, 255, 3, 0]) + ); + }); + + it('encodes basic u32 values (full)', () => { + expect( + Compact.encode(0xfffffff9) + ).toEqual( + new Uint8Array([3, 249, 255, 255, 255]) + ); + }); + }); + + describe('decode', () => { + it('decoded u8 value', () => { + expect( + Compact.decode(new Uint8Array([0b11111100])) + ).toEqual([1, new BN(63)]); + }); + + it('decodes from same u16 encoded value', () => { + expect( + Compact.decode(new Uint8Array([0b11111101, 0b00000111])) + ).toEqual([2, new BN(511)]); + }); + + it('decodes from same u32 encoded value (short)', () => { + expect( + Compact.decode(new Uint8Array([254, 255, 3, 0])) + ).toEqual([4, new BN(0xffff)]); + }); + + it('decodes from same u32 encoded value (full)', () => { + expect( + Compact.decode(new Uint8Array([3, 249, 255, 255, 255])) + ).toEqual([5, new BN(0xfffffff9)]); + }); + }); +}); diff --git a/packages/types/src/codec/Compact.ts b/packages/types/src/codec/Compact.ts index f7115a720a..dab43177af 100644 --- a/packages/types/src/codec/Compact.ts +++ b/packages/types/src/codec/Compact.ts @@ -3,15 +3,21 @@ // of the ISC license. See the LICENSE file for details. import BN from 'bn.js'; +import bnToBn from '@polkadot/util/bn/toBn'; import bnToU8a from '@polkadot/util/bn/toU8a'; import u8aConcat from '@polkadot/util/u8a/concat'; import u8aToBn from '@polkadot/util/u8a/toBn'; +import toU8a from '@polkadot/util/u8a/toU8a'; + +import UInt from './UInt'; + +// TODO Could allow for 64 & 128 https://github.com/paritytech/parity-codec/pull/6 +type BitLength = 32; const MAX_U8 = new BN(2).pow(new BN(8 - 2)).subn(1); const MAX_U16 = new BN(2).pow(new BN(16 - 2)).subn(1); const MAX_U32 = new BN(2).pow(new BN(32 - 2)).subn(1); - -type BitLength = 32; +const DEFAULT_BITLENGTH = 32; // A new compact length-encoding algorithm. It performs the same function as Length, however // differs in that it uses a variable number of bytes to do the actual encoding. From the Rust @@ -29,23 +35,28 @@ type BitLength = 32; // // Note: we use *LOW BITS* of the LSB in LE encoding to encode the 2 bit key. export default class Compact { - static decode (input: Uint8Array, bitLength: BitLength): BN { + static decode (_input: Uint8Array | string, bitLength: BitLength = DEFAULT_BITLENGTH): [number, BN] { + const input = toU8a(_input); const flag = input[0] & 0b11; if (flag === 0b00) { - return new BN(input[0]).shrn(2); + return [1, new BN(input[0]).shrn(2)]; } else if (flag === 0b01) { - return u8aToBn(input.slice(0, 2), true).shrn(2); + return [2, u8aToBn(input.slice(0, 2), true).shrn(2)]; } else if (flag === 0b10) { - return u8aToBn(input.slice(0, 4), true).shrn(2); + return [4, u8aToBn(input.slice(0, 4), true).shrn(2)]; } const byteLength = bitLength / 8; - return u8aToBn(input.subarray(1, 1 + byteLength), true); + return [byteLength + 1, u8aToBn(input.subarray(1, 1 + byteLength), true)]; } - static encode (length: BN, bitLength: BitLength): Uint8Array { + static encode (_length: UInt | BN | number, bitLength: BitLength = DEFAULT_BITLENGTH): Uint8Array { + const length = _length instanceof UInt + ? _length.toBn() + : bnToBn(_length); + if (length.lte(MAX_U8)) { return new Uint8Array([length.toNumber() << 2]); } else if (length.lte(MAX_U16)) { diff --git a/packages/types/src/codec/Length.spec.js b/packages/types/src/codec/Length.spec.js deleted file mode 100644 index a950122212..0000000000 --- a/packages/types/src/codec/Length.spec.js +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2017-2018 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the ISC license. See the LICENSE file for details. - -import Length from './Length'; -import UInt from './UInt'; - -describe('Length', () => { - it('encodes short u8', () => { - expect( - new Length(18).toU8a() - ).toEqual( - new Uint8Array([18 << 2]) - ); - }); - - it('encodes max u8 values', () => { - expect( - new Length(new UInt(63)).toU8a() - ).toEqual( - new Uint8Array([0b11111100]) - ); - }); - - it('encodes basic u16 value', () => { - expect( - new Length(511).toU8a() - ).toEqual( - new Uint8Array([0b11111101, 0b00000111]) - ); - }); - - it('encodes basic ua6 (not at edge)', () => { - expect( - new Length(111).toHex() - ).toEqual('0xbd01'); - }); - - it('decodes from same u16 encoded value', () => { - expect( - new Length() - .fromU8a(new Uint8Array([0b11111101, 0b00000111])) - .toNumber() - ).toEqual(511); - }); - - it('encodes basic u32 values (short)', () => { - expect( - new Length(0xffff).toU8a() - ).toEqual( - new Uint8Array([254, 255, 3, 0]) - ); - }); - - it('decodes from same u32 encoded value (short)', () => { - expect( - new Length() - .fromU8a(new Uint8Array([254, 255, 3, 0])) - .toNumber() - ).toEqual(0xffff); - }); - - it('encodes basic u32 values (full)', () => { - expect( - new Length(0xfffffff9).toU8a() - ).toEqual( - new Uint8Array([3, 249, 255, 255, 255]) - ); - }); - - it('decodes from same u32 encoded value (full)', () => { - expect( - new Length() - .fromU8a(new Uint8Array([3, 249, 255, 255, 255])) - .toNumber() - ).toEqual(0xfffffff9); - }); -}); diff --git a/packages/types/src/codec/Length.ts b/packages/types/src/codec/Length.ts deleted file mode 100644 index 3c6f3080fc..0000000000 --- a/packages/types/src/codec/Length.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright 2017-2018 @polkadot/types authors & contributors -// This software may be modified and distributed under the terms -// of the ISC license. See the LICENSE file for details. - -import { AnyNumber } from '../types'; - -import BN from 'bn.js'; -import bnToBn from '@polkadot/util/bn/toBn'; -import u8aToHex from '@polkadot/util/u8a/toHex'; - -import Base from './Base'; -import Compact from './Compact'; -import UInt from './UInt'; - -type BitLength = 32; - -// A Length-prefix that can be added to any variable length stream of bytes, e.g. String, -// Vectors and Bytes -// -// FIXME Not crazy about the use of Length. If we look at Vector, Bytes & String, -// the implementations are basically the same. Vector is slightly different since -// it iterates over the length. We need a cleaner way where we have less copied -// code and a more generic implementation around the use of Length. Looking at -// Array or Struct, the same type of wrapper would be useful here. -export default class Length extends Base { - private _bitLength: BitLength; - - constructor (value: AnyNumber = new BN(0), bitLength: BitLength = 32) { - super( - UInt.decode(value) - ); - - this._bitLength = bitLength; - } - - byteLength (): number { - return this.toU8a().length; - } - - fromU8a (input: Uint8Array): Length { - this.raw = Compact.decode(input, this._bitLength); - - return this; - } - - toHex (): string { - return u8aToHex(this.toU8a()); - } - - toNumber (): number { - return this.raw.toNumber(); - } - - toU8a (isBare?: boolean): Uint8Array { - return isBare - ? new Uint8Array() - : Compact.encode(this.raw, this._bitLength); - } - - setValue (value: BN | number): void { - this.raw = bnToBn(value); - } -} diff --git a/packages/types/src/codec/U8a.ts b/packages/types/src/codec/U8a.ts index ae0ada42c5..38b808eb5f 100644 --- a/packages/types/src/codec/U8a.ts +++ b/packages/types/src/codec/U8a.ts @@ -23,11 +23,11 @@ export default class U8a extends Base { } get length (): number { - return this.byteLength(); + return this.raw.length; } byteLength (): number { - return this.raw.length; + return this.length; } fromJSON (input: any): U8a { diff --git a/packages/types/src/codec/Vector.ts b/packages/types/src/codec/Vector.ts index b9f9208993..b83b32b7b3 100644 --- a/packages/types/src/codec/Vector.ts +++ b/packages/types/src/codec/Vector.ts @@ -5,7 +5,7 @@ import u8aConcat from '@polkadot/util/u8a/concat'; import Base from './Base'; -import Length from './Length'; +import Compact from './Compact'; // This manages codec arrays. Intrernally it keeps track of the length (as decoded) and allows // construction with the passed `Type` in the constructor. It aims to be an array-like structure, @@ -15,7 +15,6 @@ import Length from './Length'; export default class Vector < T extends Base > extends Base> { - private _length: Length; private _Type: { new(value?: any): T }; constructor (Type: { new(value?: any): T }, value: Array = [] as Array) { @@ -27,7 +26,6 @@ export default class Vector < ) ); - this._length = new Length(value.length); this._Type = Type; } @@ -50,7 +48,7 @@ export default class Vector < byteLength (): number { return this.raw.reduce((total, raw) => { return total + raw.byteLength(); - }, this._length.byteLength()); + }, Compact.encode(this.length).length); } filter (fn: (item: T, index?: number) => any): Array { @@ -74,10 +72,9 @@ export default class Vector < } fromU8a (input: Uint8Array): Vector { - this._length.fromU8a(input); + let [offset, _length] = Compact.decode(input); + const length = _length.toNumber(); - const length = this._length.toNumber(); - let offset = this._length.byteLength(); this.raw = []; for (let index = 0; index < length; index++) { @@ -100,7 +97,6 @@ export default class Vector < push (item: T): void { this.raw.push(item); - this._length.setValue(this.raw.length); } reduce (fn: (result: O, item: T, index?: number) => O, initial: O): O { @@ -114,12 +110,16 @@ export default class Vector < } toU8a (isBare?: boolean): Uint8Array { - return u8aConcat( - this._length.toU8a(isBare), - ...this.raw.map((entry) => - entry.toU8a(isBare) - ) + const encoded = this.raw.map((entry) => + entry.toU8a(isBare) ); + + return isBare + ? u8aConcat(...encoded) + : u8aConcat( + Compact.encode(this.length), + ...encoded + ); } toString (): string {