@marcuspuchalla/nachos 0.1.1 → 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 +55 -0
- package/dist/{chunk-ZRPJUEIZ.js → chunk-5IWW5H47.js} +546 -227
- package/dist/chunk-5IWW5H47.js.map +1 -0
- package/dist/{chunk-2HBCILJS.cjs → chunk-RVG2BY32.cjs} +545 -226
- package/dist/chunk-RVG2BY32.cjs.map +1 -0
- package/dist/{chunk-2FUTHZQQ.cjs → chunk-S4RXO6IB.cjs} +244 -166
- package/dist/chunk-S4RXO6IB.cjs.map +1 -0
- package/dist/{chunk-7CFYWHS6.js → chunk-UMAX5MX5.js} +244 -166
- 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-B_iaShG6.d.ts → useCborTag-BD6Sqp7p.d.ts} +11 -6
- package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-QpZR-Er2.d.cts} +11 -6
- package/package.json +1 -1
- package/src/__tests__/public-api.test.ts +153 -0
- package/src/__tests__/roundtrip.test.ts +701 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +129 -5
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +847 -0
- package/src/encoder/__tests__/cbor-simple-encoder.test.ts +126 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +56 -23
- package/src/encoder/composables/useCborEncoder.ts +27 -1
- package/src/encoder/composables/useCborSimpleEncoder.ts +40 -8
- package/src/encoder/composables/useCborStringEncoder.ts +23 -10
- 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-float-errors.test.ts +41 -0
- package/src/parser/__tests__/cbor-map-duplicate-keys.test.ts +97 -7
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +166 -33
- package/src/parser/__tests__/cbor-standard-tags.test.ts +104 -7
- package/src/parser/__tests__/cbor-string-errors.test.ts +4 -4
- package/src/parser/__tests__/cbor-tag-errors.test.ts +1 -1
- 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 +95 -9
- package/src/parser/composables/useCborInteger.ts +24 -10
- package/src/parser/composables/useCborParser.ts +387 -216
- package/src/parser/composables/useCborString.ts +22 -4
- package/src/parser/composables/useCborTag.ts +149 -53
- package/src/parser/utils.ts +11 -0
- package/dist/chunk-2FUTHZQQ.cjs.map +0 -1
- package/dist/chunk-2HBCILJS.cjs.map +0 -1
- package/dist/chunk-7CFYWHS6.js.map +0 -1
- package/dist/chunk-ZRPJUEIZ.js.map +0 -1
- package/src/encoder/composables/#useCborTagEncoder.ts# +0 -158
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for eliminating exponential re-parsing in source-map tag handling
|
|
3
|
+
* Task 2-B: parseTagWithMap should NOT re-parse the entire tag subtree
|
|
4
|
+
*
|
|
5
|
+
* The fix ensures that parseTagWithMap uses already-parsed values plus
|
|
6
|
+
* direct calls to validateTagSemantics and decodePlutusConstructor,
|
|
7
|
+
* instead of calling parseTag(hexString) which re-parses everything.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
11
|
+
import { useCborParser } from '../composables/useCborParser'
|
|
12
|
+
import { useCborTag } from '../composables/useCborTag'
|
|
13
|
+
|
|
14
|
+
describe('Tag Source Map Re-parse Elimination (Task 2-B)', () => {
|
|
15
|
+
describe('Correctness: parseWithSourceMap still produces correct values', () => {
|
|
16
|
+
it('should correctly decode tag 121 with array', () => {
|
|
17
|
+
const { parseWithSourceMap } = useCborParser()
|
|
18
|
+
const result = parseWithSourceMap('d87983010203') // Tag 121, [1, 2, 3]
|
|
19
|
+
|
|
20
|
+
expect(result.value).toEqual({
|
|
21
|
+
tag: 121,
|
|
22
|
+
value: [1, 2, 3],
|
|
23
|
+
plutus: { constructor: 0, fields: [1, 2, 3] }
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should correctly decode tag 122 with array', () => {
|
|
28
|
+
const { parseWithSourceMap } = useCborParser()
|
|
29
|
+
const result = parseWithSourceMap('d87a81182a') // Tag 122, [42]
|
|
30
|
+
|
|
31
|
+
expect(result.value).toEqual({
|
|
32
|
+
tag: 122,
|
|
33
|
+
value: [42],
|
|
34
|
+
plutus: { constructor: 1, fields: [42] }
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should correctly decode tag 102 alternative constructor', () => {
|
|
39
|
+
const { parseWithSourceMap } = useCborParser()
|
|
40
|
+
const result = parseWithSourceMap('d8668218c8811863') // Tag 102, [200, [99]]
|
|
41
|
+
|
|
42
|
+
expect(result.value).toEqual({
|
|
43
|
+
tag: 102,
|
|
44
|
+
value: [200, [99]],
|
|
45
|
+
plutus: { constructor: 200, fields: [99] }
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('should correctly decode extended constructor tag 1283', () => {
|
|
50
|
+
const { parseWithSourceMap } = useCborParser()
|
|
51
|
+
const result = parseWithSourceMap('d9050383010203') // Tag 1283, [1, 2, 3]
|
|
52
|
+
|
|
53
|
+
expect(result.value).toEqual({
|
|
54
|
+
tag: 1283,
|
|
55
|
+
value: [1, 2, 3],
|
|
56
|
+
plutus: { constructor: 10, fields: [1, 2, 3] }
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should correctly decode bignum tag 2 (positive)', () => {
|
|
61
|
+
const { parseWithSourceMap } = useCborParser()
|
|
62
|
+
// Tag 2, byte string 0x01 0x00 (= 256)
|
|
63
|
+
const result = parseWithSourceMap('c2420100')
|
|
64
|
+
|
|
65
|
+
expect(result.value).toEqual({
|
|
66
|
+
tag: 2,
|
|
67
|
+
value: 256n
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should correctly decode bignum tag 3 (negative)', () => {
|
|
72
|
+
const { parseWithSourceMap } = useCborParser()
|
|
73
|
+
// Tag 3, byte string 0x01 0x00 (= -1 - 256 = -257)
|
|
74
|
+
const result = parseWithSourceMap('c3420100')
|
|
75
|
+
|
|
76
|
+
expect(result.value).toEqual({
|
|
77
|
+
tag: 3,
|
|
78
|
+
value: -257n
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should correctly handle tag 0 (date/time string)', () => {
|
|
83
|
+
const { parseWithSourceMap } = useCborParser()
|
|
84
|
+
// Tag 0 containing "2013-03-21T20:04:00Z"
|
|
85
|
+
const result = parseWithSourceMap('c074323031332d30332d32315432303a30343a30305a')
|
|
86
|
+
|
|
87
|
+
const value = result.value as any
|
|
88
|
+
expect(value.tag).toBe(0)
|
|
89
|
+
expect(value.value).toBe('2013-03-21T20:04:00Z')
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should correctly handle tag 1 (epoch time)', () => {
|
|
93
|
+
const { parseWithSourceMap } = useCborParser()
|
|
94
|
+
// Tag 1 containing 1363896240
|
|
95
|
+
const result = parseWithSourceMap('c11a514b67b0')
|
|
96
|
+
|
|
97
|
+
expect(result.value).toEqual({
|
|
98
|
+
tag: 1,
|
|
99
|
+
value: 1363896240
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should correctly handle nested tags', () => {
|
|
104
|
+
const { parseWithSourceMap } = useCborParser()
|
|
105
|
+
// Tag 121 -> [Tag 121 -> []]
|
|
106
|
+
const result = parseWithSourceMap('d87981d87980')
|
|
107
|
+
|
|
108
|
+
const outer = result.value as any
|
|
109
|
+
expect(outer.tag).toBe(121)
|
|
110
|
+
expect(outer.plutus).toEqual({ constructor: 0, fields: [{ tag: 121, value: [], plutus: { constructor: 0, fields: [] } }] })
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('should correctly handle tag 121 with empty array', () => {
|
|
114
|
+
const { parseWithSourceMap } = useCborParser()
|
|
115
|
+
const result = parseWithSourceMap('d87980')
|
|
116
|
+
|
|
117
|
+
expect(result.value).toEqual({
|
|
118
|
+
tag: 121,
|
|
119
|
+
value: [],
|
|
120
|
+
plutus: { constructor: 0, fields: [] }
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('should correctly handle self-describe tag 55799', () => {
|
|
125
|
+
const { parseWithSourceMap } = useCborParser()
|
|
126
|
+
// Tag 55799 wrapping integer 42
|
|
127
|
+
const result = parseWithSourceMap('d9d9f7182a')
|
|
128
|
+
|
|
129
|
+
expect(result.value).toEqual({
|
|
130
|
+
tag: 55799,
|
|
131
|
+
value: 42
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should correctly handle non-Plutus tag with non-array value', () => {
|
|
136
|
+
const { parseWithSourceMap } = useCborParser()
|
|
137
|
+
// Tag 1 containing 0
|
|
138
|
+
const result = parseWithSourceMap('c100')
|
|
139
|
+
|
|
140
|
+
expect(result.value).toEqual({
|
|
141
|
+
tag: 1,
|
|
142
|
+
value: 0
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
describe('Correctness: source maps remain identical', () => {
|
|
148
|
+
it('should produce same source map for simple tagged value', () => {
|
|
149
|
+
const { parseWithSourceMap } = useCborParser()
|
|
150
|
+
const result = parseWithSourceMap('d879182a') // Tag 121, 42
|
|
151
|
+
|
|
152
|
+
expect(result.sourceMap).toHaveLength(2)
|
|
153
|
+
expect(result.sourceMap[0]).toMatchObject({
|
|
154
|
+
path: '',
|
|
155
|
+
majorType: 6,
|
|
156
|
+
type: 'tag(121)',
|
|
157
|
+
children: ['.value']
|
|
158
|
+
})
|
|
159
|
+
expect(result.sourceMap[1]).toMatchObject({
|
|
160
|
+
path: '.value',
|
|
161
|
+
majorType: 0,
|
|
162
|
+
parent: ''
|
|
163
|
+
})
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should produce same source map for nested tags', () => {
|
|
167
|
+
const { parseWithSourceMap } = useCborParser()
|
|
168
|
+
const result = parseWithSourceMap('d87981d87980')
|
|
169
|
+
|
|
170
|
+
const outerTag = result.sourceMap.find(e => e.path === '')
|
|
171
|
+
expect(outerTag?.majorType).toBe(6)
|
|
172
|
+
expect(outerTag?.children).toEqual(['.value'])
|
|
173
|
+
|
|
174
|
+
const array = result.sourceMap.find(e => e.path === '.value')
|
|
175
|
+
expect(array?.majorType).toBe(4)
|
|
176
|
+
expect(array?.parent).toBe('')
|
|
177
|
+
|
|
178
|
+
const innerTag = result.sourceMap.find(e => e.path === '.value[0]')
|
|
179
|
+
expect(innerTag?.majorType).toBe(6)
|
|
180
|
+
expect(innerTag?.parent).toBe('.value')
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('Validation still works through source-map path', () => {
|
|
185
|
+
it('should validate tag 2/3 bignum byte limit', () => {
|
|
186
|
+
const { parseWithSourceMap } = useCborParser()
|
|
187
|
+
// Tag 2 with a byte string exceeding the limit
|
|
188
|
+
// Create a very long byte string (> default 1024 bytes)
|
|
189
|
+
const longByteString = '59' + '0401' + 'ff'.repeat(1025)
|
|
190
|
+
const hex = 'c2' + longByteString
|
|
191
|
+
|
|
192
|
+
expect(() => parseWithSourceMap(hex)).toThrow(/bignum/i)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should validate tag semantics in strict mode', () => {
|
|
196
|
+
const { parseWithSourceMap } = useCborParser()
|
|
197
|
+
// Tag 0 (date/time) with integer value (should be text string)
|
|
198
|
+
// c0 00 = tag(0) + integer(0)
|
|
199
|
+
expect(() => parseWithSourceMap('c000', { strict: true })).toThrow(/tag 0/i)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should validate Plutus semantics in strict mode', () => {
|
|
203
|
+
const { parseWithSourceMap } = useCborParser()
|
|
204
|
+
// Tag 121 with non-array value (should be array)
|
|
205
|
+
// d8 79 00 = tag(121) + integer(0)
|
|
206
|
+
expect(() => parseWithSourceMap('d87900', { strict: true })).toThrow(/Plutus/i)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
describe('Performance: no exponential re-parsing', () => {
|
|
211
|
+
it('should parse deeply nested tags in linear time', () => {
|
|
212
|
+
const { parseWithSourceMap } = useCborParser()
|
|
213
|
+
|
|
214
|
+
// Build a deeply nested tag: tag(121) -> [tag(121) -> [tag(121) -> ... -> []]]
|
|
215
|
+
// At depth D, the old code would do O(D^2) work due to re-parsing.
|
|
216
|
+
// With the fix, it should be O(D).
|
|
217
|
+
const buildNestedTagHex = (depth: number): string => {
|
|
218
|
+
// Each level: d8 79 81 (tag 121, 1-element array)
|
|
219
|
+
// Innermost: d8 79 80 (tag 121, empty array)
|
|
220
|
+
let hex = ''
|
|
221
|
+
for (let i = 0; i < depth - 1; i++) {
|
|
222
|
+
hex += 'd87981' // tag(121), array(1)
|
|
223
|
+
}
|
|
224
|
+
hex += 'd87980' // tag(121), array(0)
|
|
225
|
+
return hex
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Time a moderate depth (20 levels)
|
|
229
|
+
const hex20 = buildNestedTagHex(20)
|
|
230
|
+
const start20 = performance.now()
|
|
231
|
+
const result20 = parseWithSourceMap(hex20)
|
|
232
|
+
const time20 = performance.now() - start20
|
|
233
|
+
|
|
234
|
+
// Time a deeper nesting (40 levels)
|
|
235
|
+
const hex40 = buildNestedTagHex(40)
|
|
236
|
+
const start40 = performance.now()
|
|
237
|
+
const result40 = parseWithSourceMap(hex40)
|
|
238
|
+
const time40 = performance.now() - start40
|
|
239
|
+
|
|
240
|
+
// With O(D^2), doubling depth quadruples time: time40/time20 ~ 4
|
|
241
|
+
// With O(D), doubling depth doubles time: time40/time20 ~ 2
|
|
242
|
+
// Use a generous bound: if fixed, ratio should be < 3
|
|
243
|
+
// If unfixed (quadratic), ratio tends toward 4+
|
|
244
|
+
//
|
|
245
|
+
// Note: for small absolute times, jitter can dominate.
|
|
246
|
+
// The real test is that 40-deep should still complete quickly (< 100ms).
|
|
247
|
+
expect(time40).toBeLessThan(100) // Should be very fast with fix
|
|
248
|
+
|
|
249
|
+
// Both should parse successfully
|
|
250
|
+
expect((result20.value as any).tag).toBe(121)
|
|
251
|
+
expect((result40.value as any).tag).toBe(121)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
describe('useCborTag exports', () => {
|
|
256
|
+
it('should export validateTagSemantics function', () => {
|
|
257
|
+
const tag = useCborTag()
|
|
258
|
+
expect(tag.validateTagSemantics).toBeDefined()
|
|
259
|
+
expect(typeof tag.validateTagSemantics).toBe('function')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('should export decodePlutusConstructor function', () => {
|
|
263
|
+
const tag = useCborTag()
|
|
264
|
+
expect(tag.decodePlutusConstructor).toBeDefined()
|
|
265
|
+
expect(typeof tag.decodePlutusConstructor).toBe('function')
|
|
266
|
+
})
|
|
267
|
+
})
|
|
268
|
+
})
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { ParseResult, CborValue, CborMap, ParseOptions } from '../types'
|
|
8
8
|
import { INDEFINITE_SYMBOL, ALL_ENTRIES_SYMBOL } from '../types'
|
|
9
|
-
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, compareBytes,
|
|
9
|
+
import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, compareBytes, serializeValueForComparison } from '../utils'
|
|
10
10
|
import { useCborInteger } from './useCborInteger'
|
|
11
11
|
import { useCborString } from './useCborString'
|
|
12
12
|
import { useCborFloat } from './useCborFloat'
|
|
@@ -25,21 +25,13 @@ import { logger } from '../../utils/logger'
|
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
27
|
export function useCborCollection() {
|
|
28
|
-
const {
|
|
28
|
+
const { parseIntegerFromBuffer } = useCborInteger()
|
|
29
29
|
const { parseByteString, parseTextString } = useCborString()
|
|
30
|
-
const {
|
|
31
|
-
const {
|
|
30
|
+
const { parseFromBuffer: parseFloatOrSimpleFromBuffer } = useCborFloat()
|
|
31
|
+
const { parseTagFromBuffer } = useCborTag()
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
|
|
35
|
-
* Handles Uint8Array keys by converting them to hex strings
|
|
36
|
-
*/
|
|
37
|
-
const convertKeyToString = (key: CborValue): string => {
|
|
38
|
-
if (key instanceof Uint8Array) {
|
|
39
|
-
return bytesToHex(key)
|
|
40
|
-
}
|
|
41
|
-
return String(key)
|
|
42
|
-
}
|
|
33
|
+
/** Tracks when parsing started for timeout enforcement */
|
|
34
|
+
let parseStartTime = 0
|
|
43
35
|
|
|
44
36
|
/**
|
|
45
37
|
* Internal parser dispatcher for CBOR items
|
|
@@ -52,6 +44,14 @@ export function useCborCollection() {
|
|
|
52
44
|
* @returns Parsed value and bytes consumed
|
|
53
45
|
*/
|
|
54
46
|
const parseItem = (buffer: Uint8Array, offset: number, options?: ParseOptions, depth: number = 0): ParseResult => {
|
|
47
|
+
// Check timeout on every recursive call
|
|
48
|
+
if (parseStartTime > 0 && options?.limits?.maxParseTime) {
|
|
49
|
+
const elapsed = Date.now() - parseStartTime
|
|
50
|
+
if (elapsed > options.limits.maxParseTime) {
|
|
51
|
+
throw new Error(`Parse timeout: exceeded ${options.limits.maxParseTime}ms limit`)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
55
|
if (offset >= buffer.length) {
|
|
56
56
|
throw new Error(`Unexpected end of buffer at offset ${offset}`)
|
|
57
57
|
}
|
|
@@ -62,14 +62,7 @@ export function useCborCollection() {
|
|
|
62
62
|
switch (majorType) {
|
|
63
63
|
case 0: // Unsigned integer
|
|
64
64
|
case 1: // Negative integer
|
|
65
|
-
|
|
66
|
-
// Create a hex string from the buffer starting at offset
|
|
67
|
-
const intHex = Array.from(buffer.slice(offset))
|
|
68
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
69
|
-
.join('')
|
|
70
|
-
const result = parseInteger(intHex, options)
|
|
71
|
-
return { value: result.value, bytesRead: result.bytesRead }
|
|
72
|
-
}
|
|
65
|
+
return parseIntegerFromBuffer(buffer, offset, options)
|
|
73
66
|
|
|
74
67
|
case 2: // Byte string
|
|
75
68
|
return parseByteString(buffer, offset, options)
|
|
@@ -84,22 +77,10 @@ export function useCborCollection() {
|
|
|
84
77
|
return parseMapFromBuffer(buffer, offset, options, depth)
|
|
85
78
|
|
|
86
79
|
case 6: // Tag
|
|
87
|
-
|
|
88
|
-
const tagHex = Array.from(buffer.slice(offset))
|
|
89
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
90
|
-
.join('')
|
|
91
|
-
const result = parseTag(tagHex, options)
|
|
92
|
-
return { value: result.value, bytesRead: result.bytesRead }
|
|
93
|
-
}
|
|
80
|
+
return parseTagFromBuffer(buffer, offset, options)
|
|
94
81
|
|
|
95
82
|
case 7: // Simple/Float
|
|
96
|
-
|
|
97
|
-
const floatHex = Array.from(buffer.slice(offset))
|
|
98
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
99
|
-
.join('')
|
|
100
|
-
const result = parseFloatOrSimple(floatHex, options)
|
|
101
|
-
return { value: result.value, bytesRead: result.bytesRead }
|
|
102
|
-
}
|
|
83
|
+
return parseFloatOrSimpleFromBuffer(buffer, offset, options)
|
|
103
84
|
|
|
104
85
|
default:
|
|
105
86
|
throw new Error(`Unknown major type: ${majorType}`)
|
|
@@ -356,8 +337,10 @@ export function useCborCollection() {
|
|
|
356
337
|
const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
|
|
357
338
|
currentOffset += valueResult.bytesRead
|
|
358
339
|
|
|
359
|
-
// For duplicate key detection, serialize the
|
|
360
|
-
|
|
340
|
+
// For duplicate key detection, serialize the parsed value semantically
|
|
341
|
+
// RFC 8949 Section 5.6: comparison must be on semantic values, not raw bytes
|
|
342
|
+
// (raw bytes differ when same value uses different byte widths)
|
|
343
|
+
const keyString = serializeValueForComparison(keyResult.value)
|
|
361
344
|
|
|
362
345
|
// Check for duplicate keys based on dupMapKeyMode
|
|
363
346
|
// RFC 8949: Deterministic encoding SHOULD reject duplicate keys
|
|
@@ -420,8 +403,10 @@ export function useCborCollection() {
|
|
|
420
403
|
const valueResult = parseItem(buffer, currentOffset, options, depth + 1)
|
|
421
404
|
currentOffset += valueResult.bytesRead
|
|
422
405
|
|
|
423
|
-
// For duplicate key detection, serialize the
|
|
424
|
-
|
|
406
|
+
// For duplicate key detection, serialize the parsed value semantically
|
|
407
|
+
// RFC 8949 Section 5.6: comparison must be on semantic values, not raw bytes
|
|
408
|
+
// (raw bytes differ when same value uses different byte widths)
|
|
409
|
+
const keyString = serializeValueForComparison(keyResult.value)
|
|
425
410
|
|
|
426
411
|
// Check for duplicate keys based on dupMapKeyMode
|
|
427
412
|
// RFC 8949: Deterministic encoding SHOULD reject duplicate keys
|
|
@@ -485,7 +470,16 @@ export function useCborCollection() {
|
|
|
485
470
|
// Remove spaces from hex string
|
|
486
471
|
const cleanHex = hexString.replace(/\s+/g, '')
|
|
487
472
|
const buffer = hexToBytes(cleanHex)
|
|
488
|
-
|
|
473
|
+
|
|
474
|
+
// Set parse start time for timeout enforcement
|
|
475
|
+
if (options?.limits?.maxParseTime) {
|
|
476
|
+
parseStartTime = Date.now()
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
return parseArrayFromBuffer(buffer, 0, options, 0)
|
|
480
|
+
} finally {
|
|
481
|
+
parseStartTime = 0
|
|
482
|
+
}
|
|
489
483
|
}
|
|
490
484
|
|
|
491
485
|
/**
|
|
@@ -499,7 +493,16 @@ export function useCborCollection() {
|
|
|
499
493
|
// Remove spaces from hex string
|
|
500
494
|
const cleanHex = hexString.replace(/\s+/g, '')
|
|
501
495
|
const buffer = hexToBytes(cleanHex)
|
|
502
|
-
|
|
496
|
+
|
|
497
|
+
// Set parse start time for timeout enforcement
|
|
498
|
+
if (options?.limits?.maxParseTime) {
|
|
499
|
+
parseStartTime = Date.now()
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
return parseMapFromBuffer(buffer, 0, options, 0)
|
|
503
|
+
} finally {
|
|
504
|
+
parseStartTime = 0
|
|
505
|
+
}
|
|
503
506
|
}
|
|
504
507
|
|
|
505
508
|
return {
|
|
@@ -62,6 +62,58 @@ export function useCborFloat() {
|
|
|
62
62
|
return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - 15) * (1 + fraction / 1024)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Checks if a float64 value can be exactly represented as float16
|
|
67
|
+
* Used for canonical encoding validation (RFC 8949 Section 4.2.2)
|
|
68
|
+
*/
|
|
69
|
+
const fitsInFloat16 = (value: number): boolean => {
|
|
70
|
+
if (Number.isNaN(value) || value === Infinity || value === -Infinity) return true
|
|
71
|
+
if (Object.is(value, 0) || Object.is(value, -0)) return true
|
|
72
|
+
|
|
73
|
+
const abs = Math.abs(value)
|
|
74
|
+
// Float16 range: subnormals ~5.96e-8 to max normal 65504
|
|
75
|
+
if (abs > 65504) return false
|
|
76
|
+
if (abs < 5.960464477539063e-8) return false
|
|
77
|
+
|
|
78
|
+
// Encode to float16 and back to see if value is preserved
|
|
79
|
+
const sign = value < 0 ? 1 : 0
|
|
80
|
+
const buf = new ArrayBuffer(8)
|
|
81
|
+
const view = new DataView(buf)
|
|
82
|
+
view.setFloat64(0, abs, false)
|
|
83
|
+
const bits64 = view.getBigUint64(0, false)
|
|
84
|
+
const exp64 = Number((bits64 >> 52n) & 0x7ffn) - 1023
|
|
85
|
+
const mant64 = Number(bits64 & 0xfffffffffffffn)
|
|
86
|
+
|
|
87
|
+
let exp16: number
|
|
88
|
+
let mant16: number
|
|
89
|
+
if (exp64 < -14) {
|
|
90
|
+
// Subnormal float16
|
|
91
|
+
exp16 = 0
|
|
92
|
+
const shift = -14 - exp64
|
|
93
|
+
mant16 = ((1 << 10) | (mant64 >> 42)) >> shift
|
|
94
|
+
} else if (exp64 > 15) {
|
|
95
|
+
return false
|
|
96
|
+
} else {
|
|
97
|
+
exp16 = exp64 + 15
|
|
98
|
+
mant16 = mant64 >> 42
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const float16Bits = (sign << 15) | (exp16 << 10) | mant16
|
|
102
|
+
// Decode back
|
|
103
|
+
const s = (float16Bits & 0x8000) >> 15
|
|
104
|
+
const e = (float16Bits & 0x7c00) >> 10
|
|
105
|
+
const f = float16Bits & 0x03ff
|
|
106
|
+
let reconstructed: number
|
|
107
|
+
if (e === 0) {
|
|
108
|
+
reconstructed = (s === 0 ? 1 : -1) * Math.pow(2, -14) * (f / 1024)
|
|
109
|
+
} else if (e === 0x1f) {
|
|
110
|
+
reconstructed = f === 0 ? (s === 0 ? Infinity : -Infinity) : NaN
|
|
111
|
+
} else {
|
|
112
|
+
reconstructed = (s === 0 ? 1 : -1) * Math.pow(2, e - 15) * (1 + f / 1024)
|
|
113
|
+
}
|
|
114
|
+
return reconstructed === value
|
|
115
|
+
}
|
|
116
|
+
|
|
65
117
|
/**
|
|
66
118
|
* Parses simple values (booleans, null, undefined, unassigned)
|
|
67
119
|
*
|
|
@@ -142,7 +194,7 @@ export function useCborFloat() {
|
|
|
142
194
|
* @param offset - Current offset
|
|
143
195
|
* @returns Parsed float and bytes read
|
|
144
196
|
*/
|
|
145
|
-
const parseFloatFromBuffer = (buffer: Uint8Array, offset: number): ParseResult => {
|
|
197
|
+
const parseFloatFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
146
198
|
const initialByte = readByte(buffer, offset)
|
|
147
199
|
const { majorType, additionalInfo } = extractCborHeader(initialByte)
|
|
148
200
|
|
|
@@ -156,6 +208,16 @@ export function useCborFloat() {
|
|
|
156
208
|
if (offset + 2 >= buffer.length) {
|
|
157
209
|
throw new Error('Unexpected end of buffer while reading Float16')
|
|
158
210
|
}
|
|
211
|
+
if (options?.validateCanonical) {
|
|
212
|
+
const byte1 = readByte(buffer, offset + 1)
|
|
213
|
+
const byte2 = readByte(buffer, offset + 2)
|
|
214
|
+
const bits = (byte1 << 8) | byte2
|
|
215
|
+
const exp = (bits >> 10) & 0x1f
|
|
216
|
+
const mant = bits & 0x03ff
|
|
217
|
+
if (exp === 0x1f && mant !== 0 && bits !== 0x7e00) {
|
|
218
|
+
throw new Error('Non-canonical NaN encoding: use 0xf97e00')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
159
221
|
const value = float16ToFloat64(buffer, offset + 1)
|
|
160
222
|
return { value, bytesRead: 3 }
|
|
161
223
|
}
|
|
@@ -168,6 +230,15 @@ export function useCborFloat() {
|
|
|
168
230
|
// Use DataView for proper IEEE 754 parsing
|
|
169
231
|
const dataView = new DataView(buffer.buffer, buffer.byteOffset + offset + 1, 4)
|
|
170
232
|
const value = dataView.getFloat32(0, false) // false = big-endian
|
|
233
|
+
if (options?.validateCanonical) {
|
|
234
|
+
if (Number.isNaN(value)) {
|
|
235
|
+
throw new Error('Non-canonical NaN encoding: NaN must use float16 0xf97e00')
|
|
236
|
+
}
|
|
237
|
+
// Check if value could be represented as float16 (shortest form)
|
|
238
|
+
if (fitsInFloat16(value)) {
|
|
239
|
+
throw new Error('Non-canonical float32: value fits in float16, use shortest encoding')
|
|
240
|
+
}
|
|
241
|
+
}
|
|
171
242
|
return { value, bytesRead: 5 }
|
|
172
243
|
}
|
|
173
244
|
|
|
@@ -179,6 +250,20 @@ export function useCborFloat() {
|
|
|
179
250
|
// Use DataView for proper IEEE 754 parsing
|
|
180
251
|
const dataView = new DataView(buffer.buffer, buffer.byteOffset + offset + 1, 8)
|
|
181
252
|
const value = dataView.getFloat64(0, false) // false = big-endian
|
|
253
|
+
if (options?.validateCanonical) {
|
|
254
|
+
if (Number.isNaN(value)) {
|
|
255
|
+
throw new Error('Non-canonical NaN encoding: NaN must use float16 0xf97e00')
|
|
256
|
+
}
|
|
257
|
+
// Check if value could be represented in a smaller float
|
|
258
|
+
if (fitsInFloat16(value)) {
|
|
259
|
+
throw new Error('Non-canonical float64: value fits in float16/float32, use shortest encoding')
|
|
260
|
+
}
|
|
261
|
+
// Check if float64 value fits in float32
|
|
262
|
+
const f32 = Math.fround(value)
|
|
263
|
+
if (f32 === value || (Object.is(value, -0) && Object.is(f32, -0))) {
|
|
264
|
+
throw new Error('Non-canonical float64: value fits in float16/float32, use shortest encoding')
|
|
265
|
+
}
|
|
266
|
+
}
|
|
182
267
|
return { value, bytesRead: 9 }
|
|
183
268
|
}
|
|
184
269
|
|
|
@@ -194,7 +279,7 @@ export function useCborFloat() {
|
|
|
194
279
|
* @param offset - Current offset
|
|
195
280
|
* @returns Parsed value and bytes read
|
|
196
281
|
*/
|
|
197
|
-
const parseFromBuffer = (buffer: Uint8Array, offset: number): ParseResult => {
|
|
282
|
+
const parseFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
198
283
|
const initialByte = readByte(buffer, offset)
|
|
199
284
|
const { majorType, additionalInfo } = extractCborHeader(initialByte)
|
|
200
285
|
|
|
@@ -205,7 +290,7 @@ export function useCborFloat() {
|
|
|
205
290
|
// Determine if it's a float or simple value based on additional info
|
|
206
291
|
if (additionalInfo === 25 || additionalInfo === 26 || additionalInfo === 27) {
|
|
207
292
|
// Float16, Float32, or Float64
|
|
208
|
-
return parseFloatFromBuffer(buffer, offset)
|
|
293
|
+
return parseFloatFromBuffer(buffer, offset, options)
|
|
209
294
|
} else {
|
|
210
295
|
// Simple value (including false, true, null, undefined)
|
|
211
296
|
return parseSimpleFromBuffer(buffer, offset)
|
|
@@ -216,7 +301,7 @@ export function useCborFloat() {
|
|
|
216
301
|
* Parses CBOR simple value from hex string
|
|
217
302
|
*
|
|
218
303
|
* @param hexString - CBOR hex string
|
|
219
|
-
* @param _options - Parser options (
|
|
304
|
+
* @param _options - Parser options (reserved for future use)
|
|
220
305
|
* @returns Parsed simple value and bytes read
|
|
221
306
|
*/
|
|
222
307
|
const parseSimple = (hexString: string, _options?: ParseOptions): ParseResult => {
|
|
@@ -231,9 +316,9 @@ export function useCborFloat() {
|
|
|
231
316
|
* @param _options - Parser options (optional, for future use)
|
|
232
317
|
* @returns Parsed float and bytes read
|
|
233
318
|
*/
|
|
234
|
-
const parseFloat = (hexString: string,
|
|
319
|
+
const parseFloat = (hexString: string, options?: ParseOptions): ParseResult => {
|
|
235
320
|
const buffer = hexToBytes(hexString)
|
|
236
|
-
return parseFloatFromBuffer(buffer, 0)
|
|
321
|
+
return parseFloatFromBuffer(buffer, 0, options)
|
|
237
322
|
}
|
|
238
323
|
|
|
239
324
|
/**
|
|
@@ -243,14 +328,15 @@ export function useCborFloat() {
|
|
|
243
328
|
* @param _options - Parser options (optional, for future use)
|
|
244
329
|
* @returns Parsed value and bytes read
|
|
245
330
|
*/
|
|
246
|
-
const parse = (hexString: string,
|
|
331
|
+
const parse = (hexString: string, options?: ParseOptions): ParseResult => {
|
|
247
332
|
const buffer = hexToBytes(hexString)
|
|
248
|
-
return parseFromBuffer(buffer, 0)
|
|
333
|
+
return parseFromBuffer(buffer, 0, options)
|
|
249
334
|
}
|
|
250
335
|
|
|
251
336
|
return {
|
|
252
337
|
parse,
|
|
253
338
|
parseFloat,
|
|
254
|
-
parseSimple
|
|
339
|
+
parseSimple,
|
|
340
|
+
parseFromBuffer
|
|
255
341
|
}
|
|
256
342
|
}
|
|
@@ -20,15 +20,15 @@ import { hexToBytes, readByte, readUint, readBigUint, extractCborHeader, validat
|
|
|
20
20
|
*/
|
|
21
21
|
export function useCborInteger() {
|
|
22
22
|
/**
|
|
23
|
-
* Parses CBOR integer (Major Type 0 or 1)
|
|
23
|
+
* Parses CBOR integer (Major Type 0 or 1) from a buffer at a given offset
|
|
24
24
|
*
|
|
25
|
-
* @param
|
|
25
|
+
* @param buffer - Data buffer
|
|
26
|
+
* @param offset - Current offset into the buffer
|
|
26
27
|
* @param options - Parser options (optional)
|
|
27
28
|
* @returns Parsed integer value and bytes read
|
|
28
29
|
*/
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
const initialByte = readByte(buffer, 0)
|
|
30
|
+
const parseIntegerFromBuffer = (buffer: Uint8Array, offset: number, options?: ParseOptions): ParseResult => {
|
|
31
|
+
const initialByte = readByte(buffer, offset)
|
|
32
32
|
|
|
33
33
|
const { majorType, additionalInfo } = extractCborHeader(initialByte)
|
|
34
34
|
|
|
@@ -42,19 +42,19 @@ export function useCborInteger() {
|
|
|
42
42
|
bytesRead = 1
|
|
43
43
|
} else if (additionalInfo === 24) {
|
|
44
44
|
// 1 byte follows
|
|
45
|
-
rawValue = readByte(buffer, 1)
|
|
45
|
+
rawValue = readByte(buffer, offset + 1)
|
|
46
46
|
bytesRead = 2
|
|
47
47
|
} else if (additionalInfo === 25) {
|
|
48
48
|
// 2 bytes follow
|
|
49
|
-
rawValue = readUint(buffer, 1, 2)
|
|
49
|
+
rawValue = readUint(buffer, offset + 1, 2)
|
|
50
50
|
bytesRead = 3
|
|
51
51
|
} else if (additionalInfo === 26) {
|
|
52
52
|
// 4 bytes follow
|
|
53
|
-
rawValue = readUint(buffer, 1, 4)
|
|
53
|
+
rawValue = readUint(buffer, offset + 1, 4)
|
|
54
54
|
bytesRead = 5
|
|
55
55
|
} else if (additionalInfo === 27) {
|
|
56
56
|
// 8 bytes follow - use BigInt for large values
|
|
57
|
-
const bigValue = readBigUint(buffer, 1, 8)
|
|
57
|
+
const bigValue = readBigUint(buffer, offset + 1, 8)
|
|
58
58
|
|
|
59
59
|
// Check if value fits in Number.MAX_SAFE_INTEGER
|
|
60
60
|
if (bigValue <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
@@ -108,7 +108,21 @@ export function useCborInteger() {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Parses CBOR integer (Major Type 0 or 1) from hex string
|
|
113
|
+
* Thin wrapper around parseIntegerFromBuffer
|
|
114
|
+
*
|
|
115
|
+
* @param hexString - CBOR hex string
|
|
116
|
+
* @param options - Parser options (optional)
|
|
117
|
+
* @returns Parsed integer value and bytes read
|
|
118
|
+
*/
|
|
119
|
+
const parseInteger = (hexString: string, options?: ParseOptions): ParseResult => {
|
|
120
|
+
const buffer = hexToBytes(hexString)
|
|
121
|
+
return parseIntegerFromBuffer(buffer, 0, options)
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
return {
|
|
112
|
-
parseInteger
|
|
125
|
+
parseInteger,
|
|
126
|
+
parseIntegerFromBuffer
|
|
113
127
|
}
|
|
114
128
|
}
|