Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 0 additions & 23 deletions src/Hashids.net/ArrayExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace HashidsNet
{
Expand Down Expand Up @@ -35,26 +32,6 @@ public static T[] Append<T>(this T[] array, T[] appendArray, int index, int leng
return newArray;
}

public static T[] CopyPooled<T>(this T[] array)
{
return SubArrayPooled(array, 0, array.Length);
}

public static T[] SubArrayPooled<T>(this T[] array, int index, int length)
{
var subarray = ArrayPool<T>.Shared.Rent(length);
Array.Copy(array, index, subarray, 0, length);
return subarray;
}

public static void ReturnToPool<T>(this T[] array)
{
if (array == null)
return;

ArrayPool<T>.Shared.Return(array);
}

#if NETCOREAPP3_1_OR_GREATER
/// <remarks>This method exists because <see cref="ReadOnlySpan{T}"/> does not implement <see cref="IEnumerable{T}"/> and using <c>.AsEnumerable()</c> will cause boxing.</remarks>
public static bool Any<T>(this ReadOnlySpan<T> span, Func<T,bool> predicate)
Expand Down
207 changes: 95 additions & 112 deletions src/Hashids.net/Hashids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Buffers;

namespace HashidsNet
{
Expand Down Expand Up @@ -99,7 +98,7 @@ private static void InitCharArrays(string alphabet, string seps, ReadOnlySpan<ch
throw new ArgumentException($"Alphabet must contain at least {MIN_ALPHABET_LENGTH:N0} unique characters that are also not present in .", paramName: nameof(alphabet));
}

ConsistentShuffle(alphabet: sepChars, alphabetLength: sepChars.Length, salt: salt, saltLength: salt.Length);
ConsistentShuffle(alphabet: sepChars, salt: salt);

if (sepChars.Length == 0 || ((float)alphabetChars.Length / sepChars.Length) > SEP_DIV)
{
Expand All @@ -122,7 +121,7 @@ private static void InitCharArrays(string alphabet, string seps, ReadOnlySpan<ch
}
}

ConsistentShuffle(alphabet: alphabetChars, alphabetChars.Length, salt: salt, salt.Length);
ConsistentShuffle(alphabet: alphabetChars, salt: salt);

// SetupGuards():

Expand Down Expand Up @@ -231,11 +230,9 @@ public bool TryDecodeSingleLong(string hash, out long id)
id = numbers[0];
return true;
}
else
{
id = 0L;
return false;
}

id = 0L;
return false;
}

/// <inheritdoc />
Expand All @@ -262,11 +259,9 @@ public virtual bool TryDecodeSingle(string hash, out int id)
id = (int)numbers[0];
return true;
}
else
{
id = 0;
return false;
}

id = 0;
return false;
}

/// <summary>
Expand Down Expand Up @@ -321,11 +316,11 @@ public virtual string DecodeHex(string hash)

private string GenerateHashFrom(ReadOnlySpan<long> numbers)
{
if(numbers.Length == 0)
if (numbers.Length == 0)
return string.Empty;

foreach (var item in numbers)
if(item < 0)
if (item < 0)
return string.Empty;

long numbersHashInt = 0;
Expand All @@ -334,92 +329,94 @@ private string GenerateHashFrom(ReadOnlySpan<long> numbers)
numbersHashInt += numbers[i] % (i + 100);
}

var builder = _sbPool.Get();

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

var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;
Span<char> alphabet = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
_alphabet.CopyTo(alphabet);

for (var i = 0; i < numbers.Length; i++)
{
var number = numbers[i];
var lottery = alphabet[(int)(numbersHashInt % _alphabet.Length)];
stringBuilder.Append(lottery);

if (length > 0)
{
Array.Copy(alphabet, 0, shuffleBuffer, startIndex, length);
}
Span<char> shuffleBuffer = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
shuffleBuffer[0] = lottery;
_salt.AsSpan().Slice(0, Math.Min(_salt.Length, _alphabet.Length - 1)).CopyTo(shuffleBuffer.Slice(1));

ConsistentShuffle(alphabet, _alphabet.Length, shuffleBuffer, _alphabet.Length);
var hashLength = BuildReversedHash(number, alphabet, hashBuffer);
var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;

for (var j = hashLength - 1; j > -1; j--)
{
builder.Append(hashBuffer[j]);
}
Span<char> hashBuffer = stackalloc char[MaxNumberHashLength];

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

builder.Append(_seps[sepsIndex]);
}
if (length > 0)
{
alphabet.Slice(0, length).CopyTo(shuffleBuffer.Slice(startIndex));
}

if (builder.Length < _minHashLength)
{
var guardIndex = (numbersHashInt + builder[0]) % _guards.Length;
var guard = _guards[guardIndex];
ConsistentShuffle(alphabet, shuffleBuffer);
var hashLength = BuildReversedHash(number, alphabet, hashBuffer);

builder.Insert(0, guard);
for (var j = hashLength - 1; j > -1; j--)
{
stringBuilder.Append(hashBuffer[j]);
}

if (builder.Length < _minHashLength)
{
guardIndex = (numbersHashInt + builder[2]) % _guards.Length;
guard = _guards[guardIndex];
if (i + 1 < numbers.Length)
{
number %= hashBuffer[hashLength - 1] + i;
var sepsIndex = number % _seps.Length;

builder.Append(guard);
}
stringBuilder.Append(_seps[sepsIndex]);
}
}

if (stringBuilder.Length < _minHashLength)
{
var guardIndex = (numbersHashInt + stringBuilder[0]) % _guards.Length;
var guard = _guards[guardIndex];

var halfLength = _alphabet.Length / 2;
stringBuilder.Insert(0, guard);

while (builder.Length < _minHashLength)
if (stringBuilder.Length < _minHashLength)
{
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);

var excess = builder.Length - _minHashLength;
if (excess > 0)
{
builder.Remove(0, excess / 2);
builder.Remove(_minHashLength, builder.Length - _minHashLength);
}
guardIndex = (numbersHashInt + stringBuilder[2]) % _guards.Length;
guard = _guards[guardIndex];

stringBuilder.Append(guard);
}
}
finally

var halfLength = _alphabet.Length / 2;

while (stringBuilder.Length < _minHashLength)
{
alphabet.ReturnToPool();
shuffleBuffer.ReturnToPool();
hashBuffer.ReturnToPool();
alphabet.CopyTo(shuffleBuffer);
ConsistentShuffle(alphabet, shuffleBuffer);

#if NETSTANDARD2_0
stringBuilder.Insert(0, alphabet.Slice(halfLength, _alphabet.Length - halfLength).ToArray());
stringBuilder.Append(alphabet.Slice(0, halfLength).ToArray());
#else
stringBuilder.Insert(0, alphabet[halfLength.._alphabet.Length]);
stringBuilder.Append(alphabet[..halfLength]);
#endif

var excess = stringBuilder.Length - _minHashLength;
if (excess > 0)
{
stringBuilder.Remove(0, excess / 2);
stringBuilder.Remove(_minHashLength, stringBuilder.Length - _minHashLength);
}
}

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

private int BuildReversedHash(long input, ReadOnlySpan<char> alphabet, char[] hashBuffer)
private int BuildReversedHash(long input, ReadOnlySpan<char> alphabet, Span<char> hashBuffer)
{
var length = 0;
do
Expand Down Expand Up @@ -469,32 +466,28 @@ private long[] GetNumbersFrom(string hash)
hashArray = hashBreakdown.Split(_seps, StringSplitOptions.RemoveEmptyEntries);

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

var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;
Span<char> alphabet = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
_alphabet.CopyTo(alphabet);

for (var j = 0; j < hashArray.Length; j++)
{
var subHash = hashArray[j];
Span<char> buffer = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
buffer[0] = lottery;
_salt.AsSpan().Slice(0, Math.Min(_salt.Length, _alphabet.Length - 1)).CopyTo(buffer.Slice(1));

if (length > 0)
{
Array.Copy(alphabet, 0, buffer, startIndex, length);
}
var startIndex = 1 + _salt.Length;
var length = _alphabet.Length - startIndex;

ConsistentShuffle(alphabet, _alphabet.Length, buffer, _alphabet.Length);
result[j] = Unhash(subHash, alphabet);
}
}
finally
for (var j = 0; j < hashArray.Length; j++)
{
alphabet.ReturnToPool();
buffer.ReturnToPool();
var subHash = hashArray[j];

if (length > 0)
{
alphabet.Slice(0, length).CopyTo(buffer.Slice(startIndex));
}

ConsistentShuffle(alphabet, buffer);
result[j] = Unhash(subHash, alphabet);
}

if (EncodeLong(result) == hash)
Expand All @@ -505,33 +498,23 @@ private long[] GetNumbersFrom(string hash)
return Array.Empty<long>();
}

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

/// <summary>NOTE: This method mutates the <paramref name="alphabet"/> argument in-place.</summary>
private static void ConsistentShuffle(char[] alphabet, int alphabetLength, ReadOnlySpan<char> salt, int saltLength)
private static void ConsistentShuffle(Span<char> alphabet, ReadOnlySpan<char> salt)
{
if (salt.Length == 0)
return;

// TODO: Document or rename these cryptically-named variables: i, v, p, n.
int n;
for (int i = alphabetLength - 1, v = 0, p = 0; i > 0; i--, v++)
for (int i = alphabet.Length - 1, v = 0, p = 0; i > 0; i--, v++)
{
v %= saltLength;
v %= salt.Length;
n = salt[v];
p += n;
var j = (n + v + p) % i;

// swap characters at positions i and j:
var temp = alphabet[j];
alphabet[j] = alphabet[i];
alphabet[i] = temp;
(alphabet[i], alphabet[j]) = (alphabet[j], alphabet[i]);
}
}
}
Expand Down
6 changes: 1 addition & 5 deletions src/Hashids.net/Hashids.net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,7 @@
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Buffers" Version="4.5.1" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="System.Memory" Version="4.5.4" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion test/Hashids.net.benchmark/Hashids.net.benchmark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net48;net5.0</TargetFrameworks>
<TargetFrameworks>net48;net5.0;net6.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Expand Down