@joebigelow / wix-1 / commits / 905a6b0c

Add WixVersion implementation to WixToolset.Data

Rob Mensching committed May 9, 2022 at 22:18 UTC 905a6b0c4a214a373cb437ca28ea5610b3ad7654
3 files changed +522
src/api/wix/WixToolset.Data/WixVersion.cs new
+275
@@ -0,0 +1,275 @@
1 +// 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.
2 +
3 +namespace WixToolset.Data
4 +{
5 + using System;
6 + using System.Collections.Generic;
7 +
8 + /// <summary>
9 + /// WiX Toolset's representation of a version designed to support
10 + /// a different version types including 4-part versions with very
11 + /// large numbers and semantic versions with 4-part versions with
12 + /// or without leading "v" indicators.
13 + /// </summary>
14 + [CLSCompliant(false)]
15 + public class WixVersion
16 + {
17 + /// <summary>
18 + /// Gets the prefix of the version if present when parsed. Usually, 'v' or 'V'.
19 + /// </summary>
20 + public char? Prefix { get; set; }
21 +
22 + /// <summary>
23 + /// Gets or sets the major version.
24 + /// </summary>
25 + public uint? Major { get; set; }
26 +
27 + /// <summary>
28 + /// Gets or sets the minor version.
29 + /// </summary>
30 + public uint? Minor { get; set; }
31 +
32 + /// <summary>
33 + /// Gets or sets the patch version.
34 + /// </summary>
35 + public uint? Patch { get; set; }
36 +
37 + /// <summary>
38 + /// Gets or sets the revision version.
39 + /// </summary>
40 + public uint? Revision { get; set; }
41 +
42 + /// <summary>
43 + /// Gets or sets the labels in the version.
44 + /// </summary>
45 + public WixVersionLabel[] Labels { get; set; }
46 +
47 + /// <summary>
48 + /// Gets or sets the metadata in the version.
49 + /// </summary>
50 + public string Metadata { get; set; }
51 +
52 + /// <summary>
53 + /// Tries to parse a string value into a valid <c>WixVersion</c>.
54 + /// </summary>
55 + /// <param name="parse">String value to parse into a version.</param>
56 + /// <param name="version">Parsed version.</param>
57 + /// <returns>True if the version was successfully parsed, or false otherwise.</returns>
58 + public static bool TryParse(string parse, out WixVersion version)
59 + {
60 + version = new WixVersion();
61 +
62 + var labels = new List<WixVersionLabel>();
63 + var start = 0;
64 + var end = parse.Length;
65 +
66 + if (end > 0 && (parse[0] == 'v' || parse[0] == 'V'))
67 + {
68 + version.Prefix = parse[0];
69 +
70 + ++start;
71 + }
72 +
73 + var partBegin = start;
74 + var partEnd = start;
75 + var lastPart = false;
76 + var trailingDot = false;
77 + var invalid = false;
78 + var currentPart = 0;
79 + var parsedVersionNumber = false;
80 + var expectedReleaseLabels = false;
81 +
82 + // Parse version number
83 + while (start < end)
84 + {
85 + trailingDot = false;
86 +
87 + // Find end of part.
88 + for (; ; )
89 + {
90 + if (partEnd >= end)
91 + {
92 + lastPart = true;
93 + break;
94 + }
95 +
96 + var ch = parse[partEnd];
97 +
98 + switch (ch)
99 + {
100 + case '0':
101 + case '1':
102 + case '2':
103 + case '3':
104 + case '4':
105 + case '5':
106 + case '6':
107 + case '7':
108 + case '8':
109 + case '9':
110 + ++partEnd;
111 + continue;
112 + case '.':
113 + trailingDot = true;
114 + break;
115 + case '-':
116 + case '+':
117 + lastPart = true;
118 + break;
119 + default:
120 + invalid = true;
121 + break;
122 + }
123 +
124 + break;
125 + }
126 +
127 + var partLength = partEnd - partBegin;
128 + if (invalid || partLength <= 0)
129 + {
130 + invalid = true;
131 + break;
132 + }
133 +
134 + // Parse version part.
135 + var s = parse.Substring(partBegin, partLength);
136 + if (!UInt32.TryParse(s, out var part))
137 + {
138 + invalid = true;
139 + break;
140 + }
141 +
142 + switch (currentPart)
143 + {
144 + case 0:
145 + version.Major = part;
146 + break;
147 + case 1:
148 + version.Minor = part;
149 + break;
150 + case 2:
151 + version.Patch = part;
152 + break;
153 + case 3:
154 + version.Revision = part;
155 + break;
156 + }
157 +
158 + if (trailingDot)
159 + {
160 + ++partEnd;
161 + }
162 + partBegin = partEnd;
163 + ++currentPart;
164 +
165 + if (4 <= currentPart || lastPart)
166 + {
167 + parsedVersionNumber = true;
168 + break;
169 + }
170 + }
171 +
172 + invalid |= !parsedVersionNumber || trailingDot;
173 +
174 + if (!invalid && partBegin < end && parse[partBegin] == '-')
175 + {
176 + partBegin = partEnd = partBegin + 1;
177 + expectedReleaseLabels = true;
178 + lastPart = false;
179 + }
180 +
181 + while (expectedReleaseLabels && partBegin < end)
182 + {
183 + trailingDot = false;
184 +
185 + // Find end of part.
186 + for (; ; )
187 + {
188 + if (partEnd >= end)
189 + {
190 + lastPart = true;
191 + break;
192 + }
193 +
194 + var ch = parse[partEnd];
195 + if (ch >= '0' && ch <= '9' ||
196 + ch >= 'A' && ch <= 'Z' ||
197 + ch >= 'a' && ch <= 'z' ||
198 + ch == '-')
199 + {
200 + ++partEnd;
201 + continue;
202 + }
203 + else if (ch == '+')
204 + {
205 + lastPart = true;
206 + }
207 + else if (ch == '.')
208 + {
209 + trailingDot = true;
210 + }
211 + else
212 + {
213 + invalid = true;
214 + }
215 +
216 + break;
217 + }
218 +
219 + var partLength = partEnd - partBegin;
220 + if (invalid || partLength <= 0)
221 + {
222 + invalid = true;
223 + break;
224 + }
225 +
226 + WixVersionLabel label;
227 + var partString = parse.Substring(partBegin, partLength);
228 + if (UInt32.TryParse(partString, out var numericPart))
229 + {
230 + label = new WixVersionLabel(partString, numericPart);
231 + }
232 + else
233 + {
234 + label = new WixVersionLabel(partString);
235 + }
236 +
237 + labels.Add(label);
238 +
239 + if (trailingDot)
240 + {
241 + ++partEnd;
242 + }
243 + partBegin = partEnd;
244 +
245 + if (lastPart)
246 + {
247 + break;
248 + }
249 + }
250 +
251 + invalid |= expectedReleaseLabels && (labels.Count == 0 || trailingDot);
252 +
253 + if (!invalid && partBegin < end)
254 + {
255 + if (parse[partBegin] == '+')
256 + {
257 + version.Metadata = parse.Substring(partBegin + 1);
258 + }
259 + else
260 + {
261 + invalid = true;
262 + }
263 + }
264 +
265 + if (invalid)
266 + {
267 + return false;
268 + }
269 +
270 + version.Labels = labels.Count == 0 ? null : labels.ToArray();
271 +
272 + return true;
273 + }
274 + }
275 +}
src/api/wix/WixToolset.Data/WixVersionLabel.cs new
+43
@@ -0,0 +1,43 @@
1 +// 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.
2 +
3 +namespace WixToolset.Data
4 +{
5 + using System;
6 +
7 + /// <summary>
8 + /// Label in a <c>WixVersion</c>.
9 + /// </summary>
10 + [CLSCompliant(false)]
11 + public class WixVersionLabel
12 + {
13 + /// <summary>
14 + /// Creates a string only version label.
15 + /// </summary>
16 + /// <param name="label">String value for version label.</param>
17 + public WixVersionLabel(string label)
18 + {
19 + this.Label = label;
20 + }
21 +
22 + /// <summary>
23 + /// Creates a string version label with numeric value.
24 + /// </summary>
25 + /// <param name="label">String value for version label.</param>
26 + /// <param name="numeric">Numeric value for the version label.</param>
27 + public WixVersionLabel(string label, uint? numeric)
28 + {
29 + this.Label = label;
30 + this.Numeric = numeric;
31 + }
32 +
33 + /// <summary>
34 + /// Gets the string label value.
35 + /// </summary>
36 + public string Label { get; set; }
37 +
38 + /// <summary>
39 + /// Gets the optional numeric label value.
40 + /// </summary>
41 + public uint? Numeric { get; set; }
42 + }
43 +}
src/api/wix/test/WixToolsetTest.Data/WixVerFixture.cs new
+204
@@ -0,0 +1,204 @@
1 +// 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.
2 +
3 +namespace WixToolsetTest.Data
4 +{
5 + using System;
6 + using WixToolset.Data;
7 + using Xunit;
8 +
9 + public class WixVerFixture
10 + {
11 + [Fact]
12 + public void CannotParseEmptyStringAsVersion()
13 + {
14 + Assert.False(WixVersion.TryParse(String.Empty, out var _));
15 + }
16 +
17 + [Fact]
18 + public void CannotParseInvalidStringAsVersion()
19 + {
20 + Assert.False(WixVersion.TryParse("invalid", out var _));
21 + }
22 +
23 + [Fact]
24 + public void CanParseFourPartVersion()
25 + {
26 + Assert.True(WixVersion.TryParse("1.2.3.4", out var version));
27 + Assert.Null(version.Prefix);
28 + Assert.Equal((uint)1, version.Major);
29 + Assert.Equal((uint)2, version.Minor);
30 + Assert.Equal((uint)3, version.Patch);
31 + Assert.Equal((uint)4, version.Revision);
32 + Assert.Null(version.Labels);
33 + Assert.Null(version.Metadata);
34 + }
35 +
36 + [Fact]
37 + public void CanParseThreePartVersion()
38 + {
39 + Assert.True(WixVersion.TryParse("1.2.3", out var version));
40 + Assert.Null(version.Prefix);
41 + Assert.Equal((uint)1, version.Major);
42 + Assert.Equal((uint)2, version.Minor);
43 + Assert.Equal((uint)3, version.Patch);
44 + Assert.Null(version.Revision);
45 + Assert.Null(version.Labels);
46 + Assert.Null(version.Metadata);
47 + }
48 +
49 + [Fact]
50 + public void CanParseFourPartVersionWithTrailingZero()
51 + {
52 + Assert.True(WixVersion.TryParse("1.2.3.0", out var version));
53 + Assert.Null(version.Prefix);
54 + Assert.Equal((uint)1, version.Major);
55 + Assert.Equal((uint)2, version.Minor);
56 + Assert.Equal((uint)3, version.Patch);
57 + Assert.Equal((uint)0, version.Revision);
58 + Assert.Null(version.Labels);
59 + Assert.Null(version.Metadata);
60 + }
61 +
62 + [Fact]
63 + public void CanParseNumericReleaseLabels()
64 + {
65 + Assert.True(WixVersion.TryParse("1.2-19", out var version));
66 + Assert.Null(version.Prefix);
67 + Assert.Equal((uint)1, version.Major);
68 + Assert.Equal((uint)2, version.Minor);
69 + Assert.Null(version.Patch);
70 + Assert.Null(version.Revision);
71 + Assert.Equal("19", version.Labels[0].Label);
72 + Assert.Equal((uint)19, version.Labels[0].Numeric);
73 + Assert.Null(version.Metadata);
74 + }
75 +
76 + [Fact]
77 + public void CanParseDottedNumericReleaseLabels()
78 + {
79 + Assert.True(WixVersion.TryParse("1.2-2.0", out var version));
80 + Assert.Null(version.Prefix);
81 + Assert.Equal((uint)1, version.Major);
82 + Assert.Equal((uint)2, version.Minor);
83 + Assert.Null(version.Patch);
84 + Assert.Null(version.Revision);
85 + Assert.Equal("2", version.Labels[0].Label);
86 + Assert.Equal((uint)2, version.Labels[0].Numeric);
87 + Assert.Equal("0", version.Labels[1].Label);
88 + Assert.Equal((uint)0, version.Labels[1].Numeric);
89 + Assert.Null(version.Metadata);
90 + }
91 +
92 + [Fact]
93 + public void CanParseHyphenAsVersionSeparator()
94 + {
95 + Assert.True(WixVersion.TryParse("0.0.1-a", out var version));
96 + Assert.Null(version.Prefix);
97 + Assert.Equal((uint)0, version.Major);
98 + Assert.Equal((uint)0, version.Minor);
99 + Assert.Equal((uint)1, version.Patch);
100 + Assert.Equal("a", version.Labels[0].Label);
101 + Assert.Null(version.Labels[0].Numeric);
102 + Assert.Null(version.Metadata);
103 + }
104 +
105 + [Fact]
106 + public void CanParseIgnoringLeadingZeros()
107 + {
108 + Assert.True(WixVersion.TryParse("0.01-a.000", out var version));
109 + Assert.Null(version.Prefix);
110 + Assert.Equal((uint)0, version.Major);
111 + Assert.Equal((uint)1, version.Minor);
112 + Assert.Null(version.Patch);
113 + Assert.Null(version.Revision);
114 + Assert.Equal("a", version.Labels[0].Label);
115 + Assert.Null(version.Labels[0].Numeric);
116 + Assert.Equal("000", version.Labels[1].Label);
117 + Assert.Equal((uint)0, version.Labels[1].Numeric);
118 + Assert.Null(version.Metadata);
119 + }
120 +
121 + [Fact]
122 + public void CanParseMetadata()
123 + {
124 + Assert.True(WixVersion.TryParse("1.2.3+abcd", out var version));
125 + Assert.Null(version.Prefix);
126 + Assert.Equal((uint)1, version.Major);
127 + Assert.Equal((uint)2, version.Minor);
128 + Assert.Equal((uint)3, version.Patch);
129 + Assert.Null(version.Revision);
130 + Assert.Null(version.Labels);
131 + Assert.Equal("abcd", version.Metadata);
132 + }
133 +
134 + [Fact]
135 + public void CannotParseUnexpectedContentAsMetadata()
136 + {
137 + Assert.False(WixVersion.TryParse("1.2.3.abcd", out var _));
138 + Assert.False(WixVersion.TryParse("1.2.3.-abcd", out var _));
139 + }
140 +
141 + [Fact]
142 + public void CanParseLeadingPrefix()
143 + {
144 + Assert.True(WixVersion.TryParse("v10.20.30.40", out var version));
145 + Assert.Equal('v', version.Prefix);
146 + Assert.Equal((uint)10, version.Major);
147 + Assert.Equal((uint)20, version.Minor);
148 + Assert.Equal((uint)30, version.Patch);
149 + Assert.Equal((uint)40, version.Revision);
150 + Assert.Null(version.Labels);
151 + Assert.Null(version.Metadata);
152 +
153 + Assert.True(WixVersion.TryParse("V100.200.300.400", out var version2));
154 + Assert.Equal('V', version2.Prefix);
155 + Assert.Equal((uint)100, version2.Major);
156 + Assert.Equal((uint)200, version2.Minor);
157 + Assert.Equal((uint)300, version2.Patch);
158 + Assert.Equal((uint)400, version2.Revision);
159 + Assert.Null(version2.Labels);
160 + Assert.Null(version2.Metadata);
161 + }
162 +
163 + [Fact]
164 + public void CanParseVeryLargeNumbers()
165 + {
166 + Assert.True(WixVersion.TryParse("4294967295.4294967295.4294967295.4294967295", out var version));
167 + Assert.Null(version.Prefix);
168 + Assert.Equal(4294967295, version.Major);
169 + Assert.Equal(4294967295, version.Minor);
170 + Assert.Equal(4294967295, version.Patch);
171 + Assert.Equal(4294967295, version.Revision);
172 + Assert.Null(version.Labels);
173 + Assert.Null(version.Metadata);
174 + }
175 +
176 + [Fact]
177 + public void CannotParseTooLargeNumbers()
178 + {
179 + Assert.False(WixVersion.TryParse("4294967296.4294967296.4294967296.4294967296", out var _));
180 + }
181 +
182 + [Fact]
183 + public void CanParseLabelsWithMetadata()
184 + {
185 + Assert.True(WixVersion.TryParse("1.2.3.4-a.b.c.d.5+abc123", out var version));
186 + Assert.Null(version.Prefix);
187 + Assert.Equal((uint)1, version.Major);
188 + Assert.Equal((uint)2, version.Minor);
189 + Assert.Equal((uint)3, version.Patch);
190 + Assert.Equal((uint)4, version.Revision);
191 + Assert.Equal("a", version.Labels[0].Label);
192 + Assert.Null(version.Labels[0].Numeric);
193 + Assert.Equal("b", version.Labels[1].Label);
194 + Assert.Null(version.Labels[1].Numeric);
195 + Assert.Equal("c", version.Labels[2].Label);
196 + Assert.Null(version.Labels[2].Numeric);
197 + Assert.Equal("d", version.Labels[3].Label);
198 + Assert.Null(version.Labels[3].Numeric);
199 + Assert.Equal("5", version.Labels[4].Label);
200 + Assert.Equal((uint)5, version.Labels[4].Numeric);
201 + Assert.Equal("abc123", version.Metadata);
202 + }
203 + }
204 +}