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"; } }