@marcuspuchalla/nachos 0.1.3 → 0.1.4
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 +23 -0
- package/dist/{chunk-5A5T56JB.js → chunk-5IWW5H47.js} +378 -207
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-PTWN7K3Y.cjs → chunk-RVG2BY32.cjs} +378 -207
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-R62CQQNI.cjs → chunk-S4RXO6IB.cjs} +195 -165
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-2MTLSQ7E.js → chunk-UMAX5MX5.js} +195 -165
- package/dist/chunk-UMAX5MX5.js.map +1 -0
- package/dist/encoder/index.cjs +13 -13
- package/dist/encoder/index.d.cts +2 -2
- package/dist/encoder/index.d.ts +2 -2
- package/dist/encoder/index.js +1 -1
- package/dist/index.cjs +32 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -19
- package/dist/index.d.ts +28 -19
- package/dist/index.js +16 -16
- package/dist/index.js.map +1 -1
- package/dist/metafile-cjs.json +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/parser/index.cjs +14 -14
- package/dist/parser/index.d.cts +3 -1
- package/dist/parser/index.d.ts +3 -1
- package/dist/parser/index.js +1 -1
- package/dist/{useCborSimpleEncoder-TVxzNJ_9.d.ts → useCborSimpleEncoder-BoKEmjP9.d.ts} +0 -2
- package/dist/{useCborSimpleEncoder-ButVU988.d.cts → useCborSimpleEncoder-C_OHxoB8.d.cts} +0 -2
- package/dist/{useCborTag-Cs1CZuXZ.d.cts → useCborTag-BD6Sqp7p.d.ts} +9 -4
- package/dist/{useCborTag-xV2Pz2VY.d.ts → useCborTag-QpZR-Er2.d.cts} +9 -4
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +5 -6
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +103 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +40 -5
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +28 -25
- package/src/encoder/composables/useCborEncoder.ts +21 -0
- package/src/encoder/composables/useCborSimpleEncoder.ts +34 -7
- package/src/encoder/types.ts +0 -2
- package/src/index.ts +29 -20
- package/src/parser/__tests__/buffer-native-parsing.test.ts +338 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +164 -31
- package/src/parser/__tests__/cbor-standard-tags.test.ts +75 -7
- package/src/parser/__tests__/cbor-tag-reparse-fix.test.ts +268 -0
- package/src/parser/composables/useCborCollection.ts +45 -42
- package/src/parser/composables/useCborFloat.ts +2 -1
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -197
- package/src/parser/composables/useCborTag.ts +45 -37
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2MTLSQ7E.js.map +0 -1
- package/dist/chunk-5A5T56JB.js.map +0 -1
- package/dist/chunk-PTWN7K3Y.cjs.map +0 -1
- package/dist/chunk-R62CQQNI.cjs.map +0 -1
package/src/index.ts
CHANGED
|
@@ -96,9 +96,14 @@ import type { ParseResult, ParseResultWithMap, ParseOptions } from './parser/typ
|
|
|
96
96
|
import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/types'
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
|
-
* Decode CBOR
|
|
99
|
+
* Decode CBOR data to JavaScript value
|
|
100
100
|
*
|
|
101
|
-
*
|
|
101
|
+
* Accepts either a hex string or a Uint8Array of raw CBOR bytes.
|
|
102
|
+
* When passing a Uint8Array, the bytes are used directly without
|
|
103
|
+
* hex conversion, which is more efficient for binary sources
|
|
104
|
+
* (WebSocket, fetch, file I/O, etc.).
|
|
105
|
+
*
|
|
106
|
+
* @param input - CBOR data as hex string (e.g., "1864") or Uint8Array
|
|
102
107
|
* @param options - Optional parser configuration
|
|
103
108
|
* @returns Decoded value and number of bytes consumed
|
|
104
109
|
*
|
|
@@ -106,9 +111,12 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
|
|
|
106
111
|
*
|
|
107
112
|
* @example
|
|
108
113
|
* ```typescript
|
|
109
|
-
* // Decode
|
|
114
|
+
* // Decode from hex string
|
|
110
115
|
* decode('1864') // { value: 100, bytesRead: 2 }
|
|
111
116
|
*
|
|
117
|
+
* // Decode from Uint8Array (zero-copy, no hex conversion)
|
|
118
|
+
* decode(new Uint8Array([0x18, 0x64])) // { value: 100, bytesRead: 2 }
|
|
119
|
+
*
|
|
112
120
|
* // Decode string
|
|
113
121
|
* decode('6449455446') // { value: "IETF", bytesRead: 5 }
|
|
114
122
|
*
|
|
@@ -124,18 +132,20 @@ import type { EncodeResult, EncodeOptions, EncodableValue } from './encoder/type
|
|
|
124
132
|
*
|
|
125
133
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc8949 | RFC 8949}
|
|
126
134
|
*/
|
|
127
|
-
export function decode(
|
|
135
|
+
export function decode(input: string | Uint8Array, options?: ParseOptions): ParseResult {
|
|
128
136
|
const { parse } = useCborParser()
|
|
129
|
-
return parse(
|
|
137
|
+
return parse(input, options)
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
|
133
|
-
* Decode CBOR
|
|
141
|
+
* Decode CBOR data with source map generation
|
|
134
142
|
*
|
|
135
143
|
* Source maps provide bidirectional linking between hex bytes and decoded values,
|
|
136
144
|
* enabling interactive debugging visualizations.
|
|
137
145
|
*
|
|
138
|
-
*
|
|
146
|
+
* Accepts either a hex string or a Uint8Array of raw CBOR bytes.
|
|
147
|
+
*
|
|
148
|
+
* @param input - CBOR data as hex string or Uint8Array
|
|
139
149
|
* @param options - Optional parser configuration
|
|
140
150
|
* @returns Decoded value, byte count, and source map
|
|
141
151
|
*
|
|
@@ -148,14 +158,13 @@ export function decode(hexString: string, options?: ParseOptions): ParseResult {
|
|
|
148
158
|
* // { path: '.value', start: 2, end: 3, majorType: 4, type: 'Array', parent: '' }
|
|
149
159
|
* // ]
|
|
150
160
|
*
|
|
151
|
-
* //
|
|
152
|
-
* const
|
|
153
|
-
* console.log(`Value is at bytes ${entry.start}-${entry.end}`)
|
|
161
|
+
* // From Uint8Array
|
|
162
|
+
* const { value, sourceMap } = decodeWithSourceMap(new Uint8Array([0xd8, 0x79, 0x80]))
|
|
154
163
|
* ```
|
|
155
164
|
*/
|
|
156
|
-
export function decodeWithSourceMap(
|
|
165
|
+
export function decodeWithSourceMap(input: string | Uint8Array, options?: ParseOptions): ParseResultWithMap {
|
|
157
166
|
const { parseWithSourceMap } = useCborParser()
|
|
158
|
-
return parseWithSourceMap(
|
|
167
|
+
return parseWithSourceMap(input, options)
|
|
159
168
|
}
|
|
160
169
|
|
|
161
170
|
/**
|
|
@@ -287,23 +296,23 @@ export class CborDecoder {
|
|
|
287
296
|
}
|
|
288
297
|
|
|
289
298
|
/**
|
|
290
|
-
* Decode CBOR
|
|
299
|
+
* Decode CBOR data
|
|
291
300
|
*
|
|
292
|
-
* @param
|
|
301
|
+
* @param input - CBOR data as hex string or Uint8Array
|
|
293
302
|
* @returns Decoded value and byte count
|
|
294
303
|
*/
|
|
295
|
-
decode(
|
|
296
|
-
return decode(
|
|
304
|
+
decode(input: string | Uint8Array): ParseResult {
|
|
305
|
+
return decode(input, this.options)
|
|
297
306
|
}
|
|
298
307
|
|
|
299
308
|
/**
|
|
300
|
-
* Decode CBOR
|
|
309
|
+
* Decode CBOR data with source map
|
|
301
310
|
*
|
|
302
|
-
* @param
|
|
311
|
+
* @param input - CBOR data as hex string or Uint8Array
|
|
303
312
|
* @returns Decoded value, byte count, and source map
|
|
304
313
|
*/
|
|
305
|
-
decodeWithSourceMap(
|
|
306
|
-
return decodeWithSourceMap(
|
|
314
|
+
decodeWithSourceMap(input: string | Uint8Array): ParseResultWithMap {
|
|
315
|
+
return decodeWithSourceMap(input, this.options)
|
|
307
316
|
}
|
|
308
317
|
}
|
|
309
318
|
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for buffer-native parsing optimization (Task 2-A)
|
|
3
|
+
* Validates that buffer-based parsers produce identical results to hex-string parsers
|
|
4
|
+
* and that O(N^2) hex conversion is eliminated.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest'
|
|
8
|
+
import { useCborInteger } from '../composables/useCborInteger'
|
|
9
|
+
import { useCborFloat } from '../composables/useCborFloat'
|
|
10
|
+
import { useCborTag } from '../composables/useCborTag'
|
|
11
|
+
import { useCborCollection } from '../composables/useCborCollection'
|
|
12
|
+
import { hexToBytes } from '../utils'
|
|
13
|
+
|
|
14
|
+
describe('Buffer-native parsing - parseIntegerFromBuffer', () => {
|
|
15
|
+
const { parseInteger, parseIntegerFromBuffer } = useCborInteger()
|
|
16
|
+
|
|
17
|
+
it('should export parseIntegerFromBuffer from useCborInteger', () => {
|
|
18
|
+
expect(typeof parseIntegerFromBuffer).toBe('function')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should parse unsigned integer 0 from buffer', () => {
|
|
22
|
+
const buffer = hexToBytes('00')
|
|
23
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
24
|
+
expect(result.value).toBe(0)
|
|
25
|
+
expect(result.bytesRead).toBe(1)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should parse unsigned integer 23 from buffer', () => {
|
|
29
|
+
const buffer = hexToBytes('17')
|
|
30
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
31
|
+
expect(result.value).toBe(23)
|
|
32
|
+
expect(result.bytesRead).toBe(1)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('should parse unsigned integer 24 from buffer (1-byte follows)', () => {
|
|
36
|
+
const buffer = hexToBytes('1818')
|
|
37
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
38
|
+
expect(result.value).toBe(24)
|
|
39
|
+
expect(result.bytesRead).toBe(2)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should parse unsigned integer 100 from buffer (1-byte follows)', () => {
|
|
43
|
+
const buffer = hexToBytes('1864')
|
|
44
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
45
|
+
expect(result.value).toBe(100)
|
|
46
|
+
expect(result.bytesRead).toBe(2)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should parse unsigned integer 1000 from buffer (2-byte follows)', () => {
|
|
50
|
+
const buffer = hexToBytes('1903e8')
|
|
51
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
52
|
+
expect(result.value).toBe(1000)
|
|
53
|
+
expect(result.bytesRead).toBe(3)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should parse unsigned integer 1000000 from buffer (4-byte follows)', () => {
|
|
57
|
+
const buffer = hexToBytes('1a000f4240')
|
|
58
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
59
|
+
expect(result.value).toBe(1000000)
|
|
60
|
+
expect(result.bytesRead).toBe(5)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should parse large unsigned integer from buffer (8-byte follows)', () => {
|
|
64
|
+
const buffer = hexToBytes('1b000000e8d4a51000')
|
|
65
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
66
|
+
expect(result.value).toBe(1000000000000)
|
|
67
|
+
expect(result.bytesRead).toBe(9)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should parse negative integer -1 from buffer', () => {
|
|
71
|
+
const buffer = hexToBytes('20')
|
|
72
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
73
|
+
expect(result.value).toBe(-1)
|
|
74
|
+
expect(result.bytesRead).toBe(1)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should parse negative integer -100 from buffer', () => {
|
|
78
|
+
const buffer = hexToBytes('3863')
|
|
79
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
80
|
+
expect(result.value).toBe(-100)
|
|
81
|
+
expect(result.bytesRead).toBe(2)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should parse BigInt unsigned from buffer', () => {
|
|
85
|
+
// 18446744073709551615 (max uint64)
|
|
86
|
+
const buffer = hexToBytes('1bffffffffffffffff')
|
|
87
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
88
|
+
expect(result.value).toBe(18446744073709551615n)
|
|
89
|
+
expect(result.bytesRead).toBe(9)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should parse BigInt negative from buffer', () => {
|
|
93
|
+
// -18446744073709551616
|
|
94
|
+
const buffer = hexToBytes('3bffffffffffffffff')
|
|
95
|
+
const result = parseIntegerFromBuffer(buffer, 0)
|
|
96
|
+
expect(result.value).toBe(-18446744073709551616n)
|
|
97
|
+
expect(result.bytesRead).toBe(9)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should parse integer at non-zero offset', () => {
|
|
101
|
+
// Buffer: [0x83, 0x01, 0x02, 0x03] - array(3) then integers 1, 2, 3
|
|
102
|
+
// Parsing integer at offset 1 should get value 1
|
|
103
|
+
const buffer = hexToBytes('83010203')
|
|
104
|
+
const result = parseIntegerFromBuffer(buffer, 1)
|
|
105
|
+
expect(result.value).toBe(1)
|
|
106
|
+
expect(result.bytesRead).toBe(1)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should parse integer at offset with trailing data', () => {
|
|
110
|
+
// Buffer with integer 100 at offset 2, followed by other data
|
|
111
|
+
const buffer = hexToBytes('0000186400')
|
|
112
|
+
const result = parseIntegerFromBuffer(buffer, 2)
|
|
113
|
+
expect(result.value).toBe(100)
|
|
114
|
+
expect(result.bytesRead).toBe(2)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should produce identical results to parseInteger for all test vectors', () => {
|
|
118
|
+
const testVectors = [
|
|
119
|
+
'00', '01', '0a', '17', '1818', '1864', '18ff',
|
|
120
|
+
'190100', '1903e8', '19ffff',
|
|
121
|
+
'1a00010000', '1a000f4240', '1affffffff',
|
|
122
|
+
'1b0000000100000000', '1b000000e8d4a51000', '1bffffffffffffffff',
|
|
123
|
+
'20', '37', '3863', '38ff',
|
|
124
|
+
'390100', '3903e7', '39ffff',
|
|
125
|
+
'3a00010000', '3affffffff',
|
|
126
|
+
'3b0000000100000000', '3bffffffffffffffff',
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
for (const hex of testVectors) {
|
|
130
|
+
const hexResult = parseInteger(hex)
|
|
131
|
+
const buffer = hexToBytes(hex)
|
|
132
|
+
const bufferResult = parseIntegerFromBuffer(buffer, 0)
|
|
133
|
+
|
|
134
|
+
expect(bufferResult.value).toEqual(hexResult.value)
|
|
135
|
+
expect(bufferResult.bytesRead).toEqual(hexResult.bytesRead)
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('should validate canonical encoding when options are passed', () => {
|
|
140
|
+
// Non-canonical: value 0 encoded with 1-byte AI
|
|
141
|
+
const buffer = hexToBytes('1800')
|
|
142
|
+
expect(() => parseIntegerFromBuffer(buffer, 0, { validateCanonical: true }))
|
|
143
|
+
.toThrow(/[Nn]on-canonical/)
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Buffer-native parsing - parseFromBuffer (float/simple)', () => {
|
|
148
|
+
const { parseFromBuffer } = useCborFloat()
|
|
149
|
+
|
|
150
|
+
it('should export parseFromBuffer from useCborFloat', () => {
|
|
151
|
+
expect(typeof parseFromBuffer).toBe('function')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should parse false from buffer', () => {
|
|
155
|
+
const buffer = hexToBytes('f4')
|
|
156
|
+
const result = parseFromBuffer(buffer, 0)
|
|
157
|
+
expect(result.value).toBe(false)
|
|
158
|
+
expect(result.bytesRead).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('should parse true from buffer', () => {
|
|
162
|
+
const buffer = hexToBytes('f5')
|
|
163
|
+
const result = parseFromBuffer(buffer, 0)
|
|
164
|
+
expect(result.value).toBe(true)
|
|
165
|
+
expect(result.bytesRead).toBe(1)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should parse null from buffer', () => {
|
|
169
|
+
const buffer = hexToBytes('f6')
|
|
170
|
+
const result = parseFromBuffer(buffer, 0)
|
|
171
|
+
expect(result.value).toBe(null)
|
|
172
|
+
expect(result.bytesRead).toBe(1)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('should parse undefined from buffer', () => {
|
|
176
|
+
const buffer = hexToBytes('f7')
|
|
177
|
+
const result = parseFromBuffer(buffer, 0)
|
|
178
|
+
expect(result.value).toBe(undefined)
|
|
179
|
+
expect(result.bytesRead).toBe(1)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('should parse float16 0.0 from buffer', () => {
|
|
183
|
+
const buffer = hexToBytes('f90000')
|
|
184
|
+
const result = parseFromBuffer(buffer, 0)
|
|
185
|
+
expect(result.value).toBe(0.0)
|
|
186
|
+
expect(result.bytesRead).toBe(3)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('should parse float16 1.0 from buffer', () => {
|
|
190
|
+
const buffer = hexToBytes('f93c00')
|
|
191
|
+
const result = parseFromBuffer(buffer, 0)
|
|
192
|
+
expect(result.value).toBe(1.0)
|
|
193
|
+
expect(result.bytesRead).toBe(3)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should parse float32 from buffer', () => {
|
|
197
|
+
const buffer = hexToBytes('fa47c35000')
|
|
198
|
+
const result = parseFromBuffer(buffer, 0)
|
|
199
|
+
expect(result.value).toBe(100000.0)
|
|
200
|
+
expect(result.bytesRead).toBe(5)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should parse float64 from buffer', () => {
|
|
204
|
+
const buffer = hexToBytes('fb3ff199999999999a')
|
|
205
|
+
const result = parseFromBuffer(buffer, 0)
|
|
206
|
+
expect(result.value).toBeCloseTo(1.1, 10)
|
|
207
|
+
expect(result.bytesRead).toBe(9)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should parse Infinity from buffer', () => {
|
|
211
|
+
const buffer = hexToBytes('f97c00')
|
|
212
|
+
const result = parseFromBuffer(buffer, 0)
|
|
213
|
+
expect(result.value).toBe(Infinity)
|
|
214
|
+
expect(result.bytesRead).toBe(3)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should parse NaN from buffer', () => {
|
|
218
|
+
const buffer = hexToBytes('f97e00')
|
|
219
|
+
const result = parseFromBuffer(buffer, 0)
|
|
220
|
+
expect(result.value).toBeNaN()
|
|
221
|
+
expect(result.bytesRead).toBe(3)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('should parse at non-zero offset', () => {
|
|
225
|
+
const buffer = hexToBytes('00f5')
|
|
226
|
+
const result = parseFromBuffer(buffer, 1)
|
|
227
|
+
expect(result.value).toBe(true)
|
|
228
|
+
expect(result.bytesRead).toBe(1)
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
describe('Buffer-native parsing - parseTagFromBuffer', () => {
|
|
233
|
+
const { parseTagFromBuffer } = useCborTag()
|
|
234
|
+
|
|
235
|
+
it('should export parseTagFromBuffer from useCborTag', () => {
|
|
236
|
+
expect(typeof parseTagFromBuffer).toBe('function')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should parse tag 1 (epoch time) from buffer', () => {
|
|
240
|
+
// c11a514b67b0 = tag(1, 1363896240)
|
|
241
|
+
const buffer = hexToBytes('c11a514b67b0')
|
|
242
|
+
const result = parseTagFromBuffer(buffer, 0)
|
|
243
|
+
expect(result.value).toEqual({
|
|
244
|
+
tag: 1,
|
|
245
|
+
value: 1363896240
|
|
246
|
+
})
|
|
247
|
+
expect(result.bytesRead).toBe(6)
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it('should parse self-describe tag 55799 from buffer', () => {
|
|
251
|
+
// d9d9f7 01 = tag(55799, 1)
|
|
252
|
+
const buffer = hexToBytes('d9d9f701')
|
|
253
|
+
const result = parseTagFromBuffer(buffer, 0)
|
|
254
|
+
expect(result.value).toEqual({
|
|
255
|
+
tag: 55799,
|
|
256
|
+
value: 1
|
|
257
|
+
})
|
|
258
|
+
expect(result.bytesRead).toBe(4)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('should parse tag at non-zero offset', () => {
|
|
262
|
+
// Prefix with 0x01, then tag 1 with value 0
|
|
263
|
+
const buffer = hexToBytes('01c100')
|
|
264
|
+
const result = parseTagFromBuffer(buffer, 1)
|
|
265
|
+
expect(result.value).toEqual({
|
|
266
|
+
tag: 1,
|
|
267
|
+
value: 0
|
|
268
|
+
})
|
|
269
|
+
expect(result.bytesRead).toBe(2)
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
describe('Buffer-native parsing - collection integration', () => {
|
|
274
|
+
const { parseArray, parseMap } = useCborCollection()
|
|
275
|
+
|
|
276
|
+
it('should parse array of integers without O(N^2) hex conversion', () => {
|
|
277
|
+
// 83 01 02 03 = [1, 2, 3]
|
|
278
|
+
const result = parseArray('83010203')
|
|
279
|
+
expect(result.value).toEqual([1, 2, 3])
|
|
280
|
+
expect(result.bytesRead).toBe(4)
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
it('should parse array with mixed types including floats', () => {
|
|
284
|
+
// 83 01 f5 f6 = [1, true, null]
|
|
285
|
+
const result = parseArray('8301f5f6')
|
|
286
|
+
expect(result.value).toEqual([1, true, null])
|
|
287
|
+
expect(result.bytesRead).toBe(4)
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
it('should parse nested array with tags', () => {
|
|
291
|
+
// 82 c1 1a514b67b0 01 = [tag(1, 1363896240), 1]
|
|
292
|
+
const result = parseArray('82c11a514b67b001')
|
|
293
|
+
expect(result.value).toHaveLength(2)
|
|
294
|
+
expect((result.value as any[])[0]).toEqual({
|
|
295
|
+
tag: 1,
|
|
296
|
+
value: 1363896240
|
|
297
|
+
})
|
|
298
|
+
expect((result.value as any[])[1]).toBe(1)
|
|
299
|
+
expect(result.bytesRead).toBe(8)
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it('should parse map with integer keys', () => {
|
|
303
|
+
// a2 01 02 03 04 = {1: 2, 3: 4}
|
|
304
|
+
const result = parseMap('a201020304')
|
|
305
|
+
const map = result.value as Map<any, any>
|
|
306
|
+
expect(map.get(1)).toBe(2)
|
|
307
|
+
expect(map.get(3)).toBe(4)
|
|
308
|
+
expect(result.bytesRead).toBe(5)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should parse map with float value', () => {
|
|
312
|
+
// a1 01 f93c00 = {1: 1.0}
|
|
313
|
+
const result = parseMap('a101f93c00')
|
|
314
|
+
const map = result.value as Map<any, any>
|
|
315
|
+
expect(map.get(1)).toBe(1.0)
|
|
316
|
+
expect(result.bytesRead).toBe(5)
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('should parse large array correctly', () => {
|
|
320
|
+
// Build array of 100 integers 0-99
|
|
321
|
+
let hex = '9864' // array(100)... but that needs proper length encoding
|
|
322
|
+
// Actually: 0x98 = array with 1-byte length, 0x64 = 100
|
|
323
|
+
hex = '9864'
|
|
324
|
+
for (let i = 0; i < 100; i++) {
|
|
325
|
+
if (i < 24) {
|
|
326
|
+
hex += i.toString(16).padStart(2, '0')
|
|
327
|
+
} else {
|
|
328
|
+
hex += '18' + i.toString(16).padStart(2, '0')
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const result = parseArray(hex)
|
|
332
|
+
const arr = result.value as number[]
|
|
333
|
+
expect(arr).toHaveLength(100)
|
|
334
|
+
for (let i = 0; i < 100; i++) {
|
|
335
|
+
expect(arr[i]).toBe(i)
|
|
336
|
+
}
|
|
337
|
+
})
|
|
338
|
+
})
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { describe, it, expect } from 'vitest'
|
|
15
15
|
import { useCborParser } from '../composables/useCborParser'
|
|
16
16
|
import { useCborCollection } from '../composables/useCborCollection'
|
|
17
|
+
import type { CborMap } from '../types'
|
|
17
18
|
|
|
18
19
|
describe('CBOR Map Duplicate Key Detection', () => {
|
|
19
20
|
describe('String Keys - Duplicate Detection', () => {
|
|
@@ -154,7 +155,7 @@ describe('CBOR Map Duplicate Key Detection', () => {
|
|
|
154
155
|
|
|
155
156
|
const result = parseMap(uniqueBytes, { dupMapKeyMode: 'reject' })
|
|
156
157
|
// Byte strings as keys become comma-separated strings in JS
|
|
157
|
-
expect(result.value.size).toBe(3)
|
|
158
|
+
expect((result.value as CborMap).size).toBe(3)
|
|
158
159
|
})
|
|
159
160
|
|
|
160
161
|
it('should reject duplicate empty byte strings', () => {
|
|
@@ -169,17 +170,16 @@ describe('CBOR Map Duplicate Key Detection', () => {
|
|
|
169
170
|
})
|
|
170
171
|
|
|
171
172
|
describe('Mixed Type Keys - Duplicate Detection', () => {
|
|
172
|
-
it('should
|
|
173
|
+
it('should allow distinct CBOR keys even if they stringify similarly', () => {
|
|
173
174
|
const { parseMap } = useCborCollection()
|
|
174
175
|
|
|
175
176
|
// Map: {1: 10, "1": 20} - different types in CBOR (int vs string)
|
|
176
177
|
// a2 (map of 2) + 01 (key:1 integer) + 0a (val:10) + 6131 (key:"1" string) + 14 (val:20)
|
|
177
|
-
//
|
|
178
|
-
// This is correctly rejected to prevent unexpected behavior in Cardano applications
|
|
178
|
+
// Keys are distinct at the CBOR level and should not be treated as duplicates
|
|
179
179
|
const mixedTypes = 'a2010a613114'
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
const result = parseMap(mixedTypes, { dupMapKeyMode: 'reject' })
|
|
182
|
+
expect((result.value as CborMap).size).toBe(2)
|
|
183
183
|
})
|
|
184
184
|
|
|
185
185
|
it('should detect duplicates when both are strings', () => {
|
|
@@ -333,7 +333,7 @@ describe('CBOR Map Duplicate Key Detection', () => {
|
|
|
333
333
|
limits: { maxMapSize: 100 }
|
|
334
334
|
})
|
|
335
335
|
|
|
336
|
-
expect(result.value.size).toBe(30)
|
|
336
|
+
expect((result.value as CborMap).size).toBe(30)
|
|
337
337
|
})
|
|
338
338
|
})
|
|
339
339
|
|
|
@@ -377,6 +377,96 @@ describe('CBOR Map Duplicate Key Detection', () => {
|
|
|
377
377
|
})
|
|
378
378
|
})
|
|
379
379
|
|
|
380
|
+
describe('Semantic Duplicate Detection (RFC 8949 Section 5.6)', () => {
|
|
381
|
+
it('should reject integer 1 encoded as 0x01 and 0x1801 as duplicate keys', () => {
|
|
382
|
+
const { parseMap } = useCborCollection()
|
|
383
|
+
|
|
384
|
+
// Map with 2 entries, key 1 (0x01) -> value 10 (0x0a), key 1 (0x1801) -> value 20 (0x14)
|
|
385
|
+
// 0x01 encodes integer 1 directly (AI=1)
|
|
386
|
+
// 0x1801 encodes integer 1 with 1-byte payload (AI=24, payload=0x01)
|
|
387
|
+
// Both represent semantic value 1 -- must be detected as duplicate
|
|
388
|
+
// a2 = map(2), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20)
|
|
389
|
+
const duplicateHex = 'a2010a180114'
|
|
390
|
+
|
|
391
|
+
expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
|
|
392
|
+
.toThrow(/duplicate/i)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should reject integer 1 encoded as 0x01 and 0x190001 as duplicate keys', () => {
|
|
396
|
+
const { parseMap } = useCborCollection()
|
|
397
|
+
|
|
398
|
+
// 0x01 encodes integer 1 directly (AI=1)
|
|
399
|
+
// 0x190001 encodes integer 1 with 2-byte payload (AI=25, payload=0x0001)
|
|
400
|
+
// Both represent semantic value 1
|
|
401
|
+
// a2 = map(2), 01 = int(1), 0a = int(10), 190001 = int(1), 14 = int(20)
|
|
402
|
+
const duplicateHex = 'a2010a19000114'
|
|
403
|
+
|
|
404
|
+
expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
|
|
405
|
+
.toThrow(/duplicate/i)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should reject integer 1 encoded as 0x1801 and 0x190001 as duplicate keys', () => {
|
|
409
|
+
const { parseMap } = useCborCollection()
|
|
410
|
+
|
|
411
|
+
// 0x1801 encodes integer 1 with 1-byte payload (AI=24, payload=0x01)
|
|
412
|
+
// 0x190001 encodes integer 1 with 2-byte payload (AI=25, payload=0x0001)
|
|
413
|
+
// Both represent semantic value 1
|
|
414
|
+
// a2 = map(2), 1801 = int(1), 0a = int(10), 190001 = int(1), 14 = int(20)
|
|
415
|
+
const duplicateHex = 'a218010a19000114'
|
|
416
|
+
|
|
417
|
+
expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
|
|
418
|
+
.toThrow(/duplicate/i)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it('should reject integer 0 encoded with different byte widths as duplicate keys', () => {
|
|
422
|
+
const { parseMap } = useCborCollection()
|
|
423
|
+
|
|
424
|
+
// 0x00 encodes integer 0 directly (AI=0)
|
|
425
|
+
// 0x1800 encodes integer 0 with 1-byte payload (AI=24, payload=0x00)
|
|
426
|
+
// Both represent semantic value 0
|
|
427
|
+
// a2 = map(2), 00 = int(0), 01 = int(1), 1800 = int(0), 02 = int(2)
|
|
428
|
+
const duplicateHex = 'a200011800 02'
|
|
429
|
+
|
|
430
|
+
expect(() => parseMap(duplicateHex, { dupMapKeyMode: 'reject' }))
|
|
431
|
+
.toThrow(/duplicate/i)
|
|
432
|
+
})
|
|
433
|
+
|
|
434
|
+
it('should still allow semantically different integer keys', () => {
|
|
435
|
+
const { parseMap } = useCborCollection()
|
|
436
|
+
|
|
437
|
+
// Map: {1: 10, 2: 20} - different semantic values, even if both use non-canonical encoding
|
|
438
|
+
// a2 = map(2), 1801 = int(1), 0a = int(10), 1802 = int(2), 14 = int(20)
|
|
439
|
+
const uniqueHex = 'a218010a180214'
|
|
440
|
+
|
|
441
|
+
const result = parseMap(uniqueHex, { dupMapKeyMode: 'reject' })
|
|
442
|
+
expect((result.value as CborMap).size).toBe(2)
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it('should detect semantic duplicates via parseWithSourceMap path', () => {
|
|
446
|
+
const { parseWithSourceMap } = useCborParser()
|
|
447
|
+
|
|
448
|
+
// Map with key 1 (0x01) and key 1 (0x1801) -- semantic duplicate
|
|
449
|
+
// a2 = map(2), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20)
|
|
450
|
+
const duplicateHex = 'a2010a180114'
|
|
451
|
+
|
|
452
|
+
expect(() => parseWithSourceMap(duplicateHex, { dupMapKeyMode: 'reject' }))
|
|
453
|
+
.toThrow(/duplicate/i)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('should detect semantic duplicates in indefinite-length maps', () => {
|
|
457
|
+
const { parseMap } = useCborCollection()
|
|
458
|
+
|
|
459
|
+
// Indefinite map: {_ 1: 10, 1(non-canonical): 20 }
|
|
460
|
+
// bf = map(indefinite), 01 = int(1), 0a = int(10), 1801 = int(1), 14 = int(20), ff = break
|
|
461
|
+
const duplicateHex = 'bf010a180114ff'
|
|
462
|
+
|
|
463
|
+
expect(() => parseMap(duplicateHex, {
|
|
464
|
+
dupMapKeyMode: 'reject',
|
|
465
|
+
allowIndefinite: true
|
|
466
|
+
})).toThrow(/duplicate/i)
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
|
|
380
470
|
describe('Default Behavior', () => {
|
|
381
471
|
it('should NOT reject duplicates by default (lenient mode)', () => {
|
|
382
472
|
const { parseMap } = useCborCollection()
|