diff --git a/.gitignore b/.gitignore index 8afca8fd22..e7ce1a4349 100644 --- a/.gitignore +++ b/.gitignore @@ -136,6 +136,7 @@ make-dev-stats.csv .uuid tsconfig.tsbuildinfo +test/js/bun/glob/fixtures *.lib *.pdb CMakeFiles @@ -158,4 +159,4 @@ x64 /src/deps/libuv /build-*/ -.vs \ No newline at end of file +.vs diff --git a/bench/bun.lockb b/bench/bun.lockb index 298e2a7c95..4e96ca3264 100755 Binary files a/bench/bun.lockb and b/bench/bun.lockb differ diff --git a/bench/glob/match.mjs b/bench/glob/match.mjs new file mode 100644 index 0000000000..0d500af668 --- /dev/null +++ b/bench/glob/match.mjs @@ -0,0 +1,113 @@ +import { run, bench, group } from "mitata"; +import fg from "fast-glob"; +import { fdir } from "fdir"; + +const normalPattern = "*.ts"; +const recursivePattern = "**/*.ts"; +const nodeModulesPattern = "**/node_modules/**/*.js"; + +const benchFdir = false; +const cwd = undefined; + +const bunOpts = { + cwd, + followSymlinks: false, + absolute: true, +}; + +const fgOpts = { + cwd, + followSymbolicLinks: false, + onlyFiles: false, + absolute: true, +}; + +const Glob = "Bun" in globalThis ? globalThis.Bun.Glob : undefined; + +group({ name: `async pattern="${normalPattern}"`, summary: true }, () => { + bench("fast-glob", async () => { + const entries = await fg.glob([normalPattern], fgOpts); + }); + + if (Glob) + bench("Bun.Glob", async () => { + const entries = await Array.fromAsync(new Glob(normalPattern).scan(bunOpts)); + }); + + if (benchFdir) + bench("fdir", async () => { + const entries = await new fdir().withFullPaths().glob(normalPattern).crawl(process.cwd()).withPromise(); + }); +}); + +group({ name: `async-recursive pattern="${recursivePattern}"`, summary: true }, () => { + bench("fast-glob", async () => { + const entries = await fg.glob([recursivePattern], fgOpts); + }); + + if (Glob) + bench("Bun.Glob", async () => { + const entries = await Array.fromAsync(new Glob(recursivePattern).scan(bunOpts)); + }); + + if (benchFdir) + bench("fdir", async () => { + const entries = await new fdir().withFullPaths().glob(recursivePattern).crawl(process.cwd()).withPromise(); + }); +}); + +group({ name: `sync pattern="${normalPattern}"`, summary: true }, () => { + bench("fast-glob", () => { + const entries = fg.globSync([normalPattern], fgOpts); + }); + + if (Glob) + bench("Bun.Glob", () => { + const entries = [...new Glob(normalPattern).scanSync(bunOpts)]; + }); + + if (benchFdir) + bench("fdir", async () => { + const entries = new fdir().withFullPaths().glob(normalPattern).crawl(process.cwd()).sync(); + }); +}); + +group({ name: `sync-recursive pattern="${recursivePattern}"`, summary: true }, () => { + bench("fast-glob", () => { + const entries = fg.globSync([recursivePattern], fgOpts); + }); + + if (Glob) + bench("Bun.Glob", () => { + const entries = [...new Glob(recursivePattern).scanSync(bunOpts)]; + }); + + if (benchFdir) + bench("fdir", async () => { + const entries = new fdir().withFullPaths().glob(recursivePattern).crawl(process.cwd()).sync(); + }); +}); + +group({ name: `node_modules pattern="${nodeModulesPattern}"`, summary: true }, () => { + bench("fast-glob", async () => { + const entries = await fg.glob([nodeModulesPattern], fgOpts); + }); + + if (Glob) + bench("Bun.Glob", async () => { + const entries = await Array.fromAsync(new Glob(nodeModulesPattern).scan(bunOpts)); + }); + + if (benchFdir) + bench("fdir", async () => { + const entries = await new fdir().withFullPaths().glob(nodeModulesPattern).crawl(process.cwd()).withPromise(); + }); +}); + +await run({ + avg: true, + colors: false, + min_max: true, + collect: true, + percentiles: true, +}); diff --git a/bench/package.json b/bench/package.json index 501dd6f51b..43f7939c39 100644 --- a/bench/package.json +++ b/bench/package.json @@ -7,6 +7,8 @@ "benchmark": "^2.1.4", "esbuild": "^0.14.12", "eventemitter3": "^5.0.0", + "fast-glob": "3.3.1", + "fdir": "^6.1.0", "mitata": "^0.1.6" }, "scripts": { diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index d7e33961c9..02204df64f 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -70,6 +70,135 @@ declare module "bun" { options?: { PATH?: string; cwd?: string }, ): string | null; + export interface GlobScanOptions { + /** + * The root directory to start matching from. Defaults to `process.cwd()` + */ + cwd?: string; + + /** + * Allow patterns to match entries that begin with a period (`.`). + * + * @default false + */ + dot?: boolean; + + /** + * Return the absolute path for entries. + * + * @default false + */ + absolute?: boolean; + + /** + * Indicates whether to traverse descendants of symbolic link directories. + * + * @default false + */ + followSymlinks?: boolean; + + /** + * Throw an error when symbolic link is broken + * + * @default false + */ + throwErrorOnBrokenSymlink?: boolean; + + /** + * Return only files. + * + * @default true + */ + onlyFiles?: boolean; + } + + /** + * Match files using [glob patterns](https://en.wikipedia.org/wiki/Glob_(programming)). + * + * The supported pattern syntax for is: + * + * - `?` + * Matches any single character. + * - `*` + * Matches zero or more characters, except for path separators ('/' or '\'). + * - `**` + * Matches zero or more characters, including path separators. + * Must match a complete path segment, i.e. followed by a path separator or + * at the end of the pattern. + * - `[ab]` + * Matches one of the characters contained in the brackets. + * Character ranges (e.g. "[a-z]") are also supported. + * Use "[!ab]" or "[^ab]" to match any character *except* those contained + * in the brackets. + * - `{a,b}` + * Match one of the patterns contained in the braces. + * Any of the wildcards listed above can be used in the sub patterns. + * Braces may be nested up to 10 levels deep. + * - `!` + * Negates the result when at the start of the pattern. + * Multiple "!" characters negate the pattern multiple times. + * - `\` + * Used to escape any of the special characters above. + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * const scannedFiles = await Array.fromAsync(glob.scan({ cwd: './src' })) + * ``` + */ + export class Glob { + constructor(pattern: string); + + /** + * Scan for files that match this glob pattern. Returns an async iterator. + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * const scannedFiles = await Array.fromAsync(glob.scan({ cwd: './src' })) + * ``` + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * for await (const path of glob.scan()) { + * // do something + * } + * ``` + */ + scan(options?: GlobScanOptions): AsyncIterableIterator; + + /** + * Scan for files that match this glob pattern. Returns an iterator. + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * const scannedFiles = Array.from(glob.scan({ cwd: './src' })) + * ``` + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * for (const path of glob.scan()) { + * // do something + * } + * ``` + */ + scanSync(options?: GlobScanOptions): IterableIterator; + + /** + * Match the glob against a string + * + * @example + * ```js + * const glob = new Glob("*.{ts,tsx}"); + * expect(glob.match('foo.ts')).toBeTrue(); + * ``` + */ + match(str: string): boolean; + } + interface TOML { /** * Parse a TOML string into a JavaScript object. diff --git a/src/bun.js/api/Glob.classes.ts b/src/bun.js/api/Glob.classes.ts new file mode 100644 index 0000000000..d4aa9c681c --- /dev/null +++ b/src/bun.js/api/Glob.classes.ts @@ -0,0 +1,39 @@ +import { define } from "../../codegen/class-definitions"; + +export default [ + define({ + name: "Glob", + construct: true, + finalize: true, + hasPendingActivity: true, + configurable: false, + klass: {}, + JSType: "0b11101110", + proto: { + scan: { + builtin: "globScanCodeGenerator", + length: 1, + }, + scanSync: { + builtin: "globScanSyncCodeGenerator", + length: 1, + }, + __scan: { + fn: "__scan", + length: 1, + // Wanted to use `resolve` and `resolveSync` but for some reason the + // resolve symbol was not working, even though `resolveSync` was. + privateSymbol: "pull", + }, + __scanSync: { + fn: "__scanSync", + length: 1, + privateSymbol: "resolveSync", + }, + match: { + fn: "match", + length: 1, + }, + }, + }), +]; diff --git a/src/bun.js/api/bun.zig b/src/bun.js/api/bun.zig index b51ed1c004..0f22049fdc 100644 --- a/src/bun.js/api/bun.zig +++ b/src/bun.js/api/bun.zig @@ -56,6 +56,7 @@ pub const BunObject = struct { pub const SHA512 = Crypto.SHA512.getter; pub const SHA512_256 = Crypto.SHA512_256.getter; pub const TOML = Bun.getTOMLObject; + pub const Glob = Bun.getGlobConstructor; pub const Transpiler = Bun.getTranspilerConstructor; pub const argv = Bun.getArgv; pub const assetPrefix = Bun.getAssetPrefix; @@ -102,6 +103,7 @@ pub const BunObject = struct { @export(BunObject.SHA512, .{ .name = getterName("SHA512") }); @export(BunObject.SHA512_256, .{ .name = getterName("SHA512_256") }); @export(BunObject.TOML, .{ .name = getterName("TOML") }); + @export(BunObject.Glob, .{ .name = getterName("Glob") }); @export(BunObject.Transpiler, .{ .name = getterName("Transpiler") }); @export(BunObject.argv, .{ .name = getterName("argv") }); @export(BunObject.assetPrefix, .{ .name = getterName("assetPrefix") }); @@ -235,6 +237,7 @@ const Which = @import("../../which.zig"); const ErrorableString = JSC.ErrorableString; const is_bindgen = JSC.is_bindgen; const max_addressible_memory = std.math.maxInt(u56); +const glob = @import("../../glob.zig"); const Async = bun.Async; const SemverObject = @import("../../install/semver.zig").SemverObject; @@ -2967,6 +2970,13 @@ pub fn getTOMLObject( return TOMLObject.create(globalThis); } +pub fn getGlobConstructor( + globalThis: *JSC.JSGlobalObject, + _: *JSC.JSObject, +) callconv(.C) JSC.JSValue { + return JSC.API.Glob.getConstructor(globalThis); +} + pub fn getSemver( globalThis: *JSC.JSGlobalObject, _: *JSC.JSObject, diff --git a/src/bun.js/api/glob.zig b/src/bun.js/api/glob.zig new file mode 100644 index 0000000000..2e2f61b10e --- /dev/null +++ b/src/bun.js/api/glob.zig @@ -0,0 +1,514 @@ +const Glob = @This(); +const globImpl = @import("../../glob.zig"); +const globImplAscii = @import("../../glob_ascii.zig"); +const GlobWalker = globImpl.BunGlobWalker; +const PathLike = @import("../node/types.zig").PathLike; +const ArgumentsSlice = @import("../node/types.zig").ArgumentsSlice; +const Syscall = @import("../../sys.zig"); +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const bun = @import("../../bun.zig"); +const BunString = @import("../../bun.zig").String; +const string = bun.string; +const JSC = bun.JSC; +const JSArray = @import("../bindings/bindings.zig").JSArray; +const JSValue = @import("../bindings/bindings.zig").JSValue; +const ZigString = @import("../bindings/bindings.zig").ZigString; +const Base = @import("../base.zig"); +const JSGlobalObject = @import("../bindings/bindings.zig").JSGlobalObject; +const getAllocator = Base.getAllocator; +const ResolvePath = @import("../../resolver/resolve_path.zig"); +const isAllAscii = @import("../../string_immutable.zig").isAllASCII; +const CodepointIterator = @import("../../string_immutable.zig").UnsignedCodepointIterator; + +const Arena = std.heap.ArenaAllocator; + +pub usingnamespace JSC.Codegen.JSGlob; + +pattern: []const u8, +pattern_codepoints: ?std.ArrayList(u32) = null, +is_ascii: bool, +has_pending_activity: std.atomic.Atomic(usize) = std.atomic.Atomic(usize).init(0), + +const ScanOpts = struct { + cwd: ?[]const u8, + dot: bool, + absolute: bool, + only_files: bool, + follow_symlinks: bool, + error_on_broken_symlinks: bool, + + fn fromJS(globalThis: *JSGlobalObject, arguments: *ArgumentsSlice, comptime fnName: []const u8, arena: *Arena) ?ScanOpts { + const optsObj: JSValue = arguments.nextEat() orelse return null; + var out: ScanOpts = .{ + .cwd = null, + .dot = false, + .absolute = false, + .follow_symlinks = false, + .error_on_broken_symlinks = false, + .only_files = true, + }; + if (optsObj.isUndefinedOrNull()) return out; + if (!optsObj.isObject()) { + globalThis.throw("{s}: expected first argument to be an object", .{fnName}); + return null; + } + + if (optsObj.getTruthy(globalThis, "onlyFiles")) |only_files| { + out.only_files = if (only_files.isBoolean()) only_files.asBoolean() else false; + } + + if (optsObj.getTruthy(globalThis, "throwErrorOnBrokenSymlink")) |error_on_broken| { + out.error_on_broken_symlinks = if (error_on_broken.isBoolean()) error_on_broken.asBoolean() else false; + } + + if (optsObj.getTruthy(globalThis, "followSymlinks")) |followSymlinksVal| { + out.follow_symlinks = if (followSymlinksVal.isBoolean()) followSymlinksVal.asBoolean() else false; + } + + if (optsObj.getTruthy(globalThis, "absolute")) |absoluteVal| { + out.absolute = if (absoluteVal.isBoolean()) absoluteVal.asBoolean() else false; + } + + if (optsObj.getTruthy(globalThis, "cwd")) |cwdVal| parse_cwd: { + if (!cwdVal.isString()) { + globalThis.throw("{s}: invalid `cwd`, not a string", .{fnName}); + return null; + } + + const cwd_str_raw = cwd_str_raw: { + // Windows wants utf-16 + if (comptime bun.Environment.isWindows) { + const cwd_zig_str = cwdVal.getZigString(globalThis); + // Dupe if already utf-16 + if (cwd_zig_str.is16Bit()) { + var duped = arena.allocator().dupe(u8, cwd_zig_str.slice()) catch { + globalThis.throwOutOfMemory(); + return null; + }; + + break :cwd_str_raw ZigString.Slice.from(duped, arena.allocator()); + } + + // Conver to utf-16 + const utf16 = (bun.strings.toUTF16Alloc( + arena.allocator(), + cwd_zig_str.slice(), + // Let windows APIs handle errors with invalid surrogate pairs, etc. + false, + ) catch { + globalThis.throwOutOfMemory(); + return null; + }) orelse brk: { + // All ascii + var output = arena.allocator().alloc(u16, cwd_zig_str.len) catch { + globalThis.throwOutOfMemory(); + return null; + }; + + bun.strings.copyU8IntoU16(output, cwd_zig_str.slice()); + break :brk output; + }; + + const ptr: [*]u8 = @ptrCast(utf16.ptr); + break :cwd_str_raw ZigString.Slice.from(ptr[0 .. utf16.len * 2], arena.allocator()); + } + + // `.toSlice()` internally converts to WTF-8 + break :cwd_str_raw cwdVal.toSlice(globalThis, arena.allocator()); + }; + + if (cwd_str_raw.len == 0) break :parse_cwd; + + const cwd_str = cwd_str: { + // If its absolute return as is + if (ResolvePath.Platform.auto.isAbsolute(cwd_str_raw.slice())) { + const cwd_str = cwd_str_raw.clone(arena.allocator()) catch { + globalThis.throwOutOfMemory(); + return null; + }; + break :cwd_str cwd_str.ptr[0..cwd_str.len]; + } + + var path_buf2: [bun.MAX_PATH_BYTES * 2]u8 = undefined; + + if (!out.absolute) { + const cwd_str = ResolvePath.joinStringBuf(&path_buf2, &[_][]const u8{cwd_str_raw.slice()}, .auto); + break :cwd_str arena.allocator().dupe(u8, cwd_str) catch { + globalThis.throwOutOfMemory(); + return null; + }; + } + + // Convert to an absolute path + + var path_buf: [bun.MAX_PATH_BYTES]u8 = undefined; + const cwd = switch (bun.sys.getcwd((&path_buf))) { + .result => |cwd| cwd, + .err => |err| { + const errJs = err.toJSC(globalThis); + globalThis.throwValue(errJs); + return null; + }, + }; + + const cwd_str = ResolvePath.joinStringBuf(&path_buf2, &[_][]const u8{ + cwd, + cwd_str_raw.slice(), + }, .auto); + + break :cwd_str arena.allocator().dupe(u8, cwd_str) catch { + globalThis.throwOutOfMemory(); + return null; + }; + }; + + if (cwd_str.len > bun.MAX_PATH_BYTES) { + globalThis.throw("{s}: invalid `cwd`, longer than {d} bytes", .{ fnName, bun.MAX_PATH_BYTES }); + return null; + } + + out.cwd = cwd_str; + } + + if (optsObj.getTruthy(globalThis, "dot")) |dot| { + out.dot = if (dot.isBoolean()) dot.asBoolean() else false; + } + + return out; + } +}; + +pub const WalkTask = struct { + walker: *GlobWalker, + alloc: Allocator, + err: ?Err = null, + global: *JSC.JSGlobalObject, + has_pending_activity: *std.atomic.Atomic(usize), + + pub const Err = union(enum) { + syscall: Syscall.Error, + unknown: anyerror, + + pub fn toJSC(this: Err, globalThis: *JSGlobalObject) JSValue { + return switch (this) { + .syscall => |err| err.toJSC(globalThis), + .unknown => |err| ZigString.fromBytes(@errorName(err)).toValueGC(globalThis), + }; + } + }; + + pub const AsyncGlobWalkTask = JSC.ConcurrentPromiseTask(WalkTask); + + pub fn create( + globalThis: *JSC.JSGlobalObject, + alloc: Allocator, + globWalker: *GlobWalker, + has_pending_activity: *std.atomic.Atomic(usize), + ) !*AsyncGlobWalkTask { + var walkTask = try alloc.create(WalkTask); + walkTask.* = .{ + .walker = globWalker, + .global = globalThis, + .alloc = alloc, + .has_pending_activity = has_pending_activity, + }; + return try AsyncGlobWalkTask.createOnJSThread(alloc, globalThis, walkTask); + } + + pub fn run(this: *WalkTask) void { + defer decrPendingActivityFlag(this.has_pending_activity); + const result = this.walker.walk() catch |err| { + this.err = .{ .unknown = err }; + return; + }; + switch (result) { + .err => |err| { + this.err = .{ .syscall = err }; + }, + .result => {}, + } + } + + pub fn then(this: *WalkTask, promise: *JSC.JSPromise) void { + defer this.deinit(); + + if (this.err) |err| { + const errJs = err.toJSC(this.global); + promise.reject(this.global, errJs); + return; + } + + const jsStrings = globWalkResultToJS(this.walker, this.global); + promise.resolve(this.global, jsStrings); + } + + fn deinit(this: *WalkTask) void { + this.walker.deinit(true); + this.alloc.destroy(this); + } +}; + +fn globWalkResultToJS(globWalk: *GlobWalker, globalThis: *JSGlobalObject) JSValue { + // if (globWalk.matchedPaths.items.len >= 0) { + if (globWalk.matchedPaths.items.len == 0) { + return JSC.JSArray.from(globalThis, &[_]JSC.JSValue{}); + } + + return BunString.toJSArray(globalThis, globWalk.matchedPaths.items[0..]); +} + +/// The reference to the arena is not used after the scope because it is copied +/// by `GlobWalker.init`/`GlobWalker.initWithCwd` if all allocations work and no +/// errors occur +fn makeGlobWalker( + this: *Glob, + globalThis: *JSGlobalObject, + arguments: *ArgumentsSlice, + comptime fnName: []const u8, + alloc: Allocator, + arena: *Arena, +) ?*GlobWalker { + const matchOpts = ScanOpts.fromJS(globalThis, arguments, fnName, arena) orelse return null; + var cwd = matchOpts.cwd; + var dot = matchOpts.dot; + var absolute = matchOpts.absolute; + var follow_symlinks = matchOpts.follow_symlinks; + var error_on_broken_symlinks = matchOpts.error_on_broken_symlinks; + var only_files = matchOpts.only_files; + + if (cwd != null) { + var globWalker = alloc.create(GlobWalker) catch { + globalThis.throw("Out of memory", .{}); + return null; + }; + + globWalker.* = .{}; + + switch (globWalker.initWithCwd( + arena, + this.pattern, + cwd.?, + dot, + absolute, + follow_symlinks, + error_on_broken_symlinks, + only_files, + ) catch { + globalThis.throw("Out of memory", .{}); + return null; + }) { + .err => |err| { + globalThis.throwValue(err.toJSC(globalThis)); + return null; + }, + else => {}, + } + return globWalker; + } + var globWalker = alloc.create(GlobWalker) catch { + globalThis.throw("Out of memory", .{}); + return null; + }; + + globWalker.* = .{}; + switch (globWalker.init( + arena, + this.pattern, + dot, + absolute, + follow_symlinks, + error_on_broken_symlinks, + only_files, + ) catch { + globalThis.throw("Out of memory", .{}); + return null; + }) { + .err => |err| { + globalThis.throwValue(err.toJSC(globalThis)); + return null; + }, + else => {}, + } + + return globWalker; +} + +pub fn constructor( + globalThis: *JSC.JSGlobalObject, + callframe: *JSC.CallFrame, +) callconv(.C) ?*Glob { + const alloc = getAllocator(globalThis); + + const arguments_ = callframe.arguments(1); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + defer arguments.deinit(); + const pat_arg = arguments.nextEat() orelse { + globalThis.throw("Glob.constructor: expected 1 arguments, got 0", .{}); + return null; + }; + + if (!pat_arg.isString()) { + globalThis.throw("Glob.constructor: first argument is not a string", .{}); + return null; + } + + var pat_str: []u8 = pat_arg.toBunString(globalThis).toOwnedSlice(bun.default_allocator) catch @panic("OOM"); + + const all_ascii = isAllAscii(pat_str); + + var glob = alloc.create(Glob) catch @panic("OOM"); + glob.* = .{ .pattern = pat_str, .is_ascii = all_ascii }; + + if (!all_ascii) { + var codepoints = std.ArrayList(u32).initCapacity(alloc, glob.pattern.len * 2) catch { + globalThis.throwOutOfMemory(); + return null; + }; + errdefer codepoints.deinit(); + + convertUtf8(&codepoints, glob.pattern) catch { + globalThis.throwOutOfMemory(); + return null; + }; + + glob.pattern_codepoints = codepoints; + } + + return glob; +} + +pub fn finalize( + this: *Glob, +) callconv(.C) void { + const alloc = JSC.VirtualMachine.get().allocator; + alloc.free(this.pattern); + if (this.pattern_codepoints) |*codepoints| { + codepoints.deinit(); + } + alloc.destroy(this); +} + +pub fn hasPendingActivity(this: *Glob) callconv(.C) bool { + @fence(.SeqCst); + return this.has_pending_activity.load(.SeqCst) > 0; +} + +fn incrPendingActivityFlag(has_pending_activity: *std.atomic.Atomic(usize)) void { + @fence(.SeqCst); + _ = has_pending_activity.fetchAdd(1, .SeqCst); +} + +fn decrPendingActivityFlag(has_pending_activity: *std.atomic.Atomic(usize)) void { + @fence(.SeqCst); + _ = has_pending_activity.fetchSub(1, .SeqCst); +} + +pub fn __scan(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const alloc = getAllocator(globalThis); + + const arguments_ = callframe.arguments(1); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + defer arguments.deinit(); + + var arena = std.heap.ArenaAllocator.init(alloc); + var globWalker = this.makeGlobWalker(globalThis, &arguments, "scan", alloc, &arena) orelse { + arena.deinit(); + return .undefined; + }; + + incrPendingActivityFlag(&this.has_pending_activity); + var task = WalkTask.create(globalThis, alloc, globWalker, &this.has_pending_activity) catch { + decrPendingActivityFlag(&this.has_pending_activity); + globalThis.throw("Out of memory", .{}); + return .undefined; + }; + task.schedule(); + + return task.promise.value(); +} + +pub fn __scanSync(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const alloc = getAllocator(globalThis); + + const arguments_ = callframe.arguments(1); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + defer arguments.deinit(); + + var arena = std.heap.ArenaAllocator.init(alloc); + var globWalker = this.makeGlobWalker(globalThis, &arguments, "scanSync", alloc, &arena) orelse { + arena.deinit(); + return .undefined; + }; + defer globWalker.deinit(true); + + switch (globWalker.walk() catch { + globalThis.throw("Out of memory", .{}); + return .undefined; + }) { + .err => |err| { + globalThis.throwValue(err.toJSC(globalThis)); + return JSValue.undefined; + }, + .result => {}, + } + + const matchedPaths = globWalkResultToJS(globWalker, globalThis); + + return matchedPaths; +} + +pub fn match(this: *Glob, globalThis: *JSGlobalObject, callframe: *JSC.CallFrame) callconv(.C) JSC.JSValue { + const alloc = getAllocator(globalThis); + var arena = Arena.init(alloc); + defer arena.deinit(); + + const arguments_ = callframe.arguments(1); + var arguments = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments_.slice()); + defer arguments.deinit(); + const str_arg = arguments.nextEat() orelse { + globalThis.throw("Glob.matchString: expected 1 arguments, got 0", .{}); + return JSC.JSValue.jsUndefined(); + }; + + if (!str_arg.isString()) { + globalThis.throw("Glob.matchString: first argument is not a string", .{}); + return JSC.JSValue.jsUndefined(); + } + + var str = str_arg.toSlice(globalThis, arena.allocator()); + defer str.deinit(); + + if (this.is_ascii and isAllAscii(str.slice())) return JSC.JSValue.jsBoolean(globImplAscii.match(this.pattern, str.slice())); + + const codepoints = codepoints: { + if (this.pattern_codepoints) |cp| break :codepoints cp.items[0..]; + + var codepoints = std.ArrayList(u32).initCapacity(alloc, this.pattern.len * 2) catch { + globalThis.throwOutOfMemory(); + return .undefined; + }; + errdefer codepoints.deinit(); + + convertUtf8(&codepoints, this.pattern) catch { + globalThis.throwOutOfMemory(); + return .undefined; + }; + + this.pattern_codepoints = codepoints; + + break :codepoints codepoints.items[0..codepoints.items.len]; + }; + + return JSC.JSValue.jsBoolean(globImpl.matchImpl(codepoints, str.slice())); +} + +pub fn convertUtf8(codepoints: *std.ArrayList(u32), pattern: []const u8) !void { + const iter = CodepointIterator.init(pattern); + var cursor = CodepointIterator.Cursor{}; + var i: u32 = 0; + while (iter.next(&cursor)) : (i += 1) { + try codepoints.append(@intCast(cursor.c)); + } +} diff --git a/src/bun.js/bindings/BunObject+exports.h b/src/bun.js/bindings/BunObject+exports.h index a05744d9e7..2a8c19981b 100644 --- a/src/bun.js/bindings/BunObject+exports.h +++ b/src/bun.js/bindings/BunObject+exports.h @@ -5,6 +5,7 @@ macro(CryptoHasher) \ macro(FFI) \ macro(FileSystemRouter) \ + macro(Glob) \ macro(MD4) \ macro(MD5) \ macro(SHA1) \ @@ -80,4 +81,4 @@ FOR_EACH_GETTER(DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER); #undef DEFINE_ZIG_BUN_OBJECT_GETTER_WRAPPER #undef FOR_EACH_GETTER -#undef FOR_EACH_CALLBACK \ No newline at end of file +#undef FOR_EACH_CALLBACK diff --git a/src/bun.js/bindings/BunObject.cpp b/src/bun.js/bindings/BunObject.cpp index e8caf3100a..e9b861744b 100644 --- a/src/bun.js/bindings/BunObject.cpp +++ b/src/bun.js/bindings/BunObject.cpp @@ -567,6 +567,7 @@ JSC_DEFINE_HOST_FUNCTION(functionHashCode, DO_NOT_USE_OR_YOU_WILL_BE_FIRED_mimalloc_dump BunObject_callback_DO_NOT_USE_OR_YOU_WILL_BE_FIRED_mimalloc_dump DontEnum|DontDelete|Function 1 FFI BunObject_getter_wrap_FFI DontDelete|PropertyCallback FileSystemRouter BunObject_getter_wrap_FileSystemRouter DontDelete|PropertyCallback + Glob BunObject_getter_wrap_Glob DontDelete|PropertyCallback MD4 BunObject_getter_wrap_MD4 DontDelete|PropertyCallback MD5 BunObject_getter_wrap_MD5 DontDelete|PropertyCallback SHA1 BunObject_getter_wrap_SHA1 DontDelete|PropertyCallback diff --git a/src/bun.js/bindings/generated_classes_list.zig b/src/bun.js/bindings/generated_classes_list.zig index 436a32dc30..31102361d3 100644 --- a/src/bun.js/bindings/generated_classes_list.zig +++ b/src/bun.js/bindings/generated_classes_list.zig @@ -19,6 +19,7 @@ pub const Classes = struct { pub const ExpectStringMatching = JSC.Expect.ExpectStringMatching; pub const ExpectArrayContaining = JSC.Expect.ExpectArrayContaining; pub const FileSystemRouter = JSC.API.FileSystemRouter; + pub const Glob = JSC.API.Glob; pub const Bundler = JSC.API.JSBundler; pub const JSBundler = Bundler; pub const Transpiler = JSC.API.JSTranspiler; diff --git a/src/bun.js/event_loop.zig b/src/bun.js/event_loop.zig index c9f65ed540..75f6852db6 100644 --- a/src/bun.js/event_loop.zig +++ b/src/bun.js/event_loop.zig @@ -10,6 +10,7 @@ const WebCore = JSC.WebCore; const Bun = JSC.API.Bun; const TaggedPointerUnion = @import("../tagged_pointer.zig").TaggedPointerUnion; const typeBaseName = @import("../meta.zig").typeBaseName; +const AsyncGlobWalkTask = JSC.API.Glob.WalkTask.AsyncGlobWalkTask; const CopyFilePromiseTask = WebCore.Blob.Store.CopyFile.CopyFilePromiseTask; const AsyncTransformTask = JSC.API.JSTranspiler.TransformTask.AsyncTransformTask; const ReadFileTask = WebCore.Blob.Store.ReadFile.ReadFileTask; @@ -347,6 +348,7 @@ const WaitPidResultTask = JSC.Subprocess.WaiterThread.WaitPidResultTask; // Task.get(ReadFileTask) -> ?ReadFileTask pub const Task = TaggedPointerUnion(.{ FetchTasklet, + AsyncGlobWalkTask, AsyncTransformTask, ReadFileTask, CopyFilePromiseTask, @@ -689,6 +691,11 @@ pub const EventLoop = struct { var fetch_task: *Fetch.FetchTasklet = task.get(Fetch.FetchTasklet).?; fetch_task.onProgressUpdate(); }, + @field(Task.Tag, @typeName(AsyncGlobWalkTask)) => { + var globWalkTask: *AsyncGlobWalkTask = task.get(AsyncGlobWalkTask).?; + globWalkTask.*.runFromJS(); + globWalkTask.deinit(); + }, @field(Task.Tag, @typeName(AsyncTransformTask)) => { var transform_task: *AsyncTransformTask = task.get(AsyncTransformTask).?; transform_task.*.runFromJS(); diff --git a/src/bun.js/node/dir_iterator.zig b/src/bun.js/node/dir_iterator.zig index 994ddaa31a..e666d2ffa9 100644 --- a/src/bun.js/node/dir_iterator.zig +++ b/src/bun.js/node/dir_iterator.zig @@ -18,7 +18,7 @@ const mem = std.mem; const strings = @import("root").bun.strings; const Maybe = JSC.Maybe; const File = std.fs.File; -const IteratorResult = struct { +pub const IteratorResult = struct { name: PathString, kind: Entry.Kind, }; @@ -318,7 +318,7 @@ pub const Iterator = switch (builtin.os.tag) { else => @compileError("unimplemented"), }; -const WrappedIterator = struct { +pub const WrappedIterator = struct { iter: Iterator, const Self = @This(); diff --git a/src/codegen/class-definitions.ts b/src/codegen/class-definitions.ts index cd462cda48..3995db0b00 100644 --- a/src/codegen/class-definitions.ts +++ b/src/codegen/class-definitions.ts @@ -1,6 +1,12 @@ interface PropertyAttribute { enumerable?: boolean; configurable?: boolean; + /** + * The name for a private symbol to use as the property name. The value should + * be a private symbol from `BunBuiltinNames.h`. This will omit the property + * from the prototype hash table, instead setting it using `putDirect()`. + */ + privateSymbol?: string; } export type Field = @@ -21,7 +27,16 @@ export type Field = pure?: boolean; }; } & PropertyAttribute) - | { internal: true }; + | { internal: true } + | { + /** + * The function is a builtin (its implementation is defined in + * src/js/builtins/), this value is the name of the code generator + * function: `camelCase(fileName + functionName + "CodeGenerator"`) + */ + builtin: string; + length?: number; + }; export interface ClassDefinition { name: string; diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index e29adeafe1..8d29f0ca0e 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -112,13 +112,13 @@ function DOMJITFunctionDeclaration(jsClassName, fnName, symName, { args, returns symName, )}(void* ptr, JSC::JSGlobalObject * lexicalGlobalObject${formattedArgs}); - static const JSC::DOMJIT::Signature DOMJITSignatureFor${fnName}(${DOMJITName(fnName)}Wrapper, - ${jsClassName}::info(), + static const JSC::DOMJIT::Signature DOMJITSignatureFor${fnName}(${DOMJITName(fnName)}Wrapper, + ${jsClassName}::info(), ${ pure ? "JSC::DOMJIT::Effect::forPure()" : "JSC::DOMJIT::Effect::forReadWrite(JSC::DOMJIT::HeapRange::top(), JSC::DOMJIT::HeapRange::top())" - }, + }, ${returns === "JSString" ? "JSC::SpecString" : DOMJITType("JSValue")}${domJITArgs}); `.trim(); } @@ -190,6 +190,7 @@ function propRow( enumerable = true, configurable = false, value, + builtin, } = (defaultPropertyAttributes ? Object.assign({}, defaultPropertyAttributes, prop) : prop) as any; var extraPropertyAttributes = ""; @@ -230,7 +231,14 @@ function propRow( } } - if (fn !== undefined) { + if (builtin !== undefined) { + if (typeof builtin !== "string") throw new Error('"builtin" should be string'); + return ` +{ "${name}"_s, static_cast(JSC::PropertyAttribute::Builtin), NoIntrinsic, { HashTableValue::BuiltinGeneratorType, ${builtin}, ${ + length || 0 + } } } +`.trim(); + } else if (fn !== undefined) { if (DOMJIT) { // { "getElementById"_s, static_cast(JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DOMJITFunction), NoIntrinsic, { HashTableValue::DOMJITFunctionType, jsTestDOMJITPrototypeFunction_getElementById, &DOMJITSignatureForTestDOMJITGetElementById } }, return ` @@ -244,7 +252,7 @@ function propRow( `.trim(); } else if (getter && setter) { return ` - + { "${name}"_s, static_cast(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::DOMAttribute${extraPropertyAttributes}), NoIntrinsic, { HashTableValue::GetterSetterType, ${getter}, ${setter} } } `.trim(); } else if (defaultValue) { @@ -278,7 +286,7 @@ export function generateHashTable(nameToUse, symbolName, typeName, obj, props = } for (const name in props) { - if ("internal" in props[name] || "value" in props[name]) continue; + if ("privateSymbol" in props[name] || "internal" in props[name] || "value" in props[name]) continue; if (name.startsWith("@@")) continue; rows.push( @@ -318,6 +326,21 @@ function generatePrototype(typeName, obj) { )}_s)), PropertyAttribute::ReadOnly | 0);`; } + if (protoFields[name].privateSymbol !== undefined) { + const privateSymbol = protoFields[name].privateSymbol; + const fn = protoFields[name].fn; + if (!fn) throw Error(`(field: ${name}) private field needs 'fn' key `); + + specialSymbols += ` + this->putDirect(vm, WebCore::clientData(vm)->builtinNames().${privateSymbol}PrivateName(), JSFunction::create(vm, globalObject, ${ + protoFields[name].length || 0 + }, String("${fn}"_s), ${protoSymbolName( + typeName, + fn, + )}Callback, ImplementationVisibility::Private), PropertyAttribute::ReadOnly | PropertyAttribute::DontEnum | 0);`; + continue; + } + if (!name.startsWith("@@")) { continue; } @@ -401,14 +424,14 @@ function generatePrototypeHeader(typename) { class ${proto} final : public JSC::JSNonFinalObject { public: using Base = JSC::JSNonFinalObject; - + static ${proto}* create(JSC::VM& vm, JSGlobalObject* globalObject, JSC::Structure* structure) { ${proto}* ptr = new (NotNull, JSC::allocateCell<${proto}>(vm)) ${proto}(vm, globalObject, structure); ptr->finishCreation(vm, globalObject); return ptr; } - + DECLARE_INFO; template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) @@ -420,13 +443,13 @@ function generatePrototypeHeader(typename) { { return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info()); } - + private: ${proto}(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure) : Base(vm, structure) { } - + void finishCreation(JSC::VM&, JSC::JSGlobalObject*); };`; } @@ -441,10 +464,10 @@ function generateConstructorHeader(typeName) { static ${name}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, ${prototypeName( typeName, )}* prototype); - + static constexpr unsigned StructureFlags = Base::StructureFlags; static constexpr bool needsDestruction = false; - + static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::InternalFunctionType, StructureFlags), info()); @@ -465,12 +488,12 @@ function generateConstructorHeader(typeName) { typeName, )}Constructor = std::forward(space); }); } - + void initializeProperties(JSC::VM& vm, JSC::JSGlobalObject* globalObject, ${prototypeName( typeName, )}* prototype); - + // Must be defined for each specialization class. static JSC::EncodedJSValue JSC_HOST_CALL_ATTRIBUTES construct(JSC::JSGlobalObject*, JSC::CallFrame*); @@ -479,7 +502,7 @@ function generateConstructorHeader(typeName) { ${name}(JSC::VM& vm, JSC::Structure* structure); void finishCreation(JSC::VM&, JSC::JSGlobalObject* globalObject, ${prototypeName(typeName)}* prototype); }; - + `; } @@ -572,7 +595,7 @@ ${ : "" } - + `; } @@ -829,13 +852,13 @@ function writeBarrier(symbolName, typeName, name, cacheName) { auto* thisObject = jsCast<${className(typeName)}*>(JSValue::decode(thisValue)); thisObject->${cacheName}.set(vm, thisObject, JSValue::decode(value)); } - + extern "C" EncodedJSValue ${symbolName(typeName, name)}GetCachedValue(JSC::EncodedJSValue thisValue) { auto* thisObject = jsCast<${className(typeName)}*>(JSValue::decode(thisValue)); return JSValue::encode(thisObject->${cacheName}.get()); } - + `; } function renderFieldsImpl( @@ -862,8 +885,8 @@ JSC_DEFINE_CUSTOM_GETTER(js${typeName}Constructor, (JSGlobalObject * lexicalGlob if (UNLIKELY(!prototype)) return throwVMTypeError(lexicalGlobalObject, throwScope, "Cannot get constructor for ${typeName}"_s); return JSValue::encode(globalObject->${className(typeName)}Constructor()); -} - +} + `); } @@ -883,10 +906,10 @@ JSC_DEFINE_CUSTOM_GETTER(${symbolName( auto throwScope = DECLARE_THROW_SCOPE(vm); ${className(typeName)}* thisObject = jsCast<${className(typeName)}*>(JSValue::decode(encodedThisValue)); JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject); - + if (JSValue cachedValue = thisObject->${cacheName}.get()) return JSValue::encode(cachedValue); - + JSC::JSValue result = JSC::JSValue::decode( ${symbolName(typeName, proto[name].getter)}(thisObject->wrapped(),${ proto[name].this!! ? " encodedThisValue, " : "" @@ -1040,7 +1063,7 @@ JSC_DEFINE_CUSTOM_SETTER(${symbolName( #ifdef BUN_DEBUG /** View the file name of the JS file that called this function - * from a debugger */ + * from a debugger */ SourceOrigin sourceOrigin = callFrame->callerSourceOrigin(vm); const char* fileName = sourceOrigin.string().utf8().data(); static const char* lastFileName = nullptr; @@ -1085,7 +1108,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { weakInit = `m_weakThis = JSC::Weak<${name}>(this, getOwner());`; weakOwner = ` JSC::Weak<${name}> m_weakThis; - + static bool hasPendingActivity(void* ctx); @@ -1104,7 +1127,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { } void finalize(JSC::Handle, void* context) final {} }; - + static JSC::WeakHandleOwner* getOwner() { static NeverDestroyed m_owner; @@ -1123,7 +1146,7 @@ function generateClassHeader(typeName, obj: ClassDefinition) { public: using Base = JSC::JSDestructibleObject; static ${name}* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure, void* ctx); - + DECLARE_EXPORT_INFO; template static JSC::GCClient::IsoSubspace* subspaceFor(JSC::VM& vm) { @@ -1140,42 +1163,42 @@ function generateClassHeader(typeName, obj: ClassDefinition) { typeName, )} = std::forward(space); }); } - + static void destroy(JSC::JSCell*); static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype) { return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(static_cast(${JSType}), StructureFlags), info()); } - + static JSObject* createPrototype(VM& vm, JSDOMGlobalObject* globalObject); ${ obj.noConstructor ? "" : `static JSObject* createConstructor(VM& vm, JSGlobalObject* globalObject, JSValue prototype)` }; - + ~${name}(); - + void* wrapped() const { return m_ctx; } - + void detach() { m_ctx = nullptr; } - + static void analyzeHeap(JSCell*, JSC::HeapAnalyzer&); static ptrdiff_t offsetOfWrapped() { return OBJECT_OFFSETOF(${name}, m_ctx); } - + void* m_ctx { nullptr }; - + ${name}(JSC::VM& vm, JSC::Structure* structure, void* sinkPtr) : Base(vm, structure) { m_ctx = sinkPtr; ${weakInit.trim()} } - + void finishCreation(JSC::VM&); ${Object.entries(obj.custom ?? {}) @@ -1305,14 +1328,14 @@ ${renderCallbacksCppImpl(typeName, callbacks)} "getInternalProperties", )}(void* ptr, JSC::JSGlobalObject *globalObject, EncodedJSValue thisValue); - JSC::JSValue getInternalProperties(JSC::VM &, JSC::JSGlobalObject *globalObject, ${name}* castedThis) + JSC::JSValue getInternalProperties(JSC::VM &, JSC::JSGlobalObject *globalObject, ${name}* castedThis) { return JSValue::decode(${symbolName( typeName, "getInternalProperties", )}(castedThis->impl(), globalObject, JSValue::encode(castedThis))); } - + `; } @@ -1341,7 +1364,7 @@ void ${name}::destroy(JSCell* cell) { static_cast<${name}*>(cell)->${name}::~${name}(); } - + const ClassInfo ${name}::s_info = { "${typeName}"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(${name}) }; void ${name}::finishCreation(VM& vm) @@ -1367,7 +1390,7 @@ extern "C" void* ${typeName}__fromJS(JSC::EncodedJSValue value) { if (!object) return nullptr; - + return object->wrapped(); } @@ -1375,7 +1398,7 @@ extern "C" bool ${typeName}__dangerouslySetPtr(JSC::EncodedJSValue value, void* ${className(typeName)}* object = JSC::jsDynamicCast<${className(typeName)}*>(JSValue::decode(value)); if (!object) return false; - + object->m_ctx = ptr; return true; } @@ -1428,7 +1451,7 @@ extern "C" EncodedJSValue ${typeName}__create(Zig::GlobalObject* globalObject, v ${DEFINE_VISIT_CHILDREN} - + `.trim(); return output; @@ -1516,12 +1539,12 @@ function generateZig( `extern fn ${protoSymbolName(typeName, name)}SetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) void; extern fn ${protoSymbolName(typeName, name)}GetCachedValue(JSC.JSValue) JSC.JSValue; - + /// \`${typeName}.${name}\` setter /// This value will be visited by the garbage collector. pub fn ${name}SetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { JSC.markBinding(@src()); - ${protoSymbolName(typeName, name)}SetCachedValue(thisValue, globalObject, value); + ${protoSymbolName(typeName, name)}SetCachedValue(thisValue, globalObject, value); } /// \`${typeName}.${name}\` getter @@ -1531,7 +1554,7 @@ function generateZig( const result = ${protoSymbolName(typeName, name)}GetCachedValue(thisValue); if (result == .zero) return null; - + return result; } `.trim() + "\n", @@ -1639,14 +1662,14 @@ function generateZig( if (fn) { if (DOMJIT) { output += ` - if (@TypeOf(${typeName}.${DOMJITName(fn)}) != ${ZigDOMJITFunctionType(typeName, DOMJIT)}) + if (@TypeOf(${typeName}.${DOMJITName(fn)}) != ${ZigDOMJITFunctionType(typeName, DOMJIT)}) @compileLog( "Expected ${typeName}.${DOMJITName(fn)} to be a DOMJIT function" );`; } output += ` - if (@TypeOf(${typeName}.${fn}) != CallbackType) + if (@TypeOf(${typeName}.${fn}) != CallbackType) @compileLog( "Expected ${typeName}.${fn} to be a callback but received " ++ @typeName(@TypeOf(${typeName}.${fn})) );`; @@ -1661,7 +1684,7 @@ function generateZig( if (getter) { output += ` - if (@TypeOf(${typeName}.${getter}) != StaticGetterType) + if (@TypeOf(${typeName}.${getter}) != StaticGetterType) @compileLog( "Expected ${typeName}.${getter} to be a static getter" ); @@ -1670,7 +1693,7 @@ function generateZig( if (setter) { output += ` - if (@TypeOf(${typeName}.${setter}) != StaticSetterType) + if (@TypeOf(${typeName}.${setter}) != StaticSetterType) @compileLog( "Expected ${typeName}.${setter} to be a static setter" );`; @@ -1678,7 +1701,7 @@ function generateZig( if (fn) { output += ` - if (@TypeOf(${typeName}.${fn}) != StaticCallbackType) + if (@TypeOf(${typeName}.${fn}) != StaticCallbackType) @compileLog( "Expected ${typeName}.${fn} to be a static callback" );`; @@ -1687,7 +1710,7 @@ function generateZig( if (!!call) { output += ` - if (@TypeOf(${typeName}.call) != StaticCallbackType) + if (@TypeOf(${typeName}.call) != StaticCallbackType) @compileLog( "Expected ${typeName}.call to be a static callback" );`; @@ -1771,7 +1794,7 @@ ${[...exports] } }; - + `; } @@ -1808,8 +1831,8 @@ function generateLazyClassStructureImpl(typeName, { klass = {}, proto = {}, noCo )}::createConstructor(init.vm, init.global, init.prototype));` } }); - - + + `.trim(); } @@ -1885,19 +1908,19 @@ function initLazyClasses(initLaterFunctions) { ALWAYS_INLINE void GlobalObject::initGeneratedLazyClasses() { ${initLaterFunctions.map(a => a.trim()).join("\n ")} } - + `.trim(); } function visitLazyClasses(classes) { return ` - + template void GlobalObject::visitGeneratedLazyClasses(GlobalObject *thisObject, Visitor& visitor) { ${classes.map(a => `thisObject->m_${className(a.name)}.visit(visitor);`).join("\n ")} } - + `.trim(); } @@ -1916,7 +1939,7 @@ const ZIG_GENERATED_CLASSES_HEADER = ` /// - Add it to generated_classes_list.zig /// - pub usingnamespace JSC.Codegen.JSMyClassName; /// 5. make clean-bindings && make bindings -j10 -/// +/// const bun = @import("root").bun; const JSC = bun.JSC; const Classes = JSC.GeneratedClassesList; @@ -1998,7 +2021,7 @@ function writeCppSerializers() { `; } - output += ` + output += ` std::optional StructuredCloneableSerialize::fromJS(JSC::JSValue value) { ${structuredClonable.map(fromJSForEachClass).join("\n").trim()} diff --git a/src/glob.zig b/src/glob.zig new file mode 100644 index 0000000000..8dbe6384a6 --- /dev/null +++ b/src/glob.zig @@ -0,0 +1,3104 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) 2023 Devon Govett +// Copyright (c) 2023 Stephen Gregoratto +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. +const std = @import("std"); +const math = std.math; +const mem = std.mem; +const BunString = @import("./bun.zig").String; +const expect = std.testing.expect; +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayListUnmanaged; +const ArrayListManaged = std.ArrayList; +const DirIterator = @import("./bun.js/node/dir_iterator.zig"); +const bun = @import("./bun.zig"); +const Syscall = bun.sys; +const PathLike = @import("./bun.js/node/types.zig").PathLike; +const Maybe = @import("./bun.js/node/types.zig").Maybe; +const Dirent = @import("./bun.js/node/types.zig").Dirent; +const PathString = @import("./string_types.zig").PathString; +const ZigString = @import("./bun.js/bindings/bindings.zig").ZigString; +const isAllAscii = @import("./string_immutable.zig").isAllASCII; +const EntryKind = @import("./bun.js/node/types.zig").Dirent.Kind; +const Arena = std.heap.ArenaAllocator; +const GlobAscii = @import("./glob_ascii.zig"); +const C = @import("./c.zig"); +const ResolvePath = @import("./resolver/resolve_path.zig"); +const eqlComptime = @import("./string_immutable.zig").eqlComptime; + +const isWindows = @import("builtin").os.tag == .windows; + +const CodepointIterator = @import("./string_immutable.zig").PackedCodepointIterator; +const Codepoint = CodepointIterator.Cursor.CodePointType; +// const Codepoint = u32; +const Cursor = CodepointIterator.Cursor; + +const CursorState = struct { + cursor: CodepointIterator.Cursor = .{}, + /// The index in terms of codepoints + // cp_idx: usize, + + fn init(iterator: *const CodepointIterator) CursorState { + var this_cursor: CodepointIterator.Cursor = .{}; + _ = iterator.next(&this_cursor); + return .{ + // .cp_idx = 0, + .cursor = this_cursor, + }; + } + + /// Return cursor pos of next codepoint without modifying the current. + /// + /// NOTE: If there is no next codepoint (cursor is at the last one), then + /// the returned cursor will have `c` as zero value and `i` will be >= + /// sourceBytes.len + fn peek(this: *const CursorState, iterator: *const CodepointIterator) CursorState { + var cpy = this.*; + // If outside of bounds + if (!iterator.next(&cpy.cursor)) { + // This will make `i >= sourceBytes.len` + cpy.cursor.i += cpy.cursor.width; + cpy.cursor.width = 1; + cpy.cursor.c = CodepointIterator.ZeroValue; + } + // cpy.cp_idx += 1; + return cpy; + } + + fn bump(this: *CursorState, iterator: *const CodepointIterator) void { + if (!iterator.next(&this.cursor)) { + this.cursor.i += this.cursor.width; + this.cursor.width = 1; + this.cursor.c = CodepointIterator.ZeroValue; + } + // this.cp_idx += 1; + } + + inline fn manualBumpAscii(this: *CursorState, i: u32, nextCp: Codepoint) void { + this.cursor.i += i; + this.cursor.c = nextCp; + this.cursor.width = 1; + } + + inline fn manualPeekAscii(this: *CursorState, i: u32, nextCp: Codepoint) CursorState { + return .{ + .cursor = CodepointIterator.Cursor{ + .i = this.cursor.i + i, + .c = @truncate(nextCp), + .width = 1, + }, + }; + } +}; + +pub const BunGlobWalker = GlobWalker_(null); + +fn dummyFilterTrue(val: []const u8) bool { + _ = val; + return true; +} + +fn dummyFilterFalse(val: []const u8) bool { + _ = val; + return false; +} + +pub fn GlobWalker_( + comptime ignore_filter_fn: ?*const fn ([]const u8) bool, +) type { + const is_ignored: *const fn ([]const u8) bool = if (comptime ignore_filter_fn) |func| func else dummyFilterFalse; + + return struct { + const GlobWalker = @This(); + pub const Result = Maybe(void); + + arena: Arena = undefined, + + /// not owned by this struct + pattern: []const u8 = "", + + pattern_codepoints: []u32 = &[_]u32{}, + cp_len: u32 = 0, + + /// If the pattern contains "./" or "../" + has_relative_components: bool = false, + + patternComponents: ArrayList(Component) = .{}, + matchedPaths: ArrayList(BunString) = .{}, + i: u32 = 0, + + dot: bool = false, + absolute: bool = false, + cwd: []const u8 = "", + follow_symlinks: bool = false, + error_on_broken_symlinks: bool = false, + only_files: bool = true, + + pathBuf: [bun.MAX_PATH_BYTES]u8 = undefined, + // iteration state + workbuf: ArrayList(WorkItem) = ArrayList(WorkItem){}, + + /// The glob walker references the .directory.path so its not safe to + /// copy/move this + const IterState = union(enum) { + get_next, + directory: Directory, + + const Directory = struct { + fd: bun.FileDescriptor, + iter: DirIterator.WrappedIterator, + path: [bun.MAX_PATH_BYTES]u8, + dir_path: [:0]const u8, + + component_idx: u32, + pattern: *Component, + next_pattern: ?*Component, + is_last: bool, + + iter_closed: bool = false, + at_cwd: bool = false, + }; + }; + + pub const Iterator = struct { + walker: *GlobWalker, + iter_state: IterState = .get_next, + cwd_fd: bun.FileDescriptor = 0, + empty_dir_path: [0:0]u8 = [0:0]u8{}, + + pub fn init(this: *Iterator) !Maybe(void) { + var path_buf: *[bun.MAX_PATH_BYTES]u8 = &this.walker.pathBuf; + const root_path = this.walker.cwd; + @memcpy(path_buf[0..root_path.len], root_path[0..root_path.len]); + path_buf[root_path.len] = 0; + var cwd_fd = switch (Syscall.open(@ptrCast(path_buf[0 .. root_path.len + 1]), std.os.O.DIRECTORY | std.os.O.RDONLY, 0)) { + .err => |err| return .{ .err = this.walker.handleSysErrWithPath(err, @ptrCast(path_buf[0 .. root_path.len + 1])) }, + .result => |fd| fd, + }; + + this.cwd_fd = cwd_fd; + + const root_work_item = WorkItem.new(this.walker.cwd, 0, .directory); + switch (try this.transitionToDirIterState(root_work_item, true)) { + .err => |err| return .{ .err = err }, + else => {}, + } + + return Maybe(void).success; + } + + pub fn deinit(this: *Iterator) void { + _ = Syscall.close(this.cwd_fd); + switch (this.iter_state) { + .directory => |dir| { + if (!dir.iter_closed and !dir.at_cwd) { + _ = Syscall.close(dir.fd); + } + }, + else => {}, + } + } + + fn transitionToDirIterState( + this: *Iterator, + work_item: WorkItem, + comptime root: bool, + ) !Maybe(void) { + // FIXME: doesn't nede to be initially set to undefined but lazy rn (can be zero initialized) + this.iter_state = .{ .directory = undefined }; + + var dir_path: [:0]u8 = dir_path: { + if (comptime root) { + if (!this.walker.absolute) { + this.iter_state.directory.path[0] = 0; + break :dir_path this.iter_state.directory.path[0..0 :0]; + } + } + // TODO Optimization: On posix systems filepaths are already null byte terminated so we can skip this if thats the case + @memcpy(this.iter_state.directory.path[0..work_item.path.len], work_item.path); + this.iter_state.directory.path[work_item.path.len] = 0; + break :dir_path this.iter_state.directory.path[0..work_item.path.len :0]; + }; + + var had_dot_dot = false; + const component_idx = this.walker.skipSpecialComponents(work_item.idx, &dir_path, &this.iter_state.directory.path, &had_dot_dot); + + this.iter_state.directory.dir_path = dir_path; + this.iter_state.directory.component_idx = component_idx; + this.iter_state.directory.pattern = &this.walker.patternComponents.items[component_idx]; + this.iter_state.directory.next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; + this.iter_state.directory.is_last = component_idx == this.walker.patternComponents.items.len - 1; + + var fd: bun.FileDescriptor = fd: { + if (comptime root) { + if (had_dot_dot) break :fd switch (Syscall.openat(this.cwd_fd, dir_path, std.os.O.DIRECTORY | std.os.O.RDONLY, 0)) { + .err => |err| return .{ + .err = this.walker.handleSysErrWithPath(err, dir_path), + }, + .result => |fd_| fd_, + }; + + this.iter_state.directory.at_cwd = true; + break :fd this.cwd_fd; + } + + break :fd switch (Syscall.openat(this.cwd_fd, dir_path, std.os.O.DIRECTORY | std.os.O.RDONLY, 0)) { + .err => |err| return .{ + .err = this.walker.handleSysErrWithPath(err, dir_path), + }, + .result => |fd_| fd_, + }; + }; + + this.iter_state.directory.fd = fd; + var dir = std.fs.Dir{ .fd = bun.fdcast(fd) }; + var iterator = DirIterator.iterate(dir); + this.iter_state.directory.iter = iterator; + + return Maybe(void).success; + } + + pub fn next(this: *Iterator) !Maybe(?[]const u8) { + while (true) { + switch (this.iter_state) { + .get_next => { + // Done + if (this.walker.workbuf.items.len == 0) return .{ .result = null }; + const work_item = this.walker.workbuf.pop(); + switch (work_item.kind) { + .directory => { + switch (try this.transitionToDirIterState(work_item, false)) { + .err => |err| return .{ .err = err }, + else => {}, + } + continue; + }, + .symlink => { + var scratch_path_buf: *[bun.MAX_PATH_BYTES]u8 = &this.walker.pathBuf; + @memcpy(scratch_path_buf[0..work_item.path.len], work_item.path); + scratch_path_buf[work_item.path.len] = 0; + var symlink_full_path_z: [:0]u8 = scratch_path_buf[0..work_item.path.len :0]; + const entry_name = symlink_full_path_z[work_item.entry_start..symlink_full_path_z.len]; + + var has_dot_dot = false; + const component_idx = this.walker.skipSpecialComponents(work_item.idx, &symlink_full_path_z, scratch_path_buf, &has_dot_dot); + var pattern = this.walker.patternComponents.items[component_idx]; + const next_pattern = if (component_idx + 1 < this.walker.patternComponents.items.len) &this.walker.patternComponents.items[component_idx + 1] else null; + const is_last = component_idx == this.walker.patternComponents.items.len - 1; + + const kind: EntryKind = kind: { + const stat_result = switch (Syscall.stat(symlink_full_path_z)) { + .err => |err| { + if (this.walker.error_on_broken_symlinks) return .{ .err = this.walker.handleSysErrWithPath(err, symlink_full_path_z) }; + // Broken symlink + if (!this.walker.only_files) { + // (See case A and B in the comment for `matchPatternFile()`) + // When we encounter a symlink we call the catch all + // matching function: `matchPatternImpl()` to see if we can avoid following the symlink. + // So for case A, we just need to check if the pattern is the last pattern. + if (is_last or + (pattern.syntax_hint == .Double and + component_idx + 1 == this.walker.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.walker.matchPatternImpl(next_pattern.?, entry_name))) + { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) }; + } + } + continue; + }, + .result => |stat| stat, + }; + + if (comptime bun.Environment.isPosix) { + const m = stat_result.mode & std.os.S.IFMT; + switch (m) { + std.os.S.IFDIR => break :kind .directory, + std.os.S.IFLNK => break :kind .sym_link, + std.os.S.IFREG => break :kind .file, + else => {}, + } + } else if (comptime bun.Environment.isWindows) { + return bun.todo(@src(), Maybe(?[]const u8).success); + } else { + // wasm? + return bun.todo(@src(), Maybe(?[]const u8).success); + } + + continue; + }; + + this.iter_state = .get_next; + + switch (kind) { + .file => { + if (is_last) + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) }; + + if (pattern.syntax_hint == .Double and + component_idx + 1 == this.walker.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.walker.matchPatternImpl(next_pattern.?, entry_name)) + { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) }; + } + + continue; + }, + .directory => { + var add_dir: bool = false; + // TODO this function calls `matchPatternImpl(pattern, + // entry_name)` which is redundant because we already called + // that when we first encountered the symlink + const recursion_idx_bump_ = this.walker.matchPatternDir(&pattern, next_pattern, entry_name, component_idx, is_last, &add_dir); + + if (recursion_idx_bump_) |recursion_idx_bump| { + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.new(work_item.path, component_idx + recursion_idx_bump, .directory), + ); + } + + if (add_dir and !this.walker.only_files) { + return .{ .result = try this.walker.prepareMatchedPathSymlink(symlink_full_path_z) }; + } + + continue; + }, + .sym_link => { + // This should not happen because if there's a symlink chain + // calling stat should follow it until it reaches the final + // file/dir + @panic("Unexpected symlink chain"); + }, + else => continue, + } + }, + } + }, + .directory => |*dir| { + const entry = switch (dir.iter.next()) { + .err => |err| { + if (!dir.at_cwd) _ = Syscall.close(dir.fd); + dir.iter_closed = true; + return .{ .err = this.walker.handleSysErrWithPath(err, dir.dir_path) }; + }, + .result => |ent| ent, + } orelse { + if (!dir.at_cwd) _ = Syscall.close(dir.fd); + dir.iter_closed = true; + this.iter_state = .get_next; + continue; + }; + + const dir_iter_state: *const IterState.Directory = &this.iter_state.directory; + + const entry_name = entry.name.slice(); + switch (entry.kind) { + .file => { + const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); + if (matches) { + const prepared = try this.walker.prepareMatchedPath(entry_name, dir.dir_path); + return .{ .result = prepared }; + } + continue; + }, + .directory => { + var add_dir: bool = false; + const recursion_idx_bump_ = this.walker.matchPatternDir(dir_iter_state.pattern, dir_iter_state.next_pattern, entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, &add_dir); + + if (recursion_idx_bump_) |recursion_idx_bump| { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir.dir_path[0..dir.dir_path.len], + entry_name, + }; + + const subdir_entry_name = try this.walker.join(subdir_parts); + + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.new(subdir_entry_name, dir_iter_state.component_idx + recursion_idx_bump, .directory), + ); + } + + if (add_dir and !this.walker.only_files) { + const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path); + return .{ .result = prepared_path }; + } + + continue; + }, + .sym_link => { + if (this.walker.follow_symlinks) { + // Following a symlink requires additional syscalls, so + // we first try it against our "catch-all" pattern match + // function + const matches = this.walker.matchPatternImpl(dir_iter_state.pattern, entry_name); + if (!matches) continue; + + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir.dir_path[0..dir.dir_path.len], + entry_name, + }; + const entry_start: u32 = @intCast(if (dir.dir_path.len == 0) 0 else dir.dir_path.len + 1); + + // const subdir_entry_name = try this.arena.allocator().dupe(u8, ResolvePath.join(subdir_parts, .auto)); + const subdir_entry_name = try this.walker.join(subdir_parts); + + try this.walker.workbuf.append( + this.walker.arena.allocator(), + WorkItem.newSymlink(subdir_entry_name, dir_iter_state.component_idx, entry_start), + ); + + continue; + } + + if (this.walker.only_files) continue; + + const matches = this.walker.matchPatternFile(entry_name, dir_iter_state.component_idx, dir_iter_state.is_last, dir_iter_state.pattern, dir_iter_state.next_pattern); + if (matches) { + const prepared_path = try this.walker.prepareMatchedPath(entry_name, dir.dir_path); + _ = prepared_path; + } + + continue; + }, + else => continue, + } + }, + } + } + } + }; + + const WorkItem = struct { + path: []const u8, + idx: u32, + kind: Kind, + entry_start: u32 = 0, + + const Kind = enum { + directory, + symlink, + }; + + fn new(path: []const u8, idx: u32, kind: Kind) WorkItem { + return .{ + .path = path, + .idx = idx, + .kind = kind, + }; + } + + fn newSymlink(path: []const u8, idx: u32, entry_start: u32) WorkItem { + return .{ + .path = path, + .idx = idx, + .kind = .symlink, + .entry_start = entry_start, + }; + } + }; + + /// A component is each part of a glob pattern, separated by directory + /// separator: + /// `src/**/*.ts` -> `src`, `**`, `*.ts` + const Component = struct { + start: u32, + len: u32, + + syntax_hint: SyntaxHint = .None, + is_ascii: bool = false, + + /// Only used when component is not ascii + unicode_set: bool = false, + start_cp: u32 = 0, + end_cp: u32 = 0, + + const SyntaxHint = enum { + None, + Single, + Double, + /// Uses special fast-path matching for components like: `*.ts` + WildcardFilepath, + /// Uses special fast-patch matching for literal components e.g. + /// "node_modules", becomes memcmp + Literal, + /// ./fixtures/*.ts + /// ^ + Dot, + /// ../ + DotBack, + }; + }; + + /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong + pub fn init( + this: *GlobWalker, + arena: *Arena, + pattern: []const u8, + dot: bool, + absolute: bool, + follow_symlinks: bool, + error_on_broken_symlinks: bool, + only_files: bool, + ) !Maybe(void) { + errdefer arena.deinit(); + var cwd: []const u8 = undefined; + switch (Syscall.getcwd(&this.pathBuf)) { + .err => |err| { + return .{ .err = err }; + }, + .result => |result| { + var copiedCwd = try arena.allocator().alloc(u8, result.len); + @memcpy(copiedCwd, result); + cwd = copiedCwd; + }, + } + + return try this.initWithCwd( + arena, + pattern, + cwd, + dot, + absolute, + follow_symlinks, + error_on_broken_symlinks, + only_files, + ); + } + + pub fn convertUtf8ToCodepoints(codepoints: []u32, pattern: []const u8) void { + switch (comptime @import("builtin").target.cpu.arch.endian()) { + .big => { + _ = bun.simdutf.convert.utf8.to.utf32.be(pattern, codepoints); + }, + .little => { + _ = bun.simdutf.convert.utf8.to.utf32.le(pattern, codepoints); + }, + } + } + + /// `cwd` should be allocated with the arena + /// The arena parameter is dereferenced and copied if all allocations go well and nothing goes wrong + pub fn initWithCwd( + this: *GlobWalker, + arena: *Arena, + pattern: []const u8, + cwd: []const u8, + dot: bool, + absolute: bool, + follow_symlinks: bool, + error_on_broken_symlinks: bool, + only_files: bool, + ) !Maybe(void) { + var patternComponents = ArrayList(Component){}; + try GlobWalker.buildPatternComponents( + arena, + &patternComponents, + pattern, + &this.cp_len, + &this.pattern_codepoints, + &this.has_relative_components, + ); + + this.cwd = cwd; + + this.patternComponents = patternComponents; + this.pattern = pattern; + this.arena = arena.*; + this.dot = dot; + this.absolute = absolute; + this.follow_symlinks = follow_symlinks; + this.error_on_broken_symlinks = error_on_broken_symlinks; + this.only_files = only_files; + + return Maybe(void).success; + } + + /// NOTE This also calls deinit on the arena, if you don't want to do that then + pub fn deinit(this: *GlobWalker, comptime clear_arena: bool) void { + if (comptime clear_arena) { + this.arena.deinit(); + } + } + + pub fn handleSysErrWithPath( + this: *GlobWalker, + err: Syscall.Error, + path_buf: [:0]const u8, + ) Syscall.Error { + @memcpy(this.pathBuf[0 .. path_buf.len + 1], @as([]const u8, @ptrCast(path_buf[0 .. path_buf.len + 1]))); + // std.mem.copyBackwards(u8, this.pathBuf[0 .. path_buf.len + 1], @as([]const u8, @ptrCast(path_buf[0 .. path_buf.len + 1]))); + return err.withPath(this.pathBuf[0 .. path_buf.len + 1]); + } + + pub fn walk(this: *GlobWalker) !Maybe(void) { + if (this.patternComponents.items.len == 0) return Maybe(void).success; + + var iter = GlobWalker.Iterator{ .walker = this }; + defer iter.deinit(); + switch (try iter.init()) { + .err => |err| return .{ .err = err }, + else => {}, + } + + while (switch (try iter.next()) { + .err => |err| return .{ .err = err }, + .result => |matched_path| matched_path, + }) |path| { + try this.matchedPaths.append(this.arena.allocator(), BunString.fromBytes(path)); + } + + return Maybe(void).success; + } + + // NOTE you must check that the pattern at `idx` has `syntax_hint == .Dot` or + // `syntax_hint == .DotBack` first + fn collapseDots( + this: *GlobWalker, + idx: u32, + dir_path: *[:0]u8, + path_buf: *[bun.MAX_PATH_BYTES]u8, + encountered_dot_dot: *bool, + ) u32 { + var component_idx = idx; + var len = dir_path.len; + while (component_idx < this.patternComponents.items.len) { + switch (this.patternComponents.items[component_idx].syntax_hint) { + .Dot => { + defer component_idx += 1; + if (len + 2 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); + if (len == 0) { + path_buf[len] = '.'; + path_buf[len + 1] = 0; + len += 1; + } else { + path_buf[len] = '/'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = 0; + len += 2; + } + }, + .DotBack => { + defer component_idx += 1; + encountered_dot_dot.* = true; + if (dir_path.len + 3 >= bun.MAX_PATH_BYTES) @panic("Invalid path"); + if (len == 0) { + path_buf[len] = '.'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = 0; + len += 2; + } else { + path_buf[len] = '/'; + path_buf[len + 1] = '.'; + path_buf[len + 2] = '.'; + path_buf[len + 3] = 0; + len += 3; + } + }, + else => break, + } + } + + dir_path.len = len; + + return component_idx; + } + + // NOTE you must check that the pattern at `idx` has `syntax_hint == .Double` first + fn collapseSuccessiveDoubleWildcards(this: *GlobWalker, idx: u32) u32 { + var component_idx = idx; + var pattern = this.patternComponents.items[idx]; + _ = pattern; + // Collapse successive double wildcards + while (component_idx + 1 < this.patternComponents.items.len and + this.patternComponents.items[component_idx + 1].syntax_hint == .Double) : (component_idx += 1) + {} + return component_idx; + } + + pub fn skipSpecialComponents( + this: *GlobWalker, + work_item_idx: u32, + dir_path: *[:0]u8, + scratch_path_buf: *[bun.MAX_PATH_BYTES]u8, + encountered_dot_dot: *bool, + ) u32 { + var component_idx = work_item_idx; + + // Skip `.` and `..` while also appending them to `dir_path` + component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { + .Dot => this.collapseDots( + component_idx, + dir_path, + scratch_path_buf, + encountered_dot_dot, + ), + .DotBack => this.collapseDots( + component_idx, + dir_path, + scratch_path_buf, + encountered_dot_dot, + ), + else => component_idx, + }; + + // Skip to the last `**` if there is a chain of them + component_idx = switch (this.patternComponents.items[component_idx].syntax_hint) { + .Double => this.collapseSuccessiveDoubleWildcards(component_idx), + else => component_idx, + }; + + return component_idx; + } + + fn matchPatternDir( + this: *GlobWalker, + pattern: *Component, + next_pattern: ?*Component, + entry_name: []const u8, + component_idx: u32, + is_last: bool, + add: *bool, + ) ?u32 { + if (!this.dot and GlobWalker.startsWithDot(entry_name)) return null; + if (is_ignored(entry_name)) return null; + + // Handle double wildcard `**`, this could possibly + // propagate the `**` to the directory's children + if (pattern.syntax_hint == .Double) { + // Stop the double wildcard if it matches the pattern afer it + // Example: src/**/*.js + // - Matches: src/bun.js/ + // src/bun.js/foo/bar/baz.js + if (!is_last and this.matchPatternImpl(next_pattern.?, entry_name)) { + // But if the next pattern is the last + // component, it should match and propagate the + // double wildcard recursion to the directory's + // children + if (component_idx + 1 == this.patternComponents.items.len - 1) { + add.* = true; + return 0; + } + + // In the normal case skip over the next pattern + // since we matched it, example: + // BEFORE: src/**/node_modules/**/*.js + // ^ + // AFTER: src/**/node_modules/**/*.js + // ^ + return 2; + } + + if (is_last) { + add.* = true; + } + + return 0; + } + + const matches = this.matchPatternImpl(pattern, entry_name); + if (matches) { + if (is_last) { + add.* = true; + return null; + } + return 1; + } + + return null; + } + + /// A file can only match if: + /// a) it matches against the the last pattern, or + /// b) it matches the next pattern, provided the current + /// pattern is a double wildcard and the next pattern is + /// not a double wildcard + /// + /// Examples: + /// a -> `src/foo/index.ts` matches + /// b -> `src/**/*.ts` (on 2nd pattern) matches + fn matchPatternFile( + this: *GlobWalker, + entry_name: []const u8, + component_idx: u32, + is_last: bool, + pattern: *Component, + next_pattern: ?*Component, + ) bool { + // Handle case b) + if (!is_last) return pattern.syntax_hint == .Double and + component_idx + 1 == this.patternComponents.items.len -| 1 and + next_pattern.?.syntax_hint != .Double and + this.matchPatternImpl(next_pattern.?, entry_name); + + // Handle case a) + return this.matchPatternImpl(pattern, entry_name); + } + + fn matchPatternImpl( + this: *GlobWalker, + pattern_component: *Component, + filepath: []const u8, + ) bool { + if (!this.dot and GlobWalker.startsWithDot(filepath)) return false; + if (is_ignored(filepath)) return false; + + return switch (pattern_component.syntax_hint) { + .Double, .Single => true, + .WildcardFilepath => if (comptime !isWindows) + matchWildcardFilepath(this.pattern[pattern_component.start .. pattern_component.start + pattern_component.len], filepath) + else + this.matchPatternSlow(pattern_component, filepath), + .Literal => if (comptime !isWindows) + matchWildcardLiteral(this.pattern[pattern_component.start .. pattern_component.start + pattern_component.len], filepath) + else + this.matchPatternSlow(pattern_component, filepath), + else => this.matchPatternSlow(pattern_component, filepath), + }; + } + + fn matchPatternSlow(this: *GlobWalker, pattern_component: *Component, filepath: []const u8) bool { + // windows filepaths are utf-16 so GlobAscii.match will never work + if (comptime !isWindows) { + if (pattern_component.is_ascii and isAllAscii(filepath)) + return GlobAscii.match( + this.pattern[pattern_component.start .. pattern_component.start + pattern_component.len], + filepath, + ); + } + const codepoints = this.componentStringUnicode(pattern_component); + return matchImpl( + codepoints, + filepath, + ); + } + + fn componentStringUnicode(this: *GlobWalker, pattern_component: *Component) []const u32 { + if (comptime isWindows) { + return this.componentStringUnicodeWindows(pattern_component); + } else { + return this.componentStringUnicodePosix(pattern_component); + } + } + + fn componentStringUnicodeWindows(this: *GlobWalker, pattern_component: *Component) []const u32 { + return this.pattern_codepoints[pattern_component.start_cp..pattern_component.end_cp]; + } + + fn componentStringUnicodePosix(this: *GlobWalker, pattern_component: *Component) []const u32 { + if (pattern_component.unicode_set) return this.pattern_codepoints[pattern_component.start_cp..pattern_component.end_cp]; + + var codepoints = this.pattern_codepoints[pattern_component.start_cp..pattern_component.end_cp]; + GlobWalker.convertUtf8ToCodepoints( + codepoints, + this.pattern[pattern_component.start .. pattern_component.start + pattern_component.len], + ); + pattern_component.unicode_set = true; + return codepoints; + } + + fn prepareMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) ![]const u8 { + const name = try this.arena.allocator().dupe(u8, symlink_full_path); + return name; + } + + fn prepareMatchedPath(this: *GlobWalker, entry_name: []const u8, dir_name: [:0]const u8) ![]const u8 { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir_name[0..dir_name.len], + entry_name, + }; + const name = try this.join(subdir_parts); + return name; + } + + fn appendMatchedPath( + this: *GlobWalker, + entry_name: []const u8, + dir_name: [:0]const u8, + ) !void { + const subdir_parts: []const []const u8 = &[_][]const u8{ + dir_name[0..dir_name.len], + entry_name, + }; + const name = try this.join(subdir_parts); + try this.matchedPaths.append(this.arena.allocator(), BunString.fromBytes(name)); + } + + fn appendMatchedPathSymlink(this: *GlobWalker, symlink_full_path: []const u8) !void { + const name = try this.arena.allocator().dupe(u8, symlink_full_path); + try this.matchedPaths.append(this.arena.allocator(), BunString.fromBytes(name)); + } + + inline fn join(this: *GlobWalker, subdir_parts: []const []const u8) ![]u8 { + return if (!this.absolute) + // If relative paths enabled, stdlib join is preferred over + // ResolvePath.joinBuf because it doesn't try to normalize the path + try std.fs.path.join(this.arena.allocator(), subdir_parts) + else + try this.arena.allocator().dupe(u8, ResolvePath.join(subdir_parts, .auto)); + } + + inline fn startsWithDot(filepath: []const u8) bool { + if (comptime !isWindows) { + return filepath[0] == '.'; + } else { + return filepath[1] == '.'; + } + } + + fn hasLeadingDot(filepath: []const u8, comptime allow_non_utf8: bool) bool { + if (comptime bun.Environment.isWindows and allow_non_utf8) { + // utf-16 + if (filepath.len >= 4 and filepath[1] == '.' and filepath[3] == '/') + return true; + } else { + if (filepath.len >= 2 and filepath[0] == '.' and filepath[1] == '/') + return true; + } + + return false; + } + + /// NOTE This doesn't check that there is leading dot, use `hasLeadingDot()` to do that + fn removeLeadingDot(filepath: []const u8, comptime allow_non_utf8: bool) []const u8 { + if (comptime bun.Environment.allow_assert) std.debug.assert(hasLeadingDot(filepath, allow_non_utf8)); + if (comptime bun.Environment.isWindows and allow_non_utf8) { + return filepath[4..]; + } else { + return filepath[2..]; + } + } + + fn checkSpecialSyntax(pattern: []const u8) bool { + if (pattern.len < 16) { + for (pattern[0..]) |c| { + switch (c) { + '*', '[', '{', '?', '!' => return true, + else => {}, + } + } + return false; + } + + const syntax_tokens = comptime [_]u8{ '*', '[', '{', '?', '!' }; + const needles: [syntax_tokens.len]@Vector(16, u8) = comptime needles: { + var needles: [syntax_tokens.len]@Vector(16, u8) = undefined; + inline for (syntax_tokens, 0..) |tok, i| { + needles[i] = @splat(tok); + } + break :needles needles; + }; + + var i: usize = 0; + while (i + 16 <= pattern.len) : (i += 16) { + const haystack: @Vector(16, u8) = pattern[i..][0..16].*; + inline for (needles) |needle| { + if (std.simd.firstTrue(needle == haystack) != null) return true; + } + } + + if (i < pattern.len) { + for (pattern[i..]) |c| { + inline for (syntax_tokens) |tok| { + if (c == tok) return true; + } + } + } + + return false; + } + + fn addComponent( + allocator: Allocator, + pattern: []const u8, + patternComponents: *ArrayList(Component), + start_cp: u32, + end_cp: u32, + start_byte: u32, + end_byte: u32, + has_relative_patterns: *bool, + ) !void { + var component: Component = .{ + .start = start_byte, + .len = end_byte - start_byte, + .start_cp = start_cp, + .end_cp = end_cp, + }; + if (component.len == 0) return; + + out: { + if (component.len == 1 and pattern[component.start] == '.') { + component.syntax_hint = .Dot; + has_relative_patterns.* = true; + break :out; + } + if (component.len == 2 and pattern[component.start] == '.' and pattern[component.start] == '.') { + component.syntax_hint = .DotBack; + has_relative_patterns.* = true; + break :out; + } + + if (!GlobWalker.checkSpecialSyntax(pattern[component.start .. component.start + component.len])) { + component.syntax_hint = .Literal; + break :out; + } + + switch (component.len) { + 1 => { + if (pattern[component.start] == '*') { + component.syntax_hint = .Single; + } + break :out; + }, + 2 => { + if (pattern[component.start] == '*' and pattern[component.start + 1] == '*') { + component.syntax_hint = .Double; + break :out; + } + }, + else => {}, + } + + out_of_check_wildcard_filepath: { + if (component.len > 1 and + pattern[component.start] == '*' and + pattern[component.start + 1] == '.' and + component.start + 2 < pattern.len) + { + for (pattern[component.start + 2 ..]) |c| { + switch (c) { + '[', '{', '!', '?' => break :out_of_check_wildcard_filepath, + else => {}, + } + } + component.syntax_hint = .WildcardFilepath; + break :out; + } + } + } + + if (component.syntax_hint != .Single and component.syntax_hint != .Double) { + if (isAllAscii(pattern[component.start .. component.start + component.len])) { + component.is_ascii = true; + } + } else { + component.is_ascii = true; + } + + try patternComponents.append(allocator, component); + } + + fn buildPatternComponents( + arena: *Arena, + patternComponents: *ArrayList(Component), + pattern: []const u8, + out_cp_len: *u32, + out_pattern_cp: *[]u32, + has_relative_patterns: *bool, + ) !void { + var start_cp: u32 = 0; + var start_byte: u32 = 0; + + const iter = CodepointIterator.init(pattern); + var cursor = CodepointIterator.Cursor{}; + + var cp_len: u32 = 0; + var prevIsBackslash = false; + while (iter.next(&cursor)) : (cp_len += 1) { + const c = cursor.c; + + switch (c) { + '\\' => { + if (comptime isWindows) { + const end_cp = cp_len; + try addComponent( + arena.allocator(), + pattern, + patternComponents, + start_cp, + end_cp, + start_byte, + cursor.i, + has_relative_patterns, + ); + start_cp = cp_len + 1; + start_byte = cursor.i + cursor.width; + continue; + } + + if (prevIsBackslash) { + prevIsBackslash = false; + continue; + } + + prevIsBackslash = true; + }, + '/' => { + var end_cp = cp_len; + var end_byte = cursor.i; + // is last char + if (cursor.i + cursor.width == pattern.len) { + end_cp += 1; + end_byte += cursor.width; + } + try addComponent( + arena.allocator(), + pattern, + patternComponents, + start_cp, + end_cp, + start_byte, + end_byte, + has_relative_patterns, + ); + start_cp = cp_len + 1; + start_byte = cursor.i + cursor.width; + }, + // TODO: Support other escaping glob syntax + else => {}, + } + } + + out_cp_len.* = cp_len; + + var codepoints = try arena.allocator().alloc(u32, cp_len); + // On Windows filepaths are UTF-16 so its better to fill the codepoints buffer upfront + if (comptime isWindows) { + GlobWalker.convertUtf8ToCodepoints(codepoints, pattern); + } + out_pattern_cp.* = codepoints; + + const end_cp = cp_len; + try addComponent( + arena.allocator(), + pattern, + patternComponents, + start_cp, + end_cp, + start_byte, + @intCast(pattern.len), + has_relative_patterns, + ); + } + }; +} + +// From: https://github.com/The-King-of-Toasters/globlin +/// State for matching a glob against a string +pub const GlobState = struct { + // These store character indices into the glob and path strings. + path_index: CursorState = .{}, + glob_index: u32 = 0, + // When we hit a * or **, we store the state for backtracking. + wildcard: Wildcard = .{}, + globstar: Wildcard = .{}, + + fn init(path_iter: *const CodepointIterator) GlobState { + var this = GlobState{}; + // this.glob_index = CursorState.init(glob_iter); + this.path_index = CursorState.init(path_iter); + return this; + } + + fn skipBraces(self: *GlobState, glob: []const u32, stop_on_comma: bool) BraceState { + var braces: u32 = 1; + var in_brackets = false; + while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { + switch (glob[self.glob_index]) { + // Skip nested braces + '{' => if (!in_brackets) { + braces += 1; + }, + '}' => if (!in_brackets) { + braces -= 1; + }, + ',' => if (stop_on_comma and braces == 1 and !in_brackets) { + self.glob_index += 1; + return .Comma; + }, + '*', '?', '[' => |c| if (!in_brackets) { + if (c == '[') + in_brackets = true; + }, + ']' => in_brackets = false, + '\\' => self.glob_index += 1, + else => {}, + } + } + + if (braces != 0) + return .Invalid; + return .EndBrace; + } + + inline fn backtrack(self: *GlobState) void { + self.glob_index = self.wildcard.glob_index; + self.path_index = self.wildcard.path_index; + } +}; + +const Wildcard = struct { + // Using u32 rather than usize for these results in 10% faster performance. + // glob_index: CursorState = .{}, + glob_index: u32 = 0, + path_index: CursorState = .{}, +}; + +const BraceState = enum { Invalid, Comma, EndBrace }; + +const BraceStack = struct { + stack: [10]GlobState = undefined, + len: u32 = 0, + longest_brace_match: CursorState = .{}, + + inline fn push(self: *BraceStack, state: *const GlobState) GlobState { + self.stack[self.len] = state.*; + self.len += 1; + return GlobState{ + .path_index = state.path_index, + .glob_index = state.glob_index + 1, + }; + } + + inline fn pop(self: *BraceStack, state: *const GlobState) GlobState { + self.len -= 1; + const s = GlobState{ + .glob_index = state.glob_index, + .path_index = self.longest_brace_match, + // Restore star state if needed later. + .wildcard = self.stack[self.len].wildcard, + .globstar = self.stack[self.len].globstar, + }; + if (self.len == 0) + self.longest_brace_match = .{}; + return s; + } + + inline fn last(self: *const BraceStack) *const GlobState { + return &self.stack[self.len - 1]; + } +}; + +/// This function checks returns a boolean value if the pathname `path` matches +/// the pattern `glob`. +/// +/// The supported pattern syntax for `glob` is: +/// +/// "?" +/// Matches any single character. +/// "*" +/// Matches zero or more characters, except for path separators ('/' or '\'). +/// "**" +/// Matches zero or more characters, including path separators. +/// Must match a complete path segment, i.e. followed by a path separator or +/// at the end of the pattern. +/// "[ab]" +/// Matches one of the characters contained in the brackets. +/// Character ranges (e.g. "[a-z]") are also supported. +/// Use "[!ab]" or "[^ab]" to match any character *except* those contained +/// in the brackets. +/// "{a,b}" +/// Match one of the patterns contained in the braces. +/// Any of the wildcards listed above can be used in the sub patterns. +/// Braces may be nested up to 10 levels deep. +/// "!" +/// Negates the result when at the start of the pattern. +/// Multiple "!" characters negate the pattern multiple times. +/// "\" +/// Used to escape any of the special characters above. +pub fn matchImpl(glob: []const u32, path: []const u8) bool { + const path_iter = CodepointIterator.init(path); + + // This algorithm is based on https://research.swtch.com/glob + var state = GlobState.init(&path_iter); + // Store the state when we see an opening '{' brace in a stack. + // Up to 10 nested braces are supported. + var brace_stack = BraceStack{}; + + // First, check if the pattern is negated with a leading '!' character. + // Multiple negations can occur. + var negated = false; + while (state.glob_index < glob.len and glob[state.glob_index] == '!') { + negated = !negated; + state.glob_index += 1; + } + + while (state.glob_index < glob.len or state.path_index.cursor.i < path.len) { + if (state.glob_index < glob.len) { + switch (glob[state.glob_index]) { + '*' => { + const is_globstar = state.glob_index + 1 < glob.len and glob[state.glob_index + 1] == '*'; + // const is_globstar = state.glob_index.cursor.i + state.glob_index.cursor.width < glob.len and + // state.glob_index.peek(&glob_iter).cursor.c == '*'; + if (is_globstar) { + // Coalesce multiple ** segments into one. + var index = state.glob_index + 2; + state.glob_index = skipGlobstars(glob, &index) - 2; + } + + state.wildcard.glob_index = state.glob_index; + state.wildcard.path_index = state.path_index.peek(&path_iter); + + // ** allows path separators, whereas * does not. + // However, ** must be a full path component, i.e. a/**/b not a**b. + if (is_globstar) { + // Skip wildcards + state.glob_index += 2; + + if (glob.len == state.glob_index) { + // A trailing ** segment without a following separator. + state.globstar = state.wildcard; + } else if (glob[state.glob_index] == '/' and + (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) + { + // Matched a full /**/ segment. If the last character in the path was a separator, + // skip the separator in the glob so we search for the next character. + // In effect, this makes the whole segment optional so that a/**/b matches a/b. + if (state.path_index.cursor.i == 0 or + (state.path_index.cursor.i < path.len and + isSeparator(path[state.path_index.cursor.i - 1]))) + { + state.glob_index += 1; + } + + // The allows_sep flag allows separator characters in ** matches. + // one is a '/', which prevents a/**/b from matching a/bb. + state.globstar = state.wildcard; + } + } else { + state.glob_index += 1; + } + + // If we are in a * segment and hit a separator, + // either jump back to a previous ** or end the wildcard. + if (state.globstar.path_index.cursor.i != state.wildcard.path_index.cursor.i and + state.path_index.cursor.i < path.len and + isSeparator(state.path_index.cursor.c)) + { + // Special case: don't jump back for a / at the end of the glob. + if (state.globstar.path_index.cursor.i > 0 and state.path_index.cursor.i + state.path_index.cursor.width < path.len) { + state.glob_index = state.globstar.glob_index; + state.wildcard.glob_index = state.globstar.glob_index; + } else { + state.wildcard.path_index.cursor.i = 0; + } + } + + // If the next char is a special brace separator, + // skip to the end of the braces so we don't try to match it. + if (brace_stack.len > 0 and + state.glob_index < glob.len and + (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) + { + if (state.skipBraces(glob, false) == .Invalid) + return false; // invalid pattern! + } + + continue; + }, + '?' => if (state.path_index.cursor.i < path.len) { + if (!isSeparator(state.path_index.cursor.c)) { + state.glob_index += 1; + state.path_index.bump(&path_iter); + continue; + } + }, + '[' => if (state.path_index.cursor.i < path.len) { + state.glob_index += 1; + const c = state.path_index.cursor.c; + + // Check if the character class is negated. + var class_negated = false; + if (state.glob_index < glob.len and + (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) + { + class_negated = true; + state.glob_index += 1; + } + + // Try each range. + var first = true; + var is_match = false; + while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { + var low = glob[state.glob_index]; + if (!unescape(&low, glob, &state.glob_index)) + return false; // Invalid pattern + state.glob_index += 1; + + // If there is a - and the following character is not ], + // read the range end character. + const high = if (state.glob_index + 1 < glob.len and + glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') + blk: { + state.glob_index += 1; + var h = glob[state.glob_index]; + if (!unescape(&h, glob, &state.glob_index)) + return false; // Invalid pattern! + state.glob_index += 1; + break :blk h; + } else low; + + if (low <= c and c <= high) + is_match = true; + first = false; + } + if (state.glob_index >= glob.len) + return false; // Invalid pattern! + state.glob_index += 1; + if (is_match != class_negated) { + state.path_index.bump(&path_iter); + continue; + } + }, + '{' => if (state.path_index.cursor.i < path.len) { + if (brace_stack.len >= brace_stack.stack.len) + return false; // Invalid pattern! Too many nested braces. + + // Push old state to the stack, and reset current state. + state = brace_stack.push(&state); + continue; + }, + '}' => if (brace_stack.len > 0) { + // If we hit the end of the braces, we matched the last option. + brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) + state.path_index + else + brace_stack.longest_brace_match; + state.glob_index += 1; + state = brace_stack.pop(&state); + continue; + }, + ',' => if (brace_stack.len > 0) { + // If we hit a comma, we matched one of the options! + // But we still need to check the others in case there is a longer match. + brace_stack.longest_brace_match = if (state.path_index.cursor.i >= brace_stack.longest_brace_match.cursor.i) + state.path_index + else + brace_stack.longest_brace_match; + state.path_index = brace_stack.last().path_index; + state.glob_index += 1; + state.wildcard = Wildcard{}; + state.globstar = Wildcard{}; + continue; + }, + else => |c| if (state.path_index.cursor.i < path.len) { + var cc = c; + // Match escaped characters as literals. + if (!unescape(&cc, glob, &state.glob_index)) + return false; // Invalid pattern; + + const is_match = if (cc == '/') + isSeparator(state.path_index.cursor.c) + else + state.path_index.cursor.c == cc; + + if (is_match) { + if (brace_stack.len > 0 and + state.glob_index > 0 and + glob[state.glob_index - 1] == '}') + { + brace_stack.longest_brace_match = state.path_index; + state = brace_stack.pop(&state); + } + state.glob_index += 1; + state.path_index.bump(&path_iter); + + // If this is not a separator, lock in the previous globstar. + if (cc != '/') + state.globstar.path_index.cursor.i = 0; + + continue; + } + }, + } + } + // If we didn't match, restore state to the previous star pattern. + if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { + state.backtrack(); + continue; + } + + if (brace_stack.len > 0) { + // If in braces, find next option and reset path to index where we saw the '{' + switch (state.skipBraces(glob, true)) { + .Invalid => return false, + .Comma => { + state.path_index = brace_stack.last().path_index; + continue; + }, + .EndBrace => {}, + } + + // Hit the end. Pop the stack. + // If we matched a previous option, use that. + if (brace_stack.longest_brace_match.cursor.i > 0) { + state = brace_stack.pop(&state); + continue; + } else { + // Didn't match. Restore state, and check if we need to jump back to a star pattern. + state = brace_stack.last().*; + brace_stack.len -= 1; + if (state.wildcard.path_index.cursor.i > 0 and state.wildcard.path_index.cursor.i <= path.len) { + state.backtrack(); + continue; + } + } + } + + return negated; + } + + return !negated; +} + +pub inline fn isSeparator(c: Codepoint) bool { + if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; + return c == '/'; +} + +inline fn unescape(c: *u32, glob: []const u32, glob_index: *u32) bool { + if (c.* == '\\') { + glob_index.* += 1; + if (glob_index.* >= glob.len) + return false; // Invalid pattern! + + c.* = switch (glob[glob_index.*]) { + 'a' => '\x61', + 'b' => '\x08', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + else => |cc| cc, + }; + } + + return true; +} + +const GLOB_STAR_MATCH_STR: []const u32 = &[_]u32{ '/', '*', '*' }; +// src/**/**/foo.ts +inline fn skipGlobstars(glob: []const u32, glob_index: *u32) u32 { + // Coalesce multiple ** segments into one. + while (glob_index.* + 3 <= glob.len and + // std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) + std.mem.eql(u32, glob[glob_index.*..][0..3], GLOB_STAR_MATCH_STR)) + { + glob_index.* += 3; + } + + return glob_index.*; +} + +const MatchAscii = struct {}; + +pub fn matchWildcardFilepath(glob: []const u8, path: []const u8) bool { + const needle = glob[1..]; + const needle_len: u32 = @intCast(needle.len); + if (path.len < needle_len) return false; + return std.mem.eql(u8, needle, path[path.len - needle_len ..]); +} + +pub fn matchWildcardLiteral(literal: []const u8, path: []const u8) bool { + return std.mem.eql(u8, literal, path); +} + +// test "basic" { +// try expect(match("abc", "abc")); +// try expect(match("*", "abc")); +// try expect(match("*", "")); +// try expect(match("**", "")); +// try expect(match("*c", "abc")); +// try expect(!match("*b", "abc")); +// try expect(match("a*", "abc")); +// try expect(!match("b*", "abc")); +// try expect(match("a*", "a")); +// try expect(match("*a", "a")); +// try expect(match("a*b*c*d*e*", "axbxcxdxe")); +// try expect(match("a*b*c*d*e*", "axbxcxdxexxx")); +// try expect(match("a*b?c*x", "abxbbxdbxebxczzx")); +// try expect(!match("a*b?c*x", "abxbbxdbxebxczzy")); + +// try expect(match("a/*/test", "a/foo/test")); +// try expect(!match("a/*/test", "a/foo/bar/test")); +// try expect(match("a/**/test", "a/foo/test")); +// try expect(match("a/**/test", "a/foo/bar/test")); +// try expect(match("a/**/b/c", "a/foo/bar/b/c")); +// try expect(match("a\\*b", "a*b")); +// try expect(!match("a\\*b", "axb")); + +// try expect(match("[abc]", "a")); +// try expect(match("[abc]", "b")); +// try expect(match("[abc]", "c")); +// try expect(!match("[abc]", "d")); +// try expect(match("x[abc]x", "xax")); +// try expect(match("x[abc]x", "xbx")); +// try expect(match("x[abc]x", "xcx")); +// try expect(!match("x[abc]x", "xdx")); +// try expect(!match("x[abc]x", "xay")); +// try expect(match("[?]", "?")); +// try expect(!match("[?]", "a")); +// try expect(match("[*]", "*")); +// try expect(!match("[*]", "a")); + +// try expect(match("[a-cx]", "a")); +// try expect(match("[a-cx]", "b")); +// try expect(match("[a-cx]", "c")); +// try expect(!match("[a-cx]", "d")); +// try expect(match("[a-cx]", "x")); + +// try expect(!match("[^abc]", "a")); +// try expect(!match("[^abc]", "b")); +// try expect(!match("[^abc]", "c")); +// try expect(match("[^abc]", "d")); +// try expect(!match("[!abc]", "a")); +// try expect(!match("[!abc]", "b")); +// try expect(!match("[!abc]", "c")); +// try expect(match("[!abc]", "d")); +// try expect(match("[\\!]", "!")); + +// try expect(match("a*b*[cy]*d*e*", "axbxcxdxexxx")); +// try expect(match("a*b*[cy]*d*e*", "axbxyxdxexxx")); +// try expect(match("a*b*[cy]*d*e*", "axbxxxyxdxexxx")); + +// try expect(match("test.{jpg,png}", "test.jpg")); +// try expect(match("test.{jpg,png}", "test.png")); +// try expect(match("test.{j*g,p*g}", "test.jpg")); +// try expect(match("test.{j*g,p*g}", "test.jpxxxg")); +// try expect(match("test.{j*g,p*g}", "test.jxg")); +// try expect(!match("test.{j*g,p*g}", "test.jnt")); +// try expect(match("test.{j*g,j*c}", "test.jnc")); +// try expect(match("test.{jpg,p*g}", "test.png")); +// try expect(match("test.{jpg,p*g}", "test.pxg")); +// try expect(!match("test.{jpg,p*g}", "test.pnt")); +// try expect(match("test.{jpeg,png}", "test.jpeg")); +// try expect(!match("test.{jpeg,png}", "test.jpg")); +// try expect(match("test.{jpeg,png}", "test.png")); +// try expect(match("test.{jp\\,g,png}", "test.jp,g")); +// try expect(!match("test.{jp\\,g,png}", "test.jxg")); +// try expect(match("test/{foo,bar}/baz", "test/foo/baz")); +// try expect(match("test/{foo,bar}/baz", "test/bar/baz")); +// try expect(!match("test/{foo,bar}/baz", "test/baz/baz")); +// try expect(match("test/{foo*,bar*}/baz", "test/foooooo/baz")); +// try expect(match("test/{foo*,bar*}/baz", "test/barrrrr/baz")); +// try expect(match("test/{*foo,*bar}/baz", "test/xxxxfoo/baz")); +// try expect(match("test/{*foo,*bar}/baz", "test/xxxxbar/baz")); +// try expect(match("test/{foo/**,bar}/baz", "test/bar/baz")); +// try expect(!match("test/{foo/**,bar}/baz", "test/bar/test/baz")); + +// try expect(!match("*.txt", "some/big/path/to/the/needle.txt")); +// try expect(match( +// "some/**/needle.{js,tsx,mdx,ts,jsx,txt}", +// "some/a/bigger/path/to/the/crazy/needle.txt", +// )); +// try expect(match( +// "some/**/{a,b,c}/**/needle.txt", +// "some/foo/a/bigger/path/to/the/crazy/needle.txt", +// )); +// try expect(!match( +// "some/**/{a,b,c}/**/needle.txt", +// "some/foo/d/bigger/path/to/the/crazy/needle.txt", +// )); +// try expect(match("a/{a{a,b},b}", "a/aa")); +// try expect(match("a/{a{a,b},b}", "a/ab")); +// try expect(!match("a/{a{a,b},b}", "a/ac")); +// try expect(match("a/{a{a,b},b}", "a/b")); +// try expect(!match("a/{a{a,b},b}", "a/c")); +// try expect(match("a/{b,c[}]*}", "a/b")); +// try expect(match("a/{b,c[}]*}", "a/c}xx")); +// } + +// // The below tests are based on Bash and micromatch. +// // https://github.com/micromatch/picomatch/blob/master/test/bash.js +// test "bash" { +// try expect(!match("a*", "*")); +// try expect(!match("a*", "**")); +// try expect(!match("a*", "\\*")); +// try expect(!match("a*", "a/*")); +// try expect(!match("a*", "b")); +// try expect(!match("a*", "bc")); +// try expect(!match("a*", "bcd")); +// try expect(!match("a*", "bdir/")); +// try expect(!match("a*", "Beware")); +// try expect(match("a*", "a")); +// try expect(match("a*", "ab")); +// try expect(match("a*", "abc")); + +// try expect(!match("\\a*", "*")); +// try expect(!match("\\a*", "**")); +// try expect(!match("\\a*", "\\*")); + +// try expect(match("\\a*", "a")); +// try expect(!match("\\a*", "a/*")); +// try expect(match("\\a*", "abc")); +// try expect(match("\\a*", "abd")); +// try expect(match("\\a*", "abe")); +// try expect(!match("\\a*", "b")); +// try expect(!match("\\a*", "bb")); +// try expect(!match("\\a*", "bcd")); +// try expect(!match("\\a*", "bdir/")); +// try expect(!match("\\a*", "Beware")); +// try expect(!match("\\a*", "c")); +// try expect(!match("\\a*", "ca")); +// try expect(!match("\\a*", "cb")); +// try expect(!match("\\a*", "d")); +// try expect(!match("\\a*", "dd")); +// try expect(!match("\\a*", "de")); +// } + +// test "bash directories" { +// try expect(!match("b*/", "*")); +// try expect(!match("b*/", "**")); +// try expect(!match("b*/", "\\*")); +// try expect(!match("b*/", "a")); +// try expect(!match("b*/", "a/*")); +// try expect(!match("b*/", "abc")); +// try expect(!match("b*/", "abd")); +// try expect(!match("b*/", "abe")); +// try expect(!match("b*/", "b")); +// try expect(!match("b*/", "bb")); +// try expect(!match("b*/", "bcd")); +// try expect(match("b*/", "bdir/")); +// try expect(!match("b*/", "Beware")); +// try expect(!match("b*/", "c")); +// try expect(!match("b*/", "ca")); +// try expect(!match("b*/", "cb")); +// try expect(!match("b*/", "d")); +// try expect(!match("b*/", "dd")); +// try expect(!match("b*/", "de")); +// } + +// test "bash escaping" { +// try expect(!match("\\^", "*")); +// try expect(!match("\\^", "**")); +// try expect(!match("\\^", "\\*")); +// try expect(!match("\\^", "a")); +// try expect(!match("\\^", "a/*")); +// try expect(!match("\\^", "abc")); +// try expect(!match("\\^", "abd")); +// try expect(!match("\\^", "abe")); +// try expect(!match("\\^", "b")); +// try expect(!match("\\^", "bb")); +// try expect(!match("\\^", "bcd")); +// try expect(!match("\\^", "bdir/")); +// try expect(!match("\\^", "Beware")); +// try expect(!match("\\^", "c")); +// try expect(!match("\\^", "ca")); +// try expect(!match("\\^", "cb")); +// try expect(!match("\\^", "d")); +// try expect(!match("\\^", "dd")); +// try expect(!match("\\^", "de")); + +// try expect(match("\\*", "*")); +// // try expect(match("\\*", "\\*")); +// try expect(!match("\\*", "**")); +// try expect(!match("\\*", "a")); +// try expect(!match("\\*", "a/*")); +// try expect(!match("\\*", "abc")); +// try expect(!match("\\*", "abd")); +// try expect(!match("\\*", "abe")); +// try expect(!match("\\*", "b")); +// try expect(!match("\\*", "bb")); +// try expect(!match("\\*", "bcd")); +// try expect(!match("\\*", "bdir/")); +// try expect(!match("\\*", "Beware")); +// try expect(!match("\\*", "c")); +// try expect(!match("\\*", "ca")); +// try expect(!match("\\*", "cb")); +// try expect(!match("\\*", "d")); +// try expect(!match("\\*", "dd")); +// try expect(!match("\\*", "de")); + +// try expect(!match("a\\*", "*")); +// try expect(!match("a\\*", "**")); +// try expect(!match("a\\*", "\\*")); +// try expect(!match("a\\*", "a")); +// try expect(!match("a\\*", "a/*")); +// try expect(!match("a\\*", "abc")); +// try expect(!match("a\\*", "abd")); +// try expect(!match("a\\*", "abe")); +// try expect(!match("a\\*", "b")); +// try expect(!match("a\\*", "bb")); +// try expect(!match("a\\*", "bcd")); +// try expect(!match("a\\*", "bdir/")); +// try expect(!match("a\\*", "Beware")); +// try expect(!match("a\\*", "c")); +// try expect(!match("a\\*", "ca")); +// try expect(!match("a\\*", "cb")); +// try expect(!match("a\\*", "d")); +// try expect(!match("a\\*", "dd")); +// try expect(!match("a\\*", "de")); + +// try expect(match("*q*", "aqa")); +// try expect(match("*q*", "aaqaa")); +// try expect(!match("*q*", "*")); +// try expect(!match("*q*", "**")); +// try expect(!match("*q*", "\\*")); +// try expect(!match("*q*", "a")); +// try expect(!match("*q*", "a/*")); +// try expect(!match("*q*", "abc")); +// try expect(!match("*q*", "abd")); +// try expect(!match("*q*", "abe")); +// try expect(!match("*q*", "b")); +// try expect(!match("*q*", "bb")); +// try expect(!match("*q*", "bcd")); +// try expect(!match("*q*", "bdir/")); +// try expect(!match("*q*", "Beware")); +// try expect(!match("*q*", "c")); +// try expect(!match("*q*", "ca")); +// try expect(!match("*q*", "cb")); +// try expect(!match("*q*", "d")); +// try expect(!match("*q*", "dd")); +// try expect(!match("*q*", "de")); + +// try expect(match("\\**", "*")); +// try expect(match("\\**", "**")); +// try expect(!match("\\**", "\\*")); +// try expect(!match("\\**", "a")); +// try expect(!match("\\**", "a/*")); +// try expect(!match("\\**", "abc")); +// try expect(!match("\\**", "abd")); +// try expect(!match("\\**", "abe")); +// try expect(!match("\\**", "b")); +// try expect(!match("\\**", "bb")); +// try expect(!match("\\**", "bcd")); +// try expect(!match("\\**", "bdir/")); +// try expect(!match("\\**", "Beware")); +// try expect(!match("\\**", "c")); +// try expect(!match("\\**", "ca")); +// try expect(!match("\\**", "cb")); +// try expect(!match("\\**", "d")); +// try expect(!match("\\**", "dd")); +// try expect(!match("\\**", "de")); +// } + +// test "bash classes" { +// try expect(!match("a*[^c]", "*")); +// try expect(!match("a*[^c]", "**")); +// try expect(!match("a*[^c]", "\\*")); +// try expect(!match("a*[^c]", "a")); +// try expect(!match("a*[^c]", "a/*")); +// try expect(!match("a*[^c]", "abc")); +// try expect(match("a*[^c]", "abd")); +// try expect(match("a*[^c]", "abe")); +// try expect(!match("a*[^c]", "b")); +// try expect(!match("a*[^c]", "bb")); +// try expect(!match("a*[^c]", "bcd")); +// try expect(!match("a*[^c]", "bdir/")); +// try expect(!match("a*[^c]", "Beware")); +// try expect(!match("a*[^c]", "c")); +// try expect(!match("a*[^c]", "ca")); +// try expect(!match("a*[^c]", "cb")); +// try expect(!match("a*[^c]", "d")); +// try expect(!match("a*[^c]", "dd")); +// try expect(!match("a*[^c]", "de")); +// try expect(!match("a*[^c]", "baz")); +// try expect(!match("a*[^c]", "bzz")); +// try expect(!match("a*[^c]", "BZZ")); +// try expect(!match("a*[^c]", "beware")); +// try expect(!match("a*[^c]", "BewAre")); + +// try expect(match("a[X-]b", "a-b")); +// try expect(match("a[X-]b", "aXb")); + +// try expect(!match("[a-y]*[^c]", "*")); +// try expect(match("[a-y]*[^c]", "a*")); +// try expect(!match("[a-y]*[^c]", "**")); +// try expect(!match("[a-y]*[^c]", "\\*")); +// try expect(!match("[a-y]*[^c]", "a")); +// try expect(match("[a-y]*[^c]", "a123b")); +// try expect(!match("[a-y]*[^c]", "a123c")); +// try expect(match("[a-y]*[^c]", "ab")); +// try expect(!match("[a-y]*[^c]", "a/*")); +// try expect(!match("[a-y]*[^c]", "abc")); +// try expect(match("[a-y]*[^c]", "abd")); +// try expect(match("[a-y]*[^c]", "abe")); +// try expect(!match("[a-y]*[^c]", "b")); +// try expect(match("[a-y]*[^c]", "bd")); +// try expect(match("[a-y]*[^c]", "bb")); +// try expect(match("[a-y]*[^c]", "bcd")); +// try expect(match("[a-y]*[^c]", "bdir/")); +// try expect(!match("[a-y]*[^c]", "Beware")); +// try expect(!match("[a-y]*[^c]", "c")); +// try expect(match("[a-y]*[^c]", "ca")); +// try expect(match("[a-y]*[^c]", "cb")); +// try expect(!match("[a-y]*[^c]", "d")); +// try expect(match("[a-y]*[^c]", "dd")); +// try expect(match("[a-y]*[^c]", "dd")); +// try expect(match("[a-y]*[^c]", "dd")); +// try expect(match("[a-y]*[^c]", "de")); +// try expect(match("[a-y]*[^c]", "baz")); +// try expect(match("[a-y]*[^c]", "bzz")); +// try expect(match("[a-y]*[^c]", "bzz")); +// // assert(!isMatch('bzz', '[a-y]*[^c]', { regex: true })); +// try expect(!match("[a-y]*[^c]", "BZZ")); +// try expect(match("[a-y]*[^c]", "beware")); +// try expect(!match("[a-y]*[^c]", "BewAre")); + +// try expect(match("a\\*b/*", "a*b/ooo")); +// try expect(match("a\\*?/*", "a*b/ooo")); + +// try expect(!match("a[b]c", "*")); +// try expect(!match("a[b]c", "**")); +// try expect(!match("a[b]c", "\\*")); +// try expect(!match("a[b]c", "a")); +// try expect(!match("a[b]c", "a/*")); +// try expect(match("a[b]c", "abc")); +// try expect(!match("a[b]c", "abd")); +// try expect(!match("a[b]c", "abe")); +// try expect(!match("a[b]c", "b")); +// try expect(!match("a[b]c", "bb")); +// try expect(!match("a[b]c", "bcd")); +// try expect(!match("a[b]c", "bdir/")); +// try expect(!match("a[b]c", "Beware")); +// try expect(!match("a[b]c", "c")); +// try expect(!match("a[b]c", "ca")); +// try expect(!match("a[b]c", "cb")); +// try expect(!match("a[b]c", "d")); +// try expect(!match("a[b]c", "dd")); +// try expect(!match("a[b]c", "de")); +// try expect(!match("a[b]c", "baz")); +// try expect(!match("a[b]c", "bzz")); +// try expect(!match("a[b]c", "BZZ")); +// try expect(!match("a[b]c", "beware")); +// try expect(!match("a[b]c", "BewAre")); + +// try expect(!match("a[\"b\"]c", "*")); +// try expect(!match("a[\"b\"]c", "**")); +// try expect(!match("a[\"b\"]c", "\\*")); +// try expect(!match("a[\"b\"]c", "a")); +// try expect(!match("a[\"b\"]c", "a/*")); +// try expect(match("a[\"b\"]c", "abc")); +// try expect(!match("a[\"b\"]c", "abd")); +// try expect(!match("a[\"b\"]c", "abe")); +// try expect(!match("a[\"b\"]c", "b")); +// try expect(!match("a[\"b\"]c", "bb")); +// try expect(!match("a[\"b\"]c", "bcd")); +// try expect(!match("a[\"b\"]c", "bdir/")); +// try expect(!match("a[\"b\"]c", "Beware")); +// try expect(!match("a[\"b\"]c", "c")); +// try expect(!match("a[\"b\"]c", "ca")); +// try expect(!match("a[\"b\"]c", "cb")); +// try expect(!match("a[\"b\"]c", "d")); +// try expect(!match("a[\"b\"]c", "dd")); +// try expect(!match("a[\"b\"]c", "de")); +// try expect(!match("a[\"b\"]c", "baz")); +// try expect(!match("a[\"b\"]c", "bzz")); +// try expect(!match("a[\"b\"]c", "BZZ")); +// try expect(!match("a[\"b\"]c", "beware")); +// try expect(!match("a[\"b\"]c", "BewAre")); + +// try expect(!match("a[\\\\b]c", "*")); +// try expect(!match("a[\\\\b]c", "**")); +// try expect(!match("a[\\\\b]c", "\\*")); +// try expect(!match("a[\\\\b]c", "a")); +// try expect(!match("a[\\\\b]c", "a/*")); +// try expect(match("a[\\\\b]c", "abc")); +// try expect(!match("a[\\\\b]c", "abd")); +// try expect(!match("a[\\\\b]c", "abe")); +// try expect(!match("a[\\\\b]c", "b")); +// try expect(!match("a[\\\\b]c", "bb")); +// try expect(!match("a[\\\\b]c", "bcd")); +// try expect(!match("a[\\\\b]c", "bdir/")); +// try expect(!match("a[\\\\b]c", "Beware")); +// try expect(!match("a[\\\\b]c", "c")); +// try expect(!match("a[\\\\b]c", "ca")); +// try expect(!match("a[\\\\b]c", "cb")); +// try expect(!match("a[\\\\b]c", "d")); +// try expect(!match("a[\\\\b]c", "dd")); +// try expect(!match("a[\\\\b]c", "de")); +// try expect(!match("a[\\\\b]c", "baz")); +// try expect(!match("a[\\\\b]c", "bzz")); +// try expect(!match("a[\\\\b]c", "BZZ")); +// try expect(!match("a[\\\\b]c", "beware")); +// try expect(!match("a[\\\\b]c", "BewAre")); + +// try expect(!match("a[\\b]c", "*")); +// try expect(!match("a[\\b]c", "**")); +// try expect(!match("a[\\b]c", "\\*")); +// try expect(!match("a[\\b]c", "a")); +// try expect(!match("a[\\b]c", "a/*")); +// try expect(!match("a[\\b]c", "abc")); +// try expect(!match("a[\\b]c", "abd")); +// try expect(!match("a[\\b]c", "abe")); +// try expect(!match("a[\\b]c", "b")); +// try expect(!match("a[\\b]c", "bb")); +// try expect(!match("a[\\b]c", "bcd")); +// try expect(!match("a[\\b]c", "bdir/")); +// try expect(!match("a[\\b]c", "Beware")); +// try expect(!match("a[\\b]c", "c")); +// try expect(!match("a[\\b]c", "ca")); +// try expect(!match("a[\\b]c", "cb")); +// try expect(!match("a[\\b]c", "d")); +// try expect(!match("a[\\b]c", "dd")); +// try expect(!match("a[\\b]c", "de")); +// try expect(!match("a[\\b]c", "baz")); +// try expect(!match("a[\\b]c", "bzz")); +// try expect(!match("a[\\b]c", "BZZ")); +// try expect(!match("a[\\b]c", "beware")); +// try expect(!match("a[\\b]c", "BewAre")); + +// try expect(!match("a[b-d]c", "*")); +// try expect(!match("a[b-d]c", "**")); +// try expect(!match("a[b-d]c", "\\*")); +// try expect(!match("a[b-d]c", "a")); +// try expect(!match("a[b-d]c", "a/*")); +// try expect(match("a[b-d]c", "abc")); +// try expect(!match("a[b-d]c", "abd")); +// try expect(!match("a[b-d]c", "abe")); +// try expect(!match("a[b-d]c", "b")); +// try expect(!match("a[b-d]c", "bb")); +// try expect(!match("a[b-d]c", "bcd")); +// try expect(!match("a[b-d]c", "bdir/")); +// try expect(!match("a[b-d]c", "Beware")); +// try expect(!match("a[b-d]c", "c")); +// try expect(!match("a[b-d]c", "ca")); +// try expect(!match("a[b-d]c", "cb")); +// try expect(!match("a[b-d]c", "d")); +// try expect(!match("a[b-d]c", "dd")); +// try expect(!match("a[b-d]c", "de")); +// try expect(!match("a[b-d]c", "baz")); +// try expect(!match("a[b-d]c", "bzz")); +// try expect(!match("a[b-d]c", "BZZ")); +// try expect(!match("a[b-d]c", "beware")); +// try expect(!match("a[b-d]c", "BewAre")); + +// try expect(!match("a?c", "*")); +// try expect(!match("a?c", "**")); +// try expect(!match("a?c", "\\*")); +// try expect(!match("a?c", "a")); +// try expect(!match("a?c", "a/*")); +// try expect(match("a?c", "abc")); +// try expect(!match("a?c", "abd")); +// try expect(!match("a?c", "abe")); +// try expect(!match("a?c", "b")); +// try expect(!match("a?c", "bb")); +// try expect(!match("a?c", "bcd")); +// try expect(!match("a?c", "bdir/")); +// try expect(!match("a?c", "Beware")); +// try expect(!match("a?c", "c")); +// try expect(!match("a?c", "ca")); +// try expect(!match("a?c", "cb")); +// try expect(!match("a?c", "d")); +// try expect(!match("a?c", "dd")); +// try expect(!match("a?c", "de")); +// try expect(!match("a?c", "baz")); +// try expect(!match("a?c", "bzz")); +// try expect(!match("a?c", "BZZ")); +// try expect(!match("a?c", "beware")); +// try expect(!match("a?c", "BewAre")); + +// try expect(match("*/man*/bash.*", "man/man1/bash.1")); + +// try expect(match("[^a-c]*", "*")); +// try expect(match("[^a-c]*", "**")); +// try expect(!match("[^a-c]*", "a")); +// try expect(!match("[^a-c]*", "a/*")); +// try expect(!match("[^a-c]*", "abc")); +// try expect(!match("[^a-c]*", "abd")); +// try expect(!match("[^a-c]*", "abe")); +// try expect(!match("[^a-c]*", "b")); +// try expect(!match("[^a-c]*", "bb")); +// try expect(!match("[^a-c]*", "bcd")); +// try expect(!match("[^a-c]*", "bdir/")); +// try expect(match("[^a-c]*", "Beware")); +// try expect(match("[^a-c]*", "Beware")); +// try expect(!match("[^a-c]*", "c")); +// try expect(!match("[^a-c]*", "ca")); +// try expect(!match("[^a-c]*", "cb")); +// try expect(match("[^a-c]*", "d")); +// try expect(match("[^a-c]*", "dd")); +// try expect(match("[^a-c]*", "de")); +// try expect(!match("[^a-c]*", "baz")); +// try expect(!match("[^a-c]*", "bzz")); +// try expect(match("[^a-c]*", "BZZ")); +// try expect(!match("[^a-c]*", "beware")); +// try expect(match("[^a-c]*", "BewAre")); +// } + +// test "bash wildmatch" { +// try expect(!match("a[]-]b", "aab")); +// try expect(!match("[ten]", "ten")); +// try expect(match("]", "]")); +// try expect(match("a[]-]b", "a-b")); +// try expect(match("a[]-]b", "a]b")); +// try expect(match("a[]]b", "a]b")); +// try expect(match("a[\\]a\\-]b", "aab")); +// try expect(match("t[a-g]n", "ten")); +// try expect(match("t[^a-g]n", "ton")); +// } + +// test "bash slashmatch" { +// // try expect(!match("f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo/bar")); +// try expect(match("foo[/]bar", "foo/bar")); +// try expect(match("f[^eiu][^eiu][^eiu][^eiu][^eiu]r", "foo-bar")); +// } + +// test "bash extra_stars" { +// try expect(!match("a**c", "bbc")); +// try expect(match("a**c", "abc")); +// try expect(!match("a**c", "bbd")); + +// try expect(!match("a***c", "bbc")); +// try expect(match("a***c", "abc")); +// try expect(!match("a***c", "bbd")); + +// try expect(!match("a*****?c", "bbc")); +// try expect(match("a*****?c", "abc")); +// try expect(!match("a*****?c", "bbc")); + +// try expect(match("?*****??", "bbc")); +// try expect(match("?*****??", "abc")); + +// try expect(match("*****??", "bbc")); +// try expect(match("*****??", "abc")); + +// try expect(match("?*****?c", "bbc")); +// try expect(match("?*****?c", "abc")); + +// try expect(match("?***?****c", "bbc")); +// try expect(match("?***?****c", "abc")); +// try expect(!match("?***?****c", "bbd")); + +// try expect(match("?***?****?", "bbc")); +// try expect(match("?***?****?", "abc")); + +// try expect(match("?***?****", "bbc")); +// try expect(match("?***?****", "abc")); + +// try expect(match("*******c", "bbc")); +// try expect(match("*******c", "abc")); + +// try expect(match("*******?", "bbc")); +// try expect(match("*******?", "abc")); + +// try expect(match("a*cd**?**??k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??k***", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??***k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??***k**", "abcdecdhjk")); +// try expect(match("a****c**?**??*****", "abcdecdhjk")); +// } + +// test "stars" { +// try expect(!match("*.js", "a/b/c/z.js")); +// try expect(!match("*.js", "a/b/z.js")); +// try expect(!match("*.js", "a/z.js")); +// try expect(match("*.js", "z.js")); + +// // try expect(!match("*/*", "a/.ab")); +// // try expect(!match("*", ".ab")); + +// try expect(match("z*.js", "z.js")); +// try expect(match("*/*", "a/z")); +// try expect(match("*/z*.js", "a/z.js")); +// try expect(match("a/z*.js", "a/z.js")); + +// try expect(match("*", "ab")); +// try expect(match("*", "abc")); + +// try expect(!match("f*", "bar")); +// try expect(!match("*r", "foo")); +// try expect(!match("b*", "foo")); +// try expect(!match("*", "foo/bar")); +// try expect(match("*c", "abc")); +// try expect(match("a*", "abc")); +// try expect(match("a*c", "abc")); +// try expect(match("*r", "bar")); +// try expect(match("b*", "bar")); +// try expect(match("f*", "foo")); + +// try expect(match("*abc*", "one abc two")); +// try expect(match("a*b", "a b")); + +// try expect(!match("*a*", "foo")); +// try expect(match("*a*", "bar")); +// try expect(match("*abc*", "oneabctwo")); +// try expect(!match("*-bc-*", "a-b.c-d")); +// try expect(match("*-*.*-*", "a-b.c-d")); +// try expect(match("*-b*c-*", "a-b.c-d")); +// try expect(match("*-b.c-*", "a-b.c-d")); +// try expect(match("*.*", "a-b.c-d")); +// try expect(match("*.*-*", "a-b.c-d")); +// try expect(match("*.*-d", "a-b.c-d")); +// try expect(match("*.c-*", "a-b.c-d")); +// try expect(match("*b.*d", "a-b.c-d")); +// try expect(match("a*.c*", "a-b.c-d")); +// try expect(match("a-*.*-d", "a-b.c-d")); +// try expect(match("*.*", "a.b")); +// try expect(match("*.b", "a.b")); +// try expect(match("a.*", "a.b")); +// try expect(match("a.b", "a.b")); + +// try expect(!match("**-bc-**", "a-b.c-d")); +// try expect(match("**-**.**-**", "a-b.c-d")); +// try expect(match("**-b**c-**", "a-b.c-d")); +// try expect(match("**-b.c-**", "a-b.c-d")); +// try expect(match("**.**", "a-b.c-d")); +// try expect(match("**.**-**", "a-b.c-d")); +// try expect(match("**.**-d", "a-b.c-d")); +// try expect(match("**.c-**", "a-b.c-d")); +// try expect(match("**b.**d", "a-b.c-d")); +// try expect(match("a**.c**", "a-b.c-d")); +// try expect(match("a-**.**-d", "a-b.c-d")); +// try expect(match("**.**", "a.b")); +// try expect(match("**.b", "a.b")); +// try expect(match("a.**", "a.b")); +// try expect(match("a.b", "a.b")); + +// try expect(match("*/*", "/ab")); +// try expect(match(".", ".")); +// try expect(!match("a/", "a/.b")); +// try expect(match("/*", "/ab")); +// try expect(match("/??", "/ab")); +// try expect(match("/?b", "/ab")); +// try expect(match("/*", "/cd")); +// try expect(match("a", "a")); +// try expect(match("a/.*", "a/.b")); +// try expect(match("?/?", "a/b")); +// try expect(match("a/**/j/**/z/*.md", "a/b/c/d/e/j/n/p/o/z/c.md")); +// try expect(match("a/**/z/*.md", "a/b/c/d/e/z/c.md")); +// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); +// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); +// try expect(match("a/*/z/.a", "a/b/z/.a")); +// try expect(!match("bz", "a/b/z/.a")); +// try expect(match("a/**/c/*.md", "a/bb.bb/aa/b.b/aa/c/xyz.md")); +// try expect(match("a/**/c/*.md", "a/bb.bb/aa/bb/aa/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bb.bb/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bb/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bbbb/c/xyz.md")); +// try expect(match("*", "aaa")); +// try expect(match("*", "ab")); +// try expect(match("ab", "ab")); + +// try expect(!match("*/*/*", "aaa")); +// try expect(!match("*/*/*", "aaa/bb/aa/rr")); +// try expect(!match("aaa*", "aaa/bba/ccc")); +// // try expect(!match("aaa**", "aaa/bba/ccc")); +// try expect(!match("aaa/*", "aaa/bba/ccc")); +// try expect(!match("aaa/*ccc", "aaa/bba/ccc")); +// try expect(!match("aaa/*z", "aaa/bba/ccc")); +// try expect(!match("*/*/*", "aaa/bbb")); +// try expect(!match("*/*jk*/*i", "ab/zzz/ejkl/hi")); +// try expect(match("*/*/*", "aaa/bba/ccc")); +// try expect(match("aaa/**", "aaa/bba/ccc")); +// try expect(match("aaa/*", "aaa/bbb")); +// try expect(match("*/*z*/*/*i", "ab/zzz/ejkl/hi")); +// try expect(match("*j*i", "abzzzejklhi")); + +// try expect(match("*", "a")); +// try expect(match("*", "b")); +// try expect(!match("*", "a/a")); +// try expect(!match("*", "a/a/a")); +// try expect(!match("*", "a/a/b")); +// try expect(!match("*", "a/a/a/a")); +// try expect(!match("*", "a/a/a/a/a")); + +// try expect(!match("*/*", "a")); +// try expect(match("*/*", "a/a")); +// try expect(!match("*/*", "a/a/a")); + +// try expect(!match("*/*/*", "a")); +// try expect(!match("*/*/*", "a/a")); +// try expect(match("*/*/*", "a/a/a")); +// try expect(!match("*/*/*", "a/a/a/a")); + +// try expect(!match("*/*/*/*", "a")); +// try expect(!match("*/*/*/*", "a/a")); +// try expect(!match("*/*/*/*", "a/a/a")); +// try expect(match("*/*/*/*", "a/a/a/a")); +// try expect(!match("*/*/*/*", "a/a/a/a/a")); + +// try expect(!match("*/*/*/*/*", "a")); +// try expect(!match("*/*/*/*/*", "a/a")); +// try expect(!match("*/*/*/*/*", "a/a/a")); +// try expect(!match("*/*/*/*/*", "a/a/b")); +// try expect(!match("*/*/*/*/*", "a/a/a/a")); +// try expect(match("*/*/*/*/*", "a/a/a/a/a")); +// try expect(!match("*/*/*/*/*", "a/a/a/a/a/a")); + +// try expect(!match("a/*", "a")); +// try expect(match("a/*", "a/a")); +// try expect(!match("a/*", "a/a/a")); +// try expect(!match("a/*", "a/a/a/a")); +// try expect(!match("a/*", "a/a/a/a/a")); + +// try expect(!match("a/*/*", "a")); +// try expect(!match("a/*/*", "a/a")); +// try expect(match("a/*/*", "a/a/a")); +// try expect(!match("a/*/*", "b/a/a")); +// try expect(!match("a/*/*", "a/a/a/a")); +// try expect(!match("a/*/*", "a/a/a/a/a")); + +// try expect(!match("a/*/*/*", "a")); +// try expect(!match("a/*/*/*", "a/a")); +// try expect(!match("a/*/*/*", "a/a/a")); +// try expect(match("a/*/*/*", "a/a/a/a")); +// try expect(!match("a/*/*/*", "a/a/a/a/a")); + +// try expect(!match("a/*/*/*/*", "a")); +// try expect(!match("a/*/*/*/*", "a/a")); +// try expect(!match("a/*/*/*/*", "a/a/a")); +// try expect(!match("a/*/*/*/*", "a/a/b")); +// try expect(!match("a/*/*/*/*", "a/a/a/a")); +// try expect(match("a/*/*/*/*", "a/a/a/a/a")); + +// try expect(!match("a/*/a", "a")); +// try expect(!match("a/*/a", "a/a")); +// try expect(match("a/*/a", "a/a/a")); +// try expect(!match("a/*/a", "a/a/b")); +// try expect(!match("a/*/a", "a/a/a/a")); +// try expect(!match("a/*/a", "a/a/a/a/a")); + +// try expect(!match("a/*/b", "a")); +// try expect(!match("a/*/b", "a/a")); +// try expect(!match("a/*/b", "a/a/a")); +// try expect(match("a/*/b", "a/a/b")); +// try expect(!match("a/*/b", "a/a/a/a")); +// try expect(!match("a/*/b", "a/a/a/a/a")); + +// try expect(!match("*/**/a", "a")); +// try expect(!match("*/**/a", "a/a/b")); +// try expect(match("*/**/a", "a/a")); +// try expect(match("*/**/a", "a/a/a")); +// try expect(match("*/**/a", "a/a/a/a")); +// try expect(match("*/**/a", "a/a/a/a/a")); + +// try expect(!match("*/", "a")); +// try expect(!match("*/*", "a")); +// try expect(!match("a/*", "a")); +// // try expect(!match("*/*", "a/")); +// // try expect(!match("a/*", "a/")); +// try expect(!match("*", "a/a")); +// try expect(!match("*/", "a/a")); +// try expect(!match("*/", "a/x/y")); +// try expect(!match("*/*", "a/x/y")); +// try expect(!match("a/*", "a/x/y")); +// // try expect(match("*", "a/")); +// try expect(match("*", "a")); +// try expect(match("*/", "a/")); +// try expect(match("*{,/}", "a/")); +// try expect(match("*/*", "a/a")); +// try expect(match("a/*", "a/a")); + +// try expect(!match("a/**/*.txt", "a.txt")); +// try expect(match("a/**/*.txt", "a/x/y.txt")); +// try expect(!match("a/**/*.txt", "a/x/y/z")); + +// try expect(!match("a/*.txt", "a.txt")); +// try expect(match("a/*.txt", "a/b.txt")); +// try expect(!match("a/*.txt", "a/x/y.txt")); +// try expect(!match("a/*.txt", "a/x/y/z")); + +// try expect(match("a*.txt", "a.txt")); +// try expect(!match("a*.txt", "a/b.txt")); +// try expect(!match("a*.txt", "a/x/y.txt")); +// try expect(!match("a*.txt", "a/x/y/z")); + +// try expect(match("*.txt", "a.txt")); +// try expect(!match("*.txt", "a/b.txt")); +// try expect(!match("*.txt", "a/x/y.txt")); +// try expect(!match("*.txt", "a/x/y/z")); + +// try expect(!match("a*", "a/b")); +// try expect(!match("a/**/b", "a/a/bb")); +// try expect(!match("a/**/b", "a/bb")); + +// try expect(!match("*/**", "foo")); +// try expect(!match("**/", "foo/bar")); +// try expect(!match("**/*/", "foo/bar")); +// try expect(!match("*/*/", "foo/bar")); + +// try expect(match("**/..", "/home/foo/..")); +// try expect(match("**/a", "a")); +// try expect(match("**", "a/a")); +// try expect(match("a/**", "a/a")); +// try expect(match("a/**", "a/")); +// // try expect(match("a/**", "a")); +// try expect(!match("**/", "a/a")); +// // try expect(match("**/a/**", "a")); +// // try expect(match("a/**", "a")); +// try expect(!match("**/", "a/a")); +// try expect(match("*/**/a", "a/a")); +// // try expect(match("a/**", "a")); +// try expect(match("*/**", "foo/")); +// try expect(match("**/*", "foo/bar")); +// try expect(match("*/*", "foo/bar")); +// try expect(match("*/**", "foo/bar")); +// try expect(match("**/", "foo/bar/")); +// // try expect(match("**/*", "foo/bar/")); +// try expect(match("**/*/", "foo/bar/")); +// try expect(match("*/**", "foo/bar/")); +// try expect(match("*/*/", "foo/bar/")); + +// try expect(!match("*/foo", "bar/baz/foo")); +// try expect(!match("**/bar/*", "deep/foo/bar")); +// try expect(!match("*/bar/**", "deep/foo/bar/baz/x")); +// try expect(!match("/*", "ef")); +// try expect(!match("foo?bar", "foo/bar")); +// try expect(!match("**/bar*", "foo/bar/baz")); +// // try expect(!match("**/bar**", "foo/bar/baz")); +// try expect(!match("foo**bar", "foo/baz/bar")); +// try expect(!match("foo*bar", "foo/baz/bar")); +// // try expect(match("foo/**", "foo")); +// try expect(match("/*", "/ab")); +// try expect(match("/*", "/cd")); +// try expect(match("/*", "/ef")); +// try expect(match("a/**/j/**/z/*.md", "a/b/j/c/z/x.md")); +// try expect(match("a/**/j/**/z/*.md", "a/j/z/x.md")); + +// try expect(match("**/foo", "bar/baz/foo")); +// try expect(match("**/bar/*", "deep/foo/bar/baz")); +// try expect(match("**/bar/**", "deep/foo/bar/baz/")); +// try expect(match("**/bar/*/*", "deep/foo/bar/baz/x")); +// try expect(match("foo/**/**/bar", "foo/b/a/z/bar")); +// try expect(match("foo/**/bar", "foo/b/a/z/bar")); +// try expect(match("foo/**/**/bar", "foo/bar")); +// try expect(match("foo/**/bar", "foo/bar")); +// try expect(match("*/bar/**", "foo/bar/baz/x")); +// try expect(match("foo/**/**/bar", "foo/baz/bar")); +// try expect(match("foo/**/bar", "foo/baz/bar")); +// try expect(match("**/foo", "XXX/foo")); +// } + +// test "globstars" { +// try expect(match("**/*.js", "a/b/c/d.js")); +// try expect(match("**/*.js", "a/b/c.js")); +// try expect(match("**/*.js", "a/b.js")); +// try expect(match("a/b/**/*.js", "a/b/c/d/e/f.js")); +// try expect(match("a/b/**/*.js", "a/b/c/d/e.js")); +// try expect(match("a/b/c/**/*.js", "a/b/c/d.js")); +// try expect(match("a/b/**/*.js", "a/b/c/d.js")); +// try expect(match("a/b/**/*.js", "a/b/d.js")); +// try expect(!match("a/b/**/*.js", "a/d.js")); +// try expect(!match("a/b/**/*.js", "d.js")); + +// try expect(!match("**c", "a/b/c")); +// try expect(!match("a/**c", "a/b/c")); +// try expect(!match("a/**z", "a/b/c")); +// try expect(!match("a/**b**/c", "a/b/c/b/c")); +// try expect(!match("a/b/c**/*.js", "a/b/c/d/e.js")); +// try expect(match("a/**/b/**/c", "a/b/c/b/c")); +// try expect(match("a/**b**/c", "a/aba/c")); +// try expect(match("a/**b**/c", "a/b/c")); +// try expect(match("a/b/c**/*.js", "a/b/c/d.js")); + +// try expect(!match("a/**/*", "a")); +// try expect(!match("a/**/**/*", "a")); +// try expect(!match("a/**/**/**/*", "a")); +// try expect(!match("**/a", "a/")); +// try expect(!match("a/**/*", "a/")); +// try expect(!match("a/**/**/*", "a/")); +// try expect(!match("a/**/**/**/*", "a/")); +// try expect(!match("**/a", "a/b")); +// try expect(!match("a/**/j/**/z/*.md", "a/b/c/j/e/z/c.txt")); +// try expect(!match("a/**/b", "a/bb")); +// try expect(!match("**/a", "a/c")); +// try expect(!match("**/a", "a/b")); +// try expect(!match("**/a", "a/x/y")); +// try expect(!match("**/a", "a/b/c/d")); +// try expect(match("**", "a")); +// try expect(match("**/a", "a")); +// // try expect(match("a/**", "a")); +// try expect(match("**", "a/")); +// try expect(match("**/a/**", "a/")); +// try expect(match("a/**", "a/")); +// try expect(match("a/**/**", "a/")); +// try expect(match("**/a", "a/a")); +// try expect(match("**", "a/b")); +// try expect(match("*/*", "a/b")); +// try expect(match("a/**", "a/b")); +// try expect(match("a/**/*", "a/b")); +// try expect(match("a/**/**/*", "a/b")); +// try expect(match("a/**/**/**/*", "a/b")); +// try expect(match("a/**/b", "a/b")); +// try expect(match("**", "a/b/c")); +// try expect(match("**/*", "a/b/c")); +// try expect(match("**/**", "a/b/c")); +// try expect(match("*/**", "a/b/c")); +// try expect(match("a/**", "a/b/c")); +// try expect(match("a/**/*", "a/b/c")); +// try expect(match("a/**/**/*", "a/b/c")); +// try expect(match("a/**/**/**/*", "a/b/c")); +// try expect(match("**", "a/b/c/d")); +// try expect(match("a/**", "a/b/c/d")); +// try expect(match("a/**/*", "a/b/c/d")); +// try expect(match("a/**/**/*", "a/b/c/d")); +// try expect(match("a/**/**/**/*", "a/b/c/d")); +// try expect(match("a/b/**/c/**/*.*", "a/b/c/d.e")); +// try expect(match("a/**/f/*.md", "a/b/c/d/e/f/g.md")); +// try expect(match("a/**/f/**/k/*.md", "a/b/c/d/e/f/g/h/i/j/k/l.md")); +// try expect(match("a/b/c/*.md", "a/b/c/def.md")); +// try expect(match("a/*/c/*.md", "a/bb.bb/c/ddd.md")); +// try expect(match("a/**/f/*.md", "a/bb.bb/cc/d.d/ee/f/ggg.md")); +// try expect(match("a/**/f/*.md", "a/bb.bb/cc/dd/ee/f/ggg.md")); +// try expect(match("a/*/c/*.md", "a/bb/c/ddd.md")); +// try expect(match("a/*/c/*.md", "a/bbbb/c/ddd.md")); + +// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/image.png")); +// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/two/image.png")); +// try expect(match("foo/bar/**/one/**/*.*", "foo/bar/baz/one/two/three/image.png")); +// try expect(!match("a/b/**/f", "a/b/c/d/")); +// // try expect(match("a/**", "a")); +// try expect(match("**", "a")); +// // try expect(match("a{,/**}", "a")); +// try expect(match("**", "a/")); +// try expect(match("a/**", "a/")); +// try expect(match("**", "a/b/c/d")); +// try expect(match("**", "a/b/c/d/")); +// try expect(match("**/**", "a/b/c/d/")); +// try expect(match("**/b/**", "a/b/c/d/")); +// try expect(match("a/b/**", "a/b/c/d/")); +// try expect(match("a/b/**/", "a/b/c/d/")); +// try expect(match("a/b/**/c/**/", "a/b/c/d/")); +// try expect(match("a/b/**/c/**/d/", "a/b/c/d/")); +// try expect(match("a/b/**/**/*.*", "a/b/c/d/e.f")); +// try expect(match("a/b/**/*.*", "a/b/c/d/e.f")); +// try expect(match("a/b/**/c/**/d/*.*", "a/b/c/d/e.f")); +// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/e.f")); +// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/g/e.f")); +// try expect(match("a/b/**/d/**/*.*", "a/b/c/d/g/g/e.f")); +// try expect(match("a/b-*/**/z.js", "a/b-c/z.js")); +// try expect(match("a/b-*/**/z.js", "a/b-c/d/e/z.js")); + +// try expect(match("*/*", "a/b")); +// try expect(match("a/b/c/*.md", "a/b/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bb.bb/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bb/c/xyz.md")); +// try expect(match("a/*/c/*.md", "a/bbbb/c/xyz.md")); + +// try expect(match("**/*", "a/b/c")); +// try expect(match("**/**", "a/b/c")); +// try expect(match("*/**", "a/b/c")); +// try expect(match("a/**/j/**/z/*.md", "a/b/c/d/e/j/n/p/o/z/c.md")); +// try expect(match("a/**/z/*.md", "a/b/c/d/e/z/c.md")); +// try expect(match("a/**/c/*.md", "a/bb.bb/aa/b.b/aa/c/xyz.md")); +// try expect(match("a/**/c/*.md", "a/bb.bb/aa/bb/aa/c/xyz.md")); +// try expect(!match("a/**/j/**/z/*.md", "a/b/c/j/e/z/c.txt")); +// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/c/xyz.md")); +// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/d/xyz.md")); +// try expect(!match("a/**/", "a/b")); +// // try expect(!match("**/*", "a/b/.js/c.txt")); +// try expect(!match("a/**/", "a/b/c/d")); +// try expect(!match("a/**/", "a/bb")); +// try expect(!match("a/**/", "a/cb")); +// try expect(match("/**", "/a/b")); +// try expect(match("**/*", "a.b")); +// try expect(match("**/*", "a.js")); +// try expect(match("**/*.js", "a.js")); +// // try expect(match("a/**/", "a/")); +// try expect(match("**/*.js", "a/a.js")); +// try expect(match("**/*.js", "a/a/b.js")); +// try expect(match("a/**/b", "a/b")); +// try expect(match("a/**b", "a/b")); +// try expect(match("**/*.md", "a/b.md")); +// try expect(match("**/*", "a/b/c.js")); +// try expect(match("**/*", "a/b/c.txt")); +// try expect(match("a/**/", "a/b/c/d/")); +// try expect(match("**/*", "a/b/c/d/a.js")); +// try expect(match("a/b/**/*.js", "a/b/c/z.js")); +// try expect(match("a/b/**/*.js", "a/b/z.js")); +// try expect(match("**/*", "ab")); +// try expect(match("**/*", "ab/c")); +// try expect(match("**/*", "ab/c/d")); +// try expect(match("**/*", "abc.js")); + +// try expect(!match("**/", "a")); +// try expect(!match("**/a/*", "a")); +// try expect(!match("**/a/*/*", "a")); +// try expect(!match("*/a/**", "a")); +// try expect(!match("a/**/*", "a")); +// try expect(!match("a/**/**/*", "a")); +// try expect(!match("**/", "a/b")); +// try expect(!match("**/b/*", "a/b")); +// try expect(!match("**/b/*/*", "a/b")); +// try expect(!match("b/**", "a/b")); +// try expect(!match("**/", "a/b/c")); +// try expect(!match("**/**/b", "a/b/c")); +// try expect(!match("**/b", "a/b/c")); +// try expect(!match("**/b/*/*", "a/b/c")); +// try expect(!match("b/**", "a/b/c")); +// try expect(!match("**/", "a/b/c/d")); +// try expect(!match("**/d/*", "a/b/c/d")); +// try expect(!match("b/**", "a/b/c/d")); +// try expect(match("**", "a")); +// try expect(match("**/**", "a")); +// try expect(match("**/**/*", "a")); +// try expect(match("**/**/a", "a")); +// try expect(match("**/a", "a")); +// // try expect(match("**/a/**", "a")); +// // try expect(match("a/**", "a")); +// try expect(match("**", "a/b")); +// try expect(match("**/**", "a/b")); +// try expect(match("**/**/*", "a/b")); +// try expect(match("**/**/b", "a/b")); +// try expect(match("**/b", "a/b")); +// // try expect(match("**/b/**", "a/b")); +// // try expect(match("*/b/**", "a/b")); +// try expect(match("a/**", "a/b")); +// try expect(match("a/**/*", "a/b")); +// try expect(match("a/**/**/*", "a/b")); +// try expect(match("**", "a/b/c")); +// try expect(match("**/**", "a/b/c")); +// try expect(match("**/**/*", "a/b/c")); +// try expect(match("**/b/*", "a/b/c")); +// try expect(match("**/b/**", "a/b/c")); +// try expect(match("*/b/**", "a/b/c")); +// try expect(match("a/**", "a/b/c")); +// try expect(match("a/**/*", "a/b/c")); +// try expect(match("a/**/**/*", "a/b/c")); +// try expect(match("**", "a/b/c/d")); +// try expect(match("**/**", "a/b/c/d")); +// try expect(match("**/**/*", "a/b/c/d")); +// try expect(match("**/**/d", "a/b/c/d")); +// try expect(match("**/b/**", "a/b/c/d")); +// try expect(match("**/b/*/*", "a/b/c/d")); +// try expect(match("**/d", "a/b/c/d")); +// try expect(match("*/b/**", "a/b/c/d")); +// try expect(match("a/**", "a/b/c/d")); +// try expect(match("a/**/*", "a/b/c/d")); +// try expect(match("a/**/**/*", "a/b/c/d")); +// } + +// test "utf8" { +// try expect(match("フ*/**/*", "フォルダ/aaa.js")); +// try expect(match("フォ*/**/*", "フォルダ/aaa.js")); +// try expect(match("フォル*/**/*", "フォルダ/aaa.js")); +// try expect(match("フ*ル*/**/*", "フォルダ/aaa.js")); +// try expect(match("フォルダ/**/*", "フォルダ/aaa.js")); +// } + +// test "negation" { +// try expect(!match("!*", "abc")); +// try expect(!match("!abc", "abc")); +// try expect(!match("*!.md", "bar.md")); +// try expect(!match("foo!.md", "bar.md")); +// try expect(!match("\\!*!*.md", "foo!.md")); +// try expect(!match("\\!*!*.md", "foo!bar.md")); +// try expect(match("*!*.md", "!foo!.md")); +// try expect(match("\\!*!*.md", "!foo!.md")); +// try expect(match("!*foo", "abc")); +// try expect(match("!foo*", "abc")); +// try expect(match("!xyz", "abc")); +// try expect(match("*!*.*", "ba!r.js")); +// try expect(match("*.md", "bar.md")); +// try expect(match("*!*.*", "foo!.md")); +// try expect(match("*!*.md", "foo!.md")); +// try expect(match("*!.md", "foo!.md")); +// try expect(match("*.md", "foo!.md")); +// try expect(match("foo!.md", "foo!.md")); +// try expect(match("*!*.md", "foo!bar.md")); +// try expect(match("*b*.md", "foobar.md")); + +// try expect(!match("a!!b", "a")); +// try expect(!match("a!!b", "aa")); +// try expect(!match("a!!b", "a/b")); +// try expect(!match("a!!b", "a!b")); +// try expect(match("a!!b", "a!!b")); +// try expect(!match("a!!b", "a/!!/b")); + +// try expect(!match("!a/b", "a/b")); +// try expect(match("!a/b", "a")); +// try expect(match("!a/b", "a.b")); +// try expect(match("!a/b", "a/a")); +// try expect(match("!a/b", "a/c")); +// try expect(match("!a/b", "b/a")); +// try expect(match("!a/b", "b/b")); +// try expect(match("!a/b", "b/c")); + +// try expect(!match("!abc", "abc")); +// try expect(match("!!abc", "abc")); +// try expect(!match("!!!abc", "abc")); +// try expect(match("!!!!abc", "abc")); +// try expect(!match("!!!!!abc", "abc")); +// try expect(match("!!!!!!abc", "abc")); +// try expect(!match("!!!!!!!abc", "abc")); +// try expect(match("!!!!!!!!abc", "abc")); + +// // try expect(!match("!(*/*)", "a/a")); +// // try expect(!match("!(*/*)", "a/b")); +// // try expect(!match("!(*/*)", "a/c")); +// // try expect(!match("!(*/*)", "b/a")); +// // try expect(!match("!(*/*)", "b/b")); +// // try expect(!match("!(*/*)", "b/c")); +// // try expect(!match("!(*/b)", "a/b")); +// // try expect(!match("!(*/b)", "b/b")); +// // try expect(!match("!(a/b)", "a/b")); +// try expect(!match("!*", "a")); +// try expect(!match("!*", "a.b")); +// try expect(!match("!*/*", "a/a")); +// try expect(!match("!*/*", "a/b")); +// try expect(!match("!*/*", "a/c")); +// try expect(!match("!*/*", "b/a")); +// try expect(!match("!*/*", "b/b")); +// try expect(!match("!*/*", "b/c")); +// try expect(!match("!*/b", "a/b")); +// try expect(!match("!*/b", "b/b")); +// try expect(!match("!*/c", "a/c")); +// try expect(!match("!*/c", "a/c")); +// try expect(!match("!*/c", "b/c")); +// try expect(!match("!*/c", "b/c")); +// try expect(!match("!*a*", "bar")); +// try expect(!match("!*a*", "fab")); +// // try expect(!match("!a/(*)", "a/a")); +// // try expect(!match("!a/(*)", "a/b")); +// // try expect(!match("!a/(*)", "a/c")); +// // try expect(!match("!a/(b)", "a/b")); +// try expect(!match("!a/*", "a/a")); +// try expect(!match("!a/*", "a/b")); +// try expect(!match("!a/*", "a/c")); +// try expect(!match("!f*b", "fab")); +// // try expect(match("!(*/*)", "a")); +// // try expect(match("!(*/*)", "a.b")); +// // try expect(match("!(*/b)", "a")); +// // try expect(match("!(*/b)", "a.b")); +// // try expect(match("!(*/b)", "a/a")); +// // try expect(match("!(*/b)", "a/c")); +// // try expect(match("!(*/b)", "b/a")); +// // try expect(match("!(*/b)", "b/c")); +// // try expect(match("!(a/b)", "a")); +// // try expect(match("!(a/b)", "a.b")); +// // try expect(match("!(a/b)", "a/a")); +// // try expect(match("!(a/b)", "a/c")); +// // try expect(match("!(a/b)", "b/a")); +// // try expect(match("!(a/b)", "b/b")); +// // try expect(match("!(a/b)", "b/c")); +// try expect(match("!*", "a/a")); +// try expect(match("!*", "a/b")); +// try expect(match("!*", "a/c")); +// try expect(match("!*", "b/a")); +// try expect(match("!*", "b/b")); +// try expect(match("!*", "b/c")); +// try expect(match("!*/*", "a")); +// try expect(match("!*/*", "a.b")); +// try expect(match("!*/b", "a")); +// try expect(match("!*/b", "a.b")); +// try expect(match("!*/b", "a/a")); +// try expect(match("!*/b", "a/c")); +// try expect(match("!*/b", "b/a")); +// try expect(match("!*/b", "b/c")); +// try expect(match("!*/c", "a")); +// try expect(match("!*/c", "a.b")); +// try expect(match("!*/c", "a/a")); +// try expect(match("!*/c", "a/b")); +// try expect(match("!*/c", "b/a")); +// try expect(match("!*/c", "b/b")); +// try expect(match("!*a*", "foo")); +// // try expect(match("!a/(*)", "a")); +// // try expect(match("!a/(*)", "a.b")); +// // try expect(match("!a/(*)", "b/a")); +// // try expect(match("!a/(*)", "b/b")); +// // try expect(match("!a/(*)", "b/c")); +// // try expect(match("!a/(b)", "a")); +// // try expect(match("!a/(b)", "a.b")); +// // try expect(match("!a/(b)", "a/a")); +// // try expect(match("!a/(b)", "a/c")); +// // try expect(match("!a/(b)", "b/a")); +// // try expect(match("!a/(b)", "b/b")); +// // try expect(match("!a/(b)", "b/c")); +// try expect(match("!a/*", "a")); +// try expect(match("!a/*", "a.b")); +// try expect(match("!a/*", "b/a")); +// try expect(match("!a/*", "b/b")); +// try expect(match("!a/*", "b/c")); +// try expect(match("!f*b", "bar")); +// try expect(match("!f*b", "foo")); + +// try expect(!match("!.md", ".md")); +// try expect(match("!**/*.md", "a.js")); +// // try expect(!match("!**/*.md", "b.md")); +// try expect(match("!**/*.md", "c.txt")); +// try expect(match("!*.md", "a.js")); +// try expect(!match("!*.md", "b.md")); +// try expect(match("!*.md", "c.txt")); +// try expect(!match("!*.md", "abc.md")); +// try expect(match("!*.md", "abc.txt")); +// try expect(!match("!*.md", "foo.md")); +// try expect(match("!.md", "foo.md")); + +// try expect(match("!*.md", "a.js")); +// try expect(match("!*.md", "b.txt")); +// try expect(!match("!*.md", "c.md")); +// try expect(!match("!a/*/a.js", "a/a/a.js")); +// try expect(!match("!a/*/a.js", "a/b/a.js")); +// try expect(!match("!a/*/a.js", "a/c/a.js")); +// try expect(!match("!a/*/*/a.js", "a/a/a/a.js")); +// try expect(match("!a/*/*/a.js", "b/a/b/a.js")); +// try expect(match("!a/*/*/a.js", "c/a/c/a.js")); +// try expect(!match("!a/a*.txt", "a/a.txt")); +// try expect(match("!a/a*.txt", "a/b.txt")); +// try expect(match("!a/a*.txt", "a/c.txt")); +// try expect(!match("!a.a*.txt", "a.a.txt")); +// try expect(match("!a.a*.txt", "a.b.txt")); +// try expect(match("!a.a*.txt", "a.c.txt")); +// try expect(!match("!a/*.txt", "a/a.txt")); +// try expect(!match("!a/*.txt", "a/b.txt")); +// try expect(!match("!a/*.txt", "a/c.txt")); + +// try expect(match("!*.md", "a.js")); +// try expect(match("!*.md", "b.txt")); +// try expect(!match("!*.md", "c.md")); +// // try expect(!match("!**/a.js", "a/a/a.js")); +// // try expect(!match("!**/a.js", "a/b/a.js")); +// // try expect(!match("!**/a.js", "a/c/a.js")); +// try expect(match("!**/a.js", "a/a/b.js")); +// try expect(!match("!a/**/a.js", "a/a/a/a.js")); +// try expect(match("!a/**/a.js", "b/a/b/a.js")); +// try expect(match("!a/**/a.js", "c/a/c/a.js")); +// try expect(match("!**/*.md", "a/b.js")); +// try expect(match("!**/*.md", "a.js")); +// try expect(!match("!**/*.md", "a/b.md")); +// // try expect(!match("!**/*.md", "a.md")); +// try expect(!match("**/*.md", "a/b.js")); +// try expect(!match("**/*.md", "a.js")); +// try expect(match("**/*.md", "a/b.md")); +// try expect(match("**/*.md", "a.md")); +// try expect(match("!**/*.md", "a/b.js")); +// try expect(match("!**/*.md", "a.js")); +// try expect(!match("!**/*.md", "a/b.md")); +// // try expect(!match("!**/*.md", "a.md")); +// try expect(match("!*.md", "a/b.js")); +// try expect(match("!*.md", "a.js")); +// try expect(match("!*.md", "a/b.md")); +// try expect(!match("!*.md", "a.md")); +// try expect(match("!**/*.md", "a.js")); +// // try expect(!match("!**/*.md", "b.md")); +// try expect(match("!**/*.md", "c.txt")); +// } + +// test "question_mark" { +// try expect(match("?", "a")); +// try expect(!match("?", "aa")); +// try expect(!match("?", "ab")); +// try expect(!match("?", "aaa")); +// try expect(!match("?", "abcdefg")); + +// try expect(!match("??", "a")); +// try expect(match("??", "aa")); +// try expect(match("??", "ab")); +// try expect(!match("??", "aaa")); +// try expect(!match("??", "abcdefg")); + +// try expect(!match("???", "a")); +// try expect(!match("???", "aa")); +// try expect(!match("???", "ab")); +// try expect(match("???", "aaa")); +// try expect(!match("???", "abcdefg")); + +// try expect(!match("a?c", "aaa")); +// try expect(match("a?c", "aac")); +// try expect(match("a?c", "abc")); +// try expect(!match("ab?", "a")); +// try expect(!match("ab?", "aa")); +// try expect(!match("ab?", "ab")); +// try expect(!match("ab?", "ac")); +// try expect(!match("ab?", "abcd")); +// try expect(!match("ab?", "abbb")); +// try expect(match("a?b", "acb")); + +// try expect(!match("a/?/c/?/e.md", "a/bb/c/dd/e.md")); +// try expect(match("a/??/c/??/e.md", "a/bb/c/dd/e.md")); +// try expect(!match("a/??/c.md", "a/bbb/c.md")); +// try expect(match("a/?/c.md", "a/b/c.md")); +// try expect(match("a/?/c/?/e.md", "a/b/c/d/e.md")); +// try expect(!match("a/?/c/???/e.md", "a/b/c/d/e.md")); +// try expect(match("a/?/c/???/e.md", "a/b/c/zzz/e.md")); +// try expect(!match("a/?/c.md", "a/bb/c.md")); +// try expect(match("a/??/c.md", "a/bb/c.md")); +// try expect(match("a/???/c.md", "a/bbb/c.md")); +// try expect(match("a/????/c.md", "a/bbbb/c.md")); +// } + +// test "braces" { +// try expect(match("{a,b,c}", "a")); +// try expect(match("{a,b,c}", "b")); +// try expect(match("{a,b,c}", "c")); +// try expect(!match("{a,b,c}", "aa")); +// try expect(!match("{a,b,c}", "bb")); +// try expect(!match("{a,b,c}", "cc")); + +// try expect(match("a/{a,b}", "a/a")); +// try expect(match("a/{a,b}", "a/b")); +// try expect(!match("a/{a,b}", "a/c")); +// try expect(!match("a/{a,b}", "b/b")); +// try expect(!match("a/{a,b,c}", "b/b")); +// try expect(match("a/{a,b,c}", "a/c")); +// try expect(match("a{b,bc}.txt", "abc.txt")); + +// try expect(match("foo[{a,b}]baz", "foo{baz")); + +// try expect(!match("a{,b}.txt", "abc.txt")); +// try expect(!match("a{a,b,}.txt", "abc.txt")); +// try expect(!match("a{b,}.txt", "abc.txt")); +// try expect(match("a{,b}.txt", "a.txt")); +// try expect(match("a{b,}.txt", "a.txt")); +// try expect(match("a{a,b,}.txt", "aa.txt")); +// try expect(match("a{a,b,}.txt", "aa.txt")); +// try expect(match("a{,b}.txt", "ab.txt")); +// try expect(match("a{b,}.txt", "ab.txt")); + +// // try expect(match("{a/,}a/**", "a")); +// try expect(match("a{a,b/}*.txt", "aa.txt")); +// try expect(match("a{a,b/}*.txt", "ab/.txt")); +// try expect(match("a{a,b/}*.txt", "ab/a.txt")); +// // try expect(match("{a/,}a/**", "a/")); +// try expect(match("{a/,}a/**", "a/a/")); +// // try expect(match("{a/,}a/**", "a/a")); +// try expect(match("{a/,}a/**", "a/a/a")); +// try expect(match("{a/,}a/**", "a/a/")); +// try expect(match("{a/,}a/**", "a/a/a/")); +// try expect(match("{a/,}b/**", "a/b/a/")); +// try expect(match("{a/,}b/**", "b/a/")); +// try expect(match("a{,/}*.txt", "a.txt")); +// try expect(match("a{,/}*.txt", "ab.txt")); +// try expect(match("a{,/}*.txt", "a/b.txt")); +// try expect(match("a{,/}*.txt", "a/ab.txt")); + +// try expect(match("a{,.*{foo,db},\\(bar\\)}.txt", "a.txt")); +// try expect(!match("a{,.*{foo,db},\\(bar\\)}.txt", "adb.txt")); +// try expect(match("a{,.*{foo,db},\\(bar\\)}.txt", "a.db.txt")); + +// try expect(match("a{,*.{foo,db},\\(bar\\)}.txt", "a.txt")); +// try expect(!match("a{,*.{foo,db},\\(bar\\)}.txt", "adb.txt")); +// try expect(match("a{,*.{foo,db},\\(bar\\)}.txt", "a.db.txt")); + +// // try expect(match("a{,.*{foo,db},\\(bar\\)}", "a")); +// try expect(!match("a{,.*{foo,db},\\(bar\\)}", "adb")); +// try expect(match("a{,.*{foo,db},\\(bar\\)}", "a.db")); + +// // try expect(match("a{,*.{foo,db},\\(bar\\)}", "a")); +// try expect(!match("a{,*.{foo,db},\\(bar\\)}", "adb")); +// try expect(match("a{,*.{foo,db},\\(bar\\)}", "a.db")); + +// try expect(!match("{,.*{foo,db},\\(bar\\)}", "a")); +// try expect(!match("{,.*{foo,db},\\(bar\\)}", "adb")); +// try expect(!match("{,.*{foo,db},\\(bar\\)}", "a.db")); +// try expect(match("{,.*{foo,db},\\(bar\\)}", ".db")); + +// try expect(!match("{,*.{foo,db},\\(bar\\)}", "a")); +// try expect(match("{*,*.{foo,db},\\(bar\\)}", "a")); +// try expect(!match("{,*.{foo,db},\\(bar\\)}", "adb")); +// try expect(match("{,*.{foo,db},\\(bar\\)}", "a.db")); + +// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/c/xyz.md")); +// try expect(!match("a/b/**/c{d,e}/**/xyz.md", "a/b/d/xyz.md")); +// try expect(match("a/b/**/c{d,e}/**/xyz.md", "a/b/cd/xyz.md")); +// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/c/xyz.md")); +// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/d/xyz.md")); +// try expect(match("a/b/**/{c,d,e}/**/xyz.md", "a/b/e/xyz.md")); + +// try expect(match("*{a,b}*", "xax")); +// try expect(match("*{a,b}*", "xxax")); +// try expect(match("*{a,b}*", "xbx")); + +// try expect(match("*{*a,b}", "xba")); +// try expect(match("*{*a,b}", "xb")); + +// try expect(!match("*??", "a")); +// try expect(!match("*???", "aa")); +// try expect(match("*???", "aaa")); +// try expect(!match("*****??", "a")); +// try expect(!match("*****???", "aa")); +// try expect(match("*****???", "aaa")); + +// try expect(!match("a*?c", "aaa")); +// try expect(match("a*?c", "aac")); +// try expect(match("a*?c", "abc")); + +// try expect(match("a**?c", "abc")); +// try expect(!match("a**?c", "abb")); +// try expect(match("a**?c", "acc")); +// try expect(match("a*****?c", "abc")); + +// try expect(match("*****?", "a")); +// try expect(match("*****?", "aa")); +// try expect(match("*****?", "abc")); +// try expect(match("*****?", "zzz")); +// try expect(match("*****?", "bbb")); +// try expect(match("*****?", "aaaa")); + +// try expect(!match("*****??", "a")); +// try expect(match("*****??", "aa")); +// try expect(match("*****??", "abc")); +// try expect(match("*****??", "zzz")); +// try expect(match("*****??", "bbb")); +// try expect(match("*****??", "aaaa")); + +// try expect(!match("?*****??", "a")); +// try expect(!match("?*****??", "aa")); +// try expect(match("?*****??", "abc")); +// try expect(match("?*****??", "zzz")); +// try expect(match("?*****??", "bbb")); +// try expect(match("?*****??", "aaaa")); + +// try expect(match("?*****?c", "abc")); +// try expect(!match("?*****?c", "abb")); +// try expect(!match("?*****?c", "zzz")); + +// try expect(match("?***?****c", "abc")); +// try expect(!match("?***?****c", "bbb")); +// try expect(!match("?***?****c", "zzz")); + +// try expect(match("?***?****?", "abc")); +// try expect(match("?***?****?", "bbb")); +// try expect(match("?***?****?", "zzz")); + +// try expect(match("?***?****", "abc")); +// try expect(match("*******c", "abc")); +// try expect(match("*******?", "abc")); +// try expect(match("a*cd**?**??k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??k***", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??***k", "abcdecdhjk")); +// try expect(match("a**?**cd**?**??***k**", "abcdecdhjk")); +// try expect(match("a****c**?**??*****", "abcdecdhjk")); + +// try expect(!match("a/?/c/?/*/e.md", "a/b/c/d/e.md")); +// try expect(match("a/?/c/?/*/e.md", "a/b/c/d/e/e.md")); +// try expect(match("a/?/c/?/*/e.md", "a/b/c/d/efghijk/e.md")); +// try expect(match("a/?/**/e.md", "a/b/c/d/efghijk/e.md")); +// try expect(!match("a/?/e.md", "a/bb/e.md")); +// try expect(match("a/??/e.md", "a/bb/e.md")); +// try expect(!match("a/?/**/e.md", "a/bb/e.md")); +// try expect(match("a/?/**/e.md", "a/b/ccc/e.md")); +// try expect(match("a/*/?/**/e.md", "a/b/c/d/efghijk/e.md")); +// try expect(match("a/*/?/**/e.md", "a/b/c/d/efgh.ijk/e.md")); +// try expect(match("a/*/?/**/e.md", "a/b.bb/c/d/efgh.ijk/e.md")); +// try expect(match("a/*/?/**/e.md", "a/bbb/c/d/efgh.ijk/e.md")); + +// try expect(match("a/*/ab??.md", "a/bbb/abcd.md")); +// try expect(match("a/bbb/ab??.md", "a/bbb/abcd.md")); +// try expect(match("a/bbb/ab???md", "a/bbb/abcd.md")); +// } + +// fn matchSame(str: []const u8) bool { +// return match(str, str); +// } +// test "fuzz_tests" { +// // https://github.com/devongovett/glob-match/issues/1 +// try expect(!matchSame( +// "{*{??*{??**,Uz*zz}w**{*{**a,z***b*[!}w??*azzzzzzzz*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!z[za,z&zz}w**z*z*}", +// )); +// try expect(!matchSame( +// "**** *{*{??*{??***\x05 *{*{??*{??***0x5,\x00U\x00}]*****0x1,\x00***\x00,\x00\x00}w****,\x00U\x00}]*****0x1,\x00***\x00,\x00\x00}w*****0x1***{}*.*\x00\x00*\x00", +// )); +// } diff --git a/src/glob_ascii.zig b/src/glob_ascii.zig new file mode 100644 index 0000000000..5de16b0d1d --- /dev/null +++ b/src/glob_ascii.zig @@ -0,0 +1,508 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) 2023 Devon Govett +// Copyright (c) 2023 Stephen Gregoratto +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +const std = @import("std"); +const math = std.math; +const mem = std.mem; +const expect = std.testing.expect; + +test "unclosed_braces" { + try expect(match("src/foo.{ts,tsx", "src/foo.{ts,tsx")); + try expect(match("src/**/foo.{ts,tsx", "src/lmao/bro/foo.{ts,tsx")); +} + +// These store character indices into the glob and path strings. +path_index: usize = 0, +glob_index: usize = 0, +// When we hit a * or **, we store the state for backtracking. +wildcard: Wildcard = .{}, +globstar: Wildcard = .{}, + +const Wildcard = struct { + // Using u32 rather than usize for these results in 10% faster performance. + glob_index: u32 = 0, + path_index: u32 = 0, +}; + +const BraceState = enum { Invalid, Comma, EndBrace }; + +fn skipBraces(self: *State, glob: []const u8, stop_on_comma: bool) BraceState { + var braces: u32 = 1; + var in_brackets = false; + while (self.glob_index < glob.len and braces > 0) : (self.glob_index += 1) { + switch (glob[self.glob_index]) { + // Skip nested braces + '{' => if (!in_brackets) { + braces += 1; + }, + '}' => if (!in_brackets) { + braces -= 1; + }, + ',' => if (stop_on_comma and braces == 1 and !in_brackets) { + self.glob_index += 1; + return .Comma; + }, + '*', '?', '[' => |c| if (!in_brackets) { + if (c == '[') + in_brackets = true; + }, + ']' => in_brackets = false, + '\\' => self.glob_index += 1, + else => {}, + } + } + + if (braces != 0) + return .Invalid; + return .EndBrace; +} + +inline fn backtrack(self: *State) void { + self.glob_index = self.wildcard.glob_index; + self.path_index = self.wildcard.path_index; +} + +const State = @This(); +const BraceStack = struct { + stack: [10]State = undefined, + len: u32 = 0, + longest_brace_match: u32 = 0, + + inline fn push(self: *BraceStack, state: *const State) State { + self.stack[self.len] = state.*; + self.len += 1; + return State{ + .path_index = state.path_index, + .glob_index = state.glob_index + 1, + }; + } + + inline fn pop(self: *BraceStack, state: *const State) State { + self.len -= 1; + const s = State{ + .glob_index = state.glob_index, + .path_index = self.longest_brace_match, + // Restore star state if needed later. + .wildcard = self.stack[self.len].wildcard, + .globstar = self.stack[self.len].globstar, + }; + if (self.len == 0) + self.longest_brace_match = 0; + return s; + } + + inline fn last(self: *const BraceStack) *const State { + return &self.stack[self.len - 1]; + } +}; + +const BraceIndex = struct { + start: u32 = 0, + end: u32 = 0, +}; + +test "glob_preprocess" { + try expect(match("{*.foo,*.ts,*.tsx}", "lmao.foo")); + try expect(match("hello\\ friends", "hello\\ friends")); + + // var brace_indices = std.mem.zeroes([10]BraceIndex); + // var brace_indices_len: u8 = 0; + // var glob: []const u8 = "{a}"; + + // _ = preprocess_glob(glob, &brace_indices, &brace_indices_len, 0); + // try std.testing.expectEqualDeep(@as(u8, 1), brace_indices_len); + // try std.testing.expectEqualDeep(@as(BraceIndex, .{.start = 0, .end = 2}), brace_indices[0]); + + // brace_indices = std.mem.zeroes([10]BraceIndex); + // brace_indices_len = 0; + // glob = "{a,{b,c}}"; + // _ = preprocess_glob(glob, &brace_indices, &brace_indices_len, 0); + // try std.testing.expectEqualDeep(@as(u8, 2), brace_indices_len); + // try std.testing.expectEqualDeep(@as(BraceIndex, .{.start = 0, .end = 8}), brace_indices[0]); + // try std.testing.expectEqualDeep(@as(BraceIndex, .{.start = 2, .end = 7}), brace_indices[1]); +} + +pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count: *u8, i: *u32) ?u32 { + while (i.* < glob.len) { + const c = glob[i]; + switch (c) { + '{' => { + if (brace_indices_len.* == brace_indices.len) continue; + const stack_idx = brace_indices_len.*; + if (i == glob.len - 1) continue; + const matching_idx = preprocess_glob(glob[i + 1 ..], brace_indices, brace_indices_len, search_count + 1); + if (matching_idx) |idx| { + if (brace_indices_len.* == brace_indices.len) continue; + brace_indices[stack_idx].start = @intCast(i); + brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; + brace_indices_len.* += 1; + } + }, + '}' => { + if (search_count > 0) return @intCast(i); + }, + else => {}, + } + } + return null; +} + +// pub fn preprocess_glob(glob: []const u8, brace_indices: *[10]BraceIndex, brace_indices_len: *u8, search_count_: u8) ?u32 { +// if (glob.len == 0) return null; + +// var search_count = search_count_; +// var i: u32 = 0; +// while (i < glob.len): (i += 1) { +// const c = glob[i]; +// switch (c) { +// '{' => { +// if (brace_indices_len.* == brace_indices.len) continue; +// const stack_idx = brace_indices_len.*; +// if (i == glob.len - 1) continue; +// const matching_idx = preprocess_glob(glob[i + 1..], brace_indices, brace_indices_len, search_count + 1); +// if (matching_idx) |idx| { +// if (brace_indices_len.* == brace_indices.len) continue; +// brace_indices[stack_idx].start = @intCast(i); +// brace_indices[stack_idx].end = @as(u32, @intCast(i)) + idx + 1; +// brace_indices_len.* += 1; +// } +// }, +// '}' => { +// if (search_count > 0) return @intCast(i); +// }, +// else => {}, +// } + +// } + +// return null; +// } + +pub fn valid_glob_indices(glob: []const u8, indices: std.ArrayList(BraceIndex)) !void { + _ = indices; + // {a,b,c} + for (glob, 0..) |c, i| { + _ = i; + _ = c; + } +} + +/// This function checks returns a boolean value if the pathname `path` matches +/// the pattern `glob`. +/// +/// The supported pattern syntax for `glob` is: +/// +/// "?" +/// Matches any single character. +/// "*" +/// Matches zero or more characters, except for path separators ('/' or '\'). +/// "**" +/// Matches zero or more characters, including path separators. +/// Must match a complete path segment, i.e. followed by a path separator or +/// at the end of the pattern. +/// "[ab]" +/// Matches one of the characters contained in the brackets. +/// Character ranges (e.g. "[a-z]") are also supported. +/// Use "[!ab]" or "[^ab]" to match any character *except* those contained +/// in the brackets. +/// "{a,b}" +/// Match one of the patterns contained in the braces. +/// Any of the wildcards listed above can be used in the sub patterns. +/// Braces may be nested up to 10 levels deep. +/// "!" +/// Negates the result when at the start of the pattern. +/// Multiple "!" characters negate the pattern multiple times. +/// "\" +/// Used to escape any of the special characters above. +pub fn match(glob: []const u8, path: []const u8) bool { + // This algorithm is based on https://research.swtch.com/glob + var state = State{}; + // Store the state when we see an opening '{' brace in a stack. + // Up to 10 nested braces are supported. + var brace_stack = BraceStack{}; + + // First, check if the pattern is negated with a leading '!' character. + // Multiple negations can occur. + var negated = false; + while (state.glob_index < glob.len and glob[state.glob_index] == '!') { + negated = !negated; + state.glob_index += 1; + } + + while (state.glob_index < glob.len or state.path_index < path.len) { + if (state.glob_index < glob.len) { + const gc = glob[state.glob_index]; + switch (gc) { + '*' => { + const is_globstar = state.glob_index + 1 < glob.len and + glob[state.glob_index + 1] == '*'; + if (is_globstar) { + // Coalesce multiple ** segments into one. + var index = state.glob_index + 2; + state.glob_index = skipGlobstars(glob, &index) - 2; + } + + state.wildcard.glob_index = @intCast(state.glob_index); + state.wildcard.path_index = @intCast(state.path_index + 1); + + // ** allows path separators, whereas * does not. + // However, ** must be a full path component, i.e. a/**/b not a**b. + if (is_globstar) { + state.glob_index += 2; + + if (glob.len == state.glob_index) { + // A trailing ** segment without a following separator. + state.globstar = state.wildcard; + } else if (glob[state.glob_index] == '/' and + (state.glob_index < 3 or glob[state.glob_index - 3] == '/')) + { + // Matched a full /**/ segment. If the last character in the path was a separator, + // skip the separator in the glob so we search for the next character. + // In effect, this makes the whole segment optional so that a/**/b matches a/b. + if (state.path_index == 0 or + (state.path_index < path.len and + isSeparator(path[state.path_index - 1]))) + { + state.glob_index += 1; + } + + // The allows_sep flag allows separator characters in ** matches. + // one is a '/', which prevents a/**/b from matching a/bb. + state.globstar = state.wildcard; + } + } else { + state.glob_index += 1; + } + + // If we are in a * segment and hit a separator, + // either jump back to a previous ** or end the wildcard. + if (state.globstar.path_index != state.wildcard.path_index and + state.path_index < path.len and + isSeparator(path[state.path_index])) + { + // Special case: don't jump back for a / at the end of the glob. + if (state.globstar.path_index > 0 and state.path_index + 1 < path.len) { + state.glob_index = state.globstar.glob_index; + state.wildcard.glob_index = state.globstar.glob_index; + } else { + state.wildcard.path_index = 0; + } + } + + // If the next char is a special brace separator, + // skip to the end of the braces so we don't try to match it. + if (brace_stack.len > 0 and + state.glob_index < glob.len and + (glob[state.glob_index] == ',' or glob[state.glob_index] == '}')) + { + if (state.skipBraces(glob, false) == .Invalid) + return false; // invalid pattern! + } + + continue; + }, + '?' => if (state.path_index < path.len) { + if (!isSeparator(path[state.path_index])) { + state.glob_index += 1; + state.path_index += 1; + continue; + } + }, + '[' => if (state.path_index < path.len) { + state.glob_index += 1; + const c = path[state.path_index]; + + // Check if the character class is negated. + var class_negated = false; + if (state.glob_index < glob.len and + (glob[state.glob_index] == '^' or glob[state.glob_index] == '!')) + { + class_negated = true; + state.glob_index += 1; + } + + // Try each range. + var first = true; + var is_match = false; + while (state.glob_index < glob.len and (first or glob[state.glob_index] != ']')) { + var low = glob[state.glob_index]; + if (!unescape(&low, glob, &state.glob_index)) + return false; // Invalid pattern + state.glob_index += 1; + + // If there is a - and the following character is not ], + // read the range end character. + const high = if (state.glob_index + 1 < glob.len and + glob[state.glob_index] == '-' and glob[state.glob_index + 1] != ']') + blk: { + state.glob_index += 1; + var h = glob[state.glob_index]; + if (!unescape(&h, glob, &state.glob_index)) + return false; // Invalid pattern! + state.glob_index += 1; + break :blk h; + } else low; + + if (low <= c and c <= high) + is_match = true; + first = false; + } + if (state.glob_index >= glob.len) + return false; // Invalid pattern! + state.glob_index += 1; + if (is_match != class_negated) { + state.path_index += 1; + continue; + } + }, + '{' => if (state.path_index < path.len) { + if (brace_stack.len >= brace_stack.stack.len) + return false; // Invalid pattern! Too many nested braces. + + // Push old state to the stack, and reset current state. + state = brace_stack.push(&state); + continue; + }, + '}' => if (brace_stack.len > 0) { + // If we hit the end of the braces, we matched the last option. + brace_stack.longest_brace_match = + @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); + state.glob_index += 1; + state = brace_stack.pop(&state); + continue; + }, + ',' => if (brace_stack.len > 0) { + // If we hit a comma, we matched one of the options! + // But we still need to check the others in case there is a longer match. + brace_stack.longest_brace_match = + @max(brace_stack.longest_brace_match, @as(u32, @intCast(state.path_index))); + state.path_index = brace_stack.last().path_index; + state.glob_index += 1; + state.wildcard = Wildcard{}; + state.globstar = Wildcard{}; + continue; + }, + else => |c| if (state.path_index < path.len) { + var cc = c; + // Match escaped characters as literals. + if (!unescape(&cc, glob, &state.glob_index)) + return false; // Invalid pattern; + + const is_match = if (cc == '/') + isSeparator(path[state.path_index]) + else + path[state.path_index] == cc; + + if (is_match) { + if (brace_stack.len > 0 and + state.glob_index > 0 and + glob[state.glob_index - 1] == '}') + { + brace_stack.longest_brace_match = @intCast(state.path_index); + state = brace_stack.pop(&state); + } + state.glob_index += 1; + state.path_index += 1; + + // If this is not a separator, lock in the previous globstar. + if (cc != '/') + state.globstar.path_index = 0; + + continue; + } + }, + } + } + // If we didn't match, restore state to the previous star pattern. + if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { + state.backtrack(); + continue; + } + + if (brace_stack.len > 0) { + // If in braces, find next option and reset path to index where we saw the '{' + switch (state.skipBraces(glob, true)) { + .Invalid => return false, + .Comma => { + state.path_index = brace_stack.last().path_index; + continue; + }, + .EndBrace => {}, + } + + // Hit the end. Pop the stack. + // If we matched a previous option, use that. + if (brace_stack.longest_brace_match > 0) { + state = brace_stack.pop(&state); + continue; + } else { + // Didn't match. Restore state, and check if we need to jump back to a star pattern. + state = brace_stack.last().*; + brace_stack.len -= 1; + if (state.wildcard.path_index > 0 and state.wildcard.path_index <= path.len) { + state.backtrack(); + continue; + } + } + } + + return negated; + } + + return !negated; +} + +inline fn isSeparator(c: u8) bool { + if (comptime @import("builtin").os.tag == .windows) return c == '/' or c == '\\'; + return c == '/'; +} + +inline fn unescape(c: *u8, glob: []const u8, glob_index: *usize) bool { + if (c.* == '\\') { + glob_index.* += 1; + if (glob_index.* >= glob.len) + return false; // Invalid pattern! + + c.* = switch (glob[glob_index.*]) { + 'a' => '\x61', + 'b' => '\x08', + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + else => |cc| cc, + }; + } + + return true; +} + +inline fn skipGlobstars(glob: []const u8, glob_index: *usize) usize { + // Coalesce multiple ** segments into one. + while (glob_index.* + 3 <= glob.len and + std.mem.eql(u8, glob[glob_index.*..][0..3], "/**")) + { + glob_index.* += 3; + } + + return glob_index.*; +} diff --git a/src/js/builtins/Glob.ts b/src/js/builtins/Glob.ts new file mode 100644 index 0000000000..6f35b61326 --- /dev/null +++ b/src/js/builtins/Glob.ts @@ -0,0 +1,21 @@ +interface Glob { + $pull(opts); + $resolveSync(opts); +} + +export function scan(this: Glob, opts) { + const valuesPromise = this.$pull(opts); + async function* iter() { + const values = (await valuesPromise) || []; + yield* values; + } + return iter(); +} + +export function scanSync(this: Glob, opts) { + const arr = this.$resolveSync(opts) || []; + function* iter() { + yield* arr; + } + return iter(); +} diff --git a/src/jsc.zig b/src/jsc.zig index 0a6eefa7ce..862f60885c 100644 --- a/src/jsc.zig +++ b/src/jsc.zig @@ -29,6 +29,7 @@ pub const Jest = @import("./bun.js/test/jest.zig"); pub const Expect = @import("./bun.js/test/expect.zig"); pub const Snapshot = @import("./bun.js/test/snapshot.zig"); pub const API = struct { + pub const Glob = @import("./bun.js/api/glob.zig"); pub const JSBundler = @import("./bun.js/api/JSBundler.zig").JSBundler; pub const BuildArtifact = @import("./bun.js/api/JSBundler.zig").BuildArtifact; pub const JSTranspiler = @import("./bun.js/api/JSTranspiler.zig"); diff --git a/src/resolver/resolve_path.zig b/src/resolver/resolve_path.zig index 0e3f9950e0..b3b257377d 100644 --- a/src/resolver/resolve_path.zig +++ b/src/resolver/resolve_path.zig @@ -436,16 +436,16 @@ pub fn relativeNormalized(from: []const u8, to: []const u8, comptime platform: P pub fn dirname(str: []const u8, comptime platform: Platform) []const u8 { switch (comptime platform.resolve()) { .loose => { - const separator = lastIndexOfSeparatorLoose(str); - return str[0 .. separator + 1]; + const separator = lastIndexOfSeparatorLoose(str) orelse return ""; + return str[0..separator]; }, .posix => { - const separator = lastIndexOfSeparatorPosix(str); - return str[0 .. separator + 1]; + const separator = lastIndexOfSeparatorPosix(str) orelse return ""; + return str[0..separator]; }, .windows => { const separator = lastIndexOfSeparatorWindows(str) orelse return std.fs.path.diskDesignatorWindows(str); - return str[0 .. separator + 1]; + return str[0..separator]; }, else => unreachable, } diff --git a/src/string_immutable.zig b/src/string_immutable.zig index 6f1b9bb40f..62393cb23f 100644 --- a/src/string_immutable.zig +++ b/src/string_immutable.zig @@ -1743,9 +1743,12 @@ pub fn toUTF8ListWithType(list_: std.ArrayList(u8), comptime Type: type, utf16: const length = bun.simdutf.length.utf8.from.utf16.le(utf16); try list.ensureTotalCapacityPrecise(length + 16); const buf = try convertUTF16ToUTF8(list, Type, utf16); - if (Environment.allow_assert) { - std.debug.assert(buf.items.len == length); - } + // Commenting out because `convertUTF16ToUTF8` may convert to WTF-8 + // which uses 3 bytes for invalid surrogates, causing the length to not + // match from simdutf. + // if (Environment.allow_assert) { + // std.debug.assert(buf.items.len == length); + // } return buf; } @@ -4488,6 +4491,147 @@ pub inline fn utf8ByteSequenceLength(first_byte: u8) u3 { }; } +pub const PackedCodepointIterator = struct { + const Iterator = @This(); + const CodePointType = u32; + const zeroValue = 0; + + bytes: []const u8, + i: usize, + next_width: usize = 0, + width: u3 = 0, + c: CodePointType = zeroValue, + + pub const ZeroValue = zeroValue; + + pub const Cursor = packed struct { + i: u32 = 0, + c: u29 = zeroValue, + width: u3 = 0, + pub const CodePointType = u29; + }; + + pub fn init(str: string) Iterator { + return Iterator{ .bytes = str, .i = 0, .c = zeroValue }; + } + + pub fn initOffset(str: string, i: usize) Iterator { + return Iterator{ .bytes = str, .i = i, .c = zeroValue }; + } + + pub inline fn next(it: *const Iterator, cursor: *Cursor) bool { + const pos: u32 = @as(u32, cursor.width) + cursor.i; + if (pos >= it.bytes.len) { + return false; + } + + const cp_len = wtf8ByteSequenceLength(it.bytes[pos]); + const error_char = comptime std.math.minInt(CodePointType); + + const codepoint = @as( + CodePointType, + switch (cp_len) { + 0 => return false, + 1 => it.bytes[pos], + else => decodeWTF8RuneTMultibyte(it.bytes[pos..].ptr[0..4], cp_len, CodePointType, error_char), + }, + ); + + { + @setRuntimeSafety(false); + cursor.* = Cursor{ + .i = pos, + .c = if (error_char != codepoint) + @truncate(codepoint) + else + unicode_replacement, + .width = if (codepoint != error_char) cp_len else 1, + }; + } + + return true; + } + + inline fn nextCodepointSlice(it: *Iterator) []const u8 { + const bytes = it.bytes; + const prev = it.i; + const next_ = prev + it.next_width; + if (bytes.len <= next_) return ""; + + const cp_len = utf8ByteSequenceLength(bytes[next_]); + it.next_width = cp_len; + it.i = @min(next_, bytes.len); + + const slice = bytes[prev..][0..cp_len]; + it.width = @as(u3, @intCast(slice.len)); + return slice; + } + + pub fn needsUTF8Decoding(slice: string) bool { + var it = Iterator{ .bytes = slice, .i = 0 }; + + while (true) { + const part = it.nextCodepointSlice(); + @setRuntimeSafety(false); + switch (part.len) { + 0 => return false, + 1 => continue, + else => return true, + } + } + } + + pub fn scanUntilQuotedValueOrEOF(iter: *Iterator, comptime quote: CodePointType) usize { + while (iter.c > -1) { + if (!switch (iter.nextCodepoint()) { + quote => false, + '\\' => brk: { + if (iter.nextCodepoint() == quote) { + continue; + } + break :brk true; + }, + else => true, + }) { + return iter.i + 1; + } + } + + return iter.i; + } + + pub fn nextCodepoint(it: *Iterator) CodePointType { + const slice = it.nextCodepointSlice(); + + it.c = switch (slice.len) { + 0 => zeroValue, + 1 => @as(CodePointType, @intCast(slice[0])), + 2 => @as(CodePointType, @intCast(std.unicode.utf8Decode2(slice) catch unreachable)), + 3 => @as(CodePointType, @intCast(std.unicode.utf8Decode3(slice) catch unreachable)), + 4 => @as(CodePointType, @intCast(std.unicode.utf8Decode4(slice) catch unreachable)), + else => unreachable, + }; + + return it.c; + } + + /// Look ahead at the next n codepoints without advancing the iterator. + /// If fewer than n codepoints are available, then return the remainder of the string. + pub fn peek(it: *Iterator, n: usize) []const u8 { + const original_i = it.i; + defer it.i = original_i; + + var end_ix = original_i; + var found: usize = 0; + while (found < n) : (found += 1) { + const next_codepoint = it.nextCodepointSlice() orelse return it.bytes[original_i..]; + end_ix += next_codepoint.len; + } + + return it.bytes[original_i..end_ix]; + } +}; + pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: comptime_int) type { return struct { const Iterator = @This(); @@ -4497,6 +4641,8 @@ pub fn NewCodePointIterator(comptime CodePointType: type, comptime zeroValue: co width: u3 = 0, c: CodePointType = zeroValue, + pub const ZeroValue = zeroValue; + pub const Cursor = struct { i: u32 = 0, c: CodePointType = zeroValue, diff --git a/test/bun.lockb b/test/bun.lockb index e9ead2eaf2..e5315c00ab 100755 Binary files a/test/bun.lockb and b/test/bun.lockb differ diff --git a/test/js/bun/glob/__snapshots__/scan.test.ts.snap b/test/js/bun/glob/__snapshots__/scan.test.ts.snap new file mode 100644 index 0000000000..61d0048c42 --- /dev/null +++ b/test/js/bun/glob/__snapshots__/scan.test.ts.snap @@ -0,0 +1,580 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`fast-glob e2e tests patterns regular fixtures/*: fixtures/* 1`] = ` +[ + "fixtures/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**: fixtures/** 1`] = ` +[ + "fixtures/file.md", + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/*: fixtures/**/* 1`] = ` +[ + "fixtures/file.md", + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/nested: fixtures/*/nested 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/nested/*: fixtures/*/nested/* 1`] = ` +[ + "fixtures/first/nested/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/nested/**: fixtures/*/nested/** 1`] = ` +[ + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/nested/**/*: fixtures/*/nested/**/* 1`] = ` +[ + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/nested/*: fixtures/**/nested/* 1`] = ` +[ + "fixtures/first/nested/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/nested/**: fixtures/**/nested/** 1`] = ` +[ + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/nested/**/*: fixtures/**/nested/**/* 1`] = ` +[ + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}: fixtures/{first,second} 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/*: fixtures/{first,second}/* 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/**: fixtures/{first,second}/** 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/**/*: fixtures/{first,second}/**/* 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/{first,second}/*: fixtures/*/{first,second}/* 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/{first,second}/*/{nested,file.md}: fixtures/*/{first,second}/*/{nested,file.md} 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/{first,second}/**: fixtures/**/{first,second}/** 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/{first,second}/{nested,file.md}: fixtures/**/{first,second}/{nested,file.md} 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/{first,second}/**/{nested,file.md}: fixtures/**/{first,second}/**/{nested,file.md} 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/{nested,file.md}: fixtures/{first,second}/{nested,file.md} 1`] = ` +[ + "fixtures/first/file.md", + "fixtures/second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/*/nested/*: fixtures/{first,second}/*/nested/* 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular fixtures/{first,second}/**/nested/**: fixtures/{first,second}/**/nested/** 1`] = ` +[ + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/*/{nested,file.md}/*: fixtures/*/{nested,file.md}/* 1`] = ` +[ + "fixtures/first/nested/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular fixtures/**/{nested,file.md}/*: fixtures/**/{nested,file.md}/* 1`] = ` +[ + "fixtures/first/nested/file.md", + "fixtures/second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular ./fixtures/*: ./fixtures/* 1`] = ` +[ + "./fixtures/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd *: * 1`] = ` +[ + "file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **: ** 1`] = ` +[ + "file.md", + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", + "third/library/a/book.md", + "third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/*: **/* 1`] = ` +[ + "file.md", + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", + "third/library/a/book.md", + "third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd */nested: */nested 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular cwd */nested/*: */nested/* 1`] = ` +[ + "first/nested/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd */nested/**: */nested/** 1`] = ` +[ + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd */nested/**/*: */nested/**/* 1`] = ` +[ + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/nested/*: **/nested/* 1`] = ` +[ + "first/nested/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/nested/**: **/nested/** 1`] = ` +[ + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/nested/**/*: **/nested/**/* 1`] = ` +[ + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}: {first,second} 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/*: {first,second}/* 1`] = ` +[ + "first/file.md", + "second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/**: {first,second}/** 1`] = ` +[ + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/**/*: {first,second}/**/* 1`] = ` +[ + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd */{first,second}/*: */{first,second}/* 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular cwd */{first,second}/*/{nested,file.md}: */{first,second}/*/{nested,file.md} 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular cwd **/{first,second}/**: **/{first,second}/** 1`] = ` +[ + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/{first,second}/{nested,file.md}: **/{first,second}/{nested,file.md} 1`] = ` +[ + "first/file.md", + "second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/{first,second}/**/{nested,file.md}: **/{first,second}/**/{nested,file.md} 1`] = ` +[ + "first/file.md", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/{nested,file.md}: {first,second}/{nested,file.md} 1`] = ` +[ + "first/file.md", + "second/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/*/nested/*: {first,second}/*/nested/* 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular cwd {first,second}/**/nested/**: {first,second}/**/nested/** 1`] = ` +[ + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd */{nested,file.md}/*: */{nested,file.md}/* 1`] = ` +[ + "first/nested/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular cwd **/{nested,file.md}/*: **/{nested,file.md}/* 1`] = ` +[ + "first/nested/file.md", + "second/nested/file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ./*: ./* 1`] = ` +[ + "./leak.test.ts", + "./match.test.ts", + "./scan.test.ts", + "./stress.test.ts", + "./util.ts", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ./*: ./* 2`] = ` +[ + "./file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ./**: ./** 1`] = ` +[ + "./file.md", + "./first/file.md", + "./first/nested/directory/file.json", + "./first/nested/directory/file.md", + "./first/nested/file.md", + "./second/file.md", + "./second/nested/directory/file.md", + "./second/nested/file.md", + "./third/library/a/book.md", + "./third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ./**/*: ./**/* 1`] = ` +[ + "./file.md", + "./first/file.md", + "./first/nested/directory/file.json", + "./first/nested/directory/file.md", + "./first/nested/file.md", + "./second/file.md", + "./second/nested/directory/file.md", + "./second/nested/file.md", + "./third/library/a/book.md", + "./third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ../*: ../* 1`] = ` +[ + "../file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ../**: ../** 1`] = ` +[ + "../file.md", + "../first/file.md", + "../first/nested/directory/file.json", + "../first/nested/directory/file.md", + "../first/nested/file.md", + "../second/file.md", + "../second/nested/directory/file.md", + "../second/nested/file.md", + "../third/library/a/book.md", + "../third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ../../*: ../../* 1`] = ` +[ + "../../file.md", +] +`; + +exports[`fast-glob e2e tests patterns regular relative cwd ../{first,second}: ../{first,second} 1`] = `[]`; + +exports[`fast-glob e2e tests patterns regular relative cwd ./../*: ./../* 1`] = ` +[ + "./../file.md", +] +`; + +exports[`fast-glob e2e tests patterns absolute cwd *: * 1`] = ` +[ + "js/bun/glob/fixtures/file.md", +] +`; + +exports[`fast-glob e2e tests patterns absolute cwd **: ** 1`] = ` +[ + "js/bun/glob/fixtures/file.md", + "js/bun/glob/fixtures/first/file.md", + "js/bun/glob/fixtures/first/nested/directory/file.json", + "js/bun/glob/fixtures/first/nested/directory/file.md", + "js/bun/glob/fixtures/first/nested/file.md", + "js/bun/glob/fixtures/second/file.md", + "js/bun/glob/fixtures/second/nested/directory/file.md", + "js/bun/glob/fixtures/second/nested/file.md", + "js/bun/glob/fixtures/third/library/a/book.md", + "js/bun/glob/fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests patterns absolute cwd **/*: **/* 1`] = ` +[ + "js/bun/glob/fixtures/file.md", + "js/bun/glob/fixtures/first/file.md", + "js/bun/glob/fixtures/first/nested/directory/file.json", + "js/bun/glob/fixtures/first/nested/directory/file.md", + "js/bun/glob/fixtures/first/nested/file.md", + "js/bun/glob/fixtures/second/file.md", + "js/bun/glob/fixtures/second/nested/directory/file.md", + "js/bun/glob/fixtures/second/nested/file.md", + "js/bun/glob/fixtures/third/library/a/book.md", + "js/bun/glob/fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests only files fixtures/*: fixtures/* 1`] = ` +[ + "fixtures/file.md", +] +`; + +exports[`fast-glob e2e tests only files fixtures/**: fixtures/** 1`] = ` +[ + "fixtures/file.md", + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests only files fixtures/**/*: fixtures/**/* 1`] = ` +[ + "fixtures/file.md", + "fixtures/first/file.md", + "fixtures/first/nested/directory/file.json", + "fixtures/first/nested/directory/file.md", + "fixtures/first/nested/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/second/nested/file.md", + "fixtures/third/library/a/book.md", + "fixtures/third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests only files (cwd) *: * 1`] = ` +[ + "file.md", +] +`; + +exports[`fast-glob e2e tests only files (cwd) **: ** 1`] = ` +[ + "file.md", + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", + "third/library/a/book.md", + "third/library/b/book.md", +] +`; + +exports[`fast-glob e2e tests only files (cwd) **/*: **/* 1`] = ` +[ + "file.md", + "first/file.md", + "first/nested/directory/file.json", + "first/nested/directory/file.md", + "first/nested/file.md", + "second/file.md", + "second/nested/directory/file.md", + "second/nested/file.md", + "third/library/a/book.md", + "third/library/b/book.md", +] +`; diff --git a/test/js/bun/glob/leak.test.ts b/test/js/bun/glob/leak.test.ts new file mode 100644 index 0000000000..f1fbc92b06 --- /dev/null +++ b/test/js/bun/glob/leak.test.ts @@ -0,0 +1,61 @@ +import { expect, test, describe, beforeAll, afterAll } from "bun:test"; +import { Glob, GlobScanOptions } from "bun"; + +describe("leaks", () => { + const bun = process.argv[0]; + const cwd = import.meta.dir; + const iters = 100; + const hundredMb = (1 << 20) * 100; + + test("scanSync", () => { + const code = /* ts */ ` + let prev: number | undefined = undefined; + for (let i = 0; i < ${iters}; i++) { + Bun.gc(true); + (function () { + const glob = new Bun.Glob("**/*"); + Array.from(glob.scanSync({ cwd: '${cwd}' })); + })(); + Bun.gc(true); + const val = process.memoryUsage.rss(); + if (prev === undefined) { + prev = val; + } else { + if (Math.abs(prev - val) >= ${hundredMb}) { + throw new Error('uh oh: ' + Math.abs(prev - val)) + } + } + } + `; + + const { stdout, stderr, exitCode } = Bun.spawnSync([bun, "--smol", "-e", code]); + console.log(stdout.toString(), stderr.toString()); + expect(exitCode).toBe(0); + }); + + test("scan", async () => { + const code = /* ts */ ` + let prev: number | undefined = undefined; + for (let i = 0; i < ${iters}; i++) { + Bun.gc(true); + await (async function () { + const glob = new Bun.Glob("**/*"); + await Array.fromAsync(glob.scan({ cwd: '${cwd}' })); + })(); + Bun.gc(true); + const val = process.memoryUsage.rss(); + if (prev === undefined) { + prev = val; + } else { + if (Math.abs(prev - val) >= ${hundredMb}) { + throw new Error('uh oh: ' + Math.abs(prev - val)) + } + } + } + `; + + const { stdout, stderr, exitCode } = Bun.spawnSync([bun, "--smol", "-e", code]); + console.log(stdout.toString(), stderr.toString()); + expect(exitCode).toBe(0); + }); +}); diff --git a/test/js/bun/glob/match.test.ts b/test/js/bun/glob/match.test.ts new file mode 100644 index 0000000000..d3925990b2 --- /dev/null +++ b/test/js/bun/glob/match.test.ts @@ -0,0 +1,1525 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) 2014-present, Jon Schlinkert +// Copyright (c) 2023 Devon Govett +// Copyright (c) 2023 Stephen Gregoratto +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { expect, test, describe } from "bun:test"; +import { Glob } from "bun"; + +describe("Glob.match", () => { + test("single wildcard", () => { + let glob: Glob; + + glob = new Glob("*"); + expect(glob.match("foo")).toBeTrue(); + expect(glob.match("lmao.ts")).toBeTrue(); + expect(glob.match("")).toBeTrue(); + expect(glob.match(" ")).toBeTrue(); + expect(glob.match("*")).toBeTrue(); + + glob = new Glob("*.ts"); + expect(glob.match("foo.ts")).toBeTrue(); + expect(glob.match(".ts")).toBeTrue(); + expect(glob.match("")).toBeFalse(); + expect(glob.match("bar.tsx")).toBeFalse(); + expect(glob.match("foo/bar.ts")).toBeFalse(); + expect(glob.match("foo/bar/baz.ts")).toBeFalse(); + + glob = new Glob("src/*/*.ts"); + expect(glob.match("src/foo/bar.ts")).toBeTrue(); + expect(glob.match("src/bar.ts")).toBeFalse(); + + glob = new Glob("src/**/hehe.ts"); + expect(glob.match("src/foo/baz/lol/hehe.ts")).toBeTrue(); + }); + + test("double wildcard", () => { + let glob: Glob; + + glob = new Glob("**"); + expect(glob.match("")).toBeTrue(); + expect(glob.match("nice/wow/great/foo.ts")).toBeTrue(); + + glob = new Glob("foo/**/bar"); + expect(glob.match("")).toBeFalse(); + expect(glob.match("foo/lmao/lol/bar")).toBeTrue(); + expect(glob.match("foo/lmao/lol/haha/wtf/nice/bar")).toBeTrue(); + expect(glob.match("foo/bar")).toBeTrue(); + + glob = new Glob("src/**/*.ts"); + expect(glob.match("src/foo/bar/baz/nice.ts")).toBeTrue(); + expect(glob.match("src/foo/bar/nice.ts")).toBeTrue(); + expect(glob.match("src/nice.ts")).toBeTrue(); + + glob = new Glob("src/foo/*/bar/**/*.ts"); + expect(glob.match("src/foo/nice/bar/baz/lmao.ts")).toBeTrue(); + expect(glob.match("src/foo/nice/bar/baz/lmao.ts")).toBeTrue(); + }); + + test("braces", () => { + let glob: Glob; + + glob = new Glob("index.{ts,tsx,js,jsx}"); + expect(glob.match("index.ts")).toBeTrue(); + expect(glob.match("index.tsx")).toBeTrue(); + expect(glob.match("index.js")).toBeTrue(); + expect(glob.match("index.jsx")).toBeTrue(); + expect(glob.match("index.jsxxxxxxxx")).toBeFalse(); + }); + + // Most of the potential bugs when dealing with non-ASCII patterns is when the + // pattern matching algorithm wants to deal with single chars, for example + // using the `[...]` syntax, it tries to match each char in the brackets. With + // multi-byte string encodings this will break. + test("non ascii", () => { + let glob: Glob; + + glob = new Glob("😎/¢£.{ts,tsx,js,jsx}"); + expect(glob.match("😎/¢£.ts")).toBeTrue(); + expect(glob.match("😎/¢£.tsx")).toBeTrue(); + expect(glob.match("😎/¢£.js")).toBeTrue(); + expect(glob.match("😎/¢£.jsx")).toBeTrue(); + expect(glob.match("😎/¢£.jsxxxxxxxx")).toBeFalse(); + + glob = new Glob("*é*"); + expect(glob.match("café noir")).toBeTrue(); + expect(glob.match("café noir")).toBeTrue(); + + glob = new Glob("caf*noir"); + expect(glob.match("café noir")).toBeTrue(); + expect(glob.match("café noir")).toBeTrue(); + expect(glob.match("cafeenoir")).toBeTrue(); + + glob = new Glob("F[ë£a]"); + expect(glob.match("Fë")).toBeTrue(); + expect(glob.match("F£")).toBeTrue(); + expect(glob.match("Fa")).toBeTrue(); + + // invalid surrogate pairs + glob = new Glob("\uD83D\u0027"); + expect(glob.match("lmao")).toBeFalse(); + + glob = new Glob("\uD800\uD800"); + expect(glob.match("lmao")).toBeFalse(); + + glob = new Glob("*"); + expect(glob.match("\uD800\uD800")).toBeTrue(); + + glob = new Glob("hello/*/friends"); + expect(glob.match("hello/\uD800\uD800/friends")).toBeTrue(); + + glob = new Glob("*.{js,\uD83D\u0027}"); + expect(glob.match("runtime.node.pre.out.ts")).toBeFalse(); + expect(glob.match("runtime.node.pre.out.js")).toBeTrue(); + }); + + /** + * These tests are ported from micromatch, glob-match, globlin + */ + describe("ported from micromatch / glob-match / globlin tests", () => { + test("basic", () => { + expect(new Glob("abc").match("abc")).toBe(true); + expect(new Glob("*").match("abc")).toBe(true); + expect(new Glob("*").match("")).toBe(true); + expect(new Glob("**").match("")).toBe(true); + expect(new Glob("*c").match("abc")).toBe(true); + expect(new Glob("*b").match("abc")).toBe(false); + expect(new Glob("a*").match("abc")).toBe(true); + expect(new Glob("b*").match("abc")).toBe(false); + expect(new Glob("a*").match("a")).toBe(true); + expect(new Glob("*a").match("a")).toBe(true); + expect(new Glob("a*b*c*d*e*").match("axbxcxdxe")).toBe(true); + expect(new Glob("a*b*c*d*e*").match("axbxcxdxexxx")).toBe(true); + expect(new Glob("a*b?c*x").match("abxbbxdbxebxczzx")).toBe(true); + expect(new Glob("a*b?c*x").match("abxbbxdbxebxczzy")).toBe(false); + + expect(new Glob("a/*/test").match("a/foo/test")).toBe(true); + expect(new Glob("a/*/test").match("a/foo/bar/test")).toBe(false); + expect(new Glob("a/**/test").match("a/foo/test")).toBe(true); + expect(new Glob("a/**/test").match("a/foo/bar/test")).toBe(true); + expect(new Glob("a/**/b/c").match("a/foo/bar/b/c")).toBe(true); + expect(new Glob("a\\*b").match("a*b")).toBe(true); + expect(new Glob("a\\*b").match("axb")).toBe(false); + + expect(new Glob("[abc]").match("a")).toBe(true); + expect(new Glob("[abc]").match("b")).toBe(true); + expect(new Glob("[abc]").match("c")).toBe(true); + expect(new Glob("[abc]").match("d")).toBe(false); + expect(new Glob("x[abc]x").match("xax")).toBe(true); + expect(new Glob("x[abc]x").match("xbx")).toBe(true); + expect(new Glob("x[abc]x").match("xcx")).toBe(true); + expect(new Glob("x[abc]x").match("xdx")).toBe(false); + expect(new Glob("x[abc]x").match("xay")).toBe(false); + expect(new Glob("[?]").match("?")).toBe(true); + expect(new Glob("[?]").match("a")).toBe(false); + expect(new Glob("[*]").match("*")).toBe(true); + expect(new Glob("[*]").match("a")).toBe(false); + + expect(new Glob("[a-cx]").match("a")).toBe(true); + expect(new Glob("[a-cx]").match("b")).toBe(true); + expect(new Glob("[a-cx]").match("c")).toBe(true); + expect(new Glob("[a-cx]").match("d")).toBe(false); + expect(new Glob("[a-cx]").match("x")).toBe(true); + + expect(new Glob("[^abc]").match("a")).toBe(false); + expect(new Glob("[^abc]").match("b")).toBe(false); + expect(new Glob("[^abc]").match("c")).toBe(false); + expect(new Glob("[^abc]").match("d")).toBe(true); + expect(new Glob("[!abc]").match("a")).toBe(false); + expect(new Glob("[!abc]").match("b")).toBe(false); + expect(new Glob("[!abc]").match("c")).toBe(false); + expect(new Glob("[!abc]").match("d")).toBe(true); + expect(new Glob("[\\!]").match("!")).toBe(true); + + expect(new Glob("a*b*[cy]*d*e*").match("axbxcxdxexxx")).toBe(true); + expect(new Glob("a*b*[cy]*d*e*").match("axbxyxdxexxx")).toBe(true); + expect(new Glob("a*b*[cy]*d*e*").match("axbxxxyxdxexxx")).toBe(true); + + expect(new Glob("test.{jpg,png}").match("test.jpg")).toBe(true); + expect(new Glob("test.{jpg,png}").match("test.png")).toBe(true); + expect(new Glob("test.{j*g,p*g}").match("test.jpg")).toBe(true); + expect(new Glob("test.{j*g,p*g}").match("test.jpxxxg")).toBe(true); + expect(new Glob("test.{j*g,p*g}").match("test.jxg")).toBe(true); + expect(new Glob("test.{j*g,p*g}").match("test.jnt")).toBe(false); + expect(new Glob("test.{j*g,j*c}").match("test.jnc")).toBe(true); + expect(new Glob("test.{jpg,p*g}").match("test.png")).toBe(true); + expect(new Glob("test.{jpg,p*g}").match("test.pxg")).toBe(true); + expect(new Glob("test.{jpg,p*g}").match("test.pnt")).toBe(false); + expect(new Glob("test.{jpeg,png}").match("test.jpeg")).toBe(true); + expect(new Glob("test.{jpeg,png}").match("test.jpg")).toBe(false); + expect(new Glob("test.{jpeg,png}").match("test.png")).toBe(true); + expect(new Glob("test.{jp\\,g,png}").match("test.jp,g")).toBe(true); + expect(new Glob("test.{jp\\,g,png}").match("test.jxg")).toBe(false); + expect(new Glob("test/{foo,bar}/baz").match("test/foo/baz")).toBe(true); + expect(new Glob("test/{foo,bar}/baz").match("test/bar/baz")).toBe(true); + expect(new Glob("test/{foo,bar}/baz").match("test/baz/baz")).toBe(false); + expect(new Glob("test/{foo*,bar*}/baz").match("test/foooooo/baz")).toBe(true); + expect(new Glob("test/{foo*,bar*}/baz").match("test/barrrrr/baz")).toBe(true); + expect(new Glob("test/{*foo,*bar}/baz").match("test/xxxxfoo/baz")).toBe(true); + expect(new Glob("test/{*foo,*bar}/baz").match("test/xxxxbar/baz")).toBe(true); + expect(new Glob("test/{foo/**,bar}/baz").match("test/bar/baz")).toBe(true); + expect(new Glob("test/{foo/**,bar}/baz").match("test/bar/test/baz")).toBe(false); + + expect(new Glob("*.txt").match("some/big/path/to/the/needle.txt")).toBe(false); + expect( + new Glob("some/**/needle.{js,tsx,mdx,ts,jsx,txt}").match("some/a/bigger/path/to/the/crazy/needle.txt"), + ).toBe(true); + expect(new Glob("some/**/{a,b,c}/**/needle.txt").match("some/foo/a/bigger/path/to/the/crazy/needle.txt")).toBe( + true, + ); + expect(new Glob("some/**/{a,b,c}/**/needle.txt").match("some/foo/d/bigger/path/to/the/crazy/needle.txt")).toBe( + false, + ); + expect(new Glob("a/{a{a,b},b}").match("a/aa")).toBe(true); + expect(new Glob("a/{a{a,b},b}").match("a/ab")).toBe(true); + expect(new Glob("a/{a{a,b},b}").match("a/ac")).toBe(false); + expect(new Glob("a/{a{a,b},b}").match("a/b")).toBe(true); + expect(new Glob("a/{a{a,b},b}").match("a/c")).toBe(false); + expect(new Glob("a/{b,c[}]*}").match("a/b")).toBe(true); + expect(new Glob("a/{b,c[}]*}").match("a/c}xx")).toBe(true); + }); + + // The below tests are based on Bash and micromatch. + // https://github.com/micromatch/picomatch/blob/master/test/bash.js + test("bash", () => { + expect(new Glob("a*").match("*")).toBeFalse(); + expect(new Glob("a*").match("**")).toBeFalse(); + expect(new Glob("a*").match("\\*")).toBeFalse(); + expect(new Glob("a*").match("a/*")).toBeFalse(); + expect(new Glob("a*").match("b")).toBeFalse(); + expect(new Glob("a*").match("bc")).toBeFalse(); + expect(new Glob("a*").match("bcd")).toBeFalse(); + expect(new Glob("a*").match("bdir/")).toBeFalse(); + expect(new Glob("a*").match("Beware")).toBeFalse(); + expect(new Glob("a*").match("a")).toBeTrue(); + expect(new Glob("a*").match("ab")).toBeTrue(); + expect(new Glob("a*").match("abc")).toBeTrue(); + + expect(new Glob("\\a*").match("*")).toBeFalse(); + expect(new Glob("\\a*").match("**")).toBeFalse(); + expect(new Glob("\\a*").match("\\*")).toBeFalse(); + + expect(new Glob("\\a*").match("a")).toBeTrue(); + expect(new Glob("\\a*").match("a/*")).toBeFalse(); + expect(new Glob("\\a*").match("abc")).toBeTrue(); + expect(new Glob("\\a*").match("abd")).toBeTrue(); + expect(new Glob("\\a*").match("abe")).toBeTrue(); + expect(new Glob("\\a*").match("b")).toBeFalse(); + expect(new Glob("\\a*").match("bb")).toBeFalse(); + expect(new Glob("\\a*").match("bcd")).toBeFalse(); + expect(new Glob("\\a*").match("bdir/")).toBeFalse(); + expect(new Glob("\\a*").match("Beware")).toBeFalse(); + expect(new Glob("\\a*").match("c")).toBeFalse(); + expect(new Glob("\\a*").match("ca")).toBeFalse(); + expect(new Glob("\\a*").match("cb")).toBeFalse(); + expect(new Glob("\\a*").match("d")).toBeFalse(); + expect(new Glob("\\a*").match("dd")).toBeFalse(); + expect(new Glob("\\a*").match("de")).toBeFalse(); + }); + + test("bash directories", () => { + expect(new Glob("b*/").match("*")).toBeFalse(); + expect(new Glob("b*/").match("**")).toBeFalse(); + expect(new Glob("b*/").match("\\*")).toBeFalse(); + expect(new Glob("b*/").match("a")).toBeFalse(); + expect(new Glob("b*/").match("a/*")).toBeFalse(); + expect(new Glob("b*/").match("abc")).toBeFalse(); + expect(new Glob("b*/").match("abd")).toBeFalse(); + expect(new Glob("b*/").match("abe")).toBeFalse(); + expect(new Glob("b*/").match("b")).toBeFalse(); + expect(new Glob("b*/").match("bb")).toBeFalse(); + expect(new Glob("b*/").match("bcd")).toBeFalse(); + expect(new Glob("b*/").match("bdir/")).toBeTrue(); + expect(new Glob("b*/").match("Beware")).toBeFalse(); + expect(new Glob("b*/").match("c")).toBeFalse(); + expect(new Glob("b*/").match("ca")).toBeFalse(); + expect(new Glob("b*/").match("cb")).toBeFalse(); + expect(new Glob("b*/").match("d")).toBeFalse(); + expect(new Glob("b*/").match("dd")).toBeFalse(); + expect(new Glob("b*/").match("de")).toBeFalse(); + }); + + test("bash escaping", () => { + expect(new Glob("\\^").match("*")).toBeFalse(); + expect(new Glob("\\^").match("**")).toBeFalse(); + expect(new Glob("\\^").match("\\*")).toBeFalse(); + expect(new Glob("\\^").match("a")).toBeFalse(); + expect(new Glob("\\^").match("a/*")).toBeFalse(); + expect(new Glob("\\^").match("abc")).toBeFalse(); + expect(new Glob("\\^").match("abd")).toBeFalse(); + expect(new Glob("\\^").match("abe")).toBeFalse(); + expect(new Glob("\\^").match("b")).toBeFalse(); + expect(new Glob("\\^").match("bb")).toBeFalse(); + expect(new Glob("\\^").match("bcd")).toBeFalse(); + expect(new Glob("\\^").match("bdir/")).toBeFalse(); + expect(new Glob("\\^").match("Beware")).toBeFalse(); + expect(new Glob("\\^").match("c")).toBeFalse(); + expect(new Glob("\\^").match("ca")).toBeFalse(); + expect(new Glob("\\^").match("cb")).toBeFalse(); + expect(new Glob("\\^").match("d")).toBeFalse(); + expect(new Glob("\\^").match("dd")).toBeFalse(); + expect(new Glob("\\^").match("de")).toBeFalse(); + + expect(new Glob("\\*").match("*")).toBeTrue(); + // expect(new Glob("\\*").match("\\*")).toBeTrue(); // This line is commented out in the original test + expect(new Glob("\\*").match("**")).toBeFalse(); + expect(new Glob("\\*").match("a")).toBeFalse(); + expect(new Glob("\\*").match("a/*")).toBeFalse(); + expect(new Glob("\\*").match("abc")).toBeFalse(); + expect(new Glob("\\*").match("abd")).toBeFalse(); + expect(new Glob("\\*").match("abe")).toBeFalse(); + expect(new Glob("\\*").match("b")).toBeFalse(); + expect(new Glob("\\*").match("bb")).toBeFalse(); + expect(new Glob("\\*").match("bcd")).toBeFalse(); + expect(new Glob("\\*").match("bdir/")).toBeFalse(); + expect(new Glob("\\*").match("Beware")).toBeFalse(); + expect(new Glob("\\*").match("c")).toBeFalse(); + expect(new Glob("\\*").match("ca")).toBeFalse(); + expect(new Glob("\\*").match("cb")).toBeFalse(); + expect(new Glob("\\*").match("d")).toBeFalse(); + expect(new Glob("\\*").match("dd")).toBeFalse(); + expect(new Glob("\\*").match("de")).toBeFalse(); + + expect(new Glob("a\\*").match("*")).toBeFalse(); + expect(new Glob("a\\*").match("**")).toBeFalse(); + expect(new Glob("a\\*").match("\\*")).toBeFalse(); + expect(new Glob("a\\*").match("a")).toBeFalse(); + expect(new Glob("a\\*").match("a/*")).toBeFalse(); + expect(new Glob("a\\*").match("abc")).toBeFalse(); + expect(new Glob("a\\*").match("abd")).toBeFalse(); + expect(new Glob("a\\*").match("abe")).toBeFalse(); + expect(new Glob("a\\*").match("b")).toBeFalse(); + expect(new Glob("a\\*").match("bb")).toBeFalse(); + expect(new Glob("a\\*").match("bcd")).toBeFalse(); + expect(new Glob("a\\*").match("bdir/")).toBeFalse(); + expect(new Glob("a\\*").match("Beware")).toBeFalse(); + expect(new Glob("a\\*").match("c")).toBeFalse(); + expect(new Glob("a\\*").match("ca")).toBeFalse(); + expect(new Glob("a\\*").match("cb")).toBeFalse(); + expect(new Glob("a\\*").match("d")).toBeFalse(); + expect(new Glob("a\\*").match("dd")).toBeFalse(); + expect(new Glob("a\\*").match("de")).toBeFalse(); + + expect(new Glob("*q*").match("aqa")).toBeTrue(); + expect(new Glob("*q*").match("aaqaa")).toBeTrue(); + expect(new Glob("*q*").match("*")).toBeFalse(); + expect(new Glob("*q*").match("**")).toBeFalse(); + expect(new Glob("*q*").match("\\*")).toBeFalse(); + expect(new Glob("*q*").match("a")).toBeFalse(); + expect(new Glob("*q*").match("a/*")).toBeFalse(); + expect(new Glob("*q*").match("abc")).toBeFalse(); + expect(new Glob("*q*").match("abd")).toBeFalse(); + expect(new Glob("*q*").match("abe")).toBeFalse(); + expect(new Glob("*q*").match("b")).toBeFalse(); + expect(new Glob("*q*").match("bb")).toBeFalse(); + expect(new Glob("*q*").match("bcd")).toBeFalse(); + expect(new Glob("*q*").match("bdir/")).toBeFalse(); + expect(new Glob("*q*").match("Beware")).toBeFalse(); + expect(new Glob("*q*").match("c")).toBeFalse(); + expect(new Glob("*q*").match("ca")).toBeFalse(); + expect(new Glob("*q*").match("cb")).toBeFalse(); + expect(new Glob("*q*").match("d")).toBeFalse(); + expect(new Glob("*q*").match("dd")).toBeFalse(); + expect(new Glob("*q*").match("de")).toBeFalse(); + + expect(new Glob("\\**").match("*")).toBeTrue(); + expect(new Glob("\\**").match("**")).toBeTrue(); + expect(new Glob("\\**").match("\\*")).toBeFalse(); + expect(new Glob("\\**").match("a")).toBeFalse(); + expect(new Glob("\\**").match("a/*")).toBeFalse(); + expect(new Glob("\\**").match("abc")).toBeFalse(); + expect(new Glob("\\**").match("abd")).toBeFalse(); + expect(new Glob("\\**").match("abe")).toBeFalse(); + expect(new Glob("\\**").match("b")).toBeFalse(); + expect(new Glob("\\**").match("bb")).toBeFalse(); + expect(new Glob("\\**").match("bcd")).toBeFalse(); + expect(new Glob("\\**").match("bdir/")).toBeFalse(); + expect(new Glob("\\**").match("Beware")).toBeFalse(); + expect(new Glob("\\**").match("c")).toBeFalse(); + expect(new Glob("\\**").match("ca")).toBeFalse(); + expect(new Glob("\\**").match("cb")).toBeFalse(); + expect(new Glob("\\**").match("d")).toBeFalse(); + expect(new Glob("\\**").match("dd")).toBeFalse(); + expect(new Glob("\\**").match("de")).toBeFalse(); + }); + + test("bash classes", () => { + expect(new Glob("a*[^c]").match("*")).toBeFalse(); + expect(new Glob("a*[^c]").match("**")).toBeFalse(); + expect(new Glob("a*[^c]").match("\\*")).toBeFalse(); + expect(new Glob("a*[^c]").match("a")).toBeFalse(); + expect(new Glob("a*[^c]").match("a/*")).toBeFalse(); + expect(new Glob("a*[^c]").match("abc")).toBeFalse(); + expect(new Glob("a*[^c]").match("abd")).toBeTrue(); + expect(new Glob("a*[^c]").match("abe")).toBeTrue(); + expect(new Glob("a*[^c]").match("b")).toBeFalse(); + expect(new Glob("a*[^c]").match("bb")).toBeFalse(); + expect(new Glob("a*[^c]").match("bcd")).toBeFalse(); + expect(new Glob("a*[^c]").match("bdir/")).toBeFalse(); + expect(new Glob("a*[^c]").match("Beware")).toBeFalse(); + expect(new Glob("a*[^c]").match("c")).toBeFalse(); + expect(new Glob("a*[^c]").match("ca")).toBeFalse(); + expect(new Glob("a*[^c]").match("cb")).toBeFalse(); + expect(new Glob("a*[^c]").match("d")).toBeFalse(); + expect(new Glob("a*[^c]").match("dd")).toBeFalse(); + expect(new Glob("a*[^c]").match("de")).toBeFalse(); + expect(new Glob("a*[^c]").match("baz")).toBeFalse(); + expect(new Glob("a*[^c]").match("bzz")).toBeFalse(); + expect(new Glob("a*[^c]").match("BZZ")).toBeFalse(); + expect(new Glob("a*[^c]").match("beware")).toBeFalse(); + expect(new Glob("a*[^c]").match("BewAre")).toBeFalse(); + expect(new Glob("a[X-]b").match("a-b")).toBeTrue(); + expect(new Glob("a[X-]b").match("aXb")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("*")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("a*")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("**")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("\\*")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("a")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("a123b")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("a123c")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("ab")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("a/*")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("abc")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("abd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("abe")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("b")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("bd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("bb")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("bcd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("bdir/")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("Beware")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("c")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("ca")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("cb")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("d")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("dd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("dd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("dd")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("de")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("baz")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("bzz")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("bzz")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("BZZ")).toBeFalse(); + expect(new Glob("[a-y]*[^c]").match("beware")).toBeTrue(); + expect(new Glob("[a-y]*[^c]").match("BewAre")).toBeFalse(); + expect(new Glob("a\\*b/*").match("a*b/ooo")).toBeTrue(); + expect(new Glob("a\\*?/*").match("a*b/ooo")).toBeTrue(); + expect(new Glob("a[b]c").match("*")).toBeFalse(); + expect(new Glob("a[b]c").match("**")).toBeFalse(); + expect(new Glob("a[b]c").match("\\*")).toBeFalse(); + expect(new Glob("a[b]c").match("a")).toBeFalse(); + expect(new Glob("a[b]c").match("a/*")).toBeFalse(); + expect(new Glob("a[b]c").match("abc")).toBeTrue(); + expect(new Glob("a[b]c").match("abd")).toBeFalse(); + expect(new Glob("a[b]c").match("abe")).toBeFalse(); + expect(new Glob("a[b]c").match("b")).toBeFalse(); + expect(new Glob("a[b]c").match("bb")).toBeFalse(); + expect(new Glob("a[b]c").match("bcd")).toBeFalse(); + expect(new Glob("a[b]c").match("bdir/")).toBeFalse(); + expect(new Glob("a[b]c").match("Beware")).toBeFalse(); + expect(new Glob("a[b]c").match("c")).toBeFalse(); + expect(new Glob("a[b]c").match("ca")).toBeFalse(); + expect(new Glob("a[b]c").match("cb")).toBeFalse(); + expect(new Glob("a[b]c").match("d")).toBeFalse(); + expect(new Glob("a[b]c").match("dd")).toBeFalse(); + expect(new Glob("a[b]c").match("de")).toBeFalse(); + expect(new Glob("a[b]c").match("baz")).toBeFalse(); + expect(new Glob("a[b]c").match("bzz")).toBeFalse(); + expect(new Glob("a[b]c").match("BZZ")).toBeFalse(); + expect(new Glob("a[b]c").match("beware")).toBeFalse(); + expect(new Glob("a[b]c").match("BewAre")).toBeFalse(); + expect(new Glob('a["b"]c').match("*")).toBeFalse(); + expect(new Glob('a["b"]c').match("**")).toBeFalse(); + expect(new Glob('a["b"]c').match("\\*")).toBeFalse(); + expect(new Glob('a["b"]c').match("a")).toBeFalse(); + expect(new Glob('a["b"]c').match("a/*")).toBeFalse(); + expect(new Glob('a["b"]c').match("abc")).toBeTrue(); + expect(new Glob('a["b"]c').match("abd")).toBeFalse(); + expect(new Glob('a["b"]c').match("abe")).toBeFalse(); + expect(new Glob('a["b"]c').match("b")).toBeFalse(); + expect(new Glob('a["b"]c').match("bb")).toBeFalse(); + expect(new Glob('a["b"]c').match("bcd")).toBeFalse(); + expect(new Glob('a["b"]c').match("bdir/")).toBeFalse(); + expect(new Glob('a["b"]c').match("Beware")).toBeFalse(); + expect(new Glob('a["b"]c').match("c")).toBeFalse(); + expect(new Glob('a["b"]c').match("ca")).toBeFalse(); + expect(new Glob('a["b"]c').match("cb")).toBeFalse(); + expect(new Glob('a["b"]c').match("d")).toBeFalse(); + expect(new Glob('a["b"]c').match("dd")).toBeFalse(); + expect(new Glob('a["b"]c').match("de")).toBeFalse(); + expect(new Glob('a["b"]c').match("baz")).toBeFalse(); + expect(new Glob('a["b"]c').match("bzz")).toBeFalse(); + expect(new Glob('a["b"]c').match("BZZ")).toBeFalse(); + expect(new Glob('a["b"]c').match("beware")).toBeFalse(); + expect(new Glob('a["b"]c').match("BewAre")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("*")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("**")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("\\*")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("a")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("a/*")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("abc")).toBeTrue(); + expect(new Glob("a[\\\\b]c").match("abd")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("abe")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("b")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("bb")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("bcd")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("bdir/")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("Beware")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("c")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("ca")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("cb")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("d")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("dd")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("de")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("baz")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("bzz")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("BZZ")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("beware")).toBeFalse(); + expect(new Glob("a[\\\\b]c").match("BewAre")).toBeFalse(); + expect(new Glob("a[\\b]c").match("*")).toBeFalse(); + expect(new Glob("a[\\b]c").match("**")).toBeFalse(); + expect(new Glob("a[\\b]c").match("\\*")).toBeFalse(); + expect(new Glob("a[\\b]c").match("a")).toBeFalse(); + expect(new Glob("a[\\b]c").match("a/*")).toBeFalse(); + expect(new Glob("a[\\b]c").match("abc")).toBeFalse(); + expect(new Glob("a[\\b]c").match("abd")).toBeFalse(); + expect(new Glob("a[\\b]c").match("abe")).toBeFalse(); + expect(new Glob("a[\\b]c").match("b")).toBeFalse(); + expect(new Glob("a[\\b]c").match("bb")).toBeFalse(); + expect(new Glob("a[\\b]c").match("bcd")).toBeFalse(); + expect(new Glob("a[\\b]c").match("bdir/")).toBeFalse(); + expect(new Glob("a[\\b]c").match("Beware")).toBeFalse(); + expect(new Glob("a[\\b]c").match("c")).toBeFalse(); + expect(new Glob("a[\\b]c").match("ca")).toBeFalse(); + expect(new Glob("a[\\b]c").match("cb")).toBeFalse(); + expect(new Glob("a[\\b]c").match("d")).toBeFalse(); + expect(new Glob("a[\\b]c").match("dd")).toBeFalse(); + expect(new Glob("a[\\b]c").match("de")).toBeFalse(); + expect(new Glob("a[\\b]c").match("baz")).toBeFalse(); + expect(new Glob("a[\\b]c").match("bzz")).toBeFalse(); + expect(new Glob("a[\\b]c").match("BZZ")).toBeFalse(); + expect(new Glob("a[\\b]c").match("beware")).toBeFalse(); + expect(new Glob("a[\\b]c").match("BewAre")).toBeFalse(); + expect(new Glob("a[b-d]c").match("*")).toBeFalse(); + expect(new Glob("a[b-d]c").match("**")).toBeFalse(); + expect(new Glob("a[b-d]c").match("\\*")).toBeFalse(); + expect(new Glob("a[b-d]c").match("a")).toBeFalse(); + expect(new Glob("a[b-d]c").match("a/*")).toBeFalse(); + expect(new Glob("a[b-d]c").match("abc")).toBeTrue(); + expect(new Glob("a[b-d]c").match("abd")).toBeFalse(); + expect(new Glob("a[b-d]c").match("abe")).toBeFalse(); + expect(new Glob("a[b-d]c").match("b")).toBeFalse(); + expect(new Glob("a[b-d]c").match("bb")).toBeFalse(); + expect(new Glob("a[b-d]c").match("bcd")).toBeFalse(); + expect(new Glob("a[b-d]c").match("bdir/")).toBeFalse(); + expect(new Glob("a[b-d]c").match("Beware")).toBeFalse(); + expect(new Glob("a[b-d]c").match("c")).toBeFalse(); + expect(new Glob("a[b-d]c").match("ca")).toBeFalse(); + expect(new Glob("a[b-d]c").match("cb")).toBeFalse(); + expect(new Glob("a[b-d]c").match("d")).toBeFalse(); + expect(new Glob("a[b-d]c").match("dd")).toBeFalse(); + expect(new Glob("a[b-d]c").match("de")).toBeFalse(); + expect(new Glob("a[b-d]c").match("baz")).toBeFalse(); + expect(new Glob("a[b-d]c").match("bzz")).toBeFalse(); + expect(new Glob("a[b-d]c").match("BZZ")).toBeFalse(); + expect(new Glob("a[b-d]c").match("beware")).toBeFalse(); + expect(new Glob("a[b-d]c").match("BewAre")).toBeFalse(); + expect(new Glob("a?c").match("*")).toBeFalse(); + expect(new Glob("a?c").match("**")).toBeFalse(); + expect(new Glob("a?c").match("\\*")).toBeFalse(); + expect(new Glob("a?c").match("a")).toBeFalse(); + expect(new Glob("a?c").match("a/*")).toBeFalse(); + expect(new Glob("a?c").match("abc")).toBeTrue(); + expect(new Glob("a?c").match("abd")).toBeFalse(); + expect(new Glob("a?c").match("abe")).toBeFalse(); + expect(new Glob("a?c").match("b")).toBeFalse(); + expect(new Glob("a?c").match("bb")).toBeFalse(); + expect(new Glob("a?c").match("bcd")).toBeFalse(); + expect(new Glob("a?c").match("bdir/")).toBeFalse(); + expect(new Glob("a?c").match("Beware")).toBeFalse(); + expect(new Glob("a?c").match("c")).toBeFalse(); + expect(new Glob("a?c").match("ca")).toBeFalse(); + expect(new Glob("a?c").match("cb")).toBeFalse(); + expect(new Glob("a?c").match("d")).toBeFalse(); + expect(new Glob("a?c").match("dd")).toBeFalse(); + expect(new Glob("a?c").match("de")).toBeFalse(); + expect(new Glob("a?c").match("baz")).toBeFalse(); + expect(new Glob("a?c").match("bzz")).toBeFalse(); + expect(new Glob("a?c").match("BZZ")).toBeFalse(); + expect(new Glob("a?c").match("beware")).toBeFalse(); + expect(new Glob("a?c").match("BewAre")).toBeFalse(); + expect(new Glob("*/man*/bash.*").match("man/man1/bash.1")).toBeTrue(); + expect(new Glob("[^a-c]*").match("*")).toBeTrue(); + expect(new Glob("[^a-c]*").match("**")).toBeTrue(); + expect(new Glob("[^a-c]*").match("a")).toBeFalse(); + expect(new Glob("[^a-c]*").match("a/*")).toBeFalse(); + expect(new Glob("[^a-c]*").match("abc")).toBeFalse(); + expect(new Glob("[^a-c]*").match("abd")).toBeFalse(); + expect(new Glob("[^a-c]*").match("abe")).toBeFalse(); + expect(new Glob("[^a-c]*").match("b")).toBeFalse(); + expect(new Glob("[^a-c]*").match("bb")).toBeFalse(); + expect(new Glob("[^a-c]*").match("bcd")).toBeFalse(); + expect(new Glob("[^a-c]*").match("bdir/")).toBeFalse(); + expect(new Glob("[^a-c]*").match("Beware")).toBeTrue(); + expect(new Glob("[^a-c]*").match("Beware")).toBeTrue(); + expect(new Glob("[^a-c]*").match("c")).toBeFalse(); + expect(new Glob("[^a-c]*").match("ca")).toBeFalse(); + expect(new Glob("[^a-c]*").match("cb")).toBeFalse(); + expect(new Glob("[^a-c]*").match("d")).toBeTrue(); + expect(new Glob("[^a-c]*").match("dd")).toBeTrue(); + expect(new Glob("[^a-c]*").match("de")).toBeTrue(); + expect(new Glob("[^a-c]*").match("baz")).toBeFalse(); + expect(new Glob("[^a-c]*").match("bzz")).toBeFalse(); + expect(new Glob("[^a-c]*").match("BZZ")).toBeTrue(); + expect(new Glob("[^a-c]*").match("beware")).toBeFalse(); + expect(new Glob("[^a-c]*").match("BewAre")).toBeTrue(); + }); + + test("bash wildmatch", () => { + expect(new Glob("a[]-]b").match("aab")).toBeFalse(); + expect(new Glob("[ten]").match("ten")).toBeFalse(); + expect(new Glob("]").match("]")).toBeTrue(); + expect(new Glob("a[]-]b").match("a-b")).toBeTrue(); + expect(new Glob("a[]-]b").match("a]b")).toBeTrue(); + expect(new Glob("a[]]b").match("a]b")).toBeTrue(); + expect(new Glob("a[\\]a\\-]b").match("aab")).toBeTrue(); + expect(new Glob("t[a-g]n").match("ten")).toBeTrue(); + expect(new Glob("t[^a-g]n").match("ton")).toBeTrue(); + }); + + test("bash slashmatch", () => { + expect(new Glob("foo[/]bar").match("foo/bar")).toBeTrue(); + expect(new Glob("f[^eiu][^eiu][^eiu][^eiu][^eiu]r").match("foo-bar")).toBeTrue(); + }); + + test("bash extra_stars", () => { + expect(new Glob("a**c").match("bbc")).toBeFalse(); + expect(new Glob("a**c").match("abc")).toBeTrue(); + expect(new Glob("a**c").match("bbd")).toBeFalse(); + expect(new Glob("a***c").match("bbc")).toBeFalse(); + expect(new Glob("a***c").match("abc")).toBeTrue(); + expect(new Glob("a***c").match("bbd")).toBeFalse(); + expect(new Glob("a*****?c").match("bbc")).toBeFalse(); + expect(new Glob("a*****?c").match("abc")).toBeTrue(); + expect(new Glob("a*****?c").match("bbc")).toBeFalse(); + expect(new Glob("?*****??").match("bbc")).toBeTrue(); + expect(new Glob("?*****??").match("abc")).toBeTrue(); + expect(new Glob("*****??").match("bbc")).toBeTrue(); + expect(new Glob("*****??").match("abc")).toBeTrue(); + expect(new Glob("?*****?c").match("bbc")).toBeTrue(); + expect(new Glob("?*****?c").match("abc")).toBeTrue(); + expect(new Glob("?***?****c").match("bbc")).toBeTrue(); + expect(new Glob("?***?****c").match("abc")).toBeTrue(); + expect(new Glob("?***?****c").match("bbd")).toBeFalse(); + expect(new Glob("?***?****?").match("bbc")).toBeTrue(); + expect(new Glob("?***?****?").match("abc")).toBeTrue(); + expect(new Glob("?***?****").match("bbc")).toBeTrue(); + expect(new Glob("?***?****").match("abc")).toBeTrue(); + expect(new Glob("*******c").match("bbc")).toBeTrue(); + expect(new Glob("*******c").match("abc")).toBeTrue(); + expect(new Glob("*******?").match("bbc")).toBeTrue(); + expect(new Glob("*******?").match("abc")).toBeTrue(); + expect(new Glob("a*cd**?**??k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??k***").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??***k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??***k**").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a****c**?**??*****").match("abcdecdhjk")).toBeTrue(); + }); + + test("stars", () => { + expect(new Glob("*.js").match("a/b/c/z.js")).toBeFalse(); + expect(new Glob("*.js").match("a/b/z.js")).toBeFalse(); + expect(new Glob("*.js").match("a/z.js")).toBeFalse(); + expect(new Glob("*.js").match("z.js")).toBeTrue(); + expect(new Glob("z*.js").match("z.js")).toBeTrue(); + expect(new Glob("*/*").match("a/z")).toBeTrue(); + expect(new Glob("*/z*.js").match("a/z.js")).toBeTrue(); + expect(new Glob("a/z*.js").match("a/z.js")).toBeTrue(); + expect(new Glob("*").match("ab")).toBeTrue(); + expect(new Glob("*").match("abc")).toBeTrue(); + expect(new Glob("f*").match("bar")).toBeFalse(); + expect(new Glob("*r").match("foo")).toBeFalse(); + expect(new Glob("b*").match("foo")).toBeFalse(); + expect(new Glob("*").match("foo/bar")).toBeFalse(); + expect(new Glob("*c").match("abc")).toBeTrue(); + expect(new Glob("a*").match("abc")).toBeTrue(); + expect(new Glob("a*c").match("abc")).toBeTrue(); + expect(new Glob("*r").match("bar")).toBeTrue(); + expect(new Glob("b*").match("bar")).toBeTrue(); + expect(new Glob("f*").match("foo")).toBeTrue(); + expect(new Glob("*abc*").match("one abc two")).toBeTrue(); + expect(new Glob("a*b").match("a b")).toBeTrue(); + expect(new Glob("*a*").match("foo")).toBeFalse(); + expect(new Glob("*a*").match("bar")).toBeTrue(); + expect(new Glob("*abc*").match("oneabctwo")).toBeTrue(); + expect(new Glob("*-bc-*").match("a-b.c-d")).toBeFalse(); + expect(new Glob("*-*.*-*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*-b*c-*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*-b.c-*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*.*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*.*-*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*.*-d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*.c-*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*b.*d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("a*.c*").match("a-b.c-d")).toBeTrue(); + expect(new Glob("a-*.*-d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("*.*").match("a.b")).toBeTrue(); + expect(new Glob("*.b").match("a.b")).toBeTrue(); + expect(new Glob("a.*").match("a.b")).toBeTrue(); + expect(new Glob("a.b").match("a.b")).toBeTrue(); + expect(new Glob("**-bc-**").match("a-b.c-d")).toBeFalse(); + expect(new Glob("**-**.**-**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**-b**c-**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**-b.c-**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**.**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**.**-**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**.**-d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**.c-**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**b.**d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("a**.c**").match("a-b.c-d")).toBeTrue(); + expect(new Glob("a-**.**-d").match("a-b.c-d")).toBeTrue(); + expect(new Glob("**.**").match("a.b")).toBeTrue(); + expect(new Glob("**.b").match("a.b")).toBeTrue(); + expect(new Glob("a.**").match("a.b")).toBeTrue(); + expect(new Glob("a.b").match("a.b")).toBeTrue(); + expect(new Glob("*/*").match("/ab")).toBeTrue(); + expect(new Glob(".").match(".")).toBeTrue(); + expect(new Glob("a/").match("a/.b")).toBeFalse(); + expect(new Glob("/*").match("/ab")).toBeTrue(); + expect(new Glob("/??").match("/ab")).toBeTrue(); + expect(new Glob("/?b").match("/ab")).toBeTrue(); + expect(new Glob("/*").match("/cd")).toBeTrue(); + expect(new Glob("a").match("a")).toBeTrue(); + expect(new Glob("a/.*").match("a/.b")).toBeTrue(); + expect(new Glob("?/?").match("a/b")).toBeTrue(); + expect(new Glob("a/**/j/**/z/*.md").match("a/b/c/d/e/j/n/p/o/z/c.md")).toBeTrue(); + expect(new Glob("a/**/z/*.md").match("a/b/c/d/e/z/c.md")).toBeTrue(); + expect(new Glob("a/b/c/*.md").match("a/b/c/xyz.md")).toBeTrue(); + expect(new Glob("a/b/c/*.md").match("a/b/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/z/.a").match("a/b/z/.a")).toBeTrue(); + expect(new Glob("bz").match("a/b/z/.a")).toBeFalse(); + expect(new Glob("a/**/c/*.md").match("a/bb.bb/aa/b.b/aa/c/xyz.md")).toBeTrue(); + expect(new Glob("a/**/c/*.md").match("a/bb.bb/aa/bb/aa/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb.bb/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bbbb/c/xyz.md")).toBeTrue(); + expect(new Glob("*").match("aaa")).toBeTrue(); + expect(new Glob("*").match("ab")).toBeTrue(); + expect(new Glob("ab").match("ab")).toBeTrue(); + expect(new Glob("*/*/*").match("aaa")).toBeFalse(); + expect(new Glob("*/*/*").match("aaa/bb/aa/rr")).toBeFalse(); + expect(new Glob("aaa*").match("aaa/bba/ccc")).toBeFalse(); + expect(new Glob("aaa/*").match("aaa/bba/ccc")).toBeFalse(); + expect(new Glob("aaa/*ccc").match("aaa/bba/ccc")).toBeFalse(); + expect(new Glob("aaa/*z").match("aaa/bba/ccc")).toBeFalse(); + expect(new Glob("*/*/*").match("aaa/bbb")).toBeFalse(); + expect(new Glob("*/*jk*/*i").match("ab/zzz/ejkl/hi")).toBeFalse(); + expect(new Glob("*/*/*").match("aaa/bba/ccc")).toBeTrue(); + expect(new Glob("aaa/**").match("aaa/bba/ccc")).toBeTrue(); + expect(new Glob("aaa/*").match("aaa/bbb")).toBeTrue(); + expect(new Glob("*/*z*/*/*i").match("ab/zzz/ejkl/hi")).toBeTrue(); + expect(new Glob("*j*i").match("abzzzejklhi")).toBeTrue(); + expect(new Glob("*").match("a")).toBeTrue(); + expect(new Glob("*").match("b")).toBeTrue(); + expect(new Glob("*").match("a/a")).toBeFalse(); + expect(new Glob("*").match("a/a/a")).toBeFalse(); + expect(new Glob("*").match("a/a/b")).toBeFalse(); + expect(new Glob("*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("*").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("*/*").match("a")).toBeFalse(); + expect(new Glob("*/*").match("a/a")).toBeTrue(); + expect(new Glob("*/*").match("a/a/a")).toBeFalse(); + expect(new Glob("*/*/*").match("a")).toBeFalse(); + expect(new Glob("*/*/*").match("a/a")).toBeFalse(); + expect(new Glob("*/*/*").match("a/a/a")).toBeTrue(); + expect(new Glob("*/*/*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("*/*/*/*").match("a")).toBeFalse(); + expect(new Glob("*/*/*/*").match("a/a")).toBeFalse(); + expect(new Glob("*/*/*/*").match("a/a/a")).toBeFalse(); + expect(new Glob("*/*/*/*").match("a/a/a/a")).toBeTrue(); + expect(new Glob("*/*/*/*").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a/a")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a/a/a")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a/a/b")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("*/*/*/*/*").match("a/a/a/a/a")).toBeTrue(); + expect(new Glob("*/*/*/*/*").match("a/a/a/a/a/a")).toBeFalse(); + expect(new Glob("a/*").match("a")).toBeFalse(); + expect(new Glob("a/*").match("a/a")).toBeTrue(); + expect(new Glob("a/*").match("a/a/a")).toBeFalse(); + expect(new Glob("a/*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("a/*").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/*").match("a")).toBeFalse(); + expect(new Glob("a/*/*").match("a/a")).toBeFalse(); + expect(new Glob("a/*/*").match("a/a/a")).toBeTrue(); + expect(new Glob("a/*/*").match("b/a/a")).toBeFalse(); + expect(new Glob("a/*/*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/*").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/*/*").match("a")).toBeFalse(); + expect(new Glob("a/*/*/*").match("a/a")).toBeFalse(); + expect(new Glob("a/*/*/*").match("a/a/a")).toBeFalse(); + expect(new Glob("a/*/*/*").match("a/a/a/a")).toBeTrue(); + expect(new Glob("a/*/*/*").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a/a")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a/a/a")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a/a/b")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/*/*/*").match("a/a/a/a/a")).toBeTrue(); + expect(new Glob("a/*/a").match("a")).toBeFalse(); + expect(new Glob("a/*/a").match("a/a")).toBeFalse(); + expect(new Glob("a/*/a").match("a/a/a")).toBeTrue(); + expect(new Glob("a/*/a").match("a/a/b")).toBeFalse(); + expect(new Glob("a/*/a").match("a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/a").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/b").match("a")).toBeFalse(); + expect(new Glob("a/*/b").match("a/a")).toBeFalse(); + expect(new Glob("a/*/b").match("a/a/a")).toBeFalse(); + expect(new Glob("a/*/b").match("a/a/b")).toBeTrue(); + expect(new Glob("a/*/b").match("a/a/a/a")).toBeFalse(); + expect(new Glob("a/*/b").match("a/a/a/a/a")).toBeFalse(); + expect(new Glob("*/**/a").match("a")).toBeFalse(); + expect(new Glob("*/**/a").match("a/a/b")).toBeFalse(); + expect(new Glob("*/**/a").match("a/a")).toBeTrue(); + expect(new Glob("*/**/a").match("a/a/a")).toBeTrue(); + expect(new Glob("*/**/a").match("a/a/a/a")).toBeTrue(); + expect(new Glob("*/**/a").match("a/a/a/a/a")).toBeTrue(); + expect(new Glob("*/").match("a")).toBeFalse(); + expect(new Glob("*/*").match("a")).toBeFalse(); + expect(new Glob("a/*").match("a")).toBeFalse(); + expect(new Glob("*").match("a/a")).toBeFalse(); + expect(new Glob("*/").match("a/a")).toBeFalse(); + expect(new Glob("*/").match("a/x/y")).toBeFalse(); + expect(new Glob("*/*").match("a/x/y")).toBeFalse(); + expect(new Glob("a/*").match("a/x/y")).toBeFalse(); + expect(new Glob("*").match("a")).toBeTrue(); + expect(new Glob("*/").match("a/")).toBeTrue(); + expect(new Glob("*{,/}").match("a/")).toBeTrue(); + expect(new Glob("*/*").match("a/a")).toBeTrue(); + expect(new Glob("a/*").match("a/a")).toBeTrue(); + expect(new Glob("a/**/*.txt").match("a.txt")).toBeFalse(); + expect(new Glob("a/**/*.txt").match("a/x/y.txt")).toBeTrue(); + expect(new Glob("a/**/*.txt").match("a/x/y/z")).toBeFalse(); + expect(new Glob("a/*.txt").match("a.txt")).toBeFalse(); + expect(new Glob("a/*.txt").match("a/b.txt")).toBeTrue(); + expect(new Glob("a/*.txt").match("a/x/y.txt")).toBeFalse(); + expect(new Glob("a/*.txt").match("a/x/y/z")).toBeFalse(); + expect(new Glob("a*.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a*.txt").match("a/b.txt")).toBeFalse(); + expect(new Glob("a*.txt").match("a/x/y.txt")).toBeFalse(); + expect(new Glob("a*.txt").match("a/x/y/z")).toBeFalse(); + expect(new Glob("*.txt").match("a.txt")).toBeTrue(); + expect(new Glob("*.txt").match("a/b.txt")).toBeFalse(); + expect(new Glob("*.txt").match("a/x/y.txt")).toBeFalse(); + expect(new Glob("*.txt").match("a/x/y/z")).toBeFalse(); + expect(new Glob("a*").match("a/b")).toBeFalse(); + expect(new Glob("a/**/b").match("a/a/bb")).toBeFalse(); + expect(new Glob("a/**/b").match("a/bb")).toBeFalse(); + expect(new Glob("*/**").match("foo")).toBeFalse(); + expect(new Glob("**/").match("foo/bar")).toBeFalse(); + expect(new Glob("**/*/").match("foo/bar")).toBeFalse(); + expect(new Glob("*/*/").match("foo/bar")).toBeFalse(); + expect(new Glob("**/..").match("/home/foo/..")).toBeTrue(); + expect(new Glob("**/a").match("a")).toBeTrue(); + expect(new Glob("**").match("a/a")).toBeTrue(); + expect(new Glob("a/**").match("a/a")).toBeTrue(); + expect(new Glob("a/**").match("a/")).toBeTrue(); + expect(new Glob("**/").match("a/a")).toBeFalse(); + expect(new Glob("**/").match("a/a")).toBeFalse(); + expect(new Glob("*/**/a").match("a/a")).toBeTrue(); + expect(new Glob("*/**").match("foo/")).toBeTrue(); + expect(new Glob("**/*").match("foo/bar")).toBeTrue(); + expect(new Glob("*/*").match("foo/bar")).toBeTrue(); + expect(new Glob("*/**").match("foo/bar")).toBeTrue(); + expect(new Glob("**/").match("foo/bar/")).toBeTrue(); + expect(new Glob("**/*/").match("foo/bar/")).toBeTrue(); + expect(new Glob("*/**").match("foo/bar/")).toBeTrue(); + expect(new Glob("*/*/").match("foo/bar/")).toBeTrue(); + expect(new Glob("*/foo").match("bar/baz/foo")).toBeFalse(); + expect(new Glob("**/bar/*").match("deep/foo/bar")).toBeFalse(); + expect(new Glob("*/bar/**").match("deep/foo/bar/baz/x")).toBeFalse(); + expect(new Glob("/*").match("ef")).toBeFalse(); + expect(new Glob("foo?bar").match("foo/bar")).toBeFalse(); + expect(new Glob("**/bar*").match("foo/bar/baz")).toBeFalse(); + expect(new Glob("foo**bar").match("foo/baz/bar")).toBeFalse(); + expect(new Glob("foo*bar").match("foo/baz/bar")).toBeFalse(); + expect(new Glob("/*").match("/ab")).toBeTrue(); + expect(new Glob("/*").match("/cd")).toBeTrue(); + expect(new Glob("/*").match("/ef")).toBeTrue(); + expect(new Glob("a/**/j/**/z/*.md").match("a/b/j/c/z/x.md")).toBeTrue(); + expect(new Glob("a/**/j/**/z/*.md").match("a/j/z/x.md")).toBeTrue(); + expect(new Glob("**/foo").match("bar/baz/foo")).toBeTrue(); + expect(new Glob("**/bar/*").match("deep/foo/bar/baz")).toBeTrue(); + expect(new Glob("**/bar/**").match("deep/foo/bar/baz/")).toBeTrue(); + expect(new Glob("**/bar/*/*").match("deep/foo/bar/baz/x")).toBeTrue(); + expect(new Glob("foo/**/**/bar").match("foo/b/a/z/bar")).toBeTrue(); + expect(new Glob("foo/**/bar").match("foo/b/a/z/bar")).toBeTrue(); + expect(new Glob("foo/**/**/bar").match("foo/bar")).toBeTrue(); + expect(new Glob("foo/**/bar").match("foo/bar")).toBeTrue(); + expect(new Glob("*/bar/**").match("foo/bar/baz/x")).toBeTrue(); + expect(new Glob("foo/**/**/bar").match("foo/baz/bar")).toBeTrue(); + expect(new Glob("foo/**/bar").match("foo/baz/bar")).toBeTrue(); + expect(new Glob("**/foo").match("XXX/foo")).toBeTrue(); + }); + + test("globstars", () => { + expect(new Glob("**/*.js").match("a/b/c/d.js")).toBeTrue(); + expect(new Glob("**/*.js").match("a/b/c.js")).toBeTrue(); + expect(new Glob("**/*.js").match("a/b.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/c/d/e/f.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/c/d/e.js")).toBeTrue(); + expect(new Glob("a/b/c/**/*.js").match("a/b/c/d.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/c/d.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/d.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/d.js")).toBeFalse(); + expect(new Glob("a/b/**/*.js").match("d.js")).toBeFalse(); + expect(new Glob("**c").match("a/b/c")).toBeFalse(); + expect(new Glob("a/**c").match("a/b/c")).toBeFalse(); + expect(new Glob("a/**z").match("a/b/c")).toBeFalse(); + expect(new Glob("a/**b**/c").match("a/b/c/b/c")).toBeFalse(); + expect(new Glob("a/b/c**/*.js").match("a/b/c/d/e.js")).toBeFalse(); + expect(new Glob("a/**/b/**/c").match("a/b/c/b/c")).toBeTrue(); + expect(new Glob("a/**b**/c").match("a/aba/c")).toBeTrue(); + expect(new Glob("a/**b**/c").match("a/b/c")).toBeTrue(); + expect(new Glob("a/b/c**/*.js").match("a/b/c/d.js")).toBeTrue(); + expect(new Glob("a/**/*").match("a")).toBeFalse(); + expect(new Glob("a/**/**/*").match("a")).toBeFalse(); + expect(new Glob("a/**/**/**/*").match("a")).toBeFalse(); + expect(new Glob("**/a").match("a/")).toBeFalse(); + expect(new Glob("a/**/*").match("a/")).toBeFalse(); + expect(new Glob("a/**/**/*").match("a/")).toBeFalse(); + expect(new Glob("a/**/**/**/*").match("a/")).toBeFalse(); + expect(new Glob("**/a").match("a/b")).toBeFalse(); + expect(new Glob("a/**/j/**/z/*.md").match("a/b/c/j/e/z/c.txt")).toBeFalse(); + expect(new Glob("a/**/b").match("a/bb")).toBeFalse(); + expect(new Glob("**/a").match("a/c")).toBeFalse(); + expect(new Glob("**/a").match("a/b")).toBeFalse(); + expect(new Glob("**/a").match("a/x/y")).toBeFalse(); + expect(new Glob("**/a").match("a/b/c/d")).toBeFalse(); + expect(new Glob("**").match("a")).toBeTrue(); + expect(new Glob("**/a").match("a")).toBeTrue(); + expect(new Glob("**").match("a/")).toBeTrue(); + expect(new Glob("**/a/**").match("a/")).toBeTrue(); + expect(new Glob("a/**").match("a/")).toBeTrue(); + expect(new Glob("a/**/**").match("a/")).toBeTrue(); + expect(new Glob("**/a").match("a/a")).toBeTrue(); + expect(new Glob("**").match("a/b")).toBeTrue(); + expect(new Glob("*/*").match("a/b")).toBeTrue(); + expect(new Glob("a/**").match("a/b")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b")).toBeTrue(); + expect(new Glob("a/**/**/**/*").match("a/b")).toBeTrue(); + expect(new Glob("a/**/b").match("a/b")).toBeTrue(); + expect(new Glob("**").match("a/b/c")).toBeTrue(); + expect(new Glob("**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**/**").match("a/b/c")).toBeTrue(); + expect(new Glob("*/**").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/**/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**/**/**/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/b/**/c/**/*.*").match("a/b/c/d.e")).toBeTrue(); + expect(new Glob("a/**/f/*.md").match("a/b/c/d/e/f/g.md")).toBeTrue(); + expect(new Glob("a/**/f/**/k/*.md").match("a/b/c/d/e/f/g/h/i/j/k/l.md")).toBeTrue(); + expect(new Glob("a/b/c/*.md").match("a/b/c/def.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb.bb/c/ddd.md")).toBeTrue(); + expect(new Glob("a/**/f/*.md").match("a/bb.bb/cc/d.d/ee/f/ggg.md")).toBeTrue(); + expect(new Glob("a/**/f/*.md").match("a/bb.bb/cc/dd/ee/f/ggg.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb/c/ddd.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bbbb/c/ddd.md")).toBeTrue(); + expect(new Glob("foo/bar/**/one/**/*.*").match("foo/bar/baz/one/image.png")).toBeTrue(); + expect(new Glob("foo/bar/**/one/**/*.*").match("foo/bar/baz/one/two/image.png")).toBeTrue(); + expect(new Glob("foo/bar/**/one/**/*.*").match("foo/bar/baz/one/two/three/image.png")).toBeTrue(); + expect(new Glob("a/b/**/f").match("a/b/c/d/")).toBeFalse(); + expect(new Glob("**").match("a")).toBeTrue(); + expect(new Glob("**").match("a/")).toBeTrue(); + expect(new Glob("a/**").match("a/")).toBeTrue(); + expect(new Glob("**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("**/**").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("**/b/**").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("a/b/**").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("a/b/**/").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("a/b/**/c/**/").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("a/b/**/c/**/d/").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("a/b/**/**/*.*").match("a/b/c/d/e.f")).toBeTrue(); + expect(new Glob("a/b/**/*.*").match("a/b/c/d/e.f")).toBeTrue(); + expect(new Glob("a/b/**/c/**/d/*.*").match("a/b/c/d/e.f")).toBeTrue(); + expect(new Glob("a/b/**/d/**/*.*").match("a/b/c/d/e.f")).toBeTrue(); + expect(new Glob("a/b/**/d/**/*.*").match("a/b/c/d/g/e.f")).toBeTrue(); + expect(new Glob("a/b/**/d/**/*.*").match("a/b/c/d/g/g/e.f")).toBeTrue(); + expect(new Glob("a/b-*/**/z.js").match("a/b-c/z.js")).toBeTrue(); + expect(new Glob("a/b-*/**/z.js").match("a/b-c/d/e/z.js")).toBeTrue(); + expect(new Glob("*/*").match("a/b")).toBeTrue(); + expect(new Glob("a/b/c/*.md").match("a/b/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb.bb/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bb/c/xyz.md")).toBeTrue(); + expect(new Glob("a/*/c/*.md").match("a/bbbb/c/xyz.md")).toBeTrue(); + expect(new Glob("**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**/**").match("a/b/c")).toBeTrue(); + expect(new Glob("*/**").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/j/**/z/*.md").match("a/b/c/d/e/j/n/p/o/z/c.md")).toBeTrue(); + expect(new Glob("a/**/z/*.md").match("a/b/c/d/e/z/c.md")).toBeTrue(); + expect(new Glob("a/**/c/*.md").match("a/bb.bb/aa/b.b/aa/c/xyz.md")).toBeTrue(); + expect(new Glob("a/**/c/*.md").match("a/bb.bb/aa/bb/aa/c/xyz.md")).toBeTrue(); + expect(new Glob("a/**/j/**/z/*.md").match("a/b/c/j/e/z/c.txt")).toBeFalse(); + expect(new Glob("a/b/**/c{d,e}/**/xyz.md").match("a/b/c/xyz.md")).toBeFalse(); + expect(new Glob("a/b/**/c{d,e}/**/xyz.md").match("a/b/d/xyz.md")).toBeFalse(); + expect(new Glob("a/**/").match("a/b")).toBeFalse(); + expect(new Glob("a/**/").match("a/b/c/d")).toBeFalse(); + expect(new Glob("a/**/").match("a/bb")).toBeFalse(); + expect(new Glob("a/**/").match("a/cb")).toBeFalse(); + expect(new Glob("/**").match("/a/b")).toBeTrue(); + expect(new Glob("**/*").match("a.b")).toBeTrue(); + expect(new Glob("**/*").match("a.js")).toBeTrue(); + expect(new Glob("**/*.js").match("a.js")).toBeTrue(); + expect(new Glob("**/*.js").match("a/a.js")).toBeTrue(); + expect(new Glob("**/*.js").match("a/a/b.js")).toBeTrue(); + expect(new Glob("a/**/b").match("a/b")).toBeTrue(); + expect(new Glob("a/**b").match("a/b")).toBeTrue(); + expect(new Glob("**/*.md").match("a/b.md")).toBeTrue(); + expect(new Glob("**/*").match("a/b/c.js")).toBeTrue(); + expect(new Glob("**/*").match("a/b/c.txt")).toBeTrue(); + expect(new Glob("a/**/").match("a/b/c/d/")).toBeTrue(); + expect(new Glob("**/*").match("a/b/c/d/a.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/c/z.js")).toBeTrue(); + expect(new Glob("a/b/**/*.js").match("a/b/z.js")).toBeTrue(); + expect(new Glob("**/*").match("ab")).toBeTrue(); + expect(new Glob("**/*").match("ab/c")).toBeTrue(); + expect(new Glob("**/*").match("ab/c/d")).toBeTrue(); + expect(new Glob("**/*").match("abc.js")).toBeTrue(); + expect(new Glob("**/").match("a")).toBeFalse(); + expect(new Glob("**/a/*").match("a")).toBeFalse(); + expect(new Glob("**/a/*/*").match("a")).toBeFalse(); + expect(new Glob("*/a/**").match("a")).toBeFalse(); + expect(new Glob("a/**/*").match("a")).toBeFalse(); + expect(new Glob("a/**/**/*").match("a")).toBeFalse(); + expect(new Glob("**/").match("a/b")).toBeFalse(); + expect(new Glob("**/b/*").match("a/b")).toBeFalse(); + expect(new Glob("**/b/*/*").match("a/b")).toBeFalse(); + expect(new Glob("b/**").match("a/b")).toBeFalse(); + expect(new Glob("**/").match("a/b/c")).toBeFalse(); + expect(new Glob("**/**/b").match("a/b/c")).toBeFalse(); + expect(new Glob("**/b").match("a/b/c")).toBeFalse(); + expect(new Glob("**/b/*/*").match("a/b/c")).toBeFalse(); + expect(new Glob("b/**").match("a/b/c")).toBeFalse(); + expect(new Glob("**/").match("a/b/c/d")).toBeFalse(); + expect(new Glob("**/d/*").match("a/b/c/d")).toBeFalse(); + expect(new Glob("b/**").match("a/b/c/d")).toBeFalse(); + expect(new Glob("**").match("a")).toBeTrue(); + expect(new Glob("**/**").match("a")).toBeTrue(); + expect(new Glob("**/**/*").match("a")).toBeTrue(); + expect(new Glob("**/**/a").match("a")).toBeTrue(); + expect(new Glob("**/a").match("a")).toBeTrue(); + expect(new Glob("**").match("a/b")).toBeTrue(); + expect(new Glob("**/**").match("a/b")).toBeTrue(); + expect(new Glob("**/**/*").match("a/b")).toBeTrue(); + expect(new Glob("**/**/b").match("a/b")).toBeTrue(); + expect(new Glob("**/b").match("a/b")).toBeTrue(); + expect(new Glob("a/**").match("a/b")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b")).toBeTrue(); + expect(new Glob("**").match("a/b/c")).toBeTrue(); + expect(new Glob("**/**").match("a/b/c")).toBeTrue(); + expect(new Glob("**/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**/b/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**/b/**").match("a/b/c")).toBeTrue(); + expect(new Glob("*/b/**").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b/c")).toBeTrue(); + expect(new Glob("**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/**/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/**/d").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/b/**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/b/*/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("**/d").match("a/b/c/d")).toBeTrue(); + expect(new Glob("*/b/**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**/*").match("a/b/c/d")).toBeTrue(); + expect(new Glob("a/**/**/*").match("a/b/c/d")).toBeTrue(); + }); + + test("utf8", () => { + expect(new Glob("フ*/**/*").match("フォルダ/aaa.js")).toBeTrue(); + expect(new Glob("フォ*/**/*").match("フォルダ/aaa.js")).toBeTrue(); + expect(new Glob("フォル*/**/*").match("フォルダ/aaa.js")).toBeTrue(); + expect(new Glob("フ*ル*/**/*").match("フォルダ/aaa.js")).toBeTrue(); + expect(new Glob("フォルダ/**/*").match("フォルダ/aaa.js")).toBeTrue(); + }); + + test("negation", () => { + expect(new Glob("!*").match("abc")).toBeFalse(); + expect(new Glob("!abc").match("abc")).toBeFalse(); + expect(new Glob("*!.md").match("bar.md")).toBeFalse(); + expect(new Glob("foo!.md").match("bar.md")).toBeFalse(); + expect(new Glob("\\!*!*.md").match("foo!.md")).toBeFalse(); + expect(new Glob("\\!*!*.md").match("foo!bar.md")).toBeFalse(); + expect(new Glob("*!*.md").match("!foo!.md")).toBeTrue(); + expect(new Glob("\\!*!*.md").match("!foo!.md")).toBeTrue(); + expect(new Glob("!*foo").match("abc")).toBeTrue(); + expect(new Glob("!foo*").match("abc")).toBeTrue(); + expect(new Glob("!xyz").match("abc")).toBeTrue(); + expect(new Glob("*!*.*").match("ba!r.js")).toBeTrue(); + expect(new Glob("*.md").match("bar.md")).toBeTrue(); + expect(new Glob("*!*.*").match("foo!.md")).toBeTrue(); + expect(new Glob("*!*.md").match("foo!.md")).toBeTrue(); + expect(new Glob("*!.md").match("foo!.md")).toBeTrue(); + expect(new Glob("*.md").match("foo!.md")).toBeTrue(); + expect(new Glob("foo!.md").match("foo!.md")).toBeTrue(); + expect(new Glob("*!*.md").match("foo!bar.md")).toBeTrue(); + expect(new Glob("*b*.md").match("foobar.md")).toBeTrue(); + expect(new Glob("a!!b").match("a")).toBeFalse(); + expect(new Glob("a!!b").match("aa")).toBeFalse(); + expect(new Glob("a!!b").match("a/b")).toBeFalse(); + expect(new Glob("a!!b").match("a!b")).toBeFalse(); + expect(new Glob("a!!b").match("a!!b")).toBeTrue(); + expect(new Glob("a!!b").match("a/!!/b")).toBeFalse(); + expect(new Glob("!a/b").match("a/b")).toBeFalse(); + expect(new Glob("!a/b").match("a")).toBeTrue(); + expect(new Glob("!a/b").match("a.b")).toBeTrue(); + expect(new Glob("!a/b").match("a/a")).toBeTrue(); + expect(new Glob("!a/b").match("a/c")).toBeTrue(); + expect(new Glob("!a/b").match("b/a")).toBeTrue(); + expect(new Glob("!a/b").match("b/b")).toBeTrue(); + expect(new Glob("!a/b").match("b/c")).toBeTrue(); + expect(new Glob("!abc").match("abc")).toBeFalse(); + expect(new Glob("!!abc").match("abc")).toBeTrue(); + expect(new Glob("!!!abc").match("abc")).toBeFalse(); + expect(new Glob("!!!!abc").match("abc")).toBeTrue(); + expect(new Glob("!!!!!abc").match("abc")).toBeFalse(); + expect(new Glob("!!!!!!abc").match("abc")).toBeTrue(); + expect(new Glob("!!!!!!!abc").match("abc")).toBeFalse(); + expect(new Glob("!!!!!!!!abc").match("abc")).toBeTrue(); + // try expect(!match("!(*/*)", "a/a")); + // try expect(!match("!(*/*)", "a/b")); + // try expect(!match("!(*/*)", "a/c")); + // try expect(!match("!(*/*)", "b/a")); + // try expect(!match("!(*/*)", "b/b")); + // try expect(!match("!(*/*)", "b/c")); + // try expect(!match("!(*/b)", "a/b")); + // try expect(!match("!(*/b)", "b/b")); + // try expect(!match("!(a/b)", "a/b")); + expect(new Glob("!*").match("a")).toBeFalse(); + expect(new Glob("!*").match("a.b")).toBeFalse(); + expect(new Glob("!*/*").match("a/a")).toBeFalse(); + expect(new Glob("!*/*").match("a/b")).toBeFalse(); + expect(new Glob("!*/*").match("a/c")).toBeFalse(); + expect(new Glob("!*/*").match("b/a")).toBeFalse(); + expect(new Glob("!*/*").match("b/b")).toBeFalse(); + expect(new Glob("!*/*").match("b/c")).toBeFalse(); + expect(new Glob("!*/b").match("a/b")).toBeFalse(); + expect(new Glob("!*/b").match("b/b")).toBeFalse(); + expect(new Glob("!*/c").match("a/c")).toBeFalse(); + expect(new Glob("!*/c").match("a/c")).toBeFalse(); + expect(new Glob("!*/c").match("b/c")).toBeFalse(); + expect(new Glob("!*/c").match("b/c")).toBeFalse(); + expect(new Glob("!*a*").match("bar")).toBeFalse(); + expect(new Glob("!*a*").match("fab")).toBeFalse(); + // try expect(!match("!a/(*)", "a/a")); + // try expect(!match("!a/(*)", "a/b")); + // try expect(!match("!a/(*)", "a/c")); + // try expect(!match("!a/(b)", "a/b")); + expect(new Glob("!a/*").match("a/a")).toBeFalse(); + expect(new Glob("!a/*").match("a/b")).toBeFalse(); + expect(new Glob("!a/*").match("a/c")).toBeFalse(); + expect(new Glob("!f*b").match("fab")).toBeFalse(); + // try expect(match("!(*/*)", "a")); + // try expect(match("!(*/*)", "a.b")); + // try expect(match("!(*/b)", "a")); + // try expect(match("!(*/b)", "a.b")); + // try expect(match("!(*/b)", "a/a")); + // try expect(match("!(*/b)", "a/c")); + // try expect(match("!(*/b)", "b/a")); + // try expect(match("!(*/b)", "b/c")); + // try expect(match("!(a/b)", "a")); + // try expect(match("!(a/b)", "a.b")); + // try expect(match("!(a/b)", "a/a")); + // try expect(match("!(a/b)", "a/c")); + // try expect(match("!(a/b)", "b/a")); + // try expect(match("!(a/b)", "b/b")); + // try expect(match("!(a/b)", "b/c")); + expect(new Glob("!*").match("a/a")).toBeTrue(); + expect(new Glob("!*").match("a/b")).toBeTrue(); + expect(new Glob("!*").match("a/c")).toBeTrue(); + expect(new Glob("!*").match("b/a")).toBeTrue(); + expect(new Glob("!*").match("b/b")).toBeTrue(); + expect(new Glob("!*").match("b/c")).toBeTrue(); + expect(new Glob("!*/*").match("a")).toBeTrue(); + expect(new Glob("!*/*").match("a.b")).toBeTrue(); + expect(new Glob("!*/b").match("a")).toBeTrue(); + expect(new Glob("!*/b").match("a.b")).toBeTrue(); + expect(new Glob("!*/b").match("a/a")).toBeTrue(); + expect(new Glob("!*/b").match("a/c")).toBeTrue(); + expect(new Glob("!*/b").match("b/a")).toBeTrue(); + expect(new Glob("!*/b").match("b/c")).toBeTrue(); + expect(new Glob("!*/c").match("a")).toBeTrue(); + expect(new Glob("!*/c").match("a.b")).toBeTrue(); + expect(new Glob("!*/c").match("a/a")).toBeTrue(); + expect(new Glob("!*/c").match("a/b")).toBeTrue(); + expect(new Glob("!*/c").match("b/a")).toBeTrue(); + expect(new Glob("!*/c").match("b/b")).toBeTrue(); + expect(new Glob("!*a*").match("foo")).toBeTrue(); + // try expect(match("!a/(*)", "a")); + // try expect(match("!a/(*)", "a.b")); + // try expect(match("!a/(*)", "b/a")); + // try expect(match("!a/(*)", "b/b")); + // try expect(match("!a/(*)", "b/c")); + // try expect(match("!a/(b)", "a")); + // try expect(match("!a/(b)", "a.b")); + // try expect(match("!a/(b)", "a/a")); + // try expect(match("!a/(b)", "a/c")); + // try expect(match("!a/(b)", "b/a")); + // try expect(match("!a/(b)", "b/b")); + // try expect(match("!a/(b)", "b/c")); + expect(new Glob("!a/*").match("a")).toBeTrue(); + expect(new Glob("!a/*").match("a.b")).toBeTrue(); + expect(new Glob("!a/*").match("b/a")).toBeTrue(); + expect(new Glob("!a/*").match("b/b")).toBeTrue(); + expect(new Glob("!a/*").match("b/c")).toBeTrue(); + expect(new Glob("!f*b").match("bar")).toBeTrue(); + expect(new Glob("!f*b").match("foo")).toBeTrue(); + expect(new Glob("!.md").match(".md")).toBeFalse(); + expect(new Glob("!**/*.md").match("a.js")).toBeTrue(); + // try expect(!match("!**/*.md", "b.md")); + expect(new Glob("!**/*.md").match("c.txt")).toBeTrue(); + expect(new Glob("!*.md").match("a.js")).toBeTrue(); + expect(new Glob("!*.md").match("b.md")).toBeFalse(); + expect(new Glob("!*.md").match("c.txt")).toBeTrue(); + expect(new Glob("!*.md").match("abc.md")).toBeFalse(); + expect(new Glob("!*.md").match("abc.txt")).toBeTrue(); + expect(new Glob("!*.md").match("foo.md")).toBeFalse(); + expect(new Glob("!.md").match("foo.md")).toBeTrue(); + expect(new Glob("!*.md").match("a.js")).toBeTrue(); + expect(new Glob("!*.md").match("b.txt")).toBeTrue(); + expect(new Glob("!*.md").match("c.md")).toBeFalse(); + expect(new Glob("!a/*/a.js").match("a/a/a.js")).toBeFalse(); + expect(new Glob("!a/*/a.js").match("a/b/a.js")).toBeFalse(); + expect(new Glob("!a/*/a.js").match("a/c/a.js")).toBeFalse(); + expect(new Glob("!a/*/*/a.js").match("a/a/a/a.js")).toBeFalse(); + expect(new Glob("!a/*/*/a.js").match("b/a/b/a.js")).toBeTrue(); + expect(new Glob("!a/*/*/a.js").match("c/a/c/a.js")).toBeTrue(); + expect(new Glob("!a/a*.txt").match("a/a.txt")).toBeFalse(); + expect(new Glob("!a/a*.txt").match("a/b.txt")).toBeTrue(); + expect(new Glob("!a/a*.txt").match("a/c.txt")).toBeTrue(); + expect(new Glob("!a.a*.txt").match("a.a.txt")).toBeFalse(); + expect(new Glob("!a.a*.txt").match("a.b.txt")).toBeTrue(); + expect(new Glob("!a.a*.txt").match("a.c.txt")).toBeTrue(); + expect(new Glob("!a/*.txt").match("a/a.txt")).toBeFalse(); + expect(new Glob("!a/*.txt").match("a/b.txt")).toBeFalse(); + expect(new Glob("!a/*.txt").match("a/c.txt")).toBeFalse(); + expect(new Glob("!*.md").match("a.js")).toBeTrue(); + expect(new Glob("!*.md").match("b.txt")).toBeTrue(); + expect(new Glob("!*.md").match("c.md")).toBeFalse(); + // try expect(!match("!**/a.js", "a/a/a.js")); + // try expect(!match("!**/a.js", "a/b/a.js")); + // try expect(!match("!**/a.js", "a/c/a.js")); + expect(new Glob("!**/a.js").match("a/a/b.js")).toBeTrue(); + expect(new Glob("!a/**/a.js").match("a/a/a/a.js")).toBeFalse(); + expect(new Glob("!a/**/a.js").match("b/a/b/a.js")).toBeTrue(); + expect(new Glob("!a/**/a.js").match("c/a/c/a.js")).toBeTrue(); + expect(new Glob("!**/*.md").match("a/b.js")).toBeTrue(); + expect(new Glob("!**/*.md").match("a.js")).toBeTrue(); + expect(new Glob("!**/*.md").match("a/b.md")).toBeFalse(); + // try expect(!match("!**/*.md", "a.md")); + expect(new Glob("**/*.md").match("a/b.js")).toBeFalse(); + expect(new Glob("**/*.md").match("a.js")).toBeFalse(); + expect(new Glob("**/*.md").match("a/b.md")).toBeTrue(); + expect(new Glob("**/*.md").match("a.md")).toBeTrue(); + expect(new Glob("!**/*.md").match("a/b.js")).toBeTrue(); + expect(new Glob("!**/*.md").match("a.js")).toBeTrue(); + expect(new Glob("!**/*.md").match("a/b.md")).toBeFalse(); + // try expect(!match("!**/*.md", "a.md")); + expect(new Glob("!*.md").match("a/b.js")).toBeTrue(); + expect(new Glob("!*.md").match("a.js")).toBeTrue(); + expect(new Glob("!*.md").match("a/b.md")).toBeTrue(); + expect(new Glob("!*.md").match("a.md")).toBeFalse(); + expect(new Glob("!**/*.md").match("a.js")).toBeTrue(); + // try expect(!match("!**/*.md", "b.md")); + expect(new Glob("!**/*.md").match("c.txt")).toBeTrue(); + }); + + test("question_mark", () => { + expect(new Glob("?").match("a")).toBeTrue(); + expect(new Glob("?").match("aa")).toBeFalse(); + expect(new Glob("?").match("ab")).toBeFalse(); + expect(new Glob("?").match("aaa")).toBeFalse(); + expect(new Glob("?").match("abcdefg")).toBeFalse(); + expect(new Glob("??").match("a")).toBeFalse(); + expect(new Glob("??").match("aa")).toBeTrue(); + expect(new Glob("??").match("ab")).toBeTrue(); + expect(new Glob("??").match("aaa")).toBeFalse(); + expect(new Glob("??").match("abcdefg")).toBeFalse(); + expect(new Glob("???").match("a")).toBeFalse(); + expect(new Glob("???").match("aa")).toBeFalse(); + expect(new Glob("???").match("ab")).toBeFalse(); + expect(new Glob("???").match("aaa")).toBeTrue(); + expect(new Glob("???").match("abcdefg")).toBeFalse(); + expect(new Glob("a?c").match("aaa")).toBeFalse(); + expect(new Glob("a?c").match("aac")).toBeTrue(); + expect(new Glob("a?c").match("abc")).toBeTrue(); + expect(new Glob("ab?").match("a")).toBeFalse(); + expect(new Glob("ab?").match("aa")).toBeFalse(); + expect(new Glob("ab?").match("ab")).toBeFalse(); + expect(new Glob("ab?").match("ac")).toBeFalse(); + expect(new Glob("ab?").match("abcd")).toBeFalse(); + expect(new Glob("ab?").match("abbb")).toBeFalse(); + expect(new Glob("a?b").match("acb")).toBeTrue(); + expect(new Glob("a/?/c/?/e.md").match("a/bb/c/dd/e.md")).toBeFalse(); + expect(new Glob("a/??/c/??/e.md").match("a/bb/c/dd/e.md")).toBeTrue(); + expect(new Glob("a/??/c.md").match("a/bbb/c.md")).toBeFalse(); + expect(new Glob("a/?/c.md").match("a/b/c.md")).toBeTrue(); + expect(new Glob("a/?/c/?/e.md").match("a/b/c/d/e.md")).toBeTrue(); + expect(new Glob("a/?/c/???/e.md").match("a/b/c/d/e.md")).toBeFalse(); + expect(new Glob("a/?/c/???/e.md").match("a/b/c/zzz/e.md")).toBeTrue(); + expect(new Glob("a/?/c.md").match("a/bb/c.md")).toBeFalse(); + expect(new Glob("a/??/c.md").match("a/bb/c.md")).toBeTrue(); + expect(new Glob("a/???/c.md").match("a/bbb/c.md")).toBeTrue(); + expect(new Glob("a/????/c.md").match("a/bbbb/c.md")).toBeTrue(); + }); + + test("braces", () => { + expect(new Glob("{a,b,c}").match("a")).toBeTrue(); + expect(new Glob("{a,b,c}").match("b")).toBeTrue(); + expect(new Glob("{a,b,c}").match("c")).toBeTrue(); + expect(new Glob("{a,b,c}").match("aa")).toBeFalse(); + expect(new Glob("{a,b,c}").match("bb")).toBeFalse(); + expect(new Glob("{a,b,c}").match("cc")).toBeFalse(); + expect(new Glob("a/{a,b}").match("a/a")).toBeTrue(); + expect(new Glob("a/{a,b}").match("a/b")).toBeTrue(); + expect(new Glob("a/{a,b}").match("a/c")).toBeFalse(); + expect(new Glob("a/{a,b}").match("b/b")).toBeFalse(); + expect(new Glob("a/{a,b,c}").match("b/b")).toBeFalse(); + expect(new Glob("a/{a,b,c}").match("a/c")).toBeTrue(); + expect(new Glob("a{b,bc}.txt").match("abc.txt")).toBeTrue(); + expect(new Glob("foo[{a,b}]baz").match("foo{baz")).toBeTrue(); + expect(new Glob("a{,b}.txt").match("abc.txt")).toBeFalse(); + expect(new Glob("a{a,b,}.txt").match("abc.txt")).toBeFalse(); + expect(new Glob("a{b,}.txt").match("abc.txt")).toBeFalse(); + expect(new Glob("a{,b}.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a{b,}.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a{a,b,}.txt").match("aa.txt")).toBeTrue(); + expect(new Glob("a{a,b,}.txt").match("aa.txt")).toBeTrue(); + expect(new Glob("a{,b}.txt").match("ab.txt")).toBeTrue(); + expect(new Glob("a{b,}.txt").match("ab.txt")).toBeTrue(); + // try expect(match("{a/,}a/**", "a")); + expect(new Glob("a{a,b/}*.txt").match("aa.txt")).toBeTrue(); + expect(new Glob("a{a,b/}*.txt").match("ab/.txt")).toBeTrue(); + expect(new Glob("a{a,b/}*.txt").match("ab/a.txt")).toBeTrue(); + // try expect(match("{a/,}a/**", "a/")); + expect(new Glob("{a/,}a/**").match("a/a/")).toBeTrue(); + // try expect(match("{a/,}a/**", "a/a")); + expect(new Glob("{a/,}a/**").match("a/a/a")).toBeTrue(); + expect(new Glob("{a/,}a/**").match("a/a/")).toBeTrue(); + expect(new Glob("{a/,}a/**").match("a/a/a/")).toBeTrue(); + expect(new Glob("{a/,}b/**").match("a/b/a/")).toBeTrue(); + expect(new Glob("{a/,}b/**").match("b/a/")).toBeTrue(); + expect(new Glob("a{,/}*.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a{,/}*.txt").match("ab.txt")).toBeTrue(); + expect(new Glob("a{,/}*.txt").match("a/b.txt")).toBeTrue(); + expect(new Glob("a{,/}*.txt").match("a/ab.txt")).toBeTrue(); + expect(new Glob("a{,.*{foo,db},\\(bar\\)}.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a{,.*{foo,db},\\(bar\\)}.txt").match("adb.txt")).toBeFalse(); + expect(new Glob("a{,.*{foo,db},\\(bar\\)}.txt").match("a.db.txt")).toBeTrue(); + expect(new Glob("a{,*.{foo,db},\\(bar\\)}.txt").match("a.txt")).toBeTrue(); + expect(new Glob("a{,*.{foo,db},\\(bar\\)}.txt").match("adb.txt")).toBeFalse(); + expect(new Glob("a{,*.{foo,db},\\(bar\\)}.txt").match("a.db.txt")).toBeTrue(); + // try expect(match("a{,.*{foo,db},\\(bar\\)}", "a")); + expect(new Glob("a{,.*{foo,db},\\(bar\\)}").match("adb")).toBeFalse(); + expect(new Glob("a{,.*{foo,db},\\(bar\\)}").match("a.db")).toBeTrue(); + // try expect(match("a{,*.{foo,db},\\(bar\\)}", "a")); + expect(new Glob("a{,*.{foo,db},\\(bar\\)}").match("adb")).toBeFalse(); + expect(new Glob("a{,*.{foo,db},\\(bar\\)}").match("a.db")).toBeTrue(); + expect(new Glob("{,.*{foo,db},\\(bar\\)}").match("a")).toBeFalse(); + expect(new Glob("{,.*{foo,db},\\(bar\\)}").match("adb")).toBeFalse(); + expect(new Glob("{,.*{foo,db},\\(bar\\)}").match("a.db")).toBeFalse(); + expect(new Glob("{,.*{foo,db},\\(bar\\)}").match(".db")).toBeTrue(); + expect(new Glob("{,*.{foo,db},\\(bar\\)}").match("a")).toBeFalse(); + expect(new Glob("{*,*.{foo,db},\\(bar\\)}").match("a")).toBeTrue(); + expect(new Glob("{,*.{foo,db},\\(bar\\)}").match("adb")).toBeFalse(); + expect(new Glob("{,*.{foo,db},\\(bar\\)}").match("a.db")).toBeTrue(); + expect(new Glob("a/b/**/c{d,e}/**/xyz.md").match("a/b/c/xyz.md")).toBeFalse(); + expect(new Glob("a/b/**/c{d,e}/**/xyz.md").match("a/b/d/xyz.md")).toBeFalse(); + expect(new Glob("a/b/**/c{d,e}/**/xyz.md").match("a/b/cd/xyz.md")).toBeTrue(); + expect(new Glob("a/b/**/{c,d,e}/**/xyz.md").match("a/b/c/xyz.md")).toBeTrue(); + expect(new Glob("a/b/**/{c,d,e}/**/xyz.md").match("a/b/d/xyz.md")).toBeTrue(); + expect(new Glob("a/b/**/{c,d,e}/**/xyz.md").match("a/b/e/xyz.md")).toBeTrue(); + expect(new Glob("*{a,b}*").match("xax")).toBeTrue(); + expect(new Glob("*{a,b}*").match("xxax")).toBeTrue(); + expect(new Glob("*{a,b}*").match("xbx")).toBeTrue(); + expect(new Glob("*{*a,b}").match("xba")).toBeTrue(); + expect(new Glob("*{*a,b}").match("xb")).toBeTrue(); + expect(new Glob("*??").match("a")).toBeFalse(); + expect(new Glob("*???").match("aa")).toBeFalse(); + expect(new Glob("*???").match("aaa")).toBeTrue(); + expect(new Glob("*****??").match("a")).toBeFalse(); + expect(new Glob("*****???").match("aa")).toBeFalse(); + expect(new Glob("*****???").match("aaa")).toBeTrue(); + expect(new Glob("a*?c").match("aaa")).toBeFalse(); + expect(new Glob("a*?c").match("aac")).toBeTrue(); + expect(new Glob("a*?c").match("abc")).toBeTrue(); + expect(new Glob("a**?c").match("abc")).toBeTrue(); + expect(new Glob("a**?c").match("abb")).toBeFalse(); + expect(new Glob("a**?c").match("acc")).toBeTrue(); + expect(new Glob("a*****?c").match("abc")).toBeTrue(); + expect(new Glob("*****?").match("a")).toBeTrue(); + expect(new Glob("*****?").match("aa")).toBeTrue(); + expect(new Glob("*****?").match("abc")).toBeTrue(); + expect(new Glob("*****?").match("zzz")).toBeTrue(); + expect(new Glob("*****?").match("bbb")).toBeTrue(); + expect(new Glob("*****?").match("aaaa")).toBeTrue(); + expect(new Glob("*****??").match("a")).toBeFalse(); + expect(new Glob("*****??").match("aa")).toBeTrue(); + expect(new Glob("*****??").match("abc")).toBeTrue(); + expect(new Glob("*****??").match("zzz")).toBeTrue(); + expect(new Glob("*****??").match("bbb")).toBeTrue(); + expect(new Glob("*****??").match("aaaa")).toBeTrue(); + expect(new Glob("?*****??").match("a")).toBeFalse(); + expect(new Glob("?*****??").match("aa")).toBeFalse(); + expect(new Glob("?*****??").match("abc")).toBeTrue(); + expect(new Glob("?*****??").match("zzz")).toBeTrue(); + expect(new Glob("?*****??").match("bbb")).toBeTrue(); + expect(new Glob("?*****??").match("aaaa")).toBeTrue(); + expect(new Glob("?*****?c").match("abc")).toBeTrue(); + expect(new Glob("?*****?c").match("abb")).toBeFalse(); + expect(new Glob("?*****?c").match("zzz")).toBeFalse(); + expect(new Glob("?***?****c").match("abc")).toBeTrue(); + expect(new Glob("?***?****c").match("bbb")).toBeFalse(); + expect(new Glob("?***?****c").match("zzz")).toBeFalse(); + expect(new Glob("?***?****?").match("abc")).toBeTrue(); + expect(new Glob("?***?****?").match("bbb")).toBeTrue(); + expect(new Glob("?***?****?").match("zzz")).toBeTrue(); + expect(new Glob("?***?****").match("abc")).toBeTrue(); + expect(new Glob("*******c").match("abc")).toBeTrue(); + expect(new Glob("*******?").match("abc")).toBeTrue(); + expect(new Glob("a*cd**?**??k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??k***").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??***k").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a**?**cd**?**??***k**").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a****c**?**??*****").match("abcdecdhjk")).toBeTrue(); + expect(new Glob("a/?/c/?/*/e.md").match("a/b/c/d/e.md")).toBeFalse(); + expect(new Glob("a/?/c/?/*/e.md").match("a/b/c/d/e/e.md")).toBeTrue(); + expect(new Glob("a/?/c/?/*/e.md").match("a/b/c/d/efghijk/e.md")).toBeTrue(); + expect(new Glob("a/?/**/e.md").match("a/b/c/d/efghijk/e.md")).toBeTrue(); + expect(new Glob("a/?/e.md").match("a/bb/e.md")).toBeFalse(); + expect(new Glob("a/??/e.md").match("a/bb/e.md")).toBeTrue(); + expect(new Glob("a/?/**/e.md").match("a/bb/e.md")).toBeFalse(); + expect(new Glob("a/?/**/e.md").match("a/b/ccc/e.md")).toBeTrue(); + expect(new Glob("a/*/?/**/e.md").match("a/b/c/d/efghijk/e.md")).toBeTrue(); + expect(new Glob("a/*/?/**/e.md").match("a/b/c/d/efgh.ijk/e.md")).toBeTrue(); + expect(new Glob("a/*/?/**/e.md").match("a/b.bb/c/d/efgh.ijk/e.md")).toBeTrue(); + expect(new Glob("a/*/?/**/e.md").match("a/bbb/c/d/efgh.ijk/e.md")).toBeTrue(); + expect(new Glob("a/*/ab??.md").match("a/bbb/abcd.md")).toBeTrue(); + expect(new Glob("a/bbb/ab??.md").match("a/bbb/abcd.md")).toBeTrue(); + expect(new Glob("a/bbb/ab???md").match("a/bbb/abcd.md")).toBeTrue(); + }); + }); + + test("invalid input", () => { + const glob = new Glob("nice"); + + expect( + returnError(() => + glob.match( + // @ts-expect-error + null, + ), + ), + ).toBeDefined(); + expect( + returnError(() => + glob.match( + // @ts-expect-error + true, + ), + ), + ).toBeDefined(); + + expect( + returnError(() => + glob.match( + // @ts-expect-error + {}, + ), + ), + ).toBeDefined(); + }); +}); + +function returnError(cb: () => any): Error | undefined { + try { + cb(); + } catch (err) { + // @ts-expect-error + return err; + } + return undefined; +} diff --git a/test/js/bun/glob/scan.test.ts b/test/js/bun/glob/scan.test.ts new file mode 100644 index 0000000000..ca174e4f1f --- /dev/null +++ b/test/js/bun/glob/scan.test.ts @@ -0,0 +1,381 @@ +// Portions of this file are derived from works under the MIT License: +// +// Copyright (c) Denis Malinochkin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import { expect, test, describe, beforeAll, afterAll } from "bun:test"; +import fg from "fast-glob"; +import { Glob, GlobScanOptions } from "bun"; +import * as path from "path"; +import { tempDirWithFiles, withoutAggressiveGC } from "harness"; +import { i } from "../http/js-sink-sourmap-fixture/index.mjs"; +import { tempFixturesDir } from "./util"; + +let origAggressiveGC = Bun.unsafe.gcAggressionLevel(); +beforeAll(() => { + process.chdir(path.join(import.meta.dir, "../../../")); + tempFixturesDir(); + Bun.unsafe.gcAggressionLevel(0); +}); +afterAll(() => { + Bun.unsafe.gcAggressionLevel(origAggressiveGC); +}); + +const followSymlinks = true; + +const bunGlobOpts = { + followSymlinks: followSymlinks, + onlyFiles: false, + // absolute: true, +} satisfies GlobScanOptions; + +type FgOpts = NonNullable[1]>; +const fgOpts = { + followSymbolicLinks: followSymlinks, + onlyFiles: false, + // absolute: true, +} satisfies FgOpts; + +describe("glob.match", async () => { + const timeout = 30 * 1000; + function testWithOpts(namePrefix: string, bunGlobOpts: GlobScanOptions, fgOpts: FgOpts) { + test( + `${namePrefix} recursively search node_modules`, + async () => { + const pattern = "**/node_modules/**/*.js"; + const glob = new Glob(pattern); + const filepaths = await Array.fromAsync(glob.scan(bunGlobOpts)); + const fgFilepths = await fg.glob(pattern, fgOpts); + + // console.error(filepaths); + expect(filepaths.length).toEqual(fgFilepths.length); + + const bunfilepaths = new Set(filepaths); + for (const filepath of fgFilepths) { + if (!bunfilepaths.has(filepath)) console.error("Missing:", filepath); + expect(bunfilepaths.has(filepath)).toBeTrue(); + } + }, + timeout, + ); + + test( + `${namePrefix} recursive search js files`, + async () => { + const pattern = "**/*.js"; + const glob = new Glob(pattern); + const filepaths = await Array.fromAsync(glob.scan(bunGlobOpts)); + const fgFilepths = await fg.glob(pattern, fgOpts); + + expect(filepaths.length).toEqual(fgFilepths.length); + + const bunfilepaths = new Set(filepaths); + for (const filepath of fgFilepths) { + if (!bunfilepaths.has(filepath)) console.error("Missing:", filepath); + expect(bunfilepaths.has(filepath)).toBeTrue(); + } + }, + timeout, + ); + + test( + `${namePrefix} recursive search ts files`, + async () => { + const pattern = "**/*.ts"; + const glob = new Glob(pattern); + const filepaths = await Array.fromAsync(glob.scan(bunGlobOpts)); + const fgFilepths = await fg.glob(pattern, fgOpts); + + expect(filepaths.length).toEqual(fgFilepths.length); + + const bunfilepaths = new Set(filepaths); + for (const filepath of fgFilepths) { + if (!bunfilepaths.has(filepath)) console.error("Missing:", filepath); + expect(bunfilepaths.has(filepath)).toBeTrue(); + } + }, + timeout, + ); + + test( + `${namePrefix} glob not freed before matching done`, + async () => { + const promise = (async () => { + const glob = new Glob("**/node_modules/**/*.js"); + const result = Array.fromAsync(glob.scan(bunGlobOpts)); + Bun.gc(true); + const result2 = await result; + return result2; + })(); + Bun.gc(true); + const values = await promise; + Bun.gc(true); + }, + timeout, + ); + } + + testWithOpts("non-absolute", bunGlobOpts, fgOpts); + testWithOpts("absolute", { ...bunGlobOpts, absolute: true }, { ...fgOpts, absolute: true }); + + test("invalid surrogate pairs", async () => { + const pattern = `**/*.{md,\uD83D\uD800}`; + const cwd = import.meta.dir; + + const glob = new Glob(pattern); + const entries = await Array.fromAsync(glob.scan({ cwd })); + + expect(entries.sort()).toEqual( + [ + "fixtures/file.md", + "fixtures/second/file.md", + "fixtures/second/nested/file.md", + "fixtures/second/nested/directory/file.md", + "fixtures/third/library/b/book.md", + "fixtures/third/library/a/book.md", + "fixtures/first/file.md", + "fixtures/first/nested/file.md", + "fixtures/first/nested/directory/file.md", + ].sort(), + ); + }); + + test("bad options", async () => { + const glob = new Glob("lmaowtf"); + expect(returnError(() => glob.scan())).toBeUndefined(); + // @ts-expect-error + expect(returnError(() => glob.scan("sldkfjsldfj"))).toBeDefined(); + expect(returnError(() => glob.scan({}))).toBeUndefined(); + expect(returnError(() => glob.scan({ cwd: "" }))).toBeUndefined(); + // @ts-expect-error + expect(returnError(() => glob.scan({ cwd: true }))).toBeDefined(); + // @ts-expect-error + expect(returnError(() => glob.scan({ cwd: 123123 }))).toBeDefined(); + + function returnError(cb: () => any): Error | undefined { + try { + cb(); + } catch (err) { + console.error("Error", err); + // @ts-expect-error + return err; + } + return undefined; + } + }); +}); + +// From fast-glob regular.e2e.tes +const regular = { + regular: [ + "fixtures/*", + "fixtures/**", + "fixtures/**/*", + + "fixtures/*/nested", + "fixtures/*/nested/*", + "fixtures/*/nested/**", + "fixtures/*/nested/**/*", + "fixtures/**/nested/*", + "fixtures/**/nested/**", + "fixtures/**/nested/**/*", + + "fixtures/{first,second}", + "fixtures/{first,second}/*", + "fixtures/{first,second}/**", + "fixtures/{first,second}/**/*", + + // The @(pattern) syntax not supported so we don't include that here + // "@(fixtures)/{first,second}", + // "@(fixtures)/{first,second}/*", + + "fixtures/*/{first,second}/*", + "fixtures/*/{first,second}/*/{nested,file.md}", + "fixtures/**/{first,second}/**", + "fixtures/**/{first,second}/{nested,file.md}", + "fixtures/**/{first,second}/**/{nested,file.md}", + + "fixtures/{first,second}/{nested,file.md}", + "fixtures/{first,second}/*/nested/*", + "fixtures/{first,second}/**/nested/**", + + "fixtures/*/{nested,file.md}/*", + "fixtures/**/{nested,file.md}/*", + + "./fixtures/*", + ], + cwd: [ + { pattern: "*", cwd: "fixtures" }, + { pattern: "**", cwd: "fixtures" }, + { pattern: "**/*", cwd: "fixtures" }, + + { pattern: "*/nested", cwd: "fixtures" }, + { pattern: "*/nested/*", cwd: "fixtures" }, + { pattern: "*/nested/**", cwd: "fixtures" }, + { pattern: "*/nested/**/*", cwd: "fixtures" }, + { pattern: "**/nested/*", cwd: "fixtures" }, + { pattern: "**/nested/**", cwd: "fixtures" }, + { pattern: "**/nested/**/*", cwd: "fixtures" }, + + { pattern: "{first,second}", cwd: "fixtures" }, + { pattern: "{first,second}/*", cwd: "fixtures" }, + { pattern: "{first,second}/**", cwd: "fixtures" }, + { pattern: "{first,second}/**/*", cwd: "fixtures" }, + + { pattern: "*/{first,second}/*", cwd: "fixtures" }, + { pattern: "*/{first,second}/*/{nested,file.md}", cwd: "fixtures" }, + { pattern: "**/{first,second}/**", cwd: "fixtures" }, + { pattern: "**/{first,second}/{nested,file.md}", cwd: "fixtures" }, + { pattern: "**/{first,second}/**/{nested,file.md}", cwd: "fixtures" }, + + { pattern: "{first,second}/{nested,file.md}", cwd: "fixtures" }, + { pattern: "{first,second}/*/nested/*", cwd: "fixtures" }, + { pattern: "{first,second}/**/nested/**", cwd: "fixtures" }, + + { pattern: "*/{nested,file.md}/*", cwd: "fixtures" }, + { pattern: "**/{nested,file.md}/*", cwd: "fixtures" }, + ], + relativeCwd: [ + { pattern: "./*" }, + { pattern: "./*", cwd: "fixtures" }, + { pattern: "./**", cwd: "fixtures" }, + { pattern: "./**/*", cwd: "fixtures" }, + + { pattern: "../*", cwd: "fixtures/first" }, + { pattern: "../**", cwd: "fixtures/first", issue: 47 }, + { pattern: "../../*", cwd: "fixtures/first/nested" }, + + { pattern: "../{first,second}", cwd: "fixtures/first" }, + { pattern: "./../*", cwd: "fixtures/first" }, + ], +}; + +// From fast-glob absolute.e2e.ts +const absolutePatterns = { + regular: ["fixtures/*", "fixtures/**", "fixtures/**/*", "fixtures/../*"], + cwd: [ + { + pattern: "*", + cwd: "fixtures", + }, + { + pattern: "**", + cwd: "fixtures", + }, + { + pattern: "**/*", + cwd: "fixtures", + }, + ], +}; + +// From fast-glob only-files.e2e.ts +const onlyFilesPatterns = { + regular: ["fixtures/*", "fixtures/**", "fixtures/**/*"], + cwd: [ + { + pattern: "*", + cwd: "fixtures", + }, + { + pattern: "**", + cwd: "fixtures", + }, + { + pattern: "**/*", + cwd: "fixtures", + }, + ], +}; + +beforeAll(() => { + tempFixturesDir(); +}); + +/** + * These are the e2e tests from fast-glob, with some omitted because we don't support features like ignored patterns + * The snapshots are generated by running fast-glob on them first + * There are slight discrepancies in the returned matches when there is a `./` in front of the pattern. + * Bun.Glob is consistent with the Unix bash shell style, which always adds the `./` + * fast-glob will randomly add it or omit it. + * In practice this discrepancy makes no difference, so the snapshots were changed accordingly to match Bun.Glob / Unix bash shell style. + */ +describe("fast-glob e2e tests", async () => { + const absoluteCwd = process.cwd(); + const cwd = import.meta.dir; + + regular.regular.forEach(pattern => + test(`patterns regular ${pattern}`, () => { + // let entries = fg.globSync(pattern, { cwd }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd, followSymlinks: true })); + entries = entries.sort(); + expect(entries).toMatchSnapshot(pattern); + }), + ); + + regular.cwd.forEach(({ pattern, cwd: secondHalf }) => + test(`patterns regular cwd ${pattern}`, () => { + const testCwd = path.join(cwd, secondHalf); + // let entries = fg.globSync(pattern, { cwd: testCwd }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd: testCwd, followSymlinks: true })); + entries = entries.sort(); + expect(entries).toMatchSnapshot(pattern); + }), + ); + + regular.relativeCwd.forEach(({ pattern, cwd: secondHalf }) => + test(`patterns regular relative cwd ${pattern}`, () => { + const testCwd = secondHalf ? path.join(cwd, secondHalf) : cwd; + // let entries = fg.globSync(pattern, { cwd: testCwd }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd: testCwd, followSymlinks: true })); + entries = entries.sort(); + expect(entries).toMatchSnapshot(pattern); + }), + ); + + absolutePatterns.cwd.forEach(({ pattern, cwd: secondHalf }) => + test(`patterns absolute cwd ${pattern}`, () => { + const testCwd = secondHalf ? path.join(cwd, secondHalf) : cwd; + // let entries = fg.globSync(pattern, { cwd: testCwd, absolute: true }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd: testCwd, followSymlinks: true, absolute: true })); + entries = entries.sort().map(entry => entry.slice(absoluteCwd.length + 1)); + expect(entries).toMatchSnapshot(pattern); + }), + ); + + onlyFilesPatterns.regular.forEach(pattern => + test(`only files ${pattern}`, () => { + // let entries = fg.globSync(pattern, { cwd, absolute: false, onlyFiles: true }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd, followSymlinks: true, onlyFiles: true })); + entries = entries.sort(); + expect(entries).toMatchSnapshot(pattern); + }), + ); + + onlyFilesPatterns.cwd.forEach(({ pattern, cwd: secondHalf }) => + test(`only files (cwd) ${pattern}`, () => { + const testCwd = secondHalf ? path.join(cwd, secondHalf) : cwd; + // let entries = fg.globSync(pattern, { cwd: testCwd, absolute: false, onlyFiles: true }); + let entries = Array.from(new Glob(pattern).scanSync({ cwd: testCwd, followSymlinks: true, onlyFiles: true })); + entries = entries.sort(); + expect(entries).toMatchSnapshot(pattern); + }), + ); +}); diff --git a/test/js/bun/glob/stress.test.ts b/test/js/bun/glob/stress.test.ts new file mode 100644 index 0000000000..43be74444f --- /dev/null +++ b/test/js/bun/glob/stress.test.ts @@ -0,0 +1,43 @@ +import { expect, test, describe, beforeAll } from "bun:test"; +import { Glob } from "bun"; +import { tempFixturesDir } from "./util"; +import path from "path"; +const paths = [ + path.join(import.meta.dir, "fixtures/file.md"), + path.join(import.meta.dir, "fixtures/second/file.md"), + path.join(import.meta.dir, "fixtures/second/nested/file.md"), + path.join(import.meta.dir, "fixtures/second/nested/directory/file.md"), + path.join(import.meta.dir, "fixtures/third/library/b/book.md"), + path.join(import.meta.dir, "fixtures/third/library/a/book.md"), + path.join(import.meta.dir, "fixtures/first/file.md"), + path.join(import.meta.dir, "fixtures/first/nested/file.md"), + path.join(import.meta.dir, "fixtures/first/nested/directory/file.md"), + path.join(import.meta.dir, "fixtures/first/nested/directory/file.json"), +]; + +beforeAll(() => { + tempFixturesDir(); +}); + +test("Glob.scan stress test", async () => { + const cwd = import.meta.dir; + + await Promise.all( + Array(1000) + .fill(null) + .map(() => + Array.fromAsync(new Glob("src/**/*.zig").scan({ cwd })).then(results => { + const set = new Set(results); + return set.size == paths.length && paths.every(path => set.has(path)); + }), + ), + ); +}); + +test("Glob.match stress test", () => { + for (let i = 0; i < 10000; i++) { + if (!new Glob("src/**/*.zig").match("src/cli/package_manager_command.zig")) { + throw new Error("test failed on run " + i); + } + } +}); diff --git a/test/js/bun/glob/util.ts b/test/js/bun/glob/util.ts new file mode 100644 index 0000000000..aad922f354 --- /dev/null +++ b/test/js/bun/glob/util.ts @@ -0,0 +1,49 @@ +export function tempFixturesDir() { + const files: Record> = { + ".directory": { + "file.md": "", + }, + first: { + "nested/directory/file.json": "", + "nested/directory/file.md": "", + "nested/file.md": "", + "file.md": "", + }, + second: { + "nested/directory/file.md": "", + "nested/file.md": "", + "file.md": "", + }, + third: { + "library/a/book.md": "", + "library/b/book.md": "", + }, + ".file": "", + "file.md": "", + }; + + var fs = require("fs"); + var path = require("path"); + + function impl(dir: string, files: Record>) { + for (const [name, contents] of Object.entries(files)) { + if (typeof contents === "object") { + for (const [_name, _contents] of Object.entries(contents)) { + fs.mkdirSync(path.dirname(path.join(dir, name, _name)), { recursive: true }); + fs.writeFileSync(path.join(dir, name, _name), _contents); + } + continue; + } + fs.mkdirSync(path.dirname(path.join(dir, name)), { recursive: true }); + fs.writeFileSync(path.join(dir, name), contents); + } + return dir; + } + + const dir = path.join(import.meta.dir, "fixtures"); + fs.mkdirSync(dir, { recursive: true }); + + impl(dir, files); + + return dir; +} diff --git a/test/package.json b/test/package.json index b9a2d7b35d..e56cfe5738 100644 --- a/test/package.json +++ b/test/package.json @@ -8,6 +8,7 @@ "@types/supertest": "2.0.12" }, "dependencies": { + "fast-glob": "3.3.1", "@prisma/client": "5.1.1", "@resvg/resvg-js": "2.4.1", "@swc/core": "1.3.38",