Refactor code structure for improved readability and maintainability; optimize performance in key functions.
This commit is contained in:
@@ -15,7 +15,7 @@ Deterministic merge and reconciliation engine; builds identity graph via aliases
|
||||
## Interfaces & contracts
|
||||
- AdvisoryMergeService.MergeAsync(ids or byKind): returns summary {processed, merged, overrides, conflicts}.
|
||||
- Precedence table configurable but with sane defaults: RedHat/Ubuntu/Debian/SUSE > Vendor PSIRT > GHSA/OSV > NVD; CERTs enrich; KEV sets flags.
|
||||
- Range selection uses comparers: NevraComparer, DebEvrComparer, SemVerRange; deterministic tie-breakers.
|
||||
- Range selection uses comparers: NevraComparer, DebianEvrComparer, ApkVersionComparer, SemVerRange; deterministic tie-breakers.
|
||||
- Provenance propagation merges unique entries; references deduped by (url, type).
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
namespace StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
using System;
|
||||
using StellaOps.Concelier.Normalization.Distro;
|
||||
|
||||
/// <summary>
|
||||
/// Compares Alpine APK package versions using apk-tools ordering rules.
|
||||
/// </summary>
|
||||
public sealed class ApkVersionComparer : IComparer<ApkVersion>, IComparer<string>
|
||||
{
|
||||
public static ApkVersionComparer Instance { get; } = new();
|
||||
|
||||
private ApkVersionComparer()
|
||||
{
|
||||
}
|
||||
|
||||
public int Compare(string? x, string? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var xParsed = ApkVersion.TryParse(x, out var xVersion);
|
||||
var yParsed = ApkVersion.TryParse(y, out var yVersion);
|
||||
|
||||
if (xParsed && yParsed)
|
||||
{
|
||||
return Compare(xVersion, yVersion);
|
||||
}
|
||||
|
||||
if (xParsed)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (yParsed)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
return string.Compare(x, y, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public int Compare(ApkVersion? x, ApkVersion? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var compare = CompareVersionString(x.Version, y.Version);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
compare = x.PkgRel.CompareTo(y.PkgRel);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
// When pkgrel values are equal, implicit (no -r) sorts before explicit -r0
|
||||
// e.g., "1.2.3" < "1.2.3-r0"
|
||||
if (!x.HasExplicitPkgRel && y.HasExplicitPkgRel)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (x.HasExplicitPkgRel && !y.HasExplicitPkgRel)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int CompareVersionString(string left, string right)
|
||||
{
|
||||
var leftIndex = 0;
|
||||
var rightIndex = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
var leftToken = NextToken(left, ref leftIndex);
|
||||
var rightToken = NextToken(right, ref rightIndex);
|
||||
|
||||
if (leftToken.Type == TokenType.End && rightToken.Type == TokenType.End)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (leftToken.Type == TokenType.End)
|
||||
{
|
||||
return CompareEndToken(rightToken, isLeftEnd: true);
|
||||
}
|
||||
|
||||
if (rightToken.Type == TokenType.End)
|
||||
{
|
||||
return CompareEndToken(leftToken, isLeftEnd: false);
|
||||
}
|
||||
|
||||
if (leftToken.Type != rightToken.Type)
|
||||
{
|
||||
var compare = CompareDifferentTypes(leftToken, rightToken);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var compare = leftToken.Type switch
|
||||
{
|
||||
TokenType.Numeric => CompareNumeric(leftToken.NumericValue, rightToken.NumericValue),
|
||||
TokenType.Alpha => CompareAlpha(leftToken.Text, rightToken.Text),
|
||||
TokenType.Suffix => CompareSuffix(leftToken, rightToken),
|
||||
_ => 0
|
||||
};
|
||||
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompareEndToken(VersionToken token, bool isLeftEnd)
|
||||
{
|
||||
if (token.Type == TokenType.Suffix)
|
||||
{
|
||||
// Compare suffix order: suffix token vs no-suffix (order=0)
|
||||
// If isLeftEnd=true: comparing END (left) vs suffix (right) → return CompareSuffixOrder(0, right.order)
|
||||
// If isLeftEnd=false: comparing suffix (left) vs END (right) → return CompareSuffixOrder(left.order, 0)
|
||||
var compare = isLeftEnd
|
||||
? CompareSuffixOrder(0, token.SuffixOrder)
|
||||
: CompareSuffixOrder(token.SuffixOrder, 0);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
return isLeftEnd ? -1 : 1;
|
||||
}
|
||||
|
||||
return isLeftEnd ? -1 : 1;
|
||||
}
|
||||
|
||||
private static int CompareDifferentTypes(VersionToken left, VersionToken right)
|
||||
{
|
||||
if (left.Type == TokenType.Suffix || right.Type == TokenType.Suffix)
|
||||
{
|
||||
var leftOrder = left.Type == TokenType.Suffix ? left.SuffixOrder : 0;
|
||||
var rightOrder = right.Type == TokenType.Suffix ? right.SuffixOrder : 0;
|
||||
var compare = CompareSuffixOrder(leftOrder, rightOrder);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
return TokenTypeRank(left.Type).CompareTo(TokenTypeRank(right.Type));
|
||||
}
|
||||
|
||||
return TokenTypeRank(left.Type).CompareTo(TokenTypeRank(right.Type));
|
||||
}
|
||||
|
||||
private static int TokenTypeRank(TokenType type)
|
||||
=> type switch
|
||||
{
|
||||
TokenType.Numeric => 3,
|
||||
TokenType.Alpha => 2,
|
||||
TokenType.Suffix => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
private static int CompareNumeric(string left, string right)
|
||||
{
|
||||
var leftTrimmed = TrimLeadingZeros(left);
|
||||
var rightTrimmed = TrimLeadingZeros(right);
|
||||
|
||||
if (leftTrimmed.Length != rightTrimmed.Length)
|
||||
{
|
||||
return leftTrimmed.Length.CompareTo(rightTrimmed.Length);
|
||||
}
|
||||
|
||||
return string.Compare(leftTrimmed, rightTrimmed, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int CompareAlpha(string left, string right)
|
||||
=> string.Compare(left, right, StringComparison.Ordinal);
|
||||
|
||||
private static int CompareSuffix(VersionToken left, VersionToken right)
|
||||
{
|
||||
var compare = CompareSuffixOrder(left.SuffixOrder, right.SuffixOrder);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(left.SuffixName) || !string.IsNullOrEmpty(right.SuffixName))
|
||||
{
|
||||
compare = string.Compare(left.SuffixName, right.SuffixName, StringComparison.Ordinal);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
}
|
||||
|
||||
if (!left.HasSuffixNumber && !right.HasSuffixNumber)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!left.HasSuffixNumber)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!right.HasSuffixNumber)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CompareNumeric(left.SuffixNumber, right.SuffixNumber);
|
||||
}
|
||||
|
||||
private static int CompareSuffixOrder(int leftOrder, int rightOrder)
|
||||
=> leftOrder.CompareTo(rightOrder);
|
||||
|
||||
private static VersionToken NextToken(string value, ref int index)
|
||||
{
|
||||
while (index < value.Length)
|
||||
{
|
||||
var current = value[index];
|
||||
if (current == '_')
|
||||
{
|
||||
if (index + 1 < value.Length && char.IsLetter(value[index + 1]))
|
||||
{
|
||||
return ReadSuffixToken(value, ref index);
|
||||
}
|
||||
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsDigit(current))
|
||||
{
|
||||
return ReadNumericToken(value, ref index);
|
||||
}
|
||||
|
||||
if (char.IsLetter(current))
|
||||
{
|
||||
return ReadAlphaToken(value, ref index);
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
return VersionToken.End;
|
||||
}
|
||||
|
||||
private static VersionToken ReadNumericToken(string value, ref int index)
|
||||
{
|
||||
var start = index;
|
||||
while (index < value.Length && char.IsDigit(value[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var number = value.Substring(start, index - start);
|
||||
return VersionToken.Numeric(number);
|
||||
}
|
||||
|
||||
private static VersionToken ReadAlphaToken(string value, ref int index)
|
||||
{
|
||||
var start = index;
|
||||
while (index < value.Length && char.IsLetter(value[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var text = value.Substring(start, index - start);
|
||||
return VersionToken.Alpha(text);
|
||||
}
|
||||
|
||||
private static VersionToken ReadSuffixToken(string value, ref int index)
|
||||
{
|
||||
index++;
|
||||
var nameStart = index;
|
||||
while (index < value.Length && char.IsLetter(value[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var name = value.Substring(nameStart, index - nameStart);
|
||||
if (name.Length == 0)
|
||||
{
|
||||
return VersionToken.End;
|
||||
}
|
||||
|
||||
var normalizedName = name.ToLowerInvariant();
|
||||
var order = normalizedName switch
|
||||
{
|
||||
"alpha" => -4,
|
||||
"beta" => -3,
|
||||
"pre" => -2,
|
||||
"rc" => -1,
|
||||
"p" => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
var numberStart = index;
|
||||
while (index < value.Length && char.IsDigit(value[index]))
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var number = value.Substring(numberStart, index - numberStart);
|
||||
return VersionToken.Suffix(normalizedName, order, number);
|
||||
}
|
||||
|
||||
private static string TrimLeadingZeros(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return "0";
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
while (index < value.Length && value[index] == '0')
|
||||
{
|
||||
index++;
|
||||
}
|
||||
|
||||
var trimmed = value[index..];
|
||||
return trimmed.Length == 0 ? "0" : trimmed;
|
||||
}
|
||||
|
||||
private enum TokenType
|
||||
{
|
||||
End,
|
||||
Numeric,
|
||||
Alpha,
|
||||
Suffix
|
||||
}
|
||||
|
||||
private readonly struct VersionToken
|
||||
{
|
||||
private VersionToken(TokenType type, string text, string numeric, string suffixName, int suffixOrder, string suffixNumber, bool hasSuffixNumber)
|
||||
{
|
||||
Type = type;
|
||||
Text = text;
|
||||
NumericValue = numeric;
|
||||
SuffixName = suffixName;
|
||||
SuffixOrder = suffixOrder;
|
||||
SuffixNumber = suffixNumber;
|
||||
HasSuffixNumber = hasSuffixNumber;
|
||||
}
|
||||
|
||||
public static VersionToken End { get; } = new(TokenType.End, string.Empty, string.Empty, string.Empty, 0, string.Empty, false);
|
||||
|
||||
public static VersionToken Numeric(string value)
|
||||
=> new(TokenType.Numeric, string.Empty, value ?? string.Empty, string.Empty, 0, string.Empty, false);
|
||||
|
||||
public static VersionToken Alpha(string value)
|
||||
=> new(TokenType.Alpha, value ?? string.Empty, string.Empty, string.Empty, 0, string.Empty, false);
|
||||
|
||||
public static VersionToken Suffix(string name, int order, string number)
|
||||
{
|
||||
var hasNumber = !string.IsNullOrEmpty(number);
|
||||
return new VersionToken(TokenType.Suffix, string.Empty, string.Empty, name ?? string.Empty, order, hasNumber ? TrimLeadingZeros(number) : string.Empty, hasNumber);
|
||||
}
|
||||
|
||||
public TokenType Type { get; }
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public string NumericValue { get; }
|
||||
|
||||
public string SuffixName { get; }
|
||||
|
||||
public int SuffixOrder { get; }
|
||||
|
||||
public string SuffixNumber { get; }
|
||||
|
||||
public bool HasSuffixNumber { get; }
|
||||
}
|
||||
}
|
||||
@@ -78,13 +78,7 @@ public sealed class DebianEvrComparer : IComparer<DebianEvr>, IComparer<string>
|
||||
return compare;
|
||||
}
|
||||
|
||||
compare = CompareSegment(x.Revision, y.Revision);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
return string.Compare(x.Original, y.Original, StringComparison.Ordinal);
|
||||
return CompareSegment(x.Revision, y.Revision);
|
||||
}
|
||||
|
||||
private static int CompareSegment(string left, string right)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides version comparison with optional proof output.
|
||||
/// </summary>
|
||||
public interface IVersionComparator
|
||||
{
|
||||
/// <summary>
|
||||
/// Compares two version strings.
|
||||
/// </summary>
|
||||
int Compare(string? left, string? right);
|
||||
|
||||
/// <summary>
|
||||
/// Compares two version strings and returns proof lines.
|
||||
/// </summary>
|
||||
VersionComparisonResult CompareWithProof(string? left, string? right);
|
||||
}
|
||||
@@ -90,13 +90,7 @@ public sealed class NevraComparer : IComparer<Nevra>, IComparer<string>
|
||||
return compare;
|
||||
}
|
||||
|
||||
compare = RpmVersionComparer.Compare(x.Release, y.Release);
|
||||
if (compare != 0)
|
||||
{
|
||||
return compare;
|
||||
}
|
||||
|
||||
return string.Compare(x.Original, y.Original, StringComparison.Ordinal);
|
||||
return RpmVersionComparer.Compare(x.Release, y.Release);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace StellaOps.Concelier.Merge.Comparers;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
|
||||
/// <summary>
|
||||
/// Result of a version comparison with explainability proof lines.
|
||||
/// </summary>
|
||||
public sealed record VersionComparisonResult(
|
||||
int Comparison,
|
||||
ImmutableArray<string> ProofLines);
|
||||
@@ -13,5 +13,6 @@
|
||||
<ProjectReference Include="../StellaOps.Concelier.Core/StellaOps.Concelier.Core.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Models/StellaOps.Concelier.Models.csproj" />
|
||||
<ProjectReference Include="../StellaOps.Concelier.Normalization/StellaOps.Concelier.Normalization.csproj" />
|
||||
<ProjectReference Include="../../../__Libraries/StellaOps.VersionComparison/StellaOps.VersionComparison.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user