diff options
| -rw-r--r-- | src/install/semver.zig | 427 |
1 files changed, 398 insertions, 29 deletions
diff --git a/src/install/semver.zig b/src/install/semver.zig index 14eb3097a..ba3659247 100644 --- a/src/install/semver.zig +++ b/src/install/semver.zig @@ -9,6 +9,23 @@ pub const Version = struct { extra_tags: []const Tag = &[_]Tag{}, raw: strings.StringOrTinyString = strings.StringOrTinyString{}, + pub fn format(self: Version, comptime layout: []const u8, opts: std.fmt.FormatOptions, writer: anytype) !void { + try std.fmt.format(writer, "{d}.{d}.{d}", .{ self.major, self.minor, self.patch }); + + const pre = self.tag.pre.slice(); + const build = self.tag.build.slice(); + + if (pre.len > 0) { + try writer.writeAll("-"); + try writer.writeAll(pre); + } + + if (build.len > 0) { + try writer.writeAll("+"); + try writer.writeAll(build); + } + } + inline fn atPart(i: u8) u32 { return switch (i) { 0 => self.major, @@ -18,31 +35,135 @@ pub const Version = struct { }; } + pub fn order(lhs: Version, rhs: Version) std.math.Order { + if (lhs.major < rhs.major) return .lt; + if (lhs.major > rhs.major) return .gt; + if (lhs.minor < rhs.minor) return .lt; + if (lhs.minor > rhs.minor) return .gt; + if (lhs.patch < rhs.patch) return .lt; + if (lhs.patch > rhs.patch) return .gt; + + return .eql; + } + pub const Tag = struct { pre: strings.StringOrTinyString = strings.StringOrTinyString{}, build: strings.StringOrTinyString = strings.StringOrTinyString{}, + pub inline fn hasPre(this: Tag) bool { + return this.pre.slice().len > 0; + } + + pub inline fn hasBuild(this: Tag) bool { + return this.build.slice().len > 0; + } + pub const TagResult = struct { tag: Tag = Tag{}, extra_tags: []const Tag = &[_]Tag{}, len: u32 = 0, }; - pub fn parse(allocator: *std.mem.Allocator, input: string) TagResult {} - }; + var multi_tag_warn = false; + // TODO: support multiple tags + pub fn parse(allocator: *std.mem.Allocator, input: string) TagResult { + var build_count: u32 = 0; + var pre_count: u32 = 0; + + for (input) |c| { + switch (c) { + ' ' => break, + '+' => { + build_count += 1; + }, + '-' => { + pre_count += 1; + }, + } + } - pub fn isGreaterThan(self: Version, other: Version) bool { - if (self.major > other.major) { - return true; - } + if (build_count == 0 and pre_count == 0) { + return TagResult{ + .len = 0, + }; + } - if (self.minor > other.minor) { - return true; - } + if (@maximum(build_count, pre_count) > 1 and !multi_tag_warn) { + Output.prettyErrorln("<r><orange>warn<r>: Multiple pre/build tags is not supported yet.", .{}); + multi_tag_warn = true; + } + + const State = enum { none, pre, build }; + var result = TagResult{}; + // Common case: no allocation is necessary. + var state = State.none; + var start: usize = 0; + + var tag_i: usize = 0; + var had_content = false; + + for (input) |c, i| { + switch (c) { + ' ' => { + switch (state) { + .none => {}, + .pre => { + result.tag.pre = strings.StringOrTinyString.init(input[start..i]); + if (comptime Environment.isDebug) { + std.debug.assert(!strings.containsChar(result.tag.pre.slice(), '-')); + } + state = State.none; + }, + .build => { + result.tag.build = strings.StringOrTinyString.init(input[start..i]); + if (comptime Environment.isDebug) { + std.debug.assert(!strings.containsChar(result.tag.build.slice(), '-')); + } + state = State.none; + }, + } + result.len = @truncate(u32, i); + break; + }, + '+' => { + // qualifier ::= ( '-' pre )? ( '+' build )? + if (state == .pre) { + result.tag.pre = strings.StringOrTinyString.init(input[start..i]); + if (comptime Environment.isDebug) { + std.debug.assert(!strings.containsChar(result.tag.pre.slice(), '-')); + } + } + + state = .build; + start = i + 1; + }, + '-' => { + state = .pre; + start = i + 1; + }, + } + } + + switch (state) { + .none => {}, + .pre => { + result.tag.pre = strings.StringOrTinyString.init(input[start..]); + if (comptime Environment.isDebug) { + std.debug.assert(!strings.containsChar(result.tag.pre.slice(), '-')); + } + result.len = @truncate(u32, input.len); + }, + .build => { + result.tag.build = strings.StringOrTinyString.init(input[start..]); + if (comptime Environment.isDebug) { + std.debug.assert(!strings.containsChar(result.tag.build.slice(), '-')); + } + result.len = @truncate(u32, input.len); + }, + } - if (self.patch > other.patch) { - return true; + return result; } - } + }; pub const ParseResult = struct { wildcard: Query.Token.Wildcard = Query.Token.Wildcard.none, @@ -197,7 +318,7 @@ pub const Version = struct { } result.stopped_at = @intCast(u32, @maximum(stopped_at, 0)); - + result.version.raw = strings.StringOrTinyString.init(input[0..result.stopped_at]); return result; } @@ -206,6 +327,8 @@ pub const Version = struct { var bytes: [10]u8 = undefined; var byte_i: u8 = 0; + std.debug.assert(input[0] != '.'); + for (input) |char, i| { switch (char) { 'X', 'x', '*' => return 0, @@ -215,6 +338,7 @@ pub const Version = struct { bytes[byte_i] = char; byte_i += 1; }, + ' ', '.' => break, // ignore invalid characters else => {}, } @@ -223,26 +347,144 @@ pub const Version = struct { // 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 { - eql, - lt, - lte, - gt, - gte, + pub const Op = enum(u8) { + unset = 0, + eql = 1, + lt = 3, + lte = 4, + gt = 5, + gte = 6, }; + left: Comparator = Comparator{}, + right: Comparator = Comparator{}, + + 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; + } + pub const Comparator = struct { - op: Op = Op.eql, + op: Op = Op.unset, version: Version = Version{}, + + pub fn satisfies(this: Comparator, version: Version) bool { + const order = this.version.order(version); + + return switch (order) { + .eql => switch (this.op) { + .lte, .gte, .eql => true, + else => false, + }, + .gt => switch (this.op) { + .gt => true, + else => false, + }, + .lt => switch (this.op) { + .lt => true, + else => false, + }, + }; + } }; - left: Comparator = Comparator{}, - right: ?Comparator = null, + pub fn satisfies(this: Range, version: Version) bool { + if (!this.hasLeft()) { + return false; + } + + if (!this.left.satisfies(version)) { + return false; + } + + if (this.hasRight() and !this.right.satisfies(version)) { + return false; + } + + return true; + } }; pub const Query = struct { @@ -261,20 +503,120 @@ pub const Query = struct { head: Query, tail: ?*Query = null, allocator: *std.mem.Allocator, - pub fn setVersion(self: *List, version: Version) void {} + pub fn orVersion(self: *List, version: Version) !void { + if (!self.head.range.hasLeft() and self.tail == null) { + std.debug.assert(!self.head.range.hasRight()); + self.head.range.left.version = version; + self.head.range.left.op = .eql; + return; + } + + var tail = try self.allocator.create(Query); + tail.* = Query{}; + tail.range.left.version = version; + tail.range.left.op = .eql; + + var last_tail = self.tail orelse &self.head; + std.debug.assert(last_tail.next_op == .none); + + last_tail.next_op = .OR; + last_tail.next = tail; + self.tail = tail; + } + + fn addRange(self: *List, range: Range, is_and: bool) !void { + if (!self.head.range.hasLeft() and !self.head.range.hasRight()) { + self.head.range = range; + return; + } - pub fn andRange(self: *List, range: Range) !void {} + var tail = try self.allocator.create(Query); + tail.range = range; - pub fn orRange(self: *List, range: Range) !void {} + var last_tail = self.tail orelse &self.head; + std.debug.assert(last_tail.next_op == .none); + last_tail.next = tail; + last_tail.next_op = if (is_and) .AND else .OR; + } + pub fn andRange(self: *List, range: Range) !void { + try self.addRange(range, true); + } + pub fn orRange(self: *List, range: Range) !void { + try self.addRange(range, false); + } }; + pub fn satisfies(this: *Query, version: Version) bool { + const left = this.range.satisfies(version); + return switch (this.next_op) { + .none => left, + .AND => left and this.next.satisfies(version), + .OR => left or this.next.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 range = Range{ .left = .{ .op = .gte, .version = Version{ .raw = version.raw } } }; + + if (version.patch > 0 or version.minor > 0) { + range.right = .{ + .op = .lt, + .version = Version{ .raw = version.raw, .major = version.major, .minor = version.minor + 1, .patch = 0 }, + }; + return range; + } + + range.right = .{ + .op = .lt, + .version = Version{ .raw = version.raw, .major = version.major + 1, .minor = 0, .patch = 0 }, + }; + + return range; + }, + .tilda => { + if (version.minor == 0 or this.wildcard == .minor or this.wildcard == .major) { + return Range.initWildcard(version, .minor); + } + + return Range.initWildcard(version, .patch); + }, + .none => unreachable, + else => {}, + } + + if (this.wildcard != Wildcard.none) { + return Range.initWildcard(version, this.wildcard); + } + + switch (this.tag) { + .version => { + return Range{ .left = .{ .op = .eql, .version = version } }; + }, + .gt => { + return Range{ .left = .{ .op = .gt, .version = version } }; + }, + .gte => { + return Range{ .left = .{ .op = .gte, .version = version } }; + }, + .lt => { + return Range{ .left = .{ .op = .lt, .version = version } }; + }, + .lte => { + return Range{ .left = .{ .op = .lte, .version = version } }; + }, + else => unreachable, + } + } + pub const Tag = enum { none, - logical_or, gt, gte, lt, @@ -292,9 +634,9 @@ pub const Query = struct { }; }; - pub fn parse(allocator: *std.mem.Allocator, input: string) !Query { + pub fn parse(allocator: *std.mem.Allocator, input: string) !List { var i: usize = 0; - var query = Query{}; + var list = List{ .allocator = allocator }; var token = Token{}; var prev_token = Token{}; @@ -303,6 +645,8 @@ pub const Query = struct { var skip_round = false; var is_or = false; + var last_non_whitespace: usize = 0; + while (i < input.len) { skip_round = false; @@ -382,10 +726,35 @@ pub const Query = struct { } if (!skip_round) { + const parse_result = Version.parse(input[i..]); + if (count == 0 and token.tag == .version) { prev_token.tag = token.tag; - const parse_result = Version.parse(input[i..]); + + token.wildcard = parse_result.wildcard; + + switch (parse_result.wildcard) { + .none => { + try list.orVersion(parse_result.version); + }, + else => { + try list.andRange(token.toRange(parse_result.version)); + }, + } + token.tag = Token.Tag.none; + token.wildcard = .none; + } else if (count == 0) { + prev_token.tag = token.tag; + token.wildcard = parse_result.wildcard; + 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)); } + + i += parse_result.stopped_at + 1; + is_or = false; } } |
