Skip to content

Commit 344cc4a

Browse files
committed
Add support for base64 url safe encoding
fix #15
1 parent 5109db1 commit 344cc4a

6 files changed

Lines changed: 111 additions & 48 deletions

File tree

CHANGELOG

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* add dedicated `md5` and `sha1` transformer methods
77
* add `from(Inputstream stream, int maxlength)` limiting stream reading constructor #13
88
* add indexOf() with `fromIndex` parameter #14
9+
* add support for base64 url safe encoding #15
910

1011
## v0.6.0
1112

src/main/java/at/favre/lib/bytes/Base64.java

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,21 @@ final class Base64 {
3333
private Base64() {
3434
}
3535

36-
public static byte[] decode(String in) {
36+
private static final byte[] MAP = new byte[]{
37+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
38+
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
39+
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
40+
'5', '6', '7', '8', '9', '+', '/'
41+
};
42+
43+
private static final byte[] URL_MAP = new byte[]{
44+
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
45+
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
46+
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
47+
'5', '6', '7', '8', '9', '-', '_'
48+
};
49+
50+
static byte[] decode(String in) {
3751
// Ignore trailing '=' padding and whitespace from the input.
3852
int limit = in.length();
3953
for (; limit > 0; limit--) {
@@ -114,19 +128,16 @@ public static byte[] decode(String in) {
114128
return prefix;
115129
}
116130

117-
private static final byte[] MAP = new byte[]{
118-
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
119-
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l',
120-
'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
121-
'5', '6', '7', '8', '9', '+', '/'
122-
};
131+
static String encode(byte[] in) {
132+
return encode(in, false, true);
133+
}
123134

124-
public static String encode(byte[] in) {
125-
return encode(in, MAP);
135+
static String encode(byte[] in, boolean urlSafe, boolean usePadding) {
136+
return encode(in, urlSafe ? URL_MAP : MAP, usePadding);
126137
}
127138

128-
private static String encode(byte[] in, byte[] map) {
129-
int length = (in.length + 2) / 3 * 4;
139+
private static String encode(byte[] in, byte[] map, boolean usePadding) {
140+
int length = outLength(in.length, usePadding);
130141
byte[] out = new byte[length];
131142
int index = 0, end = in.length - in.length % 3;
132143
for (int i = 0; i < end; i += 3) {
@@ -139,16 +150,32 @@ private static String encode(byte[] in, byte[] map) {
139150
case 1:
140151
out[index++] = map[(in[end] & 0xff) >> 2];
141152
out[index++] = map[(in[end] & 0x03) << 4];
142-
out[index++] = '=';
143-
out[index++] = '=';
153+
if (usePadding) {
154+
out[index++] = '=';
155+
out[index] = '=';
156+
}
144157
break;
145158
case 2:
146159
out[index++] = map[(in[end] & 0xff) >> 2];
147160
out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)];
148161
out[index++] = map[((in[end + 1] & 0x0f) << 2)];
149-
out[index++] = '=';
162+
163+
if (usePadding) {
164+
out[index] = '=';
165+
}
150166
break;
151167
}
152168
return new String(out, StandardCharsets.US_ASCII);
153169
}
170+
171+
private static int outLength(int srclen, boolean doPadding) {
172+
int len;
173+
if (doPadding) {
174+
len = 4 * ((srclen + 2) / 3);
175+
} else {
176+
int n = srclen % 3;
177+
len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1);
178+
}
179+
return len;
180+
}
154181
}

src/main/java/at/favre/lib/bytes/BinaryToTextEncoding.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,9 +132,21 @@ public byte[] decode(String hexString) {
132132
* Simple Base64 encoder
133133
*/
134134
class Base64Encoding implements EncoderDecoder {
135+
private final boolean urlSafe;
136+
private final boolean padding;
137+
138+
Base64Encoding() {
139+
this(false, true);
140+
}
141+
142+
Base64Encoding(boolean urlSafe, boolean padding) {
143+
this.urlSafe = urlSafe;
144+
this.padding = padding;
145+
}
146+
135147
@Override
136148
public String encode(byte[] array, ByteOrder byteOrder) {
137-
return Base64.encode((byteOrder == ByteOrder.BIG_ENDIAN) ? array : Bytes.from(array).reverse().array());
149+
return Base64.encode((byteOrder == ByteOrder.BIG_ENDIAN) ? array : Bytes.from(array).reverse().array(), urlSafe, padding);
138150
}
139151

140152
@Override

src/main/java/at/favre/lib/bytes/Bytes.java

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121

2222
package at.favre.lib.bytes;
2323

24-
import java.io.ByteArrayInputStream;
25-
import java.io.DataInput;
26-
import java.io.File;
27-
import java.io.InputStream;
28-
import java.io.Serializable;
24+
import java.io.*;
2925
import java.math.BigInteger;
3026
import java.nio.ByteBuffer;
3127
import java.nio.ByteOrder;
@@ -35,14 +31,7 @@
3531
import java.nio.charset.StandardCharsets;
3632
import java.security.SecureRandom;
3733
import java.text.Normalizer;
38-
import java.util.Arrays;
39-
import java.util.BitSet;
40-
import java.util.Collection;
41-
import java.util.Iterator;
42-
import java.util.List;
43-
import java.util.Objects;
44-
import java.util.Random;
45-
import java.util.UUID;
34+
import java.util.*;
4635

4736
/**
4837
* Bytes is wrapper class for an byte-array that allows a lot of convenience operations on it:
@@ -567,7 +556,7 @@ public static Bytes parseBase36(String base36String) {
567556
}
568557

569558
/**
570-
* Parsing of base64 encoded byte arrays.
559+
* Parsing of base64 encoded byte arrays. Supporting RFC 3548 normal and url safe encoding.
571560
*
572561
* @param base64String the encoded string
573562
* @return decoded instance
@@ -1482,7 +1471,20 @@ public String encodeBase36() {
14821471
* @see <a href="https://en.wikipedia.org/wiki/Base64">Base64</a>
14831472
*/
14841473
public String encodeBase64() {
1485-
return encode(new BinaryToTextEncoding.Base64Encoding());
1474+
return encode(new BinaryToTextEncoding.Base64Encoding(false, true));
1475+
}
1476+
1477+
/**
1478+
* Base64 representation with padding. This is the url safe variation subsitution '+' and '/' with '-' and '_'
1479+
* respectively. This encoding has a space efficiency of 75%.
1480+
* <p>
1481+
* Example: <code>SpT9_x6v7Q==</code>
1482+
*
1483+
* @return base64 url safe string
1484+
* @see <a href="https://en.wikipedia.org/wiki/Base64">Base64</a>
1485+
*/
1486+
public String encodeBase64Url() {
1487+
return encode(new BinaryToTextEncoding.Base64Encoding(true, true));
14861488
}
14871489

14881490
/**

src/test/java/at/favre/lib/bytes/BinaryToTextEncodingTest.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,23 @@ public void encodeDecodeRadixZeros() {
8181

8282
@Test
8383
public void encodeDecodeBase64() {
84-
for (int i = 4; i < 32; i += 4) {
84+
BinaryToTextEncoding.EncoderDecoder encoderPad = new BinaryToTextEncoding.Base64Encoding(false, true);
85+
BinaryToTextEncoding.EncoderDecoder encoderUrlPad = new BinaryToTextEncoding.Base64Encoding(true, true);
86+
BinaryToTextEncoding.EncoderDecoder encoderNoPad = new BinaryToTextEncoding.Base64Encoding(false, false);
87+
88+
for (int i = 0; i < 32; i += 4) {
8589
Bytes rnd = Bytes.random(i);
86-
BinaryToTextEncoding.EncoderDecoder encoding = new BinaryToTextEncoding.Base64Encoding();
87-
String encodedBigEndian = encoding.encode(rnd.array(), ByteOrder.BIG_ENDIAN);
88-
byte[] decoded = encoding.decode(encodedBigEndian);
90+
String encodedBigEndian = encoderPad.encode(rnd.array(), ByteOrder.BIG_ENDIAN);
91+
byte[] decoded = encoderPad.decode(encodedBigEndian);
8992
assertEquals(rnd, Bytes.wrap(decoded));
93+
94+
String encodedBigEndianUrlPad = encoderUrlPad.encode(rnd.array(), ByteOrder.BIG_ENDIAN);
95+
byte[] decodedUrlPad = encoderPad.decode(encodedBigEndianUrlPad);
96+
assertEquals(rnd, Bytes.wrap(decodedUrlPad));
97+
98+
String encodedBigEndianNoPad = encoderNoPad.encode(rnd.array(), ByteOrder.BIG_ENDIAN);
99+
byte[] decodedNoPad = encoderPad.decode(encodedBigEndianNoPad);
100+
assertEquals(rnd, Bytes.wrap(decodedNoPad));
90101
}
91102
}
92103

src/test/java/at/favre/lib/bytes/BytesParseAndEncodingTest.java

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,12 @@ public class BytesParseAndEncodingTest extends ABytesTest {
3131
private byte[] encodingExample;
3232

3333
@Before
34-
public void setUp() throws Exception {
34+
public void setUp() {
3535
encodingExample = new byte[]{0x4A, (byte) 0x94, (byte) 0xFD, (byte) 0xFF, 0x1E, (byte) 0xAF, (byte) 0xED};
3636
}
3737

3838
@Test
39-
public void parseHex() throws Exception {
39+
public void parseHex() {
4040
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1};
4141
assertArrayEquals(defaultArray, Bytes.parseHex("0xA0E1").array());
4242
assertArrayEquals(defaultArray, Bytes.parseHex("A0E1").array());
@@ -45,12 +45,12 @@ public void parseHex() throws Exception {
4545
}
4646

4747
@Test(expected = IllegalArgumentException.class)
48-
public void parseHexInvalid() throws Exception {
48+
public void parseHexInvalid() {
4949
Bytes.parseHex("A0E");
5050
}
5151

5252
@Test
53-
public void encodeHex() throws Exception {
53+
public void encodeHex() {
5454
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1};
5555
assertEquals("a0e1", Bytes.from(defaultArray).encodeHex());
5656
assertEquals("A0E1", Bytes.from(defaultArray).encodeHex(true));
@@ -59,58 +59,68 @@ public void encodeHex() throws Exception {
5959
}
6060

6161
@Test
62-
public void parseBase64() throws Exception {
62+
public void parseBase64() {
6363
assertArrayEquals(encodingExample, Bytes.parseBase64("SpT9/x6v7Q==").array());
64+
assertArrayEquals(encodingExample, Bytes.parseBase64("SpT9_x6v7Q==").array());
6465
}
6566

6667
@Test(expected = IllegalArgumentException.class)
67-
public void parseBase64Invalid() throws Exception {
68+
public void parseBase64Invalid() {
6869
Bytes.parseBase64("☕");
6970
}
7071

7172
@Test
72-
public void encodeBase64() throws Exception {
73+
public void encodeBase64() {
74+
assertEquals("", Bytes.from(new byte[0]).encodeBase64());
75+
assertEquals("AA==", Bytes.from(new byte[1]).encodeBase64());
7376
assertEquals("SpT9/x6v7Q==", Bytes.from(encodingExample).encodeBase64());
7477
}
7578

7679
@Test
77-
public void encodeBinary() throws Exception {
80+
public void encodeBase64Url() {
81+
assertEquals("", Bytes.from(new byte[0]).encodeBase64Url());
82+
assertEquals("AA==", Bytes.from(new byte[1]).encodeBase64Url());
83+
assertEquals("SpT9_x6v7Q==", Bytes.from(encodingExample).encodeBase64Url());
84+
}
85+
86+
@Test
87+
public void encodeBinary() {
7888
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1};
7989
assertEquals("1010000011100001", Bytes.from(defaultArray).encodeBinary());
8090
assertEquals("1001010100101001111110111111111000111101010111111101101", Bytes.from(encodingExample).encodeBinary());
8191
}
8292

8393
@Test
84-
public void parseOctal() throws Exception {
94+
public void parseOctal() {
8595
assertArrayEquals(encodingExample, Bytes.parseOctal("1124517677707527755").array());
8696
}
8797

8898
@Test
89-
public void encodeOctal() throws Exception {
99+
public void encodeOctal() {
90100
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1};
91101
assertEquals("120341", Bytes.from(defaultArray).encodeOctal());
92102
assertEquals("1124517677707527755", Bytes.from(encodingExample).encodeOctal());
93103
}
94104

95105
@Test
96-
public void parseDec() throws Exception {
106+
public void parseDec() {
97107
assertArrayEquals(encodingExample, Bytes.parseDec("20992966904426477").array());
98108
}
99109

100110
@Test
101-
public void encodeDec() throws Exception {
111+
public void encodeDec() {
102112
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1};
103113
assertEquals("41185", Bytes.from(defaultArray).encodeDec());
104114
assertEquals("20992966904426477", Bytes.from(encodingExample).encodeDec());
105115
}
106116

107117
@Test
108-
public void parseBase36() throws Exception {
118+
public void parseBase36() {
109119
assertArrayEquals(encodingExample, Bytes.parseBase36("5qpdvuwjvu5").array());
110120
}
111121

112122
@Test
113-
public void encodeBase36() throws Exception {
123+
public void encodeBase36() {
114124
byte[] defaultArray = new byte[]{(byte) 0xA0, (byte) 0xE1, (byte) 0x13};
115125
assertEquals("69zbn", Bytes.from(defaultArray).encodeBase36());
116126
assertEquals("5qpdvuwjvu5", Bytes.from(encodingExample).encodeBase36());

0 commit comments

Comments
 (0)