1use std::fmt;
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum Format {
9 Fasta,
11 Fastq,
13}
14
15impl Format {
16 pub const FASTA_EXTENSIONS: &'static [&'static str] = &[
18 "fa", "fasta", "fna", "faa", "ffn", "frn", "fas", "mpfa", "seq",
19 ];
20
21 pub const FASTQ_EXTENSIONS: &'static [&'static str] = &["fq", "fastq"];
23
24 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Format> {
33 let path = path.as_ref();
34 let ext = path.extension()?.to_str()?.to_ascii_lowercase();
35 if matches!(ext.as_str(), "gz" | "bgz" | "gzip" | "z" | "zst" | "bz2") {
36 let stem = path.file_stem()?;
37 return Format::from_path(Path::new(stem));
38 }
39 Format::from_extension(&ext)
40 }
41
42 pub fn from_extension(ext: &str) -> Option<Format> {
44 let ext = ext.trim_start_matches('.').to_ascii_lowercase();
45 if Format::FASTA_EXTENSIONS.contains(&ext.as_str()) {
46 Some(Format::Fasta)
47 } else if Format::FASTQ_EXTENSIONS.contains(&ext.as_str()) {
48 Some(Format::Fastq)
49 } else {
50 None
51 }
52 }
53
54 pub fn from_first_byte(byte: u8) -> Option<Format> {
62 match byte {
63 b'>' | b';' => Some(Format::Fasta),
64 b'@' => Some(Format::Fastq),
65 _ => None,
66 }
67 }
68
69 pub const fn header_byte(self) -> u8 {
71 match self {
72 Format::Fasta => b'>',
73 Format::Fastq => b'@',
74 }
75 }
76
77 pub const fn extension(self) -> &'static str {
79 match self {
80 Format::Fasta => "fasta",
81 Format::Fastq => "fastq",
82 }
83 }
84
85 pub const fn has_quality(self) -> bool {
87 matches!(self, Format::Fastq)
88 }
89}
90
91impl fmt::Display for Format {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 match self {
94 Format::Fasta => f.write_str("FASTA"),
95 Format::Fastq => f.write_str("FASTQ"),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
102pub enum Compression {
103 #[default]
105 None,
106 Gzip,
108 Bgzf,
115}
116
117impl Compression {
118 pub const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
120
121 pub fn from_path<P: AsRef<Path>>(path: P) -> Compression {
123 match path
124 .as_ref()
125 .extension()
126 .and_then(|e| e.to_str())
127 .map(|e| e.to_ascii_lowercase())
128 .as_deref()
129 {
130 Some("gz") | Some("bgz") | Some("gzip") => Compression::Gzip,
131 _ => Compression::None,
132 }
133 }
134
135 pub fn from_magic(bytes: &[u8]) -> Compression {
137 if bytes.len() >= 2 && bytes[..2] == Compression::GZIP_MAGIC {
138 Compression::Gzip
139 } else {
140 Compression::None
141 }
142 }
143}
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct CompressionLevel(pub u32);
154
155impl CompressionLevel {
156 pub const NONE: CompressionLevel = CompressionLevel(0);
158 pub const FAST: CompressionLevel = CompressionLevel(1);
160 pub const DEFAULT: CompressionLevel = CompressionLevel(6);
162 pub const BEST: CompressionLevel = CompressionLevel(9);
164}
165
166impl Default for CompressionLevel {
167 fn default() -> Self {
168 CompressionLevel::DEFAULT
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn detects_format_from_extensions() {
178 assert_eq!(Format::from_path("a.fa"), Some(Format::Fasta));
179 assert_eq!(Format::from_path("a.FASTA"), Some(Format::Fasta));
180 assert_eq!(Format::from_path("a.faa"), Some(Format::Fasta));
181 assert_eq!(Format::from_path("/tmp/x/a.fna.gz"), Some(Format::Fasta));
182 assert_eq!(Format::from_path("a.fq"), Some(Format::Fastq));
183 assert_eq!(Format::from_path("a.fastq.gz"), Some(Format::Fastq));
184 assert_eq!(Format::from_path("a.gz"), None);
185 assert_eq!(Format::from_path("a"), None);
186 }
187
188 #[test]
189 fn detects_compression() {
190 assert_eq!(Compression::from_path("a.fq.gz"), Compression::Gzip);
191 assert_eq!(Compression::from_path("a.fq"), Compression::None);
192 assert_eq!(
193 Compression::from_magic(&[0x1f, 0x8b, 0x08]),
194 Compression::Gzip
195 );
196 assert_eq!(Compression::from_magic(b">seq"), Compression::None);
197 assert_eq!(Compression::from_magic(&[0x1f]), Compression::None);
198 }
199}