1use crate::document::{CifValue, DataBlock, Document};
4use crate::lexer::Quoting;
5use molframe_core::ByteSpan;
6use molframe_core::io::ReadOptions;
7use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
8use quick_xml::{Reader, Writer};
9
10#[derive(Debug, thiserror::Error)]
12#[non_exhaustive]
13pub enum PdbmlError {
14 #[error("invalid PDBML/XML: {0}")]
16 Xml(#[from] quick_xml::Error),
17 #[error("could not write PDBML/XML: {0}")]
19 Io(#[from] std::io::Error),
20 #[error("PDBML/XML has no datablock")]
22 MissingDatablock,
23 #[error("invalid PDBML category or row nesting")]
25 InvalidNesting,
26 #[error("PDBML writer produced invalid UTF-8")]
28 InvalidUtf8,
29 #[error("unsupported PDBML XML entity reference")]
31 InvalidEntity,
32}
33
34#[derive(Debug)]
36#[non_exhaustive]
37pub enum PdbmlReadError {
38 Pdbml(PdbmlError),
40 Findings(Vec<molframe_core::Diagnostic>),
42}
43
44impl std::fmt::Display for PdbmlReadError {
45 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Pdbml(error) => error.fmt(formatter),
48 Self::Findings(findings) => {
49 write!(
50 formatter,
51 "PDBML semantic lowering failed with {} findings",
52 findings.len()
53 )
54 }
55 }
56 }
57}
58impl std::error::Error for PdbmlReadError {}
59
60pub fn parse_pdbml_document(bytes: &[u8]) -> Result<Document, PdbmlError> {
68 let mut reader = Reader::from_reader(bytes);
69 reader.config_mut().trim_text(false);
70 let mut document = Document::new();
71 let mut block = None;
72 let mut category: Option<String> = None;
73 let mut row: Option<Row> = None;
74 let mut item: Option<Item> = None;
75 loop {
76 match reader.read_event()? {
77 Event::Start(element) => {
78 let name = local_name(element.name().as_ref()).to_owned();
79 if name == "datablock" {
80 block = Some(DataBlock::new(datablock_name(&element, &reader)?));
81 } else if let Some(value) = name.strip_suffix("Category") {
82 if category.is_some() || row.is_some() {
83 return Err(PdbmlError::InvalidNesting);
84 }
85 category = Some(value.to_owned());
86 } else if category.as_deref() == Some(&name) {
87 if row.is_some() {
88 return Err(PdbmlError::InvalidNesting);
89 }
90 row = Some(Row::from_attributes(&element, &reader)?);
91 } else if row.is_some() {
92 item = Some(Item {
93 name,
94 text: String::new(),
95 nil: nil_attribute(&element, &reader)?,
96 });
97 }
98 }
99 Event::Empty(element) if row.is_some() => {
100 let name = local_name(element.name().as_ref()).to_owned();
101 let value = if nil_attribute(&element, &reader)? {
102 CifValue::Unknown
103 } else {
104 CifValue::Text("".into())
105 };
106 if let Some(active) = &mut row {
107 active.values.push((name, value));
108 }
109 }
110 Event::Text(text) => {
111 if let Some(active) = &mut item {
112 active
113 .text
114 .push_str(&text.decode().map_err(|_| PdbmlError::InvalidUtf8)?);
115 }
116 }
117 Event::CData(text) => {
118 if let Some(active) = &mut item {
119 active
120 .text
121 .push_str(&text.decode().map_err(|_| PdbmlError::InvalidUtf8)?);
122 }
123 }
124 Event::GeneralRef(reference) => {
125 if let Some(active) = &mut item {
126 let reference = reference.decode().map_err(|_| PdbmlError::InvalidUtf8)?;
127 active.text.push_str(&resolve_reference(&reference)?);
128 }
129 }
130 Event::End(element) => {
131 let name = local_name(element.name().as_ref()).to_owned();
132 if item.as_ref().is_some_and(|active| active.name == name) {
133 let active = item.take().ok_or(PdbmlError::InvalidNesting)?;
134 let value = if active.nil {
135 CifValue::Unknown
136 } else {
137 CifValue::parse(active.text.trim(), Quoting::Bare)
138 };
139 row.as_mut()
140 .ok_or(PdbmlError::InvalidNesting)?
141 .values
142 .push((active.name, value));
143 } else if category.as_deref() == Some(&name) {
144 let active = row.take().ok_or(PdbmlError::InvalidNesting)?;
145 let block = block.as_mut().ok_or(PdbmlError::MissingDatablock)?;
146 append_row(block, &name, active);
147 } else if name.ends_with("Category") {
148 category = None;
149 } else if name == "datablock" {
150 document.push(block.take().ok_or(PdbmlError::MissingDatablock)?);
151 }
152 }
153 Event::Eof => break,
154 _ => {}
155 }
156 }
157 if document.is_empty() {
158 return Err(PdbmlError::MissingDatablock);
159 }
160 Ok(document)
161}
162
163pub fn read_pdbml(
169 bytes: &[u8],
170 options: &ReadOptions,
171) -> Result<
172 (
173 Document,
174 molframe_core::Structure,
175 Vec<molframe_core::Diagnostic>,
176 ),
177 PdbmlReadError,
178> {
179 let document = parse_pdbml_document(bytes).map_err(PdbmlReadError::Pdbml)?;
180 let (structure, findings) =
181 crate::lower(&document, options).map_err(PdbmlReadError::Findings)?;
182 options
183 .finish(structure, findings)
184 .map(|(structure, findings)| (document, structure, findings))
185 .map_err(PdbmlReadError::Findings)
186}
187
188pub fn write_pdbml(document: &Document) -> Result<String, PdbmlError> {
198 if document.len() != 1 {
199 return Err(PdbmlError::MissingDatablock);
200 }
201 let block = document.first_block().ok_or(PdbmlError::MissingDatablock)?;
202 let mut writer = Writer::new_with_indent(Vec::new(), b' ', 3);
203 writer.write_event(Event::Decl(BytesDecl::new("1.0", Some("UTF-8"), None)))?;
204 let mut root = BytesStart::new("PDBx:datablock");
205 root.push_attribute(("datablockName", block.name()));
206 root.push_attribute(("xmlns:PDBx", "http://pdbml.pdb.org/schema/pdbx-v50.xsd"));
207 root.push_attribute(("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance"));
208 writer.write_event(Event::Start(root))?;
209 for category in block.categories() {
210 let category_name = format!("PDBx:{}Category", category.name());
211 writer.write_event(Event::Start(BytesStart::new(&category_name)))?;
212 for row in 0..category.row_count() {
213 let row_name = format!("PDBx:{}", category.name());
214 writer.write_event(Event::Start(BytesStart::new(&row_name)))?;
215 for item_name in category.items() {
216 let value = match category.value(item_name, row) {
217 Some(value) => value,
218 None => &CifValue::Unknown,
219 };
220 write_item(&mut writer, item_name, value)?;
221 }
222 writer.write_event(Event::End(BytesEnd::new(&row_name)))?;
223 }
224 writer.write_event(Event::End(BytesEnd::new(&category_name)))?;
225 }
226 writer.write_event(Event::End(BytesEnd::new("PDBx:datablock")))?;
227 String::from_utf8(writer.into_inner()).map_err(|_| PdbmlError::InvalidUtf8)
228}
229
230struct Row {
231 values: Vec<(String, CifValue)>,
232}
233impl Row {
234 fn from_attributes(
235 element: &BytesStart<'_>,
236 reader: &Reader<&[u8]>,
237 ) -> Result<Self, PdbmlError> {
238 let mut values = Vec::new();
239 for attribute in element.attributes().with_checks(false) {
240 let attribute = attribute.map_err(quick_xml::Error::from)?;
241 let name = local_name(attribute.key.as_ref());
242 if name == "nil" || name == "schemaLocation" || name.starts_with("xmlns") {
243 continue;
244 }
245 let text = attribute.decoded_and_normalized_value(
246 quick_xml::XmlVersion::Implicit1_0,
247 reader.decoder(),
248 )?;
249 values.push((name.to_owned(), CifValue::parse(&text, Quoting::Bare)));
250 }
251 Ok(Self { values })
252 }
253}
254struct Item {
255 name: String,
256 text: String,
257 nil: bool,
258}
259
260fn datablock_name(element: &BytesStart<'_>, reader: &Reader<&[u8]>) -> Result<String, PdbmlError> {
261 let _ = reader;
262
263 for attribute in element.attributes().with_checks(false) {
264 let attribute = attribute.map_err(quick_xml::Error::from)?;
265
266 if local_name(attribute.key.as_ref()) == "datablockName" {
267 return Ok(attribute
268 .normalized_value(quick_xml::XmlVersion::Implicit1_0)?
269 .into_owned());
270 }
271 }
272
273 Err(PdbmlError::MissingDatablock)
274}
275
276fn nil_attribute(element: &BytesStart<'_>, reader: &Reader<&[u8]>) -> Result<bool, PdbmlError> {
277 let _ = reader;
278
279 for attribute in element.attributes().with_checks(false) {
280 let attribute = attribute.map_err(quick_xml::Error::from)?;
281
282 if local_name(attribute.key.as_ref()) == "nil" {
283 return Ok(attribute
284 .normalized_value(quick_xml::XmlVersion::Implicit1_0)?
285 .eq_ignore_ascii_case("true"));
286 }
287 }
288
289 Ok(false)
290}
291
292fn local_name(name: &[u8]) -> &str {
293 let local = name
294 .iter()
295 .rposition(|byte| *byte == b':')
296 .map_or(name, |position| &name[position + 1..]);
297 std::str::from_utf8(local).map_or("", |value| value)
298}
299fn resolve_reference(reference: &str) -> Result<String, PdbmlError> {
300 if let Some(value) = quick_xml::escape::resolve_predefined_entity(reference) {
301 return Ok(value.to_owned());
302 }
303 let number = if let Some(hexadecimal) = reference.strip_prefix("#x") {
304 u32::from_str_radix(hexadecimal, 16).ok()
305 } else {
306 reference
307 .strip_prefix('#')
308 .and_then(|value| value.parse().ok())
309 };
310 number
311 .and_then(char::from_u32)
312 .map(|value| value.to_string())
313 .ok_or(PdbmlError::InvalidEntity)
314}
315fn append_row(block: &mut DataBlock, category_name: &str, row: Row) {
316 let category = block.category_mut(category_name, ByteSpan::default());
317 let row_index = category.row_count();
318 let existing: Vec<_> = category.items().map(str::to_owned).collect();
319 for item in existing {
320 let value = row
321 .values
322 .iter()
323 .find(|(name, _)| name == &item)
324 .map_or(CifValue::Unknown, |(_, value)| value.clone());
325 category.column_mut(&item).push(value, Quoting::Bare);
326 }
327 for (item, value) in row.values {
328 if category.column(&item).is_some() {
329 continue;
330 }
331 let column = category.column_mut(&item);
332 for _ in 0..row_index {
333 column.push(CifValue::Unknown, Quoting::Bare);
334 }
335 column.push(value, Quoting::Bare);
336 }
337}
338fn write_item(
339 writer: &mut Writer<Vec<u8>>,
340 item_name: &str,
341 value: &CifValue,
342) -> Result<(), PdbmlError> {
343 let name = format!("PDBx:{item_name}");
344 match value {
345 CifValue::Unknown | CifValue::Inapplicable => {
346 let mut element = BytesStart::new(&name);
347 element.push_attribute(("xsi:nil", "true"));
348 writer.write_event(Event::Empty(element))?;
349 }
350 CifValue::Text(text) => write_text_element(writer, &name, text)?,
351 CifValue::Integer(value) => write_text_element(writer, &name, &value.to_string())?,
352 CifValue::Float(value) => write_text_element(writer, &name, &value.to_string())?,
353 }
354 Ok(())
355}
356fn write_text_element(
357 writer: &mut Writer<Vec<u8>>,
358 name: &str,
359 text: &str,
360) -> Result<(), PdbmlError> {
361 writer.write_event(Event::Start(BytesStart::new(name)))?;
362 writer.write_event(Event::Text(BytesText::new(text)))?;
363 writer.write_event(Event::End(BytesEnd::new(name)))?;
364 Ok(())
365}
366
367#[cfg(test)]
368#[path = "codec_tests.rs"]
369mod tests;