Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Add ReadonlyUint8Array to codecs + allow decoding from it #2391

Merged
merged 1 commit into from
Apr 2, 2024
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
5 changes: 3 additions & 2 deletions packages/codecs-core/src/__tests__/codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Codec, createCodec, createDecoder, createEncoder, Encoder } from '../codec';
import { ReadonlyUint8Array } from '../readonly-uint8array';

describe('Encoder', () => {
it('can define Encoder instances', () => {
Expand Down Expand Up @@ -27,7 +28,7 @@ describe('Decoder', () => {
it('can define Decoder instances', () => {
const myDecoder = createDecoder({
fixedSize: 32,
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
const slice = bytes.slice(offset, offset + 32);
const str = [...slice].map(charCode => String.fromCharCode(charCode)).join('');
return [str, offset + 32];
Expand All @@ -45,7 +46,7 @@ describe('Codec', () => {
it('can define Codec instances', () => {
const myCodec: Codec<string> = createCodec({
fixedSize: 32,
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
const slice = bytes.slice(offset, offset + 32);
const str = [...slice].map(charCode => String.fromCharCode(charCode)).join('');
return [str, offset + 32];
Expand Down
5 changes: 3 additions & 2 deletions packages/codecs-core/src/__tests__/combine-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {

import { createDecoder, createEncoder, FixedSizeCodec, FixedSizeDecoder, FixedSizeEncoder } from '../codec';
import { combineCodec } from '../combine-codec';
import { ReadonlyUint8Array } from '../readonly-uint8array';

describe('combineCodec', () => {
it('can join encoders and decoders with the same type', () => {
Expand All @@ -19,7 +20,7 @@ describe('combineCodec', () => {

const u8Decoder = createDecoder({
fixedSize: 1,
read: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1],
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [bytes[offset], offset + 1],
});

const u8Codec = combineCodec(u8Encoder, u8Decoder);
Expand All @@ -40,7 +41,7 @@ describe('combineCodec', () => {

const u8Decoder: FixedSizeDecoder<bigint> = createDecoder({
fixedSize: 1,
read: (bytes: Uint8Array, offset = 0) => [BigInt(bytes[offset]), offset + 1],
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [BigInt(bytes[offset]), offset + 1],
});

const u8Codec: FixedSizeCodec<bigint | number, bigint> = combineCodec(u8Encoder, u8Decoder);
Expand Down
12 changes: 8 additions & 4 deletions packages/codecs-core/src/__tests__/map-codec-test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Codec, createCodec, createDecoder, createEncoder } from '../codec';
import { mapCodec, mapDecoder, mapEncoder } from '../map-codec';
import { ReadonlyUint8Array } from '../readonly-uint8array';

const numberCodec: Codec<number> = createCodec({
fixedSize: 1,
read: (bytes: Uint8Array): [number, number] => [bytes[0], 1],
read: (bytes: ReadonlyUint8Array | Uint8Array): [number, number] => [bytes[0], 1],
write: (value: number, bytes, offset) => {
bytes.set([value], offset);
return offset + 1;
Expand Down Expand Up @@ -74,7 +75,7 @@ describe('mapCodec', () => {
type Strict = { discriminator: number; label: string };
const strictCodec: Codec<Strict> = createCodec({
fixedSize: 2,
read: (bytes: Uint8Array): [Strict, number] => [
read: (bytes: ReadonlyUint8Array | Uint8Array): [Strict, number] => [
{ discriminator: bytes[0], label: 'x'.repeat(bytes[1]) },
1,
],
Expand Down Expand Up @@ -118,7 +119,10 @@ describe('mapCodec', () => {
it('can loosen a tuple codec', () => {
const codec: Codec<[number, string]> = createCodec({
fixedSize: 2,
read: (bytes: Uint8Array): [[number, string], number] => [[bytes[0], 'x'.repeat(bytes[1])], 2],
read: (bytes: ReadonlyUint8Array | Uint8Array): [[number, string], number] => [
[bytes[0], 'x'.repeat(bytes[1])],
2,
],
write: (value: [number, string], bytes, offset) => {
bytes.set([value[0], value[1].length], offset);
return offset + 2;
Expand Down Expand Up @@ -163,7 +167,7 @@ describe('mapDecoder', () => {
it('can map an encoder to another encoder', () => {
const decoder = createDecoder({
fixedSize: 1,
read: (bytes: Uint8Array, offset = 0) => [bytes[offset], offset + 1],
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [bytes[offset], offset + 1],
});

const decoderB = mapDecoder(decoder, (value: number): string => 'x'.repeat(value));
Expand Down
21 changes: 20 additions & 1 deletion packages/codecs-core/src/__tests__/reverse-codec-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SOLANA_ERROR__CODECS__EXPECTED_FIXED_LENGTH, SolanaError } from '@solan

import { createDecoder, createEncoder } from '../codec';
import { fixCodec } from '../fix-codec';
import { ReadonlyUint8Array } from '../readonly-uint8array';
import { reverseCodec, reverseDecoder, reverseEncoder } from '../reverse-codec';
import { b, base16 } from './__setup__';

Expand Down Expand Up @@ -57,7 +58,10 @@ describe('reverseDecoder', () => {
it('can reverse the bytes of a fixed-size decoder', () => {
const decoder = createDecoder({
fixedSize: 2,
read: (bytes: Uint8Array, offset = 0) => [`${bytes[offset]}-${bytes[offset + 1]}`, offset + 2],
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [
`${bytes[offset]}-${bytes[offset + 1]}`,
offset + 2,
],
});

const reversedDecoder = reverseDecoder(decoder);
Expand All @@ -67,4 +71,19 @@ describe('reverseDecoder', () => {
// @ts-expect-error Reversed decoder should be fixed-size.
expect(() => reverseDecoder(base16)).toThrow(new SolanaError(SOLANA_ERROR__CODECS__EXPECTED_FIXED_LENGTH));
});

it('does not modify the input bytes in-place', () => {
const decoder = createDecoder({
fixedSize: 2,
read: (bytes: ReadonlyUint8Array | Uint8Array, offset = 0) => [
`${bytes[offset]}-${bytes[offset + 1]}`,
offset + 2,
],
});

const reversedDecoder = reverseDecoder(decoder);
const inputBytes = new Uint8Array([42, 0]);
reversedDecoder.read(inputBytes, 0);
expect(inputBytes).toStrictEqual(new Uint8Array([42, 0]));
});
});
10 changes: 8 additions & 2 deletions packages/codecs-core/src/assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,16 @@ import {
SolanaError,
} from '@solana/errors';

import { ReadonlyUint8Array } from './readonly-uint8array';

/**
* Asserts that a given byte array is not empty.
*/
export function assertByteArrayIsNotEmptyForCodec(codecDescription: string, bytes: Uint8Array, offset = 0) {
export function assertByteArrayIsNotEmptyForCodec(
codecDescription: string,
bytes: ReadonlyUint8Array | Uint8Array,
offset = 0,
) {
if (bytes.length - offset <= 0) {
throw new SolanaError(SOLANA_ERROR__CODECS__CANNOT_DECODE_EMPTY_BYTE_ARRAY, {
codecDescription,
Expand All @@ -22,7 +28,7 @@ export function assertByteArrayIsNotEmptyForCodec(codecDescription: string, byte
export function assertByteArrayHasEnoughBytesForCodec(
codecDescription: string,
expected: number,
bytes: Uint8Array,
bytes: ReadonlyUint8Array | Uint8Array,
offset = 0,
) {
const bytesLength = bytes.length - offset;
Expand Down
6 changes: 4 additions & 2 deletions packages/codecs-core/src/bytes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ReadonlyUint8Array } from './readonly-uint8array';

/**
* Concatenates an array of `Uint8Array`s into a single `Uint8Array`.
* Reuses the original byte array when applicable.
Expand Down Expand Up @@ -26,7 +28,7 @@ export const mergeBytes = (byteArrays: Uint8Array[]): Uint8Array => {
* Pads a `Uint8Array` with zeroes to the specified length.
* If the array is longer than the specified length, it is returned as-is.
*/
export const padBytes = (bytes: Uint8Array, length: number): Uint8Array => {
export const padBytes = (bytes: ReadonlyUint8Array | Uint8Array, length: number): ReadonlyUint8Array | Uint8Array => {
if (bytes.length >= length) return bytes;
const paddedBytes = new Uint8Array(length).fill(0);
paddedBytes.set(bytes);
Expand All @@ -38,5 +40,5 @@ export const padBytes = (bytes: Uint8Array, length: number): Uint8Array => {
* If the array is longer than the specified length, it is truncated.
* If the array is shorter than the specified length, it is padded with zeroes.
*/
export const fixBytes = (bytes: Uint8Array, length: number): Uint8Array =>
export const fixBytes = (bytes: ReadonlyUint8Array | Uint8Array, length: number): ReadonlyUint8Array | Uint8Array =>
padBytes(bytes.length <= length ? bytes : bytes.slice(0, length), length);
6 changes: 4 additions & 2 deletions packages/codecs-core/src/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
SolanaError,
} from '@solana/errors';

import { ReadonlyUint8Array } from './readonly-uint8array';

/**
* Defines an offset in bytes.
*/
Expand Down Expand Up @@ -38,12 +40,12 @@ export type Encoder<TFrom> = FixedSizeEncoder<TFrom> | VariableSizeEncoder<TFrom

type BaseDecoder<TTo> = {
/** Decodes the provided byte array at the given offset (or zero) and returns the value directly. */
readonly decode: (bytes: Uint8Array, offset?: Offset) => TTo;
readonly decode: (bytes: ReadonlyUint8Array | Uint8Array, offset?: Offset) => TTo;
/**
* Reads the encoded value from the provided byte array at the given offset.
* Returns the decoded value and the offset of the next byte after the encoded value.
*/
readonly read: (bytes: Uint8Array, offset: Offset) => [TTo, Offset];
readonly read: (bytes: ReadonlyUint8Array | Uint8Array, offset: Offset) => [TTo, Offset];
};

export type FixedSizeDecoder<TTo, TSize extends number = number> = BaseDecoder<TTo> & {
Expand Down
3 changes: 2 additions & 1 deletion packages/codecs-core/src/fix-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Offset,
} from './codec';
import { combineCodec } from './combine-codec';
import { ReadonlyUint8Array } from './readonly-uint8array';

/**
* Creates a fixed-size encoder from a given encoder.
Expand Down Expand Up @@ -51,7 +52,7 @@ export function fixDecoder<TTo, TSize extends number>(
): FixedSizeDecoder<TTo, TSize> {
return createDecoder({
fixedSize: fixedBytes,
read: (bytes: Uint8Array, offset: Offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset: Offset) => {
assertByteArrayHasEnoughBytesForCodec('fixCodec', fixedBytes, bytes, offset);
// Slice the byte array to the fixed size if necessary.
if (offset > 0 || bytes.length > fixedBytes) {
Expand Down
1 change: 1 addition & 0 deletions packages/codecs-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ export * from './fix-codec';
export * from './map-codec';
export * from './offset-codec';
export * from './pad-codec';
export * from './readonly-uint8array';
export * from './resize-codec';
export * from './reverse-codec';
19 changes: 10 additions & 9 deletions packages/codecs-core/src/map-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
VariableSizeDecoder,
VariableSizeEncoder,
} from './codec';
import { ReadonlyUint8Array } from './readonly-uint8array';

/**
* Converts an encoder A to a encoder B by mapping their values.
Expand Down Expand Up @@ -46,23 +47,23 @@ export function mapEncoder<TOldFrom, TNewFrom>(
*/
export function mapDecoder<TOldTo, TNewTo, TSize extends number>(
decoder: FixedSizeDecoder<TOldTo, TSize>,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): FixedSizeDecoder<TNewTo, TSize>;
export function mapDecoder<TOldTo, TNewTo>(
decoder: VariableSizeDecoder<TOldTo>,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): VariableSizeDecoder<TNewTo>;
export function mapDecoder<TOldTo, TNewTo>(
decoder: Decoder<TOldTo>,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): Decoder<TNewTo>;
export function mapDecoder<TOldTo, TNewTo>(
decoder: Decoder<TOldTo>,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): Decoder<TNewTo> {
return createDecoder({
...decoder,
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
const [value, newOffset] = decoder.read(bytes, offset);
return [map(value, bytes, offset), newOffset];
},
Expand All @@ -87,22 +88,22 @@ export function mapCodec<TOldFrom, TNewFrom, TTo extends TNewFrom & TOldFrom>(
export function mapCodec<TOldFrom, TNewFrom, TOldTo extends TOldFrom, TNewTo extends TNewFrom, TSize extends number>(
codec: FixedSizeCodec<TOldFrom, TOldTo, TSize>,
unmap: (value: TNewFrom) => TOldFrom,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): FixedSizeCodec<TNewFrom, TNewTo, TSize>;
export function mapCodec<TOldFrom, TNewFrom, TOldTo extends TOldFrom, TNewTo extends TNewFrom>(
codec: VariableSizeCodec<TOldFrom, TOldTo>,
unmap: (value: TNewFrom) => TOldFrom,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): VariableSizeCodec<TNewFrom, TNewTo>;
export function mapCodec<TOldFrom, TNewFrom, TOldTo extends TOldFrom, TNewTo extends TNewFrom>(
codec: Codec<TOldFrom, TOldTo>,
unmap: (value: TNewFrom) => TOldFrom,
map: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): Codec<TNewFrom, TNewTo>;
export function mapCodec<TOldFrom, TNewFrom, TOldTo extends TOldFrom, TNewTo extends TNewFrom>(
codec: Codec<TOldFrom, TOldTo>,
unmap: (value: TNewFrom) => TOldFrom,
map?: (value: TOldTo, bytes: Uint8Array, offset: number) => TNewTo,
map?: (value: TOldTo, bytes: ReadonlyUint8Array | Uint8Array, offset: number) => TNewTo,
): Codec<TNewFrom, TNewTo> {
return createCodec({
...mapEncoder(codec, unmap),
Expand Down
3 changes: 2 additions & 1 deletion packages/codecs-core/src/offset-codec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { assertByteArrayOffsetIsNotOutOfRange } from './assertions';
import { Codec, createDecoder, createEncoder, Decoder, Encoder, Offset } from './codec';
import { combineCodec } from './combine-codec';
import { ReadonlyUint8Array } from './readonly-uint8array';

type OffsetConfig = {
postOffset?: PostOffsetFunction;
Expand All @@ -9,7 +10,7 @@ type OffsetConfig = {

type PreOffsetFunctionScope = {
/** The entire byte array. */
bytes: Uint8Array;
bytes: ReadonlyUint8Array | Uint8Array;
/** The original offset prior to encode or decode. */
preOffset: Offset;
/** Wraps the offset to the byte array length. */
Expand Down
4 changes: 4 additions & 0 deletions packages/codecs-core/src/readonly-uint8array.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type TypedArrayMutableProperties = 'copyWithin' | 'fill' | 'reverse' | 'set' | 'sort';
export interface ReadonlyUint8Array extends Omit<Uint8Array, TypedArrayMutableProperties> {
readonly [n: number]: number;
}
2 changes: 1 addition & 1 deletion packages/codecs-core/src/reverse-codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function reverseDecoder<TTo, TSize extends number>(
read: (bytes, offset) => {
const reverseEnd = offset + decoder.fixedSize;
if (offset === 0 && bytes.length === reverseEnd) {
return decoder.read(bytes.reverse(), offset);
return decoder.read(new Uint8Array([...bytes]).reverse(), offset);
mcintyre94 marked this conversation as resolved.
Show resolved Hide resolved
}
const reversedBytes = bytes.slice();
reversedBytes.set(bytes.slice(offset, reverseEnd).reverse(), offset);
Expand Down
3 changes: 2 additions & 1 deletion packages/codecs-data-structures/src/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
FixedSizeDecoder,
FixedSizeEncoder,
getEncodedSize,
ReadonlyUint8Array,
VariableSizeCodec,
VariableSizeDecoder,
VariableSizeEncoder,
Expand Down Expand Up @@ -120,7 +121,7 @@ export function getArrayDecoder<TTo>(item: Decoder<TTo>, config: ArrayCodecConfi

return createDecoder({
...(fixedSize !== null ? { fixedSize } : { maxSize }),
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
const array: TTo[] = [];
if (typeof size === 'object' && bytes.slice(offset).length === 0) {
return [array, offset];
Expand Down
5 changes: 3 additions & 2 deletions packages/codecs-data-structures/src/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FixedSizeEncoder,
fixEncoder,
getEncodedSize,
ReadonlyUint8Array,
VariableSizeCodec,
VariableSizeDecoder,
VariableSizeEncoder,
Expand Down Expand Up @@ -81,7 +82,7 @@ export function getBytesDecoder(config: BytesCodecConfig<NumberDecoder> = {}): D
const size = config.size ?? 'variable';

const byteDecoder: Decoder<Uint8Array> = createDecoder({
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
const slice = bytes.slice(offset);
return [slice, offset + slice.length];
},
Expand All @@ -96,7 +97,7 @@ export function getBytesDecoder(config: BytesCodecConfig<NumberDecoder> = {}): D
}

return createDecoder({
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
assertByteArrayIsNotEmptyForCodec('bytes', bytes, offset);
const [lengthBigInt, lengthOffset] = size.read(bytes, offset);
const length = Number(lengthBigInt);
Expand Down
3 changes: 2 additions & 1 deletion packages/codecs-data-structures/src/discriminated-union.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Encoder,
getEncodedSize,
isFixedSize,
ReadonlyUint8Array,
} from '@solana/codecs-core';
import { getU8Decoder, getU8Encoder, NumberCodec, NumberDecoder, NumberEncoder } from '@solana/codecs-numbers';
import {
Expand Down Expand Up @@ -174,7 +175,7 @@ export function getDiscriminatedUnionDecoder<
const fixedSize = getDiscriminatedUnionFixedSize(variants, prefix);
return createDecoder({
...(fixedSize !== null ? { fixedSize } : { maxSize: getDiscriminatedUnionMaxSize(variants, prefix) }),
read: (bytes: Uint8Array, offset) => {
read: (bytes: ReadonlyUint8Array | Uint8Array, offset) => {
assertByteArrayIsNotEmptyForCodec('discriminatedUnion', bytes, offset);
const [discriminator, dOffset] = prefix.read(bytes, offset);
offset = dOffset;
Expand Down
Loading
Loading