use std::fmt;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Format {
Fasta,
Fastq,
}
impl Format {
pub const FASTA_EXTENSIONS: &'static [&'static str] = &[
"fa", "fasta", "fna", "faa", "ffn", "frn", "fas", "mpfa", "seq",
];
pub const FASTQ_EXTENSIONS: &'static [&'static str] = &["fq", "fastq"];
pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Format> {
let path = path.as_ref();
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
if matches!(ext.as_str(), "gz" | "bgz" | "gzip" | "z" | "zst" | "bz2") {
let stem = path.file_stem()?;
return Format::from_path(Path::new(stem));
}
Format::from_extension(&ext)
}
pub fn from_extension(ext: &str) -> Option<Format> {
let ext = ext.trim_start_matches('.').to_ascii_lowercase();
if Format::FASTA_EXTENSIONS.contains(&ext.as_str()) {
Some(Format::Fasta)
} else if Format::FASTQ_EXTENSIONS.contains(&ext.as_str()) {
Some(Format::Fastq)
} else {
None
}
}
pub fn from_first_byte(byte: u8) -> Option<Format> {
match byte {
b'>' | b';' => Some(Format::Fasta),
b'@' => Some(Format::Fastq),
_ => None,
}
}
pub const fn header_byte(self) -> u8 {
match self {
Format::Fasta => b'>',
Format::Fastq => b'@',
}
}
pub const fn extension(self) -> &'static str {
match self {
Format::Fasta => "fasta",
Format::Fastq => "fastq",
}
}
pub const fn has_quality(self) -> bool {
matches!(self, Format::Fastq)
}
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Format::Fasta => f.write_str("FASTA"),
Format::Fastq => f.write_str("FASTQ"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum Compression {
#[default]
None,
Gzip,
Bgzf,
Zstd,
}
impl Compression {
pub const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
pub const ZSTD_MAGIC: [u8; 4] = [0x28, 0xb5, 0x2f, 0xfd];
pub fn from_path<P: AsRef<Path>>(path: P) -> Compression {
match path
.as_ref()
.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_ascii_lowercase())
.as_deref()
{
Some("gz") | Some("bgz") | Some("gzip") => Compression::Gzip,
Some("zst") | Some("zstd") => Compression::Zstd,
_ => Compression::None,
}
}
pub fn from_magic(bytes: &[u8]) -> Compression {
if bytes.len() >= 2 && bytes[..2] == Compression::GZIP_MAGIC {
Compression::Gzip
} else if bytes.len() >= 4 && bytes[..4] == Compression::ZSTD_MAGIC {
Compression::Zstd
} else {
Compression::None
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressionLevel(pub u32);
impl CompressionLevel {
pub const NONE: CompressionLevel = CompressionLevel(0);
pub const FAST: CompressionLevel = CompressionLevel(1);
pub const DEFAULT: CompressionLevel = CompressionLevel(6);
pub const BEST: CompressionLevel = CompressionLevel(9);
}
impl Default for CompressionLevel {
fn default() -> Self {
CompressionLevel::DEFAULT
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_format_from_extensions() {
assert_eq!(Format::from_path("a.fa"), Some(Format::Fasta));
assert_eq!(Format::from_path("a.FASTA"), Some(Format::Fasta));
assert_eq!(Format::from_path("a.faa"), Some(Format::Fasta));
assert_eq!(Format::from_path("/tmp/x/a.fna.gz"), Some(Format::Fasta));
assert_eq!(Format::from_path("a.fq"), Some(Format::Fastq));
assert_eq!(Format::from_path("a.fastq.gz"), Some(Format::Fastq));
assert_eq!(Format::from_path("a.gz"), None);
assert_eq!(Format::from_path("a"), None);
}
#[test]
fn detects_compression() {
assert_eq!(Compression::from_path("a.fq.gz"), Compression::Gzip);
assert_eq!(Compression::from_path("a.fq"), Compression::None);
assert_eq!(
Compression::from_magic(&[0x1f, 0x8b, 0x08]),
Compression::Gzip
);
assert_eq!(Compression::from_magic(b">seq"), Compression::None);
assert_eq!(Compression::from_magic(&[0x1f]), Compression::None);
}
#[test]
fn detects_zstd() {
assert_eq!(Compression::from_path("a.fq.zst"), Compression::Zstd);
assert_eq!(Compression::from_path("a.fa.zstd"), Compression::Zstd);
assert_eq!(Format::from_path("reads.fq.zst"), Some(Format::Fastq));
assert_eq!(
Compression::from_magic(&[0x28, 0xb5, 0x2f, 0xfd, 0x00]),
Compression::Zstd
);
assert_eq!(
Compression::from_magic(&[0x28, 0xb5, 0x2f]),
Compression::None
);
}
}