@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.
Files changed (100) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/LICENSE +674 -0
  3. package/README.md +345 -0
  4. package/dist/chunk-2FUTHZQQ.cjs +755 -0
  5. package/dist/chunk-2FUTHZQQ.cjs.map +1 -0
  6. package/dist/chunk-2HBCILJS.cjs +2034 -0
  7. package/dist/chunk-2HBCILJS.cjs.map +1 -0
  8. package/dist/chunk-7CFYWHS6.js +742 -0
  9. package/dist/chunk-7CFYWHS6.js.map +1 -0
  10. package/dist/chunk-PD72MVTX.cjs +160 -0
  11. package/dist/chunk-PD72MVTX.cjs.map +1 -0
  12. package/dist/chunk-ZDZ2B5PE.js +149 -0
  13. package/dist/chunk-ZDZ2B5PE.js.map +1 -0
  14. package/dist/chunk-ZRPJUEIZ.js +2020 -0
  15. package/dist/chunk-ZRPJUEIZ.js.map +1 -0
  16. package/dist/encoder/index.cjs +57 -0
  17. package/dist/encoder/index.cjs.map +1 -0
  18. package/dist/encoder/index.d.cts +72 -0
  19. package/dist/encoder/index.d.ts +72 -0
  20. package/dist/encoder/index.js +4 -0
  21. package/dist/encoder/index.js.map +1 -0
  22. package/dist/index.cjs +606 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.cts +494 -0
  25. package/dist/index.d.ts +494 -0
  26. package/dist/index.js +523 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/metafile-cjs.json +1 -0
  29. package/dist/metafile-esm.json +1 -0
  30. package/dist/parser/index.cjs +85 -0
  31. package/dist/parser/index.cjs.map +1 -0
  32. package/dist/parser/index.d.cts +72 -0
  33. package/dist/parser/index.d.ts +72 -0
  34. package/dist/parser/index.js +4 -0
  35. package/dist/parser/index.js.map +1 -0
  36. package/dist/types-DvNlfbKB.d.cts +301 -0
  37. package/dist/types-DvNlfbKB.d.ts +301 -0
  38. package/dist/useCborSimpleEncoder-ButVU988.d.cts +268 -0
  39. package/dist/useCborSimpleEncoder-TVxzNJ_9.d.ts +268 -0
  40. package/dist/useCborTag-B_iaShG6.d.ts +142 -0
  41. package/dist/useCborTag-BfTIV8HM.d.cts +142 -0
  42. package/package.json +102 -0
  43. package/src/__tests__/public-api.test.ts +326 -0
  44. package/src/encoder/__tests__/cbor-collection-encoder.test.ts +331 -0
  45. package/src/encoder/__tests__/cbor-integer-encoder.test.ts +283 -0
  46. package/src/encoder/__tests__/cbor-simple-encoder.test.ts +224 -0
  47. package/src/encoder/__tests__/cbor-string-encoder.test.ts +345 -0
  48. package/src/encoder/__tests__/cbor-tag-encoder.test.ts +565 -0
  49. package/src/encoder/composables/#useCborTagEncoder.ts# +158 -0
  50. package/src/encoder/composables/useCborCollectionEncoder.ts +424 -0
  51. package/src/encoder/composables/useCborEncoder.ts +203 -0
  52. package/src/encoder/composables/useCborIntegerEncoder.ts +188 -0
  53. package/src/encoder/composables/useCborSimpleEncoder.ts +266 -0
  54. package/src/encoder/composables/useCborStringEncoder.ts +266 -0
  55. package/src/encoder/composables/useCborTagEncoder.ts +158 -0
  56. package/src/encoder/index.ts +35 -0
  57. package/src/encoder/types.ts +88 -0
  58. package/src/encoder/utils.ts +80 -0
  59. package/src/index.ts +434 -0
  60. package/src/parser/__tests__/ast-tree-structure.test.ts +311 -0
  61. package/src/parser/__tests__/cbor-collection-errors.test.ts +296 -0
  62. package/src/parser/__tests__/cbor-collection.test.ts +369 -0
  63. package/src/parser/__tests__/cbor-deterministic-encoding.test.ts +432 -0
  64. package/src/parser/__tests__/cbor-diagnostic.test.ts +333 -0
  65. package/src/parser/__tests__/cbor-duplicate-keys.test.ts +235 -0
  66. package/src/parser/__tests__/cbor-float-errors.test.ts +222 -0
  67. package/src/parser/__tests__/cbor-float.test.ts +502 -0
  68. package/src/parser/__tests__/cbor-integer-errors.test.ts +139 -0
  69. package/src/parser/__tests__/cbor-integer.test.ts +200 -0
  70. package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +403 -0
  71. package/src/parser/__tests__/cbor-parser-errors.test.ts +126 -0
  72. package/src/parser/__tests__/cbor-security-dos-protection.test.ts +503 -0
  73. package/src/parser/__tests__/cbor-sequences.test.ts +150 -0
  74. package/src/parser/__tests__/cbor-source-map.test.ts +321 -0
  75. package/src/parser/__tests__/cbor-standard-tags.test.ts +340 -0
  76. package/src/parser/__tests__/cbor-string-errors.test.ts +227 -0
  77. package/src/parser/__tests__/cbor-string.test.ts +224 -0
  78. package/src/parser/__tests__/cbor-tag-advanced.test.ts +500 -0
  79. package/src/parser/__tests__/cbor-tag-errors.test.ts +447 -0
  80. package/src/parser/__tests__/cbor-tag-source-map.test.ts +360 -0
  81. package/src/parser/__tests__/cbor-tag.test.ts +684 -0
  82. package/src/parser/__tests__/extreme-edge-cases.test.ts +146 -0
  83. package/src/parser/__tests__/pathBuilder.test.ts +256 -0
  84. package/src/parser/__tests__/rfc-test-vectors.test.ts +607 -0
  85. package/src/parser/__tests__/security-limits.test.ts +248 -0
  86. package/src/parser/__tests__/utils-errors.test.ts +127 -0
  87. package/src/parser/composables/useCborCollection.ts +509 -0
  88. package/src/parser/composables/useCborDiagnostic.ts +381 -0
  89. package/src/parser/composables/useCborFloat.ts +256 -0
  90. package/src/parser/composables/useCborInteger.ts +114 -0
  91. package/src/parser/composables/useCborParser.ts +951 -0
  92. package/src/parser/composables/useCborString.ts +330 -0
  93. package/src/parser/composables/useCborStringTypes.ts +129 -0
  94. package/src/parser/composables/useCborTag.ts +739 -0
  95. package/src/parser/index.ts +56 -0
  96. package/src/parser/types.ts +371 -0
  97. package/src/parser/utils/pathBuilder.ts +259 -0
  98. package/src/parser/utils.ts +398 -0
  99. package/src/utils/__tests__/logger.test.ts +186 -0
  100. 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