Compare commits
5 Commits
30af957fc6
...
v1.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3665abaab8 | |||
| 2b68ba982d | |||
| 43135a5ffb | |||
| 2afd66aa57 | |||
| 71ec5b2f35 |
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: https://github.com/actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 8.x
|
||||
dotnet-version: 9.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore Core/Core.csproj
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: https://github.com/actions/setup-dotnet@v3
|
||||
with:
|
||||
dotnet-version: 8.x
|
||||
dotnet-version: 9.x
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
@@ -30,12 +30,12 @@ jobs:
|
||||
run: dotnet build --no-restore
|
||||
|
||||
- name: Test
|
||||
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-8.x"
|
||||
run: dotnet test --no-build --verbosity normal --logger trx --results-directory "TestResults-9.x"
|
||||
|
||||
- name: Upload dotnet test results
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dotnet-results-8.x
|
||||
path: TestResults-8.x
|
||||
name: dotnet-results-9.x
|
||||
path: TestResults-9.x
|
||||
if: ${{ always() }}
|
||||
retention-days: 30
|
||||
|
||||
@@ -72,7 +72,7 @@ public class Decode
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString)
|
||||
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string? testString)
|
||||
{
|
||||
Base32.Decode(testString).Should().BeEmpty();
|
||||
}
|
||||
|
||||
@@ -23,17 +23,22 @@ public class Encode
|
||||
[InlineData("GSHXGB5ORKNDSFLSU2YWALI=")]
|
||||
[InlineData("TMFSC64ZZNPQSNGCFIODS7TR")]
|
||||
[InlineData("ZTDUBU4QZFFMDJKBII334EIB")]
|
||||
[InlineData("2IO2HTALCXZWCBD2AAAAAAAAAAAA====")]
|
||||
[InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ====")]
|
||||
[InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E")]
|
||||
public void WhenDecodedFromString_ShouldBeEncodedToTheSameString(string testString)
|
||||
{
|
||||
var resultBytes = Base32.Decode(testString);
|
||||
var resultString = Base32.Encode(resultBytes);
|
||||
|
||||
resultString.Should().BeEquivalentTo(testString);
|
||||
resultString.Should().Be(testString);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("FG4M3ZQM3TVWDMBUP5L7N7V3JS7KBM2E", new byte[] { 0x29, 0xb8, 0xcd, 0xe6, 0x0c, 0xdc, 0xeb, 0x61, 0xb0, 0x34, 0x7f, 0x57, 0xf6, 0xfe, 0xbb, 0x4c, 0xbe, 0xa0, 0xb3, 0x44, })]
|
||||
[InlineData("WXYEOQUZULMCY6ZQTDOLTRUZZMKQ====", new byte[] { 0xb5, 0xf0, 0x47, 0x42, 0x99, 0xa2, 0xd8, 0x2c, 0x7b, 0x30, 0x98, 0xdc, 0xb9, 0xc6, 0x99, 0xcb, 0x15, })]
|
||||
[InlineData("2IO2HTALCXZWCBD2AAAAAAAAAAAA====", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, })]
|
||||
[InlineData("2IO2HTALCXZWCBD2AAAAAAAA", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, })]
|
||||
[InlineData("2IO2HTALCXZWCBD2", new byte[] { 0xd2, 0x1d, 0xa3, 0xcc, 0x0b, 0x15, 0xf3, 0x61, 0x04, 0x7a, })]
|
||||
[InlineData("ZFXJMF5N", new byte[] { 0b11001001, 0b01101110, 0b10010110, 0b00010111, 0b10101101, })]
|
||||
[InlineData("CPIKTMY=", new byte[] { 0b00010011, 0b11010000, 0b10101001, 0b10110011, })]
|
||||
@@ -48,9 +53,22 @@ public class Encode
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray)
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[]? testArray)
|
||||
{
|
||||
var actualBase32 = Base32.Encode(testArray);
|
||||
actualBase32?.Should().Be(string.Empty);
|
||||
actualBase32.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[]? testArray)
|
||||
{
|
||||
char[] output = ['1', '2', '3', '4'];
|
||||
|
||||
var charsWritten = Base32.Encode(testArray, output);
|
||||
|
||||
charsWritten.Should().Be(0);
|
||||
output.Should().Equal(['1', '2', '3', '4']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,4 +50,36 @@ public class Decode
|
||||
var result = Base64Url.Decode(testString);
|
||||
result.Should().BeEquivalentTo(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(" ")]
|
||||
[InlineData("hg2&515i3215")]
|
||||
[InlineData("hg712)21")]
|
||||
[InlineData("hg712f 21")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||
public void WhenCalledWithInvalidString_ShouldThrowFormatException(string testString)
|
||||
{
|
||||
Action action = () => Base64Url.Decode(testString);
|
||||
action.Should().Throw<FormatException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("5QrdUxDUV CAEGw8pvLsEw")]
|
||||
[InlineData("6nE2uKQ4$0ar9kpmybgkdw")]
|
||||
[InlineData("PyD6zwDqXkG*S1HPsp41wQ")]
|
||||
[InlineData("!dOlPOh3wEe9PlyQgTMt2g")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1806:Do not ignore method results", Justification = "Test case")]
|
||||
public void WhenCalledWithInvalidGuidString_ShouldThrowFormatException(string testString)
|
||||
{
|
||||
Action action = () => Base64Url.DecodeGuid(testString);
|
||||
action.Should().Throw<FormatException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
public void WhenCalledWithNullString_ShouldReturnEmptyArray(string testString)
|
||||
{
|
||||
Base64Url.Decode(testString).Should().BeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,26 @@ public class Encode
|
||||
var result = Base64Url.Encode(testBytes);
|
||||
result.Should().Be(expected);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnEmptyString(byte[] testArray)
|
||||
{
|
||||
var actualBase32 = Base64Url.Encode(testArray);
|
||||
actualBase32.Should().Be(string.Empty);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[] { })]
|
||||
[InlineData(null)]
|
||||
public void WhenCalledWithEmptyByteArray_ShouldReturnZeroAndNotChangeOutput(byte[] testArray)
|
||||
{
|
||||
char[] output = ['1', '2', '3', '4'];
|
||||
|
||||
var charsWritten = Base64Url.Encode(testArray, output);
|
||||
|
||||
charsWritten.Should().Be(0);
|
||||
output.Should().Equal(['1', '2', '3', '4']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
|
||||
<PackageReference Include="xunit" Version="2.4.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.3">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -3,15 +3,15 @@ namespace Just.Core.Tests.GuidV8Tests;
|
||||
public class NewGuid
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 250000000)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -22,7 +22,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -33,7 +33,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
|
||||
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
|
||||
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
|
||||
@@ -44,7 +44,7 @@ public class NewGuid
|
||||
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
|
||||
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
|
||||
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
5635912, 6673780, 17277183, 17512959, 19098799, 21672621, 30581958, 30824885, 31874213, 35192781,
|
||||
36337094, 37752116, 38387215, 39154682, 40525427, 52288093, 55218356, 59065156, 65231785, 75430932,
|
||||
76289058, 79078058, 85770685, 85925884, 94726743, 94864163, 95781967, 96150006, 96482085, 102570414,
|
||||
@@ -55,7 +55,7 @@ public class NewGuid
|
||||
292563035, 299285016, 303834917, 310357836, 315078337, 316367236, 318311758, 318873972, 319675272, 321784171,
|
||||
324204294, 327667283, 330287252, 338438172, 349863360, 360777768, 366398711, 368637150, 368776734, 371900343,
|
||||
379094084, 379818879, 381448333, 381814627, 382393101, 382483709, 385600870, 389455134, 396115960, 399364095)]
|
||||
public void Guids_Differing_By_Minutes_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds)
|
||||
public void Guids_Differing_By_Minutes_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
|
||||
{
|
||||
var rng = new Random(25);
|
||||
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
||||
@@ -77,15 +77,15 @@ public class NewGuid
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-250000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-250000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -96,7 +96,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -107,7 +107,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
||||
@@ -118,7 +118,7 @@ public class NewGuid
|
||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
||||
@@ -129,7 +129,7 @@ public class NewGuid
|
||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
|
||||
public void Guids_Differing_By_Seconds_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds)
|
||||
public void Guids_Differing_By_Seconds_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
|
||||
{
|
||||
var rng = new Random(25);
|
||||
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
||||
@@ -151,15 +151,15 @@ public class NewGuid
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-250000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-250000000, -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, 5000, 10000, 25000, 100000, 250000, 500000, 1000000, 2000000, 5000000, 10000000, 100000000, 2100000000)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -170,7 +170,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
-9863, -9740, -9214, -8878, -8674, -8652, -8640, -8565, -8518, -8449,
|
||||
-8390, -8193, -8108, -7808, -7501, -7203, -7133, -7020, -6983, -6855,
|
||||
-6576, -6162, -5630, -5505, -5472, -5382, -4706, -4680, -4509, -4454,
|
||||
@@ -181,7 +181,7 @@ public class NewGuid
|
||||
4163, 4198, 4366, 4662, 4746, 4879, 5467, 5601, 5912, 5979,
|
||||
6128, 6277, 6323, 6437, 6699, 6853, 7556, 7776, 7795, 8099,
|
||||
8336, 8592, 8682, 8683, 8818, 8904, 9375, 9466, 9551, 9708)]
|
||||
[InlineData(GuidV8Entropy.Weak,
|
||||
[InlineData(RngEntropy.Weak,
|
||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
||||
@@ -192,7 +192,7 @@ public class NewGuid
|
||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
|
||||
[InlineData(GuidV8Entropy.Strong,
|
||||
[InlineData(RngEntropy.Strong,
|
||||
6629058, 24114993, 40561510, 46245969, 46876997, 48747281, 80489854, 110237218, 117445694, 118974860,
|
||||
135132579, 141760591, 149114066, 158322437, 159065333, 164925904, 173848639, 175086337, 175704556, 176335514,
|
||||
200302773, 207133553, 230088723, 234521706, 239587338, 263755571, 264571928, 290118284, 292346548, 319322378,
|
||||
@@ -203,7 +203,7 @@ public class NewGuid
|
||||
749379956, 757561933, 758668417, 761735205, 770349479, 797570403, 805896481, 809050934, 821655964, 821980469,
|
||||
830824227, 840429528, 851772315, 859717719, 859763860, 867675943, 912124563, 914880620, 923914294, 930298008,
|
||||
932610035, 937468680, 945565998, 949277691, 949397209, 951283050, 953249971, 953953188, 976210158, 982233484, 982233485)]
|
||||
public void Guids_Differing_By_Milliseconds_Should_Be_Sortable(GuidV8Entropy entropy, params int[] seconds)
|
||||
public void Guids_Differing_By_Milliseconds_Should_Be_Sortable(RngEntropy entropy, params int[] seconds)
|
||||
{
|
||||
var rng = new Random(26);
|
||||
var referenceTime = new DateTime(2024, 05, 17, 15, 36, 13, 771, DateTimeKind.Utc);
|
||||
|
||||
140
Core.Tests/SeqIdTests/NextId.cs
Normal file
140
Core.Tests/SeqIdTests/NextId.cs
Normal file
@@ -0,0 +1,140 @@
|
||||
namespace Just.Core.Tests.SeqIdTests;
|
||||
|
||||
public class NextId
|
||||
{
|
||||
private static readonly DateTime TestEpoch = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
private readonly SeqId _seqId = new(TestEpoch);
|
||||
|
||||
private const int TimestampShift = 22; // 63 - 41 = 22
|
||||
private const long TimestampMask = 0x1FFFFFFFFFF; // 41 bits mask
|
||||
private const int SeqShift = 14; // TimestampShift - 8 = 14
|
||||
private const long SeqMask = 0xFF; // 8 bits mask
|
||||
private const long RandMask = 0x3FFF; // 14 bits mask (since 2^14 = 16384)
|
||||
|
||||
[Fact]
|
||||
public void NextId_ShouldHaveCorrectBitStructure()
|
||||
{
|
||||
// Arrange
|
||||
var time = TestEpoch.AddMilliseconds(500);
|
||||
|
||||
// Act
|
||||
long id = _seqId.Next(time);
|
||||
|
||||
// Assert
|
||||
long timestampPart = (id >> TimestampShift) & TimestampMask;
|
||||
long sequencePart = (id >> SeqShift) & SeqMask;
|
||||
long randomPart = id & RandMask;
|
||||
|
||||
timestampPart.Should().Be(500);
|
||||
sequencePart.Should().Be(0);
|
||||
randomPart.Should().BeInRange(0, RandMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_ShouldIncrementSequenceForSameTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var time = TestEpoch.AddMilliseconds(100);
|
||||
|
||||
// Act
|
||||
long id1 = _seqId.Next(time);
|
||||
long id2 = _seqId.Next(time);
|
||||
|
||||
// Assert
|
||||
long sequence1 = (id1 >> SeqShift) & SeqMask;
|
||||
long sequence2 = (id2 >> SeqShift) & SeqMask;
|
||||
|
||||
sequence2.Should().Be(sequence1 + 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_ShouldResetSequenceForNewTimestamp()
|
||||
{
|
||||
// Arrange
|
||||
var time1 = TestEpoch.AddMilliseconds(100);
|
||||
var time2 = time1.AddMilliseconds(1);
|
||||
|
||||
// Act
|
||||
_ = _seqId.Next(time1); // Sequence increments to 0
|
||||
_ = _seqId.Next(time1); // Sequence increments to 1
|
||||
long id = _seqId.Next(time2); // Should reset to 0
|
||||
|
||||
// Assert
|
||||
long sequence = (id >> SeqShift) & SeqMask;
|
||||
sequence.Should().Be(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_ShouldThrowWhenTimestampDecreases()
|
||||
{
|
||||
// Arrange
|
||||
var time1 = TestEpoch.AddMilliseconds(200);
|
||||
var time2 = TestEpoch.AddMilliseconds(100);
|
||||
|
||||
// Act & Assert
|
||||
_seqId.Next(time1); // First call sets last timestamp
|
||||
Action act = () => _seqId.Next(time2);
|
||||
act.Should().Throw<InvalidOperationException>()
|
||||
.WithMessage("Refused to create new SeqId. Last timestamp is in the future.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_ShouldThrowWhenSequenceExhausted()
|
||||
{
|
||||
// Arrange
|
||||
var time = TestEpoch.AddMilliseconds(200);
|
||||
|
||||
// Act & Assert
|
||||
for (int i = 0; i < 255; i++)
|
||||
{
|
||||
_seqId.Next(time); // Exhauste sequence
|
||||
}
|
||||
Action act = () => _seqId.Next(time);
|
||||
act.Should().Throw<IndexOutOfRangeException>()
|
||||
.WithMessage("Refused to create new SeqId. Sequence exhausted.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_WithStrongEntropy_ShouldSetLower14Bits()
|
||||
{
|
||||
// Arrange
|
||||
var time = TestEpoch.AddMilliseconds(300);
|
||||
|
||||
// Act
|
||||
long id = _seqId.Next(time, RngEntropy.Strong);
|
||||
|
||||
// Assert
|
||||
long randomPart = id & RandMask;
|
||||
randomPart.Should().BeInRange(0, RandMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NextId_WithWeakEntropy_ShouldSetLower14Bits()
|
||||
{
|
||||
// Arrange
|
||||
var time = TestEpoch.AddMilliseconds(400);
|
||||
|
||||
// Act
|
||||
long id = _seqId.Next(time, RngEntropy.Weak);
|
||||
|
||||
// Assert
|
||||
long randomPart = id & RandMask;
|
||||
randomPart.Should().BeInRange(0, RandMask);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DefaultInstance_NextId_ShouldUseDefaultEpoch()
|
||||
{
|
||||
// Arrange
|
||||
var now = DateTime.UtcNow;
|
||||
var defaultEpoch = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
long expectedTimestamp = (long)(now - defaultEpoch).TotalMilliseconds;
|
||||
|
||||
// Act
|
||||
long id = SeqId.NextId();
|
||||
|
||||
// Assert
|
||||
long timestampPart = (id >> TimestampShift) & TimestampMask;
|
||||
timestampPart.Should().BeCloseTo(expectedTimestamp & TimestampMask, 1); // Mask handles overflow
|
||||
}
|
||||
}
|
||||
58
Core.Tests/SystemIOStreamExtensionsTests/Populate.cs
Normal file
58
Core.Tests/SystemIOStreamExtensionsTests/Populate.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using Just.Core.Extensions;
|
||||
|
||||
namespace Just.Core.Tests.SystemIOStreamExtensionsTests;
|
||||
|
||||
public class Populate
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(0, 3)]
|
||||
[InlineData(0, 5)]
|
||||
[InlineData(3, 0)]
|
||||
[InlineData(3, 1)]
|
||||
[InlineData(3, 5)]
|
||||
[InlineData(5, 0)]
|
||||
[InlineData(5, 1)]
|
||||
[InlineData(5, 5)]
|
||||
public void WhenCalled_ShouldPopulateSpecifiedRange(int offset, int length)
|
||||
{
|
||||
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[10];
|
||||
|
||||
stream.Populate(buffer, offset, length);
|
||||
|
||||
buffer.Skip(offset).Take(length).Should().Equal(streamContent.Take(length));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 4)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 4)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 5)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 5)]
|
||||
public void WhenStreamContainsSameOrGreaterAmmountOfItems_ShouldPopulateBuffer(byte[] streamContent, int bufferSize)
|
||||
{
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
stream.Populate(buffer);
|
||||
|
||||
buffer.Should().Equal(streamContent.Take(bufferSize));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 5)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 6)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 10)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 9)]
|
||||
public void WhenStreamContainsLessItems_ShouldThrowEndOfStreamException(byte[] streamContent, int bufferSize)
|
||||
{
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
Action action = () => stream.Populate(buffer);
|
||||
|
||||
action.Should().Throw<EndOfStreamException>();
|
||||
}
|
||||
}
|
||||
72
Core.Tests/SystemIOStreamExtensionsTests/PopulateAsync.cs
Normal file
72
Core.Tests/SystemIOStreamExtensionsTests/PopulateAsync.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Just.Core.Extensions;
|
||||
|
||||
namespace Just.Core.Tests.SystemIOStreamExtensionsTests;
|
||||
|
||||
public class PopulateAsync
|
||||
{
|
||||
[Fact]
|
||||
public async Task WhenCancellationRequested_ShouldThrowOperationCanceledException()
|
||||
{
|
||||
using var cts = new CancellationTokenSource();
|
||||
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[5];
|
||||
|
||||
Func<Task> action = async () => await stream.PopulateAsync(buffer, cts.Token);
|
||||
cts.Cancel();
|
||||
|
||||
await action.Should().ThrowAsync<OperationCanceledException>();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0, 0)]
|
||||
[InlineData(0, 1)]
|
||||
[InlineData(0, 3)]
|
||||
[InlineData(0, 5)]
|
||||
[InlineData(3, 0)]
|
||||
[InlineData(3, 1)]
|
||||
[InlineData(3, 5)]
|
||||
[InlineData(5, 0)]
|
||||
[InlineData(5, 1)]
|
||||
[InlineData(5, 5)]
|
||||
public async Task WhenCalled_ShouldPopulateSpecifiedRange(int offset, int length)
|
||||
{
|
||||
byte[] streamContent = [0x01, 0x02, 0x03, 0x04, 0x05,];
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[10];
|
||||
|
||||
await stream.PopulateAsync(buffer, offset, length);
|
||||
|
||||
buffer.Skip(offset).Take(length).Should().Equal(streamContent.Take(length));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 4)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 4)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 5)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 5)]
|
||||
public async Task WhenStreamContainsSameOrGreaterAmmountOfItems_ShouldPopulateBuffer(byte[] streamContent, int bufferSize)
|
||||
{
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
await stream.PopulateAsync(buffer);
|
||||
|
||||
buffer.Should().Equal(streamContent.Take(bufferSize));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, }, 5)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 6)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, }, 10)]
|
||||
[InlineData(new byte[]{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, }, 9)]
|
||||
public async Task WhenStreamContainsLessItems_ShouldThrowEndOfStreamException(byte[] streamContent, int bufferSize)
|
||||
{
|
||||
using var stream = new MemoryStream(streamContent);
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
Func<Task> action = async () => await stream.PopulateAsync(buffer);
|
||||
|
||||
await action.Should().ThrowAsync<EndOfStreamException>();
|
||||
}
|
||||
}
|
||||
@@ -4,17 +4,17 @@ public static class Base32
|
||||
{
|
||||
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
public const char Padding = '=';
|
||||
public const int MaxBytesStack = 250;
|
||||
|
||||
[Pure]
|
||||
public static string Encode(ReadOnlySpan<byte> input)
|
||||
{
|
||||
if (input.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (input.IsEmpty) return string.Empty;
|
||||
|
||||
int outLength = 8 * ((input.Length + 4) / 5);
|
||||
Span<char> output = stackalloc char[outLength];
|
||||
Span<char> output = input.Length <= MaxBytesStack
|
||||
? stackalloc char[outLength]
|
||||
: new char[outLength];
|
||||
|
||||
_ = Encode(input, output);
|
||||
|
||||
@@ -24,6 +24,8 @@ public static class Base32
|
||||
[Pure]
|
||||
public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
|
||||
{
|
||||
if (input.IsEmpty) return 0;
|
||||
|
||||
int i = 0;
|
||||
ReadOnlySpan<char> alphabet = Alphabet;
|
||||
for (int offset = 0; offset < input.Length;)
|
||||
@@ -45,9 +47,13 @@ public static class Base32
|
||||
[Pure]
|
||||
public static byte[] Decode(ReadOnlySpan<char> input)
|
||||
{
|
||||
input = input.TrimEnd(Padding);
|
||||
if (input.IsEmpty) return [];
|
||||
|
||||
Span<byte> output = stackalloc byte[5 * input.Length / 8];
|
||||
var outputLength = 5 * ((input.Length + 7) / 8);
|
||||
Span<byte> output = outputLength <= MaxBytesStack
|
||||
? stackalloc byte[outputLength]
|
||||
: new byte[outputLength];
|
||||
|
||||
var size = Decode(input, output);
|
||||
|
||||
@@ -58,7 +64,14 @@ public static class Base32
|
||||
public static int Decode(ReadOnlySpan<char> input, Span<byte> output)
|
||||
{
|
||||
input = input.TrimEnd(Padding);
|
||||
Span<char> inputspan = stackalloc char[input.Length];
|
||||
|
||||
var outputLength = 5 * ((input.Length + 7) / 8);
|
||||
output = output[..outputLength];
|
||||
output.Clear();
|
||||
|
||||
Span<char> inputspan = outputLength <= MaxBytesStack
|
||||
? stackalloc char[input.Length]
|
||||
: new char[input.Length];
|
||||
input.ToUpperInvariant(inputspan);
|
||||
|
||||
int bitIndex = 0;
|
||||
@@ -104,7 +117,6 @@ public static class Base32
|
||||
return outputIndex + (outputBits + 7) / 8;
|
||||
}
|
||||
|
||||
|
||||
// returns the number of bytes that were output
|
||||
[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)
|
||||
|
||||
@@ -95,6 +95,8 @@ public static class Base64Url
|
||||
|
||||
[Pure] public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
|
||||
{
|
||||
if (input.IsEmpty) return 0;
|
||||
|
||||
var charlen = 8 * ((input.Length + 5) / 6);
|
||||
Span<char> chars = stackalloc char[charlen];
|
||||
Convert.TryToBase64Chars(input, chars, out int charsWritten);
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Immutable;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Just.Core.Collections;
|
||||
|
||||
public class ImmutableSequence<T> :
|
||||
IEnumerable<T>,
|
||||
IReadOnlyList<T>,
|
||||
IEquatable<ImmutableSequence<T>>,
|
||||
IEqualityOperators<ImmutableSequence<T>, ImmutableSequence<T>, bool>
|
||||
IEquatable<ImmutableSequence<T>>
|
||||
{
|
||||
private static readonly int InitialHash = typeof(ImmutableSequence<T>).GetHashCode();
|
||||
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Just.Core</AssemblyName>
|
||||
@@ -10,7 +10,7 @@
|
||||
<Description>Small .Net library with useful helper classes, functions and extensions.</Description>
|
||||
<PackageTags>extensions;helpers;helper-functions</PackageTags>
|
||||
<Authors>JustFixMe</Authors>
|
||||
<Copyright>Copyright (c) 2023 JustFixMe</Copyright>
|
||||
<Copyright>Copyright (c) 2023-2025 JustFixMe</Copyright>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
|
||||
|
||||
@@ -1,46 +1,114 @@
|
||||
namespace Just.Core.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
|
||||
/// </summary>
|
||||
public static class SystemIOStreamExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the specified section of the buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="offset">The starting offset in the buffer</param>
|
||||
/// <param name="length">The number of bytes to read</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, byte[] buffer, int offset, int length)
|
||||
=> stream.Populate(buffer.AsSpan(offset, length));
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the entire buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, byte[] buffer)
|
||||
=> stream.Populate(buffer.AsSpan());
|
||||
/// <summary>
|
||||
/// Reads data from the stream until the specified span is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The span to populate</param>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static void Populate(this Stream stream, Span<byte> buffer)
|
||||
{
|
||||
do
|
||||
{
|
||||
var readed = stream.Read(buffer);
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
if (readed == 0)
|
||||
while (!buffer.IsEmpty)
|
||||
{
|
||||
var bytesRead = stream.Read(buffer);
|
||||
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
buffer = buffer[readed..];
|
||||
buffer = buffer[bytesRead..];
|
||||
}
|
||||
while (buffer.Length > 0);
|
||||
}
|
||||
|
||||
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
|
||||
=> await stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
|
||||
public static async ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
|
||||
=> await stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the entire buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
|
||||
=> stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the specified section of the buffer is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The buffer to populate</param>
|
||||
/// <param name="offset">The starting offset in the buffer</param>
|
||||
/// <param name="length">The number of bytes to read</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
|
||||
=> stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
|
||||
/// <summary>
|
||||
/// Asynchronously reads data from the stream until the specified memory region is filled.
|
||||
/// </summary>
|
||||
/// <param name="stream">The stream to read from</param>
|
||||
/// <param name="buffer">The memory region to populate</param>
|
||||
/// <param name="cancellationToken">Cancellation token</param>
|
||||
/// <returns>A ValueTask representing the asynchronous operation</returns>
|
||||
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
|
||||
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
|
||||
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
|
||||
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
|
||||
public static async ValueTask PopulateAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default)
|
||||
{
|
||||
do
|
||||
ArgumentNullException.ThrowIfNull(stream);
|
||||
|
||||
while (!buffer.IsEmpty)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var readed = await stream.ReadAsync(buffer, cancellationToken);
|
||||
var bytesRead = await stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (readed == 0)
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
throw new EndOfStreamException();
|
||||
}
|
||||
|
||||
buffer = buffer[readed..];
|
||||
}
|
||||
while (buffer.Length > 0);
|
||||
buffer = buffer[bytesRead..];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,13 @@ using System.Security.Cryptography;
|
||||
|
||||
namespace Just.Core;
|
||||
|
||||
public enum GuidV8Entropy { Strong, Weak }
|
||||
|
||||
public static class GuidV8
|
||||
{
|
||||
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Guid NewGuid(GuidV8Entropy entropy = GuidV8Entropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
||||
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
|
||||
|
||||
[Pure]
|
||||
public static Guid NewGuid(DateTime dateTime, GuidV8Entropy entropy = GuidV8Entropy.Strong)
|
||||
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
||||
{
|
||||
var epoch = dateTime.Subtract(DateTime.UnixEpoch);
|
||||
var timestamp = epoch.Ticks / (TimeSpan.TicksPerMillisecond / 10);
|
||||
@@ -24,7 +22,7 @@ public static class GuidV8
|
||||
ts[0..2].CopyTo(bytes[4..6]);
|
||||
ts[2..6].CopyTo(bytes[..4]);
|
||||
|
||||
if (entropy == GuidV8Entropy.Strong)
|
||||
if (entropy == RngEntropy.Strong)
|
||||
{
|
||||
RandomNumberGenerator.Fill(bytes[6..]);
|
||||
}
|
||||
|
||||
16
Core/RngEntropy.cs
Normal file
16
Core/RngEntropy.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Just.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the quality of random entropy used in ID generation
|
||||
/// </summary>
|
||||
public enum RngEntropy
|
||||
{
|
||||
/// <summary>
|
||||
/// Cryptographically secure random numbers (slower but collision-resistant)
|
||||
/// </summary>
|
||||
Strong,
|
||||
/// <summary>
|
||||
/// Standard pseudo-random numbers (faster but less collision-resistant)
|
||||
/// </summary>
|
||||
Weak
|
||||
}
|
||||
121
Core/SeqId.cs
Normal file
121
Core/SeqId.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Just.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Generates time-based sequential IDs with entropy
|
||||
/// <para>ID Structure (64 bits):</para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>[1 bit] Always 0 (positive signed longs)</description></item>
|
||||
/// <item><description>[41 bits] Milliseconds since epoch (covers ~69 years)</description></item>
|
||||
/// <item><description>[8 bits] Sequence counter (0-255 per millisecond)</description></item>
|
||||
/// <item><description>[14 bits] Random entropy (0-16383)</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Important behaviors:
|
||||
/// <list type="bullet">
|
||||
/// <item><description>Guarantees monotonic ordering within same millisecond</description></item>
|
||||
/// <item><description>Throws <see cref="InvalidOperationException"/> on backward time jumps</description></item>
|
||||
/// <item><description>Sequence resets when timestamp advances</description></item>
|
||||
/// <item><description>Thread-safe through locking</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class SeqId(DateTime epoch)
|
||||
{
|
||||
private const int TimestampBits = 41;
|
||||
private const int TimestampShift = 63 - TimestampBits;
|
||||
private const long TimestampMask = 0x000001FF_FFFFFFFF;
|
||||
|
||||
private const int SeqBits = 8;
|
||||
private const int SeqShift = TimestampShift - SeqBits;
|
||||
private const long SeqMask = 0x00000000_000000FF;
|
||||
|
||||
private const int RandExclusiveUpper = 1 << SeqShift;
|
||||
|
||||
/// <summary>
|
||||
/// Default epoch (2025-01-01 UTC) used for <see cref="Default"/> instance
|
||||
/// </summary>
|
||||
public static DateTime DefaultEpoch { get; } = new(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
/// <summary>
|
||||
/// Default instance using <see cref="DefaultEpoch"/>
|
||||
/// </summary>
|
||||
public static SeqId Default { get; } = new(DefaultEpoch);
|
||||
|
||||
/// <summary>
|
||||
/// Generates ID using default instance and current UTC time
|
||||
/// </summary>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy);
|
||||
|
||||
#if NET9_0_OR_GREATER
|
||||
private readonly Lock _lock = new();
|
||||
#else
|
||||
private readonly object _lock = new();
|
||||
#endif
|
||||
|
||||
private readonly DateTime _epoch = epoch;
|
||||
private int _seqId = 0;
|
||||
private long _lastTimestamp = -1L;
|
||||
|
||||
/// <summary>
|
||||
/// Generates next ID using current UTC time
|
||||
/// </summary>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy);
|
||||
|
||||
/// <summary>
|
||||
/// Generates next ID with explicit timestamp
|
||||
/// </summary>
|
||||
/// <param name="dateTime">Timestamp basis for ID generation</param>
|
||||
/// <param name="entropy">Entropy quality (default: Strong)</param>
|
||||
/// <returns>64-bit sequential ID with random component</returns>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// Thrown if <paramref name="dateTime"/> is earlier than last used timestamp
|
||||
/// </exception>
|
||||
/// <exception cref="IndexOutOfRangeException">
|
||||
/// Thrown if more than 255 IDs generated in 1ms
|
||||
/// </exception>
|
||||
public long Next(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
|
||||
{
|
||||
var epoch = dateTime.Subtract(_epoch);
|
||||
var timestamp = ((epoch.Ticks / TimeSpan.TicksPerMillisecond) & TimestampMask) << TimestampShift;
|
||||
|
||||
long currentSeq;
|
||||
lock (_lock)
|
||||
{
|
||||
if (timestamp > _lastTimestamp)
|
||||
{
|
||||
_lastTimestamp = timestamp;
|
||||
_seqId = 0;
|
||||
}
|
||||
else if (timestamp < _lastTimestamp)
|
||||
{
|
||||
throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future.");
|
||||
}
|
||||
|
||||
if (_seqId == SeqMask)
|
||||
{
|
||||
throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted.");
|
||||
}
|
||||
|
||||
currentSeq = ((_seqId++) & SeqMask) << SeqShift;
|
||||
}
|
||||
|
||||
long currentRand = entropy == RngEntropy.Strong
|
||||
? RandomNumberGenerator.GetInt32(RandExclusiveUpper)
|
||||
: Random.Shared.Next(RandExclusiveUpper);
|
||||
|
||||
return timestamp | currentSeq | currentRand;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user