Base64Test.java

/*
 * 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.net.util;

import static org.junit.Assert.assertArrayEquals;
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;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;

import org.apache.commons.lang3.ArrayFill;
import org.apache.commons.lang3.ArrayUtils;
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, 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, toString(actual != null ? getJreDecoder().decode(actual) : null));
        assertEquals(expected, toString(new Base64().decode(actual)));
    }

    private Decoder getJreDecoder() {
        return java.util.Base64.getDecoder();
    }

    private Encoder getJreEncoder() {
        return java.util.Base64.getEncoder();
    }

    private Encoder getJreMimeEncoder() {
        return java.util.Base64.getMimeEncoder();
    }

    private Encoder getJreMimeEncoder(final int lineLength, final byte[] lineSeparator) {
        return java.util.Base64.getMimeEncoder(lineLength, lineSeparator);
    }

    private Encoder getJreUrlEncoder() {
        return java.util.Base64.getUrlEncoder();
    }

    @Test
    public void testBase64() {
        final Base64 b64 = new Base64();
        assertFalse(b64.isUrlSafe());
    }

    @Test
    public void testBase64Boolean() {
        final Base64 b64 = new Base64(true);
        assertTrue(b64.isUrlSafe());
        assertArrayEquals(new byte[] {}, b64.getLineSeparator());
    }

    @Test
    public void testBase64Int() {
        Base64 b64;
        b64 = new Base64(8);
        assertFalse(b64.isUrlSafe());
        assertEquals(8, b64.getLineLength());
        b64 = new Base64(11);
        assertEquals(8, b64.getLineLength());
    }

    @Test
    public void testBase64IntByteArray() {
        final Base64 b64;
        b64 = new Base64(8, new byte[] {});
        assertFalse(b64.isUrlSafe());
        assertArrayEquals(new byte[] {}, b64.getLineSeparator());
        final String stringToEncode = "<<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>>";
        final byte[] encodedData = new Base64(Base64.CHUNK_SIZE, Base64.CHUNK_SEPARATOR).encode(stringToEncode.getBytes());
        assertEquals("PDw/Pz8+Pjw8Pz8/Pj48PD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj48PD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj48\r\nPD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj4=\r\n", toString(encodedData));
        assertEquals(getJreMimeEncoder().encodeToString(stringToEncode.getBytes()) + "\r\n", toString(encodedData));
        assertEquals("PDw/Pz8+Pjw8Pz8/Pj48PD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj48PD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj48~PD8/Pz4+PDw/Pz8+Pjw8Pz8/Pj4=~",
                toString(new Base64(Base64.CHUNK_SIZE, "~".getBytes()).encode(stringToEncode.getBytes())));
        assertEquals(getJreMimeEncoder(Base64.CHUNK_SIZE, "~".getBytes()).encodeToString(stringToEncode.getBytes()) + "~",
                toString(new Base64(Base64.CHUNK_SIZE, "~".getBytes()).encode(stringToEncode.getBytes())));
        assertEquals(getJreMimeEncoder(Base64.CHUNK_SIZE - 2, "~~".getBytes()).encodeToString(stringToEncode.getBytes()) + "~~",
                toString(new Base64(Base64.CHUNK_SIZE - 2, "~~".getBytes()).encode(stringToEncode.getBytes())));
        assertEquals(getJreMimeEncoder(Base64.CHUNK_SIZE + 2, "~~~".getBytes()).encodeToString(stringToEncode.getBytes()) + "~~~",
                toString(new Base64(Base64.CHUNK_SIZE + 2, "~~~".getBytes()).encode(stringToEncode.getBytes())));
    }

    @Test
    public void testBase64IntByteArrayBoolean() {
        Base64 b64;
        b64 = new Base64(8, new byte[] {}, false);
        assertFalse(b64.isUrlSafe());
        b64 = new Base64(8, new byte[] {}, true);
        assertTrue(b64.isUrlSafe());
        assertThrows(IllegalArgumentException.class, () -> new Base64(8, new byte[] { 'A' }, false));
    }

    @Test
    public void testDecodeBase64ByteArray() {
        checkDecoders("light w", new byte[] { 'b', 'G', 'l', 'n', 'a', 'H', 'Q', 'g', 'd', 'w', '=', '=' });
    }

    @Test
    public void testDecodeBase64String() {
        checkDecoders("light w", "bGlnaHQgdw==");
    }

    @Test
    public void testDecodeByteArray() {
        checkDecoders("foobar", new byte[] { 'Z', 'm', '9', 'v', 'Y', 'm', 'F', 'y' });
    }

    @Test
    public void testDecodeByteArrayEmpty() {
        checkDecoders("", new byte[] {});
        checkDecoders(null, (byte[]) null);

    }

    @Test
    public void testDecodeByteArrayNull() {
        assertNull(new Base64().decode((byte[]) null));
    }

    @Test
    public void testDecodeInteger() {
        testDecodeInteger(BigInteger.ONE);
        testDecodeInteger(BigInteger.TEN);
        testDecodeInteger(BigInteger.ZERO);
    }

    private void testDecodeInteger(final BigInteger bi) {
        assertEquals(bi, Base64.decodeInteger(getJreEncoder().encode(bi.toByteArray())));
    }

    @Test
    public void testDecodeNullString() {
        final Base64 base64 = new Base64();
        assertThrows(NullPointerException.class, () -> base64.decode((String) null));
    }

    @Test
    public void testDecodeString() {
        checkDecoders("Hello World!", "SGVsbG8gV29ybGQh");
    }

    @Test
    public void testEncodeBase64ByteArrayBoolean() {
        final byte[] binaryData = { '1', '2', '3' };
        final byte[] urlUnsafeData = "<<???>>".getBytes();
        final byte[] urlUnsafeDataChunky = "<<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>><<???>>".getBytes();
        byte[] encoded;
        // Boolean parameter: "isChunked".
        //
        // isChunked false
        encoded = Base64.encodeBase64(binaryData, false);
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        assertEquals(Base64.getEncodeLength(binaryData, Base64.CHUNK_SIZE, Base64.CHUNK_SEPARATOR) - 2, encoded.length);
        assertEquals(getJreEncoder().encodeToString(binaryData), toString(encoded));
        assertEquals("MTIz", toString(encoded));
        // URL unsafe
        // <<???>>
        encoded = Base64.encodeBase64(urlUnsafeData, false);
        assertEquals("PDw/Pz8+Pg==", toString(encoded));
        encoded = Base64.encodeBase64(urlUnsafeDataChunky, false);
        assertEquals(getJreEncoder().encodeToString(urlUnsafeDataChunky), toString(encoded));
        //
        // isChunked false
        encoded = Base64.encodeBase64(binaryData, false);
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        assertEquals(Base64.getEncodeLength(binaryData, Base64.CHUNK_SIZE, Base64.CHUNK_SEPARATOR) - 2, encoded.length);
        assertEquals("MTIz", toString(encoded));
        //
        // isChunked true
        encoded = Base64.encodeBase64(binaryData, true);
        assertNotNull(encoded);
        assertEquals(6, encoded.length); // always adds trailer
        assertEquals(Base64.getEncodeLength(binaryData, Base64.CHUNK_SIZE, Base64.CHUNK_SEPARATOR), encoded.length);
        assertEquals("MTIz\r\n", toString(encoded));
        // URL unsafe
        // <<???>>
        encoded = Base64.encodeBase64(urlUnsafeData, true);
        assertEquals("PDw/Pz8+Pg==\r\n", toString(encoded));
        encoded = Base64.encodeBase64(urlUnsafeDataChunky, true);
        assertEquals(getJreMimeEncoder().encodeToString(urlUnsafeDataChunky) + "\r\n", toString(encoded));
        //
        // isChunked true
        encoded = Base64.encodeBase64(binaryData, true);
        assertNotNull(encoded);
        assertEquals(6, encoded.length);
        assertEquals(Base64.getEncodeLength(binaryData, Base64.CHUNK_SIZE, Base64.CHUNK_SEPARATOR), encoded.length);
        assertEquals("MTIz\r\n", toString(encoded));
    }

    @Test
    public void testEncodeBase64ByteArrayBooleanBoolean() {
        final byte[] binaryData = { '1', '2', '3' };
        byte[] encoded;
        encoded = Base64.encodeBase64(binaryData, false, false);
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        encoded = Base64.encodeBase64(binaryData, false, false);
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        encoded = Base64.encodeBase64(binaryData, true, false);
        assertNotNull(encoded);
        assertEquals(6, encoded.length); // always adds trailer
        encoded = Base64.encodeBase64(binaryData, true, false);
        assertNotNull(encoded);
        assertEquals(6, encoded.length);
    }

    @Test
    public void testEncodeBase64ByteArrayBooleanBooleanInt() {
        final byte[] binaryData = { '1', '2', '3' };
        byte[] encoded;
        encoded = Base64.encodeBase64(binaryData, false, false);
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        assertThrows(IllegalArgumentException.class, () -> Base64.encodeBase64(binaryData, false, false, 3));
        encoded = Base64.encodeBase64(binaryData, false, false, 4); // NET-483
        assertNotNull(encoded);
        assertEquals(4, encoded.length);
        encoded = Base64.encodeBase64(binaryData, true, false);
        assertNotNull(encoded);
        assertEquals(6, encoded.length); // always adds trailer
        assertThrows(IllegalArgumentException.class, () -> Base64.encodeBase64(binaryData, true, false, 5));
        encoded = Base64.encodeBase64(binaryData, true, false, 6);
        assertNotNull(encoded);
        assertEquals(6, encoded.length);
    }

    @Test
    public void testEncodeBase64ByteArrayEdges() {
        final byte[] binaryData = null;
        assertArrayEquals(binaryData, Base64.encodeBase64(binaryData));
        final byte[] binaryData2 = {};
        assertArrayEquals(binaryData2, Base64.encodeBase64(binaryData2));
    }

    @Test
    public void testEncodeBase64Chunked() {
        byte[] bytesToEncode = { 'f', 'o', 'o', 'b', 'a', 'r' };
        byte[] encodedData = Base64.encodeBase64Chunked(bytesToEncode);
        assertEquals("Zm9vYmFy\r\n", toString(encodedData));
        // URL unsafe data
        // <<???>>
        bytesToEncode = "<<???>>".getBytes();
        encodedData = Base64.encodeBase64Chunked(bytesToEncode);
        assertEquals("PDw/Pz8+Pg==\r\n", toString(encodedData));
        // > 76
        final byte[] chunkMe = ArrayFill.fill(new byte[Base64.CHUNK_SIZE * 2], (byte) 'A');
        final byte[] chunked = Base64.encodeBase64Chunked(chunkMe);
        assertEquals('\r', chunked[chunked.length - 2]);
        assertEquals('\n', chunked[chunked.length - 1]);
        assertArrayEquals(ArrayUtils.addAll(getJreMimeEncoder().encode(chunkMe), Base64.CHUNK_SEPARATOR), chunked);
    }

    @Test
    public void testEncodeBase64StringByteArray() {
        String stringToEncode = "Many hands make light work.";
        String encodedData = Base64.encodeBase64String(stringToEncode.getBytes());
        assertEquals("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu\r\n", encodedData);
        // URL unsafe data
        // <<???>>
        stringToEncode = "<<???>>";
        encodedData = Base64.encodeBase64String(stringToEncode.getBytes());
        assertEquals("PDw/Pz8+Pg==\r\n", encodedData);
        // > 76
        final byte[] chunkMe = ArrayFill.fill(new byte[Base64.CHUNK_SIZE * 2], (byte) 'A');
        final String chunked = Base64.encodeBase64String(chunkMe);
        assertEquals('\r', chunked.charAt(chunked.length() - 2));
        assertEquals('\n', chunked.charAt(chunked.length() - 1));
        assertEquals(getJreMimeEncoder().encodeToString(chunkMe) + "\r\n", chunked);
    }

    @Test
    public void testEncodeBase64StringByteArrayBoolean() {
        final byte[] bytesToEncode = "light work.".getBytes();
        final String chunkedResult = Base64.encodeBase64String(bytesToEncode, true);
        assertEquals("bGlnaHQgd29yay4=\r\n", chunkedResult);
        final String unchunkedResult = Base64.encodeBase64String(bytesToEncode, false);
        assertEquals("bGlnaHQgd29yay4=", unchunkedResult);
    }

    @Test
    public void testEncodeBase64StringUnChunked() {
        byte[] bytesToEncode = "Many hands make light work.".getBytes();
        String encodedData = Base64.encodeBase64StringUnChunked(bytesToEncode);
        assertEquals("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu", encodedData);
        // URL unsafe data
        // <<???>>
        bytesToEncode = "<<???>>".getBytes();
        encodedData = Base64.encodeBase64StringUnChunked(bytesToEncode);
        assertEquals("PDw/Pz8+Pg==", encodedData);
        // > 76
        final byte[] chunkMe = ArrayFill.fill(new byte[Base64.CHUNK_SIZE * 2], (byte) 'A');
        final String chunked = Base64.encodeBase64StringUnChunked(chunkMe);
        assertEquals(getJreEncoder().encodeToString(chunkMe), chunked);
    }

    @Test
    public void testEncodeBase64URLSafe() {
        byte[] bytesToEncode = "Many hands make light work.".getBytes();
        byte[] encodedData = Base64.encodeBase64URLSafe(bytesToEncode);
        assertEquals("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu", toString(encodedData));
        // URL unsafe data
        // <<???>>
        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
    public void testEncodeBase64URLSafeString() {
        byte[] bytesToEncode = "Many hands make light work.".getBytes();
        String encodedData = Base64.encodeBase64URLSafeString(bytesToEncode);
        assertEquals("TWFueSBoYW5kcyBtYWtlIGxpZ2h0IHdvcmsu", encodedData);
        // URL unsafe data
        // <<???>>
        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
    public void testEncodeByteArray() {
        final Base64 base64 = new Base64();
        final byte[] bytesToEncode = { 'l', 'i', 'g', 'h', 't', ' ', 'w', 'o', 'r' };
        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);
        testEncodeInteger(BigInteger.TEN);
        testEncodeInteger(BigInteger.ZERO);
    }

    private void testEncodeInteger(final BigInteger bi) {
        final byte[] decodedBytes = getJreDecoder().decode(Base64.encodeInteger(bi));
        final BigInteger decoded = decodedBytes.length == 0 ? BigInteger.ZERO : new BigInteger(decodedBytes);
        assertEquals(bi, decoded);
    }

    @Test
    public void testEncodeToString() {
        final Base64 base64 = new Base64();
        final byte[] bytesToEncode = { 'l', 'i', 'g', 'h', 't', ' ', 'w', 'o', 'r' };
        assertEquals("bGlnaHQgd29y\r\n", base64.encodeToString(bytesToEncode));
    }

    @Test
    public void testIsArrayByteBase64() {
        assertTrue(Base64.isArrayByteBase64(new byte[] { 'b', ' ' }));
        assertFalse(Base64.isArrayByteBase64(new byte[] { '?' }));
    }

    @Test
    public void testIsBase64() {
        assertTrue(Base64.isBase64((byte) '='));
        assertTrue(Base64.isBase64((byte) 'b'));
        assertFalse(Base64.isBase64((byte) ' '));
        assertFalse(Base64.isBase64((byte) -1));
    }

}