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-codec.git

commit 25c95b1b9863973283600bc8f980f4bf92e9829f
Author: Gary Gregory <garydgreg...@gmail.com>
AuthorDate: Tue Apr 16 09:40:26 2024 -0400

    Add Base64.Builder (allows custom alphabets)
---
 src/changes/changes.xml                            |   3 +-
 .../org/apache/commons/codec/binary/Base64.java    | 135 ++++++++++++++++-----
 .../apache/commons/codec/binary/Base64Test.java    |  47 +++++--
 3 files changed, 149 insertions(+), 36 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 8dc5fb1c..a609aaee 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -46,7 +46,8 @@ The <action> type attribute can be add,update,fix,remove.
     <release version="1.17.0" date="YYYY-MM-DD" description="Feature and fix 
release. Requires a minimum of Java 8.">
       <!-- ADD -->
       <action type="add" dev="ggregory" due-to="Gary Gregory">Add override 
org.apache.commons.codec.language.bm.Rule.PhonemeExpr.size().</action>
-      <action type="add" dev="ggregory" due-to="Chris Kocel, Gary 
Gregory">Allow custom alphabets for Base64 #266.</action> 
+      <action type="add" dev="ggregory" due-to="Chris Kocel, Gary Gregory">Add 
support for Base64 custom alphabets #266.</action>
+      <action type="add" dev="ggregory" due-to="Gary Gregory">Add 
Base64.Builder (allows custom alphabets).</action> 
       <!-- FIX -->
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Optimize memory 
allocation in PhoneticEngine.</action>
       <action type="fix" dev="ggregory" due-to="Gary Gregory">BCodec and 
QCodec encode() methods throw UnsupportedCharsetException instead of 
EncoderException.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base64.java 
b/src/main/java/org/apache/commons/codec/binary/Base64.java
index 775a75a7..76db3ddb 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base64.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base64.java
@@ -20,6 +20,7 @@ package org.apache.commons.codec.binary;
 import java.math.BigInteger;
 import java.util.Arrays;
 import java.util.Objects;
+import java.util.function.Supplier;
 
 import org.apache.commons.codec.CodecPolicy;
 
@@ -56,6 +57,80 @@ import org.apache.commons.codec.CodecPolicy;
  */
 public class Base64 extends BaseNCodec {
 
+    /**
+     * Builds {@link Base64} instances.
+     *
+     * @since 1.17.0
+     */
+    public static class Builder implements Supplier<Base64> {
+
+        private int lineLength;
+        private byte[] lineSeparator = CHUNK_SEPARATOR;
+        private byte[] encodeTable = STANDARD_ENCODE_TABLE;
+        private CodecPolicy decodingPolicy = DECODING_POLICY_DEFAULT;
+
+        @Override
+        public Base64 get() {
+            return new Base64(lineLength, lineSeparator, encodeTable, 
decodingPolicy);
+        }
+
+        /**
+         * Sets the decoding policy.
+         *
+         * @param decodingPolicy the decoding policy, null resets to the 
default.
+         * @return this.
+         */
+        public Builder setDecodingPolicy(final CodecPolicy decodingPolicy) {
+            this.decodingPolicy = decodingPolicy != null ? decodingPolicy : 
DECODING_POLICY_DEFAULT;
+            return this;
+        }
+
+        /**
+         * Sets the encode table.
+         *
+         * @param encodeTable the encode table, null resets to the default.
+         * @return this.
+         */
+        public Builder setEncodeTable(final byte... encodeTable) {
+            this.encodeTable = encodeTable != null ? encodeTable : 
STANDARD_ENCODE_TABLE;
+            return this;
+        }
+
+        /**
+         * Sets the line length.
+         *
+         * @param lineLength the line length, less than 0 resets to the 
default.
+         * @return this.
+         */
+        public Builder setLineLength(final int lineLength) {
+            this.lineLength = Math.max(0, lineLength);
+            return this;
+        }
+
+        /**
+         * Sets the line separator.
+         *
+         * @param lineSeparator the line separator, null resets to the default.
+         * @return this.
+         */
+        public Builder setLineSeparator(final byte... lineSeparator) {
+            this.lineSeparator = lineSeparator != null ? lineSeparator : 
CHUNK_SEPARATOR;
+            return this;
+        }
+
+        /**
+         * Sets the URL-safe encoding policy.
+         *
+         * @param urlSafe URL-safe encoding policy, null resets to the default.
+         * @return this.
+         */
+        public Builder setUrlSafe(final boolean urlSafe) {
+            this.encodeTable = toUrlSafeEncodeTable(urlSafe);
+            return this;
+        }
+
+    }
+
     /**
      * BASE64 characters are 6 bits in length.
      * They are formed by taking a block of 3 octets to form a 24-bit string,
@@ -121,19 +196,29 @@ public class Base64 extends BaseNCodec {
             41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51                      // 
70-7a p-z
     };
 
-    // The static final fields above are used for the original static byte[] 
methods on Base64.
-    // The private member fields below are used with the new streaming 
approach, which requires
-    // some state be preserved between calls of encode() and decode().
-
     /**
      * Base64 uses 6-bit fields.
      */
     /** Mask used to extract 6 bits, used when encoding */
     private static final int MASK_6BITS = 0x3f;
+
+    // The static final fields above are used for the original static byte[] 
methods on Base64.
+    // The private member fields below are used with the new streaming 
approach, which requires
+    // some state be preserved between calls of encode() and decode().
+
     /** Mask used to extract 4 bits, used when decoding final trailing 
character. */
     private static final int MASK_4BITS = 0xf;
     /** Mask used to extract 2 bits, used when decoding final trailing 
character. */
     private static final int MASK_2BITS = 0x3;
+    /**
+     * Creates a new Builder.
+     *
+     * @return a new Builder.
+     * @since 1.17.0
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
 
     /**
      * Decodes Base64 data into octets.
@@ -415,6 +500,10 @@ public class Base64 extends BaseNCodec {
         return resizedBytes;
     }
 
+    private static byte[] toUrlSafeEncodeTable(final boolean urlSafe) {
+        return urlSafe ? URL_SAFE_ENCODE_TABLE : STANDARD_ENCODE_TABLE;
+    }
+
     /**
      * Encode table to use: either STANDARD or URL_SAFE or custom.
      * Note: the DECODE_TABLE above remains static because it is able
@@ -472,17 +561,6 @@ public class Base64 extends BaseNCodec {
         this(MIME_CHUNK_SIZE, CHUNK_SEPARATOR, urlSafe);
     }
 
-    /**
-     * Creates a Base64 codec used for decoding and encoding with non-standard 
encodeTable-table
-     *
-     * @param encodeTable
-     *          The manual encodeTable - a byte array of 64 chars
-     * @since 1.17.0
-     */
-    public Base64(final byte[] encodeTable) {
-        this(0, CHUNK_SEPARATOR, encodeTable, DECODING_POLICY_DEFAULT);
-    }
-
     /**
      * Creates a Base64 codec used for decoding (all modes) and encoding in 
URL-unsafe mode.
      * <p>
@@ -506,6 +584,7 @@ public class Base64 extends BaseNCodec {
         this(lineLength, CHUNK_SEPARATOR);
     }
 
+
     /**
      * Creates a Base64 codec used for decoding (all modes) and encoding in 
URL-unsafe mode.
      * <p>
@@ -533,7 +612,6 @@ public class Base64 extends BaseNCodec {
         this(lineLength, lineSeparator, false);
     }
 
-
     /**
      * Creates a Base64 codec used for decoding (all modes) and encoding in 
URL-unsafe mode.
      * <p>
@@ -562,7 +640,7 @@ public class Base64 extends BaseNCodec {
      * @since 1.4
      */
     public Base64(final int lineLength, final byte[] lineSeparator, final 
boolean urlSafe) {
-        this(lineLength, lineSeparator, urlSafe ? URL_SAFE_ENCODE_TABLE : 
STANDARD_ENCODE_TABLE, DECODING_POLICY_DEFAULT);
+        this(lineLength, lineSeparator, toUrlSafeEncodeTable(urlSafe), 
DECODING_POLICY_DEFAULT);
     }
 
     /**
@@ -595,7 +673,7 @@ public class Base64 extends BaseNCodec {
      */
     public Base64(final int lineLength, final byte[] lineSeparator, final 
boolean urlSafe,
                   final CodecPolicy decodingPolicy) {
-        this(lineLength, lineSeparator, urlSafe ? URL_SAFE_ENCODE_TABLE : 
STANDARD_ENCODE_TABLE, decodingPolicy);
+        this(lineLength, lineSeparator, toUrlSafeEncodeTable(urlSafe), 
decodingPolicy);
     }
 
     /**
@@ -622,16 +700,10 @@ public class Base64 extends BaseNCodec {
      * @param decodingPolicy The decoding policy.
      * @throws IllegalArgumentException
      *             Thrown when the {@code lineSeparator} contains Base64 
characters.
-     * @since 1.17.0
      */
-    public Base64(final int lineLength, final byte[] lineSeparator, final 
byte[] encodeTable,
-                  final CodecPolicy decodingPolicy) {
-        super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK,
-                lineLength,
-                lineSeparator == null ? 0 : lineSeparator.length,
-                PAD_DEFAULT,
-                decodingPolicy);
-        this.encodeTable = encodeTable;
+    private Base64(final int lineLength, final byte[] lineSeparator, final 
byte[] encodeTable, final CodecPolicy decodingPolicy) {
+        super(BYTES_PER_UNENCODED_BLOCK, BYTES_PER_ENCODED_BLOCK, lineLength, 
lineSeparator == null ? 0 : lineSeparator.length, PAD_DEFAULT, decodingPolicy);
+        this.encodeTable = Objects.requireNonNull(encodeTable, "encodeTable");
 
         if (encodeTable == STANDARD_ENCODE_TABLE || encodeTable == 
URL_SAFE_ENCODE_TABLE) {
             decodeTable = DECODE_TABLE;
@@ -857,6 +929,15 @@ public class Base64 extends BaseNCodec {
         }
     }
 
+    /**
+     * Gets the line separator (for testing only).
+     *
+     * @return the line separator.
+     */
+    byte[] getLineSeparator() {
+        return lineSeparator;
+    }
+
     /**
      * Returns whether or not the {@code octet} is in the Base64 alphabet.
      *
diff --git a/src/test/java/org/apache/commons/codec/binary/Base64Test.java 
b/src/test/java/org/apache/commons/codec/binary/Base64Test.java
index 4b6d3773..37100824 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base64Test.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base64Test.java
@@ -46,6 +46,10 @@ import org.junit.jupiter.api.Test;
  */
 public class Base64Test {
 
+    private static final String FOX_BASE64 = 
"VGhlIH@$#$@%F1aWN@#@#@@rIGJyb3duIGZve\n\r\t%#%#%#%CBqd##$#$W1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg==";
+
+    private static final String FOX_TEXT = "The quick brown fox jumped over 
the lazy dogs.";
+
     private static final Charset CHARSET_UTF8 = StandardCharsets.UTF_8;
 
     /**
@@ -216,6 +220,36 @@ public class Base64Test {
         assertEquals("SGVsbG8gV29ybGQ=", encodedContent, "encoding hello 
world");
     }
 
+    @Test
+    public void testBuilderCodecPolicy() {
+        assertEquals(CodecPolicy.LENIENT, 
Base64.builder().get().getCodecPolicy());
+        assertEquals(CodecPolicy.LENIENT, 
Base64.builder().setDecodingPolicy(CodecPolicy.LENIENT).get().getCodecPolicy());
+        assertEquals(CodecPolicy.STRICT, 
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).get().getCodecPolicy());
+        assertEquals(CodecPolicy.LENIENT, 
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setDecodingPolicy(null).get().getCodecPolicy());
+        assertEquals(CodecPolicy.LENIENT, 
Base64.builder().setDecodingPolicy(null).get().getCodecPolicy());
+    }
+
+    @Test
+    public void testBuilderLineAttributes() {
+        assertNull(Base64.builder().get().getLineSeparator());
+        
assertNull(Base64.builder().setLineSeparator(BaseNCodec.CHUNK_SEPARATOR).get().getLineSeparator());
+        assertArrayEquals(BaseNCodec.CHUNK_SEPARATOR, 
Base64.builder().setLineLength(4).setLineSeparator(BaseNCodec.CHUNK_SEPARATOR).get().getLineSeparator());
+        assertArrayEquals(BaseNCodec.CHUNK_SEPARATOR, 
Base64.builder().setLineLength(4).setLineSeparator(null).get().getLineSeparator());
+        assertArrayEquals(BaseNCodec.CHUNK_SEPARATOR, 
Base64.builder().setLineLength(10).setLineSeparator(null).get().getLineSeparator());
+        
assertNull(Base64.builder().setLineLength(-1).setLineSeparator(null).get().getLineSeparator());
+        
assertNull(Base64.builder().setLineLength(0).setLineSeparator(null).get().getLineSeparator());
+        assertArrayEquals(new byte[] { 1 }, 
Base64.builder().setLineLength(4).setLineSeparator((byte) 
1).get().getLineSeparator());
+        assertEquals("Zm94\r\n", 
Base64.builder().setLineLength(4).get().encodeToString("fox".getBytes(CHARSET_UTF8)));
+    }
+
+    @Test
+    public void testBuilderUrlSafe() {
+        assertFalse(Base64.builder().get().isUrlSafe());
+        assertFalse(Base64.builder().setUrlSafe(false).get().isUrlSafe());
+        
assertFalse(Base64.builder().setUrlSafe(true).setUrlSafe(false).get().isUrlSafe());
+        
assertTrue(Base64.builder().setUrlSafe(false).setUrlSafe(true).get().isUrlSafe());
+    }
+
     @Test
     public void testByteToStringVariations() throws DecoderException {
         final Base64 base64 = new Base64(0);
@@ -432,7 +466,7 @@ public class Base64Test {
 
         // two instances: one with default table and one with adjusted 
encoding table
         final Base64 b64 = new Base64();
-        final Base64 b64customEncoding = new Base64(encodeTable);
+        final Base64 b64customEncoding = 
Base64.builder().setEncodeTable(encodeTable).get();
 
         final String content = "! Hello World - this ยง$%";
 
@@ -461,7 +495,7 @@ public class Base64Test {
         final byte[] encodeTable = {
                 '.', '-', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'
         };
-        assertThrows(IllegalArgumentException.class, () -> new 
Base64(encodeTable));
+        assertThrows(IllegalArgumentException.class, () -> 
Base64.builder().setEncodeTable(encodeTable).get());
     }
 
     private void testDecodeEncode(final String encodedText) {
@@ -632,10 +666,7 @@ public class Base64Test {
 
     @Test
     public void testIgnoringNonBase64InDecode() throws Exception {
-        assertEquals("The quick brown fox jumped over the lazy dogs.",
-                new String(Base64.decodeBase64(
-                        
"VGhlIH@$#$@%F1aWN@#@#@@rIGJyb3duIGZve\n\r\t%#%#%#%CBqd##$#$W1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg=="
-                                .getBytes(CHARSET_UTF8))));
+        assertEquals(FOX_TEXT, new 
String(Base64.decodeBase64(FOX_BASE64.getBytes(CHARSET_UTF8))));
     }
 
     @Test
@@ -692,7 +723,7 @@ public class Base64Test {
 
     @Test
     public void testKnownDecodings() {
-        assertEquals("The quick brown fox jumped over the lazy dogs.", new 
String(Base64.decodeBase64(
+        assertEquals(FOX_TEXT, new String(Base64.decodeBase64(
                 
"VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg==".getBytes(CHARSET_UTF8))));
         assertEquals("It was the best of times, it was the worst of times.", 
new String(Base64.decodeBase64(
                 
"SXQgd2FzIHRoZSBiZXN0IG9mIHRpbWVzLCBpdCB3YXMgdGhlIHdvcnN0IG9mIHRpbWVzLg==".getBytes(CHARSET_UTF8))));
@@ -708,7 +739,7 @@ public class Base64Test {
     @Test
     public void testKnownEncodings() {
         
assertEquals("VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wZWQgb3ZlciB0aGUgbGF6eSBkb2dzLg==",
 new String(
-                Base64.encodeBase64("The quick brown fox jumped over the lazy 
dogs.".getBytes(CHARSET_UTF8))));
+                Base64.encodeBase64(FOX_TEXT.getBytes(CHARSET_UTF8))));
         assertEquals(
                 
"YmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJs\r\nYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFo\r\nIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBibGFoIGJsYWggYmxhaCBi\r\nbGFoIGJsYWg=\r\n",
                 new String(Base64.encodeBase64Chunked(

Reply via email to