Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
216 changes: 168 additions & 48 deletions Knossos.NET/Classes/SemanticVersion.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Knossos.NET.Classes
{
Expand Down Expand Up @@ -258,7 +260,7 @@ private static string PreReleaseSeparateNumbers(string preReleaseString)

/// <summary>
/// Compares a semantic version string to the version string in the mod dependency to see if it sastifies the requirement.
/// Version : null -> Any, Version: "4.6.1" -> Only that version, Version: "~4.6.1" -> >=4.6.1 < 4.7.0, Version: ">=4.6.1" -> equal or newer, Version: "<=4.6.1" -> equal or older, Version: ">4.6.1" -> newer, Version: "<4.6.1" -> older
/// Version : null -> Any, Version: "4.6.1" -> Only that version, Version: "~4.6.1" -> >=4.6.1 < 4.7.0, Version: ">=4.6.1" -> equal or newer, Version: "<=4.6.1" -> equal or older, Version: ">4.6.1" -> newer, Version: "<4.6.1" -> older, Version: "[4.6.1,5.0.0)" -> NuGet interval (any combination of [/]/(/) brackets), Version: ">=4.6.1 <5.0.0" -> space-separated AND
/// </summary>
/// <param name="dependencyVersion"></param>
/// <param name="version"></param>
Expand All @@ -276,7 +278,7 @@ public static bool SastifiesDependency(string? dependencyVersion, string? versio
*/
/// <summary>
/// Compares a semantic version to the version string in the mod dependency to see if it sastifies the requirement.
/// Version : null -> Any, Version: "4.6.1" -> Only that version, Version: "~4.6.1" -> >=4.6.1 < 4.7.0, Version: ">=4.6.1" -> equal or newer, Version: "<=4.6.1" -> equal or older, Version: ">4.6.1" -> newer, Version: "<4.6.1" -> older
/// Version : null -> Any, Version: "4.6.1" -> Only that version, Version: "~4.6.1" -> >=4.6.1 < 4.7.0, Version: ">=4.6.1" -> equal or newer, Version: "<=4.6.1" -> equal or older, Version: ">4.6.1" -> newer, Version: "<4.6.1" -> older, Version: "[4.6.1,5.0.0)" -> NuGet interval (any combination of [/]/(/) brackets), Version: ">=4.6.1 <5.0.0" -> space-separated AND
/// </summary>
/// <param name="dependencyVersion"></param>
/// <param name="version"></param>
Expand All @@ -291,73 +293,191 @@ public static bool SastifiesDependency(string? dependencyVersion, SemanticVersio
return true;
}

if (dependencyVersion.Contains("~"))
var parts = NormalizeToSingleOps(dependencyVersion.Trim());
return parts.Count > 0 && parts.All(p => SatisfiesSingleOp(p, version));
}
catch (Exception ex)
{
Log.Add(Log.LogSeverity.Error, "SemanticVersion.SastifiesDependency()", ex);
return false;
}
}

/// <summary>
/// True if the constraint cannot be represented as a single-operator + version (e.g. NuGet interval
/// "[1.0,2.0)" or space-separated AND ">=1.0 <2.0"). Used by callers that strip operators or by UI
/// pickers that can only display a single operator.
/// </summary>
public static bool IsComplexConstraint(string? constraint)
{
if (string.IsNullOrWhiteSpace(constraint))
return false;
var s = constraint.Trim();
if (s.Length == 0)
return false;
if (s[0] == '[' || s[0] == '(')
return true;
if (s.Any(char.IsWhiteSpace))
return true;
return false;
}

/// <summary>
/// Returns the bare lower-bound version string from any supported constraint form, or null when the
/// constraint has no lower bound (e.g. "&lt;2.0.0" or "(,2.0]"). Null-safe and exception-safe.
/// </summary>
public static string? GetLowerBound(string? constraint)
{
if (string.IsNullOrWhiteSpace(constraint))
return null;
try
{
var parts = NormalizeToSingleOps(constraint.Trim());
foreach (var p in parts)
{
var versionDep = new SemanticVersion(dependencyVersion.Replace("~", ""));
/* major and minor has to math, revision needs to be equal or superior*/
if (version.major == versionDep.major && version.minor == versionDep.minor && version.revision >= versionDep.revision)
{
if (Compare(version, versionDep) >= 0)
{
return true;
}
}
return false;
if (p.StartsWith(">="))
return p.Substring(2).Trim();
if (p.StartsWith("~"))
return p.Substring(1).Trim();
if (p.StartsWith(">"))
return p.Substring(1).Trim();
if (p.StartsWith("<"))
continue;
return p.Trim();
}
return null;
}
catch
{
return null;
}
}

if (dependencyVersion.Contains(">="))
{
var versionDep = new SemanticVersion(dependencyVersion.Replace(">=", ""));
/* major minor and revision needs to be equal or superior*/
if (version.major >= versionDep.major || version.major == versionDep.major && version.minor >= versionDep.minor || version.major == versionDep.major && version.minor == versionDep.minor && version.revision >= versionDep.revision)
{
if (Compare(version, versionDep) >= 0)
{
return true;
}
}
/// <summary>
/// Splits a dependency version string into a list of single-operator constraints to be ANDed together.
/// Recognizes NuGet interval notation, npm hyphen ranges, and npm space-separated ranges; otherwise
/// returns the input unchanged as a one-element list (the existing single-operator path).
/// </summary>
private static List<string> NormalizeToSingleOps(string input)
{
var result = new List<string>();

if (string.IsNullOrWhiteSpace(input))
return result;

//NuGet interval notation: [X,Y], (X,Y), [X,Y), (X,Y], [X,), (X,], (,Y], (,Y), [X]
if (input[0] == '[' || input[0] == '(')
{
var openBracket = input[0];
var closeBracket = input[input.Length - 1];
if (closeBracket != ']' && closeBracket != ')')
throw new Exception("Invalid NuGet interval, missing closing bracket: " + input);

var inner = input.Substring(1, input.Length - 2);
var commaIdx = inner.IndexOf(',');

return false;
if (commaIdx < 0)
{
//No comma -> [X] form (exact match). Both brackets must be square.
if (openBracket != '[' || closeBracket != ']')
throw new Exception("NuGet exact form requires square brackets: " + input);
var v = inner.Trim();
if (v.Length == 0)
throw new Exception("NuGet exact form requires a version: " + input);
result.Add(v);
return result;
}

if (dependencyVersion.Contains("<="))
var lowStr = inner.Substring(0, commaIdx).Trim();
var highStr = inner.Substring(commaIdx + 1).Trim();

if (lowStr.Length > 0)
result.Add((openBracket == '[' ? ">=" : ">") + lowStr);
if (highStr.Length > 0)
result.Add((closeBracket == ']' ? "<=" : "<") + highStr);

return result;
}

//npm space-separated AND. Also catches single-op-with-internal-space like ">= 4.6.1".
if (input.Any(char.IsWhiteSpace))
{
var matches = Regex.Matches(input, @"(>=|<=|>|<|~)?\s*\d+(?:\.\d+){0,2}(?:-\S+)?");
if (matches.Count == 0)
throw new Exception("Could not parse range: " + input);
foreach (Match m in matches)
result.Add(Regex.Replace(m.Value, @"\s+", ""));
return result;
}

//Single-operator form, no parsing needed.
result.Add(input);
return result;
}

/// <summary>
/// Evaluates a single-operator constraint string (e.g. ">=4.6.1", "~4.6.1", "4.6.1") against a candidate
/// version. This is the original per-operator logic, extracted unchanged from SastifiesDependency.
/// </summary>
private static bool SatisfiesSingleOp(string singleOp, SemanticVersion version)
{
if (singleOp.Contains("~"))
{
var versionDep = new SemanticVersion(singleOp.Replace("~", ""));
/* major and minor has to math, revision needs to be equal or superior*/
if (version.major == versionDep.major && version.minor == versionDep.minor && version.revision >= versionDep.revision)
{
var versionDep = new SemanticVersion(dependencyVersion.Replace("<=", ""));
/* major minor and revision needs to be equal or inferior*/
if (version.major <= versionDep.major || version.major == versionDep.major && version.minor <= versionDep.minor || version.major == versionDep.major && version.minor == versionDep.minor && version.revision <= versionDep.revision)
if (Compare(version, versionDep) >= 0)
{
if (Compare(version, versionDep) <= 0)
{
return true;
}
return true;
}

return false;
}
return false;
}

if (dependencyVersion.Contains(">"))
if (singleOp.Contains(">="))
{
var versionDep = new SemanticVersion(singleOp.Replace(">=", ""));
/* major minor and revision needs to be equal or superior*/
if (version.major >= versionDep.major || version.major == versionDep.major && version.minor >= versionDep.minor || version.major == versionDep.major && version.minor == versionDep.minor && version.revision >= versionDep.revision)
{
var versionDep = new SemanticVersion(dependencyVersion.Replace(">", ""));
return Compare(version, versionDep) > 0;
if (Compare(version, versionDep) >= 0)
{
return true;
}
}

if (dependencyVersion.Contains("<"))
{
var versionDep = new SemanticVersion(dependencyVersion.Replace("<", ""));
return Compare(version, versionDep) < 0;
}
return false;
}

if (Compare(version, new SemanticVersion(dependencyVersion)) == 0)
if (singleOp.Contains("<="))
{
var versionDep = new SemanticVersion(singleOp.Replace("<=", ""));
/* major minor and revision needs to be equal or inferior*/
if (version.major <= versionDep.major || version.major == versionDep.major && version.minor <= versionDep.minor || version.major == versionDep.major && version.minor == versionDep.minor && version.revision <= versionDep.revision)
{
return true;
if (Compare(version, versionDep) <= 0)
{
return true;
}
}

return false;
}catch (Exception ex)
}

if (singleOp.Contains(">"))
{
Log.Add(Log.LogSeverity.Error, "SemanticVersion.SastifiesDependency()", ex);
return false;
var versionDep = new SemanticVersion(singleOp.Replace(">", ""));
return Compare(version, versionDep) > 0;
}

if (singleOp.Contains("<"))
{
var versionDep = new SemanticVersion(singleOp.Replace("<", ""));
return Compare(version, versionDep) < 0;
}

return Compare(version, new SemanticVersion(singleOp)) == 0;
}

public static bool operator >(SemanticVersion a, SemanticVersion b)
Expand Down
5 changes: 5 additions & 0 deletions Knossos.NET/Models/Mod.cs
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@ private List<ModDependency> FilterDependencies(List<ModDependency> unFilteredDep
{
temp.Remove(d);
}
else if (SemanticVersion.IsComplexConstraint(d.version))
{
//Range syntax (e.g. "[1.0,2.0)" or ">=1.0 <2.0") cannot be safely stripped
//to a bare version for the dedup comparisons below; leave in temp as-is.
}
else
{
if (d.version.Contains(">="))
Expand Down
Loading