Skip to main content

document_svg/document/
maf.rs

1//! Bounded UCSC Multiple Alignment Format (MAF) preview.
2//!
3//! Alignment blocks and `s` sequence rows are shown as inert tabular records.
4//! Optional `i`, `e`, `q` and `score` metadata are retained only as text; no
5//! reference genome, coordinate lift, or alignment calculation is performed.
6
7use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::error::{Error, Result};
11use crate::table::{TableAlign, TableData, convert_table_pages};
12
13const MAX_MAF_BYTES: u64 = 128 * 1024 * 1024;
14const MAX_MAF_LINES: usize = 2_000_000;
15const MAX_MAF_LINE_BYTES: usize = 1 << 20;
16const MAX_MAF_BLOCKS: usize = 100_000;
17const MAX_MAF_SEQUENCE_ROWS: usize = 500_000;
18const MAX_MAF_SEQUENCE_BYTES: usize = 10_000_000;
19const MAX_MAF_TOTAL_SEQUENCE_BYTES: usize = 64 * 1024 * 1024;
20const MAX_MAF_PREVIEW: usize = 512;
21
22pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
23    let Ok(text) = std::str::from_utf8(prefix) else {
24        return false;
25    };
26    text.lines()
27        .map(str::trim)
28        .any(|line| line.to_ascii_lowercase().starts_with("##maf"))
29}
30
31pub(crate) fn convert(
32    path: &Path,
33    options: &ConvertOptions,
34    sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36    let bytes = read_limited_file(
37        path,
38        options.max_input_bytes.min(MAX_MAF_BYTES),
39        "MAF input",
40    )?;
41    let text = String::from_utf8(bytes)
42        .map_err(|error| Error::InvalidInput(format!("MAF input must be UTF-8/ASCII: {error}")))?;
43    let (mut table, warnings) = parse_maf(&text)?;
44    let mut page_sink = MafPageSink {
45        inner: sink,
46        warnings: &warnings,
47    };
48    convert_table_pages(&mut table, "maf", options, &mut page_sink)?;
49    Ok(warnings)
50}
51
52struct MafPageSink<'a> {
53    inner: &'a mut dyn PageConsumer,
54    warnings: &'a [String],
55}
56impl PageConsumer for MafPageSink<'_> {
57    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
58        page.source_format = "maf".into();
59        page.title = "MAF multiple alignments".into();
60        page.description =
61            "MAF alignment sequence rows are displayed inertly without reference lookup".into();
62        for warning in self.warnings {
63            page.warn(warning.clone());
64        }
65        self.inner.consume(page)
66    }
67}
68
69fn parse_maf(text: &str) -> Result<(TableData, Vec<String>)> {
70    if text.len() as u64 > MAX_MAF_BYTES {
71        return Err(Error::LimitExceeded(format!(
72            "MAF input exceeds {MAX_MAF_BYTES} bytes"
73        )));
74    }
75    let lines = text.lines().collect::<Vec<_>>();
76    if lines.len() > MAX_MAF_LINES {
77        return Err(Error::LimitExceeded(format!(
78            "MAF input exceeds {MAX_MAF_LINES} lines"
79        )));
80    }
81    let mut rows = Vec::new();
82    let mut warnings = Vec::new();
83    let mut block = 0usize;
84    let mut current_score = String::new();
85    let mut in_block = false;
86    let mut metadata = false;
87    let mut total_sequence_bytes = 0usize;
88    for (line_number, original) in lines.iter().enumerate() {
89        if original.len() > MAX_MAF_LINE_BYTES {
90            return Err(Error::LimitExceeded(format!(
91                "MAF line {} exceeds {MAX_MAF_LINE_BYTES} bytes",
92                line_number + 1
93            )));
94        }
95        let line = original.trim_end_matches('\r');
96        let trimmed = line.trim();
97        if trimmed.is_empty() {
98            in_block = false;
99            current_score.clear();
100            continue;
101        }
102        if trimmed.starts_with("##") || trimmed.starts_with('#') || trimmed.starts_with("track ") {
103            metadata = true;
104            continue;
105        }
106        let fields = trimmed.split_ascii_whitespace().collect::<Vec<_>>();
107        let kind = fields.first().copied().unwrap_or_default();
108        match kind {
109            "a" => {
110                if in_block {
111                    warnings.push("MAF block began before the previous block separator; rows were kept in source order".into());
112                }
113                block += 1;
114                if block > MAX_MAF_BLOCKS {
115                    return Err(Error::LimitExceeded(format!(
116                        "MAF exceeds {MAX_MAF_BLOCKS} alignment blocks"
117                    )));
118                }
119                current_score = fields
120                    .iter()
121                    .find_map(|field| field.strip_prefix("score="))
122                    .unwrap_or_default()
123                    .to_owned();
124                in_block = true;
125            }
126            "s" => {
127                if !in_block || fields.len() != 7 {
128                    return Err(Error::InvalidInput(format!(
129                        "MAF sequence line {} must contain seven fields inside an alignment block",
130                        line_number + 1
131                    )));
132                }
133                if rows.len() >= MAX_MAF_SEQUENCE_ROWS {
134                    return Err(Error::LimitExceeded(format!(
135                        "MAF exceeds {MAX_MAF_SEQUENCE_ROWS} sequence rows"
136                    )));
137                }
138                let start = fields[2].parse::<u64>().map_err(|_| {
139                    Error::InvalidInput(format!("MAF line {} start is invalid", line_number + 1))
140                })?;
141                let size = fields[3].parse::<u64>().map_err(|_| {
142                    Error::InvalidInput(format!("MAF line {} size is invalid", line_number + 1))
143                })?;
144                let source_size = fields[5].parse::<u64>().map_err(|_| {
145                    Error::InvalidInput(format!(
146                        "MAF line {} source size is invalid",
147                        line_number + 1
148                    ))
149                })?;
150                if fields[4] != "+" && fields[4] != "-" {
151                    return Err(Error::InvalidInput(format!(
152                        "MAF line {} strand is invalid",
153                        line_number + 1
154                    )));
155                }
156                let sequence = fields[6];
157                if sequence.len() > MAX_MAF_SEQUENCE_BYTES {
158                    return Err(Error::LimitExceeded(format!(
159                        "MAF line {} sequence exceeds {MAX_MAF_SEQUENCE_BYTES} bytes",
160                        line_number + 1
161                    )));
162                }
163                if sequence
164                    .bytes()
165                    .any(|byte| !byte.is_ascii_alphabetic() && byte != b'-' && byte != b'.')
166                {
167                    return Err(Error::InvalidInput(format!(
168                        "MAF line {} sequence contains an invalid character",
169                        line_number + 1
170                    )));
171                }
172                let nongap = sequence.bytes().filter(|byte| *byte != b'-').count() as u64;
173                if nongap != size {
174                    warnings.push(format!(
175                        "MAF line {} ungapped sequence length differs from declared size",
176                        line_number + 1
177                    ));
178                }
179                total_sequence_bytes = total_sequence_bytes
180                    .checked_add(sequence.len())
181                    .ok_or_else(|| {
182                        Error::LimitExceeded("MAF sequence byte count overflowed".into())
183                    })?;
184                if total_sequence_bytes > MAX_MAF_TOTAL_SEQUENCE_BYTES {
185                    return Err(Error::LimitExceeded(format!(
186                        "MAF sequence data exceeds {MAX_MAF_TOTAL_SEQUENCE_BYTES} bytes"
187                    )));
188                }
189                let preview = if sequence.len() > MAX_MAF_PREVIEW {
190                    format!("{}…", &sequence[..MAX_MAF_PREVIEW])
191                } else {
192                    sequence.to_owned()
193                };
194                rows.push(vec![
195                    block.to_string(),
196                    fields[1].to_owned(),
197                    start.to_string(),
198                    size.to_string(),
199                    fields[4].to_owned(),
200                    source_size.to_string(),
201                    current_score.clone(),
202                    preview,
203                ]);
204            }
205            "i" | "e" | "q" => {
206                metadata = true;
207            }
208            _ => {
209                metadata = true;
210            }
211        }
212    }
213    if rows.is_empty() {
214        return Err(Error::InvalidInput("MAF contains no sequence rows".into()));
215    }
216    if metadata {
217        warnings.push(
218            "MAF comments and optional block/quality metadata were ignored or kept inert".into(),
219        );
220    }
221    let headers = [
222        "Block",
223        "Source",
224        "Start",
225        "Size",
226        "Strand",
227        "Source size",
228        "Score",
229        "Sequence",
230    ]
231    .into_iter()
232    .map(str::to_owned)
233    .collect();
234    let alignments = vec![
235        TableAlign::Right,
236        TableAlign::Left,
237        TableAlign::Right,
238        TableAlign::Right,
239        TableAlign::Center,
240        TableAlign::Right,
241        TableAlign::Right,
242        TableAlign::Left,
243    ];
244    Ok((
245        TableData {
246            headers,
247            rows,
248            alignments,
249            raw_source: String::new(),
250        },
251        dedup_warnings(warnings),
252    ))
253}
254
255fn dedup_warnings(warnings: Vec<String>) -> Vec<String> {
256    let mut seen = std::collections::HashSet::new();
257    warnings
258        .into_iter()
259        .filter(|warning| seen.insert(warning.clone()))
260        .collect()
261}