// Copyright Keysight Technologies 2012-2019
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at http://mozilla.org/MPL/2.0/.
using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
namespace OpenTap.Package
{
///
/// Holds search parameters that specifies a range of packages in the OpenTAP package system.
///
public class PackageSpecifier
{
/// Gets a readable string for this package specifier.
public override string ToString()
{
var versionString = Version == VersionSpecifier.AnyRelease ? "Any Release" : Version.ToString();
return $"[{Name} ({versionString})]";
}
///
/// Search for parameters that specifies a range of packages in the OpenTAP package system. Unset parameters will be treated as 'any'.
///
public PackageSpecifier(string name = null, VersionSpecifier version = default(VersionSpecifier), CpuArchitecture architecture = CpuArchitecture.Unspecified, string os = null)
{
Name = name;
Version = version ?? VersionSpecifier.Any;
Architecture = architecture;
OS = os;
}
///
/// Search parameters that specify an exact or a version compatible match to the given package/identifier.
///
public PackageSpecifier(IPackageIdentifier package, VersionMatchBehavior versionMatchBehavior = VersionMatchBehavior.Exact)
: this(package.Name, new VersionSpecifier(package.Version, versionMatchBehavior), package.Architecture, package.OS)
{
}
///
/// The name of the package. Can be null to indicate "any name".
///
public string Name { get; }
///
/// Specifying requirements to the version the package. Never null.
///
public VersionSpecifier Version { get; }
///
/// The CPU Architechture of the package.
///
public CpuArchitecture Architecture { get; }
///
/// Comma seperated list of operating systems that this package can run on.
///
public string OS { get; }
}
///
/// Specifies parts of a semantic version. This is used in to represent the part of a to search for.
/// E.g. the VersionSpecifier "9.0" may match the semantic version "9.0.4+abcdef" and also "9.1.x" if is set to "Compatible".
///
public class VersionSpecifier : IComparable
{
///
/// The VersionSpecifier that will match any version. VersionSpecifier.Any.IsCompatible always returns true.
///
public static readonly VersionSpecifier Any = new VersionSpecifier(null, null, null, null, null, VersionMatchBehavior.Exact | VersionMatchBehavior.AnyPrerelease);
///
/// The VersionSpecifier that will match any version. VersionSpecifier.Any.IsCompatible always returns true.
///
public static readonly VersionSpecifier AnyRelease = new VersionSpecifier(null, null, null, null, null, VersionMatchBehavior.Exact);
///
/// Major version. When not null, will return false for s with a Major version different from this.
///
public readonly int? Major;
///
/// Minor version. When not null, will return false for s with a Minor version less than this (with ) or different from this (with ).
///
public readonly int? Minor;
///
/// Patch version. When not null, will return false for s with a Patch version different from this if is .
///
public readonly int? Patch;
///
/// PreRelease identifier. will return false for s with a PreRelease less than this (with ) or different from this (with ).
///
public readonly string PreRelease;
///
/// BuildMetadata identifier. When not null, will return false for s with a BuildMetadata different from this if is .
///
public readonly string BuildMetadata;
///
/// The way matching is done. This affects the behavior of .
///
public readonly VersionMatchBehavior MatchBehavior;
///
/// Specifies parts of a semantic version. Unset parameters will be treated as 'any'.
///
///
public VersionSpecifier(int? major, int? minor, int? patch, string prerelease, string buildMetadata, VersionMatchBehavior matchBehavior)
{
if (major == null && minor != null)
throw new ArgumentException();
if (minor == null && patch != null)
throw new ArgumentException();
Major = major;
Minor = minor;
Patch = patch;
PreRelease = prerelease;
BuildMetadata = buildMetadata;
MatchBehavior = matchBehavior;
}
///
/// Creates a VersionSpecifier from a .
///
public VersionSpecifier(SemanticVersion ver, VersionMatchBehavior matchBehavior) : this(ver?.Major, ver?.Minor, ver?.Patch, ver?.PreRelease, ver?.BuildMetadata, matchBehavior)
{
}
static Regex parser = new Regex(@"^(?\^)?((?\d+)(\.(?\d+)(\.(?\d+))?)?)?(-(?([a-zA-Z0-9-\.]+)))?(\+(?[a-zA-Z0-9-\.]+))?$", RegexOptions.Compiled);
static Regex semVerPrereleaseRegex = new Regex(@"^(?\^)?(?([a-zA-Z0-9-\.]+))$", RegexOptions.Compiled);
///
/// Parses a string as a VersionSpecifier.
///
public static bool TryParse(string version, out VersionSpecifier ver)
{
if (version != null)
{
if (version.Equals("Any", StringComparison.OrdinalIgnoreCase))
{
ver = VersionSpecifier.Any;
return true;
}
if (string.IsNullOrEmpty(version))
{
ver = AnyRelease;
return true;
}
var m = parser.Match(version);
if (m.Success)
{
ver = new VersionSpecifier(
m.Groups["major"].Success ? (int?)int.Parse(m.Groups["major"].Value) : null,
m.Groups["minor"].Success ? (int?)int.Parse(m.Groups["minor"].Value) : null,
m.Groups["patch"].Success ? (int?)int.Parse(m.Groups["patch"].Value) : null,
m.Groups["prerelease"].Success ? m.Groups["prerelease"].Value : null,
m.Groups["metadata"].Success ? m.Groups["metadata"].Value : null,
m.Groups["compatible"].Success ? VersionMatchBehavior.Compatible : VersionMatchBehavior.Exact
);
return true;
}
var prerelease = semVerPrereleaseRegex.Match(version);
if (prerelease.Success)
{
var matchBehaviour = prerelease.Groups["compatible"].Success ? VersionMatchBehavior.Compatible : VersionMatchBehavior.Exact;
var pre = prerelease.Groups["prerelease"].Success ? prerelease.Groups["prerelease"].Value : null;
ver = new VersionSpecifier(null, null, null, pre, null, matchBehaviour);
return true;
}
}
ver = default(VersionSpecifier);
return false;
}
///
/// Parses a string as a VersionSpecifier.
///
/// The string is not a valid version specifier.
public static VersionSpecifier Parse(string version)
{
if (TryParse(version, out var ver))
return ver;
throw new FormatException($"The string '{version}' is not a valid version specifier.");
}
///
/// Converts this value to a string. This string can be parsed by and .
///
public override string ToString()
{
if (this == VersionSpecifier.Any)
return "Any";
if (this == VersionSpecifier.AnyRelease)
return "";
var formatter = versionFormatter.Value;
formatter.Clear();
if (MatchBehavior.HasFlag(VersionMatchBehavior.Compatible))
formatter.Append('^');
if (Major.HasValue)
formatter.Append(Major);
if (Minor.HasValue)
{
formatter.Append('.');
formatter.Append(Minor);
}
if (Patch.HasValue)
{
formatter.Append('.');
formatter.Append(Patch);
}
if (!string.IsNullOrEmpty(PreRelease))
{
if (formatter.Length != 0)
formatter.Append('-');
formatter.Append(PreRelease);
}
if (!string.IsNullOrEmpty(BuildMetadata))
{
formatter.Append('+');
formatter.Append(BuildMetadata);
}
return formatter.ToString();
}
static ThreadLocal versionFormatter = new ThreadLocal(() => new StringBuilder(), false);
///
/// Prints the string in version format. It should be parsable from the same string.
///
/// Number of values to return. Must be 1, 2, 4 or 5.
///
///
public string ToString(int fieldCount)
{
if (fieldCount < 1 || fieldCount > 5)
throw new ArgumentOutOfRangeException();
var formatter = versionFormatter.Value;
formatter.Clear();
if (this == VersionSpecifier.Any)
return "Any";
if (MatchBehavior.HasFlag(VersionMatchBehavior.Compatible))
formatter.Append('^');
if (Major.HasValue)
formatter.Append(Major);
if (Minor.HasValue && fieldCount >= 2)
{
formatter.Append('.');
formatter.Append(Minor);
}
if (Patch.HasValue && fieldCount >= 3)
{
formatter.Append('.');
formatter.Append(Patch);
}
if (!string.IsNullOrWhiteSpace(PreRelease) && fieldCount >= 4)
{
formatter.Append('-');
formatter.Append(PreRelease);
}
if (!string.IsNullOrWhiteSpace(BuildMetadata) && fieldCount == 5)
{
formatter.Append('+');
formatter.Append(BuildMetadata);
}
return formatter.ToString();
}
///
/// Compatibility comparison that returns true if the given version can fulfil this specification. I.e. 'actualVersion' can replace 'this' in every respect.
///
///
///
public bool IsCompatible(SemanticVersion actualVersion)
{
if (ReferenceEquals(this, VersionSpecifier.Any))
return true; // this is just a small performance shortcut. The below logic would have given the same result.
if (ReferenceEquals(this, VersionSpecifier.AnyRelease))
return actualVersion.PreRelease == null; // this is just a small performance shortcut. The below logic would have given the same result.
if (MatchBehavior == VersionMatchBehavior.Exact)
return MatchExact(actualVersion);
if (MatchBehavior.HasFlag(VersionMatchBehavior.Compatible))
return MatchCompatible(actualVersion);
return false;
}
private bool MatchExact(SemanticVersion actualVersion)
{
if (actualVersion == null)
return false;
if (Major.HasValue && Major.Value != actualVersion.Major)
return false;
if (Minor.HasValue && Minor.Value != actualVersion.Minor)
return false;
if (Patch.HasValue && Patch.Value != actualVersion.Patch)
return false;
if (MatchBehavior.HasFlag(VersionMatchBehavior.AnyPrerelease))
return true;
if (PreRelease != actualVersion.PreRelease)
{
if (PreRelease is null || actualVersion.PreRelease is null)
return false;
string[] actualPreReleaseIdentifiers = actualVersion.PreRelease.Split('.');
string[] preReleaseIdentifiers = PreRelease.Split('.');
if (actualPreReleaseIdentifiers.Length < preReleaseIdentifiers.Length)
return false;
for (int i = 0; i < preReleaseIdentifiers.Length; i++)
if (!actualPreReleaseIdentifiers[i].Equals(preReleaseIdentifiers[i]))
return false;
}
if (string.IsNullOrEmpty(BuildMetadata) == false && BuildMetadata != actualVersion.BuildMetadata)
return false;
return true;
}
private bool MatchCompatible(SemanticVersion actualVersion)
{
if (actualVersion == null)
return true;
if (Major.HasValue && Major.Value != actualVersion.Major)
return false;
if (Minor.HasValue && Minor.Value > actualVersion.Minor)
return false;
if (Minor.HasValue && Minor.Value == actualVersion.Minor)
if (Patch.HasValue && Patch.Value > actualVersion.Patch)
return false;
if (MatchBehavior.HasFlag(VersionMatchBehavior.AnyPrerelease))
return true;
// We want ^1.0.0 to accept 1.0.1-beta or 1.1.0-beta as compatible versions
if(actualVersion.PreRelease != null)
{
if (Minor.HasValue && Minor.Value < actualVersion.Minor)
return true;
if (Minor.HasValue && Minor.Value == actualVersion.Minor)
if (Patch.HasValue && Patch.Value < actualVersion.Patch)
return true;
// In short: We want ^1 to accept 1.x.x-beta
// In long: If minor and patch are not specified, then this version specifier is underdetermined.
// If the prerelease is also not specified that means the version specifier should be satisfiable by
// any prerelease within the appropriate major.
if (PreRelease == null && !Minor.HasValue && !Patch.HasValue && Major.HasValue &&
Major.Value == actualVersion.Major)
return true;
}
if (0 < ComparePreRelease(PreRelease, actualVersion.PreRelease))
return false;
return true;
}
///
/// Gets the hash code of this value.
///
public override int GetHashCode()
{
return ToString().GetHashCode();
}
///
/// Compares this VersionSpecifier with another object.
///
public override bool Equals(object obj)
{
if (obj is VersionSpecifier other)
{
if (Major != other.Major)
return false;
if (Minor != other.Minor)
return false;
if (Patch != other.Patch)
return false;
if (PreRelease != other.PreRelease)
return false;
if (MatchBehavior != other.MatchBehavior)
return false;
return true;
}
return false;
}
///
/// Returns -1 if obj is greater than this version, 0 if they are the same, and 1 if this is greater than obj
///
///
///
public int CompareTo(object obj)
{
if (!(obj is VersionSpecifier))
throw new ArgumentException("Object is not a TapVersion");
VersionSpecifier other = (VersionSpecifier)obj;
if (Major > other.Major) return 1;
if (Major < other.Major) return -1;
if (Minor.HasValue && !other.Minor.HasValue || Minor > other.Minor) return 1;
if (!Minor.HasValue && other.Minor.HasValue || Minor < other.Minor) return -1;
if (Patch.HasValue && !other.Patch.HasValue || Patch > other.Patch) return 1;
if (!Patch.HasValue && other.Patch.HasValue || Patch < other.Patch) return -1;
return ComparePreRelease(PreRelease, other.PreRelease);
}
class PartialComparer : IComparer
{
readonly VersionSpecifier pkg;
public PartialComparer(VersionSpecifier pkg) => this.pkg = pkg;
public int Compare(SemanticVersion a, SemanticVersion b)
{
// If pre-releases are not wanted, order them according to stability. E.g. alphas go at the end
if (pkg.PreRelease == null && (a.PreRelease != null || b.PreRelease != null))
{
if (a.PreRelease == null && b.PreRelease != null) return -1;
if (a.PreRelease != null && b.PreRelease == null) return 1;
}
if (pkg.Major.HasValue)
{
var m = a.Major.CompareTo(b.Major);
if (m != 0) return m;
}
if (pkg.Minor.HasValue)
{
var m = a.Minor.CompareTo(b.Minor);
if (m != 0) return m;
}
if (pkg.Patch.HasValue)
{
var m = a.Patch.CompareTo(b.Patch);
if (m != 0) return m;
}
// If no prerelease is specified, prefer the most "stable" version
// e.g. release > rc > beta > alpha, preferring newer versions
if (pkg.PreRelease == null)
return CompareStability(a, b);
// Otherwise just sort by prerelease
if (a.PreRelease == b.PreRelease) return 0;
if (a.PreRelease == null && b.PreRelease != null) return 1;
if (a.PreRelease != null && b.PreRelease == null) return -1;
return ComparePreRelease(a.PreRelease, b.PreRelease);
}
}
private static int CompareStability(SemanticVersion a, SemanticVersion b)
{
// If neither is a prerelease, assume the newest version is the most stable
if (a.PreRelease == null && b.PreRelease == null) return b.CompareTo(a);
// Prefer releases over everything else
if (a.PreRelease == null && b.PreRelease != null) return -1;
if (a.PreRelease != null && b.PreRelease == null) return 1;
string kind(string prerelease)
{
var idx = prerelease.IndexOf('.');
if (idx == -1) idx = prerelease.Length;
return prerelease.Substring(0, idx);
}
// Prefer rc > beta > alpha
var k1 = kind(a.PreRelease);
var k2 = kind(b.PreRelease);
var c = string.Compare(k2, k1, StringComparison.Ordinal);
if (c != 0) return c;
// Otherwise prefer the highest version number
return b.CompareTo(a);
}
///
/// This sorts versions based on a partially defined version set. For example ^1 would sort based on major version only, but ignore the rest.
/// This is useful when using in connection with other stable sortings.
///
internal IComparer SortPartial => new PartialComparer(this);
/// A version is exact if the match behavior is exact and all version fields are specified.
internal bool IsExact => MatchBehavior == VersionMatchBehavior.Exact && Major.HasValue && Minor.HasValue &&
Patch.HasValue;
internal static int ComparePreRelease(string p1, string p2)
{
if (p1 == p2) return 0;
if (string.IsNullOrEmpty(p1) && string.IsNullOrEmpty(p2)) return 0;
if (string.IsNullOrEmpty(p1)) return 1;
if (string.IsNullOrEmpty(p2)) return -1;
var identifiers1 = p1.Split('.');
var identifiers2 = p2.Split('.');
for (int i = 0; i < Math.Min(identifiers1.Length, identifiers2.Length); i++)
{
var id1 = identifiers1[i];
var id2 = identifiers2[i];
int v1, v2;
if (int.TryParse(id1, out v1) && int.TryParse(id2, out v2))
{
if (v1 != v2)
return v1.CompareTo(v2);
}
else
{
var res = string.Compare(id1, id2);
if (res != 0)
return res;
}
}
if (identifiers1.Length > identifiers2.Length) return 1;
if (identifiers1.Length < identifiers2.Length) return -1;
return 0;
}
///
/// Overloaded == operator that provides value equality (instead of the default reference equality)
///
public static bool operator ==(VersionSpecifier a, VersionSpecifier b)
{
return Object.Equals(a, b);
}
///
/// Overloaded != operator that provides value equality (instead of the default reference equality)
///
public static bool operator !=(VersionSpecifier a, VersionSpecifier b)
{
return !(a == b);
}
internal bool TryAsExactSemanticVersion(out SemanticVersion semver)
{
if (MatchBehavior == VersionMatchBehavior.Exact && Major.HasValue && Minor.HasValue && Patch.HasValue)
{
semver = new SemanticVersion(Major.Value, Minor.Value, Patch.Value, PreRelease, BuildMetadata);
return true;
}
semver = default;
return false;
}
internal VersionSpecifier WithMatchBehavior(VersionMatchBehavior matchBehavior) => new VersionSpecifier(Major, Minor, Patch, PreRelease, BuildMetadata, matchBehavior);
}
///
/// Describes the behavior of .
///
[Flags]
public enum VersionMatchBehavior
{
///
/// The must match all (non-null) fields in the specified in a for to return true.
///
Exact = 1,
///
/// The must be compatible with the version specified in a for to return true.
///
Compatible = 2,
///
/// Prerelease property of is ignored when looking for matching packages.
///
AnyPrerelease = 4,
}
}