bare-ripgrep 0.0.0 → 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 +95 -0
  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 +68 -1
  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/src/lib.rs ADDED
@@ -0,0 +1,111 @@
1
+ mod glob;
2
+ mod request;
3
+ mod scope;
4
+ mod search;
5
+ mod walk;
6
+
7
+ use std::panic::{catch_unwind, AssertUnwindSafe};
8
+ use std::slice;
9
+ use std::sync::atomic::{AtomicBool, Ordering};
10
+ use std::sync::{Arc, Mutex};
11
+
12
+ use request::{Op, Request};
13
+ use scope::Scope;
14
+ use serde::Serialize;
15
+
16
+ pub struct Job {
17
+ request: Vec<u8>,
18
+ cancel: Arc<AtomicBool>,
19
+ result: Mutex<Vec<u8>>,
20
+ }
21
+
22
+ #[derive(Serialize)]
23
+ struct Failure {
24
+ error: String,
25
+ }
26
+
27
+ fn execute(job: &Job) -> Result<Vec<u8>, String> {
28
+ let request: Request =
29
+ serde_json::from_slice(&job.request).map_err(|err| format!("invalid request: {err}"))?;
30
+ let scope = Arc::new(Scope::new(&request, Arc::clone(&job.cancel))?);
31
+ let max_matches = usize::try_from(request.max_matches).unwrap_or(usize::MAX - 1);
32
+
33
+ let json = match request.op {
34
+ Op::Search => {
35
+ let matcher = search::matcher(&request)?;
36
+ serde_json::to_vec(&search::search(scope, matcher, max_matches))
37
+ }
38
+ Op::Walk => serde_json::to_vec(&walk::walk(scope)),
39
+ };
40
+ json.map_err(|err| err.to_string())
41
+ }
42
+
43
+ fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
44
+ let detail = payload
45
+ .downcast_ref::<&str>()
46
+ .map(|text| text.to_string())
47
+ .or_else(|| payload.downcast_ref::<String>().cloned())
48
+ .unwrap_or_default();
49
+ format!("internal error: {detail}")
50
+ }
51
+
52
+ /// # Safety
53
+ /// `json` must point to `len` readable bytes.
54
+ #[no_mangle]
55
+ pub unsafe extern "C" fn rg_job_new(json: *const u8, len: usize) -> *mut Job {
56
+ let request = slice::from_raw_parts(json, len).to_vec();
57
+ Box::into_raw(Box::new(Job {
58
+ request,
59
+ cancel: Arc::new(AtomicBool::new(false)),
60
+ result: Mutex::new(Vec::new()),
61
+ }))
62
+ }
63
+
64
+ /// Blocks until the job is done; call it off the JS thread. Never unwinds.
65
+ ///
66
+ /// # Safety
67
+ /// `job` must come from `rg_job_new` and not be freed while this runs.
68
+ #[no_mangle]
69
+ pub unsafe extern "C" fn rg_job_run(job: *const Job) {
70
+ let job = &*job;
71
+ let outcome = catch_unwind(AssertUnwindSafe(|| execute(job)));
72
+ let bytes = match outcome {
73
+ Ok(Ok(bytes)) => bytes,
74
+ Ok(Err(error)) => serde_json::to_vec(&Failure { error }).unwrap_or_default(),
75
+ Err(payload) => serde_json::to_vec(&Failure {
76
+ error: panic_message(payload.as_ref()),
77
+ })
78
+ .unwrap_or_default(),
79
+ };
80
+ *job.result
81
+ .lock()
82
+ .unwrap_or_else(|poisoned| poisoned.into_inner()) = bytes;
83
+ }
84
+
85
+ /// # Safety
86
+ /// `job` must come from `rg_job_new` and not be freed.
87
+ #[no_mangle]
88
+ pub unsafe extern "C" fn rg_job_cancel(job: *const Job) {
89
+ (*job).cancel.store(true, Ordering::Relaxed);
90
+ }
91
+
92
+ /// The JSON result, valid until `rg_job_free`. Call after `rg_job_run` returned.
93
+ ///
94
+ /// # Safety
95
+ /// `job` must come from `rg_job_new`; `len` must be writable.
96
+ #[no_mangle]
97
+ pub unsafe extern "C" fn rg_job_result(job: *const Job, len: *mut usize) -> *const u8 {
98
+ let result = (*job)
99
+ .result
100
+ .lock()
101
+ .unwrap_or_else(|poisoned| poisoned.into_inner());
102
+ *len = result.len();
103
+ result.as_ptr()
104
+ }
105
+
106
+ /// # Safety
107
+ /// `job` must come from `rg_job_new` and not be used after.
108
+ #[no_mangle]
109
+ pub unsafe extern "C" fn rg_job_free(job: *mut Job) {
110
+ drop(Box::from_raw(job));
111
+ }
package/src/request.rs ADDED
@@ -0,0 +1,31 @@
1
+ use serde::Deserialize;
2
+
3
+ #[derive(Deserialize, Clone, Copy, PartialEq, Eq)]
4
+ #[serde(rename_all = "lowercase")]
5
+ pub enum Op {
6
+ Search,
7
+ Walk,
8
+ }
9
+
10
+ #[derive(Deserialize)]
11
+ #[serde(rename_all = "camelCase", deny_unknown_fields)]
12
+ pub struct Request {
13
+ pub op: Op,
14
+ pub root: String,
15
+ #[serde(default)]
16
+ pub folder: String,
17
+ #[serde(default)]
18
+ pub queries: Vec<String>,
19
+ #[serde(default)]
20
+ pub regex: bool,
21
+ #[serde(default)]
22
+ pub case_sensitive: bool,
23
+ #[serde(default)]
24
+ pub include: Vec<String>,
25
+ #[serde(default)]
26
+ pub deny: Vec<String>,
27
+ #[serde(default)]
28
+ pub include_ignored: bool,
29
+ pub max_matches: u64,
30
+ pub max_entries: u64,
31
+ }
package/src/scope.rs ADDED
@@ -0,0 +1,170 @@
1
+ use std::path::{Path, MAIN_SEPARATOR};
2
+ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
3
+ use std::sync::Arc;
4
+
5
+ use ignore::{DirEntry, WalkBuilder};
6
+
7
+ use crate::glob::Glob;
8
+ use crate::request::Request;
9
+
10
+ // Skipped even with includeIgnored.
11
+ const SKIP_DIRS: [&str; 3] = [".git", "node_modules", ".trash"];
12
+ // includeIgnored lifts these.
13
+ const HIDDEN_DIRS: [&str; 5] = [".hg", ".svn", ".venv", ".cache", ".next"];
14
+
15
+ pub struct Scope {
16
+ pub root: String,
17
+ folder: Vec<String>,
18
+ include_ignored: bool,
19
+ deny: Vec<String>,
20
+ include: Vec<Glob>,
21
+ max_entries: u64,
22
+ entries: AtomicU64,
23
+ pub truncated: AtomicBool,
24
+ pub cancel: Arc<AtomicBool>,
25
+ }
26
+
27
+ impl Scope {
28
+ pub fn new(req: &Request, cancel: Arc<AtomicBool>) -> Result<Scope, String> {
29
+ let mut folder = Vec::new();
30
+ for part in req.folder.split(['/', '\\']) {
31
+ match part {
32
+ "" | "." => {}
33
+ ".." => return Err(format!("folder escapes root: {}", req.folder)),
34
+ part => folder.push(part.to_string()),
35
+ }
36
+ }
37
+
38
+ let mut dir = Path::new(&req.root).to_path_buf();
39
+ dir.extend(&folder);
40
+ if !dir.is_dir() {
41
+ return Err(format!("not a directory: {}", dir.display()));
42
+ }
43
+
44
+ let include = req
45
+ .include
46
+ .iter()
47
+ .map(|pattern| Glob::new(pattern))
48
+ .collect::<Result<_, _>>()?;
49
+
50
+ Ok(Scope {
51
+ root: req.root.clone(),
52
+ folder,
53
+ include_ignored: req.include_ignored,
54
+ deny: req.deny.iter().map(|path| fold(path)).collect(),
55
+ include,
56
+ max_entries: req.max_entries,
57
+ entries: AtomicU64::new(0),
58
+ truncated: AtomicBool::new(false),
59
+ cancel,
60
+ })
61
+ }
62
+
63
+ pub fn walker(self: &Arc<Scope>, threads: usize) -> WalkBuilder {
64
+ let scope = Arc::clone(self);
65
+ let mut builder = WalkBuilder::new(&self.root);
66
+ builder
67
+ .standard_filters(false)
68
+ .git_ignore(!self.include_ignored)
69
+ .ignore(!self.include_ignored)
70
+ .require_git(false)
71
+ .follow_links(false)
72
+ .threads(threads)
73
+ .filter_entry(move |entry| scope.admits(entry));
74
+ builder
75
+ }
76
+
77
+ fn admits(&self, entry: &DirEntry) -> bool {
78
+ let depth = entry.depth();
79
+ if depth == 0 {
80
+ return true;
81
+ }
82
+
83
+ let name = entry.file_name().to_string_lossy();
84
+ if SKIP_DIRS.contains(&name.as_ref()) {
85
+ return false;
86
+ }
87
+
88
+ let is_dir = entry.file_type().is_some_and(|kind| kind.is_dir());
89
+ if is_dir && !self.include_ignored && HIDDEN_DIRS.contains(&name.as_ref()) {
90
+ return false;
91
+ }
92
+
93
+ // Walking from the root keeps the ignore files between it and the folder in force.
94
+ if depth <= self.folder.len() && name != self.folder[depth - 1] {
95
+ return false;
96
+ }
97
+
98
+ !self.is_denied(entry.path())
99
+ }
100
+
101
+ fn is_denied(&self, path: &Path) -> bool {
102
+ if self.deny.is_empty() {
103
+ return false;
104
+ }
105
+
106
+ let path = fold(&path.to_string_lossy());
107
+ self.deny.iter().any(|root| {
108
+ path == *root
109
+ || path
110
+ .strip_prefix(root.as_str())
111
+ .is_some_and(|rest| rest.starts_with('/'))
112
+ })
113
+ }
114
+
115
+ pub fn cancelled(&self) -> bool {
116
+ self.cancel.load(Ordering::Relaxed)
117
+ }
118
+
119
+ /// Counts what the walk keeps, below the folder. False once over the cap.
120
+ pub fn keep(&self, entry: &DirEntry) -> bool {
121
+ if entry.depth() <= self.folder.len() {
122
+ return true;
123
+ }
124
+
125
+ let kept = self.entries.fetch_add(1, Ordering::Relaxed) + 1;
126
+ if kept > self.max_entries {
127
+ self.truncated.store(true, Ordering::Relaxed);
128
+ return false;
129
+ }
130
+
131
+ true
132
+ }
133
+
134
+ pub fn relative(&self, entry: &DirEntry) -> String {
135
+ let path = entry
136
+ .path()
137
+ .strip_prefix(&self.root)
138
+ .unwrap_or(entry.path());
139
+ let path = path.to_string_lossy();
140
+ if MAIN_SEPARATOR == '/' {
141
+ path.into_owned()
142
+ } else {
143
+ path.replace(MAIN_SEPARATOR, "/")
144
+ }
145
+ }
146
+
147
+ pub fn included(&self, entry: &DirEntry, path: &str) -> bool {
148
+ if self.include.is_empty() {
149
+ return true;
150
+ }
151
+
152
+ let name = entry.file_name().to_string_lossy();
153
+ self.include.iter().any(|glob| glob.matches(&name, path))
154
+ }
155
+ }
156
+
157
+ pub fn threads() -> usize {
158
+ std::thread::available_parallelism()
159
+ .map_or(4, |n| n.get())
160
+ .min(12)
161
+ }
162
+
163
+ fn fold(path: &str) -> String {
164
+ let path = path.to_lowercase();
165
+ if cfg!(windows) {
166
+ path.replace('\\', "/")
167
+ } else {
168
+ path
169
+ }
170
+ }
package/src/search.rs ADDED
@@ -0,0 +1,224 @@
1
+ use std::collections::BinaryHeap;
2
+ use std::fs::File;
3
+ use std::io::{self, Read};
4
+ use std::sync::atomic::{AtomicBool, Ordering};
5
+ use std::sync::{Arc, Mutex};
6
+
7
+ use grep_matcher::{LineTerminator, Matcher};
8
+ use grep_regex::{RegexMatcher, RegexMatcherBuilder};
9
+ use grep_searcher::{BinaryDetection, Searcher, SearcherBuilder, Sink, SinkMatch};
10
+ use ignore::WalkState;
11
+ use serde::Serialize;
12
+
13
+ use crate::request::Request;
14
+ use crate::scope::{threads, Scope};
15
+
16
+ const MAX_LINE_CHARS: usize = 500;
17
+ // head only: a NUL further in is text, as in the harness
18
+ const BINARY_PROBE_BYTES: usize = 1024;
19
+ // a longer line is searched in its head only
20
+ const MAX_LINE_BYTES: usize = 1024 * 1024;
21
+ const HEAP_LIMIT_BYTES: usize = 16 * 1024 * 1024;
22
+
23
+ #[derive(Serialize, PartialEq, Eq, PartialOrd, Ord)]
24
+ pub struct Hit {
25
+ path: String,
26
+ line: u64,
27
+ text: String,
28
+ }
29
+
30
+ #[derive(Serialize)]
31
+ pub struct Searched {
32
+ hits: Vec<Hit>,
33
+ truncated: bool,
34
+ cut: bool,
35
+ }
36
+
37
+ /// The `max_matches` smallest hits by (path, line), whatever order the files finish in.
38
+ struct Best {
39
+ max: usize,
40
+ hits: BinaryHeap<Hit>,
41
+ over: bool,
42
+ }
43
+
44
+ impl Best {
45
+ fn add(&mut self, hits: Vec<Hit>) {
46
+ for hit in hits {
47
+ self.hits.push(hit);
48
+ if self.hits.len() > self.max {
49
+ self.hits.pop();
50
+ self.over = true;
51
+ }
52
+ }
53
+ }
54
+
55
+ /// A file sorting past a full set can add nothing.
56
+ fn beyond(&self, path: &str) -> bool {
57
+ self.over
58
+ && self
59
+ .hits
60
+ .peek()
61
+ .is_some_and(|last| path > last.path.as_str())
62
+ }
63
+ }
64
+
65
+ pub fn matcher(req: &Request) -> Result<RegexMatcher, String> {
66
+ if req.queries.is_empty() {
67
+ return Err("queries is empty".into());
68
+ }
69
+
70
+ RegexMatcherBuilder::new()
71
+ .case_insensitive(!req.case_sensitive)
72
+ .fixed_strings(!req.regex)
73
+ .line_terminator(Some(b'\n'))
74
+ .build_many(&req.queries)
75
+ .map_err(|err| format!("invalid regex: {err}"))
76
+ }
77
+
78
+ pub fn search(scope: Arc<Scope>, matcher: RegexMatcher, max_matches: usize) -> Searched {
79
+ let best = Mutex::new(Best {
80
+ max: max_matches,
81
+ hits: BinaryHeap::new(),
82
+ over: false,
83
+ });
84
+ let cut = AtomicBool::new(false);
85
+
86
+ scope.walker(threads()).build_parallel().run(|| {
87
+ let scope = &scope;
88
+ let best = &best;
89
+ let cut = &cut;
90
+ let matcher = matcher.clone();
91
+ let mut searcher = SearcherBuilder::new()
92
+ .line_number(true)
93
+ .line_terminator(LineTerminator::byte(b'\n'))
94
+ .binary_detection(BinaryDetection::none())
95
+ .heap_limit(Some(HEAP_LIMIT_BYTES))
96
+ .build();
97
+
98
+ Box::new(move |result| {
99
+ let Ok(entry) = result else {
100
+ return WalkState::Continue;
101
+ };
102
+ if scope.cancelled() || !scope.keep(&entry) {
103
+ return WalkState::Quit;
104
+ }
105
+ if !entry.file_type().is_some_and(|kind| kind.is_file()) {
106
+ return WalkState::Continue;
107
+ }
108
+
109
+ let path = scope.relative(&entry);
110
+ if !scope.included(&entry, &path) || best.lock().unwrap().beyond(&path) {
111
+ return WalkState::Continue;
112
+ }
113
+
114
+ let mut sink = Collect {
115
+ matcher: &matcher,
116
+ path: &path,
117
+ // one past the cap, so a file alone can show there is more
118
+ room: max_matches + 1,
119
+ hits: Vec::new(),
120
+ cut: false,
121
+ };
122
+ scan(entry.path(), &matcher, &mut searcher, &mut sink);
123
+ if sink.cut {
124
+ cut.store(true, Ordering::Relaxed);
125
+ }
126
+ if !sink.hits.is_empty() {
127
+ best.lock().unwrap().add(sink.hits);
128
+ }
129
+
130
+ WalkState::Continue
131
+ })
132
+ });
133
+
134
+ let best = best.into_inner().unwrap();
135
+ Searched {
136
+ hits: best.hits.into_sorted_vec(),
137
+ truncated: best.over || scope.truncated.load(Ordering::Relaxed),
138
+ cut: cut.load(Ordering::Relaxed),
139
+ }
140
+ }
141
+
142
+ fn scan(
143
+ path: &std::path::Path,
144
+ matcher: &RegexMatcher,
145
+ searcher: &mut Searcher,
146
+ sink: &mut Collect,
147
+ ) {
148
+ let Ok(mut file) = File::open(path) else {
149
+ return;
150
+ };
151
+
152
+ let mut probe = [0u8; BINARY_PROBE_BYTES];
153
+ let mut filled = 0;
154
+ while filled < probe.len() {
155
+ match file.read(&mut probe[filled..]) {
156
+ Ok(0) => break,
157
+ Ok(n) => filled += n,
158
+ Err(err) if err.kind() == io::ErrorKind::Interrupted => {}
159
+ Err(_) => return,
160
+ }
161
+ }
162
+ if probe[..filled].contains(&0) {
163
+ return;
164
+ }
165
+
166
+ // what arrived before a read error still counts, and the file is flagged cut
167
+ if searcher
168
+ .search_reader(matcher, (&probe[..filled]).chain(file), &mut *sink)
169
+ .is_err()
170
+ {
171
+ sink.cut = true;
172
+ }
173
+ }
174
+
175
+ struct Collect<'a> {
176
+ matcher: &'a RegexMatcher,
177
+ path: &'a str,
178
+ room: usize,
179
+ hits: Vec<Hit>,
180
+ cut: bool,
181
+ }
182
+
183
+ impl Sink for Collect<'_> {
184
+ type Error = io::Error;
185
+
186
+ fn matched(&mut self, _: &Searcher, found: &SinkMatch<'_>) -> Result<bool, io::Error> {
187
+ let mut line = found.bytes();
188
+ if line.ends_with(b"\n") {
189
+ line = &line[..line.len() - 1];
190
+ }
191
+
192
+ if line.len() > MAX_LINE_BYTES {
193
+ self.cut = true;
194
+ line = &line[..MAX_LINE_BYTES];
195
+ if !self.matcher.is_match(line).unwrap_or(false) {
196
+ return Ok(true);
197
+ }
198
+ }
199
+
200
+ self.hits.push(Hit {
201
+ path: self.path.to_string(),
202
+ line: found.line_number().unwrap_or(0),
203
+ text: clip(line),
204
+ });
205
+ Ok(self.hits.len() < self.room)
206
+ }
207
+ }
208
+
209
+ /// The first MAX_LINE_CHARS UTF-16 units, like `text.slice(0, 500)`.
210
+ /// A surrogate pair split by the cut is dropped whole.
211
+ fn clip(line: &[u8]) -> String {
212
+ // 500 units take at most 1500 bytes, so a cut inside a character lands past them
213
+ let head = String::from_utf8_lossy(&line[..line.len().min(2048)]);
214
+ let mut units = 0;
215
+ let mut text = String::new();
216
+ for ch in head.chars() {
217
+ units += ch.len_utf16();
218
+ if units > MAX_LINE_CHARS {
219
+ break;
220
+ }
221
+ text.push(ch);
222
+ }
223
+ text
224
+ }
package/src/walk.rs ADDED
@@ -0,0 +1,61 @@
1
+ use std::sync::atomic::Ordering;
2
+ use std::sync::{Arc, Mutex};
3
+ use std::time::UNIX_EPOCH;
4
+
5
+ use ignore::WalkState;
6
+ use serde::Serialize;
7
+
8
+ use crate::scope::{threads, Scope};
9
+
10
+ #[derive(Serialize)]
11
+ pub struct File {
12
+ path: String,
13
+ mtime: i64,
14
+ }
15
+
16
+ #[derive(Serialize)]
17
+ pub struct Walked {
18
+ files: Vec<File>,
19
+ truncated: bool,
20
+ }
21
+
22
+ pub fn walk(scope: Arc<Scope>) -> Walked {
23
+ let files = Mutex::new(Vec::new());
24
+
25
+ scope.walker(threads()).build_parallel().run(|| {
26
+ let scope = &scope;
27
+ let files = &files;
28
+ Box::new(move |result| {
29
+ let Ok(entry) = result else {
30
+ return WalkState::Continue;
31
+ };
32
+ if scope.cancelled() || !scope.keep(&entry) {
33
+ return WalkState::Quit;
34
+ }
35
+ if !entry.file_type().is_some_and(|kind| kind.is_file()) {
36
+ return WalkState::Continue;
37
+ }
38
+
39
+ let path = scope.relative(&entry);
40
+ if !scope.included(&entry, &path) {
41
+ return WalkState::Continue;
42
+ }
43
+
44
+ let mtime = entry
45
+ .metadata()
46
+ .ok()
47
+ .and_then(|meta| meta.modified().ok())
48
+ .and_then(|time| time.duration_since(UNIX_EPOCH).ok())
49
+ .map_or(0, |since| since.as_millis() as i64);
50
+ files.lock().unwrap().push(File { path, mtime });
51
+ WalkState::Continue
52
+ })
53
+ });
54
+
55
+ let mut files = files.into_inner().unwrap();
56
+ files.sort_unstable_by(|a, b| a.path.cmp(&b.path));
57
+ Walked {
58
+ files,
59
+ truncated: scope.truncated.load(Ordering::Relaxed),
60
+ }
61
+ }