Skip to content

Commit 2be984f

Browse files
NithinU2802alxkm
andauthored
To add Implementation of Base64 Algorithm (#6586)
* feat: base64 algorithm implementation * feat: test coverage implementation of Base64Test.java * Update DIRECTORY.md * fix: build and lint fix update * patch: linter fix * fix: enforce strict RFC 4648 compliance in Base64 decoding * fix: enforce strict rfc 4648 compliance in base64 decoding --------- Co-authored-by: Oleksandr Klymenko <alexanderklmn@gmail.com>
1 parent d8ddb07 commit 2be984f

File tree

3 files changed

+381
-0
lines changed

3 files changed

+381
-0
lines changed

DIRECTORY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
- 📄 [AnyBaseToAnyBase](src/main/java/com/thealgorithms/conversions/AnyBaseToAnyBase.java)
9595
- 📄 [AnyBaseToDecimal](src/main/java/com/thealgorithms/conversions/AnyBaseToDecimal.java)
9696
- 📄 [AnytoAny](src/main/java/com/thealgorithms/conversions/AnytoAny.java)
97+
- 📄 [Base64](src/main/java/com/thealgorithms/conversions/Base64.java)
9798
- 📄 [BinaryToDecimal](src/main/java/com/thealgorithms/conversions/BinaryToDecimal.java)
9899
- 📄 [BinaryToHexadecimal](src/main/java/com/thealgorithms/conversions/BinaryToHexadecimal.java)
99100
- 📄 [BinaryToOctal](src/main/java/com/thealgorithms/conversions/BinaryToOctal.java)
@@ -839,6 +840,7 @@
839840
- 📄 [AffineConverterTest](src/test/java/com/thealgorithms/conversions/AffineConverterTest.java)
840841
- 📄 [AnyBaseToDecimalTest](src/test/java/com/thealgorithms/conversions/AnyBaseToDecimalTest.java)
841842
- 📄 [AnytoAnyTest](src/test/java/com/thealgorithms/conversions/AnytoAnyTest.java)
843+
- 📄 [Base64Test](src/test/java/com/thealgorithms/conversions/Base64Test.java)
842844
- 📄 [BinaryToDecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToDecimalTest.java)
843845
- 📄 [BinaryToHexadecimalTest](src/test/java/com/thealgorithms/conversions/BinaryToHexadecimalTest.java)
844846
- 📄 [BinaryToOctalTest](src/test/java/com/thealgorithms/conversions/BinaryToOctalTest.java)
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
package com.thealgorithms.conversions;
2+
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
7+
/**
8+
* Base64 is a group of binary-to-text encoding schemes that represent binary data
9+
* in an ASCII string format by translating it into a radix-64 representation.
10+
* Each base64 digit represents exactly 6 bits of data.
11+
*
12+
* Base64 encoding is commonly used when there is a need to encode binary data
13+
* that needs to be stored and transferred over media that are designed to deal
14+
* with textual data.
15+
*
16+
* Wikipedia Reference: https://en.wikipedia.org/wiki/Base64
17+
* Author: Nithin U.
18+
* Github: https://github.com/NithinU2802
19+
*/
20+
21+
public final class Base64 {
22+
23+
// Base64 character set
24+
private static final String BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
25+
private static final char PADDING_CHAR = '=';
26+
27+
private Base64() {
28+
}
29+
30+
/**
31+
* Encodes the given byte array to a Base64 encoded string.
32+
*
33+
* @param input the byte array to encode
34+
* @return the Base64 encoded string
35+
* @throws IllegalArgumentException if input is null
36+
*/
37+
public static String encode(byte[] input) {
38+
if (input == null) {
39+
throw new IllegalArgumentException("Input cannot be null");
40+
}
41+
42+
if (input.length == 0) {
43+
return "";
44+
}
45+
46+
StringBuilder result = new StringBuilder();
47+
int padding = 0;
48+
49+
// Process input in groups of 3 bytes
50+
for (int i = 0; i < input.length; i += 3) {
51+
// Get up to 3 bytes
52+
int byte1 = input[i] & 0xFF;
53+
int byte2 = (i + 1 < input.length) ? (input[i + 1] & 0xFF) : 0;
54+
int byte3 = (i + 2 < input.length) ? (input[i + 2] & 0xFF) : 0;
55+
56+
// Calculate padding needed
57+
if (i + 1 >= input.length) {
58+
padding = 2;
59+
} else if (i + 2 >= input.length) {
60+
padding = 1;
61+
}
62+
63+
// Combine 3 bytes into a 24-bit number
64+
int combined = (byte1 << 16) | (byte2 << 8) | byte3;
65+
66+
// Extract four 6-bit groups
67+
result.append(BASE64_CHARS.charAt((combined >> 18) & 0x3F));
68+
result.append(BASE64_CHARS.charAt((combined >> 12) & 0x3F));
69+
result.append(BASE64_CHARS.charAt((combined >> 6) & 0x3F));
70+
result.append(BASE64_CHARS.charAt(combined & 0x3F));
71+
}
72+
73+
// Replace padding characters
74+
if (padding > 0) {
75+
result.setLength(result.length() - padding);
76+
for (int i = 0; i < padding; i++) {
77+
result.append(PADDING_CHAR);
78+
}
79+
}
80+
81+
return result.toString();
82+
}
83+
84+
/**
85+
* Encodes the given string to a Base64 encoded string using UTF-8 encoding.
86+
*
87+
* @param input the string to encode
88+
* @return the Base64 encoded string
89+
* @throws IllegalArgumentException if input is null
90+
*/
91+
public static String encode(String input) {
92+
if (input == null) {
93+
throw new IllegalArgumentException("Input cannot be null");
94+
}
95+
96+
return encode(input.getBytes(StandardCharsets.UTF_8));
97+
}
98+
99+
/**
100+
* Decodes the given Base64 encoded string to a byte array.
101+
*
102+
* @param input the Base64 encoded string to decode
103+
* @return the decoded byte array
104+
* @throws IllegalArgumentException if input is null or contains invalid Base64 characters
105+
*/
106+
public static byte[] decode(String input) {
107+
if (input == null) {
108+
throw new IllegalArgumentException("Input cannot be null");
109+
}
110+
111+
if (input.isEmpty()) {
112+
return new byte[0];
113+
}
114+
115+
// Strict RFC 4648 compliance: length must be a multiple of 4
116+
if (input.length() % 4 != 0) {
117+
throw new IllegalArgumentException("Invalid Base64 input length; must be multiple of 4");
118+
}
119+
120+
// Validate padding: '=' can only appear at the end (last 1 or 2 chars)
121+
int firstPadding = input.indexOf('=');
122+
if (firstPadding != -1 && firstPadding < input.length() - 2) {
123+
throw new IllegalArgumentException("Padding '=' can only appear at the end (last 1 or 2 characters)");
124+
}
125+
126+
List<Byte> result = new ArrayList<>();
127+
128+
// Process input in groups of 4 characters
129+
for (int i = 0; i < input.length(); i += 4) {
130+
// Get up to 4 characters
131+
int char1 = getBase64Value(input.charAt(i));
132+
int char2 = getBase64Value(input.charAt(i + 1));
133+
int char3 = input.charAt(i + 2) == '=' ? 0 : getBase64Value(input.charAt(i + 2));
134+
int char4 = input.charAt(i + 3) == '=' ? 0 : getBase64Value(input.charAt(i + 3));
135+
136+
// Combine four 6-bit groups into a 24-bit number
137+
int combined = (char1 << 18) | (char2 << 12) | (char3 << 6) | char4;
138+
139+
// Extract three 8-bit bytes
140+
result.add((byte) ((combined >> 16) & 0xFF));
141+
if (input.charAt(i + 2) != '=') {
142+
result.add((byte) ((combined >> 8) & 0xFF));
143+
}
144+
if (input.charAt(i + 3) != '=') {
145+
result.add((byte) (combined & 0xFF));
146+
}
147+
}
148+
149+
// Convert List<Byte> to byte[]
150+
byte[] resultArray = new byte[result.size()];
151+
for (int i = 0; i < result.size(); i++) {
152+
resultArray[i] = result.get(i);
153+
}
154+
155+
return resultArray;
156+
}
157+
158+
/**
159+
* Decodes the given Base64 encoded string to a string using UTF-8 encoding.
160+
*
161+
* @param input the Base64 encoded string to decode
162+
* @return the decoded string
163+
* @throws IllegalArgumentException if input is null or contains invalid Base64 characters
164+
*/
165+
public static String decodeToString(String input) {
166+
if (input == null) {
167+
throw new IllegalArgumentException("Input cannot be null");
168+
}
169+
170+
byte[] decodedBytes = decode(input);
171+
return new String(decodedBytes, StandardCharsets.UTF_8);
172+
}
173+
174+
/**
175+
* Gets the numeric value of a Base64 character.
176+
*
177+
* @param c the Base64 character
178+
* @return the numeric value (0-63)
179+
* @throws IllegalArgumentException if character is not a valid Base64 character
180+
*/
181+
private static int getBase64Value(char c) {
182+
if (c >= 'A' && c <= 'Z') {
183+
return c - 'A';
184+
} else if (c >= 'a' && c <= 'z') {
185+
return c - 'a' + 26;
186+
} else if (c >= '0' && c <= '9') {
187+
return c - '0' + 52;
188+
} else if (c == '+') {
189+
return 62;
190+
} else if (c == '/') {
191+
return 63;
192+
} else {
193+
throw new IllegalArgumentException("Invalid Base64 character: " + c);
194+
}
195+
}
196+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.thealgorithms.conversions;
2+
3+
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.params.ParameterizedTest;
9+
import org.junit.jupiter.params.provider.CsvSource;
10+
11+
/**
12+
* Test cases for Base64 encoding and decoding.
13+
*
14+
* Author: Nithin U.
15+
* Github: https://github.com/NithinU2802
16+
*/
17+
18+
class Base64Test {
19+
20+
@Test
21+
void testBase64Alphabet() {
22+
// Test that all Base64 characters are handled correctly
23+
String allChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
24+
String encoded = Base64.encode(allChars);
25+
String decoded = Base64.decodeToString(encoded);
26+
assertEquals(allChars, decoded);
27+
}
28+
29+
@ParameterizedTest
30+
@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=='",
31+
"123456789, MTIzNDU2Nzg5", "'Base64 encoding test', 'QmFzZTY0IGVuY29kaW5nIHRlc3Q='"})
32+
void
33+
testStringEncoding(String input, String expected) {
34+
assertEquals(expected, Base64.encode(input));
35+
}
36+
37+
@ParameterizedTest
38+
@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'",
39+
"MTIzNDU2Nzg5, 123456789", "'QmFzZTY0IGVuY29kaW5nIHRlc3Q=', 'Base64 encoding test'"})
40+
void
41+
testStringDecoding(String input, String expected) {
42+
assertEquals(expected, Base64.decodeToString(input));
43+
}
44+
45+
@Test
46+
void testByteArrayEncoding() {
47+
byte[] input = {72, 101, 108, 108, 111};
48+
String expected = "SGVsbG8=";
49+
assertEquals(expected, Base64.encode(input));
50+
}
51+
52+
@Test
53+
void testByteArrayDecoding() {
54+
String input = "SGVsbG8=";
55+
byte[] expected = {72, 101, 108, 108, 111};
56+
assertArrayEquals(expected, Base64.decode(input));
57+
}
58+
59+
@Test
60+
void testRoundTripEncoding() {
61+
String[] testStrings = {"", "A", "AB", "ABC", "Hello, World!", "The quick brown fox jumps over the lazy dog", "1234567890", "Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?",
62+
"Unicode: வணக்கம்", // Tamil for "Hello"
63+
"Multi-line\nstring\rwith\tdifferent\nwhitespace"};
64+
65+
for (String original : testStrings) {
66+
String encoded = Base64.encode(original);
67+
String decoded = Base64.decodeToString(encoded);
68+
assertEquals(original, decoded, "Round trip failed for: " + original);
69+
}
70+
}
71+
72+
@Test
73+
void testRoundTripByteArrayEncoding() {
74+
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}};
75+
76+
for (byte[] original : testArrays) {
77+
String encoded = Base64.encode(original);
78+
byte[] decoded = Base64.decode(encoded);
79+
assertArrayEquals(original, decoded, "Round trip failed for byte array");
80+
}
81+
}
82+
83+
@Test
84+
void testBinaryData() {
85+
// Test with binary data that might contain null bytes
86+
byte[] binaryData = new byte[256];
87+
for (int i = 0; i < 256; i++) {
88+
binaryData[i] = (byte) i;
89+
}
90+
91+
String encoded = Base64.encode(binaryData);
92+
byte[] decoded = Base64.decode(encoded);
93+
assertArrayEquals(binaryData, decoded);
94+
}
95+
96+
@Test
97+
void testNullInputEncoding() {
98+
assertThrows(IllegalArgumentException.class, () -> Base64.encode((String) null));
99+
assertThrows(IllegalArgumentException.class, () -> Base64.encode((byte[]) null));
100+
}
101+
102+
@Test
103+
void testNullInputDecoding() {
104+
assertThrows(IllegalArgumentException.class, () -> Base64.decode(null));
105+
assertThrows(IllegalArgumentException.class, () -> Base64.decodeToString(null));
106+
}
107+
108+
@Test
109+
void testInvalidBase64Characters() {
110+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8@"));
111+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8#"));
112+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8$"));
113+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("SGVsbG8%"));
114+
}
115+
116+
@Test
117+
void testInvalidLength() {
118+
// Length must be multiple of 4
119+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q"));
120+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQ"));
121+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("QQQ"));
122+
}
123+
124+
@Test
125+
void testInvalidPaddingPosition() {
126+
// '=' can only appear at the end
127+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=QQ"));
128+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("Q=Q="));
129+
assertThrows(IllegalArgumentException.class, () -> Base64.decode("=QQQ"));
130+
}
131+
132+
@Test
133+
void testPaddingVariations() {
134+
// Test different padding scenarios '='
135+
assertEquals("A", Base64.decodeToString("QQ=="));
136+
assertEquals("AB", Base64.decodeToString("QUI="));
137+
assertEquals("ABC", Base64.decodeToString("QUJD"));
138+
}
139+
140+
@Test
141+
void testPaddingConsistency() {
142+
// Ensure that strings requiring different amounts of padding encode/decode correctly
143+
String[] testCases = {"A", "AB", "ABC", "ABCD", "ABCDE", "ABCDEF"};
144+
145+
for (String test : testCases) {
146+
String encoded = Base64.encode(test);
147+
String decoded = Base64.decodeToString(encoded);
148+
assertEquals(test, decoded);
149+
150+
// Verify padding is correct
151+
int expectedPadding = (3 - (test.length() % 3)) % 3;
152+
int actualPadding = 0;
153+
for (int i = encoded.length() - 1; i >= 0 && encoded.charAt(i) == '='; i--) {
154+
actualPadding++;
155+
}
156+
assertEquals(expectedPadding, actualPadding, "Incorrect padding for: " + test);
157+
}
158+
}
159+
160+
@Test
161+
void testLargeData() {
162+
// Test with larger data to ensure scalability
163+
StringBuilder largeString = new StringBuilder();
164+
for (int i = 0; i < 1000; i++) {
165+
largeString.append("This is a test string for Base64 encoding. ");
166+
}
167+
168+
String original = largeString.toString();
169+
String encoded = Base64.encode(original);
170+
String decoded = Base64.decodeToString(encoded);
171+
assertEquals(original, decoded);
172+
}
173+
174+
@Test
175+
void testEmptyAndSingleCharacter() {
176+
// Test edge cases
177+
assertEquals("", Base64.encode(""));
178+
assertEquals("", Base64.decodeToString(""));
179+
180+
assertEquals("QQ==", Base64.encode("A"));
181+
assertEquals("A", Base64.decodeToString("QQ=="));
182+
}
183+
}

0 commit comments

Comments
 (0)