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, 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            None,
130        )
131    }
132
133    /// Validates an XML file against the schema of a specific taxonomy.
134    ///
135    /// This is a schema-only check via ERiC's `EricCheckXML` and does not
136    /// execute the full validation/send pipeline of [`Eric::validate`] or
137    /// [`Eric::send`].
138    ///
139    /// Note that ERiC may report unsupported data types/versions for this
140    /// API function.
141    pub fn check_xml(
142        &self,
143        xml: String,
144        taxonomy_type: &str,
145        taxonomy_version: &str,
146    ) -> Result<EricResponse, EricError> {
147        let type_version = format!("{}_{}", taxonomy_type, taxonomy_version);
148        let xml = xml.try_to_cstring()?;
149        let type_version = type_version.try_to_cstring()?;
150
151        let validation_response_buffer = ResponseBuffer::new()?;
152
153        let error_code = unsafe {
154            EricCheckXML(
155                xml.as_ptr(),
156                type_version.as_ptr(),
157                validation_response_buffer.as_ptr(),
158            )
159        };
160
161        let validation_response = validation_response_buffer.read()?;
162        let payload = EricApiPayload::new(validation_response.to_string(), String::new());
163
164        if error_code == ErrorCode::ERIC_OK as i32 {
165            Ok(EricResponse::new(payload))
166        } else {
167            let response_buffer = ResponseBuffer::new()?;
168
169            unsafe {
170                EricHoleFehlerText(error_code, response_buffer.as_ptr());
171            }
172
173            let error_text = response_buffer.read()?;
174
175            Err(EricError::ApiError {
176                code: error_code,
177                message: error_text.to_string(),
178                payload,
179            })
180        }
181    }
182
183    /// Returns the error text for a specific error code.
184    pub fn get_error_text(&self, error_code: i32) -> Result<String, EricError> {
185        let response_buffer = ResponseBuffer::new()?;
186
187        unsafe {
188            EricHoleFehlerText(error_code, response_buffer.as_ptr());
189        }
190
191        Ok(response_buffer.read()?.to_string())
192    }
193
194    #[allow(dead_code)]
195    fn decrypt(
196        &self,
197        encrypted_file: &str,
198        certificate_config: CertificateConfig,
199    ) -> Result<i32, EricError> {
200        let encrypted_data = encrypted_file.try_to_cstring()?;
201        let response_buffer = ResponseBuffer::new()?;
202
203        let error_code = unsafe {
204            EricDekodiereDaten(
205                certificate_config.certificate.handle,
206                certificate_config.password.as_ptr(),
207                encrypted_data.as_ptr(),
208                response_buffer.as_ptr(),
209            )
210        };
211
212        Ok(error_code)
213    }
214
215    fn process(
216        xml: String,
217        type_version: String,
218        processing_flag: ProcessingFlag,
219        print_config: Option<PrintConfig>,
220        certificate_config: Option<CertificateConfig>,
221        transfer_code: Option<u32>,
222    ) -> Result<EricResponse, EricError> {
223        debug!("Processing xml file");
224
225        match processing_flag {
226            ProcessingFlag::Validate => debug!("Validating xml file"),
227            ProcessingFlag::Print => debug!("Validating xml file"),
228            ProcessingFlag::Send => debug!("Sending xml file"),
229            ProcessingFlag::SendAndPrint => debug!("Send and print"),
230            ProcessingFlag::CheckHints => debug!("Check hints"),
231            ProcessingFlag::ValidateWithoutDate => debug!("Validate without release date"),
232        }
233
234        let xml = xml.try_to_cstring()?;
235        let type_version = type_version.try_to_cstring()?;
236
237        // Transfer_code should be NULL except for data retrieval; if
238        // transfer_code is not NULL in the other cases, it will be ignored.
239        // SAFETY: `transfer_code_storage` must outlive `transfer_code_ptr`.
240        let mut transfer_code_storage = transfer_code;
241        let transfer_code_ptr: *mut u32 = transfer_code_storage
242            .as_mut()
243            .map_or(ptr::null_mut(), |c| c as *mut u32);
244
245        if let Some(print_config) = &print_config {
246            info!(
247                pdf_path = %print_config
248                    .pdf_path
249                    .to_str()
250                    .context("failed to convert path to string")?,
251                "Printing confirmation to file"
252            )
253        }
254
255        let validation_response_buffer = ResponseBuffer::new()?;
256        let server_response_buffer = ResponseBuffer::new()?;
257
258        let error_code = unsafe {
259            EricBearbeiteVorgang(
260                xml.as_ptr(),
261                type_version.as_ptr(),
262                processing_flag.into_u32(),
263                // SAFETY: match a reference of print_config; otherwise
264                // print_config is moved, and print_parameter.as_ptr() would be
265                // dangling
266                match &print_config {
267                    Some(el) => el.print_parameter.as_ptr(),
268                    None => ptr::null(),
269                },
270                // SAFETY: match a reference of certificate_config; otherwise
271                // certificate_config is moved, and
272                // certificate_parameter.as_ptr() would be dangling
273                match &certificate_config {
274                    Some(config) => config.certificate_parameter.as_ptr(),
275                    None => ptr::null(),
276                },
277                transfer_code_ptr,
278                validation_response_buffer.as_ptr(),
279                server_response_buffer.as_ptr(),
280            )
281        };
282
283        let transfer_code = unsafe { transfer_code_ptr.as_ref() };
284
285        if let Some(code) = transfer_code {
286            debug!(transfer_code = %code, "Transfer code received")
287        }
288
289        let validation_response = validation_response_buffer.read()?;
290        // TODO: parse server response via EricGetErrormessagesFromXMLAnswer()
291        let server_response = server_response_buffer.read()?;
292        let payload =
293            EricApiPayload::new(validation_response.to_string(), server_response.to_string());
294
295        if error_code == ErrorCode::ERIC_OK as i32 {
296            Ok(EricResponse::new(payload))
297        } else {
298            let response_buffer = ResponseBuffer::new()?;
299
300            unsafe {
301                EricHoleFehlerText(error_code, response_buffer.as_ptr());
302            }
303
304            let error_text = response_buffer.read()?;
305
306            Err(EricError::ApiError {
307                code: error_code,
308                message: error_text.to_string(),
309                payload,
310            })
311        }
312    }
313}
314
315impl Drop for Eric {
316    fn drop(&mut self) {
317        info!("Closing eric");
318
319        let error_code = unsafe { EricEntladePlugins() };
320
321        if error_code != ErrorCode::ERIC_OK as i32 {
322            error!(error_code = %error_code, "Error while unloading plugins");
323        }
324
325        let error_code = unsafe { EricBeende() };
326
327        if error_code != ErrorCode::ERIC_OK as i32 {
328            error!(error_code = %error_code, "Can't close eric");
329        }
330    }
331}