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.
This commit is contained in:
parent
164c20aa32
commit
4943617953
374
src/core/lib/Thrift.mjs
Normal file
374
src/core/lib/Thrift.mjs
Normal file
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
|
||||
644
tests/node/tests/lib/Thrift.mjs
Normal file
644
tests/node/tests/lib/Thrift.mjs
Normal file
@ -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);
|
||||
}),
|
||||
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user