Skip to main content

document_svg/document/
embl.rs

1//! Bounded EMBL-Bank flat-file record preview.
2//!
3//! ID/AC/DE/FT/SQ tags are summarized as inert text. No accession lookup,
4//! feature evaluation, sequence analysis, or external resource access occurs.
5
6use std::path::Path;
7
8use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
9use crate::error::{Error, Result};
10use crate::table::{TableAlign, TableData, convert_table_pages};
11
12const MAX_EMBL_BYTES: u64 = 128 * 1024 * 1024;
13const MAX_EMBL_LINES: usize = 5_000_000;
14const MAX_EMBL_LINE_BYTES: usize = 1 << 20;
15const MAX_EMBL_RECORDS: usize = 100_000;
16const MAX_EMBL_FEATURES: usize = 1_000_000;
17const MAX_EMBL_SEQUENCE_BYTES: usize = 10_000_000;
18const MAX_EMBL_TOTAL_SEQUENCE_BYTES: usize = 64 * 1024 * 1024;
19const MAX_EMBL_FIELD_BYTES: usize = 64 * 1024;
20const MAX_EMBL_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        .find(|line| !line.is_empty())
29        .is_some_and(|line| line.to_ascii_uppercase().starts_with("ID   "))
30}
31
32pub(crate) fn convert(
33    path: &Path,
34    options: &ConvertOptions,
35    sink: &mut dyn PageConsumer,
36) -> Result<Vec<String>> {
37    let bytes = read_limited_file(
38        path,
39        options.max_input_bytes.min(MAX_EMBL_BYTES),
40        "EMBL input",
41    )?;
42    let text = String::from_utf8(bytes)
43        .map_err(|error| Error::InvalidInput(format!("EMBL input must be UTF-8/ASCII: {error}")))?;
44    let (mut table, warnings) = parse_embl(&text)?;
45    let mut page_sink = EmblPageSink {
46        inner: sink,
47        warnings: &warnings,
48    };
49    convert_table_pages(&mut table, "embl", options, &mut page_sink)?;
50    Ok(warnings)
51}
52
53struct EmblPageSink<'a> {
54    inner: &'a mut dyn PageConsumer,
55    warnings: &'a [String],
56}
57
58impl PageConsumer for EmblPageSink<'_> {
59    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
60        page.source_format = "embl".into();
61        page.title = "EMBL-Bank records".into();
62        page.description =
63            "EMBL metadata and sequence text are displayed inertly without external lookup".into();
64        for warning in self.warnings {
65            page.warn(warning.clone());
66        }
67        self.inner.consume(page)
68    }
69}
70
71#[derive(Default)]
72struct Record {
73    id: String,
74    declared_length: String,
75    molecule: String,
76    topology: String,
77    accession: String,
78    description: String,
79    feature_count: usize,
80    sequence: String,
81    in_sequence: bool,
82    last_field: LastField,
83}
84
85#[derive(Clone, Copy, Default, Eq, PartialEq)]
86enum LastField {
87    #[default]
88    None,
89    Accession,
90    Description,
91}
92
93fn parse_embl(text: &str) -> Result<(TableData, Vec<String>)> {
94    if text.len() as u64 > MAX_EMBL_BYTES {
95        return Err(Error::LimitExceeded(format!(
96            "EMBL input exceeds {MAX_EMBL_BYTES} bytes"
97        )));
98    }
99    let lines = text.lines().collect::<Vec<_>>();
100    if lines.len() > MAX_EMBL_LINES {
101        return Err(Error::LimitExceeded(format!(
102            "EMBL input exceeds {MAX_EMBL_LINES} lines"
103        )));
104    }
105    let mut rows = Vec::new();
106    let mut warnings = Vec::new();
107    let mut current = None::<Record>;
108    let mut record_number = 0usize;
109    let mut total_sequence_bytes = 0usize;
110    for (line_number, original) in lines.iter().enumerate() {
111        if original.len() > MAX_EMBL_LINE_BYTES {
112            return Err(Error::LimitExceeded(format!(
113                "EMBL line {} exceeds {MAX_EMBL_LINE_BYTES} bytes",
114                line_number + 1
115            )));
116        }
117        let line = original.trim_end_matches('\r');
118        let trimmed = line.trim();
119        if trimmed.is_empty() {
120            continue;
121        }
122        if line.to_ascii_uppercase().starts_with("ID   ") {
123            if let Some(record) = current.take() {
124                finalize_record(
125                    record,
126                    &mut rows,
127                    &mut warnings,
128                    &mut record_number,
129                    &mut total_sequence_bytes,
130                    true,
131                )?;
132            }
133            current = Some(parse_id(line.strip_prefix("ID   ").unwrap_or_default())?);
134            continue;
135        }
136        let Some(record) = current.as_mut() else {
137            if line.starts_with("XX") || line.starts_with("CC") {
138                warnings.push("EMBL preamble metadata was ignored".into());
139                continue;
140            }
141            return Err(Error::InvalidInput(format!(
142                "EMBL line {} appeared before ID",
143                line_number + 1
144            )));
145        };
146        if trimmed == "//" {
147            let record = current.take().expect("current record exists");
148            finalize_record(
149                record,
150                &mut rows,
151                &mut warnings,
152                &mut record_number,
153                &mut total_sequence_bytes,
154                false,
155            )?;
156            continue;
157        }
158        if let Some(value) = line.strip_prefix("DE   ") {
159            record.description = value.trim().to_owned();
160            validate_field(&record.description, "EMBL description")?;
161            record.last_field = LastField::Description;
162            continue;
163        }
164        if let Some(value) = line.strip_prefix("AC   ") {
165            append_field(&mut record.accession, value, "EMBL accession")?;
166            record.last_field = LastField::Accession;
167            continue;
168        }
169        if let Some(value) = line.strip_prefix("FT   ") {
170            if is_feature_field(value) {
171                record.feature_count = record
172                    .feature_count
173                    .checked_add(1)
174                    .ok_or_else(|| Error::LimitExceeded("EMBL feature count overflowed".into()))?;
175                if record.feature_count > MAX_EMBL_FEATURES {
176                    return Err(Error::LimitExceeded(format!(
177                        "EMBL record exceeds {MAX_EMBL_FEATURES} features"
178                    )));
179                }
180            }
181            continue;
182        }
183        if let Some(value) = line.strip_prefix("SQ   ") {
184            record.in_sequence = true;
185            record.last_field = LastField::None;
186            record.declared_length = sequence_declaration_length(value);
187            continue;
188        }
189        if record.in_sequence {
190            append_sequence(record, trimmed, line_number + 1)?;
191            continue;
192        }
193        if line
194            .as_bytes()
195            .get(..5)
196            .is_some_and(|prefix| prefix.iter().all(|byte| byte.is_ascii_whitespace()))
197        {
198            let continuation = line.get(5..).unwrap_or_default().trim();
199            if continuation.is_empty() {
200                continue;
201            }
202            match record.last_field {
203                LastField::Description => {
204                    record.description.push(' ');
205                    record.description.push_str(continuation);
206                    validate_field(&record.description, "EMBL description")?;
207                }
208                LastField::Accession => {
209                    append_field(&mut record.accession, continuation, "EMBL accession")?
210                }
211                LastField::None => {}
212            }
213        }
214    }
215    if let Some(record) = current.take() {
216        finalize_record(
217            record,
218            &mut rows,
219            &mut warnings,
220            &mut record_number,
221            &mut total_sequence_bytes,
222            true,
223        )?;
224    }
225    if rows.is_empty() {
226        return Err(Error::InvalidInput("EMBL input contains no records".into()));
227    }
228    let headers = [
229        "Record",
230        "ID",
231        "Declared length",
232        "Molecule",
233        "Topology",
234        "Accession",
235        "Description",
236        "Features",
237        "Sequence length",
238        "SQ preview",
239    ]
240    .into_iter()
241    .map(str::to_owned)
242    .collect();
243    let alignments = vec![
244        TableAlign::Right,
245        TableAlign::Left,
246        TableAlign::Right,
247        TableAlign::Left,
248        TableAlign::Left,
249        TableAlign::Left,
250        TableAlign::Left,
251        TableAlign::Right,
252        TableAlign::Right,
253        TableAlign::Left,
254    ];
255    Ok((
256        TableData {
257            headers,
258            rows,
259            alignments,
260            raw_source: String::new(),
261        },
262        dedup_warnings(warnings),
263    ))
264}
265
266fn parse_id(value: &str) -> Result<Record> {
267    let parts = value.split(';').map(str::trim).collect::<Vec<_>>();
268    let id = parts.first().copied().unwrap_or_default();
269    if id.is_empty() {
270        return Err(Error::InvalidInput("EMBL ID line has no identifier".into()));
271    }
272    let mut record = Record {
273        id: id.to_owned(),
274        ..Record::default()
275    };
276    for part in parts.iter().skip(1) {
277        let upper = part.to_ascii_uppercase();
278        if upper == "LINEAR" || upper == "CIRCULAR" {
279            record.topology = (*part).to_owned();
280        }
281        if upper.contains("DNA")
282            || upper.contains("RNA")
283            || upper.contains("PROTEIN")
284            || upper.contains("PEPTIDE")
285        {
286            record.molecule = (*part).to_owned();
287        }
288        if upper.ends_with(" BP") || upper.ends_with(" AA") {
289            record.declared_length = (*part).to_owned();
290        }
291    }
292    validate_field(&record.id, "EMBL identifier")?;
293    Ok(record)
294}
295
296fn sequence_declaration_length(value: &str) -> String {
297    value
298        .split(';')
299        .map(str::trim)
300        .find(|part| {
301            let upper = part.to_ascii_uppercase();
302            upper.starts_with("SEQUENCE ") && (upper.ends_with(" BP") || upper.ends_with(" AA"))
303        })
304        .unwrap_or_default()
305        .to_owned()
306}
307
308fn append_sequence(record: &mut Record, line: &str, line_number: usize) -> Result<()> {
309    let mut fields = line.split_ascii_whitespace().collect::<Vec<_>>();
310    let count = fields
311        .pop()
312        .ok_or_else(|| Error::InvalidInput(format!("EMBL SQ line {line_number} is empty")))?;
313    count.parse::<u64>().map_err(|_| {
314        Error::InvalidInput(format!(
315            "EMBL SQ line {line_number} has an invalid cumulative count"
316        ))
317    })?;
318    if fields
319        .first()
320        .is_some_and(|value| value.parse::<u64>().is_ok())
321    {
322        fields.remove(0);
323    }
324    let sequence = fields.into_iter().collect::<String>();
325    if sequence
326        .bytes()
327        .any(|byte| !byte.is_ascii_alphabetic() && byte != b'-' && byte != b'*')
328    {
329        return Err(Error::InvalidInput(format!(
330            "EMBL SQ line {line_number} contains a non-sequence character"
331        )));
332    }
333    record.sequence.push_str(&sequence);
334    if record.sequence.len() > MAX_EMBL_SEQUENCE_BYTES {
335        return Err(Error::LimitExceeded(format!(
336            "EMBL SQ sequence exceeds {MAX_EMBL_SEQUENCE_BYTES} bytes"
337        )));
338    }
339    Ok(())
340}
341
342fn finalize_record(
343    record: Record,
344    rows: &mut Vec<Vec<String>>,
345    warnings: &mut Vec<String>,
346    record_number: &mut usize,
347    total_sequence_bytes: &mut usize,
348    implicit_separator: bool,
349) -> Result<()> {
350    *record_number = (*record_number)
351        .checked_add(1)
352        .ok_or_else(|| Error::LimitExceeded("EMBL record count overflowed".into()))?;
353    if *record_number > MAX_EMBL_RECORDS {
354        return Err(Error::LimitExceeded(format!(
355            "EMBL exceeds {MAX_EMBL_RECORDS} records"
356        )));
357    }
358    if implicit_separator {
359        warnings.push(format!(
360            "EMBL record {} was not terminated by //; the next ID or EOF finalized it",
361            *record_number
362        ));
363    }
364    if record.sequence.is_empty() {
365        warnings.push(format!(
366            "EMBL record {} has no SQ sequence preview",
367            *record_number
368        ));
369    } else if let Some(declared) = record
370        .declared_length
371        .split_ascii_whitespace()
372        .find_map(|value| value.parse::<usize>().ok())
373        && declared != record.sequence.len()
374    {
375        warnings.push(format!(
376            "EMBL record {} SQ length {} differs from declared length {}",
377            *record_number,
378            record.sequence.len(),
379            declared
380        ));
381    }
382    *total_sequence_bytes = (*total_sequence_bytes)
383        .checked_add(record.sequence.len())
384        .ok_or_else(|| Error::LimitExceeded("EMBL total sequence byte count overflowed".into()))?;
385    if *total_sequence_bytes > MAX_EMBL_TOTAL_SEQUENCE_BYTES {
386        return Err(Error::LimitExceeded(format!(
387            "EMBL sequences exceed {MAX_EMBL_TOTAL_SEQUENCE_BYTES} bytes"
388        )));
389    }
390    let preview = if record.sequence.len() > MAX_EMBL_PREVIEW {
391        format!("{}…", &record.sequence[..MAX_EMBL_PREVIEW])
392    } else {
393        record.sequence.clone()
394    };
395    rows.push(vec![
396        record_number.to_string(),
397        record.id,
398        record.declared_length,
399        record.molecule,
400        record.topology,
401        record.accession,
402        record.description,
403        record.feature_count.to_string(),
404        record.sequence.len().to_string(),
405        preview,
406    ]);
407    Ok(())
408}
409
410fn is_feature_field(value: &str) -> bool {
411    value
412        .get(..16)
413        .is_some_and(|field| field.chars().any(|character| !character.is_whitespace()))
414}
415
416fn append_field(target: &mut String, value: &str, context: &str) -> Result<()> {
417    let value = value.trim();
418    if !value.is_empty() {
419        if !target.is_empty() {
420            target.push(' ');
421        }
422        target.push_str(value);
423    }
424    validate_field(target, context)
425}
426
427fn validate_field(value: &str, context: &str) -> Result<()> {
428    if value.len() > MAX_EMBL_FIELD_BYTES {
429        return Err(Error::LimitExceeded(format!(
430            "{context} exceeds {MAX_EMBL_FIELD_BYTES} bytes"
431        )));
432    }
433    if value.chars().any(|character| character.is_control()) {
434        return Err(Error::InvalidInput(format!(
435            "{context} contains a control character"
436        )));
437    }
438    Ok(())
439}
440
441fn dedup_warnings(warnings: Vec<String>) -> Vec<String> {
442    let mut seen = std::collections::HashSet::new();
443    warnings
444        .into_iter()
445        .filter(|warning| seen.insert(warning.clone()))
446        .collect()
447}