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 <[email protected]> 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(
