diff --git a/DIRECTORY.md b/DIRECTORY.md index 8ae7aea61922..b311b10fa177 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -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) @@ -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) diff --git a/src/main/java/com/thealgorithms/conversions/Base64.java b/src/main/java/com/thealgorithms/conversions/Base64.java new file mode 100644 index 000000000000..5219c4ba7f4e --- /dev/null +++ b/src/main/java/com/thealgorithms/conversions/Base64.java @@ -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 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 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); + } + } +} diff --git a/src/test/java/com/thealgorithms/conversions/Base64Test.java b/src/test/java/com/thealgorithms/conversions/Base64Test.java new file mode 100644 index 000000000000..fbc220c0ca95 --- /dev/null +++ b/src/test/java/com/thealgorithms/conversions/Base64Test.java @@ -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==")); + } +}