-
Notifications
You must be signed in to change notification settings - Fork 915
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(experimental): add addCodecSentinel to @solana/codecs-core
- Loading branch information
1 parent
f43a2f5
commit d9c019d
Showing
12 changed files
with
351 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
--- | ||
'@solana/codecs-core': patch | ||
'@solana/errors': patch | ||
--- | ||
|
||
Added new `addCodecSentinel` primitive | ||
|
||
The `addCodecSentinel` function provides a new way of delimiting the size of a codec. It allows us to add a sentinel to the end of the encoded data and to read until that sentinel is found when decoding. It accepts any codec and a `Uint8Array` sentinel responsible for delimiting the encoded data. | ||
|
||
```ts | ||
const codec = addCodecSentinel(getUtf8Codec(), new Uint8Array([255, 255])); | ||
codec.encode('hello'); | ||
// 0x68656c6c6fffff | ||
// | └-- Our sentinel. | ||
// └-- Our encoded string. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
78 changes: 78 additions & 0 deletions
78
packages/codecs-core/src/__tests__/add-codec-sentinel-test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { | ||
SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL, | ||
SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES, | ||
SolanaError, | ||
} from '@solana/errors'; | ||
|
||
import { addCodecSentinel } from '../add-codec-sentinel'; | ||
import { b, getMockCodec } from './__setup__'; | ||
|
||
describe('addCodecSentinel', () => { | ||
it('encodes the sentinel after the main content', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.getSizeFromValue.mockReturnValue(10); | ||
mockCodec.write.mockImplementation((_, bytes, offset) => { | ||
bytes.set(b('68656c6c6f776f726c64'), offset); | ||
return offset + 10; | ||
}); | ||
const codec = addCodecSentinel(mockCodec, b('ff')); | ||
|
||
expect(codec.encode('helloworld')).toStrictEqual(b('68656c6c6f776f726c64ff')); | ||
expect(mockCodec.write).toHaveBeenCalledWith('helloworld', expect.any(Uint8Array), 0); | ||
}); | ||
|
||
it('decodes until the first occurence of the sentinel is found', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.read.mockReturnValue(['helloworld', 10]); | ||
const codec = addCodecSentinel(mockCodec, b('ff')); | ||
|
||
expect(codec.decode(b('68656c6c6f776f726c64ff0000'))).toBe('helloworld'); | ||
expect(mockCodec.read).toHaveBeenCalledWith(b('68656c6c6f776f726c64'), 0); | ||
}); | ||
|
||
it('fails if the encoded bytes contain the sentinel', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.getSizeFromValue.mockReturnValue(10); | ||
mockCodec.write.mockImplementation((_, bytes, offset) => { | ||
bytes.set(b('68656c6c6f776f726cff'), offset); | ||
return offset + 10; | ||
}); | ||
const codec = addCodecSentinel(mockCodec, b('ff')); | ||
|
||
expect(() => codec.encode('helloworld')).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL, { | ||
encodedBytes: b('68656c6c6f776f726cff'), | ||
hexEncodedBytes: '68656c6c6f776f726cff', | ||
hexSentinel: 'ff', | ||
sentinel: b('ff'), | ||
}), | ||
); | ||
}); | ||
|
||
it('fails if the decoded bytes do not contain the sentinel', () => { | ||
const mockCodec = getMockCodec(); | ||
const codec = addCodecSentinel(mockCodec, b('ff')); | ||
|
||
expect(() => codec.decode(b('68656c6c6f776f726c64000000'))).toThrow( | ||
new SolanaError(SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES, { | ||
decodedBytes: b('68656c6c6f776f726c64000000'), | ||
hexDecodedBytes: '68656c6c6f776f726c64000000', | ||
hexSentinel: 'ff', | ||
sentinel: b('ff'), | ||
}), | ||
); | ||
}); | ||
|
||
it('returns the correct fixed size', () => { | ||
const mockCodec = getMockCodec({ size: 10 }); | ||
const codec = addCodecSentinel(mockCodec, b('ffff')); | ||
expect(codec.fixedSize).toBe(12); | ||
}); | ||
|
||
it('returns the correct variable size', () => { | ||
const mockCodec = getMockCodec(); | ||
mockCodec.getSizeFromValue.mockReturnValueOnce(10); | ||
const codec = addCodecSentinel(mockCodec, b('ffff')); | ||
expect(codec.getSizeFromValue('helloworld')).toBe(12); | ||
}); | ||
}); |
35 changes: 35 additions & 0 deletions
35
packages/codecs-core/src/__typetests__/add-codec-sentinel-typetest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { addCodecSentinel, addDecoderSentinel, addEncoderSentinel } from '../add-codec-sentinel'; | ||
import { | ||
Codec, | ||
Decoder, | ||
Encoder, | ||
FixedSizeCodec, | ||
FixedSizeDecoder, | ||
FixedSizeEncoder, | ||
VariableSizeCodec, | ||
VariableSizeDecoder, | ||
VariableSizeEncoder, | ||
} from '../codec'; | ||
|
||
const sentinel = {} as Uint8Array; | ||
|
||
{ | ||
// [addEncoderSentinel]: It knows if the encoder is fixed size or variable size. | ||
addEncoderSentinel({} as FixedSizeEncoder<string>, sentinel) satisfies FixedSizeEncoder<string>; | ||
addEncoderSentinel({} as VariableSizeEncoder<string>, sentinel) satisfies VariableSizeEncoder<string>; | ||
addEncoderSentinel({} as Encoder<string>, sentinel) satisfies VariableSizeEncoder<string>; | ||
} | ||
|
||
{ | ||
// [addDecoderSentinel]: It knows if the decoder is fixed size or variable size. | ||
addDecoderSentinel({} as FixedSizeDecoder<string>, sentinel) satisfies FixedSizeDecoder<string>; | ||
addDecoderSentinel({} as VariableSizeDecoder<string>, sentinel) satisfies VariableSizeDecoder<string>; | ||
addDecoderSentinel({} as Decoder<string>, sentinel) satisfies VariableSizeDecoder<string>; | ||
} | ||
|
||
{ | ||
// [addCodecSentinel]: It knows if the codec is fixed size or variable size. | ||
addCodecSentinel({} as FixedSizeCodec<string>, sentinel) satisfies FixedSizeCodec<string>; | ||
addCodecSentinel({} as VariableSizeCodec<string>, sentinel) satisfies VariableSizeCodec<string>; | ||
addCodecSentinel({} as Codec<string>, sentinel) satisfies VariableSizeCodec<string>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { | ||
SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL, | ||
SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES, | ||
SolanaError, | ||
} from '@solana/errors'; | ||
|
||
import { containsBytes } from './bytes'; | ||
import { | ||
Codec, | ||
createDecoder, | ||
createEncoder, | ||
Decoder, | ||
Encoder, | ||
FixedSizeCodec, | ||
FixedSizeDecoder, | ||
FixedSizeEncoder, | ||
isFixedSize, | ||
VariableSizeCodec, | ||
VariableSizeDecoder, | ||
VariableSizeEncoder, | ||
} from './codec'; | ||
import { combineCodec } from './combine-codec'; | ||
import { ReadonlyUint8Array } from './readonly-uint8array'; | ||
|
||
/** | ||
* Creates an encoder that writes a `Uint8Array` sentinel after the encoded value. | ||
* This is useful to delimit the encoded value when being read by a decoder. | ||
* | ||
* Note that, if the sentinel is found in the encoded value, an error is thrown. | ||
*/ | ||
export function addEncoderSentinel<TFrom>( | ||
encoder: FixedSizeEncoder<TFrom>, | ||
sentinel: ReadonlyUint8Array, | ||
): FixedSizeEncoder<TFrom>; | ||
export function addEncoderSentinel<TFrom>( | ||
encoder: Encoder<TFrom>, | ||
sentinel: ReadonlyUint8Array, | ||
): VariableSizeEncoder<TFrom>; | ||
export function addEncoderSentinel<TFrom>(encoder: Encoder<TFrom>, sentinel: ReadonlyUint8Array): Encoder<TFrom> { | ||
const write = ((value, bytes, offset) => { | ||
// Here we exceptionally use the `encode` function instead of the `write` | ||
// function to contain the content of the encoder within its own bounds | ||
// and to avoid writing the sentinel as part of the encoded value. | ||
const encoderBytes = encoder.encode(value); | ||
if (findSentinelIndex(encoderBytes, sentinel) >= 0) { | ||
throw new SolanaError(SOLANA_ERROR__CODECS__ENCODED_BYTES_MUST_NOT_INCLUDE_SENTINEL, { | ||
encodedBytes: encoderBytes, | ||
hexEncodedBytes: hexBytes(encoderBytes), | ||
hexSentinel: hexBytes(sentinel), | ||
sentinel, | ||
}); | ||
} | ||
bytes.set(encoderBytes, offset); | ||
offset += encoderBytes.length; | ||
bytes.set(sentinel, offset); | ||
offset += sentinel.length; | ||
return offset; | ||
}) as Encoder<TFrom>['write']; | ||
|
||
if (isFixedSize(encoder)) { | ||
return createEncoder({ ...encoder, fixedSize: encoder.fixedSize + sentinel.length, write }); | ||
} | ||
|
||
return createEncoder({ | ||
...encoder, | ||
...(encoder.maxSize != null ? { maxSize: encoder.maxSize + sentinel.length } : {}), | ||
getSizeFromValue: value => encoder.getSizeFromValue(value) + sentinel.length, | ||
write, | ||
}); | ||
} | ||
|
||
/** | ||
* Creates a decoder that continues reading until a `Uint8Array` sentinel is found. | ||
* | ||
* If the sentinel is not found in the byte array to decode, an error is thrown. | ||
*/ | ||
export function addDecoderSentinel<TTo>( | ||
decoder: FixedSizeDecoder<TTo>, | ||
sentinel: ReadonlyUint8Array, | ||
): FixedSizeDecoder<TTo>; | ||
export function addDecoderSentinel<TTo>(decoder: Decoder<TTo>, sentinel: ReadonlyUint8Array): VariableSizeDecoder<TTo>; | ||
export function addDecoderSentinel<TTo>(decoder: Decoder<TTo>, sentinel: ReadonlyUint8Array): Decoder<TTo> { | ||
const read = ((bytes, offset) => { | ||
const candidateBytes = offset === 0 ? bytes : bytes.slice(offset); | ||
const sentinelIndex = findSentinelIndex(candidateBytes, sentinel); | ||
if (sentinelIndex === -1) { | ||
throw new SolanaError(SOLANA_ERROR__CODECS__SENTINEL_MISSING_IN_DECODED_BYTES, { | ||
decodedBytes: candidateBytes, | ||
hexDecodedBytes: hexBytes(candidateBytes), | ||
hexSentinel: hexBytes(sentinel), | ||
sentinel, | ||
}); | ||
} | ||
const preSentinelBytes = candidateBytes.slice(0, sentinelIndex); | ||
// Here we exceptionally use the `decode` function instead of the `read` | ||
// function to contain the content of the decoder within its own bounds | ||
// and ensure that the sentinel is not part of the decoded value. | ||
return [decoder.decode(preSentinelBytes), offset + preSentinelBytes.length + sentinel.length]; | ||
}) as Decoder<TTo>['read']; | ||
|
||
if (isFixedSize(decoder)) { | ||
return createDecoder({ ...decoder, fixedSize: decoder.fixedSize + sentinel.length, read }); | ||
} | ||
|
||
return createDecoder({ | ||
...decoder, | ||
...(decoder.maxSize != null ? { maxSize: decoder.maxSize + sentinel.length } : {}), | ||
read, | ||
}); | ||
} | ||
|
||
/** | ||
* Creates a Codec that writes a `Uint8Array` sentinel after the encoded | ||
* value and, when decoding, continues reading until the sentinel is found. | ||
* | ||
* Note that, if the sentinel is found in the encoded value | ||
* or not found in the byte array to decode, an error is thrown. | ||
*/ | ||
export function addCodecSentinel<TFrom, TTo extends TFrom>( | ||
codec: FixedSizeCodec<TFrom, TTo>, | ||
sentinel: ReadonlyUint8Array, | ||
): FixedSizeCodec<TFrom, TTo>; | ||
export function addCodecSentinel<TFrom, TTo extends TFrom>( | ||
codec: Codec<TFrom, TTo>, | ||
sentinel: ReadonlyUint8Array, | ||
): VariableSizeCodec<TFrom, TTo>; | ||
export function addCodecSentinel<TFrom, TTo extends TFrom>( | ||
codec: Codec<TFrom, TTo>, | ||
sentinel: ReadonlyUint8Array, | ||
): Codec<TFrom, TTo> { | ||
return combineCodec(addEncoderSentinel(codec, sentinel), addDecoderSentinel(codec, sentinel)); | ||
} | ||
|
||
function findSentinelIndex(bytes: ReadonlyUint8Array, sentinel: ReadonlyUint8Array) { | ||
return bytes.findIndex((byte, index, arr) => { | ||
if (sentinel.length === 1) return byte === sentinel[0]; | ||
return containsBytes(arr, sentinel, index); | ||
}); | ||
} | ||
|
||
function hexBytes(bytes: ReadonlyUint8Array): string { | ||
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), ''); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.