From c8f629cecdfa5ebf6f4fe9d4de4528d57290b86f Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 08:01:51 -0700 Subject: [PATCH 1/8] Fix out of bounds read in formatString() on a trailing bare '%' The switch consumed the character after '%' with `switch (*(++src))`. When the '%' was the last character before the terminator, the read yielded '\0', which fell through to `default:` and did `src++`, advancing the source pointer past the terminator. The enclosing while loop then read out of bounds and kept copying until it happened to land on a zero byte. Advance the pointer explicitly and break out of the loop when the '%' has no command character after it. The other cases only run once a real command character has matched, so the single guard covers them. Reachable from application code: LogThat passes a user-supplied message string to formatString(), and a stray trailing '%' in an operator-facing log message is realistic input. Adds a table-driven self test to the AS6 example project, gated by the formatTest BOOL alongside the existing hexTest. Each format is copied into a scratch buffer padded with 'X' past the terminator, so a walk off the end drags the filler into the destination and fails the compare. Co-Authored-By: Claude Opus 5 --- .../Logical/Programs/Default/Main.c | 107 ++++++++++++++++++ .../Logical/Programs/Default/Variables.var | 4 + src/Ar/StringExt/CHANGELOG.md | 4 + src/Ar/StringExt/FormatString.c | 9 +- 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index 14a2dee..b36d445 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -159,6 +159,108 @@ static void runHexTest(void) } +/* Self test for formatString(). + Set formatTest to run it; formatTestFail must come back 0. + + The format of each case is copied into a scratch buffer whose remaining + bytes are filled with 'X' after the terminator. A formatter that walks + past the terminator - as a trailing bare '%' once did - drags that filler + into the destination, so the expected-output compare catches it. */ + +typedef struct formatCase_typ { + char* Format; + char* Expect; +} formatCase_typ; + +static const formatCase_typ formatCases[] = { + + /* Trailing bare '%' - no command character follows it */ + { "value: %", "value: " }, + { "%", "" }, + { "trail %d%", "trail 42" }, + { "%%%", "%" }, + + /* Ordinary substitutions */ + { "no format", "no format" }, + { "%d items", "42 items" }, + { "%d and %d", "42 and -7" }, + { "%s=%b", "abc=TRUE" }, + + /* Escaped percent, and an unknown command character */ + { "100%% done", "100% done" }, + { "a%zb", "ab" }, + +}; + + +static void runFormatTest(void) +{ + unsigned long i; + unsigned long j; + StrExtArgs_typ Args; + char Scratch[64]; + char Dest[80]; + signed long Length; + + formatTestPass= 0; + formatTestFail= 0; + strcpy((char*)formatTestFirstFail, ""); + + for(i=0; i<(sizeof(formatCases)/sizeof(formatCases[0])); i++){ + + memset(&Args, 0, sizeof(Args)); + Args.i[0]= 42; + Args.i[1]= -7; + Args.b[0]= 1; + Args.s[0]= (unsigned long)"abc"; + + /* Fill with junk, then lay the format in - everything after the + terminator stays 'X' and must never reach the destination */ + for(j=0; j Date: Tue, 25 Aug 2026 08:08:16 -0700 Subject: [PATCH 2/8] Address review: bump library version, tighten the truncation cases - ANSIC.lby was left at 1.1.0 while CHANGELOG declared 1.1.1. The previous release bumped both together, and without it Automation Studio cannot tell the fixed library from the broken one. - The two truncation cases passed against the pre-fix code as well: both filled the destination and exited the loop before the '%' was ever examined, so neither exercised the guard. Added the case that does - destSize 8 with "abcdef%" reaches the '%' branch with only the reserved null byte left, and yields "abcdefX"/7 pre-fix versus "abcdef"/6 after. Reworded the comments to say what each case actually covers, and routed them through the 'X'-padded scratch buffer rather than a bare literal so an over-read has known filler to land on. - Guarded the unbounded strcpy into the scratch buffer, so a format added to the table later cannot silently overrun it. - Noted on the new break that it exits the while loop, since it sits three lines above a switch full of breaks. Co-Authored-By: Claude Opus 5 --- .../Logical/Programs/Default/Main.c | 33 +++++++++++++++++-- src/Ar/StringExt/ANSIC.lby | 2 +- src/Ar/StringExt/FormatString.c | 4 +-- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index b36d445..14de210 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -217,6 +217,16 @@ static void runFormatTest(void) /* Fill with junk, then lay the format in - everything after the terminator stays 'X' and must never reach the destination */ for(j=0; j= sizeof(Scratch)){ + + formatTestFail++; + continue; + + } + strcpy(Scratch, formatCases[i].Format); memset(Dest, 0, sizeof(Dest)); @@ -250,13 +260,30 @@ static void runFormatTest(void) if( formatString(Dest, sizeof(Dest), 0, &Args) == STREXT_ERR_INVALID_INPUT ) formatTestPass++; else formatTestFail++; - /* Truncation - a trailing '%' with no room left must still terminate */ + /* Trailing '%' reached with only the reserved null byte left. The loop + still has room to enter the '%' branch, so this is a genuine + pre-fix failure - the old code stepped onto the filler and copied it */ + + for(j=0; j - + StringExt.typ StringExt.var diff --git a/src/Ar/StringExt/FormatString.c b/src/Ar/StringExt/FormatString.c index cb79758..b483298 100644 --- a/src/Ar/StringExt/FormatString.c +++ b/src/Ar/StringExt/FormatString.c @@ -73,9 +73,9 @@ signed long formatString(plcstring* destination, unsigned long destSize, plcstri src++; // A '%' at the very end of the source has no command character. - // Stop here so the cases below never step past the terminator. + // Leave the loop here - the cases below would step past the terminator. if (*src == '\0') - break; + break; // exits the while loop, not a switch switch (*src) { From f403fb72c019147deb5d00130522b250684b1f9c Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 08:12:44 -0700 Subject: [PATCH 3/8] Address second review pass: test bookkeeping nits - The too-long-format guard incremented formatTestFail without recording the format in formatTestFirstFail, so that one failure mode - a test authoring error rather than a library bug - would show a count with no input to look at. - Rejecting only at sizeof(Scratch) still admitted a format that leaves zero filler after the terminator, which is what the over-read detection relies on. Reject at sizeof(Scratch) - 8 so there is always filler to land on. - The destSize 4 case reused whatever Scratch held from the case above it. Refill it explicitly, like the other two. Co-Authored-By: Claude Opus 5 --- example/As6Project/Logical/Programs/Default/Main.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index 14de210..f7db13f 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -218,9 +218,13 @@ static void runFormatTest(void) terminator stays 'X' and must never reach the destination */ for(j=0; j= sizeof(Scratch)){ + /* A format that fills the scratch buffer would overrun it, and one + that nearly fills it leaves no filler to detect an over-read with */ + if(strlen(formatCases[i].Format) >= (sizeof(Scratch) - 8)){ + + if(strlen((char*)formatTestFirstFail) == 0){ + strncpy((char*)formatTestFirstFail, formatCases[i].Format, 79); + } formatTestFail++; continue; @@ -273,6 +277,9 @@ static void runFormatTest(void) /* Destination fills before the format character is ever reached */ + for(j=0; j Date: Tue, 25 Aug 2026 10:42:25 -0700 Subject: [PATCH 4/8] Make the example project's self tests runnable, and widen formatString coverage The self tests could be compiled but not run: the example project has no comms channel, so the trigger BOOLs could only be set from a debugger attached by hand. Enable the OPC UA server on the Intel configuration and publish both suites' trigger and result variables in OpcUaMap.uad, so a test run is a write and three reads from any OPC UA client. The endpoint is unencrypted and anonymous by design - this project exists to compile the library and run its tests, and requiring credentials would put a password in the repository. The README says plainly that this configuration must not be copied to a real machine. formatString coverage had real gaps, none of which the trailing-'%' fix touched but all of which the fix's own test file was the natural place to close: - %r and %f were not exercised at all. Compared against what brsftoa() itself produces, since the rendering is the runtime's business. - Only the TRUE half of %b was covered. - A null entry in the string argument array was untested; it must be skipped without pulling a later argument into its place. - Running past the end of an argument array was untested. - Truncation was only tested where the destination filled before a format character was reached, never part way THROUGH a substitution. - A destination size of 0 must write nothing at all, not even the terminator, since there is no byte to put it in. The size-dependent cases move into their own table rather than growing the list of hand-written assertions, and set up their arguments explicitly instead of inheriting whatever the case table left behind. Verified on ARsim (AR 6.7.6): 26/26 with the fix, 21/26 without it, the five failures being the four trailing-'%' cases plus the truncation case that reaches the '%' branch with only the reserved byte left. The pre-existing hex suite is 38/38 and unaffected either way. Co-Authored-By: Claude Opus 5 --- README.md | 25 ++++ example/As6Project/AsProject.apj | 3 + .../Logical/Programs/Default/Main.c | 113 +++++++++++++++--- .../Connectivity/OpcUaCs/OpcUaMap.uad | 15 +++ .../Connectivity/OpcUaCs/Package.pkg | 10 +- .../Connectivity/OpcUaCs/UaCsConfig.uacfg | 23 ++++ .../Connectivity/OpcUaCs/UaDvConfig.uadcfg | 15 +++ 7 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/OpcUaMap.uad create mode 100644 example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg create mode 100644 example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg diff --git a/README.md b/README.md index c86c4b1..ad00234 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,31 @@ For more documentation and examples, see https://loupeteam.github.io/LoupeDocs/l # Installation To install using the Loupe Package Manager (LPM), in an initialized Automation Studio project directory run `lpm install stringext`. For more information about LPM, see https://loupeteam.github.io/LoupeDocs/tools/lpm.html +# Self tests + +The example project in `example/As6Project` carries table-driven self tests for +some of the library functions. Each is gated by a BOOL in the `Default` program: +set it, and the test runs once on the next cycle and writes its results back. + +| Trigger | Covers | Results | +|---|---|---| +| `formatTest` | `formatString()` | `formatTestPass`, `formatTestFail`, `formatTestFirstFail` | +| `hexTest` | `HexStringToUDINT()`, `UDINTToHexString()` | `hexTestPass`, `hexTestFail`, `hexTestFirstFail` | + +`...Fail` must come back 0. `...FirstFail` names the first input that did not +behave as expected, and is left empty when the failure was a scalar assertion +rather than a table case. + +Build and deploy the `Intel` configuration to ARsim, then set the trigger from +the Automation Studio watch window, or over OPC UA from any client — the four +variables of each suite are published in `OpcUaMap.uad`. + +The `Intel` configuration enables the OPC UA server with an unencrypted +endpoint and anonymous access, so that the self tests can be driven without +credentials. That is appropriate for this example project, which exists to +compile the library and run its tests; **do not copy this OPC UA configuration +into a real machine.** + ## Licensing This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/example/As6Project/AsProject.apj b/example/As6Project/AsProject.apj index 4658f96..f85570a 100644 --- a/example/As6Project/AsProject.apj +++ b/example/As6Project/AsProject.apj @@ -7,4 +7,7 @@ + + + \ No newline at end of file diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index f7db13f..bf0da46 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -190,6 +190,48 @@ static const formatCase_typ formatCases[] = { { "100%% done", "100% done" }, { "a%zb", "ab" }, + /* Both halves of %b, and a null entry in the string array, which is + skipped without consuming a later argument in its place */ + { "%b%b", "TRUEFALSE" }, + { "%s|%s|%s", "abc||xyz" }, + + /* One more %d than the argument array holds - the sixth is dropped, + which is why the expected output ends in a space */ + { "%d %d %d %d %d %d", "42 -7 0 0 0 " }, + +}; + + +/* Cases that need a destination smaller than the formatted result. Same + scratch buffer treatment; DestSize is passed to formatString() verbatim. */ + +typedef struct formatSizeCase_typ { + char* Format; + unsigned long DestSize; + char* Expect; +} formatSizeCase_typ; + +static const formatSizeCase_typ formatSizeCases[] = { + + /* Trailing '%' reached with only the reserved null byte left. The loop + still has room to enter the '%' branch, so this is a genuine pre-fix + failure - the old code stepped onto the filler and copied it */ + { "abcdef%", 8, "abcdef" }, + + /* Destination fills before the format character is ever reached */ + { "abcdef%", 4, "abc" }, + + /* Only the reserved null byte fits - nothing is written at all */ + { "%", 1, "" }, + + /* Truncation part way through a substitution */ + { "%d", 2, "4" }, + { "%s", 3, "ab" }, + + /* Escaped percent with exactly one byte to spare, and an exact fit */ + { "%%", 2, "%" }, + { "abcd", 5, "abcd" }, + }; @@ -200,6 +242,7 @@ static void runFormatTest(void) StrExtArgs_typ Args; char Scratch[64]; char Dest[80]; + char Expected[16]; signed long Length; formatTestPass= 0; @@ -213,6 +256,7 @@ static void runFormatTest(void) Args.i[1]= -7; Args.b[0]= 1; Args.s[0]= (unsigned long)"abc"; + Args.s[2]= (unsigned long)"xyz"; /* Fill with junk, then lay the format in - everything after the terminator stays 'X' and must never reach the destination */ @@ -264,33 +308,72 @@ static void runFormatTest(void) if( formatString(Dest, sizeof(Dest), 0, &Args) == STREXT_ERR_INVALID_INPUT ) formatTestPass++; else formatTestFail++; - /* Trailing '%' reached with only the reserved null byte left. The loop - still has room to enter the '%' branch, so this is a genuine - pre-fix failure - the old code stepped onto the filler and copied it */ + /* Destinations too small for the formatted result. Arguments are set up + explicitly here rather than inherited from the loop above, so these + cases do not change meaning if that table is ever edited. */ - for(j=0; j + + + + + + + + + + + + + + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg index c43b450..989bb59 100644 --- a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg @@ -1,5 +1,9 @@ - + - - \ No newline at end of file + + UaCsConfig.uacfg + UaDvConfig.uadcfg + OpcUaMap.uad + + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg new file mode 100644 index 0000000..b84a93e --- /dev/null +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg new file mode 100644 index 0000000..6b4ef71 --- /dev/null +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + From 23b63bcdb4b4c380361c1001b4add9dac0165baf Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 11:04:06 -0700 Subject: [PATCH 5/8] Address fourth review pass: fix the OPC UA config, harden the size cases The review raised a blocker that turned out to be a false positive, and two findings that were real. Settled the blocker by experiment rather than by argument, since the harness now makes that cheap: - DID publish the variables. Deploying with an emptied OpcUaMap.uad makes the same node read BadNodeIdUnknown, so the map is load bearing and was working. Switched to the AS canonical spelling anyway - it is what Automation Studio writes, so leaving it would produce a spurious whole file diff the first time anyone opens the configuration in the IDE. - The DefaultRolePermissions group index WAS wrong. Role [0] is a no-op, so writes were succeeding only because an unconfigured namespace is unrestricted. Confirmed by deploying Role [1] with PermissionWrite 0 and watching the write come back BadUserAccessDenied. Corrected to Role [1], which means the permissions are now actually enforced rather than incidental. - Restored the UTF-8 BOM stripped from Package.pkg, and gave the three new files BOMs and CRLF endings to match every sibling. The size case loop could not detect the overrun it exists to police: it zeroed the destination and compared with strcmp, which stops at the first null. It now fills with 'Z' and asserts that every byte from DestSize to the end of the buffer is untouched. Both loops also bound the scratch buffer with a terminator, so running the suite against a library that does walk off the end stops at the buffer instead of leaving it. Coverage added where the review found untested paths: - %b under truncation. It is the only branch whose lengths are hard coded rather than computed, so it is the likeliest place for a length bug, and nothing exercised it. - %i, which is an alias for %d and had no test at all. - The %s argument array running out, which reaches exhaustion through a different condition than %d does. - A large magnitude real, since 1.5 leaves the library's 16 byte scratch buffer almost entirely untouched. The README now names the port, says the encrypted policies remain offered alongside the anonymous one, and states that the result variables are anonymously writable too, not just the triggers. Verified on ARsim (AR 6.7.6): 30/30 with the fix, 25/30 without it, the same five trailing-'%' detectors. Hex suite 38/38 and unaffected. Co-Authored-By: Claude Opus 5 --- README.md | 14 +++-- .../Logical/Programs/Default/Main.c | 60 +++++++++++++++++-- .../Connectivity/OpcUaCs/OpcUaMap.uad | 4 +- .../Connectivity/OpcUaCs/Package.pkg | 2 +- .../Connectivity/OpcUaCs/UaCsConfig.uacfg | 2 +- .../Connectivity/OpcUaCs/UaDvConfig.uadcfg | 4 +- 6 files changed, 71 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index ad00234..4f67010 100644 --- a/README.md +++ b/README.md @@ -31,11 +31,15 @@ Build and deploy the `Intel` configuration to ARsim, then set the trigger from the Automation Studio watch window, or over OPC UA from any client — the four variables of each suite are published in `OpcUaMap.uad`. -The `Intel` configuration enables the OPC UA server with an unencrypted -endpoint and anonymous access, so that the self tests can be driven without -credentials. That is appropriate for this example project, which exists to -compile the library and run its tests; **do not copy this OPC UA configuration -into a real machine.** +The `Intel` configuration enables the OPC UA server on port 4840 and adds an +unencrypted, anonymous endpoint, so that the self tests can be driven without +putting a credential in this repository. The encrypted policies AS enables by +default are still offered alongside it. Role `Everyone` is granted browse, +read and write on the default view, so all eight variables above — results +included, not just the triggers — are anonymously writable. + +That is appropriate for this project, which exists to compile the library and +run its tests. **Do not copy this OPC UA configuration into a real machine.** ## Licensing diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index bf0da46..cf1eb02 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -165,7 +165,9 @@ static void runHexTest(void) The format of each case is copied into a scratch buffer whose remaining bytes are filled with 'X' after the terminator. A formatter that walks past the terminator - as a trailing bare '%' once did - drags that filler - into the destination, so the expected-output compare catches it. */ + into the destination, so the expected-output compare catches it. Only the + trailing '%' path can reach past the terminator at all: every other branch + consumes a character it has already seen to be non-null. */ typedef struct formatCase_typ { char* Format; @@ -199,6 +201,13 @@ static const formatCase_typ formatCases[] = { which is why the expected output ends in a space */ { "%d %d %d %d %d %d", "42 -7 0 0 0 " }, + /* The %s array runs out the same way, but through a second condition: + a null entry is skipped by one test and an exhausted array by another */ + { "%s%s%s%s%s%s", "abcxyz" }, + + /* %i is an alias for %d and is otherwise never exercised */ + { "%i and %i", "42 and -7" }, + }; @@ -224,9 +233,12 @@ static const formatSizeCase_typ formatSizeCases[] = { /* Only the reserved null byte fits - nothing is written at all */ { "%", 1, "" }, - /* Truncation part way through a substitution */ + /* Truncation part way through a substitution. %b is the only branch + whose lengths are hard coded rather than computed, so it is the + likeliest place for a length bug and gets its own case. */ { "%d", 2, "4" }, { "%s", 3, "ab" }, + { "%b", 3, "TR" }, /* Escaped percent with exactly one byte to spare, and an exact fit */ { "%%", 2, "%" }, @@ -244,6 +256,7 @@ static void runFormatTest(void) char Dest[80]; char Expected[16]; signed long Length; + unsigned long Overrun; formatTestPass= 0; formatTestFail= 0; @@ -259,8 +272,11 @@ static void runFormatTest(void) Args.s[2]= (unsigned long)"xyz"; /* Fill with junk, then lay the format in - everything after the - terminator stays 'X' and must never reach the destination */ + terminator stays 'X' and must never reach the destination. The + last byte is terminated so that a library that does run off the + end stops here rather than walking out of the buffer. */ for(j=0; j= (sizeof(Scratch) - 8)){ + + if(strlen((char*)formatTestFirstFail) == 0){ + strncpy((char*)formatTestFirstFail, formatSizeCases[i].Format, 79); + } + + formatTestFail++; + continue; + + } + strcpy(Scratch, formatSizeCases[i].Format); - memset(Dest, 0, sizeof(Dest)); + /* Fill the whole destination, so that anything written at or past + DestSize survives as a 'Z' and can be checked for below */ + memset(Dest, 'Z', sizeof(Dest)); Length= formatString(Dest, formatSizeCases[i].DestSize, Scratch, &Args); + /* Nothing may be written at or beyond DestSize */ + Overrun= 0; + for(j=formatSizeCases[i].DestSize; j + - + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg index 989bb59..a636ccb 100644 --- a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/Package.pkg @@ -1,4 +1,4 @@ - + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg index b84a93e..c6ab1ad 100644 --- a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaCsConfig.uacfg @@ -1,4 +1,4 @@ - + diff --git a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg index 6b4ef71..11c4857 100644 --- a/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg +++ b/example/As6Project/Physical/Intel/5PC900_TS17_04/Connectivity/OpcUaCs/UaDvConfig.uadcfg @@ -1,8 +1,8 @@ - + - + From a73276cc09f3bb108039e6972b8e35212ba5e971 Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 11:16:44 -0700 Subject: [PATCH 6/8] Pin OpcUaCs to a version the runner has, and fix the large-real case CI rejected the build outright: error 9346, OpcUaCs 6.5.0 is not installed on the AS6 runner. The runner's inventory is not knowable from a workstation, so a throwaway diagnostic step listed it - the runner has 6.0.0 and 6.6.1, this workstation has 6.0.0, 6.5.0 and 6.7.0. That step is not part of this commit; it existed only long enough to answer the question. 6.0.0 is the only version present on both, and it does not work: the generated NodeSet fails to load at runtime with BadNodeIdUnknown while adding a reference for the first published variable, so nothing is published at all. It builds, which is exactly why it needed running rather than compiling. Pinned 6.6.1 instead, which the runner has. That leaves a real gap, recorded here rather than papered over: 6.6.1 is not installed on this workstation, so the pinned version has been verified to BUILD by CI but not to publish at runtime. Runtime publishing was verified on 6.5.0 and 6.7.0, and 6.6.1 sits between them. Aligning the runner and the workstation on one version would close this properly. Review follow-ups in the same commit: - The large-real case could not detect what it was added to detect. Expected was 16 bytes, the same budget as the library's own temp_string, so a value needing more would smash the test's stack at brsftoa() before formatString() was ever called - undefined behaviour rather than a reported failure. Expected is now 64 bytes and the case asserts strlen(Expected) < 16 explicitly, which is the condition the library actually depends on. - OpcUaMap.uad now matches what Automation Studio emits: the instruction and the block, and no xmlns attributes AS does not write. The earlier reasoning for omitting these - that the AS templates lack them - was wrong; the templates are not what AS writes on save. - The three reals blocks now bound their scratch buffer with a terminator, like both loops already did. Verified on ARsim (AR 6.7.6, OpcUaCs 6.7.0): 30/30, NodeSet loaded, and the hex suite 38/38. Co-Authored-By: Claude Opus 5 --- example/As6Project/AsProject.apj | 2 +- example/As6Project/Logical/Programs/Default/Main.c | 13 ++++++++++--- .../Connectivity/OpcUaCs/OpcUaMap.uad | 9 ++++++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/example/As6Project/AsProject.apj b/example/As6Project/AsProject.apj index f85570a..3124b04 100644 --- a/example/As6Project/AsProject.apj +++ b/example/As6Project/AsProject.apj @@ -8,6 +8,6 @@ - + \ No newline at end of file diff --git a/example/As6Project/Logical/Programs/Default/Main.c b/example/As6Project/Logical/Programs/Default/Main.c index cf1eb02..16c9ddb 100644 --- a/example/As6Project/Logical/Programs/Default/Main.c +++ b/example/As6Project/Logical/Programs/Default/Main.c @@ -254,7 +254,7 @@ static void runFormatTest(void) StrExtArgs_typ Args; char Scratch[64]; char Dest[80]; - char Expected[16]; + char Expected[64]; signed long Length; unsigned long Overrun; @@ -400,6 +400,7 @@ static void runFormatTest(void) brsftoa(Args.r[0], (unsigned long)Expected); for(j=0; j - + + + + + + + + From a6a2d61d2560f54262898ea1306ec95a3e96fe1e Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 11:24:46 -0700 Subject: [PATCH 7/8] Give the ARM configuration an OPC UA config file Declaring OpcUaCs in the .apj makes the technology package project wide, so every configuration needs a *.uacfg, not just the one that uses it. CI caught this: Intel built, ARM failed with error 5198. ARM gets a config with the server disabled, which is the Automation Studio default and leaves the configuration behaving exactly as before. The self tests are driven on Intel, which is the configuration ARsim simulates; there is no reason to open a server on the configuration meant for real hardware, and good reason not to. Verified locally as far as this workstation allows: with the file in place the ARM build gets past error 5198 and fails later on a hardware support file that AR 6.7.6 does not carry for the X20CP0410, which is a local install gap rather than a project problem. CI builds ARM against AR 6.6.2 and is the real check. Co-Authored-By: Claude Opus 5 --- .../ARM/X20CP0410/Connectivity/OpcUaCs/Package.pkg | 7 +++++++ .../ARM/X20CP0410/Connectivity/OpcUaCs/UaCsConfig.uacfg | 6 ++++++ .../Physical/ARM/X20CP0410/Connectivity/Package.pkg | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/Package.pkg create mode 100644 example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/UaCsConfig.uacfg diff --git a/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/Package.pkg b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/Package.pkg new file mode 100644 index 0000000..377aee3 --- /dev/null +++ b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/Package.pkg @@ -0,0 +1,7 @@ + + + + + UaCsConfig.uacfg + + diff --git a/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/UaCsConfig.uacfg b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/UaCsConfig.uacfg new file mode 100644 index 0000000..beb902d --- /dev/null +++ b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/OpcUaCs/UaCsConfig.uacfg @@ -0,0 +1,6 @@ + + + + + + diff --git a/example/As6Project/Physical/ARM/X20CP0410/Connectivity/Package.pkg b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/Package.pkg index bfea000..666b638 100644 --- a/example/As6Project/Physical/ARM/X20CP0410/Connectivity/Package.pkg +++ b/example/As6Project/Physical/ARM/X20CP0410/Connectivity/Package.pkg @@ -1,5 +1,7 @@  - + + OpcUaCs + \ No newline at end of file From 42b5cfd403e1d41cdf74d0aed1fff4b28e24fd72 Mon Sep 17 00:00:00 2001 From: Scott Claiborne Date: Tue, 25 Aug 2026 11:32:39 -0700 Subject: [PATCH 8/8] Record which OPC UA package versions were verified how The pinned version is the one the build runner has, which is not the one whose runtime behaviour was confirmed, and 6.0.0 fails in a way that looks like success until you look for the variables. Someone debugging a missing node should not have to reconstruct that from commit messages. Co-Authored-By: Claude Opus 5 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 4f67010..f96f27e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ included, not just the triggers — are anonymously writable. That is appropriate for this project, which exists to compile the library and run its tests. **Do not copy this OPC UA configuration into a real machine.** +The project pins OPC UA C/S 6.6.1, which is what the build runner carries. +Publishing has been confirmed at runtime on 6.5.0 and 6.7.0; 6.6.1 has only +been confirmed to build. 6.0.0 does **not** work — it compiles, and then the +generated NodeSet fails to load with `BadNodeIdUnknown`, so nothing is +published at all. If the variables do not appear, check this version first. + ## Licensing This project is licensed under the [MIT License](LICENSE). \ No newline at end of file