This is an automated email from the ASF dual-hosted git repository. chrisdutz pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/plc4x.git
commit ebd945ee49987bcc380e3fd0334c5f74cf53589c Author: Christofer Dutz <[email protected]> AuthorDate: Fri Aug 14 09:34:42 2026 +0200 fix: Hardened the OPC-UA driver against issues with unexpected variants. --- .../apache/plc4x/java/opcua/OpcuaConnection.java | 357 +++++++++++---------- .../opcua/protocol/OpcuaSubscriptionHandle.java | 7 + .../opcua/protocol/OpcuaVariantToPlcValueTest.java | 107 ++++++ 3 files changed, 310 insertions(+), 161 deletions(-) diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java index 973b2632a4..632944549d 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/OpcuaConnection.java @@ -1588,168 +1588,201 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen public static PlcValue variantToPlcValue(PlcTag tag, Variant variant) { PlcValue value = null; - if (variant instanceof VariantBoolean) { - byte[] array = ((VariantBoolean) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.length); - for (byte b : array) { - values.add(new PlcBOOL(b != 0)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantSByte) { - byte[] array = ((VariantSByte) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.length); - for (byte b : array) { - values.add(new PlcSINT(b)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantByte) { - List<Short> array = ((VariantByte) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Short s : array) { - values.add(new PlcUSINT(s)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantInt16) { - List<Short> array = ((VariantInt16) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Short s : array) { - values.add(new PlcINT(s)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantUInt16) { - List<Integer> array = ((VariantUInt16) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Integer i : array) { - values.add(new PlcUINT(i)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantInt32) { - List<Integer> array = ((VariantInt32) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Integer i : array) { - values.add(new PlcDINT(i)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantUInt32) { - List<Long> array = ((VariantUInt32) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Long l : array) { - values.add(new PlcUDINT(l)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantInt64) { - List<Long> array = ((VariantInt64) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Long l : array) { - values.add(new PlcLINT(l)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantUInt64) { - List<BigInteger> array = ((VariantUInt64) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (BigInteger bi : array) { - values.add(new PlcULINT(bi)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantFloat) { - List<Float> array = ((VariantFloat) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Float f : array) { - values.add(new PlcREAL(f)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantDouble) { - List<Double> array = ((VariantDouble) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Double d : array) { - values.add(new PlcLREAL(d)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantString) { - List<PascalString> stringArray = ((VariantString) variant).getValue(); - List<PlcValue> values = new ArrayList<>(stringArray.size()); - for (PascalString ps : stringArray) { - values.add(new PlcSTRING(ps.getStringValue())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantDateTime) { - List<Long> array = ((VariantDateTime) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (Long l : array) { - values.add(DefaultPlcValueHandler.of(tag, LocalDateTime.ofInstant(Instant.ofEpochMilli(getDateTime(l)), ZoneOffset.UTC))); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantGuid) { - List<GuidValue> array = ((VariantGuid) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (GuidValue guidValue : array) { - //These two data sections aren't little endian like the rest. - byte[] data4Bytes = guidValue.getData4(); - int data4 = 0; - for (byte data4Byte : data4Bytes) { - data4 = (data4 << 8) + (data4Byte & 0xff); + // A node that exists but currently carries no value is encoded as a Null variant + // (VariantType 0). That's a valid, successful read of an empty value, not an + // unsupported type, so it maps to PlcNull rather than to null. + switch (variant) { + case null -> { + return new PlcNull(); + } + case VariantNull variantNull -> { + return new PlcNull(); + } + case VariantBoolean variantBoolean -> { + byte[] array = variantBoolean.getValue(); + List<PlcValue> values = new ArrayList<>(array.length); + for (byte b : array) { + values.add(new PlcBOOL(b != 0)); } - byte[] data5Bytes = guidValue.getData5(); - long data5 = 0; - for (byte data5Byte : data5Bytes) { - data5 = (data5 << 8) + (data5Byte & 0xff); + value = structurePlcValues(values, variant); + } + case VariantSByte variantSByte -> { + byte[] array = variantSByte.getValue(); + List<PlcValue> values = new ArrayList<>(array.length); + for (byte b : array) { + values.add(new PlcSINT(b)); + } + value = structurePlcValues(values, variant); + } + case VariantByte variantByte -> { + List<Short> array = variantByte.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Short s : array) { + values.add(new PlcUSINT(s)); + } + value = structurePlcValues(values, variant); + } + case VariantInt16 variantInt16 -> { + List<Short> array = variantInt16.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Short s : array) { + values.add(new PlcINT(s)); + } + value = structurePlcValues(values, variant); + } + case VariantUInt16 variantUInt16 -> { + List<Integer> array = variantUInt16.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Integer i : array) { + values.add(new PlcUINT(i)); + } + value = structurePlcValues(values, variant); + } + case VariantInt32 variantInt32 -> { + List<Integer> array = variantInt32.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Integer i : array) { + values.add(new PlcDINT(i)); + } + value = structurePlcValues(values, variant); + } + case VariantUInt32 variantUInt32 -> { + List<Long> array = variantUInt32.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Long l : array) { + values.add(new PlcUDINT(l)); + } + value = structurePlcValues(values, variant); + } + case VariantInt64 variantInt64 -> { + List<Long> array = variantInt64.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Long l : array) { + values.add(new PlcLINT(l)); + } + value = structurePlcValues(values, variant); + } + case VariantUInt64 variantUInt64 -> { + List<BigInteger> array = variantUInt64.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (BigInteger bi : array) { + values.add(new PlcULINT(bi)); + } + value = structurePlcValues(values, variant); + } + case VariantFloat variantFloat -> { + List<Float> array = variantFloat.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Float f : array) { + values.add(new PlcREAL(f)); + } + value = structurePlcValues(values, variant); + } + case VariantDouble variantDouble -> { + List<Double> array = variantDouble.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Double d : array) { + values.add(new PlcLREAL(d)); + } + value = structurePlcValues(values, variant); + } + case VariantString variantString -> { + List<PascalString> stringArray = variantString.getValue(); + List<PlcValue> values = new ArrayList<>(stringArray.size()); + for (PascalString ps : stringArray) { + values.add(new PlcSTRING(ps.getStringValue())); + } + value = structurePlcValues(values, variant); + } + case VariantDateTime variantDateTime -> { + List<Long> array = variantDateTime.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (Long l : array) { + values.add(DefaultPlcValueHandler.of(tag, LocalDateTime.ofInstant(Instant.ofEpochMilli(getDateTime(l)), ZoneOffset.UTC))); + } + value = structurePlcValues(values, variant); + } + case VariantGuid variantGuid -> { + List<GuidValue> array = variantGuid.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (GuidValue guidValue : array) { + //These two data sections aren't little endian like the rest. + byte[] data4Bytes = guidValue.getData4(); + int data4 = 0; + for (byte data4Byte : data4Bytes) { + data4 = (data4 << 8) + (data4Byte & 0xff); + } + byte[] data5Bytes = guidValue.getData5(); + long data5 = 0; + for (byte data5Byte : data5Bytes) { + data5 = (data5 << 8) + (data5Byte & 0xff); + } + values.add(new PlcSTRING(Long.toHexString(guidValue.getData1()) + "-" + Integer.toHexString(guidValue.getData2()) + "-" + Integer.toHexString(guidValue.getData3()) + "-" + Integer.toHexString(data4) + "-" + Long.toHexString(data5))); + } + value = structurePlcValues(values, variant); + } + case VariantXmlElement variantXmlElement -> { + List<PascalString> strings = variantXmlElement.getValue(); + List<PlcValue> values = new ArrayList<>(strings.size()); + for (PascalString ps : strings) { + values.add(new PlcSTRING(ps.getStringValue())); + } + value = structurePlcValues(values, variant); + } + case VariantLocalizedText variantLocalizedText -> { + List<LocalizedText> strings = variantLocalizedText.getValue(); + List<PlcValue> values = new ArrayList<>(strings.size()); + for (LocalizedText lt : strings) { + String s = ""; + s += lt.getLocaleSpecified() ? lt.getLocale().getStringValue() + "|" : ""; + s += lt.getTextSpecified() ? lt.getText().getStringValue() : ""; + values.add(new PlcSTRING(s)); + } + value = structurePlcValues(values, variant); + } + case VariantQualifiedName variantQualifiedName -> { + List<QualifiedName> strings = variantQualifiedName.getValue(); + List<PlcValue> values = new ArrayList<>(strings.size()); + for (QualifiedName qn : strings) { + values.add(new PlcSTRING("ns=" + qn.getNamespaceIndex() + ";s=" + qn.getName().getStringValue())); + } + value = structurePlcValues(values, variant); + } + case VariantExtensionObject variantExtensionObject -> { + List<ExtensionObject> objects = variantExtensionObject.getValue(); + List<PlcValue> values = new ArrayList<>(objects.size()); + for (ExtensionObject eo : objects) { + values.add(new PlcSTRING(eo.toString())); + } + value = structurePlcValues(values, variant); + } + case VariantNodeId variantNodeId -> { + List<NodeId> nodeIds = variantNodeId.getValue(); + List<PlcValue> values = new ArrayList<>(nodeIds.size()); + for (NodeId nid : nodeIds) { + values.add(new PlcSTRING(nid.toString())); } - values.add(new PlcSTRING(Long.toHexString(guidValue.getData1()) + "-" + Integer.toHexString(guidValue.getData2()) + "-" + Integer.toHexString(guidValue.getData3()) + "-" + Integer.toHexString(data4) + "-" + Long.toHexString(data5))); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantXmlElement) { - List<PascalString> strings = ((VariantXmlElement) variant).getValue(); - List<PlcValue> values = new ArrayList<>(strings.size()); - for (PascalString ps : strings) { - values.add(new PlcSTRING(ps.getStringValue())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantLocalizedText) { - List<LocalizedText> strings = ((VariantLocalizedText) variant).getValue(); - List<PlcValue> values = new ArrayList<>(strings.size()); - for (LocalizedText lt : strings) { - String s = ""; - s += lt.getLocaleSpecified() ? lt.getLocale().getStringValue() + "|" : ""; - s += lt.getTextSpecified() ? lt.getText().getStringValue() : ""; - values.add(new PlcSTRING(s)); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantQualifiedName) { - List<QualifiedName> strings = ((VariantQualifiedName) variant).getValue(); - List<PlcValue> values = new ArrayList<>(strings.size()); - for (QualifiedName qn : strings) { - values.add(new PlcSTRING("ns=" + qn.getNamespaceIndex() + ";s=" + qn.getName().getStringValue())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantExtensionObject) { - List<ExtensionObject> objects = ((VariantExtensionObject) variant).getValue(); - List<PlcValue> values = new ArrayList<>(objects.size()); - for (ExtensionObject eo : objects) { - values.add(new PlcSTRING(eo.toString())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantNodeId) { - List<NodeId> nodeIds = ((VariantNodeId) variant).getValue(); - List<PlcValue> values = new ArrayList<>(nodeIds.size()); - for (NodeId nid : nodeIds) { - values.add(new PlcSTRING(nid.toString())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantStatusCode) { - List<StatusCode> statusCodes = ((VariantStatusCode) variant).getValue(); - List<PlcValue> values = new ArrayList<>(statusCodes.size()); - for (StatusCode sc : statusCodes) { - values.add(new PlcSTRING(sc.toString())); - } - value = structurePlcValues(values, variant); - } else if (variant instanceof VariantByteString) { - List<ByteStringArray> array = ((VariantByteString) variant).getValue(); - List<PlcValue> values = new ArrayList<>(array.size()); - for (ByteStringArray byteStringArray : array) { - Short[] tmpValue = byteStringArray.getValue().toArray(new Short[0]); - values.add(DefaultPlcValueHandler.of(tag, tmpValue)); - } - value = structurePlcValues(values, variant); + value = structurePlcValues(values, variant); + } + case VariantStatusCode variantStatusCode -> { + List<StatusCode> statusCodes = variantStatusCode.getValue(); + List<PlcValue> values = new ArrayList<>(statusCodes.size()); + for (StatusCode sc : statusCodes) { + values.add(new PlcSTRING(sc.toString())); + } + value = structurePlcValues(values, variant); + } + case VariantByteString variantByteString -> { + List<ByteStringArray> array = variantByteString.getValue(); + List<PlcValue> values = new ArrayList<>(array.size()); + for (ByteStringArray byteStringArray : array) { + Short[] tmpValue = byteStringArray.getValue().toArray(new Short[0]); + values.add(DefaultPlcValueHandler.of(tag, tmpValue)); + } + value = structurePlcValues(values, variant); + } + default -> { + } } // If the tag declares a specific type (via suffix like :TIME, :DATE, etc.), @@ -1773,7 +1806,9 @@ public class OpcuaConnection extends ConnectionBase<OpcuaConfiguration> implemen // If the value is already a properly typed temporal value (e.g., from DTL // conversion), skip the override to avoid corrupting it. PlcValueType currentType = value.getPlcValueType(); - if (currentType == targetType || isTemporalType(currentType)) { + // There is nothing to re-interpret for an empty value; converting it would turn a + // "no value" into a bogus zero-valued TIME/DATE/... . + if (currentType == targetType || currentType == PlcValueType.NULL || isTemporalType(currentType)) { return value; } if (value instanceof PlcList list) { diff --git a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java index 5f4b5bf214..eb71c81df7 100644 --- a/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java +++ b/plc4j/drivers/opcua/src/main/java/org/apache/plc4x/java/opcua/protocol/OpcuaSubscriptionHandle.java @@ -303,6 +303,13 @@ public class OpcuaSubscriptionHandle implements PlcSubscriptionHandle { if (fieldNames.hasNext()) { String fieldName = fieldNames.next(); PlcValue plcValue = OpcuaConnection.variantToPlcValue(tag, variant); + if (plcValue == null) { + // Unsupported variant type: keep the field in the struct as an empty + // value instead of putting a raw null into it. + logger.error("Event field '{}' has unsupported variant type {}", fieldName, + variant.getClass().getSimpleName()); + plcValue = new PlcNull(); + } mapping.put(fieldName, plcValue); tagValues.put(tagName, new DefaultPlcResponseItem<>(PlcResponseCode.OK, new PlcStruct(mapping))); } else { diff --git a/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaVariantToPlcValueTest.java b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaVariantToPlcValueTest.java new file mode 100644 index 0000000000..d42fbce230 --- /dev/null +++ b/plc4j/drivers/opcua/src/test/java/org/apache/plc4x/java/opcua/protocol/OpcuaVariantToPlcValueTest.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.plc4x.java.opcua.protocol; + +import org.apache.plc4x.java.api.value.PlcValue; +import org.apache.plc4x.java.opcua.OpcuaConnection; +import org.apache.plc4x.java.opcua.readwrite.Variant; +import org.apache.plc4x.java.opcua.readwrite.VariantDataValue; +import org.apache.plc4x.java.opcua.readwrite.VariantDiagnosticInfo; +import org.apache.plc4x.java.opcua.readwrite.VariantExpandedNodeId; +import org.apache.plc4x.java.opcua.readwrite.VariantInt32; +import org.apache.plc4x.java.opcua.readwrite.VariantNull; +import org.apache.plc4x.java.opcua.readwrite.VariantVariant; +import org.apache.plc4x.java.opcua.tag.OpcuaTag; +import org.apache.plc4x.java.spi.values.PlcDINT; +import org.apache.plc4x.java.spi.values.PlcNull; +import org.apache.plc4x.java.spi.values.PlcTIME; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests the mapping of OPC UA variants to PlcValues, in particular that a node which exists + * but currently carries no value (Null variant, VariantType 0) comes back as a PlcNull + * instead of a raw {@code null} (see GH-2511). + */ +public class OpcuaVariantToPlcValueTest { + + @Test + public void nullVariantBecomesPlcNull() { + Variant variant = new VariantNull(false, false, null, null); + + PlcValue value = OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), variant); + + assertThat(value).isInstanceOf(PlcNull.class); + assertThat(value.isNull()).isTrue(); + } + + @Test + public void nullVariantIsNotAffectedByATypeSuffix() { + // A tag with a type suffix must not have its "no value" re-interpreted as a zero-valued + // TIME - it stays a PlcNull. + Variant variant = new VariantNull(false, false, null, null); + + PlcValue value = OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1;TIME"), variant); + + assertThat(value).isInstanceOf(PlcNull.class); + } + + @Test + public void nullVariantIsHandledWithoutATag() { + PlcValue value = OpcuaConnection.variantToPlcValue(null, new VariantNull(false, false, null, null)); + + assertThat(value).isInstanceOf(PlcNull.class); + } + + @Test + public void regularVariantIsStillMapped() { + Variant variant = new VariantInt32(false, false, null, null, null, List.of(42)); + + PlcValue value = OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), variant); + + assertThat(value).isEqualTo(new PlcDINT(42)); + } + + @Test + public void unsupportedVariantTypesStillReturnNull() { + // These variant types have no mapping yet, so callers still have to deal with a null + // result - that is what tells them apart from a Null variant, which maps to PlcNull. + // If one of them ever gets a mapping, drop it from here. + assertThat(OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), + new VariantDataValue(false, false, null, null, null, List.of()))).isNull(); + assertThat(OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), + new VariantDiagnosticInfo(false, false, null, null, null, List.of()))).isNull(); + assertThat(OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), + new VariantVariant(false, false, null, null, null, List.of()))).isNull(); + assertThat(OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1"), + new VariantExpandedNodeId(false, false, null, null, null, List.of()))).isNull(); + } + + @Test + public void typeSuffixStillOverridesARegularVariant() { + Variant variant = new VariantInt32(false, false, null, null, null, List.of(42)); + + PlcValue value = OpcuaConnection.variantToPlcValue(OpcuaTag.of("ns=2;i=1;TIME"), variant); + + assertThat(value).isEqualTo(new PlcTIME(42L)); + } +}
