Skip to content

Commit

Permalink
refactor(experimental): add padLeftCodec and padRightCodec helpers to…
Browse files Browse the repository at this point in the history
… @solana/codecs-core (#2314)

This PR adds new `padLeftCodec` and `padRightCodec` helper functions. See copy/pasted documentation below:

---


## Padding codecs

The `padLeftCodec` and `padRightCodec` helpers can be used to add padding to the left or right of a given codec. They accept an `offset` number that tells us how big the padding should be.

```ts
const getLeftPaddedCodec = () => padLeftCodec(getU16Codec(), 4);
getLeftPaddedCodec().encode(0xffff);
// 0x00000000ffff
//   |       └-- Our encoded u16 number.
//   └-- Our 4-byte padding.

const getRightPaddedCodec = () => padRightCodec(getU16Codec(), 4);
getRightPaddedCodec().encode(0xffff);
// 0xffff00000000
//   |   └-- Our 4-byte padding.
//   └-- Our encoded u16 number.
```

Note that both the `padLeftCodec` and `padRightCodec` functions are simple wrappers around the `offsetCodec` and `resizeCodec` functions. For more complex padding strategies, you may want to use the `offsetCodec` and `resizeCodec` functions directly instead.

As usual, encoder-only and decoder-only helpers are available for these padding functions. Namely, `padLeftEncoder`, `padRightEncoder`, `padLeftDecoder` and `padRightDecoder`.

```ts
const getMyPaddedEncoder = () => padLeftEncoder(getU16Encoder());
const getMyPaddedDecoder = () => padLeftDecoder(getU16Decoder());
const getMyPaddedCodec = () => combineCodec(getMyPaddedEncoder(), getMyPaddedDecoder());
```
  • Loading branch information
lorisleiva committed Mar 14, 2024
1 parent 09d8cc8 commit f9509c7
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 72 deletions.
28 changes: 28 additions & 0 deletions packages/codecs-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,34 @@ const getU32InTheMiddleDecoder = () => offsetDecoder(biggerU32Decoder, { preOffs
const getU32InTheMiddleCodec = () => combineCodec(getU32InTheMiddleEncoder(), getU32InTheMiddleDecoder());
```

## Padding codecs

The `padLeftCodec` and `padRightCodec` helpers can be used to add padding to the left or right of a given codec. They accept an `offset` number that tells us how big the padding should be.

```ts
const getLeftPaddedCodec = () => padLeftCodec(getU16Codec(), 4);
getLeftPaddedCodec().encode(0xffff);
// 0x00000000ffff
// | └-- Our encoded u16 number.
// └-- Our 4-byte padding.

const getRightPaddedCodec = () => padRightCodec(getU16Codec(), 4);
getRightPaddedCodec().encode(0xffff);
// 0xffff00000000
// | └-- Our 4-byte padding.
// └-- Our encoded u16 number.
```

Note that both the `padLeftCodec` and `padRightCodec` functions are simple wrappers around the `offsetCodec` and `resizeCodec` functions. For more complex padding strategies, you may want to use the `offsetCodec` and `resizeCodec` functions directly instead.

As usual, encoder-only and decoder-only helpers are available for these padding functions. Namely, `padLeftEncoder`, `padRightEncoder`, `padLeftDecoder` and `padRightDecoder`.

```ts
const getMyPaddedEncoder = () => padLeftEncoder(getU16Encoder());
const getMyPaddedDecoder = () => padLeftDecoder(getU16Decoder());
const getMyPaddedCodec = () => combineCodec(getMyPaddedEncoder(), getMyPaddedDecoder());
```

## Reversing codecs

The `reverseCodec` helper reverses the bytes of the provided `FixedSizeCodec`.
Expand Down
60 changes: 44 additions & 16 deletions packages/codecs-core/src/__tests__/__setup__.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Codec, createCodec } from '../codec';
import { Codec, createCodec, FixedSizeCodec } from '../codec';

export const b = (s: string) => base16.encode(s);

Expand All @@ -16,19 +16,47 @@ export const base16: Codec<string> = createCodec({
},
});

export const getMockCodec = (
config: {
defaultValue?: string;
description?: string;
size?: number | null;
} = {},
) =>
createCodec({
type GetMockCodecConfig = {
defaultValue?: string;
description?: string;
innerSize?: number;
size?: number | null;
};

type GetMockCodecReturnType = Codec<unknown> & {
readonly getSizeFromValue: jest.Mock;
readonly read: jest.Mock;
readonly write: jest.Mock;
};

export function getMockCodec<TSize extends number>(
config: GetMockCodecConfig & { size: TSize },
): FixedSizeCodec<unknown, unknown, TSize> & GetMockCodecReturnType;
export function getMockCodec(config?: GetMockCodecConfig): GetMockCodecReturnType;
export function getMockCodec(config: GetMockCodecConfig = {}): GetMockCodecReturnType {
const innerSize = config.innerSize ?? config.size ?? 0;
return createCodec({
...(config.size != null ? { fixedSize: config.size } : { getSizeFromValue: jest.fn().mockReturnValue(0) }),
read: jest.fn().mockReturnValue([config.defaultValue ?? '', 0]),
write: jest.fn().mockReturnValue(0),
}) as Codec<unknown> & {
readonly getSizeFromValue: jest.Mock;
readonly read: jest.Mock;
readonly write: jest.Mock;
};
read: jest.fn().mockImplementation((_bytes, offset) => [config.defaultValue ?? '', offset + innerSize]),
write: jest.fn().mockImplementation((_value, _bytes, offset) => offset + innerSize),
}) as GetMockCodecReturnType;
}

export function expectNewPreOffset(
codec: FixedSizeCodec<unknown>,
mockCodec: FixedSizeCodec<unknown>,
preOffset: number,
expectedNewPreOffset: number,
) {
const bytes = new Uint8Array(Array.from({ length: codec.fixedSize }, () => 0));
codec.write(null, bytes, preOffset);
expect(mockCodec.write).toHaveBeenCalledWith(null, bytes, expectedNewPreOffset);
codec.read(bytes, preOffset)[1];
expect(mockCodec.read).toHaveBeenCalledWith(bytes, expectedNewPreOffset);
}

export function expectNewPostOffset(codec: FixedSizeCodec<unknown>, preOffset: number, expectedNewPostOffset: number) {
const bytes = new Uint8Array(Array.from({ length: codec.fixedSize }, () => 0));
expect(codec.write(null, bytes, preOffset)).toBe(expectedNewPostOffset);
expect(codec.read(bytes, preOffset)[1]).toBe(expectedNewPostOffset);
}
85 changes: 29 additions & 56 deletions packages/codecs-core/src/__tests__/offset-codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,44 @@
import { SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, SolanaError } from '@solana/errors';

import { FixedSizeCodec } from '../codec';
import { offsetCodec } from '../offset-codec';
import { b, getMockCodec as getBaseMockCodec } from './__setup__';

function getMockCodec({ innerSize, totalSize }: { innerSize: number; totalSize: number }) {
const mockCodec = getBaseMockCodec({ size: totalSize });
mockCodec.write.mockImplementation((_value, _bytes, offset) => offset + innerSize);
mockCodec.read.mockImplementation((bytes, offset) => [bytes, offset + innerSize]);
return mockCodec as typeof mockCodec & { fixedSize: number };
}

function expectNewPreOffset(
codec: FixedSizeCodec<unknown>,
mockCodec: FixedSizeCodec<unknown>,
preOffset: number,
expectedNewPreOffset: number,
) {
const bytes = new Uint8Array(Array.from({ length: codec.fixedSize }, () => 0));
codec.write(null, bytes, preOffset);
expect(mockCodec.write).toHaveBeenCalledWith(null, bytes, expectedNewPreOffset);
codec.read(bytes, preOffset)[1];
expect(mockCodec.read).toHaveBeenCalledWith(bytes, expectedNewPreOffset);
}

function expectNewPostOffset(codec: FixedSizeCodec<unknown>, preOffset: number, expectedNewPostOffset: number) {
const bytes = new Uint8Array(Array.from({ length: codec.fixedSize }, () => 0));
expect(codec.write(null, bytes, preOffset)).toBe(expectedNewPostOffset);
expect(codec.read(bytes, preOffset)[1]).toBe(expectedNewPostOffset);
}
import { b, expectNewPostOffset, expectNewPreOffset, getMockCodec } from './__setup__';

describe('offsetCodec', () => {
describe('with relative offsets', () => {
it('keeps the same pre-offset', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ preOffset }) => preOffset });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 3);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x000000[pre=3]ffffffff000000
});

it('keeps the same post-offset', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: ({ postOffset }) => postOffset });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 7);
// Before: 0x000000[pre=3]ffffffff[post=7]000000
// After: 0x000000[pre=3]ffffffff[post=7]000000
});

it('doubles the pre-offset', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ preOffset }) => preOffset * 2 });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 6);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x000000000000[pre=6]ffffffff
});

it('doubles the post-offset', () => {
const mockCodec = getMockCodec({ innerSize: 1, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 1, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: ({ postOffset }) => postOffset * 2 });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 8);
// Before: 0x000000[pre=3]ff[post=4]000000000000
// After: 0x000000[pre=3]ff00000000[post=8]0000
});

it('goes forwards and restores the original offset', () => {
const mockCodec = getMockCodec({ innerSize: 2, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 2, size: 10 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ preOffset }) => preOffset,
preOffset: ({ preOffset }) => preOffset + 2,
Expand All @@ -77,7 +50,7 @@ describe('offsetCodec', () => {
});

it('goes backwards and restores the original offset', () => {
const mockCodec = getMockCodec({ innerSize: 2, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 2, size: 10 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ preOffset }) => preOffset,
preOffset: ({ preOffset }) => preOffset - 3,
Expand All @@ -91,15 +64,15 @@ describe('offsetCodec', () => {

describe('with absolute offsets', () => {
it('sets an absolute pre-offset', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: () => 6 });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 6);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x000000000000[pre=6]ffffffff
});

it('sets an absolute post-offset', () => {
const mockCodec = getMockCodec({ innerSize: 1, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 1, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: () => 8 });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 8);
// Before: 0x000000[pre=3]ff[post=4]000000000000
Expand All @@ -109,15 +82,15 @@ describe('offsetCodec', () => {

describe('with wrapped relative offsets', () => {
it('uses the provided pre-offset as-is if within the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ preOffset, wrapBytes }) => wrapBytes(preOffset + 2) });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 5);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x0000000000[pre=5]ffffffff00
});

it('uses the provided post-offset as-is if within the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ postOffset, wrapBytes }) => wrapBytes(postOffset + 2),
});
Expand All @@ -127,7 +100,7 @@ describe('offsetCodec', () => {
});

it('wraps the pre-offset if it is below the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, {
preOffset: ({ preOffset, wrapBytes }) => wrapBytes(preOffset - 12),
});
Expand All @@ -137,7 +110,7 @@ describe('offsetCodec', () => {
});

it('wraps the post-offset if it is below the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ postOffset, wrapBytes }) => wrapBytes(postOffset - 12),
});
Expand All @@ -147,7 +120,7 @@ describe('offsetCodec', () => {
});

it('wraps the pre-offset if it is above the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, {
preOffset: ({ preOffset, wrapBytes }) => wrapBytes(preOffset + 12),
});
Expand All @@ -157,7 +130,7 @@ describe('offsetCodec', () => {
});

it('wraps the post-offset if it is above the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ postOffset, wrapBytes }) => wrapBytes(postOffset + 12),
});
Expand All @@ -167,7 +140,7 @@ describe('offsetCodec', () => {
});

it('always uses a zero offset if the byte array is empty', () => {
const mockCodec = getMockCodec({ innerSize: 0, totalSize: 0 });
const mockCodec = getMockCodec({ innerSize: 0, size: 0 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ postOffset, wrapBytes }) => wrapBytes(postOffset - 42),
preOffset: ({ preOffset, wrapBytes }) => wrapBytes(preOffset + 42),
Expand All @@ -181,55 +154,55 @@ describe('offsetCodec', () => {

describe('with wrapped absolute offsets', () => {
it('uses the provided pre-offset as-is if within the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ wrapBytes }) => wrapBytes(5) });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 5);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x0000000000[pre=5]ffffffff00
});

it('uses the provided post-offset as-is if within the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: ({ wrapBytes }) => wrapBytes(9) });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 9);
// Before: 0x000000[pre=3]ffffffff[post=7]000000
// After: 0x000000[pre=3]ffffffff0000[post=9]00
});

it('wraps the pre-offset if it is below the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ wrapBytes }) => wrapBytes(-19) });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 1);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x00[pre=1]ffffffff0000000000
});

it('wraps the post-offset if it is below the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: ({ wrapBytes }) => wrapBytes(-15) });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 5);
// Before: 0x000000[pre=3]ffffffff[post=7]000000
// After: 0x000000[pre=3]ffff[post=5]ffff000000
});

it('wraps the pre-offset if it is above the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: ({ wrapBytes }) => wrapBytes(105) });
expectNewPreOffset(codec, mockCodec, /* preOffset */ 3, /* newPreOffset */ 5);
// Before: 0x000000[pre=3]ffffffff000000
// After: 0x0000000000[pre=5]ffffffff00
});

it('wraps the post-offset if it is above the byte range', () => {
const mockCodec = getMockCodec({ innerSize: 4, totalSize: 10 });
const mockCodec = getMockCodec({ innerSize: 4, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: ({ wrapBytes }) => wrapBytes(109) });
expectNewPostOffset(codec, /* preOffset */ 3, /* newPostOffset */ 9);
// Before: 0x000000[pre=3]ffffffff[post=7]000000
// After: 0x000000[pre=3]ffffffff0000[post=9]00
});

it('always uses a zero offset if the byte array is empty', () => {
const mockCodec = getMockCodec({ innerSize: 0, totalSize: 0 });
const mockCodec = getMockCodec({ innerSize: 0, size: 0 });
const codec = offsetCodec(mockCodec, {
postOffset: ({ wrapBytes }) => wrapBytes(-42),
preOffset: ({ wrapBytes }) => wrapBytes(42),
Expand All @@ -243,7 +216,7 @@ describe('offsetCodec', () => {

describe('with offset overflow', () => {
it('throws an error if the pre-offset is negatif', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: () => -1 });
expect(() => codec.encode(42)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, {
Expand All @@ -262,7 +235,7 @@ describe('offsetCodec', () => {
});

it('throws an error if the pre-offset is above the byte array length', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: () => 11 });
expect(() => codec.encode(42)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, {
Expand All @@ -281,14 +254,14 @@ describe('offsetCodec', () => {
});

it('does not throw an error if the pre-offset is equal to the byte array length', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { preOffset: () => 10 });
expect(() => codec.encode(42)).not.toThrow();
expect(() => codec.decode(b('00'.repeat(10)))).not.toThrow();
});

it('throws an error if the post-offset is negatif', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: () => -1 });
expect(() => codec.encode(42)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, {
Expand All @@ -307,7 +280,7 @@ describe('offsetCodec', () => {
});

it('throws an error if the post-offset is above the byte array length', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: () => 11 });
expect(() => codec.encode(42)).toThrow(
new SolanaError(SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, {
Expand All @@ -326,7 +299,7 @@ describe('offsetCodec', () => {
});

it('does not throw an error if the post-offset is equal to the byte array length', () => {
const mockCodec = getBaseMockCodec({ size: 10 });
const mockCodec = getMockCodec({ innerSize: 0, size: 10 });
const codec = offsetCodec(mockCodec, { postOffset: () => 10 });
expect(() => codec.encode(42)).not.toThrow();
expect(() => codec.decode(b('00'.repeat(10)))).not.toThrow();
Expand Down
Loading

0 comments on commit f9509c7

Please sign in to comment.