bare-ripgrep 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/CMakeLists.txt +51 -0
  2. package/Cargo.lock +375 -0
  3. package/Cargo.toml +25 -0
  4. package/LICENSE.md +198 -0
  5. package/NOTICE.md +19 -0
  6. package/README.md +94 -2
  7. package/binding.c +169 -0
  8. package/binding.js +3 -0
  9. package/index.d.ts +70 -0
  10. package/index.js +100 -0
  11. package/package.json +66 -4
  12. package/prebuilds/android-arm64/bare-ripgrep.bare +0 -0
  13. package/prebuilds/android-arm64/bare-ripgrep.node +0 -0
  14. package/prebuilds/android-x64/bare-ripgrep.bare +0 -0
  15. package/prebuilds/android-x64/bare-ripgrep.node +0 -0
  16. package/prebuilds/darwin-arm64/bare-ripgrep.bare +0 -0
  17. package/prebuilds/darwin-arm64/bare-ripgrep.node +0 -0
  18. package/prebuilds/darwin-x64/bare-ripgrep.bare +0 -0
  19. package/prebuilds/darwin-x64/bare-ripgrep.node +0 -0
  20. package/prebuilds/ios-arm64/bare-ripgrep.bare +0 -0
  21. package/prebuilds/ios-arm64-simulator/bare-ripgrep.bare +0 -0
  22. package/prebuilds/ios-x64-simulator/bare-ripgrep.bare +0 -0
  23. package/prebuilds/linux-arm64/bare-ripgrep.bare +0 -0
  24. package/prebuilds/linux-arm64/bare-ripgrep.node +0 -0
  25. package/prebuilds/linux-x64/bare-ripgrep.bare +0 -0
  26. package/prebuilds/linux-x64/bare-ripgrep.node +0 -0
  27. package/prebuilds/win32-arm64/bare-ripgrep.bare +0 -0
  28. package/prebuilds/win32-arm64/bare-ripgrep.node +0 -0
  29. package/prebuilds/win32-x64/bare-ripgrep.bare +0 -0
  30. package/prebuilds/win32-x64/bare-ripgrep.node +0 -0
  31. package/src/bare_ripgrep.h +23 -0
  32. package/src/glob.rs +47 -0
  33. package/src/lib.rs +111 -0
  34. package/src/request.rs +31 -0
  35. package/src/scope.rs +170 -0
  36. package/src/search.rs +224 -0
  37. package/src/walk.rs +61 -0
package/README.md CHANGED
@@ -1,3 +1,95 @@
1
- # Temporary Holding Version
1
+ # bare-ripgrep
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ ripgrep's search and walk crates (`ignore`, `grep-searcher`, `grep-regex`) as a native addon for Bare and Node. It searches a local folder without an `rg` subprocess, so it also runs on iOS and Android.
4
+
5
+ The result is meant to match the `grep` tool of qvac-harness (`lib/tools/fs/search.ts`, `walk.ts`, `ignore.ts`, `glob.ts`) while being faster and not stopping at its 10k-entry JS walk cap.
6
+
7
+ ## Install
8
+
9
+ ```
10
+ npm i bare-ripgrep
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```js
16
+ const { search, walk } = require('bare-ripgrep')
17
+
18
+ const { hits, truncated, cut } = await search({
19
+ root: '/path/to/project',
20
+ queries: ['todo', 'fixme'],
21
+ include: '*.ts'
22
+ })
23
+ // hits: [{ path: 'src/a.ts', line: 12, text: '// TODO: ...' }]
24
+
25
+ const { files } = await walk({ root: '/path/to/project', folder: 'src' })
26
+ // files: [{ path: 'src/a.ts', mtime: 1760000000000 }]
27
+ ```
28
+
29
+ ### `search(options)`
30
+
31
+ | Option | Default | Meaning |
32
+ | ---------------- | ------- | ----------------------------------------------------------------------------------------- |
33
+ | `root` | | Absolute directory. Returned paths are relative to it, `/`-separated. |
34
+ | `folder` | `''` | Subfolder of `root` to search. |
35
+ | `queries` | | Each query is an alternative. Literal text unless `regex`. |
36
+ | `regex` | `false` | Queries are regular expressions (Rust `regex` syntax). |
37
+ | `caseSensitive` | `false` | |
38
+ | `include` | | Glob or array of globs. A bare name matches the basename at any depth; with `/` the path. |
39
+ | `deny` | | Absolute paths whose files and folders are skipped, case-insensitively. |
40
+ | `includeIgnored` | `false` | Also walk what `.gitignore`, `.ignore` and the hidden tool folders leave out. |
41
+ | `maxMatches` | `1000` | Most hits returned. |
42
+ | `maxEntries` | `10000` | Most entries (files and folders) the walk keeps. |
43
+ | `signal` | | `AbortSignal`. Abort rejects with an `AbortError` and stops the native work. |
44
+
45
+ Resolves `{ hits: { path, line, text }[], truncated, cut }`. Hits are sorted by path, then line. `truncated` is set when there were more hits than `maxMatches` or more entries than `maxEntries`. `cut` is set when a line past 1 MiB was searched in its head only.
46
+
47
+ ### `walk(options)`
48
+
49
+ Same scope options (`root`, `folder`, `include`, `deny`, `includeIgnored`, `maxEntries`, `signal`). Resolves `{ files: { path, mtime }[], truncated }`, sorted by path; `mtime` is in milliseconds.
50
+
51
+ ## Semantics
52
+
53
+ - Case-insensitive by default. Queries are literal unless `regex`.
54
+ - A file with a NUL in its first 1024 bytes is binary and skipped; a later NUL is text.
55
+ - `.gitignore` and `.ignore` are honoured with or without a git repository, `.ignore` wins. Ignore files between `root` and `folder` apply. `.git/info/exclude`, global ignore files and ignore files above `root` are not read. Dotfiles are searched.
56
+ - `.git`, `node_modules` and `.trash` are always skipped. `.hg`, `.svn`, `.venv`, `.cache` and `.next` are skipped unless `includeIgnored`.
57
+ - Symlinks are never followed, files or folders.
58
+ - Hit text is the line without its `\n` (a `\r` stays), cut to 500 UTF-16 units. A line past 1 MiB is searched in its head only.
59
+ - With `maxMatches`, the hits kept are the first by path then line, whatever the thread timing.
60
+
61
+ ## Differences from the harness engine
62
+
63
+ - **Regex dialect.** Queries with `regex` use Rust's `regex` syntax, not JavaScript's: no look-around and no backreferences (they reject with `invalid regex`), `\w`, `\d` and `\b` are Unicode-aware, a pattern naming a newline is rejected, and `.` also matches `\r`. Literal queries are unaffected.
64
+ - **Case folding.** `caseSensitive: false` uses Unicode simple case folding, which can differ from JS `i` on a few characters.
65
+ - **Ignore engine.** Ignore rules are git's, through the `ignore` crate, not the harness's regex translation; edge cases of exotic patterns can differ. A `folder` that an ancestor's ignore file ignores yields nothing; the harness searches it.
66
+ - **Caps under parallelism.** The walk is parallel. When `maxEntries` cuts it short, which entries were kept can differ between runs. The `maxMatches` result is deterministic. Entries under a `deny` path are pruned, not counted.
67
+ - **Long lines.** A line past 1 MiB is searched in its head only and sets `cut` (the harness sets `cut` only for a cut drive read). A line past 16 MiB ends that file's scan, keeps its earlier hits and sets `cut`.
68
+ - **Text clipping.** A surrogate pair split by the 500-unit cut is dropped whole; JS would keep a lone surrogate.
69
+ - **Not provided.** `maxMs` and `skippedLinks`. A file that cannot be opened is skipped silently.
70
+
71
+ ## Build
72
+
73
+ `bare-make generate && bare-make build && bare-make install` (`npm run build`) writes `prebuilds/<platform>-<arch>/bare-ripgrep.bare` and `bare-ripgrep.node`: one build, one C binding (`binding.c`, written against `js.h`), loaded by Bare directly and by Node through `bare-compat-napi`.
74
+
75
+ The Rust crate (`Cargo.toml`, `src/`) builds as a `staticlib` through `cmake-cargo`, which runs `cargo build --release --target <triple>` and imports the archive. It needs `cargo` and the rustup target of the platform being built. The C side only passes a JSON request to Rust and a JSON result back; the work runs on the libuv thread pool (`uv_queue_work`), and the walk fans out over its own threads. The Rust entry points never unwind into C.
76
+
77
+ | Target | Rust triple |
78
+ | ------------------------------------------ | ------------------------------------------------------- |
79
+ | `darwin-arm64`, `darwin-x64` | `aarch64-apple-darwin`, `x86_64-apple-darwin` |
80
+ | `linux-arm64`, `linux-x64` | `aarch64-unknown-linux-gnu`, `x86_64-unknown-linux-gnu` |
81
+ | `win32-arm64`, `win32-x64` | `aarch64-pc-windows-msvc`, `x86_64-pc-windows-msvc` |
82
+ | `ios-arm64` | `aarch64-apple-ios` |
83
+ | `ios-arm64-simulator`, `ios-x64-simulator` | `aarch64-apple-ios-sim`, `x86_64-apple-ios` |
84
+ | `android-arm64`, `android-x64` | `aarch64-linux-android`, `x86_64-linux-android` |
85
+
86
+ ## Test and bench
87
+
88
+ ```
89
+ npm test # brittle, on Node and on Bare
90
+ node bench/index.js <dir> [pattern...] # search() against `rg -i -F` under the same ignore rules
91
+ ```
92
+
93
+ ## License
94
+
95
+ Apache-2.0
package/binding.c ADDED
@@ -0,0 +1,169 @@
1
+ #include <assert.h>
2
+ #include <bare.h>
3
+ #include <js.h>
4
+ #include <stdbool.h>
5
+ #include <stdlib.h>
6
+ #include <string.h>
7
+ #include <uv.h>
8
+
9
+ #include "src/bare_ripgrep.h"
10
+
11
+ typedef struct {
12
+ uv_work_t handle;
13
+ js_env_t *env;
14
+ js_ref_t *cb;
15
+ js_deferred_teardown_t *teardown;
16
+ rg_job_t *job;
17
+ bool exiting;
18
+ } bare_ripgrep_job_t;
19
+
20
+ static void
21
+ bare_ripgrep__on_work (uv_work_t *handle) {
22
+ bare_ripgrep_job_t *job = handle->data;
23
+
24
+ rg_job_run(job->job);
25
+ }
26
+
27
+ static void
28
+ bare_ripgrep__on_after_work (uv_work_t *handle, int status) {
29
+ int err;
30
+
31
+ bare_ripgrep_job_t *job = handle->data;
32
+ js_env_t *env = job->env;
33
+ js_deferred_teardown_t *teardown = job->teardown;
34
+
35
+ if (!job->exiting) {
36
+ js_handle_scope_t *scope;
37
+ err = js_open_handle_scope(env, &scope);
38
+ assert(err == 0);
39
+
40
+ js_value_t *cb;
41
+ err = js_get_reference_value(env, job->cb, &cb);
42
+ assert(err == 0);
43
+
44
+ size_t len;
45
+ const char *json = rg_job_result(job->job, &len);
46
+
47
+ js_value_t *argv[1];
48
+ err = js_create_string_utf8(env, (const utf8_t *) json, len, &argv[0]);
49
+ assert(err == 0);
50
+
51
+ js_value_t *global;
52
+ err = js_get_global(env, &global);
53
+ assert(err == 0);
54
+
55
+ js_call_function_with_checkpoint(env, global, cb, 1, argv, NULL);
56
+
57
+ err = js_close_handle_scope(env, scope);
58
+ assert(err == 0);
59
+ }
60
+
61
+ err = js_delete_reference(env, job->cb);
62
+ assert(err == 0);
63
+
64
+ rg_job_free(job->job);
65
+ free(job);
66
+
67
+ err = js_finish_deferred_teardown_callback(teardown);
68
+ assert(err == 0);
69
+ }
70
+
71
+ static void
72
+ bare_ripgrep__on_teardown (js_deferred_teardown_t *handle, void *data) {
73
+ bare_ripgrep_job_t *job = data;
74
+
75
+ job->exiting = true;
76
+
77
+ rg_job_cancel(job->job);
78
+ }
79
+
80
+ static js_value_t *
81
+ bare_ripgrep_start (js_env_t *env, js_callback_info_t *info) {
82
+ int err;
83
+
84
+ size_t argc = 2;
85
+ js_value_t *argv[2];
86
+
87
+ err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
88
+ assert(err == 0);
89
+
90
+ assert(argc == 2);
91
+
92
+ size_t len;
93
+ err = js_get_value_string_utf8(env, argv[0], NULL, 0, &len);
94
+ assert(err == 0);
95
+
96
+ utf8_t *json = malloc(len + 1);
97
+ err = js_get_value_string_utf8(env, argv[0], json, len + 1, &len);
98
+ assert(err == 0);
99
+
100
+ bare_ripgrep_job_t *job = malloc(sizeof(bare_ripgrep_job_t));
101
+ job->env = env;
102
+ job->exiting = false;
103
+ job->handle.data = job;
104
+ job->job = rg_job_new((const char *) json, len);
105
+
106
+ free(json);
107
+
108
+ err = js_create_reference(env, argv[1], 1, &job->cb);
109
+ assert(err == 0);
110
+
111
+ uv_loop_t *loop;
112
+ err = js_get_env_loop(env, &loop);
113
+ assert(err == 0);
114
+
115
+ err = js_add_deferred_teardown_callback(env, bare_ripgrep__on_teardown, job, &job->teardown);
116
+ assert(err == 0);
117
+
118
+ err = uv_queue_work(loop, &job->handle, bare_ripgrep__on_work, bare_ripgrep__on_after_work);
119
+ assert(err == 0);
120
+
121
+ js_value_t *handle;
122
+ err = js_create_external(env, job, NULL, NULL, &handle);
123
+ assert(err == 0);
124
+
125
+ return handle;
126
+ }
127
+
128
+ static js_value_t *
129
+ bare_ripgrep_cancel (js_env_t *env, js_callback_info_t *info) {
130
+ int err;
131
+
132
+ size_t argc = 1;
133
+ js_value_t *argv[1];
134
+
135
+ err = js_get_callback_info(env, info, &argc, argv, NULL, NULL);
136
+ assert(err == 0);
137
+
138
+ assert(argc == 1);
139
+
140
+ bare_ripgrep_job_t *job;
141
+ err = js_get_value_external(env, argv[0], (void **) &job);
142
+ assert(err == 0);
143
+
144
+ rg_job_cancel(job->job);
145
+
146
+ return NULL;
147
+ }
148
+
149
+ static js_value_t *
150
+ bare_ripgrep_exports (js_env_t *env, js_value_t *exports) {
151
+ int err;
152
+
153
+ #define V(name, fn) \
154
+ { \
155
+ js_value_t *val; \
156
+ err = js_create_function(env, name, -1, fn, NULL, &val); \
157
+ assert(err == 0); \
158
+ err = js_set_named_property(env, exports, name, val); \
159
+ assert(err == 0); \
160
+ }
161
+
162
+ V("start", bare_ripgrep_start)
163
+ V("cancel", bare_ripgrep_cancel)
164
+ #undef V
165
+
166
+ return exports;
167
+ }
168
+
169
+ BARE_MODULE(bare_ripgrep, bare_ripgrep_exports)
package/binding.js ADDED
@@ -0,0 +1,3 @@
1
+ require.addon = require('require-addon')
2
+
3
+ module.exports = require.addon('.', __filename)
package/index.d.ts ADDED
@@ -0,0 +1,70 @@
1
+ export interface Hit {
2
+ /** Relative to `root`, `/`-separated. */
3
+ path: string
4
+ /** 1-based. */
5
+ line: number
6
+ /** The line without its newline, at most 500 UTF-16 units. */
7
+ text: string
8
+ }
9
+
10
+ export interface WalkedFile {
11
+ /** Relative to `root`, `/`-separated. */
12
+ path: string
13
+ /** Milliseconds since the epoch. */
14
+ mtime: number
15
+ }
16
+
17
+ interface Scope {
18
+ /** Absolute directory every returned path is relative to. */
19
+ root: string
20
+ /** Subfolder of `root` to search, `''` for all of it. */
21
+ folder?: string
22
+ /**
23
+ * Glob a file must match: a bare name matches the basename at any depth, a pattern with
24
+ * `/` matches the path relative to `root`. Several globs are alternatives.
25
+ */
26
+ include?: string | string[]
27
+ /** Absolute paths whose files and folders are skipped (case-insensitive). */
28
+ deny?: string[]
29
+ /** Also walk what ignore files and the hidden tool folders leave out. */
30
+ includeIgnored?: boolean
31
+ /** Most entries (files and folders) the walk keeps, default 10000. */
32
+ maxEntries?: number
33
+ signal?: AbortSignal
34
+ }
35
+
36
+ export interface SearchOptions extends Scope {
37
+ /** Each query is an alternative. */
38
+ queries: string[]
39
+ /** Queries are regular expressions rather than literals. */
40
+ regex?: boolean
41
+ caseSensitive?: boolean
42
+ /** Most hits returned, default 1000. */
43
+ maxMatches?: number
44
+ }
45
+
46
+ export interface SearchResult {
47
+ /** Sorted by path, then line. */
48
+ hits: Hit[]
49
+ /** More hits or entries than the caps allowed. */
50
+ truncated: boolean
51
+ /** A line past 1 MiB was searched in its head only. */
52
+ cut: boolean
53
+ }
54
+
55
+ export interface WalkOptions extends Scope {}
56
+
57
+ export interface WalkResult {
58
+ /** Sorted by path. */
59
+ files: WalkedFile[]
60
+ truncated: boolean
61
+ }
62
+
63
+ export class AbortError extends Error {
64
+ name: 'AbortError'
65
+ code: 'ABORT_ERR'
66
+ }
67
+
68
+ export function search(options: SearchOptions): Promise<SearchResult>
69
+
70
+ export function walk(options: WalkOptions): Promise<WalkResult>
package/index.js ADDED
@@ -0,0 +1,100 @@
1
+ const binding = require('./binding')
2
+
3
+ const MAX_MATCHES = 1000
4
+ const MAX_ENTRIES = 10_000
5
+
6
+ class AbortError extends Error {
7
+ constructor() {
8
+ super('The operation was aborted')
9
+ this.name = 'AbortError'
10
+ this.code = 'ABORT_ERR'
11
+ }
12
+ }
13
+
14
+ exports.AbortError = AbortError
15
+
16
+ exports.search = function search({
17
+ root,
18
+ folder = '',
19
+ queries,
20
+ regex = false,
21
+ caseSensitive = false,
22
+ include,
23
+ deny,
24
+ includeIgnored = false,
25
+ maxMatches = MAX_MATCHES,
26
+ maxEntries = MAX_ENTRIES,
27
+ signal
28
+ }) {
29
+ return run(
30
+ {
31
+ op: 'search',
32
+ root,
33
+ folder,
34
+ queries,
35
+ regex,
36
+ caseSensitive,
37
+ include: list(include),
38
+ deny: list(deny),
39
+ includeIgnored,
40
+ maxMatches: count(maxMatches),
41
+ maxEntries: count(maxEntries)
42
+ },
43
+ signal
44
+ )
45
+ }
46
+
47
+ exports.walk = function walk({
48
+ root,
49
+ folder = '',
50
+ include,
51
+ deny,
52
+ includeIgnored = false,
53
+ maxEntries = MAX_ENTRIES,
54
+ signal
55
+ }) {
56
+ return run(
57
+ {
58
+ op: 'walk',
59
+ root,
60
+ folder,
61
+ include: list(include),
62
+ deny: list(deny),
63
+ includeIgnored,
64
+ maxMatches: 0,
65
+ maxEntries: count(maxEntries)
66
+ },
67
+ signal
68
+ )
69
+ }
70
+
71
+ function run(request, signal) {
72
+ return new Promise((resolve, reject) => {
73
+ if (signal && signal.aborted) return reject(new AbortError())
74
+
75
+ let job = null
76
+
77
+ const onabort = () => {
78
+ binding.cancel(job)
79
+ reject(new AbortError())
80
+ }
81
+
82
+ job = binding.start(JSON.stringify(request), (json) => {
83
+ if (signal) signal.removeEventListener('abort', onabort)
84
+
85
+ const result = JSON.parse(json)
86
+ if (result.error) reject(new Error(result.error))
87
+ else resolve(result)
88
+ })
89
+
90
+ if (signal) signal.addEventListener('abort', onabort, { once: true })
91
+ })
92
+ }
93
+
94
+ function list(value) {
95
+ return value === undefined ? [] : [].concat(value)
96
+ }
97
+
98
+ function count(value) {
99
+ return Math.min(value, Number.MAX_SAFE_INTEGER)
100
+ }
package/package.json CHANGED
@@ -1,6 +1,68 @@
1
1
  {
2
2
  "name": "bare-ripgrep",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "description": "ripgrep's search and walk crates as a native addon for Bare and Node",
5
+ "main": "index.js",
6
+ "exports": {
7
+ "./package": "./package.json",
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "package.json",
15
+ "index.js",
16
+ "index.d.ts",
17
+ "binding.js",
18
+ "binding.c",
19
+ "Cargo.toml",
20
+ "Cargo.lock",
21
+ "src",
22
+ "CMakeLists.txt",
23
+ "NOTICE.md",
24
+ "prebuilds"
25
+ ],
26
+ "addon": true,
27
+ "scripts": {
28
+ "build": "bare-make generate && bare-make build && bare-make install",
29
+ "bench": "node bench/index.js",
30
+ "format": "prettier . --write",
31
+ "test": "npm run test:node && npm run test:bare",
32
+ "test:node": "brittle-node test/all.mjs",
33
+ "test:bare": "brittle-bare test/all.mjs",
34
+ "test:generate": "brittle-make-test test/all.mjs test/*.test.js",
35
+ "lint": "prettier . --check && lunte"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/tetherto/bare-ripgrep.git"
40
+ },
41
+ "author": "Tether Inc",
42
+ "license": "Apache-2.0",
43
+ "bugs": {
44
+ "url": "https://github.com/tetherto/bare-ripgrep/issues"
45
+ },
46
+ "homepage": "https://github.com/tetherto/bare-ripgrep",
47
+ "engines": {
48
+ "bare": ">=1.16.0"
49
+ },
50
+ "dependencies": {
51
+ "require-addon": "^1.1.0"
52
+ },
53
+ "devDependencies": {
54
+ "bare-abort-controller": "^1.0.0",
55
+ "bare-compat-napi": "^1.3.5",
56
+ "bare-fs": "^4.1.2",
57
+ "bare-make": "^1.7.0",
58
+ "bare-os": "^3.9.4",
59
+ "bare-path": "^3.0.0",
60
+ "brittle": "^4.1.0",
61
+ "cmake-bare": "^1.8.0",
62
+ "cmake-cargo": "^0.1.1",
63
+ "cmake-napi": "^1.2.1",
64
+ "lunte": "^1.8.4",
65
+ "prettier": "^3.9.6",
66
+ "prettier-config-holepunch": "^2.0.0"
67
+ }
68
+ }
@@ -0,0 +1,23 @@
1
+ #ifndef BARE_RIPGREP_H
2
+ #define BARE_RIPGREP_H
3
+
4
+ #include <stddef.h>
5
+
6
+ typedef struct rg_job_s rg_job_t;
7
+
8
+ rg_job_t *
9
+ rg_job_new (const char *json, size_t len);
10
+
11
+ void
12
+ rg_job_run (const rg_job_t *job);
13
+
14
+ void
15
+ rg_job_cancel (const rg_job_t *job);
16
+
17
+ const char *
18
+ rg_job_result (const rg_job_t *job, size_t *len);
19
+
20
+ void
21
+ rg_job_free (rg_job_t *job);
22
+
23
+ #endif
package/src/glob.rs ADDED
@@ -0,0 +1,47 @@
1
+ use regex::Regex;
2
+
3
+ /// Mirrors qvac-harness `compileGlob`: `*` and `?` stay inside a segment, `**/` spans
4
+ /// directories, anything else is literal. A pattern with `/` tests the path, else the name.
5
+ pub struct Glob {
6
+ regex: Regex,
7
+ on_path: bool,
8
+ }
9
+
10
+ impl Glob {
11
+ pub fn new(pattern: &str) -> Result<Glob, String> {
12
+ let pattern = pattern.strip_prefix("./").unwrap_or(pattern);
13
+ let chars: Vec<char> = pattern.chars().collect();
14
+ let mut source = String::from("(?s)^");
15
+ let mut i = 0;
16
+ while i < chars.len() {
17
+ let rest = &chars[i..];
18
+ if rest.starts_with(&['*', '*', '/']) {
19
+ source.push_str("(?:.*/)?");
20
+ i += 3;
21
+ } else if rest.starts_with(&['*', '*']) {
22
+ source.push_str(".*");
23
+ i += 2;
24
+ } else if rest[0] == '*' {
25
+ source.push_str("[^/]*");
26
+ i += 1;
27
+ } else if rest[0] == '?' {
28
+ source.push_str("[^/]");
29
+ i += 1;
30
+ } else {
31
+ source.push_str(&regex::escape(&rest[0].to_string()));
32
+ i += 1;
33
+ }
34
+ }
35
+ source.push('$');
36
+
37
+ let regex = Regex::new(&source).map_err(|err| format!("invalid include glob: {err}"))?;
38
+ Ok(Glob {
39
+ regex,
40
+ on_path: pattern.contains('/'),
41
+ })
42
+ }
43
+
44
+ pub fn matches(&self, name: &str, path: &str) -> bool {
45
+ self.regex.is_match(if self.on_path { path } else { name })
46
+ }
47
+ }