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}