This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-net.git


The following commit(s) were added to refs/heads/master by this push:
     new ec2d274e TFTPRequestPacket now throws TFTPPacketException instead of 
ArrayIndexOutOfBoundsException (#407)
ec2d274e is described below

commit ec2d274e310a5a4c934fc60669a0f06d64330efc
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Aug 14 07:07:46 2026 -0400

    TFTPRequestPacket now throws TFTPPacketException instead of 
ArrayIndexOutOfBoundsException (#407)
    
    * TFTPRequestPacket now throws TFTPPacketException instead of
    ArrayIndexOutOfBoundsException.
    
    * Refactor packet type checking
    
    * Inline local variable declaration and initialization.
    
    * Internal refactoring.
    
    * Add org.apache.commons.net.tftp.TFTPPacketTest
---
 .../org/apache/commons/net/tftp/TFTPAckPacket.java |  4 +-
 .../apache/commons/net/tftp/TFTPDataPacket.java    |  4 +-
 .../apache/commons/net/tftp/TFTPErrorPacket.java   |  4 +-
 .../org/apache/commons/net/tftp/TFTPPacket.java    |  8 ++-
 .../apache/commons/net/tftp/TFTPRequestPacket.java | 73 ++++++++-------------
 .../apache/commons/net/tftp/TFTPAckPacketTest.java |  9 ++-
 .../commons/net/tftp/TFTPDataPacketTest.java       |  9 ++-
 .../commons/net/tftp/TFTPErrorPacketTest.java      | 10 ++-
 ...{TFTPAckPacketTest.java => TFTPPacketTest.java} | 27 ++++----
 .../net/tftp/TFTPReadRequestPacketTest.java        |  9 ++-
 .../tftp/TFTPRequestPacketOptionBoundsTest.java    | 75 ++++++++++++++++++++++
 .../net/tftp/TFTPWriteRequestPacketTest.java       | 14 ++--
 12 files changed, 169 insertions(+), 77 deletions(-)

diff --git a/src/main/java/org/apache/commons/net/tftp/TFTPAckPacket.java 
b/src/main/java/org/apache/commons/net/tftp/TFTPAckPacket.java
index 2e1ca54c..6596f2e1 100644
--- a/src/main/java/org/apache/commons/net/tftp/TFTPAckPacket.java
+++ b/src/main/java/org/apache/commons/net/tftp/TFTPAckPacket.java
@@ -52,9 +52,7 @@ public final class TFTPAckPacket extends TFTPPacket {
 
         data = datagram.getData();
 
-        if (getType() != data[1]) {
-            throw new TFTPPacketException("TFTP operator code does not match 
type.");
-        }
+        checkType(data);
 
         this.blockNumber = (data[2] & 0xff) << 8 | data[3] & 0xff;
     }
diff --git a/src/main/java/org/apache/commons/net/tftp/TFTPDataPacket.java 
b/src/main/java/org/apache/commons/net/tftp/TFTPDataPacket.java
index 8f97dcb9..07acf827 100644
--- a/src/main/java/org/apache/commons/net/tftp/TFTPDataPacket.java
+++ b/src/main/java/org/apache/commons/net/tftp/TFTPDataPacket.java
@@ -66,9 +66,7 @@ public final class TFTPDataPacket extends TFTPPacket {
         this.data = datagram.getData();
         this.offset = 4;
 
-        if (getType() != this.data[1]) {
-            throw new TFTPPacketException("TFTP operator code does not match 
type.");
-        }
+        checkType(data);
 
         this.blockNumber = (this.data[2] & 0xff) << 8 | this.data[3] & 0xff;
 
diff --git a/src/main/java/org/apache/commons/net/tftp/TFTPErrorPacket.java 
b/src/main/java/org/apache/commons/net/tftp/TFTPErrorPacket.java
index c2586627..3b568c1c 100644
--- a/src/main/java/org/apache/commons/net/tftp/TFTPErrorPacket.java
+++ b/src/main/java/org/apache/commons/net/tftp/TFTPErrorPacket.java
@@ -92,9 +92,7 @@ public final class TFTPErrorPacket extends TFTPPacket {
         data = datagram.getData();
         length = datagram.getLength();
 
-        if (getType() != data[1]) {
-            throw new TFTPPacketException("TFTP operator code does not match 
type.");
-        }
+        checkType(data);
 
         error = (data[2] & 0xff) << 8 | data[3] & 0xff;
 
diff --git a/src/main/java/org/apache/commons/net/tftp/TFTPPacket.java 
b/src/main/java/org/apache/commons/net/tftp/TFTPPacket.java
index c930f427..2ad76be0 100644
--- a/src/main/java/org/apache/commons/net/tftp/TFTPPacket.java
+++ b/src/main/java/org/apache/commons/net/tftp/TFTPPacket.java
@@ -111,7 +111,7 @@ public abstract class TFTPPacket {
             packet = new TFTPErrorPacket(datagram);
             break;
         default:
-            throw new TFTPPacketException("Bad packet.  Invalid TFTP operator 
code.");
+            throw new TFTPPacketException("Bad packet. Invalid TFTP operator 
code.");
         }
         return packet;
     }
@@ -138,6 +138,12 @@ public abstract class TFTPPacket {
         this.port = port;
     }
 
+    void checkType(final byte[] data) throws TFTPPacketException {
+        if (getType() != data[1]) {
+            throw new TFTPPacketException("TFTP operator code does not match 
type.");
+        }
+    }
+
     /**
      * Gets the address of the host where the packet is going to be sent or 
where it came from.
      *
diff --git a/src/main/java/org/apache/commons/net/tftp/TFTPRequestPacket.java 
b/src/main/java/org/apache/commons/net/tftp/TFTPRequestPacket.java
index 9a7383b4..f3dec654 100644
--- a/src/main/java/org/apache/commons/net/tftp/TFTPRequestPacket.java
+++ b/src/main/java/org/apache/commons/net/tftp/TFTPRequestPacket.java
@@ -90,70 +90,57 @@ public abstract class TFTPRequestPacket extends TFTPPacket {
      */
     TFTPRequestPacket(final int type, final DatagramPacket datagram) throws 
TFTPPacketException {
         super(type, datagram.getAddress(), datagram.getPort());
-
         final byte[] data = datagram.getData();
-
-        if (getType() != data[1]) {
-            throw new TFTPPacketException("TFTP operator code does not match 
type.");
-        }
-
+        final int dataLen = datagram.getLength();
+        checkType(data);
         final StringBuilder buffer = new StringBuilder();
-
         int index = 2;
-        final int length = datagram.getLength();
-
-        while (index < length && data[index] != 0) {
+        while (isChar(data, index)) {
             buffer.append((char) data[index]);
             ++index;
         }
-
         this.fileName = buffer.toString();
-
-        if (index >= length) {
+        if (index >= dataLen) {
             throw new TFTPPacketException("Bad file name and mode format.");
         }
-
         buffer.setLength(0);
         ++index; // need to advance beyond the end of string marker
-        while (index < length && data[index] != 0) {
+        while (isChar(data, index)) {
             buffer.append((char) data[index]);
             ++index;
         }
-
         final String modeString = 
buffer.toString().toLowerCase(Locale.ENGLISH);
-        final int modeStringsLength = modeStrings.length;
-
+        final int modeStringsLen = modeStrings.length;
         int mode = 0;
         int modeIndex;
-        for (modeIndex = 0; modeIndex < modeStringsLength; modeIndex++) {
+        for (modeIndex = 0; modeIndex < modeStringsLen; modeIndex++) {
             if (modeString.equals(modeStrings[modeIndex])) {
                 mode = modeIndex;
                 break;
             }
         }
-
         this.mode = mode;
-
-        if (modeIndex >= modeStringsLength) {
+        if (modeIndex >= modeStringsLen) {
             throw new TFTPPacketException("Unrecognized TFTP transfer mode: " 
+ modeString);
             // May just want to default to binary mode instead of throwing
             // exception.
-            // _mode = TFTP.OCTET_MODE;
+            // mode = TFTP.OCTET_MODE;
         }
-
         ++index;
-        while (index < length) {
+        while (index < dataLen) {
             int start = index;
-            for (; data[index] != 0; ++index) {
-                if (index >= length) {
+            while (isChar(data, index)) {
+                index++;
+                if (index >= dataLen) {
                     throw new TFTPPacketException("Invalid option format");
                 }
             }
             final String option = new String(data, start, index - start, 
StandardCharsets.US_ASCII);
             ++index;
             start = index;
-            for (; data[index] != 0; ++index) {
-                if (index >= length) {
+            while (isChar(data, index)) {
+                index++;
+                if (index >= dataLen) {
                     throw new TFTPPacketException("Invalid option format");
                 }
             }
@@ -208,6 +195,10 @@ public abstract class TFTPRequestPacket extends TFTPPacket 
{
         }
     }
 
+    private boolean isChar(final byte[] data, int index) {
+        return index < data.length && data[index] != 0;
+    }
+
     /**
      * Creates a UDP datagram containing all the TFTP request packet data in 
the proper format. This is a method exposed to the programmer in case he wants 
to
      * implement his own TFTP client instead of using the {@link 
org.apache.commons.net.tftp.TFTPClient} class. Under normal circumstances, you 
should not have
@@ -217,28 +208,21 @@ public abstract class TFTPRequestPacket extends 
TFTPPacket {
      */
     @Override
     public final DatagramPacket newDatagram() {
-        final int fileLength;
-        final int modeLength;
-        final byte[] data;
-
-        fileLength = fileName.length();
-        modeLength = modeBytes[mode].length;
-
+        final int fileLength = fileName.length();
+        final int modeLength = modeBytes[mode].length;
         int optionsLength = 0;
         for (final Map.Entry<String, String> entry : options.entrySet()) {
             optionsLength += entry.getKey().length() + 1 + 
entry.getValue().length() + 1;
         }
-        data = new byte[fileLength + modeLength + 3 + optionsLength];
+        final byte[] data = new byte[fileLength + modeLength + 3 + 
optionsLength];
         data[0] = 0;
         data[1] = (byte) type;
         System.arraycopy(fileName.getBytes(Charset.defaultCharset()), 0, data, 
2, fileLength);
         data[fileLength + 2] = 0;
         System.arraycopy(modeBytes[mode], 0, data, fileLength + 3, modeLength);
-
         if (optionsLength > 0) {
             handleOptions(data, fileLength, modeLength);
         }
-
         return new DatagramPacket(data, data.length, address, port);
     }
 
@@ -252,25 +236,18 @@ public abstract class TFTPRequestPacket extends 
TFTPPacket {
      */
     @Override
     final DatagramPacket newDatagram(final DatagramPacket datagram, final 
byte[] data) {
-        final int fileLength;
-        final int modeLength;
-
-        fileLength = fileName.length();
-        modeLength = modeBytes[mode].length;
-
+        final int fileLength = fileName.length();
+        final int modeLength = modeBytes[mode].length;
         data[0] = 0;
         data[1] = (byte) type;
         System.arraycopy(fileName.getBytes(Charset.defaultCharset()), 0, data, 
2, fileLength);
         data[fileLength + 2] = 0;
         System.arraycopy(modeBytes[mode], 0, data, fileLength + 3, modeLength);
-
         handleOptions(data, fileLength, modeLength);
-
         datagram.setAddress(address);
         datagram.setPort(port);
         datagram.setData(data);
         datagram.setLength(fileLength + modeLength + 3);
-
         return datagram;
     }
 }
diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java
index b4eaedd3..f131d070 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java
@@ -19,15 +19,22 @@ package org.apache.commons.net.tftp;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 /**
  * Tests {@link TFTPAckPacket}.
  */
-class TFTPAckPacketTest {
+class TFTPAckPacketTest extends TFTPPacketTest {
+
+    @Override
+    protected Executable getDatagramPacketCtor(final DatagramPacket packet) {
+        return () -> new TFTPAckPacket(packet);
+    }
 
     @Test
     void testNewDatagram() throws UnknownHostException {
diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPDataPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPDataPacketTest.java
index 76c758a3..12e777a0 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPDataPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPDataPacketTest.java
@@ -19,15 +19,22 @@ package org.apache.commons.net.tftp;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 /**
  * Tests {@link TFTPDataPacket}.
  */
-class TFTPDataPacketTest {
+class TFTPDataPacketTest extends TFTPPacketTest {
+
+    @Override
+    protected Executable getDatagramPacketCtor(final DatagramPacket packet) {
+        return () -> new TFTPDataPacket(packet);
+    }
 
     @Test
     void testNewDatagram() throws UnknownHostException {
diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPErrorPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPErrorPacketTest.java
index 6183cb7d..6b339fc0 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPErrorPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPErrorPacketTest.java
@@ -19,15 +19,22 @@ package org.apache.commons.net.tftp;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 /**
  * Tests {@link TFTPErrorPacket}.
  */
-class TFTPErrorPacketTest {
+class TFTPErrorPacketTest extends TFTPPacketTest {
+
+    @Override
+    protected Executable getDatagramPacketCtor(final DatagramPacket packet) {
+        return () -> new TFTPErrorPacket(packet);
+    }
 
     @Test
     void testNewDatagram() throws UnknownHostException {
@@ -38,4 +45,5 @@ class TFTPErrorPacketTest {
     void testToString() throws UnknownHostException {
         assertNotNull(new TFTPErrorPacket(InetAddress.getLocalHost(), 0, 0, 
"").toString());
     }
+
 }
diff --git a/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPPacketTest.java
similarity index 50%
copy from src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java
copy to src/test/java/org/apache/commons/net/tftp/TFTPPacketTest.java
index b4eaedd3..4f52a5fb 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPAckPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPPacketTest.java
@@ -17,25 +17,30 @@
 
 package org.apache.commons.net.tftp;
 
-import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
-/**
- * Tests {@link TFTPAckPacket}.
- */
-class TFTPAckPacketTest {
+abstract class TFTPPacketTest {
 
-    @Test
-    void testNewDatagram() throws UnknownHostException {
-        assertNotNull(new TFTPAckPacket(InetAddress.getLocalHost(), 0, 
0).newDatagram());
-    }
+    protected abstract Executable getDatagramPacketCtor(DatagramPacket packet);
 
     @Test
-    void testToString() throws UnknownHostException {
-        assertNotNull(new TFTPAckPacket(InetAddress.getLocalHost(), 0, 
0).toString());
+    public void testConstructorBadType() throws UnknownHostException {
+        // Create a DatagramPacket with invalid TFTP packet type (not ACK)
+        final InetAddress address = InetAddress.getLocalHost();
+        final byte[] data = new byte[4];
+        data[0] = 0; // TFTP opcode 0 (invalid)
+        data[1] = 0; // TFTP opcode 0 (invalid)
+        data[2] = 0; // Block number high byte
+        data[3] = 1; // Block number low byte
+        final DatagramPacket packet = new DatagramPacket(data, data.length, 
address, 69);
+        assertThrows(TFTPPacketException.class, () -> 
TFTPPacket.newTFTPPacket(packet));
+        assertThrows(TFTPPacketException.class, getDatagramPacketCtor(packet));
     }
 }
diff --git 
a/src/test/java/org/apache/commons/net/tftp/TFTPReadRequestPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPReadRequestPacketTest.java
index c41e7264..93094574 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPReadRequestPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPReadRequestPacketTest.java
@@ -19,15 +19,22 @@ package org.apache.commons.net.tftp;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 /**
  * Tests {@link TFTPReadRequestPacket}.
  */
-class TFTPReadRequestPacketTest {
+class TFTPReadRequestPacketTest extends TFTPPacketTest {
+
+    @Override
+    protected Executable getDatagramPacketCtor(final DatagramPacket packet) {
+        return () -> new TFTPReadRequestPacket(packet);
+    }
 
     @Test
     void testToString() throws UnknownHostException {
diff --git 
a/src/test/java/org/apache/commons/net/tftp/TFTPRequestPacketOptionBoundsTest.java
 
b/src/test/java/org/apache/commons/net/tftp/TFTPRequestPacketOptionBoundsTest.java
new file mode 100644
index 00000000..8a57f089
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/net/tftp/TFTPRequestPacketOptionBoundsTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.commons.net.tftp;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link TFTPRequestPacket}.
+ */
+class TFTPRequestPacketOptionBoundsTest {
+
+    /**
+     * RRQ for "f" in octet mode, one option whose value has no terminating 
NUL.
+     */
+    private static byte[] newReadRequest() throws IOException {
+        final ByteArrayOutputStream out = new ByteArrayOutputStream();
+        out.write(0);
+        out.write(TFTPPacket.READ_REQUEST);
+        out.write("f".getBytes(StandardCharsets.US_ASCII));
+        out.write(0);
+        out.write("octet".getBytes(StandardCharsets.US_ASCII));
+        out.write(0);
+        out.write("blksize".getBytes(StandardCharsets.US_ASCII));
+        out.write(0);
+        out.write("1024".getBytes(StandardCharsets.US_ASCII));
+        return out.toByteArray();
+    }
+
+    private static String parse(final byte[] buf, final int len) throws 
TFTPPacketException {
+        final DatagramPacket packet = new DatagramPacket(buf, len, 
InetAddress.getLoopbackAddress(), 69);
+        return "OK " + ((TFTPRequestPacket) 
TFTPPacket.newTFTPPacket(packet)).getOptions();
+    }
+
+    @Test
+    void testParseIsIndependentOfBytesBeyondGetLength() throws Exception {
+        final byte[] request = newReadRequest();
+        final byte[] zeroed = Arrays.copyOf(request, request.length + 16);
+        final byte[] stale = Arrays.copyOf(request, request.length + 16);
+        Arrays.fill(stale, request.length, stale.length, (byte) 'S');
+        assertThrowsExactly(TFTPPacketException.class, () -> parse(zeroed, 
request.length));
+        assertThrowsExactly(TFTPPacketException.class, () -> parse(stale, 
request.length));
+    }
+
+    @Test
+    void testUnterminatedOptionAtEndOfBufferThrowsDeclaredException() throws 
Exception {
+        final byte[] request = newReadRequest();
+        final DatagramPacket packet = new DatagramPacket(request, 
request.length, InetAddress.getLoopbackAddress(), 69);
+        assertThrows(TFTPPacketException.class, () -> 
TFTPPacket.newTFTPPacket(packet));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/net/tftp/TFTPWriteRequestPacketTest.java 
b/src/test/java/org/apache/commons/net/tftp/TFTPWriteRequestPacketTest.java
index 50796719..014cb386 100644
--- a/src/test/java/org/apache/commons/net/tftp/TFTPWriteRequestPacketTest.java
+++ b/src/test/java/org/apache/commons/net/tftp/TFTPWriteRequestPacketTest.java
@@ -19,19 +19,25 @@ package org.apache.commons.net.tftp;
 
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
+import java.net.DatagramPacket;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
 
 /**
- * Tests {@link TFTPReadRequestPacket}.
+ * Tests {@link TFTPWriteRequestPacket}.
  */
-class TFTPWriteRequestPacketTest {
+class TFTPWriteRequestPacketTest extends TFTPPacketTest {
+
+    @Override
+    protected Executable getDatagramPacketCtor(final DatagramPacket packet) {
+        return () -> new TFTPWriteRequestPacket(packet);
+    }
 
     @Test
     void testToString() throws UnknownHostException {
-        assertNotNull(new TFTPReadRequestPacket(InetAddress.getLocalHost(), 0, 
"", 0).toString());
+        assertNotNull(new TFTPWriteRequestPacket(InetAddress.getLocalHost(), 
0, "", 0).toString());
     }
-
 }

Reply via email to