From 905a6b0c4a214a373cb437ca28ea5610b3ad7654 Mon Sep 17 00:00:00 2001 From: Rob Mensching Date: Mon, 9 May 2022 22:18:19 -0700 Subject: Add WixVersion implementation to WixToolset.Data --- src/api/wix/WixToolset.Data/WixVersion.cs | 275 +++++++++++++++++++++ src/api/wix/WixToolset.Data/WixVersionLabel.cs | 43 ++++ .../wix/test/WixToolsetTest.Data/WixVerFixture.cs | 204 +++++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 src/api/wix/WixToolset.Data/WixVersion.cs create mode 100644 src/api/wix/WixToolset.Data/WixVersionLabel.cs create mode 100644 src/api/wix/test/WixToolsetTest.Data/WixVerFixture.cs diff --git a/src/api/wix/WixToolset.Data/WixVersion.cs b/src/api/wix/WixToolset.Data/WixVersion.cs new file mode 100644 index 00000000..2b02bd7d --- /dev/null +++ b/src/api/wix/WixToolset.Data/WixVersion.cs @@ -0,0 +1,275 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Data +{ + using System; + using System.Collections.Generic; + + /// + /// WiX Toolset's representation of a version designed to support + /// a different version types including 4-part versions with very + /// large numbers and semantic versions with 4-part versions with + /// or without leading "v" indicators. + /// + [CLSCompliant(false)] + public class WixVersion + { + /// + /// Gets the prefix of the version if present when parsed. Usually, 'v' or 'V'. + /// + public char? Prefix { get; set; } + + /// + /// Gets or sets the major version. + /// + public uint? Major { get; set; } + + /// + /// Gets or sets the minor version. + /// + public uint? Minor { get; set; } + + /// + /// Gets or sets the patch version. + /// + public uint? Patch { get; set; } + + /// + /// Gets or sets the revision version. + /// + public uint? Revision { get; set; } + + /// + /// Gets or sets the labels in the version. + /// + public WixVersionLabel[] Labels { get; set; } + + /// + /// Gets or sets the metadata in the version. + /// + public string Metadata { get; set; } + + /// + /// Tries to parse a string value into a valid WixVersion. + /// + /// String value to parse into a version. + /// Parsed version. + /// True if the version was successfully parsed, or false otherwise. + public static bool TryParse(string parse, out WixVersion version) + { + version = new WixVersion(); + + var labels = new List(); + var start = 0; + var end = parse.Length; + + if (end > 0 && (parse[0] == 'v' || parse[0] == 'V')) + { + version.Prefix = parse[0]; + + ++start; + } + + var partBegin = start; + var partEnd = start; + var lastPart = false; + var trailingDot = false; + var invalid = false; + var currentPart = 0; + var parsedVersionNumber = false; + var expectedReleaseLabels = false; + + // Parse version number + while (start < end) + { + trailingDot = false; + + // Find end of part. + for (; ; ) + { + if (partEnd >= end) + { + lastPart = true; + break; + } + + var ch = parse[partEnd]; + + switch (ch) + { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + ++partEnd; + continue; + case '.': + trailingDot = true; + break; + case '-': + case '+': + lastPart = true; + break; + default: + invalid = true; + break; + } + + break; + } + + var partLength = partEnd - partBegin; + if (invalid || partLength <= 0) + { + invalid = true; + break; + } + + // Parse version part. + var s = parse.Substring(partBegin, partLength); + if (!UInt32.TryParse(s, out var part)) + { + invalid = true; + break; + } + + switch (currentPart) + { + case 0: + version.Major = part; + break; + case 1: + version.Minor = part; + break; + case 2: + version.Patch = part; + break; + case 3: + version.Revision = part; + break; + } + + if (trailingDot) + { + ++partEnd; + } + partBegin = partEnd; + ++currentPart; + + if (4 <= currentPart || lastPart) + { + parsedVersionNumber = true; + break; + } + } + + invalid |= !parsedVersionNumber || trailingDot; + + if (!invalid && partBegin < end && parse[partBegin] == '-') + { + partBegin = partEnd = partBegin + 1; + expectedReleaseLabels = true; + lastPart = false; + } + + while (expectedReleaseLabels && partBegin < end) + { + trailingDot = false; + + // Find end of part. + for (; ; ) + { + if (partEnd >= end) + { + lastPart = true; + break; + } + + var ch = parse[partEnd]; + if (ch >= '0' && ch <= '9' || + ch >= 'A' && ch <= 'Z' || + ch >= 'a' && ch <= 'z' || + ch == '-') + { + ++partEnd; + continue; + } + else if (ch == '+') + { + lastPart = true; + } + else if (ch == '.') + { + trailingDot = true; + } + else + { + invalid = true; + } + + break; + } + + var partLength = partEnd - partBegin; + if (invalid || partLength <= 0) + { + invalid = true; + break; + } + + WixVersionLabel label; + var partString = parse.Substring(partBegin, partLength); + if (UInt32.TryParse(partString, out var numericPart)) + { + label = new WixVersionLabel(partString, numericPart); + } + else + { + label = new WixVersionLabel(partString); + } + + labels.Add(label); + + if (trailingDot) + { + ++partEnd; + } + partBegin = partEnd; + + if (lastPart) + { + break; + } + } + + invalid |= expectedReleaseLabels && (labels.Count == 0 || trailingDot); + + if (!invalid && partBegin < end) + { + if (parse[partBegin] == '+') + { + version.Metadata = parse.Substring(partBegin + 1); + } + else + { + invalid = true; + } + } + + if (invalid) + { + return false; + } + + version.Labels = labels.Count == 0 ? null : labels.ToArray(); + + return true; + } + } +} diff --git a/src/api/wix/WixToolset.Data/WixVersionLabel.cs b/src/api/wix/WixToolset.Data/WixVersionLabel.cs new file mode 100644 index 00000000..c4227c49 --- /dev/null +++ b/src/api/wix/WixToolset.Data/WixVersionLabel.cs @@ -0,0 +1,43 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolset.Data +{ + using System; + + /// + /// Label in a WixVersion. + /// + [CLSCompliant(false)] + public class WixVersionLabel + { + /// + /// Creates a string only version label. + /// + /// String value for version label. + public WixVersionLabel(string label) + { + this.Label = label; + } + + /// + /// Creates a string version label with numeric value. + /// + /// String value for version label. + /// Numeric value for the version label. + public WixVersionLabel(string label, uint? numeric) + { + this.Label = label; + this.Numeric = numeric; + } + + /// + /// Gets the string label value. + /// + public string Label { get; set; } + + /// + /// Gets the optional numeric label value. + /// + public uint? Numeric { get; set; } + } +} diff --git a/src/api/wix/test/WixToolsetTest.Data/WixVerFixture.cs b/src/api/wix/test/WixToolsetTest.Data/WixVerFixture.cs new file mode 100644 index 00000000..45253460 --- /dev/null +++ b/src/api/wix/test/WixToolsetTest.Data/WixVerFixture.cs @@ -0,0 +1,204 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. + +namespace WixToolsetTest.Data +{ + using System; + using WixToolset.Data; + using Xunit; + + public class WixVerFixture + { + [Fact] + public void CannotParseEmptyStringAsVersion() + { + Assert.False(WixVersion.TryParse(String.Empty, out var _)); + } + + [Fact] + public void CannotParseInvalidStringAsVersion() + { + Assert.False(WixVersion.TryParse("invalid", out var _)); + } + + [Fact] + public void CanParseFourPartVersion() + { + Assert.True(WixVersion.TryParse("1.2.3.4", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Equal((uint)3, version.Patch); + Assert.Equal((uint)4, version.Revision); + Assert.Null(version.Labels); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseThreePartVersion() + { + Assert.True(WixVersion.TryParse("1.2.3", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Equal((uint)3, version.Patch); + Assert.Null(version.Revision); + Assert.Null(version.Labels); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseFourPartVersionWithTrailingZero() + { + Assert.True(WixVersion.TryParse("1.2.3.0", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Equal((uint)3, version.Patch); + Assert.Equal((uint)0, version.Revision); + Assert.Null(version.Labels); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseNumericReleaseLabels() + { + Assert.True(WixVersion.TryParse("1.2-19", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Null(version.Patch); + Assert.Null(version.Revision); + Assert.Equal("19", version.Labels[0].Label); + Assert.Equal((uint)19, version.Labels[0].Numeric); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseDottedNumericReleaseLabels() + { + Assert.True(WixVersion.TryParse("1.2-2.0", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Null(version.Patch); + Assert.Null(version.Revision); + Assert.Equal("2", version.Labels[0].Label); + Assert.Equal((uint)2, version.Labels[0].Numeric); + Assert.Equal("0", version.Labels[1].Label); + Assert.Equal((uint)0, version.Labels[1].Numeric); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseHyphenAsVersionSeparator() + { + Assert.True(WixVersion.TryParse("0.0.1-a", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)0, version.Major); + Assert.Equal((uint)0, version.Minor); + Assert.Equal((uint)1, version.Patch); + Assert.Equal("a", version.Labels[0].Label); + Assert.Null(version.Labels[0].Numeric); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseIgnoringLeadingZeros() + { + Assert.True(WixVersion.TryParse("0.01-a.000", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)0, version.Major); + Assert.Equal((uint)1, version.Minor); + Assert.Null(version.Patch); + Assert.Null(version.Revision); + Assert.Equal("a", version.Labels[0].Label); + Assert.Null(version.Labels[0].Numeric); + Assert.Equal("000", version.Labels[1].Label); + Assert.Equal((uint)0, version.Labels[1].Numeric); + Assert.Null(version.Metadata); + } + + [Fact] + public void CanParseMetadata() + { + Assert.True(WixVersion.TryParse("1.2.3+abcd", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Equal((uint)3, version.Patch); + Assert.Null(version.Revision); + Assert.Null(version.Labels); + Assert.Equal("abcd", version.Metadata); + } + + [Fact] + public void CannotParseUnexpectedContentAsMetadata() + { + Assert.False(WixVersion.TryParse("1.2.3.abcd", out var _)); + Assert.False(WixVersion.TryParse("1.2.3.-abcd", out var _)); + } + + [Fact] + public void CanParseLeadingPrefix() + { + Assert.True(WixVersion.TryParse("v10.20.30.40", out var version)); + Assert.Equal('v', version.Prefix); + Assert.Equal((uint)10, version.Major); + Assert.Equal((uint)20, version.Minor); + Assert.Equal((uint)30, version.Patch); + Assert.Equal((uint)40, version.Revision); + Assert.Null(version.Labels); + Assert.Null(version.Metadata); + + Assert.True(WixVersion.TryParse("V100.200.300.400", out var version2)); + Assert.Equal('V', version2.Prefix); + Assert.Equal((uint)100, version2.Major); + Assert.Equal((uint)200, version2.Minor); + Assert.Equal((uint)300, version2.Patch); + Assert.Equal((uint)400, version2.Revision); + Assert.Null(version2.Labels); + Assert.Null(version2.Metadata); + } + + [Fact] + public void CanParseVeryLargeNumbers() + { + Assert.True(WixVersion.TryParse("4294967295.4294967295.4294967295.4294967295", out var version)); + Assert.Null(version.Prefix); + Assert.Equal(4294967295, version.Major); + Assert.Equal(4294967295, version.Minor); + Assert.Equal(4294967295, version.Patch); + Assert.Equal(4294967295, version.Revision); + Assert.Null(version.Labels); + Assert.Null(version.Metadata); + } + + [Fact] + public void CannotParseTooLargeNumbers() + { + Assert.False(WixVersion.TryParse("4294967296.4294967296.4294967296.4294967296", out var _)); + } + + [Fact] + public void CanParseLabelsWithMetadata() + { + Assert.True(WixVersion.TryParse("1.2.3.4-a.b.c.d.5+abc123", out var version)); + Assert.Null(version.Prefix); + Assert.Equal((uint)1, version.Major); + Assert.Equal((uint)2, version.Minor); + Assert.Equal((uint)3, version.Patch); + Assert.Equal((uint)4, version.Revision); + Assert.Equal("a", version.Labels[0].Label); + Assert.Null(version.Labels[0].Numeric); + Assert.Equal("b", version.Labels[1].Label); + Assert.Null(version.Labels[1].Numeric); + Assert.Equal("c", version.Labels[2].Label); + Assert.Null(version.Labels[2].Numeric); + Assert.Equal("d", version.Labels[3].Label); + Assert.Null(version.Labels[3].Numeric); + Assert.Equal("5", version.Labels[4].Label); + Assert.Equal((uint)5, version.Labels[4].Numeric); + Assert.Equal("abc123", version.Metadata); + } + } +} -- cgit v1.2.3-55-g6feb