diff --git a/src/deserializer.ts b/src/deserializer.ts index 56a2de2..746bf17 100644 --- a/src/deserializer.ts +++ b/src/deserializer.ts @@ -93,9 +93,11 @@ export class Deserializer { case Format.Ext32: return this.readExt(this.reader.readU32()) // Otherwise fail. - default: throw new DeserializationError( - `Unrecognized BackPack type 0x${type.toString(16)} at ${this.reader.offset}.` - ) + default: { + throw new DeserializationError( + `Unrecognized BackPack type 0x${type.toString(16)} at ${this.reader.offset}.` + ) + } } } @@ -136,30 +138,68 @@ export class Deserializer { } private readObject(length: number): Record { - const map: Record = {} + const object: Record = {} while (length > 0) { const key = this.decode() as string const value = this.decode() - map[key] = value + object[key] = value --length } + return object + } + + private readMap(length: number): Map { + let size: number + + // prettier-ignore + switch (length) { + case 1: size = this.reader.readU8(); break + case 2: size = this.reader.readU16(); break + case 4: size = this.reader.readU32(); break + + default: { + throw new DeserializationError( + `Invalid map size marker at ${this.reader.offset}. ` + + `Expected 1, 2 or 4 bytes, but got ${length}.` + ) + } + } + + const map: Map = new Map() + + while (size > 0) { + const key = this.decode() as string + const value = this.decode() + + map.set(key, value) + + --size + } + return map } - private readRef(size: number): string { + private readRef(length: number): string { + let size: number + // prettier-ignore - switch (size) { - case 1: return this.refs.get(this.reader.readU8())! - case 2: return this.refs.get(this.reader.readU16())! + switch (length) { + case 1: size = this.reader.readU8(); break + case 2: size = this.reader.readU16(); break - default: throw new DeserializationError( - `Invalid reference size at ${this.reader.offset}. Expected 1-2 bytes, but got ${size}.` - ) + default: { + throw new DeserializationError( + `Invalid reference size marker at ${this.reader.offset}. ` + + `Expected 1 or 2 bytes, but got ${length}.` + ) + } } + + return this.refs.get(size)! } private readTimestamp(size: number): Date { @@ -199,7 +239,8 @@ export class Deserializer { default: { throw new DeserializationError( - `Invalid date size at ${this.reader.offset}. Expected 4, 8 or 12 bytes, but got ${size}.` + `Invalid date size marker at ${this.reader.offset}. ` + + `Expected 4, 8 or 12 bytes, but got ${size}.` ) } } @@ -212,6 +253,7 @@ export class Deserializer { switch (extType) { case Extension.Timestamp: return this.readTimestamp(size) case Extension.Ref: return this.readRef(size) + case Extension.Map: return this.readMap(size) default: return this.extensionCodec?.decode(extType, this.reader.readRange(size)) } } diff --git a/src/formats.ts b/src/formats.ts index e17df14..11384cb 100644 --- a/src/formats.ts +++ b/src/formats.ts @@ -64,25 +64,41 @@ export const enum Format { /** Built-in BackPack extensions. */ export const enum Extension { /** - * **Timestamp extension type** + * [Specification extension][spec-timestamp] for serializing/deserializing {@link Date} objects. * - * Used to serialize and deserialize {@link Date} objects. - * - * This extension type is assigned to extension type `-1`. It defines 3 formats: 32-bit format, - * 64-bit format, and 96-bit format. + * [spec-timestamp]: https://github.com/msgpack/msgpack/blob/master/spec.md#timestamp-extension-type */ Timestamp = -1, /** - * **Reference extension type** - * - * Opt-in extension used for "compressing" repeating data even further. + * Built-in extension for "compressing" repeating string data. * * Works by replacing short (4-16 bytes) repeating strings and property names with 1-2 bytes long - * ids (references), which will be replaced back with associated strings when deserializing. + * monotonically increasing ids (references), which will be replaced back with associated strings + * when deserializing. + * + * ### Formats + * + * - 8-bit + * - 16-bit + * + * Each represents references of (2^N)-1 at most, where N is the number of format's bits. + */ + Ref = -128, + + /** + * Built-in extension for serializing/deserializing JavaScript {@link Map}. This is **not** the + * same map defined in the [MessagePack specification][spec-map]. + * + * ### Formats + * + * - 8-bit + * - 16-bit + * - 32-bit + * + * Each represents maps with (2^N)-1 entries at most, where N is the number of format's bits. * - * This extension type is assigned to extension type `-128` to avoid collisions. It defines 2 - * formats: 8-bit format and 16-bit format. + * [spec-map]: https://github.com/msgpack/msgpack/blob/master/spec.md#map-format-family */ - Ref = -128 + Map = -127 } diff --git a/src/index.spec.ts b/src/index.spec.ts index 503eba2..26af0af 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -53,4 +53,42 @@ describe('serialize/deserialize', () => { it('date', () => { backpack(new Date(2020, 2, 22)) }) + + it('map', () => { + backpack( + new Map([ + ['hello', 42], + ['blazing', 420], + ['turning', 180], + ['warhammer', 9000] + ]) + ) + + backpack({ + map: new Map([ + [42, 'is the meaning'], + [420, 'is for blazing'], + [69, 'is for pleasure'], + [-1000, 'is for the account balance'] + ]) + }) + + backpack([ + new Map([ + ['hello', { message: 'hello' }], + ['bye', { message: 'bye' }] + ]), + new Map([ + [{ kind: 1 }, 'the first kind'], + [{ kind: 2 }, 'the second kind'] + ]) + ]) + + backpack( + new Map([ + [20200220, new Date(2020, 1, 20)], + [19901111, new Date(1990, 10, 11)] + ]) + ) + }) }) diff --git a/src/serializer.ts b/src/serializer.ts index 81db3d9..4119f7b 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -36,6 +36,7 @@ export class Serializer { // Try to apply extensions first in case `data` extends `UIint8Array` or `Array`. if (this.writeExt(data)) return + if (data instanceof Map) return this.writeMap(data) if (data instanceof Date) return this.writeTimestamp(data) if (data instanceof Uint8Array) return this.writeBinary(data) if (Array.isArray(data)) return this.writeArray(data) @@ -216,14 +217,14 @@ export class Serializer { private writeObject(o: object): void { const entries = Object.entries(o) - const length = entries.length - - // Format: fixmap + length (up to 15 elements) - if (length <= 15) this.writer.writeU8(Format.FixMap | length) - // Format: map + length (u16) - else if (length <= 65535) this.writer.writeU8(Format.Map16).writeU16(length) - // Format: map + length (u32) - else if (length <= 4294967295) this.writer.writeU8(Format.Map32).writeU32(length) + const size = entries.length + + // Format: fixmap + size (up to 15 elements) + if (size <= 15) this.writer.writeU8(Format.FixMap | size) + // Format: map + size (u16) + else if (size <= 65535) this.writer.writeU8(Format.Map16).writeU16(size) + // Format: map + size (u32) + else if (size <= 4294967295) this.writer.writeU8(Format.Map32).writeU32(size) // Otherwise fail. else throw new SerializationError('Object is too big. Max (2^32)-1 entries.') @@ -233,6 +234,27 @@ export class Serializer { } } + private writeMap(m: Map): void { + const entries = m.entries() + const size = m.size + + // Format: fixext 1 + extension map + size (u8) + if (size <= 255) this.writer.writeU8(Format.FixExt1).writeI8(Extension.Map).writeU8(size) + // Format: fixext 2 + extension map + size (u16) + else if (size <= 65535) + this.writer.writeU8(Format.FixExt2).writeI8(Extension.Map).writeU16(size) + // Format: fixext 4 + extension map + size (u32) + else if (size <= 4294967295) + this.writer.writeU8(Format.FixExt4).writeI8(Extension.Map).writeU32(size) + // Otherwise fail. + else throw new SerializationError('Map is too big. Max (2^32)-1 entries.') + + for (const [key, value] of entries) { + this.encode(key) + this.encode(value) + } + } + private writeExt(object: unknown): boolean { if (!this.extensionCodec) { return false