From e282be666f52d3c336f88ad3f8459fd4c9dccf3f Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 21 Feb 2018 20:50:46 +0200 Subject: [PATCH 01/11] performance improvements --- pom.xml | 6 +- src/main/java/org/hashids/CharUtils.java | 158 +++++ src/main/java/org/hashids/Hashids.java | 636 +++++++++++---------- src/test/java/org/hashids/HashidsTest.java | 386 ++++++------- 4 files changed, 685 insertions(+), 501 deletions(-) create mode 100644 src/main/java/org/hashids/CharUtils.java diff --git a/pom.xml b/pom.xml index 2b650d9..15f94ee 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..6018abe --- /dev/null +++ b/src/main/java/org/hashids/CharUtils.java @@ -0,0 +1,158 @@ +package org.hashids; + +import static java.util.Arrays.copyOfRange; + +public final class CharUtils { + + private CharUtils() { + throw new UnsupportedOperationException(); + } + + public static char[] concatenate(char a, char[] arrB, char[] arrC, int maxSize) { + if (maxSize == 0) { + return new char[0]; + } + + final char[] result = new char[maxSize]; + int i = 0; + + result[i++] = a; + + if (i == maxSize) { + return result; + } + + for (final char c : arrB) { + result[i++] = c; + + if (i == maxSize) { + return result; + } + } + + for (final char c : arrC) { + result[i++] = c; + + if (i == maxSize) { + return result; + } + } + + return result; + } + + public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { + final char[] result = new char[arrA.length + arrB.length + arrC.length]; + int i = 0; + + for (final char c : arrA) { + result[i++] = c; + } + + for (final char c : arrB) { + result[i++] = c; + } + + for (final char c : arrC) { + result[i++] = c; + } + + return result; + } + + public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { + final char[] result = new char[arrA.length + bTo - bFrom]; + int i = 0; + + for (final char c : arrA) { + result[i++] = c; + } + + for (int j = bFrom; j < bTo; j++) { + result[i++] = arrB[j]; + } + + return result; + } + + public static int indexOf(char[] source, char c) { + int i = 0; + + for (final char s : source) { + if (s == c) { + break; + } + i++; + } + + return i; + } + + public 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 copyOfRange(result, 0, i); + } + + public 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 copyOfRange(result, 0, i); + } + + public 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..6ff6605 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 @@ -18,371 +24,385 @@ * @since 0.3.3 */ public class Hashids { - /** - * Max number that can be encoded with 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 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); - } - - public Hashids(String salt, int minHashLength, String alphabet) { - 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)); - } + /** + * Max number that can be encoded with Hashids. + */ + public static final long MAX_NUMBER = 9007199254740992L; + + 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 char[] salt; + private final int minHashLength; + 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, DEFAULT_MIN_HASH_LENGTH, DEFAULT_ALPHABET); } - alphabet = uniqueAlphabet.toString(); - - if (alphabet.length() < MIN_ALPHABET_LENGTH) { - throw new IllegalArgumentException( - "alphabet must contain at least " + MIN_ALPHABET_LENGTH + " unique characters"); + public Hashids(String salt) { + this(salt, DEFAULT_MIN_HASH_LENGTH); } - if (alphabet.contains(" ")) { - throw new IllegalArgumentException("alphabet cannot contains spaces"); + public Hashids(String salt, int minHashLength) { + this((salt == null) ? null : salt.toCharArray(), minHashLength, DEFAULT_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); - } + public Hashids(String salt, int minHashLength, String alphabet) { + this((salt == null) ? null : salt.toCharArray(), + minHashLength, + (alphabet == null) ? null : alphabet.toCharArray()); } - alphabet = alphabet.replaceAll("\\s+", ""); - seps = seps.replaceAll("\\s+", ""); - seps = Hashids.consistentShuffle(seps, this.salt); + private Hashids(char[] salt, int minHashLength, char[] alphabet) { + assert salt != null; + assert minHashLength >= 0; + assert alphabet != null; - if ((seps.isEmpty()) || (((float) alphabet.length() / seps.length()) > SEP_DIV)) { - int seps_len = (int) Math.ceil(alphabet.length() / SEP_DIV); + this.salt = salt; + this.minHashLength = minHashLength; - if (seps_len == 1) { - seps_len++; - } + // alphabet + validateAlphabet(alphabet); - if (seps_len > seps.length()) { - final int diff = seps_len - seps.length(); - seps += alphabet.substring(0, diff); - alphabet = alphabet.substring(diff); - } else { - seps = seps.substring(0, seps_len); - } - } + // seps should contain only characters present in alphabet; + // alphabet should not contains seps + char[] seps = cleanup(DEFAULT_SEPS, alphabet); + alphabet = removeAll(alphabet, seps); - 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; - } - - /** - * Encode numbers to string - * - * @param numbers - * the numbers to encode - * @return the encoded string - */ - public String encode(long... numbers) { - if (numbers.length == 0) { - return ""; - } + seps = Hashids.consistentShuffle(seps, salt); - for (final long number : numbers) { - if (number < 0) { - return ""; - } - if (number > MAX_NUMBER) { - throw new IllegalArgumentException("number can not be greater than " + MAX_NUMBER + "L"); - } - } - return this._encode(numbers); - } - - /** - * Decode string to numbers - * - * @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 ((seps.length == 0) || (((float) alphabet.length / seps.length) > SEP_DIV)) { + int seps_len = (int) Math.ceil(alphabet.length / SEP_DIV); - return this._decode(hash, this.alphabet); - } - - /** - * Encode hexa to string - * - * @param hexa - * the hexa to encode - * @return the encoded string - */ - public String encodeHex(String hexa) { - if (!hexa.matches("^[0-9a-fA-F]+$")) { - return ""; - } + if (seps_len == 1) { + seps_len++; + } - final List matched = new ArrayList(); - final Matcher matcher = Pattern.compile("[\\w\\W]{1,12}").matcher(hexa); + 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 = copyOf(seps, seps_len); + } + } - while (matcher.find()) { - matched.add(Long.parseLong("1" + matcher.group(), 16)); - } + alphabet = Hashids.consistentShuffle(alphabet, salt); + // use double to round up + final int guardCount = (int) Math.ceil((double) alphabet.length / GUARD_DIV); - // conversion - final long[] result = new long[matched.size()]; - for (int i = 0; i < matched.size(); i++) { - result[i] = matched.get(i); - } + char[] guards; + if (alphabet.length < 3) { + guards = copyOf(seps, guardCount); + seps = copyOfRange(seps, guardCount, seps.length); + } else { + guards = copyOf(alphabet, guardCount); + alphabet = copyOfRange(alphabet, guardCount, alphabet.length); + } - return this.encode(result); - } - - /** - * Decode string to numbers - * - * @param hash - * the encoded string - * @return decoded 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)); + 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) + ']'; } - return result.toString(); - } + 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."); + } - 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); + if ((i + 1 < alphabet.length) && (alphabet[i] == alphabet[i + 1])) { + throw new IllegalArgumentException("The alphabet cannot contain duplicates."); + } + } } - return result; - } - /* Private methods */ + // --------- + + /** + * Encode numbers to string + * + * @param numbers the numbers to encode + * @return the encoded string + */ + public String encode(long... numbers) { + if (numbers.length == 0) { + throw new IllegalArgumentException("At least one number must be specified."); + } + + for (final long number : numbers) { + if (number < 0) { + return ""; // we must throw an exception here (like the case when we compare with MAX_NUMBER) + } - private String _encode(long... numbers) { - long numberHashInt = 0; - for (int i = 0; i < numbers.length; i++) { - numberHashInt += (numbers[i] % (i + 100)); + if (number > MAX_NUMBER) { + throw new IllegalArgumentException("Number can not be greater than " + MAX_NUMBER + '.'); + } + } + + return this._encode(numbers); } - String alphabet = this.alphabet; - final char ret = alphabet.charAt((int) (numberHashInt % alphabet.length())); - long num; - long sepsIndex, guardIndex; - String buffer; - final StringBuilder ret_strB = new StringBuilder(this.minHashLength); - ret_strB.append(ret); - char guard; + /** + * Decode string to numbers + * + * @param hash the encoded string + * @return decoded numbers + */ + public long[] decode(String hash) { + if (hash.isEmpty()) { + return new long[0]; + } - for (int i = 0; i < numbers.length; i++) { - num = numbers[i]; - buffer = ret + this.salt + alphabet; + if (!validate(hash.toCharArray(), validChars)) { + return new long[0]; + } - alphabet = Hashids.consistentShuffle(alphabet, buffer.substring(0, alphabet.length())); - final String last = Hashids.hash(num, alphabet); + return _decode(hash, alphabet); + } - ret_strB.append(last); + /** + * Encode hexa to string + * + * @param hexa the hexa to encode + * @return the encoded string + */ + public String encodeHex(String hexa) { + if (!hexa.matches("^[0-9a-fA-F]+$")) { + return ""; + } - if (i + 1 < numbers.length) { - if (last.length() > 0) { - num %= (last.charAt(0) + i); - sepsIndex = (int) (num % this.seps.length()); - } else { - sepsIndex = 0; + final List matched = new ArrayList(); + final Matcher matcher = WORD_PATTERN.matcher(hexa); + + while (matcher.find()) { + matched.add(Long.parseLong('1' + matcher.group(), 16)); } - ret_strB.append(this.seps.charAt((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); + // conversion + final long[] result = new long[matched.size()]; + for (int i = 0; i < matched.size(); i++) { + result[i] = matched.get(i); + } - ret_str = guard + ret_str; + return this.encode(result); + } - if (ret_str.length() < this.minHashLength) { - guardIndex = (numberHashInt + (ret_str.charAt(2))) % this.guards.length(); - guard = this.guards.charAt((int) guardIndex); + /** + * Decode string to numbers + * + * @param hash the encoded string + * @return decoded 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)); + } - ret_str += guard; - } + return result.toString(); } - 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; - if (excess > 0) { - final int start_pos = excess / 2; - ret_str = ret_str.substring(start_pos, start_pos + this.minHashLength); - } + 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; } - return ret_str; - } + /* Private methods */ - private long[] _decode(String hash, String alphabet) { - final ArrayList ret = new ArrayList(); + private String _encode(long... numbers) { + long numberHashInt = 0; + for (int i = 0; i < numbers.length; i++) { + numberHashInt += (numbers[i] % (i + 100)); + } - int i = 0; - final String regexp = "[" + this.guards + "]"; - String hashBreakdown = hash.replaceAll(regexp, " "); - String[] hashArray = hashBreakdown.split(" "); + char[] newAlphabet = alphabet; + final char ret = newAlphabet[(int) (numberHashInt % newAlphabet.length)]; + + long num; + long sepsIndex, guardIndex; + 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.setLength(0); + buffer.append(ret) + .append(salt) + .append(newAlphabet); + + 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 % seps.length); + } else { + sepsIndex = 0; + } + + ret_strB.append(seps[(int) sepsIndex]); + } + } - if (hashArray.length == 3 || hashArray.length == 2) { - i = 1; - } + if (ret_strB.length() < minHashLength) { + guardIndex = (numberHashInt + (ret_strB.charAt(0))) % guards.length; + guard = guards[(int) guardIndex]; - 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, 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)); + ret_strB.insert(0, guard); + + if (ret_strB.length() < minHashLength) { + guardIndex = (numberHashInt + (ret_strB.charAt(2))) % guards.length; + guard = guards[(int) guardIndex]; + + ret_strB.append(guard); + } } - } - } - // transform from List to long[] - long[] arr = new long[ret.size()]; - for (int k = 0; k < arr.length; k++) { - arr[k] = ret.get(k); - } + 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_strB.replace(0, ret_strB.length(), ret_strB.substring(start_pos, start_pos + minHashLength)); + } + } - if (!this.encode(arr).equals(hash)) { - arr = new long[0]; + return ret_strB.toString(); } - return arr; - } + private long[] _decode(String hash, char[] alphabet) { + final List ret = new ArrayList(); + + int i = 0; + String hashBreakdown = hash.replaceAll(guardsRegExp, SPACE); + String[] hashArray = hashBreakdown.split(SPACE); - private static String consistentShuffle(String alphabet, String salt) { - if (salt.length() <= 0) { - return alphabet; + 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(sepsRegExp, SPACE); + hashArray = hashBreakdown.split(SPACE); + + String subHash; + for (final String aHashArray : hashArray) { + subHash = aHashArray; + alphabet = Hashids.consistentShuffle(alphabet, concatenate(lottery, salt, alphabet, 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; } - 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); - p += asc_val; - j = (asc_val + v + p) % i; - final char tmp = tmpArr[j]; - tmpArr[j] = tmpArr[i]; - tmpArr[i] = tmp; + private static char[] consistentShuffle(char[] alphabet, char[] salt) { + if (salt.length <= 0) { + return alphabet.clone(); + } + + int asc_val, j; + 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 = result[j]; + result[j] = result[i]; + result[i] = tmp; + } + + return result; } - return new String(tmpArr); - } + private static String hash(long input, char[] alphabet) { + final StringBuilder hash = new StringBuilder(); + final int alphabetLen = alphabet.length; - private static String hash(long input, String alphabet) { - String hash = ""; - final int alphabetLen = alphabet.length(); + do { + final int index = (int) (input % alphabetLen); + if (index >= 0 && index < alphabet.length) { + hash.insert(0, alphabet[index]); + } + input /= alphabetLen; + } while (input > 0); - do { - final int index = (int) (input % alphabetLen); - if (index >= 0 && index < alphabet.length()) { - hash = alphabet.charAt(index) + hash; - } - input /= alphabetLen; - } while (input > 0); + return hash.toString(); + } - return hash; - } + private static Long unhash(String input, char[] alphabet) { + long number = 0, pos; - private static Long unhash(String input, String alphabet) { - long number = 0, pos; + for (int i = 0; i < input.length(); i++) { + pos = indexOf(alphabet, input.charAt(i)); + number = number * alphabet.length + pos; + } - for (int i = 0; i < input.length(); i++) { - pos = alphabet.indexOf(input.charAt(i)); - number = number * alphabet.length() + pos; + return number; } - return number; - } - - /** - * Get Hashid algorithm version. - * - * @return Hashids algorithm version implemented. - */ - public String getVersion() { - return "1.0.0"; - } + /** + * Get Hashid algorithm version. + * + * @return Hashids algorithm version implemented. + */ + public String getVersion() { + return "1.0.0"; + } } diff --git a/src/test/java/org/hashids/HashidsTest.java b/src/test/java/org/hashids/HashidsTest.java index 0f9d86f..3e3421c 100644 --- a/src/test/java/org/hashids/HashidsTest.java +++ b/src/test/java/org/hashids/HashidsTest.java @@ -1,201 +1,203 @@ package org.hashids; -import java.util.Arrays; - import org.junit.Assert; import org.junit.Test; +import java.util.Arrays; + +import static org.junit.Assert.assertEquals; + public class HashidsTest { - @Test - public void test_large_number() { - final long num_to_hash = 9007199254740992L; - final Hashids a = new Hashids("this is my salt"); - final String res = a.encode(num_to_hash); - final long[] b = a.decode(res); - Assert.assertEquals(num_to_hash, b[0]); - } - - @Test(expected = IllegalArgumentException.class) - public void test_large_number_not_supported() throws Exception { - final long num_to_hash = 9007199254740993L; - final Hashids a = new Hashids("this is my salt"); - a.encode(num_to_hash); - } - - @Test - public void test_wrong_decoding() { - final Hashids a = new Hashids("this is my pepper"); - final long[] b = a.decode("NkK9"); - Assert.assertEquals(b.length, 0); - } - - @Test - public void test_one_number() { - final String expected = "NkK9"; - String res; - final long num_to_hash = 12345L; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, 1); - Assert.assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_serveral_numbers() { - final String expected = "aBMswoO2UB3Sj"; - String res; - final long[] num_to_hash = { 683L, 94108L, 123L, 5L }; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_specifying_custom_hash_alphabet() { - final String expected = "b332db5"; - String res; - final long num_to_hash = 1234567L; - long[] res2; - final Hashids a = new Hashids("this is my salt", 0, "0123456789abcdef"); - res = a.encode(num_to_hash); - Assert.assertEquals(expected, res); - res2 = a.decode(expected); - Assert.assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_specifying_custom_hash_length() { - final String expected = "gB0NV05e"; - String res; - final long num_to_hash = 1L; - long[] res2; - final Hashids a = new Hashids("this is my salt", 8); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, 1); - Assert.assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_randomness() { - final String expected = "1Wc8cwcE"; - String res; - final long[] num_to_hash = { 5L, 5L, 5L, 5L }; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_randomness_for_incrementing_numbers() { - final String expected = "kRHnurhptKcjIDTWC3sx"; - String res; - final long[] num_to_hash = { 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L }; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_randomness_for_incrementing() { - Hashids a; - a = new Hashids("this is my salt"); - Assert.assertEquals(a.encode(1L), "NV"); - Assert.assertEquals(a.encode(2L), "6m"); - Assert.assertEquals(a.encode(3L), "yD"); - Assert.assertEquals(a.encode(4L), "2l"); - Assert.assertEquals(a.encode(5L), "rD"); - } - - @Test - public void test_for_vlues_greater_int_maxval() { - final Hashids a = new Hashids("this is my salt"); - Assert.assertEquals(a.encode(9876543210123L), "Y8r7W1kNN"); - } - - @Test - public void test_issue10() { - final String expected = "3kK3nNOe"; - String res; - final long num_to_hash = 75527867232l; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(res, expected); - res2 = a.decode(expected); - Assert.assertEquals(res2.length, 1); - Assert.assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_issue23() { - final String expected = "9Q7MJ3LVGW"; - String res; - final long num_to_hash = 1145L; - long[] res2; - final Hashids a = new Hashids("MyCamelCaseSalt", 10, "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"); - res = a.encode(num_to_hash); - Assert.assertEquals(expected, res); - res2 = a.decode(expected); - Assert.assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_issue30() { - final String expected = ""; - String res; - final long num_to_hash = -1L; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - Assert.assertEquals(expected, res); - } - - @Test - public void test_issue31() { - final long[] numbers = new long[500000]; - long current = Hashids.MAX_NUMBER; - for (int i = 0; i < numbers.length; i++) { - numbers[i] = current--; - } - final Hashids a = new Hashids("this is my salt"); - Assert.assertNotEquals("", a.encode(numbers)); - } - - @Test - public void test_issue32() throws Exception { - final long num_to_hash = -1; - final Hashids a = new Hashids("this is my salt"); - Assert.assertEquals("", a.encode(num_to_hash)); - } - - @Test - public void test_issue45() throws Exception { - Hashids hashids = new Hashids("this is my salt"); - long[] numbers = hashids.decode("()"); - Assert.assertEquals(numbers.length, 0); - numbers = hashids.decode("[]"); - Assert.assertEquals(numbers.length, 0); - numbers = hashids.decode("недействительный"); - Assert.assertEquals(numbers.length, 0); - numbers = hashids.decode("無效"); - Assert.assertEquals(numbers.length, 0); - } + @Test + public void test_large_number() { + final long num_to_hash = 9007199254740992L; + final Hashids a = new Hashids("this is my salt"); + final String res = a.encode(num_to_hash); + final long[] b = a.decode(res); + assertEquals(num_to_hash, b[0]); + } + + @Test(expected = IllegalArgumentException.class) + public void test_large_number_not_supported() throws Exception { + final long num_to_hash = 9007199254740993L; + final Hashids a = new Hashids("this is my salt"); + a.encode(num_to_hash); + } + + @Test + public void test_wrong_decoding() { + final Hashids a = new Hashids("this is my pepper"); + final long[] b = a.decode("NkK9"); + assertEquals(b.length, 0); + } + + @Test + public void test_one_number() { + final String expected = "NkK9"; + String res; + final long num_to_hash = 12345L; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, 1); + assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_serveral_numbers() { + final String expected = "aBMswoO2UB3Sj"; + String res; + final long[] num_to_hash = {683L, 94108L, 123L, 5L}; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_specifying_custom_hash_alphabet() { + final String expected = "b332db5"; + String res; + final long num_to_hash = 1234567L; + long[] res2; + final Hashids a = new Hashids("this is my salt", 0, "0123456789abcdef"); + res = a.encode(num_to_hash); + assertEquals(expected, res); + res2 = a.decode(expected); + assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_specifying_custom_hash_length() { + final String expected = "gB0NV05e"; + String res; + final long num_to_hash = 1L; + long[] res2; + final Hashids a = new Hashids("this is my salt", 8); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, 1); + assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_randomness() { + final String expected = "1Wc8cwcE"; + String res; + final long[] num_to_hash = {5L, 5L, 5L, 5L}; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_randomness_for_incrementing_numbers() { + final String expected = "kRHnurhptKcjIDTWC3sx"; + String res; + final long[] num_to_hash = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L}; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_randomness_for_incrementing() { + Hashids a; + a = new Hashids("this is my salt"); + assertEquals(a.encode(1L), "NV"); + assertEquals(a.encode(2L), "6m"); + assertEquals(a.encode(3L), "yD"); + assertEquals(a.encode(4L), "2l"); + assertEquals(a.encode(5L), "rD"); + } + + @Test + public void test_for_vlues_greater_int_maxval() { + final Hashids a = new Hashids("this is my salt"); + assertEquals(a.encode(9876543210123L), "Y8r7W1kNN"); + } + + @Test + public void test_issue10() { + final String expected = "3kK3nNOe"; + String res; + final long num_to_hash = 75527867232l; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(res, expected); + res2 = a.decode(expected); + assertEquals(res2.length, 1); + assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_issue23() { + final String expected = "9Q7MJ3LVGW"; + String res; + final long num_to_hash = 1145L; + long[] res2; + final Hashids a = new Hashids("MyCamelCaseSalt", 10, "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"); + res = a.encode(num_to_hash); + assertEquals(expected, res); + res2 = a.decode(expected); + assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_issue30() { + final String expected = ""; + String res; + final long num_to_hash = -1L; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + assertEquals(expected, res); + } + + @Test + public void test_issue31() { + final long[] numbers = new long[500000]; + long current = Hashids.MAX_NUMBER; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = current--; + } + final Hashids a = new Hashids("this is my salt"); + Assert.assertNotEquals("", a.encode(numbers)); + } + + @Test + public void test_issue32() throws Exception { + final long num_to_hash = -1; + final Hashids a = new Hashids("this is my salt"); + assertEquals("", a.encode(num_to_hash)); + } + + @Test + public void test_issue45() throws Exception { + Hashids Hashids = new Hashids("this is my salt"); + long[] numbers = Hashids.decode("()"); + assertEquals(numbers.length, 0); + numbers = Hashids.decode("[]"); + assertEquals(numbers.length, 0); + numbers = Hashids.decode("недействительный"); + assertEquals(numbers.length, 0); + numbers = Hashids.decode("無效"); + assertEquals(numbers.length, 0); + } } From d134613aa4a05cc1c95dce08a491a65169323203 Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 21 Feb 2018 22:35:44 +0200 Subject: [PATCH 02/11] fixed formatting --- src/main/java/org/hashids/CharUtils.java | 212 +++---- src/main/java/org/hashids/Hashids.java | 638 ++++++++++----------- src/test/java/org/hashids/HashidsTest.java | 386 +++++++------ 3 files changed, 617 insertions(+), 619 deletions(-) diff --git a/src/main/java/org/hashids/CharUtils.java b/src/main/java/org/hashids/CharUtils.java index 6018abe..f2afdc4 100644 --- a/src/main/java/org/hashids/CharUtils.java +++ b/src/main/java/org/hashids/CharUtils.java @@ -4,155 +4,155 @@ public final class CharUtils { - private CharUtils() { - throw new UnsupportedOperationException(); - } + private CharUtils() { + throw new UnsupportedOperationException(); + } - public static char[] concatenate(char a, char[] arrB, char[] arrC, int maxSize) { - if (maxSize == 0) { - return new char[0]; - } + public static char[] concatenate(char a, char[] arrB, char[] arrC, int maxSize) { + if (maxSize == 0) { + return new char[0]; + } - final char[] result = new char[maxSize]; - int i = 0; + final char[] result = new char[maxSize]; + int i = 0; - result[i++] = a; + result[i++] = a; - if (i == maxSize) { - return result; - } - - for (final char c : arrB) { - result[i++] = c; + if (i == maxSize) { + return result; + } - if (i == maxSize) { - return result; - } - } + for (final char c : arrB) { + result[i++] = c; - for (final char c : arrC) { - result[i++] = c; + if (i == maxSize) { + return result; + } + } - if (i == maxSize) { - return result; - } - } + for (final char c : arrC) { + result[i++] = c; + if (i == maxSize) { return result; + } } - public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { - final char[] result = new char[arrA.length + arrB.length + arrC.length]; - int i = 0; + return result; + } - for (final char c : arrA) { - result[i++] = c; - } + public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { + final char[] result = new char[arrA.length + arrB.length + arrC.length]; + int i = 0; - for (final char c : arrB) { - result[i++] = c; - } + for (final char c : arrA) { + result[i++] = c; + } - for (final char c : arrC) { - result[i++] = c; - } + for (final char c : arrB) { + result[i++] = c; + } - return result; + for (final char c : arrC) { + result[i++] = c; } - public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { - final char[] result = new char[arrA.length + bTo - bFrom]; - int i = 0; + return result; + } - for (final char c : arrA) { - result[i++] = c; - } + public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { + final char[] result = new char[arrA.length + bTo - bFrom]; + int i = 0; - for (int j = bFrom; j < bTo; j++) { - result[i++] = arrB[j]; - } + for (final char c : arrA) { + result[i++] = c; + } - return result; + for (int j = bFrom; j < bTo; j++) { + result[i++] = arrB[j]; } - public static int indexOf(char[] source, char c) { - int i = 0; + return result; + } - for (final char s : source) { - if (s == c) { - break; - } - i++; - } + public static int indexOf(char[] source, char c) { + int i = 0; - return i; + for (final char s : source) { + if (s == c) { + break; + } + i++; } - public static char[] cleanup(char[] source, char[] allowedChars) { - if ((source == null) || (allowedChars == null)) { - return source; - } + return i; + } + + public static char[] cleanup(char[] source, char[] allowedChars) { + if ((source == null) || (allowedChars == null)) { + return source; + } - final char[] result = new char[source.length]; - int i = 0; + 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; - } - } + for (final char s : source) { + for (final char a : allowedChars) { + if (s == a) { + result[i++] = s; + break; } - - return copyOfRange(result, 0, i); + } } - public static char[] removeAll(char[] source, char[] charsToRemove) { - if ((source == null) || (charsToRemove == null)) { - return source; - } + return copyOfRange(result, 0, i); + } - final char[] result = new char[source.length]; - int i = 0; - boolean found; + public static char[] removeAll(char[] source, char[] charsToRemove) { + if ((source == null) || (charsToRemove == null)) { + return source; + } - for (final char s : source) { - found = false; + final char[] result = new char[source.length]; + int i = 0; + boolean found; - for (final char c : charsToRemove) { - if (s == c) { - found = true; - break; - } - } + for (final char s : source) { + found = false; - if (!found) { - result[i++] = s; - } + for (final char c : charsToRemove) { + if (s == c) { + found = true; + break; } + } - return copyOfRange(result, 0, i); + if (!found) { + result[i++] = s; + } } - public static boolean validate(char[] source, char[] allowedChars) { - boolean found; + return copyOfRange(result, 0, i); + } - for (final char s : source) { - found = false; + public static boolean validate(char[] source, char[] allowedChars) { + boolean found; - for (final char a : allowedChars) { - if (s == a) { - found = true; - break; - } - } + for (final char s : source) { + found = false; - if (!found) { - return false; - } + for (final char a : allowedChars) { + if (s == a) { + found = true; + break; } + } - return true; + 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 6ff6605..34ac292 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -24,385 +24,385 @@ * @since 0.3.3 */ public class Hashids { - /** - * Max number that can be encoded with Hashids. - */ - public static final long MAX_NUMBER = 9007199254740992L; - - 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 char[] salt; - private final int minHashLength; - 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, DEFAULT_MIN_HASH_LENGTH, DEFAULT_ALPHABET); + /** + * Max number that can be encoded with Hashids. + */ + public static final long MAX_NUMBER = 9007199254740992L; + + 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 char[] salt; + private final int minHashLength; + 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, DEFAULT_MIN_HASH_LENGTH, DEFAULT_ALPHABET); + } + + public Hashids(String salt) { + this(salt, DEFAULT_MIN_HASH_LENGTH); + } + + public Hashids(String salt, int minHashLength) { + this((salt == null) ? null : salt.toCharArray(), minHashLength, DEFAULT_ALPHABET); + } + + public Hashids(String salt, int minHashLength, String alphabet) { + this((salt == null) ? null : salt.toCharArray(), + minHashLength, + (alphabet == null) ? null : alphabet.toCharArray()); + } + + private Hashids(char[] salt, int minHashLength, char[] alphabet) { + assert salt != null; + assert minHashLength >= 0; + assert alphabet != null; + + this.salt = salt; + this.minHashLength = minHashLength; + + // alphabet + validateAlphabet(alphabet); + + // seps should contain only characters present in alphabet; + // alphabet should not contains seps + char[] seps = cleanup(DEFAULT_SEPS, alphabet); + alphabet = removeAll(alphabet, seps); + + seps = Hashids.consistentShuffle(seps, salt); + + 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 = concatenate(seps, alphabet, 0, diff); + alphabet = copyOfRange(alphabet, diff, alphabet.length); + } else { + seps = copyOf(seps, seps_len); + } } - public Hashids(String salt) { - this(salt, DEFAULT_MIN_HASH_LENGTH); + alphabet = Hashids.consistentShuffle(alphabet, salt); + // use double to round up + final int guardCount = (int) Math.ceil((double) alphabet.length / GUARD_DIV); + + char[] guards; + if (alphabet.length < 3) { + guards = copyOf(seps, guardCount); + seps = copyOfRange(seps, guardCount, seps.length); + } else { + guards = copyOf(alphabet, guardCount); + alphabet = copyOfRange(alphabet, guardCount, alphabet.length); } - public Hashids(String salt, int minHashLength) { - this((salt == null) ? null : salt.toCharArray(), minHashLength, DEFAULT_ALPHABET); + 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."); } - public Hashids(String salt, int minHashLength, String alphabet) { - this((salt == null) ? null : salt.toCharArray(), - minHashLength, - (alphabet == null) ? null : alphabet.toCharArray()); - } - - private Hashids(char[] salt, int minHashLength, char[] alphabet) { - assert salt != null; - assert minHashLength >= 0; - assert alphabet != null; - - this.salt = salt; - this.minHashLength = minHashLength; - - // alphabet - validateAlphabet(alphabet); - - // seps should contain only characters present in alphabet; - // alphabet should not contains seps - char[] seps = cleanup(DEFAULT_SEPS, alphabet); - alphabet = removeAll(alphabet, seps); - - seps = Hashids.consistentShuffle(seps, salt); - - if ((seps.length == 0) || (((float) alphabet.length / seps.length) > SEP_DIV)) { - int seps_len = (int) Math.ceil(alphabet.length / SEP_DIV); + for (int i = 0; i < alphabet.length; i++) { + if (isSpaceChar(alphabet[i])) { + throw new IllegalArgumentException("The alphabet cannot contain spaces."); + } - if (seps_len == 1) { - seps_len++; - } + if ((i + 1 < alphabet.length) && (alphabet[i] == alphabet[i + 1])) { + throw new IllegalArgumentException("The alphabet cannot contain duplicates."); + } + } + } + + // --------- + + /** + * Encode numbers to string + * + * @param numbers the numbers to encode + * @return the encoded string + */ + public String encode(long... numbers) { + if (numbers.length == 0) { + throw new IllegalArgumentException("At least one number must be specified."); + } - 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 = copyOf(seps, seps_len); - } - } + for (final long number : numbers) { + if (number < 0) { + return ""; // we must throw an exception here (like the case when we compare with MAX_NUMBER) + } - alphabet = Hashids.consistentShuffle(alphabet, salt); - // use double to round up - final int guardCount = (int) Math.ceil((double) alphabet.length / GUARD_DIV); + if (number > MAX_NUMBER) { + throw new IllegalArgumentException("Number can not be greater than " + MAX_NUMBER + '.'); + } + } - char[] guards; - if (alphabet.length < 3) { - guards = copyOf(seps, guardCount); - seps = copyOfRange(seps, guardCount, seps.length); - } else { - guards = copyOf(alphabet, guardCount); - alphabet = copyOfRange(alphabet, guardCount, alphabet.length); - } + return this._encode(numbers); + } + + /** + * Decode string to numbers + * + * @param hash the encoded string + * @return decoded numbers + */ + public long[] decode(String hash) { + if (hash.isEmpty()) { + return new long[0]; + } - 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) + ']'; + if (!validate(hash.toCharArray(), validChars)) { + return new long[0]; } - 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."); - } + return _decode(hash, alphabet); + } + + /** + * Encode hexa to string + * + * @param hexa the hexa to encode + * @return the encoded string + */ + public String encodeHex(String hexa) { + if (!hexa.matches("^[0-9a-fA-F]+$")) { + return ""; + } - for (int i = 0; i < alphabet.length; i++) { - if (isSpaceChar(alphabet[i])) { - throw new IllegalArgumentException("The alphabet cannot contain spaces."); - } + final List matched = new ArrayList(); + final Matcher matcher = WORD_PATTERN.matcher(hexa); - if ((i + 1 < alphabet.length) && (alphabet[i] == alphabet[i + 1])) { - throw new IllegalArgumentException("The alphabet cannot contain duplicates."); - } - } + while (matcher.find()) { + matched.add(Long.parseLong('1' + matcher.group(), 16)); } - // --------- - - /** - * Encode numbers to string - * - * @param numbers the numbers to encode - * @return the encoded string - */ - public String encode(long... numbers) { - if (numbers.length == 0) { - throw new IllegalArgumentException("At least one number must be specified."); - } + // conversion + final long[] result = new long[matched.size()]; + for (int i = 0; i < matched.size(); i++) { + result[i] = matched.get(i); + } - for (final long number : numbers) { - if (number < 0) { - return ""; // we must throw an exception here (like the case when we compare with MAX_NUMBER) - } + return this.encode(result); + } + + /** + * Decode string to numbers + * + * @param hash the encoded string + * @return decoded 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)); + } - if (number > MAX_NUMBER) { - throw new IllegalArgumentException("Number can not be greater than " + MAX_NUMBER + '.'); - } - } + return result.toString(); + } - return this._encode(numbers); + 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; + } - /** - * Decode string to numbers - * - * @param hash the encoded string - * @return decoded numbers - */ - public long[] decode(String hash) { - if (hash.isEmpty()) { - return new long[0]; - } - - if (!validate(hash.toCharArray(), validChars)) { - return new long[0]; - } + /* Private methods */ - return _decode(hash, alphabet); + private String _encode(long... numbers) { + long numberHashInt = 0; + for (int i = 0; i < numbers.length; i++) { + numberHashInt += (numbers[i] % (i + 100)); } - /** - * Encode hexa to string - * - * @param hexa the hexa to encode - * @return the encoded string - */ - public String encodeHex(String hexa) { - if (!hexa.matches("^[0-9a-fA-F]+$")) { - return ""; - } + char[] newAlphabet = alphabet; + final char ret = newAlphabet[(int) (numberHashInt % newAlphabet.length)]; - final List matched = new ArrayList(); - final Matcher matcher = WORD_PATTERN.matcher(hexa); + long num; + long sepsIndex, guardIndex; + final StringBuilder buffer = new StringBuilder(); + final StringBuilder ret_strB = new StringBuilder(); + ret_strB.append(ret); + char guard; - while (matcher.find()) { - matched.add(Long.parseLong('1' + matcher.group(), 16)); - } + for (int i = 0; i < numbers.length; i++) { + num = numbers[i]; + buffer.setLength(0); + buffer.append(ret) + .append(salt) + .append(newAlphabet); - // conversion - final long[] result = new long[matched.size()]; - for (int i = 0; i < matched.size(); i++) { - result[i] = matched.get(i); - } + newAlphabet = Hashids.consistentShuffle(newAlphabet, buffer.substring(0, newAlphabet.length).toCharArray()); + final String last = Hashids.hash(num, newAlphabet); - return this.encode(result); - } + ret_strB.append(last); - /** - * Decode string to numbers - * - * @param hash the encoded string - * @return decoded 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)); + if (i + 1 < numbers.length) { + if (last.length() > 0) { + num %= last.charAt(0) + i; + sepsIndex = (int) (num % seps.length); + } else { + sepsIndex = 0; } - return result.toString(); + ret_strB.append(seps[(int) sepsIndex]); + } } - 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; - } + if (ret_strB.length() < minHashLength) { + guardIndex = (numberHashInt + (ret_strB.charAt(0))) % guards.length; + guard = guards[(int) guardIndex]; - /* Private methods */ + ret_strB.insert(0, guard); - private String _encode(long... numbers) { - long numberHashInt = 0; - for (int i = 0; i < numbers.length; i++) { - numberHashInt += (numbers[i] % (i + 100)); - } + if (ret_strB.length() < minHashLength) { + guardIndex = (numberHashInt + (ret_strB.charAt(2))) % guards.length; + guard = guards[(int) guardIndex]; - char[] newAlphabet = alphabet; - final char ret = newAlphabet[(int) (numberHashInt % newAlphabet.length)]; - - long num; - long sepsIndex, guardIndex; - 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.setLength(0); - buffer.append(ret) - .append(salt) - .append(newAlphabet); - - 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 % seps.length); - } else { - sepsIndex = 0; - } - - ret_strB.append(seps[(int) sepsIndex]); - } - } - - if (ret_strB.length() < minHashLength) { - guardIndex = (numberHashInt + (ret_strB.charAt(0))) % guards.length; - guard = guards[(int) guardIndex]; + ret_strB.append(guard); + } + } - ret_strB.insert(0, guard); + 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_strB.replace(0, ret_strB.length(), ret_strB.substring(start_pos, start_pos + minHashLength)); + } + } - if (ret_strB.length() < minHashLength) { - guardIndex = (numberHashInt + (ret_strB.charAt(2))) % guards.length; - guard = guards[(int) guardIndex]; + return ret_strB.toString(); + } - ret_strB.append(guard); - } - } + private long[] _decode(String hash, char[] alphabet) { + final List ret = new ArrayList(); - 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_strB.replace(0, ret_strB.length(), ret_strB.substring(start_pos, start_pos + minHashLength)); - } - } + int i = 0; + String hashBreakdown = hash.replaceAll(guardsRegExp, SPACE); + String[] hashArray = hashBreakdown.split(SPACE); - return ret_strB.toString(); + if ((hashArray.length == 3) || (hashArray.length == 2)) { + i = 1; } - private long[] _decode(String hash, char[] alphabet) { - final List ret = new ArrayList(); + if (hashArray.length > 0) { + hashBreakdown = hashArray[i]; + if (!hashBreakdown.isEmpty()) { + final char lottery = hashBreakdown.charAt(0); - int i = 0; - String hashBreakdown = hash.replaceAll(guardsRegExp, SPACE); - String[] hashArray = hashBreakdown.split(SPACE); + hashBreakdown = hashBreakdown.substring(1); + hashBreakdown = hashBreakdown.replaceAll(sepsRegExp, SPACE); + hashArray = hashBreakdown.split(SPACE); - 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(sepsRegExp, SPACE); - hashArray = hashBreakdown.split(SPACE); - - String subHash; - for (final String aHashArray : hashArray) { - subHash = aHashArray; - alphabet = Hashids.consistentShuffle(alphabet, concatenate(lottery, salt, alphabet, 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); + String subHash; + for (final String aHashArray : hashArray) { + subHash = aHashArray; + alphabet = Hashids.consistentShuffle(alphabet, concatenate(lottery, salt, alphabet, alphabet.length)); + ret.add(Hashids.unhash(subHash, alphabet)); } + } + } - if (!this.encode(arr).equals(hash)) { - arr = new long[0]; - } + // transform from List to long[] + long[] arr = new long[ret.size()]; + for (int k = 0; k < arr.length; k++) { + arr[k] = ret.get(k); + } - return arr; + if (!this.encode(arr).equals(hash)) { + arr = new long[0]; } - private static char[] consistentShuffle(char[] alphabet, char[] salt) { - if (salt.length <= 0) { - return alphabet.clone(); - } + return arr; + } - int asc_val, j; - 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 = result[j]; - result[j] = result[i]; - result[i] = tmp; - } + private static char[] consistentShuffle(char[] alphabet, char[] salt) { + if (salt.length <= 0) { + return alphabet.clone(); + } - return result; + int asc_val, j; + 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 = result[j]; + result[j] = result[i]; + result[i] = tmp; } - private static String hash(long input, char[] alphabet) { - final StringBuilder hash = new StringBuilder(); - final int alphabetLen = alphabet.length; + return result; + } - do { - final int index = (int) (input % alphabetLen); - if (index >= 0 && index < alphabet.length) { - hash.insert(0, alphabet[index]); - } - input /= alphabetLen; - } while (input > 0); + private static String hash(long input, char[] alphabet) { + final StringBuilder hash = new StringBuilder(); + final int alphabetLen = alphabet.length; - return hash.toString(); - } + do { + final int index = (int) (input % alphabetLen); + if (index >= 0 && index < alphabet.length) { + hash.insert(0, alphabet[index]); + } + input /= alphabetLen; + } while (input > 0); - private static Long unhash(String input, char[] alphabet) { - long number = 0, pos; + return hash.toString(); + } - for (int i = 0; i < input.length(); i++) { - pos = indexOf(alphabet, input.charAt(i)); - number = number * alphabet.length + pos; - } + private static Long unhash(String input, char[] alphabet) { + long number = 0, pos; - return number; + for (int i = 0; i < input.length(); i++) { + pos = indexOf(alphabet, input.charAt(i)); + number = number * alphabet.length + pos; } - /** - * Get Hashid algorithm version. - * - * @return Hashids algorithm version implemented. - */ - public String getVersion() { - return "1.0.0"; - } + return number; + } + + /** + * Get Hashid algorithm version. + * + * @return Hashids algorithm version implemented. + */ + public String getVersion() { + return "1.0.0"; + } } diff --git a/src/test/java/org/hashids/HashidsTest.java b/src/test/java/org/hashids/HashidsTest.java index 3e3421c..0f9d86f 100644 --- a/src/test/java/org/hashids/HashidsTest.java +++ b/src/test/java/org/hashids/HashidsTest.java @@ -1,203 +1,201 @@ package org.hashids; -import org.junit.Assert; -import org.junit.Test; - import java.util.Arrays; -import static org.junit.Assert.assertEquals; +import org.junit.Assert; +import org.junit.Test; public class HashidsTest { - @Test - public void test_large_number() { - final long num_to_hash = 9007199254740992L; - final Hashids a = new Hashids("this is my salt"); - final String res = a.encode(num_to_hash); - final long[] b = a.decode(res); - assertEquals(num_to_hash, b[0]); - } - - @Test(expected = IllegalArgumentException.class) - public void test_large_number_not_supported() throws Exception { - final long num_to_hash = 9007199254740993L; - final Hashids a = new Hashids("this is my salt"); - a.encode(num_to_hash); - } - - @Test - public void test_wrong_decoding() { - final Hashids a = new Hashids("this is my pepper"); - final long[] b = a.decode("NkK9"); - assertEquals(b.length, 0); - } - - @Test - public void test_one_number() { - final String expected = "NkK9"; - String res; - final long num_to_hash = 12345L; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, 1); - assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_serveral_numbers() { - final String expected = "aBMswoO2UB3Sj"; - String res; - final long[] num_to_hash = {683L, 94108L, 123L, 5L}; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_specifying_custom_hash_alphabet() { - final String expected = "b332db5"; - String res; - final long num_to_hash = 1234567L; - long[] res2; - final Hashids a = new Hashids("this is my salt", 0, "0123456789abcdef"); - res = a.encode(num_to_hash); - assertEquals(expected, res); - res2 = a.decode(expected); - assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_specifying_custom_hash_length() { - final String expected = "gB0NV05e"; - String res; - final long num_to_hash = 1L; - long[] res2; - final Hashids a = new Hashids("this is my salt", 8); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, 1); - assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_randomness() { - final String expected = "1Wc8cwcE"; - String res; - final long[] num_to_hash = {5L, 5L, 5L, 5L}; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_randomness_for_incrementing_numbers() { - final String expected = "kRHnurhptKcjIDTWC3sx"; - String res; - final long[] num_to_hash = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L}; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, num_to_hash.length); - Assert.assertTrue(Arrays.equals(res2, num_to_hash)); - } - - @Test - public void test_randomness_for_incrementing() { - Hashids a; - a = new Hashids("this is my salt"); - assertEquals(a.encode(1L), "NV"); - assertEquals(a.encode(2L), "6m"); - assertEquals(a.encode(3L), "yD"); - assertEquals(a.encode(4L), "2l"); - assertEquals(a.encode(5L), "rD"); - } - - @Test - public void test_for_vlues_greater_int_maxval() { - final Hashids a = new Hashids("this is my salt"); - assertEquals(a.encode(9876543210123L), "Y8r7W1kNN"); - } - - @Test - public void test_issue10() { - final String expected = "3kK3nNOe"; - String res; - final long num_to_hash = 75527867232l; - long[] res2; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(res, expected); - res2 = a.decode(expected); - assertEquals(res2.length, 1); - assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_issue23() { - final String expected = "9Q7MJ3LVGW"; - String res; - final long num_to_hash = 1145L; - long[] res2; - final Hashids a = new Hashids("MyCamelCaseSalt", 10, "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"); - res = a.encode(num_to_hash); - assertEquals(expected, res); - res2 = a.decode(expected); - assertEquals(res2[0], num_to_hash); - } - - @Test - public void test_issue30() { - final String expected = ""; - String res; - final long num_to_hash = -1L; - final Hashids a = new Hashids("this is my salt"); - res = a.encode(num_to_hash); - assertEquals(expected, res); - } - - @Test - public void test_issue31() { - final long[] numbers = new long[500000]; - long current = Hashids.MAX_NUMBER; - for (int i = 0; i < numbers.length; i++) { - numbers[i] = current--; - } - final Hashids a = new Hashids("this is my salt"); - Assert.assertNotEquals("", a.encode(numbers)); - } - - @Test - public void test_issue32() throws Exception { - final long num_to_hash = -1; - final Hashids a = new Hashids("this is my salt"); - assertEquals("", a.encode(num_to_hash)); - } - - @Test - public void test_issue45() throws Exception { - Hashids Hashids = new Hashids("this is my salt"); - long[] numbers = Hashids.decode("()"); - assertEquals(numbers.length, 0); - numbers = Hashids.decode("[]"); - assertEquals(numbers.length, 0); - numbers = Hashids.decode("недействительный"); - assertEquals(numbers.length, 0); - numbers = Hashids.decode("無效"); - assertEquals(numbers.length, 0); - } + @Test + public void test_large_number() { + final long num_to_hash = 9007199254740992L; + final Hashids a = new Hashids("this is my salt"); + final String res = a.encode(num_to_hash); + final long[] b = a.decode(res); + Assert.assertEquals(num_to_hash, b[0]); + } + + @Test(expected = IllegalArgumentException.class) + public void test_large_number_not_supported() throws Exception { + final long num_to_hash = 9007199254740993L; + final Hashids a = new Hashids("this is my salt"); + a.encode(num_to_hash); + } + + @Test + public void test_wrong_decoding() { + final Hashids a = new Hashids("this is my pepper"); + final long[] b = a.decode("NkK9"); + Assert.assertEquals(b.length, 0); + } + + @Test + public void test_one_number() { + final String expected = "NkK9"; + String res; + final long num_to_hash = 12345L; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, 1); + Assert.assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_serveral_numbers() { + final String expected = "aBMswoO2UB3Sj"; + String res; + final long[] num_to_hash = { 683L, 94108L, 123L, 5L }; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_specifying_custom_hash_alphabet() { + final String expected = "b332db5"; + String res; + final long num_to_hash = 1234567L; + long[] res2; + final Hashids a = new Hashids("this is my salt", 0, "0123456789abcdef"); + res = a.encode(num_to_hash); + Assert.assertEquals(expected, res); + res2 = a.decode(expected); + Assert.assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_specifying_custom_hash_length() { + final String expected = "gB0NV05e"; + String res; + final long num_to_hash = 1L; + long[] res2; + final Hashids a = new Hashids("this is my salt", 8); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, 1); + Assert.assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_randomness() { + final String expected = "1Wc8cwcE"; + String res; + final long[] num_to_hash = { 5L, 5L, 5L, 5L }; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_randomness_for_incrementing_numbers() { + final String expected = "kRHnurhptKcjIDTWC3sx"; + String res; + final long[] num_to_hash = { 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L }; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, num_to_hash.length); + Assert.assertTrue(Arrays.equals(res2, num_to_hash)); + } + + @Test + public void test_randomness_for_incrementing() { + Hashids a; + a = new Hashids("this is my salt"); + Assert.assertEquals(a.encode(1L), "NV"); + Assert.assertEquals(a.encode(2L), "6m"); + Assert.assertEquals(a.encode(3L), "yD"); + Assert.assertEquals(a.encode(4L), "2l"); + Assert.assertEquals(a.encode(5L), "rD"); + } + + @Test + public void test_for_vlues_greater_int_maxval() { + final Hashids a = new Hashids("this is my salt"); + Assert.assertEquals(a.encode(9876543210123L), "Y8r7W1kNN"); + } + + @Test + public void test_issue10() { + final String expected = "3kK3nNOe"; + String res; + final long num_to_hash = 75527867232l; + long[] res2; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(res, expected); + res2 = a.decode(expected); + Assert.assertEquals(res2.length, 1); + Assert.assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_issue23() { + final String expected = "9Q7MJ3LVGW"; + String res; + final long num_to_hash = 1145L; + long[] res2; + final Hashids a = new Hashids("MyCamelCaseSalt", 10, "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"); + res = a.encode(num_to_hash); + Assert.assertEquals(expected, res); + res2 = a.decode(expected); + Assert.assertEquals(res2[0], num_to_hash); + } + + @Test + public void test_issue30() { + final String expected = ""; + String res; + final long num_to_hash = -1L; + final Hashids a = new Hashids("this is my salt"); + res = a.encode(num_to_hash); + Assert.assertEquals(expected, res); + } + + @Test + public void test_issue31() { + final long[] numbers = new long[500000]; + long current = Hashids.MAX_NUMBER; + for (int i = 0; i < numbers.length; i++) { + numbers[i] = current--; + } + final Hashids a = new Hashids("this is my salt"); + Assert.assertNotEquals("", a.encode(numbers)); + } + + @Test + public void test_issue32() throws Exception { + final long num_to_hash = -1; + final Hashids a = new Hashids("this is my salt"); + Assert.assertEquals("", a.encode(num_to_hash)); + } + + @Test + public void test_issue45() throws Exception { + Hashids hashids = new Hashids("this is my salt"); + long[] numbers = hashids.decode("()"); + Assert.assertEquals(numbers.length, 0); + numbers = hashids.decode("[]"); + Assert.assertEquals(numbers.length, 0); + numbers = hashids.decode("недействительный"); + Assert.assertEquals(numbers.length, 0); + numbers = hashids.decode("無效"); + Assert.assertEquals(numbers.length, 0); + } } From 7d4e3ca031202b70e8b08a226c5d74d267710af1 Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Thu, 22 Feb 2018 16:05:38 +0200 Subject: [PATCH 03/11] - more usage of System.arraycopy - used array of primitives instead of Objects (see List ret) --- src/main/java/org/hashids/CharUtils.java | 62 +++--------------------- src/main/java/org/hashids/Hashids.java | 31 ++++++------ 2 files changed, 23 insertions(+), 70 deletions(-) diff --git a/src/main/java/org/hashids/CharUtils.java b/src/main/java/org/hashids/CharUtils.java index f2afdc4..1da31ec 100644 --- a/src/main/java/org/hashids/CharUtils.java +++ b/src/main/java/org/hashids/CharUtils.java @@ -1,5 +1,6 @@ package org.hashids; +import static java.lang.System.arraycopy; import static java.util.Arrays.copyOfRange; public final class CharUtils { @@ -8,69 +9,22 @@ private CharUtils() { throw new UnsupportedOperationException(); } - public static char[] concatenate(char a, char[] arrB, char[] arrC, int maxSize) { - if (maxSize == 0) { - return new char[0]; - } - - final char[] result = new char[maxSize]; - int i = 0; - - result[i++] = a; - - if (i == maxSize) { - return result; - } - - for (final char c : arrB) { - result[i++] = c; - - if (i == maxSize) { - return result; - } - } - - for (final char c : arrC) { - result[i++] = c; - - if (i == maxSize) { - return result; - } - } - - return result; - } - public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { final char[] result = new char[arrA.length + arrB.length + arrC.length]; - int i = 0; - - for (final char c : arrA) { - result[i++] = c; - } - for (final char c : arrB) { - result[i++] = c; - } - - for (final char c : arrC) { - result[i++] = c; - } + 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; } public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { - final char[] result = new char[arrA.length + bTo - bFrom]; - int i = 0; + final int bCopyLength = bTo - bFrom; + final char[] result = new char[arrA.length + bCopyLength]; - for (final char c : arrA) { - result[i++] = c; - } - - for (int j = bFrom; j < bTo; j++) { - result[i++] = arrB[j]; - } + arraycopy(arrA, 0, result, 0, arrA.length); + arraycopy(arrB, bFrom, result, arrA.length, bCopyLength); return result; } diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index 34ac292..ddb5d16 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -309,20 +309,21 @@ private String _encode(long... numbers) { } private long[] _decode(String hash, char[] alphabet) { - final List ret = new ArrayList(); + long[] arr = new long[hash.length()]; + int retIdx = 0; int i = 0; 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(sepsRegExp, SPACE); @@ -331,23 +332,21 @@ private long[] _decode(String hash, char[] alphabet) { String subHash; for (final String aHashArray : hashArray) { subHash = aHashArray; - alphabet = Hashids.consistentShuffle(alphabet, concatenate(lottery, salt, alphabet, alphabet.length)); - ret.add(Hashids.unhash(subHash, alphabet)); + alphabet = Hashids.consistentShuffle( + alphabet, + copyOfRange(concatenate(lottery, salt, alphabet), 0, 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); - } - - if (!this.encode(arr).equals(hash)) { - arr = new long[0]; - } + // TODO remove this comment + // we don't need this check (it can be done explicitly by the requester) + // if (!encode(arr).equals(hash)) { + // arr = new long[0]; + // } - return arr; + return copyOf(arr, retIdx); } private static char[] consistentShuffle(char[] alphabet, char[] salt) { @@ -386,7 +385,7 @@ private static String hash(long input, char[] alphabet) { return hash.toString(); } - private static Long unhash(String input, char[] alphabet) { + private static long unhash(String input, char[] alphabet) { long number = 0, pos; for (int i = 0; i < input.length(); i++) { From b1a1a4c6478199ef2f8ad955200d45dc7e69968c Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Thu, 22 Feb 2018 16:11:30 +0200 Subject: [PATCH 04/11] used copyOf instead of copyOfRange --- src/main/java/org/hashids/Hashids.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index ddb5d16..3d2d095 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -334,7 +334,7 @@ private long[] _decode(String hash, char[] alphabet) { subHash = aHashArray; alphabet = Hashids.consistentShuffle( alphabet, - copyOfRange(concatenate(lottery, salt, alphabet), 0, alphabet.length)); + copyOf(concatenate(lottery, salt, alphabet), alphabet.length)); arr[retIdx++] = Hashids.unhash(subHash, alphabet); } } From 84af659082912d4bc6f9dc2411a29a2c64603d4d Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Thu, 22 Feb 2018 16:42:32 +0200 Subject: [PATCH 05/11] revert --- src/main/java/org/hashids/Hashids.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index 3d2d095..0fe7a2e 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -340,13 +340,13 @@ private long[] _decode(String hash, char[] alphabet) { } } - // TODO remove this comment - // we don't need this check (it can be done explicitly by the requester) - // if (!encode(arr).equals(hash)) { - // arr = new long[0]; - // } + arr = copyOf(arr, retIdx); - return copyOf(arr, retIdx); + if (!encode(arr).equals(hash)) { + return new long[0]; + } + + return arr; } private static char[] consistentShuffle(char[] alphabet, char[] salt) { From 9d1e7b3b281c56f57dd87d58287e215a34efe0f6 Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Fri, 23 Feb 2018 14:16:20 +0200 Subject: [PATCH 06/11] fixed returned version --- src/main/java/org/hashids/Hashids.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index 0fe7a2e..b676a53 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -402,6 +402,6 @@ private static long unhash(String input, char[] alphabet) { * @return Hashids algorithm version implemented. */ public String getVersion() { - return "1.0.0"; + return "1.0.4-SNAPSHOT"; } } From 803995dd76c2633f16c18bfdf0be67339dae1621 Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Sat, 24 Feb 2018 13:29:51 +0200 Subject: [PATCH 07/11] used copyOf instead of copyOfRange --- src/main/java/org/hashids/CharUtils.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/hashids/CharUtils.java b/src/main/java/org/hashids/CharUtils.java index 1da31ec..f31b926 100644 --- a/src/main/java/org/hashids/CharUtils.java +++ b/src/main/java/org/hashids/CharUtils.java @@ -1,7 +1,7 @@ package org.hashids; import static java.lang.System.arraycopy; -import static java.util.Arrays.copyOfRange; +import static java.util.Arrays.copyOf; public final class CharUtils { @@ -59,7 +59,7 @@ public static char[] cleanup(char[] source, char[] allowedChars) { } } - return copyOfRange(result, 0, i); + return copyOf(result, i); } public static char[] removeAll(char[] source, char[] charsToRemove) { @@ -86,7 +86,7 @@ public static char[] removeAll(char[] source, char[] charsToRemove) { } } - return copyOfRange(result, 0, i); + return copyOf(result, i); } public static boolean validate(char[] source, char[] allowedChars) { From 0585d3efd20728f68841b640e74e23246154737f Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 28 Feb 2018 19:05:50 +0200 Subject: [PATCH 08/11] fixed the check for finding the duplicates in the alphabet --- pom.xml | 14 +++++++++++++- src/main/java/org/hashids/Hashids.java | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 15f94ee..0ef1a7b 100644 --- a/pom.xml +++ b/pom.xml @@ -190,6 +190,18 @@ 4.12 test + + org.openjdk.jmh + jmh-core + 1.9 + test + + + org.openjdk.jmh + jmh-generator-annprocess + 1.9 + test + @@ -208,7 +220,7 @@ https://github.com/TheConstructor - Mihai CAZACU + Mihai Cazacu https://github.com/cazacugmihai diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index b676a53..ef03775 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -133,8 +133,10 @@ private void validateAlphabet(char[] alphabet) { throw new IllegalArgumentException("The alphabet cannot contain spaces."); } - if ((i + 1 < alphabet.length) && (alphabet[i] == alphabet[i + 1])) { - throw new IllegalArgumentException("The alphabet cannot contain duplicates."); + for (int j = i + 1; j < alphabet.length; j++) { + if (alphabet[i] == alphabet[j]) { + throw new IllegalArgumentException("The alphabet cannot contain duplicates."); + } } } } From 38439b9dd6f3c1bae896a8ed5a96ca80cf799dc6 Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 28 Feb 2018 20:36:44 +0200 Subject: [PATCH 09/11] cleaned up the pom.xmml --- pom.xml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pom.xml b/pom.xml index 0ef1a7b..7027ab1 100644 --- a/pom.xml +++ b/pom.xml @@ -190,18 +190,6 @@ 4.12 test - - org.openjdk.jmh - jmh-core - 1.9 - test - - - org.openjdk.jmh - jmh-generator-annprocess - 1.9 - test - From d09599933ec077df7d8fb866460475f53440362e Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 28 Feb 2018 20:54:52 +0200 Subject: [PATCH 10/11] removed assertions --- src/main/java/org/hashids/Hashids.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index ef03775..4cc9de9 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -68,9 +68,17 @@ public Hashids(String salt, int minHashLength, String alphabet) { } private Hashids(char[] salt, int minHashLength, char[] alphabet) { - assert salt != null; - assert minHashLength >= 0; - assert alphabet != null; + if (salt == null) { + throw new IllegalArgumentException("The salt cannot be null,"); + } + + if (minHashLength <= 0) { + throw new IllegalArgumentException("Minimum hash length must be greater than zero,"); + } + + if (alphabet == null) { + throw new IllegalArgumentException("The alphabet cannot be null,"); + } this.salt = salt; this.minHashLength = minHashLength; From 9d7901cc72b64f48ea6353d083079ef98f107dce Mon Sep 17 00:00:00 2001 From: Mihai CAZACU Date: Wed, 28 Feb 2018 21:00:13 +0200 Subject: [PATCH 11/11] CharUtils has now a package scope --- src/main/java/org/hashids/CharUtils.java | 15 ++++++++------- src/main/java/org/hashids/Hashids.java | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/hashids/CharUtils.java b/src/main/java/org/hashids/CharUtils.java index f31b926..3685b24 100644 --- a/src/main/java/org/hashids/CharUtils.java +++ b/src/main/java/org/hashids/CharUtils.java @@ -3,13 +3,13 @@ import static java.lang.System.arraycopy; import static java.util.Arrays.copyOf; -public final class CharUtils { +final class CharUtils { private CharUtils() { throw new UnsupportedOperationException(); } - public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { + 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); @@ -19,7 +19,7 @@ public static char[] concatenate(char[] arrA, char[] arrB, char[] arrC) { return result; } - public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { + static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { final int bCopyLength = bTo - bFrom; final char[] result = new char[arrA.length + bCopyLength]; @@ -29,7 +29,7 @@ public static char[] concatenate(char[] arrA, char[] arrB, int bFrom, int bTo) { return result; } - public static int indexOf(char[] source, char c) { + static int indexOf(char[] source, char c) { int i = 0; for (final char s : source) { @@ -42,7 +42,7 @@ public static int indexOf(char[] source, char c) { return i; } - public static char[] cleanup(char[] source, char[] allowedChars) { + static char[] cleanup(char[] source, char[] allowedChars) { if ((source == null) || (allowedChars == null)) { return source; } @@ -62,7 +62,7 @@ public static char[] cleanup(char[] source, char[] allowedChars) { return copyOf(result, i); } - public static char[] removeAll(char[] source, char[] charsToRemove) { + static char[] removeAll(char[] source, char[] charsToRemove) { if ((source == null) || (charsToRemove == null)) { return source; } @@ -89,7 +89,7 @@ public static char[] removeAll(char[] source, char[] charsToRemove) { return copyOf(result, i); } - public static boolean validate(char[] source, char[] allowedChars) { + static boolean validate(char[] source, char[] allowedChars) { boolean found; for (final char s : source) { @@ -109,4 +109,5 @@ public static boolean validate(char[] source, char[] allowedChars) { return true; } + } diff --git a/src/main/java/org/hashids/Hashids.java b/src/main/java/org/hashids/Hashids.java index 4cc9de9..4257790 100644 --- a/src/main/java/org/hashids/Hashids.java +++ b/src/main/java/org/hashids/Hashids.java @@ -72,8 +72,8 @@ private Hashids(char[] salt, int minHashLength, char[] alphabet) { throw new IllegalArgumentException("The salt cannot be null,"); } - if (minHashLength <= 0) { - throw new IllegalArgumentException("Minimum hash length must be greater than zero,"); + if (minHashLength < 0) { + throw new IllegalArgumentException("The minimum hash length must be positive,"); } if (alphabet == null) {