@marcuspuchalla/nachos 0.1.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/CHANGELOG.md +64 -0
- package/LICENSE +674 -0
- package/README.md +345 -0
- package/dist/chunk-2FUTHZQQ.cjs +755 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +1 -0
- package/dist/chunk-2HBCILJS.cjs +2034 -0
- package/dist/chunk-2HBCILJS.cjs.map +1 -0
- package/dist/chunk-7CFYWHS6.js +742 -0
- package/dist/chunk-7CFYWHS6.js.map +1 -0
- package/dist/chunk-PD72MVTX.cjs +160 -0
- package/dist/chunk-PD72MVTX.cjs.map +1 -0
- package/dist/chunk-ZDZ2B5PE.js +149 -0
- package/dist/chunk-ZDZ2B5PE.js.map +1 -0
- package/dist/chunk-ZRPJUEIZ.js +2020 -0
- package/dist/chunk-ZRPJUEIZ.js.map +1 -0
- package/dist/encoder/index.cjs +57 -0
- package/dist/encoder/index.cjs.map +1 -0
- package/dist/encoder/index.d.cts +72 -0
- package/dist/encoder/index.d.ts +72 -0
- package/dist/encoder/index.js +4 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.cjs +606 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +494 -0
- package/dist/index.d.ts +494 -0
- package/dist/index.js +523 -0
- package/dist/index.js.map +1 -0
- package/dist/metafile-cjs.json +1 -0
- package/dist/metafile-esm.json +1 -0
- package/dist/parser/index.cjs +85 -0
- package/dist/parser/index.cjs.map +1 -0
- package/dist/parser/index.d.cts +72 -0
- package/dist/parser/index.d.ts +72 -0
- package/dist/parser/index.js +4 -0
- package/dist/parser/index.js.map +1 -0
- package/dist/types-DvNlfbKB.d.cts +301 -0
- package/dist/types-DvNlfbKB.d.ts +301 -0
- package/dist/useCborSimpleEncoder-ButVU988.d.cts +268 -0
- package/dist/useCborSimpleEncoder-TVxzNJ_9.d.ts +268 -0
- package/dist/useCborTag-B_iaShG6.d.ts +142 -0
- package/dist/useCborTag-BfTIV8HM.d.cts +142 -0
- package/package.json +102 -0
- package/src/__tests__/public-api.test.ts +326 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +331 -0
- package/src/encoder/__tests__/cbor-integer-encoder.test.ts +283 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +224 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +345 -0
- package/src/encoder/__tests__/cbor-tag-encoder.test.ts +565 -0
- package/src/encoder/composables/#useCborTagEncoder.ts# +158 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +424 -0
- package/src/encoder/composables/useCborEncoder.ts +203 -0
- package/src/encoder/composables/useCborIntegerEncoder.ts +188 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +266 -0
- package/src/encoder/composables/useCborStringEncoder.ts +266 -0
- package/src/encoder/composables/useCborTagEncoder.ts +158 -0
- package/src/encoder/index.ts +35 -0
- package/src/encoder/types.ts +88 -0
- package/src/encoder/utils.ts +80 -0
- package/src/index.ts +434 -0
- package/src/parser/__tests__/ast-tree-structure.test.ts +311 -0
- package/src/parser/__tests__/cbor-collection-errors.test.ts +296 -0
- package/src/parser/__tests__/cbor-collection.test.ts +369 -0
- package/src/parser/__tests__/cbor-deterministic-encoding.test.ts +432 -0
- package/src/parser/__tests__/cbor-diagnostic.test.ts +333 -0
- package/src/parser/__tests__/cbor-duplicate-keys.test.ts +235 -0
- package/src/parser/__tests__/cbor-float-errors.test.ts +222 -0
- package/src/parser/__tests__/cbor-float.test.ts +502 -0
- package/src/parser/__tests__/cbor-integer-errors.test.ts +139 -0
- package/src/parser/__tests__/cbor-integer.test.ts +200 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +403 -0
- package/src/parser/__tests__/cbor-parser-errors.test.ts +126 -0
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +503 -0
- package/src/parser/__tests__/cbor-sequences.test.ts +150 -0
- package/src/parser/__tests__/cbor-source-map.test.ts +321 -0
- package/src/parser/__tests__/cbor-standard-tags.test.ts +340 -0
- package/src/parser/__tests__/cbor-string-errors.test.ts +227 -0
- package/src/parser/__tests__/cbor-string.test.ts +224 -0
- package/src/parser/__tests__/cbor-tag-advanced.test.ts +500 -0
- package/src/parser/__tests__/cbor-tag-errors.test.ts +447 -0
- package/src/parser/__tests__/cbor-tag-source-map.test.ts +360 -0
- package/src/parser/__tests__/cbor-tag.test.ts +684 -0
- package/src/parser/__tests__/extreme-edge-cases.test.ts +146 -0
- package/src/parser/__tests__/pathBuilder.test.ts +256 -0
- package/src/parser/__tests__/rfc-test-vectors.test.ts +607 -0
- package/src/parser/__tests__/security-limits.test.ts +248 -0
- package/src/parser/__tests__/utils-errors.test.ts +127 -0
- package/src/parser/composables/useCborCollection.ts +509 -0
- package/src/parser/composables/useCborDiagnostic.ts +381 -0
- package/src/parser/composables/useCborFloat.ts +256 -0
- package/src/parser/composables/useCborInteger.ts +114 -0
- package/src/parser/composables/useCborParser.ts +951 -0
- package/src/parser/composables/useCborString.ts +330 -0
- package/src/parser/composables/useCborStringTypes.ts +129 -0
- package/src/parser/composables/useCborTag.ts +739 -0
- package/src/parser/index.ts +56 -0
- package/src/parser/types.ts +371 -0
- package/src/parser/utils/pathBuilder.ts +259 -0
- package/src/parser/utils.ts +398 -0
- package/src/utils/__tests__/logger.test.ts +186 -0
- package/src/utils/logger.ts +96 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common utility functions for CBOR parsing
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts hex string to Uint8Array
|
|
7
|
+
*
|
|
8
|
+
* @param hex - Hex string (e.g., "1864")
|
|
9
|
+
* @returns Byte array
|
|
10
|
+
*/
|
|
11
|
+
export const hexToBytes = (hex: string): Uint8Array => {
|
|
12
|
+
const bytes = hex.match(/.{1,2}/g)
|
|
13
|
+
if (!bytes) return new Uint8Array(0)
|
|
14
|
+
return new Uint8Array(bytes.map(byte => parseInt(byte, 16)))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Converts Uint8Array to hex string
|
|
19
|
+
*
|
|
20
|
+
* @param bytes - Byte array
|
|
21
|
+
* @returns Hex string (e.g., "1864")
|
|
22
|
+
*/
|
|
23
|
+
export const bytesToHex = (bytes: Uint8Array): string => {
|
|
24
|
+
return Array.from(bytes)
|
|
25
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
26
|
+
.join('')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads a single byte from buffer at offset
|
|
31
|
+
*
|
|
32
|
+
* @param buffer - Data buffer
|
|
33
|
+
* @param offset - Byte offset
|
|
34
|
+
* @returns Byte value (0-255)
|
|
35
|
+
*/
|
|
36
|
+
export const readByte = (buffer: Uint8Array, offset: number): number => {
|
|
37
|
+
if (offset >= buffer.length) {
|
|
38
|
+
throw new Error(`Offset ${offset} is out of bounds (buffer length: ${buffer.length})`)
|
|
39
|
+
}
|
|
40
|
+
const byte = buffer[offset]
|
|
41
|
+
if (byte === undefined) {
|
|
42
|
+
throw new Error(`Unexpected undefined byte at offset ${offset}`)
|
|
43
|
+
}
|
|
44
|
+
return byte
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads unsigned integer of specified byte length (big-endian)
|
|
49
|
+
*
|
|
50
|
+
* @param buffer - Data buffer
|
|
51
|
+
* @param offset - Starting byte offset
|
|
52
|
+
* @param length - Number of bytes to read (1-8)
|
|
53
|
+
* @returns Integer value
|
|
54
|
+
*/
|
|
55
|
+
export const readUint = (buffer: Uint8Array, offset: number, length: number): number => {
|
|
56
|
+
if (length < 1 || length > 8) {
|
|
57
|
+
throw new Error(`Invalid length: ${length} (must be 1-8)`)
|
|
58
|
+
}
|
|
59
|
+
if (offset + length > buffer.length) {
|
|
60
|
+
throw new Error(`Cannot read ${length} bytes at offset ${offset} (buffer length: ${buffer.length})`)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let result = 0
|
|
64
|
+
for (let i = 0; i < length; i++) {
|
|
65
|
+
result = result * 256 + readByte(buffer, offset + i)
|
|
66
|
+
}
|
|
67
|
+
return result
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Reads unsigned BigInt of specified byte length (big-endian)
|
|
72
|
+
*
|
|
73
|
+
* @param buffer - Data buffer
|
|
74
|
+
* @param offset - Starting byte offset
|
|
75
|
+
* @param length - Number of bytes to read (1-8)
|
|
76
|
+
* @returns BigInt value
|
|
77
|
+
*/
|
|
78
|
+
export const readBigUint = (buffer: Uint8Array, offset: number, length: number): bigint => {
|
|
79
|
+
if (length < 1 || length > 8) {
|
|
80
|
+
throw new Error(`Invalid length: ${length} (must be 1-8)`)
|
|
81
|
+
}
|
|
82
|
+
if (offset + length > buffer.length) {
|
|
83
|
+
throw new Error(`Cannot read ${length} bytes at offset ${offset} (buffer length: ${buffer.length})`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let result = 0n
|
|
87
|
+
for (let i = 0; i < length; i++) {
|
|
88
|
+
result = result * 256n + BigInt(readByte(buffer, offset + i))
|
|
89
|
+
}
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts major type and additional info from initial byte
|
|
95
|
+
*
|
|
96
|
+
* @param initialByte - First byte of CBOR item
|
|
97
|
+
* @returns Object with majorType (0-7) and additionalInfo (0-31)
|
|
98
|
+
*/
|
|
99
|
+
export const extractCborHeader = (initialByte: number): { majorType: number; additionalInfo: number } => {
|
|
100
|
+
return {
|
|
101
|
+
majorType: initialByte >> 5,
|
|
102
|
+
additionalInfo: initialByte & 0x1f
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Strictly validate UTF-8 encoding
|
|
108
|
+
*
|
|
109
|
+
* Rejects:
|
|
110
|
+
* - Overlong encodings (security vulnerability)
|
|
111
|
+
* - Surrogate halves (U+D800-U+DFFF)
|
|
112
|
+
* - Values beyond U+10FFFF
|
|
113
|
+
* - Invalid start bytes (0xC0, 0xC1, 0xF5-0xFF)
|
|
114
|
+
* - Incomplete sequences
|
|
115
|
+
*
|
|
116
|
+
* @param bytes - UTF-8 bytes to validate
|
|
117
|
+
* @throws Error if validation fails
|
|
118
|
+
*/
|
|
119
|
+
export function validateUtf8Strict(bytes: Uint8Array): void {
|
|
120
|
+
let i = 0
|
|
121
|
+
|
|
122
|
+
while (i < bytes.length) {
|
|
123
|
+
const byte = bytes[i]
|
|
124
|
+
if (byte === undefined) {
|
|
125
|
+
throw new Error(`Unexpected undefined byte at position ${i}`)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Invalid start bytes
|
|
129
|
+
if (byte === 0xC0 || byte === 0xC1 || byte >= 0xF5) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Invalid UTF-8 start byte 0x${byte.toString(16).padStart(2, '0')} at position ${i}`
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 1-byte sequence (ASCII: 0x00-0x7F)
|
|
136
|
+
if (byte < 0x80) {
|
|
137
|
+
i++
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 2-byte sequence (0xC2-0xDF)
|
|
142
|
+
if (byte >= 0xC2 && byte <= 0xDF) {
|
|
143
|
+
if (i + 1 >= bytes.length) {
|
|
144
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const byte2 = bytes[i + 1]
|
|
148
|
+
if (byte2 === undefined) {
|
|
149
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
150
|
+
}
|
|
151
|
+
if ((byte2 & 0xC0) !== 0x80) {
|
|
152
|
+
throw new Error(`Invalid UTF-8 continuation byte at position ${i + 1}`)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for overlong encoding
|
|
156
|
+
const codepoint = ((byte & 0x1F) << 6) | (byte2 & 0x3F)
|
|
157
|
+
if (codepoint < 0x80) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Overlong UTF-8 encoding at position ${i}: ` +
|
|
160
|
+
`U+${codepoint.toString(16).padStart(4, '0').toUpperCase()} ` +
|
|
161
|
+
`encoded as 2 bytes (should be 1 byte)`
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
i += 2
|
|
166
|
+
continue
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 3-byte sequence (0xE0-0xEF)
|
|
170
|
+
if (byte >= 0xE0 && byte <= 0xEF) {
|
|
171
|
+
if (i + 2 >= bytes.length) {
|
|
172
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const byte2 = bytes[i + 1]
|
|
176
|
+
const byte3 = bytes[i + 2]
|
|
177
|
+
|
|
178
|
+
if (byte2 === undefined || byte3 === undefined) {
|
|
179
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Invalid UTF-8 continuation byte at position ${i + 1} or ${i + 2}`
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const codepoint = ((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F)
|
|
189
|
+
|
|
190
|
+
// Check for overlong encoding
|
|
191
|
+
if (codepoint < 0x800) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Overlong UTF-8 encoding at position ${i}: ` +
|
|
194
|
+
`U+${codepoint.toString(16).padStart(4, '0').toUpperCase()} ` +
|
|
195
|
+
`encoded as 3 bytes (should be 2 bytes or less)`
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Check for surrogate range (U+D800-U+DFFF are invalid)
|
|
200
|
+
if (codepoint >= 0xD800 && codepoint <= 0xDFFF) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Invalid UTF-8 surrogate codepoint U+${codepoint.toString(16).padStart(4, '0').toUpperCase()} ` +
|
|
203
|
+
`at position ${i} (surrogates are not valid Unicode scalar values)`
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
i += 3
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 4-byte sequence (0xF0-0xF4)
|
|
212
|
+
if (byte >= 0xF0 && byte <= 0xF4) {
|
|
213
|
+
if (i + 3 >= bytes.length) {
|
|
214
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const byte2 = bytes[i + 1]
|
|
218
|
+
const byte3 = bytes[i + 2]
|
|
219
|
+
const byte4 = bytes[i + 3]
|
|
220
|
+
|
|
221
|
+
if (byte2 === undefined || byte3 === undefined || byte4 === undefined) {
|
|
222
|
+
throw new Error(`Incomplete UTF-8 sequence at position ${i}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if ((byte2 & 0xC0) !== 0x80 || (byte3 & 0xC0) !== 0x80 || (byte4 & 0xC0) !== 0x80) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
`Invalid UTF-8 continuation byte at position ${i + 1}, ${i + 2}, or ${i + 3}`
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const codepoint =
|
|
232
|
+
((byte & 0x07) << 18) |
|
|
233
|
+
((byte2 & 0x3F) << 12) |
|
|
234
|
+
((byte3 & 0x3F) << 6) |
|
|
235
|
+
(byte4 & 0x3F)
|
|
236
|
+
|
|
237
|
+
// Check for overlong encoding
|
|
238
|
+
if (codepoint < 0x10000) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`Overlong UTF-8 encoding at position ${i}: ` +
|
|
241
|
+
`U+${codepoint.toString(16).padStart(6, '0').toUpperCase()} ` +
|
|
242
|
+
`encoded as 4 bytes (should be 3 bytes or less)`
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check maximum codepoint (U+10FFFF)
|
|
247
|
+
if (codepoint > 0x10FFFF) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`UTF-8 codepoint U+${codepoint.toString(16).padStart(6, '0').toUpperCase()} ` +
|
|
250
|
+
`exceeds maximum U+10FFFF at position ${i}`
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
i += 4
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// If we get here, it's an invalid byte
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Invalid UTF-8 byte 0x${byte.toString(16).padStart(2, '0')} at position ${i}`
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Validate integer is in canonical (shortest) form
|
|
267
|
+
*/
|
|
268
|
+
export function validateCanonicalInteger(value: number | bigint, ai: number): void {
|
|
269
|
+
const v = typeof value === 'bigint' ? value : BigInt(value)
|
|
270
|
+
|
|
271
|
+
// Values 0-23 must use direct encoding (AI = value)
|
|
272
|
+
if (v >= 0n && v <= 23n && ai !== Number(v)) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
`Non-canonical integer: value ${v} must use AI ${v}, not AI ${ai}`
|
|
275
|
+
)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Values 24-255 must use 1-byte encoding (AI = 24)
|
|
279
|
+
if (v >= 24n && v <= 255n && ai !== 24) {
|
|
280
|
+
throw new Error(
|
|
281
|
+
`Non-canonical integer: value ${v} must use AI 24 (1-byte), not AI ${ai}`
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Values 256-65535 must use 2-byte encoding (AI = 25)
|
|
286
|
+
if (v >= 256n && v <= 65535n && ai !== 25) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`Non-canonical integer: value ${v} must use AI 25 (2-byte), not AI ${ai}`
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Values 65536-4294967295 must use 4-byte encoding (AI = 26)
|
|
293
|
+
if (v >= 65536n && v <= 4294967295n && ai !== 26) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Non-canonical integer: value ${v} must use AI 26 (4-byte), not AI ${ai}`
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Values > 4294967295 must use 8-byte encoding (AI = 27)
|
|
300
|
+
if (v > 4294967295n && ai !== 27) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`Non-canonical integer: value ${v} must use AI 27 (8-byte), not AI ${ai}`
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Compare two byte arrays lexicographically
|
|
309
|
+
* Returns: -1 if a < b, 0 if a === b, 1 if a > b
|
|
310
|
+
*/
|
|
311
|
+
export function compareBytes(a: Uint8Array, b: Uint8Array): number {
|
|
312
|
+
if (!a || !b) {
|
|
313
|
+
throw new Error('compareBytes: arguments cannot be null or undefined')
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Shorter arrays come first
|
|
317
|
+
if (a.length !== b.length) {
|
|
318
|
+
return a.length - b.length
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Same length: lexicographic comparison
|
|
322
|
+
for (let i = 0; i < a.length; i++) {
|
|
323
|
+
const byteA = a[i]
|
|
324
|
+
const byteB = b[i]
|
|
325
|
+
if (byteA === undefined || byteB === undefined) {
|
|
326
|
+
throw new Error(`Unexpected undefined byte at index ${i}`)
|
|
327
|
+
}
|
|
328
|
+
if (byteA !== byteB) {
|
|
329
|
+
return byteA - byteB
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return 0 // Equal
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Serializes a CBOR value to a normalized string for comparison
|
|
338
|
+
* Used for detecting duplicates in sets and ensuring uniqueness
|
|
339
|
+
*
|
|
340
|
+
* @param value - CBOR value to serialize
|
|
341
|
+
* @returns Normalized string representation
|
|
342
|
+
*/
|
|
343
|
+
export function serializeValueForComparison(value: unknown): string {
|
|
344
|
+
// Handle primitives
|
|
345
|
+
if (value === null) return 'null'
|
|
346
|
+
if (value === undefined) return 'undefined'
|
|
347
|
+
if (typeof value === 'boolean') return value.toString()
|
|
348
|
+
if (typeof value === 'number') return `num:${value}`
|
|
349
|
+
if (typeof value === 'bigint') return `bigint:${value.toString()}`
|
|
350
|
+
if (typeof value === 'string') return `str:${value}`
|
|
351
|
+
|
|
352
|
+
// Handle Uint8Array (byte strings)
|
|
353
|
+
if (value instanceof Uint8Array) {
|
|
354
|
+
return `bytes:${Array.from(value).map(b => b.toString(16).padStart(2, '0')).join('')}`
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Handle arrays
|
|
358
|
+
if (Array.isArray(value)) {
|
|
359
|
+
return `array:[${value.map(v => serializeValueForComparison(v)).join(',')}]`
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Handle objects (maps and tagged values)
|
|
363
|
+
if (typeof value === 'object') {
|
|
364
|
+
// Check if it's a tagged value
|
|
365
|
+
if ('tag' in value && 'value' in value) {
|
|
366
|
+
return `tag:${(value as any).tag}:${serializeValueForComparison((value as any).value)}`
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Regular object (map)
|
|
370
|
+
const keys = Object.keys(value).sort()
|
|
371
|
+
const pairs = keys.map(k => `${k}:${serializeValueForComparison((value as any)[k])}`)
|
|
372
|
+
return `map:{${pairs.join(',')}}`
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Fallback for unknown types
|
|
376
|
+
return String(value)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Checks if a set (array) contains duplicate values
|
|
381
|
+
* Uses serialization-based comparison to handle nested structures
|
|
382
|
+
*
|
|
383
|
+
* @param items - Array of CBOR values
|
|
384
|
+
* @returns True if duplicates found, false otherwise
|
|
385
|
+
*/
|
|
386
|
+
export function hasDuplicates(items: unknown[]): boolean {
|
|
387
|
+
const seen = new Set<string>()
|
|
388
|
+
|
|
389
|
+
for (const item of items) {
|
|
390
|
+
const serialized = serializeValueForComparison(item)
|
|
391
|
+
if (seen.has(serialized)) {
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
seen.add(serialized)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return false
|
|
398
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
6
|
+
import {
|
|
7
|
+
configureLogger,
|
|
8
|
+
getLoggerConfig,
|
|
9
|
+
debug,
|
|
10
|
+
info,
|
|
11
|
+
warn,
|
|
12
|
+
error,
|
|
13
|
+
logger
|
|
14
|
+
} from '../logger'
|
|
15
|
+
|
|
16
|
+
describe('Logger', () => {
|
|
17
|
+
// Save original console methods
|
|
18
|
+
const originalDebug = console.debug
|
|
19
|
+
const originalInfo = console.info
|
|
20
|
+
const originalWarn = console.warn
|
|
21
|
+
const originalError = console.error
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
// Reset logger to default
|
|
25
|
+
configureLogger({ level: 'warn', prefix: '[CBOR]' })
|
|
26
|
+
// Mock console methods
|
|
27
|
+
console.debug = vi.fn()
|
|
28
|
+
console.info = vi.fn()
|
|
29
|
+
console.warn = vi.fn()
|
|
30
|
+
console.error = vi.fn()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
// Restore console methods
|
|
35
|
+
console.debug = originalDebug
|
|
36
|
+
console.info = originalInfo
|
|
37
|
+
console.warn = originalWarn
|
|
38
|
+
console.error = originalError
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('configureLogger', () => {
|
|
42
|
+
it('should update logger configuration', () => {
|
|
43
|
+
configureLogger({ level: 'debug' })
|
|
44
|
+
expect(getLoggerConfig().level).toBe('debug')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should update prefix', () => {
|
|
48
|
+
configureLogger({ prefix: '[TEST]' })
|
|
49
|
+
expect(getLoggerConfig().prefix).toBe('[TEST]')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should merge with existing config', () => {
|
|
53
|
+
configureLogger({ level: 'error' })
|
|
54
|
+
configureLogger({ prefix: '[NEW]' })
|
|
55
|
+
const config = getLoggerConfig()
|
|
56
|
+
expect(config.level).toBe('error')
|
|
57
|
+
expect(config.prefix).toBe('[NEW]')
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('getLoggerConfig', () => {
|
|
62
|
+
it('should return current configuration', () => {
|
|
63
|
+
const config = getLoggerConfig()
|
|
64
|
+
expect(config).toHaveProperty('level')
|
|
65
|
+
expect(config).toHaveProperty('prefix')
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('should return a copy (not reference)', () => {
|
|
69
|
+
const config1 = getLoggerConfig()
|
|
70
|
+
const config2 = getLoggerConfig()
|
|
71
|
+
expect(config1).not.toBe(config2)
|
|
72
|
+
expect(config1).toEqual(config2)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
describe('debug', () => {
|
|
77
|
+
it('should log when level is debug', () => {
|
|
78
|
+
configureLogger({ level: 'debug' })
|
|
79
|
+
debug('test message')
|
|
80
|
+
expect(console.debug).toHaveBeenCalledWith('[CBOR] test message')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should not log when level is higher than debug', () => {
|
|
84
|
+
configureLogger({ level: 'info' })
|
|
85
|
+
debug('test message')
|
|
86
|
+
expect(console.debug).not.toHaveBeenCalled()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('should pass additional arguments', () => {
|
|
90
|
+
configureLogger({ level: 'debug' })
|
|
91
|
+
debug('test', { foo: 'bar' }, 123)
|
|
92
|
+
expect(console.debug).toHaveBeenCalledWith('[CBOR] test', { foo: 'bar' }, 123)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('info', () => {
|
|
97
|
+
it('should log when level is info or lower', () => {
|
|
98
|
+
configureLogger({ level: 'info' })
|
|
99
|
+
info('test message')
|
|
100
|
+
expect(console.info).toHaveBeenCalledWith('[CBOR] test message')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should not log when level is higher than info', () => {
|
|
104
|
+
configureLogger({ level: 'warn' })
|
|
105
|
+
info('test message')
|
|
106
|
+
expect(console.info).not.toHaveBeenCalled()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should pass additional arguments', () => {
|
|
110
|
+
configureLogger({ level: 'info' })
|
|
111
|
+
info('test', { foo: 'bar' })
|
|
112
|
+
expect(console.info).toHaveBeenCalledWith('[CBOR] test', { foo: 'bar' })
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('warn', () => {
|
|
117
|
+
it('should log when level is warn or lower', () => {
|
|
118
|
+
configureLogger({ level: 'warn' })
|
|
119
|
+
warn('test message')
|
|
120
|
+
expect(console.warn).toHaveBeenCalledWith('[CBOR] test message')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should not log when level is error', () => {
|
|
124
|
+
configureLogger({ level: 'error' })
|
|
125
|
+
warn('test message')
|
|
126
|
+
expect(console.warn).not.toHaveBeenCalled()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('should pass additional arguments', () => {
|
|
130
|
+
configureLogger({ level: 'warn' })
|
|
131
|
+
warn('test', 123)
|
|
132
|
+
expect(console.warn).toHaveBeenCalledWith('[CBOR] test', 123)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
describe('error', () => {
|
|
137
|
+
it('should log when level is error or lower', () => {
|
|
138
|
+
configureLogger({ level: 'error' })
|
|
139
|
+
error('test message')
|
|
140
|
+
expect(console.error).toHaveBeenCalledWith('[CBOR] test message')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should not log when level is silent', () => {
|
|
144
|
+
configureLogger({ level: 'silent' })
|
|
145
|
+
error('test message')
|
|
146
|
+
expect(console.error).not.toHaveBeenCalled()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should pass additional arguments', () => {
|
|
150
|
+
configureLogger({ level: 'error' })
|
|
151
|
+
error('test', new Error('oops'))
|
|
152
|
+
expect(console.error).toHaveBeenCalled()
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('formatMessage without prefix', () => {
|
|
157
|
+
it('should format message without prefix when prefix is empty', () => {
|
|
158
|
+
configureLogger({ level: 'debug', prefix: '' })
|
|
159
|
+
debug('test message')
|
|
160
|
+
expect(console.debug).toHaveBeenCalledWith('test message')
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('should format message without prefix when prefix is undefined', () => {
|
|
164
|
+
configureLogger({ level: 'debug', prefix: undefined })
|
|
165
|
+
debug('test message')
|
|
166
|
+
expect(console.debug).toHaveBeenCalledWith('test message')
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe('logger object', () => {
|
|
171
|
+
it('should expose all methods', () => {
|
|
172
|
+
expect(logger.debug).toBe(debug)
|
|
173
|
+
expect(logger.info).toBe(info)
|
|
174
|
+
expect(logger.warn).toBe(warn)
|
|
175
|
+
expect(logger.error).toBe(error)
|
|
176
|
+
expect(logger.configure).toBe(configureLogger)
|
|
177
|
+
expect(logger.getConfig).toBe(getLoggerConfig)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should work via logger object', () => {
|
|
181
|
+
logger.configure({ level: 'debug' })
|
|
182
|
+
logger.debug('via object')
|
|
183
|
+
expect(console.debug).toHaveBeenCalled()
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility for CBOR decoder library
|
|
3
|
+
* Provides configurable logging with different levels
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
7
|
+
|
|
8
|
+
interface LoggerConfig {
|
|
9
|
+
level: LogLevel
|
|
10
|
+
prefix?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
14
|
+
debug: 0,
|
|
15
|
+
info: 1,
|
|
16
|
+
warn: 2,
|
|
17
|
+
error: 3,
|
|
18
|
+
silent: 4
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let config: LoggerConfig = {
|
|
22
|
+
level: 'warn',
|
|
23
|
+
prefix: '[CBOR]'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configure the logger
|
|
28
|
+
*/
|
|
29
|
+
export function configureLogger(newConfig: Partial<LoggerConfig>): void {
|
|
30
|
+
config = { ...config, ...newConfig }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get current logger configuration
|
|
35
|
+
*/
|
|
36
|
+
export function getLoggerConfig(): LoggerConfig {
|
|
37
|
+
return { ...config }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function shouldLog(level: LogLevel): boolean {
|
|
41
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[config.level]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatMessage(message: string): string {
|
|
45
|
+
return config.prefix ? `${config.prefix} ${message}` : message
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Log a debug message
|
|
50
|
+
*/
|
|
51
|
+
export function debug(message: string, ...args: unknown[]): void {
|
|
52
|
+
if (shouldLog('debug')) {
|
|
53
|
+
console.debug(formatMessage(message), ...args)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Log an info message
|
|
59
|
+
*/
|
|
60
|
+
export function info(message: string, ...args: unknown[]): void {
|
|
61
|
+
if (shouldLog('info')) {
|
|
62
|
+
console.info(formatMessage(message), ...args)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Log a warning message
|
|
68
|
+
*/
|
|
69
|
+
export function warn(message: string, ...args: unknown[]): void {
|
|
70
|
+
if (shouldLog('warn')) {
|
|
71
|
+
console.warn(formatMessage(message), ...args)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Log an error message
|
|
77
|
+
*/
|
|
78
|
+
export function error(message: string, ...args: unknown[]): void {
|
|
79
|
+
if (shouldLog('error')) {
|
|
80
|
+
console.error(formatMessage(message), ...args)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Logger object for convenience
|
|
86
|
+
*/
|
|
87
|
+
export const logger = {
|
|
88
|
+
debug,
|
|
89
|
+
info,
|
|
90
|
+
warn,
|
|
91
|
+
error,
|
|
92
|
+
configure: configureLogger,
|
|
93
|
+
getConfig: getLoggerConfig
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default logger
|