diff --git a/src/pds.js b/src/pds.js index 65fb19e..1a75b12 100644 --- a/src/pds.js +++ b/src/pds.js @@ -22,6 +22,30 @@ class CID { // === CBOR ENCODING === // Minimal deterministic CBOR (RFC 8949) - sorted keys, minimal integers +/** + * Encode CBOR type header (major type + length) + * @param {number[]} parts - Array to push bytes to + * @param {number} majorType - CBOR major type (0-7) + * @param {number} length - Value or length to encode + */ +function encodeHead(parts, majorType, length) { + const mt = majorType << 5 + if (length < 24) { + parts.push(mt | length) + } else if (length < 256) { + parts.push(mt | 24, length) + } else if (length < 65536) { + parts.push(mt | 25, length >> 8, length & 0xff) + } else if (length < 4294967296) { + // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow + parts.push(mt | 26, + Math.floor(length / 0x1000000) & 0xff, + Math.floor(length / 0x10000) & 0xff, + Math.floor(length / 0x100) & 0xff, + length & 0xff) + } +} + export function cborEncode(value) { const parts = [] @@ -36,18 +60,18 @@ export function cborEncode(value) { encodeInteger(val) } else if (typeof val === 'string') { const bytes = new TextEncoder().encode(val) - encodeHead(3, bytes.length) // major type 3 = text string + encodeHead(parts, 3, bytes.length) // major type 3 = text string parts.push(...bytes) } else if (val instanceof Uint8Array) { - encodeHead(2, val.length) // major type 2 = byte string + encodeHead(parts, 2, val.length) // major type 2 = byte string parts.push(...val) } else if (Array.isArray(val)) { - encodeHead(4, val.length) // major type 4 = array + encodeHead(parts, 4, val.length) // major type 4 = array for (const item of val) encode(item) } else if (typeof val === 'object') { // Sort keys for deterministic encoding const keys = Object.keys(val).sort() - encodeHead(5, keys.length) // major type 5 = map + encodeHead(parts, 5, keys.length) // major type 5 = map for (const key of keys) { encode(key) encode(val[key]) @@ -55,29 +79,11 @@ export function cborEncode(value) { } } - function encodeHead(majorType, length) { - const mt = majorType << 5 - if (length < 24) { - parts.push(mt | length) - } else if (length < 256) { - parts.push(mt | 24, length) - } else if (length < 65536) { - parts.push(mt | 25, length >> 8, length & 0xff) - } else if (length < 4294967296) { - // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow - parts.push(mt | 26, - Math.floor(length / 0x1000000) & 0xff, - Math.floor(length / 0x10000) & 0xff, - Math.floor(length / 0x100) & 0xff, - length & 0xff) - } - } - function encodeInteger(n) { if (n >= 0) { - encodeHead(0, n) // major type 0 = unsigned int + encodeHead(parts, 0, n) // major type 0 = unsigned int } else { - encodeHead(1, -n - 1) // major type 1 = negative int + encodeHead(parts, 1, -n - 1) // major type 1 = negative int } } @@ -98,26 +104,26 @@ function cborEncodeDagCbor(value) { parts.push(CBOR_FALSE) } else if (typeof val === 'number') { if (Number.isInteger(val) && val >= 0) { - encodeHead(0, val) + encodeHead(parts, 0, val) } else if (Number.isInteger(val) && val < 0) { - encodeHead(1, -val - 1) + encodeHead(parts, 1, -val - 1) } } else if (typeof val === 'string') { const bytes = new TextEncoder().encode(val) - encodeHead(3, bytes.length) + encodeHead(parts, 3, bytes.length) parts.push(...bytes) } else if (val instanceof CID) { // CID - encode with CBOR tag 42 + 0x00 prefix parts.push(0xd8, CBOR_TAG_CID) - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix parts.push(0x00) // multibase identity prefix parts.push(...val.bytes) } else if (val instanceof Uint8Array) { // Regular byte string - encodeHead(2, val.length) + encodeHead(parts, 2, val.length) parts.push(...val) } else if (Array.isArray(val)) { - encodeHead(4, val.length) + encodeHead(parts, 4, val.length) for (const item of val) encode(item) } else if (typeof val === 'object') { // DAG-CBOR: sort keys by length first, then lexicographically @@ -126,34 +132,16 @@ function cborEncodeDagCbor(value) { if (a.length !== b.length) return a.length - b.length return a < b ? -1 : a > b ? 1 : 0 }) - encodeHead(5, keys.length) + encodeHead(parts, 5, keys.length) for (const key of keys) { const keyBytes = new TextEncoder().encode(key) - encodeHead(3, keyBytes.length) + encodeHead(parts, 3, keyBytes.length) parts.push(...keyBytes) encode(val[key]) } } } - function encodeHead(majorType, length) { - const mt = majorType << 5 - if (length < 24) { - parts.push(mt | length) - } else if (length < 256) { - parts.push(mt | 24, length) - } else if (length < 65536) { - parts.push(mt | 25, length >> 8, length & 0xff) - } else if (length < 4294967296) { - // Use Math.floor instead of bitshift to avoid 32-bit signed integer overflow - parts.push(mt | 26, - Math.floor(length / 0x1000000) & 0xff, - Math.floor(length / 0x10000) & 0xff, - Math.floor(length / 0x100) & 0xff, - length & 0xff) - } - } - encode(value) return new Uint8Array(parts) } @@ -580,19 +568,19 @@ function cborEncodeMstNode(node) { if (val === null || val === undefined) { parts.push(CBOR_NULL) } else if (typeof val === 'number') { - encodeHead(0, val) // unsigned int + encodeHead(parts, 0, val) // unsigned int } else if (val instanceof CID) { // CID - encode with CBOR tag 42 + 0x00 prefix (DAG-CBOR CID link) parts.push(0xd8, CBOR_TAG_CID) - encodeHead(2, val.bytes.length + 1) // +1 for 0x00 prefix + encodeHead(parts, 2, val.bytes.length + 1) // +1 for 0x00 prefix parts.push(0x00) // multibase identity prefix parts.push(...val.bytes) } else if (val instanceof Uint8Array) { // Regular bytes - encodeHead(2, val.length) + encodeHead(parts, 2, val.length) parts.push(...val) } else if (Array.isArray(val)) { - encodeHead(4, val.length) + encodeHead(parts, 4, val.length) for (const item of val) encode(item) } else if (typeof val === 'object') { // Sort keys for deterministic encoding (DAG-CBOR style) @@ -603,11 +591,11 @@ function cborEncodeMstNode(node) { if (a.length !== b.length) return a.length - b.length return a < b ? -1 : a > b ? 1 : 0 }) - encodeHead(5, keys.length) + encodeHead(parts, 5, keys.length) for (const key of keys) { // Encode key as text string const keyBytes = new TextEncoder().encode(key) - encodeHead(3, keyBytes.length) + encodeHead(parts, 3, keyBytes.length) parts.push(...keyBytes) // Encode value encode(val[key]) @@ -615,17 +603,6 @@ function cborEncodeMstNode(node) { } } - function encodeHead(majorType, length) { - const mt = majorType << 5 - if (length < 24) { - parts.push(mt | length) - } else if (length < 256) { - parts.push(mt | 24, length) - } else if (length < 65536) { - parts.push(mt | 25, length >> 8, length & 0xff) - } - } - encode(node) return new Uint8Array(parts) }