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 8b506a1c Base32 and Base64 STRICT decoding now require the encoder's
canonical alphabet, padding, and line separators, and validate streams through
EOF. Use LENIENT to retain permissive decoding.
8b506a1c is described below
commit 8b506a1ce5f6fc4bc238657838081b514874d459
Author: Gary Gregory <[email protected]>
AuthorDate: Thu Sep 17 19:25:35 2026 -0700
Base32 and Base64 STRICT decoding now require the encoder's canonical
alphabet, padding, and line separators, and validate streams through
EOF. Use LENIENT to retain permissive decoding.
---
src/changes/changes.xml | 1 +
.../org/apache/commons/codec/binary/Base32.java | 34 +--
.../org/apache/commons/codec/binary/Base64.java | 55 +++--
.../apache/commons/codec/binary/BaseNCodec.java | 137 +++++++++---
.../codec/binary/BaseNCodecInputStream.java | 7 +-
.../codec/binary/BaseNCodecOutputStream.java | 7 +-
.../codec/binary/Base32OutputStreamTest.java | 15 +-
.../codec/binary/BaseNStrictDecodingTest.java | 242 +++++++++++++++++++++
8 files changed, 416 insertions(+), 82 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index b7a31c31..76545139 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -54,6 +54,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Gary Gregory">Throw
IOException instead of IllegalArgumentException in BaseNCodecOutputStream and
BaseNCodecOutputStream IO methods.</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">Throw
DecoderException instead of IllegalArgumentException in
RFC1522Codec.decodeText(String).</action>
<action type="fix" dev="ggregory" due-to="Gary Gregory">Fix Blake3 KDF
example and clarify finalization semantics.</action>
+ <action type="fix" dev="ggregory" due-to="Gary Gregory">Base32 and
Base64 STRICT decoding now require the encoder's canonical alphabet, padding,
and line separators, and validate streams through EOF. Use LENIENT to retain
permissive decoding.</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">Add and use
PhoneticEngine.Builder and deprecate old constructors.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add
BeiderMorseEncoder.Builder and deprecate old constructor.</action>
diff --git a/src/main/java/org/apache/commons/codec/binary/Base32.java
b/src/main/java/org/apache/commons/codec/binary/Base32.java
index 40a489c5..142178a8 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base32.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base32.java
@@ -99,7 +99,7 @@ public class Base32 extends BaseNCodec {
/**
* Sets the encode table and derives the matching decode table.
* <p>
- * The RFC 4648 Base32 and Base32 Hex tables keep their
case-insensitive decoders.
+ * The RFC 4648 Base32 and Base32 Hex tables keep their
case-insensitive decoders in lenient mode. Strict decoding requires the
encoding alphabet.
* </p>
*
* @param encodeTable The encode table with exactly 32 unique entries,
null resets to the default.
@@ -335,7 +335,7 @@ public class Base32 extends BaseNCodec {
private final int encodeSize;
/**
- * Line separator for encoding. Not used when decoding. Only used if
lineLength > 0.
+ * Line separator for encoding and strict decoding. Only used if
lineLength > 0.
*/
private final byte[] lineSeparator;
@@ -431,7 +431,7 @@ public class Base32 extends BaseNCodec {
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 8). If lineLength <=
0, then
- * the output will not be divided into lines (chunks).
Ignored when decoding.
+ * the output will not be divided into lines (chunks).
Ignored when decoding leniently.
* @deprecated Use {@link #builder()} and {@link Builder}.
*/
@Deprecated
@@ -449,7 +449,7 @@ public class Base32 extends BaseNCodec {
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 8). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @throws IllegalArgumentException Thrown when the {@code lineSeparator}
contains Base32 characters.
* @deprecated Use {@link #builder()} and {@link Builder}.
@@ -469,7 +469,7 @@ public class Base32 extends BaseNCodec {
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 8). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @param useHex
* <ul>
@@ -496,7 +496,7 @@ public class Base32 extends BaseNCodec {
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 8). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @param useHex
* <ul>
@@ -524,7 +524,7 @@ public class Base32 extends BaseNCodec {
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 8). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @param useHex
* <ul>
@@ -555,11 +555,11 @@ public class Base32 extends BaseNCodec {
/**
* <p>
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
Should be called at least twice: once with the data to decode, and once with
- * inAvail set to "-1" to alert decoder that EOF has been reached. The
"-1" call is not necessary when decoding, but it doesn't hurt, either.
+ * inAvail set to "-1" to alert decoder that EOF has been reached. Strict
decoding requires the "-1" call to validate the complete input.
* </p>
* <p>
- * Ignores all non-Base32 characters. This is how chunked (for example 76
character) data is handled, since CR and LF are silently ignored, but has
implications
- * for other bytes, too. This method subscribes to the garbage-in,
garbage-out philosophy: it will not check the provided data for validity.
+ * Lenient decoding ignores non-alphabet characters and stops at the first
padding byte. Strict decoding accepts only the canonical form produced by this
+ * instance's encoder, including its alphabet, padding, and line
separators.
* </p>
* <p>
* Output is written to {@link BaseNCodec.Context#buffer Context#buffer}
as 8-bit octets, using
@@ -580,11 +580,18 @@ public class Base32 extends BaseNCodec {
}
if (inAvail < 0) {
context.eof = true;
+ if (isStrictDecoding()) {
+ validateCanonicalEnd(true, context);
+ }
}
final int decodeSize = this.encodeSize - 1;
for (int i = 0; i < inAvail; i++) {
final int b = input[inPos++] & 0xff;
- if (b == (pad & 0xff)) {
+ if (isStrictDecoding()) {
+ if (!validateCanonicalByte(b, lineSeparator, true, context)) {
+ continue;
+ }
+ } else if (b == (pad & 0xff)) {
// We're done.
context.eof = true;
break;
@@ -606,9 +613,8 @@ public class Base32 extends BaseNCodec {
}
}
}
- // Two forms of EOF as far as Base32 decoder is concerned: actual
- // EOF (-1) and first time '=' character is encountered in stream.
- // This approach makes the '=' padding characters completely optional.
+ // Strict decoding waits for physical EOF to validate the complete
input.
+ // Lenient decoding also treats the first padding byte as EOF.
if (context.eof && context.modulus > 0) { // if modulus == 0, nothing
to do
final byte[] buffer = ensureBufferSize(decodeSize, context);
// We ignore partial bytes, i.e. only multiples of 8 count.
diff --git a/src/main/java/org/apache/commons/codec/binary/Base64.java
b/src/main/java/org/apache/commons/codec/binary/Base64.java
index 3245c208..f7207ffb 100644
--- a/src/main/java/org/apache/commons/codec/binary/Base64.java
+++ b/src/main/java/org/apache/commons/codec/binary/Base64.java
@@ -44,7 +44,7 @@ import org.apache.commons.codec.CodecPolicy;
* <li>Padding; defaults is {@code '='}.</li>
* </ul>
* <p>
- * The URL-safe parameter is only applied to encode operations. Decoding
seamlessly handles both modes, see also
+ * The URL-safe parameter selects the encoding alphabet. Lenient decoding
seamlessly handles both modes; strict decoding requires the encoding alphabet.
See also
* {@code Builder#setDecodeTableFormat(DecodeTableFormat)}.
* </p>
* <p>
@@ -120,7 +120,8 @@ public class Base64 extends BaseNCodec {
* does not modify behavior on encoding operations. For configuration
of the encoding behavior, please use {@link #setUrlSafe(boolean)} method.
* <p>
* By default, the implementation uses the {@link
DecodeTableFormat#MIXED} approach, allowing a seamless handling of both
- * {@link DecodeTableFormat#URL_SAFE} and {@link
DecodeTableFormat#STANDARD} base64.
+ * {@link DecodeTableFormat#URL_SAFE} and {@link
DecodeTableFormat#STANDARD} base64 in lenient mode. Strict decoding
additionally requires each character
+ * to match the configured encoding table.
* </p>
*
* @param format table format to be used on Base64 decoding. Use
{@link DecodeTableFormat#MIXED} or null to reset to the default behavior.
@@ -158,8 +159,8 @@ public class Base64 extends BaseNCodec {
/**
* Sets the URL-safe encoding policy.
* <p>
- * This method does not modify behavior on decoding operations. For
configuration of the decoding behavior, please use
- * {@code Builder.setDecodeTableFormat(DecodeTableFormat)} method.
+ * Strict decoding requires this alphabet and its padding convention.
Lenient decoding accepts both alphabets by default; use
+ * {@code Builder.setDecodeTableFormat(DecodeTableFormat)} to select a
decoding table.
* </p>
*
* @param urlSafe URL-safe encoding policy.
@@ -799,7 +800,7 @@ public class Base64 extends BaseNCodec {
}
/**
- * Line separator for encoding. Not used when decoding. Only used if
lineLength > 0.
+ * Line separator for encoding and strict decoding. Only used if
lineLength > 0.
*/
private final byte[] lineSeparator;
@@ -816,7 +817,7 @@ public class Base64 extends BaseNCodec {
* When encoding the line length is 0 (no chunking), and the encoding
table is STANDARD_ENCODE_TABLE.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*/
public Base64() {
@@ -829,7 +830,7 @@ public class Base64 extends BaseNCodec {
* When encoding the line length is 76, the line separator is CRLF, and
the encoding table is STANDARD_ENCODE_TABLE.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*
* @param urlSafe if {@code true}, URL-safe encoding is used. In most
cases this should be set to {@code false}.
@@ -882,11 +883,11 @@ public class Base64 extends BaseNCodec {
* Line lengths that aren't multiples of 4 will still essentially end up
being multiples of 4 in the encoded data.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 4). If lineLength <=
0, then
- * the output will not be divided into lines (chunks).
Ignored when decoding.
+ * the output will not be divided into lines (chunks).
Ignored when decoding leniently.
* @since 1.4
* @deprecated Use {@link #builder()} and {@link Builder}.
*/
@@ -904,11 +905,11 @@ public class Base64 extends BaseNCodec {
* Line lengths that aren't multiples of 4 will still essentially end up
being multiples of 4 in the encoded data.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 4). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @throws IllegalArgumentException Thrown when the provided lineSeparator
included some base64 characters.
* @since 1.4
@@ -928,11 +929,11 @@ public class Base64 extends BaseNCodec {
* Line lengths that aren't multiples of 4 will still essentially end up
being multiples of 4 in the encoded data.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 4). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
* @param urlSafe Instead of emitting '+' and '/' we emit '-' and
'_' respectively. urlSafe is only applied to encode operations. Decoding
seamlessly
* handles both modes. <strong>No padding is added
when using the URL-safe alphabet.</strong>
@@ -955,14 +956,14 @@ public class Base64 extends BaseNCodec {
* Line lengths that aren't multiples of 4 will still essentially end up
being multiples of 4 in the encoded data.
* </p>
* <p>
- * When decoding all variants are supported.
+ * When decoding leniently all variants are supported. Strict decoding
requires the configured encoding alphabet and layout.
* </p>
*
* @param lineLength Each line of encoded data will be at most of the
given length (rounded down to the nearest multiple of 4). If lineLength <= 0,
- * then the output will not be divided into lines
(chunks). Ignored when decoding.
+ * then the output will not be divided into lines
(chunks). Ignored when decoding leniently.
* @param lineSeparator Each line of encoded data will end with this
sequence of bytes.
- * @param urlSafe Instead of emitting '+' and '/' we emit '-' and
'_' respectively. urlSafe is only applied to encode operations. Decoding
seamlessly
- * handles both modes. <strong>No padding is added
when using the URL-safe alphabet.</strong>
+ * @param urlSafe Instead of emitting '+' and '/' we emit '-' and
'_' respectively. Strict decoding requires this alphabet. Lenient
+ * decoding handles both modes. <strong>No padding
is added when using the URL-safe alphabet.</strong>
* @param decodingPolicy The decoding policy.
* @throws IllegalArgumentException Thrown when the {@code lineSeparator}
contains Base64 characters.
* @since 1.15
@@ -977,11 +978,11 @@ public class Base64 extends BaseNCodec {
/**
* <p>
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
Should be called at least twice: once with the data to decode, and once with
- * inAvail set to "-1" to alert decoder that EOF has been reached. The
"-1" call is not necessary when decoding, but it doesn't hurt, either.
+ * inAvail set to "-1" to alert decoder that EOF has been reached. Strict
decoding requires the "-1" call to validate the complete input.
* </p>
* <p>
- * Ignores all non-base64 characters. This is how chunked (for example 76
character) data is handled, since CR and LF are silently ignored, but has
- * implications for other bytes, too. This method subscribes to the
garbage-in, garbage-out philosophy: it will not check the provided data for
validity.
+ * Lenient decoding ignores non-alphabet characters and stops at the first
padding byte. Strict decoding accepts only the canonical form produced by this
+ * instance's encoder, including its alphabet, padding, and line
separators.
* </p>
* <p>
* Thanks to "commons" project in ws.apache.org for the bitwise
operations, and general approach.
@@ -1001,11 +1002,18 @@ public class Base64 extends BaseNCodec {
}
if (inAvail < 0) {
context.eof = true;
+ if (isStrictDecoding()) {
+ validateCanonicalEnd(isStandardEncodeTable, context);
+ }
}
final int decodeSize = this.encodeSize - 1;
for (int i = 0; i < inAvail; i++) {
final int b = input[inPos++] & 0xff;
- if (b == (pad & 0xff)) {
+ if (isStrictDecoding()) {
+ if (!validateCanonicalByte(b, lineSeparator,
isStandardEncodeTable, context)) {
+ continue;
+ }
+ } else if (b == (pad & 0xff)) {
// We're done.
context.eof = true;
break;
@@ -1025,9 +1033,8 @@ public class Base64 extends BaseNCodec {
}
}
- // Two forms of EOF as far as base64 decoder is concerned: actual
- // EOF (-1) and first time '=' character is encountered in stream.
- // This approach makes the '=' padding characters completely optional.
+ // Strict decoding waits for physical EOF to validate the complete
input.
+ // Lenient decoding also treats the first padding byte as EOF.
if (context.eof && context.modulus != 0) {
final byte[] buffer = ensureBufferSize(decodeSize, context);
diff --git a/src/main/java/org/apache/commons/codec/binary/BaseNCodec.java
b/src/main/java/org/apache/commons/codec/binary/BaseNCodec.java
index 47f6b8a9..ff3de888 100644
--- a/src/main/java/org/apache/commons/codec/binary/BaseNCodec.java
+++ b/src/main/java/org/apache/commons/codec/binary/BaseNCodec.java
@@ -37,18 +37,16 @@ import org.apache.commons.codec.EncoderException;
* This class is thread-safe.
* </p>
* <p>
- * You can set the decoding behavior when the input bytes contain leftover
trailing bits that cannot be created by a valid encoding. These can be bits
that are
- * unused from the final character or entire characters. The default mode is
lenient decoding.
- * </p>
- * <ul>
- * <li>Lenient: Any trailing bits are composed into 8-bit bytes where
possible. The remainder are discarded.</li>
- * <li>Strict: The decoding will raise an {@link IllegalArgumentException} if
trailing bits are not part of a valid encoding. Any unused bits from the final
- * character must be zero. Impossible counts of entire final characters are
not allowed.</li>
- * </ul>
- * <p>
- * When strict decoding is enabled it is expected that the decoded bytes will
be re-encoded to a byte array that matches the original, i.e. no changes occur
on
- * the final character. This requires that the input bytes use the same
padding and alphabet as the encoder.
+ * The default decoding policy is lenient. Strict decoding rejects trailing
bits that cannot be produced by an encoding, including nonzero unused bits and
+ * impossible counts of final characters.
* </p>
+ *
+ * <p>For {@link Base32} and {@link Base64}, strict decoding additionally
requires the exact canonical form produced by this instance's encoder.
Re-encoding
+ * successfully decoded input reproduces the input byte for byte. This
includes the configured alphabet, padding, line length, and line separator,
including
+ * the final line separator when chunking is enabled. Whitespace and alphabet
aliases are rejected unless the encoder produces them in that position.</p>
+ *
+ * <p>Strict validation completes only at the end of the input. When decoding
streams, consume the input stream to EOF or finish the output stream with
+ * {@link BaseNCodecOutputStream#eof()} or {@link
BaseNCodecOutputStream#close()}. A stream can emit decoded bytes before a later
validation error.</p>
*/
public abstract class BaseNCodec implements BinaryEncoder, BinaryDecoder {
@@ -288,7 +286,7 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
boolean eof;
/**
- * Variable tracks how many characters have been written to the
current line. Only used when encoding. We use it to make sure each encoded line
never
+ * Variable tracks how many characters have been written to or
strictly decoded from the current line. We use it to make sure each encoded
line never
* goes beyond lineLength (if lineLength > 0).
*/
int currentLinePos;
@@ -298,6 +296,21 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
*/
int modulus;
+ /**
+ * Number of padding bytes consumed by strict decoding.
+ */
+ int strictPadding;
+
+ /**
+ * Position within the configured line separator during strict
decoding.
+ */
+ int strictSeparatorPos;
+
+ /**
+ * Whether strict decoding has encountered a short final line.
+ */
+ boolean strictFinalLine;
+
/**
* Returns a String useful for debugging (especially within a
debugger.)
*
@@ -514,7 +527,7 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
private final int encodedBlockSize;
/**
- * Chunksize for encoding. Not used when decoding. A value of zero or less
implies no chunking of the encoded data. Rounded down to the nearest multiple of
+ * Chunk size for encoding and strict decoding. A value of zero or less
implies no chunking of the encoded data. Rounded down to the nearest multiple of
* encodedBlockSize.
*/
protected final int lineLength;
@@ -525,17 +538,7 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
private final int chunkSeparatorLength;
/**
- * Defines the decoding behavior when the input bytes contain leftover
trailing bits that cannot be created by a valid encoding. These can be bits
that are
- * unused from the final character or entire characters. The default mode
is lenient decoding. Set this to {@code true} to enable strict decoding.
- * <ul>
- * <li>Lenient: Any trailing bits are composed into 8-bit bytes where
possible. The remainder are discarded.</li>
- * <li>Strict: The decoding will raise an {@link IllegalArgumentException}
if trailing bits are not part of a valid encoding. Any unused bits from the
final
- * character must be zero. Impossible counts of entire final characters
are not allowed.</li>
- * </ul>
- * <p>
- * When strict decoding is enabled it is expected that the decoded bytes
will be re-encoded to a byte array that matches the original, i.e. no changes
occur
- * on the final character. This requires that the input bytes use the same
padding and alphabet as the encoder.
- * </p>
+ * Decoding policy, including canonical validation for Base32 and Base64.
*/
private final CodecPolicy decodingPolicy;
@@ -840,12 +843,9 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
/**
* Gets the decoding behavior policy.
*
- * <p>
- * The default is lenient. If the decoding policy is strict, then decoding
will raise an {@link IllegalArgumentException} if trailing bits are not part of
a
- * valid encoding. Decoding will compose trailing bits into 8-bit bytes
and discard the remainder.
- * </p>
+ * <p>The default is lenient. Strict decoding rejects invalid trailing
bits and, for Base32 and Base64, noncanonical input as described in this
class.</p>
*
- * @return true if using strict decoding.
+ * @return The decoding policy.
* @since 1.15
*/
public CodecPolicy getCodecPolicy() {
@@ -924,11 +924,9 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
}
/**
- * Tests true if decoding behavior is strict. Decoding will raise an
{@link IllegalArgumentException} if trailing bits are not part of a valid
encoding.
+ * Tests whether decoding behavior is strict.
*
- * <p>
- * The default is false for lenient decoding. Decoding will compose
trailing bits into 8-bit bytes and discard the remainder.
- * </p>
+ * <p>Strict decoding rejects invalid trailing bits and, for Base32 and
Base64, noncanonical input as described in this class.</p>
*
* @return true if using strict decoding.
* @since 1.15
@@ -966,4 +964,77 @@ public abstract class BaseNCodec implements BinaryEncoder,
BinaryDecoder {
}
return context.eof ? EOF : 0;
}
+
+ /**
+ * Validates a byte against the canonical Base32 or Base64 encoding,
consuming padding and line separators.
+ *
+ * @param value The unsigned input byte.
+ * @param lineSeparator The configured line separator.
+ * @param padded Whether the encoder pads partial blocks.
+ * @param context The decoding context, whose modulus counts alphabet
characters only.
+ * @return Whether the byte is an alphabet character to decode.
+ * @throws IllegalArgumentException if the byte cannot occur in a
canonical encoding.
+ */
+ boolean validateCanonicalByte(final int value, final byte[] lineSeparator,
final boolean padded, final Context context) {
+ if (lineLength > 0 && (context.strictSeparatorPos > 0 ||
context.currentLinePos == lineLength ||
+ value == (lineSeparator[0] & MASK_8BITS))) {
+ if (context.currentLinePos == 0 || value !=
(lineSeparator[context.strictSeparatorPos] & MASK_8BITS)) {
+ throw new IllegalArgumentException("Strict decoding: Invalid
line separator or line length.");
+ }
+ if (context.strictSeparatorPos == 0) {
+ validateCanonicalPadding(padded, context);
+ context.strictFinalLine = context.currentLinePos < lineLength;
+ }
+ if (++context.strictSeparatorPos == lineSeparator.length) {
+ context.strictSeparatorPos = 0;
+ context.currentLinePos = 0;
+ }
+ return false;
+ }
+ if (context.strictFinalLine) {
+ throw new IllegalArgumentException("Strict decoding: Data follows
the final line separator.");
+ }
+ if (value == (pad & MASK_8BITS)) {
+ if (!padded || context.modulus == 0 || context.strictPadding >=
encodedBlockSize - context.modulus) {
+ throw new IllegalArgumentException("Strict decoding:
Unexpected padding.");
+ }
+ context.strictPadding++;
+ } else {
+ final int decoded = value < decodeTable.length ?
decodeTable[value] : -1;
+ if (context.strictPadding != 0 || decoded < 0 || decoded >=
encodeTable.length || (encodeTable[decoded] & MASK_8BITS) != value) {
+ throw new IllegalArgumentException("Strict decoding:
Unexpected character or data after padding.");
+ }
+ }
+ if (lineLength > 0) {
+ context.currentLinePos++;
+ }
+ return value != (pad & MASK_8BITS);
+ }
+
+ /**
+ * Validates the end of a canonical Base32 or Base64 encoding.
+ *
+ * @param padded Whether the encoder pads partial blocks.
+ * @param context The decoding context.
+ * @throws IllegalArgumentException if padding or the final line separator
is incomplete.
+ */
+ void validateCanonicalEnd(final boolean padded, final Context context) {
+ validateCanonicalPadding(padded, context);
+ if (context.strictSeparatorPos != 0 || context.currentLinePos != 0) {
+ throw new IllegalArgumentException("Strict decoding: Missing or
incomplete final line separator.");
+ }
+ }
+
+ /**
+ * Validates the number of padding bytes at the end of a line or input.
+ *
+ * @param padded Whether the encoder pads partial blocks.
+ * @param context The decoding context.
+ * @throws IllegalArgumentException if required padding is missing.
+ */
+ private void validateCanonicalPadding(final boolean padded, final Context
context) {
+ if (padded && context.modulus != 0 && context.strictPadding !=
encodedBlockSize - context.modulus) {
+ throw new IllegalArgumentException("Strict decoding: Incorrect
padding length.");
+ }
+ }
}
diff --git
a/src/main/java/org/apache/commons/codec/binary/BaseNCodecInputStream.java
b/src/main/java/org/apache/commons/codec/binary/BaseNCodecInputStream.java
index e5a8766d..1ffdc16d 100644
--- a/src/main/java/org/apache/commons/codec/binary/BaseNCodecInputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/BaseNCodecInputStream.java
@@ -145,11 +145,10 @@ public class BaseNCodecInputStream<C extends BaseNCodec,
T extends BaseNCodecInp
}
/**
- * Returns true if decoding behavior is strict. Decoding will raise an
{@link IllegalArgumentException} if trailing bits are not part of a valid
encoding.
+ * Tests whether decoding behavior is strict.
*
- * <p>
- * The default is false for lenient encoding. Decoding will compose
trailing bits into 8-bit bytes and discard the remainder.
- * </p>
+ * <p>Strict decoding rejects invalid trailing bits and, for Base32 and
Base64, noncanonical input. Decoding errors are reported as {@link IOException}.
+ * To complete validation, consume this stream to EOF. Decoded bytes can
be emitted before a later validation error.</p>
*
* @return true if using strict decoding.
* @since 1.15
diff --git
a/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
b/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
index 586cb110..d037c1e6 100644
--- a/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
+++ b/src/main/java/org/apache/commons/codec/binary/BaseNCodecOutputStream.java
@@ -179,11 +179,10 @@ public class BaseNCodecOutputStream<C extends BaseNCodec,
T extends BaseNCodecOu
}
/**
- * Returns true if decoding behavior is strict. Decoding will raise an
{@link IllegalArgumentException} if trailing bits are not part of a valid
encoding.
+ * Tests whether decoding behavior is strict.
*
- * <p>
- * The default is false for lenient encoding. Decoding will compose
trailing bits into 8-bit bytes and discard the remainder.
- * </p>
+ * <p>Strict decoding rejects invalid trailing bits and, for Base32 and
Base64, noncanonical input. Decoding errors are reported as {@link IOException}.
+ * To complete validation, call {@link #eof()} or {@link #close()}.
Decoded bytes can be emitted before a later validation error.</p>
*
* @return true if using strict decoding.
* @since 1.15
diff --git
a/src/test/java/org/apache/commons/codec/binary/Base32OutputStreamTest.java
b/src/test/java/org/apache/commons/codec/binary/Base32OutputStreamTest.java
index 8d12ef54..841f9c57 100644
--- a/src/test/java/org/apache/commons/codec/binary/Base32OutputStreamTest.java
+++ b/src/test/java/org/apache/commons/codec/binary/Base32OutputStreamTest.java
@@ -308,7 +308,10 @@ class Base32OutputStreamTest extends
AbstractBaseNOutputStreamTest {
bout = new ByteArrayOutputStream();
try (Base32OutputStream out2 = new Base32OutputStream(bout,
false, 0, null, CodecPolicy.STRICT)) {
assertTrue(out2.isStrictDecoding());
- final IOException ioe = assertThrows(IOException.class, ()
-> out2.write(encoded));
+ final IOException ioe = assertThrows(IOException.class, ()
-> {
+ out2.write(encoded);
+ out2.eof();
+ });
assertTrue(ioe.getCause() instanceof
IllegalArgumentException);
}
try (Base32OutputStream out2 = Base32OutputStream.builder()
@@ -316,7 +319,10 @@ class Base32OutputStreamTest extends
AbstractBaseNOutputStreamTest {
.setBaseNCodec(Base32.builder().setLineLength(0).setLineSeparator(null).setDecodingPolicy(CodecPolicy.STRICT).get())
.get()) {
assertTrue(out2.isStrictDecoding());
- final IOException ioe = assertThrows(IOException.class, ()
-> out2.write(encoded));
+ final IOException ioe = assertThrows(IOException.class, ()
-> {
+ out2.write(encoded);
+ out2.eof();
+ });
assertTrue(ioe.getCause() instanceof
IllegalArgumentException);
}
try (Base32OutputStream out2 = Base32OutputStream.builder()
@@ -324,7 +330,10 @@ class Base32OutputStreamTest extends
AbstractBaseNOutputStreamTest {
.setBaseNCodec(Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).get())
.get()) {
assertTrue(out2.isStrictDecoding());
- final IOException ioe = assertThrows(IOException.class, ()
-> out2.write(encoded));
+ final IOException ioe = assertThrows(IOException.class, ()
-> {
+ out2.write(encoded);
+ out2.eof();
+ });
assertTrue(ioe.getCause() instanceof
IllegalArgumentException);
}
}
diff --git
a/src/test/java/org/apache/commons/codec/binary/BaseNStrictDecodingTest.java
b/src/test/java/org/apache/commons/codec/binary/BaseNStrictDecodingTest.java
new file mode 100644
index 00000000..dffc6d8b
--- /dev/null
+++ b/src/test/java/org/apache/commons/codec/binary/BaseNStrictDecodingTest.java
@@ -0,0 +1,242 @@
+/*
+ * 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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.Random;
+import java.util.stream.Stream;
+
+import org.apache.commons.codec.CodecPolicy;
+import org.apache.commons.codec.binary.BaseNCodec.Context;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/** Tests canonical decoding across array and stream boundaries. */
+class BaseNStrictDecodingTest {
+
+ private static final Base64 BASE64 =
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
+ private static final Base32 BASE32 =
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).get();
+
+ private static void assertCanonicalOrRejected(final BaseNCodec codec,
final byte[] encoded) {
+ final byte[] decoded;
+ try {
+ decoded = codec.decode(encoded);
+ } catch (final IllegalArgumentException expected) {
+ return;
+ }
+ assertArrayEquals(encoded, codec.encode(decoded));
+ }
+
+ private static void assertInvalid(final BaseNCodec codec, final String...
inputs) {
+ for (final String input : inputs) {
+ assertThrows(IllegalArgumentException.class, () ->
codec.decode(input), input);
+ }
+ }
+
+ private static Stream<BaseNCodec> codecs() {
+ final byte[] custom64 =
StringUtils.getBytesUsAscii("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
+ // A custom alphabet uses the encoder's unpadded form, even when it is
not the built-in URL-safe alphabet.
+ custom64[0] = '!';
+ final byte[] custom32 =
StringUtils.getBytesUsAscii("abcdefghijklmnopqrstuvwxyz234567");
+ return Stream.of(BASE64, BASE32,
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).get(),
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setEncodeTable(custom64).get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setEncodeTable(custom32).get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setHexEncodeTable(true).get(),
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setPadding((byte)
'.').get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setPadding((byte)
'.').get(),
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(9).setLineSeparator((byte)
'!', (byte) '?').get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(17).setLineSeparator((byte)
'!', (byte) '?').get(),
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).setLineLength(8).get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(16).get(),
+
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(1).get(),
+
Base32.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(8).setLineSeparator(new
byte[0]).get());
+ }
+
+ private static byte[] decodeInParts(final BaseNCodec codec, final byte[]
encoded, final int split) {
+ final Context context = new Context();
+ codec.decode(encoded, 0, split, context);
+ codec.decode(encoded, split, encoded.length - split, context);
+ codec.decode(encoded, 0, BaseNCodec.EOF, context);
+ final byte[] result = new byte[codec.available(context)];
+ codec.readResults(result, 0, result.length, context);
+ return result;
+ }
+
+ private static InputStream oneByteAtATime(final byte[] encoded) {
+ return new ByteArrayInputStream(encoded) {
+ @Override
+ public synchronized int read(final byte[] buffer, final int
offset, final int length) {
+ return super.read(buffer, offset, Math.min(1, length));
+ }
+ };
+ }
+
+ @ParameterizedTest
+ @MethodSource("codecs")
+ void testCanonicalRoundTripAtEverySplit(final BaseNCodec codec) {
+ final Random random = new Random(87654);
+ for (int length = 0; length <= 32; length++) {
+ final byte[] input = new byte[length];
+ random.nextBytes(input);
+ final byte[] encoded = codec.encode(input);
+ assertArrayEquals(input, codec.decode(encoded));
+ for (int split = 0; split <= encoded.length; split++) {
+ assertArrayEquals(input, decodeInParts(codec, encoded, split));
+ }
+ }
+ }
+
+ @ParameterizedTest
+ @MethodSource("codecs")
+ void testCanonicalStreams(final BaseNCodec codec) throws IOException {
+ final byte[] input = new byte[67];
+ new Random(23456).nextBytes(input);
+ final byte[] encoded = codec.encode(input);
+ final ByteArrayOutputStream decoded = new ByteArrayOutputStream();
+ try (InputStream stream = new
BaseNCodecInputStream<>(oneByteAtATime(encoded), codec, false)) {
+ for (int value; (value = stream.read()) != -1;) {
+ decoded.write(value);
+ }
+ }
+ assertArrayEquals(input, decoded.toByteArray());
+ decoded.reset();
+ try (BaseNCodecOutputStream<?, ?, ?> stream = new
BaseNCodecOutputStream<>(decoded, codec, false)) {
+ for (final byte value : encoded) {
+ stream.write(value & 0xff);
+ }
+ stream.flush();
+ }
+ assertArrayEquals(input, decoded.toByteArray());
+ }
+
+ @Test
+ void testGarbageWhitespaceAndAlphabetAliases() {
+ assertInvalid(BASE64, "QU JD", "QU$JD", "QU\u001cJD", "QQ==JUNK",
"QQ==\n", "QUJ-", "QUJ_", "QR==", "QUJ=");
+ assertInvalid(BASE32, "M Y======", "MY!======", "MY======JUNK",
"my======", "MY======\n", "MZ======");
+ final Base64 url =
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setUrlSafe(true).get();
+ assertInvalid(url, "QQ==", "QUJ+", "QUJ/");
+ assertArrayEquals(new byte[] {65}, url.decode("QQ"));
+ }
+
+ @Test
+ void testLenientBehaviorIsUnchanged() {
+ assertArrayEquals(new byte[] {65}, new Base64().decode("Q $Q==JUNK"));
+ assertArrayEquals(new byte[] {102}, new Base32().decode("m
!y======JUNK"));
+ assertArrayEquals(new Base64().decode("QUJ+"), new
Base64().decode("QUJ-"));
+ }
+
+ @Test
+ void testLineSeparators() {
+ final Base64 codec =
Base64.builder().setDecodingPolicy(CodecPolicy.STRICT).setLineLength(8).get();
+ assertEquals("ABC",
StringUtils.newStringUsAscii(codec.decode("QUJD\r\n")));
+ assertEquals("ABCDEF",
StringUtils.newStringUsAscii(codec.decode("QUJDREVG\r\n")));
+ assertInvalid(codec, "QUJD", "QUJD\r", "QUJD\n", "QUJD\rX", "\r\n",
"QUJD\r\nREVG\r\n", "QUJDREVGQUJD\r\n",
+ "QUJDREVG\r\n\r\n", "QQ==\r\nQUJD\r\n", "QUJDREVG=",
"QUJDREVG\rX");
+ }
+
+ @ParameterizedTest
+ @MethodSource("codecs")
+ void testMutationsAreCanonicalOrRejected(final BaseNCodec codec) {
+ final byte[] input = new byte[7];
+ new Random(34567).nextBytes(input);
+ final byte[] encoded = codec.encode(input);
+ for (int pos = 0; pos < encoded.length; pos++) {
+ final byte[] deleted = new byte[encoded.length - 1];
+ System.arraycopy(encoded, 0, deleted, 0, pos);
+ System.arraycopy(encoded, pos + 1, deleted, pos, encoded.length -
pos - 1);
+ assertCanonicalOrRejected(codec, deleted);
+ final byte[] changed = encoded.clone();
+ for (int value = 0; value <= 255; value++) {
+ changed[pos] = (byte) value;
+ assertCanonicalOrRejected(codec, changed);
+ }
+ }
+ for (int pos = 0; pos <= encoded.length; pos++) {
+ final byte[] inserted = new byte[encoded.length + 1];
+ System.arraycopy(encoded, 0, inserted, 0, pos);
+ System.arraycopy(encoded, pos, inserted, pos + 1, encoded.length -
pos);
+ for (int value = 0; value <= 255; value++) {
+ inserted[pos] = (byte) value;
+ assertCanonicalOrRejected(codec, inserted);
+ }
+ }
+ }
+
+ @Test
+ void testPadding() {
+ assertInvalid(BASE64, "QQ", "QQ=", "QQ===", "QQ=====", "=", "====",
"QUJD=", "Q===", "QQ==QQ==");
+ assertInvalid(BASE32, "MY", "MY=", "MY=====", "MY=======", "=",
"========", "MZXW6YTB=", "M=======", "MY======MY======");
+ }
+
+ @Test
+ void testPostPaddingDataAtEverySplit() {
+ for (final BaseNCodec codec : Arrays.asList(BASE64, BASE32)) {
+ final byte[] encoded =
StringUtils.getBytesUsAscii(codec.encodeToString(new byte[] {65}) + "JUNK");
+ for (int split = 0; split <= encoded.length; split++) {
+ final int boundary = split;
+ assertThrows(IllegalArgumentException.class, () ->
decodeInParts(codec, encoded, boundary));
+ }
+ final IOException exception = assertThrows(IOException.class, ()
-> {
+ try (InputStream stream = new
BaseNCodecInputStream<>(oneByteAtATime(encoded), codec, false)) {
+ while (stream.read() != -1) {
+ // Consume the complete stream to validate its suffix.
+ }
+ }
+ });
+ assertTrue(exception.getCause() instanceof
IllegalArgumentException);
+ assertThrows(IOException.class, () -> {
+ try (BaseNCodecOutputStream<?, ?, ?> stream = new
BaseNCodecOutputStream<>(new ByteArrayOutputStream(), codec, false)) {
+ for (final byte value : encoded) {
+ stream.write(value & 0xff);
+ }
+ }
+ });
+ }
+ }
+
+ @Test
+ void testTruncatedStreamsFailAtEof() {
+ for (final BaseNCodec codec : Arrays.asList(BASE64, BASE32)) {
+ final byte[] encoded = codec.encode(new byte[] {65});
+ final byte[] truncated = Arrays.copyOf(encoded, encoded.length -
1);
+ assertThrows(IOException.class, () -> {
+ try (InputStream stream = new
BaseNCodecInputStream<>(oneByteAtATime(truncated), codec, false)) {
+ while (stream.read() != -1) {
+ // Consume the stream to validate missing padding at
EOF.
+ }
+ }
+ });
+ assertThrows(IOException.class, () -> {
+ try (BaseNCodecOutputStream<?, ?, ?> stream = new
BaseNCodecOutputStream<>(new ByteArrayOutputStream(), codec, false)) {
+ stream.write(truncated);
+ }
+ });
+ }
+ }
+}