Merge 94832f3ebc8449b05e619d628665605d113efe95 into 4290ea753912378913b1f3f54e0fc5720afeda5d

This commit is contained in:
sky 2026-08-09 05:12:03 +03:00 committed by GitHub
commit 54f360ac32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 263 additions and 4 deletions

View File

@ -5,6 +5,7 @@
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import "reflect-metadata"; // Required as a shim for the amf library
import { AMF0, AMF3 } from "@astronautlabs/amf";
@ -44,7 +45,16 @@ class AMFDecode extends Operation {
const [format] = args;
const handler = format === "AMF0" ? AMF0 : AMF3;
const encoded = new Uint8Array(input);
return handler.Value.deserialize(encoded);
if (encoded.length === 0) {
throw new OperationError(`Could not decode ${format} data: input is empty.`);
}
try {
return handler.Value.deserialize(encoded);
} catch {
throw new OperationError(`Could not decode ${format} data. The input may be invalid or incomplete.`);
}
}
}

View File

@ -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;
}

View File

@ -0,0 +1,159 @@
/**
* 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]
}
],
},
{
name: "AMF3 Encode/Decode: object string",
input: "{\"a\": \"test\"}",
expectedMatch: /"\$value": "a"[\s\S]*"\$value": "test"/,
recipeConfig: [
{
op: "AMF Encode",
args: ["AMF3"]
},
{
op: "AMF Decode",
args: ["AMF3"]
}
],
},
{
name: "AMF3 Decode: empty input error",
input: "",
expectedOutput: "Could not decode AMF3 data: input is empty.",
recipeConfig: [
{
op: "AMF Decode",
args: ["AMF3"]
}
],
},
{
name: "AMF3 Decode: newline input error",
input: "\n",
expectedOutput: "Could not decode AMF3 data. The input may be invalid or incomplete.",
recipeConfig: [
{
op: "AMF Decode",
args: ["AMF3"]
}
],
},
{
name: "AMF3 Decode: truncated object input error",
input: "\x0a\x13\x01\x03a\x05\x40\x08",
expectedOutput: "Could not decode AMF3 data. The input may be invalid or incomplete.",
recipeConfig: [
{
op: "AMF Decode",
args: ["AMF3"]
}
],
},
{
name: "AMF3 Decode: truncated array input error",
input: "\x09\x13\x01\x03a\x05\x40\x08",
expectedOutput: "Could not decode AMF3 data. The input may be invalid or incomplete.",
recipeConfig: [
{
op: "AMF Decode",
args: ["AMF3"]
}
],
},
]);