Skip to main content

weavatrix_scan/scanner/content_visit/
api.rs

1use super::{
2    ChangedContentVisitOutcome, CompactScanReport, ContentVisitControl, ContentVisitEvent,
3    ContentVisitMode, ContentVisitReport, FinishedContentVisit, Result, Scanner,
4    apply_total_bytes_limit, discover_compact, finish_content_report, run_workers,
5    visit_changed_content_plan, visit_content_direct,
6};
7use crate::watch::WatchPlan;
8
9impl Scanner {
10    /// Visits selected file bytes once with bounded parallelism.
11    ///
12    /// Ignore rules, file limits, path safety, binary detection, content
13    /// hashing, and revision evidence use the same scanner configuration. A
14    /// worker-local visitor is created by `factory`; callback order is
15    /// intentionally concurrent. Every event carries a monotonic work
16    /// sequence plus its root and normalized relative path; use the paths for
17    /// deterministic cross-run ordering.
18    ///
19    /// `ContentVisitControl::SkipFile` stops delivering chunks for the current
20    /// file. The scanner still finishes reading when hashing or binary
21    /// detection requires complete evidence. `ContentVisitControl::Quit`
22    /// cooperatively cancels every worker.
23    ///
24    /// # Errors
25    ///
26    /// Returns root, traversal, content I/O, or worker-submission failures
27    /// according to the configured error policy.
28    ///
29    /// # Panics
30    ///
31    /// Propagates a panic from the factory or visitor after active workers
32    /// observe cancellation.
33    pub fn visit_content<Factory, Visitor>(self, factory: Factory) -> Result<ContentVisitReport>
34    where
35        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
36        Visitor:
37            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
38    {
39        self.visit_content_with_root(0, factory)
40    }
41
42    /// Visits selected file bytes once and returns the exact compact manifest
43    /// produced by those same verified reads.
44    ///
45    /// This additive API lets parsers consume bytes and retain incremental scan
46    /// evidence without changing the long-standing [`ContentVisitReport`]
47    /// construction contract used by existing scanner consumers.
48    ///
49    /// # Errors
50    ///
51    /// Returns the same errors as [`Self::visit_content`].
52    ///
53    /// # Panics
54    ///
55    /// Propagates callback panics like [`Self::visit_content`].
56    pub fn visit_content_manifest<Factory, Visitor>(
57        self,
58        factory: Factory,
59    ) -> Result<CompactScanReport>
60    where
61        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
62        Visitor:
63            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
64    {
65        self.visit_content_with_root_mode_finished(0, ContentVisitMode::Revision, factory)
66            .map(FinishedContentVisit::into_manifest)
67    }
68
69    /// Visits selected bytes without retaining a selected-file manifest or
70    /// computing a revision.
71    ///
72    /// Typed skip evidence is still retained when `EvidenceMode::Complete` is
73    /// configured. Use `selected_files_only()` as well for constant-memory
74    /// summary reporting.
75    ///
76    /// # Errors
77    ///
78    /// Returns the same errors as [`Self::visit_content`].
79    ///
80    /// # Panics
81    ///
82    /// Propagates callback panics like [`Self::visit_content`].
83    pub fn visit_content_streaming<Factory, Visitor>(
84        self,
85        factory: Factory,
86    ) -> Result<ContentVisitReport>
87    where
88        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
89        Visitor:
90            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
91    {
92        self.visit_content_with_root_mode(0, ContentVisitMode::Streaming, factory)
93    }
94
95    /// Visits only safe changed-file paths from a watcher plan.
96    ///
97    /// No directory traversal occurs. Plans that can affect directory
98    /// structure or file-selection rules return
99    /// [`ChangedContentVisitOutcome::FullRescanRequired`] before invoking the
100    /// factory. Removed paths are returned separately.
101    ///
102    /// # Errors
103    ///
104    /// Returns root, matcher, content I/O, or worker-submission failures
105    /// according to the configured error policy.
106    ///
107    /// # Panics
108    ///
109    /// Propagates a panic from the factory or visitor after active workers
110    /// observe cancellation.
111    pub fn visit_changed_content<Factory, Visitor>(
112        self,
113        plan: &WatchPlan,
114        factory: Factory,
115    ) -> Result<ChangedContentVisitOutcome>
116    where
117        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
118        Visitor:
119            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
120    {
121        visit_changed_content_plan(self, plan, ContentVisitMode::Revision, factory)
122    }
123
124    /// Visits only changed-file bytes without retaining their compact manifest
125    /// or computing a subset revision.
126    ///
127    /// # Errors
128    ///
129    /// Returns the same errors as [`Self::visit_changed_content`].
130    pub fn visit_changed_content_streaming<Factory, Visitor>(
131        self,
132        plan: &WatchPlan,
133        factory: Factory,
134    ) -> Result<ChangedContentVisitOutcome>
135    where
136        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
137        Visitor:
138            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
139    {
140        visit_changed_content_plan(self, plan, ContentVisitMode::Streaming, factory)
141    }
142
143    pub(crate) fn visit_content_with_root<Factory, Visitor>(
144        self,
145        root_index: usize,
146        factory: Factory,
147    ) -> Result<ContentVisitReport>
148    where
149        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
150        Visitor:
151            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
152    {
153        self.visit_content_with_root_mode(root_index, ContentVisitMode::Revision, factory)
154    }
155
156    pub(crate) fn visit_content_with_root_mode<Factory, Visitor>(
157        self,
158        root_index: usize,
159        mode: ContentVisitMode,
160        factory: Factory,
161    ) -> Result<ContentVisitReport>
162    where
163        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
164        Visitor:
165            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
166    {
167        self.visit_content_with_root_mode_finished(root_index, mode, factory)
168            .map(|finished| finished.report)
169    }
170
171    fn visit_content_with_root_mode_finished<Factory, Visitor>(
172        self,
173        root_index: usize,
174        mode: ContentVisitMode,
175        factory: Factory,
176    ) -> Result<FinishedContentVisit>
177    where
178        Factory: Fn(usize) -> Visitor + Send + Sync + 'static,
179        Visitor:
180            for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl + Send + 'static,
181    {
182        if self.options.limits.max_total_bytes.is_none()
183            && self.options.content_discovery == crate::ContentDiscoveryMode::Streaming
184            && !self.runtime.is_worker_thread()
185        {
186            return visit_content_direct(self, root_index, mode, factory);
187        }
188        let mut discovery_options = self.options.clone();
189        discovery_options.detect_binary_files = true;
190        let (mut evidence, mut files, scan_runtime) =
191            discover_compact(&self.root, &discovery_options, &self.runtime)?;
192        files.sort_unstable_by(|left, right| left.relative.cmp(&right.relative));
193        apply_total_bytes_limit(&mut evidence, &mut files, &self.options);
194        let discovered = u64::try_from(files.len()).unwrap_or(u64::MAX);
195
196        let cancellation = self.options.cancellation.clone().unwrap_or_default();
197        let mut visit_options = self.options.clone();
198        visit_options.cancellation = Some(cancellation.clone());
199        let workers = visit_options
200            .content_visit_worker_count(files.len())
201            .min(self.runtime.parallelism())
202            .max(1);
203        let worker_reports = run_workers(
204            evidence.root.clone(),
205            files,
206            visit_options,
207            &scan_runtime,
208            &self.runtime,
209            workers,
210            root_index,
211            mode,
212            factory,
213        )?;
214
215        Ok(finish_content_report(
216            evidence,
217            discovered,
218            worker_reports,
219            mode,
220        ))
221    }
222}