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
+}