Skip to main content

readcon_core/
error.rs

1use std::fmt;
2use std::num::{ParseFloatError, ParseIntError};
3
4#[derive(Debug)]
5pub enum ParseError {
6    IncompleteHeader,
7    IncompleteFrame,
8    IncompleteVelocitySection,
9    InvalidVectorLength {
10        expected: usize,
11        found: usize,
12    },
13    InvalidNumberFormat(String),
14    MissingSpecVersion,
15    UnsupportedSpecVersion(u32),
16    InvalidMetadataJson(String),
17    IncompleteForceSection,
18    IncompleteEnergySection,
19    /// Declared optional section was incomplete or mislabeled.
20    IncompleteSection(String),
21    UnknownSection(String),
22    ValidationError(String),
23    /// NaN, Inf, Infinity, or a signed or mixed-case spelling, in any mode.
24    /// Clause CON-num.1 (D1).
25    NonFiniteValue(String),
26    /// Column 4 is not a legal integer flag. Clause CON-mask.5 (D8).
27    InvalidFixedFlag(String),
28    /// A count field is not an unsigned integer, or a per-type count is 0
29    /// when the type count is 1 or more. A type count of 0 is legal.
30    /// Clauses CON-count.1 and CON-error.3 (D4).
31    InvalidCount(String),
32    /// A unit expression is missing, mistyped, or the wrong dimension.
33    /// A minor of 1 or later with velocities and no `units.time` is this
34    /// class on read and on write.
35    /// Clauses CON-units.11, CON-units.12, CON-units.13, and CON-units.14 (D11).
36    InvalidUnits(String),
37    /// Unknown major, or JSON `con_spec_version` 0 or 1.
38    /// Clause CON-line2.5 (D15).
39    UnsupportedVersion(u32),
40    /// An in-place builder mutation
41    /// (`ConFrameBuilder::set_atom_position` / `set_atom_velocity` /
42    /// `set_atom_force` / `set_atom_energy` / `set_atom_fixed` /
43    /// `set_atom_mass` / clear_*) was called with an atom index past
44    /// the current length. Surfaces as `IndexError` in PyO3 and as
45    /// `RKR_STATUS_INDEX_OUT_OF_BOUNDS` over the C ABI.
46    IndexOutOfBounds {
47        index: usize,
48        len: usize,
49    },
50    /// Two atoms share a CON type (symbol) but their masses differ
51    /// beyond [`crate::types::ConFrameBuilder::TYPE_MASS_ABS_TOL`].
52    /// CON line 9 stores one mass per type, so the builder cannot
53    /// represent per-atom mass variation inside a type.
54    MassMismatch {
55        symbol: String,
56        first_mass: f64,
57        found_mass: f64,
58        atom_index: usize,
59    },
60}
61
62impl fmt::Display for ParseError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            ParseError::IncompleteHeader => {
66                write!(f, "file ended unexpectedly while parsing frame header")
67            }
68            ParseError::IncompleteFrame => {
69                write!(f, "file ended unexpectedly while reading atom data")
70            }
71            ParseError::IncompleteVelocitySection => {
72                write!(f, "file ended unexpectedly while reading velocity section")
73            }
74            ParseError::InvalidVectorLength { expected, found } => {
75                write!(f, "expected {expected} values on line, found {found}")
76            }
77            ParseError::InvalidNumberFormat(msg) => {
78                write!(f, "invalid number format: {msg}")
79            }
80            ParseError::MissingSpecVersion => {
81                write!(
82                    f,
83                    "line 2 must be a JSON object containing \"con_spec_version\""
84                )
85            }
86            ParseError::UnsupportedSpecVersion(v) | ParseError::UnsupportedVersion(v) => {
87                write!(f, "unsupported con_spec_version: {v}")
88            }
89            ParseError::InvalidMetadataJson(msg) => {
90                write!(f, "invalid JSON metadata on line 2: {msg}")
91            }
92            ParseError::IncompleteForceSection => {
93                write!(f, "file ended unexpectedly while reading force section")
94            }
95            ParseError::IncompleteEnergySection => {
96                write!(f, "file ended unexpectedly while reading energy section")
97            }
98            ParseError::IncompleteSection(name) => {
99                write!(f, "file ended unexpectedly while reading {name} section")
100            }
101            ParseError::UnknownSection(name) => {
102                write!(f, "unknown section type in metadata: {name}")
103            }
104            ParseError::ValidationError(msg) => {
105                write!(f, "CON validation failed: {msg}")
106            }
107            ParseError::NonFiniteValue(token) => {
108                write!(f, "non-finite value: {token}")
109            }
110            ParseError::InvalidFixedFlag(msg) => {
111                write!(f, "invalid fixed flag: {msg}")
112            }
113            ParseError::InvalidCount(msg) => {
114                write!(f, "invalid count: {msg}")
115            }
116            ParseError::InvalidUnits(msg) => {
117                write!(f, "invalid units: {msg}")
118            }
119            ParseError::IndexOutOfBounds { index, len } => {
120                write!(
121                    f,
122                    "atom index {index} is out of bounds (builder holds {len} atoms)"
123                )
124            }
125            ParseError::MassMismatch {
126                symbol,
127                first_mass,
128                found_mass,
129                atom_index,
130            } => {
131                write!(
132                    f,
133                    "atoms of type {symbol} have inconsistent masses: first {first_mass}, atom {atom_index} has {found_mass} (CON stores one mass per type)"
134                )
135            }
136        }
137    }
138}
139
140impl std::error::Error for ParseError {}
141
142impl ParseError {
143    /// Stable class name from the v3.1 error table (CON-error.1).
144    pub fn class_name(&self) -> &'static str {
145        match self {
146            ParseError::IncompleteHeader => "IncompleteHeader",
147            ParseError::IncompleteFrame => "IncompleteFrame",
148            ParseError::IncompleteVelocitySection => "IncompleteVelocitySection",
149            ParseError::InvalidVectorLength { .. } => "InvalidVectorLength",
150            ParseError::InvalidNumberFormat(_) => "InvalidNumberFormat",
151            ParseError::MissingSpecVersion => "MissingSpecVersion",
152            ParseError::UnsupportedSpecVersion(_) | ParseError::UnsupportedVersion(_) => {
153                "UnsupportedVersion"
154            }
155            ParseError::InvalidMetadataJson(_) => "InvalidMetadataJson",
156            ParseError::IncompleteForceSection => "IncompleteForceSection",
157            ParseError::IncompleteEnergySection => "IncompleteEnergySection",
158            ParseError::IncompleteSection(_) => "IncompleteSection",
159            ParseError::UnknownSection(_) => "UnknownSection",
160            ParseError::ValidationError(_) => "ValidationError",
161            ParseError::IndexOutOfBounds { .. } => "IndexOutOfBounds",
162            ParseError::MassMismatch { .. } => "MassMismatch",
163            ParseError::NonFiniteValue(_) => "NonFiniteValue",
164            ParseError::InvalidFixedFlag(_) => "InvalidFixedFlag",
165            ParseError::InvalidCount(_) => "InvalidCount",
166            ParseError::InvalidUnits(_) => "InvalidUnits",
167        }
168    }
169
170    /// 1-based file line when this failure is tied to a known header line.
171    /// Metadata failures are line 2 of a single-frame file (CON-error.2).
172    /// Other failures report the line recorded by the iterator, or 0 when
173    /// the producing call did not have a cursor.
174    pub fn line(&self) -> u32 {
175        match self {
176            ParseError::MissingSpecVersion
177            | ParseError::InvalidMetadataJson(_)
178            | ParseError::UnsupportedSpecVersion(_)
179            | ParseError::UnsupportedVersion(_) => 2,
180            _ => 0,
181        }
182    }
183}
184
185impl From<ParseFloatError> for ParseError {
186    fn from(e: ParseFloatError) -> Self {
187        ParseError::InvalidNumberFormat(e.to_string())
188    }
189}
190
191impl From<ParseIntError> for ParseError {
192    fn from(e: ParseIntError) -> Self {
193        ParseError::InvalidNumberFormat(e.to_string())
194    }
195}
196
197impl From<serde_json::Error> for ParseError {
198    fn from(e: serde_json::Error) -> Self {
199        ParseError::InvalidMetadataJson(e.to_string())
200    }
201}