master
1//! A software version formatted according to the Semantic Versioning 2.0.0 specification.
2//!
3//! See: https://semver.org
4
5const std = @import("std");
6const Version = @This();
7
8major: usize,
9minor: usize,
10patch: usize,
11pre: ?[]const u8 = null,
12build: ?[]const u8 = null,
13
14pub const Range = struct {
15 min: Version,
16 max: Version,
17
18 pub fn includesVersion(self: Range, ver: Version) bool {
19 if (self.min.order(ver) == .gt) return false;
20 if (self.max.order(ver) == .lt) return false;
21 return true;
22 }
23
24 /// Checks if system is guaranteed to be at least `version` or older than `version`.
25 /// Returns `null` if a runtime check is required.
26 pub fn isAtLeast(self: Range, ver: Version) ?bool {
27 if (self.min.order(ver) != .lt) return true;
28 if (self.max.order(ver) == .lt) return false;
29 return null;
30 }
31};
32
33pub fn order(lhs: Version, rhs: Version) std.math.Order {
34 if (lhs.major < rhs.major) return .lt;
35 if (lhs.major > rhs.major) return .gt;
36 if (lhs.minor < rhs.minor) return .lt;
37 if (lhs.minor > rhs.minor) return .gt;
38 if (lhs.patch < rhs.patch) return .lt;
39 if (lhs.patch > rhs.patch) return .gt;
40 if (lhs.pre != null and rhs.pre == null) return .lt;
41 if (lhs.pre == null and rhs.pre == null) return .eq;
42 if (lhs.pre == null and rhs.pre != null) return .gt;
43
44 // Iterate over pre-release identifiers until a difference is found.
45 var lhs_pre_it = std.mem.splitScalar(u8, lhs.pre.?, '.');
46 var rhs_pre_it = std.mem.splitScalar(u8, rhs.pre.?, '.');
47 while (true) {
48 const next_lid = lhs_pre_it.next();
49 const next_rid = rhs_pre_it.next();
50
51 // A larger set of pre-release fields has a higher precedence than a smaller set.
52 if (next_lid == null and next_rid != null) return .lt;
53 if (next_lid == null and next_rid == null) return .eq;
54 if (next_lid != null and next_rid == null) return .gt;
55
56 const lid = next_lid.?; // Left identifier
57 const rid = next_rid.?; // Right identifier
58
59 // Attempt to parse identifiers as numbers. Overflows are checked by parse.
60 const lnum: ?usize = std.fmt.parseUnsigned(usize, lid, 10) catch |err| switch (err) {
61 error.InvalidCharacter => null,
62 error.Overflow => unreachable,
63 };
64 const rnum: ?usize = std.fmt.parseUnsigned(usize, rid, 10) catch |err| switch (err) {
65 error.InvalidCharacter => null,
66 error.Overflow => unreachable,
67 };
68
69 // Numeric identifiers always have lower precedence than non-numeric identifiers.
70 if (lnum != null and rnum == null) return .lt;
71 if (lnum == null and rnum != null) return .gt;
72
73 // Identifiers consisting of only digits are compared numerically.
74 // Identifiers with letters or hyphens are compared lexically in ASCII sort order.
75 if (lnum != null and rnum != null) {
76 if (lnum.? < rnum.?) return .lt;
77 if (lnum.? > rnum.?) return .gt;
78 } else {
79 const ord = std.mem.order(u8, lid, rid);
80 if (ord != .eq) return ord;
81 }
82 }
83}
84
85pub fn parse(text: []const u8) !Version {
86 // Parse the required major, minor, and patch numbers.
87 const extra_index = std.mem.indexOfAny(u8, text, "-+");
88 const required = text[0..(extra_index orelse text.len)];
89 var it = std.mem.splitScalar(u8, required, '.');
90 var ver = Version{
91 .major = try parseNum(it.first()),
92 .minor = try parseNum(it.next() orelse return error.InvalidVersion),
93 .patch = try parseNum(it.next() orelse return error.InvalidVersion),
94 };
95 if (it.next() != null) return error.InvalidVersion;
96 if (extra_index == null) return ver;
97
98 // Slice optional pre-release or build metadata components.
99 const extra: []const u8 = text[extra_index.?..text.len];
100 if (extra[0] == '-') {
101 const build_index = std.mem.indexOfScalar(u8, extra, '+');
102 ver.pre = extra[1..(build_index orelse extra.len)];
103 if (build_index) |idx| ver.build = extra[(idx + 1)..];
104 } else {
105 ver.build = extra[1..];
106 }
107
108 // Check validity of optional pre-release identifiers.
109 // See: https://semver.org/#spec-item-9
110 if (ver.pre) |pre| {
111 it = std.mem.splitScalar(u8, pre, '.');
112 while (it.next()) |id| {
113 // Identifiers MUST NOT be empty.
114 if (id.len == 0) return error.InvalidVersion;
115
116 // Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-].
117 for (id) |c| if (!std.ascii.isAlphanumeric(c) and c != '-') return error.InvalidVersion;
118
119 // Numeric identifiers MUST NOT include leading zeroes.
120 const is_num = for (id) |c| {
121 if (!std.ascii.isDigit(c)) break false;
122 } else true;
123 if (is_num) _ = try parseNum(id);
124 }
125 }
126
127 // Check validity of optional build metadata identifiers.
128 // See: https://semver.org/#spec-item-10
129 if (ver.build) |build| {
130 it = std.mem.splitScalar(u8, build, '.');
131 while (it.next()) |id| {
132 // Identifiers MUST NOT be empty.
133 if (id.len == 0) return error.InvalidVersion;
134
135 // Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-].
136 for (id) |c| if (!std.ascii.isAlphanumeric(c) and c != '-') return error.InvalidVersion;
137 }
138 }
139
140 return ver;
141}
142
143fn parseNum(text: []const u8) error{ InvalidVersion, Overflow }!usize {
144 // Leading zeroes are not allowed.
145 if (text.len > 1 and text[0] == '0') return error.InvalidVersion;
146
147 return std.fmt.parseUnsigned(usize, text, 10) catch |err| switch (err) {
148 error.InvalidCharacter => return error.InvalidVersion,
149 error.Overflow => return error.Overflow,
150 };
151}
152
153pub fn format(self: Version, w: *std.Io.Writer) std.Io.Writer.Error!void {
154 try w.print("{d}.{d}.{d}", .{ self.major, self.minor, self.patch });
155 if (self.pre) |pre| try w.print("-{s}", .{pre});
156 if (self.build) |build| try w.print("+{s}", .{build});
157}
158
159const expect = std.testing.expect;
160const expectError = std.testing.expectError;
161
162test format {
163 // Many of these test strings are from https://github.com/semver/semver.org/issues/59#issuecomment-390854010.
164
165 // Valid version strings should be accepted.
166 for ([_][]const u8{
167 "0.0.4",
168 "1.2.3",
169 "10.20.30",
170 "1.1.2-prerelease+meta",
171 "1.1.2+meta",
172 "1.1.2+meta-valid",
173 "1.0.0-alpha",
174 "1.0.0-beta",
175 "1.0.0-alpha.beta",
176 "1.0.0-alpha.beta.1",
177 "1.0.0-alpha.1",
178 "1.0.0-alpha0.valid",
179 "1.0.0-alpha.0valid",
180 "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay",
181 "1.0.0-rc.1+build.1",
182 "2.0.0-rc.1+build.123",
183 "1.2.3-beta",
184 "10.2.3-DEV-SNAPSHOT",
185 "1.2.3-SNAPSHOT-123",
186 "1.0.0",
187 "2.0.0",
188 "1.1.7",
189 "2.0.0+build.1848",
190 "2.0.1-alpha.1227",
191 "1.0.0-alpha+beta",
192 "1.2.3----RC-SNAPSHOT.12.9.1--.12+788",
193 "1.2.3----R-S.12.9.1--.12+meta",
194 "1.2.3----RC-SNAPSHOT.12.9.1--.12",
195 "1.0.0+0.build.1-rc.10000aaa-kk-0.1",
196 "5.4.0-1018-raspi",
197 "5.7.123",
198 }) |valid| try std.testing.expectFmt(valid, "{f}", .{try parse(valid)});
199
200 // Invalid version strings should be rejected.
201 for ([_][]const u8{
202 "",
203 "1",
204 "1.2",
205 "1.2.3-0123",
206 "1.2.3-0123.0123",
207 "1.1.2+.123",
208 "+invalid",
209 "-invalid",
210 "-invalid+invalid",
211 "-invalid.01",
212 "alpha",
213 "alpha.beta",
214 "alpha.beta.1",
215 "alpha.1",
216 "alpha+beta",
217 "alpha_beta",
218 "alpha.",
219 "alpha..",
220 "beta\\",
221 "1.0.0-alpha_beta",
222 "-alpha.",
223 "1.0.0-alpha..",
224 "1.0.0-alpha..1",
225 "1.0.0-alpha...1",
226 "1.0.0-alpha....1",
227 "1.0.0-alpha.....1",
228 "1.0.0-alpha......1",
229 "1.0.0-alpha.......1",
230 "01.1.1",
231 "1.01.1",
232 "1.1.01",
233 "1.2",
234 "1.2.3.DEV",
235 "1.2-SNAPSHOT",
236 "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788",
237 "1.2-RC-SNAPSHOT",
238 "-1.0.3-gamma+b7718",
239 "+justmeta",
240 "9.8.7+meta+meta",
241 "9.8.7-whatever+meta+meta",
242 "2.6.32.11-svn21605",
243 "2.11.2(0.329/5/3)",
244 "2.13-DEVELOPMENT",
245 "2.3-35",
246 "1a.4",
247 "3.b1.0",
248 "1.4beta",
249 "2.7.pre",
250 "0..3",
251 "8.008.",
252 "01...",
253 "55",
254 "foobar",
255 "",
256 "-1",
257 "+4",
258 ".",
259 "....3",
260 }) |invalid| try expectError(error.InvalidVersion, parse(invalid));
261
262 // Valid version string that may overflow.
263 const big_valid = "99999999999999999999999.999999999999999999.99999999999999999";
264 if (parse(big_valid)) |ver| {
265 try std.testing.expectFmt(big_valid, "{f}", .{ver});
266 } else |err| try expect(err == error.Overflow);
267
268 // Invalid version string that may overflow.
269 const big_invalid = "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12";
270 if (parse(big_invalid)) |ver| std.debug.panic("expected error, found {f}", .{ver}) else |_| {}
271}
272
273test "precedence" {
274 // SemVer 2 spec 11.2 example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1.
275 try expect(order(try parse("1.0.0"), try parse("2.0.0")) == .lt);
276 try expect(order(try parse("2.0.0"), try parse("2.1.0")) == .lt);
277 try expect(order(try parse("2.1.0"), try parse("2.1.1")) == .lt);
278
279 // SemVer 2 spec 11.3 example: 1.0.0-alpha < 1.0.0.
280 try expect(order(try parse("1.0.0-alpha"), try parse("1.0.0")) == .lt);
281
282 // SemVer 2 spec 11.4 example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta <
283 // 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
284 try expect(order(try parse("1.0.0-alpha"), try parse("1.0.0-alpha.1")) == .lt);
285 try expect(order(try parse("1.0.0-alpha.1"), try parse("1.0.0-alpha.beta")) == .lt);
286 try expect(order(try parse("1.0.0-alpha.beta"), try parse("1.0.0-beta")) == .lt);
287 try expect(order(try parse("1.0.0-beta"), try parse("1.0.0-beta.2")) == .lt);
288 try expect(order(try parse("1.0.0-beta.2"), try parse("1.0.0-beta.11")) == .lt);
289 try expect(order(try parse("1.0.0-beta.11"), try parse("1.0.0-rc.1")) == .lt);
290 try expect(order(try parse("1.0.0-rc.1"), try parse("1.0.0")) == .lt);
291}
292
293test "zig_version" {
294 // An approximate Zig build that predates this test.
295 const older_version: Version = .{ .major = 0, .minor = 8, .patch = 0, .pre = "dev.874" };
296
297 // Simulated compatibility check using Zig version.
298 const compatible = comptime @import("builtin").zig_version.order(older_version) == .gt;
299 if (!compatible) @compileError("zig_version test failed");
300}