weavatrix-scan 0.4.4

Deterministic, safe repository scanner for code intelligence
Documentation
use crate::config::{ContentValidationPolicy, ScanOptions};
use crate::content_visit::{
    ContentFile, ContentFileStatus, ContentVisitControl, ContentVisitEvent,
};
use crate::file_version::{reusable, snapshot};
use crate::hash::{ContentFingerprint, FingerprintHasher};
use crate::report::ScannedFile;
use std::fs::File;
use std::io::{self, Read as _};
use std::path::Path;

use super::ContentWorkerContext;

pub(super) enum Inspection {
    Selected(ScannedFile),
    Binary(String),
    Concurrent(String),
}

pub(super) enum CachedValidation {
    Match,
    Changed,
    Concurrent,
}

pub(super) struct VisitedInspection {
    pub status: Option<VisitedStatus>,
    pub opened: u64,
    pub chunks: u64,
    pub bytes_read: u64,
    pub bytes_emitted: u64,
    pub consumer_skipped: bool,
    pub visitor_quit: bool,
}

pub(super) enum VisitedStatus {
    Selected,
    Binary,
    Concurrent,
}

pub(super) fn inspect(mut scanned: ScannedFile, options: &ScanOptions) -> io::Result<Inspection> {
    let mut file = File::open(&scanned.absolute)?;
    let before = snapshot(&file)?;
    if before.bytes != scanned.bytes || !reusable(&scanned.version, &before.version) {
        return Ok(Inspection::Concurrent(scanned.relative));
    }
    if before.bytes > options.max_file_bytes {
        return Ok(Inspection::Concurrent(scanned.relative));
    }

    let mut hasher = options.hash_file_contents.then(FingerprintHasher::new);
    let mut fingerprint = options.hash_file_contents.then(ContentFingerprint::new);
    let reads_entire_file = hasher.is_some();
    let mut binary = false;
    let mut bytes_read = 0_u64;
    let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        bytes_read = bytes_read.saturating_add(read as u64);
        if options.detect_binary_files && buffer[..read].contains(&0) {
            binary = true;
        }
        if let Some(hasher) = hasher.as_mut() {
            hasher.write(&buffer[..read]);
        } else if bytes_read >= 8 * 1024 {
            break;
        }
        if let Some(fingerprint) = fingerprint.as_mut() {
            fingerprint.write(&buffer[..read]);
        }
    }

    let after = if options.content_validation == ContentValidationPolicy::Strict {
        Some(snapshot(&file)?)
    } else {
        None
    };
    if (reads_entire_file && bytes_read != before.bytes)
        || after.as_ref().is_some_and(|after| {
            before.bytes != after.bytes || !reusable(&before.version, &after.version)
        })
    {
        return Ok(Inspection::Concurrent(scanned.relative));
    }
    if binary {
        return Ok(Inspection::Binary(scanned.relative));
    }
    scanned.version = after.map_or(before.version, |after| after.version);
    scanned.binary_checked = options.detect_binary_files;
    scanned.content_hash = hasher.map(FingerprintHasher::finish);
    scanned.content_fingerprint = fingerprint.map(ContentFingerprint::finish);
    Ok(Inspection::Selected(scanned))
}

#[allow(clippy::too_many_lines)]
pub(super) fn inspect_with_visitor<V>(
    scanned: &mut ScannedFile,
    options: &ScanOptions,
    context: ContentWorkerContext<'_>,
    sequence: u64,
    buffer: &mut [u8],
    visitor: &mut V,
) -> io::Result<VisitedInspection>
where
    V: for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl,
{
    let mut file = File::open(&scanned.absolute)?;
    let before = snapshot(&file)?;
    if before.bytes != scanned.bytes || !reusable(&scanned.version, &before.version) {
        return Ok(VisitedInspection {
            status: Some(VisitedStatus::Concurrent),
            opened: 1,
            chunks: 0,
            bytes_read: 0,
            bytes_emitted: 0,
            consumer_skipped: false,
            visitor_quit: false,
        });
    }
    if before.bytes > options.max_file_bytes {
        return Ok(VisitedInspection {
            status: Some(VisitedStatus::Concurrent),
            opened: 1,
            chunks: 0,
            bytes_read: 0,
            bytes_emitted: 0,
            consumer_skipped: false,
            visitor_quit: false,
        });
    }

    let mut consumer_skipped = false;
    match visitor(ContentVisitEvent::FileStart {
        worker_index: context.worker_index,
        file: content_file(context.root, context.root_index, scanned, sequence),
    }) {
        ContentVisitControl::Continue => {}
        ContentVisitControl::SkipFile => consumer_skipped = true,
        ContentVisitControl::Quit => {
            cancel(options);
            return Ok(VisitedInspection {
                status: None,
                opened: 1,
                chunks: 0,
                bytes_read: 0,
                bytes_emitted: 0,
                consumer_skipped: false,
                visitor_quit: true,
            });
        }
    }

    let mut hasher = options.hash_file_contents.then(FingerprintHasher::new);
    let mut fingerprint = options.hash_file_contents.then(ContentFingerprint::new);
    let mut binary = false;
    let mut bytes_read = 0_u64;
    let mut bytes_emitted = 0_u64;
    let mut chunks = 0_u64;
    let mut reached_eof = false;
    while !consumer_skipped || hasher.is_some() || options.detect_binary_files {
        if options
            .cancellation
            .as_ref()
            .is_some_and(crate::CancellationToken::is_cancelled)
        {
            return Ok(VisitedInspection {
                status: None,
                opened: 1,
                chunks,
                bytes_read,
                bytes_emitted,
                consumer_skipped,
                visitor_quit: false,
            });
        }
        let read = file.read(buffer)?;
        if read == 0 {
            reached_eof = true;
            break;
        }
        let offset = bytes_read;
        bytes_read = bytes_read.saturating_add(read as u64);
        let bytes = &buffer[..read];
        if options.detect_binary_files && bytes.contains(&0) {
            binary = true;
            break;
        }
        if let Some(hasher) = hasher.as_mut() {
            hasher.write(bytes);
        }
        if let Some(fingerprint) = fingerprint.as_mut() {
            fingerprint.write(bytes);
        }
        if !consumer_skipped {
            chunks = chunks.saturating_add(1);
            bytes_emitted = bytes_emitted.saturating_add(read as u64);
            match visitor(ContentVisitEvent::Chunk {
                worker_index: context.worker_index,
                file: content_file(context.root, context.root_index, scanned, sequence),
                offset,
                bytes,
            }) {
                ContentVisitControl::Continue => {}
                ContentVisitControl::SkipFile => consumer_skipped = true,
                ContentVisitControl::Quit => {
                    cancel(options);
                    return Ok(VisitedInspection {
                        status: None,
                        opened: 1,
                        chunks,
                        bytes_read,
                        bytes_emitted,
                        consumer_skipped,
                        visitor_quit: true,
                    });
                }
            }
        }
    }

    let after = if options.content_validation == ContentValidationPolicy::Strict {
        Some(snapshot(&file)?)
    } else {
        None
    };
    if (reached_eof && bytes_read != before.bytes)
        || after.as_ref().is_some_and(|after| {
            before.bytes != after.bytes || !reusable(&before.version, &after.version)
        })
    {
        let visitor_quit = emit_end(
            visitor,
            context.worker_index,
            context.root,
            context.root_index,
            scanned,
            sequence,
            ContentFileStatus::Changed,
            bytes_read,
            None,
            consumer_skipped,
            options,
        );
        return Ok(VisitedInspection {
            status: Some(VisitedStatus::Concurrent),
            opened: 1,
            chunks,
            bytes_read,
            bytes_emitted,
            consumer_skipped,
            visitor_quit,
        });
    }
    if binary {
        let visitor_quit = emit_end(
            visitor,
            context.worker_index,
            context.root,
            context.root_index,
            scanned,
            sequence,
            ContentFileStatus::Binary,
            bytes_read,
            None,
            consumer_skipped,
            options,
        );
        return Ok(VisitedInspection {
            status: Some(VisitedStatus::Binary),
            opened: 1,
            chunks,
            bytes_read,
            bytes_emitted,
            consumer_skipped,
            visitor_quit,
        });
    }

    scanned.version = after.map_or(before.version, |after| after.version);
    scanned.binary_checked = options.detect_binary_files;
    scanned.content_hash = hasher.map(FingerprintHasher::finish);
    scanned.content_fingerprint = fingerprint.map(ContentFingerprint::finish);
    let visitor_quit = emit_end(
        visitor,
        context.worker_index,
        context.root,
        context.root_index,
        scanned,
        sequence,
        ContentFileStatus::Selected,
        bytes_read,
        scanned.content_hash.as_deref(),
        consumer_skipped,
        options,
    );
    Ok(VisitedInspection {
        status: Some(VisitedStatus::Selected),
        opened: 1,
        chunks,
        bytes_read,
        bytes_emitted,
        consumer_skipped,
        visitor_quit,
    })
}

fn content_file<'a>(
    root: &'a Path,
    root_index: usize,
    scanned: &'a ScannedFile,
    sequence: u64,
) -> ContentFile<'a> {
    ContentFile {
        root_index,
        sequence,
        root,
        absolute: &scanned.absolute,
        relative: &scanned.relative,
        bytes: scanned.bytes,
    }
}

#[allow(clippy::too_many_arguments)]
fn emit_end<V>(
    visitor: &mut V,
    worker_index: usize,
    root: &Path,
    root_index: usize,
    scanned: &ScannedFile,
    sequence: u64,
    status: ContentFileStatus,
    bytes_read: u64,
    content_hash: Option<&str>,
    consumer_skipped: bool,
    options: &ScanOptions,
) -> bool
where
    V: for<'event> FnMut(ContentVisitEvent<'event>) -> ContentVisitControl,
{
    if visitor(ContentVisitEvent::FileEnd {
        worker_index,
        file: content_file(root, root_index, scanned, sequence),
        status,
        bytes_read,
        content_hash,
        consumer_skipped,
    }) == ContentVisitControl::Quit
    {
        cancel(options);
        true
    } else {
        false
    }
}

fn cancel(options: &ScanOptions) {
    if let Some(cancellation) = &options.cancellation {
        cancellation.cancel();
    }
}

pub(super) fn validate_cached(
    scanned: &mut ScannedFile,
    expected_fingerprint: &str,
) -> io::Result<CachedValidation> {
    let mut file = File::open(&scanned.absolute)?;
    let before = snapshot(&file)?;
    if before.bytes != scanned.bytes || !reusable(&scanned.version, &before.version) {
        return Ok(CachedValidation::Concurrent);
    }

    let mut fingerprint = ContentFingerprint::new();
    let mut bytes_read = 0_u64;
    let mut buffer = vec![0_u8; 64 * 1024].into_boxed_slice();
    loop {
        let read = file.read(&mut buffer)?;
        if read == 0 {
            break;
        }
        bytes_read = bytes_read.saturating_add(read as u64);
        fingerprint.write(&buffer[..read]);
    }

    let after = snapshot(&file)?;
    if bytes_read != before.bytes
        || before.bytes != after.bytes
        || !reusable(&before.version, &after.version)
    {
        return Ok(CachedValidation::Concurrent);
    }
    scanned.version = after.version;
    if fingerprint.finish() == expected_fingerprint {
        Ok(CachedValidation::Match)
    } else {
        Ok(CachedValidation::Changed)
    }
}