diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..ea443389a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,583 @@ +--- +description: 'Writing Unit Tests in Cuemon' +applyTo: "**/*.{cs,csproj}" +--- + +# Writing Unit Tests in Cuemon +This document provides instructions for writing unit tests in the Cuemon codebase. Please follow these guidelines to ensure consistency and maintainability. + +## 1. Base Class + +**Always inherit from the `Test` base class** for all unit test classes. +This ensures consistent setup, teardown, and output handling across all tests. + +> Important: Do NOT add `using Xunit.Abstractions`. xUnit v3 no longer exposes that namespace; including it is incorrect and will cause compilation errors. Use the `Codebelt.Extensions.Xunit` Test base class and `using Xunit;` as shown in the examples below. If you need access to test output, rely on the Test base class (which accepts the appropriate output helper) rather than importing `Xunit.Abstractions`. + +```csharp +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Your.Namespace +{ + public class YourTestClass : Test + { + public YourTestClass(ITestOutputHelper output) : base(output) + { + } + + // Your tests here + } +} +``` + +## 2. Test Method Attributes + +- Use `[Fact]` for standard unit tests. +- Use `[Theory]` with `[InlineData]` or other data sources for parameterized tests. + +## 3. Naming Conventions + +- **Test classes**: End with `Test` (e.g., `DateSpanTest`). +- **Test methods**: Use descriptive names that state the expected behavior (e.g., `ShouldReturnTrue_WhenConditionIsMet`). + +## 4. Assertions + +- Use `Assert` methods from xUnit for all assertions. +- Prefer explicit and expressive assertions (e.g., `Assert.Equal`, `Assert.NotNull`, `Assert.Contains`). + +## 5. File and Namespace Organization + +- Place test files in the appropriate test project and folder structure. +- Use namespaces that mirror the source code structure. The namespace of a test file MUST match the namespace of the System Under Test (SUT). Do NOT append ".Tests", ".Benchmarks" or similar suffixes to the namespace. Only the assembly/project name should indicate that the file is a test/benchmark (for example: Cuemon.Foo.Tests assembly, but namespace Cuemon.Foo). + - Example: If the SUT class is declared as: + ```csharp + namespace Cuemon.Foo.Bar + { + public class Zoo { /* ... */ } + } + ``` + then the corresponding unit test class must use the exact same namespace: + ```csharp + namespace Cuemon.Foo.Bar + { + public class ZooTest : Test { /* ... */ } + } + ``` + - Do NOT use: + ```csharp + namespace Cuemon.Foo.Bar.Tests { /* ... */ } // ❌ + namespace Cuemon.Foo.Bar.Benchmarks { /* ... */ } // ❌ + ``` +- The unit tests for the Cuemon.Foo assembly live in the Cuemon.Foo.Tests assembly. +- The functional tests for the Cuemon.Foo assembly live in the Cuemon.Foo.FunctionalTests assembly. +- Test class names end with Test and live in the same namespace as the class being tested, e.g., the unit tests for the Boo class that resides in the Cuemon.Foo assembly would be named BooTest and placed in the Cuemon.Foo namespace in the Cuemon.Foo.Tests assembly. +- Modify the associated .csproj file to override the root namespace so the compiled namespace matches the SUT. Example: + ```xml + + Cuemon.Foo + + ``` +- When generating test scaffolding automatically, resolve the SUT's namespace from the source file (or project/assembly metadata) and use that exact namespace in the test file header. + +- Notes: + - This rule ensures type discovery and XML doc links behave consistently and reduces confusion when reading tests. + - Keep folder structure aligned with the production code layout to make locating SUT <-> test pairs straightforward. + +## 6. Example Test + +```csharp +using System; +using System.Globalization; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon +{ + /// + /// Tests for the class. + /// + public class DateSpanTest : Test + { + public DateSpanTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Parse_ShouldGetOneMonthOfDifference_UsingIso8601String() + { + var start = new DateTime(2021, 3, 5).ToString("O"); + var end = new DateTime(2021, 4, 5).ToString("O"); + + var span = DateSpan.Parse(start, end); + + Assert.Equal("0:01:31:00:00:00.0", span.ToString()); + Assert.Equal(0, span.Years); + Assert.Equal(1, span.Months); + Assert.Equal(31, span.Days); + Assert.Equal(0, span.Hours); + Assert.Equal(0, span.Minutes); + Assert.Equal(0, span.Seconds); + Assert.Equal(0, span.Milliseconds); + + Assert.Equal(0.08493150684931507, span.TotalYears); + Assert.Equal(1, span.TotalMonths); + Assert.Equal(31, span.TotalDays); + Assert.Equal(744, span.TotalHours); + Assert.Equal(44640, span.TotalMinutes); + Assert.Equal(2678400, span.TotalSeconds); + Assert.Equal(2678400000, span.TotalMilliseconds); + + Assert.Equal(6, span.GetWeeks()); + Assert.Equal(-1566296493, span.GetHashCode()); + + TestOutput.WriteLine(span.ToString()); + } + } +} +``` + +## 7. Additional Guidelines + +- Keep tests focused and isolated. +- Do not rely on external systems except for xUnit itself and Codebelt.Extensions.Xunit (and derived from this). +- Ensure tests are deterministic and repeatable. + +## 8. Test Doubles + +- Preferred test doubles include dummies, fakes, stubs and spies if and when the design allows it. +- Under special circumstances, mock can be used (using Moq library). +- Before overriding methods, verify that the method is virtual or abstract; this rule also applies to mocks. +- Never mock IMarshaller; always use a new instance of JsonMarshaller. + +For further examples, refer to existing test files such as +[`test/Cuemon.Core.Tests/DisposableTest.cs`](test/Cuemon.Core.Tests/DisposableTest.cs) and [`test/Cuemon.Core.Tests/Security/HashFactoryTest.cs`](test/Cuemon.Core.Tests/Security/HashFactoryTest.cs). + +--- +description: 'Writing Performance Tests in Cuemon' +applyTo: "tuning/**, **/*Benchmark*.cs" +--- + +# Writing Performance Tests in Cuemon +This document provides guidance for writing performance tests (benchmarks) in the Cuemon codebase using BenchmarkDotNet. Follow these guidelines to keep benchmarks consistent, readable, and comparable. + +## 1. Naming and Placement + +- Place micro- and component-benchmarks under the `tuning/` folder or in projects named `*.Benchmarks`. +- Place benchmark files in the appropriate benchmark project and folder structure. +- Use namespaces that mirror the source code structure, e.g. do not suffix with `Benchmarks`. +- Namespace rule: DO NOT append `.Benchmarks` to the namespace. Benchmarks must live in the same namespace as the production assembly. Example: if the production assembly uses `namespace Cuemon.Security.Cryptography`, the benchmark file should also use: + ``` + namespace Cuemon.Security.Cryptography + { + public class Sha512256Benchmark { /* ... */ } + } + ``` +The class name must end with `Benchmark`, but the namespace must match the assembly (no `.Benchmarks` suffix). +- The benchmarks for the Cuemon.Bar assembly live in the Cuemon.Bar.Benchmarks assembly. +- Benchmark class names end with Benchmark and live in the same namespace as the class being measured, e.g., the benchmarks for the Zoo class that resides in the Cuemon.Bar assembly would be named ZooBenchmark and placed in the Cuemon.Bar namespace in the Cuemon.Bar.Benchmarks assembly. +- Modify the associated .csproj file to override the root namespace, e.g., Cuemon.Bar. + +## 2. Attributes and Configuration + +- Use `BenchmarkDotNet` attributes to express intent and collect relevant metrics: + - `[MemoryDiagnoser]` to capture memory allocations. + - `[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]` to group related benchmarks. + - `[Params]` for input sizes or variations to exercise multiple scenarios. + - `[GlobalSetup]` for one-time initialization that's not part of measured work. + - `[Benchmark]` on methods representing measured operations; consider `Baseline = true` and `Description` to improve report clarity. +- Keep benchmark configuration minimal and explicit; prefer in-class attributes over large shared configs unless re-used widely. + +## 3. Structure and Best Practices + +- Keep benchmarks focused: each `Benchmark` method should measure a single logical operation. +- Avoid doing expensive setup work inside a measured method; use `[GlobalSetup]`, `[IterationSetup]`, or cached fields instead. +- Use `Params` to cover micro, mid and macro input sizes (for example: small, medium, large) and verify performance trends across them. +- Use small, deterministic data sets and avoid external systems (network, disk, DB). If external systems are necessary, mark them clearly and do not include them in CI benchmark runs by default. +- Capture results that are meaningful: time, allocations, and if needed custom counters. Prefer `MemoryDiagnoser` and descriptive `Description` values. + +## 4. Naming Conventions for Methods + +- Method names should be descriptive and indicate the scenario, e.g., `Parse_Short`, `ComputeHash_Large`. +- When comparing implementations, mark one method with `Baseline = true` and use similar names so reports are easy to read. + +## 5. Example Benchmark + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +## 6. Reporting and CI + +- Benchmarks are primarily for local and tuning runs; be cautious about running heavy BenchmarkDotNet workloads in CI. Prefer targeted runs or harnesses for CI where appropriate. +- Keep benchmark projects isolated (e.g., `tuning/*.csproj`) so they don't affect package builds or production artifacts. + +## 7. Additional Guidelines + +- Keep benchmarks readable and well-documented; add comments explaining non-obvious choices. +- If a benchmark exposes regressions or optimizations, add a short note in the benchmark file referencing the relevant issue or PR. +- For any shared helpers for benchmarking, prefer small utility classes inside the `tuning` projects rather than cross-cutting changes to production code. + +For further examples, refer to the benchmark files under the `tuning/` folder such as `tuning/Cuemon.Core.Benchmarks/DateSpanBenchmark.cs` and `tuning/Cuemon.Security.Cryptography.Benchmarks/Sha512256Benchmark.cs`. + +--- +description: 'Writing XML documentation in Cuemon' +applyTo: "**/*.cs" +--- + +# Writing XML documentation in Cuemon + +This document provides instructions for writing XML documentation. + +## 1. Documentation Style + +- Use the same documentation style as found throughout the codebase. +- Add XML doc comments to public and protected classes and methods where appropriate. +- Example: + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using Cuemon.Collections.Generic; +using Cuemon.Configuration; +using Cuemon.IO; +using Cuemon.Text; + +namespace Cuemon.Security +{ + /// + /// Represents the base class from which all implementations of hash algorithms and checksums should derive. + /// + /// The type of the configured options. + /// + /// + /// + public abstract class Hash : Hash, IConfigurable where TOptions : ConvertibleOptions, new() + { + /// + /// Initializes a new instance of the class. + /// + /// The which may be configured. + protected Hash(Action setup) + { + Options = Patterns.Configure(setup); + } + + /// + /// Gets the configured options of this instance. + /// + /// The configured options of this instance. + public TOptions Options { get; } + + + /// + /// The endian-initializer of this instance. + /// + /// An instance of the configured options. + protected sealed override void EndianInitializer(EndianOptions options) + { + options.ByteOrder = Options.ByteOrder; + } + } + + /// + /// Represents the base class that defines the public facing structure to expose. + /// + /// + public abstract class Hash : IHash + { + /// + /// Initializes a new instance of the class. + /// + protected Hash() + { + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(bool input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(byte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(char input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DateTime input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(DBNull input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(decimal input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(double input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(short input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(int input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(long input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(sbyte input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(float input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ushort input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(uint input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(ulong input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// The which may be configured. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(string input, Action setup = null) + { + return ComputeHash(Convertible.GetBytes(input, setup)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Enum input) + { + return ComputeHash(Convertible.GetBytes(input, EndianInitializer)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(params IConvertible[] input) + { + return ComputeHash(Arguments.ToEnumerableOf(input)); + } + + /// + /// Computes the hash value for the specified sequence of . + /// + /// The sequence of to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(IEnumerable input) + { + return ComputeHash(Convertible.GetBytes(input)); + } + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public abstract HashResult ComputeHash(byte[] input); + + /// + /// Computes the hash value for the specified . + /// + /// The to compute the hash code for. + /// A containing the computed hash code of the specified . + public virtual HashResult ComputeHash(Stream input) + { + return ComputeHash(Patterns.SafeInvoke(() => new MemoryStream(), destination => + { + Decorator.Enclose(input).CopyStream(destination); + return destination; + }).ToArray()); + } + + /// + /// Defines the initializer that must implement. + /// + /// An instance of the configured options. + protected abstract void EndianInitializer(EndianOptions options); + } +} + +namespace Cuemon.Security +{ + /// + /// Configuration options for . + /// + public class FowlerNollVoOptions : ConvertibleOptions + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The following table shows the initial property values for an instance of . + /// + /// + /// Property + /// Initial Value + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public FowlerNollVoOptions() + { + Algorithm = FowlerNollVoAlgorithm.Fnv1a; + ByteOrder = Endianness.BigEndian; + } + + /// + /// Gets or sets the algorithm of the Fowler-Noll-Vo hash function. + /// + /// The algorithm of the Fowler-Noll-Vo hash function. + public FowlerNollVoAlgorithm Algorithm { get; set; } + } +} +``` diff --git a/.github/prompts/benchmark.prompt.md b/.github/prompts/benchmark.prompt.md new file mode 100644 index 000000000..74c99c60f --- /dev/null +++ b/.github/prompts/benchmark.prompt.md @@ -0,0 +1,165 @@ +--- +mode: agent +description: 'Writing Performance Benchmarks in Cuemon' +--- + +# Benchmark Fixture Prompt (Cuemon Tuning Benchmarks) + +This prompt defines how to generate performance tests (“benchmarks”) for the Cuemon codebase using BenchmarkDotNet. +Benchmarks in Cuemon are *not* unit tests — they are micro- or component-level performance measurements that belong under the `tuning/` directory and follow strict conventions. + +Copilot must follow these guidelines when generating benchmark fixtures. + +--- + +## 1. Naming and Placement + +- All benchmark projects live under the `tuning/` folder. + Examples: + - `tuning/Cuemon.Core.Benchmarks/` + - `tuning/Cuemon.Security.Benchmarks/` + +- **Namespaces must NOT end with `.Benchmarks`.** + They must mirror the production assembly’s namespace. + + Example: + If benchmarking a type inside `Cuemon.Security.Cryptography`, then: + + ```csharp + namespace Cuemon.Security.Cryptography + { + public class Sha512256Benchmark { … } + } + ``` + +* **Benchmark class names must end with `Benchmark`.** + Example: `DateSpanBenchmark`, `FowlerNollVoBenchmark`. + +* Benchmark files should be located in the matching benchmark project + (e.g., benchmarks for `Cuemon.Security.Cryptography` go in `Cuemon.Security.Cryptography.Benchmarks.csproj`). + +* In the `.csproj` for each benchmark project, set the root namespace to the production namespace: + + ```xml + Cuemon.Security.Cryptography + ``` + +--- + +## 2. Attributes and Configuration + +Each benchmark class should use: + +```csharp +[MemoryDiagnoser] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +``` + +Optional but strongly recommended where meaningful: + +* `[Params(...)]` — define small, medium, large input sizes. +* `[GlobalSetup]` — deterministic initialization of benchmark data. +* `[Benchmark(Description = "...")]` — always add descriptions. +* `[Benchmark(Baseline = true)]` — when comparing two implementations. + +Avoid complex global configs; prefer explicit attributes inside the class. + +--- + +## 3. Structure and Best Practices + +A benchmark fixture must: + +* Measure a **single logical operation** per benchmark method. +* Avoid I/O, networking, disk access, logging, or side effects. +* Avoid expensive setup inside `[Benchmark]` methods. +* Use deterministic data (e.g., seeded RNG or predefined constants). +* Use `[GlobalSetup]` to allocate buffers, random payloads, or reusable test data only once. +* Avoid shared mutable state unless reset per iteration. + +Use representative input sizes such as: + +```csharp +[Params(8, 256, 4096)] +public int Count { get; set; } +``` + +BenchmarkDotNet will run each benchmark for each parameter value. + +--- + +## 4. Method Naming Conventions + +Use descriptive names that communicate intent: + +* `Parse_Short` +* `Parse_Long` +* `ComputeHash_Small` +* `ComputeHash_Large` +* `Serialize_Optimized` +* `Serialize_Baseline` + +When comparing approaches, always list them clearly and tag one as the baseline. + +--- + +## 5. Example Benchmark Fixture + +```csharp +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class SampleOperationBenchmark + { + [Params(8, 256, 4096)] + public int Count { get; set; } + + private byte[] _payload; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Count]; + // deterministic initialization + } + + [Benchmark(Baseline = true, Description = "Operation - baseline")] + public int Operation_Baseline() => SampleOperation.Process(_payload); + + [Benchmark(Description = "Operation - optimized")] + public int Operation_Optimized() => SampleOperation.ProcessOptimized(_payload); + } +} +``` + +--- + +## 6. Reporting and CI + +* Benchmark projects live exclusively under `tuning/`. They must not affect production builds. +* Heavy BenchmarkDotNet runs should *not* run in CI unless explicitly configured. +* Reports are produced by the Cuemon benchmark runner and stored under its configured artifacts directory. + +--- + +## 7. Additional Guidelines + +* Keep benchmark fixtures focused and readable. +* Document non-obvious reasoning in short comments. +* Prefer realistic but deterministic data sets. +* When benchmarks reveal regressions or improvements, reference the associated PR or issue in a comment. +* Shared benchmark helpers belong in `tuning/` projects, not in production code. + +--- + +## Final Notes + +* Benchmarks are performance tests, not unit tests. +* Use `[Benchmark]` only for pure performance measurement. +* Avoid `MethodImplOptions.NoInlining` unless absolutely necessary. +* Use small sets of meaningful benchmark scenarios — avoid combinatorial explosion. + diff --git a/.github/workflows/pipelines.yml b/.github/workflows/ci-pipeline.yml similarity index 97% rename from .github/workflows/pipelines.yml rename to .github/workflows/ci-pipeline.yml index dbf690e6c..959ae6317 100644 --- a/.github/workflows/pipelines.yml +++ b/.github/workflows/ci-pipeline.yml @@ -2,11 +2,6 @@ name: Cuemon CI/CD Pipeline on: pull_request: branches: [main] - paths-ignore: - - .codecov/** - - .docfx/** - - .nuget/** - - '**/*.md' workflow_dispatch: inputs: configuration: @@ -73,11 +68,12 @@ jobs: strategy: fail-fast: false matrix: + os: [ubuntu-24.04, ubuntu-24.04-arm] configuration: [Debug, Release] project: ${{ fromJson(needs.prepare_test.outputs.json) }} uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: - runs-on: ubuntu-24.04 + runs-on: ${{ matrix.os }} configuration: ${{ matrix.configuration }} projects: ${{ matrix.project }} build: true # we need to build due to xUnitv3 @@ -89,11 +85,12 @@ jobs: strategy: fail-fast: false matrix: + os: [windows-2025, windows-11-arm] configuration: [Debug, Release] project: ${{ fromJson(needs.prepare_test.outputs.json) }} uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3 with: - runs-on: windows-2022 + runs-on: ${{ matrix.os }} configuration: ${{ matrix.configuration }} projects: ${{ matrix.project }} test-arguments: -- RunConfiguration.DisableAppDomain=true diff --git a/.nuget/Cuemon.AspNetCore.App/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore.App/PackageReleaseNotes.txt index eedd92bcf..796611c0f 100644 --- a/.nuget/Cuemon.AspNetCore.App/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore.App/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt index 0ba059155..f84fd6b4a 100644 --- a/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore.Authentication/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.AspNetCore.Mvc/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore.Mvc/PackageReleaseNotes.txt index 7e16140c2..50b85f21a 100644 --- a/.nuget/Cuemon.AspNetCore.Mvc/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore.Mvc/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.AspNetCore.Razor.TagHelpers/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore.Razor.TagHelpers/PackageReleaseNotes.txt index eedd92bcf..796611c0f 100644 --- a/.nuget/Cuemon.AspNetCore.Razor.TagHelpers/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore.Razor.TagHelpers/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.AspNetCore/PackageReleaseNotes.txt b/.nuget/Cuemon.AspNetCore/PackageReleaseNotes.txt index 81e5aba56..908260cf7 100644 --- a/.nuget/Cuemon.AspNetCore/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.AspNetCore/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Core.App/PackageReleaseNotes.txt b/.nuget/Cuemon.Core.App/PackageReleaseNotes.txt index 8c842acb8..0329e2ce7 100644 --- a/.nuget/Cuemon.Core.App/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Core.App/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Core/PackageReleaseNotes.txt b/.nuget/Cuemon.Core/PackageReleaseNotes.txt index 4f3e0ea0c..725b68cfc 100644 --- a/.nuget/Cuemon.Core/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Core/PackageReleaseNotes.txt @@ -1,3 +1,23 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  +# New Features +- ADDED IPostConfigurableParameterObject interface in the Cuemon.Configuration namespace that defines a contract for parameter objects that require post-configuration logic +  +# Improvements +- EXTENDED Validator class in the Cuemon namespace to support invoking parameter objects implementing the IPostConfigurableParameterObject interface +- OPTIMIZED FowlerNollVoHash class in the Cuemon.Security namespace for better performance when computing hash codes +- OPTIMIZED DateSpan struct in the Cuemon namespace for better performance when computing hash codes and equality checks +- OPTIMIZED RandomString method on the Generate class in the Cuemon namespace with a sequential char-array builder and length guard, reducing overhead and allocations +- OPTIMIZED DelimitedString class in the Cuemon namespace with a specialized fast-path for single-character delimiters and qualifiers, reducing overhead and allocations +- OPTIMIZED CyclicRedundancyCheck class in the Cuemon.Security namespace switching CRC lookup tables to ulong[] and simplified initialization/loop structure, increasing throughput and reducing unnecessary copying +  +# Bug Fixes +- FIXED Patterns class in the Cuemon namespace to use generic object creation expressions to eliminate reflection overhead +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Data.Integrity/PackageReleaseNotes.txt b/.nuget/Cuemon.Data.Integrity/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Data.Integrity/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Data.Integrity/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Data.SqlClient/PackageReleaseNotes.txt b/.nuget/Cuemon.Data.SqlClient/PackageReleaseNotes.txt index f07d30c74..4fd58b541 100644 --- a/.nuget/Cuemon.Data.SqlClient/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Data.SqlClient/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Data/PackageReleaseNotes.txt b/.nuget/Cuemon.Data/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Data/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Data/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Diagnostics/PackageReleaseNotes.txt b/.nuget/Cuemon.Diagnostics/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Diagnostics/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Diagnostics/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Authentication/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Authentication/PackageReleaseNotes.txt index eedd92bcf..796611c0f 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Authentication/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Authentication/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json/PackageReleaseNotes.txt index e87802ce8..60f5cb893 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Text.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml/PackageReleaseNotes.txt index a224ddd12..b8d29c31c 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.Formatters.Xml/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.RazorPages/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.RazorPages/PackageReleaseNotes.txt index eedd92bcf..796611c0f 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Mvc.RazorPages/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Mvc.RazorPages/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Mvc/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Mvc/PackageReleaseNotes.txt index 004a3571f..7cbd9b55b 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Mvc/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Mvc/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Text.Json/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Text.Json/PackageReleaseNotes.txt index fe0e0cecd..75f8c447b 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Text.Json/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Text.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore.Xml/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore.Xml/PackageReleaseNotes.txt index bded2757e..2974fb360 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore.Xml/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore.Xml/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.AspNetCore/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.AspNetCore/PackageReleaseNotes.txt index 413115c3c..7ed13d783 100644 --- a/.nuget/Cuemon.Extensions.AspNetCore/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.AspNetCore/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10 and .NET 9 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10 and .NET 9   diff --git a/.nuget/Cuemon.Extensions.Collections.Generic/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Collections.Generic/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Collections.Generic/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Collections.Generic/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Collections.Specialized/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Collections.Specialized/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Collections.Specialized/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Collections.Specialized/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Core/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Core/PackageReleaseNotes.txt index 8329e046d..07f609ab2 100644 --- a/.nuget/Cuemon.Extensions.Core/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Core/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Data.Integrity/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Data.Integrity/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Data.Integrity/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Data.Integrity/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Data/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Data/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Data/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Data/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.DependencyInjection/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.DependencyInjection/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.DependencyInjection/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.DependencyInjection/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Diagnostics/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Diagnostics/PackageReleaseNotes.txt index fc5ebecd3..65954d2f9 100644 --- a/.nuget/Cuemon.Extensions.Diagnostics/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Diagnostics/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Hosting/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Hosting/PackageReleaseNotes.txt index 1a1be3c34..3bc2a6948 100644 --- a/.nuget/Cuemon.Extensions.Hosting/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Hosting/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.IO/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.IO/PackageReleaseNotes.txt index 2c072d146..f062d261b 100644 --- a/.nuget/Cuemon.Extensions.IO/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.IO/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9, .NET Standard 2.1 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9, .NET Standard 2.1 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Net/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Net/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Net/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Net/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Reflection/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Reflection/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Reflection/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Reflection/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Runtime.Caching/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Runtime.Caching/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Runtime.Caching/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Runtime.Caching/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Text.Json/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Text.Json/PackageReleaseNotes.txt index 61be15342..032c6f2ec 100644 --- a/.nuget/Cuemon.Extensions.Text.Json/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Text.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Text/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Text/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Extensions.Text/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Text/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Threading/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Threading/PackageReleaseNotes.txt index e06399f17..ecd9a4325 100644 --- a/.nuget/Cuemon.Extensions.Threading/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Threading/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Extensions.Xml/PackageReleaseNotes.txt b/.nuget/Cuemon.Extensions.Xml/PackageReleaseNotes.txt index 6b54780a8..b8891e9cf 100644 --- a/.nuget/Cuemon.Extensions.Xml/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Extensions.Xml/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.IO/PackageReleaseNotes.txt b/.nuget/Cuemon.IO/PackageReleaseNotes.txt index 67f0b1f48..e04b8fd8e 100644 --- a/.nuget/Cuemon.IO/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.IO/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Net/PackageReleaseNotes.txt b/.nuget/Cuemon.Net/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Net/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Net/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Resilience/PackageReleaseNotes.txt b/.nuget/Cuemon.Resilience/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Resilience/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Resilience/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Runtime.Caching/PackageReleaseNotes.txt b/.nuget/Cuemon.Runtime.Caching/PackageReleaseNotes.txt index e99bf3772..98ee18214 100644 --- a/.nuget/Cuemon.Runtime.Caching/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Runtime.Caching/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Security.Cryptography/PackageReleaseNotes.txt b/.nuget/Cuemon.Security.Cryptography/PackageReleaseNotes.txt index 87d019d90..fc701c278 100644 --- a/.nuget/Cuemon.Security.Cryptography/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Security.Cryptography/PackageReleaseNotes.txt @@ -1,3 +1,15 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  +# Improvements +- OPTIMIZED AesCryptor class in the Cuemon.Security.Cryptography namespace to use TransformFinalBlock instead of stream-based transforms, reducing overhead and allocations +  +# Bug Fixes +- FIXED SecureHashAlgorithm512 class in the Cuemon.Security.Cryptography namespace to allow optional setup delegate +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Threading/PackageReleaseNotes.txt b/.nuget/Cuemon.Threading/PackageReleaseNotes.txt index 62607fc84..1bf947d52 100644 --- a/.nuget/Cuemon.Threading/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Threading/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/.nuget/Cuemon.Xml/PackageReleaseNotes.txt b/.nuget/Cuemon.Xml/PackageReleaseNotes.txt index 403fed477..683472f49 100644 --- a/.nuget/Cuemon.Xml/PackageReleaseNotes.txt +++ b/.nuget/Cuemon.Xml/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version 10.1.0 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 +  +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) +  Version 10.0.0 Availability: .NET 10, .NET 9 and .NET Standard 2.0   diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d63eec61..49cf09073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder. +## [10.1.0] - 2025-12-06 + +This is a minor release that introduces support for post-configuration of parameter objects while delivering a significant series of performance improvements across various computations. + +These optimizations yielded a significant performance boost with impressive reductions in memory allocations, enhancing overall efficiency and responsiveness. + +### Added + +- `IPostConfigurableParameterObject` interface in the Cuemon.Configuration namespace that defines a contract for parameter objects that require post-configuration logic. + +### Changed + +- `Validator` class in the Cuemon namespace was extended to support invoking parameter objects implementing the `IPostConfigurableParameterObject` interface, +- `FowlerNollVoHash` class in the Cuemon.Security namespace was optimized with specialized compute paths for 32-bit, 64-bit, and multi-word (≥128-bit) hashes, +- `DateSpan` struct in the Cuemon namespace was optimized for better performance when computing hash codes and equality checks, +- `RandomString` method on the `Generate` class in the Cuemon namespace with a sequential char-array builder and length guard, reducing overhead and allocations +- `DelimitedString` class in the Cuemon namespace was optimized with a specialized fast-path for single-character delimiters and qualifiers, reducing overhead and allocations +- `AesCryptor` class in the Cuemon.Security.Cryptography namespace was optimized to use `TransformFinalBlock` instead of stream-based transforms, reducing overhead and allocations +- `CyclicRedundancyCheck` class in the Cuemon.Security namespace was optimized by switching CRC lookup tables to ulong[] and simplifying initialization/loop structure, increasing throughput and reducing unnecessary copying + +### Fixed + +- `Patterns` class in the Cuemon namespace had all `Activator.CreateInstance()` calls replaced with direct `new T()` (generic object creation expressions), to eliminate reflection overhead +- `SecureHashAlgorithm512` class in the Cuemon.Security.Cryptography namespace to allow optional setup delegate + ## [10.0.0] - 2025-11-11 This is a major release that focuses on adapting to the latest .NET 10 (LTS) release, while also removing support for .NET 8 (LTS). diff --git a/Cuemon.sln b/Cuemon.sln index 1cf2886f8..08c6f186e 100644 --- a/Cuemon.sln +++ b/Cuemon.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31919.166 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11217.181 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B59C8DF7-7DEC-46AF-A165-CC9E3AD01EA8}" EndProject @@ -170,6 +170,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cuemon.AspNetCore.Mvc.Funct EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cuemon.AspNetCore.FunctionalTests", "test\Cuemon.AspNetCore.FunctionalTests\Cuemon.AspNetCore.FunctionalTests.csproj", "{0618AFB5-D95A-48F9-A07A-B3A2DB876FC0}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tuning", "tuning", "{ABBB70A4-D3C3-4B7C-8AF6-2DB8379E0BB7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cuemon.Core.Benchmarks", "tuning\Cuemon.Core.Benchmarks\Cuemon.Core.Benchmarks.csproj", "{3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Cuemon.Security.Cryptography.Benchmarks", "tuning\Cuemon.Security.Cryptography.Benchmarks\Cuemon.Security.Cryptography.Benchmarks.csproj", "{3DBF53D2-5E7F-497D-B279-90CAB8275B93}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -492,6 +498,14 @@ Global {0618AFB5-D95A-48F9-A07A-B3A2DB876FC0}.Debug|Any CPU.Build.0 = Debug|Any CPU {0618AFB5-D95A-48F9-A07A-B3A2DB876FC0}.Release|Any CPU.ActiveCfg = Release|Any CPU {0618AFB5-D95A-48F9-A07A-B3A2DB876FC0}.Release|Any CPU.Build.0 = Release|Any CPU + {3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2}.Release|Any CPU.Build.0 = Release|Any CPU + {3DBF53D2-5E7F-497D-B279-90CAB8275B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3DBF53D2-5E7F-497D-B279-90CAB8275B93}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3DBF53D2-5E7F-497D-B279-90CAB8275B93}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3DBF53D2-5E7F-497D-B279-90CAB8275B93}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -576,6 +590,8 @@ Global {AE0ADF91-E7C7-4CB4-A39D-E1A5374C5602} = {B59C8DF7-7DEC-46AF-A165-CC9E3AD01EA8} {28AC63CA-9E57-4C36-81B4-C03DD0CFC0EA} = {31707D2B-843E-4D4F-B9C7-3E74EF8DA338} {0618AFB5-D95A-48F9-A07A-B3A2DB876FC0} = {31707D2B-843E-4D4F-B9C7-3E74EF8DA338} + {3F788B0A-3BFF-5F5D-BC90-F3DD5CE98CB2} = {ABBB70A4-D3C3-4B7C-8AF6-2DB8379E0BB7} + {3DBF53D2-5E7F-497D-B279-90CAB8275B93} = {ABBB70A4-D3C3-4B7C-8AF6-2DB8379E0BB7} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2A848386-B682-4F6D-8254-B5F6247C3054} diff --git a/Directory.Build.props b/Directory.Build.props index e20c05c2f..6ef625200 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,9 @@ $(MSBuildProjectName.EndsWith('Tests')) + $(MSBuildProjectName.EndsWith('Benchmarks')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())src')) + $(MSBuildProjectDirectory.ToLower().StartsWith('$(MSBuildThisFileDirectory.ToLower())tooling')) $([MSBuild]::IsOSPlatform('Linux')) $([MSBuild]::IsOSPlatform('Windows')) true @@ -13,7 +16,7 @@ true - + net10.0;net9.0;netstandard2.0 Copyright © Geekle 2009-2025. All rights reserved. gimlichael @@ -46,7 +49,7 @@ - + @@ -67,7 +70,6 @@ false false true - 0 none NU1701,NU1902,NU1903 false @@ -75,6 +77,16 @@ true + + 0 + + + + false + Exe + true + + @@ -97,4 +109,13 @@ + + net10.0;net9.0 + + + + + + + diff --git a/Directory.Packages.props b/Directory.Packages.props index 0d2375793..dc2613442 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,9 +4,11 @@ - - - + + + + + @@ -14,8 +16,8 @@ - - + + diff --git a/reports/tuning/Cuemon.DateSpanBenchmark-report-github.md b/reports/tuning/Cuemon.DateSpanBenchmark-report-github.md new file mode 100644 index 000000000..ae56746c4 --- /dev/null +++ b/reports/tuning/Cuemon.DateSpanBenchmark-report-github.md @@ -0,0 +1,41 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------- |---------- |---------- |--------------:|------------:|------------:|--------------:|--------------:|--------------:|-------:|--------:|-------:|----------:|------------:| +| 'Ctor (short span)' | .NET 10.0 | .NET 10.0 | 45.8507 ns | 5.8035 ns | 6.6833 ns | 47.2065 ns | 36.6149 ns | 61.9278 ns | 0.870 | 0.13 | 0.0049 | 80 B | 0.56 | +| 'Ctor (medium span)' | .NET 10.0 | .NET 10.0 | 211.8744 ns | 5.9982 ns | 6.1597 ns | 209.9891 ns | 205.2014 ns | 226.1942 ns | 4.021 | 0.14 | 0.0045 | 80 B | 0.56 | +| 'Ctor (long span)' | .NET 10.0 | .NET 10.0 | 1,800.3386 ns | 101.5296 ns | 112.8499 ns | 1,757.1949 ns | 1,695.9095 ns | 2,069.9047 ns | 34.165 | 2.20 | - | 80 B | 0.56 | +| 'Ctor (single-date)' | .NET 10.0 | .NET 10.0 | 85.2482 ns | 1.0630 ns | 0.8877 ns | 84.9364 ns | 84.1945 ns | 87.5016 ns | 1.618 | 0.04 | 0.0049 | 80 B | 0.56 | +| 'Parse (short)' | .NET 10.0 | .NET 10.0 | 155.4206 ns | 2.1092 ns | 1.8698 ns | 155.2286 ns | 152.9120 ns | 159.4585 ns | 2.949 | 0.07 | 0.0050 | 80 B | 0.56 | +| 'Parse (long)' | .NET 10.0 | .NET 10.0 | 1,370.1648 ns | 19.9091 ns | 17.6489 ns | 1,360.0428 ns | 1,351.5894 ns | 1,405.5931 ns | 26.002 | 0.62 | - | 80 B | 0.56 | +| 'ToString (short)' | .NET 10.0 | .NET 10.0 | 131.4024 ns | 3.1530 ns | 3.6310 ns | 129.5049 ns | 128.0854 ns | 138.6695 ns | 2.494 | 0.08 | 0.0143 | 232 B | 1.61 | +| 'ToString (long)' | .NET 10.0 | .NET 10.0 | 130.1290 ns | 1.7457 ns | 1.6330 ns | 130.0559 ns | 127.8197 ns | 132.8534 ns | 2.469 | 0.06 | 0.0150 | 240 B | 1.67 | +| 'GetWeeks (short)' | .NET 10.0 | .NET 10.0 | 1.6572 ns | 0.0395 ns | 0.0350 ns | 1.6515 ns | 1.6086 ns | 1.7354 ns | 0.031 | 0.00 | - | - | 0.00 | +| 'GetWeeks (long)' | .NET 10.0 | .NET 10.0 | 0.6293 ns | 0.0671 ns | 0.0746 ns | 0.5999 ns | 0.5525 ns | 0.8068 ns | 0.012 | 0.00 | - | - | 0.00 | +| GetHashCode | .NET 10.0 | .NET 10.0 | 0.4267 ns | 0.0095 ns | 0.0074 ns | 0.4279 ns | 0.4132 ns | 0.4374 ns | 0.008 | 0.00 | - | - | 0.00 | +| 'Equals (value vs same value)' | .NET 10.0 | .NET 10.0 | 1,716.4099 ns | 18.8782 ns | 16.7351 ns | 1,713.9468 ns | 1,695.7291 ns | 1,743.1789 ns | 32.572 | 0.73 | - | 80 B | 0.56 | +| 'Operator == (same value)' | .NET 10.0 | .NET 10.0 | 1,726.2320 ns | 36.5756 ns | 39.1354 ns | 1,712.3598 ns | 1,691.1513 ns | 1,835.6900 ns | 32.759 | 0.99 | - | 80 B | 0.56 | +| 'Ctor (short span)' | .NET 9.0 | .NET 9.0 | 52.7174 ns | 1.0899 ns | 1.1192 ns | 52.3923 ns | 50.8562 ns | 54.8449 ns | 1.000 | 0.03 | 0.0091 | 144 B | 1.00 | +| 'Ctor (medium span)' | .NET 9.0 | .NET 9.0 | 224.6956 ns | 5.2695 ns | 5.6383 ns | 221.8543 ns | 219.5452 ns | 237.8165 ns | 4.264 | 0.14 | 0.0090 | 144 B | 1.00 | +| 'Ctor (long span)' | .NET 9.0 | .NET 9.0 | 1,790.5534 ns | 33.6635 ns | 28.1106 ns | 1,779.6856 ns | 1,762.9207 ns | 1,862.1473 ns | 33.979 | 0.86 | 0.0071 | 144 B | 1.00 | +| 'Ctor (single-date)' | .NET 9.0 | .NET 9.0 | 107.9379 ns | 1.9648 ns | 1.6407 ns | 107.3828 ns | 106.3406 ns | 112.2844 ns | 2.048 | 0.05 | 0.0090 | 144 B | 1.00 | +| 'Parse (short)' | .NET 9.0 | .NET 9.0 | 198.0320 ns | 3.8471 ns | 3.2125 ns | 196.1874 ns | 195.2844 ns | 206.0204 ns | 3.758 | 0.10 | 0.0087 | 144 B | 1.00 | +| 'Parse (long)' | .NET 9.0 | .NET 9.0 | 1,555.6981 ns | 46.4246 ns | 51.6008 ns | 1,541.6413 ns | 1,495.6555 ns | 1,667.2954 ns | 29.523 | 1.13 | 0.0060 | 144 B | 1.00 | +| 'ToString (short)' | .NET 9.0 | .NET 9.0 | 123.5641 ns | 4.4744 ns | 5.1527 ns | 121.5069 ns | 118.7768 ns | 132.8566 ns | 2.345 | 0.11 | 0.0144 | 232 B | 1.61 | +| 'ToString (long)' | .NET 9.0 | .NET 9.0 | 120.8127 ns | 1.8377 ns | 1.7190 ns | 120.7993 ns | 117.4110 ns | 123.1928 ns | 2.293 | 0.06 | 0.0153 | 240 B | 1.67 | +| 'GetWeeks (short)' | .NET 9.0 | .NET 9.0 | 1.7451 ns | 0.0621 ns | 0.0690 ns | 1.7347 ns | 1.6693 ns | 1.9067 ns | 0.033 | 0.00 | - | - | 0.00 | +| 'GetWeeks (long)' | .NET 9.0 | .NET 9.0 | 1.0743 ns | 0.0576 ns | 0.0640 ns | 1.0417 ns | 1.0130 ns | 1.2203 ns | 0.020 | 0.00 | - | - | 0.00 | +| GetHashCode | .NET 9.0 | .NET 9.0 | 0.6214 ns | 0.0232 ns | 0.0194 ns | 0.6166 ns | 0.5944 ns | 0.6616 ns | 0.012 | 0.00 | - | - | 0.00 | +| 'Equals (value vs same value)' | .NET 9.0 | .NET 9.0 | 1,764.3783 ns | 38.5226 ns | 42.8178 ns | 1,741.5046 ns | 1,720.5733 ns | 1,848.1940 ns | 33.483 | 1.05 | 0.0069 | 144 B | 1.00 | +| 'Operator == (same value)' | .NET 9.0 | .NET 9.0 | 1,744.8519 ns | 26.0303 ns | 21.7365 ns | 1,744.7386 ns | 1,714.5742 ns | 1,776.5430 ns | 33.112 | 0.79 | 0.0070 | 144 B | 1.00 | diff --git a/reports/tuning/Cuemon.DelimitedStringBenchmark-report-github.md b/reports/tuning/Cuemon.DelimitedStringBenchmark-report-github.md new file mode 100644 index 000000000..68823652a --- /dev/null +++ b/reports/tuning/Cuemon.DelimitedStringBenchmark-report-github.md @@ -0,0 +1,32 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7296) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Count | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------- |---------- |---------- |------ |------------:|------------:|------------:|------------:|------------:|------------:|------:|--------:|-------:|-------:|----------:|------------:| +| **Create** | **.NET 10.0** | **.NET 10.0** | **10** | **140.7 ns** | **6.02 ns** | **6.93 ns** | **138.7 ns** | **129.7 ns** | **156.0 ns** | **0.88** | **0.05** | **0.0526** | **-** | **832 B** | **0.93** | +| Create | .NET 9.0 | .NET 9.0 | 10 | 160.0 ns | 5.48 ns | 5.12 ns | 158.6 ns | 153.2 ns | 171.2 ns | 1.00 | 0.04 | 0.0565 | - | 896 B | 1.00 | +| | | | | | | | | | | | | | | | | +| Split | .NET 10.0 | .NET 10.0 | 10 | 255.6 ns | 9.90 ns | 11.40 ns | 251.5 ns | 240.6 ns | 275.3 ns | 0.70 | 0.09 | 0.0843 | - | 1328 B | 0.98 | +| Split | .NET 9.0 | .NET 9.0 | 10 | 372.7 ns | 36.09 ns | 41.56 ns | 391.5 ns | 280.7 ns | 411.3 ns | 1.01 | 0.17 | 0.0860 | - | 1360 B | 1.00 | +| | | | | | | | | | | | | | | | | +| **Create** | **.NET 10.0** | **.NET 10.0** | **100** | **1,363.3 ns** | **193.85 ns** | **223.24 ns** | **1,437.2 ns** | **790.0 ns** | **1,569.2 ns** | **1.27** | **0.33** | **0.2872** | **-** | **4512 B** | **0.99** | +| Create | .NET 9.0 | .NET 9.0 | 100 | 1,120.6 ns | 217.09 ns | 250.00 ns | 981.8 ns | 870.3 ns | 1,483.1 ns | 1.05 | 0.31 | 0.2895 | - | 4576 B | 1.00 | +| | | | | | | | | | | | | | | | | +| Split | .NET 10.0 | .NET 10.0 | 100 | 2,606.0 ns | 380.17 ns | 437.81 ns | 2,767.8 ns | 1,764.6 ns | 3,165.0 ns | 1.64 | 0.27 | 0.6101 | 0.0139 | 9624 B | 1.00 | +| Split | .NET 9.0 | .NET 9.0 | 100 | 1,586.5 ns | 45.45 ns | 50.52 ns | 1,566.9 ns | 1,521.3 ns | 1,717.4 ns | 1.00 | 0.04 | 0.6157 | 0.0124 | 9656 B | 1.00 | +| | | | | | | | | | | | | | | | | +| **Create** | **.NET 10.0** | **.NET 10.0** | **1000** | **6,683.9 ns** | **344.72 ns** | **368.85 ns** | **6,627.7 ns** | **6,212.6 ns** | **7,487.9 ns** | **0.58** | **0.14** | **3.4157** | **0.3650** | **53673 B** | **1.00** | +| Create | .NET 9.0 | .NET 9.0 | 1000 | 12,079.2 ns | 2,144.39 ns | 2,469.49 ns | 13,295.2 ns | 7,560.4 ns | 14,804.4 ns | 1.05 | 0.33 | 3.4180 | 0.4185 | 53737 B | 1.00 | +| | | | | | | | | | | | | | | | | +| Split | .NET 10.0 | .NET 10.0 | 1000 | 24,688.8 ns | 4,767.11 ns | 5,489.81 ns | 27,260.4 ns | 14,453.3 ns | 29,350.5 ns | 0.91 | 0.29 | 5.7582 | 1.1396 | 91113 B | 1.00 | +| Split | .NET 9.0 | .NET 9.0 | 1000 | 27,918.3 ns | 3,146.75 ns | 3,623.80 ns | 29,079.6 ns | 13,664.9 ns | 29,941.2 ns | 1.03 | 0.27 | 5.7594 | 1.2265 | 91145 B | 1.00 | diff --git a/reports/tuning/Cuemon.GenerateBenchmark-report-github.md b/reports/tuning/Cuemon.GenerateBenchmark-report-github.md new file mode 100644 index 000000000..9b211d2bd --- /dev/null +++ b/reports/tuning/Cuemon.GenerateBenchmark-report-github.md @@ -0,0 +1,104 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Count | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|--------------------------------- |---------- |---------- |------ |---------------:|--------------:|--------------:|---------------:|--------------:|---------------:|------:|--------:|-------:|----------:|------------:| +| **'RangeOf - enumerate'** | **.NET 10.0** | **.NET 10.0** | **8** | **13.741 ns** | **0.2298 ns** | **0.2037 ns** | **13.785 ns** | **13.352 ns** | **14.046 ns** | **1.02** | **0.04** | **0.0035** | **56 B** | **1.00** | +| 'RangeOf - enumerate' | .NET 9.0 | .NET 9.0 | 8 | 13.428 ns | 0.4018 ns | 0.4627 ns | 13.362 ns | 12.709 ns | 14.322 ns | 1.00 | 0.05 | 0.0035 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomNumber - default' | .NET 10.0 | .NET 10.0 | 8 | 2.324 ns | 0.0630 ns | 0.0589 ns | 2.303 ns | 2.246 ns | 2.431 ns | 0.43 | 0.01 | - | - | NA | +| 'RandomNumber - default' | .NET 9.0 | .NET 9.0 | 8 | 5.449 ns | 0.1120 ns | 0.0993 ns | 5.481 ns | 5.240 ns | 5.591 ns | 1.00 | 0.03 | - | - | NA | +| | | | | | | | | | | | | | | | +| 'RandomNumber - bounded' | .NET 10.0 | .NET 10.0 | 8 | 2.917 ns | 0.0680 ns | 0.0603 ns | 2.897 ns | 2.865 ns | 3.075 ns | 0.47 | 0.02 | - | - | NA | +| 'RandomNumber - bounded' | .NET 9.0 | .NET 9.0 | 8 | 6.244 ns | 0.2248 ns | 0.2309 ns | 6.166 ns | 5.991 ns | 6.926 ns | 1.00 | 0.05 | - | - | NA | +| | | | | | | | | | | | | | | | +| FixedString | .NET 10.0 | .NET 10.0 | 8 | 4.439 ns | 0.2429 ns | 0.2797 ns | 4.459 ns | 4.055 ns | 4.967 ns | 0.72 | 0.06 | 0.0025 | 40 B | 1.00 | +| FixedString | .NET 9.0 | .NET 9.0 | 8 | 6.212 ns | 0.3843 ns | 0.4271 ns | 6.149 ns | 5.516 ns | 6.945 ns | 1.00 | 0.09 | 0.0025 | 40 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomString - letters/numbers' | .NET 10.0 | .NET 10.0 | 8 | 106.672 ns | 1.8396 ns | 1.7208 ns | 106.867 ns | 104.154 ns | 109.380 ns | 0.89 | 0.02 | 0.0047 | 80 B | 1.00 | +| 'RandomString - letters/numbers' | .NET 9.0 | .NET 9.0 | 8 | 119.646 ns | 1.5614 ns | 1.3841 ns | 119.813 ns | 117.050 ns | 121.776 ns | 1.00 | 0.02 | 0.0049 | 80 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ObjectPortrayal - basic object' | .NET 10.0 | .NET 10.0 | 8 | 1,149.664 ns | 107.3221 ns | 110.2119 ns | 1,145.727 ns | 1,008.414 ns | 1,411.077 ns | 0.93 | 0.10 | 0.2297 | 3657 B | 0.84 | +| 'ObjectPortrayal - basic object' | .NET 9.0 | .NET 9.0 | 8 | 1,241.749 ns | 59.4998 ns | 63.6641 ns | 1,230.065 ns | 1,145.552 ns | 1,389.291 ns | 1.00 | 0.07 | 0.2690 | 4345 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - params' | .NET 10.0 | .NET 10.0 | 8 | 163.185 ns | 6.9306 ns | 7.1172 ns | 160.746 ns | 155.007 ns | 179.659 ns | 0.78 | 0.05 | 0.0719 | 1136 B | 0.97 | +| 'HashCode32 - params' | .NET 9.0 | .NET 9.0 | 8 | 210.804 ns | 13.4764 ns | 13.2357 ns | 204.366 ns | 198.627 ns | 241.047 ns | 1.00 | 0.08 | 0.0737 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - enumerable' | .NET 10.0 | .NET 10.0 | 8 | 346.804 ns | 10.1448 ns | 10.8548 ns | 347.773 ns | 326.214 ns | 362.762 ns | 0.51 | 0.03 | 0.1261 | 1984 B | 1.00 | +| 'HashCode32 - enumerable' | .NET 9.0 | .NET 9.0 | 8 | 679.034 ns | 30.0450 ns | 34.5999 ns | 685.325 ns | 594.216 ns | 723.030 ns | 1.00 | 0.07 | 0.1250 | 1984 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - params' | .NET 10.0 | .NET 10.0 | 8 | 164.761 ns | 5.0784 ns | 5.6447 ns | 165.048 ns | 155.583 ns | 174.537 ns | 0.46 | 0.03 | 0.0721 | 1136 B | 0.97 | +| 'HashCode64 - params' | .NET 9.0 | .NET 9.0 | 8 | 357.811 ns | 13.6095 ns | 15.6727 ns | 359.826 ns | 309.596 ns | 374.758 ns | 1.00 | 0.06 | 0.0744 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - enumerable' | .NET 10.0 | .NET 10.0 | 8 | 413.441 ns | 20.8863 ns | 24.0527 ns | 406.271 ns | 380.932 ns | 470.696 ns | 0.61 | 0.05 | 0.1280 | 2008 B | 1.01 | +| 'HashCode64 - enumerable' | .NET 9.0 | .NET 9.0 | 8 | 685.252 ns | 35.0569 ns | 40.3716 ns | 689.595 ns | 536.782 ns | 735.485 ns | 1.00 | 0.09 | 0.1260 | 1984 B | 1.00 | +| | | | | | | | | | | | | | | | +| **'RangeOf - enumerate'** | **.NET 10.0** | **.NET 10.0** | **256** | **353.374 ns** | **10.3290 ns** | **11.8949 ns** | **351.185 ns** | **339.665 ns** | **381.541 ns** | **0.90** | **0.03** | **0.0027** | **56 B** | **1.00** | +| 'RangeOf - enumerate' | .NET 9.0 | .NET 9.0 | 256 | 391.409 ns | 6.8182 ns | 7.0018 ns | 391.614 ns | 370.741 ns | 400.163 ns | 1.00 | 0.02 | 0.0031 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomNumber - default' | .NET 10.0 | .NET 10.0 | 256 | 4.552 ns | 0.2510 ns | 0.2686 ns | 4.611 ns | 3.860 ns | 4.940 ns | 0.39 | 0.02 | - | - | NA | +| 'RandomNumber - default' | .NET 9.0 | .NET 9.0 | 256 | 11.608 ns | 0.2261 ns | 0.1888 ns | 11.572 ns | 11.328 ns | 11.942 ns | 1.00 | 0.02 | - | - | NA | +| | | | | | | | | | | | | | | | +| 'RandomNumber - bounded' | .NET 10.0 | .NET 10.0 | 256 | 5.288 ns | 0.2671 ns | 0.3076 ns | 5.346 ns | 4.581 ns | 5.718 ns | 0.42 | 0.03 | - | - | NA | +| 'RandomNumber - bounded' | .NET 9.0 | .NET 9.0 | 256 | 12.604 ns | 0.6566 ns | 0.7561 ns | 12.574 ns | 10.883 ns | 13.766 ns | 1.00 | 0.08 | - | - | NA | +| | | | | | | | | | | | | | | | +| FixedString | .NET 10.0 | .NET 10.0 | 256 | 37.146 ns | 2.2453 ns | 2.4956 ns | 37.248 ns | 30.722 ns | 41.412 ns | 0.97 | 0.07 | 0.0341 | 536 B | 1.00 | +| FixedString | .NET 9.0 | .NET 9.0 | 256 | 38.443 ns | 0.8190 ns | 0.9104 ns | 38.695 ns | 36.068 ns | 39.566 ns | 1.00 | 0.03 | 0.0342 | 536 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomString - letters/numbers' | .NET 10.0 | .NET 10.0 | 256 | 4,667.506 ns | 40.1718 ns | 37.5767 ns | 4,650.119 ns | 4,618.899 ns | 4,743.052 ns | 0.64 | 0.03 | 0.0560 | 1072 B | 1.00 | +| 'RandomString - letters/numbers' | .NET 9.0 | .NET 9.0 | 256 | 7,352.858 ns | 268.4290 ns | 309.1232 ns | 7,337.980 ns | 6,226.859 ns | 7,800.454 ns | 1.00 | 0.06 | 0.0583 | 1072 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ObjectPortrayal - basic object' | .NET 10.0 | .NET 10.0 | 256 | 1,833.849 ns | 46.3924 ns | 51.5650 ns | 1,824.859 ns | 1,697.572 ns | 1,925.368 ns | 0.78 | 0.05 | 0.2290 | 3657 B | 0.84 | +| 'ObjectPortrayal - basic object' | .NET 9.0 | .NET 9.0 | 256 | 2,347.310 ns | 109.1070 ns | 125.6478 ns | 2,372.161 ns | 1,861.056 ns | 2,463.062 ns | 1.00 | 0.08 | 0.2758 | 4345 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - params' | .NET 10.0 | .NET 10.0 | 256 | 286.667 ns | 6.9173 ns | 7.6886 ns | 286.433 ns | 274.945 ns | 303.189 ns | 0.82 | 0.04 | 0.0717 | 1136 B | 0.97 | +| 'HashCode32 - params' | .NET 9.0 | .NET 9.0 | 256 | 350.597 ns | 13.5688 ns | 15.0817 ns | 348.282 ns | 317.927 ns | 377.229 ns | 1.00 | 0.06 | 0.0732 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - enumerable' | .NET 10.0 | .NET 10.0 | 256 | 619.988 ns | 21.7047 ns | 24.9952 ns | 614.354 ns | 569.592 ns | 660.283 ns | 0.89 | 0.05 | 0.1248 | 1984 B | 1.00 | +| 'HashCode32 - enumerable' | .NET 9.0 | .NET 9.0 | 256 | 696.622 ns | 24.9704 ns | 28.7559 ns | 692.638 ns | 651.765 ns | 745.083 ns | 1.00 | 0.06 | 0.1243 | 1984 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - params' | .NET 10.0 | .NET 10.0 | 256 | 285.025 ns | 11.3291 ns | 13.0466 ns | 283.977 ns | 250.927 ns | 313.365 ns | 0.83 | 0.06 | 0.0719 | 1136 B | 0.97 | +| 'HashCode64 - params' | .NET 9.0 | .NET 9.0 | 256 | 342.778 ns | 14.3813 ns | 16.5616 ns | 341.918 ns | 289.293 ns | 369.204 ns | 1.00 | 0.07 | 0.0735 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - enumerable' | .NET 10.0 | .NET 10.0 | 256 | 599.851 ns | 25.6049 ns | 27.3969 ns | 595.686 ns | 525.835 ns | 653.825 ns | 0.88 | 0.05 | 0.1242 | 1984 B | 1.00 | +| 'HashCode64 - enumerable' | .NET 9.0 | .NET 9.0 | 256 | 684.784 ns | 21.2616 ns | 24.4849 ns | 689.465 ns | 628.502 ns | 715.424 ns | 1.00 | 0.05 | 0.1253 | 1984 B | 1.00 | +| | | | | | | | | | | | | | | | +| **'RangeOf - enumerate'** | **.NET 10.0** | **.NET 10.0** | **4096** | **4,936.170 ns** | **105.8648 ns** | **117.6684 ns** | **4,905.402 ns** | **4,807.591 ns** | **5,225.286 ns** | **0.83** | **0.02** | **-** | **56 B** | **1.00** | +| 'RangeOf - enumerate' | .NET 9.0 | .NET 9.0 | 4096 | 5,968.759 ns | 116.9022 ns | 114.8136 ns | 5,928.644 ns | 5,862.079 ns | 6,246.125 ns | 1.00 | 0.03 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomNumber - default' | .NET 10.0 | .NET 10.0 | 4096 | 4.729 ns | 0.3049 ns | 0.3389 ns | 4.620 ns | 4.132 ns | 5.533 ns | 0.41 | 0.04 | - | - | NA | +| 'RandomNumber - default' | .NET 9.0 | .NET 9.0 | 4096 | 11.471 ns | 0.6374 ns | 0.7084 ns | 11.640 ns | 8.857 ns | 12.257 ns | 1.00 | 0.10 | - | - | NA | +| | | | | | | | | | | | | | | | +| 'RandomNumber - bounded' | .NET 10.0 | .NET 10.0 | 4096 | 6.331 ns | 0.2677 ns | 0.2864 ns | 6.276 ns | 5.708 ns | 6.804 ns | 0.51 | 0.02 | - | - | NA | +| 'RandomNumber - bounded' | .NET 9.0 | .NET 9.0 | 4096 | 12.357 ns | 0.1675 ns | 0.1308 ns | 12.365 ns | 12.140 ns | 12.563 ns | 1.00 | 0.01 | - | - | NA | +| | | | | | | | | | | | | | | | +| FixedString | .NET 10.0 | .NET 10.0 | 4096 | 602.075 ns | 11.5325 ns | 10.2233 ns | 601.289 ns | 588.179 ns | 621.439 ns | 0.87 | 0.09 | 0.5236 | 8216 B | 1.00 | +| FixedString | .NET 9.0 | .NET 9.0 | 4096 | 695.317 ns | 63.0161 ns | 72.5694 ns | 696.252 ns | 594.029 ns | 843.861 ns | 1.01 | 0.15 | 0.5223 | 8216 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'RandomString - letters/numbers' | .NET 10.0 | .NET 10.0 | 4096 | 74,347.784 ns | 1,132.7188 ns | 1,059.5458 ns | 74,292.350 ns | 73,004.260 ns | 76,474.396 ns | 0.66 | 0.06 | 0.8844 | 16432 B | 1.00 | +| 'RandomString - letters/numbers' | .NET 9.0 | .NET 9.0 | 4096 | 112,870.441 ns | 7,549.6532 ns | 8,391.4196 ns | 115,330.423 ns | 89,761.903 ns | 118,344.715 ns | 1.01 | 0.11 | 0.9191 | 16432 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ObjectPortrayal - basic object' | .NET 10.0 | .NET 10.0 | 4096 | 1,837.843 ns | 44.0917 ns | 49.0078 ns | 1,820.219 ns | 1,768.095 ns | 1,946.124 ns | 0.87 | 0.06 | 0.2289 | 3657 B | 0.84 | +| 'ObjectPortrayal - basic object' | .NET 9.0 | .NET 9.0 | 4096 | 2,112.880 ns | 118.6972 ns | 127.0047 ns | 2,131.210 ns | 1,711.812 ns | 2,294.503 ns | 1.00 | 0.09 | 0.2700 | 4345 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - params' | .NET 10.0 | .NET 10.0 | 4096 | 288.135 ns | 10.9050 ns | 12.5583 ns | 289.515 ns | 245.276 ns | 305.911 ns | 0.84 | 0.04 | 0.0739 | 1160 B | 0.99 | +| 'HashCode32 - params' | .NET 9.0 | .NET 9.0 | 4096 | 344.513 ns | 9.9129 ns | 11.4157 ns | 346.800 ns | 328.897 ns | 367.482 ns | 1.00 | 0.05 | 0.0732 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode32 - enumerable' | .NET 10.0 | .NET 10.0 | 4096 | 610.338 ns | 14.7866 ns | 17.0283 ns | 608.823 ns | 576.018 ns | 638.431 ns | 0.89 | 0.05 | 0.1252 | 1984 B | 1.00 | +| 'HashCode32 - enumerable' | .NET 9.0 | .NET 9.0 | 4096 | 686.571 ns | 28.4260 ns | 31.5954 ns | 688.768 ns | 580.919 ns | 733.630 ns | 1.00 | 0.07 | 0.1243 | 1984 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - params' | .NET 10.0 | .NET 10.0 | 4096 | 284.882 ns | 6.1140 ns | 6.7957 ns | 284.407 ns | 275.375 ns | 299.561 ns | 0.83 | 0.04 | 0.0719 | 1136 B | 0.97 | +| 'HashCode64 - params' | .NET 9.0 | .NET 9.0 | 4096 | 342.300 ns | 12.2302 ns | 14.0843 ns | 342.665 ns | 304.187 ns | 365.029 ns | 1.00 | 0.06 | 0.0737 | 1168 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'HashCode64 - enumerable' | .NET 10.0 | .NET 10.0 | 4096 | 606.550 ns | 14.4639 ns | 16.6567 ns | 603.472 ns | 575.919 ns | 637.161 ns | 0.91 | 0.05 | 0.1243 | 1984 B | 1.00 | +| 'HashCode64 - enumerable' | .NET 9.0 | .NET 9.0 | 4096 | 665.939 ns | 29.0020 ns | 31.0318 ns | 670.528 ns | 555.899 ns | 697.720 ns | 1.00 | 0.07 | 0.1259 | 1984 B | 1.00 | diff --git a/reports/tuning/Cuemon.Security.Cryptography.AesCryptorBenchmarks-report-github.md b/reports/tuning/Cuemon.Security.Cryptography.AesCryptorBenchmarks-report-github.md new file mode 100644 index 000000000..dee0edc0b --- /dev/null +++ b/reports/tuning/Cuemon.Security.Cryptography.AesCryptorBenchmarks-report-github.md @@ -0,0 +1,32 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7296) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Size | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------- |---------- |---------- |------ |------------:|----------:|------------:|------------:|------------:|------------:|------:|--------:|-------:|----------:|------------:| +| **AesCryptor.Encrypt** | **.NET 10.0** | **.NET 10.0** | **128** | **692.9 ns** | **13.46 ns** | **14.96 ns** | **691.4 ns** | **670.5 ns** | **722.2 ns** | **0.71** | **0.07** | **0.0461** | **752 B** | **0.89** | +| AesCryptor.Encrypt | .NET 9.0 | .NET 9.0 | 128 | 984.7 ns | 63.73 ns | 73.40 ns | 1,003.1 ns | 686.0 ns | 1,024.9 ns | 1.01 | 0.12 | 0.0520 | 848 B | 1.00 | +| | | | | | | | | | | | | | | | +| AesCryptor.Decrypt | .NET 10.0 | .NET 10.0 | 128 | 1,110.9 ns | 80.05 ns | 88.97 ns | 1,123.8 ns | 780.4 ns | 1,204.8 ns | 1.17 | 0.22 | 0.0466 | 744 B | 0.89 | +| AesCryptor.Decrypt | .NET 9.0 | .NET 9.0 | 128 | 972.8 ns | 124.92 ns | 143.86 ns | 1,045.6 ns | 709.0 ns | 1,087.1 ns | 1.03 | 0.23 | 0.0518 | 840 B | 1.00 | +| | | | | | | | | | | | | | | | +| **AesCryptor.Encrypt** | **.NET 10.0** | **.NET 10.0** | **1024** | **1,673.8 ns** | **226.09 ns** | **260.37 ns** | **1,788.8 ns** | **1,186.2 ns** | **1,991.1 ns** | **1.37** | **0.21** | **0.1035** | **1648 B** | **0.94** | +| AesCryptor.Encrypt | .NET 9.0 | .NET 9.0 | 1024 | 1,224.0 ns | 24.81 ns | 27.57 ns | 1,223.3 ns | 1,187.7 ns | 1,277.7 ns | 1.00 | 0.03 | 0.1068 | 1744 B | 1.00 | +| | | | | | | | | | | | | | | | +| AesCryptor.Decrypt | .NET 10.0 | .NET 10.0 | 1024 | 889.2 ns | 21.42 ns | 24.67 ns | 884.9 ns | 854.0 ns | 940.9 ns | 1.00 | 0.04 | 0.1029 | 1640 B | 0.94 | +| AesCryptor.Decrypt | .NET 9.0 | .NET 9.0 | 1024 | 890.3 ns | 27.89 ns | 32.12 ns | 892.6 ns | 830.9 ns | 947.5 ns | 1.00 | 0.05 | 0.1086 | 1736 B | 1.00 | +| | | | | | | | | | | | | | | | +| **AesCryptor.Encrypt** | **.NET 10.0** | **.NET 10.0** | **65536** | **39,538.3 ns** | **959.01 ns** | **1,065.94 ns** | **39,406.2 ns** | **38,240.6 ns** | **41,237.1 ns** | **1.00** | **0.03** | **4.1881** | **66162 B** | **1.00** | +| AesCryptor.Encrypt | .NET 9.0 | .NET 9.0 | 65536 | 39,365.1 ns | 781.01 ns | 868.09 ns | 39,173.0 ns | 37,999.7 ns | 40,993.6 ns | 1.00 | 0.03 | 4.1978 | 66258 B | 1.00 | +| | | | | | | | | | | | | | | | +| AesCryptor.Decrypt | .NET 10.0 | .NET 10.0 | 65536 | 12,328.5 ns | 317.02 ns | 352.36 ns | 12,332.1 ns | 11,895.5 ns | 12,964.8 ns | 0.96 | 0.07 | 4.1764 | 66154 B | 1.00 | +| AesCryptor.Decrypt | .NET 9.0 | .NET 9.0 | 65536 | 12,844.2 ns | 729.58 ns | 840.19 ns | 12,561.5 ns | 11,901.6 ns | 14,119.0 ns | 1.00 | 0.09 | 4.1601 | 66250 B | 1.00 | diff --git a/reports/tuning/Cuemon.Security.Cryptography.Sha512256Benchmark-report-github.md b/reports/tuning/Cuemon.Security.Cryptography.Sha512256Benchmark-report-github.md new file mode 100644 index 000000000..32b01a97a --- /dev/null +++ b/reports/tuning/Cuemon.Security.Cryptography.Sha512256Benchmark-report-github.md @@ -0,0 +1,36 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7296) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Variant | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|------------------------------------------------------------------- |---------- |---------- |----------------- |---------------:|-------------:|-------------:|---------------:|---------------:|---------------:|---------:|---------:|-------:|----------:|------------:| +| **'Custom SHA-512/256 — small (64 bytes)'** | **.NET 10.0** | **.NET 10.0** | **CustomSHA512_256** | **396.9 ns** | **8.53 ns** | **9.82 ns** | **393.4 ns** | **383.9 ns** | **417.4 ns** | **0.88** | **0.20** | **0.0704** | **1104 B** | **1.00** | +| 'Custom SHA-512/256 — large (1 MB)' | .NET 10.0 | .NET 10.0 | CustomSHA512_256 | 2,215,370.3 ns | 30,618.15 ns | 27,142.19 ns | 2,200,658.2 ns | 2,187,930.5 ns | 2,263,451.6 ns | 4,910.01 | 1,093.90 | - | 1104 B | 1.00 | +| 'Built-in SHA-512 truncated -> 256 — small (64 bytes)' | .NET 10.0 | .NET 10.0 | CustomSHA512_256 | 441.2 ns | 8.58 ns | 8.42 ns | 438.1 ns | 432.7 ns | 454.7 ns | 0.98 | 0.22 | 0.0233 | 368 B | 0.33 | +| 'Built-in SHA-512 truncated -> 256 — large (1 MB)' | .NET 10.0 | .NET 10.0 | CustomSHA512_256 | 1,346,672.3 ns | 12,525.60 ns | 11,103.62 ns | 1,344,033.3 ns | 1,335,707.8 ns | 1,371,581.2 ns | 2,984.68 | 664.42 | - | 369 B | 0.33 | +| 'Param-based: ComputeHash (selects algorithm by [Params] Variant)' | .NET 10.0 | .NET 10.0 | CustomSHA512_256 | 552.2 ns | 111.87 ns | 128.83 ns | 634.6 ns | 380.5 ns | 687.4 ns | 1.22 | 0.39 | 0.0696 | 1104 B | 1.00 | +| 'Custom SHA-512/256 — small (64 bytes)' | .NET 9.0 | .NET 9.0 | CustomSHA512_256 | 479.0 ns | 112.17 ns | 129.18 ns | 400.1 ns | 373.3 ns | 691.4 ns | 1.06 | 0.37 | 0.0695 | 1104 B | 1.00 | +| 'Custom SHA-512/256 — large (1 MB)' | .NET 9.0 | .NET 9.0 | CustomSHA512_256 | 2,236,045.5 ns | 10,136.27 ns | 9,481.47 ns | 2,232,607.1 ns | 2,224,303.6 ns | 2,255,956.2 ns | 4,955.83 | 1,102.54 | - | 1104 B | 1.00 | +| 'Built-in SHA-512 truncated -> 256 — small (64 bytes)' | .NET 9.0 | .NET 9.0 | CustomSHA512_256 | 441.4 ns | 9.21 ns | 10.23 ns | 440.7 ns | 422.6 ns | 460.3 ns | 0.98 | 0.22 | 0.0229 | 368 B | 0.33 | +| 'Built-in SHA-512 truncated -> 256 — large (1 MB)' | .NET 9.0 | .NET 9.0 | CustomSHA512_256 | 1,342,722.2 ns | 16,476.57 ns | 15,412.19 ns | 1,341,708.3 ns | 1,321,103.1 ns | 1,367,723.4 ns | 2,975.92 | 662.81 | - | 369 B | 0.33 | +| 'Param-based: ComputeHash (selects algorithm by [Params] Variant)' | .NET 9.0 | .NET 9.0 | CustomSHA512_256 | 400.3 ns | 5.96 ns | 5.58 ns | 400.3 ns | 391.5 ns | 412.5 ns | 0.89 | 0.20 | 0.0702 | 1104 B | 1.00 | +| | | | | | | | | | | | | | | | +| **'Custom SHA-512/256 — small (64 bytes)'** | **.NET 10.0** | **.NET 10.0** | **SHA512_Truncated** | **371.4 ns** | **23.52 ns** | **27.08 ns** | **359.7 ns** | **345.1 ns** | **425.9 ns** | **1.02** | **0.07** | **0.0701** | **1104 B** | **1.00** | +| 'Custom SHA-512/256 — large (1 MB)' | .NET 10.0 | .NET 10.0 | SHA512_Truncated | 2,208,794.2 ns | 30,979.07 ns | 28,977.85 ns | 2,208,998.2 ns | 2,148,959.8 ns | 2,249,233.9 ns | 6,051.29 | 120.02 | - | 1104 B | 1.00 | +| 'Built-in SHA-512 truncated -> 256 — small (64 bytes)' | .NET 10.0 | .NET 10.0 | SHA512_Truncated | 427.5 ns | 8.48 ns | 9.08 ns | 424.4 ns | 418.1 ns | 449.6 ns | 1.17 | 0.03 | 0.0224 | 368 B | 0.33 | +| 'Built-in SHA-512 truncated -> 256 — large (1 MB)' | .NET 10.0 | .NET 10.0 | SHA512_Truncated | 1,343,433.9 ns | 18,287.12 ns | 17,105.78 ns | 1,342,228.6 ns | 1,315,239.6 ns | 1,371,437.5 ns | 3,680.52 | 72.13 | - | 369 B | 0.33 | +| 'Param-based: ComputeHash (selects algorithm by [Params] Variant)' | .NET 10.0 | .NET 10.0 | SHA512_Truncated | 418.4 ns | 7.87 ns | 6.97 ns | 417.8 ns | 407.8 ns | 434.0 ns | 1.15 | 0.03 | 0.0231 | 368 B | 0.33 | +| 'Custom SHA-512/256 — small (64 bytes)' | .NET 9.0 | .NET 9.0 | SHA512_Truncated | 365.1 ns | 5.89 ns | 5.78 ns | 364.6 ns | 358.5 ns | 374.9 ns | 1.00 | 0.02 | 0.0693 | 1104 B | 1.00 | +| 'Custom SHA-512/256 — large (1 MB)' | .NET 9.0 | .NET 9.0 | SHA512_Truncated | 2,159,919.2 ns | 27,489.49 ns | 24,368.72 ns | 2,152,612.9 ns | 2,132,135.2 ns | 2,197,020.3 ns | 5,917.39 | 110.84 | - | 1104 B | 1.00 | +| 'Built-in SHA-512 truncated -> 256 — small (64 bytes)' | .NET 9.0 | .NET 9.0 | SHA512_Truncated | 443.3 ns | 8.29 ns | 7.75 ns | 442.5 ns | 432.7 ns | 461.5 ns | 1.21 | 0.03 | 0.0233 | 368 B | 0.33 | +| 'Built-in SHA-512 truncated -> 256 — large (1 MB)' | .NET 9.0 | .NET 9.0 | SHA512_Truncated | 1,352,995.6 ns | 26,185.39 ns | 25,717.56 ns | 1,353,562.5 ns | 1,312,020.6 ns | 1,403,619.0 ns | 3,706.71 | 88.66 | - | 369 B | 0.33 | +| 'Param-based: ComputeHash (selects algorithm by [Params] Variant)' | .NET 9.0 | .NET 9.0 | SHA512_Truncated | 447.2 ns | 8.90 ns | 8.33 ns | 445.7 ns | 436.6 ns | 466.0 ns | 1.23 | 0.03 | 0.0231 | 368 B | 0.33 | diff --git a/reports/tuning/Cuemon.Security.CyclicRedundancyCheckBenchmark-report-github.md b/reports/tuning/Cuemon.Security.CyclicRedundancyCheckBenchmark-report-github.md new file mode 100644 index 000000000..37706e2f4 --- /dev/null +++ b/reports/tuning/Cuemon.Security.CyclicRedundancyCheckBenchmark-report-github.md @@ -0,0 +1,41 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Size | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Gen2 | Allocated | Alloc Ratio | +|--------------------------------- |---------- |---------- |-------- |---------------:|--------------:|--------------:|---------------:|---------------:|---------------:|------:|--------:|---------:|---------:|---------:|----------:|------------:| +| **'CRC32 - byte[]'** | **.NET 10.0** | **.NET 10.0** | **64** | **139.4 ns** | **3.97 ns** | **3.90 ns** | **140.8 ns** | **129.2 ns** | **141.6 ns** | **1.04** | **0.09** | **0.0049** | **-** | **-** | **80 B** | **0.56** | +| 'CRC64 - byte[]' | .NET 10.0 | .NET 10.0 | 64 | 157.7 ns | 2.53 ns | 2.24 ns | 158.3 ns | 150.0 ns | 158.8 ns | 1.17 | 0.10 | 0.0046 | - | - | 80 B | 0.56 | +| 'CRC32 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 64 | 554.0 ns | 45.22 ns | 52.07 ns | 574.8 ns | 370.3 ns | 599.3 ns | 4.12 | 0.52 | 0.0734 | - | - | 1160 B | 8.06 | +| 'CRC64 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 64 | 539.3 ns | 65.19 ns | 72.46 ns | 568.3 ns | 354.1 ns | 599.7 ns | 4.01 | 0.63 | 0.0726 | - | - | 1160 B | 8.06 | +| 'CRC32 - byte[]' | .NET 9.0 | .NET 9.0 | 64 | 135.3 ns | 9.02 ns | 10.39 ns | 139.9 ns | 111.4 ns | 142.7 ns | 1.01 | 0.12 | 0.0092 | - | - | 144 B | 1.00 | +| 'CRC64 - byte[]' | .NET 9.0 | .NET 9.0 | 64 | 152.5 ns | 10.65 ns | 11.84 ns | 157.7 ns | 125.7 ns | 159.6 ns | 1.13 | 0.13 | 0.0091 | - | - | 144 B | 1.00 | +| 'CRC32 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 64 | 532.0 ns | 48.05 ns | 55.33 ns | 551.2 ns | 361.5 ns | 568.8 ns | 3.96 | 0.53 | 0.0775 | - | - | 1224 B | 8.50 | +| 'CRC64 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 64 | 529.2 ns | 64.64 ns | 74.44 ns | 565.1 ns | 372.9 ns | 581.3 ns | 3.94 | 0.64 | 0.0778 | - | - | 1224 B | 8.50 | +| | | | | | | | | | | | | | | | | | +| **'CRC32 - byte[]'** | **.NET 10.0** | **.NET 10.0** | **4096** | **7,721.2 ns** | **250.04 ns** | **277.92 ns** | **7,768.4 ns** | **6,967.3 ns** | **7,986.1 ns** | **1.01** | **0.05** | **-** | **-** | **-** | **144 B** | **1.00** | +| 'CRC64 - byte[]' | .NET 10.0 | .NET 10.0 | 4096 | 8,842.1 ns | 441.50 ns | 508.43 ns | 8,979.8 ns | 7,659.8 ns | 9,423.5 ns | 1.15 | 0.07 | - | - | - | 144 B | 1.00 | +| 'CRC32 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 4096 | 8,938.0 ns | 485.17 ns | 558.72 ns | 9,141.2 ns | 7,730.1 ns | 9,570.1 ns | 1.16 | 0.08 | 0.5614 | - | - | 9097 B | 63.17 | +| 'CRC64 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 4096 | 10,274.8 ns | 494.99 ns | 570.04 ns | 10,440.1 ns | 8,600.8 ns | 10,645.7 ns | 1.34 | 0.08 | 0.5759 | - | - | 9097 B | 63.17 | +| 'CRC32 - byte[]' | .NET 9.0 | .NET 9.0 | 4096 | 7,690.3 ns | 226.06 ns | 241.88 ns | 7,752.2 ns | 6,976.9 ns | 7,897.0 ns | 1.00 | 0.05 | - | - | - | 144 B | 1.00 | +| 'CRC64 - byte[]' | .NET 9.0 | .NET 9.0 | 4096 | 8,811.7 ns | 302.74 ns | 348.64 ns | 8,864.9 ns | 7,821.1 ns | 9,119.8 ns | 1.15 | 0.06 | - | - | - | 144 B | 1.00 | +| 'CRC32 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 4096 | 8,635.2 ns | 452.94 ns | 521.60 ns | 8,836.1 ns | 7,652.1 ns | 9,293.9 ns | 1.12 | 0.08 | 0.5690 | - | - | 9097 B | 63.17 | +| 'CRC64 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 4096 | 9,868.1 ns | 570.02 ns | 656.43 ns | 10,164.5 ns | 8,495.0 ns | 10,538.8 ns | 1.28 | 0.09 | 0.5473 | - | - | 9097 B | 63.17 | +| | | | | | | | | | | | | | | | | | +| **'CRC32 - byte[]'** | **.NET 10.0** | **.NET 10.0** | **1048576** | **1,922,070.0 ns** | **90,218.05 ns** | **103,895.24 ns** | **1,957,771.9 ns** | **1,783,972.9 ns** | **2,105,766.7 ns** | **1.02** | **0.07** | **-** | **-** | **-** | **146 B** | **1.00** | +| 'CRC64 - byte[]' | .NET 10.0 | .NET 10.0 | 1048576 | 2,146,272.3 ns | 146,697.64 ns | 168,937.23 ns | 2,228,165.6 ns | 1,940,368.0 ns | 2,367,431.2 ns | 1.13 | 0.10 | - | - | - | 146 B | 1.00 | +| 'CRC32 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 1048576 | 2,285,845.9 ns | 154,426.14 ns | 177,837.38 ns | 2,358,128.5 ns | 2,053,800.0 ns | 2,562,028.1 ns | 1.21 | 0.11 | 484.3750 | 484.3750 | 484.3750 | 2098846 B | 14,375.66 | +| 'CRC64 - Stream (includes copy)' | .NET 10.0 | .NET 10.0 | 1048576 | 2,551,377.1 ns | 229,852.85 ns | 264,698.88 ns | 2,737,308.5 ns | 2,232,625.9 ns | 2,880,704.5 ns | 1.35 | 0.15 | 482.1429 | 482.1429 | 482.1429 | 2098788 B | 14,375.26 | +| 'CRC32 - byte[]' | .NET 9.0 | .NET 9.0 | 1048576 | 1,896,976.5 ns | 74,945.23 ns | 86,307.04 ns | 1,957,661.1 ns | 1,754,100.0 ns | 1,982,995.1 ns | 1.00 | 0.06 | - | - | - | 146 B | 1.00 | +| 'CRC64 - byte[]' | .NET 9.0 | .NET 9.0 | 1048576 | 2,151,235.2 ns | 137,428.43 ns | 158,262.78 ns | 2,240,808.2 ns | 1,965,300.0 ns | 2,401,797.7 ns | 1.14 | 0.10 | - | - | - | 146 B | 1.00 | +| 'CRC32 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 1048576 | 2,273,448.7 ns | 156,886.61 ns | 180,670.86 ns | 2,382,739.5 ns | 2,029,253.9 ns | 2,501,768.8 ns | 1.20 | 0.11 | 492.1875 | 492.1875 | 492.1875 | 2098739 B | 14,374.92 | +| 'CRC64 - Stream (includes copy)' | .NET 9.0 | .NET 9.0 | 1048576 | 2,515,326.5 ns | 200,435.05 ns | 230,821.30 ns | 2,654,724.1 ns | 2,209,127.7 ns | 2,794,425.9 ns | 1.33 | 0.13 | 482.1429 | 482.1429 | 482.1429 | 2098829 B | 14,375.54 | diff --git a/reports/tuning/Cuemon.Security.FowlerNollVoHashBenchmark-report-github.md b/reports/tuning/Cuemon.Security.FowlerNollVoHashBenchmark-report-github.md new file mode 100644 index 000000000..da9c252f8 --- /dev/null +++ b/reports/tuning/Cuemon.Security.FowlerNollVoHashBenchmark-report-github.md @@ -0,0 +1,86 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7171) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Algorithm | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Allocated | Alloc Ratio | +|-------------------------- |---------- |---------- |---------- |------------------:|------------------:|------------------:|------------------:|------------------:|------------------:|------:|--------:|-------:|----------:|------------:| +| **'ComputeHash32 (small)'** | **.NET 10.0** | **.NET 10.0** | **Fnv1** | **44.49 ns** | **0.960 ns** | **1.028 ns** | **44.50 ns** | **41.78 ns** | **46.69 ns** | **0.98** | **0.03** | **0.0035** | **56 B** | **1.00** | +| 'ComputeHash32 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 45.27 ns | 0.657 ns | 0.583 ns | 45.11 ns | 44.32 ns | 46.26 ns | 1.00 | 0.02 | 0.0035 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash32 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 475,761.25 ns | 12,785.402 ns | 13,680.237 ns | 479,767.99 ns | 434,213.64 ns | 488,995.83 ns | 0.98 | 0.03 | - | 56 B | 1.00 | +| 'ComputeHash32 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 487,741.70 ns | 6,913.366 ns | 6,466.767 ns | 485,802.84 ns | 479,649.24 ns | 503,178.60 ns | 1.00 | 0.02 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash64 (small)' | .NET 10.0 | .NET 10.0 | Fnv1 | 61.62 ns | 1.259 ns | 1.178 ns | 61.33 ns | 59.84 ns | 63.93 ns | 1.11 | 0.33 | 0.0034 | 56 B | 1.00 | +| 'ComputeHash64 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 57.90 ns | 8.583 ns | 9.183 ns | 60.78 ns | 25.57 ns | 63.32 ns | 1.05 | 0.35 | 0.0034 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash64 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 700,133.50 ns | 6,687.106 ns | 5,927.945 ns | 698,258.56 ns | 693,839.81 ns | 712,956.93 ns | 1.00 | 0.02 | - | 56 B | 1.00 | +| 'ComputeHash64 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 702,394.36 ns | 11,875.872 ns | 11,108.698 ns | 702,286.08 ns | 674,915.34 ns | 718,671.31 ns | 1.00 | 0.02 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash128 (small)' | .NET 10.0 | .NET 10.0 | Fnv1 | 1,014.18 ns | 16.251 ns | 14.406 ns | 1,017.40 ns | 976.62 ns | 1,030.23 ns | 0.92 | 0.02 | 0.0120 | 192 B | 0.75 | +| 'ComputeHash128 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 1,096.77 ns | 11.682 ns | 10.356 ns | 1,093.03 ns | 1,085.03 ns | 1,121.10 ns | 1.00 | 0.01 | 0.0132 | 256 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash128 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 9,491,544.42 ns | 410,672.854 ns | 456,461.789 ns | 9,629,732.00 ns | 7,879,044.00 ns | 9,805,012.00 ns | 0.93 | 0.08 | - | 201 B | 0.76 | +| 'ComputeHash128 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 10,297,469.17 ns | 543,543.911 ns | 625,945.977 ns | 10,479,429.17 ns | 8,199,195.83 ns | 10,764,233.33 ns | 1.00 | 0.09 | - | 265 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash256 (small)' | .NET 10.0 | .NET 10.0 | Fnv1 | 2,408.15 ns | 98.561 ns | 113.503 ns | 2,404.36 ns | 1,968.61 ns | 2,539.86 ns | 1.00 | 0.05 | 0.0086 | 240 B | 0.79 | +| 'ComputeHash256 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 2,407.95 ns | 39.906 ns | 33.323 ns | 2,401.07 ns | 2,357.00 ns | 2,493.70 ns | 1.00 | 0.02 | 0.0187 | 304 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash256 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 23,361,921.67 ns | 343,325.153 ns | 321,146.561 ns | 23,236,800.00 ns | 23,022,750.00 ns | 24,182,875.00 ns | 1.09 | 0.25 | - | 296 B | 0.83 | +| 'ComputeHash256 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 22,088,434.21 ns | 2,983,730.504 ns | 3,316,408.550 ns | 23,243,050.00 ns | 11,580,200.00 ns | 25,254,625.00 ns | 1.03 | 0.28 | - | 358 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash512 (small)' | .NET 10.0 | .NET 10.0 | Fnv1 | 7,310.60 ns | 288.216 ns | 320.351 ns | 7,380.94 ns | 6,126.43 ns | 7,567.07 ns | 1.01 | 0.07 | - | 336 B | 0.84 | +| 'ComputeHash512 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 7,284.33 ns | 371.306 ns | 381.304 ns | 7,434.16 ns | 6,262.40 ns | 7,614.62 ns | 1.00 | 0.08 | - | 400 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash512 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 72,438,354.17 ns | 2,328,745.921 ns | 2,491,732.047 ns | 72,952,287.50 ns | 64,311,150.00 ns | 75,007,950.00 ns | 1.00 | 0.05 | - | 392 B | 0.86 | +| 'ComputeHash512 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 72,777,940.79 ns | 2,378,159.721 ns | 2,643,318.229 ns | 73,390,775.00 ns | 64,024,825.00 ns | 74,868,275.00 ns | 1.00 | 0.05 | - | 454 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash1024 (small)' | .NET 10.0 | .NET 10.0 | Fnv1 | 21,839.16 ns | 607.793 ns | 650.332 ns | 22,048.48 ns | 19,799.26 ns | 22,404.92 ns | 0.97 | 0.06 | - | 528 B | 0.89 | +| 'ComputeHash1024 (small)' | .NET 9.0 | .NET 9.0 | Fnv1 | 22,499.04 ns | 839.987 ns | 967.331 ns | 22,631.62 ns | 18,620.93 ns | 23,350.81 ns | 1.00 | 0.06 | - | 592 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash1024 (large)' | .NET 10.0 | .NET 10.0 | Fnv1 | 293,379,305.88 ns | 5,519,217.527 ns | 5,667,830.054 ns | 294,238,400.00 ns | 278,105,900.00 ns | 299,262,000.00 ns | 1.09 | 0.02 | - | 752 B | 0.93 | +| 'ComputeHash1024 (large)' | .NET 9.0 | .NET 9.0 | Fnv1 | 268,643,542.86 ns | 2,198,007.588 ns | 1,948,476.476 ns | 268,287,600.00 ns | 266,328,900.00 ns | 272,958,100.00 ns | 1.00 | 0.01 | - | 808 B | 1.00 | +| | | | | | | | | | | | | | | | +| **'ComputeHash32 (small)'** | **.NET 10.0** | **.NET 10.0** | **Fnv1a** | **44.13 ns** | **1.764 ns** | **1.888 ns** | **44.35 ns** | **37.26 ns** | **46.31 ns** | **1.01** | **0.05** | **0.0035** | **56 B** | **1.00** | +| 'ComputeHash32 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 43.83 ns | 0.904 ns | 0.929 ns | 43.80 ns | 41.79 ns | 45.41 ns | 1.00 | 0.03 | 0.0035 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash32 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 484,862.53 ns | 7,613.077 ns | 6,357.264 ns | 486,333.59 ns | 464,753.71 ns | 490,745.51 ns | 1.00 | 0.01 | - | 56 B | 1.00 | +| 'ComputeHash32 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 486,370.96 ns | 1,899.973 ns | 1,483.374 ns | 486,302.73 ns | 484,818.36 ns | 489,958.79 ns | 1.00 | 0.00 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash64 (small)' | .NET 10.0 | .NET 10.0 | Fnv1a | 61.73 ns | 1.143 ns | 1.069 ns | 61.48 ns | 60.07 ns | 63.70 ns | 1.01 | 0.09 | 0.0035 | 56 B | 1.00 | +| 'ComputeHash64 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 61.44 ns | 4.144 ns | 4.772 ns | 62.61 ns | 47.59 ns | 66.51 ns | 1.01 | 0.12 | 0.0035 | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash64 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 691,714.33 ns | 59,489.249 ns | 63,652.830 ns | 709,552.98 ns | 513,634.38 ns | 729,552.56 ns | 0.97 | 0.09 | - | 56 B | 1.00 | +| 'ComputeHash64 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 713,994.81 ns | 9,296.636 ns | 8,696.079 ns | 711,014.49 ns | 702,122.73 ns | 730,892.90 ns | 1.00 | 0.02 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash128 (small)' | .NET 10.0 | .NET 10.0 | Fnv1a | 992.60 ns | 60.035 ns | 69.136 ns | 1,020.09 ns | 783.62 ns | 1,041.29 ns | 0.90 | 0.06 | 0.0082 | 192 B | 0.75 | +| 'ComputeHash128 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 1,099.42 ns | 13.869 ns | 12.973 ns | 1,095.27 ns | 1,086.14 ns | 1,125.26 ns | 1.00 | 0.02 | 0.0132 | 256 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash128 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 9,425,976.73 ns | 705,042.362 ns | 811,927.833 ns | 9,673,563.46 ns | 6,381,980.77 ns | 9,848,542.31 ns | 0.93 | 0.09 | - | 201 B | 0.76 | +| 'ComputeHash128 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 10,174,929.17 ns | 458,639.419 ns | 470,988.916 ns | 10,264,316.67 ns | 8,371,045.83 ns | 10,497,000.00 ns | 1.00 | 0.07 | - | 265 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash256 (small)' | .NET 10.0 | .NET 10.0 | Fnv1a | 2,434.92 ns | 47.154 ns | 52.411 ns | 2,413.43 ns | 2,308.16 ns | 2,540.21 ns | 1.01 | 0.02 | 0.0096 | 240 B | 0.79 | +| 'ComputeHash256 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 2,410.69 ns | 24.274 ns | 20.270 ns | 2,405.62 ns | 2,389.81 ns | 2,467.03 ns | 1.00 | 0.01 | 0.0193 | 304 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash256 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 23,223,505.56 ns | 953,208.733 ns | 1,019,922.665 ns | 23,494,662.50 ns | 20,199,400.00 ns | 24,015,300.00 ns | 0.92 | 0.06 | - | 296 B | 0.83 | +| 'ComputeHash256 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 25,412,497.50 ns | 1,005,989.704 ns | 1,158,499.239 ns | 25,560,937.50 ns | 23,667,600.00 ns | 27,531,950.00 ns | 1.00 | 0.06 | - | 358 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash512 (small)' | .NET 10.0 | .NET 10.0 | Fnv1a | 7,378.49 ns | 245.488 ns | 282.705 ns | 7,369.34 ns | 6,265.74 ns | 7,630.78 ns | 0.99 | 0.04 | - | 336 B | 0.84 | +| 'ComputeHash512 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 7,460.73 ns | 89.626 ns | 83.836 ns | 7,445.17 ns | 7,326.61 ns | 7,666.13 ns | 1.00 | 0.02 | - | 400 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash512 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 74,070,804.41 ns | 1,476,981.807 ns | 1,516,751.574 ns | 74,301,275.00 ns | 71,446,950.00 ns | 76,544,125.00 ns | 1.01 | 0.04 | - | 392 B | 0.86 | +| 'ComputeHash512 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 73,099,323.68 ns | 2,326,454.946 ns | 2,585,848.509 ns | 73,528,900.00 ns | 62,658,075.00 ns | 75,058,700.00 ns | 1.00 | 0.05 | - | 454 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash1024 (small)' | .NET 10.0 | .NET 10.0 | Fnv1a | 21,997.89 ns | 679.030 ns | 781.971 ns | 22,104.69 ns | 19,033.40 ns | 22,638.99 ns | 1.01 | 0.09 | - | 528 B | 0.89 | +| 'ComputeHash1024 (small)' | .NET 9.0 | .NET 9.0 | Fnv1a | 21,927.44 ns | 1,573.783 ns | 1,616.159 ns | 22,637.87 ns | 18,012.92 ns | 22,987.92 ns | 1.01 | 0.11 | - | 592 B | 1.00 | +| | | | | | | | | | | | | | | | +| 'ComputeHash1024 (large)' | .NET 10.0 | .NET 10.0 | Fnv1a | 290,670,140.00 ns | 11,015,382.219 ns | 12,685,330.548 ns | 292,789,800.00 ns | 251,227,700.00 ns | 301,504,900.00 ns | 1.13 | 0.23 | - | 752 B | 0.93 | +| 'ComputeHash1024 (large)' | .NET 9.0 | .NET 9.0 | Fnv1a | 261,996,177.78 ns | 28,627,002.702 ns | 30,630,572.191 ns | 270,939,850.00 ns | 142,795,000.00 ns | 274,029,300.00 ns | 1.02 | 0.23 | - | 808 B | 1.00 | diff --git a/reports/tuning/Cuemon.Security.HashResultBenchmark-report-github.md b/reports/tuning/Cuemon.Security.HashResultBenchmark-report-github.md new file mode 100644 index 000000000..86eb4537c --- /dev/null +++ b/reports/tuning/Cuemon.Security.HashResultBenchmark-report-github.md @@ -0,0 +1,104 @@ +``` + +BenchmarkDotNet v0.15.6, Windows 11 (10.0.26200.7309) +12th Gen Intel Core i9-12900KF 3.20GHz, 1 CPU, 24 logical and 16 physical cores +.NET SDK 10.0.100 + [Host] : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 10.0 : .NET 10.0.0 (10.0.0, 10.0.25.52411), X64 RyuJIT x86-64-v3 + .NET 9.0 : .NET 9.0.11 (9.0.11, 9.0.1125.51716), X64 RyuJIT x86-64-v3 + +PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 +MinIterationCount=15 WarmupCount=1 + +``` +| Method | Job | Runtime | Size | Mean | Error | StdDev | Median | Min | Max | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio | +|------------------------------------ |---------- |---------- |----- |---------------:|--------------:|--------------:|---------------:|---------------:|---------------:|------:|--------:|-------:|-------:|----------:|------------:| +| **'HashResult.GetBytes - copy bytes'** | **.NET 10.0** | **.NET 10.0** | **0** | **0.1899 ns** | **0.1607 ns** | **0.1850 ns** | **0.1019 ns** | **0.0000 ns** | **0.4901 ns** | **0.32** | **0.30** | **-** | **-** | **-** | **NA** | +| 'HashResult.GetBytes - copy bytes' | .NET 9.0 | .NET 9.0 | 0 | 0.6050 ns | 0.0575 ns | 0.0538 ns | 0.5937 ns | 0.5139 ns | 0.6962 ns | 1.01 | 0.12 | - | - | - | NA | +| | | | | | | | | | | | | | | | | +| HashResult.ToHexadecimalString | .NET 10.0 | .NET 10.0 | 0 | 4.3974 ns | 0.1999 ns | 0.2222 ns | 4.4440 ns | 4.0578 ns | 4.9124 ns | 0.78 | 0.04 | - | - | - | NA | +| HashResult.ToHexadecimalString | .NET 9.0 | .NET 9.0 | 0 | 5.6316 ns | 0.1368 ns | 0.1279 ns | 5.5889 ns | 5.5085 ns | 5.9739 ns | 1.00 | 0.03 | - | - | - | NA | +| | | | | | | | | | | | | | | | | +| HashResult.ToBase64String | .NET 10.0 | .NET 10.0 | 0 | 0.7507 ns | 0.0206 ns | 0.0182 ns | 0.7490 ns | 0.7177 ns | 0.7800 ns | 0.60 | 0.04 | - | - | - | NA | +| HashResult.ToBase64String | .NET 9.0 | .NET 9.0 | 0 | 1.2622 ns | 0.0789 ns | 0.0810 ns | 1.2306 ns | 1.1764 ns | 1.4189 ns | 1.00 | 0.09 | - | - | - | NA | +| | | | | | | | | | | | | | | | | +| HashResult.ToUrlEncodedBase64String | .NET 10.0 | .NET 10.0 | 0 | 7.9269 ns | 0.2077 ns | 0.2133 ns | 7.8810 ns | 7.5153 ns | 8.2935 ns | 0.75 | 0.03 | 0.0020 | - | 32 B | 1.00 | +| HashResult.ToUrlEncodedBase64String | .NET 9.0 | .NET 9.0 | 0 | 10.6080 ns | 0.2623 ns | 0.2694 ns | 10.6551 ns | 10.2409 ns | 11.1723 ns | 1.00 | 0.03 | 0.0020 | - | 32 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBinaryString | .NET 10.0 | .NET 10.0 | 0 | 2.3159 ns | 0.0532 ns | 0.0444 ns | 2.3027 ns | 2.2691 ns | 2.4234 ns | 0.28 | 0.01 | - | - | - | NA | +| HashResult.ToBinaryString | .NET 9.0 | .NET 9.0 | 0 | 8.2501 ns | 0.2058 ns | 0.1925 ns | 8.2087 ns | 8.0026 ns | 8.5642 ns | 1.00 | 0.03 | - | - | - | NA | +| | | | | | | | | | | | | | | | | +| 'HashResult.To<string> (converter)' | .NET 10.0 | .NET 10.0 | 0 | 1.4854 ns | 0.0419 ns | 0.0371 ns | 1.4837 ns | 1.4305 ns | 1.5463 ns | 0.77 | 0.03 | - | - | - | NA | +| 'HashResult.To<string> (converter)' | .NET 9.0 | .NET 9.0 | 0 | 1.9255 ns | 0.0500 ns | 0.0468 ns | 1.9154 ns | 1.8678 ns | 2.0306 ns | 1.00 | 0.03 | - | - | - | NA | +| | | | | | | | | | | | | | | | | +| **'HashResult.GetBytes - copy bytes'** | **.NET 10.0** | **.NET 10.0** | **8** | **3.6698 ns** | **0.1523 ns** | **0.1693 ns** | **3.6968 ns** | **3.3961 ns** | **3.9878 ns** | **0.70** | **0.08** | **0.0020** | **-** | **32 B** | **1.00** | +| 'HashResult.GetBytes - copy bytes' | .NET 9.0 | .NET 9.0 | 8 | 5.3099 ns | 0.4813 ns | 0.5543 ns | 5.1356 ns | 4.6109 ns | 6.0498 ns | 1.01 | 0.15 | 0.0020 | - | 32 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToHexadecimalString | .NET 10.0 | .NET 10.0 | 8 | 18.8266 ns | 0.4924 ns | 0.5268 ns | 18.7881 ns | 17.9686 ns | 19.7976 ns | 0.77 | 0.06 | 0.0071 | - | 112 B | 1.00 | +| HashResult.ToHexadecimalString | .NET 9.0 | .NET 9.0 | 8 | 24.4800 ns | 1.5941 ns | 1.8358 ns | 24.5886 ns | 21.8618 ns | 29.1413 ns | 1.01 | 0.10 | 0.0071 | - | 112 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBase64String | .NET 10.0 | .NET 10.0 | 8 | 8.9394 ns | 0.2436 ns | 0.2501 ns | 8.9677 ns | 8.5694 ns | 9.2560 ns | 0.83 | 0.04 | 0.0031 | - | 48 B | 1.00 | +| HashResult.ToBase64String | .NET 9.0 | .NET 9.0 | 8 | 10.8334 ns | 0.3936 ns | 0.4532 ns | 10.8449 ns | 10.2466 ns | 11.6212 ns | 1.00 | 0.06 | 0.0030 | - | 48 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToUrlEncodedBase64String | .NET 10.0 | .NET 10.0 | 8 | 31.5398 ns | 1.1195 ns | 1.2892 ns | 31.3105 ns | 30.0733 ns | 34.5121 ns | 0.84 | 0.04 | 0.0086 | - | 136 B | 1.00 | +| HashResult.ToUrlEncodedBase64String | .NET 9.0 | .NET 9.0 | 8 | 37.7741 ns | 0.7402 ns | 0.7270 ns | 37.9246 ns | 36.1056 ns | 38.6473 ns | 1.00 | 0.03 | 0.0086 | - | 136 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBinaryString | .NET 10.0 | .NET 10.0 | 8 | 121.2123 ns | 2.7938 ns | 3.2173 ns | 120.6071 ns | 116.4115 ns | 127.9913 ns | 0.82 | 0.03 | 0.0423 | - | 664 B | 1.00 | +| HashResult.ToBinaryString | .NET 9.0 | .NET 9.0 | 8 | 147.2114 ns | 2.8870 ns | 3.0890 ns | 147.1515 ns | 141.8733 ns | 152.5684 ns | 1.00 | 0.03 | 0.0421 | - | 664 B | 1.00 | +| | | | | | | | | | | | | | | | | +| 'HashResult.To<string> (converter)' | .NET 10.0 | .NET 10.0 | 8 | 9.5429 ns | 0.3250 ns | 0.3612 ns | 9.4518 ns | 9.1711 ns | 10.5143 ns | 0.66 | 0.18 | 0.0030 | - | 48 B | 1.00 | +| 'HashResult.To<string> (converter)' | .NET 9.0 | .NET 9.0 | 8 | 16.0595 ns | 5.1452 ns | 5.9252 ns | 12.3539 ns | 11.5648 ns | 26.4717 ns | 1.11 | 0.52 | 0.0030 | - | 48 B | 1.00 | +| | | | | | | | | | | | | | | | | +| **'HashResult.GetBytes - copy bytes'** | **.NET 10.0** | **.NET 10.0** | **32** | **2.9549 ns** | **0.2125 ns** | **0.2183 ns** | **2.8789 ns** | **2.7061 ns** | **3.3933 ns** | **0.62** | **0.22** | **0.0035** | **-** | **56 B** | **1.00** | +| 'HashResult.GetBytes - copy bytes' | .NET 9.0 | .NET 9.0 | 32 | 5.6959 ns | 2.3447 ns | 2.7001 ns | 3.9788 ns | 3.5512 ns | 10.1172 ns | 1.19 | 0.71 | 0.0035 | - | 56 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToHexadecimalString | .NET 10.0 | .NET 10.0 | 32 | 27.1042 ns | 0.6509 ns | 0.7235 ns | 26.9399 ns | 26.3488 ns | 28.6672 ns | 0.73 | 0.04 | 0.0193 | - | 304 B | 1.00 | +| HashResult.ToHexadecimalString | .NET 9.0 | .NET 9.0 | 32 | 37.2492 ns | 1.7830 ns | 1.9078 ns | 36.7310 ns | 34.0068 ns | 41.5047 ns | 1.00 | 0.07 | 0.0193 | - | 304 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBase64String | .NET 10.0 | .NET 10.0 | 32 | 33.9563 ns | 0.8099 ns | 0.9327 ns | 34.0774 ns | 31.0488 ns | 35.2414 ns | 1.78 | 0.11 | 0.0071 | - | 112 B | 1.00 | +| HashResult.ToBase64String | .NET 9.0 | .NET 9.0 | 32 | 19.1324 ns | 0.9202 ns | 1.0229 ns | 19.1216 ns | 16.4070 ns | 20.6657 ns | 1.00 | 0.08 | 0.0071 | - | 112 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToUrlEncodedBase64String | .NET 10.0 | .NET 10.0 | 32 | 105.9781 ns | 16.7741 ns | 19.3171 ns | 113.8672 ns | 57.6292 ns | 119.9665 ns | 1.54 | 0.29 | 0.0311 | - | 488 B | 1.00 | +| HashResult.ToUrlEncodedBase64String | .NET 9.0 | .NET 9.0 | 32 | 68.9203 ns | 3.2866 ns | 3.7848 ns | 67.4534 ns | 64.9570 ns | 77.4326 ns | 1.00 | 0.07 | 0.0310 | - | 488 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBinaryString | .NET 10.0 | .NET 10.0 | 32 | 788.0583 ns | 123.1042 ns | 141.7669 ns | 854.2335 ns | 446.7816 ns | 873.4718 ns | 0.93 | 0.32 | 0.1589 | - | 2504 B | 1.00 | +| HashResult.ToBinaryString | .NET 9.0 | .NET 9.0 | 32 | 902.7890 ns | 159.0826 ns | 183.1998 ns | 981.6323 ns | 491.7358 ns | 1,034.5140 ns | 1.06 | 0.38 | 0.1592 | - | 2504 B | 1.00 | +| | | | | | | | | | | | | | | | | +| 'HashResult.To<string> (converter)' | .NET 10.0 | .NET 10.0 | 32 | 32.1923 ns | 0.8480 ns | 0.9425 ns | 32.0620 ns | 30.5834 ns | 34.1729 ns | 0.89 | 0.06 | 0.0071 | - | 112 B | 1.00 | +| 'HashResult.To<string> (converter)' | .NET 9.0 | .NET 9.0 | 32 | 36.1355 ns | 1.9100 ns | 2.1995 ns | 36.5477 ns | 31.1890 ns | 39.8293 ns | 1.00 | 0.09 | 0.0071 | - | 112 B | 1.00 | +| | | | | | | | | | | | | | | | | +| **'HashResult.GetBytes - copy bytes'** | **.NET 10.0** | **.NET 10.0** | **256** | **29.7658 ns** | **3.6673 ns** | **4.2233 ns** | **29.8639 ns** | **22.4952 ns** | **38.0444 ns** | **0.77** | **0.13** | **0.0178** | **-** | **280 B** | **1.00** | +| 'HashResult.GetBytes - copy bytes' | .NET 9.0 | .NET 9.0 | 256 | 38.8652 ns | 3.0793 ns | 3.4226 ns | 39.6851 ns | 31.0340 ns | 44.2388 ns | 1.01 | 0.13 | 0.0178 | - | 280 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToHexadecimalString | .NET 10.0 | .NET 10.0 | 256 | 332.3619 ns | 35.7998 ns | 41.2272 ns | 333.7390 ns | 264.1860 ns | 389.6913 ns | 1.17 | 0.19 | 0.1330 | - | 2096 B | 1.00 | +| HashResult.ToHexadecimalString | .NET 9.0 | .NET 9.0 | 256 | 286.6217 ns | 27.1810 ns | 31.3017 ns | 291.8207 ns | 238.8120 ns | 341.9756 ns | 1.01 | 0.15 | 0.1335 | - | 2096 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBase64String | .NET 10.0 | .NET 10.0 | 256 | 123.0009 ns | 12.4901 ns | 14.3836 ns | 127.7483 ns | 71.2376 ns | 138.7922 ns | 1.24 | 0.21 | 0.0454 | - | 712 B | 1.00 | +| HashResult.ToBase64String | .NET 9.0 | .NET 9.0 | 256 | 100.6536 ns | 8.5627 ns | 9.8609 ns | 101.4415 ns | 66.4831 ns | 112.4094 ns | 1.01 | 0.16 | 0.0453 | - | 712 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToUrlEncodedBase64String | .NET 10.0 | .NET 10.0 | 256 | 407.1584 ns | 40.0446 ns | 46.1154 ns | 411.8714 ns | 259.9935 ns | 470.3277 ns | 1.12 | 0.26 | 0.1844 | - | 2896 B | 1.00 | +| HashResult.ToUrlEncodedBase64String | .NET 9.0 | .NET 9.0 | 256 | 371.1954 ns | 39.6840 ns | 45.7001 ns | 382.8811 ns | 197.3467 ns | 417.4752 ns | 1.02 | 0.24 | 0.1842 | - | 2896 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBinaryString | .NET 10.0 | .NET 10.0 | 256 | 6,218.3123 ns | 1,481.9248 ns | 1,706.5868 ns | 7,204.6794 ns | 3,502.0993 ns | 8,131.5561 ns | 0.82 | 0.32 | 1.2212 | - | 19296 B | 1.00 | +| HashResult.ToBinaryString | .NET 9.0 | .NET 9.0 | 256 | 7,939.5740 ns | 1,185.5915 ns | 1,365.3289 ns | 8,278.9708 ns | 4,101.9731 ns | 9,248.0146 ns | 1.05 | 0.34 | 1.2303 | - | 19296 B | 1.00 | +| | | | | | | | | | | | | | | | | +| 'HashResult.To<string> (converter)' | .NET 10.0 | .NET 10.0 | 256 | 110.8995 ns | 12.8471 ns | 14.7948 ns | 114.7455 ns | 78.0893 ns | 130.2795 ns | 1.12 | 0.24 | 0.0453 | - | 712 B | 1.00 | +| 'HashResult.To<string> (converter)' | .NET 9.0 | .NET 9.0 | 256 | 100.6368 ns | 10.6125 ns | 11.3553 ns | 101.3606 ns | 60.0488 ns | 116.4351 ns | 1.02 | 0.20 | 0.0453 | - | 712 B | 1.00 | +| | | | | | | | | | | | | | | | | +| **'HashResult.GetBytes - copy bytes'** | **.NET 10.0** | **.NET 10.0** | **1024** | **106.0886 ns** | **32.3535 ns** | **37.2584 ns** | **111.2442 ns** | **40.5701 ns** | **160.8209 ns** | **0.71** | **0.25** | **0.0667** | **-** | **1048 B** | **1.00** | +| 'HashResult.GetBytes - copy bytes' | .NET 9.0 | .NET 9.0 | 1024 | 150.7442 ns | 11.1358 ns | 12.8240 ns | 154.2401 ns | 122.7963 ns | 171.2166 ns | 1.01 | 0.12 | 0.0668 | - | 1048 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToHexadecimalString | .NET 10.0 | .NET 10.0 | 1024 | 1,191.2612 ns | 215.2370 ns | 247.8672 ns | 1,298.6681 ns | 574.7689 ns | 1,350.1792 ns | 1.04 | 0.29 | 0.5234 | - | 8240 B | 1.00 | +| HashResult.ToHexadecimalString | .NET 9.0 | .NET 9.0 | 1024 | 1,176.2450 ns | 162.5583 ns | 187.2024 ns | 1,243.0160 ns | 825.1306 ns | 1,386.8318 ns | 1.03 | 0.25 | 0.5251 | - | 8240 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBase64String | .NET 10.0 | .NET 10.0 | 1024 | 300.3005 ns | 42.9724 ns | 49.4871 ns | 317.5721 ns | 137.4879 ns | 350.7828 ns | 0.88 | 0.15 | 0.1758 | - | 2760 B | 1.00 | +| HashResult.ToBase64String | .NET 9.0 | .NET 9.0 | 1024 | 341.0496 ns | 19.4105 ns | 22.3531 ns | 335.7253 ns | 304.2064 ns | 394.2699 ns | 1.00 | 0.09 | 0.1754 | - | 2760 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToUrlEncodedBase64String | .NET 10.0 | .NET 10.0 | 1024 | 1,155.3536 ns | 224.8620 ns | 258.9515 ns | 1,206.5760 ns | 572.7688 ns | 1,607.1837 ns | 0.97 | 0.22 | 0.7055 | 0.0132 | 11088 B | 1.00 | +| HashResult.ToUrlEncodedBase64String | .NET 9.0 | .NET 9.0 | 1024 | 1,193.5769 ns | 64.3511 ns | 74.1068 ns | 1,212.5970 ns | 953.9424 ns | 1,291.8988 ns | 1.00 | 0.09 | 0.7057 | 0.0128 | 11088 B | 1.00 | +| | | | | | | | | | | | | | | | | +| HashResult.ToBinaryString | .NET 10.0 | .NET 10.0 | 1024 | 28,677.0629 ns | 6,031.6343 ns | 6,946.0390 ns | 31,176.7351 ns | 14,859.7019 ns | 35,315.9641 ns | 0.89 | 0.28 | 4.8265 | - | 76504 B | 1.00 | +| HashResult.ToBinaryString | .NET 9.0 | .NET 9.0 | 1024 | 33,046.6779 ns | 3,735.5753 ns | 3,997.0237 ns | 34,042.7982 ns | 17,311.8316 ns | 35,023.0998 ns | 1.03 | 0.25 | 4.8292 | - | 76504 B | 1.00 | +| | | | | | | | | | | | | | | | | +| 'HashResult.To<string> (converter)' | .NET 10.0 | .NET 10.0 | 1024 | 299.3046 ns | 58.4910 ns | 60.0659 ns | 318.5440 ns | 144.0678 ns | 346.3153 ns | 1.11 | 0.50 | 0.1760 | - | 2760 B | 1.00 | +| 'HashResult.To<string> (converter)' | .NET 9.0 | .NET 9.0 | 1024 | 296.0905 ns | 62.9758 ns | 72.5230 ns | 327.2458 ns | 136.8395 ns | 373.7436 ns | 1.10 | 0.52 | 0.1756 | - | 2760 B | 1.00 | diff --git a/src/Cuemon.Core/Configuration/IPostConfigurableParameterObject.cs b/src/Cuemon.Core/Configuration/IPostConfigurableParameterObject.cs new file mode 100644 index 000000000..97dabc449 --- /dev/null +++ b/src/Cuemon.Core/Configuration/IPostConfigurableParameterObject.cs @@ -0,0 +1,13 @@ +namespace Cuemon.Configuration +{ + /// + /// Denotes a Parameter Object that supports post-configuration logic after its public properties have been set. + /// + public interface IPostConfigurableParameterObject : IParameterObject + { + /// + /// Performs post-configuration logic based on the current state of the options. + /// + void PostConfigureOptions(); + } +} diff --git a/src/Cuemon.Core/DateSpan.cs b/src/Cuemon.Core/DateSpan.cs index 92fe51eb9..270f574e4 100644 --- a/src/Cuemon.Core/DateSpan.cs +++ b/src/Cuemon.Core/DateSpan.cs @@ -2,6 +2,7 @@ using System.Globalization; using System.Linq; using Cuemon.Collections.Generic; +using Cuemon.Security; namespace Cuemon { @@ -13,6 +14,9 @@ namespace Cuemon private readonly DateTime _lower; private readonly DateTime _upper; private readonly Calendar _calendar; + private readonly ulong _calendarId; + private static readonly ulong Hash = (ulong)new FowlerNollVo64().OffsetBasis; + private static readonly ulong Prime = (ulong)new FowlerNollVo64().Prime; /// /// Initializes a new instance of the structure with a default value set to . @@ -23,7 +27,7 @@ public DateSpan(DateTime start) : this(start, DateTime.Today) } /// - /// Initializes a new instance of the structure with a default value from the class. + /// Initializes a new instance of the structure with a default value from the class. /// /// A value for the calculation. /// A value for the calculation. @@ -45,69 +49,34 @@ public DateSpan(DateTime start, DateTime end, Calendar calendar) : this() _upper = Arguments.ToEnumerableOf(start, end).Max(); _calendar = calendar; + _calendarId = _calendar switch + { + ChineseLunisolarCalendar => 1, + JapaneseLunisolarCalendar => 2, + KoreanLunisolarCalendar => 3, + TaiwanLunisolarCalendar => 4, + EastAsianLunisolarCalendar => 5, + GregorianCalendar => 6, + HebrewCalendar => 7, + HijriCalendar => 8, + JapaneseCalendar => 9, + JulianCalendar => 10, + KoreanCalendar => 11, + PersianCalendar => 12, + TaiwanCalendar => 13, + ThaiBuddhistCalendar => 14, + UmAlQuraCalendar => 15, + _ => (ulong)_calendar.GetType().FullName!.GetHashCode() + }; + var lower = _lower; var upper = _upper; - var months = 0; - var days = 0; - var years = GetYears(upper, lower, out var adjustYearsMinusOne); - var hours = 0; - var milliseconds = 0; - - int daysPerYears = 0; - var y = lower.Year; - do - { - daysPerYears += _calendar.GetDaysInYear(y); - y++; - } while (y < upper.Year); - - var daysPerYearsAverage = years == 0 ? daysPerYears : Convert.ToDouble(daysPerYears) / Convert.ToDouble(years); - while (!lower.Year.Equals(upper.Year) || !lower.Month.Equals(upper.Month)) - { - var daysPerMonth = _calendar.GetDaysInMonth(lower.Year, lower.Month); - var peekNextLower = lower.AddMonths(1); - if (peekNextLower > upper) - { - while (!lower.Month.Equals(upper.Month) || !lower.Day.Equals(upper.Day)) - { - days++; - lower = lower.AddDays(1); - } - - if (lower > upper) - { - lower = lower.AddDays(-1); - days--; - - while (!lower.Hour.Equals(upper.Hour)) - { - hours++; - lower = lower.AddHours(1); - } - - while (!lower.Minute.Equals(upper.Minute) || !lower.Second.Equals(upper.Second) || !lower.Millisecond.Equals(upper.Millisecond)) - { - milliseconds++; - lower = lower.AddMilliseconds(1); - } - } - } - else - { - days += daysPerMonth; - lower = lower.AddMonths(1); - months++; - } - } + var daysPerYearsAverage = CalculateAverageDaysPerYear(lower, upper, years); - while (!lower.Day.Equals(upper.Day)) - { - days++; - lower = lower.AddDays(1); - } + CalculateDifference(ref lower, upper, out var months, out var days, out var hours, out var milliseconds); var averageDaysPerMonth = months == 0 ? days : Convert.ToDouble(days) / Convert.ToDouble(months); var remainder = new TimeSpan(days, hours, 0, 0, milliseconds); @@ -149,6 +118,81 @@ private static int GetYears(DateTime upper, DateTime lower, out bool adjustYears return years; } + private double CalculateAverageDaysPerYear(DateTime lower, DateTime upper, int years) + { + var daysPerYears = 0; + var y = lower.Year; + do + { + daysPerYears += _calendar.GetDaysInYear(y); + y++; + } while (y < upper.Year); + + return years == 0 ? daysPerYears : Convert.ToDouble(daysPerYears) / Convert.ToDouble(years); + } + + private void CalculateDifference(ref DateTime lower, DateTime upper, out int months, out int days, out int hours, out int milliseconds) + { + months = 0; + days = 0; + hours = 0; + milliseconds = 0; + + while (!lower.Year.Equals(upper.Year) || !lower.Month.Equals(upper.Month)) + { + var daysPerMonth = _calendar.GetDaysInMonth(lower.Year, lower.Month); + var peekNextLower = lower.AddMonths(1); + if (peekNextLower > upper) + { + CalculatePartialMonthDifference(ref lower, upper, ref days, ref hours, ref milliseconds); + } + else + { + days += daysPerMonth; + lower = lower.AddMonths(1); + months++; + } + } + + while (!lower.Day.Equals(upper.Day)) + { + days++; + lower = lower.AddDays(1); + } + } + + private static void CalculatePartialMonthDifference(ref DateTime lower, DateTime upper, ref int days, ref int hours, ref int milliseconds) + { + while (!lower.Month.Equals(upper.Month) || !lower.Day.Equals(upper.Day)) + { + days++; + lower = lower.AddDays(1); + } + + if (lower > upper) + { + lower = lower.AddDays(-1); + days--; + + CalculateTimeDifference(ref lower, upper, ref hours, ref milliseconds); + } + } + + private static void CalculateTimeDifference(ref DateTime lower, DateTime upper, ref int hours, ref int milliseconds) + { + while (!lower.Hour.Equals(upper.Hour)) + { + hours++; + lower = lower.AddHours(1); + } + + while (!lower.Minute.Equals(upper.Minute) || !lower.Second.Equals(upper.Second) || !lower.Millisecond.Equals(upper.Millisecond)) + { + milliseconds++; + lower = lower.AddMilliseconds(1); + } + } + /// /// Calculates the number of weeks represented by the current structure. /// @@ -156,14 +200,16 @@ private static int GetYears(DateTime upper, DateTime lower, out bool adjustYears public int GetWeeks() { var range = _upper.Subtract(_lower); - var totalDays = 0; + int totalDays; if (range.Days <= 7) { totalDays = _lower.DayOfWeek > _upper.DayOfWeek ? 2 : 1; } - if (totalDays == 0) { totalDays = range.Days - 7 + (int)_lower.DayOfWeek; } - int nextWeek = 0, weeks; - for (weeks = 1; nextWeek < totalDays; weeks++) { nextWeek += 7; } + else + { + totalDays = range.Days - 7 + (int)_lower.DayOfWeek; + } + var weeks = 1 + ((totalDays + 6) / 7); return weeks; } @@ -176,7 +222,22 @@ public int GetWeeks() /// public override int GetHashCode() { - return Generate.HashCode32(_upper, _lower, _calendar.GetType().FullName); + unchecked + { + var hash = Hash; + var prime = Prime; + + hash ^= (ulong)_upper.Ticks; + hash *= prime; + + hash ^= (ulong)_lower.Ticks; + hash *= prime; + + hash ^= _calendarId; + hash *= prime; + + return (int)(hash ^ (hash >> 32)); + } } /// @@ -199,7 +260,7 @@ public override bool Equals(object obj) /// true if the current object is equal to the other parameter; otherwise, false. public bool Equals(DateSpan other) { - if ((_upper != other._upper) || (_calendar != other._calendar)) { return false; } + if ((_upper != other._upper) || (_calendarId != other._calendarId)) { return false; } return (_lower == other._lower); } @@ -251,7 +312,7 @@ public static DateSpan Parse(string start, string end) /// /// A string that specifies the starting date and time value for the interval. /// A string that specifies the ending date and time value for the interval. - /// A to resolve a object from. + /// A to resolve a object from. /// A that corresponds to and of the interval. public static DateSpan Parse(string start, string end, CultureInfo culture) { @@ -360,4 +421,4 @@ public override string ToString() /// The total number of years represented by the current structure. public double TotalYears { get; } } -} \ No newline at end of file +} diff --git a/src/Cuemon.Core/DelimitedString.cs b/src/Cuemon.Core/DelimitedString.cs index 924f30915..c5451a8d6 100644 --- a/src/Cuemon.Core/DelimitedString.cs +++ b/src/Cuemon.Core/DelimitedString.cs @@ -11,7 +11,7 @@ namespace Cuemon /// public static class DelimitedString { - private static readonly ConcurrentDictionary CompiledSplitExpressions = new(); + private static readonly ConcurrentDictionary<(string Delimiter, string Qualifier), Regex> CompiledSplitExpressions = new(); /// /// Creates a delimited string representation from the specified . @@ -27,14 +27,20 @@ public static string Create(IEnumerable source, Action 0 ? delimitedValues.ToString(0, delimitedValues.Length - options.Delimiter.Length) : delimitedValues.ToString(); + + return delimitedValues.ToString(); } /// @@ -57,12 +63,13 @@ public static string[] Split(string value, Action setup var options = Patterns.Configure(setup); var delimiter = options.Delimiter; var qualifier = options.Qualifier; - var key = string.Concat(delimiter, "<-dq->", qualifier); - if (!CompiledSplitExpressions.TryGetValue(key, out var compiledSplit)) - { - compiledSplit = new Regex(string.Format(options.FormatProvider, "{0}(?=(?:[^{1}]*{1}[^{1}]*{1})*(?![^{1}]*{1}))", delimiter, qualifier), RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(2)); - CompiledSplitExpressions.TryAdd(key, compiledSplit); - } + + if (delimiter.Length == 1 && qualifier.Length == 1) { return SplitSingleCharCsv(value, delimiter[0], qualifier[0]); } + + var key = (delimiter, qualifier); + var compiledSplit = CompiledSplitExpressions.GetOrAdd( + key, + k => new Regex(string.Format(options.FormatProvider, "{0}(?=(?:[^{1}]*{1}[^{1}]*{1})*(?![^{1}]*{1}))", Regex.Escape(k.Delimiter), Regex.Escape(k.Qualifier)), RegexOptions.IgnoreCase | RegexOptions.Compiled, TimeSpan.FromSeconds(2))); try { @@ -73,5 +80,40 @@ public static string[] Split(string value, Action setup throw new InvalidOperationException(FormattableString.Invariant($"An error occurred while splitting '{value}' into substrings separated by '{delimiter}' and quoted with '{qualifier}'. This is typically related to data corruption, eg. a field has not been properly closed with the {nameof(options.Qualifier)} specified.")); } } + + private static string[] SplitSingleCharCsv(string value, char delimiter, char qualifier) + { + var result = new List(); + var field = new StringBuilder(value.Length); // upper bound heuristic + bool inQuotes = false; + + for (int i = 0; i < value.Length; i++) + { + var c = value[i]; + + if (c == delimiter && !inQuotes) + { + result.Add(field.ToString()); + field.Length = 0; // reuse the builder + continue; + } + + field.Append(c); + + if (c == qualifier) + { + inQuotes = !inQuotes; + } + } + + if (inQuotes) + { + throw new InvalidOperationException($"An error occurred while splitting '{value}' into substrings separated by '{delimiter}' and quoted with '{qualifier}'. This is typically related to data corruption, eg. a field has not been properly closed with the {nameof(DelimitedStringOptions.Qualifier)} specified."); + } + + result.Add(field.ToString()); + + return result.ToArray(); + } } } diff --git a/src/Cuemon.Core/Generate.cs b/src/Cuemon.Core/Generate.cs index 2f4c35df6..6100f868e 100644 --- a/src/Cuemon.Core/Generate.cs +++ b/src/Cuemon.Core/Generate.cs @@ -1,12 +1,10 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Security.Cryptography; using System.Text; using System.Threading; -using System.Threading.Tasks; using Cuemon.Collections.Generic; using Cuemon.Reflection; using Cuemon.Security; @@ -78,11 +76,15 @@ public static string ObjectPortrayal(object instance, ActionThe number of to generate. /// The function delegate that will resolve the instance of ; the parameter passed to the delegate represents the index (zero-based) of the element to return. /// An that contains a range of elements. + /// + /// is null. + /// /// /// is less than 0. /// public static IEnumerable RangeOf(int count, Func generator) { + Validator.ThrowIfNull(generator); Validator.ThrowIfLowerThan(count, 0, nameof(count)); for (var i = 0; i < count; i++) { yield return generator(i); } } @@ -157,14 +159,22 @@ public static string RandomString(int length) public static string RandomString(int length, params string[] values) { Validator.ThrowIfSequenceNullOrEmpty(values, nameof(values)); - var result = new ConcurrentBag(); - Parallel.For(0, length, _ => + if (length <= 0) { return string.Empty; } + + var random = LocalRandomizer.Value; + var buckets = values; + var bucketCount = buckets.Length; + var chars = new char[length]; + + for (var i = 0; i < length; i++) { - var index = RandomNumber(values.Length); - var indexLength = values[index].Length; - result.Add(values[index][RandomNumber(indexLength)]); - }); - return Decorator.Enclose(result).ToStringEquivalent(); + var bucketIndex = random.Next(bucketCount); + var bucket = buckets[bucketIndex]; + var charIndex = random.Next(bucket.Length); + chars[i] = bucket[charIndex]; + } + + return new string(chars); } /// diff --git a/src/Cuemon.Core/GlobalSuppressions.cs b/src/Cuemon.Core/GlobalSuppressions.cs index 567bf7a51..d187dbed3 100644 --- a/src/Cuemon.Core/GlobalSuppressions.cs +++ b/src/Cuemon.Core/GlobalSuppressions.cs @@ -36,9 +36,7 @@ [assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "By design.", Scope = "member", Target = "~F:Cuemon.Reflection.MemberReflection.Everything")] [assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "By design.", Scope = "member", Target = "~M:Cuemon.Reflection.MemberReflection.#ctor(System.Action{Cuemon.Reflection.MemberReflectionOptions})")] [assembly: SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "By design.", Scope = "member", Target = "~M:Cuemon.Globalization.ResourceAttribute.GetString(System.String)~System.String")] -[assembly: SuppressMessage("Major Code Smell", "S1854:Unused assignments should be removed", Justification = "False Positive", Scope = "member", Target = "~M:Cuemon.DateSpan.#ctor(System.DateTime,System.DateTime,System.Globalization.Calendar)")] [assembly: SuppressMessage("Style", "IDE0220:Add explicit cast", Justification = "False-Positive", Scope = "member", Target = "~M:Cuemon.StringReplaceEngine.RenderReplacement~System.String")] -[assembly: SuppressMessage("Critical Code Smell", "S3776:Cognitive Complexity of methods should not be too high", Justification = "Prioritize functionality and correctness over complexity metrics.", Scope = "member", Target = "~M:Cuemon.DateSpan.#ctor(System.DateTime,System.DateTime,System.Globalization.Calendar)")] [assembly: SuppressMessage("Major Code Smell", "S2589:Boolean expressions should not be gratuitous", Justification = "False Positive", Scope = "member", Target = "~M:Cuemon.Security.CyclicRedundancyCheck.PolynomialTableInitializerCore(System.UInt64)~System.Collections.Generic.List{System.UInt64}")] [assembly: SuppressMessage("Minor Code Smell", "S3236:Caller information arguments should not be provided explicitly", Justification = "By design - and unit tested to that no information is lost.", Scope = "member", Target = "~M:Cuemon.Decorator`1.#ctor(`0,System.Boolean,System.String)")] [assembly: SuppressMessage("Critical Code Smell", "S3776:Cognitive Complexity of methods should not be too high", Justification = "Acceptable.", Scope = "member", Target = "~M:Cuemon.Text.ParserFactory.FromValueType~Cuemon.Text.IConfigurableParser{System.Object,Cuemon.FormattingOptions}")] diff --git a/src/Cuemon.Core/Patterns.cs b/src/Cuemon.Core/Patterns.cs index 4fadecddc..bb32c344a 100644 --- a/src/Cuemon.Core/Patterns.cs +++ b/src/Cuemon.Core/Patterns.cs @@ -102,7 +102,7 @@ public static TResult InvokeOrDefault(Func method, TResult fal /// A default constructed instance of initialized with . public static T CreateInstance(Action factory) where T : class, new() { - var options = Activator.CreateInstance(); + var options = new T(); factory?.Invoke(options); return options; } @@ -118,7 +118,7 @@ public static TResult InvokeOrDefault(Func method, TResult fal /// Often referred to as part the Options pattern: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options public static TOptions Configure(Action setup, Action initializer = null, Action validator = null) where TOptions : class, IParameterObject, new() { - var options = Activator.CreateInstance(); + var options = new TOptions(); initializer?.Invoke(options); setup?.Invoke(options); validator?.Invoke(options); diff --git a/src/Cuemon.Core/Security/CyclicRedundancyCheck.cs b/src/Cuemon.Core/Security/CyclicRedundancyCheck.cs index 634753bff..2b226db32 100644 --- a/src/Cuemon.Core/Security/CyclicRedundancyCheck.cs +++ b/src/Cuemon.Core/Security/CyclicRedundancyCheck.cs @@ -9,7 +9,7 @@ namespace Cuemon.Security /// Help and inspiration was gathered @ http://www.ross.net/crc/download/crc_v3.txt public abstract class CyclicRedundancyCheck : Hash { - private readonly Lazy> _lazyLookup; + private readonly Lazy _lookupTable; /// /// Initializes a new instance of the class. @@ -20,34 +20,31 @@ public abstract class CyclicRedundancyCheck : Hash /// The which need to be configured. protected CyclicRedundancyCheck(ulong polynomial, ulong initialValue, ulong finalXor, Action setup) : base(setup) { - _lazyLookup = new Lazy>(() => PolynomialTableInitializerCore(polynomial)); + _lookupTable = new Lazy(() => PolynomialTableInitializerCore(polynomial)); InitialValue = initialValue; FinalXor = finalXor; } - private List PolynomialTableInitializerCore(ulong polynomial) + private ulong[] PolynomialTableInitializerCore(ulong polynomial) { var table = new ulong[256]; - var index = byte.MinValue; - while (true) + for (var i = 0; i < 256; i++) { - var checksum = PolynomialIndexInitializer(index); + var checksum = PolynomialIndexInitializer((byte)i); for (byte b = 0; b < 8; b++) { PolynomialSlotCalculator(ref checksum, polynomial); } - table[index] = checksum; - if (index == byte.MaxValue) { break; } - index++; + table[i] = checksum; } - return new List(table); + return table; } /// /// Gets the lookup table containing the pre-computed polynomial values. /// /// The lookup table containing the pre-computed polynomial values. - protected ulong[] LookupTable => _lazyLookup.Value.ToArray(); + protected ulong[] LookupTable => _lookupTable.Value; /// /// Returns the initial value for the specified of the polynomial . diff --git a/src/Cuemon.Core/Security/CyclicRedundancyCheck32.cs b/src/Cuemon.Core/Security/CyclicRedundancyCheck32.cs index f0e31694d..213a70c1d 100644 --- a/src/Cuemon.Core/Security/CyclicRedundancyCheck32.cs +++ b/src/Cuemon.Core/Security/CyclicRedundancyCheck32.cs @@ -56,8 +56,10 @@ protected override ulong PolynomialIndexInitializer(byte index) /// Inspiration and praises goes to http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html public override HashResult ComputeHash(byte[] input) { + Validator.ThrowIfNull(input); + var crc = (uint)InitialValue; - for (var i = 0; i < input.LongLength; i++) + for (var i = 0; i < input.Length; i++) { var cb = Options.ReflectInput ? Convertible.ReverseBits8(input[i]) : input[i]; crc ^= (uint)(cb << 24); diff --git a/src/Cuemon.Core/Security/CyclicRedundancyCheck64.cs b/src/Cuemon.Core/Security/CyclicRedundancyCheck64.cs index c71b4e327..945e43f89 100644 --- a/src/Cuemon.Core/Security/CyclicRedundancyCheck64.cs +++ b/src/Cuemon.Core/Security/CyclicRedundancyCheck64.cs @@ -56,8 +56,10 @@ protected override void PolynomialSlotCalculator(ref ulong checksum, ulong polyn /// Inspiration and praises goes to http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html public override HashResult ComputeHash(byte[] input) { + Validator.ThrowIfNull(input); + var crc = InitialValue; - for (var i = 0; i < input.LongLength; i++) + for (var i = 0; i < input.Length; i++) { var cb = Options.ReflectInput ? Convertible.ReverseBits8(input[i]) : input[i]; crc ^= ((ulong)cb << 56); diff --git a/src/Cuemon.Core/Security/FowlerNollVoHash.cs b/src/Cuemon.Core/Security/FowlerNollVoHash.cs index ab00a38cf..5d392c699 100644 --- a/src/Cuemon.Core/Security/FowlerNollVoHash.cs +++ b/src/Cuemon.Core/Security/FowlerNollVoHash.cs @@ -8,6 +8,10 @@ namespace Cuemon.Security /// public abstract class FowlerNollVoHash : Hash { + private readonly uint[] _primeWords; + private readonly uint[] _offsetBasisWords; + private readonly int _unitCount; + /// /// Initializes a new instance of the class. /// @@ -17,9 +21,28 @@ public abstract class FowlerNollVoHash : Hash /// The which need to be configured. protected FowlerNollVoHash(short bits, BigInteger prime, BigInteger offsetBasis, Action setup) : base(setup) { + switch (bits) + { + case 32: + case 64: + case 128: + case 256: + case 512: + case 1024: + break; + default: + throw new ArgumentOutOfRangeException(nameof(bits), bits, $"Unsupported Fowler–Noll–Vo hash size: {bits}. Supported sizes are: 32, 64, 128, 256, 512, 1024 bits."); + } Bits = bits; Prime = prime; OffsetBasis = offsetBasis; + + if (bits > 64) + { + _unitCount = bits / 32; + _primeWords = ToUInt32LittleEndian(prime, _unitCount); + _offsetBasisWords = ToUInt32LittleEndian(offsetBasis, _unitCount); + } } /// @@ -47,28 +70,181 @@ protected FowlerNollVoHash(short bits, BigInteger prime, BigInteger offsetBasis, /// A containing the computed hash code of the specified . public override HashResult ComputeHash(byte[] input) { - var hash = OffsetBasis; - switch (Options.Algorithm) + Validator.ThrowIfNull(input); + if (Bits == 32) { return ComputeHash32(input, (uint)Prime, (uint)OffsetBasis, Options); } + if (Bits == 64) { return ComputeHash64(input, (ulong)Prime, (ulong)OffsetBasis, Options); } + if (Bits > 64 && (Bits % 32) == 0) { return ComputeHashMultiWordInstance(input, _unitCount, _primeWords, _offsetBasisWords, Options); } + throw new InvalidOperationException($"Unsupported Fowler–Noll–Vo hash size: {Bits}. Supported sizes are: 32, 64, 128, 256, 512, 1024 bits."); + } + + private static HashResult ComputeHash32(byte[] input, uint prime, uint offsetBasis, FowlerNollVoOptions options) + { + unchecked { - case FowlerNollVoAlgorithm.Fnv1a: - foreach (var b in input) + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) { - hash ^= b; - hash *= Prime; + offsetBasis ^= input[i]; + offsetBasis *= prime; } - break; - default: - foreach (var b in input) + } + else + { + for (int i = 0; i < input.Length; i++) { - hash *= Prime; - hash ^= b; + offsetBasis *= prime; + offsetBasis ^= input[i]; } - break; + } + + var result = new byte[4]; + if (options.ByteOrder == Endianness.LittleEndian) + { + result[0] = (byte)offsetBasis; + result[1] = (byte)(offsetBasis >> 8); + result[2] = (byte)(offsetBasis >> 16); + result[3] = (byte)(offsetBasis >> 24); + } + else + { + result[3] = (byte)offsetBasis; + result[2] = (byte)(offsetBasis >> 8); + result[1] = (byte)(offsetBasis >> 16); + result[0] = (byte)(offsetBasis >> 24); + } + + return new HashResult(result); + } + } + + private static HashResult ComputeHash64(byte[] input, ulong prime, ulong offsetBasis, FowlerNollVoOptions options) + { + unchecked + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) + { + offsetBasis ^= input[i]; + offsetBasis *= prime; + } + } + else + { + for (int i = 0; i < input.Length; i++) + { + offsetBasis *= prime; + offsetBasis ^= input[i]; + } + } + + var result = new byte[8]; + if (options.ByteOrder == Endianness.LittleEndian) + { + result[0] = (byte)offsetBasis; + result[1] = (byte)(offsetBasis >> 8); + result[2] = (byte)(offsetBasis >> 16); + result[3] = (byte)(offsetBasis >> 24); + result[4] = (byte)(offsetBasis >> 32); + result[5] = (byte)(offsetBasis >> 40); + result[6] = (byte)(offsetBasis >> 48); + result[7] = (byte)(offsetBasis >> 56); + } + else + { + result[7] = (byte)offsetBasis; + result[6] = (byte)(offsetBasis >> 8); + result[5] = (byte)(offsetBasis >> 16); + result[4] = (byte)(offsetBasis >> 24); + result[3] = (byte)(offsetBasis >> 32); + result[2] = (byte)(offsetBasis >> 40); + result[1] = (byte)(offsetBasis >> 48); + result[0] = (byte)(offsetBasis >> 56); + } + + return new HashResult(result); } - var result = hash.ToByteArray(); - Array.Resize(ref result, Bits / Convertible.BitsPerByte); - result = Convertible.ReverseEndianness(result, o => o.ByteOrder = Options.ByteOrder); - return new HashResult(result); + } + + private static HashResult ComputeHashMultiWordInstance(byte[] input, int unitCount, uint[] primeWords, uint[] offsetBasisWords, FowlerNollVoOptions options) + { + var a = new uint[unitCount]; + var tmp = new uint[unitCount]; + Array.Copy(offsetBasisWords, a, unitCount); + var p = primeWords; + + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int idx = 0; idx < input.Length; idx++) + { + a[0] ^= input[idx]; + MultiplyMod32(a, p, tmp); + } + } + else + { + for (int idx = 0; idx < input.Length; idx++) + { + MultiplyMod32(a, p, tmp); + a[0] ^= input[idx]; + } + } + + var bytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + uint v = a[i]; + bytes[j] = (byte)(v & 0xFF); + bytes[j + 1] = (byte)(v >> 8); + bytes[j + 2] = (byte)(v >> 16); + bytes[j + 3] = (byte)(v >> 24); + } + + var resultBytes = Convertible.ReverseEndianness(bytes, o => o.ByteOrder = options.ByteOrder); + return new HashResult(resultBytes); + } + + private static uint[] ToUInt32LittleEndian(BigInteger value, int unitCount) + { + var bytes = value.ToByteArray(); + var needed = unitCount * 4; + if (bytes.Length < needed) + { + var extended = new byte[needed]; + Array.Copy(bytes, extended, bytes.Length); + bytes = extended; + } + var res = new uint[unitCount]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + res[i] = (uint)(bytes[j] | (bytes[j + 1] << 8) | (bytes[j + 2] << 16) | (bytes[j + 3] << 24)); + } + return res; + } + + private static void MultiplyMod32(uint[] a, uint[] p, uint[] tmp) + { + int L = a.Length; + for (int k = 0; k < L; k++) tmp[k] = 0u; + + for (int i = 0; i < L; i++) + { + if (a[i] == 0) continue; + ulong carry = 0UL; + for (int j = 0; j < L - i; j++) + { + int k = i + j; + ulong acc = tmp[k]; + acc += (ulong)a[i] * p[j] + carry; + tmp[k] = (uint)acc; + carry = acc >> 32; + } + } + + for (int i = 0; i < L; i++) a[i] = tmp[i]; } } } diff --git a/src/Cuemon.Core/Validator.cs b/src/Cuemon.Core/Validator.cs index eae0846e4..9928ea153 100644 --- a/src/Cuemon.Core/Validator.cs +++ b/src/Cuemon.Core/Validator.cs @@ -92,6 +92,7 @@ public static TResult CheckParameter(Func validator) ThrowIfNull(argument, paramName); try { + if (argument is IPostConfigurableParameterObject postConfigurable) { postConfigurable.PostConfigureOptions(); } if (argument is IValidatableParameterObject validatableArgument) { validatableArgument.ValidateOptions(); } } catch (Exception e) diff --git a/src/Cuemon.Security.Cryptography/AesCryptor.cs b/src/Cuemon.Security.Cryptography/AesCryptor.cs index b4b8ffd33..157b5e6a2 100644 --- a/src/Cuemon.Security.Cryptography/AesCryptor.cs +++ b/src/Cuemon.Security.Cryptography/AesCryptor.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Security.Cryptography; using System.Text; using Cuemon.Text; @@ -85,6 +84,7 @@ public byte[] Decrypt(byte[] value, Action setup = null) private byte[] CryptoTransformCore(byte[] value, AesMode mode, Action setup) { var options = Patterns.Configure(setup); + using (var aes = Aes.Create()) { aes.BlockSize = BlockSize; @@ -93,27 +93,11 @@ private byte[] CryptoTransformCore(byte[] value, AesMode mode, Action new MemoryStream(), (ms, rijndael, bytes) => - { - CryptoStream cryptoStream; - switch (mode) - { - case AesMode.Decrypt: - ms.Write(bytes, 0, bytes.Length); - ms.Position = 0; - cryptoStream = new CryptoStream(ms, rijndael.CreateDecryptor(), CryptoStreamMode.Read); - var cryptoBytes = new byte[bytes.Length]; - cryptoStream.Read(cryptoBytes, 0, cryptoBytes.Length); - return new MemoryStream(Eradicate.TrailingZeros(cryptoBytes)); - default: - cryptoStream = new CryptoStream(ms, rijndael.CreateEncryptor(), CryptoStreamMode.Write); - cryptoStream.Write(bytes, 0, bytes.Length); - cryptoStream.FlushFinalBlock(); - return ms; - } - }, aes, value)) + using (var transform = mode == AesMode.Encrypt + ? aes.CreateEncryptor() + : aes.CreateDecryptor()) { - return sms.ToArray(); + return transform.TransformFinalBlock(value, 0, value.Length); } } } diff --git a/src/Cuemon.Security.Cryptography/KeyedCryptoHash.cs b/src/Cuemon.Security.Cryptography/KeyedCryptoHash.cs index ca1cd7540..83bf86862 100644 --- a/src/Cuemon.Security.Cryptography/KeyedCryptoHash.cs +++ b/src/Cuemon.Security.Cryptography/KeyedCryptoHash.cs @@ -28,10 +28,9 @@ protected KeyedCryptoHash(byte[] secret, Action setup) : bas /// A containing the computed hash code of the specified . public override HashResult ComputeHash(byte[] input) { - using (var h = Initializer()) - { - return new HashResult(h.ComputeHash(input)); - } + Validator.ThrowIfNull(input); + using var h = Initializer(); + return new HashResult(h.ComputeHash(input)); } } -} \ No newline at end of file +} diff --git a/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512.cs b/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512.cs index a1f15a501..8aa5c550a 100644 --- a/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512.cs +++ b/src/Cuemon.Security.Cryptography/SecureHashAlgorithm512.cs @@ -20,8 +20,8 @@ public sealed class SecureHashAlgorithm512 : UnkeyedCryptoHash /// Initializes a new instance of the class. /// /// The which may be configured. - public SecureHashAlgorithm512(Action setup) : base(SHA512.Create, setup) + public SecureHashAlgorithm512(Action setup = null) : base(SHA512.Create, setup) { } } -} \ No newline at end of file +} diff --git a/src/Cuemon.Security.Cryptography/UnkeyedCryptoHash.cs b/src/Cuemon.Security.Cryptography/UnkeyedCryptoHash.cs index 9b2d97bce..e61de143c 100644 --- a/src/Cuemon.Security.Cryptography/UnkeyedCryptoHash.cs +++ b/src/Cuemon.Security.Cryptography/UnkeyedCryptoHash.cs @@ -38,10 +38,9 @@ protected UnkeyedCryptoHash(Func initializer, ActionA containing the computed hash code of the specified . public override HashResult ComputeHash(byte[] input) { - using (var h = Initializer()) - { - return new HashResult(h.ComputeHash(input)); - } + Validator.ThrowIfNull(input); + using var h = Initializer(); + return new HashResult(h.ComputeHash(input)); } } -} \ No newline at end of file +} diff --git a/test/Cuemon.Core.Tests/Assets/FailPostConfigurableOptions.cs b/test/Cuemon.Core.Tests/Assets/FailPostConfigurableOptions.cs new file mode 100644 index 000000000..3c3f2624a --- /dev/null +++ b/test/Cuemon.Core.Tests/Assets/FailPostConfigurableOptions.cs @@ -0,0 +1,17 @@ +using System; + +namespace Cuemon.Assets +{ + public class FailPostConfigurableOptions : PostConfigurableOptions + { + public FailPostConfigurableOptions() + { + } + + public Guid Id { get; set; } + + public override void PostConfigureOptions() + { + } + } +} diff --git a/test/Cuemon.Core.Tests/Assets/PostConfigurableOptions.cs b/test/Cuemon.Core.Tests/Assets/PostConfigurableOptions.cs new file mode 100644 index 000000000..021b87758 --- /dev/null +++ b/test/Cuemon.Core.Tests/Assets/PostConfigurableOptions.cs @@ -0,0 +1,24 @@ +using System; +using Cuemon.Configuration; + +namespace Cuemon.Assets +{ + public class PostConfigurableOptions : IPostConfigurableParameterObject, IValidatableParameterObject + { + public PostConfigurableOptions() + { + } + + public Guid Id { get; set; } + + public virtual void PostConfigureOptions() + { + Id = Guid.NewGuid(); + } + + public void ValidateOptions() + { + Validator.ThrowIfInvalidState(Id == Guid.Empty); + } + } +} diff --git a/test/Cuemon.Core.Tests/DateSpanTest.cs b/test/Cuemon.Core.Tests/DateSpanTest.cs index f86fa0017..58ae29b7e 100644 --- a/test/Cuemon.Core.Tests/DateSpanTest.cs +++ b/test/Cuemon.Core.Tests/DateSpanTest.cs @@ -1,6 +1,6 @@ -using System; +using Codebelt.Extensions.Xunit; +using System; using System.Globalization; -using Codebelt.Extensions.Xunit; using Xunit; namespace Cuemon @@ -11,6 +11,60 @@ public DateSpanTest(ITestOutputHelper output) : base(output) { } + [Fact] + public void GetHashCode_IsStableForSameInstance() + { + var span = new DateSpan(); + + var h1 = span.GetHashCode(); + var h2 = span.GetHashCode(); + var h3 = span.GetHashCode(); + + Assert.Equal(h1, h2); + Assert.Equal(h1, h3); + } + + [Fact] + public void GetHashCode_EqualInstances_HaveSameHashCode() + { + var a = new DateSpan(); + var b = new DateSpan(); + + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_ChangingUpperBoundary_UsuallyChangesHashCode() + { + var a = new DateSpan(); + var b = new DateSpan(DateTime.UtcNow, DateTime.UtcNow.Add(TimeSpan.FromDays(1)), new ChineseLunisolarCalendar()); + + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_TwoSameTypeCalendars_MustBeEqual() + { + var utcNow = DateTime.UtcNow; + + var a = new DateSpan(utcNow, utcNow.Add(TimeSpan.FromDays(1)), new GregorianCalendar()); + var b = new DateSpan(utcNow, utcNow.Add(TimeSpan.FromDays(1)), new GregorianCalendar()); + + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void GetHashCode_TwoDifferentTypeCalendars_MustBeNotEqual() + { + var utcNow = DateTime.UtcNow; + + var a = new DateSpan(utcNow, utcNow.Add(TimeSpan.FromDays(1)), new JulianCalendar()); + var b = new DateSpan(utcNow, utcNow.Add(TimeSpan.FromDays(1)), new GregorianCalendar()); + + Assert.NotEqual(a.GetHashCode(), b.GetHashCode()); + } + [Fact] public void Parse_ShouldGetPastDecadeDifference_UsingIso8601String() { @@ -37,7 +91,6 @@ public void Parse_ShouldGetPastDecadeDifference_UsingIso8601String() Assert.Equal(320699209256, span.TotalMilliseconds); Assert.Equal(531, span.GetWeeks()); - Assert.Equal(174922196, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -68,7 +121,6 @@ public void Parse_ShouldGetOneMonthOfDifference_UsingIso8601String() Assert.Equal(2678400000, span.TotalMilliseconds); Assert.Equal(6, span.GetWeeks()); - Assert.Equal(-1566296493, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -99,7 +151,6 @@ public void Parse_ShouldGetThreeMonthOfDifference_UsingIso8601String() Assert.Equal(7948800000, span.TotalMilliseconds); Assert.Equal(14, span.GetWeeks()); - Assert.Equal(-1442996082, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -130,7 +181,6 @@ public void Parse_ShouldGetSixMonthOfDifference_UsingIso8601String() Assert.Equal(15897600000, span.TotalMilliseconds); Assert.Equal(27, span.GetWeeks()); - Assert.Equal(-923802662, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -161,7 +211,6 @@ public void Parse_ShouldGetNineMonthOfDifference_UsingIso8601String() Assert.Equal(23760000000, span.TotalMilliseconds); Assert.Equal(40, span.GetWeeks()); - Assert.Equal(-2085201570, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -203,7 +252,6 @@ public void DateSpan_ShouldGetNineMonthOfDifference_UsingChineseLunisolarCalenda Assert.Equal(22982400000, span.TotalMilliseconds); Assert.Equal(40, span.GetWeeks()); - Assert.Equal(146233593, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -234,7 +282,6 @@ public void Parse_ShouldGetTwelveMonthOfDifference_UsingIso8601String() Assert.Equal(31536000000, span.TotalMilliseconds); Assert.Equal(53, span.GetWeeks()); - Assert.Equal(-252938415, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -265,7 +312,6 @@ public void Parse_ShouldGetLeapYear_UsingIso8601String() Assert.Equal(31536000000, span.TotalMilliseconds); Assert.Equal(53, span.GetWeeks()); - Assert.Equal(1501380836, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } @@ -296,7 +342,6 @@ public void Parse_ShouldGetTwelveMonthOfDifferenceWithinLeapYear_UsingIso8601Str Assert.Equal(31622400000, span.TotalMilliseconds); Assert.Equal(53, span.GetWeeks()); - Assert.Equal(1113143048, span.GetHashCode()); TestOutput.WriteLine(span.ToString()); } diff --git a/test/Cuemon.Core.Tests/GenerateTest.cs b/test/Cuemon.Core.Tests/GenerateTest.cs index f2e894d09..ca704a793 100644 --- a/test/Cuemon.Core.Tests/GenerateTest.cs +++ b/test/Cuemon.Core.Tests/GenerateTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Codebelt.Extensions.Xunit; using Xunit; @@ -11,6 +12,95 @@ public GenerateTest(ITestOutputHelper output) : base(output) { } + [Fact] + public void RangeOf_WithPositiveCount_ReturnsSequenceFromGenerator() + { + var result = Generate.RangeOf(5, i => i * 2).ToList(); + + Assert.Equal(new[] { 0, 2, 4, 6, 8 }, result); + } + + [Fact] + public void RangeOf_WithZeroCount_ReturnsEmptySequence_AndGeneratorNotCalled() + { + var calls = 0; + var result = Generate.RangeOf(0, i => + { + calls++; + return i; + }).ToList(); + + Assert.Empty(result); + Assert.Equal(0, calls); + } + + [Fact] + public void RangeOf_GeneratorReceivesCorrectIndexes_AndMaintainsOrder() + { + var seen = new List(); + var result = Generate.RangeOf(4, i => + { + seen.Add(i); + return $"Item{i}"; + }).ToList(); + + Assert.Equal(new[] { "Item0", "Item1", "Item2", "Item3" }, result); + Assert.Equal(new[] { 0, 1, 2, 3 }, seen); + } + + [Fact] + public void RangeOf_WithNegativeCount_ThrowsArgumentOutOfRangeException() + { + Assert.Throws(() => Generate.RangeOf(-1, i => i).ToList()); + } + + [Fact] + public void RandomString_WithLength_ReturnsStringOfRequestedLength_AndOnlyLettersAndDigits() + { + const int length = 64; + + var result = Generate.RandomString(length); + + Assert.NotNull(result); + Assert.Equal(length, result.Length); + Assert.All(result, c => Assert.True(char.IsLetterOrDigit(c), $"Character '{c}' is not a letter or digit.")); + } + + [Fact] + public void RandomString_WithZeroLength_ReturnsEmptyString() + { + var result = Generate.RandomString(0); + + Assert.NotNull(result); + Assert.Equal(string.Empty, result); + } + + [Fact] + public void RandomString_WithCustomValues_ProducesCharsOnlyFromProvidedValues() + { + var values = new[] { "AB", "12", "x" }; // allowed characters: A,B,1,2,x + var allowed = string.Concat(values).ToCharArray().Distinct().ToArray(); + const int length = 200; + + var result = Generate.RandomString(length, values); + + Assert.NotNull(result); + Assert.Equal(length, result.Length); + Assert.All(result, c => Assert.Contains(c, allowed)); + } + + [Fact] + public void RandomString_WithNullValues_ThrowsArgumentNullException() + { + Assert.Throws(() => Generate.RandomString(1, (string[])null)); + } + + [Fact] + public void RandomString_WithEmptyValues_ThrowsArgumentException() + { + Assert.Throws(() => Generate.RandomString(1, Array.Empty())); + } + [Fact] public void RandomString_ShouldGenerateUniqueStringsOfSpecifiedLength() { @@ -24,6 +114,7 @@ public void RandomString_ShouldGenerateUniqueStringsOfSpecifiedLength() Assert.All(strings, s => Assert.Single(strings, s)); } + [Fact] public void HashCode32_ShouldGenerateSameHashCode() { @@ -32,5 +123,170 @@ public void HashCode32_ShouldGenerateSameHashCode() Assert.Equal(-3271143, hc1); Assert.Equal(1191125869, hc2); } + + [Fact] + public void HashCode32_ParamsAndEnumerable_ProduceSameResult() + { + var inputs = new IConvertible[] { 1, 2, 3, 4, 5 }; + var fromParams = Generate.HashCode32((IConvertible[])inputs); + var fromEnumerable = Generate.HashCode32((IEnumerable)inputs); + Assert.Equal(fromParams, fromEnumerable); + } + + [Fact] + public void HashCode32_Deterministic_ForSameInput() + { + var a = Generate.HashCode32(7, 11, 13, "x"); + var b = Generate.HashCode32(7, 11, 13, "x"); + Assert.Equal(a, b); + } + + [Fact] + public void HashCode32_OrderMatters() + { + var a = Generate.HashCode32(1, 2, 3); + var b = Generate.HashCode32(3, 2, 1); + Assert.NotEqual(a, b); + } + + [Fact] + public void HashCode32_EmptySequence_ConsistentAcrossOverloads() + { + var emptyArray = Array.Empty(); + var a = Generate.HashCode32((IConvertible[])emptyArray); + var b = Generate.HashCode32((IEnumerable)emptyArray); + Assert.Equal(a, b); + Assert.Equal(a, Generate.HashCode32()); // params with zero args + } + + [Fact] + public void HashCode64_ParamsAndEnumerable_ProduceSameResult() + { + var inputs = new IConvertible[] { 42L, 24L, 100, "abc" }; + var fromParams = Generate.HashCode64((IConvertible[])inputs); + var fromEnumerable = Generate.HashCode64((IEnumerable)inputs); + Assert.Equal(fromParams, fromEnumerable); + } + + [Fact] + public void HashCode64_Deterministic_ForSameInput() + { + var a = Generate.HashCode64(123, "steady", 456.789); + var b = Generate.HashCode64(123, "steady", 456.789); + Assert.Equal(a, b); + } + + [Fact] + public void HashCode64_DifferentInputs_ProduceDifferentHashes() + { + var a = Generate.HashCode64(1, 2, 3); + var b = Generate.HashCode64(3, 2, 1); + Assert.NotEqual(a, b); + var c = Generate.HashCode64(1, 2, 4); + Assert.NotEqual(a, c); + } + + [Fact] + public void HashCode64_EmptySequence_ConsistentAcrossOverloads() + { + var emptyArray = Array.Empty(); + var a = Generate.HashCode64((IConvertible[])emptyArray); + var b = Generate.HashCode64((IEnumerable)emptyArray); + Assert.Equal(a, b); + Assert.Equal(a, Generate.HashCode64()); // params with zero args + } + + [Fact] + public void ObjectPortrayal_Null_ReturnsConfiguredNullValue() + { + var result = Generate.ObjectPortrayal(null); + Assert.Equal("", result); + } + + [Fact] + public void ObjectPortrayal_Boolean_ReturnsLowercaseBooleanString() + { + var trueResult = Generate.ObjectPortrayal(true); + var falseResult = Generate.ObjectPortrayal(false); + + Assert.Equal("true", trueResult); + Assert.Equal("false", falseResult); + } + + [Fact] + public void ObjectPortrayal_WhenToStringIsOverridden_ReturnsToStringResult() + { + var sut = new WithToStringOverride { Value = 42 }; + var result = Generate.ObjectPortrayal(sut); + + Assert.Equal("OVERRIDDEN:42", result); + } + + [Fact] + public void ObjectPortrayal_WithoutOverride_IncludesPublicProperties_AndShowsNoGetterForWriteOnly() + { + var sut = new NoOverride { Id = 7, Name = "Alice" }; + Generate.ObjectPortrayal(sut); // produce output + var result = Generate.ObjectPortrayal(sut); + + // Should start with full type name + Assert.StartsWith(typeof(NoOverride).FullName, result); + Assert.Contains(" { ", result); + Assert.Contains(" }", result); + + // Should include property representations + Assert.Contains("Id=7", result); + Assert.Contains("Name=Alice", result); + + // WriteOnly has no getter; default NoGetterValue is "" + Assert.Contains("WriteOnly=", result); + } + + [Fact] + public void ObjectPortrayal_BypassOverrideCheck_AllowsCallingFromOverriddenToString_AndReturnsPropertyListing() + { + var sut = new CallsObjectPortrayalFromToString { Value = 99 }; + // Generate.ObjectPortrayal will detect overridden ToString and call instance.ToString() + // The overridden ToString uses BypassOverrideCheck = true which should cause a property listing to be returned. + var result = Generate.ObjectPortrayal(sut); + + // Ensure we did not get the raw overridden marker but the property listing containing the Value + Assert.DoesNotContain("TOSTRING-ENTRY", result); + Assert.Contains("Value=99", result); + Assert.StartsWith(typeof(CallsObjectPortrayalFromToString).FullName, result); + } + + // Helper test types + + private class WithToStringOverride + { + public int Value { get; set; } + + public override string ToString() + { + return $"OVERRIDDEN:{Value}"; + } + } + + private class NoOverride + { + public int Id { get; set; } + public string Name { get; set; } + + // write-only property (no getter) + private int _write; + public int WriteOnly { set { _write = value; } } + } + + private class CallsObjectPortrayalFromToString + { + public int Value { get; set; } + + public override string ToString() + { + // Simulates calling Generate.ObjectPortrayal from within an overridden ToString. + return Generate.ObjectPortrayal(this, o => o.BypassOverrideCheck = true); + } + } } -} \ No newline at end of file +} diff --git a/test/Cuemon.Core.Tests/Reflection/AssemblyDecoratorExtensionsTest.cs b/test/Cuemon.Core.Tests/Reflection/AssemblyDecoratorExtensionsTest.cs index 6b67312ee..6f515cac2 100644 --- a/test/Cuemon.Core.Tests/Reflection/AssemblyDecoratorExtensionsTest.cs +++ b/test/Cuemon.Core.Tests/Reflection/AssemblyDecoratorExtensionsTest.cs @@ -42,7 +42,7 @@ public void GetTypes_ShouldReturnAllTypesFromCuemonCore() Assert.InRange(allTypesCount, 325, 375); // range because of tooling on CI adding dynamic types and high range of refactoring Assert.Equal(5, disposableTypesCount); - Assert.Equal(4, configurationTypesCount); + Assert.Equal(5, configurationTypesCount); } [Fact] diff --git a/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck32Test.cs b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck32Test.cs new file mode 100644 index 000000000..f71ec7f68 --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck32Test.cs @@ -0,0 +1,114 @@ +using System; +using System.Reflection; +using System.Text; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class CyclicRedundancyCheck32Test : Test + { + public CyclicRedundancyCheck32Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldSetDefaults() + { + var sut = new CyclicRedundancyCheck32(); + Assert.Equal((ulong)0xFFFFFFFF, sut.InitialValue); + Assert.Equal((ulong)0xFFFFFFFF, sut.FinalXor); + } + + [Fact] + public void PolynomialIndexInitializer_ShouldReturnExpected() + { + var sut = new CyclicRedundancyCheck32(); + var mi = typeof(CyclicRedundancyCheck32).GetMethod("PolynomialIndexInitializer", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(mi); + + var value0 = (ulong)mi.Invoke(sut, new object[] { (byte)0 }); + var value1 = (ulong)mi.Invoke(sut, new object[] { (byte)1 }); + var value255 = (ulong)mi.Invoke(sut, new object[] { (byte)255 }); + + Assert.Equal((ulong)((uint)0 << 24), value0); + Assert.Equal((ulong)((uint)1 << 24), value1); + Assert.Equal((ulong)((uint)255 << 24), value255); + } + + [Fact] + public void PolynomialSlotCalculator_ShouldMutateChecksumAsExpected() + { + var sut = new CyclicRedundancyCheck32(); + var mi = typeof(CyclicRedundancyCheck32).GetMethod("PolynomialSlotCalculator", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(mi); + + var polynomial = (ulong)0x4C11DB7; + + // case where highest bit is set + object[] argsSet = { (ulong)0x80000000, polynomial }; + mi.Invoke(sut, argsSet); + var resultSet = (ulong)argsSet[0]; + var expectedSet = (((ulong)0x80000000 << 1) ^ polynomial); + Assert.Equal(expectedSet, resultSet); + + // case where highest bit is not set + object[] argsNotSet = { (ulong)0x7FFFFFFF, polynomial }; + mi.Invoke(sut, argsNotSet); + var resultNotSet = (ulong)argsNotSet[0]; + var expectedNotSet = ((ulong)0x7FFFFFFF << 1); + Assert.Equal(expectedNotSet, resultNotSet); + } + + [Fact] + public void LookupTable_ShouldContainComputedValues() + { + var sut = new CyclicRedundancyCheck32(); + var prop = typeof(CyclicRedundancyCheck).GetProperty("LookupTable", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(prop); + + var table = (ulong[])prop.GetValue(sut); + Assert.Equal(256, table.Length); + + // compute expected table using same algorithm + var expected = new ulong[256]; + var polynomial = (ulong)0x4C11DB7; + for (int i = 0; i < 256; i++) + { + var checksum = (ulong)((uint)((byte)i << 24)); + for (int b = 0; b < 8; b++) + { + if ((checksum & 0x80000000) != 0) + { + checksum <<= 1; + checksum ^= polynomial; + } + else + { + checksum <<= 1; + } + } + expected[i] = checksum; + } + + Assert.True(expected.SequenceEqual(table)); + } + + [Fact] + public void ComputeHash_ShouldProduceKnownCrc32_ForStandardInput() + { + // Standard CRC-32 (IEEE 802.3) for ASCII "123456789" is 0xCBF43926 + var sut = new CyclicRedundancyCheck32(setup: o => + { + o.ReflectInput = true; + o.ReflectOutput = true; + }); + + var input = Encoding.ASCII.GetBytes("123456789"); + var result = sut.ComputeHash(input); + var hex = result.ToHexadecimalString().ToLowerInvariant(); + + Assert.Equal("cbf43926", hex); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck64Test.cs b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck64Test.cs new file mode 100644 index 000000000..836bf2e8a --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheck64Test.cs @@ -0,0 +1,123 @@ +using System; +using System.Reflection; +using System.Text; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class CyclicRedundancyCheck64Test : Test + { + public CyclicRedundancyCheck64Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldSetDefaults() + { + var sut = new CyclicRedundancyCheck64(); + Assert.Equal((ulong)0x0000000000000000, sut.InitialValue); + Assert.Equal((ulong)0x0000000000000000, sut.FinalXor); + } + + [Fact] + public void PolynomialIndexInitializer_ShouldReturnExpected() + { + var sut = new CyclicRedundancyCheck64(); + var mi = typeof(CyclicRedundancyCheck64).GetMethod("PolynomialIndexInitializer", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(mi); + + var value0 = (ulong)mi.Invoke(sut, new object[] { (byte)0 }); + var value1 = (ulong)mi.Invoke(sut, new object[] { (byte)1 }); + var value255 = (ulong)mi.Invoke(sut, new object[] { (byte)255 }); + + Assert.Equal((ulong)((ulong)0 << 56), value0); + Assert.Equal((ulong)((ulong)1 << 56), value1); + Assert.Equal((ulong)((ulong)255 << 56), value255); + } + + [Fact] + public void PolynomialSlotCalculator_ShouldMutateChecksumAsExpected() + { + var sut = new CyclicRedundancyCheck64(); + var mi = typeof(CyclicRedundancyCheck64).GetMethod("PolynomialSlotCalculator", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.NotNull(mi); + + var polynomial = (ulong)0x42F0E1EBA9EA3693; + + // case where highest bit is set + object[] argsSet = { (ulong)0x8000000000000000, polynomial }; + mi.Invoke(sut, argsSet); + var resultSet = (ulong)argsSet[0]; + var expectedSet = (((ulong)0x8000000000000000 << 1) ^ polynomial); + Assert.Equal(expectedSet, resultSet); + + // case where highest bit is not set + object[] argsNotSet = { (ulong)0x7FFFFFFFFFFFFFFF, polynomial }; + mi.Invoke(sut, argsNotSet); + var resultNotSet = (ulong)argsNotSet[0]; + var expectedNotSet = ((ulong)0x7FFFFFFFFFFFFFFF << 1); + Assert.Equal(expectedNotSet, resultNotSet); + } + + [Fact] + public void LookupTable_ShouldContainComputedValues() + { + var sut = new CyclicRedundancyCheck64(); + var prop = typeof(CyclicRedundancyCheck).GetProperty("LookupTable", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(prop); + + var table = (ulong[])prop.GetValue(sut); + Assert.Equal(256, table.Length); + + // compute expected table using same algorithm + var expected = new ulong[256]; + var polynomial = (ulong)0x42F0E1EBA9EA3693; + for (int i = 0; i < 256; i++) + { + var checksum = (ulong)((ulong)((byte)i) << 56); + for (int b = 0; b < 8; b++) + { + if ((checksum & 0x8000000000000000) != 0) + { + checksum <<= 1; + checksum ^= polynomial; + } + else + { + checksum <<= 1; + } + } + expected[i] = checksum; + } + + Assert.True(expected.SequenceEqual(table)); + } + + [Fact] + public void LookupTable_MultipleAccessesReturnEqualContent() + { + var sut = new CyclicRedundancyCheck64(); + var prop = typeof(CyclicRedundancyCheck).GetProperty("LookupTable", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Assert.NotNull(prop); + + var t1 = (ulong[])prop.GetValue(sut); + var t2 = (ulong[])prop.GetValue(sut); + + Assert.Equal(t1, t2); + } + + [Fact] + public void ComputeHash_ShouldProduceKnownCrc64Ecma_ForStandardInput() + { + // CRC-64-ECMA-182 for ASCII "123456789" is 0x6C40DF5F0B497347 + var sut = new CyclicRedundancyCheck64(); + + var input = Encoding.ASCII.GetBytes("123456789"); + var result = sut.ComputeHash(input); + var hex = result.ToHexadecimalString().ToLowerInvariant(); + + Assert.Equal("6c40df5f0b497347", hex); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheckTest.cs b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheckTest.cs new file mode 100644 index 000000000..736799823 --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/CyclicRedundancyCheckTest.cs @@ -0,0 +1,83 @@ +using System; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class CyclicRedundancyCheckTest : Test + { + public CyclicRedundancyCheckTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_ShouldSetInitialAndFinal() + { + var sut = new TestCrc(1, 123, 456); + Assert.Equal((ulong)123, sut.InitialValue); + Assert.Equal((ulong)456, sut.FinalXor); + } + + [Fact] + public void LookupTable_ShouldContainComputedValues() + { + var sut = new TestCrc(1, 0, 0); + var table = sut.PublicLookupTable; + + Assert.Equal(256, table.Length); + Assert.Equal((ulong)8, table[0]); // 0 + 1*8 + Assert.Equal((ulong)18, table[10]); // 10 + 1*8 + Assert.Equal((ulong)263, table[255]); // 255 + 1*8 + + TestOutput.WriteLine($"First: {table[0]}, Tenth: {table[10]}, Last: {table[255]}"); + } + + [Fact] + public void LookupTable_ShouldRespectPolynomial() + { + var sut = new TestCrc(2, 0, 0); + var table = sut.PublicLookupTable; + + Assert.Equal((ulong)16 + 1, table[1]); // 1 + 2*8 => 17 + Assert.Equal((ulong)(255 + 2 * 8), table[255]); // 255 + 16 => 271 + + TestOutput.WriteLine($"Index1: {table[1]}, Index255: {table[255]}"); + } + + [Fact] + public void LookupTable_MultipleAccessesReturnEqualContent() + { + var sut = new TestCrc(1, 0, 0); + var t1 = sut.PublicLookupTable; + var t2 = sut.PublicLookupTable; + + Assert.Equal(t1, t2); + } + + private sealed class TestCrc : CyclicRedundancyCheck + { + public TestCrc(ulong polynomial, ulong initialValue, ulong finalXor) : base(polynomial, initialValue, finalXor, null) + { + } + + public ulong[] PublicLookupTable => LookupTable; + + protected override ulong PolynomialIndexInitializer(byte index) + { + return index; + } + + protected override void PolynomialSlotCalculator(ref ulong checksum, ulong polynomial) + { + // simple deterministic operation: add polynomial + checksum += polynomial; + } + + public override HashResult ComputeHash(byte[] input) + { + // trivial implementation for testing purposes + return new HashResult(Array.Empty()); + } + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo1024Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo1024Test.cs new file mode 100644 index 000000000..df0dd17cd --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo1024Test.cs @@ -0,0 +1,79 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo1024Test : Test + { + public FowlerNollVo1024Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo1024(); + Assert.Equal(1024, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_LengthIs128Bytes() + { + var sut = new FowlerNollVo1024(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + Assert.Equal(128, result.Length); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentBigIntegerImplementation() + { + var data = "abc"u8.ToArray(); + var sut = new FowlerNollVo1024(); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected = IndependentFNVMultiWord(data, sut.Prime, sut.OffsetBasis, sut.Bits, sut.Options); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + private static byte[] IndependentFNVMultiWord(byte[] input, BigInteger prime, BigInteger offsetBasis, short bits, FowlerNollVoOptions options) + { + BigInteger mask = (BigInteger.One << bits) - 1; + BigInteger a = offsetBasis & mask; + for (int idx = 0; idx < input.Length; idx++) + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + a ^= input[idx]; + a = (a * prime) & mask; + } + else + { + a = (a * prime) & mask; + a ^= input[idx]; + } + } + + int unitCount = bits / 32; + int needed = unitCount * 4; + var bytes = a.ToByteArray(); + if (bytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(bytes, ext, bytes.Length); + bytes = ext; + } + + var wordsBytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + wordsBytes[j] = bytes[j]; + wordsBytes[j + 1] = bytes[j + 1]; + wordsBytes[j + 2] = bytes[j + 2]; + wordsBytes[j + 3] = bytes[j + 3]; + } + + return Convertible.ReverseEndianness(wordsBytes, o => o.ByteOrder = options.ByteOrder); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo128Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo128Test.cs new file mode 100644 index 000000000..7ae67c664 --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo128Test.cs @@ -0,0 +1,83 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo128Test : Test + { + public FowlerNollVo128Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo128(); + Assert.Equal(128, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_WithSelectedEndianness() + { + var sut = new FowlerNollVo128(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + Assert.Equal(16, result.Length); + + var sutLe = new FowlerNollVo128(o => o.ByteOrder = Endianness.LittleEndian); + var resultLe = sutLe.ComputeHash(Array.Empty()).GetBytes(); + Assert.Equal(16, resultLe.Length); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentBigIntegerImplementation() + { + var data = "abc"u8.ToArray(); + var sut = new FowlerNollVo128(); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected = IndependentFNVMultiWord(data, sut.Prime, sut.OffsetBasis, sut.Bits, sut.Options); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + private static byte[] IndependentFNVMultiWord(byte[] input, BigInteger prime, BigInteger offsetBasis, short bits, FowlerNollVoOptions options) + { + BigInteger mask = (BigInteger.One << bits) - 1; + BigInteger a = offsetBasis & mask; + for (int idx = 0; idx < input.Length; idx++) + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + a ^= input[idx]; + a = (a * prime) & mask; + } + else + { + a = (a * prime) & mask; + a ^= input[idx]; + } + } + + int unitCount = bits / 32; + int needed = unitCount * 4; + var bytes = a.ToByteArray(); + if (bytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(bytes, ext, bytes.Length); + bytes = ext; + } + + var wordsBytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + wordsBytes[j] = bytes[j]; + wordsBytes[j + 1] = bytes[j + 1]; + wordsBytes[j + 2] = bytes[j + 2]; + wordsBytes[j + 3] = bytes[j + 3]; + } + + return Convertible.ReverseEndianness(wordsBytes, o => o.ByteOrder = options.ByteOrder); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo256Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo256Test.cs new file mode 100644 index 000000000..fdfb50b4c --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo256Test.cs @@ -0,0 +1,79 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo256Test : Test + { + public FowlerNollVo256Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo256(); + Assert.Equal(256, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_LengthIs32Bytes() + { + var sut = new FowlerNollVo256(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + Assert.Equal(32, result.Length); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentBigIntegerImplementation() + { + var data = "abc"u8.ToArray(); + var sut = new FowlerNollVo256(); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected = IndependentFNVMultiWord(data, sut.Prime, sut.OffsetBasis, sut.Bits, sut.Options); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + private static byte[] IndependentFNVMultiWord(byte[] input, BigInteger prime, BigInteger offsetBasis, short bits, FowlerNollVoOptions options) + { + BigInteger mask = (BigInteger.One << bits) - 1; + BigInteger a = offsetBasis & mask; + for (int idx = 0; idx < input.Length; idx++) + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + a ^= input[idx]; + a = (a * prime) & mask; + } + else + { + a = (a * prime) & mask; + a ^= input[idx]; + } + } + + int unitCount = bits / 32; + int needed = unitCount * 4; + var bytes = a.ToByteArray(); + if (bytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(bytes, ext, bytes.Length); + bytes = ext; + } + + var wordsBytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + wordsBytes[j] = bytes[j]; + wordsBytes[j + 1] = bytes[j + 1]; + wordsBytes[j + 2] = bytes[j + 2]; + wordsBytes[j + 3] = bytes[j + 3]; + } + + return Convertible.ReverseEndianness(wordsBytes, o => o.ByteOrder = options.ByteOrder); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo32Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo32Test.cs new file mode 100644 index 000000000..43c6a1bbb --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo32Test.cs @@ -0,0 +1,95 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo32Test : Test + { + public FowlerNollVo32Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo32(); + Assert.Equal(32, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_BigEndianByDefault() + { + var sut = new FowlerNollVo32(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + var expected = new byte[] { 0x81, 0x1C, 0x9D, 0xC5 }; // 0x811C9DC5 + Assert.Equal(expected, result); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_LittleEndianWhenConfigured() + { + var sut = new FowlerNollVo32(o => o.ByteOrder = Endianness.LittleEndian); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + var expected = new byte[] { 0xC5, 0x9D, 0x1C, 0x81 }; + Assert.Equal(expected, result); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentImplementation_Fnv1aAndFnv1() + { + var data = "hello"u8.ToArray(); + var sut1a = new FowlerNollVo32(); + sut1a.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected1a = IndependentFNV32(data, (uint)new BigInteger(16777619), (uint)new BigInteger(2166136261), FowlerNollVoAlgorithm.Fnv1a, sut1a.Options.ByteOrder); + var actual1a = sut1a.ComputeHash(data).GetBytes(); + Assert.Equal(expected1a, actual1a); + + var sut1 = new FowlerNollVo32(); + sut1.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1; + var expected1 = IndependentFNV32(data, (uint)new BigInteger(16777619), (uint)new BigInteger(2166136261), FowlerNollVoAlgorithm.Fnv1, sut1.Options.ByteOrder); + var actual1 = sut1.ComputeHash(data).GetBytes(); + Assert.Equal(expected1, actual1); + } + + private static byte[] IndependentFNV32(byte[] input, uint prime, uint offsetBasis, FowlerNollVoAlgorithm algorithm, Endianness byteOrder) + { + unchecked + { + uint h = offsetBasis; + if (algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) + { + h ^= input[i]; + h *= prime; + } + } + else + { + for (int i = 0; i < input.Length; i++) + { + h *= prime; + h ^= input[i]; + } + } + + var result = new byte[4]; + if (byteOrder == Endianness.LittleEndian) + { + result[0] = (byte)h; + result[1] = (byte)(h >> 8); + result[2] = (byte)(h >> 16); + result[3] = (byte)(h >> 24); + } + else + { + result[3] = (byte)h; + result[2] = (byte)(h >> 8); + result[1] = (byte)(h >> 16); + result[0] = (byte)(h >> 24); + } + return result; + } + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo512Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo512Test.cs new file mode 100644 index 000000000..0555bb86c --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo512Test.cs @@ -0,0 +1,79 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo512Test : Test + { + public FowlerNollVo512Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo512(); + Assert.Equal(512, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_LengthIs64Bytes() + { + var sut = new FowlerNollVo512(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + Assert.Equal(64, result.Length); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentBigIntegerImplementation() + { + var data = "abc"u8.ToArray(); + var sut = new FowlerNollVo512(); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected = IndependentFNVMultiWord(data, sut.Prime, sut.OffsetBasis, sut.Bits, sut.Options); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + private static byte[] IndependentFNVMultiWord(byte[] input, BigInteger prime, BigInteger offsetBasis, short bits, FowlerNollVoOptions options) + { + BigInteger mask = (BigInteger.One << bits) - 1; + BigInteger a = offsetBasis & mask; + for (int idx = 0; idx < input.Length; idx++) + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + a ^= input[idx]; + a = (a * prime) & mask; + } + else + { + a = (a * prime) & mask; + a ^= input[idx]; + } + } + + int unitCount = bits / 32; + int needed = unitCount * 4; + var bytes = a.ToByteArray(); + if (bytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(bytes, ext, bytes.Length); + bytes = ext; + } + + var wordsBytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + wordsBytes[j] = bytes[j]; + wordsBytes[j + 1] = bytes[j + 1]; + wordsBytes[j + 2] = bytes[j + 2]; + wordsBytes[j + 3] = bytes[j + 3]; + } + + return Convertible.ReverseEndianness(wordsBytes, o => o.ByteOrder = options.ByteOrder); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVo64Test.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVo64Test.cs new file mode 100644 index 000000000..4f89e4782 --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVo64Test.cs @@ -0,0 +1,89 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVo64Test : Test + { + public FowlerNollVo64Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Constructor_Default_SetsBits() + { + var sut = new FowlerNollVo64(); + Assert.Equal(64, sut.Bits); + } + + [Fact] + public void ComputeHash_Empty_ReturnsOffsetBasis_BigEndianByDefault() + { + var sut = new FowlerNollVo64(); + var result = sut.ComputeHash(Array.Empty()).GetBytes(); + // Ensure deterministic length + Assert.Equal(8, result.Length); + } + + [Fact] + public void ComputeHash_NonEmpty_EqualsIndependentImplementation() + { + var data = "hello"u8.ToArray(); + var sut = new FowlerNollVo64(); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected = IndependentFNV64(data, (ulong)new BigInteger(1099511628211), (ulong)new BigInteger(14695981039346656037), FowlerNollVoAlgorithm.Fnv1a, sut.Options.ByteOrder); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + private static byte[] IndependentFNV64(byte[] input, ulong prime, ulong offsetBasis, FowlerNollVoAlgorithm algorithm, Endianness byteOrder) + { + unchecked + { + ulong h = offsetBasis; + if (algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) + { + h ^= input[i]; + h *= prime; + } + } + else + { + for (int i = 0; i < input.Length; i++) + { + h *= prime; + h ^= input[i]; + } + } + + var result = new byte[8]; + if (byteOrder == Endianness.LittleEndian) + { + result[0] = (byte)h; + result[1] = (byte)(h >> 8); + result[2] = (byte)(h >> 16); + result[3] = (byte)(h >> 24); + result[4] = (byte)(h >> 32); + result[5] = (byte)(h >> 40); + result[6] = (byte)(h >> 48); + result[7] = (byte)(h >> 56); + } + else + { + result[7] = (byte)h; + result[6] = (byte)(h >> 8); + result[5] = (byte)(h >> 16); + result[4] = (byte)(h >> 24); + result[3] = (byte)(h >> 32); + result[2] = (byte)(h >> 40); + result[1] = (byte)(h >> 48); + result[0] = (byte)(h >> 56); + } + + return result; + } + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/FowlerNollVoHashTest.cs b/test/Cuemon.Core.Tests/Security/FowlerNollVoHashTest.cs new file mode 100644 index 000000000..afa16af2e --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/FowlerNollVoHashTest.cs @@ -0,0 +1,328 @@ +using System; +using System.Numerics; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class FowlerNollVoHashTest : Test + { + public FowlerNollVoHashTest(ITestOutputHelper output) : base(output) + { + } + + // Minimal concrete implementation so we can instantiate the abstract class. + private sealed class TestFowlerNollVoHash : FowlerNollVoHash + { + public TestFowlerNollVoHash(short bits, BigInteger prime, BigInteger offsetBasis, Action setup = null) + : base(bits, prime, offsetBasis, setup ?? (o => { })) { } + } + + [Fact] + public void Constructor_UnsupportedBits_ThrowsArgumentOutOfRangeException() + { + var prime = new BigInteger(1); + var offset = new BigInteger(0); + Assert.Throws(() => new TestFowlerNollVoHash(16, prime, offset)); + } + + [Fact] + public void Properties_AreSet_FromConstructor() + { + short bits = 32; + var prime = new BigInteger(16777619); + var offset = new BigInteger(2166136261); + var sut = new TestFowlerNollVoHash(bits, prime, offset); + + Assert.Equal(bits, sut.Bits); + Assert.Equal(prime, sut.Prime); + Assert.Equal(offset, sut.OffsetBasis); + } + + [Fact] + public void ComputeHash_Null_ThrowsArgumentNullException() + { + var sut = new TestFowlerNollVoHash(32, new BigInteger(16777619), new BigInteger(2166136261)); + Assert.Throws(() => sut.ComputeHash((byte[])null!)); + } + + [Fact] + public void ComputeHash32_EmptyInput_ReturnsOffsetBasis_BigEndianByDefault() + { + var prime = new BigInteger(16777619); // FNV-1/1a 32-bit prime + var offset = new BigInteger(2166136261); // FNV-1/1a 32-bit offset basis (0x811C9DC5) + var sut = new TestFowlerNollVoHash(32, prime, offset); // default options: BigEndian + Fnv1a + + var result = sut.ComputeHash(Array.Empty()); + var bytes = result.GetBytes(); + + // Big-endian representation of 0x811C9DC5 + var expected = new byte[] { 0x81, 0x1C, 0x9D, 0xC5 }; + Assert.Equal(expected, bytes); + } + + [Fact] + public void ComputeHash32_EmptyInput_ReturnsOffsetBasis_LittleEndianWhenConfigured() + { + var prime = new BigInteger(16777619); + var offset = new BigInteger(2166136261); + var sut = new TestFowlerNollVoHash(32, prime, offset, o => o.ByteOrder = Endianness.LittleEndian); + + var result = sut.ComputeHash(Array.Empty()); + var bytes = result.GetBytes(); + + // Little-endian representation of 0x811C9DC5 + var expected = new byte[] { 0xC5, 0x9D, 0x1C, 0x81 }; + Assert.Equal(expected, bytes); + } + + [Fact] + public void ComputeHash32_NonEmpty_EqualsIndependentImplementation_Fnv1aAndFnv1() + { + var prime = 16777619u; + var offset = 2166136261u; + var data = "hello"u8.ToArray(); + + // FNV-1a + var sut1a = new TestFowlerNollVoHash(32, new BigInteger(prime), new BigInteger(offset)); + sut1a.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + var expected1a = IndependentFNV32(data, prime, offset, FowlerNollVoAlgorithm.Fnv1a, sut1a.Options.ByteOrder); + var actual1a = sut1a.ComputeHash(data).GetBytes(); + Assert.Equal(expected1a, actual1a); + + // FNV-1 + var sut1 = new TestFowlerNollVoHash(32, new BigInteger(prime), new BigInteger(offset)); + sut1.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1; + var expected1 = IndependentFNV32(data, prime, offset, FowlerNollVoAlgorithm.Fnv1, sut1.Options.ByteOrder); + var actual1 = sut1.ComputeHash(data).GetBytes(); + Assert.Equal(expected1, actual1); + } + + [Fact] + public void ComputeHash64_NonEmpty_EqualsIndependentImplementation() + { + // Standard 64-bit FNV constants + var prime = 1099511628211ul; + var offset = 14695981039346656037ul; + var data = "hello"u8.ToArray(); + + var sut = new TestFowlerNollVoHash(64, new BigInteger(prime), new BigInteger(offset)); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + + var expected = IndependentFNV64(data, prime, offset, FowlerNollVoAlgorithm.Fnv1a, sut.Options.ByteOrder); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + [Fact] + public void ComputeHashMultiWord_EmptyInput_ReturnsOffsetBasis_WithSelectedEndianness() + { + short bits = 128; + // Use simple arbitrary 128-bit prime and offset basis for test determinism + var primeBytes = new byte[16]; + var offsetBytes = new byte[16]; + for (int i = 0; i < 16; i++) { primeBytes[i] = (byte)(i + 1); offsetBytes[i] = (byte)(0xA0 + i); } + var prime = new BigInteger(primeBytes); + var offset = new BigInteger(offsetBytes); + + var sut = new TestFowlerNollVoHash(bits, prime, offset); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; // default but explicit + + var result = sut.ComputeHash(Array.Empty()); + var actualBytes = result.GetBytes(); + + // Build expected bytes using the same conversion described in the implementation: + int unitCount = bits / 32; + var needed = unitCount * 4; + var offBytes = offset.ToByteArray(); + if (offBytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(offBytes, ext, offBytes.Length); + offBytes = ext; + } + + // convert to uint[] little-endian + var a = new uint[unitCount]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + a[i] = (uint)(offBytes[j] | (offBytes[j + 1] << 8) | (offBytes[j + 2] << 16) | (offBytes[j + 3] << 24)); + } + + // bytes from the uint[] (little-endian words) + var bytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + var v = a[i]; + bytes[j] = (byte)(v & 0xFF); + bytes[j + 1] = (byte)((v >> 8) & 0xFF); + bytes[j + 2] = (byte)((v >> 16) & 0xFF); + bytes[j + 3] = (byte)((v >> 24) & 0xFF); + } + + var expected = Convertible.ReverseEndianness(bytes, o => o.ByteOrder = sut.Options.ByteOrder); + Assert.Equal(expected, actualBytes); + } + + [Fact] + public void ComputeHashMultiWord_NonEmpty_EqualsIndependentBigIntegerImplementation() + { + short bits = 128; + // deterministic prime & offset + var primeBytes = new byte[16]; + var offsetBytes = new byte[16]; + for (int i = 0; i < 16; i++) { primeBytes[i] = (byte)(i + 1); offsetBytes[i] = (byte)(0xB0 + i); } + var prime = new BigInteger(primeBytes); + var offset = new BigInteger(offsetBytes); + var data = "abc"u8.ToArray(); + + var sut = new TestFowlerNollVoHash(bits, prime, offset); + sut.Options.Algorithm = FowlerNollVoAlgorithm.Fnv1a; + + var expected = IndependentFNVMultiWord(data, prime, offset, bits, sut.Options); + var actual = sut.ComputeHash(data).GetBytes(); + Assert.Equal(expected, actual); + } + + #region Independent reference implementations used by tests + + private static byte[] IndependentFNV32(byte[] input, uint prime, uint offsetBasis, FowlerNollVoAlgorithm algorithm, Endianness byteOrder) + { + unchecked + { + uint h = offsetBasis; + if (algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) + { + h ^= input[i]; + h *= prime; + } + } + else + { + for (int i = 0; i < input.Length; i++) + { + h *= prime; + h ^= input[i]; + } + } + + var result = new byte[4]; + if (byteOrder == Endianness.LittleEndian) + { + result[0] = (byte)h; + result[1] = (byte)(h >> 8); + result[2] = (byte)(h >> 16); + result[3] = (byte)(h >> 24); + } + else + { + result[3] = (byte)h; + result[2] = (byte)(h >> 8); + result[1] = (byte)(h >> 16); + result[0] = (byte)(h >> 24); + } + return result; + } + } + + private static byte[] IndependentFNV64(byte[] input, ulong prime, ulong offsetBasis, FowlerNollVoAlgorithm algorithm, Endianness byteOrder) + { + unchecked + { + ulong h = offsetBasis; + if (algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + for (int i = 0; i < input.Length; i++) + { + h ^= input[i]; + h *= prime; + } + } + else + { + for (int i = 0; i < input.Length; i++) + { + h *= prime; + h ^= input[i]; + } + } + + var result = new byte[8]; + if (byteOrder == Endianness.LittleEndian) + { + result[0] = (byte)h; + result[1] = (byte)(h >> 8); + result[2] = (byte)(h >> 16); + result[3] = (byte)(h >> 24); + result[4] = (byte)(h >> 32); + result[5] = (byte)(h >> 40); + result[6] = (byte)(h >> 48); + result[7] = (byte)(h >> 56); + } + else + { + result[7] = (byte)h; + result[6] = (byte)(h >> 8); + result[5] = (byte)(h >> 16); + result[4] = (byte)(h >> 24); + result[3] = (byte)(h >> 32); + result[2] = (byte)(h >> 40); + result[1] = (byte)(h >> 48); + result[0] = (byte)(h >> 56); + } + + return result; + } + } + + private static byte[] IndependentFNVMultiWord(byte[] input, BigInteger prime, BigInteger offsetBasis, short bits, FowlerNollVoOptions options) + { + BigInteger mask = (BigInteger.One << bits) - 1; + BigInteger a = offsetBasis & mask; + for (int idx = 0; idx < input.Length; idx++) + { + if (options.Algorithm == FowlerNollVoAlgorithm.Fnv1a) + { + a ^= input[idx]; + a = (a * prime) & mask; + } + else + { + a = (a * prime) & mask; + a ^= input[idx]; + } + } + + int unitCount = bits / 32; + int needed = unitCount * 4; + var bytes = a.ToByteArray(); + if (bytes.Length < needed) + { + var ext = new byte[needed]; + Array.Copy(bytes, ext, bytes.Length); + bytes = ext; + } + + // Convert little-endian uint words to byte array (same as production code) + var wordsBytes = new byte[unitCount * 4]; + for (int i = 0; i < unitCount; i++) + { + int j = i * 4; + // ensure indices exist in bytes (they do due to padding) + wordsBytes[j] = bytes[j]; + wordsBytes[j + 1] = bytes[j + 1]; + wordsBytes[j + 2] = bytes[j + 2]; + wordsBytes[j + 3] = bytes[j + 3]; + } + + // Apply endianness conversion as production code does + return Convertible.ReverseEndianness(wordsBytes, o => o.ByteOrder = options.ByteOrder); + } + + #endregion + } +} diff --git a/test/Cuemon.Core.Tests/Security/HashFactoryTest.cs b/test/Cuemon.Core.Tests/Security/HashFactoryTest.cs index a506b54c2..8036ccb16 100644 --- a/test/Cuemon.Core.Tests/Security/HashFactoryTest.cs +++ b/test/Cuemon.Core.Tests/Security/HashFactoryTest.cs @@ -253,7 +253,53 @@ public void CreateFnv1024_Fnv1a_ShouldBeValidHashResult() Assert.Equal("199c0ace56c5c33d8bce6f7cf4bc4b555e0fc3ae8d37c4b7384678a34d96ae8192825ae6bcda63dbb9e3417d0980de290e79de582d88c97e17c9535950c35f4f16d311bc66d1ac2892d59f7b0697257eba9fc1e3accbc85729218306b34996eedf99292c814e8a75f41ddc5a5b5177b6e60c0211ad8d8f78395c7c2d2c483e7e", h.ComputeHash(Decorator.Enclose(Alphanumeric.LettersAndNumbers).ToStream()).ToHexadecimalString()); Assert.Equal("c801f8e08ae91b180b98dd7d9f65ceb687ca86358c6905f60a7d1014c182b04ee2ab1bd0066e9857a7f7de000000000000000000000000000000000000000000000000000000000000000000000000000000018045149ade1c79abe3b709a406f7d9205169bec59b126140bcb96f9d5d3e2ea91dfc0f40af8e7e3f25d14c3186", h.ComputeHash(Alphanumeric.Numbers).ToHexadecimalString()); Assert.Equal("000000000000000098d7c19fbce653df221b9f717d3490ff95ca87fdaef30d1b823372f85b24a372f50e380000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007685cd81a491dbccc21ad06648d09a5c8cf5a78482054e91470b33dde77252caef66597", h.ComputeHash(byte.MinValue).ToHexadecimalString()); + } + + // New tests to cover remaining factory overloads and switches + [Fact] + public void CreateFnv_Default_ShouldBeSameAsCreateFnv32() + { + var input = Alphanumeric.LettersAndNumbers; + var a = HashFactory.CreateFnv(); + var b = HashFactory.CreateFnv32(); + Assert.Equal(a.ComputeHash(input).ToHexadecimalString(), b.ComputeHash(input).ToHexadecimalString()); + } + + [Fact] + public void CreateFnv_Switch_ShouldReturnSpecificFactoryResults() + { + var input = Alphanumeric.Numbers; + + var h32 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv32); + Assert.Equal(HashFactory.CreateFnv32().ComputeHash(input).ToHexadecimalString(), h32.ComputeHash(input).ToHexadecimalString()); + + var h64 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv64); + Assert.Equal(HashFactory.CreateFnv64().ComputeHash(input).ToHexadecimalString(), h64.ComputeHash(input).ToHexadecimalString()); + + var h128 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv128); + Assert.Equal(HashFactory.CreateFnv128().ComputeHash(input).ToHexadecimalString(), h128.ComputeHash(input).ToHexadecimalString()); + var h256 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv256); + Assert.Equal(HashFactory.CreateFnv256().ComputeHash(input).ToHexadecimalString(), h256.ComputeHash(input).ToHexadecimalString()); + + var h512 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv512); + Assert.Equal(HashFactory.CreateFnv512().ComputeHash(input).ToHexadecimalString(), h512.ComputeHash(input).ToHexadecimalString()); + + var h1024 = HashFactory.CreateFnv(NonCryptoAlgorithm.Fnv1024); + Assert.Equal(HashFactory.CreateFnv1024().ComputeHash(input).ToHexadecimalString(), h1024.ComputeHash(input).ToHexadecimalString()); + } + + [Fact] + public void CreateCrc64_WithParameters_ShouldMatchCreateCrc64GoIso() + { + var input = Alphanumeric.LettersAndNumbers; + var hParam = HashFactory.CreateCrc64(0x000000000000001B, 0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF, o => + { + o.ReflectInput = true; + o.ReflectOutput = true; + }); + var hPreset = HashFactory.CreateCrc64GoIso(); + Assert.Equal(hPreset.ComputeHash(input).ToHexadecimalString().ToUpper(), hParam.ComputeHash(input).ToHexadecimalString().ToUpper()); } } } \ No newline at end of file diff --git a/test/Cuemon.Core.Tests/Security/HashResultTest.cs b/test/Cuemon.Core.Tests/Security/HashResultTest.cs new file mode 100644 index 000000000..3c18a2cd8 --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/HashResultTest.cs @@ -0,0 +1,146 @@ +using System; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security +{ + public class HashResultTest : Test + { + public HashResultTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void Constructor_NullInput_HasValueFalse_And_GetBytesEmpty() + { + var hr = new HashResult(null); + Assert.False(hr.HasValue); + Assert.Empty(hr.GetBytes()); + } + + [Fact] + public void Constructor_WithBytes_HasValueTrue_And_GetBytesReturnsEqualCopy() + { + var input = new byte[] { 0x01, 0x0A, 0xFF }; + var hr = new HashResult(input); + Assert.True(hr.HasValue); + + var bytesFromHr = hr.GetBytes(); + Assert.Equal(input, bytesFromHr); + // Ensure returned array is a copy (not the same reference) + Assert.NotSame(input, bytesFromHr); + } + + [Fact] + public void GetBytes_ReturnsIndependentCopies() + { + var input = new byte[] { 11, 22, 33 }; + var hr = new HashResult(input); + + var first = hr.GetBytes(); + first[0] = 99; // mutate returned copy + + var second = hr.GetBytes(); + // Mutation of the first copy must not affect subsequent copies + Assert.NotEqual(first, second); + Assert.Equal(new byte[] { 11, 22, 33 }, second); + } + + [Fact] + public void ToHexadecimalString_ReturnsExpectedLowercaseHex() + { + var bytes = new byte[] { 0x0F, 0xA0, 0x01 }; + var hr = new HashResult(bytes); + // Expected: "0fa001" (StringFactory.CreateHexadecimal uses lowercase) + Assert.Equal("0fa001", hr.ToHexadecimalString()); + Assert.Equal(hr.ToHexadecimalString(), hr.ToString()); // ToString delegates to hex + } + + [Fact] + public void ToBase64String_ReturnsExpected() + { + var bytes = new byte[] { 0x01, 0x02, 0x03 }; + var hr = new HashResult(bytes); + var expected = Convert.ToBase64String(bytes); + Assert.Equal(expected, hr.ToBase64String()); + } + + [Fact] + public void ToUrlEncodedBase64String_ReturnsExpectedUrlSafeBase64() + { + var bytes = new byte[] { 0xFF, 0xEE, 0xDD, 0xCC }; + var hr = new HashResult(bytes); + + var base64 = Convert.ToBase64String(bytes); + var expected = base64.Split('=')[0].Replace('+', '-').Replace('/', '_'); + + Assert.Equal(expected, hr.ToUrlEncodedBase64String()); + } + + [Fact] + public void ToBinaryString_ReturnsExpectedConcatenatedBinaryDigits() + { + var bytes = new byte[] { 0x01, 0x02 }; + var hr = new HashResult(bytes); + + string ToBin(byte b) => Convert.ToString(b, 2).PadLeft(8, '0'); + var expected = ToBin(0x01) + ToBin(0x02); + + Assert.Equal(expected, hr.ToBinaryString()); + } + + [Fact] + public void GetHashCode_EqualsUnderlyingArrayHashCode() + { + var arr = new byte[] { 7, 8, 9 }; + var hr = new HashResult(arr); + Assert.Equal(arr.GetHashCode(), hr.GetHashCode()); + } + + [Fact] + public void Equals_Object_NotHashResult_ReturnsFalse() + { + var hr = new HashResult(new byte[] { 1 }); + Assert.False(hr.Equals(new byte[] { 2 })); + } + + [Fact] + public void Equals_HashResult_Null_ReturnsFalse() + { + var hr = new HashResult(new byte[] { 1, 2 }); + Assert.False(hr.Equals((HashResult)null)); + } + + [Fact] + public void Equals_SameUnderlyingArrayReference_ReturnsTrue() + { + var arr = new byte[] { 5, 6, 7 }; + var a = new HashResult(arr); + var b = new HashResult(arr); + Assert.True(a.Equals(b)); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Equals_DifferentArrayWithSameContent_ReturnsFalse() + { + var a = new HashResult(new byte[] { 2, 3, 4 }); + var b = new HashResult(new byte[] { 2, 3, 4 }); // different instance + // HashResult.Equals compares GetHashCode which is reference-based on the array, + // so different array instances with equal content will not be considered equal. + Assert.False(a.Equals(b)); + } + + [Fact] + public void ToT_ConverterReceivesUnderlyingBytesAndReturnsConvertedValue() + { + var bytes = new byte[] { 10, 20, 30 }; + var hr = new HashResult(bytes); + + var convertedViaTo = hr.To(b => Convert.ToBase64String(b)); + var convertedViaMethod = hr.ToBase64String(); + + Assert.Equal(convertedViaMethod, convertedViaTo); + } + } +} diff --git a/test/Cuemon.Core.Tests/Security/HashTest.cs b/test/Cuemon.Core.Tests/Security/HashTest.cs new file mode 100644 index 000000000..a2ef9201a --- /dev/null +++ b/test/Cuemon.Core.Tests/Security/HashTest.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Codebelt.Extensions.Xunit; +using Cuemon.Collections.Generic; +using Xunit; + +namespace Cuemon.Security +{ + public class HashTest : Test + { + public HashTest(ITestOutputHelper output) : base(output) + { + } + + private sealed class PassthroughHash : Hash + { + // Return the input bytes as the computed "hash" so tests can inspect what bytes were provided. + public override HashResult ComputeHash(byte[] input) + { + return new HashResult(input ?? Array.Empty()); + } + + // No-op endian initializer to mirror default behavior used in tests. + protected override void EndianInitializer(EndianOptions options) + { + // Intentionally left blank - preserves options default. + } + } + + private static readonly PassthroughHash Sut = new(); + + [Fact] + public void ComputeHash_ByteArray_ReturnsSameBytes() + { + var input = new byte[] { 1, 2, 3, 4 }; + var hr = Sut.ComputeHash(input); + Assert.Equal(input, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Bool_UsesConvertible() + { + var input = true; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Byte_UsesConvertible() + { + byte input = 0x7F; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Char_UsesConvertible() + { + char input = 'Z'; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_DateTime_UsesConvertible() + { + var input = new DateTime(2020, 10, 20, 23, 13, 40, DateTimeKind.Utc); + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_DBNull_UsesConvertible() + { + var input = DBNull.Value; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Decimal_UsesConvertible() + { + decimal input = 12345.6789m; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Double_UsesConvertible() + { + double input = Math.PI; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Short_UsesConvertible() + { + short input = -1234; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Int_UsesConvertible() + { + int input = 42; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Long_UsesConvertible() + { + long input = 12345678901234L; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_SByte_UsesConvertible() + { + sbyte input = -12; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Float_UsesConvertible() + { + float input = 3.14f; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_UShort_UsesConvertible() + { + ushort input = 65000; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_UInt_UsesConvertible() + { + uint input = 0xDEADBEEF; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_ULong_UsesConvertible() + { + ulong input = 0xDEADBEEFCAFEBABEUL; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + private enum TestEnum : byte { Zero = 0, One = 1 } + + [Fact] + public void ComputeHash_String_UsesConvertible_DefaultEncoding() + { + var input = "hello world"; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Enum_UsesConvertible() + { + var input = TestEnum.One; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_ParamsIConvertible_AggregatesValues() + { + IConvertible[] input = { 1, "abc", (short)7 }; + var expected = Convertible.GetBytes(Arguments.ToEnumerableOf(input)); + var hr = Sut.ComputeHash(input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_IEnumerableIConvertible_AggregatesValues() + { + var input = new List { 2, "xyz", 9m }; + var expected = Convertible.GetBytes(input); + var hr = Sut.ComputeHash((IEnumerable)input); + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Stream_CopiesStreamAndComputes() + { + var payload = new byte[] { 11, 22, 33, 44, 55 }; + using var ms = new MemoryStream(payload); + // rewind to ensure stream is readable from start + ms.Position = 0; + + var hr = Sut.ComputeHash(ms); + + // underlying implementation copies stream content into a MemoryStream and then ComputeHash(byte[]) is invoked. + Assert.Equal(payload, hr.GetBytes()); + } + + private sealed class TestHash : Hash + { + public TestHash(Action setup = null) : base(setup) + { + } + + public override HashResult ComputeHash(byte[] input) + { + // For testing we simply wrap the incoming byte[] into a HashResult + return new HashResult(input); + } + } + + [Fact] + public void Ctor_ShouldConfigureOptions() + { + var sut = new TestHash(o => o.ByteOrder = Endianness.BigEndian); + Assert.NotNull(sut.Options); + Assert.Equal(Endianness.BigEndian, sut.Options.ByteOrder); + } + + [Fact] + public void ComputeHash_Int_ShouldRespectEndianInitializer() + { + // Configure to big endian explicitly + var sut = new TestHash(o => o.ByteOrder = Endianness.BigEndian); + + var value = 0x01020304; + var hr = sut.ComputeHash(value); + + var expected = Cuemon.Convertible.GetBytes(value, o => o.ByteOrder = sut.Options.ByteOrder); + + Assert.Equal(expected, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_Stream_ShouldReturnStreamBytes() + { + var sut = new TestHash(); + + var bytes = new byte[] { 0x10, 0x20, 0x30, 0x40 }; + using var ms = new MemoryStream(bytes); + + var hr = sut.ComputeHash(ms); + + Assert.Equal(bytes, hr.GetBytes()); + } + + [Fact] + public void ComputeHash_ParamsAndEnumerable_ShouldAggregateBytes() + { + var sut = new TestHash(o => o.ByteOrder = Endianness.BigEndian); + + var hrFromParams = sut.ComputeHash(1, "A", true); + var hrFromEnumerable = sut.ComputeHash(Arguments.ToEnumerableOf(1, "A", true)); + + var expected = Convertible.GetBytes(Arguments.ToEnumerableOf(1, "A", true)); + + Assert.Equal(expected, hrFromParams.GetBytes()); + Assert.Equal(expected, hrFromEnumerable.GetBytes()); + } + + [Fact] + public void ComputeHash_AllOverloads_ShouldReturnExpectedBytes() + { + var sut = new TestHash(); + + var dt = new DateTime(2020, 01, 02, 03, 04, 05, DateTimeKind.Utc); + var dec = 1234.5678m; + var dbl = 1234.5678d; + var flt = 1234.5f; + var sb = (sbyte)-5; + var us = (ushort)0xABCD; + var ui = (uint)0xDEADBEEF; + var ul = (ulong)0x0123456789ABCDEF; + var ch = 'X'; + var b = (byte)0x7F; + var sh = (short)0x1234; + var lng = (long)0x0102030405060708; + var boolean = true; + var enumVal = DayOfWeek.Wednesday; + var str = "hello"; + var dbnull = DBNull.Value; + + var cases = new List<(Func call, Func expected)> + { + (() => sut.ComputeHash(boolean), () => Convertible.GetBytes(boolean, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(b), () => Convertible.GetBytes(b, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(ch), () => Convertible.GetBytes(ch, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(dt), () => Convertible.GetBytes(dt)), + (() => sut.ComputeHash(dbnull), () => Convertible.GetBytes(dbnull)), + (() => sut.ComputeHash(dec), () => Convertible.GetBytes(dec)), + (() => sut.ComputeHash(dbl), () => Convertible.GetBytes(dbl, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(sh), () => Convertible.GetBytes(sh, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(12345), () => Convertible.GetBytes(12345, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(lng), () => Convertible.GetBytes(lng, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(sb), () => Convertible.GetBytes(sb, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(flt), () => Convertible.GetBytes(flt, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(us), () => Convertible.GetBytes(us, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(ui), () => Convertible.GetBytes(ui, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(ul), () => Convertible.GetBytes(ul, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(str, o => o.Encoding = Encoding.UTF8), () => Convertible.GetBytes(str, o => o.Encoding = Encoding.UTF8)), + (() => sut.ComputeHash(enumVal), () => Convertible.GetBytes(enumVal, o => o.ByteOrder = sut.Options.ByteOrder)), + (() => sut.ComputeHash(new byte[] { 1, 2, 3 }), () => new byte[] { 1, 2, 3 }), // ComputeHash(byte[]) implemented to return raw bytes + }; + + foreach (var (call, expected) in cases) + { + var result = call(); + var expectedBytes = expected(); + Assert.Equal(expectedBytes, result.GetBytes()); + } + } + } +} diff --git a/test/Cuemon.Core.Tests/ValidatorTest.cs b/test/Cuemon.Core.Tests/ValidatorTest.cs index e5e0f1401..9dd76e480 100644 --- a/test/Cuemon.Core.Tests/ValidatorTest.cs +++ b/test/Cuemon.Core.Tests/ValidatorTest.cs @@ -184,6 +184,23 @@ public void ThrowIfInvalidOptions_ShouldThrowArgumentNullException() }); } + [Fact] + public void ThrowIfInvalidOptions_ShouldPassSincePostConfiguredAndValid() + { + var options = new PostConfigurableOptions(); + Validator.ThrowIfInvalidOptions(options); + } + + [Fact] + public void ThrowIfInvalidOptions_ShouldFailSincePostConfiguredAndInvalid() + { + var options = new FailPostConfigurableOptions(); + Assert.Throws(() => + { + Validator.ThrowIfInvalidOptions(options); + }); + } + [Theory] [MemberData(nameof(GetValidatableOptions))] public void ThrowIfInvalidOptions_ShouldThrowArgumentException_WithInnerNotImplementedException(ValidatableOptions paramName) diff --git a/test/Cuemon.Security.Cryptography.Tests/AesCryptorTest.cs b/test/Cuemon.Security.Cryptography.Tests/AesCryptorTest.cs index 4a9010e53..ab87f6fdb 100644 --- a/test/Cuemon.Security.Cryptography.Tests/AesCryptorTest.cs +++ b/test/Cuemon.Security.Cryptography.Tests/AesCryptorTest.cs @@ -1,6 +1,7 @@ -using System; -using System.Linq; -using Codebelt.Extensions.Xunit; +using Codebelt.Extensions.Xunit; +using System; +using System.Security.Cryptography; +using System.Text; using Xunit; namespace Cuemon.Security.Cryptography @@ -33,5 +34,143 @@ public void AesCryptor_ShouldEncryptAndDecrypt() Assert.True(dec.SequenceEqual(secretMessage)); } + + [Fact] + public void DefaultConstructor_ShouldProduceValidKeyAndIvLengths() + { + var sut = new AesCryptor(); + + Assert.NotNull(sut.Key); + Assert.NotNull(sut.InitializationVector); + + // Default GenerateKey uses Aes256 => 256 bits => 32 bytes + Assert.Equal(256 / Convertible.BitsPerByte, sut.Key.Length); + // BlockSize is 128 bits => 16 bytes + Assert.Equal(AesCryptor.BlockSize / Convertible.BitsPerByte, sut.InitializationVector.Length); + } + + [Fact] + public void ParameterizedConstructor_ShouldSetProperties() + { + using var aes = Aes.Create(); + var key = aes.Key; + var iv = aes.IV; + + var sut = new AesCryptor(key, iv); + + Assert.Same(key, sut.Key); + Assert.Same(iv, sut.InitializationVector); + } + + [Fact] + public void Constructor_WithNullKey_ShouldThrowArgumentNullException() + { + var iv = AesCryptor.GenerateInitializationVector(); + Assert.Throws(() => new AesCryptor(null!, iv)); + } + + [Fact] + public void Constructor_WithNullIv_ShouldThrowArgumentNullException() + { + var key = AesCryptor.GenerateKey(); + Assert.Throws(() => new AesCryptor(key, null!)); + } + + [Fact] + public void Constructor_WithInvalidKeySize_ShouldThrowCryptographicException() + { + var invalidKey = new byte[10]; // not 128/192/256 bits + var iv = AesCryptor.GenerateInitializationVector(); + + var ex = Assert.Throws(() => new AesCryptor(invalidKey, iv)); + Assert.Contains("The key does not meet the required fixed size", ex.Message); + } + + [Fact] + public void Constructor_WithInvalidIvSize_ShouldThrowCryptographicException() + { + using var aes = Aes.Create(); + var key = aes.Key; + var invalidIv = new byte[8]; // not 128 bits + + var ex = Assert.Throws(() => new AesCryptor(key, invalidIv)); + Assert.Contains("The initialization vector does not meet the required fixed size of 128 bits.", ex.Message); + } + + [Fact] + public void EncryptAndDecrypt_ShouldReturnOriginalPayload() + { + using var aes = Aes.Create(); + var key = aes.Key; + var iv = aes.IV; + + var sut = new AesCryptor(key, iv); + + var plain = Encoding.UTF8.GetBytes("Hello, Cuemon! Encryption round-trip test."); + var encrypted = sut.Encrypt(plain); + Assert.NotNull(encrypted); + Assert.NotEmpty(encrypted); + Assert.False(plain.SequenceEqual(encrypted)); + + var decrypted = sut.Decrypt(encrypted); + Assert.NotNull(decrypted); + Assert.Equal(plain, decrypted); + } + + [Fact] + public void EncryptAndDecrypt_WithOptionsDelegate_ShouldReturnOriginalPayload() + { + using var aes = Aes.Create(); + var key = aes.Key; + var iv = aes.IV; + + var sut = new AesCryptor(key, iv); + + var plain = Encoding.UTF8.GetBytes("Hello, Cuemon! Options overload test."); + // provide explicit options (same as defaults) to ensure the overload code path runs + var encrypted = sut.Encrypt(plain, o => { o.Mode = System.Security.Cryptography.CipherMode.CBC; o.Padding = System.Security.Cryptography.PaddingMode.PKCS7; }); + Assert.NotNull(encrypted); + Assert.NotEmpty(encrypted); + + var decrypted = sut.Decrypt(encrypted, o => { o.Mode = System.Security.Cryptography.CipherMode.CBC; o.Padding = System.Security.Cryptography.PaddingMode.PKCS7; }); + Assert.Equal(plain, decrypted); + } + + [Fact] + public void GenerateInitializationVector_ShouldReturn16Bytes_AndDifferentValues() + { + var iv1 = AesCryptor.GenerateInitializationVector(); + var iv2 = AesCryptor.GenerateInitializationVector(); + + Assert.NotNull(iv1); + Assert.NotNull(iv2); + Assert.Equal(AesCryptor.BlockSize / Convertible.BitsPerByte, iv1.Length); + Assert.Equal(AesCryptor.BlockSize / Convertible.BitsPerByte, iv2.Length); + + // Very small chance of collision; assert that typical calls produce different values + Assert.False(iv1.SequenceEqual(iv2)); + } + + [Fact] + public void GenerateKey_DefaultAndCustomSizes_ShouldReturnExpectedLengths() + { + // default (Aes256) + var defaultKey = AesCryptor.GenerateKey(); + Assert.NotNull(defaultKey); + Assert.Equal(256 / Convertible.BitsPerByte, defaultKey.Length); + + // Aes128 + var key128 = AesCryptor.GenerateKey(o => o.Size = AesSize.Aes128); + Assert.NotNull(key128); + Assert.Equal(128 / Convertible.BitsPerByte, key128.Length); + + // Custom RandomStringProvider returning predictable string -> expected bytes length + var custom = AesCryptor.GenerateKey(o => + { + o.Size = AesSize.Aes128; + o.RandomStringProvider = size => new string('x', 128 / Convertible.BitsPerByte); + }); + Assert.Equal(128 / Convertible.BitsPerByte, custom.Length); + } } } \ No newline at end of file diff --git a/test/Cuemon.Security.Cryptography.Tests/HmacMessageDigest5Test.cs b/test/Cuemon.Security.Cryptography.Tests/HmacMessageDigest5Test.cs new file mode 100644 index 000000000..a1d39e448 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/HmacMessageDigest5Test.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class HmacMessageDigest5Test : Test + { + public HmacMessageDigest5Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacMd5_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacMessageDigest5(secret, null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACMD5(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacMd5_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacMessageDigest5(secret, null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACMD5(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSecret() + { + byte[] secret = null; + var sut = Record.Exception(() => new HmacMessageDigest5(secret, null)); + Assert.Null(sut); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var secret = "somesercret"u8.ToArray(); + var sut = new HmacMessageDigest5(secret, null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm1Test.cs b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm1Test.cs new file mode 100644 index 000000000..68edac382 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm1Test.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class HmacSecureHashAlgorithm1Test : Test + { + public HmacSecureHashAlgorithm1Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha1_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm1(secret, null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA1(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha1_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm1(secret, null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA1(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSecret() + { + byte[] secret = null; + var sut = Record.Exception(() => new HmacSecureHashAlgorithm1(secret, null)); + Assert.Null(sut); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var secret = "somesercret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm1(secret, null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm256Test.cs b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm256Test.cs new file mode 100644 index 000000000..7a0d1e045 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm256Test.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class HmacSecureHashAlgorithm256Test : Test + { + public HmacSecureHashAlgorithm256Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha256_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm256(secret, null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA256(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha256_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm256(secret, null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA256(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSecret() + { + byte[] secret = null; + var sut = Record.Exception(() => new HmacSecureHashAlgorithm256(secret, null)); + Assert.Null(sut); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var secret = "somesercret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm256(secret, null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm384Test.cs b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm384Test.cs new file mode 100644 index 000000000..b004c8737 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm384Test.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class HmacSecureHashAlgorithm384Test : Test + { + public HmacSecureHashAlgorithm384Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha384_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm384(secret, null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA384(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha384_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm384(secret, null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA384(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSecret() + { + byte[] secret = null; + var sut = Record.Exception(() => new HmacSecureHashAlgorithm384(secret, null)); + Assert.Null(sut); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var secret = "somesercret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm384(secret, null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm512Test.cs b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm512Test.cs new file mode 100644 index 000000000..8219a79d8 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/HmacSecureHashAlgorithm512Test.cs @@ -0,0 +1,68 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class HmacSecureHashAlgorithm512Test : Test + { + public HmacSecureHashAlgorithm512Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha512_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm512(secret, null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA512(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha512_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm512(secret, null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA512(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSecret() + { + byte[] secret = null; + var sut = Record.Exception(() => new HmacSecureHashAlgorithm512(secret, null)); + Assert.Null(sut); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var secret = "somesercret"u8.ToArray(); + var sut = new HmacSecureHashAlgorithm512(secret, null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/KeyedCryptoHashTest.cs b/test/Cuemon.Security.Cryptography.Tests/KeyedCryptoHashTest.cs new file mode 100644 index 000000000..a62cc3b0e --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/KeyedCryptoHashTest.cs @@ -0,0 +1,75 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class KeyedCryptoHashTest : Test + { + public KeyedCryptoHashTest(ITestOutputHelper output) : base(output) + { + } + + private sealed class HmacSha256TestHash : KeyedCryptoHash + { + public HmacSha256TestHash(byte[] secret) : base(secret, null) + { + } + } + + private sealed class HmacMd5TestHash : KeyedCryptoHash + { + public HmacMd5TestHash(byte[] secret) : base(secret, null) + { + } + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacSha256_ForGivenInput() + { + var secret = "unittest-secret"u8.ToArray(); + var sut = new HmacSha256TestHash(secret); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACSHA256(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedHmacMd5_ForEmptyArray() + { + var secret = "another-secret"u8.ToArray(); + var sut = new HmacMd5TestHash(secret); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var h = new HMACMD5(secret)) + { + expected = h.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + byte[] secret = null; + var sut = new HmacSha256TestHash(secret); + Assert.Throws(() => sut.ComputeHash(secret)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/MessageDigest5Test.cs b/test/Cuemon.Security.Cryptography.Tests/MessageDigest5Test.cs new file mode 100644 index 000000000..09f8bfd18 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/MessageDigest5Test.cs @@ -0,0 +1,70 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class MessageDigest5Test : Test + { + public MessageDigest5Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BitSize_ShouldBe128() + { + Assert.Equal(128, MessageDigest5.BitSize); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedMd5_ForGivenInput() + { + var sut = new MessageDigest5(null); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var md5 = MD5.Create()) + { + expected = md5.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedMd5_ForEmptyArray() + { + var sut = new MessageDigest5(null); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var md5 = MD5.Create()) + { + expected = md5.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void Constructor_ShouldAllowNullSetup() + { + var ex = Record.Exception(() => new MessageDigest5(null)); + Assert.Null(ex); + } + + [Fact] + public void ComputeHash_ShouldThrowArgumentNullException_WhenInputIsNull() + { + var sut = new MessageDigest5(null); + Assert.Throws(() => sut.ComputeHash((byte[])null)); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/SHA512256Test.cs b/test/Cuemon.Security.Cryptography.Tests/SHA512256Test.cs new file mode 100644 index 000000000..16273ebd4 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/SHA512256Test.cs @@ -0,0 +1,186 @@ +using System; +using System.Linq; +using System.Reflection; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class SHA512256Test : Test + { + public SHA512256Test(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Ctor_ShouldSetHashSizeTo256() + { + using var sut = new SHA512256(); + Assert.Equal(256, sut.HashSize); + } + + [Fact] + public void WriteULongBE_ShouldWriteBigEndianBytes() + { + // Arrange + var mi = typeof(SHA512256).GetMethod("WriteULongBE", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(mi); + + ulong value = 0x1122334455667788UL; + var buffer = new byte[16]; + + // Act + mi.Invoke(null, new object[] { value, buffer, 4 }); + + // Assert: bytes at offset 4..11 are big-endian representation of value + var expected = new byte[] { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88 }; + Assert.Equal(expected, buffer.Skip(4).Take(8).ToArray()); + } + + [Fact] + public void RotateRight_ShouldRotateBitsCorrectly() + { + var mi = typeof(SHA512256).GetMethod("RotateRight", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(mi); + + ulong x = 0x8000000000000001UL; // bits at both ends + int n = 1; + + var result = (ulong)mi.Invoke(null, new object[] { x, n }); + // manual rotate right by 1: (x >> 1) | (x << 63) + ulong expected = (x >> 1) | (x << (64 - n)); + Assert.Equal(expected, result); + + // another arbitrary rotation + x = 0x0123456789ABCDEFUL; + n = 20; + result = (ulong)mi.Invoke(null, new object[] { x, n }); + expected = (x >> n) | (x << (64 - n)); + Assert.Equal(expected, result); + } + + [Fact] + public void AddLength_ShouldAccumulateBitCountsAndCarryToHigh() + { + var sut = new SHA512256(); + var mi = typeof(SHA512256).GetMethod("AddLength", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(mi); + + var lowField = typeof(SHA512256).GetField("_bitCountLow", BindingFlags.NonPublic | BindingFlags.Instance); + var highField = typeof(SHA512256).GetField("_bitCountHigh", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(lowField); + Assert.NotNull(highField); + + // Set low to near overflow + lowField.SetValue(sut, ulong.MaxValue - 7UL); + highField.SetValue(sut, 5UL); + + // Add 8 bits -> causes low to wrap and high to increment + mi.Invoke(sut, new object[] { 8UL }); + + var low = (ulong)lowField.GetValue(sut); + var high = (ulong)highField.GetValue(sut); + + unchecked + { + Assert.Equal(ulong.MaxValue - 7UL + 8UL, low); // wraps by ulong arithmetic + } + + Assert.Equal(6UL, high); // carried 1 + } + + [Fact] + public void ProcessBlock_ShouldChangeInternalState() + { + var sut = new SHA512256(); + + var hField = typeof(SHA512256).GetField("_H", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(hField); + + // Get initial H (after constructor Initialize ran) + var initialH = (ulong[])hField.GetValue(sut); + Assert.NotNull(initialH); + + // Clone initial for comparison + var before = (ulong[])initialH.Clone(); + + // Prepare a single block with non-zero content + var block = Enumerable.Range(0, 128).Select(i => (byte)(i & 0xFF)).ToArray(); + + var mi = typeof(SHA512256).GetMethod("ProcessBlock", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(mi); + + // Act + mi.Invoke(sut, new object[] { block, 0 }); + + var after = (ulong[])hField.GetValue(sut); + + // Assert that at least one H word changed + Assert.True(before.Where((v, i) => v != after[i]).Any()); + } + + [Fact] + public void Initialize_ShouldResetInternalState() + { + var sut = new SHA512256(); + + var hField = typeof(SHA512256).GetField("_H", BindingFlags.NonPublic | BindingFlags.Instance); + var bufferPosField = typeof(SHA512256).GetField("_bufferPos", BindingFlags.NonPublic | BindingFlags.Instance); + var lowField = typeof(SHA512256).GetField("_bitCountLow", BindingFlags.NonPublic | BindingFlags.Instance); + var highField = typeof(SHA512256).GetField("_bitCountHigh", BindingFlags.NonPublic | BindingFlags.Instance); + var ivField = typeof(SHA512256).GetField("IV512_256", BindingFlags.NonPublic | BindingFlags.Static); + + Assert.NotNull(hField); + Assert.NotNull(bufferPosField); + Assert.NotNull(lowField); + Assert.NotNull(highField); + Assert.NotNull(ivField); + + // Compute a hash to ensure the algorithm runs (don't assert internal change - implementations may reset state) + var result = sut.ComputeHash("hello"u8.ToArray()); + Assert.NotNull(result); + + // Act: initialize and assert reset + sut.Initialize(); + + var resetH = (ulong[])hField.GetValue(sut); + var bufferPos = (int)bufferPosField.GetValue(sut); + var low = (ulong)lowField.GetValue(sut); + var high = (ulong)highField.GetValue(sut); + var iv = (ulong[])ivField.GetValue(null); + + Assert.Equal(0, bufferPos); + Assert.Equal(0UL, low); + Assert.Equal(0UL, high); + Assert.Equal(iv, resetH); + } + + [Fact] + public void ComputeHash_ShouldReturn32ByteDigest_AndBeDeterministic() + { + using var sut = new SHA512256(); + + var input = "hello"u8.ToArray(); + + var one = sut.ComputeHash(input); + var two = sut.ComputeHash(input); + + Assert.NotNull(one); + Assert.NotNull(two); + Assert.Equal(32, one.Length); + Assert.Equal(32, two.Length); + Assert.Equal(one, two); // deterministic + } + + [Fact] + public void ComputeHash_EmptyArray_ShouldReturn32Bytes() + { + using var sut = new SHA512256(); + + var input = Array.Empty(); + + var hash = sut.ComputeHash(input); + + Assert.NotNull(hash); + Assert.Equal(32, hash.Length); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm1Test.cs b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm1Test.cs new file mode 100644 index 000000000..024191305 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm1Test.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class SecureHashAlgorithm1Test : Test + { + public SecureHashAlgorithm1Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BitSize_ShouldBe160() + { + Assert.Equal(160, SecureHashAlgorithm1.BitSize); + } + + [Fact] + public void Ctor_ShouldConfigureOptions_WhenSetupIsProvided() + { + var sut = new SecureHashAlgorithm1(o => o.ByteOrder = Endianness.BigEndian); + + Assert.NotNull(sut.Options); + Assert.Equal(Endianness.BigEndian, sut.Options.ByteOrder); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha1_ForGivenInput() + { + var sut = new SecureHashAlgorithm1(); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA1.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha1_ForEmptyArray() + { + var sut = new SecureHashAlgorithm1(); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA1.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm256Test.cs b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm256Test.cs new file mode 100644 index 000000000..3fcf25503 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm256Test.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class SecureHashAlgorithm256Test : Test + { + public SecureHashAlgorithm256Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BitSize_ShouldBe256() + { + Assert.Equal(256, SecureHashAlgorithm256.BitSize); + } + + [Fact] + public void Ctor_ShouldConfigureOptions_WhenSetupIsProvided() + { + var sut = new SecureHashAlgorithm1(o => o.ByteOrder = Endianness.BigEndian); + + Assert.NotNull(sut.Options); + Assert.Equal(Endianness.BigEndian, sut.Options.ByteOrder); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha256_ForGivenInput() + { + var sut = new SecureHashAlgorithm256(); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA256.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha256_ForEmptyArray() + { + var sut = new SecureHashAlgorithm256(); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA256.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm384Test.cs b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm384Test.cs new file mode 100644 index 000000000..1ca566c0e --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm384Test.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class SecureHashAlgorithm384Test : Test + { + public SecureHashAlgorithm384Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BitSize_ShouldBe384() + { + Assert.Equal(384, SecureHashAlgorithm384.BitSize); + } + + [Fact] + public void Ctor_ShouldConfigureOptions_WhenSetupIsProvided() + { + var sut = new SecureHashAlgorithm384(o => o.ByteOrder = Endianness.BigEndian); + + Assert.NotNull(sut.Options); + Assert.Equal(Endianness.BigEndian, sut.Options.ByteOrder); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha384_ForGivenInput() + { + var sut = new SecureHashAlgorithm384(); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA384.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha384_ForEmptyArray() + { + var sut = new SecureHashAlgorithm384(); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA384.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm512Test.cs b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm512Test.cs new file mode 100644 index 000000000..5a308487e --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/SecureHashAlgorithm512Test.cs @@ -0,0 +1,65 @@ +using System; +using System.Security.Cryptography; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class SecureHashAlgorithm512Test : Test + { + public SecureHashAlgorithm512Test(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void BitSize_ShouldBe512() + { + Assert.Equal(512, SecureHashAlgorithm512.BitSize); + } + + [Fact] + public void Ctor_ShouldConfigureOptions_WhenSetupIsProvided() + { + var sut = new SecureHashAlgorithm512(o => o.ByteOrder = Endianness.BigEndian); + + Assert.NotNull(sut.Options); + Assert.Equal(Endianness.BigEndian, sut.Options.ByteOrder); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha512_ForGivenInput() + { + var sut = new SecureHashAlgorithm512(); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA512.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha512_ForEmptyArray() + { + var sut = new SecureHashAlgorithm512(); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA512.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + } +} diff --git a/test/Cuemon.Security.Cryptography.Tests/UnkeyedCryptoHashTest.cs b/test/Cuemon.Security.Cryptography.Tests/UnkeyedCryptoHashTest.cs new file mode 100644 index 000000000..9ff0a29f7 --- /dev/null +++ b/test/Cuemon.Security.Cryptography.Tests/UnkeyedCryptoHashTest.cs @@ -0,0 +1,75 @@ +using System; +using System.Security.Cryptography; +using System.Text; +using Codebelt.Extensions.Xunit; +using Xunit; + +namespace Cuemon.Security.Cryptography +{ + public class UnkeyedCryptoHashTest : Test + { + public UnkeyedCryptoHashTest(ITestOutputHelper output) : base(output) + { + } + + private sealed class Sha256TestHash : UnkeyedCryptoHash + { + public Sha256TestHash() : base(() => SHA256.Create(), null) + { + } + } + + // This subclass intentionally passes a null initializer to exercise the guard in the base ctor. + private sealed class NullInitializerHash : UnkeyedCryptoHash + { + public NullInitializerHash() : base(null, null) + { + } + } + + [Fact] + public void Ctor_ShouldThrowArgumentNullException_WhenInitializerIsNull() + { + var ex = Assert.Throws(() => new NullInitializerHash()); + // The base ctor validates the 'initializer' parameter; ensure param name is propagated. + Assert.Equal("initializer", ex.ParamName); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha256_ForGivenInput() + { + var sut = new Sha256TestHash(); + var input = "hello"u8.ToArray(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + // Compute expected bytes using the same algorithm to assert correctness. + byte[] expected; + using (var sha = SHA256.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + + [Fact] + public void ComputeHash_ShouldReturnExpectedSha256_ForEmptyArray() + { + var sut = new Sha256TestHash(); + var input = Array.Empty(); + var result = sut.ComputeHash(input); + + Assert.True(result.HasValue); + + byte[] expected; + using (var sha = SHA256.Create()) + { + expected = sha.ComputeHash(input); + } + + Assert.Equal(expected, result.GetBytes()); + } + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/Cuemon.Core.Benchmarks.csproj b/tuning/Cuemon.Core.Benchmarks/Cuemon.Core.Benchmarks.csproj new file mode 100644 index 000000000..96d9eeb81 --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/Cuemon.Core.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Cuemon + + + + + + + diff --git a/tuning/Cuemon.Core.Benchmarks/DateSpanBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/DateSpanBenchmark.cs new file mode 100644 index 000000000..0392e2b28 --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/DateSpanBenchmark.cs @@ -0,0 +1,94 @@ +using System; +using System.Globalization; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class DateSpanBenchmark + { + private DateTime _now; + private DateTime _shortEnd; + private DateTime _mediumEnd; + private DateTime _longEnd; + + private DateSpan _shortSpan; + private DateSpan _mediumSpan; + private DateSpan _longSpan; + + private string _shortStartString; + private string _shortEndString; + private string _longStartString; + private string _longEndString; + + [GlobalSetup] + public void Setup() + { + _now = DateTime.UtcNow; + + _shortEnd = _now.AddHours(36); // ~1.5 days + _mediumEnd = _now.AddMonths(5).AddDays(12).AddHours(3); + _longEnd = _now.AddYears(5).AddMonths(2).AddDays(10).AddHours(4).AddMilliseconds(123); + + _shortSpan = new DateSpan(_now, _shortEnd); + _mediumSpan = new DateSpan(_now, _mediumEnd); + _longSpan = new DateSpan(_now, _longEnd); + + // ISO sortable ("s") uses invariant culture, matches DateSpan.Parse overloads + _shortStartString = _now.ToString("s", CultureInfo.InvariantCulture); + _shortEndString = _shortEnd.ToString("s", CultureInfo.InvariantCulture); + + _longStartString = _now.ToString("s", CultureInfo.InvariantCulture); + _longEndString = _longEnd.ToString("s", CultureInfo.InvariantCulture); + } + + // Construction: common scenarios + [Benchmark(Baseline = true, Description = "Ctor (short span)")] + public DateSpan Construct_Short() => new DateSpan(_now, _shortEnd); + + [Benchmark(Description = "Ctor (medium span)")] + public DateSpan Construct_Medium() => new DateSpan(_now, _mediumEnd); + + [Benchmark(Description = "Ctor (long span)")] + public DateSpan Construct_Long() => new DateSpan(_now, _longEnd); + + // Single-argument ctor that uses DateTime.Today as upper bound + [Benchmark(Description = "Ctor (single-date)")] + public DateSpan Construct_SingleDate() => new DateSpan(_now); + + // Parsing from string (culture-aware overload) + [Benchmark(Description = "Parse (short)")] + public DateSpan Parse_Short() => DateSpan.Parse(_shortStartString, _shortEndString, CultureInfo.InvariantCulture); + + [Benchmark(Description = "Parse (long)")] + public DateSpan Parse_Long() => DateSpan.Parse(_longStartString, _longEndString, CultureInfo.InvariantCulture); + + // Common instance operations + [Benchmark(Description = "ToString (short)")] + public string ToString_Short() => _shortSpan.ToString(); + + [Benchmark(Description = "ToString (long)")] + public string ToString_Long() => _longSpan.ToString(); + + [Benchmark(Description = "GetWeeks (short)")] + public int GetWeeks_Short() => _shortSpan.GetWeeks(); + + [Benchmark(Description = "GetWeeks (long)")] + public int GetWeeks_Long() => _longSpan.GetWeeks(); + + [Benchmark(Description = "GetHashCode")] + public int GetHashCode_Benchmark() => _longSpan.GetHashCode(); + + [Benchmark(Description = "Equals (value vs same value)")] + public bool Equals_Same() => _longSpan.Equals(new DateSpan(_now, _longEnd)); + + [Benchmark(Description = "Operator == (same value)")] + public bool OperatorEquality_Same() + { + var other = new DateSpan(_now, _longEnd); + return _longSpan == other; + } + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/DelimitedStringBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/DelimitedStringBenchmark.cs new file mode 100644 index 000000000..dbb408fea --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/DelimitedStringBenchmark.cs @@ -0,0 +1,55 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using System.Collections.Generic; +using System.Text; + +namespace Cuemon +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class DelimitedStringBenchmark + { + [Params(10, 100, 1000)] + public int Count { get; set; } + + private List _items = null!; + private string _delimited = null!; + private readonly char _delimiter = ','; + private readonly char _qualifier = '"'; + + [GlobalSetup] + public void Setup() + { + _items = new List(Count); + for (var i = 0; i < Count; i++) + { + // include some items that contain delimiters to exercise quoting behavior + _items.Add(i % 10 == 0 ? $"value {i},with,commas" : $"value{i}"); + } + + var sb = new StringBuilder(); + for (var i = 0; i < Count; i++) + { + // mix quoted and unquoted fields to resemble realistic CSV input + var item = i % 7 == 0 + ? $"{_qualifier}value {i},has,commas{_qualifier}" + : $"value{i}"; + sb.Append(item).Append(_delimiter); + } + _delimited = sb.Length > 0 ? sb.ToString(0, sb.Length - 1) : sb.ToString(); + } + + [Benchmark] + public string Create() => DelimitedString.Create(_items, o => + { + o.Delimiter = _delimiter.ToString(); + }); + + [Benchmark] + public string[] Split() => DelimitedString.Split(_delimited, o => + { + o.Delimiter = _delimiter.ToString(); + o.Qualifier = _qualifier.ToString(); + }); + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/GenerateBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/GenerateBenchmark.cs new file mode 100644 index 000000000..655b3e66e --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/GenerateBenchmark.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class GenerateBenchmark + { + // Parameterized input sizes to exercise micro / mid / macro scenarios + [Params(8, 256, 4096)] + public int Count { get; set; } + + private object _sampleObject; + private string[] _randomStringValues; + private IEnumerable _hashConvertibles; + + [GlobalSetup] + public void Setup() + { + _sampleObject = new Sample + { + Id = 42, + Name = "BenchmarkSample", + Created = DateTime.UtcNow, + Tags = new[] { "alpha", "beta", "gamma" } + }; + + _randomStringValues = new[] { "abcdefghijklmnopqrstuvwxyz", "0123456789", "ABCDEFGHIJKLMNOPQRSTUVWXYZ" }; + + _hashConvertibles = new IConvertible[] { 1, "two", 3.0, (short)4, 5L, 6.5m }; + } + + // --------------------- + // RangeOf + // --------------------- + private int _sink; + + [Benchmark(Description = "RangeOf - enumerate")] + public void RangeOf_Enumerate() + { + _sink = 0; + + foreach (var value in Generate.RangeOf(Count, i => i)) + { + _sink += value; + } + } + + // --------------------- + // RandomNumber + // --------------------- + [Benchmark(Description = "RandomNumber - default")] + public int RandomNumber_Default() => Generate.RandomNumber(); + + [Benchmark(Description = "RandomNumber - bounded")] + public int RandomNumber_Bounded() => Generate.RandomNumber(0, Math.Max(1, Count)); + + // --------------------- + // FixedString + // --------------------- + [Benchmark(Description = "FixedString")] + public string FixedString_Benchmark() => Generate.FixedString('x', Count); + + // --------------------- + // RandomString + // --------------------- + [Benchmark(Description = "RandomString - letters/numbers")] + public string RandomString_Benchmark() => Generate.RandomString(Count, _randomStringValues); + + // --------------------- + // ObjectPortrayal + // --------------------- + [Benchmark(Description = "ObjectPortrayal - basic object")] + public string ObjectPortrayal_Basic() => Generate.ObjectPortrayal(_sampleObject); + + // --------------------- + // HashCode32 / HashCode64 + // --------------------- + [Benchmark(Description = "HashCode32 - params")] + public int HashCode32_Params() => Generate.HashCode32(1, "a", 3.14); + + [Benchmark(Description = "HashCode32 - enumerable")] + public int HashCode32_Enumerable() => Generate.HashCode32(_hashConvertibles); + + [Benchmark(Description = "HashCode64 - params")] + public long HashCode64_Params() => Generate.HashCode64(1, "a", 3.14); + + [Benchmark(Description = "HashCode64 - enumerable")] + public long HashCode64_Enumerable() => Generate.HashCode64(_hashConvertibles); + + // --------------------- + // Helpers / sample types + // --------------------- + private sealed class Sample + { + public int Id { get; set; } + public string Name { get; set; } + public DateTime Created { get; set; } + public string[] Tags { get; set; } + } + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/Security/CyclicRedundancyCheckBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/Security/CyclicRedundancyCheckBenchmark.cs new file mode 100644 index 000000000..5eb429832 --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/Security/CyclicRedundancyCheckBenchmark.cs @@ -0,0 +1,54 @@ +using System; +using System.IO; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon.Security +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class CyclicRedundancyCheckBenchmark + { + [Params(64, 4096, 1048576)] + public int Size { get; set; } + + private byte[] _payload; + private CyclicRedundancyCheck32 _crc32; + private CyclicRedundancyCheck64 _crc64; + + [GlobalSetup] + public void Setup() + { + _payload = new byte[Size]; + var rnd = new Random(42); + rnd.NextBytes(_payload); + + _crc32 = new CyclicRedundancyCheck32(); + _crc64 = new CyclicRedundancyCheck64(); + + // Warm-up to ensure lazy lookup tables are initialized outside measured runs + _crc32.ComputeHash(new byte[] { 0x0 }); + _crc64.ComputeHash(new byte[] { 0x0 }); + } + + [Benchmark(Baseline = true, Description = "CRC32 - byte[]")] + public HashResult ComputeHash_Crc32_Bytes() => _crc32.ComputeHash(_payload); + + [Benchmark(Description = "CRC64 - byte[]")] + public HashResult ComputeHash_Crc64_Bytes() => _crc64.ComputeHash(_payload); + + [Benchmark(Description = "CRC32 - Stream (includes copy)")] + public HashResult ComputeHash_Crc32_Stream() + { + using var ms = new MemoryStream(_payload, writable: false); + return _crc32.ComputeHash(ms); + } + + [Benchmark(Description = "CRC64 - Stream (includes copy)")] + public HashResult ComputeHash_Crc64_Stream() + { + using var ms = new MemoryStream(_payload, writable: false); + return _crc64.ComputeHash(ms); + } + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/Security/FowlerNollVoHashBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/Security/FowlerNollVoHashBenchmark.cs new file mode 100644 index 000000000..f891c2b83 --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/Security/FowlerNollVoHashBenchmark.cs @@ -0,0 +1,92 @@ +using System.Text; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon.Security +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class FowlerNollVoHashBenchmark + { + private byte[] _smallPayload; + private byte[] _largePayload; + private FowlerNollVo32 _fnv32 = null!; + private FowlerNollVo64 _fnv64 = null!; + private FowlerNollVo128 _fnv128 = null!; + private FowlerNollVo256 _fnv256 = null!; + private FowlerNollVo512 _fnv512 = null!; + private FowlerNollVo1024 _fnv1024 = null!; + + + + [GlobalSetup] + public void Setup() + { + _smallPayload = "The quick brown fox jumps over the lazy dog"u8.ToArray(); + var sb = new StringBuilder(); + for (int i = 0; i < 10000; i++) + { + sb.Append("The quick brown fox jumps over the lazy dog"); + } + _largePayload = Encoding.UTF8.GetBytes(sb.ToString()); + + _fnv32 = new FowlerNollVo32(o => o.Algorithm = Algorithm); + _fnv64 = new FowlerNollVo64(o => o.Algorithm = Algorithm); + _fnv128 = new FowlerNollVo128(o => o.Algorithm = Algorithm); + _fnv256 = new FowlerNollVo256(o => o.Algorithm = Algorithm); + _fnv512 = new FowlerNollVo512(o => o.Algorithm = Algorithm); + _fnv1024 = new FowlerNollVo1024(o => o.Algorithm = Algorithm); + } + + [Params(FowlerNollVoAlgorithm.Fnv1, FowlerNollVoAlgorithm.Fnv1a)] + public FowlerNollVoAlgorithm Algorithm { get; set; } + + [Benchmark(Description = "ComputeHash32 (small)")] + public HashResult ComputeHash32_Small() + => _fnv32.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash32 (large)")] + public HashResult ComputeHash32_Large() + => _fnv32.ComputeHash(_largePayload); + + [Benchmark(Description = "ComputeHash64 (small)")] + public HashResult ComputeHash64_Small() + => _fnv64.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash64 (large)")] + public HashResult ComputeHash64_Large() + => _fnv64.ComputeHash(_largePayload); + + [Benchmark(Description = "ComputeHash128 (small)")] + public HashResult ComputeHash128_Small() + => _fnv128.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash128 (large)")] + public HashResult ComputeHash128_Large() + => _fnv128.ComputeHash(_largePayload); + + [Benchmark(Description = "ComputeHash256 (small)")] + public HashResult ComputeHash256_Small() + => _fnv256.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash256 (large)")] + public HashResult ComputeHash256_Large() + => _fnv256.ComputeHash(_largePayload); + + [Benchmark(Description = "ComputeHash512 (small)")] + public HashResult ComputeHash512_Small() + => _fnv512.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash512 (large)")] + public HashResult ComputeHash512_Large() + => _fnv512.ComputeHash(_largePayload); + + [Benchmark(Description = "ComputeHash1024 (small)")] + public HashResult ComputeHash1024_Small() + => _fnv1024.ComputeHash(_smallPayload); + + [Benchmark(Description = "ComputeHash1024 (large)")] + public HashResult ComputeHash1024_Large() + => _fnv1024.ComputeHash(_largePayload); + } +} diff --git a/tuning/Cuemon.Core.Benchmarks/Security/HashResultBenchmark.cs b/tuning/Cuemon.Core.Benchmarks/Security/HashResultBenchmark.cs new file mode 100644 index 000000000..20f3ed11b --- /dev/null +++ b/tuning/Cuemon.Core.Benchmarks/Security/HashResultBenchmark.cs @@ -0,0 +1,62 @@ +using System; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon.Security +{ + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] + public class HashResultBenchmark + { + [Params(0, 8, 32, 256, 1024)] + public int Size { get; set; } + + private byte[] _data; + private HashResult _hashResult; + + [GlobalSetup] + public void Setup() + { + _data = new byte[Size]; + var rng = new Random(42); + rng.NextBytes(_data); + _hashResult = new HashResult(_data); + } + + [Benchmark(Description = "HashResult.GetBytes - copy bytes")] + public byte[] GetBytes_Copy() + { + return _hashResult.GetBytes(); + } + + [Benchmark(Description = "HashResult.ToHexadecimalString")] + public string ToHexadecimalString() + { + return _hashResult.ToHexadecimalString(); + } + + [Benchmark(Description = "HashResult.ToBase64String")] + public string ToBase64String() + { + return _hashResult.ToBase64String(); + } + + [Benchmark(Description = "HashResult.ToUrlEncodedBase64String")] + public string ToUrlEncodedBase64String() + { + return _hashResult.ToUrlEncodedBase64String(); + } + + [Benchmark(Description = "HashResult.ToBinaryString")] + public string ToBinaryString() + { + return _hashResult.ToBinaryString(); + } + + [Benchmark(Description = "HashResult.To (converter)")] + public string ToWithConverter() + { + return _hashResult.To(Convert.ToBase64String); + } + } +} diff --git a/tuning/Cuemon.Security.Cryptography.Benchmarks/AesCryptorBenchmarks.cs b/tuning/Cuemon.Security.Cryptography.Benchmarks/AesCryptorBenchmarks.cs new file mode 100644 index 000000000..20b7bab08 --- /dev/null +++ b/tuning/Cuemon.Security.Cryptography.Benchmarks/AesCryptorBenchmarks.cs @@ -0,0 +1,40 @@ +using System; +using System.Security.Cryptography; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; + +namespace Cuemon.Security.Cryptography +{ + [MemoryDiagnoser] + public class AesCryptorBenchmarks + { + [Params(128, 1024, 65536)] + public int Size { get; set; } + + private AesCryptor _cryptor; + private byte[] _plaintext; + private byte[] _ciphertext; + + [GlobalSetup] + public void GlobalSetup() + { + using var aes = Aes.Create(); + var key = aes.Key; + var iv = aes.IV; + _cryptor = new AesCryptor(key, iv); + + _plaintext = new byte[Size]; + var rnd = new Random(42); + rnd.NextBytes(_plaintext); + + // Precompute ciphertext so Decrypt benchmark measures only decryption. + _ciphertext = _cryptor.Encrypt(_plaintext); + } + + [Benchmark(Description = "AesCryptor.Encrypt")] + public byte[] Encrypt() => _cryptor.Encrypt(_plaintext); + + [Benchmark(Description = "AesCryptor.Decrypt")] + public byte[] Decrypt() => _cryptor.Decrypt(_ciphertext); + } +} diff --git a/tuning/Cuemon.Security.Cryptography.Benchmarks/Cuemon.Security.Cryptography.Benchmarks.csproj b/tuning/Cuemon.Security.Cryptography.Benchmarks/Cuemon.Security.Cryptography.Benchmarks.csproj new file mode 100644 index 000000000..1428bca9d --- /dev/null +++ b/tuning/Cuemon.Security.Cryptography.Benchmarks/Cuemon.Security.Cryptography.Benchmarks.csproj @@ -0,0 +1,11 @@ + + + + Cuemon.Security.Cryptography + + + + + + + diff --git a/tuning/Cuemon.Security.Cryptography.Benchmarks/Sha512256Benchmark.cs b/tuning/Cuemon.Security.Cryptography.Benchmarks/Sha512256Benchmark.cs new file mode 100644 index 000000000..83dfeb759 --- /dev/null +++ b/tuning/Cuemon.Security.Cryptography.Benchmarks/Sha512256Benchmark.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; + +namespace Cuemon.Security.Cryptography +{ + // Group results by Params (makes comparing algorithm variants easy) + [MemoryDiagnoser] + [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByParams)] + public class Sha512256Benchmark + { + // Expose algorithm variants so runs can be filtered/controlled via Params + public enum AlgorithmVariant + { + CustomSHA512_256, + SHA512_Truncated + } + + // Allows switching algorithm variant at runtime via BenchmarkDotNet params + [Params(AlgorithmVariant.CustomSHA512_256, AlgorithmVariant.SHA512_Truncated)] + public AlgorithmVariant Variant { get; set; } + + // Prepared inputs (created once in GlobalSetup) + private byte[] _smallInput; + private byte[] _largeInput; + + // Factory map to create fresh HashAlgorithm instances per invocation + private readonly Dictionary> _factories = new(); + + [GlobalSetup] + public void GlobalSetup() + { + // Prepare deterministic payloads once + var rng = new Random(42); + + _smallInput = new byte[64]; // small payload (~64 bytes) + rng.NextBytes(_smallInput); + + _largeInput = new byte[1_048_576]; // large payload (~1 MB) + rng.NextBytes(_largeInput); + + // Initialize algorithm factories (do not reuse instances across invocations) + _factories[AlgorithmVariant.CustomSHA512_256] = () => new Cuemon.Security.Cryptography.SHA512256(); + + // Built-in SHA-512 then truncate to 256 bits (first 32 bytes) implemented by computing full SHA-512 + _factories[AlgorithmVariant.SHA512_Truncated] = () => SHA512.Create(); + } + + // ---- Explicit benchmark methods for each combination of implementation and input size ---- + // These methods return the algorithm result (byte[]), have descriptive names and are measured separately. + + [Benchmark(Baseline = true, Description = "Custom SHA-512/256 small (64 bytes)")] + public byte[] CustomSHA512256_Small() + { + using var alg = _factories[AlgorithmVariant.CustomSHA512_256](); + return alg.ComputeHash(_smallInput); + } + + [Benchmark(Description = "Custom SHA-512/256 large (1 MB)")] + public byte[] CustomSHA512256_Large() + { + using var alg = _factories[AlgorithmVariant.CustomSHA512_256](); + return alg.ComputeHash(_largeInput); + } + + [Benchmark(Description = "Built-in SHA-512 truncated -> 256 small (64 bytes)")] + public byte[] BuiltInSHA512_Truncated_Small() + { + using var alg = _factories[AlgorithmVariant.SHA512_Truncated](); + var full = alg.ComputeHash(_smallInput); + // Truncate to 256 bits (first 32 bytes) to mimic SHA-512/256 output length + var truncated = new byte[32]; + Array.Copy(full, 0, truncated, 0, truncated.Length); + return truncated; + } + + [Benchmark(Description = "Built-in SHA-512 truncated -> 256 large (1 MB)")] + public byte[] BuiltInSHA512_Truncated_Large() + { + using var alg = _factories[AlgorithmVariant.SHA512_Truncated](); + var full = alg.ComputeHash(_largeInput); + var truncated = new byte[32]; + Array.Copy(full, 0, truncated, 0, truncated.Length); + return truncated; + } + + // ---- Generic method that uses the [Params] Variant (optional; useful for grouped runs) ---- + // Returns byte[] and chooses algorithm based on the Variant param. + [Benchmark(Description = "Param-based: ComputeHash (selects algorithm by [Params] Variant)")] + public byte[] ParamBased_ComputeHash() + { + if (Variant == AlgorithmVariant.CustomSHA512_256) + { + using var alg = _factories[AlgorithmVariant.CustomSHA512_256](); + return alg.ComputeHash(_smallInput); // small input chosen for param-based path + } + + using var sha = _factories[AlgorithmVariant.SHA512_Truncated](); + var full = sha.ComputeHash(_smallInput); + var truncated = new byte[32]; + Array.Copy(full, 0, truncated, 0, truncated.Length); + return truncated; + } + } +} \ No newline at end of file