Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion src/core/operations/AMFDecode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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.`);
}
}

}
Expand Down
96 changes: 93 additions & 3 deletions src/core/operations/AMFEncode.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions tests/operations/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
import TestRegister from "../lib/TestRegister.mjs";
import "./tests/A1Z26CipherDecode.mjs";
import "./tests/AESKeyWrap.mjs";
import "./tests/AMF.mjs";
import "./tests/AnalyseUUID.mjs";
import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs";
Expand Down
159 changes: 159 additions & 0 deletions tests/operations/tests/AMF.mjs
Comment thread
GCHQDeveloper581 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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"]
}
],
},
]);