base32 refactoring
.NET Test / .NET tests (push) Failing after 1m54s

This commit is contained in:
2026-07-10 21:53:37 +04:00
parent d11c74e5d6
commit 566c813e8d
18 changed files with 375 additions and 88 deletions
+88 -27
View File
@@ -1,28 +1,37 @@
namespace Just.Core;
public enum Base32EncodeOptions
{
None = 0x00,
LowerCase = 0x01,
NoPadding = 0x02,
LowerCaseNoPadding = 0x03,
}
public static class Base32
{
public const string Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
public const string AlphabetLower = "abcdefghijklmnopqrstuvwxyz234567";
public const char Padding = '=';
public const int MaxBytesStack = 250;
[Pure]
public static string Encode(ReadOnlySpan<byte> input)
public static string Encode(ReadOnlySpan<byte> input, Base32EncodeOptions options = Base32EncodeOptions.None)
{
if (input.IsEmpty) return string.Empty;
int outLength = 8 * ((input.Length + 4) / 5);
Span<char> output = input.Length <= MaxBytesStack
? stackalloc char[outLength]
: new char[outLength];
Span<char> output = input.Length > MaxBytesStack
? new char[outLength]
: stackalloc char[outLength];
var size = Encode(input, output);
var size = Encode(input, output, options);
return new string(output[..size]);
}
[Pure]
public static int Encode(ReadOnlySpan<byte> input, Span<char> output)
public static int Encode(ReadOnlySpan<byte> input, Span<char> output, Base32EncodeOptions options = Base32EncodeOptions.None)
{
if (input.IsEmpty) return 0;
@@ -34,25 +43,63 @@ public static class Base32
output = output[..outputLength];
int i = 0;
ReadOnlySpan<char> alphabet = Alphabet;
ReadOnlySpan<char> alphabet = (options & Base32EncodeOptions.LowerCase) == Base32EncodeOptions.LowerCase
? AlphabetLower
: Alphabet;
Span<byte> alphabetKeys = stackalloc byte[8];
int i = 0;
for (int offset = 0; offset < input.Length;)
{
alphabetKeys.Clear();
int numCharsToOutput = GetNextGroup(input, ref offset, alphabetKeys);
output[i++] = (numCharsToOutput > 0) ? alphabet[alphabetKeys[0]] : Padding;
output[i++] = (numCharsToOutput > 1) ? alphabet[alphabetKeys[1]] : Padding;
output[i++] = (numCharsToOutput > 2) ? alphabet[alphabetKeys[2]] : Padding;
output[i++] = (numCharsToOutput > 3) ? alphabet[alphabetKeys[3]] : Padding;
output[i++] = (numCharsToOutput > 4) ? alphabet[alphabetKeys[4]] : Padding;
output[i++] = (numCharsToOutput > 5) ? alphabet[alphabetKeys[5]] : Padding;
output[i++] = (numCharsToOutput > 6) ? alphabet[alphabetKeys[6]] : Padding;
output[i++] = (numCharsToOutput > 7) ? alphabet[alphabetKeys[7]] : Padding;
output[i++] = alphabet[alphabetKeys[0]];
output[i++] = alphabet[alphabetKeys[1]];
if (numCharsToOutput < 3)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[2]];
output[i++] = alphabet[alphabetKeys[3]];
if (numCharsToOutput < 5)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[4]];
if (numCharsToOutput < 6)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[5]];
output[i++] = alphabet[alphabetKeys[6]];
if (numCharsToOutput < 8)
{
i = FillWithPadding(output, i, numCharsToOutput, options);
break;
}
output[i++] = alphabet[alphabetKeys[7]];
}
return i;
static int FillWithPadding(Span<char> output, int i, int numCharsToOutput, Base32EncodeOptions options)
{
if ((options & Base32EncodeOptions.NoPadding) == Base32EncodeOptions.NoPadding)
{
return i;
}
var pad = 8 - numCharsToOutput;
output[i..(i+pad)].Fill(Padding);
return i + pad;
}
}
[Pure]
@@ -61,10 +108,10 @@ public static class Base32
input = input.TrimEnd(Padding);
if (input.IsEmpty) return [];
var outputLength = 5 * ((input.Length + 7) / 8);
Span<byte> output = outputLength <= MaxBytesStack
? stackalloc byte[outputLength]
: new byte[outputLength];
var outputLength = 5 * input.Length / 8;
Span<byte> output = outputLength > MaxBytesStack
? new byte[outputLength]
: stackalloc byte[outputLength];
var size = Decode(input, output);
@@ -76,7 +123,13 @@ public static class Base32
{
input = input.TrimEnd(Padding);
var outputLength = 5 * ((input.Length + 7) / 8);
var rem = input.Length % 8;
if (rem == 1 || rem == 3 || rem == 6)
{
throw new FormatException("Invalid Base32 string length.");
}
var outputLength = 5 * input.Length / 8;
if (output.Length < outputLength)
{
throw new ArgumentException("Decoded input can not fit in output span.", nameof(output));
@@ -85,9 +138,9 @@ public static class Base32
output = output[..outputLength];
output.Clear();
Span<char> inputspan = outputLength <= MaxBytesStack
? stackalloc char[input.Length]
: new char[input.Length];
Span<char> inputspan = outputLength > MaxBytesStack
? new char[input.Length]
: stackalloc char[input.Length];
input.ToUpperInvariant(inputspan);
int bitIndex = 0;
@@ -127,10 +180,17 @@ public static class Base32
inputIndex++;
bitIndex = 0;
}
else if (inputIndex == input.Length -1) break;
else if (inputIndex == input.Length -1)
{
if ((byteIndex & ~(0x1f << (bitPos - bits))) > 0)
{
throw new FormatException("Invalid Base32 string. Inconsistent tail bits.");
}
break;
}
}
return outputIndex + (outputBits + 7) / 8;
return outputIndex;
}
// returns the number of bytes that were output
@@ -145,7 +205,8 @@ public static class Base32
4 => 7,
_ => 8,
};
uint b1 = (offset < input.Length) ? input[offset++] : 0U;
uint b1 = input[offset++];
uint b2 = (offset < input.Length) ? input[offset++] : 0U;
uint b3 = (offset < input.Length) ? input[offset++] : 0U;
uint b4 = (offset < input.Length) ? input[offset++] : 0U;
+4 -5
View File
@@ -1,6 +1,5 @@
using System.Numerics;
using System.Runtime.InteropServices;
using Just.Core.Extensions;
namespace Just.Core.Collections;
@@ -122,14 +121,14 @@ public static class DataMap
where T : unmanaged, IComparisonOperators<T, T, bool>, IEqualityOperators<T, T, bool>
{
byte[] headBytes = new byte[HeaderSize];
await stream.PopulateAsync(headBytes, cancellationToken);
await stream.ReadExactlyAsync(headBytes, cancellationToken);
var head = MemoryMarshal.Read<Header>(headBytes);
var bodySize = DataMap<T>.ElementSize * head.Width * head.Height;
if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch.");
byte[] bodyBytes = new byte[bodySize];
await stream.PopulateAsync(bodyBytes, cancellationToken);
await stream.ReadExactlyAsync(bodyBytes, cancellationToken);
T[] body = MemoryMarshal.Cast<byte, T>(bodyBytes).ToArray();
return new DataMap<T>((int)head.Width, (int)head.Height, body);
@@ -157,7 +156,7 @@ public static class DataMap
{
Header head = default;
var headSpan = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref head, 1));
stream.Populate(headSpan);
stream.ReadExactly(headSpan);
var bodySize = DataMap<T>.ElementSize * head.Width * head.Height;
if (bodySize != head.BodySize) throw new InvalidOperationException("Can not read DataMap. Element size mismatch.");
@@ -165,7 +164,7 @@ public static class DataMap
T[] body = new T[bodySize];
var bodySpan = MemoryMarshal.AsBytes(body.AsSpan());
stream.Populate(bodySpan);
stream.ReadExactly(bodySpan);
return new DataMap<T>((int)head.Width, (int)head.Height, body);
}
+117 -1
View File
@@ -3,6 +3,22 @@ using System.Collections.Immutable;
namespace Just.Core.Collections;
/// <summary>
/// Represents an immutable, ordered sequence of items with valueequality semantics.
/// </summary>
/// <typeparam name="T">The type of elements in the sequence.</typeparam>
/// <remarks>
/// <para>
/// This class is a thin wrapper around <see cref="ImmutableList{T}"/> that implements
/// <see cref="IReadOnlyList{T}"/>, <see cref="IEquatable{T}"/>, and valuebased equality.
/// All modifications return new <see cref="ImmutableSequence{T}"/> instances, leaving the
/// original unchanged.
/// </para>
/// <para>
/// Subclasses may override <see cref="ConstructNew"/> to ensure mutation methods return
/// the correct derived type.
/// </para>
/// </remarks>
public class ImmutableSequence<T> :
IEnumerable<T>,
IReadOnlyList<T>,
@@ -11,17 +27,61 @@ public class ImmutableSequence<T> :
private static readonly Func<T?, T?, bool> CompareItem = EqualityComparer<T>.Default.Equals;
private readonly ImmutableList<T> _values;
/// <summary>
/// Initializes a new empty instance of the <see cref="ImmutableSequence{T}"/> class.
/// </summary>
public ImmutableSequence() => _values = [];
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class that
/// wraps the specified <see cref="ImmutableList{T}"/>.
/// </summary>
/// <param name="values">The immutable list to wrap.</param>
public ImmutableSequence(ImmutableList<T> values) => _values = values;
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class with
/// the elements from the provided enumerable sequence.
/// </summary>
/// <param name="values">The items to include in the sequence.</param>
public ImmutableSequence(IEnumerable<T> values) => _values = [..values];
/// <summary>
/// Initializes a new instance of the <see cref="ImmutableSequence{T}"/> class with
/// the elements from the provided readonly span.
/// </summary>
/// <param name="values">The items to include in the sequence.</param>
public ImmutableSequence(ReadOnlySpan<T> values) : this(ImmutableList.Create(values))
{
}
/// <summary>
/// Gets a value indicating whether the sequence contains any elements.
/// </summary>
public bool IsEmpty => _values.IsEmpty;
/// <summary>
/// Gets the number of elements in the sequence.
/// </summary>
public int Count => _values.Count;
/// <summary>
/// Gets the element at the specified zerobased index.
/// </summary>
/// <param name="index">The zerobased index of the element to get.</param>
/// <returns>The element at the specified index.</returns>
public T this[int index] => _values[index];
/// <summary>
/// Gets the element at the specified position from the start or end of the sequence.
/// </summary>
/// <param name="index">An <see cref="Index"/> value (e.g., <c>^1</c> for the last element).</param>
/// <returns>The element at the specified position.</returns>
public T this[Index index] => _values[index];
/// <summary>
/// Gets a new <see cref="ImmutableSequence{T}"/> containing the elements in the specified range.
/// </summary>
/// <param name="range">The range of elements to include.</param>
/// <returns>A new sequence representing the slice.</returns>
public ImmutableSequence<T> this[Range range]
{
get
@@ -31,25 +91,60 @@ public class ImmutableSequence<T> :
}
}
/// <summary>
/// Creates a new <see cref="ImmutableSequence{T}"/> from the provided immutable list.
/// Subclasses can override this to return instances of a more specific type.
/// </summary>
/// <param name="values">The immutable list that will become the internal storage.</param>
/// <returns>A new sequence containing the given items.</returns>
protected virtual ImmutableSequence<T> ConstructNew(ImmutableList<T> values) => [..values];
/// <summary>
/// Returns a new sequence with the specified value appended to the end.
/// </summary>
/// <param name="value">The value to add.</param>
/// <returns>A new sequence containing the original items followed by <paramref name="value"/>.</returns>
public ImmutableSequence<T> Add(T value) => ConstructNew(_values.Add(value));
/// <summary>
/// Returns a new sequence with the specified value inserted at the beginning.
/// </summary>
/// <param name="value">The value to add.</param>
/// <returns>A new sequence that starts with <paramref name="value"/> and then contains the original items.</returns>
public ImmutableSequence<T> AddFront(T value) => ConstructNew(_values.Insert(0, value));
/// <summary>
/// Returns an enumerator that iterates through the sequence.
/// </summary>
/// <returns>A <see cref="ImmutableList{T}.Enumerator"/> value type enumerator.</returns>
public ImmutableList<T>.Enumerator GetEnumerator() => _values.GetEnumerator();
IEnumerator<T> IEnumerable<T>.GetEnumerator() => ((IEnumerable<T>)_values).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_values).GetEnumerator();
public override string ToString() => string.Join(Environment.NewLine, _values);
/// <summary>
/// Determines whether this sequence is equal to another <see cref="ImmutableSequence{T}"/>.
/// Equality is based on the number of elements and the elementwise equality comparison
/// using the default equality comparer for <typeparamref name="T"/>.
/// </summary>
/// <param name="other">The sequence to compare with this instance. Can be <c>null</c>.</param>
/// <returns>
/// <c>true</c> if the sequences have the same length and all elements are equal;
/// <c>false</c> otherwise.
/// </returns>
public virtual bool Equals([NotNullWhen(true)] ImmutableSequence<T>? other)
{
if (other is null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (_values.Count != other?._values.Count)
if (_values.Count != other._values.Count)
{
return false;
}
@@ -65,7 +160,16 @@ public class ImmutableSequence<T> :
return true;
}
/// <summary>
/// Determines whether the specified object is equal to the current sequence.
/// </summary>
/// <param name="obj">The object to compare with the current sequence.</param>
/// <returns><c>true</c> if <paramref name="obj"/> is an <see cref="ImmutableSequence{T}"/> and equals this instance; otherwise <c>false</c>.</returns>
public override bool Equals([NotNullWhen(true)] object? obj) => Equals(obj as ImmutableSequence<T>);
/// <summary>
/// Serves as a hash function for the sequence.
/// </summary>
/// <returns>A hash code that incorporates all elements in order.</returns>
public override int GetHashCode()
{
HashCode hash = new();
@@ -78,6 +182,18 @@ public class ImmutableSequence<T> :
return hash.ToHashCode();
}
/// <summary>
/// Determines whether two sequences are equal.
/// </summary>
/// <param name="left">The first sequence to compare.</param>
/// <param name="right">The second sequence to compare.</param>
/// <returns><c>true</c> if both sequences are <c>null</c> or they are considered equal; otherwise <c>false</c>.</returns>
public static bool operator ==(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => left is null ? right is null : left.Equals(right);
/// <summary>
/// Determines whether two sequences are not equal.
/// </summary>
/// <param name="left">The first sequence to compare.</param>
/// <param name="right">The second sequence to compare.</param>
/// <returns><c>true</c> if the sequences are not equal; otherwise <c>false</c>.</returns>
public static bool operator !=(ImmutableSequence<T>? left, ImmutableSequence<T>? right) => !(left == right);
}
+2 -2
View File
@@ -68,8 +68,8 @@ public class Map<T> : IMap<T>
{
get
{
x = Math.Clamp(x, 0, Width);
y = Math.Clamp(y, 0, Height);
x = Math.Clamp(x, 0, Width - 1);
y = Math.Clamp(y, 0, Height - 1);
return ref _values[(y * Width) + x];
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
<Description>Small .Net library with useful helper classes, functions and extensions.</Description>
<PackageTags>extensions;helpers;helper-functions</PackageTags>
<Authors>JustFixMe</Authors>
<Copyright>Copyright (c) 2023-2025 JustFixMe</Copyright>
<Copyright>Copyright (c) 2023-2026 JustFixMe</Copyright>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/JustFixMe/Just.Core/</RepositoryUrl>
@@ -3,6 +3,7 @@ namespace Just.Core.Extensions;
/// <summary>
/// Provides extension methods for <see cref="Stream"/> to fully populate buffers.
/// </summary>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static class SystemIOStreamExtensions
{
/// <summary>
@@ -16,6 +17,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="offset"/> or <paramref name="length"/> is invalid</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, byte[] buffer, int offset, int length)
=> stream.Populate(buffer.AsSpan(offset, length));
/// <summary>
@@ -26,6 +28,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, byte[] buffer)
=> stream.Populate(buffer.AsSpan());
/// <summary>
@@ -36,6 +39,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="ArgumentNullException">Thrown when <paramref name="stream"/> is null</exception>
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static void Populate(this Stream stream, Span<byte> buffer)
{
ArgumentNullException.ThrowIfNull(stream);
@@ -64,6 +68,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(), cancellationToken);
/// <summary>
@@ -80,6 +85,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static ValueTask PopulateAsync(this Stream stream, byte[] buffer, int offset, int length, CancellationToken cancellationToken = default)
=> stream.PopulateAsync(buffer.AsMemory(offset, length), cancellationToken);
/// <summary>
@@ -93,6 +99,7 @@ public static class SystemIOStreamExtensions
/// <exception cref="EndOfStreamException">Thrown if the stream ends before filling the buffer</exception>
/// <exception cref="OperationCanceledException">Thrown if canceled via cancellation token</exception>
/// <exception cref="IOException">Thrown for I/O errors during reading</exception>
[Obsolete("Stream.ReadExactly and Stream.ReadExactlyAsync have the same functionality. Will be removed in the next version.")]
public static async ValueTask PopulateAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
+1
View File
@@ -7,6 +7,7 @@ public static class GuidV8
private const long TicksPrecision = TimeSpan.TicksPerMillisecond / 10;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
[ExcludeFromCodeCoverage]
public static Guid NewGuid(RngEntropy entropy = RngEntropy.Strong) => NewGuid(DateTime.UtcNow, entropy);
public static Guid NewGuid(DateTime dateTime, RngEntropy entropy = RngEntropy.Strong)
+6 -3
View File
@@ -51,7 +51,7 @@ public sealed class SeqId(DateTime epoch)
/// Thrown if more than 255 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(DateTime.UtcNow, entropy);
public static long NextId(RngEntropy entropy = RngEntropy.Strong) => Default.Next(entropy);
#if NET9_0_OR_GREATER
private readonly Lock _lock = new();
@@ -62,6 +62,7 @@ public sealed class SeqId(DateTime epoch)
private readonly DateTime _epoch = epoch;
private int _seqId = 0;
private long _lastTimestamp = -1L;
private Func<DateTime> _defaultTimeFactory = static () => DateTime.UtcNow;
/// <summary>
/// Generates next ID using current UTC time
@@ -72,7 +73,7 @@ public sealed class SeqId(DateTime epoch)
/// Thrown if more than 255 IDs generated in 1ms
/// </exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(DateTime.UtcNow, entropy);
public long Next(RngEntropy entropy = RngEntropy.Strong) => Next(_defaultTimeFactory(), entropy);
/// <summary>
/// Generates next ID with explicit timestamp
@@ -104,7 +105,7 @@ public sealed class SeqId(DateTime epoch)
throw new InvalidOperationException("Refused to create new SeqId. Last timestamp is in the future.");
}
if (_seqId == SeqMask)
if (_seqId > SeqMask)
{
throw new IndexOutOfRangeException("Refused to create new SeqId. Sequence exhausted.");
}
@@ -118,4 +119,6 @@ public sealed class SeqId(DateTime epoch)
return timestamp | currentSeq | currentRand;
}
internal void UnsafeReplaceDefaultTimeFactory(Func<DateTime> timeFactory) => _defaultTimeFactory = timeFactory;
}