dino-seq 0.1.0

Low-allocation FASTQ and FASTA parser core
Documentation
use crate::error::{FastqError, FastqPosition, Result};

#[derive(Clone, Copy)]
pub(crate) struct Line<'a> {
    pub(crate) bytes: &'a [u8],
    pub(crate) start: usize,
}

impl Line<'_> {
    #[inline]
    pub(crate) fn len(self) -> usize {
        self.bytes.len()
    }
}

#[derive(Clone, Copy)]
pub(crate) struct RecordLines<'a> {
    pub(crate) name: Line<'a>,
    pub(crate) seq: Line<'a>,
    pub(crate) plus: Line<'a>,
    pub(crate) qual: Line<'a>,
}

#[derive(Clone, Copy)]
pub(crate) struct RecordValidation {
    require_nonempty_id: bool,
}

impl RecordValidation {
    pub(crate) const TRUSTED_PACK: Self = Self {
        require_nonempty_id: false,
    };
}

pub(crate) fn validate_record(
    record: RecordLines<'_>,
    base_offset: u64,
    record_index: u64,
    validation: RecordValidation,
) -> Result<()> {
    if record.name.bytes.first() != Some(&b'@') {
        return Err(format_at(
            "header must start with `@`",
            base_offset,
            record.name.start,
            record_index,
            0,
        ));
    }
    if validation.require_nonempty_id {
        match record.name.bytes.get(1) {
            Some(byte) if !byte.is_ascii_whitespace() => {}
            _ => {
                return Err(format_at(
                    "empty FASTQ id",
                    base_offset,
                    record.name.start,
                    record_index,
                    0,
                ));
            }
        }
    }
    if record.plus.bytes.first() != Some(&b'+') {
        return Err(format_at(
            "plus line must start with `+`",
            base_offset,
            record.plus.start,
            record_index,
            2,
        ));
    }
    let seq_len = record.seq.len();
    let qual_len = record.qual.len();
    if seq_len != qual_len {
        return Err(format_at(
            format!("quality length {qual_len} != sequence length {seq_len}"),
            base_offset,
            record.qual.start,
            record_index,
            3,
        ));
    }
    Ok(())
}

pub(crate) fn fast_slash_pair_ids_match(first_name: &[u8], second_name: &[u8]) -> Option<bool> {
    let first = first_name.strip_prefix(b"@").unwrap_or(first_name);
    let second = second_name.strip_prefix(b"@").unwrap_or(second_name);
    if first.len() >= 3 && first.len() == second.len() && first.ends_with(b"/1") {
        return Some(
            second.ends_with(b"/2") && first[..first.len() - 2] == second[..second.len() - 2],
        );
    }

    let first_end = token_end(first);
    let second_end = token_end(second);
    let first = &first[..first_end];
    let second = &second[..second_end];
    if first.len() < 3 || second.len() < 3 {
        return None;
    }
    let first_suffix = &first[first.len() - 2..];
    let second_suffix = &second[second.len() - 2..];
    if first_suffix != b"/1" || first.len() != second.len() {
        return None;
    }
    Some(second_suffix == b"/2" && first[..first.len() - 2] == second[..second.len() - 2])
}

pub(crate) fn normalized_pair_id(name: &[u8]) -> &[u8] {
    let name = name.strip_prefix(b"@").unwrap_or(name);
    let token = &name[..token_end(name)];
    strip_pair_suffix(token)
}

pub(crate) fn strip_pair_suffix(id: &[u8]) -> &[u8] {
    if id.len() >= 2 && (id.ends_with(b"/1") || id.ends_with(b"/2")) {
        &id[..id.len() - 2]
    } else {
        id
    }
}

#[inline]
pub(crate) fn format_at(
    message: impl Into<String>,
    base_offset: u64,
    local_offset: usize,
    record_index: u64,
    line_index: u8,
) -> FastqError {
    FastqError::FormatAt {
        message: message.into(),
        position: FastqPosition::new(base_offset + local_offset as u64, record_index, line_index),
    }
}

#[inline]
pub(crate) fn trim_cr_end(bytes: &[u8], start: usize, end: usize) -> usize {
    if end > start && bytes[end - 1] == b'\r' {
        end - 1
    } else {
        end
    }
}

#[inline]
fn token_end(bytes: &[u8]) -> usize {
    let mut end = 0;
    while end < bytes.len() && !bytes[end].is_ascii_whitespace() {
        end += 1;
    }
    end
}