From 294ee9fa2d95cc291e0b78c19863296bb8aeb1cd Mon Sep 17 00:00:00 2001 From: 8pro Date: Sun, 22 Mar 2026 12:21:01 +0300 Subject: [PATCH] fix(cbor): eliminate dead WithLength decoder family (#159) The WithLength slice-based decoder family (decodeItemWithLengthSync, decodeBytesWithLengthSync, decodeArrayWithLengthSync, decodeMapWithLengthSync, decodeTagWithLengthSync, decodeTextWithLengthSync, decodeUintSync, decodeNintSync, decodeSimpleOrFloatSync, decodeLengthSync) was never called from any live code path and was never exported. The actual decode path was always: fromCBORBytes / fromCBORHex -> internalDecodeSync -> decodeItemAt (At-family) The WithLength family called data.slice(offset) at every recursive level, allocating a new Uint8Array per nested CBOR item - O(n^2) allocation for deeply nested PlutusData. The At-family threads an integer offset with zero allocation. Deleted ~369 lines of dead code. All 973 tests pass. Closes #159 --- packages/evolution/src/CBOR.ts | 369 --------------------------------- 1 file changed, 369 deletions(-) diff --git a/packages/evolution/src/CBOR.ts b/packages/evolution/src/CBOR.ts index d06c5c1f..ac21be81 100644 --- a/packages/evolution/src/CBOR.ts +++ b/packages/evolution/src/CBOR.ts @@ -1582,372 +1582,3 @@ const decodeSimpleOrFloatAt = (data: Uint8Array, offset: number): DecodeAtResult } throw new CBORError({ message: `Unsupported simple/float encoding: ${additionalInfo}` }) } - -const decodeUintSync = (data: Uint8Array): CBOR => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo < 24) { - return BigInt(additionalInfo) - } else if (additionalInfo === 24) { - if (data.length < 2) throw new CBORError({ message: "Insufficient data for 1-byte unsigned integer" }) - return BigInt(data[1]) - } else if (additionalInfo === 25) { - if (data.length < 3) throw new CBORError({ message: "Insufficient data for 2-byte unsigned integer" }) - return BigInt(data[1]) * 256n + BigInt(data[2]) - } else if (additionalInfo === 26) { - if (data.length < 5) throw new CBORError({ message: "Insufficient data for 4-byte unsigned integer" }) - return BigInt(data[1]) * 16777216n + BigInt(data[2]) * 65536n + BigInt(data[3]) * 256n + BigInt(data[4]) - } else if (additionalInfo === 27) { - if (data.length < 9) throw new CBORError({ message: "Insufficient data for 8-byte unsigned integer" }) - let result = 0n - for (let i = 1; i <= 8; i++) result = result * 256n + BigInt(data[i]) - return result - } else { - throw new CBORError({ message: `Unsupported additional info for unsigned integer: ${additionalInfo}` }) - } -} - -const decodeNintSync = (data: Uint8Array): CBOR => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo < 24) { - return -1n - BigInt(additionalInfo) - } else if (additionalInfo === 24) { - if (data.length < 2) throw new CBORError({ message: "Insufficient data for 1-byte negative integer" }) - return -1n - BigInt(data[1]) - } else if (additionalInfo === 25) { - if (data.length < 3) throw new CBORError({ message: "Insufficient data for 2-byte negative integer" }) - return -1n - (BigInt(data[1]) * 256n + BigInt(data[2])) - } else if (additionalInfo === 26) { - if (data.length < 5) throw new CBORError({ message: "Insufficient data for 4-byte negative integer" }) - return -1n - (BigInt(data[1]) * 16777216n + BigInt(data[2]) * 65536n + BigInt(data[3]) * 256n + BigInt(data[4])) - } else if (additionalInfo === 27) { - if (data.length < 9) throw new CBORError({ message: "Insufficient data for 8-byte negative integer" }) - let result = 0n - for (let i = 1; i <= 8; i++) result = result * 256n + BigInt(data[i]) - return -1n - result - } else { - throw new CBORError({ message: `Unsupported additional info for negative integer: ${additionalInfo}` }) - } -} - -const decodeBytesWithLengthSync = (data: Uint8Array): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo === CBOR_ADDITIONAL_INFO.INDEFINITE) { - let offset = 1 - const chunks: Array = [] - let foundBreak = false - while (offset < data.length) { - if (data[offset] === 0xff) { - offset++ - foundBreak = true - break - } - const chunkFirstByte = data[offset] - const chunkMajorType = (chunkFirstByte >> 5) & 0x07 - if (chunkMajorType !== CBOR_MAJOR_TYPE.BYTE_STRING) { - throw new CBORError({ message: `Invalid chunk in indefinite byte string: major type ${chunkMajorType}` }) - } - const { bytesRead, length: chunkLength } = decodeLengthSync(data, offset) - offset += bytesRead - if (data.length < offset + chunkLength) - throw new CBORError({ message: "Insufficient data for byte string chunk" }) - const chunk = data.slice(offset, offset + chunkLength) - chunks.push(chunk) - offset += chunkLength - } - if (!foundBreak) throw new CBORError({ message: "Missing break in indefinite-length byte string" }) - const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0) - const bytes = new Uint8Array(totalLength) - let pos = 0 - for (const chunk of chunks) { - bytes.set(chunk, pos) - pos += chunk.length - } - return { item: bytes, bytesConsumed: offset } - } else { - const { bytesRead, length } = decodeLengthSync(data, 0) - if (data.length < bytesRead + length) throw new CBORError({ message: "Insufficient data for byte string" }) - const bytes = data.slice(bytesRead, bytesRead + length) - return { item: bytes, bytesConsumed: bytesRead + length } - } -} - -const decodeTextWithLengthSync = (data: Uint8Array): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo === CBOR_ADDITIONAL_INFO.INDEFINITE) { - let offset = 1 - const parts: Array = [] - let foundBreak = false - while (offset < data.length) { - if (data[offset] === 0xff) { - offset++ - foundBreak = true - break - } - const chunkFirstByte = data[offset] - const chunkMajorType = (chunkFirstByte >> 5) & 0x07 - if (chunkMajorType !== CBOR_MAJOR_TYPE.TEXT_STRING) { - throw new CBORError({ message: `Invalid chunk in indefinite text string: major type ${chunkMajorType}` }) - } - const { bytesRead, length: chunkLength } = decodeLengthSync(data, offset) - offset += bytesRead - if (data.length < offset + chunkLength) - throw new CBORError({ message: "Insufficient data for text string chunk" }) - const chunkBytes = data.slice(offset, offset + chunkLength) - const chunkText = TEXT_DECODER.decode(chunkBytes) - parts.push(chunkText) - offset += chunkLength - } - if (!foundBreak) throw new CBORError({ message: "Missing break in indefinite-length text string" }) - return { item: parts.join(""), bytesConsumed: offset } - } else { - const { bytesRead, length } = decodeLengthSync(data, 0) - if (data.length < bytesRead + length) throw new CBORError({ message: "Insufficient data for text string" }) - const textBytes = data.slice(bytesRead, bytesRead + length) - const text = TEXT_DECODER.decode(textBytes) - return { item: text, bytesConsumed: bytesRead + length } - } -} - -// Decode an item and return both the item and bytes consumed (sync) -const decodeItemWithLengthSync = (data: Uint8Array, options: CodecOptions): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const majorType = (firstByte >> 5) & 0x07 - let item: CBOR - let bytesConsumed: number - switch (majorType) { - case CBOR_MAJOR_TYPE.UNSIGNED_INTEGER: { - item = decodeUintSync(data) - const additionalInfo = firstByte & 0x1f - bytesConsumed = - additionalInfo < 24 ? 1 : additionalInfo === 24 ? 2 : additionalInfo === 25 ? 3 : additionalInfo === 26 ? 5 : 9 - break - } - case CBOR_MAJOR_TYPE.NEGATIVE_INTEGER: { - item = decodeNintSync(data) - const additionalInfo = firstByte & 0x1f - bytesConsumed = - additionalInfo < 24 ? 1 : additionalInfo === 24 ? 2 : additionalInfo === 25 ? 3 : additionalInfo === 26 ? 5 : 9 - break - } - case CBOR_MAJOR_TYPE.BYTE_STRING: { - const { bytesConsumed: b, item: it } = decodeBytesWithLengthSync(data) - item = it - bytesConsumed = b - break - } - case CBOR_MAJOR_TYPE.TEXT_STRING: { - const { bytesConsumed: b, item: it } = decodeTextWithLengthSync(data) - item = it - bytesConsumed = b - break - } - case CBOR_MAJOR_TYPE.ARRAY: { - const { bytesConsumed: b, item: it } = decodeArrayWithLengthSync(data, options) - item = it - bytesConsumed = b - break - } - case CBOR_MAJOR_TYPE.MAP: { - const { bytesConsumed: b, item: it } = decodeMapWithLengthSync(data, options) - item = it - bytesConsumed = b - break - } - case CBOR_MAJOR_TYPE.TAG: { - const { bytesConsumed: b, item: it } = decodeTagWithLengthSync(data, options) - item = it - bytesConsumed = b - break - } - case CBOR_MAJOR_TYPE.SIMPLE_FLOAT: { - item = decodeSimpleOrFloatSync(data) - const additionalInfo = firstByte & 0x1f - bytesConsumed = - additionalInfo < 24 ? 1 : additionalInfo === 24 ? 2 : additionalInfo === 25 ? 3 : additionalInfo === 26 ? 5 : 9 - break - } - default: - throw new CBORError({ message: `Unsupported major type: ${majorType}` }) - } - return { item, bytesConsumed } -} - -const decodeArrayWithLengthSync = (data: Uint8Array, options: CodecOptions): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo === CBOR_ADDITIONAL_INFO.INDEFINITE) { - const result: Array = [] - let offset = 1 - while (offset < data.length) { - if (data[offset] === 0xff) { - offset++ - break - } - const { bytesConsumed, item } = decodeItemWithLengthSync(data.slice(offset), options) - result.push(item) - offset += bytesConsumed - } - return { item: result, bytesConsumed: offset } - } else { - const { bytesRead, length } = decodeLengthSync(data, 0) - const result: Array = [] - let offset = bytesRead - for (let i = 0; i < length; i++) { - const { bytesConsumed, item } = decodeItemWithLengthSync(data.slice(offset), options) - result.push(item) - offset += bytesConsumed - } - return { item: result, bytesConsumed: offset } - } -} - -const decodeMapWithLengthSync = (data: Uint8Array, options: CodecOptions): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo === CBOR_ADDITIONAL_INFO.INDEFINITE) { - const result = - options.mode === "custom" && options.mapsAsObjects ? ({} as Record) : new Map() - let offset = 1 - while (offset < data.length) { - if (data[offset] === 0xff) { - offset++ - break - } - const { bytesConsumed: keyBytes, item: key } = decodeItemWithLengthSync(data.slice(offset), options) - offset += keyBytes - const { bytesConsumed: valueBytes, item: value } = decodeItemWithLengthSync(data.slice(offset), options) - offset += valueBytes - if (result instanceof Map) { - result.set(key, value) - } else { - result[String(key as any)] = value - } - } - return { item: result, bytesConsumed: offset } - } else { - const { bytesRead, length } = decodeLengthSync(data, 0) - const result = - options.mode === "custom" && options.mapsAsObjects ? ({} as Record) : new Map() - let offset = bytesRead - for (let i = 0; i < length; i++) { - const { bytesConsumed: keyBytes, item: key } = decodeItemWithLengthSync(data.slice(offset), options) - offset += keyBytes - const { bytesConsumed: valueBytes, item: value } = decodeItemWithLengthSync(data.slice(offset), options) - offset += valueBytes - if (result instanceof Map) { - result.set(key, value) - } else { - result[String(key as any)] = value - } - } - return { item: result, bytesConsumed: offset } - } -} - -const decodeTagWithLengthSync = (data: Uint8Array, options: CodecOptions): { item: CBOR; bytesConsumed: number } => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - let tag: number - let dataOffset: number - if (additionalInfo < 24) { - tag = additionalInfo - dataOffset = 1 - } else if (additionalInfo === 24) { - if (data.length < 2) throw new CBORError({ message: "Insufficient data for 1-byte tag" }) - tag = data[1] - dataOffset = 2 - } else if (additionalInfo === 25) { - if (data.length < 3) throw new CBORError({ message: "Insufficient data for 2-byte tag" }) - tag = data[1] * 256 + data[2] - dataOffset = 3 - } else { - throw new CBORError({ message: `Unsupported additional info for tag: ${additionalInfo}` }) - } - const { bytesConsumed, item: innerValue } = decodeItemWithLengthSync(data.slice(dataOffset), options) - if (tag === 2 || tag === 3) { - if (!(innerValue instanceof Uint8Array)) throw new CBORError({ message: `Invalid value for bigint tag ${tag}` }) - const bigintValue = (() => { - let result = 0n - for (let i = 0; i < innerValue.length; i++) result = (result << 8n) + BigInt(innerValue[i]) - return tag === 2 ? result : -1n - result - })() - return { item: bigintValue, bytesConsumed: dataOffset + bytesConsumed } - } - return { item: { _tag: "Tag", tag, value: innerValue }, bytesConsumed: dataOffset + bytesConsumed } -} - -const decodeSimpleOrFloatSync = (data: Uint8Array): CBOR => { - const firstByte = data[0] - const additionalInfo = firstByte & 0x1f - if (additionalInfo < 20) { - // Return unassigned simple values as numbers - return additionalInfo - } else if (additionalInfo === CBOR_SIMPLE.FALSE) { - return false - } else if (additionalInfo === CBOR_SIMPLE.TRUE) { - return true - } else if (additionalInfo === CBOR_SIMPLE.NULL) { - return null - } else if (additionalInfo === CBOR_SIMPLE.UNDEFINED) { - return undefined - } else if (additionalInfo === CBOR_ADDITIONAL_INFO.DIRECT) { - if (data.length < 2) throw new CBORError({ message: "Insufficient data for simple value (one byte)" }) - const simpleValue = data[1] - return simpleValue - } else if (additionalInfo === CBOR_ADDITIONAL_INFO.UINT16) { - if (data.length < 3) throw new CBORError({ message: "Insufficient data for half-precision float" }) - const value = (data[1] << 8) | data[2] - const float = decodeFloat16(value) - return float - } else if (additionalInfo === CBOR_ADDITIONAL_INFO.UINT32) { - if (data.length < 5) throw new CBORError({ message: "Insufficient data for single-precision float" }) - const buffer = data.slice(1, 5) - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) - return view.getFloat32(0, false) - } else if (additionalInfo === CBOR_ADDITIONAL_INFO.UINT64) { - if (data.length < 9) throw new CBORError({ message: "Insufficient data for double-precision float" }) - const buffer = data.slice(1, 9) - const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) - return view.getFloat64(0, false) - } - throw new CBORError({ message: `Unsupported additional info for simple/float: ${additionalInfo}` }) -} - -const decodeLengthSync = (data: Uint8Array, offset: number): { length: number; bytesRead: number } => { - const firstByte = data[offset] - const majorType = (firstByte >> 5) & 0x07 - const additionalInfo = firstByte & 0x1f - let length = 0 - let bytesRead = 0 - if ( - majorType !== CBOR_MAJOR_TYPE.BYTE_STRING && - majorType !== CBOR_MAJOR_TYPE.TEXT_STRING && - majorType !== CBOR_MAJOR_TYPE.ARRAY && - majorType !== CBOR_MAJOR_TYPE.MAP - ) { - throw new CBORError({ message: `Invalid major type for length decoding: ${majorType}` }) - } - if (additionalInfo < 24) { - length = additionalInfo - bytesRead = 1 - } else if (additionalInfo === 24) { - if (data.length < offset + 2) throw new CBORError({ message: "Insufficient data for 1-byte length" }) - length = data[offset + 1] - bytesRead = 2 - } else if (additionalInfo === 25) { - if (data.length < offset + 3) throw new CBORError({ message: "Insufficient data for 2-byte length" }) - length = data[offset + 1] * 256 + data[offset + 2] - bytesRead = 3 - } else if (additionalInfo === 26) { - if (data.length < offset + 5) throw new CBORError({ message: "Insufficient data for 4-byte length" }) - length = (data[offset + 1] << 24) | (data[offset + 2] << 16) | (data[offset + 3] << 8) | data[offset + 4] - bytesRead = 5 - } else { - throw new CBORError({ message: `Unsupported additional info for length: ${additionalInfo}` }) - } - return { length, bytesRead } -}