From 6ba4e950cc2e2d7f1564275aa86a7bb30ad90c79 Mon Sep 17 00:00:00 2001 From: Zack Radisic <56137411+zackradisic@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:26:09 -0800 Subject: [PATCH] Add Bun.Glob (#6861) * initial glob impl * Add `Bun.globMatch` * Glob boilerplate setup * Experiment with Rust glob implementation * Rust impl is slow revert * Setup glob walking * Basic glob walker working * Fix segfault * Recursive directory traversal * Fix glob match non-ascii * Make faster lil bit * use arena * ASCII fast path * Experiment with packed codepoint cursor Results in ~4% perf boost if the glob pattern needs to create/manipulate cursors (for example when the pattern uses braces) * Try converting to u32 array Made it pretty slow * Lazily create codepoint buffer * Different walk algorithm * Fast path optimizations * Add `dot` option to `Glob` * . * Fix some bugs * Fix bug, clean up lil bit * Windows fix * Non absolute paths * use specific version of fast-glob for benchmarks and tests * . * Fix some stuff * Fix more stuff * Add `hasPendingActivity()` to glob * accident * Symlinks * fast-glob e2e tests * remove * woops * Fix relative paths * Fix absolute * add test for `onlyFiles` * Fix invalid surrogate pairs problem * Rename: `match/matchSync` -> `scan/scanSync` and `matchString` -> `match` * forgot to close cwd fd * Update types * Add stress test * Port `micromatch` / `glob-match` / `globlin` tests * fix stale reference arena thing * stupid bug * Add builtins to classes code generator and add `Glob.scanIter()` * all iterables * generate fixtures, remove from git * fix test * Fix * woops on test * Fix stuff licenses license `has_pending_activity` to usize cwd threadSafe fix atomic compile errors `GlobWalker` own `cwd` Fix windows path and absolute test stuff * Fixes * Fix stuff * Use Syscall.close * Use private symbols for underlying scan functions to preevent misuse * Update types * Fix build for zig * Fix tests * Fix more tests * Prevent these tests from GC'ing too much * Make this benchmark work in Node and Bun * Fix memory leak * Add leak test * Fix windows * comment about arena allocator use for glob walker * Make leak test run in separate process * Iterator api for glob * GlobWalker.Iterator * fix leak test * Remove old impl * filter functions wip start * stuff * wip lockfile use glob * glob working with lockfile * revert lockfile changes * Update bun.lockb * Manually set to cwd to prevent test failing on linux CI --------- Co-authored-by: Jarred Sumner Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com> --- .gitignore | 3 +- bench/bun.lockb | Bin 35848 -> 47636 bytes bench/glob/match.mjs | 113 + bench/package.json | 2 + packages/bun-types/bun.d.ts | 129 + src/bun.js/api/Glob.classes.ts | 39 + src/bun.js/api/bun.zig | 10 + src/bun.js/api/glob.zig | 514 +++ src/bun.js/bindings/BunObject+exports.h | 3 +- src/bun.js/bindings/BunObject.cpp | 1 + .../bindings/generated_classes_list.zig | 1 + src/bun.js/event_loop.zig | 7 + src/bun.js/node/dir_iterator.zig | 4 +- src/codegen/class-definitions.ts | 17 +- src/codegen/generate-classes.ts | 139 +- src/glob.zig | 3104 +++++++++++++++++ src/glob_ascii.zig | 508 +++ src/js/builtins/Glob.ts | 21 + src/jsc.zig | 1 + src/resolver/resolve_path.zig | 10 +- src/string_immutable.zig | 152 +- test/bun.lockb | Bin 273772 -> 280860 bytes .../bun/glob/__snapshots__/scan.test.ts.snap | 580 +++ test/js/bun/glob/leak.test.ts | 61 + test/js/bun/glob/match.test.ts | 1525 ++++++++ test/js/bun/glob/scan.test.ts | 381 ++ test/js/bun/glob/stress.test.ts | 43 + test/js/bun/glob/util.ts | 49 + test/package.json | 1 + 29 files changed, 7347 insertions(+), 71 deletions(-) create mode 100644 bench/glob/match.mjs create mode 100644 src/bun.js/api/Glob.classes.ts create mode 100644 src/bun.js/api/glob.zig create mode 100644 src/glob.zig create mode 100644 src/glob_ascii.zig create mode 100644 src/js/builtins/Glob.ts create mode 100644 test/js/bun/glob/__snapshots__/scan.test.ts.snap create mode 100644 test/js/bun/glob/leak.test.ts create mode 100644 test/js/bun/glob/match.test.ts create mode 100644 test/js/bun/glob/scan.test.ts create mode 100644 test/js/bun/glob/stress.test.ts create mode 100644 test/js/bun/glob/util.ts 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 298e2a7c95315af2a5b71db83940210f316c77b5..4e96ca3264feadd0a7c8051c9718a398a3600846 100755 GIT binary patch delta 13717 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 ABme*a 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 e9ead2eaf23dd2ca549f8f532fe9135cd6e06000..e5315c00ab6f7f85de193ff0e8622f855d9880b6 100755 GIT binary patch delta 47397 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 M%&Fvxm2tl2f8-NGvH$=8 delta 43063 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 MY$r>ux3*CJ8;Z##IRF3v 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",