fastx-io 0.3.0

Fast, streaming FASTA/FASTQ reader and writer for bioinformatics pipelines
Documentation
//! Error types returned by this crate.

use std::fmt;
use std::io;

/// Convenience alias for results produced by `fastx`.
pub type Result<T> = std::result::Result<T, Error>;

/// Everything that can go wrong while reading, writing or indexing sequences.
///
/// Marked `#[non_exhaustive]`: match with a `_` arm, so that a future release can
/// describe a new failure without it being a breaking change.
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
    /// Underlying I/O failure.
    Io(io::Error),
    /// The input is not valid FASTA/FASTQ.
    Parse {
        /// 1-based line number on which the problem was detected.
        line: u64,
        /// What exactly was wrong.
        kind: ParseError,
    },
    /// The format could not be inferred from a path or from the first bytes.
    UnknownFormat {
        /// What was inspected, for the error message.
        hint: String,
    },
    /// A record contains a byte that is not allowed by the requested alphabet.
    InvalidByte {
        /// Record identifier, empty if unknown.
        id: String,
        /// Offset of the offending byte inside the sequence.
        pos: usize,
        /// The offending byte.
        byte: u8,
    },
    /// Sequence and quality strings have different lengths.
    LengthMismatch {
        /// Record identifier.
        id: String,
        /// Number of residues.
        seq: usize,
        /// Number of quality characters.
        quality: usize,
    },
    /// A FASTQ record was requested but the sequence carries no quality string.
    MissingQuality {
        /// Record identifier.
        id: String,
    },
    /// A FASTA index (`.fai`) is malformed or does not match the FASTA file.
    Index(String),
    /// A sequence name was not found in a FASTA index.
    UnknownSequence(String),
    /// Paired reads went out of step: the mate names do not agree.
    PairMismatch {
        /// How many pairs were read before this one.
        pairs_read: u64,
        /// Identifier of the forward read.
        first: String,
        /// Identifier of the reverse read.
        second: String,
    },
    /// One of a pair of inputs ended before the other.
    PairTruncated {
        /// How many complete pairs were read.
        pairs_read: u64,
        /// Which side ran out: `"R1"`, `"R2"` or `"the second mate"`.
        missing: &'static str,
    },
    /// A requested region lies outside the sequence.
    OutOfBounds {
        /// Record identifier.
        id: String,
        /// Requested 0-based start.
        start: u64,
        /// Requested exclusive end.
        end: u64,
        /// Actual sequence length.
        length: u64,
    },
    /// A line or record exceeded a configured size limit.
    ///
    /// Only produced when [`crate::ReaderBuilder::max_line_length`] or
    /// [`crate::ReaderBuilder::max_record_length`] is set.
    TooLarge {
        /// 1-based line number at which the limit was hit.
        line: u64,
        /// What grew too large: `"line"` or `"sequence"`.
        what: &'static str,
        /// The configured limit, in bytes.
        limit: usize,
    },
    /// The operation requires a cargo feature that was not enabled.
    FeatureDisabled(&'static str),
    /// The operation cannot be performed on this input.
    Unsupported(&'static str),
    /// A context-specific message, for tools built on top of this library.
    Other(String),
}

/// Detail for [`Error::Parse`]. Also `#[non_exhaustive]`.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseError {
    /// A record header was expected but another byte was found.
    ExpectedHeader {
        /// The byte that was found instead.
        found: u8,
    },
    /// The `+` separator line of a FASTQ record is missing.
    ExpectedSeparator {
        /// The byte that was found instead.
        found: u8,
    },
    /// Input ended in the middle of a record.
    UnexpectedEof {
        /// What the parser was looking for.
        expected: &'static str,
    },
    /// A record header carries no identifier.
    EmptyId,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ParseError::ExpectedHeader { found } => {
                write!(
                    f,
                    "expected a record header ('>' or '@'), found {}",
                    Byte(*found)
                )
            }
            ParseError::ExpectedSeparator { found } => {
                write!(
                    f,
                    "expected the FASTQ '+' separator line, found {}",
                    Byte(*found)
                )
            }
            ParseError::UnexpectedEof { expected } => {
                write!(f, "unexpected end of input, expected {expected}")
            }
            ParseError::EmptyId => write!(f, "record header does not contain an identifier"),
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io(e) => write!(f, "I/O error: {e}"),
            Error::Parse { line, kind } => write!(f, "parse error on line {line}: {kind}"),
            Error::UnknownFormat { hint } => {
                write!(f, "cannot determine sequence format ({hint})")
            }
            Error::InvalidByte { id, pos, byte } => write!(
                f,
                "record {}: invalid residue {} at position {pos}",
                Id(id),
                Byte(*byte)
            ),
            Error::LengthMismatch { id, seq, quality } => write!(
                f,
                "record {}: sequence has {seq} bases but quality has {quality} scores",
                Id(id)
            ),
            Error::MissingQuality { id } => {
                write!(f, "record {}: FASTQ output requires quality scores", Id(id))
            }
            Error::Index(msg) => write!(f, "invalid FASTA index: {msg}"),
            Error::UnknownSequence(name) => write!(f, "sequence {name:?} is not in the index"),
            Error::PairMismatch {
                pairs_read,
                first,
                second,
            } => write!(
                f,
                "reads are mispaired after {pairs_read} pairs: {first:?} and {second:?} are not \
                 mates. Are the two files sorted the same way?"
            ),
            Error::PairTruncated {
                pairs_read,
                missing,
            } => write!(
                f,
                "{missing} ended after {pairs_read} pairs, the other did not"
            ),
            Error::OutOfBounds {
                id,
                start,
                end,
                length,
            } => write!(
                f,
                "region {start}..{end} is out of bounds for record {} of length {length}",
                Id(id)
            ),
            Error::TooLarge { line, what, limit } => write!(
                f,
                "line {line}: {what} exceeds the configured limit of {limit} bytes"
            ),
            Error::FeatureDisabled(feature) => {
                write!(
                    f,
                    "this build of fastx was compiled without the {feature:?} feature"
                )
            }
            Error::Unsupported(what) => write!(f, "unsupported: {what}"),
            Error::Other(message) => f.write_str(message),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Error::Io(e)
    }
}

impl From<Error> for io::Error {
    fn from(e: Error) -> Self {
        match e {
            Error::Io(e) => e,
            other => io::Error::new(io::ErrorKind::InvalidData, other),
        }
    }
}

impl Error {
    pub(crate) fn parse(line: u64, kind: ParseError) -> Self {
        Error::Parse { line, kind }
    }

    /// True if the error describes malformed input rather than an I/O failure.
    ///
    /// Useful for pipelines that want to skip bad records but abort on a broken pipe.
    pub fn is_malformed(&self) -> bool {
        !matches!(self, Error::Io(_))
    }
}

/// Helper that renders a byte as a readable character or escape.
struct Byte(u8);

impl fmt::Display for Byte {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.0 {
            b'\n' => f.write_str("'\\n'"),
            b'\r' => f.write_str("'\\r'"),
            b'\t' => f.write_str("'\\t'"),
            b if b.is_ascii_graphic() => write!(f, "'{}'", b as char),
            b => write!(f, "0x{b:02x}"),
        }
    }
}

/// Helper that renders a possibly empty identifier.
struct Id<'a>(&'a str);

impl fmt::Display for Id<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.0.is_empty() {
            f.write_str("<unnamed>")
        } else {
            write!(f, "{:?}", self.0)
        }
    }
}