// 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);
}
}
}