Skip to content

Commit baa86a3

Browse files
committed
GROOVY-7089: Base64 URL Safe encoder (closes groovy#450)
1 parent 81b1cc7 commit baa86a3

File tree

3 files changed

+289
-11
lines changed

3 files changed

+289
-11
lines changed

src/main/org/codehaus/groovy/runtime/EncodingGroovyMethods.java

+109-11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
import java.io.UnsupportedEncodingException;
2828
import java.io.Writer;
2929

30+
import static org.codehaus.groovy.runtime.EncodingGroovyMethodsSupport.TRANSLATE_TABLE;
31+
import static org.codehaus.groovy.runtime.EncodingGroovyMethodsSupport.TRANSLATE_TABLE_URLSAFE;
32+
3033
/**
3134
* This class defines all the encoding/decoding groovy methods which enhance
3235
* the normal JDK classes when inside the Groovy environment.
@@ -36,6 +39,8 @@ public class EncodingGroovyMethods {
3639

3740
private static final char[] T_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=".toCharArray();
3841

42+
private static final char[] T_TABLE_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=".toCharArray();
43+
3944
private static final String CHUNK_SEPARATOR = "\r\n";
4045

4146
/**
@@ -76,18 +81,22 @@ public static Writable encodeBase64(Byte[] data) {
7681
* @since 1.5.7
7782
*/
7883
public static Writable encodeBase64(final byte[] data, final boolean chunked) {
84+
return encodeBase64(data, chunked, false, true);
85+
}
86+
87+
private static Writable encodeBase64(final byte[] data, final boolean chunked, final boolean urlSafe, final boolean pad) {
7988
return new Writable() {
8089
public Writer writeTo(final Writer writer) throws IOException {
8190
int charCount = 0;
8291
final int dLimit = (data.length / 3) * 3;
83-
92+
final char[] table = urlSafe ? T_TABLE_URLSAFE : T_TABLE;
8493
for (int dIndex = 0; dIndex != dLimit; dIndex += 3) {
8594
int d = ((data[dIndex] & 0XFF) << 16) | ((data[dIndex + 1] & 0XFF) << 8) | (data[dIndex + 2] & 0XFF);
8695

87-
writer.write(T_TABLE[d >> 18]);
88-
writer.write(T_TABLE[(d >> 12) & 0X3F]);
89-
writer.write(T_TABLE[(d >> 6) & 0X3F]);
90-
writer.write(T_TABLE[d & 0X3F]);
96+
writer.write(table[d >> 18]);
97+
writer.write(table[(d >> 12) & 0X3F]);
98+
writer.write(table[(d >> 6) & 0X3F]);
99+
writer.write(table[d & 0X3F]);
91100

92101
if (chunked && ++charCount == 19) {
93102
writer.write(CHUNK_SEPARATOR);
@@ -102,10 +111,16 @@ public Writer writeTo(final Writer writer) throws IOException {
102111
d |= (data[dLimit + 1] & 0XFF) << 8;
103112
}
104113

105-
writer.write(T_TABLE[d >> 18]);
106-
writer.write(T_TABLE[(d >> 12) & 0X3F]);
107-
writer.write((dLimit + 1 < data.length) ? T_TABLE[(d >> 6) & 0X3F] : '=');
108-
writer.write('=');
114+
writer.write(table[d >> 18]);
115+
writer.write(table[(d >> 12) & 0X3F]);
116+
if (pad) {
117+
writer.write((dLimit + 1 < data.length) ? table[(d >> 6) & 0X3F] : '=');
118+
writer.write('=');
119+
} else {
120+
if (dLimit + 1 < data.length) {
121+
writer.write(table[(d >> 6) & 0X3F]);
122+
}
123+
}
109124
if (chunked && charCount != 0) {
110125
writer.write(CHUNK_SEPARATOR);
111126
}
@@ -141,6 +156,74 @@ public static Writable encodeBase64(final byte[] data) {
141156
return encodeBase64(data, false);
142157
}
143158

159+
/**
160+
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
161+
* Calling toString() on the result returns the encoding as a String. For more
162+
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
163+
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
164+
* <p>
165+
* The method omits padding and is equivalent to calling
166+
* {@link org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(Byte[], boolean)} with a
167+
* value of {@code false}.
168+
*
169+
* @param data Byte array to be encoded
170+
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
171+
* @see org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(Byte[], boolean)
172+
* @since 2.5
173+
*/
174+
public static Writable encodeBase64Url(Byte[] data) {
175+
return encodeBase64Url(data, false);
176+
}
177+
178+
/**
179+
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
180+
* Calling toString() on the result returns the encoding as a String. For more
181+
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
182+
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
183+
*
184+
* @param data Byte array to be encoded
185+
* @param pad whether or not the encoded data should be padded
186+
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
187+
* @since 2.5
188+
*/
189+
public static Writable encodeBase64Url(Byte[] data, boolean pad) {
190+
return encodeBase64Url(DefaultTypeTransformation.convertToByteArray(data), pad);
191+
}
192+
193+
/**
194+
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
195+
* Calling toString() on the result returns the encoding as a String. For more
196+
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
197+
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
198+
* <p>
199+
* The method omits padding and is equivalent to calling
200+
* {@link org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(byte[], boolean)} with a
201+
* value of {@code false}.
202+
*
203+
* @param data Byte array to be encoded
204+
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
205+
* @see org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(byte[], boolean)
206+
* @since 2.5
207+
*/
208+
public static Writable encodeBase64Url(final byte[] data) {
209+
return encodeBase64Url(data, false);
210+
}
211+
212+
/**
213+
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
214+
* Calling toString() on the result returns the encoding as a String. For more
215+
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
216+
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
217+
*
218+
* @param data Byte array to be encoded
219+
* @param pad whether or not the encoded data should be padded
220+
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
221+
* @since 2.5
222+
*/
223+
public static Writable encodeBase64Url(final byte[] data, final boolean pad) {
224+
return encodeBase64(data, false, true, pad);
225+
}
226+
144227
/**
145228
* Decode the String from Base64 into a byte array.
146229
*
@@ -149,14 +232,29 @@ public static Writable encodeBase64(final byte[] data) {
149232
* @since 1.0
150233
*/
151234
public static byte[] decodeBase64(String value) {
235+
return decodeBase64(value, false);
236+
}
237+
238+
/**
239+
* Decodes a Base64 URL and Filename Safe encoded String into a byte array.
240+
*
241+
* @param value the string to be decoded
242+
* @return the decoded bytes as an array
243+
* @since 2.5
244+
*/
245+
public static byte[] decodeBase64Url(String value) {
246+
return decodeBase64(value, true);
247+
}
248+
249+
private static byte[] decodeBase64(String value, boolean urlSafe) {
152250
int byteShift = 4;
153251
int tmp = 0;
154252
boolean done = false;
155253
final StringBuilder buffer = new StringBuilder();
156-
254+
final byte[] table = urlSafe ? TRANSLATE_TABLE_URLSAFE : TRANSLATE_TABLE;
157255
for (int i = 0; i != value.length(); i++) {
158256
final char c = value.charAt(i);
159-
final int sixBit = (c < 123) ? EncodingGroovyMethodsSupport.TRANSLATE_TABLE[c] : 66;
257+
final int sixBit = (c < 123) ? table[c] : 66;
160258

161259
if (sixBit < 64) {
162260
if (done)

src/main/org/codehaus/groovy/runtime/EncodingGroovyMethodsSupport.java

+33
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,37 @@ public class EncodingGroovyMethodsSupport {
5454
+ "\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030"
5555
// x y z
5656
+ "\u0031\u0032\u0033").getBytes();
57+
58+
static final byte[] TRANSLATE_TABLE_URLSAFE = (
59+
"\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
60+
// \t \n \r
61+
+ "\u0042\u0042\u0041\u0041\u0042\u0042\u0041\u0042"
62+
//
63+
+ "\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
64+
//
65+
+ "\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
66+
// sp ! " # $ % & '
67+
+ "\u0041\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
68+
// ( ) * + , - . /
69+
+ "\u0042\u0042\u0042\u0042\u0042\u003E\u0042\u0042"
70+
// 0 1 2 3 4 5 6 7
71+
+ "\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B"
72+
// 8 9 : ; < = > ?
73+
+ "\u003C\u003D\u0042\u0042\u0042\u0040\u0042\u0042"
74+
// @ A B C D E F G
75+
+ "\u0042\u0000\u0001\u0002\u0003\u0004\u0005\u0006"
76+
// H I J K L M N O
77+
+ "\u0007\u0008\t\n\u000B\u000C\r\u000E"
78+
// P Q R S T U V W
79+
+ "\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016"
80+
// X Y Z [ \ ] ^ _
81+
+ "\u0017\u0018\u0019\u0042\u0042\u0042\u0042\u003F"
82+
// ' a b c d e f g
83+
+ "\u0042\u001A\u001B\u001C\u001D\u001E\u001F\u0020"
84+
// h i j k l m n o p
85+
+ "\u0021\"\u0023\u0024\u0025\u0026\u0027\u0028"
86+
// p q r s t u v w
87+
+ "\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030"
88+
// x y z
89+
+ "\u0031\u0032\u0033").getBytes();
5790
}

src/test/groovy/Base64Test.groovy

+147
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,15 @@
1818
*/
1919
package groovy
2020

21+
import java.nio.charset.StandardCharsets
22+
2123
class Base64Test extends GroovyTestCase {
2224
String testString = '\u00A71234567890-=\u00B1!@\u00A3\$%^&*()_+qwertyuiop[]QWERTYUIOP{}asdfghjkl;\'\\ASDFGHJKL:"|`zxcvbnm,./~ZXCVBNM<>?\u0003\u00ff\u00f0\u000f'
2325
byte[] testBytes = testString.getBytes("ISO-8859-1")
2426

27+
// Test bytes that have both the 62nd and 63rd base64 alphabet in the encoded string
28+
static final byte[] testBytesChar62And63 = new BigInteger('4bf7ce5201fe239ab42ebead5acd8fa3', 16).toByteArray()
29+
2530
void testCodec() {
2631
// turn the bytes back into a string for later comparison
2732
def savedString = new String(testBytes, "ISO-8859-1")
@@ -62,4 +67,146 @@ class Base64Test extends GroovyTestCase {
6267
def encodedBytes = testBytes.encodeBase64().toString()
6368
assert encodedBytes == 'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31hc2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4/A//wDw=='
6469
}
70+
71+
void testRfc4648Section10Encoding() {
72+
assert b64('') == ''
73+
assert b64('f') == 'Zg=='
74+
assert b64('fo') == 'Zm8='
75+
assert b64('foo') == 'Zm9v'
76+
assert b64('foob') == 'Zm9vYg=='
77+
assert b64('fooba') == 'Zm9vYmE='
78+
assert b64('foobar') == 'Zm9vYmFy'
79+
}
80+
81+
void testRfc4648Section10Decoding() {
82+
assert decodeB64('') == ''
83+
84+
assert decodeB64('Zg') == 'f'
85+
assert decodeB64('Zg==') == 'f'
86+
87+
assert decodeB64('Zm8') == 'fo'
88+
assert decodeB64('Zm8=') == 'fo'
89+
90+
assert decodeB64('Zm9v') == 'foo'
91+
92+
assert decodeB64('Zm9vYg') == 'foob'
93+
assert decodeB64('Zm9vYg==') == 'foob'
94+
95+
assert decodeB64('Zm9vYmE') == 'fooba'
96+
assert decodeB64('Zm9vYmE=') == 'fooba'
97+
98+
assert decodeB64('Zm9vYmFy') == 'foobar'
99+
}
100+
101+
void testRfc4648Section10EncodingUrlSafe() {
102+
assert b64url('') == ''
103+
assert b64url('f') == 'Zg'
104+
assert b64url('fo') == 'Zm8'
105+
assert b64url('foo') == 'Zm9v'
106+
assert b64url('foob') == 'Zm9vYg'
107+
assert b64url('fooba') == 'Zm9vYmE'
108+
assert b64url('foobar') == 'Zm9vYmFy'
109+
}
110+
111+
void testRfc4648Section10EncodingUrlSafeWithPadding() {
112+
assert b64url('', true) == ''
113+
assert b64url('f', true) == 'Zg=='
114+
assert b64url('fo', true) == 'Zm8='
115+
assert b64url('foo', true) == 'Zm9v'
116+
assert b64url('foob', true) == 'Zm9vYg=='
117+
assert b64url('fooba', true) == 'Zm9vYmE='
118+
assert b64url('foobar', true) == 'Zm9vYmFy'
119+
}
120+
121+
void testRfc4648Section10DecodingUrlSafe() {
122+
assert decodeB64url('') == ''
123+
124+
assert decodeB64url('Zg') == 'f'
125+
assert decodeB64url('Zg==') == 'f'
126+
127+
assert decodeB64url('Zm8') == 'fo'
128+
assert decodeB64url('Zm8=') == 'fo'
129+
130+
assert decodeB64url('Zm9v') == 'foo'
131+
132+
assert decodeB64url('Zm9vYg') == 'foob'
133+
assert decodeB64url('Zm9vYg==') == 'foob'
134+
135+
assert decodeB64url('Zm9vYmE') == 'fooba'
136+
assert decodeB64url('Zm9vYmE=') == 'fooba'
137+
138+
assert decodeB64url('Zm9vYmFy') == 'foobar'
139+
}
140+
141+
void testEncodingWithChar62And63() {
142+
assert testBytesChar62And63.encodeBase64().toString() == 'S/fOUgH+I5q0Lr6tWs2Pow=='
143+
}
144+
145+
void testUrlSafeEncodingWithChar62And63() {
146+
assert testBytesChar62And63.encodeBase64Url().toString() == 'S_fOUgH-I5q0Lr6tWs2Pow'
147+
assert testBytesChar62And63.encodeBase64Url(true).toString() == 'S_fOUgH-I5q0Lr6tWs2Pow=='
148+
}
149+
150+
void testDecodingWithChar62And63() {
151+
assert 'S/fOUgH+I5q0Lr6tWs2Pow=='.decodeBase64() == testBytesChar62And63
152+
assert 'S/fOUgH+I5q0Lr6tWs2Pow'.decodeBase64() == testBytesChar62And63
153+
}
154+
155+
void testUrlSafeDecodingWithChar62And63() {
156+
assert 'S_fOUgH-I5q0Lr6tWs2Pow=='.decodeBase64Url() == testBytesChar62And63
157+
assert 'S_fOUgH-I5q0Lr6tWs2Pow'.decodeBase64Url() == testBytesChar62And63
158+
}
159+
160+
void testUrlSafeEncodingByDefaultOmitsPadding() {
161+
assert testBytes.encodeBase64Url().toString() ==
162+
'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31h' +
163+
'c2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4_A__wDw'
164+
}
165+
166+
void testUrlSafeEncodingWithPadding() {
167+
assert testBytes.encodeBase64Url(true).toString() ==
168+
'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31h' +
169+
'c2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4_A__wDw=='
170+
}
171+
172+
void testDecodingNonBase64Alphabet() {
173+
shouldFail {
174+
decodeB64('S_fOUgH-I5q0Lr6tWs2Pow==')
175+
}
176+
}
177+
178+
void testUrlSafeDecodingNonUrlSafeAlphabet() {
179+
shouldFail {
180+
decodeB64url('S/fOUgH+I5q0Lr6tWs2Pow==')
181+
}
182+
}
183+
184+
void testDecodingWithInnerPad() {
185+
shouldFail {
186+
decodeB64('Zm9v=YmE=')
187+
}
188+
}
189+
190+
void testUrlSafeDecodingWithInnerPad() {
191+
shouldFail {
192+
decodeB64url('Zm9v=YmE=')
193+
}
194+
}
195+
196+
// Test helper methods
197+
private static String b64(String s) {
198+
s.getBytes(StandardCharsets.UTF_8).encodeBase64().toString()
199+
}
200+
201+
private static String b64url(String s, boolean pad=false) {
202+
s.getBytes(StandardCharsets.UTF_8).encodeBase64Url(pad).toString()
203+
}
204+
205+
private static String decodeB64(String s) {
206+
new String(s.decodeBase64(), StandardCharsets.UTF_8)
207+
}
208+
209+
private static String decodeB64url(String s) {
210+
new String(s.decodeBase64Url(), StandardCharsets.UTF_8)
211+
}
65212
}

0 commit comments

Comments
 (0)