Skip to main content

fastx/
error.rs

1//! Error types returned by this crate.
2
3use std::fmt;
4use std::io;
5
6/// Convenience alias for results produced by `fastx`.
7pub type Result<T> = std::result::Result<T, Error>;
8
9/// Everything that can go wrong while reading, writing or indexing sequences.
10///
11/// Marked `#[non_exhaustive]`: match with a `_` arm, so that a future release can
12/// describe a new failure without it being a breaking change.
13#[derive(Debug)]
14#[non_exhaustive]
15pub enum Error {
16    /// Underlying I/O failure.
17    Io(io::Error),
18    /// The input is not valid FASTA/FASTQ.
19    Parse {
20        /// 1-based line number on which the problem was detected.
21        line: u64,
22        /// What exactly was wrong.
23        kind: ParseError,
24    },
25    /// The format could not be inferred from a path or from the first bytes.
26    UnknownFormat {
27        /// What was inspected, for the error message.
28        hint: String,
29    },
30    /// A record contains a byte that is not allowed by the requested alphabet.
31    InvalidByte {
32        /// Record identifier, empty if unknown.
33        id: String,
34        /// Offset of the offending byte inside the sequence.
35        pos: usize,
36        /// The offending byte.
37        byte: u8,
38    },
39    /// Sequence and quality strings have different lengths.
40    LengthMismatch {
41        /// Record identifier.
42        id: String,
43        /// Number of residues.
44        seq: usize,
45        /// Number of quality characters.
46        quality: usize,
47    },
48    /// A FASTQ record was requested but the sequence carries no quality string.
49    MissingQuality {
50        /// Record identifier.
51        id: String,
52    },
53    /// A FASTA index (`.fai`) is malformed or does not match the FASTA file.
54    Index(String),
55    /// A sequence name was not found in a FASTA index.
56    UnknownSequence(String),
57    /// Paired reads went out of step: the mate names do not agree.
58    PairMismatch {
59        /// How many pairs were read before this one.
60        pairs_read: u64,
61        /// Identifier of the forward read.
62        first: String,
63        /// Identifier of the reverse read.
64        second: String,
65    },
66    /// One of a pair of inputs ended before the other.
67    PairTruncated {
68        /// How many complete pairs were read.
69        pairs_read: u64,
70        /// Which side ran out: `"R1"`, `"R2"` or `"the second mate"`.
71        missing: &'static str,
72    },
73    /// A requested region lies outside the sequence.
74    OutOfBounds {
75        /// Record identifier.
76        id: String,
77        /// Requested 0-based start.
78        start: u64,
79        /// Requested exclusive end.
80        end: u64,
81        /// Actual sequence length.
82        length: u64,
83    },
84    /// A line or record exceeded a configured size limit.
85    ///
86    /// Only produced when [`crate::ReaderBuilder::max_line_length`] or
87    /// [`crate::ReaderBuilder::max_record_length`] is set.
88    TooLarge {
89        /// 1-based line number at which the limit was hit.
90        line: u64,
91        /// What grew too large: `"line"` or `"sequence"`.
92        what: &'static str,
93        /// The configured limit, in bytes.
94        limit: usize,
95    },
96    /// The operation requires a cargo feature that was not enabled.
97    FeatureDisabled(&'static str),
98    /// The operation cannot be performed on this input.
99    Unsupported(&'static str),
100    /// A context-specific message, for tools built on top of this library.
101    Other(String),
102}
103
104/// Detail for [`Error::Parse`]. Also `#[non_exhaustive]`.
105#[derive(Debug, Clone, PartialEq, Eq)]
106#[non_exhaustive]
107pub enum ParseError {
108    /// A record header was expected but another byte was found.
109    ExpectedHeader {
110        /// The byte that was found instead.
111        found: u8,
112    },
113    /// The `+` separator line of a FASTQ record is missing.
114    ExpectedSeparator {
115        /// The byte that was found instead.
116        found: u8,
117    },
118    /// Input ended in the middle of a record.
119    UnexpectedEof {
120        /// What the parser was looking for.
121        expected: &'static str,
122    },
123    /// A record header carries no identifier.
124    EmptyId,
125}
126
127impl fmt::Display for ParseError {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        match self {
130            ParseError::ExpectedHeader { found } => {
131                write!(
132                    f,
133                    "expected a record header ('>' or '@'), found {}",
134                    Byte(*found)
135                )
136            }
137            ParseError::ExpectedSeparator { found } => {
138                write!(
139                    f,
140                    "expected the FASTQ '+' separator line, found {}",
141                    Byte(*found)
142                )
143            }
144            ParseError::UnexpectedEof { expected } => {
145                write!(f, "unexpected end of input, expected {expected}")
146            }
147            ParseError::EmptyId => write!(f, "record header does not contain an identifier"),
148        }
149    }
150}
151
152impl fmt::Display for Error {
153    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Error::Io(e) => write!(f, "I/O error: {e}"),
156            Error::Parse { line, kind } => write!(f, "parse error on line {line}: {kind}"),
157            Error::UnknownFormat { hint } => {
158                write!(f, "cannot determine sequence format ({hint})")
159            }
160            Error::InvalidByte { id, pos, byte } => write!(
161                f,
162                "record {}: invalid residue {} at position {pos}",
163                Id(id),
164                Byte(*byte)
165            ),
166            Error::LengthMismatch { id, seq, quality } => write!(
167                f,
168                "record {}: sequence has {seq} bases but quality has {quality} scores",
169                Id(id)
170            ),
171            Error::MissingQuality { id } => {
172                write!(f, "record {}: FASTQ output requires quality scores", Id(id))
173            }
174            Error::Index(msg) => write!(f, "invalid FASTA index: {msg}"),
175            Error::UnknownSequence(name) => write!(f, "sequence {name:?} is not in the index"),
176            Error::PairMismatch {
177                pairs_read,
178                first,
179                second,
180            } => write!(
181                f,
182                "reads are mispaired after {pairs_read} pairs: {first:?} and {second:?} are not \
183                 mates. Are the two files sorted the same way?"
184            ),
185            Error::PairTruncated {
186                pairs_read,
187                missing,
188            } => write!(
189                f,
190                "{missing} ended after {pairs_read} pairs, the other did not"
191            ),
192            Error::OutOfBounds {
193                id,
194                start,
195                end,
196                length,
197            } => write!(
198                f,
199                "region {start}..{end} is out of bounds for record {} of length {length}",
200                Id(id)
201            ),
202            Error::TooLarge { line, what, limit } => write!(
203                f,
204                "line {line}: {what} exceeds the configured limit of {limit} bytes"
205            ),
206            Error::FeatureDisabled(feature) => {
207                write!(
208                    f,
209                    "this build of fastx was compiled without the {feature:?} feature"
210                )
211            }
212            Error::Unsupported(what) => write!(f, "unsupported: {what}"),
213            Error::Other(message) => f.write_str(message),
214        }
215    }
216}
217
218impl std::error::Error for Error {
219    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
220        match self {
221            Error::Io(e) => Some(e),
222            _ => None,
223        }
224    }
225}
226
227impl From<io::Error> for Error {
228    fn from(e: io::Error) -> Self {
229        Error::Io(e)
230    }
231}
232
233impl From<Error> for io::Error {
234    fn from(e: Error) -> Self {
235        match e {
236            Error::Io(e) => e,
237            other => io::Error::new(io::ErrorKind::InvalidData, other),
238        }
239    }
240}
241
242impl Error {
243    pub(crate) fn parse(line: u64, kind: ParseError) -> Self {
244        Error::Parse { line, kind }
245    }
246
247    /// True if the error describes malformed input rather than an I/O failure.
248    ///
249    /// Useful for pipelines that want to skip bad records but abort on a broken pipe.
250    pub fn is_malformed(&self) -> bool {
251        !matches!(self, Error::Io(_))
252    }
253}
254
255/// Helper that renders a byte as a readable character or escape.
256struct Byte(u8);
257
258impl fmt::Display for Byte {
259    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
260        match self.0 {
261            b'\n' => f.write_str("'\\n'"),
262            b'\r' => f.write_str("'\\r'"),
263            b'\t' => f.write_str("'\\t'"),
264            b if b.is_ascii_graphic() => write!(f, "'{}'", b as char),
265            b => write!(f, "0x{b:02x}"),
266        }
267    }
268}
269
270/// Helper that renders a possibly empty identifier.
271struct Id<'a>(&'a str);
272
273impl fmt::Display for Id<'_> {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
275        if self.0.is_empty() {
276            f.write_str("<unnamed>")
277        } else {
278            write!(f, "{:?}", self.0)
279        }
280    }
281}