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


The following commit(s) were added to refs/heads/master by this push:
     new f288a1ce Add Base45 support (#442)
f288a1ce is described below

commit f288a1ce41c19570ca79362abdc2bfa1c446263e
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 12 20:10:06 2026 -0500

    Add Base45 support (#442)
    
    * Add Base45 support.
    
    * Fix whitespace.
    
    * Update note on closing the Base45OutputStream
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * Fix spelling
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
---
 .../org/apache/commons/codec/binary/Base45.java    | 476 +++++++++++
 .../commons/codec/binary/Base45InputStream.java    |  75 ++
 .../commons/codec/binary/Base45OutputStream.java   |  89 ++
 .../codec/binary/Base45InputStreamTest.java        | 341 ++++++++
 .../codec/binary/Base45OutputStreamTest.java       | 179 ++++
 .../apache/commons/codec/binary/Base45Test.java    | 944 +++++++++++++++++++++
 6 files changed, 2104 insertions(+)

diff --git a/src/main/java/org/apache/commons/codec/binary/Base45.java 
b/src/main/java/org/apache/commons/codec/binary/Base45.java
new file mode 100644
index 00000000..49fe6d44
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base45.java
@@ -0,0 +1,476 @@
+/*
+ * 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.codec.binary;
+
+import java.util.Arrays;
+
+import org.apache.commons.codec.CodecPolicy;
+
+/**
+ * Provides Base45 encoding and decoding as defined by <a 
href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285</a>.
+ * <p>
+ * Base45 is designed for efficient encoding of binary data in environments 
where a subset of ASCII characters is available, specifically 45 characters 
chosen
+ * from the QR code alphanumeric mode character set. Base45 is used in 
European Union Digital COVID Certificates (EUDCC) and similar applications.
+ * </p>
+ * <p>
+ * The Base45 alphabet consists of 45 characters:
+ * </p>
+ *
+ * <pre>
+ * Value Encoding  Value Encoding  Value Encoding  Value Encoding
+ *    0 0           12 C           24 O           36 Space
+ *    1 1           13 D           25 P           37 $
+ *    2 2           14 E           26 Q           38 %
+ *    3 3           15 F           27 R           39 *
+ *    4 4           16 G           28 S           40 +
+ *    5 5           17 H           29 T           41 -
+ *    6 6           18 I           30 U           42 .
+ *    7 7           19 J           31 V           43 /
+ *    8 8           20 K           32 W           44 :
+ *    9 9           21 L           33 X
+ *   10 A           22 M           34 Y
+ *   11 B           23 N           35 Z
+ * </pre>
+ *
+ * <h2>Encoding</h2>
+ * <p>
+ * Input bytes are grouped in pairs (2 bytes). Each pair is encoded as 3 
Base45 characters. A single remaining byte is encoded as 2 Base45 characters. 
There is
+ * no padding.
+ * </p>
+ * <ul>
+ * <li>For each 2-byte pair {@code (b0, b1)}: {@code n = b0 * 256 + b1}; 
output 3 characters {@code alphabet[n % 45]}, {@code alphabet[(n / 45) % 45]},
+ * {@code alphabet[n / 2025]}</li>
+ * <li>For a final single byte {@code b0}: {@code n = b0}; output 2 characters 
{@code alphabet[n % 45]}, {@code alphabet[n / 45]}</li>
+ * </ul>
+ * <h2>Decoding</h2>
+ * <p>
+ * Input characters are grouped in triples (3 characters). Each triple decodes 
to 2 bytes. A pair of trailing characters decodes to 1 byte. An input whose
+ * length modulo 3 equals 1 is invalid.
+ * </p>
+ * <p>
+ * This class is thread-safe.
+ * </p>
+ * <p>
+ * To create an instance, use the default constructor or the builder:
+ * </p>
+ *
+ * <pre>
+ * Base45 codec = new Base45();
+ *
+ * // Or, use the builder to customize the encode table:
+ * Base45 custom = Base45.builder().setEncodeTable(...).get();
+ * </pre>
+ *
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285 – The 
Base45 Data Encoding</a>
+ * @since 1.23.0
+ */
+public class Base45 extends BaseNCodec {
+
+    /**
+     * Builds {@link Base45} instances.
+     * <p>
+     * To configure a new instance, use a {@link Builder}. For example:
+     * </p>
+     *
+     * <pre>
+     *
+     * Base45 base45 = Base45.builder().get();
+     * </pre>
+     *
+     * @since 1.23.0
+     */
+    public static class Builder extends AbstractBuilder<Base45, Builder> {
+
+        /**
+         * Constructs a new instance using the Base45 alphabet as defined by 
RFC 9285.
+         */
+        public Builder() {
+            super(ENCODE_TABLE);
+            setDecodingPolicy(CodecPolicy.STRICT);
+            setDecodeTableRaw(DECODE_TABLE);
+            setEncodeTableRaw(ENCODE_TABLE);
+            setEncodedBlockSize(BYTES_PER_ENCODED_BLOCK);
+            setUnencodedBlockSize(BYTES_PER_UNENCODED_BLOCK);
+        }
+
+        @Override
+        public Base45 get() {
+            return new Base45(this);
+        }
+
+        /**
+         * Sets the decoding policy. {@link CodecPolicy#STRICT} is the only 
supported policy.
+         *
+         * @param decodingPolicy The decoding policy; {@code null} resets to 
the default ({@link CodecPolicy#STRICT}).
+         * @return {@code this} instance.
+         * @throws IllegalArgumentException if the given policy is {@link 
CodecPolicy#LENIENT}.
+         */
+        @Override
+        public Builder setDecodingPolicy(final CodecPolicy decodingPolicy) {
+            if (decodingPolicy == CodecPolicy.LENIENT) {
+                throw new IllegalArgumentException("CodecPolicy.STRICT is the 
only supported policy.");
+            }
+            return super.setDecodingPolicy(decodingPolicy != null ? 
decodingPolicy : CodecPolicy.STRICT);
+        }
+
+        /**
+         * Sets the encode table and derives the matching decode table, so the 
codec can always decode its own output.
+         *
+         * @param encodeTable The encode table with exactly 45 unique entries, 
null resets to the default.
+         * @return {@code this} instance.
+         * @throws IllegalArgumentException if the encode table does not 
contain exactly 45 unique entries.
+         */
+        @Override
+        public Builder setEncodeTable(final byte... encodeTable) {
+            super.setDecodeTableRaw(toDecodeTable(encodeTable));
+            return super.setEncodeTable(encodeTable);
+        }
+
+        /**
+         * Always throws UnsupportedOperationException: Unsupported by Base45 
RFC 9285.
+         *
+         * @throws UnsupportedOperationException Always thrown: Unsupported by 
Base45 RFC 9285.
+         */
+        @Override
+        public Builder setLineLength(final int lineLength) {
+            throw new UnsupportedOperationException("Unsupported by Base45 RFC 
9285");
+        }
+
+        /**
+         * Always throws UnsupportedOperationException: Unsupported by Base45 
RFC 9285.
+         *
+         * @throws UnsupportedOperationException Always thrown: Unsupported by 
Base45 RFC 9285.
+         */
+        @Override
+        public Builder setLineSeparator(final byte... lineSeparator) {
+            throw new UnsupportedOperationException("Unsupported by Base45 RFC 
9285");
+        }
+
+        /**
+         * Always throws UnsupportedOperationException: Unsupported by Base45 
RFC 9285.
+         *
+         * @throws UnsupportedOperationException Always thrown: Unsupported by 
Base45 RFC 9285.
+         */
+        @Override
+        public Builder setPadding(final byte padding) {
+            throw new UnsupportedOperationException("Unsupported by Base45 RFC 
9285");
+        }
+    }
+
+    /**
+     * The number of characters in the Base45 alphabet.
+     */
+    private static final int BASE = 45;
+
+    /**
+     * The square of the Base45 alphabet size (45 * 45 = 2025), used during 
decoding.
+     */
+    private static final int BASE_SQUARED = BASE * BASE; // 2025
+
+    /**
+     * Number of Base45 characters in an encoded block (encoding 2 unencoded 
bytes).
+     */
+    static final int BYTES_PER_ENCODED_BLOCK = 3;
+
+    private static final int TAIL_ENCODED_BLOCK = BYTES_PER_ENCODED_BLOCK - 1;
+
+    /**
+     * Number of unencoded bytes per full encoding block.
+     */
+    static final int BYTES_PER_UNENCODED_BLOCK = 2;
+
+    /**
+     * Lookup table translating ASCII character values (0–127) to their Base45 
alphabet index (0–44), or -1 if the character is not in the Base45 alphabet.
+     */
+    // @formatter:off
+    static final byte[] DECODE_TABLE = {
+            //  0   1   2   3   4   5   6   7   8   9   A   B   C   D   E   F
+               -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
// 00-0f
+               -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
// 10-1f
+               36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, 
// 20-2f ' ','$','%','*','+','-','.','/
+                0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 44, -1, -1, -1, -1, -1, 
// 30-3f '0'-'9', ':'
+               -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 
// 40-4f 'A'-'O'
+               25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, 
// 50-5f 'P'-'Z'
+               -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
// 60-6f
+               -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
// 70-7f
+    };
+
+    // @formatter:on
+    /**
+     * Lookup table translating Base45 values (0–44) to their ASCII character 
equivalents.
+     * <p>
+     * As specified in <a 
href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285</a>: {@code 0-9, 
A-Z, Space, $, %, *, +, -, ., /, :}
+     * </p>
+     */
+    // @formatter:off
+    static final byte[] ENCODE_TABLE = {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',            //  
0-9
+            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',  // 
10-21
+            'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',  // 
22-33
+            'Y', 'Z',                                                    // 
34-35
+            ' ', '$', '%', '*', '+', '-', '.', '/', ':',                 // 
36-44
+    };
+    // @formatter:on
+
+    /**
+     * Creates a new {@link Builder} for configuring a {@link Base45} instance.
+     *
+     * @return A new {@link Builder}.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    /**
+     * Constructs the decode table matching the given encode table.
+     *
+     * @param encodeTable The encode table.
+     * @return A new decode table.
+     * @throws IllegalArgumentException if the encode table does not contain 
exactly 45 unique entries.
+     */
+    private static byte[] calculateDecodeTable(final byte[] encodeTable) {
+        if (encodeTable.length != BASE) {
+            throw new IllegalArgumentException("encodeTable must have exactly 
" + BASE + " entries.");
+        }
+        final byte[] decodeTable = new byte[DECODE_TABLE.length];
+        Arrays.fill(decodeTable, (byte) -1);
+        for (int i = 0; i < encodeTable.length; i++) {
+            final int encodedByte = encodeTable[i] & 0xff;
+            if (encodedByte >= decodeTable.length || decodeTable[encodedByte] 
!= -1) {
+                throw new IllegalArgumentException("encodeTable entries must 
be unique values in the range 0-127.");
+            }
+            decodeTable[encodedByte] = (byte) i;
+        }
+        return decodeTable;
+    }
+
+    /**
+     * Gets the decode table that matches the given encode table.
+     *
+     * @param encodeTable The encode table used to determine the decode lookup 
table.
+     * @return The matching decode table.
+     */
+    private static byte[] toDecodeTable(final byte[] encodeTable) {
+        final byte[] table = encodeTable != null ? encodeTable : ENCODE_TABLE;
+        if (Arrays.equals(table, ENCODE_TABLE)) {
+            return DECODE_TABLE;
+        }
+        return calculateDecodeTable(table);
+    }
+
+    /**
+     * Constructs a Base45 codec using the default settings (strict decoding 
policy, the only supported policy).
+     */
+    public Base45() {
+        this(builder());
+    }
+
+    /**
+     * Constructs a Base45 codec from a builder.
+     *
+     * @param builder The builder to configure this instance.
+     */
+    private Base45(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Decodes all of the provided data, starting at {@code inPos}, for {@code 
inAvail} bytes.
+     * <p>
+     * This method must be called at least twice: once with the data to 
decode, and once with {@code inAvail} set to {@code -1} to notify the decoder 
that EOF
+     * has been reached.
+     * </p>
+     * <p>
+     * Input characters not in the Base45 alphabet, including CR, LF, and TAB, 
cause an {@link IllegalArgumentException}. Space {@code ' '} is part of the
+     * Base45 alphabet and is decoded as data.
+     * </p>
+     *
+     * @param input   byte array of Base45-encoded character data to decode.
+     * @param inPos   Position to start reading data from.
+     * @param inAvail Number of bytes available from {@code input} for 
decoding, or {@code -1} to signal EOF.
+     * @param context The context to be used.
+     * @throws IllegalArgumentException if the input contains an invalid 
character, if the encoded length modulo 3 equals 1, or if an encoded triple 
decodes to
+     *                                  a value exceeding 65535, or if a 
trailing 2-character sequence decodes to a value greater than 255.
+     */
+    @Override
+    void decode(final byte[] input, int inPos, final int inAvail, final 
Context context) {
+        // package-protected for access from I/O streams
+        if (context.eof) {
+            return;
+        }
+        if (inAvail < 0) {
+            context.eof = true;
+            switch (context.modulus) {
+            case 0:
+                // Nothing to do; input length is a multiple of 3.
+                break;
+            case 1:
+                // RFC 9285: "It is an error if the remaining string length is 
1 character."
+                throw new IllegalArgumentException("Invalid Base45 encoding: 
encoded input length modulo 3 must not equal 1.");
+            case 2:
+                // Two trailing characters decode to one byte.
+                // Maximum decodable value from two Base45 characters: 44 + 
44*45 = 2024.
+                // Valid single-byte encodings have a decoded value in [0, 
255].
+                if (context.ibitWorkArea > 0xFF) {
+                    throw new IllegalArgumentException("Invalid Base45 
encoding: trailing 2-character sequence decodes to " + context.ibitWorkArea +
+                            ", which exceeds the valid byte range (0-255).");
+                }
+                ensureBufferSize(1, context)[context.pos++] = (byte) 
context.ibitWorkArea;
+                break;
+            default:
+                throw new IllegalStateException("Impossible modulus " + 
context.modulus);
+            }
+            return;
+        }
+        for (int i = 0; i < inAvail; i++) {
+            final int b = input[inPos++] & 0xFF;
+            if (b >= decodeTable.length || decodeTable[b] < 0) {
+                throw new IllegalArgumentException("Invalid Base45 character 
'" + (char) b + "' (value " + b + ").");
+            }
+            final int value = decodeTable[b];
+            switch (context.modulus) {
+            case 0:
+                // First character of a 3-character group: initialize 
accumulator.
+                context.ibitWorkArea = value;
+                context.modulus = 1;
+                break;
+            case 1:
+                // Second character of a 3-character group.
+                context.ibitWorkArea += value * BASE;
+                context.modulus = 2;
+                break;
+            case 2:
+                // Third character of a 3-character group: compute value and 
output 2 bytes.
+                context.ibitWorkArea += value * BASE_SQUARED;
+                context.modulus = 0;
+                if (context.ibitWorkArea > 0xFFFF) {
+                    throw new IllegalArgumentException("Invalid Base45 
encoding: 3-character sequence decodes to " + context.ibitWorkArea +
+                            ", which exceeds the valid 16-bit range 
(0-65535).");
+                }
+                final byte[] buffer = 
ensureBufferSize(BYTES_PER_UNENCODED_BLOCK, context);
+                buffer[context.pos++] = (byte) (context.ibitWorkArea >> 8);
+                buffer[context.pos++] = (byte) (context.ibitWorkArea & 0xFF);
+                context.ibitWorkArea = 0;
+                break;
+            default:
+                throw new IllegalStateException("Impossible modulus " + 
context.modulus);
+            }
+        }
+    }
+
+    /**
+     * Encodes all of the provided data, starting at {@code inPos}, for {@code 
inAvail} bytes.
+     * <p>
+     * This method must be called at least twice: once with the data to 
encode, and once with {@code inAvail} set to {@code -1} to notify the encoder 
that EOF
+     * has been reached.
+     * </p>
+     * <p>
+     * Each pair of input bytes is encoded to 3 Base45 characters. A final 
single byte is encoded as 2 Base45 characters. No padding is used.
+     * </p>
+     *
+     * @param input   byte array of binary data to Base45-encode.
+     * @param inPos   Position to start reading data from.
+     * @param inAvail Number of bytes available from {@code input} for 
encoding, or {@code -1} to signal EOF.
+     * @param context The context to be used.
+     */
+    @Override
+    void encode(final byte[] input, int inPos, final int inAvail, final 
Context context) {
+        // package-protected for access from I/O streams
+        if (context.eof) {
+            return;
+        }
+        if (inAvail < 0) {
+            context.eof = true;
+            if (context.modulus == 1) {
+                // One remaining byte: encode as 2 Base45 characters.
+                final byte[] buffer = ensureBufferSize(TAIL_ENCODED_BLOCK, 
context);
+                final int n = context.ibitWorkArea & 0xFF;
+                buffer[context.pos++] = encodeTable[n % BASE];
+                buffer[context.pos++] = encodeTable[n / BASE];
+            }
+            // If modulus == 0, all bytes have been encoded; nothing to flush.
+            return;
+        }
+        for (int i = 0; i < inAvail; i++) {
+            final int b = input[inPos++] & 0xFF;
+            // Accumulate byte into work area and advance modulus.
+            context.modulus = (context.modulus + 1) % 
BYTES_PER_UNENCODED_BLOCK;
+            // Shift the accumulated value left by 8 bits and add the new byte.
+            context.ibitWorkArea = (context.ibitWorkArea << 8) + b;
+            if (context.modulus == 0) {
+                // We have a complete 2-byte group; encode as 3 Base45 
characters.
+                final byte[] buffer = 
ensureBufferSize(BYTES_PER_ENCODED_BLOCK, context);
+                // The work area holds: b0 * 256 + b1 (a 16-bit value, 
0–65535).
+                int n = context.ibitWorkArea & 0xFFFF;
+                buffer[context.pos++] = encodeTable[n % BASE];
+                n /= BASE;
+                buffer[context.pos++] = encodeTable[n % BASE];
+                n /= BASE;
+                buffer[context.pos++] = encodeTable[n];
+                context.ibitWorkArea = 0;
+            }
+        }
+    }
+
+    /**
+     * Gets the number of Base45-encoded characters needed to encode the given 
byte array, as specified by RFC 9285.
+     * <p>
+     * The formula is: {@code (n / 2) * 3 + (n % 2 != 0 ? 2 : 0)}, where 
{@code n} is the number of unencoded bytes.
+     * </p>
+     *
+     * @param array The byte array to encode (used only for its length).
+     * @return The number of Base45 characters that would be produced by 
encoding {@code array}.
+     */
+    @Override
+    public long getEncodedLength(final byte[] array) {
+        final long n = array.length;
+        return n / 2 * 3 + (n % 2 != 0 ? 2 : 0);
+    }
+
+    /**
+     * Tests whether or not the {@code value} is a valid Base45 alphabet 
character.
+     *
+     * @param value The byte value to test.
+     * @return {@code true} if the byte corresponds to a character in the 
Base45 alphabet (RFC 9285); {@code false} otherwise.
+     */
+    @Override
+    public boolean isInAlphabet(final byte value) {
+        final int v = value & 0xFF;
+        return v < decodeTable.length && decodeTable[v] >= 0;
+    }
+
+    /**
+     * Tests a given byte array to see if it contains only valid characters 
within the alphabet. The method optionally treats whitespace as valid.
+     * <p>
+     * Unlike the {@link BaseNCodec} implementation, the pad character is 
<em>not</em> considered valid, because Base45 (RFC 9285) has no padding.
+     * </p>
+     *
+     * @param arrayOctet         byte array to test.
+     * @param allowWhitespacePad if {@code true}, then whitespace is also 
allowed.
+     * @return {@code true} if all bytes are valid characters in the alphabet 
or if the byte array is empty; {@code false}, otherwise.
+     */
+    @Override
+    public boolean isInAlphabet(final byte[] arrayOctet, final boolean 
allowWhitespacePad) {
+        for (final byte octet : arrayOctet) {
+            if (!isInAlphabet(octet) && (!allowWhitespacePad || 
!Character.isWhitespace(octet))) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base45InputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base45InputStream.java
new file mode 100644
index 00000000..5b3c3990
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base45InputStream.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.codec.binary;
+
+import java.io.InputStream;
+
+/**
+ * Provides <a href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285 
Base45</a> decoding in a streaming fashion (unlimited size).
+ *
+ * @see Base45
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285 – The 
Base45 Data Encoding</a>
+ * @since 1.23.0
+ */
+public class Base45InputStream extends BaseNCodecInputStream<Base45, 
Base45InputStream, Base45InputStream.Builder> {
+
+    /**
+     * Builds instances of Base45InputStream.
+     */
+    public static class Builder extends 
BaseNCodecInputStream.AbstracBuilder<Base45InputStream, Base45, Builder> {
+
+        /**
+         * Constructs a new instance.
+         */
+        public Builder() {
+            // empty
+        }
+
+        @Override
+        public Base45InputStream get() {
+            return new Base45InputStream(this);
+        }
+
+        @Override
+        protected Base45 newBaseNCodec() {
+            return new Base45();
+        }
+    }
+
+    /**
+     * Constructs a new Builder.
+     *
+     * @return A new Builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private Base45InputStream(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Constructs a Base45InputStream such that all data read is 
Base45-decoded from the original provided InputStream.
+     *
+     * @param inputStream InputStream to wrap.
+     */
+    public Base45InputStream(final InputStream inputStream) {
+        super(builder().setInputStream(inputStream));
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/codec/binary/Base45OutputStream.java 
b/src/main/java/org/apache/commons/codec/binary/Base45OutputStream.java
new file mode 100644
index 00000000..638e9280
--- /dev/null
+++ b/src/main/java/org/apache/commons/codec/binary/Base45OutputStream.java
@@ -0,0 +1,89 @@
+/*
+ * 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.codec.binary;
+
+import java.io.OutputStream;
+
+/**
+ * Provides <a href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285 
Base45</a> encoding in a streaming fashion (unlimited size).
+ * <p>
+ * <strong>Note:</strong> Call {@link #close()} or {@link #eof()} after the 
last byte has been written; otherwise the final
+ * partial block will be omitted and the resulting data will be incomplete.
+ * </p>
+ *
+ * @see Base45
+ * @see <a href="https://datatracker.ietf.org/doc/html/rfc9285";>RFC 9285 – The 
Base45 Data Encoding</a>
+ * @since 1.23.0
+ */
+public class Base45OutputStream extends BaseNCodecOutputStream<Base45, 
Base45OutputStream, Base45OutputStream.Builder> {
+
+    /**
+     * Builds instances of Base45OutputStream.
+     */
+    public static class Builder extends 
BaseNCodecOutputStream.AbstractBuilder<Base45OutputStream, Base45, Builder> {
+
+        /**
+         * Constructs a new instance.
+         */
+        public Builder() {
+            setEncode(true);
+        }
+
+        /**
+         * Builds a new Base45OutputStream instance with the configured 
settings.
+         *
+         * @return A new Base45OutputStream.
+         */
+        @Override
+        public Base45OutputStream get() {
+            return new Base45OutputStream(this);
+        }
+
+        /**
+         * Creates a new Base45 codec instance.
+         *
+         * @return A new Base45 codec.
+         */
+        @Override
+        protected Base45 newBaseNCodec() {
+            return new Base45();
+        }
+    }
+
+    /**
+     * Constructs a new Builder.
+     *
+     * @return A new Builder.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private Base45OutputStream(final Builder builder) {
+        super(builder);
+    }
+
+    /**
+     * Constructs a Base45OutputStream such that all data written is 
Base45-encoded to the original provided OutputStream.
+     *
+     * @param outputStream OutputStream to wrap.
+     */
+    public Base45OutputStream(final OutputStream outputStream) {
+        this(builder().setOutputStream(outputStream));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base45InputStreamTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base45InputStreamTest.java
new file mode 100644
index 00000000..da3b4a0c
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base45InputStreamTest.java
@@ -0,0 +1,341 @@
+/*
+ * 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.codec.binary;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Base45InputStream}.
+ */
+class Base45InputStreamTest {
+
+    private static final byte[] CRLF = { (byte) '\r', (byte) '\n' };
+    private static final byte[] LF = { (byte) '\n' };
+    private static final String STRING_FIXTURE = "Hello World";
+
+    @Test
+    void testAvailable() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            final int initialAvailable = b45stream.available();
+            assertTrue(initialAvailable > 0, "Initial available should be 
greater than 0");
+            assertEquals(3, b45stream.skip(10), "Skip should return 3 (decoded 
bytes)");
+            assertEquals(0, b45stream.available());
+            assertEquals(-1, b45stream.read());
+            assertEquals(-1, b45stream.read());
+        }
+    }
+
+    private void testBase45EmptyInputStream(final int chunkSize) throws 
Exception {
+        final byte[] emptyEncoded = {};
+        final byte[] emptyDecoded = {};
+        testByChunk(emptyEncoded, emptyDecoded, chunkSize, CRLF);
+        testByteByByte(emptyEncoded, emptyDecoded, chunkSize, CRLF);
+    }
+
+    /**
+     * Tests the Base45InputStream implementation against empty input.
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testBase45EmptyInputStreamMimeChunkSize() throws Exception {
+        testBase45EmptyInputStream(BaseNCodec.MIME_CHUNK_SIZE);
+    }
+
+    /**
+     * Tests the Base45InputStream implementation against empty input.
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testBase45EmptyInputStreamPemChunkSize() throws Exception {
+        testBase45EmptyInputStream(BaseNCodec.PEM_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase45InputStreamByChunk() throws Exception {
+        // Hello World test.
+        byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        byte[] encoded = new Base45().encode(decoded);
+        testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
+        // test random data of sizes 0 through 150
+        final BaseNCodec codec = new Base45();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByChunk(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBase45InputStreamByteByByte() throws Exception {
+        // Hello World test.
+        byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        byte[] encoded = new Base45().encode(decoded);
+        testByteByByte(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CRLF);
+        // test random data of sizes 0 through 150
+        final BaseNCodec codec = new Base45();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByteByByte(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBuilder() {
+        assertNotNull(Base45InputStream.builder().getBaseNCodec());
+    }
+
+    /**
+     * Tests method does three tests on the supplied data: 1. encoded 
---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
+     * ---[WRAP-WRAP-WRAP-etc...] --> decoded
+     * <p/>
+     * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the 
Base45InputStream wraps itself in encode and decode mode over and over again.
+     *
+     * @param encoded   Base45 encoded data
+     * @param decoded   The data from above, but decoded
+     * @param chunkSize chunk size (line-length) of the Base45 encoded data.
+     * @param separator Line separator in the Base45 encoded data.
+     * @throws Exception Usually signifies a bug in the Base45 commons-codec 
implementation.
+     */
+    private void testByChunk(final byte[] encoded, final byte[] decoded, final 
int chunkSize, final byte[] separator) throws Exception {
+        try (InputStream in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get()) {
+            final byte[] output = IOUtils.toByteArray(in);
+            assertEquals(-1, in.read(), "EOF");
+            assertEquals(-1, in.read(), "Still EOF");
+            assertArrayEquals(encoded, output, "Streaming Base45 encode");
+        }
+        try (InputStream in = new Base45InputStream(new 
ByteArrayInputStream(encoded))) {
+            final byte[] output = IOUtils.toByteArray(in);
+            assertEquals(-1, in.read(), "EOF");
+            assertEquals(-1, in.read(), "Still EOF");
+            assertArrayEquals(decoded, output, "Streaming Base45 decode");
+        }
+        InputStream in = new ByteArrayInputStream(decoded);
+        for (int i = 0; i < 10; i++) {
+            in = 
Base45InputStream.builder().setInputStream(in).setEncode(true).get();
+            in = 
Base45InputStream.builder().setInputStream(in).setEncode(false).get();
+        }
+        final InputStream in1 = in;
+        final byte[] output = IOUtils.toByteArray(in1);
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming Base45 wrap-wrap-wrap!");
+        in.close();
+    }
+
+    /**
+     * Tests method does three tests on the supplied data: 1. encoded 
---[DECODE]--> decoded 2. decoded ---[ENCODE]--> encoded 3. decoded
+     * ---[WRAP-WRAP-WRAP-etc...] --> decoded
+     * <p/>
+     * By "[WRAP-WRAP-WRAP-etc...]" we mean situation where the 
Base45InputStream wraps itself in encode and decode mode over and over again.
+     *
+     * @param encoded   Base45 encoded data
+     * @param decoded   The data from above, but decoded
+     * @param chunkSize chunk size (line-length) of the Base45 encoded data.
+     * @param separator Line separator in the Base45 encoded data.
+     * @throws Exception Usually signifies a bug in the Base45 commons-codec 
implementation.
+     */
+    private void testByteByByte(final byte[] encoded, final byte[] decoded, 
final int chunkSize, final byte[] separator) throws Exception {
+        InputStream in;
+        in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get();
+        final InputStream in1 = in;
+        byte[] output = IOUtils.toByteArray(in1);
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(encoded, output, "Streaming Base45 encode");
+        in.close();
+        in = new Base45InputStream(new ByteArrayInputStream(encoded));
+        final InputStream in2 = in;
+        output = IOUtils.toByteArray(in2);
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming Base45 decode");
+        in.close();
+        in = new ByteArrayInputStream(decoded);
+        for (int i = 0; i < 10; i++) {
+            in = 
Base45InputStream.builder().setInputStream(in).setEncode(true).get();
+            in = 
Base45InputStream.builder().setInputStream(in).setEncode(false).get();
+        }
+        final InputStream in3 = in;
+        output = IOUtils.toByteArray(in3);
+        assertEquals(-1, in.read(), "EOF");
+        assertEquals(-1, in.read(), "Still EOF");
+        assertArrayEquals(decoded, output, "Streaming Base45 wrap-wrap-wrap!");
+    }
+
+    /**
+     * Tests markSupported.
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testMarkSupported() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        try (Base45InputStream in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get()) {
+            // Always returns false for now.
+            assertFalse(in.markSupported(), "Base45InputStream.markSupported() 
is false");
+        }
+    }
+
+    /**
+     * Tests read returning 0
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testRead0() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final byte[] buf = new byte[1024];
+        int bytesRead = 0;
+        try (Base45InputStream in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get()) {
+            bytesRead = in.read(buf, 0, 0);
+            assertEquals(0, bytesRead, "Base45InputStream.read(buf, 0, 0) 
returns 0");
+        }
+    }
+
+    /**
+     * Tests read with null.
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testReadNull() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        try (Base45InputStream in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get()) {
+            assertThrows(NullPointerException.class, () -> in.read(null, 0, 
0));
+        }
+    }
+
+    /**
+     * Tests read throwing IndexOutOfBoundsException
+     *
+     * @throws Exception for some failure scenarios.
+     */
+    @Test
+    void testReadOutOfBounds() throws Exception {
+        final byte[] decoded = StringUtils.getBytesUtf8(STRING_FIXTURE);
+        final byte[] buf = new byte[1024];
+        try (Base45InputStream in = 
Base45InputStream.builder().setByteArray(decoded).setEncode(true).get()) {
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
-1, 0), "Base45InputStream.read(buf, -1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
0, -1), "Base45InputStream.read(buf, 0, -1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
buf.length + 1, 0), "Base45InputStream.read(buf, buf.length + 1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> in.read(buf, 
buf.length - 1, 2), "Base45InputStream.read(buf, buf.length - 1, 2)");
+        }
+    }
+
+    /**
+     * Tests skipping number of characters larger than the internal buffer.
+     *
+     * @throws Throwable for some failure scenarios.
+     */
+    @Test
+    void testSkipBig() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            assertEquals(3, b45stream.skip(1024));
+            // End of stream reached
+            assertEquals(-1, b45stream.read());
+            assertEquals(-1, b45stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping as a noop
+     *
+     * @throws Throwable for some failure scenarios.
+     */
+    @Test
+    void testSkipNone() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            final byte[] actualBytes = new byte[3];
+            assertEquals(0, b45stream.skip(0));
+            b45stream.read(actualBytes, 0, actualBytes.length);
+            assertArrayEquals(actualBytes, new byte[] { 102, 111, 111 });
+            // End of stream reached
+            assertEquals(-1, b45stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping past the end of a stream.
+     *
+     * @throws Throwable for some failure scenarios.
+     */
+    @Test
+    void testSkipPastEnd() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            // skip correctly decoded characters
+            assertEquals(3, b45stream.skip(10));
+            // End of stream reached
+            assertEquals(-1, b45stream.read());
+            assertEquals(-1, b45stream.read());
+        }
+    }
+
+    /**
+     * Tests skipping to the end of a stream.
+     *
+     * @throws Throwable for some failure scenarios.
+     */
+    @Test
+    void testSkipToEnd() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            // skip correctly decoded characters
+            assertEquals(3, b45stream.skip(3));
+            assertEquals(-1, b45stream.read());
+        }
+    }
+
+    /**
+     * Tests if negative arguments to skip are handled correctly.
+     *
+     * @throws Throwable for some failure scenarios.
+     */
+    @Test
+    void testSkipWrongArgument() throws Throwable {
+        final String encoded = new String(new 
Base45().encode(StringUtils.getBytesUtf8("foo")));
+        final InputStream ins = new 
ByteArrayInputStream(StringUtils.getBytesIso8859_1(encoded));
+        try (Base45InputStream b45stream = new Base45InputStream(ins)) {
+            assertThrows(IllegalArgumentException.class, () -> 
b45stream.skip(-1));
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/codec/binary/Base45OutputStreamTest.java 
b/src/test/java/org/apache/commons/codec/binary/Base45OutputStreamTest.java
new file mode 100644
index 00000000..97a7eb82
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base45OutputStreamTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.codec.binary;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests {@link Base45OutputStream}.
+ */
+class Base45OutputStreamTest extends AbstractBaseNOutputStreamTest {
+
+    private static final byte[] CR_LF = { (byte) '\r', (byte) '\n' };
+    private static final byte[] LF = { (byte) '\n' };
+
+    @Override
+    OutputStream newOutputStream() {
+        return new Base45OutputStream(new ByteArrayOutputStream());
+    }
+
+    private void testBase45EmptyOutputStream(final int chunkSize) throws 
Exception {
+        final byte[] emptyEncoded = {};
+        final byte[] emptyDecoded = {};
+        testByteByByte(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
+        testByChunk(emptyEncoded, emptyDecoded, chunkSize, CR_LF);
+    }
+
+    @Test
+    void testBase45EmptyOutputStreamMimeChunkSize() throws Exception {
+        testBase45EmptyOutputStream(BaseNCodec.MIME_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase45EmptyOutputStreamPemChunkSize() throws Exception {
+        testBase45EmptyOutputStream(BaseNCodec.PEM_CHUNK_SIZE);
+    }
+
+    @Test
+    void testBase45OutputStreamByChunk() throws Exception {
+        byte[] decoded = StringUtils.getBytesUtf8("Hello World");
+        byte[] encoded = new Base45().encode(decoded);
+        testByChunk(encoded, decoded, BaseNCodec.MIME_CHUNK_SIZE, CR_LF);
+        final BaseNCodec codec = new Base45();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByChunk(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBase45OutputStreamByteByByte() throws Exception {
+        byte[] decoded = StringUtils.getBytesUtf8("Hello World");
+        byte[] encoded = new Base45().encode(decoded);
+        testByteByByte(encoded, decoded, 76, CR_LF);
+        final BaseNCodec codec = new Base45();
+        for (int i = 0; i <= 150; i++) {
+            final byte[][] randomData = BaseNTestData.randomData(codec, i);
+            encoded = randomData[1];
+            decoded = randomData[0];
+            testByteByByte(encoded, decoded, 0, LF);
+        }
+    }
+
+    @Test
+    void testBuilder() {
+        assertNotNull(Base45OutputStream.builder().getBaseNCodec());
+    }
+
+    private void testByChunk(final byte[] encoded, final byte[] decoded, final 
int chunkSize, final byte[] separator) throws Exception {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = 
Base45OutputStream.builder().setOutputStream(byteOut).setEncode(true).get()) {
+            out.write(decoded);
+        }
+        byte[] output = byteOut.toByteArray();
+        assertArrayEquals(encoded, output, "Streaming chunked Base45 encode");
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = 
Base45OutputStream.builder().setOutputStream(byteOut).setEncode(false).get()) {
+            out.write(encoded);
+        }
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming chunked Base45 decode");
+        byteOut = new ByteArrayOutputStream();
+        OutputStream out = byteOut;
+        for (int i = 0; i < 10; i++) {
+            out = 
Base45OutputStream.builder().setOutputStream(out).setEncode(false).get();
+            out = 
Base45OutputStream.builder().setOutputStream(out).setEncode(true).get();
+        }
+        out.write(decoded);
+        out.close();
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, byteOut.toByteArray(), "Streaming chunked 
Base45 wrap-wrap-wrap!");
+    }
+
+    private void testByteByByte(final byte[] encoded, final byte[] decoded, 
final int chunkSize, final byte[] separator) throws Exception {
+        ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = 
Base45OutputStream.builder().setOutputStream(byteOut).setEncode(true).get()) {
+            for (final byte element : decoded) {
+                out.write(element);
+            }
+        }
+        final byte[] output0 = byteOut.toByteArray();
+        assertArrayEquals(encoded, output0, "Streaming byte-by-byte Base45 
encode");
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = 
Base45OutputStream.builder().setOutputStream(byteOut).setEncode(false).get()) {
+            for (final byte element : encoded) {
+                out.write(element);
+            }
+        }
+        final byte[] output1 = byteOut.toByteArray();
+        assertArrayEquals(decoded, output1,
+                () -> String.format("Streaming byte-by-byte Base45 decode, 
chunkSize=%d, separator=%s, encoded=%s, decoded=%s, output=%s", chunkSize,
+                        Arrays.toString(separator), Arrays.toString(encoded), 
Arrays.toString(decoded), Arrays.toString(output1)));
+        byteOut = new ByteArrayOutputStream();
+        try (OutputStream out = 
Base45OutputStream.builder().setOutputStream(byteOut).setEncode(false).get()) {
+            for (final byte element : encoded) {
+                out.write(element);
+                out.flush();
+            }
+        }
+        byte[] output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming byte-by-byte flush() 
Base45 decode");
+        byteOut = new ByteArrayOutputStream();
+        OutputStream out = byteOut;
+        for (int i = 0; i < 10; i++) {
+            out = 
Base45OutputStream.builder().setOutputStream(out).setEncode(false).get();
+            out = 
Base45OutputStream.builder().setOutputStream(out).setEncode(true).get();
+        }
+        for (final byte element : decoded) {
+            out.write(element);
+        }
+        out.close();
+        output = byteOut.toByteArray();
+        assertArrayEquals(decoded, output, "Streaming byte-by-byte Base45 
wrap-wrap-wrap!");
+    }
+
+    @Test
+    void testWriteOutOfBounds() throws Exception {
+        final byte[] buf = new byte[1024];
+        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        try (Base45OutputStream out = new Base45OutputStream(bout)) {
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
-1, 1), "Base45OutputStream.write(buf, -1, 1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
1, -1), "Base45OutputStream.write(buf, 1, -1)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
buf.length + 1, 0), "Base45OutputStream.write(buf, buf.length + 1, 0)");
+            assertThrows(IndexOutOfBoundsException.class, () -> out.write(buf, 
buf.length - 1, 2), "Base45OutputStream.write(buf, buf.length - 1, 2)");
+        }
+    }
+
+    @Test
+    void testWriteToNullCoverage() throws Exception {
+        final ByteArrayOutputStream bout = new ByteArrayOutputStream();
+        try (Base45OutputStream out = new Base45OutputStream(bout)) {
+            assertThrows(NullPointerException.class, () -> out.write(null, 0, 
0));
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/codec/binary/Base45Test.java 
b/src/test/java/org/apache/commons/codec/binary/Base45Test.java
new file mode 100644
index 00000000..9348c5b0
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/Base45Test.java
@@ -0,0 +1,944 @@
+/*
+ * 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.codec.binary;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Random;
+
+import org.apache.commons.codec.CodecPolicy;
+import org.apache.commons.codec.DecoderException;
+import org.apache.commons.codec.EncoderException;
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests {@link Base45} as defined by <a 
href="https://www.rfc-editor.org/rfc/rfc9285";>RFC 9285</a>.
+ * <p>
+ * Test vectors are taken from RFC 9285 Section 4.4 and independently verified.
+ * </p>
+ */
+class Base45Test {
+
+    /**
+     * RFC 9285 Section 4.4 official test vectors: {plain-text, 
base45-encoded}. Verified against the RFC 9285 specification.
+     */
+    // @formatter:off
+    private static final String[][] RFC9285_TEST_VECTORS = {
+        // RFC 9285 Section 4.4 Test Vectors
+        { "",        ""            },  // empty
+        { "ietf!",   "QED8WEX0"    },  // RFC 9285 Test Vector 1
+        { "base-45", "UJCLQE7W581" },  // RFC 9285 Test Vector 2
+        // Independently verified test vectors
+        { "AB",      "BB8"         },  // classic two-byte example
+    };
+    // @formatter:on
+
+    private void compare(final byte[] input, final ByteArrayOutputStream baos) 
throws IOException {
+        final byte[] encoded = baos.toByteArray();
+        final ByteArrayInputStream bais = new ByteArrayInputStream(encoded);
+        final ByteArrayOutputStream decodedBaos = new ByteArrayOutputStream();
+        try (Base45InputStream in = new Base45InputStream(bais)) {
+            IOUtils.copy(in, decodedBaos);
+        }
+        assertArrayEquals(input, decodedBaos.toByteArray());
+    }
+
+    /**
+     * Tests that the builder creates a functional Base45 codec.
+     */
+    @Test
+    void testBuilder() {
+        final Base45 codec = Base45.builder().get();
+        assertNotNull(codec);
+        assertEquals("QED8WEX0", 
codec.encodeToString("ietf!".getBytes(StandardCharsets.US_ASCII)));
+    }
+
+    @Test
+    void testBuilderLenientDecodingPolicyThrows() {
+        assertThrows(IllegalArgumentException.class, () -> 
Base45.builder().setDecodingPolicy(CodecPolicy.LENIENT));
+        assertEquals(CodecPolicy.STRICT, 
Base45.builder().setDecodingPolicy(null).get().getCodecPolicy());
+    }
+
+    /**
+     * Tests that {@code setEncodeTable} derives a matching decode table, so a 
codec with a custom alphabet can always decode its own output.
+     */
+    @Test
+    void testBuilderSetEncodeTableDerivesDecodeTable() {
+        final byte[] custom = new byte[45];
+        int k = 0;
+        for (char c = 'a'; c <= 'z'; c++) {
+            custom[k++] = (byte) c;
+        }
+        for (char c = 'A'; c <= 'I'; c++) {
+            custom[k++] = (byte) c;
+        }
+        for (char c = '0'; c <= '9'; c++) {
+            custom[k++] = (byte) c;
+        }
+        final Base45 codec = Base45.builder().setEncodeTable(custom).get();
+        final byte[] input = "hello world".getBytes(StandardCharsets.US_ASCII);
+        final byte[] encoded = codec.encode(input);
+        for (final byte b : encoded) {
+            assertTrue(codec.isInAlphabet(b), "Encoded byte '" + (char) b + "' 
must be in the custom alphabet");
+        }
+        assertArrayEquals(input, codec.decode(encoded), "A custom-alphabet 
codec must be able to decode its own output");
+        // null resets to the default table.
+        assertArrayEquals("QED8WEX0".getBytes(StandardCharsets.US_ASCII),
+                Base45.builder().setEncodeTable((byte[]) 
null).get().encode("ietf!".getBytes(StandardCharsets.US_ASCII)));
+    }
+
+    /**
+     * Tests that {@code setEncodeTable} rejects tables that are not exactly 
45 unique entries.
+     */
+    @Test
+    void testBuilderSetEncodeTableRejectsInvalidTable() {
+        assertThrows(IllegalArgumentException.class, () -> 
Base45.builder().setEncodeTable(new byte[] { 'a', 'b', 'c' }), "wrong size");
+        assertThrows(IllegalArgumentException.class, () -> 
Base45.builder().setEncodeTable(), "zero entries");
+        final byte[] duplicates = new byte[45];
+        Arrays.fill(duplicates, (byte) 'a');
+        assertThrows(IllegalArgumentException.class, () -> 
Base45.builder().setEncodeTable(duplicates), "duplicates");
+    }
+
+    @Test
+    void testBuilderSetLineLength() {
+        assertThrows(UnsupportedOperationException.class, () -> 
Base45.builder().setLineLength(0));
+    }
+
+    @Test
+    void testBuilderSetLineSeparator() {
+        assertThrows(UnsupportedOperationException.class, () -> 
Base45.builder().setLineSeparator((byte) 0));
+    }
+
+    @Test
+    void testBuilderSetPadding() {
+        assertThrows(UnsupportedOperationException.class, () -> 
Base45.builder().setPadding((byte) 0));
+    }
+
+    /**
+     * Tests the builder with strict decoding policy.
+     */
+    @Test
+    void testBuilderStrictDecoding() {
+        final Base45 strict = 
Base45.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
+        assertTrue(strict.isStrictDecoding());
+        assertTrue(new Base45().isStrictDecoding());
+        // Valid data should still decode successfully
+        assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), 
strict.decode("QED8WEX0"));
+    }
+
+    /**
+     * Tests that strict decoding rejects trailing characters that represent 
invalid values.
+     */
+    @Test
+    void testBuilderStrictDecodingRejectsHighPair() {
+        final Base45 strict = 
Base45.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
+        // ':' ':' = 2024 > 255; this should be rejected in both lenient and 
strict modes
+        assertThrows(IllegalArgumentException.class, () -> 
strict.decode("::"), "Strict mode: ':' ':' decodes to 2024 > 255, should be 
rejected");
+    }
+
+    /**
+     * Tests the codec type constants match RFC 9285 requirements.
+     */
+    @Test
+    void testCodecConstants() {
+        assertEquals(3, Base45.BYTES_PER_ENCODED_BLOCK, 
"BYTES_PER_ENCODED_BLOCK should be 3 (3 Base45 chars per 2 bytes)");
+        assertEquals(2, Base45.BYTES_PER_UNENCODED_BLOCK, 
"BYTES_PER_UNENCODED_BLOCK should be 2 (2 bytes per 3 Base45 chars)");
+    }
+
+    /**
+     * Tests that an empty string decodes to an empty byte array.
+     */
+    @Test
+    void testDecodeEmpty() {
+        final Base45 codec = new Base45();
+        assertArrayEquals(new byte[0], codec.decode(""));
+        assertArrayEquals(new byte[0], codec.decode(new byte[0]));
+    }
+
+    /**
+     * Tests that invalid characters cause an exception. RFC 9285: "Receivers 
MUST reject any input string that is not valid Base45 encoding."
+     */
+    @ParameterizedTest
+    @ValueSource(chars = { '!', '"', '#', '&', '\'', '(', ')', ',', ';', '<', 
'=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', 'a', 'z' })
+    void testDecodeInvalidCharacters(final char c) {
+        final Base45 codec = new Base45();
+        // Characters not in the Base45 alphabet
+        final String input = "Q" + c + "D";
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(input), () -> "Should reject character '" + c + "' (ASCII " + 
(int) c + ")");
+    }
+
+    /**
+     * Tests that an encoded input length modulo 3 equal to 1 is rejected. RFC 
9285: "It is an error if the remaining string is 1 character long."
+     */
+    @Test
+    void testDecodeInvalidLengthMod3Equals1() {
+        final Base45 codec = new Base45();
+        assertThrows(IllegalArgumentException.class, () -> codec.decode("Q"), 
"Single character input should be rejected");
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode("QEDB"), "4-character input (length % 3 == 1) should be rejected");
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode("QEDBWEC"), "7-character input (length % 3 == 1) should be 
rejected");
+    }
+
+    /**
+     * Tests decoding of "FGW" -> [0xFF, 0xFF].
+     */
+    @Test
+    void testDecodeMaxMax() {
+        assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF }, new 
Base45().decode("FGW"));
+    }
+
+    /**
+     * Tests that {@link Base45} implements BinaryDecoder correctly via the 
{@code decode(Object)} method.
+     */
+    @Test
+    void testDecodeObject() throws DecoderException {
+        final Base45 codec = new Base45();
+        final byte[] encoded = "QED8WEX0".getBytes(StandardCharsets.US_ASCII);
+        Object result = codec.decode((Object) encoded);
+        assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), 
(byte[]) result);
+        // Also test with String input
+        result = codec.decode((Object) "QED8WEX0");
+        assertArrayEquals("ietf!".getBytes(StandardCharsets.US_ASCII), 
(byte[]) result);
+    }
+
+    /**
+     * Tests that decoding an unsupported Object type throws DecoderException.
+     */
+    @Test
+    void testDecodeObjectThrowsForUnsupportedType() {
+        final Base45 codec = new Base45();
+        assertThrows(DecoderException.class, () -> 
codec.decode(Integer.valueOf(42)));
+    }
+
+    /**
+     * Tests that a 2-character pair that decodes to a value exceeding 255 is 
rejected. The maximum valid single-byte encoding decodes to at most 255. 
However,
+     * some 2-char combos decode to values 256-2024.
+     */
+    @Test
+    void testDecodePairExceedingMaxByte() {
+        final Base45 codec = new Base45();
+        // ':' ':' = 44 + 44*45 = 44 + 1980 = 2024 > 255 -> invalid
+        assertThrows(IllegalArgumentException.class, () -> codec.decode("::"), 
"Pair ':' ':' decodes to 2024 which exceeds 255 and should be rejected");
+        // ':' '6' = 44 + 6*45 = 44 + 270 = 314 > 255 -> invalid
+        assertThrows(IllegalArgumentException.class, () -> codec.decode(":6"), 
"Pair ':' '6' decodes to 314 which exceeds 255 and should be rejected");
+    }
+
+    /**
+     * Tests the RFC 9285 Section 4.4 decoding test vectors.
+     */
+    @Test
+    void testDecodeRfc9285TestVectors() {
+        final Base45 codec = new Base45();
+        for (final String[] testCase : RFC9285_TEST_VECTORS) {
+            final String plainText = testCase[0];
+            final String encoded = testCase[1];
+            final byte[] expected = 
plainText.getBytes(StandardCharsets.ISO_8859_1);
+            final byte[] actual = 
codec.decode(encoded.getBytes(StandardCharsets.US_ASCII));
+            assertArrayEquals(expected, actual, "RFC 9285 decode failed for: 
'" + encoded + "'");
+        }
+    }
+
+    /**
+     * Tests decoding of "U5" -> [0xFF] (single byte 255).
+     */
+    @Test
+    void testDecodeSingleMax() {
+        assertArrayEquals(new byte[] { (byte) 0xFF }, new 
Base45().decode("U5"));
+    }
+
+    /**
+     * Tests decoding of "00" -> [0x00] (single byte 0).
+     */
+    @Test
+    void testDecodeSingleZero() {
+        assertArrayEquals(new byte[] { 0 }, new Base45().decode("00"));
+    }
+
+    /**
+     * Tests that non-alphabet whitespace is rejected, including within groups 
and at the start and end of the input.
+     */
+    @ParameterizedTest
+    @ValueSource(strings = { "\r", "\n", "\t", "\r\n", "\u000B", "\f", 
"\u001C", "\u001D", "\u001E", "\u001F" })
+    void testDecodeRejectsNonAlphabetWhitespace(final String whitespace) {
+        final Base45 codec = new Base45();
+        assertThrows(IllegalArgumentException.class, () -> 
codec.decode(whitespace));
+        final String encoded = "QED8WEX0";
+        for (int i = 0; i <= encoded.length(); i++) {
+            final String input = encoded.substring(0, i) + whitespace + 
encoded.substring(i);
+            assertThrows(IllegalArgumentException.class, () -> 
codec.decode(input));
+            assertThrows(IllegalArgumentException.class, () -> 
codec.decode(input.getBytes(StandardCharsets.US_ASCII)));
+        }
+    }
+
+    /**
+     * Tests that spaces (ASCII 32) ARE in the Base45 alphabet and are NOT 
skipped during decoding. Space has alphabet value 36.
+     */
+    @Test
+    void testDecodeSpaceIsInAlphabet() {
+        final Base45 codec = new Base45();
+        // ' ' has value 36 in Base45 alphabet
+        // Verify space is in alphabet
+        assertTrue(codec.isInAlphabet((byte) ' '), "Space should be in Base45 
alphabet");
+        assertArrayEquals("Hello!!".getBytes(StandardCharsets.US_ASCII), 
codec.decode("%69 VD92EX0"));
+        // Round-trip test for data that encodes to/contains a space
+        final byte[] input = { (byte) 0xF0, (byte) 0xF0 }; // Some value that 
produces a space in output
+        final byte[] encoded = codec.encode(input);
+        final byte[] decoded = codec.decode(encoded);
+        assertArrayEquals(input, decoded, "Round-trip with space in encoding 
failed");
+    }
+
+    /**
+     * Tests decoding via the String overload of {@code decode}.
+     */
+    @Test
+    void testDecodeStringRfc9285TestVectors() {
+        final Base45 codec = new Base45();
+        for (final String[] testCase : RFC9285_TEST_VECTORS) {
+            final String plainText = testCase[0];
+            final String encoded = testCase[1];
+            final byte[] expected = 
plainText.getBytes(StandardCharsets.ISO_8859_1);
+            final byte[] actual = codec.decode(encoded);
+            assertArrayEquals(expected, actual, () -> "RFC 9285 decode(String) 
failed for: '" + encoded + "'");
+        }
+    }
+
+    /**
+     * Tests that the DECODE_TABLE has exactly 128 entries (covering the full 
ASCII range).
+     */
+    @Test
+    void testDecodeTableLength() {
+        assertEquals(128, Base45.DECODE_TABLE.length);
+    }
+
+    /**
+     * Tests that an encoded 3-character triple that decodes to a value 
exceeding 65535 is rejected. The maximum valid 3-character value is 44 + 44*45 
+ 44*2025
+     * = 91124, which exceeds 65535.
+     */
+    @Test
+    void testDecodeTripleExceedingMaxValue() {
+        final Base45 codec = new Base45();
+        // ':' ':' ':' = value 44, 44*45, 44*2025 = 44 + 1980 + 89100 = 91124 
> 65535
+        // In the alphabet ':' = 44 (last entry)
+        assertThrows(IllegalArgumentException.class, () -> codec.decode(":::"),
+                "Triple ':' ':' ':' decodes to 91124 which exceeds 65535 and 
should be rejected");
+    }
+
+    /**
+     * Tests decoding of "000" -> [0x00, 0x00].
+     */
+    @Test
+    void testDecodeZeroZero() {
+        assertArrayEquals(new byte[] { 0, 0 }, new Base45().decode("000"));
+    }
+
+    /**
+     * Tests encoding of "AB" -> "BB8". A=65, B=66: n = 65*256+66 = 16706; 
c=11('B'), d=11('B'), e=8('8') -> "BB8"
+     */
+    @Test
+    void testEncodeAB() {
+        final byte[] input = "AB".getBytes(StandardCharsets.US_ASCII);
+        assertEquals("BB8", new Base45().encodeToString(input));
+        assertArrayEquals(input, new Base45().decode("BB8"));
+    }
+
+    /**
+     * Tests the {@link Base45#encodeAsString(byte[])} convenience method.
+     */
+    @Test
+    void testEncodeAsString() {
+        final Base45 codec = new Base45();
+        final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
+        assertEquals("QED8WEX0", codec.encodeAsString(input));
+        assertEquals(codec.encodeToString(input), codec.encodeAsString(input));
+    }
+
+    /**
+     * Tests encoding of all possible single-byte values for round-trip 
correctness.
+     */
+    @Test
+    void testEncodeDecodeSingleByteRoundTrip() {
+        final Base45 codec = new Base45();
+        for (int i = 0; i <= 255; i++) {
+            final byte[] input = { (byte) i };
+            final byte[] encoded = codec.encode(input);
+            assertEquals(2, encoded.length, "Single byte should encode to 2 
chars, byte value: " + i);
+            final byte[] decoded = codec.decode(encoded);
+            assertArrayEquals(input, decoded, "Round-trip failed for byte 
value: " + i);
+        }
+    }
+
+    /**
+     * Verifies that all 45 ENCODE_TABLE entries are in the DECODE_TABLE with 
the correct index.
+     */
+    @Test
+    void testEncodeDecodeTableConsistency() {
+        final byte[] encodeTable = Base45.ENCODE_TABLE;
+        final byte[] decodeTable = Base45.DECODE_TABLE;
+        for (int i = 0; i < encodeTable.length; i++) {
+            final int encoded = encodeTable[i] & 0xFF;
+            assertTrue(encoded < decodeTable.length, "Encode table char " + 
(char) encoded + " at index " + i + " exceeds decode table length");
+            assertEquals(i, decodeTable[encoded], "Decode table mismatch for 
char '" + (char) encoded + "' at encode index " + i);
+        }
+    }
+
+    /**
+     * Tests encoding of all possible two-byte values for round-trip 
correctness. Checks a sample to avoid exhaustive O(65536) iterations being slow.
+     */
+    @Test
+    void testEncodeDecodeTwoByteRoundTrip() {
+        final Base45 codec = new Base45();
+        // Test specific important values
+        final int[] interestingValues = { 0, 1, 44, 45, 254, 255, 256, 2024, 
2025, 65534, 65535 };
+        for (final int n : interestingValues) {
+            final byte[] input = { (byte) (n >> 8), (byte) (n & 0xFF) };
+            final byte[] encoded = codec.encode(input);
+            assertEquals(3, encoded.length, "Two bytes should encode to 3 
chars, n=" + n);
+            final byte[] decoded = codec.decode(encoded);
+            assertArrayEquals(input, decoded, "Round-trip failed for two-byte 
value n=" + n);
+        }
+    }
+
+    /**
+     * Tests that the codec correctly handles inputs where encoded output 
contains spaces (space = Base45 value 36), ensuring they are preserved through 
the
+     * encode-decode cycle.
+     */
+    @Test
+    void testEncodeDecodeWithSpaceInOutput() {
+        final Base45 codec = new Base45();
+        // Find byte pairs that encode to include a space (' ' = value 36):
+        // We need n such that n % 45 == 36, or (n/45) % 45 == 36, or n/2025 
== 36.
+        // For n % 45 == 36: e.g., n = 36 -> b0=0, b1=36
+        final byte[] input = { 0, 36 }; // n = 36, first char = ' '
+        final byte[] encoded = codec.encode(input);
+        assertTrue(codec.isInAlphabet(encoded[0]), "First encoded char should 
be in alphabet");
+        assertEquals((byte) ' ', encoded[0], "First encoded char should be 
space (value 36)");
+        assertArrayEquals(input, codec.decode(encoded));
+    }
+
+    /**
+     * Tests the relationship between input length and encoded length. Per RFC 
9285: encoded_length = (n / 2) * 3 + (n % 2 != 0 ? 2 : 0).
+     */
+    @Test
+    void testEncodedLength() {
+        final Base45 codec = new Base45();
+        final int[] inputLengths = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 100, 
1000 };
+        for (final int n : inputLengths) {
+            final byte[] input = new byte[n];
+            final long expected = (long) n / 2 * 3 + (n % 2 != 0 ? 2 : 0);
+            assertEquals(expected, codec.getEncodedLength(input), 
"getEncodedLength incorrect for input length " + n);
+        }
+    }
+
+    /**
+     * Tests that encoded output contains only valid Base45 characters.
+     */
+    @Test
+    void testEncodedOutputIsInAlphabet() {
+        final Base45 codec = new Base45();
+        final Random rng = new Random(42L);
+        for (int len = 0; len <= 100; len++) {
+            final byte[] input = new byte[len];
+            rng.nextBytes(input);
+            final byte[] encoded = codec.encode(input);
+            for (final byte b : encoded) {
+                assertTrue(codec.isInAlphabet(b), "Encoded byte " + b + " ('" 
+ (char) b + "') is not in Base45 alphabet");
+            }
+        }
+    }
+
+    /**
+     * Tests that an empty byte array encodes to an empty byte array.
+     */
+    @Test
+    void testEncodeEmpty() {
+        final Base45 codec = new Base45();
+        assertArrayEquals(new byte[0], codec.encode(new byte[0]));
+        assertEquals("", codec.encodeToString(new byte[0]));
+    }
+
+    /**
+     * Tests that [0xFF, 0xFF] encodes to "FGW". n=65535: c=15('F'), 
d=16('G'), e=32('W') -> "FGW"
+     */
+    @Test
+    void testEncodeMaxMax() {
+        final byte[] input = { (byte) 0xFF, (byte) 0xFF };
+        assertEquals("FGW", new Base45().encodeToString(input));
+    }
+
+    /**
+     * Tests that {@link Base45} implements BinaryEncoder correctly via the 
{@code encode(Object)} method.
+     */
+    @Test
+    void testEncodeObject() throws EncoderException {
+        final Base45 codec = new Base45();
+        final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
+        final Object result = codec.encode((Object) input);
+        assertArrayEquals("QED8WEX0".getBytes(StandardCharsets.US_ASCII), 
(byte[]) result);
+    }
+
+    /**
+     * Tests that encoding a non-byte-array Object throws EncoderException.
+     */
+    @Test
+    void testEncodeObjectThrowsForNonByteArray() {
+        final Base45 codec = new Base45();
+        assertThrows(EncoderException.class, () -> codec.encode("not a byte 
array"));
+    }
+
+    /**
+     * Pins the {@code encode} postcondition that {@code Context#ibitWorkArea} 
is reset after each complete 2-byte group is emitted.
+     * <p>
+     * Regression guard: without the reset, the work area would hold stale 
high bits from previous groups and output correctness would depend solely on the
+     * {@code & 0xFFFF}/{@code & 0xFF} output masks.
+     * </p>
+     */
+    @Test
+    void testEncodeResetsWorkAreaAfterCompleteGroup() {
+        final Base45 codec = new Base45();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        final byte[] data = { 1, 2, 3, 4, 5, 6 }; // three complete 2-byte 
groups
+        codec.encode(data, 0, data.length, context);
+        assertEquals(0, context.modulus, "modulus must be 0 after an even 
number of encoded bytes");
+        assertEquals(0, context.ibitWorkArea, "encode() must reset 
ibitWorkArea after emitting each complete group, not leave stale accumulator 
data");
+    }
+
+    /**
+     * Tests the RFC 9285 Section 4.4 encoding test vectors.
+     */
+    @Test
+    void testEncodeRfc9285TestVectors() {
+        final Base45 codec = new Base45();
+        for (final String[] testCase : RFC9285_TEST_VECTORS) {
+            final String plainText = testCase[0];
+            final String expected = testCase[1];
+            final byte[] input = 
plainText.getBytes(StandardCharsets.ISO_8859_1);
+            final String actual = codec.encodeToString(input);
+            assertEquals(expected, actual, "RFC 9285 encode failed for: '" + 
plainText + "'");
+        }
+    }
+
+    /**
+     * Tests encoding of a single byte with value 65 ('A'). n=65: 65%45=20 
('K'), 65/45=1 ('1') -> "K1"
+     */
+    @Test
+    void testEncodeSingleByteLetterA() {
+        final byte[] input = "A".getBytes(StandardCharsets.US_ASCII); // 'A' = 
65
+        assertEquals("K1", new Base45().encodeToString(input));
+        assertArrayEquals(input, new Base45().decode("K1"));
+    }
+
+    /**
+     * Tests encoding of a single byte with value 1. n=1: 1%45=1 ('1'), 1/45=0 
('0') -> "10"
+     */
+    @Test
+    void testEncodeSingleByteOne() {
+        final byte[] input = { 1 };
+        assertEquals("10", new Base45().encodeToString(input));
+        assertArrayEquals(input, new Base45().decode("10"));
+    }
+
+    /**
+     * Tests encoding of a single max-value byte (0xFF = 255). n=255: 
255%45=30('U'), 255/45=5('5') -> "U5"
+     */
+    @Test
+    void testEncodeSingleMaxByte() {
+        final byte[] input = { (byte) 0xFF };
+        assertEquals("U5", new Base45().encodeToString(input));
+    }
+
+    /**
+     * Tests encoding of a single zero byte. n=0: c=0('0'), d=0('0') -> "00"
+     */
+    @Test
+    void testEncodeSingleZeroByte() {
+        final byte[] input = { 0 };
+        assertEquals("00", new Base45().encodeToString(input));
+    }
+
+    /**
+     * Verifies the first 10 entries (digits 0-9) of the ENCODE_TABLE.
+     */
+    @Test
+    void testEncodeTableDigits() {
+        for (int i = 0; i <= 9; i++) {
+            assertEquals((byte) ('0' + i), Base45.ENCODE_TABLE[i], 
"ENCODE_TABLE[" + i + "] should be digit '" + (char) ('0' + i) + "'");
+        }
+    }
+
+    /**
+     * Verifies that the ENCODE_TABLE has exactly 45 entries.
+     */
+    @Test
+    void testEncodeTableHas45Entries() {
+        assertEquals(45, Base45.ENCODE_TABLE.length);
+    }
+
+    /**
+     * Verifies the special-character entries 36-44 of the ENCODE_TABLE.
+     */
+    @Test
+    void testEncodeTableSpecialChars() {
+        assertEquals((byte) ' ', Base45.ENCODE_TABLE[36], "ENCODE_TABLE[36] 
should be space");
+        assertEquals((byte) '$', Base45.ENCODE_TABLE[37], "ENCODE_TABLE[37] 
should be '$'");
+        assertEquals((byte) '%', Base45.ENCODE_TABLE[38], "ENCODE_TABLE[38] 
should be '%'");
+        assertEquals((byte) '*', Base45.ENCODE_TABLE[39], "ENCODE_TABLE[39] 
should be '*'");
+        assertEquals((byte) '+', Base45.ENCODE_TABLE[40], "ENCODE_TABLE[40] 
should be '+'");
+        assertEquals((byte) '-', Base45.ENCODE_TABLE[41], "ENCODE_TABLE[41] 
should be '-'");
+        assertEquals((byte) '.', Base45.ENCODE_TABLE[42], "ENCODE_TABLE[42] 
should be '.'");
+        assertEquals((byte) '/', Base45.ENCODE_TABLE[43], "ENCODE_TABLE[43] 
should be '/'");
+        assertEquals((byte) ':', Base45.ENCODE_TABLE[44], "ENCODE_TABLE[44] 
should be ':'");
+    }
+
+    /**
+     * Verifies entries 10-35 (uppercase A-Z) of the ENCODE_TABLE.
+     */
+    @Test
+    void testEncodeTableUppercase() {
+        for (int i = 0; i < 26; i++) {
+            assertEquals((byte) ('A' + i), Base45.ENCODE_TABLE[10 + i], 
"ENCODE_TABLE[" + (10 + i) + "] should be letter '" + (char) ('A' + i) + "'");
+        }
+    }
+
+    /**
+     * Tests compatibility with the {@code encode(byte[], int, int)} overload.
+     */
+    @Test
+    void testEncodeWithOffsetAndLength() {
+        final Base45 codec = new Base45();
+        final byte[] buffer = new byte[10];
+        // Fill with "ietf!" bytes at offset 2
+        final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
+        System.arraycopy(input, 0, buffer, 2, input.length);
+        final byte[] encoded = codec.encode(buffer, 2, 5);
+        assertEquals("QED8WEX0", new String(encoded, 
StandardCharsets.US_ASCII));
+    }
+
+    /**
+     * Companion to {@link #testEncodeResetsWorkAreaAfterCompleteGroup}: with 
a pending (odd) byte, the work area must hold exactly that pending byte, nothing
+     * more.
+     * <p>
+     * Regression guard for the {@code Base45#encode} work-area reset.
+     * </p>
+     */
+    @Test
+    void testEncodeWorkAreaHoldsOnlyPendingByte() {
+        final Base45 codec = new Base45();
+        final BaseNCodec.Context context = new BaseNCodec.Context();
+        final byte[] data = { 1, 2, 3, 4, 99 }; // two complete groups + one 
pending byte (99)
+        codec.encode(data, 0, data.length, context);
+        assertEquals(1, context.modulus, "one pending byte must leave modulus 
== 1");
+        assertEquals(99, context.ibitWorkArea, "after two complete groups, 
ibitWorkArea must hold only the pending byte, not stale high bits");
+    }
+
+    /**
+     * Tests that two zero bytes encode to "000". n=0: c=0('0'), d=0('0'), 
e=0('0') -> "000"
+     */
+    @Test
+    void testEncodeZeroZero() {
+        final byte[] input = { 0, 0 };
+        assertEquals("000", new Base45().encodeToString(input));
+    }
+
+    /**
+     * Tests {@link Base45#getEncodedLength(byte[])} for known input lengths.
+     */
+    @Test
+    void testGetEncodedLength() {
+        final Base45 codec = new Base45();
+        assertEquals(0L, codec.getEncodedLength(new byte[0])); // 0 bytes -> 0 
chars
+        assertEquals(2L, codec.getEncodedLength(new byte[1])); // 1 byte -> 2 
chars
+        assertEquals(3L, codec.getEncodedLength(new byte[2])); // 2 bytes -> 3 
chars
+        assertEquals(5L, codec.getEncodedLength(new byte[3])); // 3 bytes -> 5 
chars
+        assertEquals(6L, codec.getEncodedLength(new byte[4])); // 4 bytes -> 6 
chars
+        assertEquals(8L, codec.getEncodedLength(new byte[5])); // 5 bytes -> 8 
chars
+        assertEquals(9L, codec.getEncodedLength(new byte[6])); // 6 bytes -> 9 
chars
+        assertEquals(11L, codec.getEncodedLength(new byte[7])); // 7 bytes -> 
11 chars
+    }
+
+    /**
+     * Tests that the actual encoded length matches the value returned by 
getEncodedLength.
+     */
+    @Test
+    void testGetEncodedLengthMatchesActual() {
+        final Base45 codec = new Base45();
+        for (int len = 0; len <= 30; len++) {
+            final byte[] input = new byte[len];
+            Arrays.fill(input, (byte) 0xAB);
+            final byte[] encoded = codec.encode(input);
+            assertEquals(codec.getEncodedLength(input), encoded.length, 
"getEncodedLength disagrees with actual length for input length " + len);
+        }
+    }
+
+    /**
+     * Tests {@link Base45#isInAlphabet(byte)} for all 45 valid alphabet 
characters.
+     */
+    @Test
+    void testIsInAlphabet_allValidChars() {
+        final Base45 codec = new Base45();
+        final String alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ 
$%*+-./:";
+        assertEquals(45, alphabet.length());
+        for (final char c : alphabet.toCharArray()) {
+            assertTrue(codec.isInAlphabet((byte) c), "Character '" + c + "' 
(ASCII " + (int) c + ") should be in Base45 alphabet");
+        }
+    }
+
+    /**
+     * Tests {@link Base45#isInAlphabet(byte)} for characters NOT in the 
Base45 alphabet.
+     */
+    @Test
+    void testIsInAlphabet_invalidChars() {
+        final Base45 codec = new Base45();
+        // Control characters
+        for (int i = 0; i < 32; i++) {
+            if (i != ' ') { // space is at 32
+                // None of 0-31 are in the alphabet
+                assertFalse(codec.isInAlphabet((byte) i), "Control char " + i 
+ " should not be in Base45 alphabet");
+            }
+        }
+        // Characters between valid ranges
+        assertFalse(codec.isInAlphabet((byte) '!'), "! should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '"'), "\" should not be in 
Base45 alphabet");
+        assertFalse(codec.isInAlphabet((byte) '#'), "# should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '&'), "& should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '\''), "' should not be in 
Base45 alphabet");
+        assertFalse(codec.isInAlphabet((byte) '('), "( should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) ')'), ") should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) ','), ", should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) ';'), "; should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '<'), "< should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '='), "= should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '>'), "> should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '?'), "? should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '@'), "@ should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '['), "[ should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '\\'), "\\ should not be in 
Base45 alphabet");
+        assertFalse(codec.isInAlphabet((byte) ']'), "] should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '^'), "^ should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '_'), "_ should not be in Base45 
alphabet");
+        assertFalse(codec.isInAlphabet((byte) '`'), "` should not be in Base45 
alphabet");
+        // lowercase letters
+        for (char c = 'a'; c <= 'z'; c++) {
+            assertFalse(codec.isInAlphabet((byte) c), "Lowercase '" + c + "' 
should not be in Base45 alphabet");
+        }
+        // High bytes (> 127)
+        assertFalse(codec.isInAlphabet((byte) 0x80), "Byte 0x80 should not be 
in Base45 alphabet");
+        assertFalse(codec.isInAlphabet((byte) 0xFF), "Byte 0xFF should not be 
in Base45 alphabet");
+    }
+
+    /**
+     * Tests that the pad character is NOT treated as part of the alphabet, 
because Base45 (RFC 9285) has no padding, while whitespace is still honored per 
the
+     * {@code allowWhitespacePad} flag. Decoding still rejects non-alphabet 
whitespace.
+     */
+    @Test
+    void testIsInAlphabetArrayDoesNotAllowPad() {
+        final Base45 codec = new Base45();
+        assertFalse(codec.isInAlphabet("QED="), "'=' (inherited pad) must not 
be treated as in-alphabet for padding-less Base45");
+        
assertFalse(codec.isInAlphabet("QED=".getBytes(StandardCharsets.US_ASCII), 
true));
+        
assertFalse(codec.isInAlphabet("QED=".getBytes(StandardCharsets.US_ASCII), 
false));
+        
assertTrue(codec.isInAlphabet("QED\t8WE".getBytes(StandardCharsets.US_ASCII), 
true));
+        
assertFalse(codec.isInAlphabet("QED\t8WE".getBytes(StandardCharsets.US_ASCII), 
false));
+        
assertTrue(codec.isInAlphabet("QED8WEX0".getBytes(StandardCharsets.US_ASCII), 
true));
+    }
+
+    /**
+     * Tests {@link Base45#isInAlphabet(byte[])} for valid and invalid arrays.
+     */
+    @Test
+    void testIsInAlphabetByteArray() {
+        final Base45 codec = new Base45();
+        
assertTrue(codec.isInAlphabet("QED8WEX0".getBytes(StandardCharsets.US_ASCII), 
false));
+        assertTrue(codec.isInAlphabet(new byte[0], false));
+        
assertFalse(codec.isInAlphabet("QED!WEX0".getBytes(StandardCharsets.US_ASCII), 
false));
+        
assertFalse(codec.isInAlphabet("abc".getBytes(StandardCharsets.US_ASCII), 
false));
+    }
+
+    /**
+     * Tests that long inputs (more than 2 encoding blocks) encode and decode 
correctly.
+     */
+    @Test
+    void testLongInputRoundTrip() {
+        final Base45 codec = new Base45();
+        final byte[] input = new byte[1000];
+        new Random(99999L).nextBytes(input);
+        final byte[] encoded = codec.encode(input);
+        assertEquals(codec.getEncodedLength(input), encoded.length);
+        assertArrayEquals(input, codec.decode(encoded));
+    }
+
+    /**
+     * Verifies RFC 9285 Test Vector 1: "ietf!" encodes to "QED8WEX0".
+     * <p>
+     * Manually verified:
+     * <ul>
+     * <li>Group [i=105, e=101]: n=26981; c=26 (Q), d=14 (E), e=13 (D) -> 
"QED"</li>
+     * <li>Group [t=116, f=102]: n=29798; c=8 (8), d=32 (W), e=14 (E) -> 
"8WE"</li>
+     * <li>Tail [!=33]: n=33; c=33 (X), d=0 (0) -> "X0"</li>
+     * </ul>
+     */
+    @Test
+    void testRfc9285TestVector1_ietf() {
+        final byte[] input = "ietf!".getBytes(StandardCharsets.US_ASCII);
+        assertEquals("QED8WEX0", new Base45().encodeToString(input));
+        assertArrayEquals(input, new Base45().decode("QED8WEX0"));
+    }
+
+    /**
+     * Verifies RFC 9285 Test Vector 2: "base-45" encodes to "UJCLQE7W581".
+     * <p>
+     * Manually verified:
+     * <ul>
+     * <li>Group [b=98, a=97]: n=25185; c=30 (U), d=19 (J), e=12 (C) -> 
"UJC"</li>
+     * <li>Group [s=115, e=101]: n=29541; c=21 (L), d=26 (Q), e=14 (E) -> 
"LQE"</li>
+     * <li>Group [-=45, 4=52]: n=11572; c=7 (7), d=32 (W), e=5 (5) -> 
"7W5"</li>
+     * <li>Tail [5=53]: n=53; c=8 (8), d=1 (1) -> "81"</li>
+     * </ul>
+     */
+    @Test
+    void testRfc9285TestVector2_base45() {
+        final byte[] input = "base-45".getBytes(StandardCharsets.US_ASCII);
+        assertEquals("UJCLQE7W581", new Base45().encodeToString(input));
+        assertArrayEquals(input, new Base45().decode("UJCLQE7W581"));
+    }
+
+    /**
+     * Tests round-trip for binary data with all byte values.
+     */
+    @Test
+    void testRoundTripAllByteValues() {
+        final Base45 codec = new Base45();
+        final byte[] allBytes = new byte[256];
+        for (int i = 0; i < 256; i++) {
+            allBytes[i] = (byte) i;
+        }
+        final byte[] encoded = codec.encode(allBytes);
+        final byte[] decoded = codec.decode(encoded);
+        assertArrayEquals(allBytes, decoded, "Round-trip failed for all byte 
values");
+    }
+
+    /**
+     * Tests round-trip encoding/decoding of all byte lengths from 0 to 50.
+     */
+    @Test
+    void testRoundTripAllLengths() {
+        final Base45 codec = new Base45();
+        final Random rng = new Random(12345L);
+        for (int len = 0; len <= 50; len++) {
+            final byte[] input = new byte[len];
+            rng.nextBytes(input);
+            final byte[] encoded = codec.encode(input);
+            final byte[] decoded = codec.decode(encoded);
+            assertArrayEquals(input, decoded, "Round-trip failed for length " 
+ len);
+        }
+    }
+
+    /**
+     * Tests round-trip for various well-known strings.
+     */
+    @Test
+    void testRoundTripStrings() {
+        final Base45 codec = new Base45();
+        // @formatter:off
+        final String[] inputs = {
+            "Hello, World!",
+            "The quick brown fox jumps over the lazy dog",
+            "Apache Commons Codec",
+            "0123456789",
+            "\u0000\u0001\u0002",
+            "Base45 (RFC 9285)",
+        };
+        // @formatter:on
+        for (final String input : inputs) {
+            final byte[] bytes = input.getBytes(StandardCharsets.UTF_8);
+            final byte[] decoded = codec.decode(codec.encode(bytes));
+            assertArrayEquals(bytes, decoded, () -> "Round-trip failed for: " 
+ input);
+        }
+    }
+
+    /**
+     * Tests that encoding a single byte and then decoding gives the correct 
value for boundary cases around multiples of 45.
+     */
+    @Test
+    void testSingleByteAroundBase45Multiples() {
+        final Base45 codec = new Base45();
+        // Test byte values at multiples of 45: 0, 45, 90, 135, 180, 225
+        for (int i = 0; i <= 255; i += 45) {
+            final byte[] input = { (byte) i };
+            final byte[] encoded = codec.encode(input);
+            assertEquals(2, encoded.length, "Encoded length should be 2 for 
single byte, value=" + i);
+            assertArrayEquals(input, codec.decode(encoded), "Round-trip failed 
for single byte value=" + i);
+        }
+    }
+
+    /**
+     * Tests streaming encode/decode with non-aligned chunk sizes to verify 
the encoder accumulator is correctly reset between blocks.
+     */
+    @Test
+    void testStreamingEncodeDecodeIncremental() throws IOException {
+        final byte[] input = "The quick brown fox jumps over the lazy 
dog".getBytes(StandardCharsets.UTF_8);
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (Base45OutputStream out = new Base45OutputStream(baos)) {
+            // Write in 3-byte chunks, which is not aligned to the 2-byte 
Base45 block size
+            for (int i = 0; i < input.length; i += 3) {
+                final int len = Math.min(3, input.length - i);
+                out.write(input, i, len);
+            }
+        }
+        compare(input, baos);
+    }
+
+    /**
+     * Tests streaming encode/decode with 1-byte writes to verify the encoder 
accumulator is correctly reset between blocks.
+     */
+    @Test
+    void testStreamingEncodeDecodeOneByteChunks() throws IOException {
+        final byte[] input = "The quick brown fox jumps over the lazy 
dog".getBytes(StandardCharsets.UTF_8);
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (Base45OutputStream out = new Base45OutputStream(baos)) {
+            for (final byte element : input) {
+                out.write(element);
+            }
+        }
+        compare(input, baos);
+    }
+
+    /**
+     * Tests the encoding/decoding of two-byte pairs that produce all three 
encoded characters at the extremes of the Base45 alphabet (e.g., '0' and ':').
+     */
+    @Test
+    void testTwoByteExtremeValues() {
+        final Base45 codec = new Base45();
+        // Value 0: n=0 -> "000"
+        assertArrayEquals(new byte[] { 0, 0 }, codec.decode("000"));
+        assertEquals("000", codec.encodeToString(new byte[] { 0, 0 }));
+        // Value 65535: n=65535 -> "FGW" (verified: 15 + 16*45 + 32*2025 = 
65535)
+        assertArrayEquals(new byte[] { (byte) 0xFF, (byte) 0xFF }, 
codec.decode("FGW"));
+        assertEquals("FGW", codec.encodeToString(new byte[] { (byte) 0xFF, 
(byte) 0xFF }));
+    }
+}

Reply via email to