| File: SemanticVersionFactory.cs | Web Access |
| Project: src\nuget-client\src\NuGet.Core\NuGet.Versioning\NuGet.Versioning.csproj (NuGet.Versioning) |
// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Diagnostics.CodeAnalysis; using System.Globalization; namespace NuGet.Versioning { public partial class SemanticVersion { // Reusable set of empty release labels internal static readonly string[] EmptyReleaseLabels = Array.Empty<string>(); /// <summary> /// Parses a SemVer string using strict SemVer rules. /// </summary> public static SemanticVersion Parse(string value) { if (!TryParse(value, out SemanticVersion? ver)) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.Invalidvalue, value), nameof(value)); } return ver; } /// <summary> /// Parse a version string /// </summary> /// <returns>false if the version is not a strict semver</returns> public static bool TryParse(string value, [NotNullWhen(true)] out SemanticVersion? version) { version = null; if (value != null) { ParseSections(value, out string? versionString, out string[]? releaseLabels, out string? buildMetadata); // null indicates the string did not meet the rules if (Version.TryParse(versionString, out Version? systemVersion)) { // validate the version string var parts = versionString!.Split('.'); if (parts.Length != 3) { // versions must be 3 parts return false; } foreach (var part in parts) { if (!IsValidPart(part, false)) { // leading zeros are not allowed return false; } } // labels if (releaseLabels != null) { for (int i = 0; i < releaseLabels.Length; i++) { if (!IsValidPart(releaseLabels[i], allowLeadingZeros: false)) { return false; } } } // build metadata if (buildMetadata != null && !IsValid(buildMetadata, true)) { return false; } var ver = NormalizeVersionValue(systemVersion); version = new SemanticVersion(version: ver, releaseLabels: releaseLabels, metadata: buildMetadata ?? string.Empty); return true; } } return false; } internal static bool IsLetterOrDigitOrDash(char c) { var x = (int)c; // "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-" return (x >= 48 && x <= 57) || (x >= 65 && x <= 90) || (x >= 97 && x <= 122) || x == 45; } internal static bool IsDigit(char c) { var x = (int)c; // "0123456789" return (x >= 48 && x <= 57); } internal static bool IsValid(string s, bool allowLeadingZeros) { var parts = s.Split('.'); // Check each part individually for (int i = 0; i < parts.Length; i++) { if (!IsValidPart(parts[i], allowLeadingZeros)) { return false; } } return true; } internal static bool IsValidPart(string s, bool allowLeadingZeros) { if (s.Length == 0) { // empty labels are not allowed return false; } // 0 is fine, but 00 is not. // 0A counts as an alpha numeric string where zeros are not counted if (!allowLeadingZeros && s.Length > 1 && s[0] == '0') { var allDigits = true; // Check if all characters are digits. // The first is already checked above for (int i = 1; i < s.Length; i++) { if (!IsDigit(s[i])) { allDigits = false; break; } } if (allDigits) { // leading zeros are not allowed in numeric labels return false; } } for (int i = 0; i < s.Length; i++) { // Verify that the part contains only allowed characters if (!IsLetterOrDigitOrDash(s[i])) { return false; } } return true; } /// <summary> /// Parse the version string into version/release/build /// The goal of this code is to take the most direct and optimized path /// to parsing and validating a semver. Regex would be much cleaner, but /// due to the number of versions created in NuGet Regex is too slow. /// </summary> internal static void ParseSections(string value, out string? versionString, out string[]? releaseLabels, out string? buildMetadata) { versionString = null; releaseLabels = null; buildMetadata = null; var versionEndPos = -1; var plusPos = -1; var end = false; for (var i = 0; i < value.Length; i++) { end = (i == value.Length - 1); if (versionEndPos < 0) { if (end || value[i] == '-' || value[i] == '+') { var endPos = i + (end ? 1 : 0); versionString = value.Substring(0, endPos); versionEndPos = i; if (value[i] == '+') { plusPos = i; } } } else if (plusPos < 0) { if (end || value[i] == '+') { var start = versionEndPos + 1; var endPos = i + (end ? 1 : 0); var releaseLabel = value.Substring(start, endPos - start); releaseLabels = releaseLabel.Split('.'); plusPos = i; } } else if (end) { var start = plusPos + 1; var endPos = i + (end ? 1 : 0); buildMetadata = value.Substring(start, endPos - start); } } } internal static Version NormalizeVersionValue(Version version) { var normalized = version; if (version.Build < 0 || version.Revision < 0) { normalized = new Version( version.Major, version.Minor, Math.Max(version.Build, 0), Math.Max(version.Revision, 0)); } return normalized; } private static string[]? ParseReleaseLabels(string? releaseLabels) { if (!string.IsNullOrEmpty(releaseLabels)) { return releaseLabels!.Split('.'); } return null; } } }