Skip to content
Merged
2 changes: 2 additions & 0 deletions DIRECTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
- 📄 [AnyBaseToAnyBase](src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java)
- 📄 [AnyBaseToDecimal](src/main/java/com/thealgorithms/conversions/AnyBaseToDecimal.java)
- 📄 [AnytoAny](src/main/java/com/thealgorithms/conversions/AnytoAny.java)
- 📄 [Base64](src/main/java/com/thealgorithms/conversions/Base64.java)
- 📄 [BinaryToDecimal](src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java)
- 📄 [BinaryToHexadecimal](src/main/java/com/thealgorithms/conversions/BinaryToHexadecimal.java)
- 📄 [BinaryToOctal](src/main/java/com/thealgorithms/conversions/BinaryToOctal.java)
Expand Down Expand Up @@ -839,6 +840,7 @@
- 📄 [AffineConverterTest](src/test/java/com/thealgorithms/conversions/AffineConverterTest.java)
- 📄 [AnyBaseToDecimalTest](src/test/java/com/thealgorithms/conversions/AnyBaseToDecimalTest.java)
- 📄 [AnytoAnyTest](src/test/java/com/thealgorithms/conversions/AnytoAnyTest.java)
- 📄 [Base64Test](src/test/java/com/thealgorithms/conversions/Base64Test.java)
- 📄 [BinaryToDecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java)
- 📄 [BinaryToHexadecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToHexadecimalTest.java)
- 📄 [BinaryToOctalTest](src/test/java/com/thealgorithms/conversions/BinaryToOctalTest.java)
Expand Down
196 changes: 196 additions & 0 deletions src/main/java/com/thealgorithms/conversions/Base64.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
package com.thealgorithms.conversions;

import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
* Base64 is a group of binary-to-text encoding schemes that represent binary data
* in an ASCII string format by translating it into a radix-64 representation.
* Each base64 digit represents exactly 6 bits of data.
*
* Base64 encoding is commonly used when there is a need to encode binary data
* that needs to be stored and transferred over media that are designed to deal
* with textual data.
*
* Wikipedia Reference: https://en.wikipedia.org/wiki/Base64
* Author: Nithin U.
* Github: https://github.com/NithinU2802
*/

public final class Base64 {

// Base64 character set
private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private static final char PADDING_CHAR = '=';

private Base64() {
}

/**
* Encodes the given byte array to a Base64 encoded string.
*
* @param input the byte array to encode
* @return the Base64 encoded string
* @throws IllegalArgumentException if input is null
*/
public static String encode(byte[] input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}

if (input.length == 0) {
return "";
}

StringBuilder result = new StringBuilder();
int padding = 0;

// Process input in groups of 3 bytes
for (int i = 0; i < input.length; i += 3) {
// Get up to 3 bytes
int byte1 = input[i] & 0xFF;
int byte2 = (i + 1 < input.length) ? (input[i + 1] & 0xFF) : 0;
int byte3 = (i + 2 < input.length) ? (input[i + 2] & 0xFF) : 0;

// Calculate padding needed
if (i + 1 >= input.length) {
padding = 2;
} else if (i + 2 >= input.length) {
padding = 1;
}

// Combine 3 bytes into a 24-bit number
int combined = (byte1 << 16) | (byte2 << 8) | byte3;

// Extract four 6-bit groups
result.append(BASE64_CHARS.charAt((combined >> 18) & 0x3F));
result.append(BASE64_CHARS.charAt((combined >> 12) & 0x3F));
result.append(BASE64_CHARS.charAt((combined >> 6) & 0x3F));
result.append(BASE64_CHARS.charAt(combined & 0x3F));
}

// Replace padding characters
if (padding > 0) {
result.setLength(result.length() - padding);
for (int i = 0; i < padding; i++) {
result.append(PADDING_CHAR);
}
}

return result.toString();
}

/**
* Encodes the given string to a Base64 encoded string using UTF-8 encoding.
*
* @param input the string to encode
* @return the Base64 encoded string
* @throws IllegalArgumentException if input is null
*/
public static String encode(String input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}

return encode(input.getBytes(StandardCharsets.UTF_8));
}

/**
* Decodes the given Base64 encoded string to a byte array.
*
* @param input the Base64 encoded string to decode
* @return the decoded byte array
* @throws IllegalArgumentException if input is null or contains invalid Base64 characters
*/
public static byte[] decode(String input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}

if (input.isEmpty()) {
return new byte[0];
}

// Strict RFC 4648 compliance: length must be a multiple of 4
if (input.length() % 4 != 0) {
throw new IllegalArgumentException("Invalid Base64 input length; must be multiple of 4");
}

// Validate padding: '=' can only appear at the end (last 1 or 2 chars)
int firstPadding = input.indexOf('=');
if (firstPadding != -1 && firstPadding < input.length() - 2) {
throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)");
}

List<Byte> result = new ArrayList<>();

// Process input in groups of 4 characters
for (int i = 0; i < input.length(); i += 4) {
// Get up to 4 characters
int char1 = getBase64Value(input.charAt(i));
int char2 = getBase64Value(input.charAt(i + 1));
int char3 = input.charAt(i + 2) == '=' ? 0 : getBase64Value(input.charAt(i + 2));
int char4 = input.charAt(i + 3) == '=' ? 0 : getBase64Value(input.charAt(i + 3));

// Combine four 6-bit groups into a 24-bit number
int combined = (char1 << 18) | (char2 << 12) | (char3 << 6) | char4;

// Extract three 8-bit bytes
result.add((byte) ((combined >> 16) & 0xFF));
if (input.charAt(i + 2) != '=') {
result.add((byte) ((combined >> 8) & 0xFF));
}
if (input.charAt(i + 3) != '=') {
result.add((byte) (combined & 0xFF));
}
}

// Convert List<Byte> to byte[]
byte[] resultArray = new byte[result.size()];
for (int i = 0; i < result.size(); i++) {
resultArray[i] = result.get(i);
}

return resultArray;
}

/**
* Decodes the given Base64 encoded string to a string using UTF-8 encoding.
*
* @param input the Base64 encoded string to decode
* @return the decoded string
* @throws IllegalArgumentException if input is null or contains invalid Base64 characters
*/
public static String decodeToString(String input) {
if (input == null) {
throw new IllegalArgumentException("Input cannot be null");
}

byte[] decodedBytes = decode(input);
return new String(decodedBytes, StandardCharsets.UTF_8);
}

/**
* Gets the numeric value of a Base64 character.
*
* @param c the Base64 character
* @return the numeric value (0-63)
* @throws IllegalArgumentException if character is not a valid Base64 character
*/
private static int getBase64Value(char c) {
if (c >= 'A' && c <= 'Z') {
return c - 'A';
} else if (c >= 'a' && c <= 'z') {
return c - 'a' + 26;
} else if (c >= '0' && c <= '9') {
return c - '0' + 52;
} else if (c == '+') {
return 62;
} else if (c == '/') {
return 63;
} else {
throw new IllegalArgumentException("Invalid Base64 character: " + c);
}
}
}
183 changes: 183 additions & 0 deletions src/test/java/com/thealgorithms/conversions/Base64Test.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package com.thealgorithms.conversions;

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 org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

/**
* Test cases for Base64 encoding and decoding.
*
* Author: Nithin U.
* Github: https://github.com/NithinU2802
*/

class Base64Test {

@Test
void testBase64Alphabet() {
// Test that all Base64 characters are handled correctly
String allChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
String encoded = Base64.encode(allChars);
String decoded = Base64.decodeToString(encoded);
assertEquals(allChars, decoded);
}

@ParameterizedTest
@CsvSource({"'', ''", "A, QQ==", "AB, QUI=", "ABC, QUJD", "ABCD, QUJDRA==", "Hello, SGVsbG8=", "'Hello World', SGVsbG8gV29ybGQ=", "'Hello, World!', 'SGVsbG8sIFdvcmxkIQ=='", "'The quick brown fox jumps over the lazy dog', 'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw=='",
"123456789, MTIzNDU2Nzg5", "'Base64 encoding test', 'QmFzZTY0IGVuY29kaW5nIHRlc3Q='"})
void
testStringEncoding(String input, String expected) {
assertEquals(expected, Base64.encode(input));
}

@ParameterizedTest
@CsvSource({"'', ''", "QQ==, A", "QUI=, AB", "QUJD, ABC", "QUJDRA==, ABCD", "SGVsbG8=, Hello", "'SGVsbG8gV29ybGQ=', 'Hello World'", "'SGVsbG8sIFdvcmxkIQ==', 'Hello, World!'", "'VGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==', 'The quick brown fox jumps over the lazy dog'",
"MTIzNDU2Nzg5, 123456789", "'QmFzZTY0IGVuY29kaW5nIHRlc3Q=', 'Base64 encoding test'"})
void
testStringDecoding(String input, String expected) {
assertEquals(expected, Base64.decodeToString(input));
}

@Test
void testByteArrayEncoding() {
byte[] input = {72, 101, 108, 108, 111};
String expected = "SGVsbG8=";
assertEquals(expected, Base64.encode(input));
}

@Test
void testByteArrayDecoding() {
String input = "SGVsbG8=";
byte[] expected = {72, 101, 108, 108, 111};
assertArrayEquals(expected, Base64.decode(input));
}

@Test
void testRoundTripEncoding() {
String[] testStrings = {"", "A", "AB", "ABC", "Hello, World!", "The quick brown fox jumps over the lazy dog", "1234567890", "Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?",
"Unicode: வணக்கம்", // Tamil for "Hello"
"Multi-line\nstring\rwith\tdifferent\nwhitespace"};

for (String original : testStrings) {
String encoded = Base64.encode(original);
String decoded = Base64.decodeToString(encoded);
assertEquals(original, decoded, "Round trip failed for: " + original);
}
}

@Test
void testRoundTripByteArrayEncoding() {
byte[][] testArrays = {{}, {0}, {-1}, {0, 1, 2, 3, 4, 5}, {-128, -1, 0, 1, 127}, {72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33}};

for (byte[] original : testArrays) {
String encoded = Base64.encode(original);
byte[] decoded = Base64.decode(encoded);
assertArrayEquals(original, decoded, "Round trip failed for byte array");
}
}

@Test
void testBinaryData() {
// Test with binary data that might contain null bytes
byte[] binaryData = new byte[256];
for (int i = 0; i < 256; i++) {
binaryData[i] = (byte) i;
}

String encoded = Base64.encode(binaryData);
byte[] decoded = Base64.decode(encoded);
assertArrayEquals(binaryData, decoded);
}

@Test
void testNullInputEncoding() {
assertThrows(IllegalArgumentException.class, () -> Base64.encode((String) null));
assertThrows(IllegalArgumentException.class, () -> Base64.encode((byte[]) null));
}

@Test
void testNullInputDecoding() {
assertThrows(IllegalArgumentException.class, () -> Base64.decode(null));
assertThrows(IllegalArgumentException.class, () -> Base64.decodeToString(null));
}

@Test
void testInvalidBase64Characters() {
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8@"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8#"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8$"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8%"));
}

@Test
void testInvalidLength() {
// Length must be multiple of 4
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQ"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQQ"));
}

@Test
void testInvalidPaddingPosition() {
// '=' can only appear at the end
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=QQ"));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=Q="));
assertThrows(IllegalArgumentException.class, () -> Base64.decode("=QQQ"));
}

@Test
void testPaddingVariations() {
// Test different padding scenarios '='
assertEquals("A", Base64.decodeToString("QQ=="));
assertEquals("AB", Base64.decodeToString("QUI="));
assertEquals("ABC", Base64.decodeToString("QUJD"));
}

@Test
void testPaddingConsistency() {
// Ensure that strings requiring different amounts of padding encode/decode correctly
String[] testCases = {"A", "AB", "ABC", "ABCD", "ABCDE", "ABCDEF"};

for (String test : testCases) {
String encoded = Base64.encode(test);
String decoded = Base64.decodeToString(encoded);
assertEquals(test, decoded);

// Verify padding is correct
int expectedPadding = (3 - (test.length() % 3)) % 3;
int actualPadding = 0;
for (int i = encoded.length() - 1; i >= 0 && encoded.charAt(i) == '='; i--) {
actualPadding++;
}
assertEquals(expectedPadding, actualPadding, "Incorrect padding for: " + test);
}
}

@Test
void testLargeData() {
// Test with larger data to ensure scalability
StringBuilder largeString = new StringBuilder();
for (int i = 0; i < 1000; i++) {
largeString.append("This is a test string for Base64 encoding. ");
}

String original = largeString.toString();
String encoded = Base64.encode(original);
String decoded = Base64.decodeToString(encoded);
assertEquals(original, decoded);
}

@Test
void testEmptyAndSingleCharacter() {
// Test edge cases
assertEquals("", Base64.encode(""));
assertEquals("", Base64.decodeToString(""));

assertEquals("QQ==", Base64.encode("A"));
assertEquals("A", Base64.decodeToString("QQ=="));
}
}