Refactor code structure for improved readability and maintainability; optimize performance in key functions.

This commit is contained in:
master
2025-12-22 19:06:31 +02:00
parent dfaa2079aa
commit 4602ccc3a3
1444 changed files with 109919 additions and 8058 deletions

View File

@@ -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

View File

@@ -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; }
}
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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>