// 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, } }