
dev.soffa.foundation.commons.Hashids Maven / Gradle / Ivy
package dev.soffa.foundation.commons;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressWarnings({"PMD.GodClass", "PMD.AvoidLiteralsInIfCondition"})
public class Hashids {
private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
private static final String DEFAULT_SEPS = "cfhistuCFHISTU";
private static final String DEFAULT_SALT = "";
private static final int DEFAULT_MIN_HASH_LENGTH = 0;
private static final int MIN_ALPHABET_LENGTH = 16;
private static final double SEP_DIV = 3.5;
private static final int GUARD_DIV = 12;
private final String salt;
private final int minHashLength;
private final String alphabet;
private final String seps;
private final String guards;
public Hashids() {
this(DEFAULT_SALT);
}
public Hashids(String salt) {
this(salt, 0);
}
public Hashids(String salt, int minHashLength) {
this(salt, minHashLength, DEFAULT_ALPHABET);
}
@SuppressWarnings("PMD.CyclomaticComplexity")
public Hashids(String salt, int minHashLength, final String chars) {
String alphabet = chars;
this.salt = salt != null ? salt : DEFAULT_SALT;
this.minHashLength = minHashLength > 0 ? minHashLength : DEFAULT_MIN_HASH_LENGTH;
final StringBuilder uniqueAlphabet = new StringBuilder();
for (int i = 0; i < alphabet.length(); i++) {
if (uniqueAlphabet.indexOf(String.valueOf(alphabet.charAt(i))) == -1) {
uniqueAlphabet.append(alphabet.charAt(i));
}
}
alphabet = uniqueAlphabet.toString();
if (alphabet.length() < MIN_ALPHABET_LENGTH) {
throw new IllegalArgumentException(
"alphabet must contain at least " + MIN_ALPHABET_LENGTH + " unique characters");
}
if (alphabet.contains(" ")) {
throw new IllegalArgumentException("alphabet cannot contains spaces");
}
// seps should contain only characters present in alphabet;
// alphabet should not contains seps
String seps = DEFAULT_SEPS;
for (int i = 0; i < seps.length(); i++) {
final int j = alphabet.indexOf(seps.charAt(i));
if (j == -1) {
seps = seps.substring(0, i) + " " + seps.substring(i + 1);
} else {
alphabet = alphabet.substring(0, j) + " " + alphabet.substring(j + 1);
}
}
alphabet = alphabet.replaceAll("\\s+", "");
seps = seps.replaceAll("\\s+", "");
seps = Hashids.consistentShuffle(seps, this.salt);
if (seps.isEmpty() || ((float) alphabet.length() / seps.length()) > SEP_DIV) {
int sepsLen = (int) Math.ceil(alphabet.length() / SEP_DIV);
if (sepsLen == 1) {
sepsLen++;
}
if (sepsLen > seps.length()) {
final int diff = sepsLen - seps.length();
seps += alphabet.substring(0, diff);
alphabet = alphabet.substring(diff);
} else {
seps = seps.substring(0, sepsLen);
}
}
alphabet = Hashids.consistentShuffle(alphabet, this.salt);
// use double to round up
final int guardCount = (int) Math.ceil((double) alphabet.length() / GUARD_DIV);
String guards;
if (alphabet.length() < 3) {
guards = seps.substring(0, guardCount);
seps = seps.substring(guardCount);
} else {
guards = alphabet.substring(0, guardCount);
alphabet = alphabet.substring(guardCount);
}
this.guards = guards;
this.alphabet = alphabet;
this.seps = seps;
}
public static int checkedCast(long value) {
final int result = (int) value;
if (result != value) {
// don't use checkArgument here, to avoid boxing
throw new IllegalArgumentException("Out of range: " + value);
}
return result;
}
@SuppressWarnings({"PMD.AvoidReassigningLoopVariables", "PMD.ForLoopVariableCount"})
private static String consistentShuffle(final String alphabet, final String salt) {
if (salt.length() <= 0) {
return alphabet;
}
int ascVal;
int j;
final char[] tmpArr = alphabet.toCharArray();
for (int i = tmpArr.length - 1, v = 0, p = 0; i > 0; i--, v++) {
v %= salt.length();
ascVal = salt.charAt(v);
p += ascVal;
j = (ascVal + v + p) % i;
final char tmp = tmpArr[j];
tmpArr[j] = tmpArr[i];
tmpArr[i] = tmp;
}
return new String(tmpArr);
}
private static String hash(final long value, final String alphabet) {
StringBuilder hash = new StringBuilder();
final int alphabetLen = alphabet.length();
long input = value;
do {
final int index = (int) (input % alphabetLen);
if (index >= 0 && index < alphabet.length()) {
hash.insert(0, alphabet.charAt(index));
}
input /= alphabetLen;
} while (input > 0);
return hash.toString();
}
private static Long unhash(String input, String alphabet) {
long number = 0;
long pos;
for (int i = 0; i < input.length(); i++) {
pos = alphabet.indexOf(input.charAt(i));
number = number * alphabet.length() + pos;
}
return number;
}
/**
* Encrypt numbers to string
*
* @param numbers the numbers to encrypt
* @return the encrypt string
*/
public String encode(long... numbers) {
if (numbers.length == 0) {
return "";
}
for (final long number : numbers) {
if (number < 0) {
return "";
}
}
return this.internalEncode(numbers);
}
/* Private methods */
/**
* Decrypt string to numbers
*
* @param hash the encrypt string
* @return decryped numbers
*/
public long[] decode(String hash) {
if (hash.isEmpty()) {
return new long[0];
}
String validChars = this.alphabet + this.guards + this.seps;
for (int i = 0; i < hash.length(); i++) {
if (validChars.indexOf(hash.charAt(i)) == -1) {
return new long[0];
}
}
return this.internalDecode(hash, this.alphabet);
}
/**
* Encrypt hexa to string
*
* @param hexa the hexa to encrypt
* @return the encrypt string
*/
public String encodeHex(String hexa) {
if (!hexa.matches("^[\\da-fA-F]+$")) {
return "";
}
final List matched = new ArrayList<>();
final Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hexa);
while (matcher.find()) {
matched.add(Long.parseLong("1" + matcher.group(), 16));
}
// conversion
final long[] result = new long[matched.size()];
for (int i = 0; i < matched.size(); i++) {
result[i] = matched.get(i);
}
return this.encode(result);
}
/**
* Decrypt string to numbers
*
* @param hash the encrypt string
* @return decryped numbers
*/
public String decodeHex(String hash) {
final StringBuilder result = new StringBuilder();
final long[] numbers = this.decode(hash);
for (final long number : numbers) {
result.append(Long.toHexString(number).substring(1));
}
return result.toString();
}
private String internalEncode(long... numbers) {
long numberHashInt = 0;
for (int i = 0; i < numbers.length; i++) {
numberHashInt += numbers[i] % (i + 100);
}
String alphabet = this.alphabet;
final char ret = alphabet.charAt((int) (numberHashInt % alphabet.length()));
long num;
long sepsIndex;
long guardIndex;
String buffer;
final StringBuilder retStrB = new StringBuilder(this.minHashLength);
retStrB.append(ret);
char guard;
for (int i = 0; i < numbers.length; i++) {
num = numbers[i];
buffer = ret + this.salt + alphabet;
alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
final String last = Hashids.hash(num, alphabet);
retStrB.append(last);
if (i + 1 < numbers.length) {
if (last.length() > 0) {
num %= last.charAt(0) + i;
sepsIndex = (int) (num % this.seps.length());
} else {
sepsIndex = 0;
}
retStrB.append(this.seps.charAt((int) sepsIndex));
}
}
StringBuilder retStr = new StringBuilder(retStrB.toString());
if (retStr.length() < this.minHashLength) {
guardIndex = (numberHashInt + (retStr.charAt(0))) % this.guards.length();
guard = this.guards.charAt((int) guardIndex);
retStr.insert(0, guard);
if (retStr.length() < this.minHashLength) {
guardIndex = (numberHashInt + (retStr.charAt(2))) % this.guards.length();
guard = this.guards.charAt((int) guardIndex);
retStr.append(guard);
}
}
final int halfLen = alphabet.length() / 2;
while (retStr.length() < this.minHashLength) {
alphabet = Hashids.consistentShuffle(alphabet, alphabet);
retStr = new StringBuilder(alphabet.substring(halfLen) + retStr + alphabet.substring(0, halfLen));
final int excess = retStr.length() - this.minHashLength;
if (excess > 0) {
final int startPos = excess / 2;
retStr = new StringBuilder(retStr.substring(startPos, startPos + this.minHashLength));
}
}
return retStr.toString();
}
private long[] internalDecode(final String hash, final String chars) {
final ArrayList ret = new ArrayList<>();
String alphabet = chars;
int i = 0;
final String regexp = "[" + this.guards + "]";
String hashBreakdown = hash.replaceAll(regexp, " ");
String[] hashArray = hashBreakdown.split(" ");
if (hashArray.length == 3 || hashArray.length == 2) {
i = 1;
}
if (hashArray.length > 0) {
hashBreakdown = hashArray[i];
if (!hashBreakdown.isEmpty()) {
final char lottery = hashBreakdown.charAt(0);
hashBreakdown = hashBreakdown.substring(1);
hashBreakdown = hashBreakdown.replaceAll("[" + this.seps + "]", " ");
hashArray = hashBreakdown.split(" ");
String subHash;
String buffer;
for (final String aHashArray : hashArray) {
subHash = aHashArray;
buffer = lottery + this.salt + alphabet;
alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
ret.add(Hashids.unhash(subHash, alphabet));
}
}
}
// transform from List to long[]
long[] arr = new long[ret.size()];
for (int k = 0; k < arr.length; k++) {
arr[k] = ret.get(k);
}
if (!this.encode(arr).equals(hash)) {
arr = new long[0];
}
return arr;
}
/**
* Get Hashid algorithm version.
*
* @return Hashids algorithm version implemented.
*/
public String getVersion() {
return "1.0.0";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy