@effectstream/utils 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +28 -0
- package/src/binary-search.test.ts +38 -0
- package/src/binary-search.ts +45 -0
- package/src/captcha.ts +21 -0
- package/src/concurrency/condVar.ts +52 -0
- package/src/concurrency/latch.test.ts +51 -0
- package/src/concurrency/latch.ts +61 -0
- package/src/concurrency/mod.ts +4 -0
- package/src/concurrency/retry.test.ts +59 -0
- package/src/concurrency/retry.ts +67 -0
- package/src/concurrency/utils.ts +33 -0
- package/src/config.ts +484 -0
- package/src/constants.ts +10 -0
- package/src/decorators/bound.ts +22 -0
- package/src/decorators/mod.ts +1 -0
- package/src/mod.ts +8 -0
- package/src/runtime-spawn.ts +188 -0
- package/src/runtime.ts +89 -0
- package/src/types/json-query.ts +36 -0
- package/src/types/misc.ts +173 -0
- package/src/types/mod.ts +6 -0
- package/src/types/nominal.ts +168 -0
- package/src/types/typebox-helpers.ts +446 -0
- package/src/types/utils.ts +60 -0
- package/src/types/validators/algorand.ts +120 -0
- package/src/types/validators/cardano.ts +139 -0
- package/src/types/validators/mina.ts +54 -0
- package/src/types/validators/minicbor.ts +468 -0
- package/src/types/validators/mod.ts +2 -0
- package/src/types/validators/utils.ts +34 -0
- package/src/viem.ts +41 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { crc32 } from "@foxglove/crc";
|
|
2
|
+
import { CHECKSUM_ERROR_MSG, MALFORMED_ERROR_MSG } from "./utils.ts";
|
|
3
|
+
import { MiniCborReader } from "./minicbor.ts";
|
|
4
|
+
import bs58 from "bs58";
|
|
5
|
+
import { bech32 } from "bech32";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
|
|
8
|
+
const CHECKSUM_ADDRESS_ERROR_MSG = CHECKSUM_ERROR_MSG("address");
|
|
9
|
+
|
|
10
|
+
export const InvalidArgumentError = Error;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Example address:
|
|
14
|
+
* - (mainnet icarus) Ae2tdPwUPEZFRbyhz3cpfC2CumGzNkFBN2L42rcUc2yjQpEkxDbkPodpMAi
|
|
15
|
+
* - (testnet byron) 37btjrVyb4KDXBNC4haBVPCrro8AQPHwvCMp3RFhhSVWwfFmZ6wwzSK6JK1hY6wHNmtrpTf1kdbva8TCneM2YsiXT7mrzT21EacHnPpz5YyUdj64na
|
|
16
|
+
* - (mainnet byron) 82d818584283581caf335162c1ecb0a5b95342e3d1d0c9c38a246fa976b077e81dea9d9ba101581e581c2d574aa85a82a80fd6d1c51ea115c2b03b68166230a3c383c9f89ad5001ac3de014e
|
|
17
|
+
*/
|
|
18
|
+
export function verifyCardanoByronAddress(address: string): boolean {
|
|
19
|
+
return verifyCardanoByronAddressBytes(bs58.decode(address));
|
|
20
|
+
}
|
|
21
|
+
function verifyCardanoByronAddressBytes(data: Uint8Array): boolean {
|
|
22
|
+
const reader = new MiniCborReader(Buffer.from(data).toString("hex"));
|
|
23
|
+
|
|
24
|
+
reader.readStartArray();
|
|
25
|
+
reader.readTag();
|
|
26
|
+
|
|
27
|
+
const addressDataEncoded = reader.readByteString();
|
|
28
|
+
|
|
29
|
+
if (Number(reader.readInt()) !== crc32(addressDataEncoded)) {
|
|
30
|
+
throw new Error(CHECKSUM_ADDRESS_ERROR_MSG);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// skip the rest of the address verification and assume if the checksum matches the rest is okay
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export enum AddressType {
|
|
39
|
+
BasePaymentKeyStakeKey = 0b0000,
|
|
40
|
+
BasePaymentScriptStakeKey = 0b0001,
|
|
41
|
+
BasePaymentKeyStakeScript = 0b0010,
|
|
42
|
+
BasePaymentScriptStakeScript = 0b0011,
|
|
43
|
+
PointerKey = 0b0100,
|
|
44
|
+
PointerScript = 0b0101,
|
|
45
|
+
EnterpriseKey = 0b0110,
|
|
46
|
+
EnterpriseScript = 0b0111,
|
|
47
|
+
// 0b1000 was chosen because all existing Byron addresses actually start with 0b1000,
|
|
48
|
+
// therefore we can re-use Byron addresses as-is
|
|
49
|
+
Byron = 0b1000,
|
|
50
|
+
RewardKey = 0b1110,
|
|
51
|
+
RewardScript = 0b1111,
|
|
52
|
+
// 1001-1101 are left for future formats
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* See: https://github.com/input-output-hk/cardano-js-sdk/blob/d1544fc9d1fb3997eb7c5b94a233789bb9e5cd9f/packages/core/src/Cardano/Address/Address.ts
|
|
57
|
+
*/
|
|
58
|
+
export function verifyCardanoShelleyAddress(address: string): boolean {
|
|
59
|
+
const bech32data = bech32.decode(address, Number.MAX_SAFE_INTEGER / 2);
|
|
60
|
+
const data = bech32.fromWords(bech32data.words);
|
|
61
|
+
|
|
62
|
+
// strictly speaking, you don't have to use these prefixes
|
|
63
|
+
// but it's a good sanity check
|
|
64
|
+
// see CIP5
|
|
65
|
+
if (
|
|
66
|
+
!["addr", "addr_test", "stake", "stake_test"].includes(bech32data.prefix)
|
|
67
|
+
) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const type = data[0] >> 4;
|
|
72
|
+
|
|
73
|
+
switch (type) {
|
|
74
|
+
case AddressType.BasePaymentKeyStakeKey:
|
|
75
|
+
case AddressType.BasePaymentScriptStakeKey:
|
|
76
|
+
case AddressType.BasePaymentKeyStakeScript:
|
|
77
|
+
case AddressType.BasePaymentScriptStakeScript: {
|
|
78
|
+
return data.length === 57;
|
|
79
|
+
}
|
|
80
|
+
case AddressType.PointerKey:
|
|
81
|
+
case AddressType.PointerScript: {
|
|
82
|
+
if (data.length <= 29) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
let index = 29;
|
|
86
|
+
const dataBuffer = Buffer.from(data);
|
|
87
|
+
const { bytesRead: slotBytes } = variableLengthDecode(
|
|
88
|
+
dataBuffer.subarray(index),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
index += slotBytes;
|
|
92
|
+
const { bytesRead: txIndexBytes } = variableLengthDecode(
|
|
93
|
+
dataBuffer.subarray(index),
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
index += txIndexBytes;
|
|
97
|
+
variableLengthDecode(dataBuffer.subarray(index));
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
case AddressType.EnterpriseKey:
|
|
101
|
+
case AddressType.EnterpriseScript: {
|
|
102
|
+
return data.length === 47;
|
|
103
|
+
}
|
|
104
|
+
case AddressType.RewardKey:
|
|
105
|
+
case AddressType.RewardScript: {
|
|
106
|
+
return data.length === 47;
|
|
107
|
+
}
|
|
108
|
+
case AddressType.Byron: {
|
|
109
|
+
return verifyCardanoByronAddressBytes(new Uint8Array(data));
|
|
110
|
+
}
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(MALFORMED_ERROR_MSG("address"));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Decodes a value previously encoded in a variable length array.
|
|
118
|
+
*
|
|
119
|
+
* @param array The encoded value.
|
|
120
|
+
* @returns the decoded value.
|
|
121
|
+
*/
|
|
122
|
+
const variableLengthDecode = (
|
|
123
|
+
array: Buffer,
|
|
124
|
+
): { value: bigint; bytesRead: number } => {
|
|
125
|
+
let more = true;
|
|
126
|
+
let value = 0n;
|
|
127
|
+
|
|
128
|
+
let bytesRead = 0;
|
|
129
|
+
while (more && bytesRead < array.length) {
|
|
130
|
+
const b = array[bytesRead];
|
|
131
|
+
value <<= 7n;
|
|
132
|
+
value |= BigInt(b & 127);
|
|
133
|
+
|
|
134
|
+
more = (b & 128) !== 0;
|
|
135
|
+
++bytesRead;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { bytesRead, value };
|
|
139
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import bs58check from "bs58check";
|
|
2
|
+
import { MALFORMED_ERROR_MSG } from "./utils.ts";
|
|
3
|
+
|
|
4
|
+
const BLOCK_HASH_BYTE_LENGTH = 34;
|
|
5
|
+
const TX_ID_BYTE_LENGTH = 34;
|
|
6
|
+
const ADDRESS_BYTE_LENGTH = 36;
|
|
7
|
+
|
|
8
|
+
const MALFORMED_BLOCK_HASH_ERROR_MSG = MALFORMED_ERROR_MSG("block hash");
|
|
9
|
+
const MALFORMED_TX_ID_ERROR_MSG = MALFORMED_ERROR_MSG("tx id");
|
|
10
|
+
const MALFORMED_ADDRESS_ERROR_MSG = MALFORMED_ERROR_MSG("address");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Example address: B62qpLST3UC1rpVT6SHfB7wqW2iQgiopFAGfrcovPgLjgfpDUN2LLeg
|
|
14
|
+
*/
|
|
15
|
+
export function verifyMinaAddress(address: string): boolean {
|
|
16
|
+
const decoded = bs58check.decode(address);
|
|
17
|
+
// Sanity check
|
|
18
|
+
if (decoded.length !== ADDRESS_BYTE_LENGTH) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${MALFORMED_ADDRESS_ERROR_MSG}: expected byte length ${ADDRESS_BYTE_LENGTH}, got ${decoded.length}`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Example block hash: 3NK9ToRM1gnzyDbafWzdde7EYemjUN2cb5isjtchcCuupty2MgbC
|
|
28
|
+
*/
|
|
29
|
+
export function verifyMinaBlockHash(hash: string): boolean {
|
|
30
|
+
const decoded = bs58check.decode(hash);
|
|
31
|
+
// Sanity check
|
|
32
|
+
if (decoded.length !== TX_ID_BYTE_LENGTH) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`${MALFORMED_BLOCK_HASH_ERROR_MSG}: expected byte length ${BLOCK_HASH_BYTE_LENGTH}, got ${decoded.length}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* See https://github.com/openmina/openmina/blob/26354df0b4165a82ba67fa9ab1f2301841f7b033/mina-p2p-messages/src/v2/hashing.rs#L89-L92
|
|
42
|
+
*
|
|
43
|
+
* Example tx hash: 5JuYVpv6njuwUjgf8WLfXhTEJGHck1pwJXt92Lpcskiwu8kBL632
|
|
44
|
+
*/
|
|
45
|
+
export function verifyMinaTransactionHash(hash: string): boolean {
|
|
46
|
+
const decoded = bs58check.decode(hash);
|
|
47
|
+
// Sanity check
|
|
48
|
+
if (decoded.length !== TX_ID_BYTE_LENGTH) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${MALFORMED_TX_ID_ERROR_MSG}: expected byte length ${TX_ID_BYTE_LENGTH}, got ${decoded.length}`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* We need CBOR to parse Cardano Byron addresses
|
|
3
|
+
* but CBOR parsing libraries are massive
|
|
4
|
+
* we want to avoid having to import megabytes JS code in the browser just for Cardano Byron address parsing
|
|
5
|
+
*
|
|
6
|
+
* As a middle ground, this file copies just the CBOR parsing code required for Byron checksum matching
|
|
7
|
+
* reference: https://github.com/input-output-hk/cardano-js-sdk/blob/d1544fc9d1fb3997eb7c5b94a233789bb9e5cd9f/packages/core/src/Serialization/CBOR/MiniCborReader.ts#L26
|
|
8
|
+
*
|
|
9
|
+
* This file is 3kb when compressing with gzip
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Buffer } from "node:buffer";
|
|
13
|
+
|
|
14
|
+
type HexBlob = string;
|
|
15
|
+
|
|
16
|
+
export const CborContentException = Error;
|
|
17
|
+
export const CborInvalidOperationException = Error;
|
|
18
|
+
|
|
19
|
+
export enum CborReaderState {
|
|
20
|
+
Undefined = 0,
|
|
21
|
+
UnsignedInteger,
|
|
22
|
+
NegativeInteger,
|
|
23
|
+
ByteString,
|
|
24
|
+
StartIndefiniteLengthByteString,
|
|
25
|
+
EndIndefiniteLengthByteString,
|
|
26
|
+
TextString,
|
|
27
|
+
StartIndefiniteLengthTextString,
|
|
28
|
+
EndIndefiniteLengthTextString,
|
|
29
|
+
StartArray,
|
|
30
|
+
EndArray,
|
|
31
|
+
StartMap,
|
|
32
|
+
EndMap,
|
|
33
|
+
Tag,
|
|
34
|
+
SimpleValue,
|
|
35
|
+
HalfPrecisionFloat,
|
|
36
|
+
SinglePrecisionFloat,
|
|
37
|
+
DoublePrecisionFloat,
|
|
38
|
+
Null,
|
|
39
|
+
Boolean,
|
|
40
|
+
Finished,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export enum CborMajorType {
|
|
44
|
+
UnsignedInteger = 0,
|
|
45
|
+
NegativeInteger = 1,
|
|
46
|
+
ByteString = 2,
|
|
47
|
+
Utf8String = 3,
|
|
48
|
+
Array = 4,
|
|
49
|
+
Map = 5,
|
|
50
|
+
Tag = 6,
|
|
51
|
+
Simple = 7,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type StackFrame = {
|
|
55
|
+
type: CborMajorType | null;
|
|
56
|
+
frameOffset: number;
|
|
57
|
+
definiteLength?: number;
|
|
58
|
+
itemsRead: number;
|
|
59
|
+
currentKeyOffset: number | null;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export enum CborTag {
|
|
63
|
+
DateTimeString = 0,
|
|
64
|
+
UnixTimeSeconds = 1,
|
|
65
|
+
UnsignedBigNum = 2,
|
|
66
|
+
NegativeBigNum = 3,
|
|
67
|
+
DecimalFraction = 4,
|
|
68
|
+
BigFloat = 5,
|
|
69
|
+
Base64UrlLaterEncoding = 21,
|
|
70
|
+
Base64StringLaterEncoding = 22,
|
|
71
|
+
Base16StringLaterEncoding = 23,
|
|
72
|
+
EncodedCborDataItem = 24,
|
|
73
|
+
RationalNumber = 30,
|
|
74
|
+
Uri = 32,
|
|
75
|
+
Base64Url = 33,
|
|
76
|
+
Base64 = 34,
|
|
77
|
+
Regex = 35,
|
|
78
|
+
MimeMessage = 36,
|
|
79
|
+
Set = 258,
|
|
80
|
+
SelfDescribeCbor = 55_799,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const UNEXPECTED_END_OF_BUFFER_MSG = "Unexpected end of buffer";
|
|
84
|
+
|
|
85
|
+
export enum CborAdditionalInfo {
|
|
86
|
+
AdditionalFalse = 20,
|
|
87
|
+
AdditionalTrue = 21,
|
|
88
|
+
AdditionalNull = 22,
|
|
89
|
+
Additional8BitData = 24,
|
|
90
|
+
Additional16BitData = 25,
|
|
91
|
+
Additional32BitData = 26,
|
|
92
|
+
Additional64BitData = 27,
|
|
93
|
+
IndefiniteLength = 31,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class CborInitialByte {
|
|
97
|
+
static readonly IndefiniteLengthBreakByte = 0xff;
|
|
98
|
+
static readonly AdditionalInformationMask = 0b0001_1111;
|
|
99
|
+
|
|
100
|
+
constructor(private initialByte: number) {}
|
|
101
|
+
CborInitialByte(
|
|
102
|
+
majorType: CborMajorType,
|
|
103
|
+
additionalInfo: CborAdditionalInfo,
|
|
104
|
+
) {
|
|
105
|
+
this.initialByte = (majorType << 5) | additionalInfo;
|
|
106
|
+
}
|
|
107
|
+
static from(initialByte: number) {
|
|
108
|
+
const init = new CborInitialByte(initialByte);
|
|
109
|
+
|
|
110
|
+
return init;
|
|
111
|
+
}
|
|
112
|
+
getInitialByte(): number {
|
|
113
|
+
return this.initialByte;
|
|
114
|
+
}
|
|
115
|
+
getMajorType(): CborMajorType {
|
|
116
|
+
return this.initialByte >> 5;
|
|
117
|
+
}
|
|
118
|
+
getAdditionalInfo(): CborAdditionalInfo {
|
|
119
|
+
return this.initialByte & CborInitialByte.AdditionalInformationMask;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class MiniCborReader {
|
|
124
|
+
readonly #data: Uint8Array;
|
|
125
|
+
#offset = 0;
|
|
126
|
+
#nestedItems: Array<StackFrame> = new Array<StackFrame>();
|
|
127
|
+
#isTagContext = false;
|
|
128
|
+
#currentFrame: StackFrame;
|
|
129
|
+
#cachedState = CborReaderState.Undefined;
|
|
130
|
+
|
|
131
|
+
constructor(data: HexBlob) {
|
|
132
|
+
this.#data = new Uint8Array(Buffer.from(data, "hex"));
|
|
133
|
+
this.#currentFrame = {
|
|
134
|
+
currentKeyOffset: null,
|
|
135
|
+
frameOffset: 0,
|
|
136
|
+
itemsRead: 0,
|
|
137
|
+
type: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
readStartArray(): number | null {
|
|
142
|
+
const header: CborInitialByte = this.#peekInitialByte(CborMajorType.Array);
|
|
143
|
+
|
|
144
|
+
if (header.getAdditionalInfo() === CborAdditionalInfo.IndefiniteLength) {
|
|
145
|
+
this.#advanceBuffer(1);
|
|
146
|
+
this.#pushDataItem(CborMajorType.Array);
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const buffer = this.#getRemainingBytes();
|
|
151
|
+
const { length, bytesRead } = MiniCborReader.#peekDefiniteLength(
|
|
152
|
+
header,
|
|
153
|
+
buffer,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
this.#advanceBuffer(bytesRead);
|
|
157
|
+
this.#pushDataItem(CborMajorType.Array, length);
|
|
158
|
+
return length;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
readTag(): CborTag {
|
|
162
|
+
const { tag, bytesRead } = this.#peekTagCore();
|
|
163
|
+
|
|
164
|
+
this.#advanceBuffer(bytesRead);
|
|
165
|
+
this.#isTagContext = true;
|
|
166
|
+
return tag;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
readByteString(): Uint8Array {
|
|
170
|
+
const header = this.#peekInitialByte(CborMajorType.ByteString);
|
|
171
|
+
|
|
172
|
+
if (header.getAdditionalInfo() === CborAdditionalInfo.IndefiniteLength) {
|
|
173
|
+
const { val, encodingLength } = this
|
|
174
|
+
.#readIndefiniteLengthByteStringConcatenated(
|
|
175
|
+
CborMajorType.ByteString,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
this.#advanceBuffer(encodingLength);
|
|
179
|
+
this.#advanceDataItemCounters();
|
|
180
|
+
|
|
181
|
+
return val;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const buffer = this.#getRemainingBytes();
|
|
185
|
+
const { length, bytesRead } = MiniCborReader.#peekDefiniteLength(
|
|
186
|
+
header,
|
|
187
|
+
buffer,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
this.#ensureReadCapacity(bytesRead + length);
|
|
191
|
+
this.#advanceBuffer(bytesRead + length);
|
|
192
|
+
this.#advanceDataItemCounters();
|
|
193
|
+
|
|
194
|
+
return buffer.slice(bytesRead, bytesRead + length);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
readInt(): bigint {
|
|
198
|
+
const value = this.#peekSignedInteger();
|
|
199
|
+
this.#advanceBuffer(value.bytesRead);
|
|
200
|
+
this.#advanceDataItemCounters();
|
|
201
|
+
return value.signedInt;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#peekSignedInteger(): { signedInt: bigint; bytesRead: number } {
|
|
205
|
+
const header: CborInitialByte = this.#peekInitialByte();
|
|
206
|
+
|
|
207
|
+
switch (header.getMajorType()) {
|
|
208
|
+
case CborMajorType.UnsignedInteger: {
|
|
209
|
+
const { unsignedInt: signedInt, bytesRead } = MiniCborReader
|
|
210
|
+
.#decodeUnsignedInteger(
|
|
211
|
+
header,
|
|
212
|
+
this.#getRemainingBytes(),
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
return { bytesRead, signedInt: BigInt(signedInt) };
|
|
216
|
+
}
|
|
217
|
+
case CborMajorType.NegativeInteger: {
|
|
218
|
+
const { unsignedInt, bytesRead } = MiniCborReader
|
|
219
|
+
.#decodeUnsignedInteger(
|
|
220
|
+
header,
|
|
221
|
+
this.#getRemainingBytes(),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return { bytesRead, signedInt: BigInt(-1) - unsignedInt };
|
|
225
|
+
}
|
|
226
|
+
default:
|
|
227
|
+
throw new CborInvalidOperationException(
|
|
228
|
+
`Reader type mismatch, expected ${CborMajorType.UnsignedInteger} or ${CborMajorType.NegativeInteger} but got ${header.getMajorType()}`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#ensureReadCapacity(bytesToRead: number) {
|
|
234
|
+
if (this.#data.length - this.#offset < bytesToRead) {
|
|
235
|
+
throw new CborContentException(UNEXPECTED_END_OF_BUFFER_MSG);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#readIndefiniteLengthByteStringConcatenated(type: CborMajorType): {
|
|
240
|
+
val: Uint8Array;
|
|
241
|
+
encodingLength: number;
|
|
242
|
+
} {
|
|
243
|
+
const data = this.#getRemainingBytes();
|
|
244
|
+
let concat = Buffer.from([]);
|
|
245
|
+
let encodingLength = 0;
|
|
246
|
+
|
|
247
|
+
let i = 1; // skip the indefinite-length initial byte
|
|
248
|
+
|
|
249
|
+
let nextInitialByte = MiniCborReader.#peekNextInitialByte(
|
|
250
|
+
data.slice(i),
|
|
251
|
+
type,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
while (
|
|
255
|
+
nextInitialByte.getInitialByte() !==
|
|
256
|
+
CborInitialByte.IndefiniteLengthBreakByte
|
|
257
|
+
) {
|
|
258
|
+
const { length: chunkLength, bytesRead } = MiniCborReader
|
|
259
|
+
.#peekDefiniteLength(
|
|
260
|
+
nextInitialByte,
|
|
261
|
+
data.slice(i),
|
|
262
|
+
);
|
|
263
|
+
const payloadSize = bytesRead + Number(chunkLength);
|
|
264
|
+
|
|
265
|
+
concat = Buffer.concat([
|
|
266
|
+
concat as any, // TODO: remove `as any` once @types/node gets updated properly
|
|
267
|
+
data.slice(i + (payloadSize - chunkLength), i + payloadSize),
|
|
268
|
+
]);
|
|
269
|
+
|
|
270
|
+
i += payloadSize;
|
|
271
|
+
|
|
272
|
+
nextInitialByte = MiniCborReader.#peekNextInitialByte(
|
|
273
|
+
data.slice(i),
|
|
274
|
+
type,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
encodingLength = i + 1; // include the break byte
|
|
279
|
+
|
|
280
|
+
return { encodingLength, val: new Uint8Array(concat) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
#advanceDataItemCounters() {
|
|
284
|
+
++this.#currentFrame.itemsRead;
|
|
285
|
+
this.#isTagContext = false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static #peekNextInitialByte(
|
|
289
|
+
buffer: Uint8Array,
|
|
290
|
+
expectedType?: CborMajorType,
|
|
291
|
+
): CborInitialByte {
|
|
292
|
+
MiniCborReader.ensureReadCapacityInArray(buffer, 1);
|
|
293
|
+
const header = CborInitialByte.from(buffer[0]);
|
|
294
|
+
|
|
295
|
+
if (
|
|
296
|
+
header.getInitialByte() !== CborInitialByte.IndefiniteLengthBreakByte &&
|
|
297
|
+
header.getMajorType() !== expectedType
|
|
298
|
+
) {
|
|
299
|
+
throw new CborContentException(
|
|
300
|
+
"Indefinite length string contains invalid data item",
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return header;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#peekTagCore(): { tag: CborTag; bytesRead: number } {
|
|
308
|
+
const header: CborInitialByte = this.#peekInitialByte(CborMajorType.Tag);
|
|
309
|
+
const { unsignedInt: result, bytesRead } = MiniCborReader
|
|
310
|
+
.#decodeUnsignedInteger(
|
|
311
|
+
header,
|
|
312
|
+
this.#getRemainingBytes(),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
return { bytesRead, tag: Number(result) as CborTag };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
static #peekDefiniteLength(
|
|
319
|
+
header: CborInitialByte,
|
|
320
|
+
data: Uint8Array,
|
|
321
|
+
): { length: number; bytesRead: number } {
|
|
322
|
+
const { unsignedInt: length, bytesRead } = MiniCborReader
|
|
323
|
+
.#decodeUnsignedInteger(header, data);
|
|
324
|
+
return { bytesRead, length: Number(length) };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
#peekInitialByte(expectedType?: CborMajorType): CborInitialByte {
|
|
328
|
+
if (
|
|
329
|
+
this.#currentFrame.definiteLength !== undefined &&
|
|
330
|
+
this.#currentFrame.definiteLength - this.#currentFrame.itemsRead === 0
|
|
331
|
+
) {
|
|
332
|
+
throw new CborInvalidOperationException("No more data items to read");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (this.#offset === this.#data.length) {
|
|
336
|
+
if (
|
|
337
|
+
this.#currentFrame.type === null &&
|
|
338
|
+
this.#currentFrame.definiteLength === undefined &&
|
|
339
|
+
this.#offset > 0
|
|
340
|
+
) {
|
|
341
|
+
throw new CborInvalidOperationException(
|
|
342
|
+
"End of root-level. No more data items to read",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
throw new CborContentException(UNEXPECTED_END_OF_BUFFER_MSG);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const nextByte = CborInitialByte.from(this.#data[this.#offset]);
|
|
350
|
+
|
|
351
|
+
switch (this.#currentFrame.type) {
|
|
352
|
+
case CborMajorType.ByteString:
|
|
353
|
+
case CborMajorType.Utf8String:
|
|
354
|
+
// Indefinite-length string contexts allow two possible data items:
|
|
355
|
+
// 1) Definite-length string chunks of the same major type OR
|
|
356
|
+
// 2) a break byte denoting the end of the indefinite-length string context.
|
|
357
|
+
if (
|
|
358
|
+
nextByte.getInitialByte() ===
|
|
359
|
+
CborInitialByte.IndefiniteLengthBreakByte ||
|
|
360
|
+
(nextByte.getMajorType() === this.#currentFrame.type &&
|
|
361
|
+
nextByte.getAdditionalInfo() !==
|
|
362
|
+
CborAdditionalInfo.IndefiniteLength)
|
|
363
|
+
) {
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
throw new CborContentException(
|
|
368
|
+
`Indefinite length string contains invalid data item, ${nextByte.getMajorType()}`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (expectedType && expectedType !== nextByte.getMajorType()) {
|
|
373
|
+
throw new CborInvalidOperationException(
|
|
374
|
+
`Major type mismatch, expected type ${expectedType} but got ${nextByte.getMajorType()}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return nextByte;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
static #decodeUnsignedInteger(
|
|
382
|
+
header: CborInitialByte,
|
|
383
|
+
data: Uint8Array,
|
|
384
|
+
): { unsignedInt: bigint; bytesRead: number } {
|
|
385
|
+
if (
|
|
386
|
+
(header.getInitialByte() & CborInitialByte.AdditionalInformationMask) <
|
|
387
|
+
CborAdditionalInfo.Additional8BitData
|
|
388
|
+
) {
|
|
389
|
+
return { bytesRead: 1, unsignedInt: BigInt(header.getAdditionalInfo()) };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
switch (header.getAdditionalInfo()) {
|
|
393
|
+
case CborAdditionalInfo.Additional8BitData: {
|
|
394
|
+
MiniCborReader.ensureReadCapacityInArray(data, 2);
|
|
395
|
+
|
|
396
|
+
return { bytesRead: 2, unsignedInt: BigInt(data[1]) };
|
|
397
|
+
}
|
|
398
|
+
case CborAdditionalInfo.Additional16BitData: {
|
|
399
|
+
MiniCborReader.ensureReadCapacityInArray(data, 3);
|
|
400
|
+
|
|
401
|
+
const buffer = Buffer.from(data.slice(1));
|
|
402
|
+
const val = buffer.readUInt16BE();
|
|
403
|
+
|
|
404
|
+
return { bytesRead: 3, unsignedInt: BigInt(val) };
|
|
405
|
+
}
|
|
406
|
+
case CborAdditionalInfo.Additional32BitData: {
|
|
407
|
+
MiniCborReader.ensureReadCapacityInArray(data, 5);
|
|
408
|
+
|
|
409
|
+
const buffer = Buffer.from(data.slice(1));
|
|
410
|
+
const val = buffer.readUInt32BE();
|
|
411
|
+
|
|
412
|
+
return { bytesRead: 5, unsignedInt: BigInt(val) };
|
|
413
|
+
}
|
|
414
|
+
case CborAdditionalInfo.Additional64BitData: {
|
|
415
|
+
MiniCborReader.ensureReadCapacityInArray(data, 9);
|
|
416
|
+
|
|
417
|
+
const buffer = Buffer.from(data.slice(1, 9));
|
|
418
|
+
|
|
419
|
+
let result = BigInt(0);
|
|
420
|
+
|
|
421
|
+
for (const element of buffer) {
|
|
422
|
+
result = (result << BigInt(8)) + BigInt(element);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return { bytesRead: 9, unsignedInt: result };
|
|
426
|
+
}
|
|
427
|
+
default:
|
|
428
|
+
throw new CborContentException("Invalid integer encoding");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
static ensureReadCapacityInArray(data: Uint8Array, bytesToRead: number) {
|
|
433
|
+
if (data.length < bytesToRead) {
|
|
434
|
+
throw new CborContentException(UNEXPECTED_END_OF_BUFFER_MSG);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#advanceBuffer(length: number) {
|
|
439
|
+
if (this.#offset + length > this.#data.length) {
|
|
440
|
+
throw new CborContentException("Buffer offset out of bounds");
|
|
441
|
+
}
|
|
442
|
+
this.#offset += length;
|
|
443
|
+
this.#cachedState = CborReaderState.Undefined;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
#pushDataItem(majorType: CborMajorType, definiteLength?: number): void {
|
|
447
|
+
const frame: StackFrame = {
|
|
448
|
+
currentKeyOffset: this.#currentFrame.currentKeyOffset,
|
|
449
|
+
definiteLength: this.#currentFrame.definiteLength,
|
|
450
|
+
frameOffset: this.#currentFrame.frameOffset,
|
|
451
|
+
itemsRead: this.#currentFrame.itemsRead,
|
|
452
|
+
type: this.#currentFrame.type,
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
this.#nestedItems.push(frame);
|
|
456
|
+
|
|
457
|
+
this.#currentFrame.type = majorType;
|
|
458
|
+
this.#currentFrame.definiteLength = definiteLength;
|
|
459
|
+
this.#currentFrame.itemsRead = 0;
|
|
460
|
+
this.#currentFrame.frameOffset = this.#offset;
|
|
461
|
+
this.#isTagContext = false;
|
|
462
|
+
this.#currentFrame.currentKeyOffset = null;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#getRemainingBytes(): Uint8Array {
|
|
466
|
+
return this.#data.slice(this.#offset);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HexStringNo0x, UnknownFormat } from "../nominal.ts";
|
|
2
|
+
|
|
3
|
+
function hexStringToBytes(hexString: HexStringNo0x): number[] {
|
|
4
|
+
if (!/^[0-9a-fA-F]+$/.test(hexString)) {
|
|
5
|
+
throw new Error("Non-hex digits found in hex string");
|
|
6
|
+
}
|
|
7
|
+
const bytes: number[] = [];
|
|
8
|
+
if (hexString.length % 2 !== 0) {
|
|
9
|
+
hexString = "0" + hexString;
|
|
10
|
+
}
|
|
11
|
+
for (let c = 0; c < hexString.length; c += 2) {
|
|
12
|
+
const nextByte = hexString.slice(c, c + 2);
|
|
13
|
+
bytes.push(parseInt(nextByte, 16));
|
|
14
|
+
}
|
|
15
|
+
return bytes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function hexStringToUint8Array(hexString: UnknownFormat): Uint8Array {
|
|
19
|
+
return new Uint8Array(hexStringToBytes(hexString));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function uint8ArrayToHexString(uint8Array: Uint8Array): HexStringNo0x {
|
|
23
|
+
// recall: slice(-2) takes two characters starting from the end of the string
|
|
24
|
+
// so this works because we <16 numbers with '0'
|
|
25
|
+
return Array.prototype.map.call(
|
|
26
|
+
uint8Array,
|
|
27
|
+
(byte) => ("0" + byte.toString(16)).slice(-2),
|
|
28
|
+
).join("");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const MALFORMED_ERROR_MSG = (str: string): string =>
|
|
32
|
+
`${str} is malformed`;
|
|
33
|
+
export const CHECKSUM_ERROR_MSG = (str: string): string =>
|
|
34
|
+
`${str} checksum is wrong`;
|