From 164c20aa32a819b311ba0196b8ff902d0d91c59e Mon Sep 17 00:00:00 2001 From: engin0223 Date: Sun, 31 May 2026 21:18:09 +0300 Subject: [PATCH 01/17] feat(thrift): add Thrift serialization and deserialization operations --- src/core/config/Categories.json | 4 +- src/core/operations/ThriftDeserialize.mjs | 317 ++++++++++++++++++++++ src/core/operations/ThriftSerialize.mjs | 161 +++++++++++ tests/operations/tests/Thrift.mjs | 123 +++++++++ 4 files changed, 604 insertions(+), 1 deletion(-) create mode 100644 src/core/operations/ThriftDeserialize.mjs create mode 100644 src/core/operations/ThriftSerialize.mjs create mode 100644 tests/operations/tests/Thrift.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 3404dc6d..c20a12a0 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -82,7 +82,9 @@ "Rison Decode", "To Modhex", "From Modhex", - "MIME Decoding" + "MIME Decoding", + "Thrift Serialize", + "Thrift Deserialize" ] }, { diff --git a/src/core/operations/ThriftDeserialize.mjs b/src/core/operations/ThriftDeserialize.mjs new file mode 100644 index 00000000..7b04e7ce --- /dev/null +++ b/src/core/operations/ThriftDeserialize.mjs @@ -0,0 +1,317 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; + +/** + * Operation to decode Apache Thrift binary blobs into JSON structures. + */ +class ThriftDeserialize extends Operation { + /** + * ThriftDeserialize constructor. + */ + constructor() { + super(); + this.name = "Thrift Deserialize"; + this.module = "Default"; + this.description = "Decodes an Apache Thrift binary blob into a JSON representation without requiring an IDL schema. Supports Binary and Compact protocols."; + this.infoURL = "https://github.com/apache/thrift/tree/master/doc/specs"; + this.inputType = "ArrayBuffer"; + this.outputType = "String"; + this.args = [ + { + "name": "Protocol", + "type": "option", + "value": ["TBinaryProtocol", "TCompactProtocol"] + } + ]; + } + + /** + * Runs the operation. + * + * @param {ArrayBuffer} input + * @param {Array} args + * @returns {string} + */ + run(input, args) { + const protocol = args[0]; + const data = new DataView(input); + if (input.byteLength === 0) return ""; + + let decodedObject = {}; + try { + if (protocol === "TBinaryProtocol") { + decodedObject = this.parseBinaryProtocol(data, 0).result; + } else if (protocol === "TCompactProtocol") { + decodedObject = this.parseCompactProtocol(data, 0).result; + } + return JSON.stringify(decodedObject, null, 4); + } catch (err) { + return `Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`; + } + } + + // --- TBinaryProtocol Implementation --- + + /** + * Parses the incoming schema using TBinaryProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ + parseBinaryProtocol(data, offset) { + const result = {}; + while (offset < data.byteLength) { + const fieldType = data.getUint8(offset++); + if (fieldType === 0) break; // T_STOP + + const fieldId = data.getInt16(offset); + offset += 2; + + const parsed = this.readBinaryType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: this.getBinaryTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; + } + + /** + * Reads and transforms binary datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ + readBinaryType(data, offset, type) { + let value; + switch (type) { + case 2: // BOOL + value = data.getUint8(offset++) === 1; + break; + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // DOUBLE + value = data.getFloat64(offset); + offset += 8; + break; + case 6: // I16 + value = data.getInt16(offset); + offset += 2; + break; + case 8: // I32 + value = data.getInt32(offset); + offset += 4; + break; + case 10: // I64 + // Note: using BigInt to avoid precision loss on 64-bit integers + value = data.getBigInt64(offset).toString(); + offset += 8; + break; + case 11: { // BINARY/STRING + const strLen = data.getInt32(offset); + offset += 4; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = this.parseBinaryProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + case 13: { // MAP + const keyType = data.getUint8(offset++); + const valType = data.getUint8(offset++); + const mapSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < mapSize; i++) { + const k = this.readBinaryType(data, offset, keyType); + offset = k.offset; + const v = this.readBinaryType(data, offset, valType); + offset = v.offset; + value.push({ key: k.value, val: v.value }); + } + break; + } + case 14: // SET + case 15: { // LIST + const elemType = data.getUint8(offset++); + const listSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < listSize; i++) { + const elem = this.readBinaryType(data, offset, elemType); + value.push(elem.value); + offset = elem.offset; + } + break; + } + default: + throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); + } + return { value, offset }; + } + + /** + * Returns string names for Binary protocol types. + * + * @param {number} type + * @returns {string} + */ + getBinaryTypeName(type) { + const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; + return types[type] || `UNKNOWN(${type})`; + } + + // --- TCompactProtocol Implementation --- + + /** + * Parses the incoming schema using TCompactProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ + parseCompactProtocol(data, offset) { + const result = {}; + let lastFieldId = 0; + + while (offset < data.byteLength) { + const byte = data.getUint8(offset++); + if (byte === 0) break; // STOP field + + const modifier = (byte & 0xf0) >> 4; + const fieldType = byte & 0x0f; + + let fieldId; + if (modifier === 0) { + // Long form: read zigzag varint field ID + const idParsed = this.readVarint(data, offset); + fieldId = this.fromZigZag(idParsed.value); + offset = idParsed.offset; + } else { + // Short form: delta + fieldId = lastFieldId + modifier; + } + lastFieldId = fieldId; + + // Types 1 and 2 are boolean true/false encoded directly in the modifier + if (fieldType === 1) { + result[`field_${fieldId}`] = { type: "BOOL", value: true }; + continue; + } else if (fieldType === 2) { + result[`field_${fieldId}`] = { type: "BOOL", value: false }; + continue; + } + + const parsed = this.readCompactType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: this.getCompactTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; + } + + /** + * Reads and transforms compact datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ + readCompactType(data, offset, type) { + let value, varintParsed; + switch (type) { + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // I16 + case 5: // I32 + case 6: // I64 + varintParsed = this.readVarint(data, offset); + value = this.fromZigZag(varintParsed.value); // Decodes ZigZag + offset = varintParsed.offset; + break; + case 7: // DOUBLE + value = data.getFloat64(offset, true); // Little endian + offset += 8; + break; + case 8: { // BINARY/STRING + varintParsed = this.readVarint(data, offset); + const strLen = Number(varintParsed.value); // Not zigzagged + offset = varintParsed.offset; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = this.parseCompactProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact + // Implement based on the spec provided (e.g., sssstttt for lists) + default: + throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); + } + return { value, offset }; + } + + /** + * Returns string names for Compact protocol types. + * + * @param {number} type + * @returns {string} + */ + getCompactTypeName(type) { + const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; + return types[type] || `UNKNOWN(${type})`; + } + + /** + * Variable-length integer parsing logic helper. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ + readVarint(data, offset) { + let result = 0n; + let shift = 0n; + while (true) { + if (offset >= data.byteLength) throw new Error("EOF reading varint"); + const byte = BigInt(data.getUint8(offset++)); + result |= (byte & 0x7fn) << shift; + if ((byte & 0x80n) === 0n) break; + shift += 7n; + } + return { value: result, offset: offset }; + } + + /** + * Decodes ZigZag parameters to system numbers. + * + * @param {bigint} n + * @returns {bigint} + */ + fromZigZag(n) { + // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation + return (n >> 1n) ^ -(n & 1n); + } +} + +export default ThriftDeserialize; diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs new file mode 100644 index 00000000..ae20948b --- /dev/null +++ b/src/core/operations/ThriftSerialize.mjs @@ -0,0 +1,161 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Operation to encode a JSON structure into Apache Thrift TBinaryProtocol binary format. + */ +class ThriftSerialize extends Operation { + /** + * ThriftSerialize constructor. + */ + constructor() { + super(); + this.name = "Thrift Serialize"; + this.module = "Default"; + this.description = "Encodes a JSON representation back into an Apache Thrift TBinaryProtocol binary format."; + this.infoURL = "https://github.com/apache/thrift/blob/master/doc/specs/thrift-binary-encoding.md"; + this.inputType = "String"; + this.outputType = "ArrayBuffer"; + this.args = []; + } + + /** + * Runs the operation. + * + * @param {string} input + * @param {Array} args + * @returns {ArrayBuffer} + */ + run(input, args) { + if (!input || input.trim() === "") return new ArrayBuffer(0); + + let parsedInput; + try { + parsedInput = JSON.parse(input); + } catch (e) { + throw new Error("Input must be valid JSON."); + } + + const bytes = []; + this.buildBinaryStruct(parsedInput, bytes); + return new Uint8Array(bytes).buffer; + } + + /** + * Recursively parses the JSON object to build out the Thrift byte array structure. + * + * @param {Object} jsonStruct + * @param {number[]} bytes + */ + buildBinaryStruct(jsonStruct, bytes) { + const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; + + for (const [key, fieldData] of Object.entries(jsonStruct)) { + const fieldId = parseInt(key.replace("field_", ""), 10); + if (isNaN(fieldId)) continue; + + const typeName = fieldData.type; + const fieldType = typeMap[typeName]; + const value = fieldData.value; + + // 1. Write Field Type (1 byte) + bytes.push(fieldType); + // 2. Write Field ID (2 bytes, Big Endian) + bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); + + // 3. Write Value + this.writeValue(typeName, value, bytes, typeMap); + } + + // Write T_STOP to close the struct + bytes.push(0); + } + + /** + * Serializes values into the byte stream according to their specific Thrift types. + * + * @param {string} typeName + * @param {*} value + * @param {number[]} bytes + * @param {Object} typeMap + */ + writeValue(typeName, value, bytes, typeMap) { + switch (typeName) { + case "BOOL": + bytes.push(value ? 1 : 0); + break; + case "I8": + bytes.push(value & 0xFF); + break; + case "I16": + bytes.push((value >> 8) & 0xFF, value & 0xFF); + break; + case "I32": + bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + break; + case "I64": { + const bigVal = BigInt(value); + for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); + break; + } + case "DOUBLE": { + // 8 bytes IEEE 754 floating point (Big Endian) + const floatView = new DataView(new ArrayBuffer(8)); + floatView.setFloat64(0, value, false); + for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); + break; + } + case "BINARY": { + const textEncoder = new TextEncoder(); + const strBytes = textEncoder.encode(value); + const len = strBytes.length; + bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); + strBytes.forEach(b => bytes.push(b)); + break; + } + case "STRUCT": + // Recursively build nested structs + this.buildBinaryStruct(value, bytes); + break; + case "LIST": + case "SET": { + // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } + const elType = typeMap[value.elementType]; + bytes.push(elType); // 1 byte element type + + const listSize = value.elements.length; + bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size + + // Write each element recursively + value.elements.forEach(el => this.writeValue(value.elementType, el, bytes, typeMap)); + break; + } + case "MAP": { + // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } + const kType = typeMap[value.keyType]; + const vType = typeMap[value.valType]; + bytes.push(kType, vType); // 1 byte key type, 1 byte val type + + const mapSize = value.elements.length; + bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size + + // Write pairs + value.elements.forEach(pair => { + this.writeValue(value.keyType, pair.key, bytes, typeMap); + this.writeValue(value.valType, pair.val, bytes, typeMap); + }); + break; + } + default: + throw new Error(`Unsupported serialization type: ${typeName}`); + } + } +} + +export default ThriftSerialize; diff --git a/tests/operations/tests/Thrift.mjs b/tests/operations/tests/Thrift.mjs new file mode 100644 index 00000000..1d0785f6 --- /dev/null +++ b/tests/operations/tests/Thrift.mjs @@ -0,0 +1,123 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +// Helper to generate the exact 4-space indented JSON strings produced by the operation +const formatJson = (obj) => JSON.stringify(obj, null, 4); + +TestRegister.addTests([ + // ========================================== + // TBinaryProtocol Serialization Tests + // ========================================== + { + name: "Thrift Serialize: TBinaryProtocol (Basic Types: I32, BINARY, BOOL)", + input: JSON.stringify({ + "field_1": { "type": "I32", "value": 1337 }, + "field_2": { "type": "BINARY", "value": "Test" }, + "field_3": { "type": "BOOL", "value": true } + }), + // Hex breakdown: + // Field 1 (I32): 08 00 01 00 00 05 39 + // Field 2 (STR): 0b 00 02 00 00 00 04 54 65 73 74 + // Field 3 (BOOL): 02 00 03 01 + // STOP: 00 + expectedOutput: "08 00 01 00 00 05 39 0b 00 02 00 00 00 04 54 65 73 74 02 00 03 01 00", + recipeConfig: [ + { op: "Thrift Serialize", args: ["TBinaryProtocol"] }, + { op: "To Hex", args: ["Space", 0] } + ] + }, + { + name: "Thrift Serialize: TBinaryProtocol (List of I32)", + input: JSON.stringify({ + "field_1": { + "type": "LIST", + "value": { + "elementType": "I32", + "elements": [10, 20] + } + } + }), + // Hex breakdown: + // Field 1 (LIST): 0f 00 01 + // Element Type (I32 = 08), Size (2 = 00 00 00 02) + // Values: 00 00 00 0a, 00 00 00 14 + // STOP: 00 + expectedOutput: "0f 00 01 08 00 00 00 02 00 00 00 0a 00 00 00 14 00", + recipeConfig: [ + { op: "Thrift Serialize", args: ["TBinaryProtocol"] }, + { op: "To Hex", args: ["Space", 0] } + ] + }, + + // ========================================== + // TBinaryProtocol Deserialization Tests + // ========================================== + { + name: "Thrift Deserialize: TBinaryProtocol (Basic Types)", + input: "08 00 01 00 00 05 39 0b 00 02 00 00 00 04 54 65 73 74 02 00 03 01 00", + expectedOutput: formatJson({ + "field_1": { "type": "I32", "value": 1337 }, + "field_2": { "type": "BINARY", "value": "Test" }, + "field_3": { "type": "BOOL", "value": true } + }), + recipeConfig: [ + { op: "From Hex", args: ["Auto"] }, + { op: "Thrift Deserialize", args: ["TBinaryProtocol"] } + ] + }, + { + name: "Thrift Deserialize: TBinaryProtocol (List of I32)", + input: "0f 00 01 08 00 00 00 02 00 00 00 0a 00 00 00 14 00", + expectedOutput: formatJson({ + "field_1": { + "type": "LIST", + "value": [10, 20] + } + }), + recipeConfig: [ + { op: "From Hex", args: ["Auto"] }, + { op: "Thrift Deserialize", args: ["TBinaryProtocol"] } + ] + }, + + // ========================================== + // TCompactProtocol Deserialization Tests + // ========================================== + { + name: "Thrift Deserialize: TCompactProtocol (Basic Types: I32, BINARY, BOOL)", + // Hex breakdown for Compact Protocol: + // Field 1 (I32, ID 1): Delta 1, Type 5 -> 15 | ZigZag(1337) = 2674 -> Varint = f2 14 + // Field 2 (STR, ID 2): Delta 1, Type 8 -> 18 | Len 4 -> 04 | "Test" -> 54 65 73 74 + // Field 3 (BOOL TRUE, ID 3): Delta 1, Type 1 -> 11 + // STOP: 00 + input: "15 f2 14 18 04 54 65 73 74 11 00", + expectedOutput: formatJson({ + "field_1": { "type": "I32", "value": 1337 }, + "field_2": { "type": "BINARY", "value": "Test" }, + "field_3": { "type": "BOOL", "value": true } + }), + recipeConfig: [ + { op: "From Hex", args: ["Auto"] }, + { op: "Thrift Deserialize", args: ["TCompactProtocol"] } + ] + }, + { + name: "Thrift Deserialize: TCompactProtocol (Negative Integers / ZigZag Verification)", + // Validates that negative numbers correctly decode from ZigZag to normal integers + // Field 1 (I32, ID 1): Delta 1, Type 5 -> 15 | ZigZag(-1337) = 2673 -> Varint = f1 14 + input: "15 f1 14 00", + expectedOutput: formatJson({ + "field_1": { "type": "I32", "value": -1337 } + }), + recipeConfig: [ + { op: "From Hex", args: ["Auto"] }, + { op: "Thrift Deserialize", args: ["TCompactProtocol"] } + ] + } +]); From 49436179536d669ef013fadff07689c7f3441806 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:10:04 +0300 Subject: [PATCH 02/17] Refactor Thrift serialization and deserialization operations - Moved binary and compact protocol parsing logic from ThriftDeserialize and ThriftSerialize classes to utility functions in Thrift.mjs. - Updated ThriftDeserialize to use new parseBinaryProtocol and parseCompactProtocol functions. - Simplified ThriftSerialize by utilizing buildBinaryStruct and writeValue functions for serialization. - Added comprehensive tests for Thrift serialization and deserialization, covering various data types and structures. - Ensured proper handling of edge cases in readVarint and fromZigZag functions. --- src/core/lib/Thrift.mjs | 374 +++++++++++++ src/core/operations/ThriftDeserialize.mjs | 265 +-------- src/core/operations/ThriftSerialize.mjs | 116 +--- tests/browser/02_ops.js | 2 + tests/node/tests/lib/Thrift.mjs | 644 ++++++++++++++++++++++ 5 files changed, 1031 insertions(+), 370 deletions(-) create mode 100644 src/core/lib/Thrift.mjs create mode 100644 tests/node/tests/lib/Thrift.mjs diff --git a/src/core/lib/Thrift.mjs b/src/core/lib/Thrift.mjs new file mode 100644 index 00000000..28212cf1 --- /dev/null +++ b/src/core/lib/Thrift.mjs @@ -0,0 +1,374 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; + +/** + * Recursively parses the JSON object to build out the Thrift byte array structure. + * + * @param {Object} jsonStruct + * @param {number[]} bytes + */ +export function buildBinaryStruct(jsonStruct, bytes) { + const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; + + for (const [key, fieldData] of Object.entries(jsonStruct)) { + const fieldId = parseInt(key.replace("field_", ""), 10); + if (isNaN(fieldId)) continue; + + const typeName = fieldData.type; + const fieldType = typeMap[typeName]; + const value = fieldData.value; + + // 1. Write Field Type (1 byte) + bytes.push(fieldType); + // 2. Write Field ID (2 bytes, Big Endian) + bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); + + // 3. Write Value + writeValue(typeName, value, bytes, typeMap); + } + + // Write T_STOP to close the struct + bytes.push(0); +} + +/** + * Serializes values into the byte stream according to their specific Thrift types. + * + * @param {string} typeName + * @param {*} value + * @param {number[]} bytes + * @param {Object} typeMap + */ +export function writeValue(typeName, value, bytes, typeMap) { + switch (typeName) { + case "BOOL": + bytes.push(value ? 1 : 0); + break; + case "I8": + bytes.push(value & 0xFF); + break; + case "I16": + bytes.push((value >> 8) & 0xFF, value & 0xFF); + break; + case "I32": + bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + break; + case "I64": { + const bigVal = BigInt(value); + for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); + break; + } + case "DOUBLE": { + // 8 bytes IEEE 754 floating point (Big Endian) + const floatView = new DataView(new ArrayBuffer(8)); + floatView.setFloat64(0, value, false); + for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); + break; + } + case "BINARY": { + const textEncoder = new TextEncoder(); + const strBytes = textEncoder.encode(value); + const len = strBytes.length; + bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); + strBytes.forEach(b => bytes.push(b)); + break; + } + case "STRUCT": + // Recursively build nested structs + buildBinaryStruct(value, bytes); + break; + case "LIST": + case "SET": { + // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } + const elType = typeMap[value.elementType]; + bytes.push(elType); // 1 byte element type + + const listSize = value.elements.length; + bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size + + // Write each element recursively + value.elements.forEach(el => writeValue(value.elementType, el, bytes, typeMap)); + break; + } + case "MAP": { + // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } + const kType = typeMap[value.keyType]; + const vType = typeMap[value.valType]; + bytes.push(kType, vType); // 1 byte key type, 1 byte val type + + const mapSize = value.elements.length; + bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size + + // Write pairs + value.elements.forEach(pair => { + writeValue(value.keyType, pair.key, bytes, typeMap); + writeValue(value.valType, pair.val, bytes, typeMap); + }); + break; + } + default: + throw new Error(`Unsupported serialization type: ${typeName}`); + } +} + +// --- TBinaryProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TBinaryProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseBinaryProtocol(data, offset) { + const result = {}; + while (offset < data.byteLength) { + const fieldType = data.getUint8(offset++); + if (fieldType === 0) break; // T_STOP + + const fieldId = data.getInt16(offset); + offset += 2; + + const parsed = readBinaryType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getBinaryTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms binary datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readBinaryType(data, offset, type) { + let value; + switch (type) { + case 2: // BOOL + value = data.getUint8(offset++) === 1; + break; + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // DOUBLE + value = data.getFloat64(offset); + offset += 8; + break; + case 6: // I16 + value = data.getInt16(offset); + offset += 2; + break; + case 8: // I32 + value = data.getInt32(offset); + offset += 4; + break; + case 10: // I64 + // Note: using BigInt to avoid precision loss on 64-bit integers + value = data.getBigInt64(offset).toString(); + offset += 8; + break; + case 11: { // BINARY/STRING + const strLen = data.getInt32(offset); + offset += 4; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseBinaryProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + case 13: { // MAP + const keyType = data.getUint8(offset++); + const valType = data.getUint8(offset++); + const mapSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < mapSize; i++) { + const k = readBinaryType(data, offset, keyType); + offset = k.offset; + const v = readBinaryType(data, offset, valType); + offset = v.offset; + value.push({ key: k.value, val: v.value }); + } + break; + } + case 14: // SET + case 15: { // LIST + const elemType = data.getUint8(offset++); + const listSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < listSize; i++) { + const elem = readBinaryType(data, offset, elemType); + value.push(elem.value); + offset = elem.offset; + } + break; + } + default: + throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Binary protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getBinaryTypeName(type) { + const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; + return types[type] || `UNKNOWN(${type})`; +} + +// --- TCompactProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TCompactProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseCompactProtocol(data, offset) { + const result = {}; + let lastFieldId = 0; + + while (offset < data.byteLength) { + const byte = data.getUint8(offset++); + if (byte === 0) break; // STOP field + + const modifier = (byte & 0xf0) >> 4; + const fieldType = byte & 0x0f; + + let fieldId; + if (modifier === 0) { + // Long form: read zigzag varint field ID + const idParsed = readVarint(data, offset); + fieldId = fromZigZag(idParsed.value); + offset = idParsed.offset; + } else { + // Short form: delta + fieldId = lastFieldId + modifier; + } + lastFieldId = fieldId; + + // Types 1 and 2 are boolean true/false encoded directly in the modifier + if (fieldType === 1) { + result[`field_${fieldId}`] = { type: "BOOL", value: true }; + continue; + } else if (fieldType === 2) { + result[`field_${fieldId}`] = { type: "BOOL", value: false }; + continue; + } + + const parsed = readCompactType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getCompactTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms compact datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readCompactType(data, offset, type) { + let value, varintParsed; + switch (type) { + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // I16 + case 5: // I32 + case 6: // I64 + varintParsed = readVarint(data, offset); + value = fromZigZag(varintParsed.value); // Decodes ZigZag + offset = varintParsed.offset; + break; + case 7: // DOUBLE + value = data.getFloat64(offset, true); // Little endian + offset += 8; + break; + case 8: { // BINARY/STRING + varintParsed = readVarint(data, offset); + const strLen = Number(varintParsed.value); // Not zigzagged + offset = varintParsed.offset; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseCompactProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact + // Implement based on the spec provided (e.g., sssstttt for lists) + default: + throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Compact protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getCompactTypeName(type) { + const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; + return types[type] || `UNKNOWN(${type})`; +} + +/** + * Variable-length integer parsing logic helper. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function readVarint(data, offset) { + let result = 0n; + let shift = 0n; + while (true) { + if (offset >= data.byteLength) throw new Error("EOF reading varint"); + const byte = BigInt(data.getUint8(offset++)); + result |= (byte & 0x7fn) << shift; + if ((byte & 0x80n) === 0n) break; + shift += 7n; + } + return { value: result, offset: offset }; +} + +/** + * Decodes ZigZag parameters to system numbers. + * + * @param {bigint} n + * @returns {bigint} + */ +export function fromZigZag(n) { + // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation + return (n >> 1n) ^ -(n & 1n); +} diff --git a/src/core/operations/ThriftDeserialize.mjs b/src/core/operations/ThriftDeserialize.mjs index 7b04e7ce..0a00af70 100644 --- a/src/core/operations/ThriftDeserialize.mjs +++ b/src/core/operations/ThriftDeserialize.mjs @@ -6,7 +6,10 @@ */ import Operation from "../Operation.mjs"; -import Utils from "../Utils.mjs"; +import { + parseBinaryProtocol, + parseCompactProtocol +} from "../lib/Thrift.mjs"; /** * Operation to decode Apache Thrift binary blobs into JSON structures. @@ -47,271 +50,15 @@ class ThriftDeserialize extends Operation { let decodedObject = {}; try { if (protocol === "TBinaryProtocol") { - decodedObject = this.parseBinaryProtocol(data, 0).result; + decodedObject = parseBinaryProtocol(data, 0).result; } else if (protocol === "TCompactProtocol") { - decodedObject = this.parseCompactProtocol(data, 0).result; + decodedObject = parseCompactProtocol(data, 0).result; } return JSON.stringify(decodedObject, null, 4); } catch (err) { return `Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`; } } - - // --- TBinaryProtocol Implementation --- - - /** - * Parses the incoming schema using TBinaryProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseBinaryProtocol(data, offset) { - const result = {}; - while (offset < data.byteLength) { - const fieldType = data.getUint8(offset++); - if (fieldType === 0) break; // T_STOP - - const fieldId = data.getInt16(offset); - offset += 2; - - const parsed = this.readBinaryType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getBinaryTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms binary datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readBinaryType(data, offset, type) { - let value; - switch (type) { - case 2: // BOOL - value = data.getUint8(offset++) === 1; - break; - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // DOUBLE - value = data.getFloat64(offset); - offset += 8; - break; - case 6: // I16 - value = data.getInt16(offset); - offset += 2; - break; - case 8: // I32 - value = data.getInt32(offset); - offset += 4; - break; - case 10: // I64 - // Note: using BigInt to avoid precision loss on 64-bit integers - value = data.getBigInt64(offset).toString(); - offset += 8; - break; - case 11: { // BINARY/STRING - const strLen = data.getInt32(offset); - offset += 4; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseBinaryProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - case 13: { // MAP - const keyType = data.getUint8(offset++); - const valType = data.getUint8(offset++); - const mapSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < mapSize; i++) { - const k = this.readBinaryType(data, offset, keyType); - offset = k.offset; - const v = this.readBinaryType(data, offset, valType); - offset = v.offset; - value.push({ key: k.value, val: v.value }); - } - break; - } - case 14: // SET - case 15: { // LIST - const elemType = data.getUint8(offset++); - const listSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < listSize; i++) { - const elem = this.readBinaryType(data, offset, elemType); - value.push(elem.value); - offset = elem.offset; - } - break; - } - default: - throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Binary protocol types. - * - * @param {number} type - * @returns {string} - */ - getBinaryTypeName(type) { - const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; - return types[type] || `UNKNOWN(${type})`; - } - - // --- TCompactProtocol Implementation --- - - /** - * Parses the incoming schema using TCompactProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseCompactProtocol(data, offset) { - const result = {}; - let lastFieldId = 0; - - while (offset < data.byteLength) { - const byte = data.getUint8(offset++); - if (byte === 0) break; // STOP field - - const modifier = (byte & 0xf0) >> 4; - const fieldType = byte & 0x0f; - - let fieldId; - if (modifier === 0) { - // Long form: read zigzag varint field ID - const idParsed = this.readVarint(data, offset); - fieldId = this.fromZigZag(idParsed.value); - offset = idParsed.offset; - } else { - // Short form: delta - fieldId = lastFieldId + modifier; - } - lastFieldId = fieldId; - - // Types 1 and 2 are boolean true/false encoded directly in the modifier - if (fieldType === 1) { - result[`field_${fieldId}`] = { type: "BOOL", value: true }; - continue; - } else if (fieldType === 2) { - result[`field_${fieldId}`] = { type: "BOOL", value: false }; - continue; - } - - const parsed = this.readCompactType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getCompactTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms compact datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readCompactType(data, offset, type) { - let value, varintParsed; - switch (type) { - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // I16 - case 5: // I32 - case 6: // I64 - varintParsed = this.readVarint(data, offset); - value = this.fromZigZag(varintParsed.value); // Decodes ZigZag - offset = varintParsed.offset; - break; - case 7: // DOUBLE - value = data.getFloat64(offset, true); // Little endian - offset += 8; - break; - case 8: { // BINARY/STRING - varintParsed = this.readVarint(data, offset); - const strLen = Number(varintParsed.value); // Not zigzagged - offset = varintParsed.offset; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseCompactProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact - // Implement based on the spec provided (e.g., sssstttt for lists) - default: - throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Compact protocol types. - * - * @param {number} type - * @returns {string} - */ - getCompactTypeName(type) { - const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; - return types[type] || `UNKNOWN(${type})`; - } - - /** - * Variable-length integer parsing logic helper. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - readVarint(data, offset) { - let result = 0n; - let shift = 0n; - while (true) { - if (offset >= data.byteLength) throw new Error("EOF reading varint"); - const byte = BigInt(data.getUint8(offset++)); - result |= (byte & 0x7fn) << shift; - if ((byte & 0x80n) === 0n) break; - shift += 7n; - } - return { value: result, offset: offset }; - } - - /** - * Decodes ZigZag parameters to system numbers. - * - * @param {bigint} n - * @returns {bigint} - */ - fromZigZag(n) { - // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation - return (n >> 1n) ^ -(n & 1n); - } } export default ThriftDeserialize; diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index ae20948b..5fd357a7 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,6 +6,10 @@ */ import Operation from "../Operation.mjs"; +import { + buildBinaryStruct, + writeValue +} from "../lib/Thrift.mjs"; /** * Operation to encode a JSON structure into Apache Thrift TBinaryProtocol binary format. @@ -43,119 +47,9 @@ class ThriftSerialize extends Operation { } const bytes = []; - this.buildBinaryStruct(parsedInput, bytes); + buildBinaryStruct(parsedInput, bytes); return new Uint8Array(bytes).buffer; } - - /** - * Recursively parses the JSON object to build out the Thrift byte array structure. - * - * @param {Object} jsonStruct - * @param {number[]} bytes - */ - buildBinaryStruct(jsonStruct, bytes) { - const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; - - for (const [key, fieldData] of Object.entries(jsonStruct)) { - const fieldId = parseInt(key.replace("field_", ""), 10); - if (isNaN(fieldId)) continue; - - const typeName = fieldData.type; - const fieldType = typeMap[typeName]; - const value = fieldData.value; - - // 1. Write Field Type (1 byte) - bytes.push(fieldType); - // 2. Write Field ID (2 bytes, Big Endian) - bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); - - // 3. Write Value - this.writeValue(typeName, value, bytes, typeMap); - } - - // Write T_STOP to close the struct - bytes.push(0); - } - - /** - * Serializes values into the byte stream according to their specific Thrift types. - * - * @param {string} typeName - * @param {*} value - * @param {number[]} bytes - * @param {Object} typeMap - */ - writeValue(typeName, value, bytes, typeMap) { - switch (typeName) { - case "BOOL": - bytes.push(value ? 1 : 0); - break; - case "I8": - bytes.push(value & 0xFF); - break; - case "I16": - bytes.push((value >> 8) & 0xFF, value & 0xFF); - break; - case "I32": - bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); - break; - case "I64": { - const bigVal = BigInt(value); - for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); - break; - } - case "DOUBLE": { - // 8 bytes IEEE 754 floating point (Big Endian) - const floatView = new DataView(new ArrayBuffer(8)); - floatView.setFloat64(0, value, false); - for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); - break; - } - case "BINARY": { - const textEncoder = new TextEncoder(); - const strBytes = textEncoder.encode(value); - const len = strBytes.length; - bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); - strBytes.forEach(b => bytes.push(b)); - break; - } - case "STRUCT": - // Recursively build nested structs - this.buildBinaryStruct(value, bytes); - break; - case "LIST": - case "SET": { - // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } - const elType = typeMap[value.elementType]; - bytes.push(elType); // 1 byte element type - - const listSize = value.elements.length; - bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size - - // Write each element recursively - value.elements.forEach(el => this.writeValue(value.elementType, el, bytes, typeMap)); - break; - } - case "MAP": { - // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } - const kType = typeMap[value.keyType]; - const vType = typeMap[value.valType]; - bytes.push(kType, vType); // 1 byte key type, 1 byte val type - - const mapSize = value.elements.length; - bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size - - // Write pairs - value.elements.forEach(pair => { - this.writeValue(value.keyType, pair.key, bytes, typeMap); - this.writeValue(value.valType, pair.val, bytes, typeMap); - }); - break; - } - default: - throw new Error(`Unsupported serialization type: ${typeName}`); - } - } } export default ThriftSerialize; diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 9d29f50d..3a23506f 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -355,6 +355,8 @@ module.exports = { // testOp(browser, "Take bytes", "test input", "test_output"); testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/); testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]); + testOp(browser, ["Thrift Serialize", "To Hex"], "{\"field_1\": { \"type\": \"I32\", \"value\": 100 }}", "08 00 01 00 00 00 64 00"); + testOpHtml(browser, ["From Hex", "Thrift Deserialize"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"]]); testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/); // testOp(browser, "To BCD", "test input", "test_output"); // testOp(browser, "To Base", "test input", "test_output"); diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs new file mode 100644 index 00000000..7a975c86 --- /dev/null +++ b/tests/node/tests/lib/Thrift.mjs @@ -0,0 +1,644 @@ +import TestRegister from "../../../lib/TestRegister.mjs"; +import { + buildBinaryStruct, + writeValue, + parseBinaryProtocol, + readBinaryType, + getBinaryTypeName, + parseCompactProtocol, + readCompactType, + getCompactTypeName, + readVarint, + fromZigZag +} from "../../../../src/core/lib/Thrift.mjs"; +import it from "../../assertionHandler.mjs"; +import assert from "assert"; + +TestRegister.addApiTests([ + // ===== readVarint tests ===== + it("Thrift: readVarint - single byte (0)", () => { + const bytes = new Uint8Array([0x00]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 0n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - single byte (127)", () => { + const bytes = new Uint8Array([0x7F]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 127n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - two bytes (128)", () => { + const bytes = new Uint8Array([0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 128n); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readVarint - multiple bytes (16384)", () => { + const bytes = new Uint8Array([0x80, 0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 16384n); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - starting from non-zero offset", () => { + const bytes = new Uint8Array([0xFF, 0xFF, 0x42]); // 0xFF is continuation, 0xFF is continuation, 0x42 is end + const data = new DataView(bytes.buffer); + const result = readVarint(data, 1); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - EOF error", () => { + const bytes = new Uint8Array([0x80]); // Missing continuation byte + const data = new DataView(bytes.buffer); + assert.throws(() => readVarint(data, 0), /EOF reading varint/); + }), + + // ===== fromZigZag tests ===== + it("Thrift: fromZigZag - zero", () => { + assert.strictEqual(fromZigZag(0n), 0n); + }), + + it("Thrift: fromZigZag - positive numbers", () => { + assert.strictEqual(fromZigZag(2n), 1n); + assert.strictEqual(fromZigZag(4n), 2n); + assert.strictEqual(fromZigZag(6n), 3n); + }), + + it("Thrift: fromZigZag - negative numbers", () => { + assert.strictEqual(fromZigZag(1n), -1n); + assert.strictEqual(fromZigZag(3n), -2n); + assert.strictEqual(fromZigZag(5n), -3n); + }), + + it("Thrift: fromZigZag - large positive", () => { + assert.strictEqual(fromZigZag(BigInt("1000000000")), BigInt("500000000")); + }), + + it("Thrift: fromZigZag - large negative", () => { + assert.strictEqual(fromZigZag(BigInt("1000000001")), BigInt("-500000001")); + }), + + // ===== getBinaryTypeName tests ===== + it("Thrift: getBinaryTypeName - all type mappings", () => { + assert.strictEqual(getBinaryTypeName(2), "BOOL"); + assert.strictEqual(getBinaryTypeName(3), "I8"); + assert.strictEqual(getBinaryTypeName(4), "DOUBLE"); + assert.strictEqual(getBinaryTypeName(6), "I16"); + assert.strictEqual(getBinaryTypeName(8), "I32"); + assert.strictEqual(getBinaryTypeName(10), "I64"); + assert.strictEqual(getBinaryTypeName(11), "BINARY"); + assert.strictEqual(getBinaryTypeName(12), "STRUCT"); + assert.strictEqual(getBinaryTypeName(13), "MAP"); + assert.strictEqual(getBinaryTypeName(14), "SET"); + assert.strictEqual(getBinaryTypeName(15), "LIST"); + }), + + it("Thrift: getBinaryTypeName - unknown type", () => { + assert.strictEqual(getBinaryTypeName(99), "UNKNOWN(99)"); + }), + + // ===== getCompactTypeName tests ===== + it("Thrift: getCompactTypeName - all type mappings", () => { + assert.strictEqual(getCompactTypeName(1), "BOOLEAN_TRUE"); + assert.strictEqual(getCompactTypeName(2), "BOOLEAN_FALSE"); + assert.strictEqual(getCompactTypeName(3), "I8"); + assert.strictEqual(getCompactTypeName(4), "I16"); + assert.strictEqual(getCompactTypeName(5), "I32"); + assert.strictEqual(getCompactTypeName(6), "I64"); + assert.strictEqual(getCompactTypeName(7), "DOUBLE"); + assert.strictEqual(getCompactTypeName(8), "BINARY"); + assert.strictEqual(getCompactTypeName(9), "LIST"); + assert.strictEqual(getCompactTypeName(10), "SET"); + assert.strictEqual(getCompactTypeName(11), "MAP"); + assert.strictEqual(getCompactTypeName(12), "STRUCT"); + assert.strictEqual(getCompactTypeName(13), "UUID"); + }), + + it("Thrift: getCompactTypeName - unknown type", () => { + assert.strictEqual(getCompactTypeName(99), "UNKNOWN(99)"); + }), + + // ===== writeValue tests - basic types ===== + it("Thrift: writeValue - BOOL true", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", true, bytes, typeMap); + assert.deepStrictEqual(bytes, [1]); + }), + + it("Thrift: writeValue - BOOL false", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", false, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 zero", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 positive", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 42, bytes, typeMap); + assert.deepStrictEqual(bytes, [42]); + }), + + it("Thrift: writeValue - I8 max", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 127, bytes, typeMap); + assert.deepStrictEqual(bytes, [127]); + }), + + it("Thrift: writeValue - I16", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0x1234, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34]); + }), + + it("Thrift: writeValue - I16 zero", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0]); + }), + + it("Thrift: writeValue - I32", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0x12345678, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34, 0x56, 0x78]); + }), + + it("Thrift: writeValue - I32 zero", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - I32 negative", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", -1, bytes, typeMap); + assert.deepStrictEqual(bytes, [0xFF, 0xFF, 0xFF, 0xFF]); + }), + + it("Thrift: writeValue - I64", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0x0102030405060708", bytes, typeMap); + assert.deepStrictEqual(bytes, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + }), + + it("Thrift: writeValue - I64 zero", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - DOUBLE", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 3.14159, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + // Verify it's a valid IEEE 754 double by reading it back + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.ok(Math.abs(view.getFloat64(0, false) - 3.14159) < 0.00001); + }), + + it("Thrift: writeValue - DOUBLE zero", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 0.0, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), 0); + }), + + it("Thrift: writeValue - DOUBLE negative", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", -42.5, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), -42.5); + }), + + it("Thrift: writeValue - BINARY empty string", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - BINARY ASCII", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "hello", bytes, typeMap); + const expected = [0, 0, 0, 5, ...new TextEncoder().encode("hello")]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - BINARY UTF-8", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "你好", bytes, typeMap); + const encoded = new TextEncoder().encode("你好"); + const expected = [0, 0, 0, encoded.length, ...Array.from(encoded)]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== writeValue tests - collections ===== + it("Thrift: writeValue - LIST of I32 empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [] }; + writeValue("LIST", listValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - LIST of I32", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [1, 2, 3] }; + writeValue("LIST", listValue, bytes, typeMap); + const expected = [ + 8, // element type (I32) + 0, 0, 0, 3, // list size = 3 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2, // element 2 + 0, 0, 0, 3 // element 3 + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - SET of BINARY", () => { + const bytes = []; + const typeMap = { "BINARY": 11, "SET": 14 }; + const setValue = { elementType: "BINARY", elements: ["a"] }; + writeValue("SET", setValue, bytes, typeMap); + const encoded = new TextEncoder().encode("a"); + const expected = [ + 11, // element type (BINARY) + 0, 0, 0, 1, // set size = 1 + 0, 0, 0, 1, // string length + ...encoded // string data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - MAP empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { keyType: "I32", valType: "BINARY", elements: [] }; + writeValue("MAP", mapValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 11, 0, 0, 0, 0]); // key type, val type, size = 0 + }), + + it("Thrift: writeValue - MAP with entry", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { + keyType: "I32", + valType: "BINARY", + elements: [{ key: 1, val: "test" }] + }; + writeValue("MAP", mapValue, bytes, typeMap); + const encoded = new TextEncoder().encode("test"); + const expected = [ + 8, 11, // key type, val type + 0, 0, 0, 1, // map size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 4, // val length + ...encoded // val data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== buildBinaryStruct tests ===== + it("Thrift: buildBinaryStruct - simple struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - multiple fields", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 }, + field_2: { type: "BINARY", value: "hello" } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should end with T_STOP + assert.strictEqual(bytes[bytes.length - 1], 0); + }), + + it("Thrift: buildBinaryStruct - BOOL field", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "BOOL", value: true } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 2, 0, 1, // field type (BOOL), field id = 1 + 1, // value = true + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - nested struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { + type: "STRUCT", + value: { + field_1: { type: "I32", value: 10 } + } + } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should contain nested structure with T_STOP markers + assert.ok(bytes.length > 0); + assert.strictEqual(bytes[bytes.length - 1], 0); // ends with T_STOP + }), + + it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { + const bytes = []; + const jsonStruct = { + invalid_field: { type: "I32", value: 42 }, + field_1: { type: "I32", value: 10 } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should skip invalid_field and only process field_1 + assert.strictEqual(bytes[0], 8); // field type + assert.strictEqual(bytes[3], 10); // field value + }), + + // ===== readBinaryType tests ===== + it("Thrift: readBinaryType - BOOL true", () => { + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, true); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - BOOL false", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, false); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I16", () => { + const bytes = new Uint8Array([0x12, 0x34]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 6); + assert.strictEqual(result.value, 0x1234); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readBinaryType - I32", () => { + const bytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 8); + assert.strictEqual(result.value, 0x12345678); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - I64", () => { + const bytes = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 10); + assert.strictEqual(result.value, "66"); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 3.14159, false); + const result = readBinaryType(view, 0, 4); + assert.ok(Math.abs(result.value - 3.14159) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - BINARY empty", () => { + const bytes = new Uint8Array([0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - BINARY with data", () => { + const str = "hello"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([0, 0, 0, 5, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 9); + }), + + it("Thrift: readBinaryType - LIST of I32", () => { + const bytes = new Uint8Array([ + 8, // element type (I32) + 0, 0, 0, 2, // size = 2 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2 // element 2 + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, [1, 2]); + assert.strictEqual(result.offset, 13); + }), + + it("Thrift: readBinaryType - LIST empty", () => { + const bytes = new Uint8Array([8, 0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, []); + assert.strictEqual(result.offset, 5); + }), + + it("Thrift: readBinaryType - MAP", () => { + const str = "key"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([ + 8, 11, // key type (I32), val type (BINARY) + 0, 0, 0, 1, // size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 3, // val length + ...encoded // val data + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 13); + assert.deepStrictEqual(result.value, [{ key: 1, val: "key" }]); + assert.strictEqual(result.offset, 13); + }), + + // ===== parseBinaryProtocol tests ===== + it("Thrift: parseBinaryProtocol - simple I32", () => { + const bytes = new Uint8Array([ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.deepStrictEqual(result.result.field_1, { type: "I32", value: 42 }); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: parseBinaryProtocol - multiple fields", () => { + const bytes = new Uint8Array([ + 2, 0, 1, // BOOL field 1 + 1, // value = true + 8, 0, 2, // I32 field 2 + 0, 0, 0, 42, // value = 42 + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOL"); + assert.strictEqual(result.result.field_1.value, true); + assert.strictEqual(result.result.field_2.type, "I32"); + assert.strictEqual(result.result.field_2.value, 42); + }), + + it("Thrift: parseBinaryProtocol - nested struct", () => { + const bytes = new Uint8Array([ + 12, 0, 1, // STRUCT field 1 + 8, 0, 1, // nested I32 field 1 + 0, 0, 0, 10, // value + 0, // nested T_STOP + 0 // outer T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.ok(result.result.field_1); + assert.strictEqual(result.result.field_1.type, "STRUCT"); + assert.deepStrictEqual(result.result.field_1.value.field_1, { type: "I32", value: 10 }); + }), + + // ===== readCompactType tests ===== + it("Thrift: readCompactType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - I16 zigzag", () => { + // ZigZag of -1 is 1 + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 4); + assert.strictEqual(result.value, -1n); + }), + + it("Thrift: readCompactType - I32 zigzag", () => { + // ZigZag of 100 is 200 + const bytes = new Uint8Array([200]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 5); + assert.strictEqual(result.value, 100n); + }), + + it("Thrift: readCompactType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 2.71828, true); + const result = readCompactType(view, 0, 7); + assert.ok(Math.abs(result.value - 2.71828) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readCompactType - BINARY empty", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - BINARY with data", () => { + const str = "test"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([4, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 5); + }), + + // ===== parseCompactProtocol tests ===== + it("Thrift: parseCompactProtocol - empty struct", () => { + const bytes = new Uint8Array([0]); // Just STOP + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.deepStrictEqual(result.result, {}); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: parseCompactProtocol - boolean true field", () => { + const bytes = new Uint8Array([ + 0x11, // field modifier=1 (delta=1), type=1 (BOOL_TRUE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_TRUE"); + assert.strictEqual(result.result.field_1.value, true); + }), + + it("Thrift: parseCompactProtocol - boolean false field", () => { + const bytes = new Uint8Array([ + 0x12, // field modifier=1 (delta=1), type=2 (BOOL_FALSE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_FALSE"); + assert.strictEqual(result.result.field_1.value, false); + }), + +]); From 6c54bdeb6ed627f869a14d1fd74a97f60cbba8b9 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:10:04 +0300 Subject: [PATCH 03/17] Refactor Thrift serialization and deserialization operations - Moved binary and compact protocol parsing logic from ThriftDeserialize and ThriftSerialize classes to utility functions in Thrift.mjs. - Updated ThriftDeserialize to use new parseBinaryProtocol and parseCompactProtocol functions. - Simplified ThriftSerialize by utilizing buildBinaryStruct and writeValue functions for serialization. - Added comprehensive tests for Thrift serialization and deserialization, covering various data types and structures. - Ensured proper handling of edge cases in readVarint and fromZigZag functions. --- src/core/lib/Thrift.mjs | 374 +++++++++++++ src/core/operations/ThriftDeserialize.mjs | 265 +-------- src/core/operations/ThriftSerialize.mjs | 115 +--- tests/browser/02_ops.js | 2 + tests/node/tests/lib/Thrift.mjs | 644 ++++++++++++++++++++++ 5 files changed, 1030 insertions(+), 370 deletions(-) create mode 100644 src/core/lib/Thrift.mjs create mode 100644 tests/node/tests/lib/Thrift.mjs diff --git a/src/core/lib/Thrift.mjs b/src/core/lib/Thrift.mjs new file mode 100644 index 00000000..28212cf1 --- /dev/null +++ b/src/core/lib/Thrift.mjs @@ -0,0 +1,374 @@ +/** + * @author Engin Kaya + * @author engin0223 [engineda2014@hotmail.com] + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Utils from "../Utils.mjs"; + +/** + * Recursively parses the JSON object to build out the Thrift byte array structure. + * + * @param {Object} jsonStruct + * @param {number[]} bytes + */ +export function buildBinaryStruct(jsonStruct, bytes) { + const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; + + for (const [key, fieldData] of Object.entries(jsonStruct)) { + const fieldId = parseInt(key.replace("field_", ""), 10); + if (isNaN(fieldId)) continue; + + const typeName = fieldData.type; + const fieldType = typeMap[typeName]; + const value = fieldData.value; + + // 1. Write Field Type (1 byte) + bytes.push(fieldType); + // 2. Write Field ID (2 bytes, Big Endian) + bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); + + // 3. Write Value + writeValue(typeName, value, bytes, typeMap); + } + + // Write T_STOP to close the struct + bytes.push(0); +} + +/** + * Serializes values into the byte stream according to their specific Thrift types. + * + * @param {string} typeName + * @param {*} value + * @param {number[]} bytes + * @param {Object} typeMap + */ +export function writeValue(typeName, value, bytes, typeMap) { + switch (typeName) { + case "BOOL": + bytes.push(value ? 1 : 0); + break; + case "I8": + bytes.push(value & 0xFF); + break; + case "I16": + bytes.push((value >> 8) & 0xFF, value & 0xFF); + break; + case "I32": + bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); + break; + case "I64": { + const bigVal = BigInt(value); + for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); + break; + } + case "DOUBLE": { + // 8 bytes IEEE 754 floating point (Big Endian) + const floatView = new DataView(new ArrayBuffer(8)); + floatView.setFloat64(0, value, false); + for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); + break; + } + case "BINARY": { + const textEncoder = new TextEncoder(); + const strBytes = textEncoder.encode(value); + const len = strBytes.length; + bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); + strBytes.forEach(b => bytes.push(b)); + break; + } + case "STRUCT": + // Recursively build nested structs + buildBinaryStruct(value, bytes); + break; + case "LIST": + case "SET": { + // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } + const elType = typeMap[value.elementType]; + bytes.push(elType); // 1 byte element type + + const listSize = value.elements.length; + bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size + + // Write each element recursively + value.elements.forEach(el => writeValue(value.elementType, el, bytes, typeMap)); + break; + } + case "MAP": { + // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } + const kType = typeMap[value.keyType]; + const vType = typeMap[value.valType]; + bytes.push(kType, vType); // 1 byte key type, 1 byte val type + + const mapSize = value.elements.length; + bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size + + // Write pairs + value.elements.forEach(pair => { + writeValue(value.keyType, pair.key, bytes, typeMap); + writeValue(value.valType, pair.val, bytes, typeMap); + }); + break; + } + default: + throw new Error(`Unsupported serialization type: ${typeName}`); + } +} + +// --- TBinaryProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TBinaryProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseBinaryProtocol(data, offset) { + const result = {}; + while (offset < data.byteLength) { + const fieldType = data.getUint8(offset++); + if (fieldType === 0) break; // T_STOP + + const fieldId = data.getInt16(offset); + offset += 2; + + const parsed = readBinaryType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getBinaryTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms binary datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readBinaryType(data, offset, type) { + let value; + switch (type) { + case 2: // BOOL + value = data.getUint8(offset++) === 1; + break; + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // DOUBLE + value = data.getFloat64(offset); + offset += 8; + break; + case 6: // I16 + value = data.getInt16(offset); + offset += 2; + break; + case 8: // I32 + value = data.getInt32(offset); + offset += 4; + break; + case 10: // I64 + // Note: using BigInt to avoid precision loss on 64-bit integers + value = data.getBigInt64(offset).toString(); + offset += 8; + break; + case 11: { // BINARY/STRING + const strLen = data.getInt32(offset); + offset += 4; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseBinaryProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + case 13: { // MAP + const keyType = data.getUint8(offset++); + const valType = data.getUint8(offset++); + const mapSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < mapSize; i++) { + const k = readBinaryType(data, offset, keyType); + offset = k.offset; + const v = readBinaryType(data, offset, valType); + offset = v.offset; + value.push({ key: k.value, val: v.value }); + } + break; + } + case 14: // SET + case 15: { // LIST + const elemType = data.getUint8(offset++); + const listSize = data.getInt32(offset); + offset += 4; + value = []; + for (let i = 0; i < listSize; i++) { + const elem = readBinaryType(data, offset, elemType); + value.push(elem.value); + offset = elem.offset; + } + break; + } + default: + throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Binary protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getBinaryTypeName(type) { + const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; + return types[type] || `UNKNOWN(${type})`; +} + +// --- TCompactProtocol Deserialization Functions --- + +/** + * Parses the incoming schema using TCompactProtocol constraints. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function parseCompactProtocol(data, offset) { + const result = {}; + let lastFieldId = 0; + + while (offset < data.byteLength) { + const byte = data.getUint8(offset++); + if (byte === 0) break; // STOP field + + const modifier = (byte & 0xf0) >> 4; + const fieldType = byte & 0x0f; + + let fieldId; + if (modifier === 0) { + // Long form: read zigzag varint field ID + const idParsed = readVarint(data, offset); + fieldId = fromZigZag(idParsed.value); + offset = idParsed.offset; + } else { + // Short form: delta + fieldId = lastFieldId + modifier; + } + lastFieldId = fieldId; + + // Types 1 and 2 are boolean true/false encoded directly in the modifier + if (fieldType === 1) { + result[`field_${fieldId}`] = { type: "BOOL", value: true }; + continue; + } else if (fieldType === 2) { + result[`field_${fieldId}`] = { type: "BOOL", value: false }; + continue; + } + + const parsed = readCompactType(data, offset, fieldType); + result[`field_${fieldId}`] = { type: getCompactTypeName(fieldType), value: parsed.value }; + offset = parsed.offset; + } + return { result, offset }; +} + +/** + * Reads and transforms compact datatypes based on identifier rules. + * + * @param {DataView} data + * @param {number} offset + * @param {number} type + * @returns {Object} + */ +export function readCompactType(data, offset, type) { + let value, varintParsed; + switch (type) { + case 3: // I8 + value = data.getInt8(offset++); + break; + case 4: // I16 + case 5: // I32 + case 6: // I64 + varintParsed = readVarint(data, offset); + value = fromZigZag(varintParsed.value); // Decodes ZigZag + offset = varintParsed.offset; + break; + case 7: // DOUBLE + value = data.getFloat64(offset, true); // Little endian + offset += 8; + break; + case 8: { // BINARY/STRING + varintParsed = readVarint(data, offset); + const strLen = Number(varintParsed.value); // Not zigzagged + offset = varintParsed.offset; + const strBytes = new Uint8Array(data.buffer, offset, strLen); + value = Utils.byteArrayToUtf8(strBytes); + offset += strLen; + break; + } + case 12: { // STRUCT + const structParsed = parseCompactProtocol(data, offset); + value = structParsed.result; + offset = structParsed.offset; + break; + } + // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact + // Implement based on the spec provided (e.g., sssstttt for lists) + default: + throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); + } + return { value, offset }; +} + +/** + * Returns string names for Compact protocol types. + * + * @param {number} type + * @returns {string} + */ +export function getCompactTypeName(type) { + const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; + return types[type] || `UNKNOWN(${type})`; +} + +/** + * Variable-length integer parsing logic helper. + * + * @param {DataView} data + * @param {number} offset + * @returns {Object} + */ +export function readVarint(data, offset) { + let result = 0n; + let shift = 0n; + while (true) { + if (offset >= data.byteLength) throw new Error("EOF reading varint"); + const byte = BigInt(data.getUint8(offset++)); + result |= (byte & 0x7fn) << shift; + if ((byte & 0x80n) === 0n) break; + shift += 7n; + } + return { value: result, offset: offset }; +} + +/** + * Decodes ZigZag parameters to system numbers. + * + * @param {bigint} n + * @returns {bigint} + */ +export function fromZigZag(n) { + // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation + return (n >> 1n) ^ -(n & 1n); +} diff --git a/src/core/operations/ThriftDeserialize.mjs b/src/core/operations/ThriftDeserialize.mjs index 7b04e7ce..0a00af70 100644 --- a/src/core/operations/ThriftDeserialize.mjs +++ b/src/core/operations/ThriftDeserialize.mjs @@ -6,7 +6,10 @@ */ import Operation from "../Operation.mjs"; -import Utils from "../Utils.mjs"; +import { + parseBinaryProtocol, + parseCompactProtocol +} from "../lib/Thrift.mjs"; /** * Operation to decode Apache Thrift binary blobs into JSON structures. @@ -47,271 +50,15 @@ class ThriftDeserialize extends Operation { let decodedObject = {}; try { if (protocol === "TBinaryProtocol") { - decodedObject = this.parseBinaryProtocol(data, 0).result; + decodedObject = parseBinaryProtocol(data, 0).result; } else if (protocol === "TCompactProtocol") { - decodedObject = this.parseCompactProtocol(data, 0).result; + decodedObject = parseCompactProtocol(data, 0).result; } return JSON.stringify(decodedObject, null, 4); } catch (err) { return `Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`; } } - - // --- TBinaryProtocol Implementation --- - - /** - * Parses the incoming schema using TBinaryProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseBinaryProtocol(data, offset) { - const result = {}; - while (offset < data.byteLength) { - const fieldType = data.getUint8(offset++); - if (fieldType === 0) break; // T_STOP - - const fieldId = data.getInt16(offset); - offset += 2; - - const parsed = this.readBinaryType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getBinaryTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms binary datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readBinaryType(data, offset, type) { - let value; - switch (type) { - case 2: // BOOL - value = data.getUint8(offset++) === 1; - break; - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // DOUBLE - value = data.getFloat64(offset); - offset += 8; - break; - case 6: // I16 - value = data.getInt16(offset); - offset += 2; - break; - case 8: // I32 - value = data.getInt32(offset); - offset += 4; - break; - case 10: // I64 - // Note: using BigInt to avoid precision loss on 64-bit integers - value = data.getBigInt64(offset).toString(); - offset += 8; - break; - case 11: { // BINARY/STRING - const strLen = data.getInt32(offset); - offset += 4; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseBinaryProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - case 13: { // MAP - const keyType = data.getUint8(offset++); - const valType = data.getUint8(offset++); - const mapSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < mapSize; i++) { - const k = this.readBinaryType(data, offset, keyType); - offset = k.offset; - const v = this.readBinaryType(data, offset, valType); - offset = v.offset; - value.push({ key: k.value, val: v.value }); - } - break; - } - case 14: // SET - case 15: { // LIST - const elemType = data.getUint8(offset++); - const listSize = data.getInt32(offset); - offset += 4; - value = []; - for (let i = 0; i < listSize; i++) { - const elem = this.readBinaryType(data, offset, elemType); - value.push(elem.value); - offset = elem.offset; - } - break; - } - default: - throw new Error(`Unknown Binary Protocol Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Binary protocol types. - * - * @param {number} type - * @returns {string} - */ - getBinaryTypeName(type) { - const types = { 2: "BOOL", 3: "I8", 4: "DOUBLE", 6: "I16", 8: "I32", 10: "I64", 11: "BINARY", 12: "STRUCT", 13: "MAP", 14: "SET", 15: "LIST" }; - return types[type] || `UNKNOWN(${type})`; - } - - // --- TCompactProtocol Implementation --- - - /** - * Parses the incoming schema using TCompactProtocol constraints. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - parseCompactProtocol(data, offset) { - const result = {}; - let lastFieldId = 0; - - while (offset < data.byteLength) { - const byte = data.getUint8(offset++); - if (byte === 0) break; // STOP field - - const modifier = (byte & 0xf0) >> 4; - const fieldType = byte & 0x0f; - - let fieldId; - if (modifier === 0) { - // Long form: read zigzag varint field ID - const idParsed = this.readVarint(data, offset); - fieldId = this.fromZigZag(idParsed.value); - offset = idParsed.offset; - } else { - // Short form: delta - fieldId = lastFieldId + modifier; - } - lastFieldId = fieldId; - - // Types 1 and 2 are boolean true/false encoded directly in the modifier - if (fieldType === 1) { - result[`field_${fieldId}`] = { type: "BOOL", value: true }; - continue; - } else if (fieldType === 2) { - result[`field_${fieldId}`] = { type: "BOOL", value: false }; - continue; - } - - const parsed = this.readCompactType(data, offset, fieldType); - result[`field_${fieldId}`] = { type: this.getCompactTypeName(fieldType), value: parsed.value }; - offset = parsed.offset; - } - return { result, offset }; - } - - /** - * Reads and transforms compact datatypes based on identifier rules. - * - * @param {DataView} data - * @param {number} offset - * @param {number} type - * @returns {Object} - */ - readCompactType(data, offset, type) { - let value, varintParsed; - switch (type) { - case 3: // I8 - value = data.getInt8(offset++); - break; - case 4: // I16 - case 5: // I32 - case 6: // I64 - varintParsed = this.readVarint(data, offset); - value = this.fromZigZag(varintParsed.value); // Decodes ZigZag - offset = varintParsed.offset; - break; - case 7: // DOUBLE - value = data.getFloat64(offset, true); // Little endian - offset += 8; - break; - case 8: { // BINARY/STRING - varintParsed = this.readVarint(data, offset); - const strLen = Number(varintParsed.value); // Not zigzagged - offset = varintParsed.offset; - const strBytes = new Uint8Array(data.buffer, offset, strLen); - value = Utils.byteArrayToUtf8(strBytes); - offset += strLen; - break; - } - case 12: { // STRUCT - const structParsed = this.parseCompactProtocol(data, offset); - value = structParsed.result; - offset = structParsed.offset; - break; - } - // Note: Lists (9), Sets (10), Maps (11) follow slightly different header rules in Compact - // Implement based on the spec provided (e.g., sssstttt for lists) - default: - throw new Error(`Unimplemented/Unknown Compact Type: ${type} at offset ${offset}`); - } - return { value, offset }; - } - - /** - * Returns string names for Compact protocol types. - * - * @param {number} type - * @returns {string} - */ - getCompactTypeName(type) { - const types = { 1: "BOOLEAN_TRUE", 2: "BOOLEAN_FALSE", 3: "I8", 4: "I16", 5: "I32", 6: "I64", 7: "DOUBLE", 8: "BINARY", 9: "LIST", 10: "SET", 11: "MAP", 12: "STRUCT", 13: "UUID" }; - return types[type] || `UNKNOWN(${type})`; - } - - /** - * Variable-length integer parsing logic helper. - * - * @param {DataView} data - * @param {number} offset - * @returns {Object} - */ - readVarint(data, offset) { - let result = 0n; - let shift = 0n; - while (true) { - if (offset >= data.byteLength) throw new Error("EOF reading varint"); - const byte = BigInt(data.getUint8(offset++)); - result |= (byte & 0x7fn) << shift; - if ((byte & 0x80n) === 0n) break; - shift += 7n; - } - return { value: result, offset: offset }; - } - - /** - * Decodes ZigZag parameters to system numbers. - * - * @param {bigint} n - * @returns {bigint} - */ - fromZigZag(n) { - // n >>> 1 ^ -(n & 1) using BigInt to prevent 32-bit truncation - return (n >> 1n) ^ -(n & 1n); - } } export default ThriftDeserialize; diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index ae20948b..c744306a 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,6 +6,9 @@ */ import Operation from "../Operation.mjs"; +import { + buildBinaryStruct +} from "../lib/Thrift.mjs"; /** * Operation to encode a JSON structure into Apache Thrift TBinaryProtocol binary format. @@ -43,119 +46,9 @@ class ThriftSerialize extends Operation { } const bytes = []; - this.buildBinaryStruct(parsedInput, bytes); + buildBinaryStruct(parsedInput, bytes); return new Uint8Array(bytes).buffer; } - - /** - * Recursively parses the JSON object to build out the Thrift byte array structure. - * - * @param {Object} jsonStruct - * @param {number[]} bytes - */ - buildBinaryStruct(jsonStruct, bytes) { - const typeMap = { "BOOL": 2, "I8": 3, "DOUBLE": 4, "I16": 6, "I32": 8, "I64": 10, "BINARY": 11, "STRUCT": 12, "MAP": 13, "SET": 14, "LIST": 15 }; - - for (const [key, fieldData] of Object.entries(jsonStruct)) { - const fieldId = parseInt(key.replace("field_", ""), 10); - if (isNaN(fieldId)) continue; - - const typeName = fieldData.type; - const fieldType = typeMap[typeName]; - const value = fieldData.value; - - // 1. Write Field Type (1 byte) - bytes.push(fieldType); - // 2. Write Field ID (2 bytes, Big Endian) - bytes.push((fieldId >> 8) & 0xFF, fieldId & 0xFF); - - // 3. Write Value - this.writeValue(typeName, value, bytes, typeMap); - } - - // Write T_STOP to close the struct - bytes.push(0); - } - - /** - * Serializes values into the byte stream according to their specific Thrift types. - * - * @param {string} typeName - * @param {*} value - * @param {number[]} bytes - * @param {Object} typeMap - */ - writeValue(typeName, value, bytes, typeMap) { - switch (typeName) { - case "BOOL": - bytes.push(value ? 1 : 0); - break; - case "I8": - bytes.push(value & 0xFF); - break; - case "I16": - bytes.push((value >> 8) & 0xFF, value & 0xFF); - break; - case "I32": - bytes.push((value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF); - break; - case "I64": { - const bigVal = BigInt(value); - for (let i = 7n; i >= 0n; i--) bytes.push(Number((bigVal >> (i * 8n)) & 0xFFn)); - break; - } - case "DOUBLE": { - // 8 bytes IEEE 754 floating point (Big Endian) - const floatView = new DataView(new ArrayBuffer(8)); - floatView.setFloat64(0, value, false); - for (let i = 0; i < 8; i++) bytes.push(floatView.getUint8(i)); - break; - } - case "BINARY": { - const textEncoder = new TextEncoder(); - const strBytes = textEncoder.encode(value); - const len = strBytes.length; - bytes.push((len >> 24) & 0xFF, (len >> 16) & 0xFF, (len >> 8) & 0xFF, len & 0xFF); - strBytes.forEach(b => bytes.push(b)); - break; - } - case "STRUCT": - // Recursively build nested structs - this.buildBinaryStruct(value, bytes); - break; - case "LIST": - case "SET": { - // Expects JSON format: { "elementType": "I32", "elements": [1, 2, 3] } - const elType = typeMap[value.elementType]; - bytes.push(elType); // 1 byte element type - - const listSize = value.elements.length; - bytes.push((listSize >> 24) & 0xFF, (listSize >> 16) & 0xFF, (listSize >> 8) & 0xFF, listSize & 0xFF); // 4 byte size - - // Write each element recursively - value.elements.forEach(el => this.writeValue(value.elementType, el, bytes, typeMap)); - break; - } - case "MAP": { - // Expects JSON format: { "keyType": "I32", "valType": "BINARY", "elements": [{"key": 1, "val": "hello"}] } - const kType = typeMap[value.keyType]; - const vType = typeMap[value.valType]; - bytes.push(kType, vType); // 1 byte key type, 1 byte val type - - const mapSize = value.elements.length; - bytes.push((mapSize >> 24) & 0xFF, (mapSize >> 16) & 0xFF, (mapSize >> 8) & 0xFF, mapSize & 0xFF); // 4 byte size - - // Write pairs - value.elements.forEach(pair => { - this.writeValue(value.keyType, pair.key, bytes, typeMap); - this.writeValue(value.valType, pair.val, bytes, typeMap); - }); - break; - } - default: - throw new Error(`Unsupported serialization type: ${typeName}`); - } - } } export default ThriftSerialize; diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 9d29f50d..3a23506f 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -355,6 +355,8 @@ module.exports = { // testOp(browser, "Take bytes", "test input", "test_output"); testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/); testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]); + testOp(browser, ["Thrift Serialize", "To Hex"], "{\"field_1\": { \"type\": \"I32\", \"value\": 100 }}", "08 00 01 00 00 00 64 00"); + testOpHtml(browser, ["From Hex", "Thrift Deserialize"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"]]); testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/); // testOp(browser, "To BCD", "test input", "test_output"); // testOp(browser, "To Base", "test input", "test_output"); diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs new file mode 100644 index 00000000..7a975c86 --- /dev/null +++ b/tests/node/tests/lib/Thrift.mjs @@ -0,0 +1,644 @@ +import TestRegister from "../../../lib/TestRegister.mjs"; +import { + buildBinaryStruct, + writeValue, + parseBinaryProtocol, + readBinaryType, + getBinaryTypeName, + parseCompactProtocol, + readCompactType, + getCompactTypeName, + readVarint, + fromZigZag +} from "../../../../src/core/lib/Thrift.mjs"; +import it from "../../assertionHandler.mjs"; +import assert from "assert"; + +TestRegister.addApiTests([ + // ===== readVarint tests ===== + it("Thrift: readVarint - single byte (0)", () => { + const bytes = new Uint8Array([0x00]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 0n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - single byte (127)", () => { + const bytes = new Uint8Array([0x7F]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 127n); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readVarint - two bytes (128)", () => { + const bytes = new Uint8Array([0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 128n); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readVarint - multiple bytes (16384)", () => { + const bytes = new Uint8Array([0x80, 0x80, 0x01]); + const data = new DataView(bytes.buffer); + const result = readVarint(data, 0); + assert.strictEqual(result.value, 16384n); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - starting from non-zero offset", () => { + const bytes = new Uint8Array([0xFF, 0xFF, 0x42]); // 0xFF is continuation, 0xFF is continuation, 0x42 is end + const data = new DataView(bytes.buffer); + const result = readVarint(data, 1); + assert.strictEqual(result.offset, 3); + }), + + it("Thrift: readVarint - EOF error", () => { + const bytes = new Uint8Array([0x80]); // Missing continuation byte + const data = new DataView(bytes.buffer); + assert.throws(() => readVarint(data, 0), /EOF reading varint/); + }), + + // ===== fromZigZag tests ===== + it("Thrift: fromZigZag - zero", () => { + assert.strictEqual(fromZigZag(0n), 0n); + }), + + it("Thrift: fromZigZag - positive numbers", () => { + assert.strictEqual(fromZigZag(2n), 1n); + assert.strictEqual(fromZigZag(4n), 2n); + assert.strictEqual(fromZigZag(6n), 3n); + }), + + it("Thrift: fromZigZag - negative numbers", () => { + assert.strictEqual(fromZigZag(1n), -1n); + assert.strictEqual(fromZigZag(3n), -2n); + assert.strictEqual(fromZigZag(5n), -3n); + }), + + it("Thrift: fromZigZag - large positive", () => { + assert.strictEqual(fromZigZag(BigInt("1000000000")), BigInt("500000000")); + }), + + it("Thrift: fromZigZag - large negative", () => { + assert.strictEqual(fromZigZag(BigInt("1000000001")), BigInt("-500000001")); + }), + + // ===== getBinaryTypeName tests ===== + it("Thrift: getBinaryTypeName - all type mappings", () => { + assert.strictEqual(getBinaryTypeName(2), "BOOL"); + assert.strictEqual(getBinaryTypeName(3), "I8"); + assert.strictEqual(getBinaryTypeName(4), "DOUBLE"); + assert.strictEqual(getBinaryTypeName(6), "I16"); + assert.strictEqual(getBinaryTypeName(8), "I32"); + assert.strictEqual(getBinaryTypeName(10), "I64"); + assert.strictEqual(getBinaryTypeName(11), "BINARY"); + assert.strictEqual(getBinaryTypeName(12), "STRUCT"); + assert.strictEqual(getBinaryTypeName(13), "MAP"); + assert.strictEqual(getBinaryTypeName(14), "SET"); + assert.strictEqual(getBinaryTypeName(15), "LIST"); + }), + + it("Thrift: getBinaryTypeName - unknown type", () => { + assert.strictEqual(getBinaryTypeName(99), "UNKNOWN(99)"); + }), + + // ===== getCompactTypeName tests ===== + it("Thrift: getCompactTypeName - all type mappings", () => { + assert.strictEqual(getCompactTypeName(1), "BOOLEAN_TRUE"); + assert.strictEqual(getCompactTypeName(2), "BOOLEAN_FALSE"); + assert.strictEqual(getCompactTypeName(3), "I8"); + assert.strictEqual(getCompactTypeName(4), "I16"); + assert.strictEqual(getCompactTypeName(5), "I32"); + assert.strictEqual(getCompactTypeName(6), "I64"); + assert.strictEqual(getCompactTypeName(7), "DOUBLE"); + assert.strictEqual(getCompactTypeName(8), "BINARY"); + assert.strictEqual(getCompactTypeName(9), "LIST"); + assert.strictEqual(getCompactTypeName(10), "SET"); + assert.strictEqual(getCompactTypeName(11), "MAP"); + assert.strictEqual(getCompactTypeName(12), "STRUCT"); + assert.strictEqual(getCompactTypeName(13), "UUID"); + }), + + it("Thrift: getCompactTypeName - unknown type", () => { + assert.strictEqual(getCompactTypeName(99), "UNKNOWN(99)"); + }), + + // ===== writeValue tests - basic types ===== + it("Thrift: writeValue - BOOL true", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", true, bytes, typeMap); + assert.deepStrictEqual(bytes, [1]); + }), + + it("Thrift: writeValue - BOOL false", () => { + const bytes = []; + const typeMap = { "BOOL": 2 }; + writeValue("BOOL", false, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 zero", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0]); + }), + + it("Thrift: writeValue - I8 positive", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 42, bytes, typeMap); + assert.deepStrictEqual(bytes, [42]); + }), + + it("Thrift: writeValue - I8 max", () => { + const bytes = []; + const typeMap = { "I8": 3 }; + writeValue("I8", 127, bytes, typeMap); + assert.deepStrictEqual(bytes, [127]); + }), + + it("Thrift: writeValue - I16", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0x1234, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34]); + }), + + it("Thrift: writeValue - I16 zero", () => { + const bytes = []; + const typeMap = { "I16": 6 }; + writeValue("I16", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0]); + }), + + it("Thrift: writeValue - I32", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0x12345678, bytes, typeMap); + assert.deepStrictEqual(bytes, [0x12, 0x34, 0x56, 0x78]); + }), + + it("Thrift: writeValue - I32 zero", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", 0, bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - I32 negative", () => { + const bytes = []; + const typeMap = { "I32": 8 }; + writeValue("I32", -1, bytes, typeMap); + assert.deepStrictEqual(bytes, [0xFF, 0xFF, 0xFF, 0xFF]); + }), + + it("Thrift: writeValue - I64", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0x0102030405060708", bytes, typeMap); + assert.deepStrictEqual(bytes, [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + }), + + it("Thrift: writeValue - I64 zero", () => { + const bytes = []; + const typeMap = { "I64": 10 }; + writeValue("I64", "0", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - DOUBLE", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 3.14159, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + // Verify it's a valid IEEE 754 double by reading it back + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.ok(Math.abs(view.getFloat64(0, false) - 3.14159) < 0.00001); + }), + + it("Thrift: writeValue - DOUBLE zero", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", 0.0, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), 0); + }), + + it("Thrift: writeValue - DOUBLE negative", () => { + const bytes = []; + const typeMap = { "DOUBLE": 4 }; + writeValue("DOUBLE", -42.5, bytes, typeMap); + assert.strictEqual(bytes.length, 8); + const view = new DataView(new ArrayBuffer(8)); + bytes.forEach((b, i) => view.setUint8(i, b)); + assert.strictEqual(view.getFloat64(0, false), -42.5); + }), + + it("Thrift: writeValue - BINARY empty string", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "", bytes, typeMap); + assert.deepStrictEqual(bytes, [0, 0, 0, 0]); + }), + + it("Thrift: writeValue - BINARY ASCII", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "hello", bytes, typeMap); + const expected = [0, 0, 0, 5, ...new TextEncoder().encode("hello")]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - BINARY UTF-8", () => { + const bytes = []; + const typeMap = { "BINARY": 11 }; + writeValue("BINARY", "你好", bytes, typeMap); + const encoded = new TextEncoder().encode("你好"); + const expected = [0, 0, 0, encoded.length, ...Array.from(encoded)]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== writeValue tests - collections ===== + it("Thrift: writeValue - LIST of I32 empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [] }; + writeValue("LIST", listValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 0, 0, 0, 0]); + }), + + it("Thrift: writeValue - LIST of I32", () => { + const bytes = []; + const typeMap = { "I32": 8, "LIST": 15 }; + const listValue = { elementType: "I32", elements: [1, 2, 3] }; + writeValue("LIST", listValue, bytes, typeMap); + const expected = [ + 8, // element type (I32) + 0, 0, 0, 3, // list size = 3 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2, // element 2 + 0, 0, 0, 3 // element 3 + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - SET of BINARY", () => { + const bytes = []; + const typeMap = { "BINARY": 11, "SET": 14 }; + const setValue = { elementType: "BINARY", elements: ["a"] }; + writeValue("SET", setValue, bytes, typeMap); + const encoded = new TextEncoder().encode("a"); + const expected = [ + 11, // element type (BINARY) + 0, 0, 0, 1, // set size = 1 + 0, 0, 0, 1, // string length + ...encoded // string data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: writeValue - MAP empty", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { keyType: "I32", valType: "BINARY", elements: [] }; + writeValue("MAP", mapValue, bytes, typeMap); + assert.deepStrictEqual(bytes, [8, 11, 0, 0, 0, 0]); // key type, val type, size = 0 + }), + + it("Thrift: writeValue - MAP with entry", () => { + const bytes = []; + const typeMap = { "I32": 8, "BINARY": 11, "MAP": 13 }; + const mapValue = { + keyType: "I32", + valType: "BINARY", + elements: [{ key: 1, val: "test" }] + }; + writeValue("MAP", mapValue, bytes, typeMap); + const encoded = new TextEncoder().encode("test"); + const expected = [ + 8, 11, // key type, val type + 0, 0, 0, 1, // map size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 4, // val length + ...encoded // val data + ]; + assert.deepStrictEqual(bytes, expected); + }), + + // ===== buildBinaryStruct tests ===== + it("Thrift: buildBinaryStruct - simple struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - multiple fields", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "I32", value: 42 }, + field_2: { type: "BINARY", value: "hello" } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should end with T_STOP + assert.strictEqual(bytes[bytes.length - 1], 0); + }), + + it("Thrift: buildBinaryStruct - BOOL field", () => { + const bytes = []; + const jsonStruct = { + field_1: { type: "BOOL", value: true } + }; + buildBinaryStruct(jsonStruct, bytes); + const expected = [ + 2, 0, 1, // field type (BOOL), field id = 1 + 1, // value = true + 0 // T_STOP + ]; + assert.deepStrictEqual(bytes, expected); + }), + + it("Thrift: buildBinaryStruct - nested struct", () => { + const bytes = []; + const jsonStruct = { + field_1: { + type: "STRUCT", + value: { + field_1: { type: "I32", value: 10 } + } + } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should contain nested structure with T_STOP markers + assert.ok(bytes.length > 0); + assert.strictEqual(bytes[bytes.length - 1], 0); // ends with T_STOP + }), + + it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { + const bytes = []; + const jsonStruct = { + invalid_field: { type: "I32", value: 42 }, + field_1: { type: "I32", value: 10 } + }; + buildBinaryStruct(jsonStruct, bytes); + // Should skip invalid_field and only process field_1 + assert.strictEqual(bytes[0], 8); // field type + assert.strictEqual(bytes[3], 10); // field value + }), + + // ===== readBinaryType tests ===== + it("Thrift: readBinaryType - BOOL true", () => { + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, true); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - BOOL false", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 2); + assert.strictEqual(result.value, false); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readBinaryType - I16", () => { + const bytes = new Uint8Array([0x12, 0x34]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 6); + assert.strictEqual(result.value, 0x1234); + assert.strictEqual(result.offset, 2); + }), + + it("Thrift: readBinaryType - I32", () => { + const bytes = new Uint8Array([0x12, 0x34, 0x56, 0x78]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 8); + assert.strictEqual(result.value, 0x12345678); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - I64", () => { + const bytes = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 10); + assert.strictEqual(result.value, "66"); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 3.14159, false); + const result = readBinaryType(view, 0, 4); + assert.ok(Math.abs(result.value - 3.14159) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readBinaryType - BINARY empty", () => { + const bytes = new Uint8Array([0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 4); + }), + + it("Thrift: readBinaryType - BINARY with data", () => { + const str = "hello"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([0, 0, 0, 5, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 11); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 9); + }), + + it("Thrift: readBinaryType - LIST of I32", () => { + const bytes = new Uint8Array([ + 8, // element type (I32) + 0, 0, 0, 2, // size = 2 + 0, 0, 0, 1, // element 1 + 0, 0, 0, 2 // element 2 + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, [1, 2]); + assert.strictEqual(result.offset, 13); + }), + + it("Thrift: readBinaryType - LIST empty", () => { + const bytes = new Uint8Array([8, 0, 0, 0, 0]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 15); + assert.deepStrictEqual(result.value, []); + assert.strictEqual(result.offset, 5); + }), + + it("Thrift: readBinaryType - MAP", () => { + const str = "key"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([ + 8, 11, // key type (I32), val type (BINARY) + 0, 0, 0, 1, // size = 1 + 0, 0, 0, 1, // key value + 0, 0, 0, 3, // val length + ...encoded // val data + ]); + const data = new DataView(bytes.buffer); + const result = readBinaryType(data, 0, 13); + assert.deepStrictEqual(result.value, [{ key: 1, val: "key" }]); + assert.strictEqual(result.offset, 13); + }), + + // ===== parseBinaryProtocol tests ===== + it("Thrift: parseBinaryProtocol - simple I32", () => { + const bytes = new Uint8Array([ + 8, 0, 1, // field type (I32), field id = 1 + 0, 0, 0, 42, // value + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.deepStrictEqual(result.result.field_1, { type: "I32", value: 42 }); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: parseBinaryProtocol - multiple fields", () => { + const bytes = new Uint8Array([ + 2, 0, 1, // BOOL field 1 + 1, // value = true + 8, 0, 2, // I32 field 2 + 0, 0, 0, 42, // value = 42 + 0 // T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOL"); + assert.strictEqual(result.result.field_1.value, true); + assert.strictEqual(result.result.field_2.type, "I32"); + assert.strictEqual(result.result.field_2.value, 42); + }), + + it("Thrift: parseBinaryProtocol - nested struct", () => { + const bytes = new Uint8Array([ + 12, 0, 1, // STRUCT field 1 + 8, 0, 1, // nested I32 field 1 + 0, 0, 0, 10, // value + 0, // nested T_STOP + 0 // outer T_STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseBinaryProtocol(data, 0); + assert.ok(result.result.field_1); + assert.strictEqual(result.result.field_1.type, "STRUCT"); + assert.deepStrictEqual(result.result.field_1.value.field_1, { type: "I32", value: 10 }); + }), + + // ===== readCompactType tests ===== + it("Thrift: readCompactType - I8", () => { + const bytes = new Uint8Array([42]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 3); + assert.strictEqual(result.value, 42); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - I16 zigzag", () => { + // ZigZag of -1 is 1 + const bytes = new Uint8Array([1]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 4); + assert.strictEqual(result.value, -1n); + }), + + it("Thrift: readCompactType - I32 zigzag", () => { + // ZigZag of 100 is 200 + const bytes = new Uint8Array([200]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 5); + assert.strictEqual(result.value, 100n); + }), + + it("Thrift: readCompactType - DOUBLE", () => { + const bytes = new Uint8Array(8); + const view = new DataView(bytes.buffer); + view.setFloat64(0, 2.71828, true); + const result = readCompactType(view, 0, 7); + assert.ok(Math.abs(result.value - 2.71828) < 0.00001); + assert.strictEqual(result.offset, 8); + }), + + it("Thrift: readCompactType - BINARY empty", () => { + const bytes = new Uint8Array([0]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, ""); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: readCompactType - BINARY with data", () => { + const str = "test"; + const encoded = new TextEncoder().encode(str); + const bytes = new Uint8Array([4, ...encoded]); + const data = new DataView(bytes.buffer); + const result = readCompactType(data, 0, 8); + assert.strictEqual(result.value, str); + assert.strictEqual(result.offset, 5); + }), + + // ===== parseCompactProtocol tests ===== + it("Thrift: parseCompactProtocol - empty struct", () => { + const bytes = new Uint8Array([0]); // Just STOP + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.deepStrictEqual(result.result, {}); + assert.strictEqual(result.offset, 1); + }), + + it("Thrift: parseCompactProtocol - boolean true field", () => { + const bytes = new Uint8Array([ + 0x11, // field modifier=1 (delta=1), type=1 (BOOL_TRUE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_TRUE"); + assert.strictEqual(result.result.field_1.value, true); + }), + + it("Thrift: parseCompactProtocol - boolean false field", () => { + const bytes = new Uint8Array([ + 0x12, // field modifier=1 (delta=1), type=2 (BOOL_FALSE) + 0 // STOP + ]); + const data = new DataView(bytes.buffer); + const result = parseCompactProtocol(data, 0); + assert.strictEqual(result.result.field_1.type, "BOOLEAN_FALSE"); + assert.strictEqual(result.result.field_1.value, false); + }), + +]); From 90a9c38a4fa9930d19d123932f8acf3438c19d13 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:25:33 +0300 Subject: [PATCH 04/17] fix: add eslint-disable comments in Thrift tests --- src/core/operations/ThriftSerialize.mjs | 2 +- tests/node/tests/lib/Thrift.mjs | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index c744306a..b7c06fe5 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,7 +6,7 @@ */ import Operation from "../Operation.mjs"; -import { +import { buildBinaryStruct } from "../lib/Thrift.mjs"; diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index 7a975c86..b980502d 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -336,6 +336,7 @@ TestRegister.addApiTests([ // ===== buildBinaryStruct tests ===== it("Thrift: buildBinaryStruct - simple struct", () => { const bytes = []; + // eslint-disable-next-line camelcase const jsonStruct = { field_1: { type: "I32", value: 42 } }; @@ -350,6 +351,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - multiple fields", () => { const bytes = []; + // eslint-disable-next-line camelcase const jsonStruct = { field_1: { type: "I32", value: 42 }, field_2: { type: "BINARY", value: "hello" } @@ -361,6 +363,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - BOOL field", () => { const bytes = []; + // eslint-disable-next-line camelcase const jsonStruct = { field_1: { type: "BOOL", value: true } }; @@ -375,6 +378,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - nested struct", () => { const bytes = []; + // eslint-disable-next-line camelcase const jsonStruct = { field_1: { type: "STRUCT", @@ -391,6 +395,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { const bytes = []; + // eslint-disable-next-line camelcase const jsonStruct = { invalid_field: { type: "I32", value: 42 }, field_1: { type: "I32", value: 10 } From 2ebf46e6ca7f75f2292d755513b41edd4d565a88 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:25:33 +0300 Subject: [PATCH 05/17] fix: add eslint-disable comments in Thrift tests --- src/core/operations/ThriftSerialize.mjs | 2 +- tests/node/tests/lib/Thrift.mjs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index c744306a..b7c06fe5 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,7 +6,7 @@ */ import Operation from "../Operation.mjs"; -import { +import { buildBinaryStruct } from "../lib/Thrift.mjs"; diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index 7a975c86..7c153347 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -336,9 +336,11 @@ TestRegister.addApiTests([ // ===== buildBinaryStruct tests ===== it("Thrift: buildBinaryStruct - simple struct", () => { const bytes = []; + // eslint-disable camelcase const jsonStruct = { field_1: { type: "I32", value: 42 } }; + // eslint-enable camelcase buildBinaryStruct(jsonStruct, bytes); const expected = [ 8, 0, 1, // field type (I32), field id = 1 @@ -350,10 +352,12 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - multiple fields", () => { const bytes = []; + // eslint-disable camelcase const jsonStruct = { field_1: { type: "I32", value: 42 }, field_2: { type: "BINARY", value: "hello" } }; + // eslint-enable camelcase buildBinaryStruct(jsonStruct, bytes); // Should end with T_STOP assert.strictEqual(bytes[bytes.length - 1], 0); @@ -361,9 +365,11 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - BOOL field", () => { const bytes = []; + // eslint-disable camelcase const jsonStruct = { field_1: { type: "BOOL", value: true } }; + // eslint-enable camelcase buildBinaryStruct(jsonStruct, bytes); const expected = [ 2, 0, 1, // field type (BOOL), field id = 1 @@ -375,6 +381,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - nested struct", () => { const bytes = []; + // eslint-disable camelcase const jsonStruct = { field_1: { type: "STRUCT", @@ -383,6 +390,7 @@ TestRegister.addApiTests([ } } }; + // eslint-enable camelcase buildBinaryStruct(jsonStruct, bytes); // Should contain nested structure with T_STOP markers assert.ok(bytes.length > 0); @@ -391,10 +399,12 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { const bytes = []; + // eslint-disable camelcase const jsonStruct = { invalid_field: { type: "I32", value: 42 }, field_1: { type: "I32", value: 10 } }; + // eslint-enable camelcase buildBinaryStruct(jsonStruct, bytes); // Should skip invalid_field and only process field_1 assert.strictEqual(bytes[0], 8); // field type From 6cc2d8e745867405c76733514a5924a4386877e4 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:33:03 +0300 Subject: [PATCH 06/17] fix: update eslint-disable comments to block style in Thrift tests --- tests/node/tests/lib/Thrift.mjs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index 7c153347..ca791300 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -336,11 +336,11 @@ TestRegister.addApiTests([ // ===== buildBinaryStruct tests ===== it("Thrift: buildBinaryStruct - simple struct", () => { const bytes = []; - // eslint-disable camelcase + /* eslint-disable camelcase */ const jsonStruct = { field_1: { type: "I32", value: 42 } }; - // eslint-enable camelcase + /* eslint-enable camelcase */ buildBinaryStruct(jsonStruct, bytes); const expected = [ 8, 0, 1, // field type (I32), field id = 1 @@ -352,12 +352,12 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - multiple fields", () => { const bytes = []; - // eslint-disable camelcase + /* eslint-disable camelcase */ const jsonStruct = { field_1: { type: "I32", value: 42 }, field_2: { type: "BINARY", value: "hello" } }; - // eslint-enable camelcase + /* eslint-enable camelcase */ buildBinaryStruct(jsonStruct, bytes); // Should end with T_STOP assert.strictEqual(bytes[bytes.length - 1], 0); @@ -365,11 +365,11 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - BOOL field", () => { const bytes = []; - // eslint-disable camelcase + /* eslint-disable camelcase */ const jsonStruct = { field_1: { type: "BOOL", value: true } }; - // eslint-enable camelcase + /* eslint-enable camelcase */ buildBinaryStruct(jsonStruct, bytes); const expected = [ 2, 0, 1, // field type (BOOL), field id = 1 @@ -381,7 +381,7 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - nested struct", () => { const bytes = []; - // eslint-disable camelcase + /* eslint-disable camelcase */ const jsonStruct = { field_1: { type: "STRUCT", @@ -390,7 +390,7 @@ TestRegister.addApiTests([ } } }; - // eslint-enable camelcase + /* eslint-enable camelcase */ buildBinaryStruct(jsonStruct, bytes); // Should contain nested structure with T_STOP markers assert.ok(bytes.length > 0); @@ -399,12 +399,12 @@ TestRegister.addApiTests([ it("Thrift: buildBinaryStruct - invalid field ID skipped", () => { const bytes = []; - // eslint-disable camelcase + /* eslint-disable camelcase */ const jsonStruct = { invalid_field: { type: "I32", value: 42 }, field_1: { type: "I32", value: 10 } }; - // eslint-enable camelcase + /* eslint-enable camelcase */ buildBinaryStruct(jsonStruct, bytes); // Should skip invalid_field and only process field_1 assert.strictEqual(bytes[0], 8); // field type From 42aee999d7d8b89acf6827ea3a137508305b014e Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 14:45:41 +0300 Subject: [PATCH 07/17] fix: add "JSON Beautify" operation to Thrift Deserialize test case --- tests/browser/02_ops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index 88c97c4e..b6a923e5 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -356,7 +356,7 @@ module.exports = { testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/); testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]); testOp(browser, ["Thrift Serialize", "To Hex"], "{\"field_1\": { \"type\": \"I32\", \"value\": 100 }}", "08 00 01 00 00 00 64 00"); - testOpHtml(browser, ["From Hex", "Thrift Deserialize"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"]]); + testOpHtml(browser, ["From Hex", "Thrift Deserialize", "JSON Beautify"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"], []]); testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/); // testOp(browser, "To BCD", "test input", "test_output"); // testOp(browser, "To Base", "test input", "test_output"); From 3636d4309f6513d2c918292de63da9bb3939822a Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 15:08:15 +0300 Subject: [PATCH 08/17] fix: update Thrift Deserialize test case to remove JSON Beautify operation to prevent element was not found error --- tests/browser/02_ops.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index b6a923e5..2d272f25 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -356,7 +356,7 @@ module.exports = { testOp(browser, "Tar", "test input", /^file\.txt\x00{92}/); testOp(browser, "Template", "{\"one\": 1, \"two\": 2}", "1 2", ["{{ one }} {{ two }}"]); testOp(browser, ["Thrift Serialize", "To Hex"], "{\"field_1\": { \"type\": \"I32\", \"value\": 100 }}", "08 00 01 00 00 00 64 00"); - testOpHtml(browser, ["From Hex", "Thrift Deserialize", "JSON Beautify"], "08 00 01 00 00 00 64 00", ".json-dict .json-literal", "100", [["Auto"], ["TBinaryProtocol"], []]); + testOp(browser, ["From Hex", "Thrift Deserialize"], "08 00 01 00 00 00 64 00", /100/, [["Auto"], ["TBinaryProtocol"]]); testOpHtml(browser, "Text Encoding Brute Force", "test input", "tr:nth-of-type(4) td:last-child", /t\u2400e\u2400s\u2400t\u2400/); // testOp(browser, "To BCD", "test input", "test_output"); // testOp(browser, "To Base", "test input", "test_output"); From ce49b545b8f2e31ee5ac79ee61b6daa58b8c5d10 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 15:45:42 +0300 Subject: [PATCH 09/17] fix: add missing Thrift import in Node API test runner --- tests/node/index.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/node/index.mjs b/tests/node/index.mjs index 52670d48..6a5948bf 100644 --- a/tests/node/index.mjs +++ b/tests/node/index.mjs @@ -25,6 +25,7 @@ import "./tests/NodeDish.mjs"; import "./tests/Utils.mjs"; import "./tests/Categories.mjs"; import "./tests/lib/BigIntUtils.mjs"; +import "./tests/lib/Thrift.mjs"; const testStatus = { allTestsPassing: true, From 294f5973931eb7adcb0ff80a02d2825ae28af7ed Mon Sep 17 00:00:00 2001 From: engin0223 Date: Fri, 5 Jun 2026 15:51:34 +0300 Subject: [PATCH 10/17] fix: correct byte offsets and field types in Thrift tests and correct compact protocol expect value --- tests/node/tests/lib/Thrift.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index ca791300..075dda60 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -408,7 +408,7 @@ TestRegister.addApiTests([ buildBinaryStruct(jsonStruct, bytes); // Should skip invalid_field and only process field_1 assert.strictEqual(bytes[0], 8); // field type - assert.strictEqual(bytes[3], 10); // field value + assert.strictEqual(bytes[6], 10); // field value }), // ===== readBinaryType tests ===== @@ -521,7 +521,7 @@ TestRegister.addApiTests([ const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 13); assert.deepStrictEqual(result.value, [{ key: 1, val: "key" }]); - assert.strictEqual(result.offset, 13); + assert.strictEqual(result.offset, 17); }), // ===== parseBinaryProtocol tests ===== @@ -587,7 +587,7 @@ TestRegister.addApiTests([ it("Thrift: readCompactType - I32 zigzag", () => { // ZigZag of 100 is 200 - const bytes = new Uint8Array([200]); + const bytes = new Uint8Array([200, 1]); const data = new DataView(bytes.buffer); const result = readCompactType(data, 0, 5); assert.strictEqual(result.value, 100n); @@ -636,7 +636,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = parseCompactProtocol(data, 0); - assert.strictEqual(result.result.field_1.type, "BOOLEAN_TRUE"); + assert.strictEqual(result.result.field_1.type, "BOOL"); assert.strictEqual(result.result.field_1.value, true); }), @@ -647,7 +647,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = parseCompactProtocol(data, 0); - assert.strictEqual(result.result.field_1.type, "BOOLEAN_FALSE"); + assert.strictEqual(result.result.field_1.type, "BOOL"); assert.strictEqual(result.result.field_1.value, false); }), From 064d625954a9eddcae63aa83cd0ae3c91cc5c585 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Sat, 6 Jun 2026 13:37:49 +0300 Subject: [PATCH 11/17] fix: replace generic Error with OperationError in Thrift serialization and deserialization operations --- src/core/operations/ThriftDeserialize.mjs | 3 ++- src/core/operations/ThriftSerialize.mjs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/operations/ThriftDeserialize.mjs b/src/core/operations/ThriftDeserialize.mjs index 0a00af70..4187849d 100644 --- a/src/core/operations/ThriftDeserialize.mjs +++ b/src/core/operations/ThriftDeserialize.mjs @@ -6,6 +6,7 @@ */ import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; import { parseBinaryProtocol, parseCompactProtocol @@ -56,7 +57,7 @@ class ThriftDeserialize extends Operation { } return JSON.stringify(decodedObject, null, 4); } catch (err) { - return `Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`; + throw new OperationError(`Error decoding Thrift payload: ${err.message}\n\nPartial output:\n${JSON.stringify(decodedObject, null, 4)}`); } } } diff --git a/src/core/operations/ThriftSerialize.mjs b/src/core/operations/ThriftSerialize.mjs index b7c06fe5..1584f033 100644 --- a/src/core/operations/ThriftSerialize.mjs +++ b/src/core/operations/ThriftSerialize.mjs @@ -6,6 +6,7 @@ */ import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; import { buildBinaryStruct } from "../lib/Thrift.mjs"; @@ -42,7 +43,7 @@ class ThriftSerialize extends Operation { try { parsedInput = JSON.parse(input); } catch (e) { - throw new Error("Input must be valid JSON."); + throw new OperationError("Input must be valid JSON."); } const bytes = []; From 7fd31b9bab4278c4c4ce20ba9c277727eb4d0dd6 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:47:44 +0000 Subject: [PATCH 12/17] Connect Operations tests to main test suite --- tests/operations/index.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 44792560..f5ff5fc5 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -179,6 +179,7 @@ import "./tests/TakeNthBytes.mjs"; import "./tests/Template.mjs"; import "./tests/TextEncodingBruteForce.mjs"; import "./tests/TextIntegerConverter.mjs"; +import "./tests/Thrift.mjs"; import "./tests/ToFromInsensitiveRegex.mjs"; import "./tests/TranslateDateTimeFormat.mjs"; import "./tests/Typex.mjs"; From 4a9fd9016ef2fbc4f281578d51b03574da249bd7 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:48:29 +0000 Subject: [PATCH 13/17] Add failing test for LIST round-trip --- tests/operations/tests/Thrift.mjs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/operations/tests/Thrift.mjs b/tests/operations/tests/Thrift.mjs index 1d0785f6..fd1b29e8 100644 --- a/tests/operations/tests/Thrift.mjs +++ b/tests/operations/tests/Thrift.mjs @@ -119,5 +119,18 @@ TestRegister.addTests([ { op: "From Hex", args: ["Auto"] }, { op: "Thrift Deserialize", args: ["TCompactProtocol"] } ] + }, + { + name: "Thrift Deserialize/Serialize: TBinaryProtocol List round-trip", + // Validates that a TBinaryProtocal LIST is successfully deserialised + // and that the result can be Serialized back into the original binary data + input: "0f 00 0b 08 00 00 00 03 00 00 00 01 00 00 00 02 00 00 00 03 00", + expectedOutput: "0f 00 0b 08 00 00 00 03 00 00 00 01 00 00 00 02 00 00 00 03 00", + recipeConfig: [ + { "op": "From Hex", "args": ["Auto"] }, + { "op": "Thrift Deserialize", "args": ["TBinaryProtocol"] }, + { "op": "Thrift Serialize", "args": [] }, + { "op": "To Hex", "args": ["Space", 0] } + ] } ]); From b88ef53b781466532d2059bd538ba9a3e141a6c4 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Sat, 27 Jun 2026 12:58:51 +0300 Subject: [PATCH 14/17] fix(thrift): preserve inner object structure for LIST, SET, and MAP to ensure correct round-tripping --- src/core/lib/Thrift.mjs | 23 +++++++++--- tests/operations/tests/Thrift.mjs | 58 +++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/core/lib/Thrift.mjs b/src/core/lib/Thrift.mjs index 28212cf1..64549158 100644 --- a/src/core/lib/Thrift.mjs +++ b/src/core/lib/Thrift.mjs @@ -193,29 +193,34 @@ export function readBinaryType(data, offset, type) { case 13: { // MAP const keyType = data.getUint8(offset++); const valType = data.getUint8(offset++); + const keyTypeName = getBinaryTypeName(keyType); + const valTypeName = getBinaryTypeName(valType); const mapSize = data.getInt32(offset); offset += 4; - value = []; + const elements = []; for (let i = 0; i < mapSize; i++) { const k = readBinaryType(data, offset, keyType); offset = k.offset; const v = readBinaryType(data, offset, valType); offset = v.offset; - value.push({ key: k.value, val: v.value }); + elements.push({ key: k.value, val: v.value }); } + value = { keyType: keyTypeName, valType: valTypeName, elements: elements }; break; } case 14: // SET case 15: { // LIST const elemType = data.getUint8(offset++); + const elemTypeName = getBinaryTypeName(elemType); const listSize = data.getInt32(offset); offset += 4; - value = []; + const elements = []; for (let i = 0; i < listSize; i++) { const elem = readBinaryType(data, offset, elemType); - value.push(elem.value); + elements.push(elem.value); offset = elem.offset; } + value = { elementType: elemTypeName, elements: elements }; break; } default: @@ -298,10 +303,18 @@ export function readCompactType(data, offset, type) { value = data.getInt8(offset++); break; case 4: // I16 + varintParsed = readVarint(data, offset); + value = Number(fromZigZag(varintParsed.value)); + offset = varintParsed.offset; + break; case 5: // I32 + varintParsed = readVarint(data, offset); + value = Number(fromZigZag(varintParsed.value)); + offset = varintParsed.offset; + break; case 6: // I64 varintParsed = readVarint(data, offset); - value = fromZigZag(varintParsed.value); // Decodes ZigZag + value = fromZigZag(varintParsed.value).toString(); offset = varintParsed.offset; break; case 7: // DOUBLE diff --git a/tests/operations/tests/Thrift.mjs b/tests/operations/tests/Thrift.mjs index fd1b29e8..6b47ede3 100644 --- a/tests/operations/tests/Thrift.mjs +++ b/tests/operations/tests/Thrift.mjs @@ -28,7 +28,7 @@ TestRegister.addTests([ // STOP: 00 expectedOutput: "08 00 01 00 00 05 39 0b 00 02 00 00 00 04 54 65 73 74 02 00 03 01 00", recipeConfig: [ - { op: "Thrift Serialize", args: ["TBinaryProtocol"] }, + { op: "Thrift Serialize", args: [] }, { op: "To Hex", args: ["Space", 0] } ] }, @@ -50,7 +50,30 @@ TestRegister.addTests([ // STOP: 00 expectedOutput: "0f 00 01 08 00 00 00 02 00 00 00 0a 00 00 00 14 00", recipeConfig: [ - { op: "Thrift Serialize", args: ["TBinaryProtocol"] }, + { op: "Thrift Serialize", args: [] }, + { op: "To Hex", args: ["Space", 0] } + ] + }, + + { + name: "Thrift Serialize: TBinaryProtocol (Set of BINARY)", + input: JSON.stringify({ + "field_1": { + "type": "SET", + "value": { + "elementType": "BINARY", + "elements": ["a", "b"] + } + } + }), + // Hex breakdown: + // Field 1 (SET): 0e 00 01 + // Element Type (BINARY = 0b), Size (2 = 00 00 00 02) + // Values: 00 00 00 01 61, 00 00 00 01 62 + // STOP: 00 + expectedOutput: "0e 00 01 0b 00 00 00 02 00 00 00 01 61 00 00 00 01 62 00", + recipeConfig: [ + { op: "Thrift Serialize", args: [] }, { op: "To Hex", args: ["Space", 0] } ] }, @@ -77,7 +100,10 @@ TestRegister.addTests([ expectedOutput: formatJson({ "field_1": { "type": "LIST", - "value": [10, 20] + "value": { + "elementType": "I32", + "elements": [10, 20] + } } }), recipeConfig: [ @@ -132,5 +158,31 @@ TestRegister.addTests([ { "op": "Thrift Serialize", "args": [] }, { "op": "To Hex", "args": ["Space", 0] } ] + }, + { + name: "Thrift Deserialize/Serialize: TBinaryProtocol Set round-trip", + // Validates that a TBinaryProtocal SET is successfully deserialised + // and that the result can be Serialized back into the original binary data + input: "0e 00 01 0b 00 00 00 02 00 00 00 01 61 00 00 00 01 62 00", + expectedOutput: "0e 00 01 0b 00 00 00 02 00 00 00 01 61 00 00 00 01 62 00", + recipeConfig: [ + { "op": "From Hex", "args": ["Auto"] }, + { "op": "Thrift Deserialize", "args": ["TBinaryProtocol"] }, + { "op": "Thrift Serialize", "args": [] }, + { "op": "To Hex", "args": ["Space", 0] } + ] + }, + { + name: "Thrift Deserialize/Serialize: TBinaryProtocol Map round-trip", + // Validates that a TBinaryProtocal MAP is successfully deserialised + // and that the result can be Serialized back into the original binary data + input: "0d 00 01 08 0b 00 00 00 01 00 00 00 01 00 00 00 01 61 00", + expectedOutput: "0d 00 01 08 0b 00 00 00 01 00 00 00 01 00 00 00 01 61 00", + recipeConfig: [ + { "op": "From Hex", "args": ["Auto"] }, + { "op": "Thrift Deserialize", "args": ["TBinaryProtocol"] }, + { "op": "Thrift Serialize", "args": [] }, + { "op": "To Hex", "args": ["Space", 0] } + ] } ]); From 1a5a1b67c1199d6194bb3e4d8b825197b4aacb64 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Sat, 27 Jun 2026 13:15:13 +0300 Subject: [PATCH 15/17] test(thrift): fix assertion mismatches --- tests/node/tests/lib/Thrift.mjs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index 075dda60..6718ac02 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -496,7 +496,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 15); - assert.deepStrictEqual(result.value, [1, 2]); + assert.deepStrictEqual(result.value, { elementType: 'I32', elements: [ 1, 2 ] }); assert.strictEqual(result.offset, 13); }), @@ -504,7 +504,7 @@ TestRegister.addApiTests([ const bytes = new Uint8Array([8, 0, 0, 0, 0]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 15); - assert.deepStrictEqual(result.value, []); + assert.deepStrictEqual(result.value, { elementType: 'I32', elements: [] }); assert.strictEqual(result.offset, 5); }), @@ -520,7 +520,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 13); - assert.deepStrictEqual(result.value, [{ key: 1, val: "key" }]); + assert.deepStrictEqual(result.value, { keyType: 'I32', valType: 'BINARY', elements: [{ key: 1, val: "key" }] }); assert.strictEqual(result.offset, 17); }), @@ -582,7 +582,7 @@ TestRegister.addApiTests([ const bytes = new Uint8Array([1]); const data = new DataView(bytes.buffer); const result = readCompactType(data, 0, 4); - assert.strictEqual(result.value, -1n); + assert.strictEqual(result.value, -1); }), it("Thrift: readCompactType - I32 zigzag", () => { @@ -590,7 +590,7 @@ TestRegister.addApiTests([ const bytes = new Uint8Array([200, 1]); const data = new DataView(bytes.buffer); const result = readCompactType(data, 0, 5); - assert.strictEqual(result.value, 100n); + assert.strictEqual(result.value, 100); }), it("Thrift: readCompactType - DOUBLE", () => { From f81a208e5e85e17e2f7ab923bd0d7f598c4f0c34 Mon Sep 17 00:00:00 2001 From: engin0223 Date: Sat, 27 Jun 2026 13:20:45 +0300 Subject: [PATCH 16/17] fix(lint): fix ESLint style violations in Thrift.mjs --- tests/node/tests/lib/Thrift.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/node/tests/lib/Thrift.mjs b/tests/node/tests/lib/Thrift.mjs index 6718ac02..ce5c2c33 100644 --- a/tests/node/tests/lib/Thrift.mjs +++ b/tests/node/tests/lib/Thrift.mjs @@ -496,7 +496,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 15); - assert.deepStrictEqual(result.value, { elementType: 'I32', elements: [ 1, 2 ] }); + assert.deepStrictEqual(result.value, { elementType: "I32", elements: [1, 2] }); assert.strictEqual(result.offset, 13); }), @@ -504,7 +504,7 @@ TestRegister.addApiTests([ const bytes = new Uint8Array([8, 0, 0, 0, 0]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 15); - assert.deepStrictEqual(result.value, { elementType: 'I32', elements: [] }); + assert.deepStrictEqual(result.value, { elementType: "I32", elements: [] }); assert.strictEqual(result.offset, 5); }), @@ -520,7 +520,7 @@ TestRegister.addApiTests([ ]); const data = new DataView(bytes.buffer); const result = readBinaryType(data, 0, 13); - assert.deepStrictEqual(result.value, { keyType: 'I32', valType: 'BINARY', elements: [{ key: 1, val: "key" }] }); + assert.deepStrictEqual(result.value, { keyType: "I32", valType: "BINARY", elements: [{ key: 1, val: "key" }] }); assert.strictEqual(result.offset, 17); }), From 22f042dc33377d9a86cc9b5d8cd84fb7565f368d Mon Sep 17 00:00:00 2001 From: engin0223 <99678175+engin0223@users.noreply.github.com> Date: Tue, 21 Jul 2026 18:32:17 +0300 Subject: [PATCH 17/17] Fix add missing comma --- src/core/config/Categories.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 9d7dea3d..1c0ffe90 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -85,7 +85,7 @@ "From Modhex", "MIME Decoding", "Thrift Serialize", - "Thrift Deserialize" + "Thrift Deserialize", "To COBS", "From COBS" ] @@ -619,4 +619,4 @@ "Comment" ] } -] \ No newline at end of file +]