diff --git a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java index 8205f4e61d..a48653b2fb 100644 --- a/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java +++ b/core/pva/src/main/java/org/epics/pva/client/ClientUDPHandler.java @@ -61,7 +61,7 @@ public interface SearchResponseHandler * @param server Server that replied to a search request * @param version Server version * @param guid Globally unique ID of the server - * @param tcp Does server require TLS? + * @param tls Does server require TLS? */ void handleSearchResponse(int channel_id, InetSocketAddress server, int version, Guid guid, boolean tls); } @@ -250,15 +250,15 @@ private boolean handleBeacon(final InetSocketAddress from, final byte version, else server = new InetSocketAddress(addr, port); - final String protocol = PVAString.decodeString(buffer); - if (! "tcp".equals(protocol)) - { - logger.log(Level.WARNING, "PVA Server " + from + " sent beacon for protocol '" + protocol + "'"); - return false; - } - try { + final String protocol = PVAString.decodeString(buffer); + if (! "tcp".equals(protocol)) + { + logger.log(Level.WARNING, "PVA Server " + from + " sent beacon for protocol '" + protocol + "'"); + return false; + } + // Decode optional server status (likely null) final PVATypeRegistry types = new PVATypeRegistry(); final PVAData server_status = types.decodeType("", buffer); diff --git a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java index 7a868ff1be..71baa61031 100644 --- a/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java +++ b/core/pva/src/main/java/org/epics/pva/common/PVAHeader.java @@ -10,6 +10,8 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; +import org.epics.pva.exceptions.PVAProtocolException; + /** PVA Message Header * *
@@ -174,9 +176,9 @@ public static void encodeMessageHeader(final ByteBuffer buffer, byte flags, fina
      *  @param buffer Buffer as start of protocol header
      *  @param expect_server Expect a server message? Else client message
      *  @return Expected total message size (header + payload)
-     *  @throws Exception on protocol violation
+     *  @throws PVAProtocolException on protocol violation
      */
-    public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean expect_server) throws Exception
+    public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean expect_server) throws PVAProtocolException
     {
         if (buffer.position() < PVAHeader.HEADER_SIZE)
             return PVAHeader.HEADER_SIZE;
@@ -185,11 +187,11 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
         // parsing the initial set of bytes
         final byte magic = buffer.get(0);
         if (magic != PVAHeader.PVA_MAGIC)
-            throw new Exception(String.format("Message lacks magic 0x%02X, got 0x%02X", PVAHeader.PVA_MAGIC, magic));
+            throw new PVAProtocolException(String.format("Message lacks magic 0x%02X, got 0x%02X", PVAHeader.PVA_MAGIC, magic));
 
         final byte version = buffer.get(1);
         if (version < PVAHeader.REQUIRED_PVA_PROTOCOL_REVISION)
-            throw new Exception("Cannot handle protocol version " + version +
+            throw new PVAProtocolException("Cannot handle protocol version " + version +
                                 ", expect version " +
                                 PVAHeader.REQUIRED_PVA_PROTOCOL_REVISION +
                                 " or higher");
@@ -197,7 +199,7 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
         final byte flags = buffer.get(2);
         final boolean is_server = (flags & PVAHeader.FLAG_SERVER) != 0;
         if (is_server != expect_server)
-                throw new Exception(expect_server ? "Expected server message" : "Expected client message");
+                throw new PVAProtocolException(expect_server ? "Expected server message" : "Expected client message");
 
         // With each received message, check the byte order
         // and adjust buffer to read further content which usually
@@ -213,6 +215,17 @@ public static int checkMessageAndGetSize(final ByteBuffer buffer, final boolean
 
         // Application messages are followed by this number of data bytes
         final int payload = buffer.getInt(PVAHeader.HEADER_OFFSET_PAYLOAD_SIZE);
+        // Java implementation for now does not handle large 'unsigned' sizes,
+        // limited to the positive range of a signed int.
+        // Could use `Integer.toUnsignedLong(payload)`, but JDK API
+        // like buffer buffer.remaining() or buffer.get(10) is using int,
+        // so us updating to long would be of limited use
+        if (payload < 0)
+            throw new PVAProtocolException("Payload size " + payload +
+                                " exceeds max signed integer " + Integer.toHexString(Integer.MAX_VALUE));
+        // Could check against a PVA variant of EPICS_CA_MAX_ARRAY_BYTES,
+        // but PVA design specifically aims to use all available memory
+        // without self-enforced limitations (confirmed in 2026-07-10 EPICS code telecon)
 
         // Total message size: Header followed by data
         return PVAHeader.HEADER_SIZE + payload;
diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
index 970a449a3b..d89dbf1a2e 100644
--- a/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
+++ b/core/pva/src/main/java/org/epics/pva/common/SearchRequest.java
@@ -26,6 +26,9 @@
 @SuppressWarnings("nls")
 public class SearchRequest
 {
+    private static final String LOG_PVA_CLIENT = "PVA Client ";
+    private static final String LOG_SENT_SEARCH = " sent search #";
+
     /** Channel with CID to be searched */
     public static class Channel
     {
@@ -61,7 +64,7 @@ public String toString()
         {
             return "'" + name + "' [CID " + cid + "]";
         }
-    };
+    }
 
     /** Server should reply with its GUID and empty CID list
      *  even if it does not host any of the searched channels
@@ -125,7 +128,7 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre
         // plus the list of names.
         if (payload < 4+1+3+16+2+1+2)
         {
-            logger.log(Level.WARNING, "PVA client " + from + " sent only " + payload + " bytes for search request");
+            logger.log(Level.WARNING, () -> "PVA client " + from + " sent only " + payload + " bytes for search request");
             return null;
         }
         final SearchRequest search = new SearchRequest();
@@ -151,7 +154,7 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre
         }
         catch (Exception ex)
         {
-            logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " with invalid address");
+            logger.log(Level.WARNING, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid address");
             return null;
         }
         int port = Short.toUnsignedInt(buffer.getShort());
@@ -173,15 +176,23 @@ public static SearchRequest decode(final OriginTag origin, final InetSocketAddre
         boolean tcp = search.tls = false;
         int count = Byte.toUnsignedInt(buffer.get());
         String unknown_protocol = "";
-        for (int i=0; i LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " with invalid protocol");
+            return null;
         }
 
         // Loop over searched channels
@@ -190,28 +201,37 @@ else if ("tcp".equals(protocol))
         if (count == 0)
         {   // pvlist request
             search.channels = null;
-            logger.log(Level.FINER, () -> "PVA Client " + from + " sent search #" + search.seq + " to list servers");
+            logger.log(Level.FINER, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " to list servers");
         }
         else
         {   // Channel search request
             if (! (tcp || search.tls))
             {
-                logger.log(Level.WARNING, "PVA Client " + from + " sent search #" + search.seq + " for protocol '" + unknown_protocol + "', need 'tcp' or 'tls'");
+                final String unsupported_protocol = unknown_protocol;
+                logger.log(Level.WARNING, () -> LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for protocol '" + unsupported_protocol + "', need 'tcp' or 'tls'");
                 return null;
             }
             search.channels = new ArrayList<>(count);
-            for (int i=0; i "PVA Client " + from + " sent search #" + search.seq + " for " + name + " [cid " + cid + "]"
-                                            + ", reply addr " + orig_response_addr
-                                            + (orig_response_addr.equals(search.client) ? "" : ", using " + search.client)
-                                            + (search.tls               ? " (TLS)" : "")
-                                            + (search.unicast           ? " (unicast)" : "")
-                                            + (search.reply_required    ? " (reply required)" : "")
-                                            + (search.reply_to_src_port ? (origin == null ?  " (reply to source port)"  : " (reply to source port ignored because of origin tag)") : ""));
-                search.channels.add(new Channel(cid, name));
+                for (int i=0; i LOG_PVA_CLIENT + from + LOG_SENT_SEARCH + search.seq + " for " + name + " [cid " + cid + "]"
+                            + ", reply addr " + orig_response_addr
+                            + (orig_response_addr.equals(search.client) ? "" : ", using " + search.client)
+                            + (search.tls               ? " (TLS)" : "")
+                            + (search.unicast           ? " (unicast)" : "")
+                            + (search.reply_required    ? " (reply required)" : "")
+                            + (search.reply_to_src_port ? (origin == null ? " (reply to source port)" : " (reply to source port ignored because of origin tag)") : ""));
+                    search.channels.add(new Channel(cid, name));
+                }
+            }
+            catch (Exception ex)
+            {
+                logger.log(Level.WARNING, ex, () -> LOG_PVA_CLIENT + from + " sent damaged search #" + search.seq);
+                return null;
             }
         }
 
diff --git a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java
index 1367d34626..967ab76dcf 100644
--- a/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java
+++ b/core/pva/src/main/java/org/epics/pva/common/SearchResponse.java
@@ -97,6 +97,9 @@ public static SearchResponse decode(final int payload, final ByteBuffer buffer)
         result.found = PVABool.decodeBoolean(buffer);
 
         final int count = Short.toUnsignedInt(buffer.getShort());
+        if (count*Integer.BYTES > buffer.remaining())
+            throw new Exception("PVA Server sent search reply #" + result.seq + " for " + count + " CIDs " +
+                                " with only " + buffer.remaining() + " bytes in buffer");
         result.cid = new int[count];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(count, buffer.remaining());
         // Try to re-use elements
         PVAny[] new_elements = elements;
         if (new_elements == null  ||  new_elements.length != count)
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java
index 28e17094ef..36fd5d28b4 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVABitSet.java
@@ -27,10 +27,13 @@ public static void encodeBitSet(final BitSet bits, final ByteBuffer buffer)
 
     /** @param buffer Source buffer
      *  @return Decoded bits
+     *  @throws Exception on error
      */
-    public static BitSet decodeBitSet(final ByteBuffer buffer)
+    public static BitSet decodeBitSet(final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
+        if (size < 0  ||  size > buffer.remaining())
+            throw new Exception("Bitset size " + size + " with only " + buffer.remaining() + " bytes in buffer");
         final byte[] bytes = new byte[size];
         buffer.get(bytes);
         return BitSet.valueOf(bytes);
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
index 15b1df5fc0..7d46fdc0a8 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVABoolArray.java
@@ -13,6 +13,7 @@
 import java.util.List;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -95,6 +96,8 @@ public void encodeType(ByteBuffer buffer, BitSet described) throws Exception
     public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
+        if (size < 0  ||  size > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining());
         final boolean[] new_value = new boolean[size];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining());
         final byte[] new_value = new byte[size];
         buffer.get(new_value);
         value = new_value;
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
index 9e6dce6a5b..c5ac4be0e8 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVADoubleArray.java
@@ -13,6 +13,7 @@
 import java.util.List;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -98,6 +99,8 @@ public void encodeType(final ByteBuffer buffer, final BitSet described) throws E
     public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
+        if (size < 0  ||  (long) size * Double.BYTES > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Double.BYTES);
         final double[] new_value = new double[size];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Float.BYTES);
         final float[] new_value = new float[size];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Integer.BYTES);
         final int[] new_value = new int[size];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Long.BYTES);
         final long[] new_value = new long[size];
         for (int i=0; i buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining(), Short.BYTES);
         // Try to re-use existing array
         final short[] new_value = new short[size];
         // Considered using
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVASize.java b/core/pva/src/main/java/org/epics/pva/data/PVASize.java
index 9ad204796a..9e96c72154 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVASize.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVASize.java
@@ -54,6 +54,9 @@ else if (size < 254)
      */
     public static final int decodeSize(final ByteBuffer buffer)
     {
+        // XXXX Update to long, using Integer.toUnsignedLong(..)?
+        // JDK api like buffer.remaining() is limited to int,
+        // so this would have limited effect...
         byte b = buffer.get();
         if (b == -1)
             return -1;
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java b/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java
index 90f1e3bf35..1c17ed43d5 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStatus.java
@@ -72,8 +72,9 @@ public void encode(final ByteBuffer buffer)
 
     /** @param buffer Source buffer
      *  @return Decoded status
+     *  @throws Exception on error
      */
-    public static PVAStatus decode(final ByteBuffer buffer)
+    public static PVAStatus decode(final ByteBuffer buffer) throws Exception
     {
         final byte b = buffer.get();
         if (b == -1)
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAString.java b/core/pva/src/main/java/org/epics/pva/data/PVAString.java
index a8651f5b85..f4706c9a52 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAString.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAString.java
@@ -49,12 +49,16 @@ public static void encodeString(final String string, final ByteBuffer buffer)
 
     /** @param buffer Buffer from which to decode string
      *  @return Decoded string
+     *  @throws Exception on error
      */
-    public static String decodeString(final ByteBuffer buffer)
+    public static String decodeString(final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
         if (size >= 0)
         {
+            if (size > buffer.remaining())
+                throw new Exception("PVAString size " + size +
+                                    " exceeds remaining buffer size " + buffer.remaining());
             byte[] bytes = new byte[size];
             buffer.get(bytes);
             return new String(bytes);
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
index d01d2b2e9d..157eacd298 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStringArray.java
@@ -14,6 +14,7 @@
 import java.util.Objects;
 
 import org.epics.pva.PVASettings;
+import org.epics.pva.exceptions.PVAArraySizeException;
 
 /** 'Primitive' PV Access data type
  *   @author Kay Kasemir
@@ -89,6 +90,9 @@ public void encodeType(ByteBuffer buffer, BitSet described) throws Exception
     public void decode(final PVATypeRegistry types, final ByteBuffer buffer) throws Exception
     {
         final int size = PVASize.decodeSize(buffer);
+        // Each array element needs to contain at least a byte for the string size
+        if (size < 0  ||  size > buffer.remaining())
+            throw new PVAArraySizeException(size, buffer.remaining());
         String[] new_value = value;
         if (new_value == null  ||  new_value.length != size)
             new_value = new String[size];
diff --git a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
index 88bb0ac667..ea2d22ee27 100644
--- a/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
+++ b/core/pva/src/main/java/org/epics/pva/data/PVAStructure.java
@@ -7,6 +7,8 @@
  ******************************************************************************/
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.PVAProtocolException;
+
 import static org.epics.pva.PVASettings.logger;
 
 import java.nio.ByteBuffer;
@@ -55,6 +57,10 @@ static PVAStructure decodeType(final PVATypeRegistry types, final String name, f
         // number of elements
         final int size = PVASize.decodeSize(buffer);
 
+        // Each element needs name (at least byte for length) and value (at least byte)
+        if (size < 0  ||  size*2 > buffer.remaining())
+            throw new PVAProtocolException("Structure with " + size + " elements but only " + buffer.remaining() + " bytes in buffer");
+
         // (name, FieldDesc)[]
         final List values = new ArrayList<>(size);
         for (int i=0; i buffer.remaining())
+            throw new PVAProtocolException("Structure element count " + count + " with only " + buffer.remaining() + " bytes in buffer");
+
         // Try to re-use elements
         PVAStructure[] new_elements = elements;
         if (new_elements == null  ||  new_elements.length != count)
diff --git a/core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java b/core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java
similarity index 90%
rename from core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java
rename to core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java
index 3a3bcfe1e8..b983a6edd4 100644
--- a/core/pva/src/main/java/org/epics/pva/data/ElementTypeException.java
+++ b/core/pva/src/main/java/org/epics/pva/exceptions/ElementTypeException.java
@@ -17,13 +17,15 @@
  *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
  */
 
-package org.epics.pva.data;
+package org.epics.pva.exceptions;
+
+import org.epics.pva.data.PVAStructure;
 
 /**
  * Exception for when updating a PVAStructure array with a value that
  * includes a PVAStructure which does not match the element type of the array.
  */
-public class ElementTypeException extends Exception {
+public class ElementTypeException extends PVAProtocolException {
 
     /**
      * Constructor returns an exception with a message based on the
diff --git a/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java b/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java
new file mode 100644
index 0000000000..8bc6128a65
--- /dev/null
+++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAArraySizeException.java
@@ -0,0 +1,35 @@
+package org.epics.pva.exceptions;
+
+/** Protocol exception for malformed array payloads. */
+@SuppressWarnings("nls")
+public class PVAArraySizeException extends PVAProtocolException
+{
+    /**
+     * @param size Decoded array element count
+     * @param remaining Number of bytes left in buffer
+     */
+    public PVAArraySizeException(final int size, final int remaining)
+    {
+        this(size, remaining, 1);
+    }
+
+    /**
+     * @param size Decoded array element count
+     * @param remaining Number of bytes left in buffer
+     * @param bytesPerElement Number of bytes required per array element
+     */
+    public PVAArraySizeException(final int size, final int remaining, final int bytesPerElement)
+    {
+        super(createMessage(size, remaining, bytesPerElement));
+    }
+
+    private static String createMessage(final int size, final int remaining, final int bytesPerElement)
+    {
+        if (size < 0)
+            return "Negative array size " + size;
+
+        final long needed = (long) size * bytesPerElement;
+        return "Array size " + size + " needs " + needed + " bytes with element size " + bytesPerElement +
+               " but buffer has only " + remaining + " bytes";
+    }
+}
diff --git a/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
new file mode 100644
index 0000000000..bc73e8ffa0
--- /dev/null
+++ b/core/pva/src/main/java/org/epics/pva/exceptions/PVAProtocolException.java
@@ -0,0 +1,11 @@
+package org.epics.pva.exceptions;
+
+/** Protocol violation while decoding a PVA message. */
+@SuppressWarnings("nls")
+public class PVAProtocolException extends Exception
+{
+    public PVAProtocolException(final String message)
+    {
+        super(message);
+    }
+}
diff --git a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
index 64892be4b0..b478c91390 100644
--- a/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
+++ b/core/pva/src/test/java/org/epics/pva/data/BitSetTest.java
@@ -18,7 +18,7 @@
 public class BitSetTest
 {
     @Test
-    public void testBitSet()
+    public void testBitSet() throws Exception
     {
         final ByteBuffer buffer = ByteBuffer.allocate(100);
         BitSet bits = new BitSet();
@@ -68,4 +68,4 @@ public void testBitSet()
         final BitSet copy = PVABitSet.decodeBitSet(buffer);
         assertThat(copy, equalTo(bits));
     }
-}
\ No newline at end of file
+}
diff --git a/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java b/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
index 54ba39bfe1..12061ed29c 100644
--- a/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
+++ b/core/pva/src/test/java/org/epics/pva/data/PVAStructureArrayTest.java
@@ -19,6 +19,7 @@
 
 package org.epics.pva.data;
 
+import org.epics.pva.exceptions.ElementTypeException;
 import org.junit.jupiter.api.Test;
 
 import java.util.BitSet;
@@ -112,4 +113,4 @@ void set() throws Exception {
                 new PVAStructure("different", "diff"));
         assertThrows(ElementTypeException.class, () -> structureArray.set(diffTypeArray.get()));
     }
-}
\ No newline at end of file
+}