diff --git a/changelog.md b/changelog.md index 623abe2..79da0f1 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,13 @@ ChangeLog ========= +2.1.0 (????-??-??) +------------------ + +* Byte Sequences can now be serialized from any `BufferSource` such as + a `Uint8Array`, a Node `Buffer` or a `DataView`. + + 2.0.3 (2026-07-12) ------------------ diff --git a/readme.md b/readme.md index e606bd7..78e5e2d 100644 --- a/readme.md +++ b/readme.md @@ -106,8 +106,10 @@ the second is a `Map` object with parameters. The type is roughly: ```typescript -// The raw value -type BareItem = number | string | Token | ArrayBuffer | boolean | Date | DisplayString; +// The raw value. A Byte Sequence is parsed as an ArrayBuffer, and may be +// serialized from any BufferSource (an ArrayBuffer, a Uint8Array, a Node +// Buffer or a DataView). +type BareItem = number | string | Token | BufferSource | boolean | Date | DisplayString; // The return type of parseItem type Item = [ diff --git a/src/serializer.ts b/src/serializer.ts index c1499fc..27e81d3 100644 --- a/src/serializer.ts +++ b/src/serializer.ts @@ -99,7 +99,7 @@ export function serializeBareItem(input: BareItem): string { if (input instanceof Token) { return serializeToken(input); } - if (input instanceof ArrayBuffer) { + if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) { return serializeByteSequence(input); } if (input instanceof DisplayString) { @@ -161,7 +161,7 @@ export function serializeBoolean(input: boolean): string { return input ? '?1' : '?0'; } -export function serializeByteSequence(input: ArrayBuffer): string { +export function serializeByteSequence(input: BufferSource): string { return `:${arrayBufferToBase64(input)}:`; } diff --git a/src/types.ts b/src/types.ts index 1c4be36..d041150 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,13 @@ export type Dictionary = Map; */ export type DictionaryObject = Record; -export type BareItem = number | string | Token | ArrayBuffer | Date | boolean | DisplayString; +/** + * A standalone value: the value of an Item, or of a parameter. + * + * A Byte Sequence may be serialized from any `BufferSource`, so a + * `Uint8Array`, a Node `Buffer` or a `DataView` can be passed as-is. Parsing + * always returns an `ArrayBuffer`. + */ +export type BareItem = number | string | Token | BufferSource | Date | boolean | DisplayString; export type Item = [BareItem, Parameters]; diff --git a/src/util.ts b/src/util.ts index b05d7d8..cd1d128 100644 --- a/src/util.ts +++ b/src/util.ts @@ -29,10 +29,14 @@ export function isInnerList(input: Item | InnerList): input is InnerList { } -export function arrayBufferToBase64(ab: ArrayBuffer): string { - - // Create a Uint8Array to read the ArrayBuffer as bytes - const bytes = new Uint8Array(ab); +export function arrayBufferToBase64(input: BufferSource): string { + + // Create a Uint8Array to read the bytes. A view may cover only part of a + // larger buffer -- Node allocates small Buffers out of a shared pool -- so + // it is read through its own offset and length, never its whole buffer. + const bytes = ArrayBuffer.isView(input) + ? new Uint8Array(input.buffer, input.byteOffset, input.byteLength) + : new Uint8Array(input); let binary = ''; // Convert each byte to a character diff --git a/test/serializer-tests.js b/test/serializer-tests.js index 376d547..6991d9a 100644 --- a/test/serializer-tests.js +++ b/test/serializer-tests.js @@ -1,4 +1,5 @@ import { + parseItem, serializeDictionary, serializeItem, SerializeError @@ -32,6 +33,72 @@ describe('serializer shorthands', () => { }); + describe('serializeByteSequence', () => { + + // The bytes of sha512(''), which is 64 bytes and therefore always taken + // from Node's shared Buffer pool rather than its own allocation. + const hex = + 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce' + + '47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e'; + const expected = + ':z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==:'; + + const bytes = Uint8Array.from( + hex.match(/../g).map(byte => parseInt(byte, 16)) + ); + + it('should serialize an ArrayBuffer', () => { + + assert.equal(serializeItem(bytes.buffer), expected); + + }); + + it('should serialize a Uint8Array', () => { + + assert.equal(serializeItem(bytes), expected); + + }); + + it('should serialize a Node Buffer', () => { + + // A Buffer this size is a view onto the shared pool, so its own bytes + // sit at an offset inside a much larger buffer. Reading the whole + // buffer would emit unrelated memory. + const buffer = Buffer.from(hex, 'hex'); + assert.ok(buffer.buffer.byteLength > buffer.byteLength); + assert.equal(serializeItem(buffer), expected); + + }); + + it('should serialize a view onto part of a buffer', () => { + + const padded = new Uint8Array(bytes.byteLength + 8); + padded.fill(0xff); + padded.set(bytes, 4); + assert.equal( + serializeItem(padded.subarray(4, 4 + bytes.byteLength)), + expected + ); + + }); + + it('should serialize a DataView', () => { + + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + assert.equal(serializeItem(view), expected); + + }); + + it('should round-trip a parsed Byte Sequence', () => { + + const [value] = parseItem(expected); + assert.ok(value instanceof ArrayBuffer); + assert.equal(serializeItem(value), expected); + + }); + + }); + describe('serializeItem', () => { it('should error when passing a type that\'s not recognized', () => {