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
10 changes: 1 addition & 9 deletions src/Hashids.net/ArrayExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Buffers;

namespace HashidsNet
{
public static class ArrayExtensions
{
public static T[] Copy<T>(this T[] array)
{
return SubArray(array, 0, array.Length);
}

public static T[] SubArray<T>(this T[] array, int index)
{
return SubArray(array, index, array.Length - index);
Expand Down Expand Up @@ -53,4 +45,4 @@ public static void ReturnToPool<T>(this T[] array)
ArrayPool<T>.Shared.Return(array);
}
}
}
}
203 changes: 98 additions & 105 deletions src/Hashids.net/Hashids.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.ObjectPool;

namespace HashidsNet
{
Expand All @@ -24,9 +25,11 @@ public partial class Hashids : IHashids
private char[] _salt;
private readonly int _minHashLength;

// Creates the Regex in the first usage, speed up first use of non hex methods
private static readonly Lazy<Regex> hexValidator = new Lazy<Regex>(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
private static readonly Lazy<Regex> hexSplitter = new Lazy<Regex>(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));
private readonly ObjectPool<StringBuilder> _sbPool = new DefaultObjectPool<StringBuilder>(new StringBuilderPooledObjectPolicy());

// Creates the Regex in the first usage, speed up first use of non-hex methods
private static readonly Lazy<Regex> hexValidator = new(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
private static readonly Lazy<Regex> hexSplitter = new(() => new Regex(@"[\w\W]{1,12}", RegexOptions.Compiled));

/// <summary>
/// Instantiates a new Hashids with the default setup.
Expand Down Expand Up @@ -70,6 +73,57 @@ public Hashids(
SetupGuards();
}

private void SetupSeps()
{
// seps should contain only characters present in alphabet;
_seps = _seps.Intersect(_alphabet).ToArray();

// alphabet should not contain seps.
_alphabet = _alphabet.Except(_seps).ToArray();

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

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

if (sepsLength == 1)
{
sepsLength = 2;
}

if (sepsLength > _seps.Length)
{
var diff = sepsLength - _seps.Length;
_seps = _seps.Append(_alphabet, 0, diff);
_alphabet = _alphabet.SubArray(diff);
}
else
{
_seps = _seps.SubArray(0, sepsLength);
}
}

ConsistentShuffle(_alphabet, _alphabet.Length, _salt, _salt.Length);
}

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

if (_alphabet.Length < 3)
{
_guards = _seps.SubArray(0, guardCount);
_seps = _seps.SubArray(guardCount);
}

else
{
_guards = _alphabet.SubArray(0, guardCount);
_alphabet = _alphabet.SubArray(guardCount);
}
}

/// <summary>
/// Encodes the provided numbers into a hashed string.
/// </summary>
Expand Down Expand Up @@ -105,46 +159,6 @@ public virtual int[] Decode(string hash)
return Array.ConvertAll(numbers, n => (int) n);
}

/// <summary>
/// Encodes the provided hex string to a hashids hash.
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public virtual string EncodeHex(string hex)
{
if (!hexValidator.Value.IsMatch(hex))
return string.Empty;

var matches = hexSplitter.Value.Matches(hex);
var numbers = new List<long>(matches.Count);

foreach (Match match in matches)
{
var number = Convert.ToInt64(string.Concat("1", match.Value), 16);
numbers.Add(number);
}

return EncodeLong(numbers.ToArray());
}

/// <summary>
/// Decodes the provided hash into a hex-string.
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
public virtual string DecodeHex(string hash)
{
var builder = new StringBuilder();
var numbers = DecodeLong(hash);

foreach (var number in numbers)
{
builder.Append(string.Format("{0:X}", number).Substring(1));
}

return builder.ToString();
}

/// <summary>
/// Decodes the provided hashed string into an array of longs.
/// </summary>
Expand Down Expand Up @@ -178,55 +192,45 @@ public string EncodeLong(IEnumerable<long> numbers)
return EncodeLong(numbers.ToArray());
}

private void SetupSeps()
/// <summary>
/// Encodes the provided hex string to a hashids hash.
/// </summary>
/// <param name="hex"></param>
/// <returns></returns>
public virtual string EncodeHex(string hex)
{
// seps should contain only characters present in alphabet;
_seps = _seps.Intersect(_alphabet).ToArray();

// alphabet should not contain seps.
_alphabet = _alphabet.Except(_seps).ToArray();
if (!hexValidator.Value.IsMatch(hex))
return string.Empty;

ConsistentShuffle(_seps, _seps.Length, _salt, _salt.Length);
var matches = hexSplitter.Value.Matches(hex);
var numbers = new List<long>(matches.Count);

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

if (sepsLength == 1)
{
sepsLength = 2;
}

if (sepsLength > _seps.Length)
{
var diff = sepsLength - _seps.Length;
_seps = _seps.Append(_alphabet, 0, diff);
_alphabet = _alphabet.SubArray(diff);
}
else
{
_seps = _seps.SubArray(0, sepsLength);
}
var number = Convert.ToInt64(string.Concat("1", match.Value), 16);
numbers.Add(number);
}

ConsistentShuffle(_alphabet, _alphabet.Length, _salt, _salt.Length);
return EncodeLong(numbers.ToArray());
}

private void SetupGuards()
/// <summary>
/// Decodes the provided hash into a hex-string.
/// </summary>
/// <param name="hash"></param>
/// <returns></returns>
public virtual string DecodeHex(string hash)
{
var guardCount = (int) Math.Ceiling(_alphabet.Length / GUARD_DIV);
var builder = _sbPool.Get();
var numbers = DecodeLong(hash);

if (_alphabet.Length < 3)
{
_guards = _seps.SubArray(0, guardCount);
_seps = _seps.SubArray(guardCount);
}
foreach (var number in numbers)
foreach (var ch in number.ToString("X").AsSpan().Slice(1))
builder.Append(ch);

else
{
_guards = _alphabet.SubArray(0, guardCount);
_alphabet = _alphabet.SubArray(guardCount);
}
var result = builder.ToString();
_sbPool.Return(builder);
return result;
}

/// <summary>
Expand All @@ -245,7 +249,7 @@ private string GenerateHashFrom(long[] numbers)
numbersHashInt += numbers[i] % (i + 100);
}

var builder = new StringBuilder();
var builder = _sbPool.Get();

char[] buffer = null;
var alphabet = _alphabet.CopyPooled();
Expand Down Expand Up @@ -320,15 +324,9 @@ private string GenerateHashFrom(long[] numbers)
buffer.ReturnToPool();
}

return builder.ToString();
}

private char[] CreateBuffer(int alphabetLength, char lottery)
{
var buffer = System.Buffers.ArrayPool<char>.Shared.Rent(alphabetLength);
buffer[0] = lottery;
Array.Copy(_salt, 0, buffer, 1, Math.Min(_salt.Length, alphabetLength - 1));
return buffer;
var result = builder.ToString();
_sbPool.Return(builder);
return result;
}

private char[] Hash(long input, char[] alphabet, int alphabetLength)
Expand All @@ -352,25 +350,12 @@ private long Unhash(string input, char[] alphabet, int alphabetLength)
for (var i = 0; i < input.Length; i++)
{
var pos = Array.IndexOf(alphabet, input[i]);
number += pos * LongPow(alphabetLength, input.Length - i - 1);
number = number * alphabetLength + pos;
}

return number;
}

private static long LongPow(int target, int power)
{
if (power == 0) return 1;
long result = target;
while (power > 1)
{
result *= target;
power--;
}

return result;
}

private long[] GetNumbersFrom(string hash)
{
if (string.IsNullOrWhiteSpace(hash))
Expand Down Expand Up @@ -432,6 +417,14 @@ private long[] GetNumbersFrom(string hash)
return result.ToArray();
}

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

private void ConsistentShuffle(char[] alphabet, int alphabetLength, char[] salt, int saltLength)
{
if (salt.Length == 0)
Expand Down
6 changes: 6 additions & 0 deletions src/Hashids.net/Hashids.net.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool">
<Version>5.0.5</Version>
</PackageReference>
<PackageReference Include="System.Buffers">
<Version>4.5.1</Version>
</PackageReference>
<PackageReference Include="System.Memory">
<Version>4.5.4</Version>
</PackageReference>
</ItemGroup>

</Project>
4 changes: 1 addition & 3 deletions src/Hashids.net/IHashids.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Generic;

namespace HashidsNet
{
Expand Down
22 changes: 19 additions & 3 deletions test/Hashids.net.benchmark/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,35 @@ public static void Main(string[] args)
public class HashBenchmark
{
private readonly HashidsNet.Hashids _hashids;
private readonly int[] _input = { 12345, 1234567890, int.MaxValue };
private readonly int[] _ints = { 12345, 1234567890, int.MaxValue };
private readonly long[] _longs = { 12345, 1234567890123456789, long.MaxValue };
private readonly string _hex = "507f1f77bcf86cd799439011";

public HashBenchmark()
{
_hashids = new HashidsNet.Hashids();
}

[Benchmark]
public void Run()
public void RoundtripInts()
{
var encodedValue = _hashids.Encode(_input);
var encodedValue = _hashids.Encode(_ints);
var decodedValue = _hashids.Decode(encodedValue);
}

[Benchmark]
public void RoundtripLongs()
{
var encodedValue = _hashids.EncodeLong(_longs);
var decodedValue = _hashids.DecodeLong(encodedValue);
}

[Benchmark]
public void RoundtripHex()
{
var encodedValue = _hashids.EncodeHex(_hex);
var decodedValue = _hashids.DecodeHex(encodedValue);
}
}
}
}