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, EricHoleZertifikatEigenschaften, 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    /// Reads the properties of a certificate as an XML document, via
194    /// `EricHoleZertifikatEigenschaften`.
195    ///
196    /// The returned XML conforms to the `EricHoleZertifikatEigenschaften`
197    /// return schema and carries, among other fields, `<TokenTyp>`
198    /// (`Software` / `Stick` / `Karte` / ...) and an optional
199    /// `<Testzertifikat>` boolean.
200    ///
201    /// Useful as a local check of certificate suitability before `send`.
202    /// Purely local: no server contact.
203    pub fn certificate_properties(
204        &self,
205        certificate_path: &Path,
206        pin: &str,
207    ) -> Result<String, EricError> {
208        let path = certificate_path
209            .to_str()
210            .context("failed to convert path to string")?
211            .try_to_cstring()?;
212        let pin = pin.try_to_cstring()?;
213        let certificate = crate::certificate::Certificate::new(&path)?;
214
215        let response_buffer = ResponseBuffer::new()?;
216
217        let error_code = unsafe {
218            EricHoleZertifikatEigenschaften(
219                certificate.handle,
220                pin.as_ptr(),
221                response_buffer.as_ptr(),
222            )
223        };
224
225        if error_code == ErrorCode::ERIC_OK as i32 {
226            Ok(response_buffer.read()?.to_string())
227        } else {
228            let error_text = self
229                .get_error_text(error_code)
230                .unwrap_or_else(|_| String::new());
231            Err(EricError::ApiError {
232                code: error_code,
233                message: error_text,
234                payload: EricApiPayload::new(String::new(), String::new()),
235            })
236        }
237    }
238
239    #[allow(dead_code)]
240    fn decrypt(
241        &self,
242        encrypted_file: &str,
243        certificate_config: CertificateConfig,
244    ) -> Result<i32, EricError> {
245        let encrypted_data = encrypted_file.try_to_cstring()?;
246        let response_buffer = ResponseBuffer::new()?;
247
248        let error_code = unsafe {
249            EricDekodiereDaten(
250                certificate_config.certificate.handle,
251                certificate_config.password.as_ptr(),
252                encrypted_data.as_ptr(),
253                response_buffer.as_ptr(),
254            )
255        };
256
257        Ok(error_code)
258    }
259
260    fn process(
261        xml: String,
262        type_version: String,
263        processing_flag: ProcessingFlag,
264        print_config: Option<PrintConfig>,
265        certificate_config: Option<CertificateConfig>,
266    ) -> Result<EricResponse, EricError> {
267        debug!("Processing xml file");
268
269        match processing_flag {
270            ProcessingFlag::Validate => debug!("Validating xml file"),
271            ProcessingFlag::Print => debug!("Validating xml file"),
272            ProcessingFlag::Send => debug!("Sending xml file"),
273            ProcessingFlag::SendAndPrint => debug!("Send and print"),
274            ProcessingFlag::CheckHints => debug!("Check hints"),
275            ProcessingFlag::ValidateWithoutDate => debug!("Validate without release date"),
276        }
277
278        let xml = xml.try_to_cstring()?;
279        let type_version = type_version.try_to_cstring()?;
280
281        if let Some(print_config) = &print_config {
282            info!(
283                pdf_path = %print_config
284                    .pdf_path
285                    .to_str()
286                    .context("failed to convert path to string")?,
287                "Printing confirmation to file"
288            )
289        }
290
291        let validation_response_buffer = ResponseBuffer::new()?;
292        let server_response_buffer = ResponseBuffer::new()?;
293
294        let error_code = unsafe {
295            EricBearbeiteVorgang(
296                xml.as_ptr(),
297                type_version.as_ptr(),
298                processing_flag.into_u32(),
299                // SAFETY: match a reference of print_config; otherwise
300                // print_config is moved, and print_parameter.as_ptr() would be
301                // dangling
302                match &print_config {
303                    Some(el) => el.print_parameter.as_ptr(),
304                    None => ptr::null(),
305                },
306                // SAFETY: match a reference of certificate_config; otherwise
307                // certificate_config is moved, and
308                // certificate_parameter.as_ptr() would be dangling
309                match &certificate_config {
310                    Some(config) => config.certificate_parameter.as_ptr(),
311                    None => ptr::null(),
312                },
313                validation_response_buffer.as_ptr(),
314                server_response_buffer.as_ptr(),
315            )
316        };
317
318        let validation_response = validation_response_buffer.read()?;
319        // TODO: parse server response via EricGetErrormessagesFromXMLAnswer()
320        let server_response = server_response_buffer.read()?;
321        let payload =
322            EricApiPayload::new(validation_response.to_string(), server_response.to_string());
323
324        if error_code == ErrorCode::ERIC_OK as i32 {
325            Ok(EricResponse::new(payload))
326        } else {
327            let response_buffer = ResponseBuffer::new()?;
328
329            unsafe {
330                EricHoleFehlerText(error_code, response_buffer.as_ptr());
331            }
332
333            let error_text = response_buffer.read()?;
334
335            Err(EricError::ApiError {
336                code: error_code,
337                message: error_text.to_string(),
338                payload,
339            })
340        }
341    }
342}
343
344impl Drop for Eric {
345    fn drop(&mut self) {
346        info!("Closing eric");
347
348        let error_code = unsafe { EricEntladePlugins() };
349
350        if error_code != ErrorCode::ERIC_OK as i32 {
351            error!(error_code = %error_code, "Error while unloading plugins");
352        }
353
354        let error_code = unsafe { EricBeende() };
355
356        if error_code != ErrorCode::ERIC_OK as i32 {
357            error!(error_code = %error_code, "Can't close eric");
358        }
359    }
360}