@marcuspuchalla/nachos 0.1.0 → 0.1.3
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 +32 -0
- package/README.md +1 -1
- package/dist/{chunk-7CFYWHS6.js → chunk-2MTLSQ7E.js} +58 -10
- package/dist/chunk-2MTLSQ7E.js.map +1 -0
- package/dist/{chunk-ZRPJUEIZ.js → chunk-5A5T56JB.js} +170 -22
- package/dist/chunk-5A5T56JB.js.map +1 -0
- package/dist/{chunk-2HBCILJS.cjs → chunk-PTWN7K3Y.cjs} +169 -21
- package/dist/chunk-PTWN7K3Y.cjs.map +1 -0
- package/dist/{chunk-2FUTHZQQ.cjs → chunk-R62CQQNI.cjs} +58 -10
- package/dist/chunk-R62CQQNI.cjs.map +1 -0
- package/dist/encoder/index.cjs +13 -13
- package/dist/encoder/index.js +1 -1
- package/dist/index.cjs +20 -20
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- 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 +1 -1
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/{useCborTag-BfTIV8HM.d.cts → useCborTag-Cs1CZuXZ.d.cts} +2 -2
- package/dist/{useCborTag-B_iaShG6.d.ts → useCborTag-xV2Pz2VY.d.ts} +2 -2
- package/package.json +1 -1
- package/src/__tests__/roundtrip.test.ts +702 -0
- package/src/encoder/__tests__/cbor-collection-encoder.test.ts +26 -0
- package/src/encoder/__tests__/cbor-encoder-errors.test.ts +812 -0
- package/src/encoder/__tests__/cbor-string-encoder.test.ts +14 -0
- package/src/encoder/composables/useCborCollectionEncoder.ts +30 -0
- package/src/encoder/composables/useCborEncoder.ts +6 -1
- package/src/encoder/composables/useCborSimpleEncoder.ts +7 -2
- package/src/encoder/composables/useCborStringEncoder.ts +23 -10
- package/src/parser/__tests__/cbor-float-errors.test.ts +41 -0
- package/src/parser/__tests__/cbor-security-dos-protection.test.ts +2 -2
- package/src/parser/__tests__/cbor-standard-tags.test.ts +29 -0
- 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/composables/useCborFloat.ts +93 -8
- package/src/parser/composables/useCborParser.ts +0 -19
- package/src/parser/composables/useCborString.ts +22 -4
- package/src/parser/composables/useCborTag.ts +104 -16
- 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
|
@@ -329,6 +329,20 @@ describe('CBOR String Encoder', () => {
|
|
|
329
329
|
expect(() => encodeByteStringIndefinite([new Uint8Array([1])]))
|
|
330
330
|
.toThrow('Indefinite-length encoding not allowed in canonical mode')
|
|
331
331
|
})
|
|
332
|
+
|
|
333
|
+
it('should reject allowIndefinite=false for text strings', () => {
|
|
334
|
+
const { encodeTextStringIndefinite } = useCborStringEncoder({ allowIndefinite: false })
|
|
335
|
+
|
|
336
|
+
expect(() => encodeTextStringIndefinite(['a', 'b']))
|
|
337
|
+
.toThrow('Indefinite-length encoding is not allowed')
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it('should reject allowIndefinite=false for byte strings', () => {
|
|
341
|
+
const { encodeByteStringIndefinite } = useCborStringEncoder({ allowIndefinite: false })
|
|
342
|
+
|
|
343
|
+
expect(() => encodeByteStringIndefinite([new Uint8Array([1])]))
|
|
344
|
+
.toThrow('Indefinite-length encoding is not allowed')
|
|
345
|
+
})
|
|
332
346
|
})
|
|
333
347
|
|
|
334
348
|
describe('Byte string array handling', () => {
|
|
@@ -105,6 +105,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
105
105
|
const newCtx = { ...ctx, depth: ctx.depth + 1 }
|
|
106
106
|
|
|
107
107
|
if (isIndefinite) {
|
|
108
|
+
if (ctx.options.allowIndefinite === false) {
|
|
109
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
110
|
+
}
|
|
108
111
|
// Use indefinite encoding - but need to recursively encode items
|
|
109
112
|
const parts: Uint8Array[] = [new Uint8Array([0x9f])] // Start marker
|
|
110
113
|
for (const item of value) {
|
|
@@ -130,6 +133,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
130
133
|
const newCtx = { ...ctx, depth: ctx.depth + 1 }
|
|
131
134
|
|
|
132
135
|
if (isIndefinite) {
|
|
136
|
+
if (ctx.options.allowIndefinite === false) {
|
|
137
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
138
|
+
}
|
|
133
139
|
// Use indefinite encoding
|
|
134
140
|
const entries: Array<[EncodableValue, EncodableValue]> =
|
|
135
141
|
value instanceof Map
|
|
@@ -233,6 +239,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
233
239
|
|
|
234
240
|
// Handle indefinite-length encoding
|
|
235
241
|
if (encodeOptions?.indefinite || isIndefinite) {
|
|
242
|
+
if (options.allowIndefinite === false) {
|
|
243
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
244
|
+
}
|
|
236
245
|
if (options.canonical) {
|
|
237
246
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
238
247
|
}
|
|
@@ -256,6 +265,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
256
265
|
* @returns Encoded CBOR bytes and hex string
|
|
257
266
|
*/
|
|
258
267
|
const encodeArrayIndefinite = (array: EncodableValue[]): EncodeResult => {
|
|
268
|
+
if (options.allowIndefinite === false) {
|
|
269
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
270
|
+
}
|
|
259
271
|
if (options.canonical) {
|
|
260
272
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
261
273
|
}
|
|
@@ -313,6 +325,18 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
313
325
|
})
|
|
314
326
|
}
|
|
315
327
|
|
|
328
|
+
if (ctx.options.rejectDuplicateKeys) {
|
|
329
|
+
const seen = new Set<string>()
|
|
330
|
+
for (const [key] of entries) {
|
|
331
|
+
const keyBytes = encodeValue(key, { ...ctx, depth: ctx.depth + 1, bytesWritten: 0 })
|
|
332
|
+
const keyHex = bytesToHex(keyBytes)
|
|
333
|
+
if (seen.has(keyHex)) {
|
|
334
|
+
throw new Error('Duplicate map key detected')
|
|
335
|
+
}
|
|
336
|
+
seen.add(keyHex)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
316
340
|
const header = encodeLengthHeader(5, entries.length)
|
|
317
341
|
const parts: Uint8Array[] = [header]
|
|
318
342
|
|
|
@@ -354,6 +378,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
354
378
|
|
|
355
379
|
// Handle indefinite-length encoding
|
|
356
380
|
if (encodeOptions?.indefinite || isIndefinite) {
|
|
381
|
+
if (options.allowIndefinite === false) {
|
|
382
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
383
|
+
}
|
|
357
384
|
if (options.canonical) {
|
|
358
385
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
359
386
|
}
|
|
@@ -379,6 +406,9 @@ export function useCborCollectionEncoder(globalOptions?: Partial<EncodeOptions>)
|
|
|
379
406
|
const encodeMapIndefinite = (
|
|
380
407
|
map: Map<EncodableValue, EncodableValue> | { [key: string]: EncodableValue }
|
|
381
408
|
): EncodeResult => {
|
|
409
|
+
if (options.allowIndefinite === false) {
|
|
410
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
411
|
+
}
|
|
382
412
|
if (options.canonical) {
|
|
383
413
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
384
414
|
}
|
|
@@ -42,6 +42,11 @@ import { useCborByteString, useCborTextString } from '../../parser/composables/u
|
|
|
42
42
|
export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
43
43
|
const options = { ...DEFAULT_ENCODE_OPTIONS, ...globalOptions }
|
|
44
44
|
|
|
45
|
+
// Canonical mode overrides: indefinite-length is forbidden per RFC 8949 Section 4.2
|
|
46
|
+
if (options.canonical && options.allowIndefinite) {
|
|
47
|
+
options.allowIndefinite = false
|
|
48
|
+
}
|
|
49
|
+
|
|
45
50
|
// Get all specialized encoders
|
|
46
51
|
const { encodeInteger } = useCborIntegerEncoder()
|
|
47
52
|
const { encodeTextString, encodeByteString } = useCborStringEncoder(options)
|
|
@@ -78,7 +83,7 @@ export function useCborEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
78
83
|
// Handle numbers
|
|
79
84
|
if (typeof value === 'number') {
|
|
80
85
|
// Check if it's an integer
|
|
81
|
-
if (Number.
|
|
86
|
+
if (Number.isSafeInteger(value)) {
|
|
82
87
|
return encodeInteger(value)
|
|
83
88
|
}
|
|
84
89
|
// It's a float
|
|
@@ -91,10 +91,15 @@ export function useCborSimpleEncoder(_globalOptions?: Partial<EncodeOptions>) {
|
|
|
91
91
|
let exp16: number
|
|
92
92
|
let mant16: number
|
|
93
93
|
|
|
94
|
-
if (exp64 < -
|
|
95
|
-
//
|
|
94
|
+
if (exp64 < -24) {
|
|
95
|
+
// Too small even for subnormal float16
|
|
96
96
|
exp16 = 0
|
|
97
97
|
mant16 = 0
|
|
98
|
+
} else if (exp64 < -14) {
|
|
99
|
+
// Subnormal float16: shift the implicit 1.mantissa into the fraction bits
|
|
100
|
+
exp16 = 0
|
|
101
|
+
const shift = -14 - exp64
|
|
102
|
+
mant16 = (((1 << 10) | (mant64 >> 42)) + ((1 << (shift - 1)) - 1)) >> shift
|
|
98
103
|
} else if (exp64 > 15) {
|
|
99
104
|
// Overflow to infinity
|
|
100
105
|
exp16 = 31
|
|
@@ -106,6 +106,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
106
106
|
|
|
107
107
|
// Handle indefinite-length encoding
|
|
108
108
|
if (encodeOptions?.indefinite || Array.isArray(data) || isIndefinite) {
|
|
109
|
+
if (options.allowIndefinite === false) {
|
|
110
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
111
|
+
}
|
|
109
112
|
if (options.canonical) {
|
|
110
113
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
111
114
|
}
|
|
@@ -122,14 +125,15 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
122
125
|
|
|
123
126
|
// Definite-length encoding - extract bytes from CborByteString if needed
|
|
124
127
|
const bytes = isCborByteString(data) ? data.bytes : (data as Uint8Array)
|
|
125
|
-
const header = encodeLengthHeader(2, bytes.length)
|
|
126
|
-
const result = concatenateUint8Arrays([header, bytes])
|
|
127
128
|
|
|
128
|
-
// Check
|
|
129
|
-
if (
|
|
130
|
-
throw new Error(
|
|
129
|
+
// Check size limit before allocating (header is at most 9 bytes)
|
|
130
|
+
if (bytes.length + 9 > options.maxOutputSize) {
|
|
131
|
+
throw new Error(`Encoded output exceeds maximum size`)
|
|
131
132
|
}
|
|
132
133
|
|
|
134
|
+
const header = encodeLengthHeader(2, bytes.length)
|
|
135
|
+
const result = concatenateUint8Arrays([header, bytes])
|
|
136
|
+
|
|
133
137
|
return {
|
|
134
138
|
bytes: result,
|
|
135
139
|
hex: bytesToHex(result)
|
|
@@ -146,6 +150,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
146
150
|
* @returns Encoded CBOR bytes and hex string
|
|
147
151
|
*/
|
|
148
152
|
const encodeByteStringIndefinite = (chunks: Uint8Array[]): EncodeResult => {
|
|
153
|
+
if (options.allowIndefinite === false) {
|
|
154
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
155
|
+
}
|
|
149
156
|
if (options.canonical) {
|
|
150
157
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
151
158
|
}
|
|
@@ -187,6 +194,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
187
194
|
|
|
188
195
|
// Handle indefinite-length encoding
|
|
189
196
|
if (encodeOptions?.indefinite || isIndefinite) {
|
|
197
|
+
if (options.allowIndefinite === false) {
|
|
198
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
199
|
+
}
|
|
190
200
|
if (options.canonical) {
|
|
191
201
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
192
202
|
}
|
|
@@ -208,14 +218,14 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
208
218
|
const encoder = new TextEncoder()
|
|
209
219
|
const utf8Bytes = encoder.encode(textStr)
|
|
210
220
|
|
|
221
|
+
// Check size limit before allocating (header is at most 9 bytes)
|
|
222
|
+
if (utf8Bytes.length + 9 > options.maxOutputSize) {
|
|
223
|
+
throw new Error(`Encoded output exceeds maximum size`)
|
|
224
|
+
}
|
|
225
|
+
|
|
211
226
|
const header = encodeLengthHeader(3, utf8Bytes.length)
|
|
212
227
|
const result = concatenateUint8Arrays([header, utf8Bytes])
|
|
213
228
|
|
|
214
|
-
// Check output size limit
|
|
215
|
-
if (result.length > options.maxOutputSize) {
|
|
216
|
-
throw new Error('Encoded output exceeds maximum size')
|
|
217
|
-
}
|
|
218
|
-
|
|
219
229
|
return {
|
|
220
230
|
bytes: result,
|
|
221
231
|
hex: bytesToHex(result)
|
|
@@ -232,6 +242,9 @@ export function useCborStringEncoder(globalOptions?: Partial<EncodeOptions>) {
|
|
|
232
242
|
* @returns Encoded CBOR bytes and hex string
|
|
233
243
|
*/
|
|
234
244
|
const encodeTextStringIndefinite = (chunks: string[]): EncodeResult => {
|
|
245
|
+
if (options.allowIndefinite === false) {
|
|
246
|
+
throw new Error('Indefinite-length encoding is not allowed')
|
|
247
|
+
}
|
|
235
248
|
if (options.canonical) {
|
|
236
249
|
throw new Error('Indefinite-length encoding not allowed in canonical mode')
|
|
237
250
|
}
|
|
@@ -211,6 +211,47 @@ describe('useCborFloat - Error Handling', () => {
|
|
|
211
211
|
})
|
|
212
212
|
})
|
|
213
213
|
|
|
214
|
+
describe('Canonical float validation', () => {
|
|
215
|
+
it('should reject float32 when value fits in float16', () => {
|
|
216
|
+
const { parseFloat } = useCborFloat()
|
|
217
|
+
|
|
218
|
+
// 1.0 fits in float16, but encoded as float32
|
|
219
|
+
expect(() => parseFloat('fa3f800000', { validateCanonical: true }))
|
|
220
|
+
.toThrow(/canonical.*float16/i)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should reject float64 when value fits in float32', () => {
|
|
224
|
+
const { parseFloat } = useCborFloat()
|
|
225
|
+
|
|
226
|
+
// 1.0 fits in float32, but encoded as float64
|
|
227
|
+
expect(() => parseFloat('fb3ff0000000000000', { validateCanonical: true }))
|
|
228
|
+
.toThrow(/canonical.*float16\/float32/i)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should reject non-canonical NaN encodings for float32/float64', () => {
|
|
232
|
+
const { parseFloat } = useCborFloat()
|
|
233
|
+
|
|
234
|
+
// Float16 NaN with non-canonical payload
|
|
235
|
+
expect(() => parseFloat('f97e01', { validateCanonical: true }))
|
|
236
|
+
.toThrow(/nan/i)
|
|
237
|
+
|
|
238
|
+
// Float32 NaN
|
|
239
|
+
expect(() => parseFloat('fa7fc00000', { validateCanonical: true }))
|
|
240
|
+
.toThrow(/nan/i)
|
|
241
|
+
|
|
242
|
+
// Float64 NaN
|
|
243
|
+
expect(() => parseFloat('fb7ff8000000000001', { validateCanonical: true }))
|
|
244
|
+
.toThrow(/nan/i)
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should accept float16 when canonical validation is enabled', () => {
|
|
248
|
+
const { parseFloat } = useCborFloat()
|
|
249
|
+
|
|
250
|
+
const result = parseFloat('f93e00', { validateCanonical: true })
|
|
251
|
+
expect(result.value).toBe(1.5)
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
214
255
|
describe('Wrong Major Type in parse()', () => {
|
|
215
256
|
it('should throw error when auto-detecting with wrong major type', () => {
|
|
216
257
|
const { parse } = useCborFloat()
|
|
@@ -261,8 +261,8 @@ describe('RUSTSEC-2019-0025: Tag Nesting Stack Overflow Protection', () => {
|
|
|
261
261
|
it('should use default tag depth limit when not specified', () => {
|
|
262
262
|
const { parseTag } = useCborTag()
|
|
263
263
|
|
|
264
|
-
//
|
|
265
|
-
const nested = 'c0'.repeat(
|
|
264
|
+
// 150 nested tags (exceeds default 100)
|
|
265
|
+
const nested = 'c0'.repeat(150) + '00'
|
|
266
266
|
|
|
267
267
|
expect(() => parseTag(nested)) // No options = use defaults
|
|
268
268
|
.toThrow(/tag nesting depth.*exceeds/i)
|
|
@@ -221,6 +221,35 @@ describe('CBOR Standard Tags (RFC 8949)', () => {
|
|
|
221
221
|
})
|
|
222
222
|
})
|
|
223
223
|
|
|
224
|
+
describe('Tags 2-3: Bignums', () => {
|
|
225
|
+
it('should parse tag 2 with byte string', () => {
|
|
226
|
+
// c2 (tag 2) + 41 (1-byte byte string) + 01
|
|
227
|
+
const hex = 'c24101'
|
|
228
|
+
const result = parse(hex)
|
|
229
|
+
|
|
230
|
+
expect(result.value).toMatchObject({
|
|
231
|
+
tag: 2,
|
|
232
|
+
value: 1n
|
|
233
|
+
})
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it('should reject non-byte-string tag 2 in strict mode', () => {
|
|
237
|
+
// c2 (tag 2) + 01 (integer)
|
|
238
|
+
const hex = 'c201'
|
|
239
|
+
|
|
240
|
+
expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
|
|
241
|
+
.toThrow(/byte string/i)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it('should reject non-byte-string tag 3 in strict mode', () => {
|
|
245
|
+
// c3 (tag 3) + 01 (integer)
|
|
246
|
+
const hex = 'c301'
|
|
247
|
+
|
|
248
|
+
expect(() => parse(hex, { strict: true, validateTagSemantics: true }))
|
|
249
|
+
.toThrow(/byte string/i)
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
224
253
|
describe('Tag 32: URI', () => {
|
|
225
254
|
it('should parse valid URI', () => {
|
|
226
255
|
// d8 20 (tag 32) + 76 (22-byte text) + "http://www.example.com"
|
|
@@ -112,8 +112,8 @@ describe('useCborString - Error Handling', () => {
|
|
|
112
112
|
// 5f (indefinite byte string) + 6161 (text "a") + ff (break)
|
|
113
113
|
const buffer = new Uint8Array([0x5f, 0x61, 0x61, 0xff])
|
|
114
114
|
|
|
115
|
-
// The error is thrown when
|
|
116
|
-
expect(() => parseByteString(buffer, 0)).toThrow('
|
|
115
|
+
// The error is thrown when validating the chunk type
|
|
116
|
+
expect(() => parseByteString(buffer, 0)).toThrow('chunks must be byte strings')
|
|
117
117
|
})
|
|
118
118
|
})
|
|
119
119
|
|
|
@@ -125,8 +125,8 @@ describe('useCborString - Error Handling', () => {
|
|
|
125
125
|
// 7f (indefinite text string) + 4161 (byte string containing 'a') + ff (break)
|
|
126
126
|
const buffer = new Uint8Array([0x7f, 0x41, 0x61, 0xff])
|
|
127
127
|
|
|
128
|
-
// The error is thrown when
|
|
129
|
-
expect(() => parseTextString(buffer, 0)).toThrow('
|
|
128
|
+
// The error is thrown when validating the chunk type
|
|
129
|
+
expect(() => parseTextString(buffer, 0)).toThrow('chunks must be text strings')
|
|
130
130
|
})
|
|
131
131
|
})
|
|
132
132
|
|
|
@@ -20,7 +20,7 @@ describe('useCborTag - Error Handling', () => {
|
|
|
20
20
|
|
|
21
21
|
// Tag with array that's incomplete
|
|
22
22
|
// c1 (tag 1) + 82 (array of 2) + 01 (element 1) - missing element 2
|
|
23
|
-
expect(() => parseTag('c18201')).toThrow('Unexpected end of buffer
|
|
23
|
+
expect(() => parseTag('c18201')).toThrow('Unexpected end of buffer')
|
|
24
24
|
})
|
|
25
25
|
})
|
|
26
26
|
|
|
@@ -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,9 +328,9 @@ 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 {
|
|
@@ -793,25 +793,6 @@ export function useCborParser() {
|
|
|
793
793
|
}
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
-
/**
|
|
797
|
-
* Get tag number for description
|
|
798
|
-
* Note: Reserved for future tag description features
|
|
799
|
-
*/
|
|
800
|
-
// const getTagNumber = (buffer: Uint8Array, offset: number): number => {
|
|
801
|
-
// const initialByte = readByte(buffer, offset)
|
|
802
|
-
// const { additionalInfo } = extractCborHeader(initialByte)
|
|
803
|
-
//
|
|
804
|
-
// if (additionalInfo < 24) return additionalInfo
|
|
805
|
-
// if (additionalInfo === 24) return readByte(buffer, offset + 1)
|
|
806
|
-
// if (additionalInfo === 25) return readUint(buffer, offset + 1, 2)
|
|
807
|
-
// if (additionalInfo === 26) return readUint(buffer, offset + 1, 4)
|
|
808
|
-
// if (additionalInfo === 27) {
|
|
809
|
-
// const bigNum = readBigUint(buffer, offset + 1, 8)
|
|
810
|
-
// return bigNum <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(bigNum) : -1
|
|
811
|
-
// }
|
|
812
|
-
// return -1
|
|
813
|
-
// }
|
|
814
|
-
|
|
815
796
|
/**
|
|
816
797
|
* Get simple type description
|
|
817
798
|
*/
|
|
@@ -133,8 +133,17 @@ export function useCborString() {
|
|
|
133
133
|
break
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
//
|
|
137
|
-
|
|
136
|
+
// Validate chunk is definite-length (RFC 8949 Section 3.2.3)
|
|
137
|
+
const chunkInitialByte = readByte(buffer, currentOffset)
|
|
138
|
+
const chunkHeader = extractCborHeader(chunkInitialByte)
|
|
139
|
+
if (chunkHeader.majorType !== 2) {
|
|
140
|
+
throw new Error(`Indefinite byte string chunks must be byte strings (major type 2), got ${chunkHeader.majorType}`)
|
|
141
|
+
}
|
|
142
|
+
if (chunkHeader.additionalInfo === 31) {
|
|
143
|
+
throw new Error('Indefinite byte string chunks must be definite-length (RFC 8949 Section 3.2.3)')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Parse chunk
|
|
138
147
|
let chunkResult
|
|
139
148
|
try {
|
|
140
149
|
chunkResult = parseByteString(buffer, currentOffset, options)
|
|
@@ -237,8 +246,17 @@ export function useCborString() {
|
|
|
237
246
|
break
|
|
238
247
|
}
|
|
239
248
|
|
|
240
|
-
//
|
|
241
|
-
|
|
249
|
+
// Validate chunk is definite-length (RFC 8949 Section 3.2.3)
|
|
250
|
+
const chunkInitialByte = readByte(buffer, currentOffset)
|
|
251
|
+
const chunkHeader = extractCborHeader(chunkInitialByte)
|
|
252
|
+
if (chunkHeader.majorType !== 3) {
|
|
253
|
+
throw new Error(`Indefinite text string chunks must be text strings (major type 3), got ${chunkHeader.majorType}`)
|
|
254
|
+
}
|
|
255
|
+
if (chunkHeader.additionalInfo === 31) {
|
|
256
|
+
throw new Error('Indefinite text string chunks must be definite-length (RFC 8949 Section 3.2.3)')
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Parse chunk
|
|
242
260
|
let chunkResult
|
|
243
261
|
try {
|
|
244
262
|
chunkResult = parseTextString(buffer, currentOffset, options)
|