pub const Version = VersionType(u64); pub const OldV2Version = VersionType(u32); pub fn VersionType(comptime IntType: type) type { return extern struct { major: IntType = 0, minor: IntType = 0, patch: IntType = 0, _tag_padding: [if (IntType == u32) 4 else 0]u8 = .{0} ** if (IntType == u32) 4 else 0, // [see padding_checker.zig] tag: Tag = .{}, const This = @This(); pub fn migrate(this: This) VersionType(u64) { if (comptime IntType != u32) { @compileError("unexpected IntType"); } return .{ .major = this.major, .minor = this.minor, .patch = this.patch, ._tag_padding = .{}, .tag = .{ .pre = this.tag.pre, .build = this.tag.build, }, }; } /// Assumes that there is only one buffer for all the strings pub fn sortGt(ctx: []const u8, lhs: This, rhs: This) bool { return orderFn(ctx, lhs, rhs) == .gt; } pub fn orderFn(ctx: []const u8, lhs: This, rhs: This) std.math.Order { return lhs.order(rhs, ctx, ctx); } pub fn isZero(this: This) bool { return this.patch == 0 and this.minor == 0 and this.major == 0; } pub fn parseUTF8(slice: []const u8) ParseResult { return parse(.{ .buf = slice, .slice = slice }); } pub fn cloneInto(this: This, slice: []const u8, buf: *[]u8) This { return .{ .major = this.major, .minor = this.minor, .patch = this.patch, .tag = this.tag.cloneInto(slice, buf), }; } pub inline fn len(this: *const This) u32 { return this.tag.build.len + this.tag.pre.len; } pub const Formatter = struct { version: This, input: string, pub fn format(formatter: Formatter, writer: *std.Io.Writer) !void { const self = formatter.version; try writer.print("{d}.{d}.{d}", .{ self.major, self.minor, self.patch }); if (self.tag.hasPre()) { const pre = self.tag.pre.slice(formatter.input); try writer.writeAll("-"); try writer.writeAll(pre); } if (self.tag.hasBuild()) { const build = self.tag.build.slice(formatter.input); try writer.writeAll("+"); try writer.writeAll(build); } } }; pub fn fmt(this: This, input: string) Formatter { return .{ .version = this, .input = input }; } pub const DiffFormatter = struct { version: This, buf: string, other: This, other_buf: string, pub fn format(this: DiffFormatter, writer: *std.Io.Writer) !void { if (!Output.enable_ansi_colors_stdout) { // print normally if no colors const formatter: Formatter = .{ .version = this.version, .input = this.buf }; return Formatter.format(formatter, writer); } const diff = this.version.whichVersionIsDifferent(this.other, this.buf, this.other_buf) orelse .none; switch (diff) { .major => try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }), .minor => { if (this.version.major == 0) { try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }); } else { try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }); } }, .patch => { if (this.version.major == 0 and this.version.minor == 0) { try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }); } else { try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }); } }, .none, .pre, .build => try writer.print(Output.prettyFmt("{d}.{d}.{d}", true), .{ this.version.major, this.version.minor, this.version.patch, }), } // might be pre or build. loop through all characters, and insert on // first diff. var set_color = false; if (this.version.tag.hasPre()) { if (this.other.tag.hasPre()) { const pre = this.version.tag.pre.slice(this.buf); const other_pre = this.other.tag.pre.slice(this.other_buf); var first = true; for (pre, 0..) |c, i| { if (!set_color and i < other_pre.len and c != other_pre[i]) { set_color = true; try writer.writeAll(Output.prettyFmt("", true)); } if (first) { first = false; try writer.writeByte('-'); } try writer.writeByte(c); } } else { try writer.print(Output.prettyFmt("-{f}", true), .{this.version.tag.pre.fmt(this.buf)}); set_color = true; } } if (this.version.tag.hasBuild()) { if (this.other.tag.hasBuild()) { const build = this.version.tag.build.slice(this.buf); const other_build = this.other.tag.build.slice(this.other_buf); var first = true; for (build, 0..) |c, i| { if (!set_color and i < other_build.len and c != other_build[i]) { set_color = true; try writer.writeAll(Output.prettyFmt("", true)); } if (first) { first = false; try writer.writeByte('+'); } try writer.writeByte(c); } } else { if (!set_color) { try writer.print(Output.prettyFmt("+{f}", true), .{this.version.tag.build.fmt(this.buf)}); } else { try writer.print("+{f}", .{this.version.tag.build.fmt(this.other_buf)}); } } } try writer.writeAll(Output.prettyFmt("", true)); } }; pub fn diffFmt(this: This, other: This, this_buf: string, other_buf: string) DiffFormatter { return .{ .version = this, .buf = this_buf, .other = other, .other_buf = other_buf, }; } pub const ChangedVersion = enum { major, minor, patch, pre, build, none, }; pub fn whichVersionIsDifferent( left: This, right: This, left_buf: string, right_buf: string, ) ?ChangedVersion { if (left.major != right.major) return .major; if (left.minor != right.minor) return .minor; if (left.patch != right.patch) return .patch; if (left.tag.hasPre() != right.tag.hasPre()) return .pre; if (!left.tag.hasPre() and !right.tag.hasPre()) return null; if (left.tag.orderPre(right.tag, left_buf, right_buf) != .eq) return .pre; if (left.tag.hasBuild() != right.tag.hasBuild()) return .build; if (!left.tag.hasBuild() and !right.tag.hasBuild()) return null; return if (left.tag.build.order(&right.tag.build, left_buf, right_buf) != .eq) .build else null; } pub fn count(this: *const This, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) void { if (this.tag.hasPre() and !this.tag.pre.isInline()) builder.count(this.tag.pre.slice(buf)); if (this.tag.hasBuild() and !this.tag.build.isInline()) builder.count(this.tag.build.slice(buf)); } pub fn append(this: *const This, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) This { var that = this.*; if (this.tag.hasPre() and !this.tag.pre.isInline()) that.tag.pre = builder.append(ExternalString, this.tag.pre.slice(buf)); if (this.tag.hasBuild() and !this.tag.build.isInline()) that.tag.build = builder.append(ExternalString, this.tag.build.slice(buf)); return that; } pub const Partial = struct { major: ?IntType = null, minor: ?IntType = null, patch: ?IntType = null, tag: Tag = .{}, pub fn min(this: Partial) This { return .{ .major = this.major orelse 0, .minor = this.minor orelse 0, .patch = this.patch orelse 0, .tag = this.tag, }; } pub fn max(this: Partial) This { return .{ .major = this.major orelse std.math.maxInt(IntType), .minor = this.minor orelse std.math.maxInt(IntType), .patch = this.patch orelse std.math.maxInt(IntType), .tag = this.tag, }; } }; pub fn eql(lhs: This, rhs: This) bool { return lhs.major == rhs.major and lhs.minor == rhs.minor and lhs.patch == rhs.patch and rhs.tag.eql(lhs.tag); } pub const PinnedVersion = enum { major, // ^ minor, // ~ patch, // = }; /// Modified version of pnpm's `whichVersionIsPinned` /// https://github.com/pnpm/pnpm/blob/bc0618cf192a9cafd0ab171a3673e23ed0869bbd/packages/which-version-is-pinned/src/index.ts#L9 /// /// Differences: /// - It's not used for workspaces /// - `npm:` is assumed already removed from aliased versions /// - Invalid input is considered major pinned (important because these strings are coming /// from package.json) /// /// The goal of this function is to avoid a complete parse of semver that's unused pub fn whichVersionIsPinned(input: string) PinnedVersion { const version = strings.trim(input, &strings.whitespace_chars); var i: usize = 0; const pinned: PinnedVersion = pinned: { for (0..version.len) |j| { switch (version[j]) { // newlines & whitespace ' ', '\t', '\n', '\r', std.ascii.control_code.vt, std.ascii.control_code.ff, // version separators 'v', '=', => {}, else => |c| { i = j; switch (c) { '~', '^' => { i += 1; for (i..version.len) |k| { switch (version[k]) { ' ', '\t', '\n', '\r', std.ascii.control_code.vt, std.ascii.control_code.ff, => { // `v` and `=` not included. // `~v==1` would update to `^1.1.0` if versions `1.0.0`, `1.0.1`, `1.1.0`, and `2.0.0` are available // note that `~` changes to `^` }, else => { i = k; break :pinned if (c == '~') .minor else .major; }, } } // entire version after `~` is whitespace. invalid return .major; }, '0'...'9' => break :pinned .patch, // could be invalid, could also be valid range syntax (>=, ...) // either way, pin major else => return .major, } }, } } // entire semver is whitespace, `v`, and `=`. Invalid return .major; }; // `pinned` is `.major`, `.minor`, or `.patch`. Check for each version core number: // - if major is missing, return `if (pinned == .patch) .major else pinned` // - if minor is missing, return `if (pinned == .patch) .minor else pinned` // - if patch is missing, return `pinned` // - if there's whitespace or non-digit characters between core numbers, return `.major` // - if the end is reached, return `pinned` // major if (i >= version.len or !std.ascii.isDigit(version[i])) return .major; var d = version[i]; while (std.ascii.isDigit(d)) { i += 1; if (i >= version.len) return if (pinned == .patch) .major else pinned; d = version[i]; } if (d != '.') return .major; // minor i += 1; if (i >= version.len or !std.ascii.isDigit(version[i])) return .major; d = version[i]; while (std.ascii.isDigit(d)) { i += 1; if (i >= version.len) return if (pinned == .patch) .minor else pinned; d = version[i]; } if (d != '.') return .major; // patch i += 1; if (i >= version.len or !std.ascii.isDigit(version[i])) return .major; d = version[i]; while (std.ascii.isDigit(d)) { i += 1; // patch is done and at input end, valid if (i >= version.len) return pinned; d = version[i]; } // Skip remaining valid pre/build tag characters and whitespace. // Does not validate whitespace used inside pre/build tags. if (!validPreOrBuildTagCharacter(d) or std.ascii.isWhitespace(d)) return .major; i += 1; // at this point the semver is valid so we can return true if it ends if (i >= version.len) return pinned; d = version[i]; while (validPreOrBuildTagCharacter(d) and !std.ascii.isWhitespace(d)) { i += 1; if (i >= version.len) return pinned; d = version[i]; } // We've come across a character that is not valid for tags or is whitespace. // Trailing whitespace was trimmed so we can assume there's another range return .major; } fn validPreOrBuildTagCharacter(c: u8) bool { return switch (c) { '-', '+', '.', 'A'...'Z', 'a'...'z', '0'...'9' => true, else => false, }; } pub fn isTaggedVersionOnly(input: []const u8) bool { const version = strings.trim(input, &strings.whitespace_chars); // first needs to be a-z if (version.len == 0 or !std.ascii.isAlphabetic(version[0])) return false; for (1..version.len) |i| { if (!std.ascii.isAlphanumeric(version[i])) return false; } return true; } pub fn orderWithoutTag( lhs: This, rhs: This, ) 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.tag.hasPre()) { if (!rhs.tag.hasPre()) return .lt; } else { if (rhs.tag.hasPre()) return .gt; } return .eq; } pub fn order( lhs: This, rhs: This, lhs_buf: []const u8, rhs_buf: []const u8, ) std.math.Order { const order_without_tag = orderWithoutTag(lhs, rhs); if (order_without_tag != .eq) return order_without_tag; return lhs.tag.order(rhs.tag, lhs_buf, rhs_buf); } pub fn orderWithoutBuild( lhs: This, rhs: This, lhs_buf: []const u8, rhs_buf: []const u8, ) std.math.Order { const order_without_tag = orderWithoutTag(lhs, rhs); if (order_without_tag != .eq) return order_without_tag; return lhs.tag.orderWithoutBuild(rhs.tag, lhs_buf, rhs_buf); } pub const Tag = extern struct { pre: ExternalString = ExternalString{}, build: ExternalString = ExternalString{}, pub fn orderPre(lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order { const lhs_str = lhs.pre.slice(lhs_buf); const rhs_str = rhs.pre.slice(rhs_buf); // 1. split each by '.', iterating through each one looking for integers // 2. compare as integers, or if not possible compare as string // 3. whichever is greater is the greater one // // 1.0.0-canary.0.0.0.0.0.0 < 1.0.0-canary.0.0.0.0.0.1 var lhs_itr = strings.split(lhs_str, "."); var rhs_itr = strings.split(rhs_str, "."); while (true) { const lhs_part = lhs_itr.next(); const rhs_part = rhs_itr.next(); if (lhs_part == null and rhs_part == null) return .eq; // if right is null, left is greater than. if (rhs_part == null) return .gt; // if left is null, left is less than. if (lhs_part == null) return .lt; const lhs_uint: ?IntType = std.fmt.parseUnsigned(IntType, lhs_part.?, 10) catch null; const rhs_uint: ?IntType = std.fmt.parseUnsigned(IntType, rhs_part.?, 10) catch null; // a part that doesn't parse as an integer is greater than a part that does // https://github.com/npm/node-semver/blob/816c7b2cbfcb1986958a290f941eddfd0441139e/internal/identifiers.js#L12 if (lhs_uint != null and rhs_uint == null) return .lt; if (lhs_uint == null and rhs_uint != null) return .gt; if (lhs_uint == null and rhs_uint == null) { switch (strings.order(lhs_part.?, rhs_part.?)) { .eq => { // continue to the next part continue; }, else => |not_equal| return not_equal, } } switch (std.math.order(lhs_uint.?, rhs_uint.?)) { .eq => continue, else => |not_equal| return not_equal, } } unreachable; } pub fn order( lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8, ) std.math.Order { if (!lhs.pre.isEmpty() and !rhs.pre.isEmpty()) { return lhs.orderPre(rhs, lhs_buf, rhs_buf); } const pre_order = lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf); if (pre_order != .eq) return pre_order; return lhs.build.order(&rhs.build, lhs_buf, rhs_buf); } pub fn orderWithoutBuild( lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8, ) std.math.Order { if (!lhs.pre.isEmpty() and !rhs.pre.isEmpty()) { return lhs.orderPre(rhs, lhs_buf, rhs_buf); } return lhs.pre.order(&rhs.pre, lhs_buf, rhs_buf); } pub fn cloneInto(this: Tag, slice: []const u8, buf: *[]u8) Tag { var pre: String = this.pre.value; var build: String = this.build.value; if (this.pre.isInline()) { pre = this.pre.value; } else { const pre_slice = this.pre.slice(slice); bun.copy(u8, buf.*, pre_slice); pre = String.init(buf.*, buf.*[0..pre_slice.len]); buf.* = buf.*[pre_slice.len..]; } if (this.build.isInline()) { build = this.build.value; } else { const build_slice = this.build.slice(slice); bun.copy(u8, buf.*, build_slice); build = String.init(buf.*, buf.*[0..build_slice.len]); buf.* = buf.*[build_slice.len..]; } return .{ .pre = .{ .value = pre, .hash = this.pre.hash, }, .build = .{ .value = build, .hash = this.build.hash, }, }; } pub inline fn hasPre(this: Tag) bool { return !this.pre.isEmpty(); } pub inline fn hasBuild(this: Tag) bool { return !this.build.isEmpty(); } pub fn eql(lhs: Tag, rhs: Tag) bool { return lhs.pre.hash == rhs.pre.hash; } pub const TagResult = struct { tag: Tag = Tag{}, len: u32 = 0, }; var multi_tag_warn = false; // TODO: support multiple tags pub fn parse(sliced_string: SlicedString) TagResult { return parseWithPreCount(sliced_string, 0); } pub fn parseWithPreCount(sliced_string: SlicedString, initial_pre_count: u32) TagResult { var input = sliced_string.slice; var build_count: u32 = 0; var pre_count: u32 = initial_pre_count; for (input) |c| { switch (c) { ' ' => break, '+' => { build_count += 1; }, '-' => { pre_count += 1; }, else => {}, } } if (build_count == 0 and pre_count == 0) { return TagResult{ .len = 0, }; } const State = enum { none, pre, build }; var result = TagResult{}; // Common case: no allocation is necessary. var state = State.none; var start: usize = 0; var i: usize = 0; while (i < input.len) : (i += 1) { const c = input[i]; switch (c) { '+' => { // qualifier ::= ( '-' pre )? ( '+' build )? if (state == .pre or state == .none and initial_pre_count > 0) { result.tag.pre = sliced_string.sub(input[start..i]).external(); } if (state != .build) { state = .build; start = i + 1; } }, '-' => { if (state != .pre) { state = .pre; start = i + 1; } }, // only continue if character is a valid pre/build tag character // https://semver.org/#spec-item-9 'a'...'z', 'A'...'Z', '0'...'9', '.' => {}, else => { switch (state) { .none => {}, .pre => { result.tag.pre = sliced_string.sub(input[start..i]).external(); state = State.none; }, .build => { result.tag.build = sliced_string.sub(input[start..i]).external(); if (comptime Environment.isDebug) { assert(!strings.containsChar(result.tag.build.slice(sliced_string.buf), '-')); } state = State.none; }, } result.len = @truncate(i); break; }, } } if (state == .none and initial_pre_count > 0) { state = .pre; start = 0; } switch (state) { .none => {}, .pre => { result.tag.pre = sliced_string.sub(input[start..i]).external(); // a pre can contain multiple consecutive tags // checking for "-" prefix is not enough, as --canary.67e7966.0 is a valid tag state = State.none; }, .build => { // a build can contain multiple consecutive tags result.tag.build = sliced_string.sub(input[start..i]).external(); state = State.none; }, } result.len = @as(u32, @truncate(i)); return result; } }; pub const ParseResult = struct { wildcard: Query.Token.Wildcard = .none, valid: bool = true, version: This.Partial = .{}, len: u32 = 0, }; pub fn parse(sliced_string: SlicedString) ParseResult { var input = sliced_string.slice; var result = ParseResult{}; var part_i: u8 = 0; var part_start_i: usize = 0; var last_char_i: usize = 0; if (input.len == 0) { result.valid = false; return result; } var is_done = false; var i: usize = 0; for (0..input.len) |c| { switch (input[c]) { // newlines & whitespace ' ', '\t', '\n', '\r', std.ascii.control_code.vt, std.ascii.control_code.ff, // version separators 'v', '=', => {}, else => { i = c; break; }, } } if (i == input.len) { result.valid = false; return result; } // two passes :( while (i < input.len) { if (is_done) { break; } switch (input[i]) { ' ' => { is_done = true; break; }, '|', '^', '#', '&', '%', '!' => { is_done = true; if (i > 0) { i -= 1; } break; }, '0'...'9' => { part_start_i = i; i += 1; while (i < input.len and switch (input[i]) { '0'...'9' => true, else => false, }) { i += 1; } last_char_i = i; switch (part_i) { 0 => { result.version.major = parseVersionNumber(input[part_start_i..last_char_i]); part_i = 1; }, 1 => { result.version.minor = parseVersionNumber(input[part_start_i..last_char_i]); part_i = 2; }, 2 => { result.version.patch = parseVersionNumber(input[part_start_i..last_char_i]); part_i = 3; }, else => {}, } if (i < input.len and switch (input[i]) { // `.` is expected only if there are remaining core version numbers '.' => part_i != 3, else => false, }) { i += 1; } }, '.' => { result.valid = false; is_done = true; break; }, '-', '+' => { // Just a plain tag with no version is invalid. if (part_i < 2 and result.wildcard == .none) { result.valid = false; is_done = true; break; } part_start_i = i; while (i < input.len and switch (input[i]) { ' ' => true, else => false, }) { i += 1; } const tag_result = Tag.parse(sliced_string.sub(input[part_start_i..])); result.version.tag = tag_result.tag; i += tag_result.len; break; }, 'x', '*', 'X' => { part_start_i = i; i += 1; while (i < input.len and switch (input[i]) { 'x', '*', 'X' => true, else => false, }) { i += 1; } last_char_i = i; if (i < input.len and switch (input[i]) { '.' => true, else => false, }) { i += 1; } if (result.wildcard == .none) { switch (part_i) { 0 => { result.wildcard = Query.Token.Wildcard.major; part_i = 1; }, 1 => { result.wildcard = Query.Token.Wildcard.minor; part_i = 2; }, 2 => { result.wildcard = Query.Token.Wildcard.patch; part_i = 3; }, else => {}, } } }, else => |c| { // Some weirdo npm packages in the wild have a version like "1.0.0rc.1" // npm just expects that to work...even though it has no "-" qualifier. if (result.wildcard == .none and part_i >= 2 and switch (c) { 'a'...'z', 'A'...'Z' => true, else => false, }) { part_start_i = i; const tag_result = Tag.parseWithPreCount(sliced_string.sub(input[part_start_i..]), 1); result.version.tag = tag_result.tag; i += tag_result.len; is_done = true; last_char_i = i; break; } last_char_i = 0; result.valid = false; is_done = true; break; }, } } if (result.wildcard == .none) { switch (part_i) { 0 => { result.wildcard = Query.Token.Wildcard.major; }, 1 => { result.wildcard = Query.Token.Wildcard.minor; }, 2 => { result.wildcard = Query.Token.Wildcard.patch; }, else => {}, } } result.len = @as(u32, @intCast(i)); return result; } fn parseVersionNumber(input: string) ?IntType { // max decimal u64 is 18446744073709551615 var bytes: [20]u8 = undefined; var byte_i: u8 = 0; assert(input[0] != '.'); for (input) |char| { switch (char) { 'X', 'x', '*' => return null, '0'...'9' => { // out of bounds if (byte_i + 1 > bytes.len) return null; bytes[byte_i] = char; byte_i += 1; }, ' ', '.' => break, // ignore invalid characters else => {}, } } // If there are no numbers if (byte_i == 0) return null; if (comptime Environment.isDebug) { return std.fmt.parseInt(IntType, bytes[0..byte_i], 10) catch |err| { Output.prettyErrorln("ERROR {s} parsing version: \"{s}\", bytes: {s}", .{ @errorName(err), input, bytes[0..byte_i], }); return 0; }; } return std.fmt.parseInt(IntType, bytes[0..byte_i], 10) catch 0; } }; } const string = []const u8; const std = @import("std"); const bun = @import("bun"); const Environment = bun.Environment; const Output = bun.Output; const assert = bun.assert; const strings = bun.strings; const ExternalString = bun.Semver.ExternalString; const Query = bun.Semver.Query; const SlicedString = bun.Semver.SlicedString; const String = bun.Semver.String;