diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs index f74b42d6..d64bf763 100755 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -113,6 +113,7 @@ class Ingredient { return data; } case "number": + if (data === null) return data; number = parseFloat(data); if (isNaN(number)) { const sample = Utils.truncate(data.toString(), 10); diff --git a/src/core/lib/Hex.mjs b/src/core/lib/Hex.mjs index 5ae06a7e..0108eabd 100644 --- a/src/core/lib/Hex.mjs +++ b/src/core/lib/Hex.mjs @@ -23,25 +23,42 @@ import Utils from "../Utils.mjs"; * * // returns "0a:14:1e" * toHex([10,20,30], ":"); + * + * // returns "0x0a,0x14,0x1e" + * toHex([10,20,30], "0x", 2, ",") */ -export function toHex(data, delim=" ", padding=2) { +export function toHex(data, delim=" ", padding=2, extraDelim="", lineSize=0) { if (!data) return ""; if (data instanceof ArrayBuffer) data = new Uint8Array(data); let output = ""; + const prepend = (delim === "0x" || delim === "\\x"); for (let i = 0; i < data.length; i++) { - output += data[i].toString(16).padStart(padding, "0") + delim; + const hex = data[i].toString(16).padStart(padding, "0"); + if (prepend) { + output += delim + hex; + } else { + output += hex + delim; + } + if (extraDelim) { + output += extraDelim; + } + // Add LF after each lineSize amount of bytes but not at the end + if ((i !== data.length - 1) && ((i + 1) % lineSize === 0)) { + output += "\n"; + } } - // Add \x or 0x to beginning - if (delim === "0x") output = "0x" + output; - if (delim === "\\x") output = "\\x" + output; - - if (delim.length) - return output.slice(0, -delim.length); - else + // Remove the extraDelim at the end (if there is); + // and remove the delim at the end, but if it's prepended there's nothing to remove + const rTruncLen = extraDelim.length + (prepend ? 0 : delim.length); + if (rTruncLen) { + // If rTruncLen === 0 then output.slice(0,0) will be returned, which is nothing + return output.slice(0, -rTruncLen); + } else { return output; + } } @@ -87,7 +104,7 @@ export function toHexFast(data) { */ export function fromHex(data, delim="Auto", byteLen=2) { if (delim !== "None") { - const delimRegex = delim === "Auto" ? /[^a-f\d]/gi : Utils.regexRep(delim); + const delimRegex = delim === "Auto" ? /[^a-f\d]|(0x)/gi : Utils.regexRep(delim); data = data.replace(delimRegex, ""); } diff --git a/src/core/operations/ToHex.mjs b/src/core/operations/ToHex.mjs index 6ae48da9..25d4e059 100644 --- a/src/core/operations/ToHex.mjs +++ b/src/core/operations/ToHex.mjs @@ -5,7 +5,7 @@ */ import Operation from "../Operation.mjs"; -import {toHex, TO_HEX_DELIM_OPTIONS} from "../lib/Hex.mjs"; +import {toHex} from "../lib/Hex.mjs"; import Utils from "../Utils.mjs"; /** @@ -29,7 +29,12 @@ class ToHex extends Operation { { name: "Delimiter", type: "option", - value: TO_HEX_DELIM_OPTIONS + value: ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "0x", "0x with comma", "\\x", "None"] + }, + { + name: "Bytes per line", + type: "number", + value: 0 } ]; } @@ -40,8 +45,16 @@ class ToHex extends Operation { * @returns {string} */ run(input, args) { - const delim = Utils.charRep(args[0] || "Space"); - return toHex(new Uint8Array(input), delim, 2); + let delim, comma; + if (args[0] === "0x with comma") { + delim = "0x"; + comma = ","; + } else { + delim = Utils.charRep(args[0] || "Space"); + } + const lineSize = args[1]; + + return toHex(new Uint8Array(input), delim, 2, comma, lineSize); } /** @@ -54,17 +67,31 @@ class ToHex extends Operation { * @returns {Object[]} pos */ highlight(pos, args) { - const delim = Utils.charRep(args[0] || "Space"), - len = delim === "\r\n" ? 1 : delim.length; - - pos[0].start = pos[0].start * (2 + len); - pos[0].end = pos[0].end * (2 + len) - len; - - // 0x and \x are added to the beginning if they are selected, so increment the positions accordingly - if (delim === "0x" || delim === "\\x") { - pos[0].start += 2; - pos[0].end += 2; + let delim, commaLen; + if (args[0] === "0x with comma") { + delim = "0x"; + commaLen = 1; + } else { + delim = Utils.charRep(args[0] || "Space"); } + + const lineSize = args[1], + len = (delim === "\r\n" ? 1 : delim.length) + commaLen; + + const countLF = function(p) { + // Count the number of LFs from 0 upto p + return (p / lineSize | 0) - (p >= lineSize && p % lineSize === 0); + }; + + pos[0].start = pos[0].start * (2 + len) + countLF(pos[0].start); + pos[0].end = pos[0].end * (2 + len) + countLF(pos[0].end); + + // if the deliminators are not prepended, trim the trailing deliminator + if (!(delim === "0x" || delim === "\\x")) { + pos[0].end -= delim.length; + } + // if there is comma, trim the trailing comma + pos[0].end -= commaLen; return pos; } @@ -78,20 +105,26 @@ class ToHex extends Operation { * @returns {Object[]} pos */ highlightReverse(pos, args) { - const delim = Utils.charRep(args[0] || "Space"), - len = delim === "\r\n" ? 1 : delim.length, - width = len + 2; - - // 0x and \x are added to the beginning if they are selected, so increment the positions accordingly - if (delim === "0x" || delim === "\\x") { - if (pos[0].start > 1) pos[0].start -= 2; - else pos[0].start = 0; - if (pos[0].end > 1) pos[0].end -= 2; - else pos[0].end = 0; + let delim, commaLen; + if (args[0] === "0x with comma") { + delim = "0x"; + commaLen = 1; + } else { + delim = Utils.charRep(args[0] || "Space"); } - pos[0].start = pos[0].start === 0 ? 0 : Math.round(pos[0].start / width); - pos[0].end = pos[0].end === 0 ? 0 : Math.ceil(pos[0].end / width); + const lineSize = args[1], + len = (delim === "\r\n" ? 1 : delim.length) + commaLen, + width = len + 2; + + const countLF = function(p) { + // Count the number of LFs from 0 up to p + const lineLength = width * lineSize; + return (p / lineLength | 0) - (p >= lineLength && p % lineLength === 0); + }; + + pos[0].start = pos[0].start === 0 ? 0 : Math.round((pos[0].start - countLF(pos[0].start)) / width); + pos[0].end = pos[0].end === 0 ? 0 : Math.ceil((pos[0].end - countLF(pos[0].end)) / width); return pos; } } diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index bf440414..3257905d 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -45,6 +45,7 @@ import "./tests/Gzip.mjs"; import "./tests/Gunzip.mjs"; import "./tests/Hash.mjs"; import "./tests/HaversineDistance.mjs"; +import "./tests/Hex.mjs"; import "./tests/Hexdump.mjs"; import "./tests/Image.mjs"; import "./tests/IndexOfCoincidence.mjs"; diff --git a/tests/operations/tests/Hex.mjs b/tests/operations/tests/Hex.mjs new file mode 100644 index 00000000..91520d88 --- /dev/null +++ b/tests/operations/tests/Hex.mjs @@ -0,0 +1,97 @@ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "ASCII to Hex stream", + input: "aberystwyth", + expectedOutput: "6162657279737477797468", + recipeConfig: [ + { + "op": "To Hex", + "args": [ + "None", + 0 + ] + }, + ] + }, + { + name: "ASCII to Hex with colon deliminator ", + input: "aberystwyth", + expectedOutput: "61:62:65:72:79:73:74:77:79:74:68", + recipeConfig: [ + { + "op": "To Hex", + "args": [ + "Colon", + 0 + ] + } + ] + }, + { + name: "ASCII to 0x Hex with comma", + input: "aberystwyth", + expectedOutput: "0x61,0x62,0x65,0x72,0x79,0x73,0x74,0x77,0x79,0x74,0x68", + recipeConfig: [ + { + "op": "To Hex", + "args": [ + "0x with comma", + 0 + ] + } + ] + }, + { + name: "ASCII to 0x Hex with comma and line breaks", + input: "aberystwyth", + expectedOutput: "0x61,0x62,0x65,0x72,\n0x79,0x73,0x74,0x77,\n0x79,0x74,0x68", + recipeConfig: [ + { + "op": "To Hex", + "args": [ + "0x with comma", + 4 + ] + } + ] + }, + { + name: "Hex stream to UTF-8", + input: "e69591e69591e5ada9e5ad90", + expectedOutput: "救救孩子", + recipeConfig: [ + { + "op": "From Hex", + "args": [ + "Auto" + ] + } + ] + + }, + { + name: "Multiline 0x hex to ASCII", + input: "0x49,0x20,0x73,0x61,0x77,0x20,0x6d,0x79,0x73,0x65,0x6c,0x66,0x20,0x73,0x69,\ +0x74,0x74,0x69,0x6e,0x67,0x20,0x69,0x6e,0x20,0x74,0x68,0x65,0x20,0x63,0x72,\ +0x6f,0x74,0x63,0x68,0x20,0x6f,0x66,0x20,0x74,0x68,0x65,0x20,0x74,0x68,0x69,\ +0x73,0x20,0x66,0x69,0x67,0x20,0x74,0x72,0x65,0x65,0x2c,0x20,0x73,0x74,0x61,\ +0x72,0x76,0x69,0x6e,0x67,0x20,0x74,0x6f,0x20,0x64,0x65,0x61,0x74,0x68,0x2c,\ +0x20,0x6a,0x75,0x73,0x74,0x20,0x62,0x65,0x63,0x61,0x75,0x73,0x65,0x20,0x49,\ +0x20,0x63,0x6f,0x75,0x6c,0x64,0x6e,0x27,0x74,0x20,0x6d,0x61,0x6b,0x65,0x20,\ +0x75,0x70,0x20,0x6d,0x79,0x20,0x6d,0x69,0x6e,0x64,0x20,0x77,0x68,0x69,0x63,\ +0x68,0x20,0x6f,0x66,0x20,0x74,0x68,0x65,0x20,0x66,0x69,0x67,0x73,0x20,0x49,\ +0x20,0x77,0x6f,0x75,0x6c,0x64,0x20,0x63,0x68,0x6f,0x6f,0x73,0x65,0x2e", + expectedOutput: "I saw myself sitting in the crotch of the this fig tree, starving to death, just because I couldn't make up my mind which of the figs I would choose.", + recipeConfig: [ + { + "op": "From Hex", + "args": [ + "Auto" + ] + } + ] + + } +]);