neco-fuzzy 0.2.0

Minimal fuzzy score core for commands, paths, and short identifiers
Documentation
use core::fmt;

pub const PREPARED_CANDIDATE_FORMAT_VERSION: u16 = 1;
pub const PREPARED_CANDIDATE_ALGORITHM_VERSION: u16 = 2;
pub const PREPARED_CANDIDATE_HEADER_LEN: usize = 16;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PreparedCandidateHeader {
    pub format_version: u16,
    pub algorithm_version: u16,
    pub fingerprint: u64,
    pub text_len: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncodeError {
    BufferTooSmall { required: usize },
    TextTooLong { len: usize },
}

impl fmt::Display for EncodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::BufferTooSmall { required } => {
                write!(
                    f,
                    "buffer too small for prepared candidate archive: need {required} bytes"
                )
            }
            Self::TextTooLong { len } => {
                write!(
                    f,
                    "prepared candidate text length {len} exceeds u32 archive header"
                )
            }
        }
    }
}

impl std::error::Error for EncodeError {}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DecodeError {
    Truncated { required: usize, actual: usize },
    UnsupportedFormatVersion { expected: u16, actual: u16 },
    UnsupportedAlgorithmVersion { expected: u16, actual: u16 },
    FingerprintMismatch { expected: u64, actual: u64 },
    PlatformLengthOverflow { text_len: u32 },
    InvalidUtf8,
}

impl fmt::Display for DecodeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Truncated { required, actual } => {
                write!(
                    f,
                    "prepared candidate archive truncated: need {required} bytes, got {actual}"
                )
            }
            Self::UnsupportedFormatVersion { expected, actual } => {
                write!(
                    f,
                    "unsupported prepared candidate format version: expected {expected}, got {actual}"
                )
            }
            Self::UnsupportedAlgorithmVersion { expected, actual } => {
                write!(
                    f,
                    "unsupported prepared candidate algorithm version: expected {expected}, got {actual}"
                )
            }
            Self::FingerprintMismatch { expected, actual } => {
                write!(
                    f,
                    "prepared candidate fingerprint mismatch: expected {expected}, got {actual}"
                )
            }
            Self::PlatformLengthOverflow { text_len } => {
                write!(
                    f,
                    "prepared candidate archive length {text_len} does not fit on this platform"
                )
            }
            Self::InvalidUtf8 => write!(f, "prepared candidate archive contains invalid utf-8"),
        }
    }
}

impl std::error::Error for DecodeError {}

pub(crate) fn checked_archive_text_len(len: usize) -> Result<u32, EncodeError> {
    u32::try_from(len).map_err(|_| EncodeError::TextTooLong { len })
}

pub(crate) fn decode_header(bytes: &[u8]) -> Result<PreparedCandidateHeader, DecodeError> {
    if bytes.len() < PREPARED_CANDIDATE_HEADER_LEN {
        return Err(DecodeError::Truncated {
            required: PREPARED_CANDIDATE_HEADER_LEN,
            actual: bytes.len(),
        });
    }

    let format_version = u16::from_le_bytes([bytes[0], bytes[1]]);
    let algorithm_version = u16::from_le_bytes([bytes[2], bytes[3]]);
    let fingerprint = u64::from_le_bytes([
        bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11],
    ]);
    let text_len = u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
    Ok(PreparedCandidateHeader {
        format_version,
        algorithm_version,
        fingerprint,
        text_len,
    })
}

pub(crate) fn encode_candidate(
    text: &str,
    fingerprint: u64,
    out: &mut [u8],
) -> Result<usize, EncodeError> {
    let required = encoded_len(text.len());
    if out.len() < required {
        return Err(EncodeError::BufferTooSmall { required });
    }

    let header = PreparedCandidateHeader {
        format_version: PREPARED_CANDIDATE_FORMAT_VERSION,
        algorithm_version: PREPARED_CANDIDATE_ALGORITHM_VERSION,
        fingerprint,
        text_len: checked_archive_text_len(text.len())?,
    };
    out[0..2].copy_from_slice(&header.format_version.to_le_bytes());
    out[2..4].copy_from_slice(&header.algorithm_version.to_le_bytes());
    out[4..12].copy_from_slice(&header.fingerprint.to_le_bytes());
    out[12..16].copy_from_slice(&header.text_len.to_le_bytes());
    out[16..required].copy_from_slice(text.as_bytes());
    Ok(required)
}

pub(crate) fn encoded_len(text_len: usize) -> usize {
    PREPARED_CANDIDATE_HEADER_LEN + text_len
}

pub(crate) fn decode_candidate_text(
    bytes: &[u8],
) -> Result<(&str, PreparedCandidateHeader), DecodeError> {
    let header = decode_header(bytes)?;
    if header.format_version != PREPARED_CANDIDATE_FORMAT_VERSION {
        return Err(DecodeError::UnsupportedFormatVersion {
            expected: PREPARED_CANDIDATE_FORMAT_VERSION,
            actual: header.format_version,
        });
    }
    if header.algorithm_version != PREPARED_CANDIDATE_ALGORITHM_VERSION {
        return Err(DecodeError::UnsupportedAlgorithmVersion {
            expected: PREPARED_CANDIDATE_ALGORITHM_VERSION,
            actual: header.algorithm_version,
        });
    }

    let text_len =
        usize::try_from(header.text_len).map_err(|_| DecodeError::PlatformLengthOverflow {
            text_len: header.text_len,
        })?;
    let required = PREPARED_CANDIDATE_HEADER_LEN + text_len;
    if bytes.len() < required {
        return Err(DecodeError::Truncated {
            required,
            actual: bytes.len(),
        });
    }

    let text_bytes = &bytes[PREPARED_CANDIDATE_HEADER_LEN..required];
    let text = core::str::from_utf8(text_bytes).map_err(|_| DecodeError::InvalidUtf8)?;
    Ok((text, header))
}