diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 66663f4a7..faedaed0a 100755 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -53,7 +53,8 @@ "To MessagePack", "From MessagePack", "To Braille", - "From Braille" + "From Braille", + "LV Decode" ] }, { diff --git a/src/core/lib/LengthValue.mjs b/src/core/lib/LengthValue.mjs new file mode 100644 index 000000000..3ec0c5e57 --- /dev/null +++ b/src/core/lib/LengthValue.mjs @@ -0,0 +1,71 @@ +/** + * @author gchq77703 [] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +const defaults = { + location: 0, + bytesInLength: 1, + basicEncodingRules: false +}; + +/** + * Length Value library + */ +export default class LengthValue { + + /** + * LengthValue constructor + */ + constructor(input, options) { + this.input = input; + Object.assign(this, defaults, options); + } + + /** + * @returns {Number} + */ + getLength() { + if (this.basicEncodingRules) { + const bit = this.input[this.location]; + if (bit & 0x80) { + this.bytesInLength = bit & ~0x80; + } else { + this.location++; + return bit & ~0x80; + } + } + + let length = 0; + + for (let i = 0; i < this.bytesInLength; i++) { + length += this.input[this.location] * Math.pow(Math.pow(2, 8), i); + this.location++; + } + + return length; + } + + /** + * @param {Number} length + * @returns {Number[]} + */ + getValue(length) { + const value = []; + + for (let i = 0; i < length; i++) { + value.push(this.input[this.location]); + this.location++; + } + + return value; + } + + /** + * @returns {Boolean} + */ + atEnd() { + return this.input.length <= this.location; + } +} diff --git a/src/core/operations/LVDecode.mjs b/src/core/operations/LVDecode.mjs new file mode 100644 index 000000000..63af65f6b --- /dev/null +++ b/src/core/operations/LVDecode.mjs @@ -0,0 +1,81 @@ +/** + * @author gchq77703 [] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import Operation from "../Operation"; +import LengthValue from "../lib/LengthValue"; + +/** + * From LV Decode operation + */ +class LVDecode extends Operation { + + /** + * LVDecode constructor + */ + constructor() { + super(); + + this.name = "LV Decode"; + this.module = "Default"; + this.description = "Converts a Length-Value (LV) encoded string into a JSON object. Can optionally include a Key / Type entry."; + this.infoURL = "https://wikipedia.org/wiki/KLV"; + this.inputType = "byteArray"; + this.outputType = "JSON"; + this.args = [ + { + name: "Bytes in Key Value", + type: "option", + value: [ + "0 Bytes (No Key)", + "1 Byte", + "2 Bytes", + "4 Bytes" + ] + }, + { + name: "Bytes in Length Value", + type: "option", + value: [ + "1 Byte", + "2 Bytes", + "4 Bytes" + ] + }, + { + name: "Use Basic Encoding Rules", + type: "boolean" + } + ]; + } + + /** + * @param {byteArray} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const bytesInKey = parseInt(args[0].split(" ")[0], 10); + const bytesInLength = parseInt(args[1].split(" ")[0], 10); + const basicEncodingRules = args[2]; + + const lv = new LengthValue(input, { bytesInLength, basicEncodingRules }); + + const data = []; + + while (!lv.atEnd()) { + const key = bytesInKey ? lv.getValue(bytesInKey) : undefined; + const length = lv.getLength(); + const value = lv.getValue(length); + + data.push({ key, length, value }); + } + + return data; + } + +} + +export default LVDecode; diff --git a/test/index.mjs b/test/index.mjs index 8cf69732a..06b2b1810 100644 --- a/test/index.mjs +++ b/test/index.mjs @@ -64,6 +64,7 @@ import "./tests/operations/SetUnion"; import "./tests/operations/SymmetricDifference"; import "./tests/operations/TranslateDateTimeFormat"; import "./tests/operations/Magic"; +import "./tests/operations/LVDecode"; let allTestsPassing = true; const testStatusCounts = { diff --git a/test/tests/operations/LVDecode.mjs b/test/tests/operations/LVDecode.mjs new file mode 100644 index 000000000..4fd7fc831 --- /dev/null +++ b/test/tests/operations/LVDecode.mjs @@ -0,0 +1,56 @@ +/** + * LV Decoder tests. + * + * @author gchq77703 [] + * @copyright Crown Copyright 2018 + * @license Apache-2.0 + */ + +import TestRegister from "../../TestRegister"; + +TestRegister.addTests([ + { + name: "LVDecode: LengthValue", + input: "\x05\x48\x6f\x75\x73\x65\x04\x72\x6f\x6f\x6d\x04\x64\x6f\x6f\x72", + expectedOutput: JSON.stringify([{"length": 5, "value": [72, 111, 117, 115, 101]}, {"length": 4, "value": [114, 111, 111, 109]}, {"length": 4, "value": [100, 111, 111, 114]}]), + recipeConfig: [ + { + "op": "LV Decode", + "args": ["0 Bytes (No Key)", "1 Byte", false] + } + ] + }, + { + name: "LVDecode: LengthValue with BER", + input: "\x05\x48\x6f\x75\x73\x65\x04\x72\x6f\x6f\x6d\x04\x64\x6f\x6f\x72", + expectedOutput: JSON.stringify([{"length": 5, "value": [72, 111, 117, 115, 101]}, {"length": 4, "value": [114, 111, 111, 109]}, {"length": 4, "value": [100, 111, 111, 114]}]), + recipeConfig: [ + { + "op": "LV Decode", + "args": ["0 Bytes (No Key)", "4 Bytes", false] // length value is patently wrong, should be ignored by BER. + } + ] + }, + { + name: "LVDecode: KeyLengthValue", + input: "\x04\x05\x48\x6f\x75\x73\x65\x05\x04\x72\x6f\x6f\x6d\x42\x04\x64\x6f\x6f\x72", + expectedOutput: JSON.stringify([{"key":[4],"length":5,"value":[72,111,117,115,101]},{"key":[5],"length":4,"value":[114,111,111,109]},{"key":[66],"length":4,"value":[100,111,111,114]}]), + recipeConfig: [ + { + "op": "LV Decode", + "args": ["1 Byte", "1 Byte", false] + } + ] + }, + { + name: "LVDecode: KeyLengthValue with BER", + input: "\x04\x05\x48\x6f\x75\x73\x65\x05\x04\x72\x6f\x6f\x6d\x42\x04\x64\x6f\x6f\x72", + expectedOutput: JSON.stringify([{"key":[4],"length":5,"value":[72,111,117,115,101]},{"key":[5],"length":4,"value":[114,111,111,109]},{"key":[66],"length":4,"value":[100,111,111,114]}]), + recipeConfig: [ + { + "op": "LV Decode", + "args": ["1 Byte", "4 Byte", true] // length value is patently wrong, should be ignored by BER. + } + ] + } +]);