Skip to main content

eric_sdk/
eric.rs

1use crate::{
2    config::{CertificateConfig, PrintConfig},
3    error::EricError,
4    error_code::ErrorCode,
5    response::{EricApiPayload, EricResponse, ResponseBuffer},
6    utils::ToCString,
7    ProcessingFlag,
8};
9use anyhow::{anyhow, Context};
10use eric_bindings::{
11    EricBearbeiteVorgang, EricBeende, EricCheckXML, EricDekodiereDaten, EricEntladePlugins,
12    EricHoleFehlerText, EricInitialisiere,
13};
14use std::{path::Path, ptr};
15use tracing::{debug, error, info};
16
17/// A structure to manage the Eric instance from the shared C library.
18///
19/// Use [`Eric::new`] to initialize Eric. Closes Eric when dropped.
20pub struct Eric;
21
22impl Eric {
23    /// Initializes a single-threaded Eric instance.
24    ///
25    /// If `log_path` is `None`, the system directory for temporary files is
26    /// used. If `plugin_path` is `None`, the path to the shared C library is
27    /// used.
28    pub fn new(log_path: Option<&Path>, plugin_path: Option<&Path>) -> Result<Self, EricError> {
29        info!("Initializing eric");
30
31        if let Some(log_path) = log_path {
32            info!(log_path = %log_path.display(), "Setting log path");
33            info!(log_file = %log_path.join("eric.log").display(), "Logging to file");
34        } else {
35            info!("No log path provided, using ERiC default temporary directory");
36        }
37
38        if let Some(plugin_path) = plugin_path {
39            info!(plugin_path = %plugin_path.display(), "Setting plugin path");
40        } else {
41            info!("No plugin path provided, using ERiC default plugin directory");
42        }
43
44        // SAFETY: `plugin_path_cstring` must outlive `plugin_ptr`.
45        let plugin_path_cstring = plugin_path
46            .map(|plugin_path| plugin_path.try_to_cstring())
47            .transpose()
48            .context("failed to convert plugin path to CString")?;
49        let plugin_ptr = plugin_path_cstring
50            .as_deref()
51            .map_or(ptr::null(), |cstr| cstr.as_ptr());
52
53        // SAFETY: `log_path_cstring` must outlive `log_path_ptr`.
54        let log_path_cstring = log_path
55            .map(|path| path.try_to_cstring())
56            .transpose()
57            .context("failed to convert log path to CString")?;
58        let log_path_ptr = log_path_cstring
59            .as_deref()
60            .map_or(ptr::null(), |cstr| cstr.as_ptr());
61
62        let error_code = unsafe { EricInitialisiere(plugin_ptr, log_path_ptr) };
63
64        match error_code {
65            x if x == ErrorCode::ERIC_OK as i32 => Ok(Eric),
66            error_code => Err(EricError::Internal(anyhow!(
67                "Can't init eric: {}",
68                error_code
69            ))),
70        }
71    }
72
73    /// Validates an XML file for a specific taxonomy.
74    ///
75    /// Optionally, a confirmation is printed to `pdf_path`.
76    pub fn validate(
77        &self,
78        xml: String,
79        taxonomy_type: &str,
80        taxonomy_version: &str,
81        pdf_path: Option<&str>,
82    ) -> Result<EricResponse, EricError> {
83        let processing_flag: ProcessingFlag;
84        let type_version = format!("{}_{}", taxonomy_type, taxonomy_version);
85        let print_config = if let Some(pdf_path) = pdf_path {
86            processing_flag = ProcessingFlag::Print;
87            Some(PrintConfig::new(pdf_path, &processing_flag)?)
88        } else {
89            processing_flag = ProcessingFlag::Validate;
90            None
91        };
92        Self::process(xml, type_version, processing_flag, print_config, None)
93    }
94
95    /// Sends an XML file for a specific taxonomy to the tax authorities.
96    ///
97    /// The Elster certificate needs to be provided at path `certificate_path`
98    /// with password `certificate_password`.
99    ///
100    /// Optionally, a confirmation is printed to `pdf_path`.
101    pub fn send(
102        &self,
103        xml: String,
104        taxonomy_type: &str,
105        taxonomy_version: &str,
106        certificate_path: &Path,
107        certificate_password: &str,
108        pdf_path: Option<&str>,
109    ) -> Result<EricResponse, EricError> {
110        let certificate_path = certificate_path
111            .to_str()
112            .context("failed to convert path to string")?;
113        let processing_flag: ProcessingFlag;
114        let type_version = format!("{}_{}", taxonomy_type, taxonomy_version);
115        let print_config = if let Some(pdf_path) = pdf_path {
116            processing_flag = ProcessingFlag::SendAndPrint;
117            Some(PrintConfig::new(pdf_path, &processing_flag)?)
118        } else {
119            processing_flag = ProcessingFlag::Send;
120            None
121        };
122        let certificate_config = CertificateConfig::new(certificate_path, certificate_password)?;
123        Self::process(
124            xml,
125            type_version,
126            processing_flag,
127            print_config,
128            Some(certificate_config),
129        )
130    }
131
132    /// Validates an XML file against the schema of a specific taxonomy.
133    ///
134    /// This is a schema-only check via ERiC's `EricCheckXML` and does not
135    /// execute the full validation/send pipeline of [`Eric::validate`] or
136    /// [`Eric::send`].
137    ///
138    /// Note that ERiC may report unsupported data types/versions for this
139    /// API function.
140    pub fn check_xml(
141        &self,
142        xml: String,
143        taxonomy_type: &str,
144        taxonomy_version: &str,
145    ) -> Result<EricResponse, EricError> {
146        let type_version = format!("{}_{}", taxonomy_type, taxonomy_version);
147        let xml = xml.try_to_cstring()?;
148        let type_version = type_version.try_to_cstring()?;
149
150        let validation_response_buffer = ResponseBuffer::new()?;
151
152        let error_code = unsafe {
153            EricCheckXML(
154                xml.as_ptr(),
155                type_version.as_ptr(),
156                validation_response_buffer.as_ptr(),
157            )
158        };
159
160        let validation_response = validation_response_buffer.read()?;
161        let payload = EricApiPayload::new(validation_response.to_string(), String::new());
162
163        if error_code == ErrorCode::ERIC_OK as i32 {
164            Ok(EricResponse::new(payload))
165        } else {
166            let response_buffer = ResponseBuffer::new()?;
167
168            unsafe {
169                EricHoleFehlerText(error_code, response_buffer.as_ptr());
170            }
171
172            let error_text = response_buffer.read()?;
173
174            Err(EricError::ApiError {
175                code: error_code,
176                message: error_text.to_string(),
177                payload,
178            })
179        }
180    }
181
182    /// Returns the error text for a specific error code.
183    pub fn get_error_text(&self, error_code: i32) -> Result<String, EricError> {
184        let response_buffer = ResponseBuffer::new()?;
185
186        unsafe {
187            EricHoleFehlerText(error_code, response_buffer.as_ptr());
188        }
189
190        Ok(response_buffer.read()?.to_string())
191    }
192
193    #[allow(dead_code)]
194    fn decrypt(
195        &self,
196        encrypted_file: &str,
197        certificate_config: CertificateConfig,
198    ) -> Result<i32, EricError> {
199        let encrypted_data = encrypted_file.try_to_cstring()?;
200        let response_buffer = ResponseBuffer::new()?;
201
202        let error_code = unsafe {
203            EricDekodiereDaten(
204                certificate_config.certificate.handle,
205                certificate_config.password.as_ptr(),
206                encrypted_data.as_ptr(),
207                response_buffer.as_ptr(),
208            )
209        };
210
211        Ok(error_code)
212    }
213
214    fn process(
215        xml: String,
216        type_version: String,
217        processing_flag: ProcessingFlag,
218        print_config: Option<PrintConfig>,
219        certificate_config: Option<CertificateConfig>,
220    ) -> Result<EricResponse, EricError> {
221        debug!("Processing xml file");
222
223        match processing_flag {
224            ProcessingFlag::Validate => debug!("Validating xml file"),
225            ProcessingFlag::Print => debug!("Validating xml file"),
226            ProcessingFlag::Send => debug!("Sending xml file"),
227            ProcessingFlag::SendAndPrint => debug!("Send and print"),
228            ProcessingFlag::CheckHints => debug!("Check hints"),
229            ProcessingFlag::ValidateWithoutDate => debug!("Validate without release date"),
230        }
231
232        let xml = xml.try_to_cstring()?;
233        let type_version = type_version.try_to_cstring()?;
234
235        if let Some(print_config) = &print_config {
236            info!(
237                pdf_path = %print_config
238                    .pdf_path
239                    .to_str()
240                    .context("failed to convert path to string")?,
241                "Printing confirmation to file"
242            )
243        }
244
245        let validation_response_buffer = ResponseBuffer::new()?;
246        let server_response_buffer = ResponseBuffer::new()?;
247
248        let error_code = unsafe {
249            EricBearbeiteVorgang(
250                xml.as_ptr(),
251                type_version.as_ptr(),
252                processing_flag.into_u32(),
253                // SAFETY: match a reference of print_config; otherwise
254                // print_config is moved, and print_parameter.as_ptr() would be
255                // dangling
256                match &print_config {
257                    Some(el) => el.print_parameter.as_ptr(),
258                    None => ptr::null(),
259                },
260                // SAFETY: match a reference of certificate_config; otherwise
261                // certificate_config is moved, and
262                // certificate_parameter.as_ptr() would be dangling
263                match &certificate_config {
264                    Some(config) => config.certificate_parameter.as_ptr(),
265                    None => ptr::null(),
266                },
267                validation_response_buffer.as_ptr(),
268                server_response_buffer.as_ptr(),
269            )
270        };
271
272        let validation_response = validation_response_buffer.read()?;
273        // TODO: parse server response via EricGetErrormessagesFromXMLAnswer()
274        let server_response = server_response_buffer.read()?;
275        let payload =
276            EricApiPayload::new(validation_response.to_string(), server_response.to_string());
277
278        if error_code == ErrorCode::ERIC_OK as i32 {
279            Ok(EricResponse::new(payload))
280        } else {
281            let response_buffer = ResponseBuffer::new()?;
282
283            unsafe {
284                EricHoleFehlerText(error_code, response_buffer.as_ptr());
285            }
286
287            let error_text = response_buffer.read()?;
288
289            Err(EricError::ApiError {
290                code: error_code,
291                message: error_text.to_string(),
292                payload,
293            })
294        }
295    }
296}
297
298impl Drop for Eric {
299    fn drop(&mut self) {
300        info!("Closing eric");
301
302        let error_code = unsafe { EricEntladePlugins() };
303
304        if error_code != ErrorCode::ERIC_OK as i32 {
305            error!(error_code = %error_code, "Error while unloading plugins");
306        }
307
308        let error_code = unsafe { EricBeende() };
309
310        if error_code != ErrorCode::ERIC_OK as i32 {
311            error!(error_code = %error_code, "Can't close eric");
312        }
313    }
314}