mirror of
https://github.com/oven-sh/bun
synced 2026-02-12 20:09:04 +00:00
2198 lines
74 KiB
Zig
2198 lines
74 KiB
Zig
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 },
|
|
|
|
/// Create an inline string
|
|
pub fn from(comptime inlinable_buffer: []const u8) String {
|
|
comptime {
|
|
if (inlinable_buffer.len > max_inline_len or
|
|
inlinable_buffer.len == max_inline_len and
|
|
inlinable_buffer[max_inline_len - 1] >= 0x80)
|
|
{
|
|
@compileError("string constant too long to be inlined");
|
|
}
|
|
}
|
|
return String.init(inlinable_buffer, inlinable_buffer);
|
|
}
|
|
|
|
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: *const 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
|
|
// 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 = @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) {
|
|
// From a semver perspective, treat "--foo" the same as "-foo"
|
|
// example: foo/bar@1.2.3@--canary.24
|
|
// ^
|
|
if (token.tag == .none) {
|
|
is_or = false;
|
|
token.wildcard = .none;
|
|
prev_token.tag = .none;
|
|
continue;
|
|
}
|
|
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("<r><red>Fail<r> Expected range <b>\"{s}\"<r> to match <b>\"{s}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{
|
|
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("<r><red>Fail<r> Expected range <b>\"{s}\"<r> NOT match <b>\"{s}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{
|
|
input,
|
|
version_str,
|
|
src.file,
|
|
src.line,
|
|
src.column,
|
|
src.fn_name,
|
|
});
|
|
}
|
|
}
|
|
|
|
pub fn done(src: std.builtin.SourceLocation) void {
|
|
Output.prettyErrorln("<r><green>{d} passed expectations <d>in {s}<r>", .{ 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("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{d}.{d}.{d}\" but received <red>\"{d}.{d}.{d}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{
|
|
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("<r><red>Fail<r> Expected version <b>\"{s}\"<r> to match <b>\"{s}\" but received <red>\"{}\"<r>\nAt: <blue><b>{s}:{d}:{d}<r><d> in {s}<r>", .{
|
|
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());
|
|
}
|