document_svg/document/
mol2.rs1use std::collections::HashMap;
8use std::path::Path;
9
10use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
11use crate::document::molfile::{Atom, Bond, Molecule, render_molecule};
12use crate::error::{Error, Result};
13
14const MAX_MOL2_INPUT_BYTES: u64 = 64 * 1024 * 1024;
15const MAX_MOL2_LINES: usize = 1_000_000;
16const MAX_MOL2_LINE_BYTES: usize = 1024 * 1024;
17const MAX_MOL2_MOLECULES: usize = 10_000;
18const MAX_MOL2_ATOMS: usize = 100_000;
19const MAX_MOL2_BONDS: usize = 200_000;
20const MAX_MOL2_TOTAL_ATOMS: usize = 500_000;
21
22pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
23 String::from_utf8_lossy(bytes)
24 .lines()
25 .take(32)
26 .any(|line| line.trim().eq_ignore_ascii_case("@<TRIPOS>MOLECULE"))
27}
28
29pub(crate) fn convert(
30 path: &Path,
31 options: &ConvertOptions,
32 sink: &mut dyn PageConsumer,
33) -> Result<Vec<String>> {
34 let bytes = read_limited_file(
35 path,
36 options.max_input_bytes.min(MAX_MOL2_INPUT_BYTES),
37 "MOL2 input",
38 )?;
39 let text = std::str::from_utf8(&bytes).map_err(|error| {
40 Error::InvalidInput(format!("MOL2 input is not UTF-8 or ASCII: {error}"))
41 })?;
42 let (molecules, mut warnings) = parse_molecules(text)?;
43 if molecules.is_empty() {
44 return Err(Error::InvalidInput(
45 "MOL2 contains no MOLECULE sections".into(),
46 ));
47 }
48 if molecules.len() > options.max_pages {
49 return Err(Error::LimitExceeded(format!(
50 "MOL2 contains {} molecules but max_pages is {}",
51 molecules.len(),
52 options.max_pages
53 )));
54 }
55 for (index, molecule) in molecules.iter().enumerate() {
56 let page = render_molecule(molecule, index + 1, "mol2")?;
57 warnings.extend(page.warnings.iter().cloned());
58 sink.consume(page)?;
59 }
60 warnings.sort();
61 warnings.dedup();
62 Ok(warnings)
63}
64
65fn parse_molecules(text: &str) -> Result<(Vec<Molecule>, Vec<String>)> {
66 let lines = text.lines().collect::<Vec<_>>();
67 let mut molecules = Vec::new();
68 let mut warnings = Vec::new();
69 let mut index = 0usize;
70 let mut total_atoms = 0usize;
71 while index < lines.len() {
72 if index >= MAX_MOL2_LINES {
73 return Err(Error::LimitExceeded(format!(
74 "MOL2 exceeds {MAX_MOL2_LINES} lines"
75 )));
76 }
77 if lines[index].len() > MAX_MOL2_LINE_BYTES {
78 return Err(Error::LimitExceeded(format!(
79 "MOL2 line exceeds {MAX_MOL2_LINE_BYTES} bytes"
80 )));
81 }
82 if !lines[index]
83 .trim()
84 .eq_ignore_ascii_case("@<TRIPOS>MOLECULE")
85 {
86 index += 1;
87 continue;
88 }
89 index += 1;
90 let title = lines
91 .get(index)
92 .map(|line| line.trim().to_owned())
93 .unwrap_or_default();
94 index += 1;
95 let counts = lines
96 .get(index)
97 .unwrap_or(&"")
98 .split_whitespace()
99 .collect::<Vec<_>>();
100 let atom_count = counts
101 .first()
102 .and_then(|value| value.parse::<usize>().ok())
103 .unwrap_or(0);
104 let bond_count = counts
105 .get(1)
106 .and_then(|value| value.parse::<usize>().ok())
107 .unwrap_or(0);
108 if atom_count > MAX_MOL2_ATOMS || bond_count > MAX_MOL2_BONDS {
109 return Err(Error::LimitExceeded(format!(
110 "MOL2 molecule exceeds {MAX_MOL2_ATOMS} atoms or {MAX_MOL2_BONDS} bonds"
111 )));
112 }
113 index += 1;
114 while index < lines.len() && !lines[index].trim().eq_ignore_ascii_case("@<TRIPOS>ATOM") {
115 index += 1;
116 }
117 if index >= lines.len() {
118 return Err(Error::InvalidInput(
119 "MOL2 molecule has no ATOM section".into(),
120 ));
121 }
122 index += 1;
123 let mut molecule = Molecule {
124 title,
125 atoms: Vec::with_capacity(atom_count),
126 bonds: Vec::with_capacity(bond_count),
127 warnings: Vec::new(),
128 };
129 let mut id_map = HashMap::new();
130 while index < lines.len() && !lines[index].trim_start().starts_with("@<TRIPOS>") {
131 let fields = lines[index].split_whitespace().collect::<Vec<_>>();
132 if fields.len() >= 6 {
133 let id = fields[0]
134 .parse::<i64>()
135 .map_err(|_| Error::InvalidInput("MOL2 atom id is invalid".into()))?;
136 let x = fields[2]
137 .parse::<f64>()
138 .map_err(|_| Error::InvalidInput("MOL2 atom x is invalid".into()))?;
139 let y = fields[3]
140 .parse::<f64>()
141 .map_err(|_| Error::InvalidInput("MOL2 atom y is invalid".into()))?;
142 let element = clean_element(fields[5]);
143 id_map.insert(id, molecule.atoms.len());
144 molecule.atoms.push(Atom {
145 x,
146 y,
147 element,
148 charge: 0,
149 isotope: None,
150 });
151 }
152 index += 1;
153 }
154 total_atoms = total_atoms.saturating_add(molecule.atoms.len());
155 if total_atoms > MAX_MOL2_TOTAL_ATOMS {
156 return Err(Error::LimitExceeded(format!(
157 "MOL2 exceeds {MAX_MOL2_TOTAL_ATOMS} total atoms"
158 )));
159 }
160 while index < lines.len() && !lines[index].trim().eq_ignore_ascii_case("@<TRIPOS>BOND") {
161 index += 1;
162 }
163 if index < lines.len() {
164 index += 1;
165 while index < lines.len() && !lines[index].trim_start().starts_with("@<TRIPOS>") {
166 let fields = lines[index].split_whitespace().collect::<Vec<_>>();
167 if fields.len() >= 4 {
168 let from = fields[1]
169 .parse::<i64>()
170 .ok()
171 .and_then(|id| id_map.get(&id).copied());
172 let to = fields[2]
173 .parse::<i64>()
174 .ok()
175 .and_then(|id| id_map.get(&id).copied());
176 if let (Some(from), Some(to)) = (from, to)
177 && from != to
178 {
179 molecule.bonds.push(Bond {
180 from,
181 to,
182 order: bond_order(fields[3]),
183 stereo: 0,
184 });
185 }
186 }
187 index += 1;
188 }
189 }
190 if molecule.atoms.is_empty() {
191 warnings.push("MOL2 molecule without valid atoms was omitted".into());
192 } else {
193 if molecules.len() >= MAX_MOL2_MOLECULES {
194 return Err(Error::LimitExceeded(format!(
195 "MOL2 exceeds {MAX_MOL2_MOLECULES} molecules"
196 )));
197 }
198 molecules.push(molecule);
199 }
200 }
201 Ok((molecules, warnings))
202}
203
204fn bond_order(value: &str) -> u8 {
205 match value.to_ascii_lowercase().as_str() {
206 "1" | "un" | "du" | "am" => 1,
207 "2" => 2,
208 "3" => 3,
209 "ar" => 4,
210 _ => 1,
211 }
212}
213
214fn clean_element(value: &str) -> String {
215 let value = value.split('.').next().unwrap_or(value);
216 let mut chars = value
217 .chars()
218 .filter(|character| character.is_ascii_alphabetic());
219 let Some(first) = chars.next() else {
220 return "X".into();
221 };
222 let mut result = first.to_ascii_uppercase().to_string();
223 if let Some(second) = chars.next() {
224 result.push(second.to_ascii_lowercase());
225 }
226 result
227}