wip
This commit is contained in:
parent
eaca525ec2
commit
797a1d3721
14 changed files with 1377 additions and 987 deletions
|
|
@ -34,6 +34,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
dotnet pack -p:ContinuousIntegrationBuild=true
|
dotnet pack -p:ContinuousIntegrationBuild=true
|
||||||
|
|
||||||
|
- name: dotnet format --verify-no-changes
|
||||||
|
run: |
|
||||||
|
dotnet format --no-restore --verify-no-changes --verbosity normal
|
||||||
|
|
||||||
- name: dotnet test
|
- name: dotnet test
|
||||||
run: |
|
run: |
|
||||||
dotnet test -p:ContinuousIntegrationBuild=true
|
dotnet test -p:ContinuousIntegrationBuild=true
|
||||||
|
|
@ -25,6 +25,10 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
dotnet pack -p:ContinuousIntegrationBuild=true
|
dotnet pack -p:ContinuousIntegrationBuild=true
|
||||||
|
|
||||||
|
- name: dotnet format --verify-no-changes
|
||||||
|
run: |
|
||||||
|
dotnet format --no-restore --verify-no-changes --verbosity normal
|
||||||
|
|
||||||
- name: dotnet test
|
- name: dotnet test
|
||||||
run: |
|
run: |
|
||||||
dotnet test -p:ContinuousIntegrationBuild=true
|
dotnet test -p:ContinuousIntegrationBuild=true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace Geekeey.SemVer.Tests;
|
namespace Geekeey.SemVer.Tests;
|
||||||
|
|
||||||
internal sealed class SemanticVersionRangeTests
|
internal sealed class SemanticVersionRangeTests
|
||||||
|
|
@ -152,7 +154,7 @@ internal sealed class SemanticVersionRangeTests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_can_serialize_to_json()
|
public async Task I_can_serialize_to_json()
|
||||||
{
|
{
|
||||||
var r = SemanticVersionRange.Parse("^1.2.3");
|
var r = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
var json = System.Text.Json.JsonSerializer.Serialize(r);
|
var json = System.Text.Json.JsonSerializer.Serialize(r);
|
||||||
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
|
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
|
||||||
}
|
}
|
||||||
|
|
@ -165,4 +167,37 @@ internal sealed class SemanticVersionRangeTests
|
||||||
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
|
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
|
||||||
await Assert.That(r.Satisfies(new SemanticVersion(1, 2, 4))).IsTrue();
|
await Assert.That(r.Satisfies(new SemanticVersion(1, 2, 4))).IsTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_use_npm_short_format_by_default()
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
await Assert.That(value.ToString()).IsEqualTo("^1.2.3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
[Arguments("^1.2.3", "m", "[1.2.3,2.0.0)")]
|
||||||
|
[Arguments("~1.2.3", "m", "[1.2.3,1.3.0)")]
|
||||||
|
[Arguments("1.2.3", "m", "[1.2.3]")]
|
||||||
|
[Arguments("[1.2.3,2.0.0)", "n", ">=1.2.3 <2.0.0")]
|
||||||
|
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
|
||||||
|
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
|
||||||
|
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
|
||||||
|
[Arguments("*", "m", "(,)")]
|
||||||
|
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse(range);
|
||||||
|
await Assert.That(value.ToString(format, null)).IsEqualTo(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task I_can_format_ranges_to_chars()
|
||||||
|
{
|
||||||
|
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||||
|
var destination = new char[32];
|
||||||
|
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||||
|
|
||||||
|
await Assert.That(success).IsTrue();
|
||||||
|
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ internal sealed class SemanticVersionTests
|
||||||
{
|
{
|
||||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
};
|
};
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Arguments(0, 0, 0, 0)]
|
[Arguments(0, 0, 0, 0)]
|
||||||
[Arguments(1, 1, 1, 0)]
|
[Arguments(1, 1, 1, 0)]
|
||||||
|
|
@ -137,17 +138,6 @@ internal sealed class SemanticVersionTests
|
||||||
await Assert.That(result.Metadata).IsEqualTo("build.1");
|
await Assert.That(result.Metadata).IsEqualTo("build.1");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_parse_from_utf8()
|
|
||||||
{
|
|
||||||
var utf8 = "2.0.0-rc.1"u8;
|
|
||||||
var result = SemanticVersion.Parse(utf8);
|
|
||||||
await Assert.That(result.Major).IsEqualTo(2UL);
|
|
||||||
await Assert.That(result.Minor).IsEqualTo(0UL);
|
|
||||||
await Assert.That(result.Patch).IsEqualTo(0UL);
|
|
||||||
await Assert.That(result.Prerelease).IsEqualTo("rc.1");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Arguments(1, 2, 3, null, null, "1.2.3")]
|
[Arguments(1, 2, 3, null, null, "1.2.3")]
|
||||||
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
|
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
|
||||||
|
|
@ -172,17 +162,6 @@ internal sealed class SemanticVersionTests
|
||||||
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
|
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_format_to_utf8()
|
|
||||||
{
|
|
||||||
var v = new SemanticVersion(1, 2, 3, "beta", "456");
|
|
||||||
var dest = new byte[32];
|
|
||||||
var success = v.TryFormat(dest, out var bytesWritten, "f", null);
|
|
||||||
|
|
||||||
await Assert.That(success).IsTrue();
|
|
||||||
await Assert.That(Encoding.UTF8.GetString(dest[..bytesWritten])).IsEqualTo("1.2.3-beta+456");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
|
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
|
||||||
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
|
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
|
||||||
|
|
@ -221,10 +200,7 @@ internal sealed class SemanticVersionTests
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_can_serialize_as_part_of_object()
|
public async Task I_can_serialize_as_part_of_object()
|
||||||
{
|
{
|
||||||
var obj = new
|
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
|
||||||
{
|
|
||||||
Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata")
|
|
||||||
};
|
|
||||||
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
|
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
|
||||||
|
|
||||||
await Assert.That(json).IsEqualTo("{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
await Assert.That(json).IsEqualTo("{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||||
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
|
<InternalsVisibleTo Include="Geekeey.SemVer.Tests" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|
|
||||||
48
src/semver/SemanticVersion.Comparison.cs
Normal file
48
src/semver/SemanticVersion.Comparison.cs
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CompareTo(object? obj)
|
||||||
|
{
|
||||||
|
return obj is not SemanticVersion other ? 1 : CompareTo(other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public int CompareTo(SemanticVersion other)
|
||||||
|
{
|
||||||
|
return SemanticVersionComparer.Priority.Compare(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is less than another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator <(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) < 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is less than or equal to another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator <=(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is greater than another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator >(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether one version is greater than or equal to another version.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator >=(SemanticVersion left, SemanticVersion right)
|
||||||
|
{
|
||||||
|
return left.CompareTo(right) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/semver/SemanticVersion.Formatting.cs
Normal file
114
src/semver/SemanticVersion.Formatting.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
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>
|
||||||
|
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||||
|
{
|
||||||
|
var capacity = RequiredBufferSize;
|
||||||
|
var shared = capacity > 256 ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scoped var buffer = shared.AsSpan();
|
||||||
|
|
||||||
|
if (shared is null)
|
||||||
|
{
|
||||||
|
buffer = stackalloc char[capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = TryFormat(buffer, out var bytesWritten, format, formatProvider);
|
||||||
|
|
||||||
|
return new string(buffer[..bytesWritten]);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (shared is not null)
|
||||||
|
{
|
||||||
|
ArrayPool<char>.Shared.Return(shared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanFormattable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to format the semantic version 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)
|
||||||
|
{
|
||||||
|
if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = destination[charsWritten..];
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Metadata is { Length: > 0 } && format is "f")
|
||||||
|
{
|
||||||
|
destination[0] = '+';
|
||||||
|
|
||||||
|
if (!Metadata.AsSpan().TryCopyTo(destination[1..]))
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destination = destination[(Metadata.Length + 1)..];
|
||||||
|
charsWritten += Metadata.Length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = destination;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
132
src/semver/SemanticVersion.Parsing.cs
Normal file
132
src/semver/SemanticVersion.Parsing.cs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||||
|
{
|
||||||
|
#region IParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a string into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||||
|
public static SemanticVersion Parse(string s)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersion Parse(string s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
return Parse(s.AsSpan(), provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a string into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersion)"/>
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s.AsSpan(), provider, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ISpanParsable
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parses a span of characters into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||||
|
public static SemanticVersion Parse(ReadOnlySpan<char> s)
|
||||||
|
{
|
||||||
|
return Parse(s, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static SemanticVersion Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
if (!TryParse(s, provider, out var version))
|
||||||
|
{
|
||||||
|
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tries to parse a span of characters into a <see cref="SemanticVersion"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersion)"/>
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
return TryParse(s, null, out result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
if (s.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata = default(string);
|
||||||
|
var plusIndex = s.IndexOf('+');
|
||||||
|
if (plusIndex >= 0)
|
||||||
|
{
|
||||||
|
metadata = s[(plusIndex + 1)..].ToString();
|
||||||
|
s = s[..plusIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
var prerelease = default(string);
|
||||||
|
var dashIndex = s.IndexOf('-');
|
||||||
|
if (dashIndex >= 0)
|
||||||
|
{
|
||||||
|
prerelease = s[(dashIndex + 1)..].ToString();
|
||||||
|
s = s[..dashIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstDot = s.IndexOf('.');
|
||||||
|
if (firstDot < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ulong.TryParse(s[..firstDot], NumberStyles.None, CultureInfo.InvariantCulture, out var major))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[(firstDot + 1)..];
|
||||||
|
var secondDot = s.IndexOf('.');
|
||||||
|
if (secondDot < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ulong.TryParse(s[..secondDot], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
using System.Buffers;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Unicode;
|
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
|
@ -117,394 +112,3 @@ public readonly partial record struct SemanticVersion
|
||||||
return ToString(null, null);
|
return ToString(null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CompareTo(object? obj)
|
|
||||||
{
|
|
||||||
return obj is not SemanticVersion other ? 1 : CompareTo(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public int CompareTo(SemanticVersion other)
|
|
||||||
{
|
|
||||||
return SemanticVersionComparer.Priority.Compare(this, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether one version is less than another version.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator <(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return left.CompareTo(right) < 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether one version is less than or equal to another version.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator <=(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return left.CompareTo(right) <= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether one version is greater than another version.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator >(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return left.CompareTo(right) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether one version is greater than or equal to another version.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator >=(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return left.CompareTo(right) >= 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion>, IUtf8SpanParsable<SemanticVersion>
|
|
||||||
{
|
|
||||||
#region IParsable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a string into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
|
||||||
public static SemanticVersion Parse(string s)
|
|
||||||
{
|
|
||||||
return Parse(s.AsSpan(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static SemanticVersion Parse(string s, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
return Parse(s.AsSpan(), provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to parse a string into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersion)"/>
|
|
||||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
return TryParse(s, null, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
return TryParse(s.AsSpan(), provider, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ISpanParsable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a span of characters into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
|
||||||
public static SemanticVersion Parse(ReadOnlySpan<char> s)
|
|
||||||
{
|
|
||||||
return Parse(s, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static SemanticVersion Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
if (!TryParse(s, provider, out var version))
|
|
||||||
{
|
|
||||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to parse a span of characters into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersion)"/>
|
|
||||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
return TryParse(s, null, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
if (s.IsEmpty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata = default(string);
|
|
||||||
var plusIndex = s.IndexOf('+');
|
|
||||||
if (plusIndex >= 0)
|
|
||||||
{
|
|
||||||
metadata = s[(plusIndex + 1)..].ToString();
|
|
||||||
s = s[..plusIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
var prerelease = default(string);
|
|
||||||
var dashIndex = s.IndexOf('-');
|
|
||||||
if (dashIndex >= 0)
|
|
||||||
{
|
|
||||||
prerelease = s[(dashIndex + 1)..].ToString();
|
|
||||||
s = s[..dashIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
var firstDot = s.IndexOf('.');
|
|
||||||
if (firstDot < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ulong.TryParse(s[..firstDot], NumberStyles.None, CultureInfo.InvariantCulture, out var major))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
s = s[(firstDot + 1)..];
|
|
||||||
var secondDot = s.IndexOf('.');
|
|
||||||
if (secondDot < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ulong.TryParse(s[..secondDot], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
|
|
||||||
{
|
|
||||||
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);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IUtf8SpanParsable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a span of UTF-8 bytes into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="Parse(ReadOnlySpan{byte}, IFormatProvider)"/>
|
|
||||||
public static SemanticVersion Parse(ReadOnlySpan<byte> utf8Text)
|
|
||||||
{
|
|
||||||
return Parse(utf8Text, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static SemanticVersion Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
if (!TryParse(utf8Text, provider, out var version))
|
|
||||||
{
|
|
||||||
throw new FormatException($"The input string '{Encoding.UTF8.GetString(utf8Text)}' was not in a correct format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to parse a span of UTF-8 bytes into a <see cref="SemanticVersion"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="TryParse(ReadOnlySpan{byte}, IFormatProvider, out SemanticVersion)"/>
|
|
||||||
public static bool TryParse(ReadOnlySpan<byte> utf8Text, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
return TryParse(utf8Text, null, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static bool TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
|
||||||
{
|
|
||||||
if (utf8Text.IsEmpty)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var charCount = Encoding.UTF8.GetCharCount(utf8Text);
|
|
||||||
var chars = charCount <= 256 ? stackalloc char[charCount] : new char[charCount];
|
|
||||||
Encoding.UTF8.GetChars(utf8Text, chars);
|
|
||||||
|
|
||||||
return TryParse(chars, provider, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
||||||
public readonly partial record struct SemanticVersion : ISpanFormattable, IUtf8SpanFormattable
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
|
||||||
{
|
|
||||||
var capacity = RequiredBufferSize;
|
|
||||||
var shared = capacity > 256 ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
scoped var buffer = shared.AsSpan();
|
|
||||||
|
|
||||||
if (shared is null)
|
|
||||||
{
|
|
||||||
buffer = stackalloc char[capacity];
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = TryFormat(buffer, out var bytesWritten, format, formatProvider);
|
|
||||||
|
|
||||||
return new string(buffer[..bytesWritten]);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (shared is not null)
|
|
||||||
{
|
|
||||||
ArrayPool<char>.Shared.Return(shared);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ISpanFormattable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to format the semantic version 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)
|
|
||||||
{
|
|
||||||
if (!destination.TryWrite(NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out charsWritten))
|
|
||||||
{
|
|
||||||
charsWritten = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destination = destination[charsWritten..];
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Metadata is { Length: > 0 } && format is "f")
|
|
||||||
{
|
|
||||||
destination[0] = '+';
|
|
||||||
|
|
||||||
if (!Metadata.AsSpan().TryCopyTo(destination[1..]))
|
|
||||||
{
|
|
||||||
charsWritten = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destination = destination[(Metadata.Length + 1)..];
|
|
||||||
charsWritten += Metadata.Length + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = destination;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IUtf8SpanFormattable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Tries to format the semantic version into the specified span of UTF-8 bytes.
|
|
||||||
/// </summary>
|
|
||||||
/// <see cref="TryFormat(Span{byte},out int,ReadOnlySpan{char},IFormatProvider)">IUtf8SpanFormattable.TryFormat</see>
|
|
||||||
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten)
|
|
||||||
{
|
|
||||||
return TryFormat(utf8Destination, out bytesWritten, default, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
if (!Utf8.TryWrite(utf8Destination, NumberFormatInfo.InvariantInfo, $"{Major}.{Minor}.{Patch}", out bytesWritten))
|
|
||||||
{
|
|
||||||
bytesWritten = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
utf8Destination = utf8Destination[bytesWritten..];
|
|
||||||
|
|
||||||
if (Prerelease is { Length: > 0 } && format is "s" or "f")
|
|
||||||
{
|
|
||||||
utf8Destination[0] = (byte)'-';
|
|
||||||
|
|
||||||
if (Utf8.FromUtf16(Prerelease, utf8Destination[1..], out _, out var b) is not OperationStatus.Done)
|
|
||||||
{
|
|
||||||
bytesWritten = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
utf8Destination = utf8Destination[(b + 1)..];
|
|
||||||
bytesWritten += b + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Metadata is { Length: > 0 } && format is "f")
|
|
||||||
{
|
|
||||||
utf8Destination[0] = (byte)'+';
|
|
||||||
|
|
||||||
if (Utf8.FromUtf16(Metadata, utf8Destination[1..], out _, out var b) is not OperationStatus.Done)
|
|
||||||
{
|
|
||||||
bytesWritten = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
utf8Destination = utf8Destination[(b + 1)..];
|
|
||||||
bytesWritten += b + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = utf8Destination;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
|
|
@ -25,7 +25,7 @@ internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersi
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return SemanticVersion.Parse(reader.ValueSpan);
|
return SemanticVersion.Parse(reader.GetString().AsSpan());
|
||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException e)
|
||||||
{
|
{
|
||||||
|
|
@ -36,14 +36,14 @@ internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersi
|
||||||
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
var capacity = value.RequiredBufferSize;
|
var capacity = value.RequiredBufferSize;
|
||||||
var shared = capacity > StackBufferThreshold ? ArrayPool<byte>.Shared.Rent(capacity) : null;
|
var shared = capacity > StackBufferThreshold ? ArrayPool<char>.Shared.Rent(capacity) : null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
scoped var buffer = shared.AsSpan();
|
scoped var buffer = shared.AsSpan();
|
||||||
|
|
||||||
if (shared is null)
|
if (shared is null)
|
||||||
{
|
{
|
||||||
buffer = stackalloc byte[capacity];
|
buffer = stackalloc char[capacity];
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = value.TryFormat(buffer, out var bytesWritten, "f", null);
|
_ = value.TryFormat(buffer, out var bytesWritten, "f", null);
|
||||||
|
|
@ -54,7 +54,7 @@ internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersi
|
||||||
{
|
{
|
||||||
if (shared is not null)
|
if (shared is not null)
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(shared);
|
ArrayPool<char>.Shared.Return(shared);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
465
src/semver/SemanticVersionRange.Formatting.cs
Normal file
465
src/semver/SemanticVersionRange.Formatting.cs
Normal file
|
|
@ -0,0 +1,465 @@
|
||||||
|
// 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
|
||||||
|
}
|
||||||
502
src/semver/SemanticVersionRange.Parsing.cs
Normal file
502
src/semver/SemanticVersionRange.Parsing.cs
Normal file
|
|
@ -0,0 +1,502 @@
|
||||||
|
// 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,10 +1,7 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Unicode;
|
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
|
|
@ -12,15 +9,13 @@ namespace Geekeey.SemVer;
|
||||||
/// Represents a semantic version range.
|
/// Represents a semantic version range.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>, IUtf8SpanParsable<SemanticVersionRange>, ISpanFormattable, IUtf8SpanFormattable
|
public readonly partial record struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
private readonly VersionConstraint[][]? _sets;
|
private readonly VersionConstraint[][]? _sets;
|
||||||
private readonly string? _originalString;
|
|
||||||
|
|
||||||
private SemanticVersionRange(VersionConstraint[][] sets, string originalString)
|
private SemanticVersionRange(VersionConstraint[][] sets)
|
||||||
{
|
{
|
||||||
_sets = sets;
|
_sets = sets;
|
||||||
_originalString = originalString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -30,14 +25,25 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
/// <returns><c>true</c> if the version satisfies the range; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if the version satisfies the range; otherwise, <c>false</c>.</returns>
|
||||||
public bool Satisfies(SemanticVersion version)
|
public bool Satisfies(SemanticVersion version)
|
||||||
{
|
{
|
||||||
if (_sets == null || _sets.Length == 0)
|
if (_sets?.Length is null or 0)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var set in _sets)
|
foreach (var set in _sets)
|
||||||
{
|
{
|
||||||
if (IsSatisfiedBySet(set, version))
|
if (version.Prerelease is not null)
|
||||||
|
{
|
||||||
|
if (!set.Any(constraint => constraint.Version.Prerelease is not null &&
|
||||||
|
constraint.Version.Major == version.Major &&
|
||||||
|
constraint.Version.Minor == version.Minor &&
|
||||||
|
constraint.Version.Patch == version.Patch))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (set.All(constraint => constraint.Satisfies(version)))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -46,571 +52,49 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsSatisfiedBySet(VersionConstraint[] set, SemanticVersion version)
|
|
||||||
{
|
|
||||||
if (version.Prerelease != null)
|
|
||||||
{
|
|
||||||
var hasPrereleaseMatch = false;
|
|
||||||
foreach (var constraint in set)
|
|
||||||
{
|
|
||||||
if (constraint.Version.Prerelease != null &&
|
|
||||||
constraint.Version.Major == version.Major &&
|
|
||||||
constraint.Version.Minor == version.Minor &&
|
|
||||||
constraint.Version.Patch == version.Patch)
|
|
||||||
{
|
|
||||||
hasPrereleaseMatch = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasPrereleaseMatch)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var constraint in set)
|
|
||||||
{
|
|
||||||
if (!constraint.Satisfies(version))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return _originalString ?? "*";
|
return ToString(null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal int RequiredBufferSize => _originalString?.Length ?? 1;
|
internal int RequiredBufferSize => GetRequiredBufferSize(RangeStringFormat.NpmShort);
|
||||||
|
|
||||||
#region IFormattable
|
internal readonly record struct VersionConstraint
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
|
||||||
{
|
{
|
||||||
return ToString();
|
public VersionConstraint(Comparator comparator, SemanticVersion version)
|
||||||
|
{
|
||||||
|
Comparator = comparator;
|
||||||
|
Version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Comparator Comparator { get; }
|
||||||
|
public SemanticVersion Version { get; }
|
||||||
|
|
||||||
|
public bool Satisfies(SemanticVersion version)
|
||||||
|
{
|
||||||
|
return Comparator switch
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
internal enum Comparator
|
||||||
|
|
||||||
#region ISpanFormattable
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
|
||||||
{
|
{
|
||||||
var s = ToString();
|
Any,
|
||||||
if (s.Length > destination.Length)
|
Equal,
|
||||||
{
|
GreaterThan,
|
||||||
charsWritten = 0;
|
GreaterThanOrEqual,
|
||||||
return false;
|
LessThan,
|
||||||
}
|
LessThanOrEqual,
|
||||||
|
NotEqual
|
||||||
s.AsSpan().CopyTo(destination);
|
|
||||||
charsWritten = s.Length;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IUtf8SpanFormattable
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
return Utf8.TryWrite(utf8Destination, $"{ToString()}", out bytesWritten);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IParsable
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
return Parse(s.AsSpan(), provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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)
|
|
||||||
{
|
|
||||||
s = s.Trim();
|
|
||||||
if (s.IsEmpty)
|
|
||||||
{
|
|
||||||
result = new SemanticVersionRange([], "*");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region IUtf8SpanParsable
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Parses a span of UTF-8 bytes into a <see cref="SemanticVersionRange"/>.
|
|
||||||
/// </summary>
|
|
||||||
public static SemanticVersionRange Parse(ReadOnlySpan<byte> utf8Text)
|
|
||||||
{
|
|
||||||
return Parse(utf8Text, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static SemanticVersionRange Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider)
|
|
||||||
{
|
|
||||||
if (!TryParse(utf8Text, provider, out var range))
|
|
||||||
{
|
|
||||||
throw new FormatException($"The input string '{Encoding.UTF8.GetString(utf8Text)}' was not in a correct format.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return range;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public static bool TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
|
||||||
{
|
|
||||||
if (utf8Text.IsEmpty)
|
|
||||||
{
|
|
||||||
result = new SemanticVersionRange([], "*");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var charCount = Encoding.UTF8.GetCharCount(utf8Text);
|
|
||||||
var chars = charCount <= 256 ? stackalloc char[charCount] : new char[charCount];
|
|
||||||
Encoding.UTF8.GetChars(utf8Text, chars);
|
|
||||||
|
|
||||||
return TryParse(chars, provider, out result);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
var original = s.ToString();
|
|
||||||
var alternatives = new List<VersionConstraint[]>();
|
|
||||||
|
|
||||||
var start = 0;
|
|
||||||
var bracketLevel = 0;
|
|
||||||
for (var i = 0; i < s.Length; i++)
|
|
||||||
{
|
|
||||||
if (s[i] is '[' or '(')
|
|
||||||
{
|
|
||||||
bracketLevel++;
|
|
||||||
}
|
|
||||||
else if (s[i] is ']' or ')')
|
|
||||||
{
|
|
||||||
bracketLevel--;
|
|
||||||
}
|
|
||||||
else if (s[i] is ',' && bracketLevel == 0)
|
|
||||||
{
|
|
||||||
if (!TryParseSingleMavenRange(s[start..i].Trim(), out var constraints))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alternatives.Add(constraints);
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseSingleMavenRange(s[start..].Trim(), out var lastConstraints))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alternatives.Add(lastConstraints);
|
|
||||||
|
|
||||||
result = new SemanticVersionRange([.. alternatives], original);
|
|
||||||
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 v, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints = [new VersionConstraint(Comparator.GreaterThanOrEqual, v)];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var last = s.Length - 1;
|
|
||||||
if (s[last] is not ']' and not ')')
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var inclusiveStart = s[0] == '[';
|
|
||||||
var inclusiveEnd = s[last] == ']';
|
|
||||||
var inner = s[1..last];
|
|
||||||
var commaIndex = inner.IndexOf(',');
|
|
||||||
|
|
||||||
if (commaIndex < 0)
|
|
||||||
{
|
|
||||||
if (!inclusiveStart || !inclusiveEnd)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseVersionPartially(inner, out var v, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints = [new VersionConstraint(Comparator.Equal, v)];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var startStr = inner[..commaIndex].Trim();
|
|
||||||
var endStr = inner[(commaIndex + 1)..].Trim();
|
|
||||||
var list = new List<VersionConstraint>();
|
|
||||||
|
|
||||||
if (!startStr.IsEmpty)
|
|
||||||
{
|
|
||||||
if (!TryParseVersionPartially(startStr, out var vStart, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new VersionConstraint(inclusiveStart ? Comparator.GreaterThanOrEqual : Comparator.GreaterThan, vStart));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!endStr.IsEmpty)
|
|
||||||
{
|
|
||||||
if (!TryParseVersionPartially(endStr, out var vEnd, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new VersionConstraint(inclusiveEnd ? Comparator.LessThanOrEqual : Comparator.LessThan, vEnd));
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints = [.. list];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
|
||||||
{
|
|
||||||
result = default;
|
|
||||||
var original = s.ToString();
|
|
||||||
var alternatives = new List<VersionConstraint[]>();
|
|
||||||
|
|
||||||
var start = 0;
|
|
||||||
while (start < s.Length)
|
|
||||||
{
|
|
||||||
var orIndex = s[start..].IndexOf("||".AsSpan());
|
|
||||||
var part = orIndex < 0 ? s[start..].Trim() : s[start..(start + orIndex)].Trim();
|
|
||||||
start = orIndex < 0 ? s.Length : start + orIndex + 2;
|
|
||||||
|
|
||||||
if (part.IsEmpty)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseNpmSet(part, out var constraints))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
alternatives.Add(constraints);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = new SemanticVersionRange([.. alternatives], original);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseNpmSet(ReadOnlySpan<char> s, [NotNullWhen(true)] out VersionConstraint[]? constraints)
|
|
||||||
{
|
|
||||||
constraints = null;
|
|
||||||
var list = new List<VersionConstraint>();
|
|
||||||
|
|
||||||
var hyphenIndex = s.IndexOf(" - ".AsSpan());
|
|
||||||
if (hyphenIndex > 0)
|
|
||||||
{
|
|
||||||
var v1Str = s[..hyphenIndex].Trim();
|
|
||||||
var v2Str = s[(hyphenIndex + 3)..].Trim();
|
|
||||||
if (!SemanticVersion.TryParse(v1Str, out var v1) || !SemanticVersion.TryParse(v2Str, out var v2))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints =
|
|
||||||
[
|
|
||||||
new VersionConstraint(Comparator.GreaterThanOrEqual, v1),
|
|
||||||
new VersionConstraint(Comparator.LessThanOrEqual, v2)
|
|
||||||
];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var current = s;
|
|
||||||
while (!current.IsEmpty)
|
|
||||||
{
|
|
||||||
current = current.TrimStart();
|
|
||||||
if (current.IsEmpty)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var spaceIndex = current.IndexOf(' ');
|
|
||||||
var part = spaceIndex < 0 ? current : current[..spaceIndex];
|
|
||||||
current = spaceIndex < 0 ? default : current[spaceIndex..];
|
|
||||||
|
|
||||||
if (IsOperatorOnly(part))
|
|
||||||
{
|
|
||||||
current = current.TrimStart();
|
|
||||||
var nextSpace = current.IndexOf(' ');
|
|
||||||
var nextPart = nextSpace < 0 ? current : current[..nextSpace];
|
|
||||||
current = nextSpace < 0 ? default : current[nextSpace..];
|
|
||||||
|
|
||||||
if (!TryParseNpmConstraint(part, nextPart, list))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!TryParseNpmConstraint(default, part, list))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constraints = [.. list];
|
|
||||||
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> vStr, List<VersionConstraint> constraints)
|
|
||||||
{
|
|
||||||
if (op.IsEmpty)
|
|
||||||
{
|
|
||||||
if (vStr.StartsWith(">="))
|
|
||||||
{
|
|
||||||
op = ">=";
|
|
||||||
vStr = vStr[2..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith("<="))
|
|
||||||
{
|
|
||||||
op = "<=";
|
|
||||||
vStr = vStr[2..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith(">"))
|
|
||||||
{
|
|
||||||
op = ">";
|
|
||||||
vStr = vStr[1..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith("<"))
|
|
||||||
{
|
|
||||||
op = "<";
|
|
||||||
vStr = vStr[1..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith("^"))
|
|
||||||
{
|
|
||||||
op = "^";
|
|
||||||
vStr = vStr[1..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith("~"))
|
|
||||||
{
|
|
||||||
op = "~";
|
|
||||||
vStr = vStr[1..];
|
|
||||||
}
|
|
||||||
else if (vStr.StartsWith("="))
|
|
||||||
{
|
|
||||||
op = "=";
|
|
||||||
vStr = vStr[1..];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
vStr = vStr.Trim();
|
|
||||||
if (vStr is "*" or "x" or "X")
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.Any, default));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryParseVersionPartially(vStr, out var v, out var components))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (op is "^")
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
|
|
||||||
var endV = v.Major > 0 ? new SemanticVersion(v.Major + 1, 0, 0) : (v.Minor > 0 ? new SemanticVersion(0, v.Minor + 1, 0) : new SemanticVersion(0, 0, v.Patch + 1));
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.LessThan, endV));
|
|
||||||
}
|
|
||||||
else if (op is "~")
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
|
|
||||||
var endV = components >= 2 ? new SemanticVersion(v.Major, v.Minor + 1, 0) : new SemanticVersion(v.Major + 1, 0, 0);
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.LessThan, endV));
|
|
||||||
}
|
|
||||||
else if (op is ">") constraints.Add(new VersionConstraint(Comparator.GreaterThan, v));
|
|
||||||
else if (op is ">=") constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
|
|
||||||
else if (op is "<") constraints.Add(new VersionConstraint(Comparator.LessThan, v));
|
|
||||||
else if (op is "<=") constraints.Add(new VersionConstraint(Comparator.LessThanOrEqual, v));
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (components == 3)
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.Equal, v));
|
|
||||||
}
|
|
||||||
else if (components == 2)
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(v.Major, v.Minor + 1, 0)));
|
|
||||||
}
|
|
||||||
else if (components == 1)
|
|
||||||
{
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.GreaterThanOrEqual, v));
|
|
||||||
constraints.Add(new VersionConstraint(Comparator.LessThan, new SemanticVersion(v.Major + 1, 0, 0)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryParseVersionPartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
|
||||||
{
|
|
||||||
version = default;
|
|
||||||
components = 0;
|
|
||||||
|
|
||||||
var dashIndex = s.IndexOf('-');
|
|
||||||
var metadataIndex = s.IndexOf('+');
|
|
||||||
var suffixIndex = dashIndex >= 0 ? (metadataIndex >= 0 ? Math.Min(dashIndex, metadataIndex) : dashIndex) : metadataIndex;
|
|
||||||
|
|
||||||
var versionPart = suffixIndex >= 0 ? s[..suffixIndex] : s;
|
|
||||||
var prerelease = dashIndex >= 0 ? (metadataIndex > dashIndex ? s[(dashIndex + 1)..metadataIndex].ToString() : s[(dashIndex + 1)..].ToString()) : null;
|
|
||||||
var metadata = metadataIndex >= 0 ? s[(metadataIndex + 1)..].ToString() : null;
|
|
||||||
|
|
||||||
ulong major = 0, minor = 0, patch = 0;
|
|
||||||
var current = versionPart;
|
|
||||||
var componentCount = 0;
|
|
||||||
|
|
||||||
while (!current.IsEmpty && componentCount < 3)
|
|
||||||
{
|
|
||||||
var dotIndex = current.IndexOf('.');
|
|
||||||
var part = dotIndex < 0 ? current : current[..dotIndex];
|
|
||||||
|
|
||||||
if (part is "*" or "x" or "X")
|
|
||||||
{
|
|
||||||
components = componentCount;
|
|
||||||
version = componentCount switch
|
|
||||||
{
|
|
||||||
1 => new SemanticVersion(major, 0, 0),
|
|
||||||
2 => new SemanticVersion(major, minor, 0),
|
|
||||||
_ => default
|
|
||||||
};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ulong.TryParse(part, out var value))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (componentCount == 0) major = value;
|
|
||||||
else if (componentCount == 1) minor = value;
|
|
||||||
else if (componentCount == 2) patch = value;
|
|
||||||
|
|
||||||
componentCount++;
|
|
||||||
if (dotIndex < 0) break;
|
|
||||||
current = current[(dotIndex + 1)..];
|
|
||||||
}
|
|
||||||
|
|
||||||
components = componentCount;
|
|
||||||
version = new SemanticVersion(major, minor, patch, prerelease, metadata);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal enum Comparator
|
|
||||||
{
|
|
||||||
Any,
|
|
||||||
Equal,
|
|
||||||
GreaterThan,
|
|
||||||
GreaterThanOrEqual,
|
|
||||||
LessThan,
|
|
||||||
LessThanOrEqual,
|
|
||||||
NotEqual
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct VersionConstraint(Comparator Comparator, SemanticVersion Version)
|
|
||||||
{
|
|
||||||
public bool Satisfies(SemanticVersion version)
|
|
||||||
{
|
|
||||||
if (Comparator is Comparator.Any)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cmp = version.CompareTo(Version);
|
|
||||||
return Comparator switch
|
|
||||||
{
|
|
||||||
Comparator.Equal => cmp == 0,
|
|
||||||
Comparator.NotEqual => cmp != 0,
|
|
||||||
Comparator.GreaterThan => cmp > 0,
|
|
||||||
Comparator.GreaterThanOrEqual => cmp >= 0,
|
|
||||||
Comparator.LessThan => cmp < 0,
|
|
||||||
Comparator.LessThanOrEqual => cmp <= 0,
|
|
||||||
_ => false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
|
@ -8,6 +9,8 @@ namespace Geekeey.SemVer;
|
||||||
|
|
||||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||||
{
|
{
|
||||||
|
private const int StackBufferThreshold = 256;
|
||||||
|
|
||||||
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
if (reader.TokenType is JsonTokenType.Null)
|
if (reader.TokenType is JsonTokenType.Null)
|
||||||
|
|
@ -22,16 +25,35 @@ internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<Semantic
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return SemanticVersionRange.Parse(reader.ValueSpan);
|
return SemanticVersionRange.Parse(reader.GetString().AsSpan());
|
||||||
}
|
}
|
||||||
catch (FormatException e)
|
catch (FormatException exception)
|
||||||
{
|
{
|
||||||
throw new JsonException(e.Message, e);
|
throw new JsonException(exception.Message, exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(value.ToString());
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue