document_svg/document/
maf.rs1use 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}