jsplanet
Version:
A controller for Trackmania 2020 dedicated server.
223 lines (222 loc) • 7.64 kB
JavaScript
import { XMLBuilder, XMLParser } from "fast-xml-parser";
import { decode, encode } from "html-entities";
import { z } from "zod";
import XmlRpcFaultError from "./xmlRpcFaultError.js";
var MessageType;
(function (MessageType) {
MessageType["MethodCall"] = "MethodCall";
MessageType["MethodResponse"] = "MethodResponse";
})(MessageType || (MessageType = {}));
const methodParameterSchema = z.lazy(() => z.union([
z.object({
array: z.object({
data: z.union([
z.object({ value: z.array(methodParameterSchema) }),
z.literal(""),
]),
}),
}),
z.object({ base64: z.string() }),
z.object({ boolean: z.union([z.literal(0), z.literal(1)]) }),
z.object({ "dateTime.iso8601": z.string() }),
z.object({ double: z.number() }),
z.object({ i4: z.number().int() }),
z.object({ int: z.number().int() }),
z.string(),
z.object({ string: z.string() }),
z.object({
struct: z.union([
z.object({
member: z.array(z.object({ name: z.string(), value: methodParameterSchema })),
}),
z.literal(""),
]),
}),
]));
const methodCallSchema = z.object({
methodCall: z.object({
methodName: z.string(),
params: z.object({
param: z.array(z.object({ value: methodParameterSchema })),
}),
}),
});
const methodResponseSchema = z.object({
methodResponse: z.object({
params: z.object({
param: z.array(z.object({ value: methodParameterSchema })),
}),
}),
});
const methodResponseFaultSchema = z.object({
methodResponse: z.object({
fault: z.object({ value: methodParameterSchema }),
}),
});
const methodResponseFaultParametersSchema = z.object({
faultCode: z.number(),
faultString: z.string(),
});
function parameterParser(parameter) {
if (typeof parameter.value === "string") {
return decode(parameter.value);
}
if ("array" in parameter.value) {
if (parameter.value.array.data === "") {
return [];
}
return parameter.value.array.data.value.map((value) => parameterParser({ value: value }));
}
if ("base64" in parameter.value) {
return Buffer.from(parameter.value.base64, "base64");
}
if ("boolean" in parameter.value) {
return parameter.value.boolean === 1;
}
if ("dateTime.iso8601" in parameter.value) {
return new Date(parameter.value["dateTime.iso8601"]);
}
if ("i4" in parameter.value) {
return parameter.value.i4;
}
if ("int" in parameter.value) {
return parameter.value.int;
}
if ("double" in parameter.value) {
return parameter.value.double;
}
if ("string" in parameter.value) {
return decode(parameter.value.string);
}
if ("struct" in parameter.value) {
return Object.fromEntries(parameter.value.struct === ""
? []
: parameter.value.struct.member.map((member) => {
return [member.name, parameterParser(member)];
}));
}
/* istanbul ignore next */
throw new Error("Unsupported param value.");
}
function parameterSerializer(parameter) {
if (Array.isArray(parameter)) {
return {
array: {
data: {
value: parameter.map((parameter) => parameterSerializer(parameter)),
},
},
};
}
if (parameter instanceof Buffer) {
return { base64: parameter.toString("base64") };
}
if (typeof parameter === "boolean") {
return { boolean: parameter ? 1 : 0 };
}
if (parameter instanceof Date) {
return { "dateTime.iso8601": parameter.toISOString() };
}
if (typeof parameter === "number") {
return Number.isInteger(parameter)
? { i4: parameter }
: { double: parameter };
}
if (typeof parameter === "string") {
return encode(parameter, { level: "xml", mode: "nonAscii" });
}
if (typeof parameter === "object") {
return {
struct: {
member: Object.entries(parameter).map(([name, member]) => {
return { name: name, value: parameterSerializer(member) };
}),
},
};
}
/* istanbul ignore next */
throw new Error("Unsupported param type.");
}
function parser(type, response, schema) {
const parser = new XMLParser({
isArray(_tagName, indexPath) {
if (indexPath === "methodCall.params.param") {
return true;
}
if (indexPath === "methodResponse.params.param") {
return true;
}
if (indexPath.endsWith(".array.data.value")) {
return true;
}
if (indexPath.endsWith(".struct.member")) {
return true;
}
return false;
},
// eslint-disable-next-line @typescript-eslint/naming-convention
processEntities: false,
tagValueProcessor(_tagName, tagValue, indexPath, _hasAttributes, isLeafNode) {
if (isLeafNode &&
(indexPath.endsWith(".string") || indexPath.endsWith(".value"))) {
return null;
}
return tagValue;
},
});
const json = parser.parse(response);
const methodResponseFaultParsed = methodResponseFaultSchema.safeParse(json);
if (methodResponseFaultParsed.success) {
const parameters = parameterParser(methodResponseFaultParsed.data.methodResponse.fault);
const fault = methodResponseFaultParametersSchema.parse(parameters);
throw new XmlRpcFaultError(fault.faultCode, fault.faultString);
}
switch (type) {
case MessageType.MethodCall: {
const call = methodCallSchema.parse(json);
const parameters = call.methodCall.params.param.map((parameter) => parameterParser(parameter));
const typedSchemas = schema;
const methodName = call.methodCall.methodName;
const typedSchema = typedSchemas[methodName] ?? null;
if (typedSchema === null) {
throw new Error(`Unknown method call (${methodName}).`);
}
return {
methodName: methodName,
params: typedSchema.parse(parameters),
};
}
case MessageType.MethodResponse: {
const response = methodResponseSchema.parse(json);
const parameters = response.methodResponse.params.param.map((parameter) => parameterParser(parameter));
const typedSchema = schema;
return typedSchema.parse(parameters);
}
}
}
function serializer(type, name, ...parameters) {
// eslint-disable-next-line @typescript-eslint/naming-convention
const builder = new XMLBuilder({ processEntities: false });
let object = null;
switch (type) {
case MessageType.MethodCall: {
object = {
methodCall: {
methodName: name,
params: {
param: parameters.map((parameter) => {
return { value: parameterSerializer(parameter) };
}),
},
},
};
break;
}
}
const xml = builder.build(object);
if (typeof xml !== "string") {
throw new TypeError("Error during serializing.");
}
return xml;
}
export { MessageType, parser, serializer };