diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 66663f4a7f..faedaed0a0 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 0000000000..3ec0c5e571
--- /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 0000000000..63af65f6be
--- /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 8cf69732ad..06b2b18100 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 0000000000..4fd7fc8314
--- /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.
+ }
+ ]
+ }
+]);