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
}