package org.javersion.util;
import static com.google.common.base.Charsets.UTF_8;
import static java.lang.Long.parseUnsignedLong;
import static java.lang.String.format;
import static java.lang.System.nanoTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.javersion.util.BinaryEncoder.BASE32;
import static org.javersion.util.BinaryEncoder.BASE32_CROCKFORD_NUMBER;
import static org.javersion.util.BinaryEncoder.BASE64;
import static org.javersion.util.BinaryEncoder.Builder;
import static org.javersion.util.BinaryEncoder.NUMBER_BASE64_URL;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.util.Base64;
import org.junit.Ignore;
import org.junit.Test;
public class BinaryEncoderTest {
private static final String TEXT =
"Base32 is a notation for encoding arbitrary byte data using a restricted set of symbols which can be conveniently " +
"used by humans and processed by old computer systems which only recognize restricted character sets.";
private static final byte[] TEXT_BYTES = TEXT.getBytes(UTF_8);
private static final Builder BUILDER = new Builder("01234567");
private static final BinaryEncoder NUMBER8 = BUILDER.buildUnsignedNumberEncoder();
private static final BinaryEncoder SIGNED_NUMBER8 = BUILDER.buildSignedNumberEncoder();
private static final BinaryEncoder BASE8 = BUILDER.buildBaseEncoder();
private static final Base64.Encoder JAVA_BASE64_ENCODER = Base64.getEncoder().withoutPadding();
private static final Base64.Decoder JAVA_BASE64_DECODER = Base64.getDecoder();
@Test
public void short_one() {
assertThat(NUMBER8.encode(twoBytes("0-000-000-0 00-000-001"))).isEqualTo("000001");
assertThat( BASE8.encode(twoBytes("000-000-00 0-000-000-1"))).isEqualTo("000004");
byte[] bytes = NUMBER8.decode("000001");
assertThat(bytes).isEqualTo(twoBytes("00000000 00000001"));
assertThat(BASE8.decode("000004")).isEqualTo(bytes);
}
@Test
public void all_ones_byte() {
assertThat(NUMBER8.encode(oneByte("11-111-111"))).isEqualTo("377");
assertThat( BASE8.encode(oneByte("111-111-11"))).isEqualTo("776");
byte[] bytes = NUMBER8.decode("377");
assertThat(bytes).isEqualTo(oneByte("11111111"));
assertThat(BASE8.decode("776")).isEqualTo(bytes);
}
@Test
public void two_byte_alternate_ones() {
assertThat(NUMBER8.encode(twoBytes("1-010-101-0 10-101-010"))).isEqualTo("125252");
assertThat( BASE8.encode(twoBytes("101-010-10 1-010-101-0"))).isEqualTo("525250");
byte[] bytes = NUMBER8.decode("125252");
assertThat(bytes).isEqualTo(twoBytes("10101010 10101010"));
assertThat(BASE8.decode("525250")).isEqualTo(bytes);
}
@Test
public void two_byte_alternate_zeros() {
assertThat(NUMBER8.encode(twoBytes("0-101-010-1 01-010-101"))).isEqualTo("052525");
assertThat( BASE8.encode(twoBytes("010-101-01 0-101-010-1"))).isEqualTo("252524");
byte[] bytes = NUMBER8.decode("052525");
assertThat(bytes).isEqualTo(twoBytes("01010101 01010101"));
assertThat(BASE8.decode("252524")).isEqualTo(bytes);
}
@Test
public void one_zeros_one() {
assertThat(NUMBER8.encode(fourBytes("10-000-000 000-000-00 0-000-000-0 00-000-001")))
.isEqualTo("20000000001");
assertThat( BASE8.encode(fourBytes("100-000-00 0-000-000-0 00-000-000 000-000-01")))
.isEqualTo("40000000002");
byte[] bytes = NUMBER8.decode("20000000001");
assertThat(bytes).isEqualTo(fourBytes("10000000 00000000 00000000 00000001"));
assertThat(BASE8.decode("40000000002")).isEqualTo(bytes);
}
@Test
public void long_sweep() {
String prev = null;
String current = null;
long step = Long.parseLong("10100110", 2);
for (int i=0; i < 64-8; i++) {
prev = current;
long val = step << i;
current = NUMBER8.encodeLong(val);
assertOrder(prev, current, i);
assertThat(NUMBER8.decodeLong(current)).isEqualTo(val);
assertThat(BASE8.decodeLong(BASE8.encodeLong(val))).isEqualTo(val);
}
assertOrder(prev, current, 64 - 8);
}
@Test
public void int_sweep() {
String prev = null;
String current = null;
int step = Integer.parseInt("10100110", 2);
for (int i=0; i < 32-8; i++) {
prev = current;
int val = step << i;
current = NUMBER8.encodeInt(val);
assertOrder(prev, current, i);
assertThat(NUMBER8.decodeInt(current)).isEqualTo(val);
assertThat(BASE8.decodeInt(BASE8.encodeInt(val))).isEqualTo(val);
}
assertOrder(prev, current, 64 - 8);
}
private void assertOrder(String prev, String current, int i) {
if (prev != null) {
if (prev.compareTo(current) >= 0) {
fail(format("Round %s: expected %s to be less than %s", i+1, current, prev));
}
}
}
@Test(expected = IllegalArgumentException.class)
public void detect_illegal_chars_below_range() {
BASE32.decode("A1B");
}
@Test(expected = IllegalArgumentException.class)
public void detect_illegal_chars_above_range() {
BASE32.decode("A{B");
}
@Test(expected = IllegalArgumentException.class)
public void detect_illegal_chars_within_range() {
BASE32.decode("A=B");
}
@Test
public void base32_no_padding() throws UnsupportedEncodingException {
final String base32 = BASE32.encode(TEXT_BYTES);
// Checker generated with http://online-calculators.appspot.com/base32/ (padding removed)
assertThat(base32).isEqualTo("IJQXGZJTGIQGS4ZAMEQG433UMF2GS33OEBTG64RAMVXGG33ENFXGOIDBOJRGS5DSMFZHSIDCPF2GKIDEMF2GCIDVONUW4ZZAMEQHEZLTORZGSY3UMVSCA43" +
"FOQQG6ZRAON4W2YTPNRZSA53INFRWQIDDMFXCAYTFEBRW63TWMVXGSZLOORWHSIDVONSWIIDCPEQGQ5LNMFXHGIDBNZSCA4DSN5RWK43TMVSCAYTZEBXWYZBAMNXW24DVORSXEIDTPFZ" +
"XIZLNOMQHO2DJMNUCA33ONR4SA4TFMNXWO3TJPJSSA4TFON2HE2LDORSWIIDDNBQXEYLDORSXEIDTMV2HGLQ");
final byte[] bytes = BASE32.decode(base32.toLowerCase());
assertThat(new String(bytes, UTF_8)).isEqualTo(TEXT);
}
@Test
public void two_ints_combined_to_long() {
String res = BASE32.encode(new Bytes.Long(-1));
assertThat(BASE32.encode(new Bytes.Long(-1, -1))).isEqualTo(res);
}
@Test
public void aliases() {
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("00000oO")).isEqualTo(0);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("0000001")).isEqualTo(1);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("000000l")).isEqualTo(1);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("00000oL")).isEqualTo(1);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("00000oi")).isEqualTo(1);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("00000oI")).isEqualTo(1);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("000000A")).isEqualTo(10);
assertThat(BASE32_CROCKFORD_NUMBER.decodeInt("000000a")).isEqualTo(10);
}
@Test(expected = IllegalArgumentException.class)
public void alias_for_unknown_char() {
new Builder("01").withAliasesFor('2', "b");
}
@Test
@Ignore
public void performance() {
// NOTE: It seems that the order in which these tests are run affects more than actual optimizations... GC?
final int rounds = 100000;
long start, time;
runBase64(rounds);
start = nanoTime();
runBase64(rounds);
time = nanoTime() - start;
System.out.println("Encode/decode bytes, nanos per round: " + (time/rounds));
// ~6050
// encode through Bytes -> ~5650
// decode through Bytes -> ~6000
// while-to-for-loop optimization -> ~5200
runIntBase64(rounds);
start = nanoTime();
runIntBase64(rounds);
time = nanoTime() - start;
System.out.println("Encode/decode int, nanos per round: " + (time/rounds));
// ~265
// encode through Bytes -> ~200
// decode through Bytes -> ~160
// while-to-for-loop optimization -> ~140
runLongBase64(rounds);
start = nanoTime();
runLongBase64(rounds);
time = nanoTime() - start;
System.out.println("Encode/decode long, nanos per round: " + (time/rounds));
// ~375
// encode through Bytes -> ~280
// decode through Bytes -> ~210
// while-to-for-loop optimization -> ~200
}
@Test
public void compare_base64_performance() {
final int rounds = 10000;
long start, time;
run_compare_base64_java(rounds);
start = nanoTime();
run_compare_base64_java(rounds);
time = nanoTime() - start;
System.out.println("Java encode/decode bytes, nanos per round: " + (time/rounds));
// ~1400
run_compare_base64_my(rounds);
start = nanoTime();
run_compare_base64_my(rounds);
time = nanoTime() - start;
System.out.println("My encode/decode bytes, nanos per round: " + (time/rounds));
// ~2800
}
private void run_compare_base64_my(int rounds) {
for (int i=0; i < rounds; i++) {
BASE64.decode(BASE64.encode(TEXT_BYTES));
}
}
private void run_compare_base64_java(int rounds) {
for (int i=0; i < rounds; i++) {
JAVA_BASE64_DECODER.decode(JAVA_BASE64_ENCODER.encodeToString(TEXT_BYTES));
}
}
private void runBase64(int rounds) {
for (int i=0; i < rounds; i++) {
BASE64.decode(BASE64.encode(TEXT_BYTES));
NUMBER_BASE64_URL.decode(NUMBER_BASE64_URL.encode(TEXT_BYTES));
}
}
private void runLongBase64(int rounds) {
for (int i=0; i < rounds; i++) {
long val = 123 * (456 + i);
BASE64.decodeLong(BASE64.encodeLong(val));
NUMBER_BASE64_URL.decodeLong(NUMBER_BASE64_URL.encodeLong(val));
}
}
private void runIntBase64(int rounds) {
for (int i=0; i < rounds; i++) {
int val = 123 * (456 + i);
BASE64.decodeInt(BASE64.encodeInt(val));
NUMBER_BASE64_URL.decodeInt(NUMBER_BASE64_URL.encodeInt(val));
}
}
@Test
public void base64_no_padding() {
final String base64 = BASE64.encode(TEXT_BYTES);
final String javaBase64 = Base64.getEncoder().withoutPadding().encodeToString(TEXT_BYTES);
assertThat(base64).isEqualTo(javaBase64);
}
@Test
public void base64_number() {
assertThat(NUMBER_BASE64_URL.encodeLong(-1)).isEqualTo("Ezzzzzzzzzz");
assertThat(NUMBER_BASE64_URL.decodeLong("Ezzzzzzzzzz")).isEqualTo(-1);
}
@Test
public void unsinged_comparison() {
assertOrder(NUMBER8, 0, 1);
assertOrder(NUMBER8, 1, Long.MAX_VALUE);
assertThat(Long.MAX_VALUE + 1).isEqualTo(Long.MIN_VALUE);
assertOrder(NUMBER8, Long.MAX_VALUE, Long.MAX_VALUE + 1);
assertOrder(NUMBER8, Long.MIN_VALUE, -1);
}
@Test
public void singed_comparison() {
assertOrder(SIGNED_NUMBER8, Long.MIN_VALUE, -1);
assertOrder(SIGNED_NUMBER8, -1, 0);
assertOrder(SIGNED_NUMBER8, 0, 1);
assertOrder(SIGNED_NUMBER8, 1, Long.MAX_VALUE);
}
private static int parseInt(String bits) {
bits = bits.replaceAll("[^\\-01]", "");
return Integer.parseUnsignedInt(bits, 2);
}
private void assertOrder(BinaryEncoder encoder, long first, long second) {
assertOrder(encoder.encodeLong(first), encoder.encodeLong(second));
}
private void assertOrder(String first, String second) {
assertThat(first.compareTo(second)).isLessThan(0);
}
private static char[] chars(char... chars) {
return chars;
}
private static byte[] oneByte(String bits) {
return new byte[] { (byte) parseLong(bits) };
}
private static byte[] twoBytes(String bits) {
return ByteBuffer.allocate(2).putShort((short) parseLong(bits)).array();
}
private static byte[] fourBytes(String bits) {
return ByteBuffer.allocate(4).putInt((int) parseLong(bits)).array();
}
private static long parseLong(String bits) {
return parseUnsignedLong(bits.replaceAll("\\D", ""), 2);
}
private static int[] ints(int... ints) {
return ints;
}
}