@tsplc/compiler 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +16 -2
- package/backends/codesys/config.ts +54 -0
- package/backends/codesys/emit.ts +1063 -0
- package/backends/codesys/engineering.ts +88 -0
- package/backends/codesys/http.ts +598 -0
- package/backends/codesys/index.ts +12 -0
- package/backends/codesys/io-paths.ts +103 -0
- package/backends/codesys/names.ts +76 -0
- package/backends/codesys/numeric.ts +356 -0
- package/backends/codesys/plan.ts +512 -0
- package/backends/codesys/publication.ts +59 -0
- package/backends/codesys/storage.ts +63 -0
- package/backends/codesys/task-access.ts +118 -0
- package/backends/codesys/types.ts +178 -0
- package/backends/codesys/vendor.ts +108 -0
- package/dist/cli.js +740 -0
- package/dist/cli.js.map +13 -0
- package/dist/clock.js +8 -0
- package/dist/clock.js.map +10 -0
- package/dist/codesys.js +2587 -0
- package/dist/codesys.js.map +22 -0
- package/dist/elaborate-capture.js +118 -0
- package/dist/elaborate-capture.js.map +10 -0
- package/dist/index.js +6859 -0
- package/dist/index.js.map +25 -0
- package/dist/ir.js +1888 -0
- package/dist/ir.js.map +22 -0
- package/dist/math.js +24 -0
- package/dist/math.js.map +10 -0
- package/dist/project.js +423 -0
- package/dist/project.js.map +11 -0
- package/ir/aggregates.ts +333 -0
- package/ir/allocation.ts +74 -0
- package/ir/core-model.ts +116 -0
- package/ir/core.ts +63 -0
- package/ir/declarations.ts +201 -0
- package/ir/diagnostics.ts +43 -0
- package/ir/domains.ts +96 -0
- package/ir/index.ts +7 -0
- package/ir/link.ts +176 -0
- package/ir/model.ts +268 -0
- package/ir/normalization-error.ts +10 -0
- package/ir/routine.ts +826 -0
- package/ir/scalars.ts +675 -0
- package/ir/types.ts +70 -0
- package/package.json +105 -4
- package/project/artifacts.ts +277 -0
- package/project/cli.ts +282 -0
- package/project/index.ts +1 -0
- package/project/project.ts +298 -0
- package/project/server.ts +83 -0
- package/src/frontend/async.ts +1060 -0
- package/src/frontend/blocks.ts +419 -0
- package/src/frontend/configuration.ts +167 -0
- package/src/frontend/const-functions.ts +153 -0
- package/src/frontend/cooperative.ts +75 -0
- package/src/frontend/elaborate/capture.ts +138 -0
- package/src/frontend/elaborate/graph.ts +94 -0
- package/src/frontend/elaborate/index.ts +302 -0
- package/src/frontend/elaborate/layouts.ts +390 -0
- package/src/frontend/elaborate/model.ts +177 -0
- package/src/frontend/elaborate/specialize.ts +2280 -0
- package/src/frontend/exposure.ts +147 -0
- package/src/frontend/module-graph.ts +63 -0
- package/src/frontend/promises.ts +751 -0
- package/src/frontend/services.ts +435 -0
- package/src/frontend/types.ts +578 -0
- package/src/frontend/typescript.ts +2442 -0
- package/src/index.ts +7 -0
- package/stdlib/clock.ts +2 -0
- package/stdlib/math.ts +19 -0
- package/types/async.d.ts +81 -0
- package/types/clock.d.ts +4 -0
- package/types/config.d.ts +2 -0
- package/types/data-types.d.ts +31 -0
- package/types/globals.d.ts +40 -0
- package/types/index.d.ts +111 -0
- package/types/plc.d.ts +5 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { EmitResult } from "@tsplc/compiler/ir";
|
|
3
|
+
import { buildProject, configSchema, type InstalledLibrary, resolveConfig } from "@tsplc/build";
|
|
4
|
+
import type { Controller, DeviceTree } from "@tsplc/config";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { CodesysPlan } from "./plan.ts";
|
|
7
|
+
|
|
8
|
+
export const engineeringBuildSchema = configSchema.omit({
|
|
9
|
+
sourceDir: true,
|
|
10
|
+
entry: true,
|
|
11
|
+
outDir: true,
|
|
12
|
+
task: true,
|
|
13
|
+
tasks: true,
|
|
14
|
+
});
|
|
15
|
+
export const engineeringSchema = z.strictObject({
|
|
16
|
+
kind: z.literal("v3.5"),
|
|
17
|
+
build: engineeringBuildSchema.optional(),
|
|
18
|
+
});
|
|
19
|
+
export type CodesysEngineeringBuild = z.input<typeof engineeringBuildSchema>;
|
|
20
|
+
|
|
21
|
+
export function codesysBuildConfig(
|
|
22
|
+
plan: CodesysPlan,
|
|
23
|
+
bundle: Pick<EmitResult, "objects">,
|
|
24
|
+
paths: {
|
|
25
|
+
configFile: string;
|
|
26
|
+
sourceDir: string;
|
|
27
|
+
outDir: string;
|
|
28
|
+
libraries?: InstalledLibrary[];
|
|
29
|
+
deviceTree?: DeviceTree;
|
|
30
|
+
controller?: Controller;
|
|
31
|
+
},
|
|
32
|
+
) {
|
|
33
|
+
const settings = plan.options.engineering.build;
|
|
34
|
+
if (!settings)
|
|
35
|
+
throw new Error(
|
|
36
|
+
"CODESYS build requires engineering.build settings (codesys installation and template). Configure them or use --emit-only.",
|
|
37
|
+
);
|
|
38
|
+
const programs = new Map(
|
|
39
|
+
plan.core.programs.map((program) => {
|
|
40
|
+
const matches = bundle.objects.filter(
|
|
41
|
+
(object) => object.id === program.id && object.kind === "program",
|
|
42
|
+
);
|
|
43
|
+
const generated = matches[0];
|
|
44
|
+
if (matches.length !== 1 || !generated)
|
|
45
|
+
throw new Error(`Generated bundle must contain one PROGRAM object for ${program.name}.`);
|
|
46
|
+
return [program.name, generated] as const;
|
|
47
|
+
}),
|
|
48
|
+
);
|
|
49
|
+
const tasks = plan.deployment.tasks.map((task) => ({
|
|
50
|
+
...task,
|
|
51
|
+
programs: task.programs.map((name) => {
|
|
52
|
+
const program = programs.get(name);
|
|
53
|
+
if (!program)
|
|
54
|
+
throw new Error(`Task ${task.name} references a missing generated PROGRAM ${name}.`);
|
|
55
|
+
return program.name;
|
|
56
|
+
}),
|
|
57
|
+
}));
|
|
58
|
+
const first = plan.core.programs[0],
|
|
59
|
+
entry = first && programs.get(first.name);
|
|
60
|
+
if (!entry) throw new Error("CODESYS build requires a generated PROGRAM entry.");
|
|
61
|
+
const sourceDir = resolve(paths.sourceDir);
|
|
62
|
+
return resolveConfig(
|
|
63
|
+
{
|
|
64
|
+
...settings,
|
|
65
|
+
libraryReferences: [
|
|
66
|
+
...(settings.libraryReferences ?? []),
|
|
67
|
+
...(Object.values(plan.http).some((endpoint) => endpoint.transport !== "opcua")
|
|
68
|
+
? ["Net Base Services, 4.0.0.0 (CODESYS)"]
|
|
69
|
+
: []),
|
|
70
|
+
...(Object.values(plan.http).some((endpoint) => endpoint.tls)
|
|
71
|
+
? ["CmpX509Cert Implementation, 3.5.22.0 (System)"]
|
|
72
|
+
: []),
|
|
73
|
+
],
|
|
74
|
+
...(paths.libraries ? { libraries: paths.libraries } : {}),
|
|
75
|
+
...(paths.deviceTree ? { deviceTree: paths.deviceTree } : {}),
|
|
76
|
+
...(paths.controller ? { controller: paths.controller } : {}),
|
|
77
|
+
sourceDir,
|
|
78
|
+
entry: resolve(sourceDir, entry.sourcePath ?? `${entry.name}/declaration.st`),
|
|
79
|
+
outDir: resolve(paths.outDir),
|
|
80
|
+
tasks,
|
|
81
|
+
},
|
|
82
|
+
paths.configFile,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function buildCodesys(...arguments_: Parameters<typeof codesysBuildConfig>) {
|
|
87
|
+
return buildProject(codesysBuildConfig(...arguments_));
|
|
88
|
+
}
|
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import type { CoreFunctionBlock, GeneratedObject, PlcType } from "@tsplc/compiler/ir";
|
|
2
|
+
import type { HttpOptions } from "@tsplc/config";
|
|
3
|
+
import type { CodesysTypes } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export const opcuaServiceCapacity = 16;
|
|
6
|
+
|
|
7
|
+
export interface HttpEndpoint {
|
|
8
|
+
transport?: "http" | "opcua";
|
|
9
|
+
host: string;
|
|
10
|
+
port: number;
|
|
11
|
+
authority: string;
|
|
12
|
+
path: string;
|
|
13
|
+
token: string;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
tls: boolean;
|
|
16
|
+
}
|
|
17
|
+
export const resolveHttpEndpoint = (value: string, input: HttpOptions): HttpEndpoint => {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (
|
|
20
|
+
!["http:", "https:"].includes(url.protocol) ||
|
|
21
|
+
url.username ||
|
|
22
|
+
url.password ||
|
|
23
|
+
url.hash ||
|
|
24
|
+
url.hostname.includes(":")
|
|
25
|
+
)
|
|
26
|
+
throw new Error(
|
|
27
|
+
"fetch requires an HTTP(S) URL with an IPv4 address or DNS hostname, without credentials or fragment.",
|
|
28
|
+
);
|
|
29
|
+
if (
|
|
30
|
+
(url.pathname + url.search).length > 2048 ||
|
|
31
|
+
url.hostname.length > 253 ||
|
|
32
|
+
Number(url.port || (url.protocol === "https:" ? 443 : 80)) === 0
|
|
33
|
+
)
|
|
34
|
+
throw new Error("HTTP URL exceeds the native host/path limits or has an invalid port.");
|
|
35
|
+
let token = "";
|
|
36
|
+
const origins = new Set<string>();
|
|
37
|
+
for (const credential of input.credentials) {
|
|
38
|
+
const configured = process.env[credential.originEnv],
|
|
39
|
+
secret = process.env[credential.bearerTokenEnv];
|
|
40
|
+
if (!configured || !secret)
|
|
41
|
+
throw new Error(
|
|
42
|
+
`HTTP credentials require ${credential.originEnv} and ${credential.bearerTokenEnv}.`,
|
|
43
|
+
);
|
|
44
|
+
const origin = new URL(configured);
|
|
45
|
+
if (
|
|
46
|
+
!["http:", "https:"].includes(origin.protocol) ||
|
|
47
|
+
origin.username ||
|
|
48
|
+
origin.password ||
|
|
49
|
+
origin.pathname !== "/" ||
|
|
50
|
+
origin.search ||
|
|
51
|
+
origin.hash
|
|
52
|
+
)
|
|
53
|
+
throw new Error(
|
|
54
|
+
`HTTP credential origin ${credential.originEnv} must contain only a scheme, host and port.`,
|
|
55
|
+
);
|
|
56
|
+
if (origins.has(origin.origin)) throw new Error("HTTP credential origins must be unique.");
|
|
57
|
+
origins.add(origin.origin);
|
|
58
|
+
if (/[\r\n\0]/.test(secret) || secret.length > 512)
|
|
59
|
+
throw new Error(`Invalid bearer token in ${credential.bearerTokenEnv}.`);
|
|
60
|
+
if (origin.origin === url.origin) token = secret;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
host: url.hostname,
|
|
64
|
+
port: Number(url.port || (url.protocol === "https:" ? 443 : 80)),
|
|
65
|
+
authority: url.host,
|
|
66
|
+
path: url.pathname + url.search,
|
|
67
|
+
token,
|
|
68
|
+
timeoutMs: input.timeoutMs,
|
|
69
|
+
tls: url.protocol === "https:",
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const quote = (text: string) =>
|
|
73
|
+
"'" +
|
|
74
|
+
text.replaceAll("$", "$$").replaceAll("'", "$'").replaceAll("\r", "$R").replaceAll("\n", "$N") +
|
|
75
|
+
"'";
|
|
76
|
+
|
|
77
|
+
const httpLayouts = (block: CoreFunctionBlock, types: ReadonlyMap<string, PlcType>) => {
|
|
78
|
+
const request = block.inputs.find((v) => v.name === "request"),
|
|
79
|
+
response = block.outputs.find((v) => v.name === "result");
|
|
80
|
+
if (!request || !response) throw new Error("Missing HTTP record ports.");
|
|
81
|
+
const requestLayout = types.get(request.type),
|
|
82
|
+
responseLayout = types.get(response.type);
|
|
83
|
+
if (
|
|
84
|
+
requestLayout?.kind !== "record" ||
|
|
85
|
+
responseLayout?.kind !== "record" ||
|
|
86
|
+
responseLayout.fields.length === 0 ||
|
|
87
|
+
responseLayout.fields.length > 31
|
|
88
|
+
)
|
|
89
|
+
throw new Error("HTTP requires closed request/response records with 1..31 response fields.");
|
|
90
|
+
const scalarSize = (id: string): number => {
|
|
91
|
+
const type = types.get(id);
|
|
92
|
+
if (type?.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
93
|
+
return type.capacity * 6 + 2;
|
|
94
|
+
if (type?.kind === "bool") return 5;
|
|
95
|
+
if (type?.kind === "enum")
|
|
96
|
+
return Math.max(...type.members.map((member) => member.value.length));
|
|
97
|
+
if ((type?.kind === "integer" || type?.kind === "bits") && type.bits <= 32) {
|
|
98
|
+
const signed = type.kind === "integer" && type.signed;
|
|
99
|
+
return String(signed ? -(2 ** (type.bits - 1)) : 2 ** type.bits - 1).length;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(
|
|
102
|
+
"HTTP JSON supports bounded UTF-8 strings, BOOL, enums, and integers up to 32 bits.",
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
const recordSize = (record: typeof requestLayout, response: boolean) =>
|
|
106
|
+
2 +
|
|
107
|
+
record.fields.reduce((size, field) => {
|
|
108
|
+
const type = types.get(field.type);
|
|
109
|
+
const valueSize =
|
|
110
|
+
type?.kind === "array" && !response
|
|
111
|
+
? 2 + type.length * (scalarSize(type.element) + 1)
|
|
112
|
+
: scalarSize(field.type);
|
|
113
|
+
return size + field.name.length + 4 + valueSize;
|
|
114
|
+
}, 0);
|
|
115
|
+
if (recordSize(requestLayout, false) > 2048 || recordSize(responseLayout, true) > 2048)
|
|
116
|
+
throw new Error(
|
|
117
|
+
"The worst-case HTTP JSON payload exceeds the native 2048-byte buffer; reduce record/string/array bounds.",
|
|
118
|
+
);
|
|
119
|
+
return { request, response, requestLayout, responseLayout };
|
|
120
|
+
};
|
|
121
|
+
export const validateHttpLayout = (
|
|
122
|
+
block: CoreFunctionBlock,
|
|
123
|
+
types: ReadonlyMap<string, PlcType>,
|
|
124
|
+
): void => {
|
|
125
|
+
httpLayouts(block, types);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Native transport and typed JSON codecs; no application protocol lives here. */
|
|
129
|
+
export const emitHttp = (
|
|
130
|
+
block: CoreFunctionBlock,
|
|
131
|
+
name: string,
|
|
132
|
+
fields: ReadonlyMap<string, string>,
|
|
133
|
+
types: ReadonlyMap<string, PlcType>,
|
|
134
|
+
target: CodesysTypes,
|
|
135
|
+
endpoint: HttpEndpoint,
|
|
136
|
+
network: string,
|
|
137
|
+
rpcGlobal?: string,
|
|
138
|
+
): GeneratedObject => {
|
|
139
|
+
const port = (label: string) => {
|
|
140
|
+
const variable = [...block.inputs, ...block.outputs].find((v) => v.name === label);
|
|
141
|
+
if (!variable) throw new Error("Missing HTTP port " + label);
|
|
142
|
+
return fields.get(variable.id)!;
|
|
143
|
+
};
|
|
144
|
+
const { request, response, requestLayout, responseLayout } = httpLayouts(block, types);
|
|
145
|
+
const encode = (typeId: string, value: string): string => {
|
|
146
|
+
const type = types.get(typeId)!;
|
|
147
|
+
if (type.kind === "array")
|
|
148
|
+
return Array.from(
|
|
149
|
+
{ length: type.length },
|
|
150
|
+
(_, i) =>
|
|
151
|
+
`${i ? "Append(',');" : ""}${encode(type.element, `${value}[${i + type.lowerBound}]`)}`,
|
|
152
|
+
).join("\n");
|
|
153
|
+
if (type.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
154
|
+
return `WriteString(${value});`;
|
|
155
|
+
if (type.kind === "bool")
|
|
156
|
+
return `IF ${value} THEN Append('true'); ELSE Append('false'); END_IF;`;
|
|
157
|
+
if (type.kind === "enum") return `Append(DINT_TO_STRING(TO_DINT(${value})));`;
|
|
158
|
+
if (type.kind === "bits" && type.bits <= 32)
|
|
159
|
+
return `Append(UDINT_TO_STRING(TO_UDINT(${value})));`;
|
|
160
|
+
if (type.kind === "integer" && type.bits <= 32)
|
|
161
|
+
return `Append(${target.name(typeId)}_TO_STRING(${value}));`;
|
|
162
|
+
throw new Error(
|
|
163
|
+
"HTTP JSON supports bounded UTF-8 strings, BOOL, enums, integers up to 32 bits and fixed arrays.",
|
|
164
|
+
);
|
|
165
|
+
};
|
|
166
|
+
const requestCode = requestLayout.fields
|
|
167
|
+
.map((field, i) => {
|
|
168
|
+
const type = types.get(field.type)!,
|
|
169
|
+
value = `${port("request")}.${target.field(request.type, field.name)}`;
|
|
170
|
+
return `Append(${quote((i ? "," : "") + '"' + field.name + '":')});${type.kind === "array" ? "Append('[');" : ""}${encode(field.type, value)}${type.kind === "array" ? "Append(']');" : ""}`;
|
|
171
|
+
})
|
|
172
|
+
.join("\n");
|
|
173
|
+
const responseCode = responseLayout.fields
|
|
174
|
+
.map((field, i) => {
|
|
175
|
+
const type = types.get(field.type)!,
|
|
176
|
+
value = `${port("result")}.${target.field(response.type, field.name)}`;
|
|
177
|
+
let decode: string;
|
|
178
|
+
if (type.kind === "string" && type.encoding === "utf8" && type.capacity <= 2048)
|
|
179
|
+
decode = `ReadString(); IF ByteLength(token) > ${type.capacity} THEN malformed := TRUE; ELSE ${value} := token; END_IF;`;
|
|
180
|
+
else if (type.kind === "bool")
|
|
181
|
+
decode = `IF MatchLiteral('true') THEN ${value}:=TRUE; ELSIF MatchLiteral('false') THEN ${value}:=FALSE; ELSE malformed:=TRUE;END_IF;`;
|
|
182
|
+
else if (
|
|
183
|
+
((type.kind === "integer" || type.kind === "bits") && type.bits <= 32) ||
|
|
184
|
+
type.kind === "enum"
|
|
185
|
+
) {
|
|
186
|
+
const signed = type.kind === "enum" || (type.kind === "integer" && type.signed),
|
|
187
|
+
bits = type.kind === "enum" ? 32 : type.bits;
|
|
188
|
+
const min = signed ? -(2 ** (bits - 1)) : 0,
|
|
189
|
+
max = signed ? 2 ** (bits - 1) - 1 : 2 ** bits - 1;
|
|
190
|
+
decode = `ReadInteger(); IF numberValue < ${min} OR numberValue > ${max} THEN malformed:=TRUE; ELSE `;
|
|
191
|
+
if (type.kind === "enum")
|
|
192
|
+
decode += `CASE numberValue OF ${type.members.map((m) => `${m.value}: ${value}:=${target.name(field.type)}.${target.field(field.type, m.name)};`).join(" ")} ELSE malformed:=TRUE; END_CASE;`;
|
|
193
|
+
else decode += `${value}:=TO_${target.name(field.type)}(numberValue);`;
|
|
194
|
+
decode += " END_IF;";
|
|
195
|
+
} else throw new Error("HTTP responses currently require flat scalar records.");
|
|
196
|
+
return `${i === 0 ? "IF" : "ELSIF"} part = ${quote(field.name)} THEN IF (seen AND SHL(DWORD#1,${i})) <> 0 THEN malformed:=TRUE; END_IF; seen:=seen OR SHL(DWORD#1,${i}); ${decode}`;
|
|
197
|
+
})
|
|
198
|
+
.join("\n");
|
|
199
|
+
const inputs = block.inputs
|
|
200
|
+
.map((v) => `${fields.get(v.id)} : ${target.name(v.type)};`)
|
|
201
|
+
.join("\n");
|
|
202
|
+
const outputs = block.outputs
|
|
203
|
+
.map((v) => `${fields.get(v.id)} : ${target.name(v.type)};`)
|
|
204
|
+
.join("\n");
|
|
205
|
+
const decodeResponse = `cursor:=1;malformed:=FALSE;seen:=0;
|
|
206
|
+
SkipWhitespace();Expect(123);SkipWhitespace();
|
|
207
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) AND NOT malformed DO
|
|
208
|
+
IF payload[cursor-1]=125 THEN cursor:=cursor+1;EXIT;END_IF;
|
|
209
|
+
ReadString();part:=token;SkipWhitespace();Expect(58);SkipWhitespace();
|
|
210
|
+
${responseCode} ELSE malformed:=TRUE;END_IF;
|
|
211
|
+
SkipWhitespace();
|
|
212
|
+
IF payload[cursor-1]=44 THEN cursor:=cursor+1;SkipWhitespace();IF payload[cursor-1]=125 THEN malformed:=TRUE;END_IF;
|
|
213
|
+
ELSIF payload[cursor-1]<>125 THEN malformed:=TRUE;END_IF;
|
|
214
|
+
END_WHILE;
|
|
215
|
+
SkipWhitespace();
|
|
216
|
+
IF malformed OR cursor<>TO_UINT(ByteLength(payload))+1 OR seen<>DWORD#${2 ** responseLayout.fields.length - 1} THEN ${port("error")}:=4;END_IF;`;
|
|
217
|
+
if (endpoint.transport === "opcua") {
|
|
218
|
+
if (!rpcGlobal) throw new Error("Missing OPC UA RPC global.");
|
|
219
|
+
const channel = (field: string, index = "slotIndex") => `${rpcGlobal}.${field}[${index}]`;
|
|
220
|
+
return {
|
|
221
|
+
id: block.id,
|
|
222
|
+
kind: "functionBlock",
|
|
223
|
+
name,
|
|
224
|
+
declaration: `FUNCTION_BLOCK ${name}
|
|
225
|
+
VAR_INPUT ${inputs} END_VAR
|
|
226
|
+
VAR_OUTPUT ${outputs} END_VAR
|
|
227
|
+
VAR
|
|
228
|
+
timeout:TON;stage:UINT;slotIndex:UINT;candidate:UINT;
|
|
229
|
+
identity:STRING(64);epoch:STRING(32);prefix:STRING(80);
|
|
230
|
+
payload:STRING(2048);token:STRING(2048);part:STRING(2048);
|
|
231
|
+
cursor:UINT;malformed:BOOL;seen:DWORD;numberValue:LINT;tokenUsed:UINT;
|
|
232
|
+
END_VAR`,
|
|
233
|
+
implementation: `IF NOT ${port("start")} OR ${port("cancel")} THEN
|
|
234
|
+
IF slotIndex>0 THEN ${channel("requestIds")}:='';${channel("requests")}:='';${channel("inUse")}:=0;END_IF;
|
|
235
|
+
slotIndex:=0;stage:=0;timeout(IN:=FALSE);${port("busy")}:=FALSE;${port("done")}:=FALSE;${port("error")}:=0;RETURN;
|
|
236
|
+
END_IF;
|
|
237
|
+
IF stage=0 THEN
|
|
238
|
+
FOR candidate:=1 TO ${opcuaServiceCapacity} DO
|
|
239
|
+
(* __COMPARE_AND_SWAP can fail spuriously on ARM64; retry while the slot reads free. *)
|
|
240
|
+
WHILE slotIndex=0 AND ${channel("epochs", "candidate")}<>'' AND ${channel("inUse", "candidate")}=0 DO
|
|
241
|
+
IF __COMPARE_AND_SWAP(ADR(${channel("inUse", "candidate")}),0,1) THEN slotIndex:=candidate;END_IF;
|
|
242
|
+
END_WHILE;
|
|
243
|
+
IF slotIndex>0 THEN EXIT;END_IF;
|
|
244
|
+
END_FOR;
|
|
245
|
+
IF slotIndex=0 THEN ${port("error")}:=7;stage:=90;
|
|
246
|
+
ELSE
|
|
247
|
+
${channel("sequence")}:=${channel("sequence")}+1;
|
|
248
|
+
IF ${channel("sequence")}=0 THEN ${channel("epochs")}:='';${port("error")}:=5;stage:=90;
|
|
249
|
+
ELSE
|
|
250
|
+
epoch:=${channel("epochs")};identity:=CONCAT(CONCAT(epoch,':'),UDINT_TO_STRING(${channel("sequence")}));
|
|
251
|
+
prefix:=CONCAT(identity,'|');payload:='';malformed:=FALSE;
|
|
252
|
+
Append('{');${requestCode}Append('}');
|
|
253
|
+
IF malformed THEN ${port("error")}:=3;stage:=90;
|
|
254
|
+
ELSIF ByteLength(payload)+LEN(prefix)>2048 THEN ${port("error")}:=3;stage:=90;
|
|
255
|
+
ELSE
|
|
256
|
+
FOR candidate:=0 TO LEN(prefix)-1 DO ${channel("requests")}[candidate]:=prefix[candidate];END_FOR;
|
|
257
|
+
FOR candidate:=0 TO ByteLength(payload)-1 DO ${channel("requests")}[candidate+LEN(prefix)]:=payload[candidate];END_FOR;
|
|
258
|
+
${channel("requests")}[ByteLength(payload)+LEN(prefix)]:=0;${channel("requestIds")}:=identity;stage:=1;
|
|
259
|
+
END_IF;
|
|
260
|
+
END_IF;
|
|
261
|
+
END_IF;
|
|
262
|
+
END_IF;
|
|
263
|
+
timeout(IN:=stage<90,PT:=T#${endpoint.timeoutMs}ms);
|
|
264
|
+
IF timeout.Q THEN ${port("error")}:=2;stage:=90;END_IF;
|
|
265
|
+
IF stage=1 THEN
|
|
266
|
+
IF ${channel("epochs")}<>epoch THEN ${port("error")}:=5;stage:=90;
|
|
267
|
+
ELSIF ${channel("replyIds")}=identity AND_THEN LEFT(${channel("replies")},LEN(prefix))=prefix THEN
|
|
268
|
+
payload:='';cursor:=LEN(prefix);
|
|
269
|
+
IF ByteLength(${channel("replies")})<cursor+4 THEN ${port("error")}:=4;
|
|
270
|
+
ELSIF ${channel("replies")}[cursor]<>48 OR ${channel("replies")}[cursor+1]<>124 THEN ${port("error")}:=5;
|
|
271
|
+
ELSE
|
|
272
|
+
FOR candidate:=cursor+2 TO ByteLength(${channel("replies")})-1 DO payload[candidate-cursor-2]:=${channel("replies")}[candidate];END_FOR;
|
|
273
|
+
payload[ByteLength(${channel("replies")})-cursor-2]:=0;
|
|
274
|
+
${decodeResponse}
|
|
275
|
+
END_IF;
|
|
276
|
+
stage:=90;
|
|
277
|
+
END_IF;
|
|
278
|
+
END_IF;
|
|
279
|
+
${port("busy")}:=stage<90;
|
|
280
|
+
IF stage=90 THEN
|
|
281
|
+
IF slotIndex>0 THEN ${channel("requestIds")}:='';${channel("requests")}:='';${channel("inUse")}:=0;slotIndex:=0;END_IF;
|
|
282
|
+
timeout(IN:=FALSE);${port("busy")}:=FALSE;${port("done")}:=TRUE;
|
|
283
|
+
END_IF;`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
// Helpers are emitted as native methods under the HTTP block by the emitter.
|
|
287
|
+
return {
|
|
288
|
+
id: block.id,
|
|
289
|
+
kind: "functionBlock",
|
|
290
|
+
name,
|
|
291
|
+
declaration: `FUNCTION_BLOCK ${name}
|
|
292
|
+
VAR_INPUT
|
|
293
|
+
${inputs}
|
|
294
|
+
END_VAR
|
|
295
|
+
VAR_OUTPUT
|
|
296
|
+
${outputs}
|
|
297
|
+
END_VAR
|
|
298
|
+
VAR
|
|
299
|
+
remoteAddress : NBS.IPv4Address;
|
|
300
|
+
remoteIP : NBS.IIPAddress := remoteAddress;
|
|
301
|
+
serverHost : STRING(255) := ${quote(endpoint.host)};
|
|
302
|
+
resolver : NBS.ResolveHostname := (itfAsyncProperty:=${network});
|
|
303
|
+
${endpoint.tls ? `certificateVerifier : ${name}_CertificateVerifier;\n tlsContext : NBS.TLSContext := (itfCertVerifer:=certificateVerifier,sUseCaseName:='tsPLC HTTPS',ePurpose:=NBS.PURPOSE.CLIENT_SIDE,sTLSVersion:='1.2',sHostname:=${quote(endpoint.host)},udiVerificationMode:=2);` : ""}
|
|
304
|
+
client : NBS.TCP_Client := (itfAsyncProperty:=${network},itfIPAddress:=remoteAddress,uiPort:=${endpoint.port},udiTimeOut:=${endpoint.timeoutMs * 1000}${endpoint.tls ? ",itfTLSContext:=tlsContext" : ""});
|
|
305
|
+
writer : NBS.TCP_Write;
|
|
306
|
+
reader : NBS.TCP_Read;
|
|
307
|
+
timeout : TON;
|
|
308
|
+
stage : UINT;
|
|
309
|
+
wire : STRING(8192);
|
|
310
|
+
received : STRING(8192);
|
|
311
|
+
chunk : ARRAY[0..8191] OF BYTE;
|
|
312
|
+
payload : STRING(2048);
|
|
313
|
+
token : STRING(2048);
|
|
314
|
+
part : STRING(2048);
|
|
315
|
+
used : UINT;
|
|
316
|
+
headerEnd : UINT;
|
|
317
|
+
contentLength : DINT;
|
|
318
|
+
status : UINT;
|
|
319
|
+
cursor : UINT;
|
|
320
|
+
malformed : BOOL;
|
|
321
|
+
seen : DWORD;
|
|
322
|
+
numberValue : LINT;
|
|
323
|
+
wireUsed : UINT;
|
|
324
|
+
tokenUsed : UINT;
|
|
325
|
+
index : UINT;
|
|
326
|
+
END_VAR
|
|
327
|
+
`,
|
|
328
|
+
implementation: `IF NOT ${port("start")} OR ${port("cancel")} THEN
|
|
329
|
+
resolver(xExecute:=FALSE,itfIPAddress:=remoteIP);client(xEnable:=FALSE); writer(xExecute:=FALSE); reader(xEnable:=FALSE); timeout(IN:=FALSE);
|
|
330
|
+
stage:=0; ${port("busy")}:=FALSE; ${port("done")}:=FALSE; ${port("error")}:=0;
|
|
331
|
+
RETURN;
|
|
332
|
+
END_IF;
|
|
333
|
+
IF stage=0 THEN
|
|
334
|
+
payload:=''; malformed:=FALSE; used:=0; received:='';
|
|
335
|
+
${block.runtime?.kind === "http" && block.runtime.json ? `Append('{'); ${requestCode} Append('}');` : ""}
|
|
336
|
+
IF malformed THEN ${port("error")}:=3; stage:=90;
|
|
337
|
+
ELSE
|
|
338
|
+
wire:='';wireUsed:=0;
|
|
339
|
+
AppendWire(${quote((block.runtime?.kind === "http" ? block.runtime.method : "GET") + " " + endpoint.path + " HTTP/1.1\r\nHost: " + endpoint.authority + "\r\n" + (endpoint.token ? "Authorization: Bearer " + endpoint.token + "\r\n" : "") + (block.runtime?.kind === "http" && block.runtime.json ? "Content-Type: application/json\r\n" : "") + "Accept: application/json\r\nConnection: close\r\nContent-Length: ")});
|
|
340
|
+
AppendWire(UINT_TO_STRING(ByteLength(payload)));AppendWire('$R$N$R$N');AppendWire(payload); stage:=10;
|
|
341
|
+
END_IF;
|
|
342
|
+
END_IF;
|
|
343
|
+
timeout(IN:=stage<90,PT:=T#${endpoint.timeoutMs}ms);
|
|
344
|
+
IF timeout.Q THEN ${port("error")}:=2; stage:=90; END_IF;
|
|
345
|
+
${port("busy")}:=stage<90;
|
|
346
|
+
IF stage=10 THEN
|
|
347
|
+
resolver(xExecute:=TRUE,sHostname:=serverHost,itfIPAddress:=remoteIP);
|
|
348
|
+
IF resolver.xError THEN ${port("error")}:=5;stage:=90;ELSIF resolver.xDone THEN stage:=1;END_IF;
|
|
349
|
+
END_IF;
|
|
350
|
+
IF stage=1 THEN
|
|
351
|
+
client(xEnable:=TRUE);
|
|
352
|
+
IF client.xError THEN ${port("error")}:=${endpoint.tls ? 6 : 5};stage:=90; ELSIF client.xActive THEN stage:=2;END_IF;
|
|
353
|
+
END_IF;
|
|
354
|
+
IF stage=2 THEN
|
|
355
|
+
writer(xExecute:=TRUE,itfConnection:=client.itfConnection,pData:=ADR(wire),udiSize:=UINT_TO_UDINT(wireUsed));
|
|
356
|
+
IF writer.xError THEN ${port("error")}:=5;stage:=90;ELSIF writer.xDone THEN stage:=3;END_IF;
|
|
357
|
+
END_IF;
|
|
358
|
+
IF stage=3 THEN
|
|
359
|
+
IF used>=8191 THEN ${port("error")}:=3;stage:=90;
|
|
360
|
+
ELSE
|
|
361
|
+
reader(xEnable:=TRUE,itfConnection:=client.itfConnection,pData:=ADR(chunk),udiSize:=8191);
|
|
362
|
+
IF reader.xError THEN ${port("error")}:=5;stage:=90;
|
|
363
|
+
ELSIF reader.xReady AND reader.udiCount>0 THEN
|
|
364
|
+
IF TO_UDINT(used)+reader.udiCount>8191 THEN ${port("error")}:=3;stage:=90;
|
|
365
|
+
ELSE
|
|
366
|
+
FOR index:=0 TO TO_UINT(reader.udiCount)-1 DO received[used+index]:=chunk[index];END_FOR;
|
|
367
|
+
used:=used+TO_UINT(reader.udiCount);received[used]:=0;
|
|
368
|
+
FindHeaders();
|
|
369
|
+
IF headerEnd=0 AND used>4096 THEN ${port("error")}:=3;stage:=90;END_IF;
|
|
370
|
+
IF headerEnd>0 THEN
|
|
371
|
+
IF headerEnd>4096 THEN ${port("error")}:=3;stage:=90;
|
|
372
|
+
ELSE
|
|
373
|
+
ReadHeaders();
|
|
374
|
+
IF malformed OR contentLength<0 OR contentLength>2048 THEN ${port("error")}:=3;stage:=90;
|
|
375
|
+
ELSIF TO_DINT(used)>=TO_DINT(headerEnd)+3+contentLength THEN
|
|
376
|
+
IF status<200 OR status>=300 THEN ${port("error")}:=10000+status;stage:=90;
|
|
377
|
+
ELSE
|
|
378
|
+
payload:='';IF contentLength>0 THEN FOR index:=0 TO TO_UINT(contentLength)-1 DO payload[index]:=received[headerEnd+3+index]; END_FOR;END_IF;payload[TO_UINT(contentLength)]:=0; ${decodeResponse}
|
|
379
|
+
stage:=90;
|
|
380
|
+
END_IF;
|
|
381
|
+
END_IF;
|
|
382
|
+
END_IF;
|
|
383
|
+
END_IF;
|
|
384
|
+
END_IF;
|
|
385
|
+
END_IF;
|
|
386
|
+
END_IF;
|
|
387
|
+
END_IF;
|
|
388
|
+
IF stage=90 THEN
|
|
389
|
+
resolver(xExecute:=FALSE,itfIPAddress:=remoteIP);client(xEnable:=FALSE);writer(xExecute:=FALSE);reader(xEnable:=FALSE);timeout(IN:=FALSE);
|
|
390
|
+
${port("busy")}:=FALSE;${port("done")}:=TRUE;
|
|
391
|
+
END_IF;
|
|
392
|
+
`,
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
export const httpCertificateVerifier = (
|
|
397
|
+
owner: string,
|
|
398
|
+
name: string,
|
|
399
|
+
endpoint: HttpEndpoint,
|
|
400
|
+
): GeneratedObject[] => {
|
|
401
|
+
if (!endpoint.tls) return [];
|
|
402
|
+
const id = `${owner}:certificateVerifier`;
|
|
403
|
+
// NBS represents certificate handles as pointers; verification keeps the native trust result.
|
|
404
|
+
const ipv4 = /^\d+\.\d+\.\d+\.\d+$/.test(endpoint.host);
|
|
405
|
+
return [
|
|
406
|
+
{
|
|
407
|
+
id,
|
|
408
|
+
kind: "functionBlock",
|
|
409
|
+
name: `${name}_CertificateVerifier`,
|
|
410
|
+
declaration: `FUNCTION_BLOCK ${name}_CertificateVerifier IMPLEMENTS NBS.ICertificateVerifier
|
|
411
|
+
VAR
|
|
412
|
+
hostname:STRING(255):=${quote(endpoint.host)};
|
|
413
|
+
${ipv4 ? `address:ARRAY[0..3] OF BYTE := [${endpoint.host.split(".").join(",")}];` : ""}
|
|
414
|
+
END_VAR`,
|
|
415
|
+
implementation: "",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
id: `${id}:VerifyCertificate`,
|
|
419
|
+
parent: id,
|
|
420
|
+
kind: "method",
|
|
421
|
+
name: "VerifyCertificate",
|
|
422
|
+
declaration: `METHOD PUBLIC VerifyCertificate : UDINT
|
|
423
|
+
VAR_INPUT hCert:POINTER TO BYTE;eCurState:UDINT;END_VAR
|
|
424
|
+
VAR matched:BOOL;result:UDINT;END_VAR`,
|
|
425
|
+
implementation: `VerifyCertificate:=eCurState;
|
|
426
|
+
IF eCurState<>0 THEN RETURN;END_IF;
|
|
427
|
+
matched:=CmpX509Cert_Implementation.${ipv4 ? "X509CertCheckIP(hCert,ADR(address),0,ADR(result))" : "X509CertCheckHost(hCert,ADR(hostname),4,ADR(result))"};
|
|
428
|
+
IF result<>0 THEN VerifyCertificate:=result;ELSIF NOT matched THEN VerifyCertificate:=1;END_IF;`,
|
|
429
|
+
},
|
|
430
|
+
];
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
export const httpMethods = (
|
|
434
|
+
owner: string,
|
|
435
|
+
transport: "http" | "opcua" = "http",
|
|
436
|
+
): GeneratedObject[] => {
|
|
437
|
+
const method = (
|
|
438
|
+
name: string,
|
|
439
|
+
declaration: string,
|
|
440
|
+
implementation: string,
|
|
441
|
+
result?: string,
|
|
442
|
+
): GeneratedObject => ({
|
|
443
|
+
id: `${owner}:${name}`,
|
|
444
|
+
parent: owner,
|
|
445
|
+
kind: "method",
|
|
446
|
+
name,
|
|
447
|
+
declaration: `METHOD PRIVATE ${name}${result ? ` : ${result}` : ""}\n${declaration}`,
|
|
448
|
+
implementation,
|
|
449
|
+
});
|
|
450
|
+
return [
|
|
451
|
+
// Standard string helpers accept STRING(255); native buffers must retain their full byte length.
|
|
452
|
+
method(
|
|
453
|
+
"ByteLength",
|
|
454
|
+
"VAR_INPUT value:STRING(8192);END_VAR\nVAR i:UINT;END_VAR",
|
|
455
|
+
"ByteLength:=0;FOR i:=0 TO 8191 DO IF value[i]=0 THEN ByteLength:=i;RETURN;END_IF;END_FOR;ByteLength:=8192;",
|
|
456
|
+
"UINT",
|
|
457
|
+
),
|
|
458
|
+
method(
|
|
459
|
+
"Append",
|
|
460
|
+
"VAR_INPUT value : STRING(2048); END_VAR\nVAR offset:UINT;i:UINT;END_VAR",
|
|
461
|
+
`offset:=TO_UINT(ByteLength(payload));IF offset+TO_UINT(ByteLength(value))>2048 THEN malformed:=TRUE; ELSE IF ByteLength(value)>0 THEN FOR i:=0 TO TO_UINT(ByteLength(value))-1 DO payload[offset+i]:=value[i];END_FOR;END_IF;payload[offset+TO_UINT(ByteLength(value))]:=0;END_IF;`,
|
|
462
|
+
),
|
|
463
|
+
method(
|
|
464
|
+
"AppendWire",
|
|
465
|
+
"VAR_INPUT value:STRING(8192);END_VAR\nVAR i:UINT;END_VAR",
|
|
466
|
+
`IF wireUsed+TO_UINT(ByteLength(value))>8191 THEN malformed:=TRUE;RETURN;END_IF;IF ByteLength(value)>0 THEN FOR i:=0 TO TO_UINT(ByteLength(value))-1 DO wire[wireUsed+i]:=value[i];END_FOR;END_IF;wireUsed:=wireUsed+TO_UINT(ByteLength(value));wire[wireUsed]:=0;`,
|
|
467
|
+
),
|
|
468
|
+
method(
|
|
469
|
+
"FindHeaders",
|
|
470
|
+
"VAR i:UINT;END_VAR",
|
|
471
|
+
`headerEnd:=0;IF used<4 THEN RETURN;END_IF;
|
|
472
|
+
FOR i:=0 TO used-4 DO IF received[i]=13 AND received[i+1]=10 AND received[i+2]=13 AND received[i+3]=10 THEN headerEnd:=i+1;RETURN;END_IF;END_FOR;`,
|
|
473
|
+
),
|
|
474
|
+
method(
|
|
475
|
+
"MatchLiteral",
|
|
476
|
+
"VAR_INPUT value:STRING(8);END_VAR\nVAR i:UINT;size:UINT;END_VAR",
|
|
477
|
+
`MatchLiteral:=FALSE;size:=TO_UINT(ByteLength(value));IF cursor+size-1>TO_UINT(ByteLength(payload)) THEN RETURN;END_IF;
|
|
478
|
+
FOR i:=0 TO size-1 DO IF payload[cursor-1+i]<>value[i] THEN RETURN;END_IF;END_FOR;cursor:=cursor+size;MatchLiteral:=TRUE;`,
|
|
479
|
+
"BOOL",
|
|
480
|
+
),
|
|
481
|
+
method(
|
|
482
|
+
"ReadHeaders",
|
|
483
|
+
`VAR lineStart:UINT;lineEnd:UINT;i:UINT;j:UINT;key:STRING(64);octet:BYTE;found:BOOL;digits:UINT;END_VAR`,
|
|
484
|
+
`malformed:=FALSE;found:=FALSE;contentLength:=-1;
|
|
485
|
+
IF LEFT(received,9)<>'HTTP/1.1 ' AND LEFT(received,9)<>'HTTP/1.0 ' THEN malformed:=TRUE;RETURN;END_IF;
|
|
486
|
+
status:=0;FOR i:=9 TO 11 DO IF received[i]<48 OR received[i]>57 THEN malformed:=TRUE;RETURN;END_IF;status:=status*10+TO_UINT(received[i]-48);END_FOR;
|
|
487
|
+
lineStart:=1;
|
|
488
|
+
WHILE lineStart<headerEnd DO
|
|
489
|
+
lineEnd:=lineStart;WHILE lineEnd<headerEnd AND received[lineEnd-1]<>13 DO lineEnd:=lineEnd+1;END_WHILE;
|
|
490
|
+
key:='';j:=0;i:=lineStart;
|
|
491
|
+
WHILE i<lineEnd AND received[i-1]<>58 AND j<63 DO octet:=received[i-1];IF octet>=65 AND octet<=90 THEN octet:=octet+32;END_IF;key[j]:=octet;j:=j+1;i:=i+1;END_WHILE;key[j]:=0;
|
|
492
|
+
IF key='transfer-encoding' THEN malformed:=TRUE;RETURN;END_IF;
|
|
493
|
+
IF key='content-length' THEN
|
|
494
|
+
IF found THEN malformed:=TRUE;RETURN;END_IF;found:=TRUE;contentLength:=0;digits:=0;i:=i+1;
|
|
495
|
+
WHILE i<lineEnd AND received[i-1]=32 DO i:=i+1;END_WHILE;
|
|
496
|
+
WHILE i<lineEnd DO octet:=received[i-1];IF octet<48 OR octet>57 OR digits>=5 THEN malformed:=TRUE;RETURN;END_IF;contentLength:=contentLength*10+TO_DINT(octet-48);digits:=digits+1;i:=i+1;END_WHILE;
|
|
497
|
+
IF digits=0 THEN malformed:=TRUE;RETURN;END_IF;
|
|
498
|
+
END_IF;
|
|
499
|
+
lineStart:=lineEnd+2;
|
|
500
|
+
END_WHILE;`,
|
|
501
|
+
),
|
|
502
|
+
method(
|
|
503
|
+
"WriteString",
|
|
504
|
+
`VAR_INPUT value : STRING(2048); END_VAR\nVAR i:UINT; one:STRING(1); hex:STRING(16):='0123456789abcdef'; END_VAR`,
|
|
505
|
+
`Append('"');IF ByteLength(value)>0 THEN FOR i:=0 TO ByteLength(value)-1 DO
|
|
506
|
+
one[0]:=value[i];one[1]:=0;
|
|
507
|
+
IF value[i]=34 OR value[i]=92 THEN Append('\\');Append(one);
|
|
508
|
+
ELSIF value[i]<32 THEN Append('\\u00');one[0]:=hex[SHR(value[i],4)];Append(one);one[0]:=hex[value[i] AND 15];Append(one);
|
|
509
|
+
ELSE Append(one);END_IF;
|
|
510
|
+
END_FOR;END_IF;Append('"');`,
|
|
511
|
+
),
|
|
512
|
+
method(
|
|
513
|
+
"SkipWhitespace",
|
|
514
|
+
"",
|
|
515
|
+
`WHILE cursor<=TO_UINT(ByteLength(payload)) DO IF payload[cursor-1]<>32 AND payload[cursor-1]<>9 AND payload[cursor-1]<>10 AND payload[cursor-1]<>13 THEN EXIT;END_IF;cursor:=cursor+1;END_WHILE;`,
|
|
516
|
+
),
|
|
517
|
+
method(
|
|
518
|
+
"Expect",
|
|
519
|
+
"VAR_INPUT value:BYTE; END_VAR",
|
|
520
|
+
`IF cursor>TO_UINT(ByteLength(payload)) OR_ELSE payload[cursor-1]<>value THEN malformed:=TRUE;ELSE cursor:=cursor+1;END_IF;`,
|
|
521
|
+
),
|
|
522
|
+
method(
|
|
523
|
+
"TokenByte",
|
|
524
|
+
"VAR_INPUT value:BYTE;END_VAR",
|
|
525
|
+
`IF tokenUsed>=2048 OR value=0 THEN malformed:=TRUE;RETURN;END_IF;token[tokenUsed]:=value;tokenUsed:=tokenUsed+1;token[tokenUsed]:=0;`,
|
|
526
|
+
),
|
|
527
|
+
method(
|
|
528
|
+
"ReadHex",
|
|
529
|
+
"VAR value:UDINT;i:UINT;octet:BYTE;END_VAR",
|
|
530
|
+
`numberValue:=0;FOR i:=1 TO 4 DO
|
|
531
|
+
IF cursor>TO_UINT(ByteLength(payload)) THEN malformed:=TRUE;RETURN;END_IF;
|
|
532
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
533
|
+
IF octet>=48 AND octet<=57 THEN value:=TO_UDINT(octet-48);ELSIF octet>=65 AND octet<=70 THEN value:=TO_UDINT(octet-55);ELSIF octet>=97 AND octet<=102 THEN value:=TO_UDINT(octet-87);ELSE malformed:=TRUE;RETURN;END_IF;
|
|
534
|
+
numberValue:=numberValue*16+TO_LINT(value);END_FOR;`,
|
|
535
|
+
),
|
|
536
|
+
method(
|
|
537
|
+
"ReadString",
|
|
538
|
+
`VAR octet:BYTE;point:UDINT;high:UDINT;END_VAR`,
|
|
539
|
+
`token:='';tokenUsed:=0;Expect(34);
|
|
540
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) AND NOT malformed DO
|
|
541
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
542
|
+
IF octet=34 THEN ValidateUtf8();RETURN;END_IF;
|
|
543
|
+
IF octet=92 THEN
|
|
544
|
+
IF cursor>TO_UINT(ByteLength(payload)) THEN malformed:=TRUE;RETURN;END_IF;
|
|
545
|
+
octet:=payload[cursor-1];cursor:=cursor+1;
|
|
546
|
+
CASE octet OF
|
|
547
|
+
34,47,92: TokenByte(octet);
|
|
548
|
+
98:TokenByte(8);102:TokenByte(12);110:TokenByte(10);114:TokenByte(13);116:TokenByte(9);
|
|
549
|
+
117:
|
|
550
|
+
ReadHex();point:=TO_UDINT(numberValue);
|
|
551
|
+
IF point>=55296 AND point<=56319 THEN
|
|
552
|
+
high:=point;Expect(92);Expect(117);ReadHex();
|
|
553
|
+
IF numberValue<56320 OR numberValue>57343 THEN malformed:=TRUE;RETURN;END_IF;
|
|
554
|
+
point:=65536+(high-55296)*1024+TO_UDINT(numberValue)-56320;
|
|
555
|
+
ELSIF point>=56320 AND point<=57343 THEN malformed:=TRUE;RETURN;END_IF;
|
|
556
|
+
IF point<128 THEN TokenByte(TO_BYTE(point));
|
|
557
|
+
ELSIF point<2048 THEN TokenByte(TO_BYTE(192+SHR(point,6)));TokenByte(TO_BYTE(128+(point AND 63)));
|
|
558
|
+
ELSIF point<65536 THEN TokenByte(TO_BYTE(224+SHR(point,12)));TokenByte(TO_BYTE(128+(SHR(point,6) AND 63)));TokenByte(TO_BYTE(128+(point AND 63)));
|
|
559
|
+
ELSE TokenByte(TO_BYTE(240+SHR(point,18)));TokenByte(TO_BYTE(128+(SHR(point,12) AND 63)));TokenByte(TO_BYTE(128+(SHR(point,6) AND 63)));TokenByte(TO_BYTE(128+(point AND 63)));END_IF;
|
|
560
|
+
ELSE malformed:=TRUE;RETURN;
|
|
561
|
+
END_CASE;
|
|
562
|
+
ELSIF octet<32 THEN malformed:=TRUE;RETURN;ELSE TokenByte(octet);END_IF;
|
|
563
|
+
END_WHILE;malformed:=TRUE;`,
|
|
564
|
+
),
|
|
565
|
+
method(
|
|
566
|
+
"ValidateUtf8",
|
|
567
|
+
"VAR i:UINT;j:UINT;count:UINT;first:BYTE;next:BYTE;END_VAR",
|
|
568
|
+
`i:=0;
|
|
569
|
+
WHILE i<tokenUsed AND NOT malformed DO
|
|
570
|
+
first:=token[i];
|
|
571
|
+
IF first<128 THEN i:=i+1;
|
|
572
|
+
ELSE
|
|
573
|
+
IF first>=194 AND first<=223 THEN count:=1;ELSIF first>=224 AND first<=239 THEN count:=2;ELSIF first>=240 AND first<=244 THEN count:=3;ELSE malformed:=TRUE;RETURN;END_IF;
|
|
574
|
+
IF i+count>=tokenUsed THEN malformed:=TRUE;RETURN;END_IF;
|
|
575
|
+
next:=token[i+1];
|
|
576
|
+
IF (first=224 AND next<160) OR (first=237 AND next>=160) OR (first=240 AND next<144) OR (first=244 AND next>=144) THEN malformed:=TRUE;RETURN;END_IF;
|
|
577
|
+
FOR j:=1 TO count DO IF token[i+j]<128 OR token[i+j]>191 THEN malformed:=TRUE;RETURN;END_IF;END_FOR;
|
|
578
|
+
i:=i+count+1;
|
|
579
|
+
END_IF;
|
|
580
|
+
END_WHILE;`,
|
|
581
|
+
),
|
|
582
|
+
method(
|
|
583
|
+
"ReadInteger",
|
|
584
|
+
"VAR negative:BOOL;digits:UINT;octet:BYTE;END_VAR",
|
|
585
|
+
`numberValue:=0;negative:=FALSE;digits:=0;
|
|
586
|
+
IF payload[cursor-1]=45 THEN negative:=TRUE;cursor:=cursor+1;END_IF;
|
|
587
|
+
WHILE cursor<=TO_UINT(ByteLength(payload)) DO
|
|
588
|
+
octet:=payload[cursor-1];IF octet<48 OR octet>57 THEN EXIT;END_IF;
|
|
589
|
+
IF digits>=10 THEN malformed:=TRUE;RETURN;END_IF;
|
|
590
|
+
IF digits>0 AND numberValue=0 THEN malformed:=TRUE;RETURN;END_IF;numberValue:=numberValue*10+TO_LINT(octet-48);digits:=digits+1;cursor:=cursor+1;
|
|
591
|
+
END_WHILE;
|
|
592
|
+
IF digits=0 THEN malformed:=TRUE;END_IF;IF negative THEN numberValue:=-numberValue;END_IF;`,
|
|
593
|
+
),
|
|
594
|
+
].filter(
|
|
595
|
+
(item) =>
|
|
596
|
+
transport === "http" || !["AppendWire", "FindHeaders", "ReadHeaders"].includes(item.name),
|
|
597
|
+
);
|
|
598
|
+
};
|