Skip to content
138 changes: 77 additions & 61 deletions src/Hashids.net/Hashids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.ObjectPool;
using System.Buffers;

namespace HashidsNet
{
Expand All @@ -19,7 +20,10 @@ public partial class Hashids : IHashids
private const double SEP_DIV = 3.5;
private const double GUARD_DIV = 12.0;

private const int MaxNumberHashLength = 12; // Length of long.MaxValue;

private char[] _alphabet;
private long _alphabetLength;
private char[] _seps;
private char[] _guards;
private char[] _salt;
Expand Down Expand Up @@ -73,6 +77,8 @@ public Hashids(

SetupSeps();
SetupGuards();

_alphabetLength = _alphabet.Length;
}

private void SetupSeps()
Expand All @@ -85,9 +91,9 @@ private void SetupSeps()

ConsistentShuffle(_seps, _seps.Length, _salt, _salt.Length);

if (_seps.Length == 0 || ((float) _alphabet.Length / _seps.Length) > SEP_DIV)
if (_seps.Length == 0 || ((float)_alphabet.Length / _seps.Length) > SEP_DIV)
{
var sepsLength = (int) Math.Ceiling((float) _alphabet.Length / SEP_DIV);
var sepsLength = (int)Math.Ceiling((float)_alphabet.Length / SEP_DIV);

if (sepsLength == 1)
{
Expand All @@ -111,7 +117,7 @@ private void SetupSeps()

private void SetupGuards()
{
var guardCount = (int) Math.Ceiling(_alphabet.Length / GUARD_DIV);
var guardCount = (int)Math.Ceiling(_alphabet.Length / GUARD_DIV);

if (_alphabet.Length < 3)
{
Expand All @@ -131,7 +137,7 @@ private void SetupGuards()
/// </summary>
/// <param name="numbers">List of integers.</param>
/// <returns>Encoded hash string.</returns>
public virtual string Encode(params int[] numbers) => GenerateHashFrom(Array.ConvertAll(numbers, n => (long) n));
public virtual string Encode(params int[] numbers) => GenerateHashFrom(Array.ConvertAll(numbers, n => (long)n));

/// <summary>
/// Encodes the provided numbers into a hash string.
Expand Down Expand Up @@ -160,7 +166,7 @@ private void SetupGuards()
/// <param name="hash">Hash string to decode.</param>
/// <returns>Array of integers.</returns>
/// <exception cref="T:System.OverflowException">If the decoded number overflows integer.</exception>
public virtual int[] Decode(string hash) => Array.ConvertAll(GetNumbersFrom(hash), n => (int) n);
public virtual int[] Decode(string hash) => Array.ConvertAll(GetNumbersFrom(hash), n => (int)n);

/// <summary>
/// Decodes the provided hash into numbers.
Expand Down Expand Up @@ -202,8 +208,14 @@ public virtual string DecodeHex(string hash)
var numbers = DecodeLong(hash);

foreach (var number in numbers)
foreach (var ch in number.ToString("X").AsSpan().Slice(1))
builder.Append(ch);
{
var s = number.ToString("X");

for (var i = 1; i < s.Length; i++)
{
builder.Append(s[i]);
}
}

var result = builder.ToString();
_sbPool.Return(builder);
Expand All @@ -223,13 +235,14 @@ private string GenerateHashFrom(long[] numbers)

var builder = _sbPool.Get();

char[] buffer = null;
char[] shuffleBuffer = null;
var alphabet = _alphabet.CopyPooled();
var hashBuffer = ArrayPool<char>.Shared.Rent(MaxNumberHashLength);
try
{
var lottery = alphabet[numbersHashInt % _alphabet.Length];
var lottery = alphabet[numbersHashInt % _alphabetLength];
builder.Append(lottery);
buffer = CreatePooledBuffer(_alphabet.Length, lottery);
shuffleBuffer = CreatePooledBuffer(_alphabet.Length, lottery);

var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;
Expand All @@ -240,17 +253,20 @@ private string GenerateHashFrom(long[] numbers)

if (length > 0)
{
Array.Copy(alphabet, 0, buffer, startIndex, length);
Array.Copy(alphabet, 0, shuffleBuffer, startIndex, length);
}

ConsistentShuffle(alphabet, _alphabet.Length, shuffleBuffer, _alphabet.Length);
var hashLength = BuildReversedHash(number, alphabet, hashBuffer);

ConsistentShuffle(alphabet, _alphabet.Length, buffer, _alphabet.Length);
var last = Hash(number, alphabet, _alphabet.Length);

builder.Append(last);
for (var j = hashLength - 1; j > -1; j--)
{
builder.Append(hashBuffer[j]);
}

if (i + 1 < numbers.Length)
{
number %= last[0] + i;
number %= hashBuffer[hashLength - 1] + i;
var sepsIndex = number % _seps.Length;

builder.Append(_seps[sepsIndex]);
Expand All @@ -277,8 +293,8 @@ private string GenerateHashFrom(long[] numbers)

while (builder.Length < _minHashLength)
{
Array.Copy(alphabet, buffer, _alphabet.Length);
ConsistentShuffle(alphabet, _alphabet.Length, buffer, _alphabet.Length);
Array.Copy(alphabet, shuffleBuffer, _alphabet.Length);
ConsistentShuffle(alphabet, _alphabet.Length, shuffleBuffer, _alphabet.Length);
builder.Insert(0, alphabet, halfLength, _alphabet.Length - halfLength);
builder.Append(alphabet, 0, halfLength);

Expand All @@ -293,36 +309,35 @@ private string GenerateHashFrom(long[] numbers)
finally
{
alphabet.ReturnToPool();
buffer.ReturnToPool();
shuffleBuffer.ReturnToPool();
hashBuffer.ReturnToPool();
}

var result = builder.ToString();
_sbPool.Return(builder);
return result;
}

private char[] Hash(long input, char[] alphabet, int alphabetLength)
private int BuildReversedHash(long input, char[] alphabet, char[] hashBuffer)
{
var hash = new List<char>(4);

var length = 0;
do
{
hash.Add(alphabet[input % alphabetLength]);
input /= alphabetLength;
hashBuffer[length++] = alphabet[input % _alphabetLength];
input /= _alphabetLength;
} while (input > 0);

hash.Reverse();
return hash.ToArray();
return length;
}

private long Unhash(string input, char[] alphabet, int alphabetLength)
private long Unhash(string input, char[] alphabet)
{
long number = 0;

for (var i = 0; i < input.Length; i++)
{
var pos = Array.IndexOf(alphabet, input[i]);
number = number * alphabetLength + pos;
number = number * _alphabetLength + pos;
}

return number;
Expand All @@ -343,55 +358,56 @@ private long[] GetNumbersFrom(string hash)
i = 1;
}

var result = new List<long>();
var hashBreakdown = hashArray[i];
if (hashBreakdown[0] != default(char))
{
var lottery = hashBreakdown[0];
hashBreakdown = hashBreakdown.Substring(1);
var lottery = hashBreakdown[0];

hashArray = hashBreakdown.Split(_seps, StringSplitOptions.RemoveEmptyEntries);
if (lottery == default(char))
return Array.Empty<long>();

hashBreakdown = hashBreakdown.Substring(1);

char[] buffer = null;
var alphabet = _alphabet.CopyPooled();
try
{
buffer = CreatePooledBuffer(_alphabet.Length, lottery);
hashArray = hashBreakdown.Split(_seps, StringSplitOptions.RemoveEmptyEntries);

var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;
var result = new long[hashArray.Length];
char[] buffer = null;
var alphabet = _alphabet.CopyPooled();
try
{
buffer = CreatePooledBuffer(_alphabet.Length, lottery);

for (var j = 0; j < hashArray.Length; j++)
{
var subHash = hashArray[j];
var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;

if (length > 0)
{
Array.Copy(alphabet, 0, buffer, startIndex, length);
}
for (var j = 0; j < hashArray.Length; j++)
{
var subHash = hashArray[j];

ConsistentShuffle(alphabet, _alphabet.Length, buffer, _alphabet.Length);
result.Add(Unhash(subHash, alphabet, _alphabet.Length));
if (length > 0)
{
Array.Copy(alphabet, 0, buffer, startIndex, length);
}
}
finally
{
alphabet.ReturnToPool();
buffer.ReturnToPool();
}

if (EncodeLong(result.ToArray()) != hash)
{
result.Clear();
ConsistentShuffle(alphabet, _alphabet.Length, buffer, _alphabet.Length);
result[j] = Unhash(subHash, alphabet);
}
}
finally
{
alphabet.ReturnToPool();
buffer.ReturnToPool();
}

if (EncodeLong(result) == hash)
{
return result;
}

return result.ToArray();
return Array.Empty<long>();
}

private char[] CreatePooledBuffer(int alphabetLength, char lottery)
{
var buffer = System.Buffers.ArrayPool<char>.Shared.Rent(alphabetLength);
var buffer = ArrayPool<char>.Shared.Rent(alphabetLength);
buffer[0] = lottery;
Array.Copy(_salt, 0, buffer, 1, Math.Min(_salt.Length, alphabetLength - 1));
return buffer;
Expand Down