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