dsh-ops 0.0.0-stage → 0.2.2
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/CHANGELOG.md +202 -0
- package/LICENSE +30 -0
- package/NOTICE +106 -0
- package/PROVENANCE.md +435 -0
- package/README.en.md +126 -0
- package/README.md +115 -2
- package/README.zh.md +116 -0
- package/bin/dsh-ops.mjs +1216 -0
- package/cordis.patch.yml +160 -0
- package/docs/manual-validation.md +53 -0
- package/docs/release-0.2.1.md +72 -0
- package/docs/schema-baseline.json +64 -0
- package/docs/schema-current.json +84 -0
- package/docs/schema-measurement.md +17 -0
- package/dsh-plugin.json +88 -0
- package/icon.svg +12 -0
- package/lib/binary.js +409 -0
- package/lib/config.js +198 -0
- package/lib/handshake.js +252 -0
- package/lib/index.js +108 -0
- package/lib/jobs.js +42 -0
- package/lib/policy.js +64 -0
- package/lib/presentation.js +63 -0
- package/lib/profile-install.js +61 -0
- package/lib/rust.js +194 -0
- package/lib/session-shells.js +78 -0
- package/lib/shells.js +998 -0
- package/lib/tools.js +657 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +114 -4
- package/vendor/fastctx/Cargo.lock +3210 -0
- package/vendor/fastctx/Cargo.toml +94 -0
- package/vendor/fastctx/FORK.md +119 -0
- package/vendor/fastctx/LICENSE-APACHE +201 -0
- package/vendor/fastctx/NOTICE +40 -0
- package/vendor/fastctx/README.md +439 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES.md +17 -0
- package/vendor/fastctx/THIRD_PARTY_LICENSES_RUST.md +7914 -0
- package/vendor/fastctx/UPSTREAM.md +49 -0
- package/vendor/fastctx/build.rs +413 -0
- package/vendor/fastctx/src/background_status.rs +403 -0
- package/vendor/fastctx/src/binary.rs +75 -0
- package/vendor/fastctx/src/bounded_sort.rs +500 -0
- package/vendor/fastctx/src/budget.rs +781 -0
- package/vendor/fastctx/src/cli/mod.rs +110 -0
- package/vendor/fastctx/src/context_guard.rs +289 -0
- package/vendor/fastctx/src/control/mod.rs +6 -0
- package/vendor/fastctx/src/control/paths.rs +49 -0
- package/vendor/fastctx/src/control/settings.rs +753 -0
- package/vendor/fastctx/src/control/transaction.rs +531 -0
- package/vendor/fastctx/src/edit/document.rs +535 -0
- package/vendor/fastctx/src/edit/locks.rs +371 -0
- package/vendor/fastctx/src/edit/mod.rs +213 -0
- package/vendor/fastctx/src/edit/private_storage/unix.rs +315 -0
- package/vendor/fastctx/src/edit/private_storage/windows.rs +793 -0
- package/vendor/fastctx/src/edit/private_storage.rs +234 -0
- package/vendor/fastctx/src/edit/replace.rs +1030 -0
- package/vendor/fastctx/src/edit_server.rs +53 -0
- package/vendor/fastctx/src/encoding/reference_v011.rs +587 -0
- package/vendor/fastctx/src/encoding/snapshot_pipeline.rs +1678 -0
- package/vendor/fastctx/src/encoding.rs +1118 -0
- package/vendor/fastctx/src/file_executor.rs +1151 -0
- package/vendor/fastctx/src/file_snapshot.rs +1491 -0
- package/vendor/fastctx/src/glob_filter.rs +98 -0
- package/vendor/fastctx/src/glob_tool.rs +653 -0
- package/vendor/fastctx/src/grep_sink.rs +1162 -0
- package/vendor/fastctx/src/grep_tool.rs +2449 -0
- package/vendor/fastctx/src/lib.rs +45 -0
- package/vendor/fastctx/src/main.rs +15 -0
- package/vendor/fastctx/src/model.rs +51 -0
- package/vendor/fastctx/src/model_guidance.rs +62 -0
- package/vendor/fastctx/src/operation.rs +356 -0
- package/vendor/fastctx/src/ordered_window.rs +1235 -0
- package/vendor/fastctx/src/os_environment.rs +414 -0
- package/vendor/fastctx/src/path_codec.rs +850 -0
- package/vendor/fastctx/src/paths.rs +244 -0
- package/vendor/fastctx/src/process_identity.rs +763 -0
- package/vendor/fastctx/src/process_policy.rs +74 -0
- package/vendor/fastctx/src/read_tool/batch.rs +496 -0
- package/vendor/fastctx/src/read_tool/hex_file.rs +141 -0
- package/vendor/fastctx/src/read_tool/image_file.rs +88 -0
- package/vendor/fastctx/src/read_tool/mod.rs +245 -0
- package/vendor/fastctx/src/read_tool/pdf.rs +470 -0
- package/vendor/fastctx/src/read_tool/pdf_disabled.rs +47 -0
- package/vendor/fastctx/src/read_tool/pdf_engine.rs +664 -0
- package/vendor/fastctx/src/read_tool/text_file.rs +351 -0
- package/vendor/fastctx/src/render_plan.rs +468 -0
- package/vendor/fastctx/src/runtime/activity.rs +159 -0
- package/vendor/fastctx/src/runtime/hosts.rs +99 -0
- package/vendor/fastctx/src/runtime/journal.rs +556 -0
- package/vendor/fastctx/src/runtime/local_ipc.rs +186 -0
- package/vendor/fastctx/src/runtime/mod.rs +746 -0
- package/vendor/fastctx/src/runtime/protocol.rs +296 -0
- package/vendor/fastctx/src/runtime/session.rs +536 -0
- package/vendor/fastctx/src/runtime/windows_process.rs +66 -0
- package/vendor/fastctx/src/search_parallelism.rs +106 -0
- package/vendor/fastctx/src/search_text.rs +227 -0
- package/vendor/fastctx/src/server.rs +359 -0
- package/vendor/fastctx/src/server_manifest.rs +468 -0
- package/vendor/fastctx/src/server_support.rs +826 -0
- package/vendor/fastctx/src/session.rs +629 -0
- package/vendor/fastctx/src/shell/apply_patch_hint.rs +41 -0
- package/vendor/fastctx/src/shell/bash.rs +263 -0
- package/vendor/fastctx/src/shell/buffer.rs +108 -0
- package/vendor/fastctx/src/shell/encoding.rs +403 -0
- package/vendor/fastctx/src/shell/foreground.rs +115 -0
- package/vendor/fastctx/src/shell/jobs/admission.rs +91 -0
- package/vendor/fastctx/src/shell/jobs/background.rs +146 -0
- package/vendor/fastctx/src/shell/jobs/host.rs +830 -0
- package/vendor/fastctx/src/shell/jobs/identity.rs +29 -0
- package/vendor/fastctx/src/shell/jobs/mod.rs +1513 -0
- package/vendor/fastctx/src/shell/jobs/model.rs +244 -0
- package/vendor/fastctx/src/shell/jobs/output_log.rs +1148 -0
- package/vendor/fastctx/src/shell/jobs/store.rs +1300 -0
- package/vendor/fastctx/src/shell/mod.rs +345 -0
- package/vendor/fastctx/src/shell/normalize.rs +389 -0
- package/vendor/fastctx/src/shell/output.rs +406 -0
- package/vendor/fastctx/src/shell/process.rs +493 -0
- package/vendor/fastctx/src/shell_server.rs +156 -0
- package/vendor/fastctx/src/skip_report.rs +83 -0
- package/vendor/fastctx/src/stdio_transport.rs +177 -0
- package/vendor/fastctx/src/tool_schema.rs +204 -0
- package/vendor/fastctx/src/traversal.rs +846 -0
- package/vendor/fastctx/third-party/pdfium-7763/LICENSE +9 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/abseil.txt +202 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/agg23.txt +14 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/fast_float.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/freetype.txt +169 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/icu.txt +542 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/lcms.txt +27 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.ijg +260 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libjpeg_turbo.md +135 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libopenjpeg.txt +32 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libpng.txt +134 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/libtiff.txt +21 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/llvm-libc.txt +278 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/pdfium.txt +230 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/simdutf.txt +18 -0
- package/vendor/fastctx/third-party/pdfium-7763/licenses/zlib.txt +29 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
//! Shared project traversal with lossless search paths, partial results, and a
|
|
2
|
+
//! deterministic report of whatever the walk could not reach.
|
|
3
|
+
|
|
4
|
+
use crate::bounded_sort::sort_cancelable;
|
|
5
|
+
use crate::file_executor::{BurstUse, GrepGlobExecutor};
|
|
6
|
+
use crate::glob_filter::PathGlobFilter;
|
|
7
|
+
use crate::operation::OperationCtx;
|
|
8
|
+
#[cfg(test)]
|
|
9
|
+
use crate::operation::TestStage;
|
|
10
|
+
use crate::path_codec::{
|
|
11
|
+
PathRecord, ResolvedRoot, RootKind, display_path as search_display_path,
|
|
12
|
+
io_error_message as search_io_error_message,
|
|
13
|
+
};
|
|
14
|
+
use ignore::types::TypesBuilder;
|
|
15
|
+
use ignore::{DirEntry, WalkBuilder, WalkState};
|
|
16
|
+
use parking_lot::Mutex;
|
|
17
|
+
use std::collections::BTreeMap;
|
|
18
|
+
use std::fs;
|
|
19
|
+
use std::io;
|
|
20
|
+
use std::panic::{AssertUnwindSafe, catch_unwind};
|
|
21
|
+
use std::path::Path;
|
|
22
|
+
use std::sync::Arc;
|
|
23
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
24
|
+
|
|
25
|
+
pub(crate) const TRAVERSAL_BATCH_ITEMS: usize = 256;
|
|
26
|
+
|
|
27
|
+
/// Upper bound on skipped paths kept with full detail. A damaged tree can raise
|
|
28
|
+
/// far more failures than any response could carry, so detail is bounded while
|
|
29
|
+
/// the tally stays exact.
|
|
30
|
+
const SKIPPED_DETAIL_CAP: usize = 256;
|
|
31
|
+
|
|
32
|
+
/// Legacy replace candidate retained while search uses `PathRecord` directly.
|
|
33
|
+
#[derive(Debug)]
|
|
34
|
+
pub(crate) struct ProjectCandidate {
|
|
35
|
+
pub(crate) display: String,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// The schedule-independent ordering key for one traversal failure.
|
|
39
|
+
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
40
|
+
pub(crate) struct TraversalErrorKey {
|
|
41
|
+
/// Doubles as the reported path: `String` orders by the same bytes the key
|
|
42
|
+
/// needs, so the failure never carries a second copy of it.
|
|
43
|
+
pub(crate) display: String,
|
|
44
|
+
pub(crate) kind_rank: u8,
|
|
45
|
+
pub(crate) raw_os_error: Option<i32>,
|
|
46
|
+
pub(crate) normalized_message: String,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
50
|
+
pub(crate) struct TraversalFailure {
|
|
51
|
+
pub(crate) key: TraversalErrorKey,
|
|
52
|
+
/// Standalone sentence used when the failure ends the whole request.
|
|
53
|
+
pub(crate) message: String,
|
|
54
|
+
/// Short cause phrase for skip reporting, where the path is already shown.
|
|
55
|
+
reason: String,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Existing collection limit enforced at the first item beyond `maximum`.
|
|
59
|
+
#[derive(Clone, Copy)]
|
|
60
|
+
pub(crate) struct TraversalLimit {
|
|
61
|
+
pub(crate) maximum: usize,
|
|
62
|
+
pub(crate) message: &'static str,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// One path a walk could not enter or evaluate.
|
|
66
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
67
|
+
pub(crate) struct SkippedPath<'a> {
|
|
68
|
+
pub(crate) display: &'a str,
|
|
69
|
+
pub(crate) reason: &'a str,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Paths a walk had to skip, deduplicated by failure identity and ordered by the
|
|
73
|
+
/// same schedule-independent key the walk uses everywhere else, so the report is
|
|
74
|
+
/// identical across serial and parallel runs.
|
|
75
|
+
#[derive(Debug, Default)]
|
|
76
|
+
pub(crate) struct SkippedPaths {
|
|
77
|
+
/// Keyed by failure identity; the value is that failure's cause phrase.
|
|
78
|
+
entries: BTreeMap<TraversalErrorKey, String>,
|
|
79
|
+
beyond_cap: usize,
|
|
80
|
+
/// Lowest-ranked failure, kept verbatim for the paths that still fail whole.
|
|
81
|
+
minimum: Option<(TraversalErrorKey, String)>,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Batched traversal output: what the walk collected, plus what it could not reach.
|
|
85
|
+
pub(crate) struct TraversalCollection<T> {
|
|
86
|
+
pub(crate) items: Vec<T>,
|
|
87
|
+
pub(crate) skipped: SkippedPaths,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl SkippedPaths {
|
|
91
|
+
fn record(&mut self, failure: TraversalFailure) {
|
|
92
|
+
let TraversalFailure {
|
|
93
|
+
key,
|
|
94
|
+
message,
|
|
95
|
+
reason,
|
|
96
|
+
} = failure;
|
|
97
|
+
if self
|
|
98
|
+
.minimum
|
|
99
|
+
.as_ref()
|
|
100
|
+
.is_none_or(|(existing, _)| key < *existing)
|
|
101
|
+
{
|
|
102
|
+
self.minimum = Some((key.clone(), message));
|
|
103
|
+
}
|
|
104
|
+
self.insert(key, reason);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Past the cap the key is dropped, so repeats of an already-overflowed
|
|
108
|
+
/// failure add to the tally again. Holding every key just to keep the count
|
|
109
|
+
/// exact would defeat the bound the cap exists to provide, and a walk that
|
|
110
|
+
/// raised more than `SKIPPED_DETAIL_CAP` distinct failures is already being
|
|
111
|
+
/// told "large parts of this tree were not searched".
|
|
112
|
+
fn insert(&mut self, key: TraversalErrorKey, reason: String) {
|
|
113
|
+
if self.entries.contains_key(&key) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if self.entries.len() < SKIPPED_DETAIL_CAP {
|
|
117
|
+
self.entries.insert(key, reason);
|
|
118
|
+
} else {
|
|
119
|
+
self.beyond_cap = self.beyond_cap.saturating_add(1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
fn merge(&mut self, other: Self) {
|
|
124
|
+
if let Some((key, message)) = other.minimum
|
|
125
|
+
&& self
|
|
126
|
+
.minimum
|
|
127
|
+
.as_ref()
|
|
128
|
+
.is_none_or(|(existing, _)| key < *existing)
|
|
129
|
+
{
|
|
130
|
+
self.minimum = Some((key, message));
|
|
131
|
+
}
|
|
132
|
+
for (key, reason) in other.entries {
|
|
133
|
+
self.insert(key, reason);
|
|
134
|
+
}
|
|
135
|
+
self.beyond_cap = self.beyond_cap.saturating_add(other.beyond_cap);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// The message a walk reports when the skip cannot be survived.
|
|
139
|
+
fn root_failure_message(&self) -> Option<String> {
|
|
140
|
+
self.minimum.as_ref().map(|(_, message)| message.clone())
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Also answers "has anything been recorded at all": `record` always both
|
|
144
|
+
/// sets `minimum` and lands in one of the two counters, so an empty report
|
|
145
|
+
/// can never be hiding a failure the parallel merge would drop.
|
|
146
|
+
pub(crate) fn is_empty(&self) -> bool {
|
|
147
|
+
self.entries.is_empty() && self.beyond_cap == 0
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Every skipped path, listed and unlisted alike; exact up to the cap.
|
|
151
|
+
pub(crate) fn total(&self) -> usize {
|
|
152
|
+
self.entries.len().saturating_add(self.beyond_cap)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Paths kept with full detail, in deterministic order.
|
|
156
|
+
pub(crate) fn listed(&self) -> impl ExactSizeIterator<Item = SkippedPath<'_>> {
|
|
157
|
+
self.entries.iter().map(|(key, reason)| SkippedPath {
|
|
158
|
+
display: &key.display,
|
|
159
|
+
reason,
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Paths counted but dropped from the detail list at the cap.
|
|
164
|
+
pub(crate) fn unlisted(&self) -> usize {
|
|
165
|
+
self.beyond_cap
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
impl TraversalFailure {
|
|
170
|
+
pub(crate) fn from_io(path: &Path, error: &io::Error) -> Self {
|
|
171
|
+
Self {
|
|
172
|
+
key: TraversalErrorKey {
|
|
173
|
+
display: search_display_path(path),
|
|
174
|
+
kind_rank: io_kind_rank(error.kind()),
|
|
175
|
+
raw_os_error: error.raw_os_error(),
|
|
176
|
+
normalized_message: normalize_error_message(&error.to_string()),
|
|
177
|
+
},
|
|
178
|
+
message: search_io_error_message(path, error),
|
|
179
|
+
reason: io_reason(error),
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pub(crate) fn from_other(path: &Path, message: String, reason: String) -> Self {
|
|
184
|
+
Self {
|
|
185
|
+
key: TraversalErrorKey {
|
|
186
|
+
display: search_display_path(path),
|
|
187
|
+
kind_rank: u8::MAX,
|
|
188
|
+
raw_os_error: None,
|
|
189
|
+
normalized_message: normalize_error_message(&message),
|
|
190
|
+
},
|
|
191
|
+
message,
|
|
192
|
+
reason,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Mirrors `path_codec::io_error_message`'s cases without repeating the path,
|
|
198
|
+
/// which skip reports already carry in their own column.
|
|
199
|
+
fn io_reason(error: &io::Error) -> String {
|
|
200
|
+
#[cfg(windows)]
|
|
201
|
+
if matches!(error.raw_os_error(), Some(32 | 33)) {
|
|
202
|
+
return "locked by another process".to_string();
|
|
203
|
+
}
|
|
204
|
+
if error.kind() == io::ErrorKind::PermissionDenied {
|
|
205
|
+
return "permission denied".to_string();
|
|
206
|
+
}
|
|
207
|
+
error.to_string()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Collects lossless grep candidates while reusing the root's sole metadata result.
|
|
211
|
+
pub(crate) fn collect_search_candidates(
|
|
212
|
+
root: &ResolvedRoot,
|
|
213
|
+
glob: Option<&PathGlobFilter>,
|
|
214
|
+
file_type: Option<&str>,
|
|
215
|
+
operation: Option<&OperationCtx>,
|
|
216
|
+
executor: Option<&Arc<GrepGlobExecutor>>,
|
|
217
|
+
) -> Result<TraversalCollection<PathRecord>, String> {
|
|
218
|
+
if operation_cancelled(operation) {
|
|
219
|
+
return Err("Request cancelled.".to_string());
|
|
220
|
+
}
|
|
221
|
+
let type_filter = build_type_filter(file_type)?;
|
|
222
|
+
let mut candidates = Vec::new();
|
|
223
|
+
let mut skipped = SkippedPaths::default();
|
|
224
|
+
if root.kind == RootKind::File {
|
|
225
|
+
let candidate =
|
|
226
|
+
PathRecord::from_metadata(&root.native, root.match_root(), &root.metadata, true)
|
|
227
|
+
.map_err(|error| search_io_error_message(&root.native, &error))?;
|
|
228
|
+
if matches_record(&candidate, glob, type_filter.as_ref()) {
|
|
229
|
+
candidates.push(candidate);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
let collected = collect_directory_candidates(root, glob, type_filter, operation, executor)?;
|
|
233
|
+
candidates = collected.items;
|
|
234
|
+
skipped = collected.skipped;
|
|
235
|
+
}
|
|
236
|
+
let items = sort_cancelable(candidates, compare_search_candidates, operation, executor)
|
|
237
|
+
.map(|sorted| sorted.items)
|
|
238
|
+
.map_err(|error| error.to_string())?;
|
|
239
|
+
Ok(TraversalCollection { items, skipped })
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Collects files for replace while preserving its pre-codec display contract.
|
|
243
|
+
pub(crate) fn collect_project_candidates(
|
|
244
|
+
root: &Path,
|
|
245
|
+
glob: Option<&PathGlobFilter>,
|
|
246
|
+
file_type: Option<&str>,
|
|
247
|
+
) -> Result<TraversalCollection<ProjectCandidate>, String> {
|
|
248
|
+
let metadata =
|
|
249
|
+
fs::metadata(root).map_err(|error| crate::paths::io_error_message(root, &error))?;
|
|
250
|
+
let resolved = ResolvedRoot::from_metadata(root.to_path_buf(), metadata)?;
|
|
251
|
+
collect_search_candidates(&resolved, glob, file_type, None, None).map(|collected| {
|
|
252
|
+
TraversalCollection {
|
|
253
|
+
items: collected
|
|
254
|
+
.items
|
|
255
|
+
.into_iter()
|
|
256
|
+
.map(|candidate| ProjectCandidate {
|
|
257
|
+
display: crate::paths::display_path(&candidate.native),
|
|
258
|
+
})
|
|
259
|
+
.collect(),
|
|
260
|
+
skipped: collected.skipped,
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn build_type_filter(file_type: Option<&str>) -> Result<Option<ignore::types::Types>, String> {
|
|
266
|
+
let Some(file_type) = file_type else {
|
|
267
|
+
return Ok(None);
|
|
268
|
+
};
|
|
269
|
+
let mut builder = TypesBuilder::new();
|
|
270
|
+
builder.add_defaults();
|
|
271
|
+
builder.select(file_type);
|
|
272
|
+
builder.build().map(Some).map_err(|_| {
|
|
273
|
+
format!(
|
|
274
|
+
"Unknown file type: \"{file_type}\". Run with a glob filter instead, or use a standard type like js, py, rust, go, java."
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn collect_directory_candidates(
|
|
280
|
+
root: &ResolvedRoot,
|
|
281
|
+
glob: Option<&PathGlobFilter>,
|
|
282
|
+
type_filter: Option<ignore::types::Types>,
|
|
283
|
+
operation: Option<&OperationCtx>,
|
|
284
|
+
executor: Option<&Arc<GrepGlobExecutor>>,
|
|
285
|
+
) -> Result<TraversalCollection<PathRecord>, String> {
|
|
286
|
+
let mut builder = WalkBuilder::new(&root.native);
|
|
287
|
+
builder
|
|
288
|
+
.hidden(false)
|
|
289
|
+
.ignore(true)
|
|
290
|
+
.git_ignore(true)
|
|
291
|
+
.git_global(true)
|
|
292
|
+
.git_exclude(true)
|
|
293
|
+
.follow_links(false)
|
|
294
|
+
.filter_entry(|entry| entry.depth() == 0 || entry.file_name() != ".git");
|
|
295
|
+
if let Some(types) = type_filter {
|
|
296
|
+
builder.types(types);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
collect_walk_batched(builder, &root.native, operation, executor, None, |entry| {
|
|
300
|
+
if !entry
|
|
301
|
+
.file_type()
|
|
302
|
+
.is_some_and(|file_type| file_type.is_file() || file_type.is_symlink())
|
|
303
|
+
{
|
|
304
|
+
return Ok(None);
|
|
305
|
+
}
|
|
306
|
+
let preliminary = PathRecord::without_metadata(entry.path(), &root.native);
|
|
307
|
+
if !matches_record(&preliminary, glob, None) {
|
|
308
|
+
return Ok(None);
|
|
309
|
+
}
|
|
310
|
+
candidate_from_entry(entry, &root.native)
|
|
311
|
+
})
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Runs a true serial walker when no traversal credit is immediately available;
|
|
315
|
+
/// parallel walkers merge only fixed-size thread-local batches.
|
|
316
|
+
pub(crate) fn collect_walk_batched<T, F>(
|
|
317
|
+
mut builder: WalkBuilder,
|
|
318
|
+
root: &Path,
|
|
319
|
+
operation: Option<&OperationCtx>,
|
|
320
|
+
executor: Option<&Arc<GrepGlobExecutor>>,
|
|
321
|
+
limit: Option<TraversalLimit>,
|
|
322
|
+
evaluate: F,
|
|
323
|
+
) -> Result<TraversalCollection<T>, String>
|
|
324
|
+
where
|
|
325
|
+
T: Send,
|
|
326
|
+
F: Fn(&DirEntry) -> Result<Option<T>, TraversalFailure> + Send + Sync,
|
|
327
|
+
{
|
|
328
|
+
if operation_cancelled(operation) {
|
|
329
|
+
return Err("Request cancelled.".to_string());
|
|
330
|
+
}
|
|
331
|
+
let permits = executor
|
|
332
|
+
.map(|executor| executor.try_bursts(executor.extra_capacity(), BurstUse::TraversalExtra))
|
|
333
|
+
.unwrap_or_default();
|
|
334
|
+
if permits.is_empty() {
|
|
335
|
+
return collect_walk_serial(builder, root, operation, limit, &evaluate);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let thread_count = permits.len().saturating_add(1);
|
|
339
|
+
builder.threads(thread_count);
|
|
340
|
+
let shared = Mutex::new(ParallelCollectionState::<T>::default());
|
|
341
|
+
let stop = AtomicBool::new(false);
|
|
342
|
+
let cancelled = AtomicBool::new(false);
|
|
343
|
+
let evaluate = &evaluate;
|
|
344
|
+
let run = catch_unwind(AssertUnwindSafe(|| {
|
|
345
|
+
builder.build_parallel().run(|| {
|
|
346
|
+
let mut local = ParallelLocalBatch::new(&shared, &stop, &cancelled, operation, limit);
|
|
347
|
+
Box::new(move |entry| {
|
|
348
|
+
process_parallel_entry(entry, root, operation, evaluate, &mut local)
|
|
349
|
+
})
|
|
350
|
+
});
|
|
351
|
+
}));
|
|
352
|
+
drop(permits);
|
|
353
|
+
if cancelled.load(Ordering::Acquire) || operation_cancelled(operation) {
|
|
354
|
+
return Err("Request cancelled.".to_string());
|
|
355
|
+
}
|
|
356
|
+
if run.is_err() {
|
|
357
|
+
return Err("Internal traversal worker failure.".to_string());
|
|
358
|
+
}
|
|
359
|
+
finish_parallel_collection(shared.into_inner(), limit)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
fn collect_walk_serial<T, F>(
|
|
363
|
+
builder: WalkBuilder,
|
|
364
|
+
root: &Path,
|
|
365
|
+
operation: Option<&OperationCtx>,
|
|
366
|
+
limit: Option<TraversalLimit>,
|
|
367
|
+
evaluate: &F,
|
|
368
|
+
) -> Result<TraversalCollection<T>, String>
|
|
369
|
+
where
|
|
370
|
+
F: Fn(&DirEntry) -> Result<Option<T>, TraversalFailure>,
|
|
371
|
+
{
|
|
372
|
+
let mut items = Vec::new();
|
|
373
|
+
let mut skipped = SkippedPaths::default();
|
|
374
|
+
let mut entries_seen = 0_usize;
|
|
375
|
+
let mut too_many = false;
|
|
376
|
+
for entry in builder.build() {
|
|
377
|
+
stage_traversal_entry(operation);
|
|
378
|
+
if operation_cancelled(operation) {
|
|
379
|
+
return Err("Request cancelled.".to_string());
|
|
380
|
+
}
|
|
381
|
+
let entry = match entry {
|
|
382
|
+
Ok(entry) => entry,
|
|
383
|
+
Err(error) => {
|
|
384
|
+
for failure in traversal_errors_from_ignore(&error, root) {
|
|
385
|
+
skipped.record(failure);
|
|
386
|
+
}
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
entries_seen = entries_seen.saturating_add(1);
|
|
391
|
+
let evaluated = catch_unwind(AssertUnwindSafe(|| evaluate(&entry)));
|
|
392
|
+
match evaluated {
|
|
393
|
+
Ok(Ok(Some(item))) => {
|
|
394
|
+
if limit.is_some_and(|limit| items.len() >= limit.maximum) {
|
|
395
|
+
too_many = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
items.push(item);
|
|
399
|
+
}
|
|
400
|
+
Ok(Ok(None)) => {}
|
|
401
|
+
Ok(Err(failure)) => skipped.record(failure),
|
|
402
|
+
Err(_) => skipped.record(evaluation_panic(entry.path())),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if operation_cancelled(operation) {
|
|
406
|
+
return Err("Request cancelled.".to_string());
|
|
407
|
+
}
|
|
408
|
+
if too_many {
|
|
409
|
+
return match limit {
|
|
410
|
+
Some(limit) => Err(limit.message.to_string()),
|
|
411
|
+
None => Err("Internal traversal limit state was inconsistent.".to_string()),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
finish_collection(items, skipped, entries_seen)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/// A walk that never yielded a single entry never happened: the root itself was
|
|
418
|
+
/// unreadable, so reporting it as "no results, some paths skipped" would read as
|
|
419
|
+
/// "searched, found nothing". Those still fail whole, with the message the walk
|
|
420
|
+
/// would have produced before partial results existed.
|
|
421
|
+
fn finish_collection<T>(
|
|
422
|
+
items: Vec<T>,
|
|
423
|
+
skipped: SkippedPaths,
|
|
424
|
+
entries_seen: usize,
|
|
425
|
+
) -> Result<TraversalCollection<T>, String> {
|
|
426
|
+
if entries_seen == 0
|
|
427
|
+
&& let Some(message) = skipped.root_failure_message()
|
|
428
|
+
{
|
|
429
|
+
return Err(message);
|
|
430
|
+
}
|
|
431
|
+
Ok(TraversalCollection { items, skipped })
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
fn evaluation_panic(path: &Path) -> TraversalFailure {
|
|
435
|
+
TraversalFailure::from_other(
|
|
436
|
+
path,
|
|
437
|
+
"Internal traversal failure while evaluating a file candidate.".to_string(),
|
|
438
|
+
"internal failure while evaluating this path".to_string(),
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
struct ParallelCollectionState<T> {
|
|
443
|
+
items: Vec<T>,
|
|
444
|
+
skipped: SkippedPaths,
|
|
445
|
+
entries_seen: usize,
|
|
446
|
+
too_many: bool,
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
impl<T> Default for ParallelCollectionState<T> {
|
|
450
|
+
fn default() -> Self {
|
|
451
|
+
Self {
|
|
452
|
+
items: Vec::new(),
|
|
453
|
+
skipped: SkippedPaths::default(),
|
|
454
|
+
entries_seen: 0,
|
|
455
|
+
too_many: false,
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
struct ParallelLocalBatch<'a, T> {
|
|
461
|
+
shared: &'a Mutex<ParallelCollectionState<T>>,
|
|
462
|
+
stop: &'a AtomicBool,
|
|
463
|
+
cancelled: &'a AtomicBool,
|
|
464
|
+
operation: Option<&'a OperationCtx>,
|
|
465
|
+
limit: Option<TraversalLimit>,
|
|
466
|
+
items: Vec<T>,
|
|
467
|
+
skipped: SkippedPaths,
|
|
468
|
+
entries_seen: usize,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
impl<'a, T> ParallelLocalBatch<'a, T> {
|
|
472
|
+
fn new(
|
|
473
|
+
shared: &'a Mutex<ParallelCollectionState<T>>,
|
|
474
|
+
stop: &'a AtomicBool,
|
|
475
|
+
cancelled: &'a AtomicBool,
|
|
476
|
+
operation: Option<&'a OperationCtx>,
|
|
477
|
+
limit: Option<TraversalLimit>,
|
|
478
|
+
) -> Self {
|
|
479
|
+
Self {
|
|
480
|
+
shared,
|
|
481
|
+
stop,
|
|
482
|
+
cancelled,
|
|
483
|
+
operation,
|
|
484
|
+
limit,
|
|
485
|
+
items: Vec::with_capacity(TRAVERSAL_BATCH_ITEMS),
|
|
486
|
+
skipped: SkippedPaths::default(),
|
|
487
|
+
entries_seen: 0,
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
fn push(&mut self, item: T) {
|
|
492
|
+
self.items.push(item);
|
|
493
|
+
if self.items.len() == TRAVERSAL_BATCH_ITEMS {
|
|
494
|
+
self.flush();
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
fn record_failure(&mut self, failure: TraversalFailure) {
|
|
499
|
+
self.skipped.record(failure);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
fn flush(&mut self) {
|
|
503
|
+
if self.items.is_empty() && self.skipped.is_empty() && self.entries_seen == 0 {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
stage_traversal_batch_flush(self.operation);
|
|
507
|
+
if operation_cancelled(self.operation) {
|
|
508
|
+
self.cancelled.store(true, Ordering::Release);
|
|
509
|
+
self.stop.store(true, Ordering::Release);
|
|
510
|
+
self.items.clear();
|
|
511
|
+
self.skipped = SkippedPaths::default();
|
|
512
|
+
self.entries_seen = 0;
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
let mut shared = self.shared.lock();
|
|
517
|
+
shared.skipped.merge(std::mem::take(&mut self.skipped));
|
|
518
|
+
shared.entries_seen = shared
|
|
519
|
+
.entries_seen
|
|
520
|
+
.saturating_add(std::mem::take(&mut self.entries_seen));
|
|
521
|
+
for item in self.items.drain(..) {
|
|
522
|
+
if self
|
|
523
|
+
.limit
|
|
524
|
+
.is_some_and(|limit| shared.items.len() >= limit.maximum)
|
|
525
|
+
{
|
|
526
|
+
shared.too_many = true;
|
|
527
|
+
self.stop.store(true, Ordering::Release);
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
shared.items.push(item);
|
|
531
|
+
}
|
|
532
|
+
drop(shared);
|
|
533
|
+
stage_traversal_batch_flush(self.operation);
|
|
534
|
+
if operation_cancelled(self.operation) {
|
|
535
|
+
self.cancelled.store(true, Ordering::Release);
|
|
536
|
+
self.stop.store(true, Ordering::Release);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
impl<T> Drop for ParallelLocalBatch<'_, T> {
|
|
542
|
+
fn drop(&mut self) {
|
|
543
|
+
self.flush();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
fn process_parallel_entry<'a, T, F>(
|
|
548
|
+
entry: Result<DirEntry, ignore::Error>,
|
|
549
|
+
root: &Path,
|
|
550
|
+
operation: Option<&OperationCtx>,
|
|
551
|
+
evaluate: &F,
|
|
552
|
+
local: &mut ParallelLocalBatch<'a, T>,
|
|
553
|
+
) -> WalkState
|
|
554
|
+
where
|
|
555
|
+
F: Fn(&DirEntry) -> Result<Option<T>, TraversalFailure>,
|
|
556
|
+
{
|
|
557
|
+
if local.stop.load(Ordering::Acquire) {
|
|
558
|
+
return WalkState::Quit;
|
|
559
|
+
}
|
|
560
|
+
stage_traversal_entry(operation);
|
|
561
|
+
if operation_cancelled(operation) {
|
|
562
|
+
local.cancelled.store(true, Ordering::Release);
|
|
563
|
+
local.stop.store(true, Ordering::Release);
|
|
564
|
+
return WalkState::Quit;
|
|
565
|
+
}
|
|
566
|
+
let entry = match entry {
|
|
567
|
+
Ok(entry) => entry,
|
|
568
|
+
Err(error) => {
|
|
569
|
+
for failure in traversal_errors_from_ignore(&error, root) {
|
|
570
|
+
local.record_failure(failure);
|
|
571
|
+
}
|
|
572
|
+
return WalkState::Continue;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
local.entries_seen = local.entries_seen.saturating_add(1);
|
|
576
|
+
let evaluated = catch_unwind(AssertUnwindSafe(|| evaluate(&entry)));
|
|
577
|
+
match evaluated {
|
|
578
|
+
Ok(Ok(Some(item))) => local.push(item),
|
|
579
|
+
Ok(Ok(None)) => {}
|
|
580
|
+
Ok(Err(failure)) => local.record_failure(failure),
|
|
581
|
+
Err(_) => local.record_failure(evaluation_panic(entry.path())),
|
|
582
|
+
}
|
|
583
|
+
if local.stop.load(Ordering::Acquire) {
|
|
584
|
+
WalkState::Quit
|
|
585
|
+
} else {
|
|
586
|
+
WalkState::Continue
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
fn finish_parallel_collection<T>(
|
|
591
|
+
state: ParallelCollectionState<T>,
|
|
592
|
+
limit: Option<TraversalLimit>,
|
|
593
|
+
) -> Result<TraversalCollection<T>, String> {
|
|
594
|
+
if state.too_many {
|
|
595
|
+
return match limit {
|
|
596
|
+
Some(limit) => Err(limit.message.to_string()),
|
|
597
|
+
None => Err("Internal traversal limit state was inconsistent.".to_string()),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
finish_collection(state.items, state.skipped, state.entries_seen)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
fn compare_search_candidates(left: &PathRecord, right: &PathRecord) -> std::cmp::Ordering {
|
|
604
|
+
right
|
|
605
|
+
.modified
|
|
606
|
+
.cmp(&left.modified)
|
|
607
|
+
.then_with(|| left.display.as_bytes().cmp(right.display.as_bytes()))
|
|
608
|
+
.then_with(|| left.native_key.cmp(&right.native_key))
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn matches_record(
|
|
612
|
+
candidate: &PathRecord,
|
|
613
|
+
glob: Option<&PathGlobFilter>,
|
|
614
|
+
types: Option<&ignore::types::Types>,
|
|
615
|
+
) -> bool {
|
|
616
|
+
if let Some(types) = types
|
|
617
|
+
&& !types.matched(&candidate.native, false).is_whitelist()
|
|
618
|
+
{
|
|
619
|
+
return false;
|
|
620
|
+
}
|
|
621
|
+
glob.is_none_or(|glob| glob.is_match(candidate.relative_match.as_ref()))
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
fn candidate_from_path(
|
|
625
|
+
path: &Path,
|
|
626
|
+
match_root: &Path,
|
|
627
|
+
) -> Result<Option<PathRecord>, TraversalFailure> {
|
|
628
|
+
let metadata = match fs::metadata(path) {
|
|
629
|
+
Ok(metadata) if metadata.is_file() => metadata,
|
|
630
|
+
Ok(_) => return Ok(None),
|
|
631
|
+
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(None),
|
|
632
|
+
Err(error) => return Err(TraversalFailure::from_io(path, &error)),
|
|
633
|
+
};
|
|
634
|
+
candidate_from_metadata(path, match_root, &metadata).map(Some)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/// Symlinks follow their target for the regular-file check and ordering metadata.
|
|
638
|
+
fn candidate_from_entry(
|
|
639
|
+
entry: &ignore::DirEntry,
|
|
640
|
+
match_root: &Path,
|
|
641
|
+
) -> Result<Option<PathRecord>, TraversalFailure> {
|
|
642
|
+
if entry
|
|
643
|
+
.file_type()
|
|
644
|
+
.is_some_and(|file_type| file_type.is_symlink())
|
|
645
|
+
{
|
|
646
|
+
return candidate_from_path(entry.path(), match_root);
|
|
647
|
+
}
|
|
648
|
+
match entry.metadata() {
|
|
649
|
+
Ok(metadata) if metadata.is_file() => {
|
|
650
|
+
candidate_from_metadata(entry.path(), match_root, &metadata).map(Some)
|
|
651
|
+
}
|
|
652
|
+
Ok(_) => Ok(None),
|
|
653
|
+
Err(_) => candidate_from_path(entry.path(), match_root),
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fn candidate_from_metadata(
|
|
658
|
+
path: &Path,
|
|
659
|
+
match_root: &Path,
|
|
660
|
+
metadata: &fs::Metadata,
|
|
661
|
+
) -> Result<PathRecord, TraversalFailure> {
|
|
662
|
+
PathRecord::from_metadata(path, match_root, metadata, true)
|
|
663
|
+
.map_err(|error| TraversalFailure::from_io(path, &error))
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fn operation_cancelled(operation: Option<&OperationCtx>) -> bool {
|
|
667
|
+
operation.is_some_and(|operation| operation.check().is_err())
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
fn stage_traversal_entry(operation: Option<&OperationCtx>) {
|
|
671
|
+
#[cfg(test)]
|
|
672
|
+
if let Some(operation) = operation {
|
|
673
|
+
operation.stage(TestStage::TraversalEntry);
|
|
674
|
+
}
|
|
675
|
+
#[cfg(not(test))]
|
|
676
|
+
let _ = operation;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
fn stage_traversal_batch_flush(operation: Option<&OperationCtx>) {
|
|
680
|
+
#[cfg(test)]
|
|
681
|
+
if let Some(operation) = operation {
|
|
682
|
+
operation.stage(TestStage::TraversalBatchFlush);
|
|
683
|
+
}
|
|
684
|
+
#[cfg(not(test))]
|
|
685
|
+
let _ = operation;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
pub(crate) fn traversal_errors_from_ignore(
|
|
689
|
+
error: &ignore::Error,
|
|
690
|
+
root: &Path,
|
|
691
|
+
) -> Vec<TraversalFailure> {
|
|
692
|
+
let mut failures = Vec::new();
|
|
693
|
+
collect_ignore_error(error, None, root, &mut failures);
|
|
694
|
+
failures
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
fn collect_ignore_error(
|
|
698
|
+
error: &ignore::Error,
|
|
699
|
+
inherited_path: Option<&Path>,
|
|
700
|
+
root: &Path,
|
|
701
|
+
failures: &mut Vec<TraversalFailure>,
|
|
702
|
+
) {
|
|
703
|
+
match error {
|
|
704
|
+
ignore::Error::Partial(errors) => {
|
|
705
|
+
for error in errors {
|
|
706
|
+
collect_ignore_error(error, inherited_path, root, failures);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
ignore::Error::WithLineNumber { err, .. } | ignore::Error::WithDepth { err, .. } => {
|
|
710
|
+
collect_ignore_error(err, inherited_path, root, failures);
|
|
711
|
+
}
|
|
712
|
+
ignore::Error::WithPath { path, err } => {
|
|
713
|
+
collect_ignore_error(err, Some(path), root, failures);
|
|
714
|
+
}
|
|
715
|
+
ignore::Error::Loop { child, .. } => failures.push(TraversalFailure::from_other(
|
|
716
|
+
child,
|
|
717
|
+
format!("Cannot traverse path: {error}"),
|
|
718
|
+
error.to_string(),
|
|
719
|
+
)),
|
|
720
|
+
ignore::Error::Io(error) => failures.push(TraversalFailure::from_io(
|
|
721
|
+
inherited_path.unwrap_or(root),
|
|
722
|
+
error,
|
|
723
|
+
)),
|
|
724
|
+
ignore::Error::Glob { .. }
|
|
725
|
+
| ignore::Error::UnrecognizedFileType(_)
|
|
726
|
+
| ignore::Error::InvalidDefinition => failures.push(TraversalFailure::from_other(
|
|
727
|
+
inherited_path.unwrap_or(root),
|
|
728
|
+
format!("Cannot traverse path: {error}"),
|
|
729
|
+
error.to_string(),
|
|
730
|
+
)),
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
fn normalize_error_message(message: &str) -> String {
|
|
735
|
+
message.replace("\r\n", "\n").replace('\r', "\n")
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
fn io_kind_rank(kind: io::ErrorKind) -> u8 {
|
|
739
|
+
match kind {
|
|
740
|
+
io::ErrorKind::NotFound => 0,
|
|
741
|
+
io::ErrorKind::PermissionDenied => 1,
|
|
742
|
+
io::ErrorKind::WouldBlock => 2,
|
|
743
|
+
io::ErrorKind::TimedOut => 3,
|
|
744
|
+
io::ErrorKind::Interrupted => 4,
|
|
745
|
+
io::ErrorKind::InvalidInput | io::ErrorKind::InvalidData => 5,
|
|
746
|
+
io::ErrorKind::UnexpectedEof => 6,
|
|
747
|
+
_ => 254,
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
#[cfg(test)]
|
|
752
|
+
mod tests {
|
|
753
|
+
use super::*;
|
|
754
|
+
use std::path::PathBuf;
|
|
755
|
+
|
|
756
|
+
fn failure(path: &str) -> TraversalFailure {
|
|
757
|
+
TraversalFailure::from_other(
|
|
758
|
+
Path::new(path),
|
|
759
|
+
format!("Cannot traverse path: {path}"),
|
|
760
|
+
format!("reason for {path}"),
|
|
761
|
+
)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
fn listing(skipped: &SkippedPaths) -> Vec<(String, String)> {
|
|
765
|
+
skipped
|
|
766
|
+
.listed()
|
|
767
|
+
.map(|path| (path.display.to_string(), path.reason.to_string()))
|
|
768
|
+
.collect()
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/// The defect this whole module exists to prevent: one unreadable corner of a
|
|
772
|
+
/// tree used to discard every result collected around it.
|
|
773
|
+
#[test]
|
|
774
|
+
fn a_failure_inside_the_tree_keeps_the_items_around_it() {
|
|
775
|
+
let dir = tempfile::TempDir::new().expect("temp dir");
|
|
776
|
+
for name in ["a.txt", "b.txt", "c.txt"] {
|
|
777
|
+
fs::write(dir.path().join(name), "x").expect("seed");
|
|
778
|
+
}
|
|
779
|
+
let collected = collect_walk_serial(
|
|
780
|
+
WalkBuilder::new(dir.path()),
|
|
781
|
+
dir.path(),
|
|
782
|
+
None,
|
|
783
|
+
None,
|
|
784
|
+
&|entry: &DirEntry| -> Result<Option<PathBuf>, TraversalFailure> {
|
|
785
|
+
if !entry.file_type().is_some_and(|kind| kind.is_file()) {
|
|
786
|
+
return Ok(None);
|
|
787
|
+
}
|
|
788
|
+
if entry.file_name() == "b.txt" {
|
|
789
|
+
return Err(failure("b.txt"));
|
|
790
|
+
}
|
|
791
|
+
Ok(Some(entry.path().to_path_buf()))
|
|
792
|
+
},
|
|
793
|
+
)
|
|
794
|
+
.expect("a failure inside the tree must not fail the walk");
|
|
795
|
+
assert_eq!(collected.items.len(), 2);
|
|
796
|
+
assert_eq!(collected.skipped.total(), 1);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/// A walk with nothing to report on cannot claim it searched: an unreadable
|
|
800
|
+
/// root must stay an error rather than become "found nothing".
|
|
801
|
+
#[test]
|
|
802
|
+
fn a_walk_that_never_reached_an_entry_still_fails_whole() {
|
|
803
|
+
let dir = tempfile::TempDir::new().expect("temp dir");
|
|
804
|
+
let missing = dir.path().join("missing");
|
|
805
|
+
let error = collect_walk_serial(
|
|
806
|
+
WalkBuilder::new(&missing),
|
|
807
|
+
&missing,
|
|
808
|
+
None,
|
|
809
|
+
None,
|
|
810
|
+
&|_: &DirEntry| -> Result<Option<PathBuf>, TraversalFailure> { Ok(None) },
|
|
811
|
+
)
|
|
812
|
+
.map(|collected| collected.items)
|
|
813
|
+
.expect_err("an unreachable root has no partial result to offer");
|
|
814
|
+
assert!(!error.is_empty());
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/// Parallel walkers merge thread-local batches in scheduling order, so the
|
|
818
|
+
/// report has to be a function of the failures alone.
|
|
819
|
+
#[test]
|
|
820
|
+
fn skip_reports_stay_identical_regardless_of_arrival_order() {
|
|
821
|
+
let record = |paths: &[&str]| {
|
|
822
|
+
let mut skipped = SkippedPaths::default();
|
|
823
|
+
for path in paths {
|
|
824
|
+
skipped.record(failure(path));
|
|
825
|
+
}
|
|
826
|
+
skipped
|
|
827
|
+
};
|
|
828
|
+
let forward = record(&["/a", "/b", "/c"]);
|
|
829
|
+
let mut merged = record(&["/c"]);
|
|
830
|
+
merged.merge(record(&["/b", "/a"]));
|
|
831
|
+
assert_eq!(listing(&forward), listing(&merged));
|
|
832
|
+
assert_eq!(forward.total(), merged.total());
|
|
833
|
+
|
|
834
|
+
let mut duplicated = record(&["/a", "/a", "/a"]);
|
|
835
|
+
duplicated.merge(record(&["/a"]));
|
|
836
|
+
assert_eq!(duplicated.total(), 1);
|
|
837
|
+
|
|
838
|
+
let flood = (0..SKIPPED_DETAIL_CAP + 40)
|
|
839
|
+
.map(|index| format!("/flood/{index:05}"))
|
|
840
|
+
.collect::<Vec<_>>();
|
|
841
|
+
let overflowed = record(&flood.iter().map(String::as_str).collect::<Vec<_>>());
|
|
842
|
+
assert_eq!(overflowed.total(), flood.len());
|
|
843
|
+
assert_eq!(overflowed.listed().len(), SKIPPED_DETAIL_CAP);
|
|
844
|
+
assert_eq!(overflowed.unlisted(), 40);
|
|
845
|
+
}
|
|
846
|
+
}
|