UNPKG

jsplanet

Version:

A controller for Trackmania 2020 dedicated server.

223 lines (222 loc) 7.64 kB
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 };