@@ -16,7 +16,6 @@ public partial class Hashids : IHashids
1616
1717 private const double SEP_DIV = 3.5 ;
1818 private const double GUARD_DIV = 12.0 ;
19-
2019 private const int MaxNumberHashLength = 12 ; // Length of long.MaxValue;
2120
2221 private readonly char [ ] _alphabet ;
@@ -25,11 +24,10 @@ public partial class Hashids : IHashids
2524 private readonly char [ ] _salt ;
2625 private readonly int _minHashLength ;
2726
28- private readonly StringBuilderPool _sbPool = new ( ) ;
29-
3027 // Creates the Regex in the first usage, speed up first use of non-hex methods
31- private static readonly Lazy < Regex > hexValidator = new ( ( ) => new Regex ( "^[0-9a-fA-F]+$" , RegexOptions . Compiled ) ) ;
32- private static readonly Lazy < Regex > hexSplitter = new ( ( ) => new Regex ( @"[\w\W]{1,12}" , RegexOptions . Compiled ) ) ;
28+ private static readonly Lazy < Regex > HexValidator = new ( ( ) => new Regex ( "^[0-9a-fA-F]+$" , RegexOptions . Compiled ) ) ;
29+ private static readonly Lazy < Regex > HexSplitter = new ( ( ) => new Regex ( @"[\w\W]{1,12}" , RegexOptions . Compiled ) ) ;
30+ private readonly StringBuilderPool StringBuilderPool = new ( ) ;
3331
3432 /// <summary>
3533 /// Instantiates a new Hashids encoder/decoder with defaults.
@@ -53,101 +51,74 @@ public Hashids(
5351 string alphabet = DEFAULT_ALPHABET ,
5452 string seps = DEFAULT_SEPS )
5553 {
56- if ( salt == null )
57- throw new ArgumentNullException ( nameof ( salt ) ) ;
58- if ( string . IsNullOrWhiteSpace ( alphabet ) )
59- throw new ArgumentNullException ( nameof ( alphabet ) ) ;
60- if ( minHashLength < 0 )
61- throw new ArgumentOutOfRangeException ( nameof ( minHashLength ) , "Value must be zero or greater." ) ;
62- if ( string . IsNullOrWhiteSpace ( seps ) )
63- throw new ArgumentNullException ( nameof ( seps ) ) ;
54+ if ( salt == null ) throw new ArgumentNullException ( nameof ( salt ) ) ;
55+ if ( minHashLength < 0 ) throw new ArgumentOutOfRangeException ( nameof ( minHashLength ) , "Value must be zero or greater." ) ;
56+ if ( string . IsNullOrWhiteSpace ( alphabet ) ) throw new ArgumentNullException ( nameof ( alphabet ) ) ;
57+ if ( string . IsNullOrWhiteSpace ( seps ) ) throw new ArgumentNullException ( nameof ( seps ) ) ;
6458
6559 _salt = salt . Trim ( ) . ToCharArray ( ) ;
6660 _minHashLength = minHashLength ;
61+ _alphabet = alphabet . ToCharArray ( ) . Distinct ( ) . ToArray ( ) ;
62+ _seps = seps . ToCharArray ( ) ;
6763
68- InitCharArrays ( alphabet : alphabet , seps : seps , salt : _salt , alphabetChars : out _alphabet , sepChars : out _seps , guardChars : out _guards ) ;
69- }
70-
71- /// <remarks>This method uses <c>out</c> params instead of returning a ValueTuple so it works with .NET 4.6.1.</remarks>
72- private static void InitCharArrays ( string alphabet , string seps , ReadOnlySpan < char > salt , out char [ ] alphabetChars , out char [ ] sepChars , out char [ ] guardChars )
73- {
74- alphabetChars = alphabet . ToCharArray ( ) . Distinct ( ) . ToArray ( ) ;
75- sepChars = seps . ToCharArray ( ) ;
76-
77- if ( alphabetChars . Length < MIN_ALPHABET_LENGTH )
78- {
64+ if ( _alphabet . Length < MIN_ALPHABET_LENGTH )
7965 throw new ArgumentException ( $ "Alphabet must contain at least { MIN_ALPHABET_LENGTH : N0} unique characters.", paramName : nameof ( alphabet ) ) ;
80- }
8166
82- // SetupGuards():
67+ // separator characters can only be chosen from the characters in the alphabet
68+ if ( _seps . Length > 0 )
69+ _seps = _seps . Intersect ( _alphabet ) . ToArray ( ) ;
8370
84- // seps should contain only characters present in alphabet:
85- if ( sepChars . Length > 0 )
86- {
87- sepChars = sepChars . Intersect ( alphabetChars ) . ToArray ( ) ;
88- }
89-
90- // alphabet should not contain seps // TODO: This comment contradicts the above, it needs rephrasing.
91- if ( sepChars . Length > 0 )
92- {
93- alphabetChars = alphabetChars . Except ( sepChars ) . ToArray ( ) ;
94- }
71+ // once separator characters are chosen, they must be removed from the alphabet available for hash generation
72+ if ( _seps . Length > 0 )
73+ _alphabet = _alphabet . Except ( _seps ) . ToArray ( ) ;
9574
96- if ( alphabetChars . Length < ( MIN_ALPHABET_LENGTH - 6 ) )
97- {
75+ if ( _alphabet . Length < ( MIN_ALPHABET_LENGTH - 6 ) )
9876 throw new ArgumentException ( $ "Alphabet must contain at least { MIN_ALPHABET_LENGTH : N0} unique characters that are also not present in .", paramName : nameof ( alphabet ) ) ;
99- }
10077
101- ConsistentShuffle ( alphabet : sepChars , salt : salt ) ;
78+ ConsistentShuffle ( alphabet : _seps , salt : _salt ) ;
10279
103- if ( sepChars . Length == 0 || ( ( float ) alphabetChars . Length / sepChars . Length ) > SEP_DIV )
80+ if ( _seps . Length == 0 || ( ( float ) _alphabet . Length / _seps . Length ) > SEP_DIV )
10481 {
105- var sepsLength = ( int ) Math . Ceiling ( ( float ) alphabetChars . Length / SEP_DIV ) ;
82+ var sepsLength = ( int ) Math . Ceiling ( ( float ) _alphabet . Length / SEP_DIV ) ;
10683
10784 if ( sepsLength == 1 )
108- {
10985 sepsLength = 2 ;
110- }
11186
112- if ( sepsLength > sepChars . Length )
87+ if ( sepsLength > _seps . Length )
11388 {
114- var diff = sepsLength - sepChars . Length ;
115- sepChars = sepChars . Append ( alphabetChars , 0 , diff ) ;
116- alphabetChars = alphabetChars . SubArray ( diff ) ;
89+ var diff = sepsLength - _seps . Length ;
90+ _seps = _seps . Append ( _alphabet , 0 , diff ) ;
91+ _alphabet = _alphabet . SubArray ( diff ) ;
11792 }
11893 else
11994 {
120- sepChars = sepChars . SubArray ( 0 , sepsLength ) ;
95+ _seps = _seps . SubArray ( 0 , sepsLength ) ;
12196 }
12297 }
12398
124- ConsistentShuffle ( alphabet : alphabetChars , salt : salt ) ;
125-
126- // SetupGuards():
127-
128- var guardCount = ( int ) Math . Ceiling ( alphabetChars . Length / GUARD_DIV ) ;
99+ ConsistentShuffle ( alphabet : _alphabet , salt : _salt ) ;
129100
130- if ( alphabetChars . Length < 3 )
101+ var guardCount = ( int ) Math . Ceiling ( _alphabet . Length / GUARD_DIV ) ;
102+
103+ if ( _alphabet . Length < 3 )
131104 {
132- guardChars = sepChars . SubArray ( index : 0 , length : guardCount ) ;
133- sepChars = sepChars . SubArray ( index : guardCount ) ;
105+ _guards = _seps . SubArray ( index : 0 , length : guardCount ) ;
106+ _seps = _seps . SubArray ( index : guardCount ) ;
134107 }
135108
136109 else
137110 {
138- guardChars = alphabetChars . SubArray ( index : 0 , length : guardCount ) ;
139- alphabetChars = alphabetChars . SubArray ( index : guardCount ) ;
111+ _guards = _alphabet . SubArray ( index : 0 , length : guardCount ) ;
112+ _alphabet = _alphabet . SubArray ( index : guardCount ) ;
140113 }
141114 }
142115
143- #if NETCOREAPP3_1_OR_GREATER
144116 /// <summary>
145117 /// Encodes the provided number into a hashed string
146118 /// </summary>
147119 /// <param name="number">the number</param>
148120 /// <returns>the hashed string</returns>
149121 public string Encode ( int number ) => EncodeLong ( number ) ;
150- #endif
151122
152123 /// <summary>
153124 /// Encodes the provided numbers into a hash string.
@@ -163,19 +134,12 @@ private static void InitCharArrays(string alphabet, string seps, ReadOnlySpan<ch
163134 /// <returns>Encoded hash string.</returns>
164135 public virtual string Encode ( IEnumerable < int > numbers ) => Encode ( numbers . ToArray ( ) ) ;
165136
166- #if NETCOREAPP3_1_OR_GREATER
167137 /// <summary>
168138 /// Encodes the provided number into a hashed string
169139 /// </summary>
170140 /// <param name="number">the number</param>
171141 /// <returns>the hashed string</returns>
172- public string EncodeLong ( long number )
173- {
174- ReadOnlySpan < long > span = stackalloc [ ] { number } ;
175-
176- return GenerateHashFrom ( span ) ;
177- }
178- #endif
142+ public string EncodeLong ( long number ) => GenerateHashFrom ( stackalloc [ ] { number } ) ;
179143
180144 /// <summary>
181145 /// Encodes the provided numbers into a hash string.
@@ -230,7 +194,7 @@ public bool TryDecodeSingleLong(string hash, out long id)
230194 id = numbers [ 0 ] ;
231195 return true ;
232196 }
233-
197+
234198 id = 0L ;
235199 return false ;
236200 }
@@ -259,7 +223,7 @@ public virtual bool TryDecodeSingle(string hash, out int id)
259223 id = ( int ) numbers [ 0 ] ;
260224 return true ;
261225 }
262-
226+
263227 id = 0 ;
264228 return false ;
265229 }
@@ -271,17 +235,17 @@ public virtual bool TryDecodeSingle(string hash, out int id)
271235 /// <returns>Encoded hash string.</returns>
272236 public virtual string EncodeHex ( string hex )
273237 {
274- if ( string . IsNullOrWhiteSpace ( hex ) || ! hexValidator . Value . IsMatch ( hex ) )
238+ if ( string . IsNullOrWhiteSpace ( hex ) || ! HexValidator . Value . IsMatch ( hex ) )
275239 return string . Empty ;
276240
277- var matches = hexSplitter . Value . Matches ( hex ) ;
241+ var matches = HexSplitter . Value . Matches ( hex ) ;
278242 if ( matches . Count == 0 ) return string . Empty ;
279243
280244 var numbers = new long [ matches . Count ] ;
281245 for ( int i = 0 ; i < numbers . Length ; i ++ )
282246 {
283- Match match = matches [ i ] ;
284- string concat = string . Concat ( "1" , match . Value ) ;
247+ var match = matches [ i ] ;
248+ var concat = string . Concat ( "1" , match . Value ) ;
285249 var number = Convert . ToInt64 ( concat , fromBase : 16 ) ;
286250 numbers [ i ] = number ;
287251 }
@@ -296,40 +260,35 @@ public virtual string EncodeHex(string hex)
296260 /// <returns>Decoded hex string.</returns>
297261 public virtual string DecodeHex ( string hash )
298262 {
299- var builder = _sbPool . Get ( ) ;
263+ var builder = StringBuilderPool . Get ( ) ;
300264 var numbers = DecodeLong ( hash ) ;
301265
302266 foreach ( var number in numbers )
303267 {
304268 var s = number . ToString ( "X" ) ;
305-
306269 for ( var i = 1 ; i < s . Length ; i ++ )
307- {
308270 builder . Append ( s [ i ] ) ;
309- }
310271 }
311272
312273 var result = builder . ToString ( ) ;
313- _sbPool . Return ( builder ) ;
274+ StringBuilderPool . Return ( builder ) ;
314275 return result ;
315276 }
316277
317278 private string GenerateHashFrom ( ReadOnlySpan < long > numbers )
318279 {
319- if ( numbers . Length == 0 )
280+ if ( numbers . Length == 0 )
320281 return string . Empty ;
321282
322- foreach ( var item in numbers )
323- if ( item < 0 )
283+ foreach ( var num in numbers )
284+ if ( num < 0 )
324285 return string . Empty ;
325286
326287 long numbersHashInt = 0 ;
327288 for ( var i = 0 ; i < numbers . Length ; i ++ )
328- {
329289 numbersHashInt += numbers [ i ] % ( i + 100 ) ;
330- }
331290
332- var stringBuilder = _sbPool . Get ( ) ;
291+ var stringBuilder = StringBuilderPool . Get ( ) ;
333292
334293 Span < char > alphabet = _alphabet . Length < 512 ? stackalloc char [ _alphabet . Length ] : new char [ _alphabet . Length ] ;
335294 _alphabet . CopyTo ( alphabet ) ;
@@ -351,17 +310,13 @@ private string GenerateHashFrom(ReadOnlySpan<long> numbers)
351310 var number = numbers [ i ] ;
352311
353312 if ( length > 0 )
354- {
355313 alphabet . Slice ( 0 , length ) . CopyTo ( shuffleBuffer . Slice ( startIndex ) ) ;
356- }
357314
358315 ConsistentShuffle ( alphabet , shuffleBuffer ) ;
359316 var hashLength = BuildReversedHash ( number , alphabet , hashBuffer ) ;
360317
361318 for ( var j = hashLength - 1 ; j > - 1 ; j -- )
362- {
363319 stringBuilder . Append ( hashBuffer [ j ] ) ;
364- }
365320
366321 if ( i + 1 < numbers . Length )
367322 {
@@ -412,7 +367,7 @@ private string GenerateHashFrom(ReadOnlySpan<long> numbers)
412367 }
413368
414369 var result = stringBuilder . ToString ( ) ;
415- _sbPool . Return ( stringBuilder ) ;
370+ StringBuilderPool . Return ( stringBuilder ) ;
416371 return result ;
417372 }
418373
@@ -453,17 +408,14 @@ private long[] GetNumbersFrom(string hash)
453408 if ( hashArray . Length == 0 )
454409 return Array . Empty < long > ( ) ;
455410
456- var i = ( hashArray . Length is 3 or 2 ) ? 1 : 0 ;
411+ var unguardedIdx = ( hashArray . Length is 3 or 2 ) ? 1 : 0 ;
412+ var hashBreakdown = hashArray [ unguardedIdx ] ;
457413
458- var hashBreakdown = hashArray [ i ] ;
459414 var lottery = hashBreakdown [ 0 ] ;
460-
461- if ( lottery == '\0 ' ) /* default(char) == '\0' */
415+ if ( lottery == '\0 ' ) // default(char) is '\0'
462416 return Array . Empty < long > ( ) ;
463417
464- hashBreakdown = hashBreakdown . Substring ( 1 ) ;
465-
466- hashArray = hashBreakdown . Split ( _seps , StringSplitOptions . RemoveEmptyEntries ) ;
418+ hashArray = hashBreakdown . Substring ( 1 ) . Split ( _seps , StringSplitOptions . RemoveEmptyEntries ) ;
467419
468420 var result = new long [ hashArray . Length ] ;
469421
@@ -482,31 +434,29 @@ private long[] GetNumbersFrom(string hash)
482434 var subHash = hashArray [ j ] ;
483435
484436 if ( length > 0 )
485- {
486437 alphabet . Slice ( 0 , length ) . CopyTo ( buffer . Slice ( startIndex ) ) ;
487- }
488438
489439 ConsistentShuffle ( alphabet , buffer ) ;
490440 result [ j ] = Unhash ( subHash , alphabet ) ;
491441 }
492442
493- if ( EncodeLong ( result ) == hash )
494- {
443+ // regenerate hash from numbers and compare to given hash to ensure the correct parameters were used
444+ if ( GenerateHashFrom ( result ) . Equals ( hash , StringComparison . Ordinal ) )
495445 return result ;
496- }
497446
498447 return Array . Empty < long > ( ) ;
499448 }
500449
501- /// <summary>NOTE: This method mutates the <paramref name="alphabet"/> argument in-place.</summary>
450+ /// <summary>
451+ /// NOTE: This method mutates the <paramref name="alphabet"/> argument in-place.
452+ /// </summary>
502453 private static void ConsistentShuffle ( Span < char > alphabet , ReadOnlySpan < char > salt )
503454 {
504455 if ( salt . Length == 0 )
505456 return ;
506457
507458 // TODO: Document or rename these cryptically-named variables: i, v, p, n.
508- int n ;
509- for ( int i = alphabet . Length - 1 , v = 0 , p = 0 ; i > 0 ; i -- , v ++ )
459+ for ( int i = alphabet . Length - 1 , v = 0 , n , p = 0 ; i > 0 ; i -- , v ++ )
510460 {
511461 v %= salt . Length ;
512462 n = salt [ v ] ;
0 commit comments