diff --git a/lib/std/SemanticVersion.zig b/lib/std/SemanticVersion.zig new file mode 100644 index 000000000..74e515f9d --- /dev/null +++ b/lib/std/SemanticVersion.zig @@ -0,0 +1,295 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020 Zig Contributors +// This file is part of [zig](https://ziglang.org/), which is MIT licensed. +// The MIT license requires this copyright notice to be included in all copies +// and substantial portions of the software. + +//! A software version formatted according to the Semantic Version 2 specification. +//! +//! See: https://semver.org + +const std = @import("std"); +const Version = @This(); + +major: usize, +minor: usize, +patch: usize, +pre: ?[]const u8 = null, +build: ?[]const u8 = null, + +pub const Range = struct { + min: Version, + max: Version, + + pub fn includesVersion(self: Range, ver: Version) bool { + if (self.min.order(ver) == .gt) return false; + if (self.max.order(ver) == .lt) return false; + return true; + } + + /// Checks if system is guaranteed to be at least `version` or older than `version`. + /// Returns `null` if a runtime check is required. + pub fn isAtLeast(self: Range, ver: Version) ?bool { + if (self.min.order(ver) != .lt) return true; + if (self.max.order(ver) == .lt) return false; + return null; + } +}; + +pub fn order(lhs: Version, rhs: Version) std.math.Order { + if (lhs.major < rhs.major) return .lt; + if (lhs.major > rhs.major) return .gt; + if (lhs.minor < rhs.minor) return .lt; + if (lhs.minor > rhs.minor) return .gt; + if (lhs.patch < rhs.patch) return .lt; + if (lhs.patch > rhs.patch) return .gt; + if (lhs.pre != null and rhs.pre == null) return .lt; + if (lhs.pre == null and rhs.pre == null) return .eq; + if (lhs.pre == null and rhs.pre != null) return .gt; + + // Iterate over pre-release identifiers until a difference is found. + var lhs_pre_it = std.mem.split(lhs.pre.?, "."); + var rhs_pre_it = std.mem.split(rhs.pre.?, "."); + while (true) { + const next_lid = lhs_pre_it.next(); + const next_rid = rhs_pre_it.next(); + + // A larger set of pre-release fields has a higher precedence than a smaller set. + if (next_lid == null and next_rid != null) return .lt; + if (next_lid == null and next_rid == null) return .eq; + if (next_lid != null and next_rid == null) return .gt; + + const lid = next_lid.?; // Left identifier + const rid = next_rid.?; // Right identifier + + // Attempt to parse identifiers as numbers. Overflows are checked by parse. + const lnum: ?usize = std.fmt.parseUnsigned(usize, lid, 10) catch |err| switch (err) { + error.InvalidCharacter => null, + error.Overflow => unreachable, + }; + const rnum: ?usize = std.fmt.parseUnsigned(usize, rid, 10) catch |err| switch (err) { + error.InvalidCharacter => null, + error.Overflow => unreachable, + }; + + // Numeric identifiers always have lower precedence than non-numeric identifiers. + if (lnum != null and rnum == null) return .lt; + if (lnum == null and rnum != null) return .gt; + + // Identifiers consisting of only digits are compared numerically. + // Identifiers with letters or hyphens are compared lexically in ASCII sort order. + if (lnum != null and rnum != null) { + if (lnum.? < rnum.?) return .lt; + if (lnum.? > rnum.?) return .gt; + } else { + const ord = std.mem.order(u8, lid, rid); + if (ord != .eq) return ord; + } + } +} + +pub fn parse(text: []const u8) !Version { + // Parse the required major, minor, and patch numbers. + const extra_index = std.mem.indexOfAny(u8, text, "-+"); + const required = text[0..(extra_index orelse text.len)]; + var it = std.mem.split(required, "."); + var ver = Version{ + .major = try parseNum(it.next() orelse return error.InvalidVersion), + .minor = try parseNum(it.next() orelse return error.InvalidVersion), + .patch = try parseNum(it.next() orelse return error.InvalidVersion), + }; + if (it.next() != null) return error.InvalidVersion; + if (extra_index == null) return ver; + + // Slice optional pre-release or build metadata components. + const extra = text[extra_index.?..text.len]; + if (extra[0] == '-') { + const build_index = std.mem.indexOfScalar(u8, extra, '+'); + ver.pre = extra[1..(build_index orelse extra.len)]; + if (build_index) |idx| ver.build = extra[(idx + 1)..]; + } else { + ver.build = extra[1..]; + } + + // Check validity of optional pre-release identifiers. + // See: https://semver.org/#spec-item-9 + if (ver.pre) |pre| { + it = std.mem.split(pre, "."); + while (it.next()) |id| { + // Identifiers MUST NOT be empty. + if (id.len == 0) return error.InvalidVersion; + + // Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. + for (id) |c| if (!std.ascii.isAlNum(c) and c != '-') return error.InvalidVersion; + + // Numeric identifiers MUST NOT include leading zeroes. + const is_num = for (id) |c| { + if (!std.ascii.isDigit(c)) break false; + } else true; + if (is_num) _ = try parseNum(id); + } + } + + // Check validity of optional build metadata identifiers. + // See: https://semver.org/#spec-item-10 + if (ver.build) |build| { + it = std.mem.split(build, "."); + while (it.next()) |id| { + // Identifiers MUST NOT be empty. + if (id.len == 0) return error.InvalidVersion; + + // Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. + for (id) |c| if (!std.ascii.isAlNum(c) and c != '-') return error.InvalidVersion; + } + } + + return ver; +} + +fn parseNum(text: []const u8) !usize { + // Leading zeroes are not allowed. + if (text.len > 1 and text[0] == '0') return error.InvalidVersion; + + return std.fmt.parseUnsigned(usize, text, 10) catch |err| switch (err) { + error.InvalidCharacter => return error.InvalidVersion, + else => |e| return e, + }; +} + +pub fn format( + self: Version, + comptime fmt: []const u8, + options: std.fmt.FormatOptions, + out_stream: anytype, +) !void { + if (fmt.len != 0) @compileError("Unknown format string: '" ++ fmt ++ "'"); + try std.fmt.format(out_stream, "{}.{}.{}", .{ self.major, self.minor, self.patch }); + if (self.pre) |pre| try std.fmt.format(out_stream, "-{}", .{pre}); + if (self.build) |build| try std.fmt.format(out_stream, "+{}", .{build}); +} + +const expect = std.testing.expect; +const expectError = std.testing.expectError; + +test "SemanticVersion format" { + // Test vectors are from https://github.com/semver/semver.org/issues/59#issuecomment-390854010. + + // Valid version strings should be accepted. + for ([_][]const u8{ + "0.0.4", + "1.2.3", + "10.20.30", + "1.1.2-prerelease+meta", + "1.1.2+meta", + "1.1.2+meta-valid", + "1.0.0-alpha", + "1.0.0-beta", + "1.0.0-alpha.beta", + "1.0.0-alpha.beta.1", + "1.0.0-alpha.1", + "1.0.0-alpha0.valid", + "1.0.0-alpha.0valid", + "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", + "1.0.0-rc.1+build.1", + "2.0.0-rc.1+build.123", + "1.2.3-beta", + "10.2.3-DEV-SNAPSHOT", + "1.2.3-SNAPSHOT-123", + "1.0.0", + "2.0.0", + "1.1.7", + "2.0.0+build.1848", + "2.0.1-alpha.1227", + "1.0.0-alpha+beta", + "1.2.3----RC-SNAPSHOT.12.9.1--.12+788", + "1.2.3----R-S.12.9.1--.12+meta", + "1.2.3----RC-SNAPSHOT.12.9.1--.12", + "1.0.0+0.build.1-rc.10000aaa-kk-0.1", + }) |valid| try testFmt(valid, "{}", .{try parse(valid)}); + + // Invalid version strings should be rejected. + for ([_][]const u8{ + "", + "1", + "1.2", + "1.2.3-0123", + "1.2.3-0123.0123", + "1.1.2+.123", + "+invalid", + "-invalid", + "-invalid+invalid", + "-invalid.01", + "alpha", + "alpha.beta", + "alpha.beta.1", + "alpha.1", + "alpha+beta", + "alpha_beta", + "alpha.", + "alpha..", + "beta\\", + "1.0.0-alpha_beta", + "-alpha.", + "1.0.0-alpha..", + "1.0.0-alpha..1", + "1.0.0-alpha...1", + "1.0.0-alpha....1", + "1.0.0-alpha.....1", + "1.0.0-alpha......1", + "1.0.0-alpha.......1", + "01.1.1", + "1.01.1", + "1.1.01", + "1.2", + "1.2.3.DEV", + "1.2-SNAPSHOT", + "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788", + "1.2-RC-SNAPSHOT", + "-1.0.3-gamma+b7718", + "+justmeta", + "9.8.7+meta+meta", + "9.8.7-whatever+meta+meta", + }) |invalid| expectError(error.InvalidVersion, parse(invalid)); + + // Valid version string that may overflow. + const big_valid = "99999999999999999999999.999999999999999999.99999999999999999"; + if (parse(big_valid)) |ver| try testFmt(big_valid, "{}", .{ver}) else |err| expect(err == error.Overflow); + + // Invalid version string that may overflow. + const big_invalid = "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"; + if (parse(big_invalid)) |ver| std.debug.panic("expected error, found {}", .{ver}) else |err| {} +} + +test "SemanticVersion precedence" { + // SemVer 2 spec 11.2 example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. + expect(order(try parse("1.0.0"), try parse("2.0.0")) == .lt); + expect(order(try parse("2.0.0"), try parse("2.1.0")) == .lt); + expect(order(try parse("2.1.0"), try parse("2.1.1")) == .lt); + + // SemVer 2 spec 11.3 example: 1.0.0-alpha < 1.0.0. + expect(order(try parse("1.0.0-alpha"), try parse("1.0.0")) == .lt); + + // SemVer 2 spec 11.4 example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < + // 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0. + expect(order(try parse("1.0.0-alpha"), try parse("1.0.0-alpha.1")) == .lt); + expect(order(try parse("1.0.0-alpha.1"), try parse("1.0.0-alpha.beta")) == .lt); + expect(order(try parse("1.0.0-alpha.beta"), try parse("1.0.0-beta")) == .lt); + expect(order(try parse("1.0.0-beta"), try parse("1.0.0-beta.2")) == .lt); + expect(order(try parse("1.0.0-beta.2"), try parse("1.0.0-beta.11")) == .lt); + expect(order(try parse("1.0.0-beta.11"), try parse("1.0.0-rc.1")) == .lt); + expect(order(try parse("1.0.0-rc.1"), try parse("1.0.0")) == .lt); +} + +// This is copy-pasted from fmt.zig since it is not public. +fn testFmt(expected: []const u8, comptime template: []const u8, args: anytype) !void { + var buf: [100]u8 = undefined; + const result = try std.fmt.bufPrint(buf[0..], template, args); + if (std.mem.eql(u8, result, expected)) return; + + std.debug.warn("\n====== expected this output: =========\n", .{}); + std.debug.warn("{}", .{expected}); + std.debug.warn("\n======== instead found this: =========\n", .{}); + std.debug.warn("{}", .{result}); + std.debug.warn("\n======================================\n", .{}); + return error.TestFailed; +} diff --git a/lib/std/std.zig b/lib/std/std.zig index c499f7a5c..5386e2e03 100644 --- a/lib/std/std.zig +++ b/lib/std/std.zig @@ -32,6 +32,7 @@ pub const PriorityQueue = @import("priority_queue.zig").PriorityQueue; pub const Progress = @import("progress.zig").Progress; pub const ResetEvent = @import("reset_event.zig").ResetEvent; pub const SegmentedList = @import("segmented_list.zig").SegmentedList; +pub const SemanticVersion = @import("SemanticVersion.zig"); pub const SinglyLinkedList = @import("linked_list.zig").SinglyLinkedList; pub const SpinLock = @import("spinlock.zig").SpinLock; pub const StringHashMap = hash_map.StringHashMap;