Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -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)
------------------

Expand Down
6 changes: 4 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
4 changes: 2 additions & 2 deletions src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)}:`;
}

Expand Down
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export type Dictionary = Map<string, Item|InnerList>;
*/
export type DictionaryObject = Record<string, BareItem|Item|InnerList>;

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];
12 changes: 8 additions & 4 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
67 changes: 67 additions & 0 deletions test/serializer-tests.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
parseItem,
serializeDictionary,
serializeItem,
SerializeError
Expand Down Expand Up @@ -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', () => {
Expand Down