wip
This commit is contained in:
parent
797a1d3721
commit
84b3b5c150
12 changed files with 1073 additions and 1344 deletions
|
|
@ -1,56 +1,29 @@
|
|||
using System.Buffers;
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersion : ISpanFormattable
|
||||
{
|
||||
internal int RequiredBufferSize
|
||||
{
|
||||
get
|
||||
{
|
||||
var length = 0;
|
||||
length += ulong.CountDigits(Major) + 1;
|
||||
length += ulong.CountDigits(Minor) + 1;
|
||||
length += ulong.CountDigits(Patch);
|
||||
length += Prerelease is { Length: > 0 } ? 1 + Prerelease.Length : 0;
|
||||
length += Metadata is { Length: > 0 } ? 1 + Metadata.Length : 0;
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// <para>s - Default SemVer - [1.2.3-beta.4]</para>
|
||||
/// <para>f - Full SemVer - [1.2.3-beta.4+5]</para>
|
||||
/// <para>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</para>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
var capacity = RequiredBufferSize;
|
||||
var shared = capacity > 256 ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||
try
|
||||
if (format is not null and not "s" and not "f" and not "r")
|
||||
{
|
||||
scoped var buffer = shared.AsSpan();
|
||||
|
||||
if (shared is null)
|
||||
{
|
||||
buffer = stackalloc char[capacity];
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
_ = TryFormat(buffer, out var bytesWritten, format, formatProvider);
|
||||
|
||||
return new string(buffer[..bytesWritten]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shared is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(shared);
|
||||
}
|
||||
}
|
||||
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||
handler.AppendFormatted(this, format);
|
||||
return handler.ToStringAndClear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
@ -67,47 +40,46 @@ public readonly partial record struct SemanticVersion : ISpanFormattable
|
|||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "s";
|
||||
}
|
||||
|
||||
if (format is not "s" and not "f" and not "r")
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[charsWritten..];
|
||||
var builder = new SpanStringBuilder(destination, provider);
|
||||
builder.AppendFormatted(Major);
|
||||
builder.AppendLiteral(".");
|
||||
builder.AppendFormatted(Minor);
|
||||
builder.AppendLiteral(".");
|
||||
builder.AppendFormatted(Patch);
|
||||
|
||||
if (Prerelease is { Length: > 0 } && format is "s" or "f")
|
||||
{
|
||||
destination[0] = '-';
|
||||
|
||||
if (!Prerelease.AsSpan().TryCopyTo(destination[1..]))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[(Prerelease.Length + 1)..];
|
||||
charsWritten += Prerelease.Length + 1;
|
||||
builder.AppendLiteral("-");
|
||||
builder.AppendFormatted(Prerelease);
|
||||
}
|
||||
|
||||
if (Metadata is { Length: > 0 } && format is "f")
|
||||
{
|
||||
destination[0] = '+';
|
||||
|
||||
if (!Metadata.AsSpan().TryCopyTo(destination[1..]))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
builder.AppendLiteral("+");
|
||||
builder.AppendFormatted(Metadata);
|
||||
}
|
||||
|
||||
destination = destination[(Metadata.Length + 1)..];
|
||||
charsWritten += Metadata.Length + 1;
|
||||
}
|
||||
|
||||
_ = destination;
|
||||
|
||||
return true;
|
||||
return builder.TryComplete(out charsWritten);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
|
|
|||
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
||||
public readonly partial record struct SemanticVersion
|
||||
{
|
||||
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
||||
{
|
||||
public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Parse(value);
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new JsonException(exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("f", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -53,12 +53,12 @@ public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVe
|
|||
/// <inheritdoc />
|
||||
public static SemanticVersion Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var version))
|
||||
if (!TryParse(s, provider, out var result))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return version;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -73,60 +73,76 @@ public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVe
|
|||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||
{
|
||||
result = default;
|
||||
return TryParsePartially(s, out result, out var components) && components is 3;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
internal static bool TryParsePartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
||||
{
|
||||
version = default;
|
||||
components = 0;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var metadata = default(string);
|
||||
var plusIndex = s.IndexOf('+');
|
||||
if (plusIndex >= 0)
|
||||
if (s.IndexOf('+') is >= 0 and var metadataIndex)
|
||||
{
|
||||
metadata = s[(plusIndex + 1)..].ToString();
|
||||
s = s[..plusIndex];
|
||||
if (s[(metadataIndex + 1)..] is not { IsEmpty: false } value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
metadata = new string(value);
|
||||
s = s[..metadataIndex];
|
||||
}
|
||||
|
||||
var prerelease = default(string);
|
||||
var dashIndex = s.IndexOf('-');
|
||||
if (dashIndex >= 0)
|
||||
if (s.IndexOf('-') is >= 0 and var prereleaseIndex)
|
||||
{
|
||||
prerelease = s[(dashIndex + 1)..].ToString();
|
||||
s = s[..dashIndex];
|
||||
}
|
||||
|
||||
var firstDot = s.IndexOf('.');
|
||||
if (firstDot < 0)
|
||||
if (s[(prereleaseIndex + 1)..] is not { IsEmpty: false } value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(s[..firstDot], NumberStyles.None, CultureInfo.InvariantCulture, out var major))
|
||||
prerelease = new string(value);
|
||||
s = s[..prereleaseIndex];
|
||||
}
|
||||
|
||||
Span<Range> destination = stackalloc Range[3];
|
||||
Span<ulong> component = stackalloc ulong[3];
|
||||
|
||||
foreach (var range in destination[..s.Split(destination, '.')])
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } segment)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s[(firstDot + 1)..];
|
||||
var secondDot = s.IndexOf('.');
|
||||
if (secondDot < 0)
|
||||
if (segment is "*" or "x" or "X")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(s[..secondDot], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
|
||||
version = components switch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s[(secondDot + 1)..];
|
||||
if (!ulong.TryParse(s, NumberStyles.None, CultureInfo.InvariantCulture, out var patch))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new SemanticVersion(major, minor, patch, prerelease, metadata);
|
||||
0 => default,
|
||||
1 => new SemanticVersion(component[0], 0, 0),
|
||||
2 => new SemanticVersion(component[0], component[1], 0),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
if (!ulong.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
component[components++] = value;
|
||||
}
|
||||
|
||||
version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
||||
public readonly partial record struct SemanticVersion
|
||||
{
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Buffers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
||||
{
|
||||
private const int StackBufferThreshold = 256;
|
||||
|
||||
public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SemanticVersion.Parse(reader.GetString().AsSpan());
|
||||
}
|
||||
catch (FormatException e)
|
||||
{
|
||||
throw new JsonException(e.Message, e);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
||||
{
|
||||
var capacity = value.RequiredBufferSize;
|
||||
var shared = capacity > StackBufferThreshold ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||
try
|
||||
{
|
||||
scoped var buffer = shared.AsSpan();
|
||||
|
||||
if (shared is null)
|
||||
{
|
||||
buffer = stackalloc char[capacity];
|
||||
}
|
||||
|
||||
_ = value.TryFormat(buffer, out var bytesWritten, "f", null);
|
||||
|
||||
writer.WriteStringValue(buffer[..bytesWritten]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shared is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(shared);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,465 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Buffers;
|
||||
using System.Text.Unicode;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
private const int StackBufferThreshold = 256;
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
if (!TryGetFormat(format, out var rangeFormat))
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' was not recognized.");
|
||||
}
|
||||
|
||||
var capacity = GetRequiredBufferSize(rangeFormat);
|
||||
var shared = capacity > StackBufferThreshold ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||
try
|
||||
{
|
||||
scoped var buffer = shared.AsSpan();
|
||||
|
||||
if (shared is null)
|
||||
{
|
||||
buffer = stackalloc char[capacity];
|
||||
}
|
||||
|
||||
_ = TryFormatCore(buffer, out var charsWritten, rangeFormat);
|
||||
return new string(buffer[..charsWritten]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shared is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(shared);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryGetFormat(format, out var rangeFormat))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryFormatCore(destination, out charsWritten, rangeFormat);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool TryFormatCore(Span<char> destination, out int charsWritten, RangeStringFormat format)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (_sets is not { Length: > 0 } sets)
|
||||
{
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
|
||||
}
|
||||
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "," : " || "))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteSet(ref destination, ref charsWritten, sets[i], format))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private int GetRequiredBufferSize(RangeStringFormat format)
|
||||
{
|
||||
if (_sets is not { Length: > 0 } sets)
|
||||
{
|
||||
return format is RangeStringFormat.Maven ? 3 : 1;
|
||||
}
|
||||
|
||||
var length = 0;
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
length += format is RangeStringFormat.Maven ? 1 : 4;
|
||||
}
|
||||
|
||||
length += GetSetRequiredBufferSize(sets[i], format);
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
|
||||
private static int GetSetRequiredBufferSize(VersionConstraint[] set, RangeStringFormat format)
|
||||
{
|
||||
AnalyzeSet(set, out var effectiveCount, out var hasExact, out var exact, out var hasLower, out var lower, out var hasUpper, out var upper, out var hasUnsupported);
|
||||
|
||||
if (hasUnsupported)
|
||||
{
|
||||
return GetConstraintsRequiredBufferSize(set, false);
|
||||
}
|
||||
|
||||
if (effectiveCount is 0)
|
||||
{
|
||||
return format is RangeStringFormat.Maven ? 3 : 1;
|
||||
}
|
||||
|
||||
if (hasExact && effectiveCount is 1)
|
||||
{
|
||||
return exact.Version.RequiredBufferSize + (format is RangeStringFormat.Maven ? 2 : 0);
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.NpmShort && hasLower && hasUpper && effectiveCount is 2 && TryGetShortNpmOperator(lower, upper, out _))
|
||||
{
|
||||
return lower.Version.RequiredBufferSize + 1;
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.Maven)
|
||||
{
|
||||
return (hasLower ? lower.Version.RequiredBufferSize : 0) +
|
||||
(hasUpper ? upper.Version.RequiredBufferSize : 0) +
|
||||
3;
|
||||
}
|
||||
|
||||
return GetConstraintsRequiredBufferSize(set, true);
|
||||
}
|
||||
|
||||
private static int GetConstraintsRequiredBufferSize(VersionConstraint[] set, bool bareEquals)
|
||||
{
|
||||
var length = 0;
|
||||
var written = 0;
|
||||
foreach (var constraint in set)
|
||||
{
|
||||
if (constraint.Comparator is Comparator.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (written > 0)
|
||||
{
|
||||
length++;
|
||||
}
|
||||
|
||||
length += GetConstraintRequiredBufferSize(constraint, bareEquals);
|
||||
written++;
|
||||
}
|
||||
|
||||
return written is 0 ? 1 : length;
|
||||
}
|
||||
|
||||
private static int GetConstraintRequiredBufferSize(VersionConstraint constraint, bool bareEquals)
|
||||
{
|
||||
return constraint.Comparator switch
|
||||
{
|
||||
Comparator.Equal when bareEquals => constraint.Version.RequiredBufferSize,
|
||||
Comparator.Equal => constraint.Version.RequiredBufferSize + 1,
|
||||
Comparator.GreaterThan => constraint.Version.RequiredBufferSize + 1,
|
||||
Comparator.GreaterThanOrEqual => constraint.Version.RequiredBufferSize + 2,
|
||||
Comparator.LessThan => constraint.Version.RequiredBufferSize + 1,
|
||||
Comparator.LessThanOrEqual => constraint.Version.RequiredBufferSize + 2,
|
||||
Comparator.NotEqual => constraint.Version.RequiredBufferSize + 2,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
private static bool TryWriteSet(ref Span<char> destination, ref int charsWritten, VersionConstraint[] set, RangeStringFormat format)
|
||||
{
|
||||
AnalyzeSet(set, out var effectiveCount, out var hasExact, out var exact, out var hasLower, out var lower, out var hasUpper, out var upper, out var hasUnsupported);
|
||||
|
||||
if (hasUnsupported)
|
||||
{
|
||||
return TryWriteConstraints(ref destination, ref charsWritten, set, false);
|
||||
}
|
||||
|
||||
if (effectiveCount is 0)
|
||||
{
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
|
||||
}
|
||||
|
||||
if (hasExact && effectiveCount is 1)
|
||||
{
|
||||
if (format is RangeStringFormat.Maven && !TryWriteLiteral(ref destination, ref charsWritten, "["))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteVersion(ref destination, ref charsWritten, exact.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return format is not RangeStringFormat.Maven || TryWriteLiteral(ref destination, ref charsWritten, "]");
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.NpmShort && hasLower && hasUpper && effectiveCount is 2 && TryGetShortNpmOperator(lower, upper, out var shortOperator))
|
||||
{
|
||||
return TryWriteChar(ref destination, ref charsWritten, shortOperator) &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, lower.Version);
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.Maven)
|
||||
{
|
||||
return TryWriteMavenSet(ref destination, ref charsWritten, hasLower, lower, hasUpper, upper);
|
||||
}
|
||||
|
||||
return TryWriteConstraints(ref destination, ref charsWritten, set, true);
|
||||
}
|
||||
|
||||
private static bool TryWriteMavenSet(ref Span<char> destination, ref int charsWritten, bool hasLower, VersionConstraint lower, bool hasUpper, VersionConstraint upper)
|
||||
{
|
||||
if (!TryWriteChar(ref destination, ref charsWritten, hasLower && lower.Comparator is Comparator.GreaterThanOrEqual ? '[' : '('))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasLower && !TryWriteVersion(ref destination, ref charsWritten, lower.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteChar(ref destination, ref charsWritten, ','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasUpper && !TryWriteVersion(ref destination, ref charsWritten, upper.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryWriteChar(ref destination, ref charsWritten, hasUpper && upper.Comparator is Comparator.LessThanOrEqual ? ']' : ')');
|
||||
}
|
||||
|
||||
private static bool TryWriteConstraints(ref Span<char> destination, ref int charsWritten, VersionConstraint[] set, bool bareEquals)
|
||||
{
|
||||
var written = 0;
|
||||
foreach (var constraint in set)
|
||||
{
|
||||
if (constraint.Comparator is Comparator.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (written > 0 && !TryWriteChar(ref destination, ref charsWritten, ' '))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteConstraint(ref destination, ref charsWritten, constraint, bareEquals))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
return written > 0 || TryWriteLiteral(ref destination, ref charsWritten, "*");
|
||||
}
|
||||
|
||||
private static bool TryWriteConstraint(ref Span<char> destination, ref int charsWritten, VersionConstraint constraint, bool bareEquals)
|
||||
{
|
||||
switch (constraint.Comparator)
|
||||
{
|
||||
case Comparator.Equal when bareEquals:
|
||||
return TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.Equal:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.GreaterThan:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, ">") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.GreaterThanOrEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, ">=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.LessThan:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "<") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.LessThanOrEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "<=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.NotEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "!=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetShortNpmOperator(VersionConstraint lower, VersionConstraint upper, out char shortOperator)
|
||||
{
|
||||
if (lower.Comparator is Comparator.GreaterThanOrEqual && upper.Comparator is Comparator.LessThan)
|
||||
{
|
||||
if (upper.Version == GetCaretUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '^';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (upper.Version == GetTildeUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '~';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
shortOperator = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SemanticVersion GetCaretUpperBound(SemanticVersion version)
|
||||
{
|
||||
return version.Major > 0
|
||||
? new SemanticVersion(version.Major + 1, 0, 0)
|
||||
: version.Minor > 0
|
||||
? new SemanticVersion(0, version.Minor + 1, 0)
|
||||
: new SemanticVersion(0, 0, version.Patch + 1);
|
||||
}
|
||||
|
||||
private static SemanticVersion GetTildeUpperBound(SemanticVersion version)
|
||||
{
|
||||
return new SemanticVersion(version.Major, version.Minor + 1, 0);
|
||||
}
|
||||
|
||||
private static void AnalyzeSet(
|
||||
VersionConstraint[] set,
|
||||
out int effectiveCount,
|
||||
out bool hasExact,
|
||||
out VersionConstraint exact,
|
||||
out bool hasLower,
|
||||
out VersionConstraint lower,
|
||||
out bool hasUpper,
|
||||
out VersionConstraint upper,
|
||||
out bool hasUnsupported)
|
||||
{
|
||||
effectiveCount = 0;
|
||||
hasExact = false;
|
||||
exact = default;
|
||||
hasLower = false;
|
||||
lower = default;
|
||||
hasUpper = false;
|
||||
upper = default;
|
||||
hasUnsupported = false;
|
||||
|
||||
foreach (var constraint in set)
|
||||
{
|
||||
if (constraint.Comparator is Comparator.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
effectiveCount++;
|
||||
switch (constraint.Comparator)
|
||||
{
|
||||
case Comparator.Equal:
|
||||
hasExact = true;
|
||||
exact = constraint;
|
||||
break;
|
||||
case Comparator.GreaterThan:
|
||||
case Comparator.GreaterThanOrEqual:
|
||||
hasLower = true;
|
||||
lower = constraint;
|
||||
break;
|
||||
case Comparator.LessThan:
|
||||
case Comparator.LessThanOrEqual:
|
||||
hasUpper = true;
|
||||
upper = constraint;
|
||||
break;
|
||||
default:
|
||||
hasUnsupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryWriteVersion(ref Span<char> destination, ref int charsWritten, SemanticVersion version)
|
||||
{
|
||||
if (!version.TryFormat(destination, out var written, "f", null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteChar(ref Span<char> destination, ref int charsWritten, char value)
|
||||
{
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination[0] = value;
|
||||
destination = destination[1..];
|
||||
charsWritten++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteLiteral(ref Span<char> destination, ref int charsWritten, ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!value.TryCopyTo(destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[value.Length..];
|
||||
charsWritten += value.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetFormat(string? format, out RangeStringFormat rangeFormat)
|
||||
{
|
||||
return TryGetFormat(format.AsSpan(), out rangeFormat);
|
||||
}
|
||||
|
||||
private static bool TryGetFormat(ReadOnlySpan<char> format, out RangeStringFormat rangeFormat)
|
||||
{
|
||||
if (format.IsEmpty || format is "ns")
|
||||
{
|
||||
rangeFormat = RangeStringFormat.NpmShort;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (format is "m")
|
||||
{
|
||||
rangeFormat = RangeStringFormat.Maven;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (format is "n")
|
||||
{
|
||||
rangeFormat = RangeStringFormat.Npm;
|
||||
return true;
|
||||
}
|
||||
|
||||
rangeFormat = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal enum RangeStringFormat
|
||||
{
|
||||
Maven,
|
||||
Npm,
|
||||
NpmShort
|
||||
}
|
||||
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||
{
|
||||
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Parse(value);
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new JsonException(exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,502 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(string s)
|
||||
{
|
||||
return Parse(s.AsSpan(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s)
|
||||
{
|
||||
return Parse(s, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var range))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return range;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryParseJava(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
var alternatives = new List<VersionConstraint[]>();
|
||||
var start = 0;
|
||||
var bracketLevel = 0;
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
var c = s[i];
|
||||
switch (c)
|
||||
{
|
||||
case '[' or '(':
|
||||
bracketLevel++;
|
||||
break;
|
||||
|
||||
case ']' or ')':
|
||||
if (--bracketLevel < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
case ',' when bracketLevel == 0:
|
||||
if (!TryParseSingleMavenRange(s[start..i].Trim(), out var constraints))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
alternatives.Add(constraints);
|
||||
start = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bracketLevel is not 0 || !TryParseSingleMavenRange(s[start..].Trim(), out var lastConstraints))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
alternatives.Add(lastConstraints);
|
||||
result = new SemanticVersionRange([.. alternatives]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseSingleMavenRange(ReadOnlySpan<char> s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
|
||||
{
|
||||
constraints = null;
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s[0] is not '[' and not '(')
|
||||
{
|
||||
if (!TryParseVersionPartially(s, out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints = [new VersionConstraint(Comparator.GreaterThanOrEqual, version)];
|
||||
return true;
|
||||
}
|
||||
|
||||
var last = s.Length - 1;
|
||||
if (s[last] is not ']' and not ')')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var inclusiveStart = s[0] is '[';
|
||||
var inclusiveEnd = s[last] is ']';
|
||||
var inner = s[1..last];
|
||||
var commaIndex = inner.IndexOf(',');
|
||||
|
||||
if (commaIndex < 0)
|
||||
{
|
||||
if (!inclusiveStart || !inclusiveEnd || !TryParseVersionPartially(inner.Trim(), out var exactVersion, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints = [new VersionConstraint(Comparator.Equal, exactVersion)];
|
||||
return true;
|
||||
}
|
||||
|
||||
var lowerText = inner[..commaIndex].Trim();
|
||||
var upperText = inner[(commaIndex + 1)..].Trim();
|
||||
var list = new List<VersionConstraint>(2);
|
||||
|
||||
if (!lowerText.IsEmpty)
|
||||
{
|
||||
if (!TryParseVersionPartially(lowerText, out var lowerVersion, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
list.Add(new VersionConstraint(inclusiveStart ? Comparator.GreaterThanOrEqual : Comparator.GreaterThan, lowerVersion));
|
||||
}
|
||||
|
||||
if (!upperText.IsEmpty)
|
||||
{
|
||||
if (!TryParseVersionPartially(upperText, out var upperVersion, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
list.Add(new VersionConstraint(inclusiveEnd ? Comparator.LessThanOrEqual : Comparator.LessThan, upperVersion));
|
||||
}
|
||||
|
||||
constraints = [.. list];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNode(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var alternatives = new List<VersionConstraint[]>();
|
||||
var start = 0;
|
||||
|
||||
while (start < s.Length)
|
||||
{
|
||||
var separator = s[start..].IndexOf("||".AsSpan());
|
||||
var end = separator < 0 ? s.Length : start + separator;
|
||||
var part = s[start..end].Trim();
|
||||
|
||||
if (!part.IsEmpty)
|
||||
{
|
||||
if (!TryParseNpmSet(part, out var constraints))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
alternatives.Add(constraints);
|
||||
}
|
||||
|
||||
if (separator < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
start = end + 2;
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. alternatives]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNpmSet(ReadOnlySpan<char> s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
|
||||
{
|
||||
constraints = null;
|
||||
var hyphenIndex = s.IndexOf(" - ".AsSpan());
|
||||
if (hyphenIndex > 0)
|
||||
{
|
||||
var lowerText = s[..hyphenIndex].Trim();
|
||||
var upperText = s[(hyphenIndex + 3)..].Trim();
|
||||
if (!SemanticVersion.TryParse(lowerText, out var lowerVersion) || !SemanticVersion.TryParse(upperText, out var upperVersion))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints =
|
||||
[
|
||||
new VersionConstraint(Comparator.GreaterThanOrEqual, lowerVersion),
|
||||
new VersionConstraint(Comparator.LessThanOrEqual, upperVersion)
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
var list = new List<VersionConstraint>(2);
|
||||
var current = s;
|
||||
while (TryReadToken(ref current, out var token))
|
||||
{
|
||||
if (IsOperatorOnly(token))
|
||||
{
|
||||
if (!TryReadToken(ref current, out var valueToken) || !TryParseNpmConstraint(token, valueToken, list))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseNpmConstraint(default, token, list))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
constraints = [.. list];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadToken(ref ReadOnlySpan<char> s, out ReadOnlySpan<char> token)
|
||||
{
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
token = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var separator = s.IndexOf(' ');
|
||||
if (separator < 0)
|
||||
{
|
||||
token = s;
|
||||
s = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
token = s[..separator];
|
||||
s = s[separator..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsOperatorOnly(ReadOnlySpan<char> s)
|
||||
{
|
||||
return s is ">=" or "<=" or ">" or "<" or "~" or "^" or "=";
|
||||
}
|
||||
|
||||
private static bool TryParseNpmConstraint(ReadOnlySpan<char> op, ReadOnlySpan<char> valueText, List<VersionConstraint> constraints)
|
||||
{
|
||||
if (op.IsEmpty)
|
||||
{
|
||||
if (valueText.StartsWith(">="))
|
||||
{
|
||||
op = ">=";
|
||||
valueText = valueText[2..];
|
||||
}
|
||||
else if (valueText.StartsWith("<="))
|
||||
{
|
||||
op = "<=";
|
||||
valueText = valueText[2..];
|
||||
}
|
||||
else if (valueText.StartsWith(">"))
|
||||
{
|
||||
op = ">";
|
||||
valueText = valueText[1..];
|
||||
}
|
||||
else if (valueText.StartsWith("<"))
|
||||
{
|
||||
op = "<";
|
||||
valueText = valueText[1..];
|
||||
}
|
||||
else if (valueText.StartsWith("^"))
|
||||
{
|
||||
op = "^";
|
||||
valueText = valueText[1..];
|
||||
}
|
||||
else if (valueText.StartsWith("~"))
|
||||
{
|
||||
op = "~";
|
||||
valueText = valueText[1..];
|
||||
}
|
||||
else if (valueText.StartsWith("="))
|
||||
{
|
||||
op = "=";
|
||||
valueText = valueText[1..];
|
||||
}
|
||||
}
|
||||
|
||||
valueText = valueText.Trim();
|
||||
if (IsWildcard(valueText))
|
||||
{
|
||||
constraints.Add(new VersionConstraint(Comparator.Any, default));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryParseVersionPartially(valueText, out var version, out var components))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case "^":
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThan, GetCaretUpperBound(version)));
|
||||
return true;
|
||||
case "~":
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThan, components >= 2 ? GetTildeUpperBound(version) : new SemanticVersion(version.Major + 1, 0, 0)));
|
||||
return true;
|
||||
case ">":
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThan, version));
|
||||
return true;
|
||||
case ">=":
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
|
||||
return true;
|
||||
case "<":
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThan, version));
|
||||
return true;
|
||||
case "<=":
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThanOrEqual, version));
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (components)
|
||||
{
|
||||
case 3:
|
||||
constraints.Add(new VersionConstraint(Comparator.Equal, version));
|
||||
return true;
|
||||
case 2:
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(version.Major, version.Minor + 1, 0)));
|
||||
return true;
|
||||
case 1:
|
||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, version));
|
||||
constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(version.Major + 1, 0, 0)));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseVersionPartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
||||
{
|
||||
version = default;
|
||||
components = 0;
|
||||
s = s.Trim();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var core = s;
|
||||
ReadOnlySpan<char> prerelease = default;
|
||||
ReadOnlySpan<char> metadata = default;
|
||||
var prereleaseIndex = s.IndexOf('-');
|
||||
var metadataIndex = s.IndexOf('+');
|
||||
|
||||
if (metadataIndex >= 0)
|
||||
{
|
||||
core = s[..metadataIndex];
|
||||
metadata = s[(metadataIndex + 1)..];
|
||||
}
|
||||
|
||||
if (prereleaseIndex >= 0)
|
||||
{
|
||||
if (metadataIndex >= 0 && prereleaseIndex > metadataIndex)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
core = s[..prereleaseIndex];
|
||||
var prereleaseEnd = metadataIndex >= 0 ? metadataIndex : s.Length;
|
||||
prerelease = s[(prereleaseIndex + 1)..prereleaseEnd];
|
||||
}
|
||||
|
||||
ulong major = 0;
|
||||
ulong minor = 0;
|
||||
ulong patch = 0;
|
||||
var segmentStart = 0;
|
||||
|
||||
while (segmentStart <= core.Length)
|
||||
{
|
||||
var nextSeparator = core[segmentStart..].IndexOf('.');
|
||||
var segmentEnd = nextSeparator < 0 ? core.Length : segmentStart + nextSeparator;
|
||||
var segment = core[segmentStart..segmentEnd];
|
||||
if (segment.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsWildcard(segment))
|
||||
{
|
||||
version = components switch
|
||||
{
|
||||
1 => new SemanticVersion(major, 0, 0),
|
||||
2 => new SemanticVersion(major, minor, 0),
|
||||
_ => default
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(segment, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (components)
|
||||
{
|
||||
case 0:
|
||||
major = value;
|
||||
break;
|
||||
case 1:
|
||||
minor = value;
|
||||
break;
|
||||
case 2:
|
||||
patch = value;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
components++;
|
||||
if (nextSeparator < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
segmentStart = segmentEnd + 1;
|
||||
}
|
||||
|
||||
version = new SemanticVersion(major, minor, patch, prerelease.IsEmpty ? null : prerelease.ToString(), metadata.IsEmpty ? null : metadata.ToString());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsWildcard(ReadOnlySpan<char> s)
|
||||
{
|
||||
return s is "*" or "x" or "X";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +1,29 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a semantic version range.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
private readonly VersionConstraint[][]? _sets;
|
||||
private readonly ImmutableArray<ImmutableArray<Constraint>> _constraints;
|
||||
|
||||
private SemanticVersionRange(VersionConstraint[][] sets)
|
||||
private SemanticVersionRange(ImmutableArray<ImmutableArray<Constraint>> constraints)
|
||||
{
|
||||
_sets = sets;
|
||||
_constraints = constraints;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified version satisfies the range.
|
||||
/// </summary>
|
||||
/// <param name="version">The version to check.</param>
|
||||
/// <returns><c>true</c> if the version satisfies the range; otherwise, <c>false</c>.</returns>
|
||||
public bool Satisfies(SemanticVersion version)
|
||||
{
|
||||
if (_sets?.Length is null or 0)
|
||||
if (_constraints.Length is 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var set in _sets)
|
||||
foreach (var set in _constraints)
|
||||
{
|
||||
if (version.Prerelease is not null)
|
||||
{
|
||||
|
|
@ -43,7 +36,17 @@ public readonly partial record struct SemanticVersionRange
|
|||
}
|
||||
}
|
||||
|
||||
if (set.All(constraint => constraint.Satisfies(version)))
|
||||
if (set.All(constraint => constraint.Comparator switch
|
||||
{
|
||||
Comparator.Any => true,
|
||||
Comparator.Equal => version.CompareTo(constraint.Version) is 0,
|
||||
Comparator.NotEqual => version.CompareTo(constraint.Version) is not 0,
|
||||
Comparator.Greater => version.CompareTo(constraint.Version) > 0,
|
||||
Comparator.GreaterOrEqual => version.CompareTo(constraint.Version) >= 0,
|
||||
Comparator.Less => version.CompareTo(constraint.Version) < 0,
|
||||
Comparator.LessOrEqual => version.CompareTo(constraint.Version) <= 0,
|
||||
_ => false,
|
||||
}))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -58,43 +61,818 @@ public readonly partial record struct SemanticVersionRange
|
|||
return ToString(null, null);
|
||||
}
|
||||
|
||||
internal int RequiredBufferSize => GetRequiredBufferSize(RangeStringFormat.NpmShort);
|
||||
|
||||
internal readonly record struct VersionConstraint
|
||||
private readonly struct Constraint
|
||||
{
|
||||
public VersionConstraint(Comparator comparator, SemanticVersion version)
|
||||
public Constraint(Comparator comparator, SemanticVersion version)
|
||||
{
|
||||
Comparator = comparator;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public Comparator Comparator { get; }
|
||||
public SemanticVersion Version { get; }
|
||||
|
||||
public bool Satisfies(SemanticVersion version)
|
||||
public SemanticVersion Version { get; }
|
||||
}
|
||||
|
||||
private enum Comparator
|
||||
{
|
||||
return Comparator switch
|
||||
Any,
|
||||
Equal,
|
||||
Greater,
|
||||
GreaterOrEqual,
|
||||
Less,
|
||||
LessOrEqual,
|
||||
NotEqual,
|
||||
}
|
||||
}
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
Comparator.Any => true,
|
||||
Comparator.Equal => version.CompareTo(Version) is 0,
|
||||
Comparator.NotEqual => version.CompareTo(Version) is not 0,
|
||||
Comparator.GreaterThan => version.CompareTo(Version) > 0,
|
||||
Comparator.GreaterThanOrEqual => version.CompareTo(Version) >= 0,
|
||||
Comparator.LessThan => version.CompareTo(Version) < 0,
|
||||
Comparator.LessThanOrEqual => version.CompareTo(Version) <= 0,
|
||||
_ => false,
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||
handler.AppendFormatted(this, format);
|
||||
return handler.ToStringAndClear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanFormattable
|
||||
|
||||
/// <summary>
|
||||
/// Tries to format the semantic version range into the specified span of characters.
|
||||
/// </summary>
|
||||
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||
{
|
||||
return TryFormat(destination, out charsWritten, default, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
return TryFormatCore(destination, out charsWritten, format);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private bool TryFormatCore(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (_constraints is not { Length: > 0 } sets)
|
||||
{
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
|
||||
}
|
||||
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "," : " || "))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteSet(ref destination, ref charsWritten, sets[i], format))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteSet(ref Span<char> destination, ref int charsWritten, ImmutableArray<Constraint> set, RangeStringFormat format)
|
||||
{
|
||||
AnalyzeSet(set, out var effectiveCount, out var hasExact, out var exact, out var hasLower, out var lower, out var hasUpper, out var upper, out var hasUnsupported);
|
||||
|
||||
if (hasUnsupported)
|
||||
{
|
||||
return TryWriteConstraints(ref destination, ref charsWritten, set, false);
|
||||
}
|
||||
|
||||
if (effectiveCount is 0)
|
||||
{
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, format is RangeStringFormat.Maven ? "(,)" : "*");
|
||||
}
|
||||
|
||||
if (hasExact && effectiveCount is 1)
|
||||
{
|
||||
if (format is RangeStringFormat.Maven && !TryWriteLiteral(ref destination, ref charsWritten, "["))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteVersion(ref destination, ref charsWritten, exact.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return format is not RangeStringFormat.Maven || TryWriteLiteral(ref destination, ref charsWritten, "]");
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.NpmShort && hasLower && hasUpper && effectiveCount is 2 && TryGetShortNpmOperator(lower, upper, out var shortOperator))
|
||||
{
|
||||
return TryWriteChar(ref destination, ref charsWritten, shortOperator) &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, lower.Version);
|
||||
}
|
||||
|
||||
if (format is RangeStringFormat.Maven)
|
||||
{
|
||||
return TryWriteMavenSet(ref destination, ref charsWritten, hasLower, lower, hasUpper, upper);
|
||||
}
|
||||
|
||||
return TryWriteConstraints(ref destination, ref charsWritten, set, true);
|
||||
}
|
||||
|
||||
private static bool TryWriteMavenSet(ref Span<char> destination, ref int charsWritten, bool hasLower, Constraint lower, bool hasUpper, Constraint upper)
|
||||
{
|
||||
if (!TryWriteChar(ref destination, ref charsWritten, hasLower && lower.Comparator is Comparator.GreaterOrEqual ? '[' : '('))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasLower && !TryWriteVersion(ref destination, ref charsWritten, lower.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteChar(ref destination, ref charsWritten, ','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasUpper && !TryWriteVersion(ref destination, ref charsWritten, upper.Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryWriteChar(ref destination, ref charsWritten, hasUpper && upper.Comparator is Comparator.LessOrEqual ? ']' : ')');
|
||||
}
|
||||
|
||||
private static bool TryWriteConstraints(ref Span<char> destination, ref int charsWritten, ImmutableArray<Constraint> set, bool bareEquals)
|
||||
{
|
||||
var written = 0;
|
||||
foreach (var constraint in set)
|
||||
{
|
||||
if (constraint.Comparator is Comparator.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (written > 0 && !TryWriteChar(ref destination, ref charsWritten, ' '))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryWriteConstraint(ref destination, ref charsWritten, constraint, bareEquals))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
written++;
|
||||
}
|
||||
|
||||
return written > 0 || TryWriteLiteral(ref destination, ref charsWritten, "*");
|
||||
}
|
||||
|
||||
private static bool TryWriteConstraint(ref Span<char> destination, ref int charsWritten, Constraint constraint, bool bareEquals)
|
||||
{
|
||||
switch (constraint.Comparator)
|
||||
{
|
||||
case Comparator.Equal when bareEquals:
|
||||
return TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.Equal:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.Greater:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, ">") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.GreaterOrEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, ">=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.Less:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "<") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.LessOrEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "<=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
case Comparator.NotEqual:
|
||||
return TryWriteLiteral(ref destination, ref charsWritten, "!=") &&
|
||||
TryWriteVersion(ref destination, ref charsWritten, constraint.Version);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetShortNpmOperator(Constraint lower, Constraint upper, out char shortOperator)
|
||||
{
|
||||
if (lower.Comparator is Comparator.GreaterOrEqual && upper.Comparator is Comparator.Less)
|
||||
{
|
||||
if (upper.Version == GetCaretUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '^';
|
||||
return true;
|
||||
}
|
||||
|
||||
if (upper.Version == GetTildeUpperBound(lower.Version))
|
||||
{
|
||||
shortOperator = '~';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
shortOperator = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static SemanticVersion GetCaretUpperBound(SemanticVersion version)
|
||||
{
|
||||
return version.Major > 0
|
||||
? new SemanticVersion(version.Major + 1, 0, 0)
|
||||
: version.Minor > 0
|
||||
? new SemanticVersion(0, version.Minor + 1, 0)
|
||||
: new SemanticVersion(0, 0, version.Patch + 1);
|
||||
}
|
||||
|
||||
private static SemanticVersion GetTildeUpperBound(SemanticVersion version)
|
||||
{
|
||||
return new SemanticVersion(version.Major, version.Minor + 1, 0);
|
||||
}
|
||||
|
||||
private static void AnalyzeSet(
|
||||
ImmutableArray<Constraint> set,
|
||||
out int effectiveCount,
|
||||
out bool hasExact,
|
||||
out Constraint exact,
|
||||
out bool hasLower,
|
||||
out Constraint lower,
|
||||
out bool hasUpper,
|
||||
out Constraint upper,
|
||||
out bool hasUnsupported)
|
||||
{
|
||||
effectiveCount = 0;
|
||||
hasExact = false;
|
||||
exact = default;
|
||||
hasLower = false;
|
||||
lower = default;
|
||||
hasUpper = false;
|
||||
upper = default;
|
||||
hasUnsupported = false;
|
||||
|
||||
foreach (var constraint in set)
|
||||
{
|
||||
if (constraint.Comparator is Comparator.Any)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
effectiveCount++;
|
||||
switch (constraint.Comparator)
|
||||
{
|
||||
case Comparator.Equal:
|
||||
hasExact = true;
|
||||
exact = constraint;
|
||||
break;
|
||||
case Comparator.Greater:
|
||||
case Comparator.GreaterOrEqual:
|
||||
hasLower = true;
|
||||
lower = constraint;
|
||||
break;
|
||||
case Comparator.Less:
|
||||
case Comparator.LessOrEqual:
|
||||
hasUpper = true;
|
||||
upper = constraint;
|
||||
break;
|
||||
default:
|
||||
hasUnsupported = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryWriteVersion(ref Span<char> destination, ref int charsWritten, SemanticVersion version)
|
||||
{
|
||||
if (!version.TryFormat(destination, out var written, "f", null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteChar(ref Span<char> destination, ref int charsWritten, char value)
|
||||
{
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination[0] = value;
|
||||
destination = destination[1..];
|
||||
charsWritten++;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteLiteral(ref Span<char> destination, ref int charsWritten, ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!value.TryCopyTo(destination))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[value.Length..];
|
||||
charsWritten += value.Length;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(string s)
|
||||
{
|
||||
return Parse(s.AsSpan(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s)
|
||||
{
|
||||
return Parse(s, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var result))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return s[0] is '[' or '(' ? TryParseJava(s, out result) : TryParseNode(s, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryParseJava(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ImmutableArray<Constraint>>();
|
||||
|
||||
foreach (var range in new JavaSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseJavaSet(part, out var constraints))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(constraints);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([..sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseJavaSet(ReadOnlySpan<char> s, [NotNullWhen(true)] out ImmutableArray<Constraint> constraints)
|
||||
{
|
||||
constraints = [];
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s is not ['[' or '(', .., ']' or ')'])
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s, out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints = [new Constraint(Comparator.GreaterOrEqual, version)];
|
||||
return true;
|
||||
}
|
||||
|
||||
var inclusiveStart = s[0] is '[';
|
||||
var inclusiveEnd = s[^1] is ']';
|
||||
var text = s[1..^1];
|
||||
var comma = text.IndexOf(',');
|
||||
|
||||
if (comma < 0)
|
||||
{
|
||||
if (!inclusiveStart || !inclusiveEnd || !SemanticVersion.TryParsePartially(text.Trim(), out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints = [new Constraint(Comparator.Equal, version)];
|
||||
return true;
|
||||
}
|
||||
|
||||
var result = new List<Constraint>(2);
|
||||
|
||||
var lowerComparator = inclusiveStart ? Comparator.GreaterOrEqual : Comparator.Greater;
|
||||
if (!TryAddConstraint(text[..comma].Trim(), lowerComparator, result))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var upperComparator = inclusiveEnd ? Comparator.LessOrEqual : Comparator.Less;
|
||||
if (!TryAddConstraint(text[(comma + 1)..].Trim(), upperComparator, result))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints = [.. result];
|
||||
return true;
|
||||
|
||||
static bool TryAddConstraint(ReadOnlySpan<char> text, Comparator comparator, ICollection<Constraint> result)
|
||||
{
|
||||
if (text.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(text, out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result.Add(new Constraint(comparator, version));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private ref struct JavaSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public JavaSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||
{
|
||||
_span = readOnlySpan;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var depth = 0;
|
||||
var i = _currentStart;
|
||||
|
||||
while (i < _span.Length)
|
||||
{
|
||||
var ch = _span[i];
|
||||
|
||||
if (ch is '[' or '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch is ']' or ')')
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else if (ch is ',' && depth is 0)
|
||||
{
|
||||
_currentEnd = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
_currentEnd = _span.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly JavaSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNode(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
var sets = new List<ImmutableArray<Constraint>>();
|
||||
|
||||
foreach (var range in new NodeSetGrouping(s))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } part)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseNodeSet(part, out var constraints))
|
||||
{
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(constraints);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([..sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNodeSet(ReadOnlySpan<char> s, [NotNullWhen(true)] out ImmutableArray<Constraint> constraints)
|
||||
{
|
||||
if (s.IndexOf(" - ") is not -1 and var hyphen)
|
||||
{
|
||||
if (!SemanticVersion.TryParse(s[..hyphen].Trim(), out var lowerVersion))
|
||||
{
|
||||
constraints = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(s[(hyphen + 3)..].Trim(), out var upperVersion))
|
||||
{
|
||||
constraints = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
constraints =
|
||||
[
|
||||
new Constraint(Comparator.GreaterOrEqual, lowerVersion),
|
||||
new Constraint(Comparator.LessOrEqual, upperVersion),
|
||||
];
|
||||
return true;
|
||||
}
|
||||
|
||||
var list = new List<Constraint>(2);
|
||||
var current = s;
|
||||
while (TryReadToken(ref current, out var token))
|
||||
{
|
||||
scoped ReadOnlySpan<char> instruction = [];
|
||||
|
||||
if (token is ">=" or "<=" or ">" or "<" or "~" or "^" or "=")
|
||||
{
|
||||
instruction = token;
|
||||
|
||||
if (!TryReadToken(ref current, out token))
|
||||
{
|
||||
constraints = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (token.StartsWith(">="))
|
||||
{
|
||||
instruction = ">=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith("<="))
|
||||
{
|
||||
instruction = "<=";
|
||||
token = token[2..];
|
||||
}
|
||||
else if (token.StartsWith(">"))
|
||||
{
|
||||
instruction = ">";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("<"))
|
||||
{
|
||||
instruction = "<";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("^"))
|
||||
{
|
||||
instruction = "^";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("~"))
|
||||
{
|
||||
instruction = "~";
|
||||
token = token[1..];
|
||||
}
|
||||
else if (token.StartsWith("="))
|
||||
{
|
||||
instruction = "=";
|
||||
token = token[1..];
|
||||
}
|
||||
}
|
||||
|
||||
if (token is "*" or "x" or "X")
|
||||
{
|
||||
constraints = [new Constraint(Comparator.Any, default)];
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryParseNpmConstraint(instruction, token.Trim(), list))
|
||||
{
|
||||
constraints = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
constraints = [.. list];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadToken(ref ReadOnlySpan<char> s, out ReadOnlySpan<char> token)
|
||||
{
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
token = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var separator = s.IndexOf(' ');
|
||||
if (separator < 0)
|
||||
{
|
||||
token = s;
|
||||
s = default;
|
||||
return true;
|
||||
}
|
||||
|
||||
token = s[..separator];
|
||||
s = s[separator..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseNpmConstraint(ReadOnlySpan<char> op, ReadOnlySpan<char> s, ICollection<Constraint> constraints)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out var components))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case "^":
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
constraints.Add(new Constraint(Comparator.Less, GetCaretUpperBound(version, components)));
|
||||
return true;
|
||||
case "~":
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
constraints.Add(new Constraint(Comparator.Less, GetTildeUpperBound(version, components)));
|
||||
return true;
|
||||
case ">":
|
||||
constraints.Add(new Constraint(Comparator.Greater, version));
|
||||
return true;
|
||||
case ">=":
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
return true;
|
||||
case "<":
|
||||
constraints.Add(new Constraint(Comparator.Less, version));
|
||||
return true;
|
||||
case "<=":
|
||||
constraints.Add(new Constraint(Comparator.LessOrEqual, version));
|
||||
return true;
|
||||
default:
|
||||
switch (components)
|
||||
{
|
||||
case 3:
|
||||
constraints.Add(new Constraint(Comparator.Equal, version));
|
||||
return true;
|
||||
case 2:
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
constraints.Add(new Constraint(Comparator.Less, new SemanticVersion(version.Major, version.Minor + 1, 0)));
|
||||
return true;
|
||||
case 1:
|
||||
constraints.Add(new Constraint(Comparator.GreaterOrEqual, version));
|
||||
constraints.Add(new Constraint(Comparator.Less, new SemanticVersion(version.Major + 1, 0, 0)));
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static SemanticVersion GetCaretUpperBound(SemanticVersion version, int components)
|
||||
{
|
||||
return components switch
|
||||
{
|
||||
1 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
2 when version.Major is 0 => new SemanticVersion(0, version.Minor + 1, 0),
|
||||
2 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
_ when version.Major > 0 => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
_ when version.Minor > 0 => new SemanticVersion(0, version.Minor + 1, 0),
|
||||
_ => new SemanticVersion(0, 0, version.Patch + 1),
|
||||
};
|
||||
}
|
||||
|
||||
static SemanticVersion GetTildeUpperBound(SemanticVersion version, int components)
|
||||
{
|
||||
return components switch
|
||||
{
|
||||
>= 2 => new SemanticVersion(version.Major, version.Minor + 1, 0),
|
||||
_ => new SemanticVersion(version.Major + 1, 0, 0),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal enum Comparator
|
||||
private ref struct NodeSetGrouping
|
||||
{
|
||||
Any,
|
||||
Equal,
|
||||
GreaterThan,
|
||||
GreaterThanOrEqual,
|
||||
LessThan,
|
||||
LessThanOrEqual,
|
||||
NotEqual
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public NodeSetGrouping(ReadOnlySpan<char> span)
|
||||
{
|
||||
_span = span;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 2;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = _span[_currentStart..].IndexOf("||");
|
||||
_currentEnd = index >= 0 ? _currentStart + index : _span.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly NodeSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Buffers;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||
{
|
||||
private const int StackBufferThreshold = 256;
|
||||
|
||||
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return SemanticVersionRange.Parse(reader.GetString().AsSpan());
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new JsonException(exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
||||
{
|
||||
var capacity = value.RequiredBufferSize;
|
||||
var shared = capacity > StackBufferThreshold ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||
try
|
||||
{
|
||||
scoped var buffer = shared.AsSpan();
|
||||
if (shared is null)
|
||||
{
|
||||
buffer = stackalloc char[capacity];
|
||||
}
|
||||
|
||||
_ = value.TryFormat(buffer, out var bytesWritten, default, null);
|
||||
writer.WriteStringValue(buffer[..bytesWritten]);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shared is not null)
|
||||
{
|
||||
ArrayPool<char>.Shared.Return(shared);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/semver/SpanStringBuilder.cs
Normal file
82
src/semver/SpanStringBuilder.cs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal ref struct SpanStringBuilder
|
||||
{
|
||||
private Span<char> _destination;
|
||||
private readonly IFormatProvider? _provider;
|
||||
|
||||
public SpanStringBuilder(Span<char> destination, IFormatProvider? provider = null)
|
||||
{
|
||||
_destination = destination;
|
||||
_provider = provider;
|
||||
CharsWritten = 0;
|
||||
Success = true;
|
||||
}
|
||||
|
||||
public int CharsWritten { get; private set; }
|
||||
|
||||
public bool Success { get; private set; }
|
||||
|
||||
public readonly bool TryComplete(out int charsWritten)
|
||||
{
|
||||
charsWritten = Success ? CharsWritten : 0;
|
||||
return Success;
|
||||
}
|
||||
|
||||
public void AppendLiteral(string? value)
|
||||
{
|
||||
AppendLiteral(value.AsSpan());
|
||||
}
|
||||
|
||||
public void AppendLiteral(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.TryCopyTo(_destination))
|
||||
{
|
||||
Success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_destination = _destination[value.Length..];
|
||||
CharsWritten += value.Length;
|
||||
}
|
||||
|
||||
public void AppendFormatted(string? value)
|
||||
{
|
||||
AppendLiteral(value.AsSpan());
|
||||
}
|
||||
|
||||
public void AppendFormatted(ReadOnlySpan<char> value)
|
||||
{
|
||||
AppendLiteral(value);
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value) where T : ISpanFormattable
|
||||
{
|
||||
AppendFormatted(value, null);
|
||||
}
|
||||
|
||||
public void AppendFormatted<T>(T value, string? format) where T : ISpanFormattable
|
||||
{
|
||||
if (!Success)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!value.TryFormat(_destination, out var written, format, _provider))
|
||||
{
|
||||
Success = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_destination = _destination[written..];
|
||||
CharsWritten += written;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Buffers/Text/FormattingHelpers.CountDigits.cs
|
||||
internal static class FormattingHelpers
|
||||
{
|
||||
extension(ulong)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountDigits(ulong value)
|
||||
{
|
||||
// Map the log2(value) to a power of 10.
|
||||
ReadOnlySpan<byte> log2ToPow10 =
|
||||
[
|
||||
1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
|
||||
6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
|
||||
10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
|
||||
15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20
|
||||
];
|
||||
Debug.Assert(log2ToPow10.Length == 64);
|
||||
|
||||
nint elementOffset = log2ToPow10[(int)ulong.Log2(value)];
|
||||
|
||||
// Read the associated power of 10.
|
||||
ReadOnlySpan<ulong> powersOf10 =
|
||||
[
|
||||
0, // unused entry to avoid needing to subtract
|
||||
0,
|
||||
10,
|
||||
100,
|
||||
1000,
|
||||
10000,
|
||||
100000,
|
||||
1000000,
|
||||
10000000,
|
||||
100000000,
|
||||
1000000000,
|
||||
10000000000,
|
||||
100000000000,
|
||||
1000000000000,
|
||||
10000000000000,
|
||||
100000000000000,
|
||||
1000000000000000,
|
||||
10000000000000000,
|
||||
100000000000000000,
|
||||
1000000000000000000,
|
||||
10000000000000000000,
|
||||
];
|
||||
Debug.Assert((elementOffset + 1) <= powersOf10.Length);
|
||||
var powerOf10 = Unsafe.Add(ref MemoryMarshal.GetReference(powersOf10), elementOffset);
|
||||
|
||||
// Return the number of digits based on the power of 10, shifted by 1
|
||||
// if it falls below the threshold.
|
||||
var index = (int)elementOffset;
|
||||
return index - (value < powerOf10 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
extension(uint)
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static int CountDigits(uint value)
|
||||
{
|
||||
ReadOnlySpan<long> table =
|
||||
[
|
||||
4294967296,
|
||||
8589934582,
|
||||
8589934582,
|
||||
8589934582,
|
||||
12884901788,
|
||||
12884901788,
|
||||
12884901788,
|
||||
17179868184,
|
||||
17179868184,
|
||||
17179868184,
|
||||
21474826480,
|
||||
21474826480,
|
||||
21474826480,
|
||||
21474826480,
|
||||
25769703776,
|
||||
25769703776,
|
||||
25769703776,
|
||||
30063771072,
|
||||
30063771072,
|
||||
30063771072,
|
||||
34349738368,
|
||||
34349738368,
|
||||
34349738368,
|
||||
34349738368,
|
||||
38554705664,
|
||||
38554705664,
|
||||
38554705664,
|
||||
41949672960,
|
||||
41949672960,
|
||||
41949672960,
|
||||
42949672960,
|
||||
42949672960,
|
||||
];
|
||||
Debug.Assert(table.Length == 32, "Every result of uint.Log2(value) needs a long entry in the table.");
|
||||
|
||||
var tableValue = table[(int)uint.Log2(value)];
|
||||
return (int)((value + tableValue) >> 32);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue