1use 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}