river/common/globber.zig

232 lines
7.6 KiB
Zig

// Basic prefix, suffix, and substring glob matching.
//
// Released under the Zero Clause BSD (0BSD) license:
//
// Copyright 2023 Isaac Freund
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
const std = @import("std");
const mem = std.mem;
/// Validate a glob, returning error.InvalidGlob if is "**" or has a '*'
/// at any position other than the first and/or last byte.
pub fn validate(glob: []const u8) error{InvalidGlob}!void {
switch (glob.len) {
0, 1 => {},
2 => if (glob[0] == '*' and glob[1] == '*') return error.InvalidGlob,
else => if (mem.indexOfScalar(u8, glob[1 .. glob.len - 1], '*') != null) {
return error.InvalidGlob;
},
}
}
test validate {
const testing = std.testing;
try validate("");
try validate("*");
try validate("a");
try validate("*a");
try validate("a*");
try validate("*a*");
try validate("ab");
try validate("*ab");
try validate("ab*");
try validate("*ab*");
try validate("abc");
try validate("*abc");
try validate("abc*");
try validate("*abc*");
try testing.expectError(error.InvalidGlob, validate("**"));
try testing.expectError(error.InvalidGlob, validate("***"));
try testing.expectError(error.InvalidGlob, validate("a*c"));
try testing.expectError(error.InvalidGlob, validate("ab*c*"));
try testing.expectError(error.InvalidGlob, validate("*ab*c"));
try testing.expectError(error.InvalidGlob, validate("ab*c"));
try testing.expectError(error.InvalidGlob, validate("a*bc*"));
try testing.expectError(error.InvalidGlob, validate("**a"));
try testing.expectError(error.InvalidGlob, validate("abc**"));
}
/// Return true if s is matched by glob.
/// Asserts that the glob is valid, see `validate()`.
pub fn match(s: []const u8, glob: []const u8) bool {
if (std.debug.runtime_safety) {
validate(glob) catch unreachable;
}
if (glob.len == 0) {
return s.len == 0;
} else if (glob.len == 1) {
return glob[0] == '*' or mem.eql(u8, s, glob);
}
const suffix_match = glob[0] == '*';
const prefix_match = glob[glob.len - 1] == '*';
if (suffix_match and prefix_match) {
return mem.indexOf(u8, s, glob[1 .. glob.len - 1]) != null;
} else if (suffix_match) {
return mem.endsWith(u8, s, glob[1..]);
} else if (prefix_match) {
return mem.startsWith(u8, s, glob[0 .. glob.len - 1]);
} else {
return mem.eql(u8, s, glob);
}
}
test match {
const testing = std.testing;
try testing.expect(match("", "*"));
try testing.expect(match("", ""));
try testing.expect(!match("a", ""));
try testing.expect(!match("", "a"));
try testing.expect(match("a", "*"));
try testing.expect(match("a", "*a*"));
try testing.expect(match("a", "a*"));
try testing.expect(match("a", "*a"));
try testing.expect(match("a", "a"));
try testing.expect(!match("a", "b"));
try testing.expect(!match("a", "*b*"));
try testing.expect(!match("a", "b*"));
try testing.expect(!match("a", "*b"));
try testing.expect(match("ab", "*"));
try testing.expect(match("ab", "*a*"));
try testing.expect(match("ab", "*b*"));
try testing.expect(match("ab", "a*"));
try testing.expect(match("ab", "*b"));
try testing.expect(match("ab", "*ab*"));
try testing.expect(match("ab", "ab*"));
try testing.expect(match("ab", "*ab"));
try testing.expect(match("ab", "ab"));
try testing.expect(!match("ab", "b*"));
try testing.expect(!match("ab", "*a"));
try testing.expect(!match("ab", "*c*"));
try testing.expect(!match("ab", "c*"));
try testing.expect(!match("ab", "*c"));
try testing.expect(!match("ab", "ac"));
try testing.expect(!match("ab", "*ac*"));
try testing.expect(!match("ab", "ac*"));
try testing.expect(!match("ab", "*ac"));
try testing.expect(match("abc", "*"));
try testing.expect(match("abc", "*a*"));
try testing.expect(match("abc", "*b*"));
try testing.expect(match("abc", "*c*"));
try testing.expect(match("abc", "a*"));
try testing.expect(match("abc", "*c"));
try testing.expect(match("abc", "*ab*"));
try testing.expect(match("abc", "ab*"));
try testing.expect(match("abc", "*bc*"));
try testing.expect(match("abc", "*bc"));
try testing.expect(match("abc", "*abc*"));
try testing.expect(match("abc", "abc*"));
try testing.expect(match("abc", "*abc"));
try testing.expect(match("abc", "abc"));
try testing.expect(!match("abc", "*a"));
try testing.expect(!match("abc", "*b"));
try testing.expect(!match("abc", "b*"));
try testing.expect(!match("abc", "c*"));
try testing.expect(!match("abc", "*ab"));
try testing.expect(!match("abc", "bc*"));
try testing.expect(!match("abc", "*d*"));
try testing.expect(!match("abc", "d*"));
try testing.expect(!match("abc", "*d"));
}
/// Returns .lt if a is less general than b.
/// Returns .gt if a is more general than b.
/// Returns .eq if a and b are equally general.
/// Both a and b must be valid globs, see `validate()`.
pub fn order(a: []const u8, b: []const u8) std.math.Order {
if (std.debug.runtime_safety) {
validate(a) catch unreachable;
validate(b) catch unreachable;
}
if (mem.eql(u8, a, "*") and mem.eql(u8, b, "*")) {
return .eq;
} else if (mem.eql(u8, a, "*")) {
return .gt;
} else if (mem.eql(u8, b, "*")) {
return .lt;
}
const count_a = if (a.len != 0) @as(u2, @intFromBool(a[0] == '*')) +
@intFromBool(a[a.len - 1] == '*') else 0;
const count_b = if (b.len != 0) @as(u2, @intFromBool(b[0] == '*')) +
@intFromBool(b[b.len - 1] == '*') else 0;
if (count_a == 0 and count_b == 0) {
return .eq;
} else if (count_a == count_b) {
// This may look backwards since e.g. "c*" is more general than "cc*"
return std.math.order(b.len, a.len);
} else {
return std.math.order(count_a, count_b);
}
}
test order {
const testing = std.testing;
const Order = std.math.Order;
try testing.expectEqual(Order.eq, order("", ""));
try testing.expectEqual(Order.eq, order("*", "*"));
try testing.expectEqual(Order.eq, order("*a*", "*b*"));
try testing.expectEqual(Order.eq, order("a*", "*b"));
try testing.expectEqual(Order.eq, order("*a", "*b"));
try testing.expectEqual(Order.eq, order("*a", "b*"));
try testing.expectEqual(Order.eq, order("a*", "b*"));
const descending = [_][]const u8{
"*",
"*a*",
"*b*",
"*a*",
"*ab*",
"*bab*",
"*a",
"b*",
"*b",
"*a",
"a",
"bababab",
"b",
"a",
"",
};
for (descending, 0..) |a, i| {
for (descending[i..]) |b| {
try testing.expect(order(a, b) != .lt);
}
}
var ascending = descending;
mem.reverse([]const u8, &ascending);
for (ascending, 0..) |a, i| {
for (ascending[i..]) |b| {
try testing.expect(order(a, b) != .gt);
}
}
}