const bun = @import("bun"); const string = bun.string; const Output = bun.Output; const Global = bun.Global; const Environment = bun.Environment; const strings = bun.strings; const MutableString = bun.MutableString; const stringZ = bun.stringZ; const default_allocator = bun.default_allocator; const C = bun.C; const std = @import("std"); /// String type that stores either an offset/length into an external buffer or a string inline directly pub const String = extern struct { pub const max_inline_len: usize = 8; /// This is three different types of string. /// 1. Empty string. If it's all zeroes, then it's an empty string. /// 2. If the final bit is set, then it's a string that is stored inline. /// 3. If the final bit is not set, then it's a string that is stored in an external buffer. bytes: [max_inline_len]u8 = [8]u8{ 0, 0, 0, 0, 0, 0, 0, 0 }, pub const Tag = enum { small, big, }; pub inline fn fmt(self: *const String, buf: []const u8) Formatter { return Formatter{ .buf = buf, .str = self, }; } pub const Formatter = struct { str: *const String, buf: string, pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { const str = formatter.str; try writer.writeAll(str.slice(formatter.buf)); } }; pub inline fn order( lhs: *const String, rhs: *const String, lhs_buf: []const u8, rhs_buf: []const u8, ) std.math.Order { return std.mem.order(u8, lhs.slice(lhs_buf), rhs.slice(rhs_buf)); } pub inline fn canInline(buf: []const u8) bool { return switch (buf.len) { 0...max_inline_len - 1 => true, max_inline_len => buf[max_inline_len - 1] & 0x80 == 0, else => false, }; } pub inline fn isInline(this: String) bool { return this.bytes[max_inline_len - 1] & 0x80 == 0; } pub inline fn sliced(this: *const String, buf: []const u8) SlicedString { return if (this.isInline()) SlicedString.init(this.slice(""), this.slice("")) else SlicedString.init(buf, this.slice(buf)); } // https://en.wikipedia.org/wiki/Intel_5-level_paging // https://developer.arm.com/documentation/101811/0101/Address-spaces-in-AArch64#:~:text=0%2DA%2C%20the%20maximum%20size,2%2DA. // X64 seems to need some of the pointer bits const max_addressable_space = u63; comptime { if (@sizeOf(usize) != 8) { @compileError("This code needs to be updated for non-64-bit architectures"); } } pub const HashContext = struct { a_buf: []const u8, b_buf: []const u8, pub fn eql(ctx: HashContext, a: String, b: String) bool { return a.eql(b, ctx.a_buf, ctx.b_buf); } pub fn hash(ctx: HashContext, a: String) u64 { const str = a.slice(ctx.a_buf); return bun.hash(str); } }; pub const ArrayHashContext = struct { a_buf: []const u8, b_buf: []const u8, pub fn eql(ctx: ArrayHashContext, a: String, b: String, _: usize) bool { return a.eql(b, ctx.a_buf, ctx.b_buf); } pub fn hash(ctx: ArrayHashContext, a: String) u32 { const str = a.slice(ctx.a_buf); return @truncate(u32, bun.hash(str)); } }; pub fn init( buf: string, in: string, ) String { return switch (in.len) { 0 => String{}, 1 => String{ .bytes = .{ in[0], 0, 0, 0, 0, 0, 0, 0 } }, 2 => String{ .bytes = .{ in[0], in[1], 0, 0, 0, 0, 0, 0 } }, 3 => String{ .bytes = .{ in[0], in[1], in[2], 0, 0, 0, 0, 0 } }, 4 => String{ .bytes = .{ in[0], in[1], in[2], in[3], 0, 0, 0, 0 } }, 5 => String{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], 0, 0, 0 } }, 6 => String{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], 0, 0 } }, 7 => String{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], 0 } }, max_inline_len => // If they use the final bit, then it's a big string. // This should only happen for non-ascii strings that are exactly 8 bytes. // so that's an edge-case if ((in[max_inline_len - 1]) >= 128) @bitCast(String, (@as( u64, 0, ) | @as( u64, @truncate( max_addressable_space, @bitCast( u64, Pointer.init(buf, in), ), ), )) | 1 << 63) else String{ .bytes = .{ in[0], in[1], in[2], in[3], in[4], in[5], in[6], in[7] } }, else => @bitCast( String, (@as( u64, 0, ) | @as( u64, @truncate( max_addressable_space, @bitCast( u64, Pointer.init(buf, in), ), ), )) | 1 << 63, ), }; } pub fn eql(this: String, that: String, this_buf: []const u8, that_buf: []const u8) bool { if (this.isInline() and that.isInline()) { return @bitCast(u64, this.bytes) == @bitCast(u64, that.bytes); } else if (this.isInline() != that.isInline()) { return false; } else { const a = this.ptr(); const b = that.ptr(); return strings.eql(this_buf[0..a.len], that_buf[0..b.len]); } } pub inline fn isEmpty(this: String) bool { return @bitCast(u64, this.bytes) == @as(u64, 0); } pub fn len(this: String) usize { switch (this.bytes[max_inline_len - 1] & 128) { 0 => { // Edgecase: string that starts with a 0 byte will be considered empty. switch (this.bytes[0]) { 0 => { return 0; }, else => { comptime var i: usize = 0; inline while (i < this.bytes.len) : (i += 1) { if (this.bytes[i] == 0) return i; } return 8; }, } }, else => { const ptr_ = this.ptr(); return ptr_.len; }, } } pub const Pointer = extern struct { off: u32 = 0, len: u32 = 0, pub inline fn init( buf: string, in: string, ) Pointer { std.debug.assert(bun.isSliceInBuffer(in, buf)); return Pointer{ .off = @truncate(u32, @ptrToInt(in.ptr) - @ptrToInt(buf.ptr)), .len = @truncate(u32, in.len), }; } }; pub inline fn ptr(this: String) Pointer { return @bitCast(Pointer, @as(u64, @truncate(u63, @bitCast(u64, this)))); } // String must be a pointer because we reference it as a slice. It will become a dead pointer if it is copied. pub fn slice(this: *const String, buf: string) string { switch (this.bytes[max_inline_len - 1] & 128) { 0 => { // Edgecase: string that starts with a 0 byte will be considered empty. switch (this.bytes[0]) { 0 => { return ""; }, else => { comptime var i: usize = 0; inline while (i < this.bytes.len) : (i += 1) { if (this.bytes[i] == 0) return this.bytes[0..i]; } return &this.bytes; }, } }, else => { const ptr_ = this.*.ptr(); return buf[ptr_.off..][0..ptr_.len]; }, } } pub const Builder = struct { const Allocator = @import("std").mem.Allocator; const assert = @import("std").debug.assert; const copy = @import("std").mem.copy; const IdentityContext = @import("../identity_context.zig").IdentityContext; len: usize = 0, cap: usize = 0, ptr: ?[*]u8 = null, string_pool: StringPool = undefined, pub const StringPool = std.HashMap(u64, String, IdentityContext(u64), 80); pub inline fn stringHash(buf: []const u8) u64 { return std.hash.Wyhash.hash(0, buf); } pub inline fn count(this: *Builder, slice_: string) void { return countWithHash(this, slice_, if (slice_.len >= String.max_inline_len) stringHash(slice_) else std.math.maxInt(u64)); } pub inline fn countWithHash(this: *Builder, slice_: string, hash: u64) void { if (slice_.len <= String.max_inline_len) return; if (!this.string_pool.contains(hash)) { this.cap += slice_.len; } } pub inline fn allocatedSlice(this: *Builder) []u8 { return if (this.cap > 0) this.ptr.?[0..this.cap] else &[_]u8{}; } pub fn allocate(this: *Builder, allocator: std.mem.Allocator) !void { var ptr_ = try allocator.alloc(u8, this.cap); this.ptr = ptr_.ptr; } pub fn append(this: *Builder, comptime Type: type, slice_: string) Type { return @call(.always_inline, appendWithHash, .{ this, Type, slice_, stringHash(slice_) }); } pub fn appendUTF8WithoutPool(this: *Builder, comptime Type: type, slice_: string, hash: u64) Type { if (slice_.len <= String.max_inline_len) { if (strings.isAllASCII(slice_)) { switch (Type) { String => { return String.init(this.allocatedSlice(), slice_); }, ExternalString => { return ExternalString.init(this.allocatedSlice(), slice_, hash); }, else => @compileError("Invalid type passed to StringBuilder"), } } } assert(this.len <= this.cap); // didn't count everything assert(this.ptr != null); // must call allocate first copy(u8, this.ptr.?[this.len..this.cap], slice_); const final_slice = this.ptr.?[this.len..this.cap][0..slice_.len]; this.len += slice_.len; assert(this.len <= this.cap); switch (Type) { String => { return String.init(this.allocatedSlice(), final_slice); }, ExternalString => { return ExternalString.init(this.allocatedSlice(), final_slice, hash); }, else => @compileError("Invalid type passed to StringBuilder"), } } // SlicedString is not supported due to inline strings. pub fn appendWithoutPool(this: *Builder, comptime Type: type, slice_: string, hash: u64) Type { if (slice_.len <= String.max_inline_len) { switch (Type) { String => { return String.init(this.allocatedSlice(), slice_); }, ExternalString => { return ExternalString.init(this.allocatedSlice(), slice_, hash); }, else => @compileError("Invalid type passed to StringBuilder"), } } assert(this.len <= this.cap); // didn't count everything assert(this.ptr != null); // must call allocate first copy(u8, this.ptr.?[this.len..this.cap], slice_); const final_slice = this.ptr.?[this.len..this.cap][0..slice_.len]; this.len += slice_.len; assert(this.len <= this.cap); switch (Type) { String => { return String.init(this.allocatedSlice(), final_slice); }, ExternalString => { return ExternalString.init(this.allocatedSlice(), final_slice, hash); }, else => @compileError("Invalid type passed to StringBuilder"), } } pub fn appendWithHash(this: *Builder, comptime Type: type, slice_: string, hash: u64) Type { if (slice_.len <= String.max_inline_len) { switch (Type) { String => { return String.init(this.allocatedSlice(), slice_); }, ExternalString => { return ExternalString.init(this.allocatedSlice(), slice_, hash); }, else => @compileError("Invalid type passed to StringBuilder"), } } assert(this.len <= this.cap); // didn't count everything assert(this.ptr != null); // must call allocate first var string_entry = this.string_pool.getOrPut(hash) catch unreachable; if (!string_entry.found_existing) { copy(u8, this.ptr.?[this.len..this.cap], slice_); const final_slice = this.ptr.?[this.len..this.cap][0..slice_.len]; this.len += slice_.len; string_entry.value_ptr.* = String.init(this.allocatedSlice(), final_slice); } assert(this.len <= this.cap); switch (Type) { String => { return string_entry.value_ptr.*; }, ExternalString => { return ExternalString{ .value = string_entry.value_ptr.*, .hash = hash, }; }, else => @compileError("Invalid type passed to StringBuilder"), } } }; comptime { if (@sizeOf(String) != @sizeOf(Pointer)) { @compileError("String types must be the same size"); } } }; test "String works" { { var buf: string = "hello world"; var world: string = buf[6..]; var str = String.init( buf, world, ); try std.testing.expectEqualStrings("world", str.slice(buf)); } { var buf: string = "hello"; var world: string = buf; var str = String.init( buf, world, ); try std.testing.expectEqualStrings("hello", str.slice(buf)); try std.testing.expectEqual(@bitCast(u64, str), @bitCast(u64, [8]u8{ 'h', 'e', 'l', 'l', 'o', 0, 0, 0 })); } { var buf: string = &[8]u8{ 'h', 'e', 'l', 'l', 'o', 'k', 'k', 129 }; var world: string = buf; var str = String.init( buf, world, ); try std.testing.expectEqualStrings(buf, str.slice(buf)); } } pub const ExternalString = extern struct { value: String = String{}, hash: u64 = 0, pub inline fn fmt(this: *const ExternalString, buf: []const u8) String.Formatter { return this.value.fmt(buf); } pub fn order(lhs: *const ExternalString, rhs: *const ExternalString, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order { if (lhs.hash == rhs.hash and lhs.hash > 0) return .eq; return lhs.value.order(&rhs.value, lhs_buf, rhs_buf); } /// ExternalString but without the hash pub inline fn from(in: string) ExternalString { return ExternalString{ .value = String.init(in, in), .hash = std.hash.Wyhash.hash(0, in), }; } pub inline fn isInline(this: ExternalString) bool { return this.value.isInline(); } pub inline fn isEmpty(this: ExternalString) bool { return this.value.isEmpty(); } pub inline fn len(this: ExternalString) usize { return this.value.len(); } pub inline fn init(buf: string, in: string, hash: u64) ExternalString { return ExternalString{ .value = String.init(buf, in), .hash = hash, }; } pub inline fn slice(this: ExternalString, buf: string) string { return this.value.slice(buf); } }; pub const BigExternalString = extern struct { off: u32 = 0, len: u32 = 0, hash: u64 = 0, pub fn from(in: string) BigExternalString { return BigExternalString{ .off = 0, .len = @truncate(u32, in.len), .hash = std.hash.Wyhash.hash(0, in), }; } pub inline fn init(buf: string, in: string, hash: u64) BigExternalString { std.debug.assert(@ptrToInt(buf.ptr) <= @ptrToInt(in.ptr) and ((@ptrToInt(in.ptr) + in.len) <= (@ptrToInt(buf.ptr) + buf.len))); return BigExternalString{ .off = @truncate(u32, @ptrToInt(in.ptr) - @ptrToInt(buf.ptr)), .len = @truncate(u32, in.len), .hash = hash, }; } pub fn slice(this: BigExternalString, buf: string) string { return buf[this.off..][0..this.len]; } }; pub const SlicedString = struct { buf: string, slice: string, pub inline fn init(buf: string, slice: string) SlicedString { return SlicedString{ .buf = buf, .slice = slice }; } pub inline fn external(this: SlicedString) ExternalString { if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); return ExternalString.init(this.buf, this.slice, std.hash.Wyhash.hash(0, this.slice)); } pub inline fn value(this: SlicedString) String { if (comptime Environment.isDebug or Environment.isTest) std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.slice.ptr) and ((@ptrToInt(this.slice.ptr) + this.slice.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); return String.init(this.buf, this.slice); } pub inline fn sub(this: SlicedString, input: string) SlicedString { std.debug.assert(@ptrToInt(this.buf.ptr) <= @ptrToInt(this.buf.ptr) and ((@ptrToInt(input.ptr) + input.len) <= (@ptrToInt(this.buf.ptr) + this.buf.len))); return SlicedString{ .buf = this.buf, .slice = input }; } }; const RawType = void; pub const Version = extern struct { major: u32 = 0, minor: u32 = 0, patch: u32 = 0, tag: Tag = Tag{}, // raw: RawType = RawType{}, /// Assumes that there is only one buffer for all the strings pub fn sortGt(ctx: []const u8, lhs: Version, rhs: Version) bool { return orderFn(ctx, lhs, rhs) == .gt; } pub fn orderFn(ctx: []const u8, lhs: Version, rhs: Version) std.math.Order { return lhs.order(rhs, ctx, ctx); } pub fn cloneInto(this: Version, slice: []const u8, buf: *[]u8) Version { return Version{ .major = this.major, .minor = this.minor, .patch = this.patch, .tag = this.tag.cloneInto(slice, buf), }; } pub inline fn len(this: *const Version) u32 { return this.tag.build.len + this.tag.pre.len; } pub fn fmt(this: Version, input: string) Formatter { return Formatter{ .version = this, .input = input }; } pub fn count(this: Version, 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 clone(this: Version, buf: []const u8, comptime StringBuilder: type, builder: StringBuilder) Version { 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; } const HashableVersion = extern struct { major: u32, minor: u32, patch: u32, pre: u64, build: u64 }; pub fn hash(this: Version) u64 { const hashable = HashableVersion{ .major = this.major, .minor = this.minor, .patch = this.patch, .pre = this.tag.pre.hash, .build = this.tag.build.hash }; const bytes = std.mem.asBytes(&hashable); return std.hash.Wyhash.hash(0, bytes); } pub const Formatter = struct { version: Version, input: string, pub fn format(formatter: Formatter, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { const self = formatter.version; try std.fmt.format(writer, "{d}.{d}.{d}", .{ self.major, self.minor, self.patch }); if (self.tag.pre.len() > 0) { const pre = self.tag.pre.slice(formatter.input); try writer.writeAll("-"); try writer.writeAll(pre); } if (self.tag.build.len() > 0) { const build = self.tag.build.slice(formatter.input); try writer.writeAll("+"); try writer.writeAll(build); } } }; pub fn eql(lhs: Version, rhs: Version) bool { return lhs.major == rhs.major and lhs.minor == rhs.minor and lhs.patch == rhs.patch and rhs.tag.eql(lhs.tag); } pub const HashContext = struct { pub fn hash(_: @This(), lhs: Version) u32 { return @truncate(u32, lhs.hash()); } pub fn eql(_: @This(), lhs: Version, rhs: Version) bool { return lhs.eql(rhs); } }; pub fn orderWithoutTag( 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.tag.hasPre() != rhs.tag.hasPre()) return if (lhs.tag.hasPre()) .lt else .gt; if (lhs.tag.hasBuild() != rhs.tag.hasBuild()) return if (lhs.tag.hasBuild()) .gt else .lt; return .eq; } pub fn order( lhs: Version, rhs: Version, 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 const Tag = extern struct { pre: ExternalString = ExternalString{}, build: ExternalString = ExternalString{}, pub fn order(lhs: Tag, rhs: Tag, lhs_buf: []const u8, rhs_buf: []const u8) std.math.Order { 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 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); std.mem.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); std.mem.copy(u8, buf.*, build_slice); build = String.init(buf.*, buf.*[0..build_slice.len]); buf.* = buf.*[build_slice.len..]; } return Tag{ .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.build.hash == rhs.build.hash and 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(allocator: std.mem.Allocator, sliced_string: SlicedString) TagResult { return parseWithPreCount(allocator, sliced_string, 0); } pub fn parseWithPreCount(_: std.mem.Allocator, 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) { ' ' => { switch (state) { .none => {}, .pre => { result.tag.pre = sliced_string.sub(input[start..i]).external(); if (comptime Environment.isDebug) { std.debug.assert(!strings.containsChar(result.tag.pre.slice(sliced_string.buf), '-')); } state = State.none; }, .build => { result.tag.build = sliced_string.sub(input[start..i]).external(); if (comptime Environment.isDebug) { std.debug.assert(!strings.containsChar(result.tag.build.slice(sliced_string.buf), '-')); } state = State.none; }, } result.len = @truncate(u32, i); break; }, '+' => { // qualifier ::= ( '-' pre )? ( '+' build )? if (state == .pre) { result.tag.pre = sliced_string.sub(input[start..i]).external(); if (comptime Environment.isDebug) { std.debug.assert(!strings.containsChar(result.tag.pre.slice(sliced_string.buf), '-')); } } if (state != .build) { state = .build; start = i + 1; } }, '-' => { if (state != .pre) { state = .pre; start = i + 1; } }, else => {}, } } if (state == .none and initial_pre_count > 0) { state = .pre; start = 0; result.tag.pre = sliced_string.sub(input[start..i]).external(); } switch (state) { .none => {}, .pre => { result.tag.pre = sliced_string.sub(input[start..i]).external(); // a pre can contain multiple consecutive tags if (comptime Environment.isDebug) { std.debug.assert(!strings.startsWithChar(result.tag.pre.slice(sliced_string.buf), '-')); } state = State.none; }, .build => { // a build can contain multiple consecutive tags result.tag.build = sliced_string.sub(input[start..i]).external(); if (comptime Environment.isDebug) { std.debug.assert(!strings.startsWithChar(result.tag.build.slice(sliced_string.buf), '+')); } state = State.none; }, } result.len = @truncate(u32, i); return result; } }; pub const ParseResult = struct { wildcard: Query.Token.Wildcard = Query.Token.Wildcard.none, valid: bool = true, version: Version = Version{}, stopped_at: u32 = 0, }; pub fn parse(sliced_string: SlicedString, allocator: std.mem.Allocator) 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 stopped_at: i32 = 0; var i: usize = 0; // two passes :( while (i < input.len) { if (is_done) { break; } stopped_at = @intCast(i32, i); switch (input[i]) { ' ' => { is_done = true; break; }, '|', '^', '#', '&', '%', '!' => { is_done = true; stopped_at -= 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]) { '.' => true, else => false, }) { i += 1; } }, '.' => { result.valid = false; is_done = true; break; }, '-', '+' => { // Just a plain tag with no version is invalid. if (part_i < 2) { result.valid = false; is_done = true; break; } part_start_i = i; i += 1; while (i < input.len and switch (input[i]) { ' ' => true, else => false, }) { i += 1; } const tag_result = Tag.parse(allocator, 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 => unreachable, } } }, 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 ((c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z'))) { part_start_i = i; const tag_result = Tag.parseWithPreCount(allocator, 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.stopped_at = @intCast(u32, i); if (comptime RawType != void) { result.version.raw = sliced_string.sub(input[0..i]).external(); } return result; } fn parseVersionNumber(input: string) u32 { // max decimal u32 is 4294967295 var bytes: [10]u8 = undefined; var byte_i: u8 = 0; std.debug.assert(input[0] != '.'); for (input) |char| { switch (char) { 'X', 'x', '*' => return 0, '0'...'9' => { // out of bounds if (byte_i + 1 > bytes.len) return 0; bytes[byte_i] = char; byte_i += 1; }, ' ', '.' => break, // ignore invalid characters else => {}, } } // If there are no numbers, it's 0. if (byte_i == 0) return 0; if (comptime Environment.isDebug) { return std.fmt.parseInt(u32, 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(u32, bytes[0..byte_i], 10) catch 0; } }; pub const Range = struct { pub const Op = enum(u8) { unset = 0, eql = 1, lt = 3, lte = 4, gt = 5, gte = 6, }; left: Comparator = Comparator{}, right: Comparator = Comparator{}, /// * /// >= 0.0.0 /// >= 0 /// >= 0.0 /// >= x /// >= 0 pub fn anyRangeSatisfies(this: *const Range) bool { return this.left.op == .gte and this.left.version.eql(Version{}); } pub fn initWildcard(version: Version, wildcard: Query.Token.Wildcard) Range { switch (wildcard) { .none => { return Range{ .left = Comparator{ .op = Op.eql, .version = version, }, }; }, .major => { return Range{ .left = Comparator{ .op = Op.gte, .version = Version{ // .raw = version.raw }, }, }; }, .minor => { var lhs = Version{ // .raw = version.raw }; lhs.major = version.major + 1; var rhs = Version{ // .raw = version.raw }; rhs.major = version.major; return Range{ .left = Comparator{ .op = Op.lt, .version = lhs, }, .right = Comparator{ .op = Op.gte, .version = rhs, }, }; }, .patch => { var lhs = Version{}; lhs.major = version.major; lhs.minor = version.minor + 1; var rhs = Version{}; rhs.major = version.major; rhs.minor = version.minor; // rhs.raw = version.raw; // lhs.raw = version.raw; return Range{ .left = Comparator{ .op = Op.lt, .version = lhs, }, .right = Comparator{ .op = Op.gte, .version = rhs, }, }; }, } } pub inline fn hasLeft(this: Range) bool { return this.left.op != Op.unset; } pub inline fn hasRight(this: Range) bool { return this.right.op != Op.unset; } /// Is the Range equal to another Range /// This does not evaluate the range. pub inline fn eql(lhs: Range, rhs: Range) bool { return lhs.left.eql(rhs.left) and lhs.right.eql(rhs.right); } pub const Comparator = struct { op: Op = Op.unset, version: Version = Version{}, pub inline fn eql(lhs: Comparator, rhs: Comparator) bool { return lhs.op == rhs.op and lhs.version.eql(rhs.version); } pub fn satisfies(this: Comparator, version: Version) bool { const order = version.orderWithoutTag(this.version); return switch (order) { .eq => switch (this.op) { .lte, .gte, .eql => true, else => false, }, .gt => switch (this.op) { .gt, .gte => true, else => false, }, .lt => switch (this.op) { .lt, .lte => true, else => false, }, }; } }; pub fn satisfies(this: Range, version: Version) bool { if (!this.hasLeft()) { return true; } if (!this.left.satisfies(version)) { return false; } if (this.hasRight() and !this.right.satisfies(version)) { return false; } return true; } }; /// Linked-list of AND ranges /// "^1 ^2" /// ----|----- /// That is two Query pub const Query = struct { pub const Op = enum { none, AND, OR, }; range: Range = Range{}, // AND next: ?*Query = null, /// Linked-list of Queries OR'd together /// "^1 || ^2" /// ----|----- /// That is two List pub const List = struct { head: Query = Query{}, tail: ?*Query = null, // OR next: ?*List = null, pub fn satisfies(this: *const List, version: Version) bool { return this.head.satisfies(version) or (this.next orelse return false).satisfies(version); } pub fn eql(lhs: *const List, rhs: *const List) bool { if (!lhs.head.eql(&rhs.head)) return false; var lhs_next = lhs.next orelse return rhs.next == null; var rhs_next = rhs.next orelse return false; return lhs_next.eql(rhs_next); } pub fn andRange(self: *List, allocator: std.mem.Allocator, range: Range) !void { if (!self.head.range.hasLeft() and !self.head.range.hasRight()) { self.head.range = range; return; } var tail = try allocator.create(Query); tail.* = Query{ .range = range, }; tail.range = range; var last_tail = self.tail orelse &self.head; last_tail.next = tail; self.tail = tail; } }; pub const Group = struct { head: List = List{}, tail: ?*List = null, allocator: std.mem.Allocator, input: string = "", flags: FlagsBitSet = FlagsBitSet.initEmpty(), pub const Flags = struct { pub const pre = 1; pub const build = 0; }; pub fn deinit(this: *Group) void { var list = this.head; var allocator = this.allocator; while (list.next) |next| { var query = list.head; while (query.next) |next_query| { allocator.destroy(next_query); query = next_query.*; } allocator.destroy(next); list = next.*; } } pub fn from(version: Version) Group { return .{ .allocator = bun.default_allocator, .head = .{ .head = .{ .range = .{ .left = .{ .op = .eql, .version = version, }, }, }, }, }; } pub const FlagsBitSet = std.bit_set.IntegerBitSet(3); pub fn isExact(this: *const Group) bool { return this.head.next == null and this.head.head.next == null and !this.head.head.range.hasRight() and this.head.head.range.left.op == .eql; } pub inline fn eql(lhs: Group, rhs: Group) bool { return lhs.head.eql(&rhs.head); } pub fn toVersion(this: Group) Version { std.debug.assert(this.isExact() or this.head.head.range.left.op == .unset); return this.head.head.range.left.version; } pub fn orVersion(self: *Group, version: Version) !void { if (self.tail == null and !self.head.head.range.hasLeft()) { self.head.head.range.left.version = version; self.head.head.range.left.op = .eql; return; } var new_tail = try self.allocator.create(List); new_tail.* = List{}; new_tail.head.range.left.version = version; new_tail.head.range.left.op = .eql; var prev_tail = self.tail orelse &self.head; prev_tail.next = new_tail; self.tail = new_tail; } pub fn andRange(self: *Group, range: Range) !void { var tail = self.tail orelse &self.head; try tail.andRange(self.allocator, range); } pub fn orRange(self: *Group, range: Range) !void { if (self.tail == null and self.head.tail == null and !self.head.head.range.hasLeft()) { self.head.head.range = range; return; } var new_tail = try self.allocator.create(List); new_tail.* = List{}; new_tail.head.range = range; var prev_tail = self.tail orelse &self.head; prev_tail.next = new_tail; self.tail = new_tail; } pub inline fn satisfies(this: *const Group, version: Version) bool { return this.head.satisfies(version); } }; pub fn eql(lhs: *const Query, rhs: *const Query) bool { if (!lhs.range.eql(rhs.range)) return false; const lhs_next = lhs.next orelse return rhs.next == null; const rhs_next = rhs.next orelse return false; return lhs_next.eql(rhs_next); } pub fn satisfies(this: *const Query, version: Version) bool { const left = this.range.satisfies(version); return left and (this.next orelse return true).satisfies(version); } pub const Token = struct { tag: Tag = Tag.none, wildcard: Wildcard = Wildcard.none, pub fn toRange(this: Token, version: Version) Range { switch (this.tag) { // Allows changes that do not modify the left-most non-zero element in the [major, minor, patch] tuple .caret => { var right_version = version; // https://github.com/npm/node-semver/blob/cb1ca1d5480a6c07c12ac31ba5f2071ed530c4ed/classes/range.js#L310-L336 if (right_version.major == 0) { if (right_version.minor == 0) { right_version.patch += 1; } else { right_version.minor += 1; right_version.patch = 0; } } else { right_version.major += 1; right_version.patch = 0; right_version.minor = 0; } return Range{ .left = .{ .op = .gte, .version = version, }, .right = .{ .op = .lt, .version = right_version, }, }; }, .tilda => { if (this.wildcard == .minor or this.wildcard == .major) { return Range.initWildcard(version, .minor); } // This feels like it needs to be tested more. var right_version = version; right_version.minor += 1; right_version.patch = 0; return Range{ .left = .{ .op = .gte, .version = version, }, .right = .{ .op = .lt, .version = right_version, }, }; }, .none => unreachable, .version => { if (this.wildcard != Wildcard.none) { return Range.initWildcard(version, this.wildcard); } return Range{ .left = .{ .op = .eql, .version = version } }; }, else => {}, } return switch (this.wildcard) { .major => Range{ .left = .{ .op = .gte, .version = version }, .right = .{ .op = .lte, .version = Version{ .major = std.math.maxInt(u32), .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), }, }, }, .minor => switch (this.tag) { .lte => Range{ .left = .{ .op = .lte, .version = Version{ .major = version.major, .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), }, }, }, .lt => Range{ .left = .{ .op = .lt, .version = Version{ .major = version.major, .minor = 0, .patch = 0, }, }, }, .gt => Range{ .left = .{ .op = .gt, .version = Version{ .major = version.major, .minor = std.math.maxInt(u32), .patch = std.math.maxInt(u32), }, }, }, .gte => Range{ .left = .{ .op = .gte, .version = Version{ .major = version.major, .minor = 0, .patch = 0, }, }, }, else => unreachable, }, .patch => switch (this.tag) { .lte => Range{ .left = .{ .op = .lte, .version = Version{ .major = version.major, .minor = version.minor, .patch = std.math.maxInt(u32), }, }, }, .lt => Range{ .left = .{ .op = .lt, .version = Version{ .major = version.major, .minor = version.minor, .patch = 0, }, }, }, .gt => Range{ .left = .{ .op = .gt, .version = Version{ .major = version.major, .minor = version.minor, .patch = std.math.maxInt(u32), }, }, }, .gte => Range{ .left = .{ .op = .gte, .version = Version{ .major = version.major, .minor = version.minor, .patch = 0, }, }, }, else => unreachable, }, .none => Range{ .left = .{ .op = switch (this.tag) { .gt => .gt, .gte => .gte, .lt => .lt, .lte => .lte, else => unreachable, }, .version = version, }, }, }; } pub const Tag = enum { none, gt, gte, lt, lte, version, tilda, caret, }; pub const Wildcard = enum { none, major, minor, patch, }; }; pub fn parse( allocator: std.mem.Allocator, input: string, sliced: SlicedString, ) !Group { var i: usize = 0; var list = Group{ .allocator = allocator, .input = input, }; var token = Token{}; var prev_token = Token{}; var count: u8 = 0; var skip_round = false; var is_or = false; while (i < input.len) { skip_round = false; switch (input[i]) { '>' => { if (input.len > i + 1 and input[i + 1] == '=') { token.tag = .gte; i += 1; } else { token.tag = .gt; } i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, '<' => { if (input.len > i + 1 and input[i + 1] == '=') { token.tag = .lte; i += 1; } else { token.tag = .lt; } i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, '=', 'v' => { token.tag = .version; is_or = true; i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, '~' => { token.tag = .tilda; i += 1; if (i < input.len and input[i] == '>') i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, '^' => { token.tag = .caret; i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, '0'...'9', 'X', 'x', '*' => { token.tag = .version; is_or = true; }, '|' => { i += 1; while (i < input.len and input[i] == '|') : (i += 1) {} while (i < input.len and input[i] == ' ') : (i += 1) {} is_or = true; token.tag = Token.Tag.none; skip_round = true; }, '-' => { i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} }, ' ' => { i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} skip_round = true; }, else => { i += 1; token.tag = Token.Tag.none; skip_round = true; }, } if (!skip_round) { const parse_result = Version.parse(sliced.sub(input[i..]), allocator); if (parse_result.version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); if (parse_result.version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); token.wildcard = parse_result.wildcard; i += parse_result.stopped_at; const rollback = i; const had_space = i < input.len and input[i] == ' '; // TODO: can we do this without rolling back? const hyphenate: bool = had_space and possibly_hyphenate: { i += 1; while (i < input.len and input[i] == ' ') : (i += 1) {} if (!(i < input.len and input[i] == '-')) break :possibly_hyphenate false; i += 1; if (!(i < input.len and input[i] == ' ')) break :possibly_hyphenate false; i += 1; while (i < input.len and switch (input[i]) { ' ', 'v', '=' => true, else => false, }) : (i += 1) {} if (!(i < input.len and switch (input[i]) { '0'...'9', 'X', 'x', '*' => true, else => false, })) break :possibly_hyphenate false; break :possibly_hyphenate true; }; if (!hyphenate) i = rollback; i += @as(usize, @boolToInt(!hyphenate)); if (hyphenate) { var second_version = Version.parse(sliced.sub(input[i..]), allocator); if (second_version.version.tag.hasBuild()) list.flags.setValue(Group.Flags.build, true); if (second_version.version.tag.hasPre()) list.flags.setValue(Group.Flags.pre, true); const range: Range = brk: { switch (second_version.wildcard) { .major => { second_version.version.major += 1; break :brk Range{ .left = .{ .op = .gte, .version = parse_result.version }, .right = .{ .op = .lte, .version = second_version.version }, }; }, .minor => { second_version.version.major += 1; second_version.version.minor = 0; second_version.version.patch = 0; break :brk Range{ .left = .{ .op = .gte, .version = parse_result.version }, .right = .{ .op = .lt, .version = second_version.version }, }; }, .patch => { second_version.version.minor += 1; second_version.version.patch = 0; break :brk Range{ .left = .{ .op = .gte, .version = parse_result.version }, .right = .{ .op = .lt, .version = second_version.version }, }; }, .none => { break :brk Range{ .left = .{ .op = .gte, .version = parse_result.version }, .right = .{ .op = .lte, .version = second_version.version }, }; }, } }; if (is_or) { try list.orRange(range); } else { try list.andRange(range); } i += second_version.stopped_at + 1; } else if (count == 0 and token.tag == .version) { switch (parse_result.wildcard) { .none => { try list.orVersion(parse_result.version); }, else => { try list.orRange(token.toRange(parse_result.version)); }, } } else if (count == 0) { try list.andRange(token.toRange(parse_result.version)); } else if (is_or) { try list.orRange(token.toRange(parse_result.version)); } else { try list.andRange(token.toRange(parse_result.version)); } is_or = false; count += 1; token.wildcard = .none; prev_token.tag = token.tag; } } return list; } }; const expect = struct { pub var counter: usize = 0; pub fn isRangeMatch(input: string, version_str: string) bool { var parsed = Version.parse(SlicedString.init(version_str, version_str), default_allocator); std.debug.assert(parsed.valid); // std.debug.assert(strings.eql(parsed.version.raw.slice(version_str), version_str)); var list = Query.parse( default_allocator, input, SlicedString.init(input, input), ) catch |err| Output.panic("Test fail due to error {s}", .{@errorName(err)}); return list.satisfies(parsed.version); } pub fn range(input: string, version_str: string, src: std.builtin.SourceLocation) void { Output.initTest(); defer counter += 1; if (!isRangeMatch(input, version_str)) { Output.panic("Fail Expected range \"{s}\" to match \"{s}\"\nAt: {s}:{d}:{d} in {s}", .{ input, version_str, src.file, src.line, src.column, src.fn_name, }); } } pub fn notRange(input: string, version_str: string, src: std.builtin.SourceLocation) void { Output.initTest(); defer counter += 1; if (isRangeMatch(input, version_str)) { Output.panic("Fail Expected range \"{s}\" NOT match \"{s}\"\nAt: {s}:{d}:{d} in {s}", .{ input, version_str, src.file, src.line, src.column, src.fn_name, }); } } pub fn done(src: std.builtin.SourceLocation) void { Output.prettyErrorln("{d} passed expectations in {s}", .{ counter, src.fn_name }); Output.flush(); counter = 0; } pub fn version(input: string, v: [3]u32, src: std.builtin.SourceLocation) void { Output.initTest(); defer counter += 1; var result = Version.parse(SlicedString.init(input, input), default_allocator); var other = Version{ .major = v[0], .minor = v[1], .patch = v[2] }; std.debug.assert(result.valid); if (!other.eql(result.version)) { Output.panic("Fail Expected version \"{s}\" to match \"{d}.{d}.{d}\" but received \"{d}.{d}.{d}\"\nAt: {s}:{d}:{d} in {s}", .{ input, v[0], v[1], v[2], result.version.major, result.version.minor, result.version.patch, src.file, src.line, src.column, src.fn_name, }); } } pub fn versionT(input: string, v: Version, src: std.builtin.SourceLocation) void { Output.initTest(); defer counter += 1; var result = Version.parse(SlicedString.init(input, input), default_allocator); if (!v.eql(result.version)) { Output.panic("Fail Expected version \"{s}\" to match \"{s}\" but received \"{}\"\nAt: {s}:{d}:{d} in {s}", .{ input, v, result.version, src.file, src.line, src.column, src.fn_name, }); } } }; test "Version parsing" { defer expect.done(@src()); expect.version("1.0.0", .{ 1, 0, 0 }, @src()); expect.version("1.1.0", .{ 1, 1, 0 }, @src()); expect.version("1.1.1", .{ 1, 1, 1 }, @src()); expect.version("1.1.0", .{ 1, 1, 0 }, @src()); expect.version("0.1.1", .{ 0, 1, 1 }, @src()); expect.version("0.0.1", .{ 0, 0, 1 }, @src()); expect.version("0.0.0", .{ 0, 0, 0 }, @src()); expect.version("1.x", .{ 1, 0, 0 }, @src()); expect.version("2.2.x", .{ 2, 2, 0 }, @src()); expect.version("2.x.2", .{ 2, 0, 2 }, @src()); expect.version("1.X", .{ 1, 0, 0 }, @src()); expect.version("2.2.X", .{ 2, 2, 0 }, @src()); expect.version("2.X.2", .{ 2, 0, 2 }, @src()); expect.version("1.*", .{ 1, 0, 0 }, @src()); expect.version("2.2.*", .{ 2, 2, 0 }, @src()); expect.version("2.*.2", .{ 2, 0, 2 }, @src()); expect.version("3", .{ 3, 0, 0 }, @src()); expect.version("3.x", .{ 3, 0, 0 }, @src()); expect.version("3.x.x", .{ 3, 0, 0 }, @src()); expect.version("3.*.*", .{ 3, 0, 0 }, @src()); expect.version("3.X.x", .{ 3, 0, 0 }, @src()); expect.version("0.0.0", .{ 0, 0, 0 }, @src()); { var v = Version{ .major = 1, .minor = 0, .patch = 0, }; var input: string = "1.0.0-beta"; v.tag.pre = SlicedString.init(input, input["1.0.0-".len..]).external(); expect.versionT(input, v, @src()); } { var v = Version{ .major = 1, .minor = 0, .patch = 0, }; var input: string = "1.0.0beta"; v.tag.pre = SlicedString.init(input, input["1.0.0".len..]).external(); expect.versionT(input, v, @src()); } { var v = Version{ .major = 1, .minor = 0, .patch = 0, }; var input: string = "1.0.0-build101"; v.tag.pre = SlicedString.init(input, input["1.0.0-".len..]).external(); expect.versionT(input, v, @src()); } { var v = Version{ .major = 0, .minor = 21, .patch = 0, }; var input: string = "0.21.0-beta-96ca8d915-20211115"; v.tag.pre = SlicedString.init(input, input["0.21.0-".len..]).external(); expect.versionT(input, v, @src()); } { var v = Version{ .major = 1, .minor = 0, .patch = 0, }; var input: string = "1.0.0-beta+build101"; v.tag.build = SlicedString.init(input, input["1.0.0-beta+".len..]).external(); v.tag.pre = SlicedString.init(input, input["1.0.0-".len..][0..4]).external(); expect.versionT(input, v, @src()); } var buf: [1024]u8 = undefined; var triplet = [3]u32{ 0, 0, 0 }; var x: u32 = 0; var y: u32 = 0; var z: u32 = 0; while (x < 32) : (x += 1) { while (y < 32) : (y += 1) { while (z < 32) : (z += 1) { triplet[0] = x; triplet[1] = y; triplet[2] = z; expect.version(try std.fmt.bufPrint(&buf, "{d}.{d}.{d}", .{ x, y, z }), triplet, @src()); triplet[0] = z; triplet[1] = x; triplet[2] = y; expect.version(try std.fmt.bufPrint(&buf, "{d}.{d}.{d}", .{ z, x, y }), triplet, @src()); triplet[0] = y; triplet[1] = x; triplet[2] = z; expect.version(try std.fmt.bufPrint(&buf, "{d}.{d}.{d}", .{ y, x, z }), triplet, @src()); } } } } test "Range parsing" { defer expect.done(@src()); expect.range("~1.2.3", "1.2.3", @src()); expect.range("~1.2", "1.2.0", @src()); expect.range("~1", "1.0.0", @src()); expect.range("~1", "1.2.0", @src()); expect.range("~1", "1.2.999", @src()); expect.range("~0.2.3", "0.2.3", @src()); expect.range("~0.2", "0.2.0", @src()); expect.range("~0.2", "0.2.1", @src()); expect.range("~0 ", "0.0.0", @src()); expect.notRange("~1.2.3", "1.3.0", @src()); expect.notRange("~1.2", "1.3.0", @src()); expect.notRange("~1", "2.0.0", @src()); expect.notRange("~0.2.3", "0.3.0", @src()); expect.notRange("~0.2.3", "1.0.0", @src()); expect.notRange("~0 ", "1.0.0", @src()); expect.notRange("~0.2", "0.1.0", @src()); expect.notRange("~0.2", "0.3.0", @src()); expect.notRange("~3.0.5", "3.3.0", @src()); expect.range("^1.1.4", "1.1.4", @src()); expect.range(">=3", "3.5.0", @src()); expect.notRange(">=3", "2.999.999", @src()); expect.range(">=3", "3.5.1", @src()); expect.range(">=3", "4", @src()); expect.range("<6 >= 5", "5.0.0", @src()); expect.notRange("<6 >= 5", "4.0.0", @src()); expect.notRange("<6 >= 5", "6.0.0", @src()); expect.notRange("<6 >= 5", "6.0.1", @src()); expect.range(">2", "3", @src()); expect.notRange(">2", "2.1", @src()); expect.notRange(">2", "2", @src()); expect.notRange(">2", "1.0", @src()); expect.notRange(">1.3", "1.3.1", @src()); expect.range(">1.3", "2.0.0", @src()); expect.range(">2.1.0", "2.2.0", @src()); expect.range("<=2.2.99999", "2.2.0", @src()); expect.range(">=2.1.99999", "2.2.0", @src()); expect.range("<2.2.99999", "2.2.0", @src()); expect.range(">2.1.99999", "2.2.0", @src()); expect.range(">1.0.0", "2.0.0", @src()); expect.range("1.0.0", "1.0.0", @src()); expect.notRange("1.0.0", "2.0.0", @src()); expect.range("1.0.0 || 2.0.0", "1.0.0", @src()); expect.range("2.0.0 || 1.0.0", "1.0.0", @src()); expect.range("1.0.0 || 2.0.0", "2.0.0", @src()); expect.range("2.0.0 || 1.0.0", "2.0.0", @src()); expect.range("2.0.0 || >1.0.0", "2.0.0", @src()); expect.range(">1.0.0 <2.0.0 <2.0.1 >1.0.1", "1.0.2", @src()); expect.range("2.x", "2.0.0", @src()); expect.range("2.x", "2.1.0", @src()); expect.range("2.x", "2.2.0", @src()); expect.range("2.x", "2.3.0", @src()); expect.range("2.x", "2.1.1", @src()); expect.range("2.x", "2.2.2", @src()); expect.range("2.x", "2.3.3", @src()); expect.range("<2.0.1 >1.0.0", "2.0.0", @src()); expect.range("<=2.0.1 >=1.0.0", "2.0.0", @src()); expect.range("^2", "2.0.0", @src()); expect.range("^2", "2.9.9", @src()); expect.range("~2", "2.0.0", @src()); expect.range("~2", "2.1.0", @src()); expect.range("~2.2", "2.2.1", @src()); { const passing = [_]string{ "2.4.0", "2.4.1", "3.0.0", "3.0.1", "3.1.0", "3.2.0", "3.3.0", "3.3.1", "3.4.0", "3.5.0", "3.6.0", "3.7.0", "2.4.2", "3.8.0", "3.9.0", "3.9.1", "3.9.2", "3.9.3", "3.10.0", "3.10.1", "4.0.0", "4.0.1", "4.1.0", "4.2.0", "4.2.1", "4.3.0", "4.4.0", "4.5.0", "4.5.1", "4.6.0", "4.6.1", "4.7.0", "4.8.0", "4.8.1", "4.8.2", "4.9.0", "4.10.0", "4.11.0", "4.11.1", "4.11.2", "4.12.0", "4.13.0", "4.13.1", "4.14.0", "4.14.1", "4.14.2", "4.15.0", "4.16.0", "4.16.1", "4.16.2", "4.16.3", "4.16.4", "4.16.5", "4.16.6", "4.17.0", "4.17.1", "4.17.2", "4.17.3", "4.17.4", "4.17.5", "4.17.9", "4.17.10", "4.17.11", "2.0.0", "2.1.0" }; for (passing) |item| { expect.range("^2 <2.2 || > 2.3", item, @src()); expect.range("> 2.3 || ^2 <2.2", item, @src()); } const not_passing = [_]string{ "0.1.0", "0.10.0", "0.2.0", "0.2.1", "0.2.2", "0.3.0", "0.3.1", "0.3.2", "0.4.0", "0.4.1", "0.4.2", "0.5.0", // "0.5.0-rc.1", "0.5.1", "0.5.2", "0.6.0", "0.6.1", "0.7.0", "0.8.0", "0.8.1", "0.8.2", "0.9.0", "0.9.1", "0.9.2", "1.0.0", "1.0.1", "1.0.2", "1.1.0", "1.1.1", "1.2.0", "1.2.1", "1.3.0", "1.3.1", "2.2.0", "2.2.1", "2.3.0", // "1.0.0-rc.1", // "1.0.0-rc.2", // "1.0.0-rc.3", }; for (not_passing) |item| { expect.notRange("^2 <2.2 || > 2.3", item, @src()); expect.notRange("> 2.3 || ^2 <2.2", item, @src()); } } expect.range("2.1.0 || > 2.2 || >3", "2.1.0", @src()); expect.range(" > 2.2 || >3 || 2.1.0", "2.1.0", @src()); expect.range(" > 2.2 || 2.1.0 || >3", "2.1.0", @src()); expect.range("> 2.2 || 2.1.0 || >3", "2.3.0", @src()); expect.notRange("> 2.2 || 2.1.0 || >3", "2.2.1", @src()); expect.notRange("> 2.2 || 2.1.0 || >3", "2.2.0", @src()); expect.range("> 2.2 || 2.1.0 || >3", "2.3.0", @src()); expect.range("> 2.2 || 2.1.0 || >3", "3.0.1", @src()); expect.range("~2", "2.0.0", @src()); expect.range("~2", "2.1.0", @src()); expect.range("1.2.0 - 1.3.0", "1.2.2", @src()); expect.range("1.2 - 1.3", "1.2.2", @src()); expect.range("1 - 1.3", "1.2.2", @src()); expect.range("1 - 1.3", "1.3.0", @src()); expect.range("1.2 - 1.3", "1.3.1", @src()); expect.notRange("1.2 - 1.3", "1.4.0", @src()); expect.range("1 - 1.3", "1.3.1", @src()); expect.notRange("1.2 - 1.3 || 5.0", "6.4.0", @src()); expect.range("1.2 - 1.3 || 5.0", "1.2.1", @src()); expect.range("5.0 || 1.2 - 1.3", "1.2.1", @src()); expect.range("1.2 - 1.3 || 5.0", "5.0", @src()); expect.range("5.0 || 1.2 - 1.3", "5.0", @src()); expect.range("1.2 - 1.3 || 5.0", "5.0.2", @src()); expect.range("5.0 || 1.2 - 1.3", "5.0.2", @src()); expect.range("1.2 - 1.3 || 5.0", "5.0.2", @src()); expect.range("5.0 || 1.2 - 1.3", "5.0.2", @src()); expect.range("5.0 || 1.2 - 1.3 || >8", "9.0.2", @src()); }