From 3051cf6aceb49717a50c97319a3e6cc8793fc674 Mon Sep 17 00:00:00 2001 From: Sudip Bhattarai Date: Tue, 10 Sep 2024 14:25:20 +0545 Subject: [PATCH] Support re-encoding of indefinite length types. --- decode.js | 57 +++++++++++++++++++++++++++++++++++++++++++-- encode.js | 64 ++++++++++++++++++++++++++++++++++++++++----------- tests/test.js | 30 ++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 16 deletions(-) diff --git a/decode.js b/decode.js index 362ff36..12a7be1 100644 --- a/decode.js +++ b/decode.js @@ -50,6 +50,49 @@ try { inlineObjectReadThreshold = Infinity } +// required in order to make toJSON work with String proxy +function stringProxy(strings) { + const combinedString =strings.join(''); + const stringObject =new String(combinedString) + // const stringObject = {value:combinedString} + const handlers ={ + toJSON : () => combinedString, + valueOf : () => combinedString, + toString : () => combinedString, + _cbor_indef: strings + } + return new Proxy(stringObject, { + get(target, prop) { + if (typeof combinedString[prop] === 'function') { + if(prop == 'constructor'){ + return target[prop] + } + return (combinedString[prop]).bind(combinedString); + } + if(handlers[prop]){ + return handlers[prop] + } + return combinedString[prop]; + } + }); +} + +function indefWrapper(target, extra_prop, value) { + return new Proxy(target, { + get(target, prop) { + if (typeof target[prop] === 'function') { + if(prop == 'constructor'){ + return target[prop] + } + return target[prop].bind(target); + } + if (prop === extra_prop) { + return value; + } + return target[prop]; + } + }); +} export class Decoder { @@ -69,6 +112,7 @@ export class Decoder { this.mapKey = new Map() for (let [k,v] of Object.entries(options.keyMap)) this.mapKey.set(v,k) } + options.preserveIndefiniteLength = options.preserveIndefiniteLength === true ? true:false } Object.assign(this, options) } @@ -296,7 +340,6 @@ export function read() { switch(majorType) { case 2: // byte string case 3: // text string - throw new Error('Indefinite length not supported for byte or text strings') case 4: // array let array = [] let value, i = 0 @@ -304,7 +347,17 @@ export function read() { if (i >= maxArraySize) throw new Error(`Array length exceeds ${maxArraySize}`) array[i++] = value } + if(currentDecoder.preserveIndefiniteLength){// decoded data for later re-encode + if (majorType == 2) + return indefWrapper(Buffer.concat(array),"_cbor_indef",array) + else if (majorType == 4) + return indefWrapper(array,"_cbor_indef",true) + else if (majorType == 3) + return stringProxy(array) + } + return majorType == 4 ? array : majorType == 3 ? array.join('') : Buffer.concat(array) + case 5: // map let key if (currentDecoder.mapsAsObjects) { @@ -347,7 +400,7 @@ export function read() { map.set(key, read()) } } - return map + return currentDecoder.preserveIndefiniteLength?indefWrapper(map,"_cbor_indef",true):map } case 7: return STOP_CODE diff --git a/encode.js b/encode.js index 1e22443..02f0186 100644 --- a/encode.js +++ b/encode.js @@ -447,6 +447,18 @@ export class Encoder extends Decoder { position += 8 } } else if (type === 'object') { + if( value instanceof String){ + if (value._cbor_indef && this.preserveIndefiniteLength){ + target[position++]= 0x7f + value._cbor_indef.forEach(str=>{ + encode(str) + }) + target[position++]=0xff + }else{ + encode(String(value)) + } + return + } if (!value) target[position++] = 0xf6 else { @@ -471,16 +483,26 @@ export class Encoder extends Decoder { if (constructor === Object) { writeObject(value) } else if (constructor === Array) { + const write=()=>{ + for (let i = 0; i < length; i++) { + encode(value[i]) + } + } length = value.length + if (value._cbor_indef && this.preserveIndefiniteLength){ + target[position++]=0x9f + write() + target[position++]= 0xff + return + } if (length < 0x18) { target[position++] = 0x80 | length } else { writeArrayHeader(length) } - for (let i = 0; i < length; i++) { - encode(value[i]) - } + write() } else if (constructor === Map) { + if (this.mapsAsObjects ? this.useTag259ForMaps !== false : this.useTag259ForMaps) { // use Tag 259 (https://github.com/shanewholloway/js-cbor-codec/blob/master/docs/CBOR-259-spec--explicit-maps.md) for maps if the user wants it that way target[position++] = 0xd9 @@ -488,7 +510,11 @@ export class Encoder extends Decoder { target[position++] = 3 } length = value.size - if (length < 0x18) { + + if(encoder.preserveIndefiniteLength && value._cbor_indef){ + target[position++]=0xbf + } + else if (length < 0x18) { target[position++] = 0xa0 | length } else if (length < 0x100) { target[position++] = 0xb8 @@ -513,6 +539,10 @@ export class Encoder extends Decoder { encode(entryValue) } } + if(encoder.preserveIndefiniteLength && value._cbor_indef){ + target[position++]=0xff + } + } else { for (let i = 0, l = extensions.length; i < l; i++) { let extensionClass = extensionClasses[i] @@ -988,8 +1018,8 @@ function isBlob(object) { return tag === 'Blob' || tag === 'File'; } function findRepetitiveStrings(value, packedValues) { - switch(typeof value) { - case 'string': + const valType = typeof value + if (valType == 'string' || value instanceof String){ if (value.length > 3) { if (packedValues.objectMap[value] > -1 || packedValues.values.length >= packedValues.maxValues) return @@ -1013,9 +1043,8 @@ function findRepetitiveStrings(value, packedValues) { } } } - break - case 'object': - if (value) { + } + else if (valType === 'object'){ if (value instanceof Array) { for (let i = 0, l = value.length; i < l; i++) { findRepetitiveStrings(value[i], packedValues) @@ -1031,10 +1060,9 @@ function findRepetitiveStrings(value, packedValues) { } } } - } - break - case 'function': console.log(value) - } + } + else if(valType ==='function') + console.log(value) } const isLittleEndianMachine = new Uint8Array(new Uint16Array([1]).buffer)[0] == 1 extensionClasses = [ Date, Set, Error, RegExp, Tag, ArrayBuffer, @@ -1095,7 +1123,7 @@ extensions = [{ // Date } // else no tag }, encode(typedArray, encode, makeRoom) { - writeBuffer(typedArray, makeRoom) + writeBuffer.bind(this)(typedArray, makeRoom) } }, typedArrayEncoder(68, 1), @@ -1152,6 +1180,14 @@ function typedArrayEncoder(tag, size) { } function writeBuffer(buffer, makeRoom) { let length = buffer.byteLength + if(buffer._cbor_indef && this && this.preserveIndefiniteLength){ + target[position++]= 0x5f + buffer._cbor_indef.forEach(buf=>{ + writeBuffer(buf,makeRoom) + }) + target[position++] = 0xff + return; + } if (length < 0x18) { target[position++] = 0x40 + length } else if (length < 0x100) { diff --git a/tests/test.js b/tests/test.js index fefda28..42c4357 100644 --- a/tests/test.js +++ b/tests/test.js @@ -25,6 +25,8 @@ function tryRequire(module) { var assert = chai.assert var Encoder = CBOR.Encoder +var Decoder = CBOR.Encoder + var EncoderStream = CBOR.EncoderStream var DecoderStream = CBOR.DecoderStream var decode = CBOR.decode @@ -47,6 +49,10 @@ try { var ITERATIONS = 4000 +if(process.env.LOG_LEVEL !=='DEBUG' ){ + console.debug=()=>{} +} + suite('CBOR basic tests', function(){ test('encode/decode with keyMaps (basic)', function() { var data = senmlData @@ -746,6 +752,30 @@ suite('CBOR basic tests', function(){ var deserialized = decode(serialized) assert.deepEqual(deserialized, data) }) + test ('re-encode indefinite length',()=>{ + const decoder=new Decoder({preserveIndefiniteLength: true}) + const encoder=new Encoder({preserveIndefiniteLength: true}) + + + const data=Buffer.from('9F81007F6563626F72786563626F7278FF5F43ABCDEFFF7F6563626F7278FFBF6563626F7278F5FFFF','hex') + console.debug("re-encode indefinite length:initial ",data.toString('hex')) + + const decoded = decoder.decode(data) + console.debug("re-encode indefinite length:decoded ",decoded) + + const reEncoded= encoder.encode(decoded) + console.debug("re-encode indefinite length:re-encoded",reEncoded.toString('hex')) + + const defaultEncoded = CBOR.encode(decoded) + console.debug("re-encode not preserved: encoded ",defaultEncoded.toString('hex')) + assert.equal(defaultEncoded.toString('hex'),'8581006a63626f727863626f727843abcdef6563626f7278d90103a16563626f7278f5') + + + const reDecoded= decoder.decode(reEncoded) + console.debug("re-encode indefinite length:re-decoded",reDecoded) + + assert.equal(data.toString('hex'), encoder.encode(reDecoded).toString('hex')) + }) test('decodeMultiple', () => { let values = CBOR.decodeMultiple(new Uint8Array([1, 2, 3, 4])) assert.deepEqual(values, [1, 2, 3, 4])