Skip to content
Merged
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
69 changes: 55 additions & 14 deletions Knossos.NET/ViewModels/Templates/DevModPkgMgrViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Knossos.NET.Classes;
using Knossos.NET.Models;
using Knossos.NET.Views;
using System;
Expand All @@ -26,6 +27,11 @@ public partial class EditorDependencyItem : ObservableObject
[ObservableProperty]
internal bool displayPackages = false;

//True when the dep was loaded with a range/complex constraint that the dropdowns can't represent.
//Cleared on any user interaction with the mod, version, or operator dropdowns. While set,
//GetDependency() returns the original Dependency unchanged so the constraint round-trips intact.
private bool preserveOriginalConstraint = false;

internal int versionSelectedIndex = 0;
internal int VersionSelectedIndex
{
Expand All @@ -38,6 +44,7 @@ internal int VersionSelectedIndex
if(versionSelectedIndex != value)
{
SetProperty(ref versionSelectedIndex, value);
preserveOriginalConstraint = false;
FillPackages();
}
}
Expand All @@ -46,6 +53,19 @@ internal int VersionSelectedIndex
[ObservableProperty]
internal int versionTypeIndex = 0;

partial void OnVersionTypeIndexChanged(int value)
{
if (preserveOriginalConstraint)
{
//User edited the operator while a complex constraint was preserved — exit preserve mode
//and rebuild the version dropdown so the placeholder is gone and a real version is selected.
preserveOriginalConstraint = false;
VersionItems.Clear();
FillAllVersions();
VersionSelectedIndex = 1;
}
}

internal int modSelectedIndex = 0;
internal int ModSelectedIndex
{
Expand All @@ -58,6 +78,7 @@ internal int ModSelectedIndex
if (modSelectedIndex != value)
{
SetProperty(ref modSelectedIndex, value);
preserveOriginalConstraint = false;
VersionItems.Clear();
FillAllVersions();
VersionSelectedIndex = 1;
Expand Down Expand Up @@ -90,25 +111,39 @@ public EditorDependencyItem(ModDependency dep, EditorModPackageItem pkgItem, str

FillAllVersions();

versionTypeIndex = OperatorTypeIndexFromVersion(dep.version);

var bareVersion = dep.version != null ? StripVersionOperators(dep.version) : null;
var currentVersion = VersionItems.FirstOrDefault(x => x.Content != null && bareVersion != null && x.Content.ToString() == bareVersion);
if (currentVersion != null)
{
versionSelectedIndex = VersionItems.IndexOf(currentVersion);
}
else if (!string.IsNullOrEmpty(bareVersion))
if (SemanticVersion.IsComplexConstraint(dep.version))
{
//Requested version isn't installed — surface it as its own entry so the UI matches the JSON.
var itemVer = new ComboBoxItem();
itemVer.Content = bareVersion;
VersionItems.Add(itemVer);
//Range syntax cannot be expressed via the operator+version dropdowns; show the original
//string in a placeholder item and enter preserve mode so GetDependency() round-trips
//the dep unchanged unless the user edits one of the dropdowns.
preserveOriginalConstraint = true;
var complexItem = new ComboBoxItem { Content = dep.version };
VersionItems.Add(complexItem);
versionSelectedIndex = VersionItems.Count - 1;
versionTypeIndex = 0;
}
else
{
VersionSelectedIndex = 0;
versionTypeIndex = OperatorTypeIndexFromVersion(dep.version);

var bareVersion = dep.version != null ? StripVersionOperators(dep.version) : null;
var currentVersion = VersionItems.FirstOrDefault(x => x.Content != null && bareVersion != null && x.Content.ToString() == bareVersion);
if (currentVersion != null)
{
versionSelectedIndex = VersionItems.IndexOf(currentVersion);
}
else if (!string.IsNullOrEmpty(bareVersion))
{
//Requested version isn't installed — surface it as its own entry so the UI matches the JSON.
var itemVer = new ComboBoxItem();
itemVer.Content = bareVersion;
VersionItems.Add(itemVer);
versionSelectedIndex = VersionItems.Count - 1;
}
else
{
VersionSelectedIndex = 0;
}
}

FillPackages();
Expand Down Expand Up @@ -291,6 +326,12 @@ internal void ReloadDependency()
return Dependency;
}

//If the version is a preserved complex constraint and the user hasn't touched anything, round-trip it unchanged
if (preserveOriginalConstraint)
{
return Dependency;
}

var depId = ModItems[ModSelectedIndex].Tag as string;
var depVersion = VersionItems[VersionSelectedIndex].Content as string;

Expand Down
Loading