Skip to content

Commit

Permalink
GROOVY-7089: Base64 URL Safe encoder (closes groovy#450)
Browse files Browse the repository at this point in the history
  • Loading branch information
jwagenleitner committed Nov 13, 2016
1 parent 81b1cc7 commit baa86a3
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 11 deletions.
120 changes: 109 additions & 11 deletions src/main/org/codehaus/groovy/runtime/EncodingGroovyMethods.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
import java.io.UnsupportedEncodingException;
import java.io.Writer;

import static org.codehaus.groovy.runtime.EncodingGroovyMethodsSupport.TRANSLATE_TABLE;
import static org.codehaus.groovy.runtime.EncodingGroovyMethodsSupport.TRANSLATE_TABLE_URLSAFE;

/**
* This class defines all the encoding/decoding groovy methods which enhance
* the normal JDK classes when inside the Groovy environment.
Expand All @@ -36,6 +39,8 @@ public class EncodingGroovyMethods {

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

private static final char[] T_TABLE_URLSAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_=".toCharArray();

private static final String CHUNK_SEPARATOR = "\r\n";

/**
Expand Down Expand Up @@ -76,18 +81,22 @@ public static Writable encodeBase64(Byte[] data) {
* @since 1.5.7
*/
public static Writable encodeBase64(final byte[] data, final boolean chunked) {
return encodeBase64(data, chunked, false, true);
}

private static Writable encodeBase64(final byte[] data, final boolean chunked, final boolean urlSafe, final boolean pad) {
return new Writable() {
public Writer writeTo(final Writer writer) throws IOException {
int charCount = 0;
final int dLimit = (data.length / 3) * 3;

final char[] table = urlSafe ? T_TABLE_URLSAFE : T_TABLE;
for (int dIndex = 0; dIndex != dLimit; dIndex += 3) {
int d = ((data[dIndex] & 0XFF) << 16) | ((data[dIndex + 1] & 0XFF) << 8) | (data[dIndex + 2] & 0XFF);

writer.write(T_TABLE[d >> 18]);
writer.write(T_TABLE[(d >> 12) & 0X3F]);
writer.write(T_TABLE[(d >> 6) & 0X3F]);
writer.write(T_TABLE[d & 0X3F]);
writer.write(table[d >> 18]);
writer.write(table[(d >> 12) & 0X3F]);
writer.write(table[(d >> 6) & 0X3F]);
writer.write(table[d & 0X3F]);

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

writer.write(T_TABLE[d >> 18]);
writer.write(T_TABLE[(d >> 12) & 0X3F]);
writer.write((dLimit + 1 < data.length) ? T_TABLE[(d >> 6) & 0X3F] : '=');
writer.write('=');
writer.write(table[d >> 18]);
writer.write(table[(d >> 12) & 0X3F]);
if (pad) {
writer.write((dLimit + 1 < data.length) ? table[(d >> 6) & 0X3F] : '=');
writer.write('=');
} else {
if (dLimit + 1 < data.length) {
writer.write(table[(d >> 6) & 0X3F]);
}
}
if (chunked && charCount != 0) {
writer.write(CHUNK_SEPARATOR);
}
Expand Down Expand Up @@ -141,6 +156,74 @@ public static Writable encodeBase64(final byte[] data) {
return encodeBase64(data, false);
}

/**
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
* Calling toString() on the result returns the encoding as a String. For more
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
* <p>
* The method omits padding and is equivalent to calling
* {@link org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(Byte[], boolean)} with a
* value of {@code false}.
*
* @param data Byte array to be encoded
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
* @see org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(Byte[], boolean)
* @since 2.5
*/
public static Writable encodeBase64Url(Byte[] data) {
return encodeBase64Url(data, false);
}

/**
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
* Calling toString() on the result returns the encoding as a String. For more
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
*
* @param data Byte array to be encoded
* @param pad whether or not the encoded data should be padded
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
* @since 2.5
*/
public static Writable encodeBase64Url(Byte[] data, boolean pad) {
return encodeBase64Url(DefaultTypeTransformation.convertToByteArray(data), pad);
}

/**
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
* Calling toString() on the result returns the encoding as a String. For more
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
* <p>
* The method omits padding and is equivalent to calling
* {@link org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(byte[], boolean)} with a
* value of {@code false}.
*
* @param data Byte array to be encoded
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
* @see org.codehaus.groovy.runtime.EncodingGroovyMethods#encodeBase64Url(byte[], boolean)
* @since 2.5
*/
public static Writable encodeBase64Url(final byte[] data) {
return encodeBase64Url(data, false);
}

/**
* Produce a Writable object which writes the Base64 URL and Filename Safe encoding of the byte array.
* Calling toString() on the result returns the encoding as a String. For more
* information on Base64 URL and Filename Safe encoding see <code>RFC 4648 - Section 5
* Base 64 Encoding with URL and Filename Safe Alphabet</code>.
*
* @param data Byte array to be encoded
* @param pad whether or not the encoded data should be padded
* @return object which will write the Base64 URL and Filename Safe encoding of the byte array
* @since 2.5
*/
public static Writable encodeBase64Url(final byte[] data, final boolean pad) {
return encodeBase64(data, false, true, pad);
}

/**
* Decode the String from Base64 into a byte array.
*
Expand All @@ -149,14 +232,29 @@ public static Writable encodeBase64(final byte[] data) {
* @since 1.0
*/
public static byte[] decodeBase64(String value) {
return decodeBase64(value, false);
}

/**
* Decodes a Base64 URL and Filename Safe encoded String into a byte array.
*
* @param value the string to be decoded
* @return the decoded bytes as an array
* @since 2.5
*/
public static byte[] decodeBase64Url(String value) {
return decodeBase64(value, true);
}

private static byte[] decodeBase64(String value, boolean urlSafe) {
int byteShift = 4;
int tmp = 0;
boolean done = false;
final StringBuilder buffer = new StringBuilder();

final byte[] table = urlSafe ? TRANSLATE_TABLE_URLSAFE : TRANSLATE_TABLE;
for (int i = 0; i != value.length(); i++) {
final char c = value.charAt(i);
final int sixBit = (c < 123) ? EncodingGroovyMethodsSupport.TRANSLATE_TABLE[c] : 66;
final int sixBit = (c < 123) ? table[c] : 66;

if (sixBit < 64) {
if (done)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,37 @@ public class EncodingGroovyMethodsSupport {
+ "\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030"
// x y z
+ "\u0031\u0032\u0033").getBytes();

static final byte[] TRANSLATE_TABLE_URLSAFE = (
"\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
// \t \n \r
+ "\u0042\u0042\u0041\u0041\u0042\u0042\u0041\u0042"
//
+ "\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
//
+ "\u0042\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
// sp ! " # $ % & '
+ "\u0041\u0042\u0042\u0042\u0042\u0042\u0042\u0042"
// ( ) * + , - . /
+ "\u0042\u0042\u0042\u0042\u0042\u003E\u0042\u0042"
// 0 1 2 3 4 5 6 7
+ "\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B"
// 8 9 : ; < = > ?
+ "\u003C\u003D\u0042\u0042\u0042\u0040\u0042\u0042"
// @ A B C D E F G
+ "\u0042\u0000\u0001\u0002\u0003\u0004\u0005\u0006"
// H I J K L M N O
+ "\u0007\u0008\t\n\u000B\u000C\r\u000E"
// P Q R S T U V W
+ "\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016"
// X Y Z [ \ ] ^ _
+ "\u0017\u0018\u0019\u0042\u0042\u0042\u0042\u003F"
// ' a b c d e f g
+ "\u0042\u001A\u001B\u001C\u001D\u001E\u001F\u0020"
// h i j k l m n o p
+ "\u0021\"\u0023\u0024\u0025\u0026\u0027\u0028"
// p q r s t u v w
+ "\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030"
// x y z
+ "\u0031\u0032\u0033").getBytes();
}
147 changes: 147 additions & 0 deletions src/test/groovy/Base64Test.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
*/
package groovy

import java.nio.charset.StandardCharsets

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

// Test bytes that have both the 62nd and 63rd base64 alphabet in the encoded string
static final byte[] testBytesChar62And63 = new BigInteger('4bf7ce5201fe239ab42ebead5acd8fa3', 16).toByteArray()

void testCodec() {
// turn the bytes back into a string for later comparison
def savedString = new String(testBytes, "ISO-8859-1")
Expand Down Expand Up @@ -62,4 +67,146 @@ class Base64Test extends GroovyTestCase {
def encodedBytes = testBytes.encodeBase64().toString()
assert encodedBytes == 'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31hc2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4/A//wDw=='
}

void testRfc4648Section10Encoding() {
assert b64('') == ''
assert b64('f') == 'Zg=='
assert b64('fo') == 'Zm8='
assert b64('foo') == 'Zm9v'
assert b64('foob') == 'Zm9vYg=='
assert b64('fooba') == 'Zm9vYmE='
assert b64('foobar') == 'Zm9vYmFy'
}

void testRfc4648Section10Decoding() {
assert decodeB64('') == ''

assert decodeB64('Zg') == 'f'
assert decodeB64('Zg==') == 'f'

assert decodeB64('Zm8') == 'fo'
assert decodeB64('Zm8=') == 'fo'

assert decodeB64('Zm9v') == 'foo'

assert decodeB64('Zm9vYg') == 'foob'
assert decodeB64('Zm9vYg==') == 'foob'

assert decodeB64('Zm9vYmE') == 'fooba'
assert decodeB64('Zm9vYmE=') == 'fooba'

assert decodeB64('Zm9vYmFy') == 'foobar'
}

void testRfc4648Section10EncodingUrlSafe() {
assert b64url('') == ''
assert b64url('f') == 'Zg'
assert b64url('fo') == 'Zm8'
assert b64url('foo') == 'Zm9v'
assert b64url('foob') == 'Zm9vYg'
assert b64url('fooba') == 'Zm9vYmE'
assert b64url('foobar') == 'Zm9vYmFy'
}

void testRfc4648Section10EncodingUrlSafeWithPadding() {
assert b64url('', true) == ''
assert b64url('f', true) == 'Zg=='
assert b64url('fo', true) == 'Zm8='
assert b64url('foo', true) == 'Zm9v'
assert b64url('foob', true) == 'Zm9vYg=='
assert b64url('fooba', true) == 'Zm9vYmE='
assert b64url('foobar', true) == 'Zm9vYmFy'
}

void testRfc4648Section10DecodingUrlSafe() {
assert decodeB64url('') == ''

assert decodeB64url('Zg') == 'f'
assert decodeB64url('Zg==') == 'f'

assert decodeB64url('Zm8') == 'fo'
assert decodeB64url('Zm8=') == 'fo'

assert decodeB64url('Zm9v') == 'foo'

assert decodeB64url('Zm9vYg') == 'foob'
assert decodeB64url('Zm9vYg==') == 'foob'

assert decodeB64url('Zm9vYmE') == 'fooba'
assert decodeB64url('Zm9vYmE=') == 'fooba'

assert decodeB64url('Zm9vYmFy') == 'foobar'
}

void testEncodingWithChar62And63() {
assert testBytesChar62And63.encodeBase64().toString() == 'S/fOUgH+I5q0Lr6tWs2Pow=='
}

void testUrlSafeEncodingWithChar62And63() {
assert testBytesChar62And63.encodeBase64Url().toString() == 'S_fOUgH-I5q0Lr6tWs2Pow'
assert testBytesChar62And63.encodeBase64Url(true).toString() == 'S_fOUgH-I5q0Lr6tWs2Pow=='
}

void testDecodingWithChar62And63() {
assert 'S/fOUgH+I5q0Lr6tWs2Pow=='.decodeBase64() == testBytesChar62And63
assert 'S/fOUgH+I5q0Lr6tWs2Pow'.decodeBase64() == testBytesChar62And63
}

void testUrlSafeDecodingWithChar62And63() {
assert 'S_fOUgH-I5q0Lr6tWs2Pow=='.decodeBase64Url() == testBytesChar62And63
assert 'S_fOUgH-I5q0Lr6tWs2Pow'.decodeBase64Url() == testBytesChar62And63
}

void testUrlSafeEncodingByDefaultOmitsPadding() {
assert testBytes.encodeBase64Url().toString() ==
'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31h' +
'c2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4_A__wDw'
}

void testUrlSafeEncodingWithPadding() {
assert testBytes.encodeBase64Url(true).toString() ==
'pzEyMzQ1Njc4OTAtPbEhQKMkJV4mKigpXytxd2VydHl1aW9wW11RV0VSVFlVSU9Qe31h' +
'c2RmZ2hqa2w7J1xBU0RGR0hKS0w6InxgenhjdmJubSwuL35aWENWQk5NPD4_A__wDw=='
}

void testDecodingNonBase64Alphabet() {
shouldFail {
decodeB64('S_fOUgH-I5q0Lr6tWs2Pow==')
}
}

void testUrlSafeDecodingNonUrlSafeAlphabet() {
shouldFail {
decodeB64url('S/fOUgH+I5q0Lr6tWs2Pow==')
}
}

void testDecodingWithInnerPad() {
shouldFail {
decodeB64('Zm9v=YmE=')
}
}

void testUrlSafeDecodingWithInnerPad() {
shouldFail {
decodeB64url('Zm9v=YmE=')
}
}

// Test helper methods
private static String b64(String s) {
s.getBytes(StandardCharsets.UTF_8).encodeBase64().toString()
}

private static String b64url(String s, boolean pad=false) {
s.getBytes(StandardCharsets.UTF_8).encodeBase64Url(pad).toString()
}

private static String decodeB64(String s) {
new String(s.decodeBase64(), StandardCharsets.UTF_8)
}

private static String decodeB64url(String s) {
new String(s.decodeBase64Url(), StandardCharsets.UTF_8)
}
}

0 comments on commit baa86a3

Please sign in to comment.