2 Commits

Author SHA1 Message Date
312219d42f minor refactoring and tests
All checks were successful
.NET Test / test (push) Successful in 45s
.NET Publish / publish (push) Successful in 42s
2025-08-06 22:02:38 +04:00
7eb3008738 guid generation fix
All checks were successful
.NET Test / test (push) Successful in 1m2s
2025-08-06 21:59:44 +04:00
7 changed files with 252 additions and 118 deletions

View File

@@ -7,7 +7,7 @@ public class Decode
[InlineData(554121)] [InlineData(554121)]
[InlineData(100454567)] [InlineData(100454567)]
[InlineData(3210589)] [InlineData(3210589)]
public void WhenEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed) public void WhenBytesEncodedToString_ShouldBeDecodedToTheSameByteArray(int seed)
{ {
var rng = new Random(seed); var rng = new Random(seed);
@@ -23,6 +23,43 @@ public class Decode
} }
} }
[Theory]
[InlineData(72121)]
[InlineData(554121)]
[InlineData(100454567)]
[InlineData(3210589)]
public void WhenLongEncodedToString_ShouldBeDecodedToTheSameLongArray(int seed)
{
var rng = new Random(seed);
for (int i = 1; i <= 512; i++)
{
var testLong = rng.NextInt64();
var resultString = Base64Url.Encode(testLong);
var resultLong = Base64Url.DecodeLong(resultString);
resultLong.Should().Be(testLong);
}
}
[Theory]
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
[InlineData("awxjIqZWz10", 6759716837247880299L)]
[InlineData("VjNe72vug4U", -8825948697371200682L)]
[InlineData("AAAAAAAAAAA", 0L)]
[InlineData("__________8", -1L)]
[InlineData("AQAAAAAAAAA", 1L)]
[InlineData("CgAAAAAAAAA", 10L)]
[InlineData("6AMAAAAAAAA", 1000L)]
public void WhenCalled_ShouldReturnValidLong(string testString, long expected)
{
var result = Base64Url.DecodeLong(testString);
result.Should().Be(expected);
}
[Theory] [Theory]
[InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")] [InlineData("5QrdUxDUVkCAEGw8pvLsEw", "53dd0ae5-d410-4056-8010-6c3ca6f2ec13")]
[InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")] [InlineData("6nE2uKQ4_0ar9kpmybgkdw", "b83671ea-38a4-46ff-abf6-4a66c9b82477")]
@@ -75,10 +112,23 @@ public class Decode
action.Should().Throw<FormatException>(); action.Should().Throw<FormatException>();
} }
[Theory]
[InlineData("RgG&r0_n1ZI")]
[InlineData("sxA fpKB5kY")]
[InlineData("lO4_uitvL)g")]
[InlineData("awxjIqZ^z10")]
[InlineData("VjNe7!vug4U")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
public void WhenCalledWithInvalidLongString_ShouldThrowFormatException(string testString)
{
Action action = () => Base64Url.DecodeLong(testString);
action.Should().Throw<FormatException>();
}
[Theory] [Theory]
[InlineData(null)] [InlineData(null)]
[InlineData("")] [InlineData("")]
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString) public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
{ {
Base64Url.Decode(testString).Should().BeEmpty(); Base64Url.Decode(testString).Should().BeEmpty();
} }

View File

@@ -15,6 +15,23 @@ public class Encode
result.Should().Be(expected); result.Should().Be(expected);
} }
[Theory]
[InlineData("RgGxr0_n1ZI", -7866126844696657594L)]
[InlineData("sxAPfpKB5kY", 5108913293478531251L)]
[InlineData("lO4_uitvLCg", 2894810894091415188L)]
[InlineData("awxjIqZWz10", 6759716837247880299L)]
[InlineData("VjNe72vug4U", -8825948697371200682L)]
[InlineData("AAAAAAAAAAA", 0L)]
[InlineData("__________8", -1L)]
[InlineData("AQAAAAAAAAA", 1L)]
[InlineData("CgAAAAAAAAA", 10L)]
[InlineData("6AMAAAAAAAA", 1000L)]
public void WhenCalledWithLong_ShouldReturnValidString(string expected, long testLong)
{
var result = Base64Url.Encode(testLong);
result.Should().Be(expected);
}
[Theory] [Theory]
[InlineData("IA", new byte[]{ 0x20, })] [InlineData("IA", new byte[]{ 0x20, })]
[InlineData("Ag", new byte[]{ 0x02, })] [InlineData("Ag", new byte[]{ 0x02, })]
@@ -33,7 +50,7 @@ public class Encode
[Theory] [Theory]
[InlineData(new byte[] { })] [InlineData(new byte[] { })]
[InlineData(null)] [InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray) public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
{ {
var actualBase32 = Base64Url.Encode(testArray); var actualBase32 = Base64Url.Encode(testArray);
actualBase32.Should().Be(string.Empty); actualBase32.Should().Be(string.Empty);
@@ -42,7 +59,7 @@ public class Encode
[Theory] [Theory]
[InlineData(new byte[] { })] [InlineData(new byte[] { })]
[InlineData(null)] [InlineData(null)]
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[] testArray) public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
{ {
char[] output = ['1', '2', '3', '4']; char[] output = ['1', '2', '3', '4'];

View File

@@ -2,15 +2,32 @@ namespace Just.Core.Tests.GuidV8Tests;
public class NewGuid public class NewGuid
{ {
[Theory]
[InlineData(RngEntropy.Weak)]
[InlineData(RngEntropy.Strong)]
public void Version_And_Variant_Should_Be_Correct(RngEntropy entropy)
{
var rng = new Random(25);
var referenceTime = new DateTime(2020, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
for (int i = 0; i < 2000; i++)
{
var timestamp = referenceTime.AddSeconds(rng.Next());
var result = GuidV8.NewGuid(timestamp, entropy);
result.Version.Should().Be(8);
(result.Variant & 0b1100).Should().Be(0b1000);
}
}
[Theory] [Theory]
[InlineData(RngEntropy.Weak, [InlineData(RngEntropy.Weak,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -25000000, -20000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
[InlineData(RngEntropy.Strong, [InlineData(RngEntropy.Strong,
-25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -25000000, -20000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 20000000, 50000000, 100000000)]
[InlineData(RngEntropy.Weak, [InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
@@ -34,27 +51,27 @@ public class NewGuid
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099, 6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)] 8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
[InlineData(RngEntropy.Weak, [InlineData(RngEntropy.Weak,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781, 10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932, 884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414, 2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
107768232, 110571078, 110680108, 117974892, 119800380, 126381415, 135895862, 140034471, 149039187, 150906974, 2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
156853001, 160514433, 166446323, 170148965, 171759448, 176494242, 184537553, 188558155, 197194403, 197615804, 4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
201195323, 202294490, 203040975, 203331457, 205016944, 213460258, 217072025, 217185345, 231344025, 232390198, 5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
235053215, 240175073, 245030721, 252275255, 252310334, 277070940, 277359970, 280624756, 288601124, 292427106, 6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171, 7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343, 8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)] 9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
[InlineData(RngEntropy.Strong, [InlineData(RngEntropy.Strong,
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781, 10024, 28660, 289641, 356015, 443164, 478759, 599586, 705860, 791271, 876512,
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932, 884503, 894899, 898584, 980136, 1007927, 1680328, 1690193, 1804615, 1847117, 2005534,
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414, 2106684, 2111936, 2252935, 2271396, 2298685, 2385409, 2414094, 2451706, 2549138, 2605538,
107768232, 110571078, 110680108, 117974892, 119800380, 126381415, 135895862, 140034471, 149039187, 150906974, 2864629, 2923476, 3004288, 3182875, 3266693, 3379019, 3542110, 3851467, 3871420, 4035463,
156853001, 160514433, 166446323, 170148965, 171759448, 176494242, 184537553, 188558155, 197194403, 197615804, 4316004, 4726381, 4814068, 4902666, 4979292, 4993443, 5117765, 5240585, 5249671, 5319528,
201195323, 202294490, 203040975, 203331457, 205016944, 213460258, 217072025, 217185345, 231344025, 232390198, 5387595, 5434544, 5504506, 5531264, 5546173, 5780381, 5889341, 6066328, 6167883, 6185073,
235053215, 240175073, 245030721, 252275255, 252310334, 277070940, 277359970, 280624756, 288601124, 292427106, 6299021, 6412136, 6621498, 6666562, 6741169, 6870279, 6949855, 6965427, 7153467, 7184150,
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171, 7300456, 7311381, 7413697, 7505235, 7802811, 7979134, 8053665, 8177676, 8260284, 8260773,
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343, 8269944, 8406526, 8442932, 8475162, 8555250, 8853347, 8861733, 8892200, 9069869, 9117839,
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)] 9225445, 9245837, 9378644, 9497874, 9553625, 9650968, 9704053, 9713592, 9715054, 9735988)]
public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds) public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
{ {
var rng = new Random(25); var rng = new Random(25);
@@ -78,13 +95,13 @@ public class NewGuid
[Theory] [Theory]
[InlineData(RngEntropy.Weak, [InlineData(RngEntropy.Weak,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -100000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
[InlineData(RngEntropy.Strong, [InlineData(RngEntropy.Strong,
-250000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000, -100000000, -25000000, -10000000, -5000000, -2000000, -1000000, -500000, -250000, -100000, -25000, -10000, -5000,
-2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000, -2500, -1000, -500, -499, -497, -450, -300, -200, -50, -1, 0, 1, 2, 5, 10, 20, 50, 100, 200, 350, 500, 1000,
2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)] 2500, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 200000000)]
[InlineData(RngEntropy.Weak, [InlineData(RngEntropy.Weak,
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449, -9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855, -8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,

View File

@@ -16,9 +16,9 @@ public static class Base32
? stackalloc char[outLength] ? stackalloc char[outLength]
: new char[outLength]; : new char[outLength];
_ = Encode(input, output); var size = Encode(input, output);
return new string(output); return new string(output[..size]);
} }
[Pure] [Pure]
@@ -26,20 +26,31 @@ public static class Base32
{ {
if (input.IsEmpty) return 0; if (input.IsEmpty) return 0;
int outputLength = 8 * ((input.Length + 4) / 5);
if (output.Length < outputLength)
{
throw new ArgumentException("Encoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength];
int i = 0; int i = 0;
ReadOnlySpan<char> alphabet = Alphabet; ReadOnlySpan<char> alphabet = Alphabet;
Span<byte> alphabetKeys = stackalloc byte[8];
for (int offset = 0; offset < input.Length;) for (int offset = 0; offset < input.Length;)
{ {
int numCharsToOutput = GetNextGroup(input, ref offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h); alphabetKeys.Clear();
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
output[i++] = (numCharsToOutput > 0) ? alphabet[a] : Padding; output[i++] = (numCharsToOutput > 0) ? alphabet[alphabetKeys[0]] : Padding;
output[i++] = (numCharsToOutput > 1) ? alphabet[b] : Padding; output[i++] = (numCharsToOutput > 1) ? alphabet[alphabetKeys[1]] : Padding;
output[i++] = (numCharsToOutput > 2) ? alphabet[c] : Padding; output[i++] = (numCharsToOutput > 2) ? alphabet[alphabetKeys[2]] : Padding;
output[i++] = (numCharsToOutput > 3) ? alphabet[d] : Padding; output[i++] = (numCharsToOutput > 3) ? alphabet[alphabetKeys[3]] : Padding;
output[i++] = (numCharsToOutput > 4) ? alphabet[e] : Padding; output[i++] = (numCharsToOutput > 4) ? alphabet[alphabetKeys[4]] : Padding;
output[i++] = (numCharsToOutput > 5) ? alphabet[f] : Padding; output[i++] = (numCharsToOutput > 5) ? alphabet[alphabetKeys[5]] : Padding;
output[i++] = (numCharsToOutput > 6) ? alphabet[g] : Padding; output[i++] = (numCharsToOutput > 6) ? alphabet[alphabetKeys[6]] : Padding;
output[i++] = (numCharsToOutput > 7) ? alphabet[h] : Padding; output[i++] = (numCharsToOutput > 7) ? alphabet[alphabetKeys[7]] : Padding;
} }
return i; return i;
} }
@@ -66,6 +77,11 @@ public static class Base32
input = input.TrimEnd(Padding); input = input.TrimEnd(Padding);
var outputLength = 5 * ((input.Length + 7) / 8); var outputLength = 5 * ((input.Length + 7) / 8);
if (output.Length < outputLength)
{
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
}
output = output[..outputLength]; output = output[..outputLength];
output.Clear(); output.Clear();
@@ -119,7 +135,7 @@ public static class Base32
// returns the number of bytes that were output // returns the number of bytes that were output
[Pure] [Pure]
private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, out byte a, out byte b, out byte c, out byte d, out byte e, out byte f, out byte g, out byte h) private static int GetNextGroup(ReadOnlySpan<byte> input, ref int offset, Span<byte> alphabetKeys)
{ {
var retVal = (input.Length - offset) switch var retVal = (input.Length - offset) switch
{ {
@@ -135,14 +151,14 @@ public static class Base32
uint b4 = (offset < input.Length) ? input[offset++] : 0U; uint b4 = (offset < input.Length) ? input[offset++] : 0U;
uint b5 = (offset < input.Length) ? input[offset++] : 0U; uint b5 = (offset < input.Length) ? input[offset++] : 0U;
a = (byte)(b1 >> 3); alphabetKeys[0] = (byte)(b1 >> 3);
b = (byte)(((b1 & 0x07) << 2) | (b2 >> 6)); alphabetKeys[1] = (byte)(((b1 & 0x07) << 2) | (b2 >> 6));
c = (byte)((b2 >> 1) & 0x1f); alphabetKeys[2] = (byte)((b2 >> 1) & 0x1f);
d = (byte)(((b2 & 0x01) << 4) | (b3 >> 4)); alphabetKeys[3] = (byte)(((b2 & 0x01) << 4) | (b3 >> 4));
e = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7)); alphabetKeys[4] = (byte)(((b3 & 0x0f) << 1) | (b4 >> 7));
f = (byte)((b4 >> 2) & 0x1f); alphabetKeys[5] = (byte)((b4 >> 2) & 0x1f);
g = (byte)(((b4 & 0x3) << 3) | (b5 >> 5)); alphabetKeys[6] = (byte)(((b4 & 0x3) << 3) | (b5 >> 5));
h = (byte)(b5 & 0x1f); alphabetKeys[7] = (byte)(b5 & 0x1f);
return retVal; return retVal;
} }

View File

@@ -1,24 +1,41 @@
using System.Runtime.InteropServices;
namespace Just.Core; namespace Just.Core;
public static class Base64Url public static class Base64Url
{ {
private const char Padding = '=';
[Pure] public static long DecodeLong(ReadOnlySpan<char> value)
{
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 11);
Span<byte> longBytes = stackalloc byte[8];
Span<char> chars = stackalloc char[12];
value.CopyTo(chars);
chars[^1] = Padding;
ReplaceNonUrlChars(chars);
if (!Convert.TryFromBase64Chars(chars, longBytes, out int _))
throw new FormatException("Invalid Base64 string.");
return MemoryMarshal.Read<long>(longBytes);
}
[Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value) [Pure] public static Guid DecodeGuid(ReadOnlySpan<char> value)
{ {
ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22); ArgumentOutOfRangeException.ThrowIfNotEqual(value.Length, 22);
Span<byte> guidBytes = stackalloc byte[16]; Span<byte> guidBytes = stackalloc byte[16];
Span<char> chars = stackalloc char[24]; Span<char> chars = stackalloc char[24];
value.CopyTo(chars); value.CopyTo(chars);
for (int i = 0; i < value.Length; i++) chars[^2..].Fill(Padding);
{
switch (value[i]) ReplaceNonUrlChars(chars);
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
default: continue;
}
}
chars[^2..].Fill('=');
if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _)) if (!Convert.TryFromBase64Chars(chars, guidBytes, out int _))
throw new FormatException("Invalid Base64 string."); throw new FormatException("Invalid Base64 string.");
@@ -28,9 +45,9 @@ public static class Base64Url
[Pure] public static byte[] Decode(ReadOnlySpan<char> input) [Pure] public static byte[] Decode(ReadOnlySpan<char> input)
{ {
if (input.IsEmpty) return []; if (input.IsEmpty) return [];
Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)]; Span<byte> output = stackalloc byte[3 * ((input.Length + 3) / 4)];
var size = Decode(input, output); var size = Decode(input, output);
return output[..size].ToArray(); return output[..size].ToArray();
@@ -40,28 +57,33 @@ public static class Base64Url
{ {
var padding = (4 - (value.Length & 3)) & 3; var padding = (4 - (value.Length & 3)) & 3;
var charlen = value.Length + padding; var charlen = value.Length + padding;
var outputBytes = charlen / 4; var outputBytes = 3 * (charlen / 4);
ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes); ArgumentOutOfRangeException.ThrowIfLessThan(output.Length, outputBytes);
Span<char> chars = stackalloc char[charlen]; Span<char> chars = stackalloc char[charlen];
value.CopyTo(chars); value.CopyTo(chars);
for (int i = 0; i < value.Length; i++) chars[^padding..].Fill(Padding);
{
switch (value[i]) ReplaceNonUrlChars(chars);
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
default: continue;
}
}
chars[^padding..].Fill('=');
if (!Convert.TryFromBase64Chars(chars, output, out outputBytes)) if (!Convert.TryFromBase64Chars(chars, output, out outputBytes))
throw new FormatException("Invalid Base64 string."); throw new FormatException("Invalid Base64 string.");
return outputBytes; return outputBytes;
} }
[Pure] public static string Encode(in long id)
{
Span<byte> longBytes = stackalloc byte[8];
MemoryMarshal.Write(longBytes, id);
Span<char> chars = stackalloc char[12];
Convert.TryToBase64Chars(longBytes, chars, out int _);
ReplaceUrlChars(chars[..^1]);
return new string(chars[..^1]);
}
[Pure] public static string Encode(in Guid id) [Pure] public static string Encode(in Guid id)
{ {
Span<byte> guidBytes = stackalloc byte[16]; Span<byte> guidBytes = stackalloc byte[16];
@@ -69,15 +91,7 @@ public static class Base64Url
Span<char> chars = stackalloc char[24]; Span<char> chars = stackalloc char[24];
Convert.TryToBase64Chars(guidBytes, chars, out int _); Convert.TryToBase64Chars(guidBytes, chars, out int _);
for (int i = 0; i < chars.Length - 2; i++) ReplaceUrlChars(chars[..^2]);
{
switch (chars[i])
{
case '+': chars[i] = '-'; continue;
case '/': chars[i] = '_'; continue;
default: continue;
}
}
return new string(chars[..^2]); return new string(chars[..^2]);
} }
@@ -86,7 +100,7 @@ public static class Base64Url
{ {
if (input.IsEmpty) return string.Empty; if (input.IsEmpty) return string.Empty;
int outLength = 8 * ((input.Length + 5) / 6); int outLength = 4 * ((input.Length + 2) / 3);
Span<char> output = stackalloc char[outLength]; Span<char> output = stackalloc char[outLength];
int strlen = Encode(input, output); int strlen = Encode(input, output);
@@ -97,24 +111,47 @@ public static class Base64Url
{ {
if (input.IsEmpty) return 0; if (input.IsEmpty) return 0;
var charlen = 8 * ((input.Length + 5) / 6); var charlen = 4 * ((input.Length + 2) / 3);
Span<char> chars = stackalloc char[charlen]; Span<char> chars = stackalloc char[charlen];
Convert.TryToBase64Chars(input, chars, out int charsWritten); Convert.TryToBase64Chars(input, chars, out int charsWritten);
int i; int i = ReplaceUrlChars(chars[..charsWritten]);
for (i = 0; i < charsWritten; i++) chars[..i].CopyTo(output);
return i;
}
private static int ReplaceUrlChars(Span<char> chars)
{
int i = 0;
for (; i < chars.Length; i++)
{ {
switch (chars[i]) switch (chars[i])
{ {
case '+': chars[i] = '-'; continue; case '+': chars[i] = '-'; continue;
case '/': chars[i] = '_'; continue; case '/': chars[i] = '_'; continue;
case '=': goto exitLoop; case Padding: goto break_loop;
default: continue; default: continue;
} }
} }
exitLoop: break_loop:
chars[..i].CopyTo(output); return i;
}
private static int ReplaceNonUrlChars(Span<char> chars)
{
int i = 0;
for (; i < chars.Length; i++)
{
switch (chars[i])
{
case '-': chars[i] = '+'; continue;
case '_': chars[i] = '/'; continue;
case Padding: goto break_loop;
default: continue;
}
}
break_loop:
return i; return i;
} }
} }

View File

@@ -8,18 +8,12 @@ public class ImmutableSequence<T> :
IReadOnlyList<T>, IReadOnlyList<T>,
IEquatable<ImmutableSequence<T>> IEquatable<ImmutableSequence<T>>
{ {
private static readonly int InitialHash = typeof(ImmutableSequence<T>).GetHashCode();
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals; private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
private readonly ImmutableList<T> _values; private readonly ImmutableList<T> _values;
public ImmutableSequence() => _values = [];
public ImmutableSequence(ImmutableList<T> values) => _values = values; public ImmutableSequence(ImmutableList<T> values) => _values = values;
public ImmutableSequence() : this(ImmutableArray<T>.Empty) public ImmutableSequence(IEnumerable<T> values) => _values = [..values];
{
}
public ImmutableSequence(IEnumerable<T> values)
{
_values = [..values];
}
public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values)) public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values))
{ {
} }
@@ -37,10 +31,10 @@ public class ImmutableSequence<T> :
} }
} }
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => new(values); protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values];
public ImmutableSequence<T> Add(T value) => ConstructNew([.._values, value]); public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value));
public ImmutableSequence<T> AddFront(T value) => ConstructNew([value, .._values]); public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value));
public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator(); public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator(); IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
@@ -75,7 +69,6 @@ public class ImmutableSequence<T> :
public override int GetHashCode() public override int GetHashCode()
{ {
HashCode hash = new(); HashCode hash = new();
hash.Add(InitialHash);
foreach (var value in _values) foreach (var value in _values)
{ {

View File

@@ -1,38 +1,42 @@
using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
namespace Just.Core; namespace Just.Core;
public static class GuidV8 public static class GuidV8
{ {
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy); public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
[Pure]
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong) public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
{ {
var epoch = dateTime.Subtract(DateTime.UnixEpoch); var epoch = dateTime.Subtract(DateTime.UnixEpoch);
var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10); var timestamp = epoch.Ticks / TicksPrecision;
Span<byte> ts = stackalloc byte[8]; uint tsHigh = (uint)((timestamp >> 16) & 0xFFFFFFFF);
MemoryMarshal.Write(ts, timestamp); ushort tsLow = (ushort)(timestamp & 0x0000FFFF);
Span<byte> bytes = stackalloc byte[16]; Span<byte> bytes = stackalloc byte[10];
ts[0..2].CopyTo(bytes[4..6]);
ts[2..6].CopyTo(bytes[..4]);
if (entropy == RngEntropy.Strong) if (entropy == RngEntropy.Strong)
{ {
RandomNumberGenerator.Fill(bytes[6..]); RandomNumberGenerator.Fill(bytes);
} }
else else
{ {
Random.Shared.NextBytes(bytes[6..]); Random.Shared.NextBytes(bytes);
} }
bytes[7] = (byte)((bytes[7] & 0x0F) | 0x80); bytes[0] = (byte)((bytes[0] & 0x0F) | 0x80); // Version 8
bytes[2] = (byte)((bytes[2] & 0x1F) | 0x80); // Variant 0b1000
return new Guid(bytes); ushort version = (ushort)((bytes[0] << 8) | bytes[1]);
return new Guid(
tsHigh,
tsLow,
version,
bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9]);
} }
} }