weavatrix-scan 0.4.6

Deterministic, safe repository scanner for code intelligence
Documentation
use super::{
    CompactContentEvidence, CompactScannedFile, ContentVisitControl, ContentVisitEvent,
    ContentVisitMode, ContentWorkerContext, EvidenceMode, InspectionStop, Instant, Result,
    ScanOptions, ScanTermination, ScannedFile, SkipKind, SkippedEntry, VisitedFiles, VisitedStatus,
    inspect, record_binary_skip, record_concurrent_modification, record_io_error,
    record_limit_skip,
};

pub(crate) fn visit_files<V, I>(
    files: I,
    options: &ScanOptions,
    started: Instant,
    context: ContentWorkerContext<'_>,
    buffer: &mut [u8],
    visitor: &mut V,
) -> Result<VisitedFiles>
where
    V: for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl,
    I: IntoIterator<Item = (u64, CompactScannedFile)>,
{
    let stop = InspectionStop::default();
    let mut iterator = files.into_iter();
    let mut visited = VisitedFiles::empty(iterator.size_hint().0);
    while let Some((sequence, compact)) = iterator.next() {
        if let Some(reason) = stop.reason(options, started) {
            record_limit_skip(
                &mut visited.evidence,
                compact.relative.into(),
                reason,
                options,
            );
            if options.evidence == EvidenceMode::Complete {
                visited
                    .evidence
                    .skipped
                    .extend(iterator.map(|(_, file)| SkippedEntry {
                        relative: file.relative.into(),
                        kind: SkipKind::ScanLimit,
                        detail: Some(format!("{reason:?}")),
                    }));
            }
            visited.evidence.termination = Some(reason);
            break;
        }
        if visit_file(
            sequence,
            compact,
            options,
            context,
            buffer,
            visitor,
            &mut visited,
        )? {
            break;
        }
    }
    Ok(visited)
}

#[allow(clippy::too_many_arguments)]
fn visit_file<V>(
    sequence: u64,
    mut compact: CompactScannedFile,
    options: &ScanOptions,
    context: ContentWorkerContext<'_>,
    buffer: &mut [u8],
    visitor: &mut V,
    visited: &mut VisitedFiles,
) -> Result<bool>
where
    V: for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl,
{
    let discovery = compact
        .content
        .take()
        .expect("content visit retains file-version evidence");
    let relative = compact.relative.into_string();
    let mut scanned = ScannedFile {
        absolute: context.root.join(&relative),
        relative,
        bytes: compact.bytes,
        content_hash: None,
        content_fingerprint: None,
        version: discovery.version,
        binary_checked: false,
    };
    visited.evidence.cache.content_reads = visited.evidence.cache.content_reads.saturating_add(1);
    match inspect::inspect_with_visitor(&mut scanned, options, context, sequence, buffer, visitor) {
        Ok(result) => record_visit_result(visited, scanned, &result, options, context, sequence),
        Err(source) => {
            record_io_error(
                &mut visited.evidence,
                &scanned,
                "visit file content",
                source,
                options,
            )?;
            Ok(false)
        }
    }
}

fn record_visit_result(
    visited: &mut VisitedFiles,
    scanned: ScannedFile,
    result: &inspect::VisitedInspection,
    options: &ScanOptions,
    context: ContentWorkerContext<'_>,
    sequence: u64,
) -> Result<bool> {
    let cancelled_without_result = result.status.is_none()
        && options
            .cancellation
            .as_ref()
            .is_some_and(crate::control::CancellationToken::is_cancelled);
    visited.opened = visited.opened.saturating_add(result.opened);
    visited.chunks = visited.chunks.saturating_add(result.chunks);
    visited.bytes_read = visited.bytes_read.saturating_add(result.bytes_read);
    visited.bytes_emitted = visited.bytes_emitted.saturating_add(result.bytes_emitted);
    if result.consumer_skipped {
        visited.consumer_skipped = visited.consumer_skipped.saturating_add(1);
    }
    match result.status.as_ref() {
        Some(VisitedStatus::Selected) => {
            record_selected(visited, scanned, options, context, sequence);
        }
        Some(VisitedStatus::Binary) => {
            record_binary_skip(&mut visited.evidence, scanned.relative, options);
        }
        Some(VisitedStatus::Concurrent) => {
            record_concurrent_modification(&mut visited.evidence, scanned.relative, options)?;
        }
        None => {}
    }
    if result.visitor_quit {
        visited.visitor_quit = true;
        visited.evidence.termination = Some(ScanTermination::Cancelled);
        return Ok(true);
    }
    if cancelled_without_result {
        visited.evidence.termination = Some(ScanTermination::Cancelled);
        return Ok(true);
    }
    Ok(false)
}

fn record_selected(
    visited: &mut VisitedFiles,
    scanned: ScannedFile,
    options: &ScanOptions,
    context: ContentWorkerContext<'_>,
    sequence: u64,
) {
    visited.completed = visited.completed.saturating_add(1);
    if context.mode != ContentVisitMode::Revision {
        return;
    }
    visited.files.push((
        sequence,
        CompactScannedFile {
            relative: scanned.relative.into_boxed_str(),
            bytes: scanned.bytes,
            content: (options.hash_file_contents || options.detect_binary_files).then(|| {
                Box::new(CompactContentEvidence {
                    content_hash: scanned.content_hash.map(String::into_boxed_str),
                    content_fingerprint: scanned.content_fingerprint.map(String::into_boxed_str),
                    version: scanned.version,
                    binary_checked: scanned.binary_checked,
                })
            }),
        },
    ));
}