1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// SPDX-License-Identifier: MIT OR Apache-2.0
// The `--target files` branch of `search`: match on file names, not contents.
// Included from search.rs.
/// Run the file-name search when `--target` asks for it.
///
/// Returns `Ok(true)` when the run is complete (`--target files`, summary
/// already emitted) and `Ok(false)` when the caller continues with the
/// content search (`--target content`, or `both` after the name hits).
fn search_file_names(
args: &SearchArgs,
global: &GlobalArgs,
writer: &mut NdjsonWriter<impl Write>,
shutdown: &ShutdownSignal,
canonical_paths: &[std::path::PathBuf],
missing_roots: &[std::path::PathBuf],
start: Instant,
) -> Result<bool> {
use crate::cli_args::SearchTarget;
if matches!(args.target, SearchTarget::Files | SearchTarget::Both) {
// The name search matches the union of every pattern, the same set the
// content search compiles, lowercased once when casefold applies.
let casefold = args.case_insensitive || args.smart_case;
let patterns: Vec<String> = search_patterns(args)
.into_iter()
.map(|pat| if casefold { pat.to_ascii_lowercase() } else { pat })
.collect();
// WalkParallel collect + parallel name filter; sort for stable NDJSON.
let walker = build_walker(args, canonical_paths, global)?;
// A subtree the walk could not read must reach the summary: the answer
// here is "every name under this root", and one lost subtree makes it
// partial.
let failures = Arc::new(Mutex::new(root_failures(missing_roots)));
let files = crate::concurrency::collect_files_parallel_tallied(
&walker,
Some(Arc::clone(&failures)),
);
let mut matches: Vec<(String, String)> =
if crate::concurrency::should_parallelize(files.len()) {
use rayon::prelude::*;
files
.par_iter()
.filter_map(|path| {
if shutdown.is_shutdown() {
return None;
}
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let hay = if casefold {
name.to_ascii_lowercase()
} else {
name.clone()
};
if patterns.iter().any(|pat| hay.contains(pat)) {
Some((path.display().to_string(), name))
} else {
None
}
})
.collect()
} else {
files
.iter()
.filter_map(|path| {
let name = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let hay = if casefold {
name.to_ascii_lowercase()
} else {
name.clone()
};
if patterns.iter().any(|pat| hay.contains(pat)) {
Some((path.display().to_string(), name))
} else {
None
}
})
.collect()
};
crate::concurrency::sort_parallel(&mut matches);
let total_name_hits = matches.len() as u64;
let start_idx = args.offset.min(total_name_hits) as usize;
let end_idx = match args.limit {
Some(lim) => start_idx.saturating_add(lim as usize).min(matches.len()),
None => matches.len(),
};
for (path, name) in &matches[start_idx..end_idx] {
writer.write_event(&crate::ndjson_types::FileMatchEvent {
r#type: "file_match",
path: path.clone(),
name: name.clone(),
target: "files",
})?;
}
let matched_emitted = (end_idx - start_idx) as u64;
// `files_matched` describes files that contributed a published record,
// so the page is what decides it: one file with two published names
// counts once, and a file whose only names were paged out counts zero.
let matched_files_published = {
let mut paths: Vec<&str> = matches[start_idx..end_idx]
.iter()
.map(|(path, _)| path.as_str())
.collect();
paths.sort_unstable();
paths.dedup();
paths.len() as u64
};
if matches!(args.target, SearchTarget::Files) {
let failures = std::mem::take(
&mut *failures
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner),
);
let base = Summary {
r#type: "summary",
// Every file the walk handed over was examined;
// `files_matched` counts the files that contributed a
// published name, and the page omitted by `--offset` and
// `--limit` is what `offset_skipped` and `total_matches`
// describe.
files_visited: files.len() as u64,
files_matched: matched_files_published,
files_modified: None,
files_skipped: None,
total_matches: Some(matched_emitted),
total_replacements: None,
offset_skipped: Some(start_idx as u64),
elapsed_ms: start.elapsed().as_millis() as u64,
// This path collects every name hit and slices the page
// afterwards, so it never cuts its own walk: reporting the
// writer's budget here said "stopped early" on a walk that
// read the whole tree. The omitted hits are visible in
// `files_matched` versus `total_matches` of the full run.
walker_stopped_early: None,
..Summary::default()
};
// A name run with no hit still answered "no name matches", so it
// keeps its zero status; a missing root is what fails it.
finish_search_run(
writer,
base,
failures,
missing_roots.len() as u64,
true,
args.format == Some(crate::cli::SearchFormat::Text),
)?;
return Ok(true);
}
}
Ok(false)
}