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

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

commit 140eaecb1766c445fdfe124fbb344d6e29e407a6
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Mon May 27 15:08:50 2024 -0400

    Base 64 Encoding with URL and Filename Safe Alphabet should not chunk
    per RFC 4648
---
 pom.xml                                            |  2 +-
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/commons/net/util/Base64.java   | 74 +++++++++++-----------
 .../org/apache/commons/net/util/Base64Test.java    | 57 ++++++++++++++---
 4 files changed, 89 insertions(+), 45 deletions(-)

diff --git a/pom.xml b/pom.xml
index 131c1313..39799983 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,8 +71,8 @@ Supported protocols include Echo, Finger, FTP, NNTP, NTP, 
POP3(S), SMTP(S), Teln
         <commons.net.trace_calls>false</commons.net.trace_calls>
         <commons.net.add_listener>false</commons.net.add_listener>
         <!-- JaCoCo: Don't make code coverage worse than: -->
-        <jacoco.skip>false</jacoco.skip>
         <commons.jacoco.haltOnFailure>true</commons.jacoco.haltOnFailure>
+        <jacoco.skip>false</jacoco.skip>
         <!-- TODO Improve dismal coverage -->
         <commons.jacoco.classRatio>0.47</commons.jacoco.classRatio>
         <commons.jacoco.instructionRatio>0.41</commons.jacoco.instructionRatio>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 8f1e4772..97501f01 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -71,6 +71,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Guard against 
polynomial regular expression used on uncontrolled data in 
IMAPReply.TAGGED_RESPONSE.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Guard against 
polynomial regular expression used on uncontrolled data in 
IMAPReply.UNTAGGED_RESPONSE.</action>
       <action type="fix" issue="NET-730" dev="ggregory" due-to="Johannes 
Thalmair, Gary Gregory">Cannot connect to FTP server with HTTP proxy.</action>
+      <action type="fix" dev="ggregory" due-to="Gary Gregory">Base 64 Encoding 
with URL and Filename Safe Alphabet should not chunk per RFC 4648.</action>
       <!-- ADD -->
       <action type="add" issue="NET-726" dev="ggregory" due-to="PJ Fanning, 
Gary Gregory">Add protected getters to FTPSClient #204.</action>
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
SubnetUtils.toString().</action> 
diff --git a/src/main/java/org/apache/commons/net/util/Base64.java 
b/src/main/java/org/apache/commons/net/util/Base64.java
index aa1730c4..e9bd97f5 100644
--- a/src/main/java/org/apache/commons/net/util/Base64.java
+++ b/src/main/java/org/apache/commons/net/util/Base64.java
@@ -53,8 +53,6 @@ public class Base64 {
 
     private static final int DEFAULT_BUFFER_SIZE = 8192;
 
-    private static final Base64 DECODER = new Base64();
-
     /**
      * Chunk size per RFC 2045 section 6.8.
      *
@@ -135,22 +133,22 @@ public class Base64 {
     /**
      * Decodes Base64 data into octets.
      *
-     * @param base64Data Byte array containing Base64 data
+     * @param base64 Byte array containing Base64 data
      * @return Array containing decoded data.
      */
-    public static byte[] decodeBase64(final byte[] base64Data) {
-        return DECODER.decode(base64Data);
+    public static byte[] decodeBase64(final byte[] base64) {
+        return isEmpty(base64) ? base64 : getDecoder().decode(base64);
     }
 
     /**
      * Decodes a Base64 String into octets.
      *
-     * @param base64String String containing Base64 data
+     * @param base64 String containing Base64 data
      * @return Array containing decoded data.
      * @since 1.4
      */
-    public static byte[] decodeBase64(final String base64String) {
-        return DECODER.decode(base64String);
+    public static byte[] decodeBase64(final String base64) {
+        return getDecoder().decode(base64);
     }
 
     /**
@@ -167,61 +165,60 @@ public class Base64 {
     /**
      * Encodes binary data using the base64 algorithm but does not chunk the 
output.
      *
-     * @param binaryData binary data to encode
+     * @param source binary data to encode
      * @return byte[] containing Base64 characters in their UTF-8 
representation.
      */
-    public static byte[] encodeBase64(final byte[] binaryData) {
-        return encodeBase64(binaryData, false);
+    public static byte[] encodeBase64(final byte[] source) {
+        return isEmpty(source) ? source : getEncoder().encode(source);
     }
 
     /**
      * Encodes binary data using the base64 algorithm, optionally chunking the 
output into 76 character blocks.
      *
      * @param binaryData Array containing binary data to encode.
-     * @param isChunked  if {@code true} this encoder will chunk the base64 
output into 76 character blocks
+     * @param chunked  if {@code true} this encoder will chunk the base64 
output into 76 character blocks
      * @return Base64-encoded data.
      * @throws IllegalArgumentException Thrown when the input array needs an 
output array bigger than {@link Integer#MAX_VALUE}
      */
-    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
isChunked) {
-        return encodeBase64(binaryData, isChunked, false);
+    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
chunked) {
+        return chunked ? encodeBase64Chunked(binaryData) : 
encodeBase64(binaryData, false, false);
     }
 
     /**
      * Encodes binary data using the base64 algorithm, optionally chunking the 
output into 76 character blocks.
      *
      * @param binaryData Array containing binary data to encode.
-     * @param isChunked  if {@code true} this encoder will chunk the base64 
output into 76 character blocks
+     * @param chunked  if {@code true} this encoder will chunk the base64 
output into 76 character blocks
      * @param urlSafe    if {@code true} this encoder will emit - and _ 
instead of the usual + and / characters.
      * @return Base64-encoded data.
      * @throws IllegalArgumentException Thrown when the input array needs an 
output array bigger than {@link Integer#MAX_VALUE}
      * @since 1.4
      */
-    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
isChunked, final boolean urlSafe) {
-        return encodeBase64(binaryData, isChunked, urlSafe, Integer.MAX_VALUE);
+    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
chunked, final boolean urlSafe) {
+        return encodeBase64(binaryData, chunked, urlSafe, Integer.MAX_VALUE);
     }
 
     /**
      * Encodes binary data using the base64 algorithm, optionally chunking the 
output into 76 character blocks.
      *
      * @param binaryData    Array containing binary data to encode.
-     * @param isChunked     if {@code true} this encoder will chunk the base64 
output into 76 character blocks
+     * @param chunked     if {@code true} this encoder will chunk the base64 
output into 76 character blocks
      * @param urlSafe       if {@code true} this encoder will emit - and _ 
instead of the usual + and / characters.
      * @param maxResultSize The maximum result size to accept.
      * @return Base64-encoded data.
      * @throws IllegalArgumentException Thrown when the input array needs an 
output array bigger than maxResultSize
      * @since 1.4
      */
-    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
isChunked, final boolean urlSafe, final int maxResultSize) {
-        if (binaryData == null || binaryData.length == 0) {
+    public static byte[] encodeBase64(final byte[] binaryData, final boolean 
chunked, final boolean urlSafe, final int maxResultSize) {
+        if (isEmpty(binaryData)) {
             return binaryData;
         }
-        final long len = getEncodeLength(binaryData, isChunked ? CHUNK_SIZE : 
0, isChunked ? CHUNK_SEPARATOR : NetConstants.EMPTY_BTYE_ARRAY);
+        final long len = getEncodeLength(binaryData, chunked ? CHUNK_SIZE : 0, 
chunked ? CHUNK_SEPARATOR : NetConstants.EMPTY_BTYE_ARRAY);
         if (len > maxResultSize) {
             throw new IllegalArgumentException(
                     "Input array too big, the output array would be bigger (" 
+ len + ") than the specified maxium size of " + maxResultSize);
         }
-        final Base64 b64 = isChunked ? new Base64(urlSafe) : new Base64(0, 
CHUNK_SEPARATOR, urlSafe);
-        return b64.encode(binaryData);
+        return chunked ? encodeBase64Chunked(binaryData) : urlSafe ? 
encodeBase64URLSafe(binaryData) : encodeBase64(binaryData);
     }
 
     /**
@@ -264,12 +261,12 @@ public class Base64 {
      * Encodes binary data using the base64 algorithm.
      *
      * @param binaryData  binary data to encode
-     * @param useChunking whether to split the output into chunks
+     * @param chunked whether to split the output into chunks
      * @return String containing Base64 characters.
      * @since 3.2
      */
-    public static String encodeBase64String(final byte[] binaryData, final 
boolean useChunking) {
-        return newStringUtf8(encodeBase64(binaryData, useChunking));
+    public static String encodeBase64String(final byte[] binaryData, final 
boolean chunked) {
+        return newStringUtf8(encodeBase64(binaryData, chunked));
     }
 
     /**
@@ -295,7 +292,7 @@ public class Base64 {
      * @since 1.4
      */
     public static byte[] encodeBase64URLSafe(final byte[] binaryData) {
-        return encodeBase64(binaryData, false, true);
+        return getUrlEncoder().withoutPadding().encode(binaryData);
     }
 
     /**
@@ -307,7 +304,7 @@ public class Base64 {
      * @since 1.4
      */
     public static String encodeBase64URLSafeString(final byte[] binaryData) {
-        return newStringUtf8(encodeBase64(binaryData, false, true));
+        return getUrlEncoder().withoutPadding().encodeToString(binaryData);
     }
 
     /**
@@ -361,6 +358,10 @@ public class Base64 {
         return java.util.Base64.getMimeEncoder();
     }
 
+    private static Encoder getUrlEncoder() {
+        return java.util.Base64.getUrlEncoder();
+    }
+
     /**
      * Tests a given byte array to see if it contains only valid characters 
within the Base64 alphabet. Currently, the method treats whitespace as valid.
      *
@@ -387,6 +388,10 @@ public class Base64 {
         return octet == PAD || octet >= 0 && octet < DECODE_TABLE.length && 
DECODE_TABLE[octet] != -1;
     }
 
+    private static boolean isEmpty(final byte[] array) {
+        return array == null || array.length == 0;
+    }
+
     /**
      * Checks if a byte value is whitespace or not.
      *
@@ -592,7 +597,7 @@ public class Base64 {
      * @since 1.4
      */
     public Base64(int lineLength, byte[] lineSeparator, final boolean urlSafe) 
{
-        if (lineSeparator == null) {
+        if (lineSeparator == null || urlSafe) {
             lineLength = 0; // disable chunk-separating
             lineSeparator = NetConstants.EMPTY_BTYE_ARRAY; // this just gets 
ignored
         }
@@ -628,10 +633,7 @@ public class Base64 {
      */
     public byte[] decode(final byte[] source) {
         reset();
-        if (source == null || source.length == 0) {
-            return source;
-        }
-        return getDecoder().decode(source);
+        return isEmpty(source) ? source : getDecoder().decode(source);
     }
 
     /**
@@ -653,7 +655,7 @@ public class Base64 {
      */
     public byte[] encode(final byte[] source) {
         reset();
-        if (source == null || source.length == 0) {
+        if (isEmpty(source)) {
             return source;
         }
         final long len = getEncodeLength(source, lineLength, lineSeparator);
@@ -795,7 +797,7 @@ public class Base64 {
      * @param bAvail amount of bytes we're allowed to extract. We may extract 
fewer (if fewer are available).
      * @return The number of bytes successfully extracted into the provided 
byte[] array.
      */
-    int readResults(final byte[] b, final int bPos, final int bAvail) {
+    private int readResults(final byte[] b, final int bPos, final int bAvail) {
         if (buffer != null) {
             final int len = Math.min(avail(), bAvail);
             if (buffer != b) {
@@ -849,7 +851,7 @@ public class Base64 {
      * @param outPos   Position to start buffering into.
      * @param outAvail Amount of bytes available for direct buffering.
      */
-    void setInitialBuffer(final byte[] out, final int outPos, final int 
outAvail) {
+    private void setInitialBuffer(final byte[] out, final int outPos, final 
int outAvail) {
         // We can re-use consumer's original output array under
         // special circumstances, saving on some System.arraycopy().
         if (out != null && out.length == outAvail) {
diff --git a/src/test/java/org/apache/commons/net/util/Base64Test.java 
b/src/test/java/org/apache/commons/net/util/Base64Test.java
index 45588677..dfc89695 100644
--- a/src/test/java/org/apache/commons/net/util/Base64Test.java
+++ b/src/test/java/org/apache/commons/net/util/Base64Test.java
@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
@@ -37,17 +38,23 @@ import org.junit.Test;
 @SuppressWarnings({ "deprecation" })
 public class Base64Test {
 
+    private static String toString(final byte[] encodedData) {
+        return encodedData != null ? new String(encodedData, 
StandardCharsets.UTF_8) : null;
+    }
+
     private void checkDecoders(final String expected, final byte[] actual) {
         final byte[] decoded = Base64.decodeBase64(actual);
         assertEquals(expected, toString(decoded));
-        assertEquals(expected, new String(getJreDecoder().decode(actual), 
StandardCharsets.UTF_8));
+        assertEquals(expected, toString(actual != null ? 
getJreDecoder().decode(actual) : null));
+        assertEquals(expected, toString(new Base64().decode(actual)));
     }
 
     private void checkDecoders(final String expected, final String actual) {
         final byte[] decoded = Base64.decodeBase64(actual);
         assertEquals(expected, new String(decoded));
         assertEquals(expected, toString(decoded));
-        assertEquals(expected, new String(getJreDecoder().decode(actual), 
StandardCharsets.UTF_8));
+        assertEquals(expected, toString(actual != null ? 
getJreDecoder().decode(actual) : null));
+        assertEquals(expected, toString(new Base64().decode(actual)));
     }
 
     private Decoder getJreDecoder() {
@@ -62,6 +69,10 @@ public class Base64Test {
         return java.util.Base64.getMimeEncoder();
     }
 
+    private Encoder getJreUrlEncoder() {
+        return java.util.Base64.getUrlEncoder();
+    }
+
     @Test
     public void testBase64() {
         final Base64 b64 = new Base64();
@@ -72,7 +83,7 @@ public class Base64Test {
     public void testBase64Boolean() {
         final Base64 b64 = new Base64(true);
         assertTrue(b64.isUrlSafe());
-        assertArrayEquals(new byte[] { '\r', '\n' }, b64.getLineSeparator());
+        assertArrayEquals(new byte[] {}, b64.getLineSeparator());
     }
 
     @Test
@@ -120,6 +131,7 @@ public class Base64Test {
     @Test
     public void testDecodeByteArrayEmpty() {
         checkDecoders("", new byte[] {});
+        checkDecoders(null, (byte[]) null);
 
     }
 
@@ -242,7 +254,7 @@ public class Base64Test {
     public void testEncodeBase64ByteArrayEdges() {
         final byte[] binaryData = null;
         assertArrayEquals(binaryData, Base64.encodeBase64(binaryData));
-        final byte[] binaryData2 = new byte[0];
+        final byte[] binaryData2 = {};
         assertArrayEquals(binaryData2, Base64.encodeBase64(binaryData2));
     }
 
@@ -317,6 +329,22 @@ public class Base64Test {
         bytesToEncode = "<<???>>".getBytes();
         encodedData = Base64.encodeBase64URLSafe(bytesToEncode);
         assertEquals("PDw_Pz8-Pg", toString(encodedData));
+        // > 76 line length
+        bytesToEncode = 
"<<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>>".getBytes();
+        encodedData = Base64.encodeBase64URLSafe(bytesToEncode);
+        
assertEquals(getJreUrlEncoder().withoutPadding().encodeToString(bytesToEncode), 
toString(encodedData));
+        final String encodedUrlSafe = 
"PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj4";
+        assertEquals(encodedUrlSafe, toString(encodedData));
+        // instance
+        assertEquals(encodedUrlSafe, toString(new Base64(0, null, 
true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(1, null, 
true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(999, null, 
true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(0, new byte[0], 
true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(0, new byte[10], 
true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(0, 
Base64.CHUNK_SEPARATOR, true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(Base64.CHUNK_SIZE, 
Base64.CHUNK_SEPARATOR, true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new Base64(999, 
Base64.CHUNK_SEPARATOR, true).encode(bytesToEncode)));
+        assertEquals(encodedUrlSafe, toString(new 
Base64(true).encode(bytesToEncode)));
     }
 
     @Test
@@ -329,6 +357,11 @@ public class Base64Test {
         bytesToEncode = "<<???>>".getBytes();
         encodedData = Base64.encodeBase64URLSafeString(bytesToEncode);
         assertEquals("PDw_Pz8-Pg", encodedData);
+        // > 76 line length
+        bytesToEncode = 
"<<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>>".getBytes();
+        encodedData = Base64.encodeBase64URLSafeString(bytesToEncode);
+        
assertEquals(getJreUrlEncoder().withoutPadding().encodeToString(bytesToEncode), 
encodedData);
+        
assertEquals("PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj48PD8_Pz4-PDw_Pz8-Pjw8Pz8_Pj4",
 encodedData);
     }
 
     @Test
@@ -338,6 +371,18 @@ public class Base64Test {
         assertEquals("bGlnaHQgd29y\r\n", new 
String(base64.encode(bytesToEncode), StandardCharsets.UTF_8));
     }
 
+    @Test
+    public void testEncodeByteArrayEmpty() {
+        assertNull(new Base64().encode((byte[]) null));
+        final byte[] empty = {};
+        assertSame(empty, new Base64().encode(empty));
+    }
+
+    @Test
+    public void testEncodeByteArrayNull() {
+        assertNull(new Base64().encode((byte[]) null));
+    }
+
     @Test
     public void testEncodeInteger() {
         testEncodeInteger(BigInteger.ONE);
@@ -370,8 +415,4 @@ public class Base64Test {
         assertFalse(Base64.isBase64((byte) ' '));
     }
 
-    private String toString(byte[] encodedData) {
-        return new String(encodedData, StandardCharsets.UTF_8);
-    }
-
 }

Reply via email to