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