Merge 94832f3ebc8449b05e619d628665605d113efe95 into 4290ea753912378913b1f3f54e0fc5720afeda5d
This commit is contained in:
commit
54f360ac32
@ -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.`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
159
tests/operations/tests/AMF.mjs
Normal file
159
tests/operations/tests/AMF.mjs
Normal 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"]
|
||||
}
|
||||
],
|
||||
},
|
||||
]);
|
||||
Loading…
x
Reference in New Issue
Block a user