diff --git a/pom.xml b/pom.xml
index 2b650d9..7027ab1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
4.0.0
org.hashids
hashids
- 1.0.3-SNAPSHOT
+ 1.0.4-SNAPSHOT
jar
hashids
Implementing Hashids algorithm v1.0.0 version(http://hashids.org)
@@ -207,5 +207,9 @@
Matthias Vill
https://github.com/TheConstructor
+
+ Mihai Cazacu
+ https://github.com/cazacugmihai
+
diff --git a/src/main/java/org/hashids/CharUtils.java b/src/main/java/org/hashids/CharUtils.java
new file mode 100644
index 0000000..3685b24
--- /dev/null
+++ b/src/main/java/org/hashids/CharUtils.java
@@ -0,0 +1,113 @@
+package org.hashids;
+
+import static java.lang.System.arraycopy;
+import static java.util.Arrays.copyOf;
+
+final class CharUtils {
+
+ private CharUtils() {
+ throw new UnsupportedOperationException();
+ }
+
+ static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) {
+ final char[] result = new char[arrA.length + arrB.length + arrC.length];
+
+ arraycopy(arrA, 0, result, 0, arrA.length);
+ arraycopy(arrB, 0, result, arrA.length, arrB.length);
+ arraycopy(arrC, 0, result, arrA.length + arrB.length, arrC.length);
+
+ return result;
+ }
+
+ static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) {
+ final int bCopyLength = bTo - bFrom;
+ final char[] result = new char[arrA.length + bCopyLength];
+
+ arraycopy(arrA, 0, result, 0, arrA.length);
+ arraycopy(arrB, bFrom, result, arrA.length, bCopyLength);
+
+ return result;
+ }
+
+ static int indexOf(char[] source, char c) {
+ int i = 0;
+
+ for (final char s : source) {
+ if (s == c) {
+ break;
+ }
+ i++;
+ }
+
+ return i;
+ }
+
+ static char[] cleanup(char[] source, char[] allowedChars) {
+ if ((source == null) || (allowedChars == null)) {
+ return source;
+ }
+
+ final char[] result = new char[source.length];
+ int i = 0;
+
+ for (final char s : source) {
+ for (final char a : allowedChars) {
+ if (s == a) {
+ result[i++] = s;
+ break;
+ }
+ }
+ }
+
+ return copyOf(result, i);
+ }
+
+ static char[] removeAll(char[] source, char[] charsToRemove) {
+ if ((source == null) || (charsToRemove == null)) {
+ return source;
+ }
+
+ final char[] result = new char[source.length];
+ int i = 0;
+ boolean found;
+
+ for (final char s : source) {
+ found = false;
+
+ for (final char c : charsToRemove) {
+ if (s == c) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ result[i++] = s;
+ }
+ }
+
+ return copyOf(result, i);
+ }
+
+ static boolean validate(char[] source, char[] allowedChars) {
+ boolean found;
+
+ for (final char s : source) {
+ found = false;
+
+ for (final char a : allowedChars) {
+ if (s == a) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+}
diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java
index 6d688ea..4257790 100644
--- a/src/main/java/org/hashids/Hashids.java
+++ b/src/main/java/org/hashids/Hashids.java
@@ -1,3 +1,4 @@
+
package org.hashids;
import java.util.ArrayList;
@@ -5,12 +6,17 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import static java.lang.Character.isSpaceChar;
+import static java.util.Arrays.copyOf;
+import static java.util.Arrays.copyOfRange;
+import static org.hashids.CharUtils.*;
+
/**
* Hashids designed for Generating short hashes from numbers (like YouTube and Bitly), obfuscate
* database IDs, use them as forgotten password hashes, invitation codes, store shard numbers.
*
* This is implementation of http://hashids.org v1.0.0 version.
- *
+ *
* This implementation is immutable, thread-safe, no lock is necessary.
*
* @author fanweixiao
@@ -23,154 +29,174 @@ public class Hashids {
*/
public static final long MAX_NUMBER = 9007199254740992L;
- private static final String DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
- private static final String DEFAULT_SEPS = "cfhistuCFHISTU";
- private static final String DEFAULT_SALT = "";
+ private static final String SPACE = " ";
+ private static final char[] DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".toCharArray();
+ private static final char[] DEFAULT_SEPS = "cfhistuCFHISTU".toCharArray();
+ private static final char[] DEFAULT_SALT = new char[0];
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 static final Pattern WORD_PATTERN = Pattern.compile("[\\w\\W]{1,12}");
- private final String salt;
+ private final char[] salt;
private final int minHashLength;
- private final String alphabet;
- private final String seps;
- private final String guards;
+ private final char[] alphabet;
+ private final char[] seps;
+ private final char[] guards;
+ private final String guardsRegExp;
+ private final String sepsRegExp;
+ private final char[] validChars;
public Hashids() {
- this(DEFAULT_SALT);
+ this(DEFAULT_SALT, DEFAULT_MIN_HASH_LENGTH, DEFAULT_ALPHABET);
}
public Hashids(String salt) {
- this(salt, 0);
+ this(salt, DEFAULT_MIN_HASH_LENGTH);
}
public Hashids(String salt, int minHashLength) {
- this(salt, minHashLength, DEFAULT_ALPHABET);
+ this((salt == null) ? null : salt.toCharArray(), minHashLength, DEFAULT_ALPHABET);
}
public Hashids(String salt, int minHashLength, String alphabet) {
- this.salt = salt != null ? salt : DEFAULT_SALT;
- this.minHashLength = minHashLength > 0 ? minHashLength : DEFAULT_MIN_HASH_LENGTH;
+ this((salt == null) ? null : salt.toCharArray(),
+ minHashLength,
+ (alphabet == null) ? null : alphabet.toCharArray());
+ }
- 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));
- }
+ private Hashids(char[] salt, int minHashLength, char[] alphabet) {
+ if (salt == null) {
+ throw new IllegalArgumentException("The salt cannot be null,");
}
- alphabet = uniqueAlphabet.toString();
-
- if (alphabet.length() < MIN_ALPHABET_LENGTH) {
- throw new IllegalArgumentException(
- "alphabet must contain at least " + MIN_ALPHABET_LENGTH + " unique characters");
+ if (minHashLength < 0) {
+ throw new IllegalArgumentException("The minimum hash length must be positive,");
}
- if (alphabet.contains(" ")) {
- throw new IllegalArgumentException("alphabet cannot contains spaces");
+ if (alphabet == null) {
+ throw new IllegalArgumentException("The alphabet cannot be null,");
}
+ this.salt = salt;
+ this.minHashLength = minHashLength;
+
+ // alphabet
+ validateAlphabet(alphabet);
+
// 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);
- }
- }
+ char[] seps = cleanup(DEFAULT_SEPS, alphabet);
+ alphabet = removeAll(alphabet, seps);
- alphabet = alphabet.replaceAll("\\s+", "");
- seps = seps.replaceAll("\\s+", "");
- seps = Hashids.consistentShuffle(seps, this.salt);
+ seps = Hashids.consistentShuffle(seps, salt);
- if ((seps.isEmpty()) || (((float) alphabet.length() / seps.length()) > SEP_DIV)) {
- int seps_len = (int) Math.ceil(alphabet.length() / SEP_DIV);
+ if ((seps.length == 0) || (((float) alphabet.length / seps.length) > SEP_DIV)) {
+ int seps_len = (int) Math.ceil(alphabet.length / SEP_DIV);
if (seps_len == 1) {
seps_len++;
}
- if (seps_len > seps.length()) {
- final int diff = seps_len - seps.length();
- seps += alphabet.substring(0, diff);
- alphabet = alphabet.substring(diff);
+ if (seps_len > seps.length) {
+ final int diff = seps_len - seps.length;
+ seps = concatenate(seps, alphabet, 0, diff);
+ alphabet = copyOfRange(alphabet, diff, alphabet.length);
} else {
- seps = seps.substring(0, seps_len);
+ seps = copyOf(seps, seps_len);
}
}
- alphabet = Hashids.consistentShuffle(alphabet, this.salt);
+ alphabet = Hashids.consistentShuffle(alphabet, salt);
// use double to round up
- final int guardCount = (int) Math.ceil((double) alphabet.length() / GUARD_DIV);
+ 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);
+ char[] guards;
+ if (alphabet.length < 3) {
+ guards = copyOf(seps, guardCount);
+ seps = copyOfRange(seps, guardCount, seps.length);
} else {
- guards = alphabet.substring(0, guardCount);
- alphabet = alphabet.substring(guardCount);
+ guards = copyOf(alphabet, guardCount);
+ alphabet = copyOfRange(alphabet, guardCount, alphabet.length);
}
+
this.guards = guards;
this.alphabet = alphabet;
this.seps = seps;
+ this.validChars = concatenate(alphabet, guards, seps);
+ this.guardsRegExp = '[' + String.valueOf(guards) + ']';
+ this.sepsRegExp = '[' + String.valueOf(seps) + ']';
+ }
+
+ private void validateAlphabet(char[] alphabet) {
+ if (alphabet.length < MIN_ALPHABET_LENGTH) {
+ throw new IllegalArgumentException(
+ "The alphabet must contain at least " + MIN_ALPHABET_LENGTH + " unique characters.");
+ }
+
+ for (int i = 0; i < alphabet.length; i++) {
+ if (isSpaceChar(alphabet[i])) {
+ throw new IllegalArgumentException("The alphabet cannot contain spaces.");
+ }
+
+ for (int j = i + 1; j < alphabet.length; j++) {
+ if (alphabet[i] == alphabet[j]) {
+ throw new IllegalArgumentException("The alphabet cannot contain duplicates.");
+ }
+ }
+ }
}
+ // ---------
+
/**
* Encode numbers to string
*
- * @param numbers
- * the numbers to encode
+ * @param numbers the numbers to encode
* @return the encoded string
*/
public String encode(long... numbers) {
if (numbers.length == 0) {
- return "";
+ throw new IllegalArgumentException("At least one number must be specified.");
}
for (final long number : numbers) {
if (number < 0) {
- return "";
+ return ""; // we must throw an exception here (like the case when we compare with MAX_NUMBER)
}
+
if (number > MAX_NUMBER) {
- throw new IllegalArgumentException("number can not be greater than " + MAX_NUMBER + "L");
+ throw new IllegalArgumentException("Number can not be greater than " + MAX_NUMBER + '.');
}
}
+
return this._encode(numbers);
}
/**
* Decode string to numbers
*
- * @param hash
- * the encoded string
+ * @param hash the encoded string
* @return decoded 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];
- }
+
+ if (!validate(hash.toCharArray(), validChars)) {
+ return new long[0];
}
- return this._decode(hash, this.alphabet);
+ return _decode(hash, alphabet);
}
/**
* Encode hexa to string
*
- * @param hexa
- * the hexa to encode
+ * @param hexa the hexa to encode
* @return the encoded string
*/
public String encodeHex(String hexa) {
@@ -179,10 +205,10 @@ public String encodeHex(String hexa) {
}
final List matched = new ArrayList();
- final Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hexa);
+ final Matcher matcher = WORD_PATTERN.matcher(hexa);
while (matcher.find()) {
- matched.add(Long.parseLong("1" + matcher.group(), 16));
+ matched.add(Long.parseLong('1' + matcher.group(), 16));
}
// conversion
@@ -197,8 +223,7 @@ public String encodeHex(String hexa) {
/**
* Decode string to numbers
*
- * @param hash
- * the encoded string
+ * @param hash the encoded string
* @return decoded numbers
*/
public String decodeHex(String hash) {
@@ -228,150 +253,154 @@ private String _encode(long... numbers) {
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()));
+
+ char[] newAlphabet = alphabet;
+ final char ret = newAlphabet[(int) (numberHashInt % newAlphabet.length)];
long num;
long sepsIndex, guardIndex;
- String buffer;
- final StringBuilder ret_strB = new StringBuilder(this.minHashLength);
+ final StringBuilder buffer = new StringBuilder();
+ final StringBuilder ret_strB = new StringBuilder();
ret_strB.append(ret);
char guard;
for (int i = 0; i < numbers.length; i++) {
num = numbers[i];
- buffer = ret + this.salt + alphabet;
+ buffer.setLength(0);
+ buffer.append(ret)
+ .append(salt)
+ .append(newAlphabet);
- alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length()));
- final String last = Hashids.hash(num, alphabet);
+ newAlphabet = Hashids.consistentShuffle(newAlphabet, buffer.substring(0, newAlphabet.length).toCharArray());
+ final String last = Hashids.hash(num, newAlphabet);
ret_strB.append(last);
if (i + 1 < numbers.length) {
if (last.length() > 0) {
- num %= (last.charAt(0) + i);
- sepsIndex = (int) (num % this.seps.length());
+ num %= last.charAt(0) + i;
+ sepsIndex = (int) (num % seps.length);
} else {
sepsIndex = 0;
}
- ret_strB.append(this.seps.charAt((int) sepsIndex));
+
+ ret_strB.append(seps[(int) sepsIndex]);
}
}
- String ret_str = ret_strB.toString();
- if (ret_str.length() < this.minHashLength) {
- guardIndex = (numberHashInt + (ret_str.charAt(0))) % this.guards.length();
- guard = this.guards.charAt((int) guardIndex);
+ if (ret_strB.length() < minHashLength) {
+ guardIndex = (numberHashInt + (ret_strB.charAt(0))) % guards.length;
+ guard = guards[(int) guardIndex];
- ret_str = guard + ret_str;
+ ret_strB.insert(0, guard);
- if (ret_str.length() < this.minHashLength) {
- guardIndex = (numberHashInt + (ret_str.charAt(2))) % this.guards.length();
- guard = this.guards.charAt((int) guardIndex);
+ if (ret_strB.length() < minHashLength) {
+ guardIndex = (numberHashInt + (ret_strB.charAt(2))) % guards.length;
+ guard = guards[(int) guardIndex];
- ret_str += guard;
+ ret_strB.append(guard);
}
}
- final int halfLen = alphabet.length() / 2;
- while (ret_str.length() < this.minHashLength) {
- alphabet = Hashids.consistentShuffle(alphabet, alphabet);
- ret_str = alphabet.substring(halfLen) + ret_str + alphabet.substring(0, halfLen);
- final int excess = ret_str.length() - this.minHashLength;
+ final int halfLen = newAlphabet.length / 2;
+ while (ret_strB.length() < minHashLength) {
+ newAlphabet = Hashids.consistentShuffle(newAlphabet, newAlphabet);
+ ret_strB.insert(0, newAlphabet, halfLen, newAlphabet.length - halfLen)
+ .append(newAlphabet, 0, halfLen);
+
+ final int excess = ret_strB.length() - minHashLength;
if (excess > 0) {
final int start_pos = excess / 2;
- ret_str = ret_str.substring(start_pos, start_pos + this.minHashLength);
+ ret_strB.replace(0, ret_strB.length(), ret_strB.substring(start_pos, start_pos + minHashLength));
}
}
- return ret_str;
+ return ret_strB.toString();
}
- private long[] _decode(String hash, String alphabet) {
- final ArrayList ret = new ArrayList();
+ private long[] _decode(String hash, char[] alphabet) {
+ long[] arr = new long[hash.length()];
+ int retIdx = 0;
int i = 0;
- final String regexp = "[" + this.guards + "]";
- String hashBreakdown = hash.replaceAll(regexp, " ");
- String[] hashArray = hashBreakdown.split(" ");
+ String hashBreakdown = hash.replaceAll(guardsRegExp, SPACE);
+ String[] hashArray = hashBreakdown.split(SPACE);
- if (hashArray.length == 3 || hashArray.length == 2) {
+ if ((hashArray.length == 2) || (hashArray.length == 3)) {
i = 1;
}
if (hashArray.length > 0) {
hashBreakdown = hashArray[i];
if (!hashBreakdown.isEmpty()) {
- final char lottery = hashBreakdown.charAt(0);
+ final char[] lottery = new char[] { hashBreakdown.charAt(0) };
hashBreakdown = hashBreakdown.substring(1);
- hashBreakdown = hashBreakdown.replaceAll("[" + this.seps + "]", " ");
- hashArray = hashBreakdown.split(" ");
+ hashBreakdown = hashBreakdown.replaceAll(sepsRegExp, SPACE);
+ hashArray = hashBreakdown.split(SPACE);
- String subHash, buffer;
+ String subHash;
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));
+ alphabet = Hashids.consistentShuffle(
+ alphabet,
+ copyOf(concatenate(lottery, salt, alphabet), alphabet.length));
+ arr[retIdx++] = 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);
- }
+ arr = copyOf(arr, retIdx);
- if (!this.encode(arr).equals(hash)) {
- arr = new long[0];
+ if (!encode(arr).equals(hash)) {
+ return new long[0];
}
return arr;
}
- private static String consistentShuffle(String alphabet, String salt) {
- if (salt.length() <= 0) {
- return alphabet;
+ private static char[] consistentShuffle(char[] alphabet, char[] salt) {
+ if (salt.length <= 0) {
+ return alphabet.clone();
}
int asc_val, j;
- final char[] tmpArr = alphabet.toCharArray();
- for (int i = tmpArr.length - 1, v = 0, p = 0; i > 0; i--, v++) {
- v %= salt.length();
- asc_val = salt.charAt(v);
+ final char[] result = alphabet.clone();
+ for (int i = result.length - 1, v = 0, p = 0; i > 0; i--, v++) {
+ v %= salt.length;
+ asc_val = salt[v];
p += asc_val;
j = (asc_val + v + p) % i;
- final char tmp = tmpArr[j];
- tmpArr[j] = tmpArr[i];
- tmpArr[i] = tmp;
+
+ final char tmp = result[j];
+ result[j] = result[i];
+ result[i] = tmp;
}
- return new String(tmpArr);
+ return result;
}
- private static String hash(long input, String alphabet) {
- String hash = "";
- final int alphabetLen = alphabet.length();
+ private static String hash(long input, char[] alphabet) {
+ final StringBuilder hash = new StringBuilder();
+ final int alphabetLen = alphabet.length;
do {
final int index = (int) (input % alphabetLen);
- if (index >= 0 && index < alphabet.length()) {
- hash = alphabet.charAt(index) + hash;
+ if (index >= 0 && index < alphabet.length) {
+ hash.insert(0, alphabet[index]);
}
input /= alphabetLen;
} while (input > 0);
- return hash;
+ return hash.toString();
}
- private static Long unhash(String input, String alphabet) {
+ private static long unhash(String input, char[] alphabet) {
long number = 0, pos;
for (int i = 0; i < input.length(); i++) {
- pos = alphabet.indexOf(input.charAt(i));
- number = number * alphabet.length() + pos;
+ pos = indexOf(alphabet, input.charAt(i));
+ number = number * alphabet.length + pos;
}
return number;
@@ -383,6 +412,6 @@ private static Long unhash(String input, String alphabet) {
* @return Hashids algorithm version implemented.
*/
public String getVersion() {
- return "1.0.0";
+ return "1.0.4-SNAPSHOT";
}
}