Skip to content

Commit

Permalink
v0.0.6
Browse files Browse the repository at this point in the history
Base58
Base85
Base122
Functional utils
Object utils extension
Random utils extension
Date utils extension
  • Loading branch information
HijackerMax committed Apr 25, 2023
1 parent c5fd890 commit d48ded8
Show file tree
Hide file tree
Showing 22 changed files with 934 additions and 34 deletions.
32 changes: 30 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Utils [![GitHub CI Status](https://github.com/hijackermax/utils/workflows/CI/badge.svg)](https://github.com/hijackermax/utils/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.hijackermax/utils)](https://search.maven.org/search?q=g:com.hijackermax%20AND%20a:utils)
# Utils [![GitHub CI Status](https://github.com/hijackermax/utils/workflows/CI/badge.svg)](https://github.com/hijackermax/utils/actions) [![Maven Central](https://img.shields.io/maven-central/v/com.hijackermax/utils)](https://search.maven.org/search?q=g:com.hijackermax%20AND%20a:utils) ![](https://img.shields.io/github/license/HijackerMax/utils)
A set of utils that can help in app development

### Prerequisites
Expand Down Expand Up @@ -45,16 +45,44 @@ Licensed under the Apache 2.0 License
* QuadPredicate

#### Wrappers

* Single
* Tuple
* Triple

#### IO

##### TemporaryFile

* Closeable wrapper for temporary files that can be used with try-with-resources

#### Encoders / decoders

##### Base58

* Binary-to-text encoding. Character set includes all uppercase and lowercase letters except for "0", "O", "I", and "l"
to improve human readability.

##### Base85

* Binary-to-text encoding. Resulting data is ~25% bigger than source, while base64 is typically ~33%. Description can be
found [here](https://en.wikipedia.org/wiki/Ascii85)

##### Base122

* Binary-to-text encoding inspired by [idea](https://blog.kevinalbs.com/base122)
and [JS library](https://github.com/kevinAlbs/Base122) of Kevin Albertson. Resulting data is ~13% bigger than source,
while base64 is typically ~33%

### How to use it

Just add this to your **pom.xml**

```xml

<dependency>
<groupId>com.hijackermax</groupId>
<artifactId>utils</artifactId>
<version>0.0.5</version>
<version>0.0.6</version>
</dependency>
```
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.hijackermax</groupId>
<artifactId>utils</artifactId>
<version>0.0.5</version>
<version>0.0.6</version>

<name>utils</name>
<description>A set of utils that can help in app development</description>
Expand Down
117 changes: 117 additions & 0 deletions src/main/java/com/hijackermax/utils/encoders/Base122.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.hijackermax.utils.encoders;

import com.hijackermax.utils.entities.Tuple;
import com.hijackermax.utils.lang.StringUtils;

import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Objects;
import java.util.function.Consumer;

/**
* Binary-to-text encoding that is more space-efficient than base64
*
* @since 0.0.6
*/
public final class Base122 {
private static final byte[] INVALID_CHARS = {0x00, 0x0A, 0x0D, 0x22, 0x26, 0x5C};
private static final byte K_SHORTENED = 0x07;
private static final byte STOP_BYTE = (byte) 0x80;

private Base122() {
}

/**
* Encodes provided byte array to Base122 UTF-8 {@link String}
*
* @param source byte array
* @return encoded UTF-8 {@link String}
* @since 0.0.6
*/
public static String encode(byte[] source) {
int dataLength = Objects.requireNonNull(source).length;
ByteArrayOutputStream result = new ByteArrayOutputStream(calculateEncodeBufferSize(dataLength));
Tuple<Integer, Integer> dataPointers = new Tuple<>(0, 0);
for (byte partition = nextPartition(source, dataLength, dataPointers); STOP_BYTE != partition; partition = nextPartition(source, dataLength, dataPointers)) {
int illegalIndex = Arrays.binarySearch(INVALID_CHARS, partition);
if (0 > illegalIndex) {
result.write(partition);
continue;
}
byte nextSevenBits = nextPartition(source, dataLength, dataPointers);
byte firstByte = (byte) (0xC2 | (0x07 & (STOP_BYTE == nextSevenBits ? K_SHORTENED : illegalIndex)) << 2);
if (STOP_BYTE == nextSevenBits) {
nextSevenBits = partition;
}
result.write(firstByte | ((nextSevenBits & 0x40) > 0 ? 1 : 0));
result.write(0x80 | (nextSevenBits & 0x3F));
}
return result.toString(StandardCharsets.UTF_8);
}

/**
* Decodes provided UTF-8 {@link String} to byte array
*
* @param source UTF-8 {@link String}
* @return decoded byte array
* @since 0.0.6
*/
public static byte[] decode(String source) {
if (StringUtils.isBlank(source)) {
return new byte[0];
}
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(calculateDecodedBufferSize(source.length()));
Tuple<Byte, Byte> dataPointers = new Tuple<>((byte) 0, (byte) 0);
for (char nextChar : source.toCharArray()) {
if (nextChar > 0x7F) {
int illegalIndex = (nextChar >>> 8) & 0x07;
if (illegalIndex != K_SHORTENED) {
pushPartition(INVALID_CHARS[illegalIndex] << 1, dataPointers, outputStream::write);
}
pushPartition((nextChar & 0x7F) << 1, dataPointers, outputStream::write);
} else {
pushPartition(nextChar << 1, dataPointers, outputStream::write);
}
}
return outputStream.toByteArray();
}

private static byte nextPartition(byte[] data, int length, Tuple<Integer, Integer> dataPointers) {
if (dataPointers.getKey() >= length) {
return STOP_BYTE;
}
int firstPart = (((0xFE >>> dataPointers.getValue()) & (data[dataPointers.getKey()] & 0xFF))
<< dataPointers.getValue()) >>> 1;
dataPointers.modifyValue(v -> v + 7);
if (dataPointers.getValue() < 8) {
return (byte) firstPart;
}
dataPointers.modifyValue(v -> v - 8);
dataPointers.modifyKey(k -> k + 1);
if (dataPointers.getKey() >= length) {
return (byte) firstPart;
}
int secondPart = (((0xFF00 >>> dataPointers.getValue()) & (data[dataPointers.getKey()] & 0xFF)) & 0xFF)
>>> 8 - dataPointers.getValue();
return (byte) (firstPart | secondPart);
}

private static void pushPartition(int nextValue, Tuple<Byte, Byte> dataPointers, Consumer<Byte> valueConsumer) {
dataPointers.modifyKey(k -> (byte) (k | (nextValue >>> dataPointers.getValue())));
dataPointers.modifyValue(v -> (byte) (v + 7));
if (dataPointers.getValue() >= 8) {
valueConsumer.accept(dataPointers.getKey());
dataPointers.modifyValue(v -> (byte) (v - 8));
dataPointers.setKey((byte) (nextValue << (7 - dataPointers.getValue()) & 0xFF));
}
}

private static int calculateEncodeBufferSize(int byteArrayLength) {
return byteArrayLength + (byteArrayLength / 7);
}

private static int calculateDecodedBufferSize(int stringLength) {
return (stringLength * 7) >> 3;
}
}
101 changes: 101 additions & 0 deletions src/main/java/com/hijackermax/utils/encoders/Base58.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.hijackermax.utils.encoders;

import com.hijackermax.utils.lang.StringUtils;

import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Objects;

/**
* Binary-to-text encoding
*
* @since 0.0.6
*/
public final class Base58 {
private static final char[] CHARSET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz".toCharArray();
private static final BigInteger FIFTY_EIGHT = BigInteger.valueOf(58L);

private Base58() {
}

/**
* Encodes provided byte array to Base58 ASCII {@link String}
*
* @param source byte array
* @return encoded ASCII {@link String}
* @since 0.0.6
*/
public static String encode(byte[] source) {
int dataLength = Objects.requireNonNull(source).length;
if (0 == dataLength) {
return StringUtils.EMPTY;
}
ByteArrayOutputStream bufferStream = new ByteArrayOutputStream(calculateEncodeBufferSize(dataLength));
BigInteger reminder = new BigInteger(1, source);
while (reminder.compareTo(BigInteger.ZERO) > 0) {
int idx = reminder.mod(FIFTY_EIGHT).intValue();
reminder = reminder.divide(FIFTY_EIGHT);
bufferStream.write(CHARSET[idx]);
}
byte[] buffer = bufferStream.toByteArray();
StringBuilder result = new StringBuilder();
boolean isLeadingZero = true;
for (int idx = buffer.length - 1; idx >= 0; idx--) {
byte nextByte = buffer[idx];
if (isLeadingZero) {
boolean isByteZero = 0 == nextByte;
if (isByteZero) {
result.insert(0, (char) 0x49);
}
isLeadingZero = isByteZero;
}
result.append((char) nextByte);
}
return result.toString();
}


/**
* Decodes provided ASCII {@link String} to byte array
*
* @param source ASCII {@link String}
* @return decoded byte array
* @throws IllegalArgumentException if provided string is not base85 encoded
* @since 0.0.6
*/
public static byte[] decode(String source) {
if (StringUtils.isBlank(source)) {
return new byte[0];
}
ByteArrayOutputStream result = new ByteArrayOutputStream(calculateDecodedBufferSize(source.length()));
BigInteger buffer = BigInteger.ZERO;
boolean isLeadingOne = true;
for (char nextChar : source.toCharArray()) {
int idx = Arrays.binarySearch(CHARSET, nextChar);
if (idx < 0) {
throw new IllegalArgumentException("Provided source string is not Base58 encoded");
}
if (isLeadingOne) {
boolean isCharOne = 0x49 == nextChar;
if (isCharOne) {
result.write(0x00);
}
isLeadingOne = isCharOne;
}
buffer = buffer.multiply(FIFTY_EIGHT).add(BigInteger.valueOf(idx));
}
byte[] bufferBytes = buffer.toByteArray();
int offset = 0x00 == bufferBytes[0] ? 1 : 0;
result.write(bufferBytes, offset, bufferBytes.length - offset);
return result.toByteArray();
}

private static int calculateEncodeBufferSize(int byteArrayLength) {
return byteArrayLength * 7 / 5;
}

private static int calculateDecodedBufferSize(int stringLength) {
return stringLength * 5 / 7;
}
}
Loading

0 comments on commit d48ded8

Please sign in to comment.