magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use std::{
    fs::File,
    io::{self, Read},
    path::Path,
};

pub(super) const MAX_FILE_BYTES: u64 = 4 * 1024 * 1024;

#[derive(Debug)]
pub(super) enum ReadOutcome {
    Bytes {
        bytes: Vec<u8>,
        bytes_scanned: u64,
        /// The file exceeded the per-file prefix cap. This is separate from
        /// exhaustion of the invocation-wide scan-byte budget.
        truncated: bool,
        scan_byte_limit_reached: bool,
    },
    /// The path was not a regular file (for example, a FIFO or directory).
    Skipped,
    /// The path disappeared or became unreadable after traversal found it.
    ReadErrorSkipped,
    /// No invocation-wide scan bytes remained for this file.
    ScanByteLimitReached,
}

pub(super) fn read_file_bytes(path: &Path, remaining_bytes: u64) -> io::Result<ReadOutcome> {
    if remaining_bytes == 0 {
        return Ok(ReadOutcome::ScanByteLimitReached);
    }

    let metadata = match path.symlink_metadata() {
        Ok(metadata) => metadata,
        Err(error) if is_recoverable(&error) => return Ok(ReadOutcome::ReadErrorSkipped),
        Err(error) => return Err(error),
    };
    if !metadata.file_type().is_file() {
        return Ok(ReadOutcome::Skipped);
    }
    let size = metadata.len();
    if size == 0 {
        return Ok(ReadOutcome::Bytes {
            bytes: Vec::new(),
            bytes_scanned: 0,
            truncated: false,
            scan_byte_limit_reached: false,
        });
    }

    let file = match File::open(path) {
        Ok(file) => file,
        Err(error) if is_recoverable(&error) => return Ok(ReadOutcome::ReadErrorSkipped),
        Err(error) => return Err(error),
    };

    read_opened_file(file, size, remaining_bytes)
}

/// Reads the searchable prefix of an opened file. `size` comes from metadata
/// read before opening, so the file may have grown, shrunk, or been replaced
/// at its path since then.
fn read_opened_file(file: impl Read, size: u64, remaining_bytes: u64) -> io::Result<ReadOutcome> {
    // A one-byte probe detects a file that grew after metadata was read, but
    // only when the probe cannot consume the last byte of the total budget.
    // The returned byte count deliberately covers searchable bytes, not the
    // discarded probe byte.
    let read_limit = MAX_FILE_BYTES.min(remaining_bytes).min(size);
    let probe = size <= MAX_FILE_BYTES && read_limit < remaining_bytes;
    let read_request = read_limit.saturating_add(u64::from(probe));
    let mut bytes = Vec::with_capacity(read_request as usize);
    if let Err(error) = file.take(read_request).read_to_end(&mut bytes) {
        if is_recoverable(&error) {
            return Ok(ReadOutcome::ReadErrorSkipped);
        }
        return Err(error);
    }

    let grew = bytes.len() as u64 > read_limit;
    bytes.truncate(read_limit as usize);
    let file_prefix_truncated = size > MAX_FILE_BYTES || grew;
    let scan_byte_limit_reached = size > remaining_bytes && remaining_bytes < MAX_FILE_BYTES;

    Ok(ReadOutcome::Bytes {
        bytes_scanned: bytes.len() as u64,
        bytes,
        truncated: file_prefix_truncated,
        scan_byte_limit_reached,
    })
}

fn is_recoverable(error: &io::Error) -> bool {
    matches!(
        error.kind(),
        io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
    )
}