Skip to main content

phasesmith_io/cif/
mod.rs

1//! Bounded native CIF import into parser-independent crystallographic records.
2
3mod import;
4mod syntax;
5
6use std::collections::BTreeMap;
7use std::error::Error;
8use std::fmt::{Display, Formatter};
9use std::path::PathBuf;
10
11use phasesmith_crystallography::{CellError, SpaceGroup, SymmetryError, UnitCell};
12
13use crate::SpaceGroupLookupError;
14
15pub use import::{parse_cif_text, read_cif_file};
16
17/// Native CIF parser implementation identifier.
18pub const NATIVE_CIF_BACKEND: &str = "phasesmith-native";
19/// Version of the native CIF adapter contract.
20pub const NATIVE_CIF_BACKEND_VERSION: &str = env!("CARGO_PKG_VERSION");
21
22/// Resource limits checked before and during CIF parsing.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct CifReadLimits {
25    /// Maximum UTF-8 byte count.
26    pub max_bytes: usize,
27    /// Maximum number of data blocks.
28    pub max_blocks: usize,
29    /// Maximum row count of any loop.
30    pub max_loop_rows: usize,
31    /// Maximum atom or anisotropic-site rows.
32    pub max_atom_sites: usize,
33}
34
35impl Default for CifReadLimits {
36    fn default() -> Self {
37        Self {
38            max_bytes: 16 * 1024 * 1024,
39            max_blocks: 100,
40            max_loop_rows: 1_000_000,
41            max_atom_sites: 100_000,
42        }
43    }
44}
45
46impl CifReadLimits {
47    /// Validate that every limit is positive.
48    ///
49    /// # Errors
50    ///
51    /// Returns [`CifIoError::InvalidLimits`] when any limit is zero.
52    pub fn validate(self) -> Result<(), CifIoError> {
53        if self.max_bytes == 0
54            || self.max_blocks == 0
55            || self.max_loop_rows == 0
56            || self.max_atom_sites == 0
57        {
58            return Err(CifIoError::InvalidLimits);
59        }
60        Ok(())
61    }
62}
63
64/// Stable diagnostic severity.
65#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum CifDiagnosticSeverity {
67    /// Recoverable condition visible to the caller.
68    Warning,
69    /// Non-recoverable condition retained in a partial record.
70    Error,
71}
72
73/// One stable CIF import diagnostic.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct CifDiagnostic {
76    /// Warning or error severity.
77    pub severity: CifDiagnosticSeverity,
78    /// Stable machine-readable code.
79    pub code: String,
80    /// Human-readable explanation.
81    pub message: String,
82    /// Related CIF tag when applicable.
83    pub tag: Option<String>,
84    /// Zero-based loop row when applicable.
85    pub row: Option<usize>,
86}
87
88impl CifDiagnostic {
89    fn warning(code: impl Into<String>, message: impl Into<String>) -> Self {
90        Self {
91            severity: CifDiagnosticSeverity::Warning,
92            code: code.into(),
93            message: message.into(),
94            tag: None,
95            row: None,
96        }
97    }
98
99    fn with_tag(mut self, tag: impl Into<String>) -> Self {
100        self.tag = Some(tag.into());
101        self
102    }
103
104    fn with_row(mut self, row: usize) -> Self {
105        self.row = Some(row);
106        self
107    }
108}
109
110/// Source provenance retained after CIF parsing.
111#[derive(Clone, Debug, PartialEq, Eq)]
112pub struct CifStructureSource {
113    /// Source format, currently `CIF`.
114    pub format: String,
115    /// Selected data-block name.
116    pub block_name: String,
117    /// Parser backend identifier.
118    pub backend: String,
119    /// Parser backend version.
120    pub backend_version: String,
121    /// Source path when read from a file.
122    pub source_path: Option<PathBuf>,
123}
124
125/// Original CIF displacement convention.
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum DisplacementConvention {
128    /// CIF `U_ij` components.
129    CifU,
130    /// CIF `B_ij` components converted to `U_ij`.
131    CifB,
132}
133
134/// Fixed anisotropic displacement attached to one site.
135#[derive(Clone, Debug, PartialEq)]
136pub struct CifAnisotropicDisplacement {
137    /// CIF U tensor in component order `11,22,33,23,13,12`.
138    pub u_cif_angstrom2: [f64; 6],
139    /// Convention present in the source file.
140    pub source_convention: DisplacementConvention,
141    /// Optional standard uncertainties in the same component order.
142    pub standard_uncertainty: [Option<f64>; 6],
143}
144
145/// One independent atom site imported from CIF.
146#[derive(Clone, Debug, PartialEq)]
147pub struct CifAtomSite {
148    /// Unique stable site identifier within the structure.
149    pub site_id: String,
150    /// Original CIF label before permissive duplicate renaming.
151    pub source_label: String,
152    /// Original atom type symbol.
153    pub type_symbol: String,
154    /// Parsed element symbol.
155    pub element_symbol: String,
156    /// Fractional coordinates in the selected cell.
157    pub fractional_xyz: [f64; 3],
158    /// Site occupancy.
159    pub occupancy: f64,
160    /// Optional isotropic U in square ångströms.
161    pub u_iso_angstrom2: Option<f64>,
162    /// Optional fixed anisotropic CIF U tensor.
163    pub anisotropic_displacement: Option<CifAnisotropicDisplacement>,
164    /// Parsed formal charge.
165    pub charge: Option<i32>,
166    /// Parsed isotope mass number.
167    pub isotope: Option<u32>,
168    /// Optional disorder group.
169    pub disorder_group: Option<String>,
170    /// Optional fractional-coordinate standard uncertainties.
171    pub fractional_xyz_standard_uncertainty: [Option<f64>; 3],
172    /// Optional occupancy standard uncertainty.
173    pub occupancy_standard_uncertainty: Option<f64>,
174    /// Optional isotropic-U standard uncertainty.
175    pub u_iso_standard_uncertainty: Option<f64>,
176}
177
178/// Parser-independent native crystallographic structure.
179#[derive(Clone, Debug, PartialEq)]
180pub struct CifStructure {
181    /// Stable ID derived from the selected block name.
182    pub structure_id: String,
183    /// Human-readable chemical/phase name.
184    pub name: String,
185    /// Validated direct unit cell.
186    pub cell: UnitCell,
187    /// Exact validated conventional symmetry operations.
188    pub space_group: SpaceGroup,
189    /// Independent atom sites in source order.
190    pub sites: Vec<CifAtomSite>,
191    /// Source provenance.
192    pub source: CifStructureSource,
193    /// Optional standard uncertainties for `a,b,c,alpha,beta,gamma`.
194    pub cell_standard_uncertainties: [Option<f64>; 6],
195    /// Import diagnostics also returned at the top level.
196    pub diagnostics: Vec<CifDiagnostic>,
197    /// Small textual metadata extracted from CIF.
198    pub metadata: BTreeMap<String, String>,
199}
200
201/// One selected CIF structure plus block-selection context.
202#[derive(Clone, Debug, PartialEq)]
203pub struct CifReadResult {
204    /// Imported native structure.
205    pub structure: CifStructure,
206    /// Visible import diagnostics.
207    pub diagnostics: Vec<CifDiagnostic>,
208    /// Selected display block name.
209    pub selected_block: String,
210    /// All display block names in source order.
211    pub available_blocks: Vec<String>,
212}
213
214/// Native CIF syntax, limit, lookup, or domain failure.
215#[derive(Debug)]
216pub enum CifIoError {
217    /// One or more configured limits are zero.
218    InvalidLimits,
219    /// UTF-8 input exceeds the configured byte limit.
220    ByteLimitExceeded {
221        /// Observed byte count.
222        actual: u64,
223        /// Configured maximum.
224        maximum: usize,
225    },
226    /// Filesystem or UTF-8 reading failed.
227    Io(std::io::Error),
228    /// CIF tokenization or document structure is invalid.
229    Syntax {
230        /// Stable explanation.
231        message: String,
232        /// One-based source line when known.
233        line: Option<usize>,
234    },
235    /// A parser or import resource limit was exceeded.
236    Limit {
237        /// Stable explanation including the limit name.
238        message: String,
239    },
240    /// A syntactically valid CIF cannot form the requested structure.
241    Import {
242        /// Stable explanation.
243        message: String,
244    },
245    /// A deliberately unsupported CIF feature was encountered in strict mode.
246    Unsupported {
247        /// Unsupported feature family.
248        feature: String,
249        /// Human-readable explanation.
250        message: String,
251    },
252    /// Native space-group lookup failed.
253    SpaceGroup(SpaceGroupLookupError),
254    /// Unit-cell validation failed.
255    Cell(CellError),
256    /// Exact operation validation failed.
257    Symmetry(SymmetryError),
258}
259
260impl Display for CifIoError {
261    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
262        match self {
263            Self::InvalidLimits => formatter.write_str("all CIF read limits must be positive"),
264            Self::ByteLimitExceeded { actual, maximum } => {
265                write!(
266                    formatter,
267                    "CIF input exceeds max_bytes: {actual} > {maximum}"
268                )
269            }
270            Self::Io(error) => Display::fmt(error, formatter),
271            Self::Syntax {
272                message,
273                line: Some(line),
274            } => write!(formatter, "invalid CIF syntax at line {line}: {message}"),
275            Self::Syntax {
276                message,
277                line: None,
278            } => write!(formatter, "invalid CIF syntax: {message}"),
279            Self::Limit { message }
280            | Self::Import { message }
281            | Self::Unsupported { message, .. } => formatter.write_str(message),
282            Self::SpaceGroup(error) => Display::fmt(error, formatter),
283            Self::Cell(error) => Display::fmt(error, formatter),
284            Self::Symmetry(error) => Display::fmt(error, formatter),
285        }
286    }
287}
288
289impl Error for CifIoError {
290    fn source(&self) -> Option<&(dyn Error + 'static)> {
291        match self {
292            Self::Io(error) => Some(error),
293            Self::SpaceGroup(error) => Some(error),
294            Self::Cell(error) => Some(error),
295            Self::Symmetry(error) => Some(error),
296            _ => None,
297        }
298    }
299}
300
301fn import_error(message: impl Into<String>) -> CifIoError {
302    CifIoError::Import {
303        message: message.into(),
304    }
305}