From 1c73e03f5d97f02ca64155280021c72302fe7215 Mon Sep 17 00:00:00 2001 From: skywalker Date: Sat, 6 Jun 2026 18:54:43 +0800 Subject: [PATCH] fix AMF3 JSON value encoding --- src/core/operations/AMFEncode.mjs | 96 +++++++++++++++++++++++++++- tests/operations/tests/AMF.mjs | 100 ++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 tests/operations/tests/AMF.mjs diff --git a/src/core/operations/AMFEncode.mjs b/src/core/operations/AMFEncode.mjs index c21ba7dc..bb396aba 100644 --- a/src/core/operations/AMFEncode.mjs +++ b/src/core/operations/AMFEncode.mjs @@ -5,8 +5,97 @@ */ import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; import "reflect-metadata"; // Required as a shim for the amf library -import { AMF0, AMF3 } from "@astronautlabs/amf"; +import { AMF0 } from "@astronautlabs/amf"; + +const AMF3_MARKER = { + Null: 0x01, + False: 0x02, + True: 0x03, + Double: 0x05, + String: 0x06, + Array: 0x09, + Object: 0x0a +}; + +/** + * @param {number} value + * @returns {number[]} + */ +function encodeU29(value) { + if (value < 0x80) { + return [value]; + } + if (value < 0x4000) { + return [ + ((value >> 7) & 0x7f) | 0x80, + value & 0x7f + ]; + } + if (value < 0x200000) { + return [ + ((value >> 14) & 0x7f) | 0x80, + ((value >> 7) & 0x7f) | 0x80, + value & 0x7f + ]; + } + return [ + ((value >> 22) & 0x7f) | 0x80, + ((value >> 15) & 0x7f) | 0x80, + ((value >> 8) & 0x7f) | 0x80, + value & 0xff + ]; +} + +/** + * @param {string} value + * @returns {number[]} + */ +function encodeAMF3Utf8(value) { + const bytes = Utils.strToUtf8ByteArray(value); + return encodeU29((bytes.length << 1) | 1).concat(bytes); +} + +/** + * @param {number} value + * @returns {number[]} + */ +function encodeAMF3Double(value) { + const bytes = new Uint8Array(8); + new DataView(bytes.buffer).setFloat64(0, value, false); + return [AMF3_MARKER.Double].concat(Array.from(bytes)); +} + +/** + * @param {JSON} value + * @returns {number[]} + */ +function encodeAMF3Value(value) { + if (value === null) return [AMF3_MARKER.Null]; + if (value === false) return [AMF3_MARKER.False]; + if (value === true) return [AMF3_MARKER.True]; + if (typeof value === "number") return encodeAMF3Double(value); + if (typeof value === "string") return [AMF3_MARKER.String].concat(encodeAMF3Utf8(value)); + + if (Array.isArray(value)) { + return [ + AMF3_MARKER.Array, + ...encodeU29((value.length << 1) | 1), + 0x01, + ...value.flatMap(encodeAMF3Value) + ]; + } + + const keys = Object.keys(value); + return [ + AMF3_MARKER.Object, + ...encodeU29((keys.length << 4) | 0x03), + 0x01, + ...keys.flatMap(encodeAMF3Utf8), + ...keys.flatMap(key => encodeAMF3Value(value[key])) + ]; +} /** * AMF Encode operation @@ -42,8 +131,9 @@ class AMFEncode extends Operation { */ run(input, args) { const [format] = args; - const handler = format === "AMF0" ? AMF0 : AMF3; - const output = handler.Value.any(input).serialize(); + const output = format === "AMF0" ? + AMF0.Value.any(input).serialize() : + Uint8Array.from(encodeAMF3Value(input)); return output.buffer; } diff --git a/tests/operations/tests/AMF.mjs b/tests/operations/tests/AMF.mjs new file mode 100644 index 00000000..e98039ea --- /dev/null +++ b/tests/operations/tests/AMF.mjs @@ -0,0 +1,100 @@ +/** + * AMF tests. + * + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "AMF3 Encode: object string", + input: "{\"a\": \"test\"}", + expectedOutput: "0a13010361060974657374", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, + { + name: "AMF3 Encode: object boolean true", + input: "{\"a\": true}", + expectedOutput: "0a1301036103", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, + { + name: "AMF3 Encode: object boolean false", + input: "{\"a\": false}", + expectedOutput: "0a1301036102", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, + { + name: "AMF3 Encode: object null", + input: "{\"a\": null}", + expectedOutput: "0a1301036101", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, + { + name: "AMF3 Encode: object array", + input: "{\"a\": []}", + expectedOutput: "0a13010361090101", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, + { + name: "AMF3 Encode: object nested JSON values", + input: "{\"a\": [true, false, null, \"x\", 1]}", + expectedOutput: "0a13010361090b01030201060378053ff0000000000000", + recipeConfig: [ + { + op: "AMF Encode", + args: ["AMF3"] + }, + { + op: "To Hex", + args: ["None", 0] + } + ], + }, +]);