static void InlineAssertInvariants(CSVersion v) { #if DEBUG if (!_alreadyInCheck && v.IsValid) { _alreadyInCheck = true; try { if (v.IsLongForm) { Debug.Assert(v.NormalizedText == ComputeLongFormVersion(v.Major, v.Minor, v.Patch, v.PrereleaseNameIdx, v.PrereleaseNumber, v.PrereleasePatch, v.BuildMetaData)); } else { Debug.Assert(v.NormalizedText == ComputeShortFormVersion(v.Major, v.Minor, v.Patch, v.PrereleaseNameIdx, v.PrereleaseNumber, v.PrereleasePatch, v.BuildMetaData)); } //// Systematically checks that a valid CSVersion can be parsed back in Long or Short form. Debug.Assert(SVersion.TryParse(v.ToString(CSVersionFormat.Normalized)).Equals(v.ToNormalizedForm())); Debug.Assert(SVersion.TryParse(v.ToString(CSVersionFormat.LongForm)).Equals(v.ToLongForm())); } finally { _alreadyInCheck = false; } } #endif }
/// <summary> /// Parses the specified string to a constrained semantic version and throws an <see cref="ArgumentException"/> /// it the resulting <see cref="SVersion"/> is not a <see cref="CSVersion"/> or <see cref="SVersion.IsValid"/> is false. /// </summary> /// <param name="s">The string to parse.</param> /// <param name="checkBuildMetaDataSyntax">False to opt-out of strict <see cref="SVersion.BuildMetaData"/> compliance.</param> /// <returns>The CSVersion object.</returns> public static CSVersion Parse(string s, bool checkBuildMetaDataSyntax = true) { SVersion sv = SVersion.TryParse(s, true, checkBuildMetaDataSyntax); if (!sv.IsValid) { throw new ArgumentException(sv.ErrorMessage, nameof(s)); } return(sv as CSVersion ?? throw new ArgumentException("Not a CSVersion.", nameof(s))); }
/// <summary> /// Initializes a new <see cref="InformationalVersion"/> by parsing a string. /// This never throws: <see cref="IsValidSyntax"/> may be false and <see cref="ParseErrorMessage"/> exposes /// the error message. /// </summary> /// <param name="informationalVersion">Informational version. Can be null.</param> public InformationalVersion(string?informationalVersion) { if ((OriginalInformationalVersion = informationalVersion) != null) { Match m = _rV7.Match(informationalVersion); if (!m.Success) { m = _rV6.Match(informationalVersion); } if (!m.Success) { m = _rOld.Match(informationalVersion); } if (m.Success) { RawVersion = m.Groups[2].Value; CommitSha = m.Groups[3].Value; var v = SVersion.TryParse(RawVersion); Version = v.AsCSVersion != null?v.AsCSVersion.ToNormalizedForm() : v; if (DateTime.TryParseExact(m.Groups[4].Value, "u", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out DateTime t)) { CommitDate = t; if (t.Kind != DateTimeKind.Utc) { ParseErrorMessage = $"The CommitDate must be Utc: {m.Groups[4].Value} must be {DateTime.SpecifyKind( t, DateTimeKind.Utc ):u}."; } else if (!Version.IsValid) { ParseErrorMessage = "The SemVersion is invalid: " + Version.ErrorMessage; } else if (CommitSha.Length != 40 || !CommitSha.All(IsHexDigit)) { ParseErrorMessage = "The CommitSha is invalid (must be 40 hex digit)."; } else { IsValidSyntax = true; } } else { ParseErrorMessage = "The CommitDate is invalid.It must be a UTC DateTime in \"u\" format."; } } else { ParseErrorMessage = "The String to parse does not match the standard CSemVer informational version pattern."; } } else { ParseErrorMessage = "String to parse is null."; } }
/// <summary> /// Parses the specified string to a constrained semantic version and returns a <see cref="CSVersion"/> that /// may not be <see cref="SVersion.IsValid"/>. /// </summary> /// <param name="s">The string to parse.</param> /// <param name="checkBuildMetaDataSyntax">False to opt-out of strict <see cref="SVersion.BuildMetaData"/> compliance.</param> /// <returns>The CSVersion object that may not be <see cref="SVersion.IsValid"/>.</returns> public static CSVersion TryParse(string s, bool checkBuildMetaDataSyntax = true) { SVersion sv = SVersion.TryParse(s, true, checkBuildMetaDataSyntax); if (sv is CSVersion v) { return(v); } Debug.Assert(sv.IsValid == (sv.ErrorMessage == null)); return(new CSVersion(sv.ErrorMessage ?? "Not a CSVersion.", s)); }
/// <summary> /// Standard TryParse pattern that returns a boolean rather than the resulting <see cref="CSVersion"/>. /// See <see cref="TryParse(string,bool)"/>. /// </summary> /// <param name="s">String to parse.</param> /// <param name="v">Resulting version.</param> /// <param name="checkBuildMetaDataSyntax">False to opt-out of strict <see cref="SVersion.BuildMetaData"/> compliance.</param> /// <returns>True on success, false otherwise.</returns> public static bool TryParse(string s, out CSVersion v, bool checkBuildMetaDataSyntax = true) { v = null; SVersion sv = SVersion.TryParse(s, true, checkBuildMetaDataSyntax); if (!sv.IsValid) { return(false); } v = sv as CSVersion; return(v != null); }
/// <summary> /// Gets the string version in the given format. /// Returns the <see cref="SVersion.ErrorMessage"/> if it is not null. /// </summary> /// <param name="f">Format to use.</param> /// <param name="buildInfo">Not null to generate a post-release version.</param> /// <returns>Formated string (or <see cref="SVersion.ErrorMessage"/> if any).</returns> public string ToString(CSVersionFormat f, CIBuildDescriptor?buildInfo = null) { if (ErrorMessage != null) { return(ErrorMessage); } Debug.Assert(NormalizedText != null); // Fast path and cache for format with no build info. if (buildInfo == null) { if (f == CSVersionFormat.Normalized) { if (IsLongForm) { if (_cacheOtherForm == null) { _cacheOtherForm = ComputeShortFormVersion(Major, Minor, Patch, PrereleaseNameIdx, PrereleaseNumber, PrereleasePatch, BuildMetaData, null); } return(_cacheOtherForm); } return(NormalizedText); } if (f == CSVersionFormat.LongForm) { if (IsLongForm) { return(NormalizedText); } if (_cacheOtherForm == null) { _cacheOtherForm = ComputeLongFormVersion(Major, Minor, Patch, PrereleaseNameIdx, PrereleaseNumber, PrereleasePatch, BuildMetaData, null); } return(_cacheOtherForm); } } if (f == CSVersionFormat.FileVersion) { return(ToStringFileVersion(buildInfo != null)); } if (f == CSVersionFormat.LongForm) { return(ComputeLongFormVersion(Major, Minor, Patch, PrereleaseNameIdx, PrereleaseNumber, PrereleasePatch, BuildMetaData, buildInfo)); } else { Debug.Assert(f == CSVersionFormat.Normalized); Debug.Assert(SVersion.Parse(ComputeShortFormVersion(Major, Minor, Patch, PrereleaseNameIdx, PrereleaseNumber, PrereleasePatch, BuildMetaData, buildInfo)).PackageQuality == PackageQuality.CI); return(ComputeShortFormVersion(Major, Minor, Patch, PrereleaseNameIdx, PrereleaseNumber, PrereleasePatch, BuildMetaData, buildInfo)); } }
/// <summary> /// Parses the specified string to a constrained semantic version and returns a <see cref="CSVersion"/> that /// may not be <see cref="SVersion.IsValid"/>. /// </summary> /// <param name="s">The string to parse.</param> /// <param name="checkBuildMetaDataSyntax">False to opt-out of strict <see cref="SVersion.BuildMetaData"/> compliance.</param> /// <returns>The CSVersion object that may not be <see cref="SVersion.IsValid"/>.</returns> public static CSVersion TryParse(string s, bool checkBuildMetaDataSyntax = true) { SVersion sv = SVersion.TryParse(s, true, checkBuildMetaDataSyntax); if (sv is CSVersion v) { return(v); } if (!sv.IsValid) { new CSVersion(sv.ErrorMessage, s); } return(new CSVersion("Not a CSVersion.", s)); }
static (SVersion?Version, int FMajor, int FMinor, string?Error, bool FourtPartLost) TryMatchFloatingVersion(ref ReadOnlySpan <char> s) { // Handling the marvelous "" (empty string), that is like '*'. if (s.Length == 0) { return(null, -1, 0, null, false); } var version = SVersion.TryParse(ref s); if (version.IsValid) { // If only the 3 first parts have been read: launch SkipExtraPartsAndPrereleaseIfAny on the head to skip // potential extra parts and prerelease. return(version, 0, 0, null, SkipExtraPartsAndPrereleaseIfAny(ref s)); } int major, minor = -1; if (TryMatchXStarInt(ref s, out major)) { if (major >= 0) { if (s.Length > 0 && TryMatch(ref s, '.')) { if (s.Length == 0 || !TryMatchXStarInt(ref s, out minor)) { return(null, 0, 0, "Expecting minor number or *.", false); } if (minor >= 0) { // If a fourth part caused the version parse to fail, handle it here. if (s.Length > 0 && TryMatch(ref s, '.') && s.Length > 0 && TryMatchNonNegativeInt(ref s, out int patch)) { return(SVersion.Create(major, minor, patch), 0, 0, null, SkipExtraPartsAndPrereleaseIfAny(ref s)); } } } } else { minor = 0; } // Forgetting any trailing "X.Y.*" since it is like "X.Y". // Even if the npm grammar allows "3.*-alpha" or "3.1.*+meta", we ignores this: https://semver.npmjs.com/ selects nothing. // We consider this stupid trail as being FourthPartLost. return(null, major, minor, null, SkipExtraPartsAndPrereleaseIfAny(ref s)); } return(null, 0, 0, version.ErrorMessage, false); }
/// <summary> /// Builds a standard Informational version string. /// </summary> /// <param name="version">The version. Must not be null nor invalid.</param> /// <param name="commitSha">The SHA1 of the commit (must be 40 hex digits).</param> /// <param name="commitDateUtc">The commit date (must be in UTC).</param> /// <returns>The informational version.</returns> static public string BuildInformationalVersion(SVersion version, string commitSha, DateTime commitDateUtc) { if (version == null || !version.IsValid) { throw new ArgumentException(nameof(version)); } if (commitSha == null || commitSha.Length != 40 || !commitSha.All(IsHexDigit)) { throw new ArgumentException("Must be a 40 hex digits string.", nameof(commitSha)); } if (commitDateUtc.Kind != DateTimeKind.Utc) { throw new ArgumentException("Must be a UTC date.", nameof(commitDateUtc)); } return($"{version.ToNormalizedString()}/{commitSha}/{commitDateUtc:u}"); }
static (ParseResult Result, bool IsFloatingMinor) TryMatchRangeAlone(ref ReadOnlySpan <char> s, SVersionLock defaultBound, bool includePrerelease) { var r = TryMatchFloatingVersion(ref s); if (r.Error != null) { return(new ParseResult(r.Error), false); } PackageQuality quality = includePrerelease ? PackageQuality.None : PackageQuality.Stable; if (r.Version != null) { // As soon as a prerelease appears, this can only be an approximation (with one exception - see below) since for npm: // // "For example, the range >1.2.3-alpha.3 would be allowed to match the version 1.2.3-alpha.7, but it // would not be satisfied by 3.4.5-alpha.9, even though 3.4.5-alpha.9 is technically "greater than" // 1.2.3-alpha.3 according to the SemVer sort rules. The version range only accepts prerelease tags // on the 1.2.3 version. The version 3.4.5 would satisfy the range, because it does not have a prerelease // flag, and 3.4.5 is greater than 1.2.3-alpha.7." // // We also set the MinQuality to CI (otherwise alpha.7 will not be authorized for alpha.3) regardless of the includePrerelease. // Moreover, if we are coming from the ~ (tilde range), the lock is on the patch, not on the minor, and this exactly matches the // npm behavior. // bool isApproximated = !includePrerelease; if (r.Version.IsPrerelease) { quality = PackageQuality.None; if (defaultBound == SVersionLock.LockMinor) { defaultBound = SVersionLock.LockPatch; isApproximated = false; } } return(new ParseResult(new SVersionBound(r.Version, defaultBound, quality), isApproximated, r.FourtPartLost), false); } if (r.FMajor < 0) { return(new ParseResult(new SVersionBound(_000Version, SVersionLock.None, quality), isApproximated: !includePrerelease, false), false); } if (r.FMinor < 0) { return(new ParseResult(new SVersionBound(SVersion.Create(r.FMajor, 0, 0), SVersionLock.LockMajor, quality), isApproximated: !includePrerelease, false), true); } return(new ParseResult(new SVersionBound(SVersion.Create(r.FMajor, r.FMinor, 0), SVersionLock.LockMinor, quality), isApproximated: !includePrerelease, false), false); }
/// <summary> /// Protected copy constructor with <see cref="BuildMetaData"/>. /// </summary> /// <param name="other">Origin version.</param> /// <param name="buildMetaData">New BuildMetaData. Must not be null.</param> /// <param name="csVersion">Companion CSVersion.</param> protected SVersion(SVersion other, string buildMetaData, CSVersion?csVersion) { if (other == null) { throw new ArgumentNullException(nameof(other)); } if (!other.IsValid) { throw new InvalidOperationException("Version must be valid."); } _csVersion = csVersion ?? (this as CSVersion); Major = other.Major; Minor = other.Minor; Patch = other.Patch; Prerelease = other.Prerelease; BuildMetaData = buildMetaData ?? throw new ArgumentNullException(nameof(buildMetaData)); NormalizedText = ComputeNormalizedText(Major, Minor, Patch, Prerelease, buildMetaData); }
static void InlineAssertInvariants(CSVersion v) { #if DEBUG if (!_alreadyInCheck && v.IsValid) { _alreadyInCheck = true; try { //// Systematically checks that a valid CSVersion can be parsed back in Long or Short form. Debug.Assert(SVersion.TryParse(v.ToString(CSVersionFormat.Normalized)).Equals(v)); Debug.Assert(SVersion.TryParse(v.ToString(CSVersionFormat.ShortForm)).Equals(v)); } finally { _alreadyInCheck = false; } } #endif }
int CompareValid(SVersion other) { var r = Major - other.Major; if (r != 0) { return(r); } r = Minor - other.Minor; if (r != 0) { return(r); } r = Patch - other.Patch; if (r != 0) { return(r); } return(ComparePreRelease(Prerelease, other.Prerelease)); }
static SVersion TryParseVersion(ref ReadOnlySpan <char> s, out bool fourthPartLost) { Debug.Assert(s.Length > 0); fourthPartLost = false; var v = SVersion.TryParse(ref s); if (v.IsValid) { // If only the 3 first parts have been read... fourthPartLost = SkipExtraPartsAndPrereleaseIfAny(ref s); } else { if (TryMatchNonNegativeInt(ref s, out int major)) { if (s.Length == 0 || !TryMatch(ref s, '.')) { return(SVersion.Create(major, 0, 0)); } if (!TryMatchNonNegativeInt(ref s, out int minor)) { return(new SVersion("Expected Nuget minor part.", null)); } // Try to save the fourth part: in such case the patch is read. int patch = 0; if (s.Length > 0 && TryMatch(ref s, '.') && s.Length > 0 && TryMatchNonNegativeInt(ref s, out patch) && s.Length > 0 && TryMatch(ref s, '.') && s.Length > 0 && TryMatchNonNegativeInt(ref s, out int _)) { fourthPartLost = true; } return(SVersion.Create(major, minor, patch)); } } return(v); }
/// <summary> /// Tries to parse a version bound: it is a <see cref="SVersion.TryParse(ref ReadOnlySpan{char}, bool, bool, bool)"/> that may be /// followed by an optional bracketed "[<see cref="TryParseLockAndMinQuality"/>]". /// The head is forwarded right after the match: on success, the head may be on any kind of character. /// </summary> /// <param name="head">The string to parse (leading and internal white spaces between tokens are skipped).</param> /// <param name="bound">The result. This is <see cref="SVersionBound.None"/> on error.</param> /// <param name="defaultLock">Default lock to apply if none is provided.</param> /// <param name="defaultQuality">Default quality to apply if none is provided.</param> /// <returns>True on success, false otherwise.</returns> public static bool TryParse(ref ReadOnlySpan <char> head, out SVersionBound bound, SVersionLock defaultLock = SVersionLock.None, PackageQuality defaultQuality = PackageQuality.None) { var sHead = head; bound = SVersionBound.None; var v = SVersion.TryParse(ref Trim(ref head), checkBuildMetaDataSyntax: false); if (!v.IsValid) { head = sHead; return(false); } SVersionLock l = SVersionLock.None; PackageQuality q = PackageQuality.None; if (Trim(ref head).Length > 0 && TryMatch(ref head, '[')) { // Allows empty []. Note that TryParseLockAndMinQuality calls Trim. TryParseLockAndMinQuality(ref head, out l, out q); // Match the closing ] if it's here. Ignores it if it's not here. if (Trim(ref head).Length > 0) { TryMatch(ref head, ']'); } } if (l == SVersionLock.None) { l = defaultLock; } if (q == PackageQuality.None) { q = defaultQuality; } bound = new SVersionBound(v, l, q); return(true); }
/// <summary> /// Standard TryParse pattern that returns a boolean rather than the resulting <see cref="SVersion"/>. /// See <see cref="TryParse(string,bool,bool)"/>. /// </summary> /// <param name="s">String to parse.</param> /// <param name="v">Resulting version.</param> /// <param name="handleCSVersion"> /// False to skip <see cref="CSVersion"/> conformance lookup. The resulting version /// will be a <see cref="SVersion"/> even if it is a valid <see cref="CSVersion"/>. /// This should be used in rare scenario where the normalization of a <see cref="CSVersion"/> (standardization /// of prerelease names) must not be done. /// </param> /// <param name="checkBuildMetaDataSyntax">False to opt-out of strict <see cref="BuildMetaData"/> compliance.</param> /// <returns>True on success, false otherwise.</returns> public static bool TryParse(string s, out SVersion v, bool handleCSVersion = true, bool checkBuildMetaDataSyntax = true) { v = TryParse(s, handleCSVersion, checkBuildMetaDataSyntax); return(v.IsValid); }
/// <summary> /// Gets the standard Informational version string. /// If <see cref="SVersion.IsValid"/> is false this throws an <see cref="InvalidOperationException"/>: /// the constant <see cref="InformationalVersion.ZeroInformationalVersion"/> should be used when IsValid is false. /// </summary> /// <param name="commitSha">The SHA1 of the commit (must be 40 hex digits).</param> /// <param name="commitDateUtc">The commit date (must be in UTC).</param> /// <param name="buildInfo">Can be null: not null for post-release version.</param> /// <returns>The informational version.</returns> public string GetInformationalVersion(string commitSha, DateTime commitDateUtc, CIBuildDescriptor buildInfo) { return(IsValid && buildInfo != null ? SVersion.Parse(ToString( CSVersionFormat.Normalized, buildInfo )).GetInformationalVersion(commitSha, commitDateUtc) : GetInformationalVersion(commitSha, commitDateUtc)); }