// 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.Diagnostics; using System.Text; using System.Text.RegularExpressions; using System.Threading; namespace OpenTap { /// /// Version object for OpenTAP versions. Adheres to Semantic Version 2.0 formatting and behavior, see http://semver.org. /// Supported formats: /// Major.Minor.Patch /// Major.Minor.Patch-PreRelease /// Major.Minor.Patch+BuildMetadata. /// Major.Minor.Patch-PreRelease+BuildMetadata. /// [Serializable] [DebuggerDisplay("{Major}.{Minor}.{Patch}-{PreRelease}+{BuildMetadata}")] public class SemanticVersion : IComparable { /// /// Major version. Incrementing this number signifies a backward incompatible change in the API. /// public readonly int Major; /// /// Minor version. Incrementing this number usually signifies a backward compatible addition to the API. /// public readonly int Minor; /// /// Patch version. Incrementing this number signifies a change that is both backward and forward compatible. /// public readonly int Patch; /// /// Optional build related metadata. Usually a short git commit hash (8 chars). Ignored when determining version presedence. Only ASCII alphanumeric characters and hyphen is allowed [0-9A-Za-z-] /// public readonly string BuildMetadata; /// /// Optional pre-release version, denoted by a -. Only ASCII alphanumeric characters and hyphen is allowed [0-9A-Za-z-]. /// A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. /// public readonly string PreRelease; private static Regex semVerRegex = new Regex(@"^(?\d+)\.(?\d+)(?:\.(?\d+))?(?:-(?[a-zA-Z0-9-.]+))?(?:\+(?[a-zA-Z0-9-.]+))?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture); private static Regex validChars = new Regex("^[a-zA-Z0-9-.]*$", RegexOptions.Compiled); /// /// Creates a new SemanticVersion instance /// /// Major version. Incrementing this number signifies a backward incompatible change in the API. /// Minor version. Incrementing this number usually signifies a backward compatible addition to the API. /// Patch version. Incrementing this number signifies a change that is both backward and forward compatible. /// Optional pre-release version, denoted by a -. Only ASCII alphanumeric characters and hyphen is allowed [0-9A-Za-z-]. /// Optional build related metadata. Usually a short git commit hash (8 chars). Ignored when determining version presedence. Only ASCII alphanumeric characters and hyphen is allowed [0-9A-Za-z-] public SemanticVersion(int major, int minor, int patch, string preRelease, string buildMetadata) { Major = major; Minor = minor; Patch = patch; if (preRelease != null && !validChars.IsMatch(preRelease)) throw new ArgumentException("Only ASCII alphanumeric characters, hyphen and dot is allowed (0-9, A-Z, a-z, '-' and '.').", "preRelease"); PreRelease = preRelease; if (buildMetadata != null && !validChars.IsMatch(buildMetadata)) throw new ArgumentException("Only ASCII alphanumeric characters, hyphen and dot is allowed (0-9, A-Z, a-z, '-' and '.').", "buildMetadata"); BuildMetadata = buildMetadata; } /// /// Tries to parse a SemanticVersion from string. Input must strictly adhere to http://semver.org for this method to return true. /// /// True if the string was sucessfully parsed. public static bool TryParse(string version, out SemanticVersion result) { if (version != null) { // Do real parsing of semantic versioning style versions. var match = semVerRegex.Match(version); if (match.Success) { result = new SemanticVersion( int.Parse(match.Groups["major"].Value), int.Parse(match.Groups["minor"].Value), match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0, match.Groups["prerelease"].Success ? match.Groups["prerelease"].Value : null, match.Groups["metadata"].Success ? match.Groups["metadata"].Value : null); return true; } } result = default(SemanticVersion); return false; } /// /// Parses a SemanticVersion from string. In addition to the http://semver.org format, this also supports a four value number (x.x.x.x) which will be interpreted as Major.Minor.BuildMetadata.Patch. /// The non semver format is supported to be compatible with Microsofts definition of version numbers (e.g. for .NET assemblies), see https://docs.microsoft.com/en-us/dotnet/api/system.version /// /// /// public static SemanticVersion Parse(string version) { if(TryParse(version, out SemanticVersion semver)) { return semver; } throw new FormatException("The version string is not in a Semantic Version compliant format."); } static ThreadLocal versionFormatter = new ThreadLocal(() => new StringBuilder(), false); /// /// Prints the string in version format. It should be parsable from the same string. /// /// public override string ToString() { return ToString(5); } /// /// 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(); if (fieldCount == 1) return Major.ToString(); var formatter = versionFormatter.Value; formatter.Clear(); formatter.Append(Major); formatter.Append('.'); formatter.Append(Minor); if (fieldCount == 2) return formatter.ToString(); if (Patch != int.MaxValue) { formatter.Append('.'); formatter.Append(Patch); } if (fieldCount == 3) return formatter.ToString(); if (false == string.IsNullOrWhiteSpace(PreRelease)) { formatter.Append('-'); formatter.Append(PreRelease); } if (fieldCount == 4) return formatter.ToString(); if (false == string.IsNullOrWhiteSpace(BuildMetadata)) { formatter.Append('+'); formatter.Append(BuildMetadata); } return formatter.ToString(); } /// /// Returns true if the given version is backwards compatible with this. Meaning that 'other' can replace 'this' in every respect. /// /// /// public bool IsCompatible(SemanticVersion other) { if (other == null) throw new ArgumentNullException("other"); if (other.Major != Major) return false; if (other.Minor < Minor) return false; return true; } /// /// 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 SemanticVersion)) throw new ArgumentException("Object is not a TapVersion"); SemanticVersion other = (SemanticVersion)obj; if (Major > other.Major) return 1; if (Major < other.Major) return -1; if (Minor > other.Minor) return 1; if (Minor < other.Minor) return -1; if (Patch > other.Patch) return 1; if (Patch < other.Patch) return -1; return ComparePreRelease(PreRelease, other.PreRelease); } private 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; } /// /// Returns true if the two versions are equal. /// /// /// public override bool Equals(object obj) { if (obj is SemanticVersion) return this.ToString() == obj.ToString(); return false; } /// /// Returns the hashcode for the version. /// /// public override int GetHashCode() { return this.ToString().GetHashCode(); } /// /// Overloaded == operator that provides value equality (instead of the default reference equality) /// public static bool operator ==(SemanticVersion a, SemanticVersion b) { return Object.Equals(a, b); } /// /// Overloaded != operator that provides value equality (instead of the default reference equality) /// public static bool operator !=(SemanticVersion a, SemanticVersion b) { return !(a == b); } } }