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.
- package/CMakeLists.txt +51 -0
- package/Cargo.lock +375 -0
- package/Cargo.toml +25 -0
- package/LICENSE.md +198 -0
- package/NOTICE.md +19 -0
- package/README.md +94 -2
- package/binding.c +169 -0
- package/binding.js +3 -0
- package/index.d.ts +70 -0
- package/index.js +100 -0
- package/package.json +66 -4
- package/prebuilds/android-arm64/bare-ripgrep.bare +0 -0
- package/prebuilds/android-arm64/bare-ripgrep.node +0 -0
- package/prebuilds/android-x64/bare-ripgrep.bare +0 -0
- package/prebuilds/android-x64/bare-ripgrep.node +0 -0
- package/prebuilds/darwin-arm64/bare-ripgrep.bare +0 -0
- package/prebuilds/darwin-arm64/bare-ripgrep.node +0 -0
- package/prebuilds/darwin-x64/bare-ripgrep.bare +0 -0
- package/prebuilds/darwin-x64/bare-ripgrep.node +0 -0
- package/prebuilds/ios-arm64/bare-ripgrep.bare +0 -0
- package/prebuilds/ios-arm64-simulator/bare-ripgrep.bare +0 -0
- package/prebuilds/ios-x64-simulator/bare-ripgrep.bare +0 -0
- package/prebuilds/linux-arm64/bare-ripgrep.bare +0 -0
- package/prebuilds/linux-arm64/bare-ripgrep.node +0 -0
- package/prebuilds/linux-x64/bare-ripgrep.bare +0 -0
- package/prebuilds/linux-x64/bare-ripgrep.node +0 -0
- package/prebuilds/win32-arm64/bare-ripgrep.bare +0 -0
- package/prebuilds/win32-arm64/bare-ripgrep.node +0 -0
- package/prebuilds/win32-x64/bare-ripgrep.bare +0 -0
- package/prebuilds/win32-x64/bare-ripgrep.node +0 -0
- package/src/bare_ripgrep.h +23 -0
- package/src/glob.rs +47 -0
- package/src/lib.rs +111 -0
- package/src/request.rs +31 -0
- package/src/scope.rs +170 -0
- package/src/search.rs +224 -0
- 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
|
+
}
|