From 288029a55a5eeb863b6df960027a59214ffc37f1 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Tue, 2 Apr 2024 23:49:10 +0100 Subject: [PATCH] refactor(experimental): add getLiteralUnionCodec to @solana/codecs-data-structures (#2394) This PR adds a new `getLiteralUnionCodec` that functions similarly to `getEnumCodec` but uses TypeScript unions to describe all possible values. ```ts const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down']); // ^? FixedSizeCodec<"left" | "right" | "up" | "down"> const bytes = codec.encode('left'); // 0x00 const value = codec.decode(bytes); // 'left' ``` Fixes https://github.com/solana-labs/solana-web3.js/issues/2296 --- .changeset/tender-turtles-bake.md | 14 ++ packages/codecs-data-structures/README.md | 34 +++++ .../src/__tests__/literal-union-test.ts | 133 +++++++++++++++++ .../__typetests__/literal-union-typetest.ts | 42 ++++++ .../src/literal-union.ts | 134 ++++++++++++++++++ packages/codecs/README.md | 1 + packages/errors/src/codes.ts | 4 + packages/errors/src/context.ts | 11 ++ packages/errors/src/messages.ts | 6 + 9 files changed, 379 insertions(+) create mode 100644 .changeset/tender-turtles-bake.md create mode 100644 packages/codecs-data-structures/src/__tests__/literal-union-test.ts create mode 100644 packages/codecs-data-structures/src/__typetests__/literal-union-typetest.ts create mode 100644 packages/codecs-data-structures/src/literal-union.ts diff --git a/.changeset/tender-turtles-bake.md b/.changeset/tender-turtles-bake.md new file mode 100644 index 000000000000..c9cf685aff1c --- /dev/null +++ b/.changeset/tender-turtles-bake.md @@ -0,0 +1,14 @@ +--- +'@solana/codecs-data-structures': patch +'@solana/errors': patch +--- + +Added a new `getLiteralUnionCodec` + +```ts +const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down']); +// ^? FixedSizeCodec<"left" | "right" | "up" | "down"> + +const bytes = codec.encode('left'); // 0x00 +const value = codec.decode(bytes); // 'left' +``` \ No newline at end of file diff --git a/packages/codecs-data-structures/README.md b/packages/codecs-data-structures/README.md index 253a9c2159ef..08315f095dd8 100644 --- a/packages/codecs-data-structures/README.md +++ b/packages/codecs-data-structures/README.md @@ -365,6 +365,40 @@ const bytes = getDiscriminatedUnionEncoder(variantEncoders).encode({ __kind: 'Qu const message = getDiscriminatedUnionDecoder(variantDecoders).decode(bytes); ``` +## Literal union codec + +The `getLiteralUnionCodec` function works similarly to the `getUnionCodec` function but does not require a JavaScript `enum` to exist. + +It accepts an array of literal values — such as `string`, `number`, `boolean`, etc. — and returns a codec that encodes and decodes such values using by using their index in the array. It uses TypeScript unions to represent all the possible values. + +```ts +const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down']); +// ^? FixedSizeCodec<"left" | "right" | "up" | "down"> + +const bytes = codec.encode('left'); // 0x00 +const value = codec.decode(bytes); // 'left' +``` + +As you can see, it uses a `u8` number by default to store the index of the value. However, you may provide a number codec as the `size` option of the `getLiteralUnionCodec` function to customise that behaviour. + +```ts +const codec = getLiteralUnionCodec(['left', 'right', 'up', 'down'], { + size: getU32Codec(), +}); + +codec.encode('left'); // 0x00000000 +codec.encode('right'); // 0x01000000 +codec.encode('up'); // 0x02000000 +codec.encode('down'); // 0x03000000 +``` + +Separate `getLiteralUnionEncoder` and `getLiteralUnionDecoder` functions are also available. + +```ts +const bytes = getLiteralUnionEncoder(['left', 'right']).encode('left'); // 0x00 +const value = getLiteralUnionDecoder(['left', 'right']).decode(bytes); // 'left' +``` + ## Boolean codec The `getBooleanCodec` function returns a `Codec` that stores the boolean as `0` or `1` using a `u8` number by default. diff --git a/packages/codecs-data-structures/src/__tests__/literal-union-test.ts b/packages/codecs-data-structures/src/__tests__/literal-union-test.ts new file mode 100644 index 000000000000..e9454726e846 --- /dev/null +++ b/packages/codecs-data-structures/src/__tests__/literal-union-test.ts @@ -0,0 +1,133 @@ +import { assertIsFixedSize, assertIsVariableSize } from '@solana/codecs-core'; +import { getShortU16Codec, getU32Codec } from '@solana/codecs-numbers'; +import { + SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, + SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; + +import { getLiteralUnionCodec } from '../literal-union'; +import { b } from './__setup__'; + +describe('getLiteralUnionCodec', () => { + it('encodes string unions', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C']); + expect(codec.encode('A')).toStrictEqual(b('00')); + expect(codec.encode('B')).toStrictEqual(b('01')); + expect(codec.encode('C')).toStrictEqual(b('02')); + }); + + it('decodes string unions', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C']); + expect(codec.decode(b('00'))).toBe('A'); + expect(codec.decode(b('01'))).toBe('B'); + expect(codec.decode(b('02'))).toBe('C'); + }); + + it('encodes number and bigint unions', () => { + const codec = getLiteralUnionCodec([1, 2n, 3]); + expect(codec.encode(1)).toStrictEqual(b('00')); + expect(codec.encode(2n)).toStrictEqual(b('01')); + expect(codec.encode(3)).toStrictEqual(b('02')); + }); + + it('decodes number and bigint unions', () => { + const codec = getLiteralUnionCodec([1, 2n, 3]); + expect(codec.decode(b('00'))).toBe(1); + expect(codec.decode(b('01'))).toBe(2n); + expect(codec.decode(b('02'))).toBe(3); + }); + + it('encodes boolean unions', () => { + const codec = getLiteralUnionCodec([true, false]); + expect(codec.encode(true)).toStrictEqual(b('00')); + expect(codec.encode(false)).toStrictEqual(b('01')); + }); + + it('decodes boolean unions', () => { + const codec = getLiteralUnionCodec([true, false]); + expect(codec.decode(b('00'))).toBe(true); + expect(codec.decode(b('01'))).toBe(false); + }); + + it('encodes null and undefined unions', () => { + const codec = getLiteralUnionCodec([null, undefined]); + expect(codec.encode(null)).toStrictEqual(b('00')); + expect(codec.encode(undefined)).toStrictEqual(b('01')); + }); + + it('decodes null and undefined unions', () => { + const codec = getLiteralUnionCodec([null, undefined]); + expect(codec.decode(b('00'))).toBeNull(); + expect(codec.decode(b('01'))).toBeUndefined(); + }); + + it('pushes the offset forward when writing', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C']); + expect(codec.write('A', new Uint8Array(10), 6)).toBe(7); + }); + + it('pushes the offset forward when reading', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C']); + expect(codec.read(b('ffff00'), 2)).toStrictEqual(['A', 3]); + }); + + it('encodes using a custom discriminator size', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C'], { + size: getU32Codec(), + }); + expect(codec.encode('A')).toStrictEqual(b('00000000')); + expect(codec.encode('B')).toStrictEqual(b('01000000')); + expect(codec.encode('C')).toStrictEqual(b('02000000')); + }); + + it('decodes using a custom discriminator size', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C'], { + size: getU32Codec(), + }); + expect(codec.decode(b('00000000'))).toBe('A'); + expect(codec.decode(b('01000000'))).toBe('B'); + expect(codec.decode(b('02000000'))).toBe('C'); + }); + + it('throws when provided with an invalid variant', () => { + const codec = getLiteralUnionCodec(['one', 2, 3n, false, null]); + // @ts-expect-error Expected invalid variant. + expect(() => codec.encode('missing')).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, { + value: 'missing', + variants: ['one', 2, 3n, false, null], + }), + ); + }); + + it('throws when provided with an invalid discriminator', () => { + const codec = getLiteralUnionCodec(['one', 2, 3n, false, null]); + expect(() => codec.decode(b('05'))).toThrow( + new SolanaError(SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: 5, + maxRange: 4, + minRange: 0, + }), + ); + }); + + it('returns the correct default fixed size', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C']); + assertIsFixedSize(codec); + expect(codec.fixedSize).toBe(1); + }); + + it('returns the correct custom fixed size', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C'], { size: getU32Codec() }); + assertIsFixedSize(codec); + expect(codec.fixedSize).toBe(4); + }); + + it('returns the correct custom variable size', () => { + const codec = getLiteralUnionCodec(['A', 'B', 'C'], { size: getShortU16Codec() }); + assertIsVariableSize(codec); + expect(codec.getSizeFromValue('A')).toBe(1); + expect(codec.maxSize).toBe(3); + }); +}); diff --git a/packages/codecs-data-structures/src/__typetests__/literal-union-typetest.ts b/packages/codecs-data-structures/src/__typetests__/literal-union-typetest.ts new file mode 100644 index 000000000000..eff042eca7c6 --- /dev/null +++ b/packages/codecs-data-structures/src/__typetests__/literal-union-typetest.ts @@ -0,0 +1,42 @@ +import { + FixedSizeCodec, + FixedSizeDecoder, + FixedSizeEncoder, + VariableSizeCodec, + VariableSizeDecoder, + VariableSizeEncoder, +} from '@solana/codecs-core'; +import { getU32Codec, getU32Decoder, getU32Encoder } from '@solana/codecs-numbers'; + +import { getLiteralUnionCodec, getLiteralUnionDecoder, getLiteralUnionEncoder } from '../literal-union'; + +{ + // [getLiteralUnionEncoder]: It knows if the encoder is fixed size or variable size. + getLiteralUnionEncoder(['one', 2, 3n]) satisfies FixedSizeEncoder<'one' | 2 | 3n, 1>; + getLiteralUnionEncoder(['one', 2, 3n], { size: getU32Encoder() }) satisfies FixedSizeEncoder<'one' | 2 | 3n, 4>; + getLiteralUnionEncoder(['one', 2, 3n], { size: {} as VariableSizeEncoder }) satisfies VariableSizeEncoder< + 'one' | 2 | 3n + >; +} + +{ + // [getLiteralUnionDecoder]: It knows if the decoder is fixed size or variable size. + getLiteralUnionDecoder(['one', 2, 3n]) satisfies FixedSizeDecoder<'one' | 2 | 3n, 1>; + getLiteralUnionDecoder(['one', 2, 3n], { size: getU32Decoder() }) satisfies FixedSizeDecoder<'one' | 2 | 3n, 4>; + getLiteralUnionDecoder(['one', 2, 3n], { size: {} as VariableSizeDecoder }) satisfies VariableSizeDecoder< + 'one' | 2 | 3n + >; +} + +{ + // [getLiteralUnionCodec]: It knows if the codec is fixed size or variable size. + getLiteralUnionCodec(['one', 2, 3n]) satisfies FixedSizeCodec<'one' | 2 | 3n, 'one' | 2 | 3n, 1>; + getLiteralUnionCodec(['one', 2, 3n], { size: getU32Codec() }) satisfies FixedSizeCodec< + 'one' | 2 | 3n, + 'one' | 2 | 3n, + 4 + >; + getLiteralUnionCodec(['one', 2, 3n], { size: {} as VariableSizeCodec }) satisfies VariableSizeCodec< + 'one' | 2 | 3n + >; +} diff --git a/packages/codecs-data-structures/src/literal-union.ts b/packages/codecs-data-structures/src/literal-union.ts new file mode 100644 index 000000000000..04e32d5d3b42 --- /dev/null +++ b/packages/codecs-data-structures/src/literal-union.ts @@ -0,0 +1,134 @@ +import { + Codec, + combineCodec, + Decoder, + Encoder, + FixedSizeCodec, + FixedSizeDecoder, + FixedSizeEncoder, + mapDecoder, + mapEncoder, + VariableSizeCodec, + VariableSizeDecoder, + VariableSizeEncoder, +} from '@solana/codecs-core'; +import { + FixedSizeNumberCodec, + FixedSizeNumberDecoder, + FixedSizeNumberEncoder, + getU8Decoder, + getU8Encoder, + NumberCodec, + NumberDecoder, + NumberEncoder, +} from '@solana/codecs-numbers'; +import { + SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, + SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; + +/** Defines the config for literal union codecs. */ +export type LiteralUnionCodecConfig = { + /** + * The number codec to use for the literal union discriminator. + * @defaultValue u8 discriminator. + */ + size?: TDiscriminator; +}; + +type Variant = bigint | boolean | number | string | null | undefined; +type GetTypeFromVariants = TVariants[number]; + +/** + * Creates a literal union encoder. + * + * @param variants - The variant encoders of the literal union. + * @param config - A set of config for the encoder. + */ +export function getLiteralUnionEncoder( + variants: TVariants, +): FixedSizeEncoder, 1>; +export function getLiteralUnionEncoder( + variants: TVariants, + config: LiteralUnionCodecConfig & { size: FixedSizeNumberEncoder }, +): FixedSizeEncoder, TSize>; +export function getLiteralUnionEncoder( + variants: TVariants, + config?: LiteralUnionCodecConfig, +): VariableSizeEncoder>; +export function getLiteralUnionEncoder( + variants: TVariants, + config: LiteralUnionCodecConfig = {}, +): Encoder> { + const discriminator = config.size ?? getU8Encoder(); + return mapEncoder(discriminator, variant => { + const index = variants.indexOf(variant); + if (index < 0) { + throw new SolanaError(SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, { + value: variant, + variants, + }); + } + return index; + }); +} + +/** + * Creates a literal union decoder. + * + * @param variants - The variant decoders of the literal union. + * @param config - A set of config for the decoder. + */ +export function getLiteralUnionDecoder( + variants: TVariants, +): FixedSizeDecoder, 1>; +export function getLiteralUnionDecoder( + variants: TVariants, + config: LiteralUnionCodecConfig & { size: FixedSizeNumberDecoder }, +): FixedSizeDecoder, TSize>; +export function getLiteralUnionDecoder( + variants: TVariants, + config?: LiteralUnionCodecConfig, +): VariableSizeDecoder>; +export function getLiteralUnionDecoder( + variants: TVariants, + config: LiteralUnionCodecConfig = {}, +): Decoder> { + const discriminator = config.size ?? getU8Decoder(); + return mapDecoder(discriminator, (index: bigint | number) => { + if (index < 0 || index >= variants.length) { + throw new SolanaError(SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, { + discriminator: index, + maxRange: variants.length - 1, + minRange: 0, + }); + } + return variants[Number(index)]; + }); +} + +/** + * Creates a literal union codec. + * + * @param variants - The variant codecs of the literal union. + * @param config - A set of config for the codec. + */ + +export function getLiteralUnionCodec( + variants: TVariants, +): FixedSizeCodec, GetTypeFromVariants, 1>; +export function getLiteralUnionCodec( + variants: TVariants, + config: LiteralUnionCodecConfig & { size: FixedSizeNumberCodec }, +): FixedSizeCodec, GetTypeFromVariants, TSize>; +export function getLiteralUnionCodec( + variants: TVariants, + config?: LiteralUnionCodecConfig, +): VariableSizeCodec>; +export function getLiteralUnionCodec( + variants: TVariants, + config: LiteralUnionCodecConfig = {}, +): Codec> { + return combineCodec(getLiteralUnionEncoder(variants, config), getLiteralUnionDecoder(variants, config)); +} diff --git a/packages/codecs/README.md b/packages/codecs/README.md index 431e2f8eda26..95646d14d645 100644 --- a/packages/codecs/README.md +++ b/packages/codecs/README.md @@ -83,6 +83,7 @@ The `@solana/codecs` package is composed of several smaller packages, each with - [Struct codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#struct-codec). - [Enum codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#enum-codec). - [Discriminated union codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#discriminated-union-codec). + - [Literal union codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#literal-union-codec). - [Boolean codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#boolean-codec). - [Nullable codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#nullable-codec). - [Bytes codec](https://github.com/solana-labs/solana-web3.js/tree/master/packages/codecs-data-structures#bytes-codec). diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 3545322397d5..bea4e70b2a7e 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -259,6 +259,8 @@ export const SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE = 8078011 as const; export const SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE = 8078012 as const; export const SOLANA_ERROR__CODECS__EXPECTED_POSITIVE_BYTE_LENGTH = 8078013 as const; export const SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE = 8078014 as const; +export const SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT = 8078015 as const; +export const SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE = 8078016 as const; // RPC-related errors. // Reserve error codes in the range [8100000-8100999]. @@ -329,8 +331,10 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH | typeof SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT + | typeof SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT | typeof SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS | typeof SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE + | typeof SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE | typeof SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE | typeof SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE | typeof SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 52103591f2e3..0b18192867c7 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -20,8 +20,10 @@ import { SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, + SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, + SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE, SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, SOLANA_ERROR__INSTRUCTION__EXPECTED_TO_HAVE_ACCOUNTS, @@ -290,6 +292,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< value: number | string; variants: string[]; }; + [SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT]: { + value: bigint | boolean | number | string | null | undefined; + variants: readonly (bigint | boolean | number | string | null | undefined)[]; + }; [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: { actual: bigint | number; codecDescription: string; @@ -300,6 +306,11 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< base: number; value: string; }; + [SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE]: { + discriminator: bigint | number; + maxRange: number; + minRange: number; + }; [SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE]: { codecDescription: string; max: bigint | number; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index d95214b54c3b..c10aca19505e 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -28,8 +28,10 @@ import { SOLANA_ERROR__CODECS__INVALID_BYTE_LENGTH, SOLANA_ERROR__CODECS__INVALID_DISCRIMINATED_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT, + SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT, SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS, SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE, + SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE, SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE, SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE, SOLANA_ERROR__CRYPTO__RANDOM_VALUES_FUNCTION_UNIMPLEMENTED, @@ -269,9 +271,13 @@ export const SolanaErrorMessages: Readonly<{ 'Invalid discriminated union variant. Expected one of [$variants], got $value.', [SOLANA_ERROR__CODECS__INVALID_ENUM_VARIANT]: 'Invalid enum variant. Expected one of [$variants] or a number between $minRange and $maxRange, got $value.', + [SOLANA_ERROR__CODECS__INVALID_LITERAL_UNION_VARIANT]: + 'Invalid literal union variant. Expected one of [$variants], got $value.', [SOLANA_ERROR__CODECS__INVALID_NUMBER_OF_ITEMS]: 'Expected [$codecDescription] to have $expected items, got $actual.', [SOLANA_ERROR__CODECS__INVALID_STRING_FOR_BASE]: 'Invalid value $value for base $base with alphabet $alphabet.', + [SOLANA_ERROR__CODECS__LITERAL_UNION_DISCRIMINATOR_OUT_OF_RANGE]: + 'Literal union discriminator out of range. Expected a number between $minRange and $maxRange, got $discriminator.', [SOLANA_ERROR__CODECS__NUMBER_OUT_OF_RANGE]: 'Codec [$codecDescription] expected number to be in the range [$min, $max], got $value.', [SOLANA_ERROR__CODECS__OFFSET_OUT_OF_RANGE]: