Skip to content

Commit c7372cc

Browse files
authored
Fix min buffer sizes (#76)
* add new const for min buffer size * use max stackalloc size const instead of numbers * switch to private var for min buffer in ctor * add new tests
1 parent 4ffc6ea commit c7372cc

2 files changed

Lines changed: 44 additions & 19 deletions

File tree

src/Hashids.net/Hashids.cs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public partial class Hashids : IHashids
1414
public const string DEFAULT_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
1515
public const string DEFAULT_SEPS = "cfhistuCFHISTU";
1616
public const int MIN_ALPHABET_LENGTH = 16;
17-
public const int MAX_STACKALLOC_SIZE = 512;
17+
private const int MAX_STACKALLOC_SIZE = 512;
1818

1919
private const double SEP_DIV = 3.5;
2020
private const double GUARD_DIV = 12.0;
@@ -24,6 +24,7 @@ public partial class Hashids : IHashids
2424
private readonly char[] _guards;
2525
private readonly char[] _salt;
2626
private readonly int _minHashLength;
27+
private readonly int _minBufferSize;
2728

2829
// Creates the Regex in the first usage, speed up first use of non-hex methods
2930
private static readonly Lazy<Regex> HexValidator = new(() => new Regex("^[0-9a-fA-F]+$", RegexOptions.Compiled));
@@ -61,6 +62,9 @@ public Hashids(
6162
_minHashLength = minHashLength;
6263
_alphabet = alphabet.ToCharArray().Distinct().ToArray();
6364
_seps = seps.ToCharArray();
65+
66+
// use min buffer size of 20 which is 1 digit longer than the biggest 64-bit integer (long.MaxValue = 9223372036854775807)
67+
_minBufferSize = Math.Max(20, minHashLength);
6468

6569
if (_alphabet.Length < MIN_ALPHABET_LENGTH)
6670
throw new ArgumentException($"Alphabet must contain at least {MIN_ALPHABET_LENGTH:N0} unique characters.", paramName: nameof(alphabet));
@@ -142,8 +146,8 @@ public Hashids(
142146
/// <returns>the hashed string</returns>
143147
public string EncodeLong(long number)
144148
{
145-
var numberLength = _minHashLength > 20 ? _minHashLength : 20;
146-
var result = numberLength < 512 ? stackalloc char[numberLength] : new char[numberLength];
149+
var numberLength = _minBufferSize;
150+
var result = numberLength < MAX_STACKALLOC_SIZE ? stackalloc char[numberLength] : new char[numberLength];
147151
var length = GenerateHashFrom(number, ref result);
148152
return length == -1 ? string.Empty : result.Slice(0, length).ToString();
149153
}
@@ -155,8 +159,8 @@ public string EncodeLong(long number)
155159
/// <returns>Encoded hash string.</returns>
156160
public string EncodeLong(params long[] numbers)
157161
{
158-
var numbersLength = _minHashLength > 20 ? _minHashLength * numbers.Length : numbers.Length * 20;
159-
var result = numbersLength < 512 ? stackalloc char[numbersLength] : new char[numbersLength];
162+
var numbersLength = _minBufferSize * numbers.Length;
163+
var result = numbersLength < MAX_STACKALLOC_SIZE ? stackalloc char[numbersLength] : new char[numbersLength];
160164
var length = GenerateHashFrom(numbers, ref result);
161165
return length == -1 ? string.Empty : result.Slice(0, length).ToString();
162166
}
@@ -290,21 +294,20 @@ private int GenerateHashFrom(long number, ref Span<char> result)
290294

291295
var numberHashInt = number % 100;
292296

293-
var alphabet = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
297+
var alphabet = _alphabet.Length < MAX_STACKALLOC_SIZE ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
294298
_alphabet.CopyTo(alphabet);
295299

296300
var lottery = alphabet[(int)(numberHashInt % _alphabet.Length)];
297301
result[0] = lottery;
298302

299-
var shuffleBuffer = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
303+
var shuffleBuffer = _alphabet.Length < MAX_STACKALLOC_SIZE ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
300304
shuffleBuffer[0] = lottery;
301305
_salt.AsSpan().Slice(0, Math.Min(_salt.Length, _alphabet.Length - 1)).CopyTo(shuffleBuffer.Slice(1));
302306

303307
var startIndex = 1 + _salt.Length;
304308
var length = _alphabet.Length - startIndex;
305309

306-
// use buffer size of 19 which is the length of the biggest 64-bit integer (long.MaxValue = 9223372036854775807)
307-
Span<char> hashBuffer = stackalloc char[19];
310+
Span<char> hashBuffer = stackalloc char[_minBufferSize];
308311

309312
if (length > 0)
310313
alphabet.Slice(0, length).CopyTo(shuffleBuffer.Slice(startIndex));
@@ -409,8 +412,7 @@ private int GenerateHashFrom(ReadOnlySpan<long> numbers, ref Span<char> result)
409412
var startIndex = 1 + _salt.Length;
410413
var length = _alphabet.Length - startIndex;
411414

412-
// use buffer size of 19 which is the length of the biggest 64-bit integer (long.MaxValue = 9223372036854775807)
413-
Span<char> hashBuffer = stackalloc char[19];
415+
Span<char> hashBuffer = stackalloc char[_minBufferSize];
414416

415417
for (var i = 0; i < numbers.Length; i++)
416418
{
@@ -495,8 +497,7 @@ private int BuildReversedHash(long input, ReadOnlySpan<char> alphabet, Span<char
495497
hashBuffer[length] = alphabet[idx];
496498
length += 1;
497499
input /= _alphabet.Length;
498-
}
499-
while (input > 0);
500+
} while (input > 0);
500501

501502
return length;
502503
}
@@ -534,10 +535,10 @@ private long GetNumberFrom(string hash)
534535

535536
var hashBuffer = hashBreakdown.Slice(1);
536537

537-
Span<char> alphabet = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
538+
Span<char> alphabet = _alphabet.Length < MAX_STACKALLOC_SIZE ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
538539
_alphabet.CopyTo(alphabet);
539540

540-
Span<char> buffer = _alphabet.Length < 512 ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
541+
Span<char> buffer = _alphabet.Length < MAX_STACKALLOC_SIZE ? stackalloc char[_alphabet.Length] : new char[_alphabet.Length];
541542
buffer[0] = lottery;
542543
_salt.AsSpan().Slice(0, Math.Min(_salt.Length, _alphabet.Length - 1)).CopyTo(buffer.Slice(1));
543544

@@ -550,10 +551,12 @@ private long GetNumberFrom(string hash)
550551
ConsistentShuffle(alphabet, buffer);
551552
var result = Unhash(hashBuffer, alphabet);
552553

553-
Span<char> resultBuffer = stackalloc char[guardedHash.Length];
554+
// regenerate hash from numbers and compare to given hash to ensure the correct parameters were used
555+
// ensure buffer is big enough based on what was generated
556+
var bufferSize = Math.Max(_minBufferSize, guardedHash.Length);
557+
Span<char> resultBuffer = stackalloc char[bufferSize];
554558
var hashLength = GenerateHashFrom(result, ref resultBuffer);
555559
ReadOnlySpan<char> rehash = resultBuffer.Slice(0, hashLength);
556-
// regenerate hash from numbers and compare to given hash to ensure the correct parameters were used
557560
if (guardedHash.Equals(rehash, StringComparison.Ordinal))
558561
return result;
559562

@@ -564,10 +567,11 @@ private long[] GetNumbersFrom(string hash)
564567
{
565568
var result = NumbersFrom(hash);
566569

567-
Span<char> hashBuffer = hash.Length < 512 ? stackalloc char[hash.Length] : new char[hash.Length];
570+
Span<char> hashBuffer = hash.Length < MAX_STACKALLOC_SIZE ? stackalloc char[hash.Length] : new char[hash.Length];
568571
var hashLength = GenerateHashFrom(result, ref hashBuffer);
569572
if (hashLength == -1)
570573
return Array.Empty<long>();
574+
571575
ReadOnlySpan<char> rehash = hashBuffer.Slice(0, hashLength);
572576
// regenerate hash from numbers and compare to given hash to ensure the correct parameters were used
573577
if (hash.AsSpan().Equals(rehash, StringComparison.Ordinal))

test/Hashids.net.test/IssueSpecificTests.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,26 @@ void Issue_64_long_max_value_with_min_alphabet_length()
6565

6666
decoded.Should().Be(long.MaxValue);
6767
}
68+
69+
[Fact]
70+
void Issue75_1CharacterHashShouldNotThrowException()
71+
{
72+
var hashids = new Hashids("salt");
73+
Assert.Throws<NoResultException>(() => hashids.DecodeSingle("a"));
74+
}
75+
76+
[Fact]
77+
void Issue75_TooShortHashShouldNotThrowException()
78+
{
79+
var hashids = new Hashids("salt");
80+
Assert.Throws<NoResultException>(() => hashids.DecodeSingle("ab"));
81+
}
82+
83+
[Fact]
84+
void Issue75_TooShortHashWithLargerHashLengthShouldNotThrowException()
85+
{
86+
var hashids = new Hashids("salt", 40);
87+
Assert.Throws<NoResultException>(() => hashids.DecodeSingle("ab"));
88+
}
6889
}
69-
}
90+
}

0 commit comments

Comments
 (0)