Skip to main content

core_invoice_sys/
lib.rs

1//! C ABI. Python binds here via ctypes. WASM is the model crate, not this cdylib.
2
3use core_invoice::Profile;
4use core_invoice_formats::{Syntax, convert_with_profile, diff, validate_xml};
5use std::os::raw::{c_char, c_int};
6
7/// 0 valid, 1 invalid, 2 unreadable / bad args. Same contract as the CLI.
8///
9/// `profile` NULL means auto from BT-24 (CustomizationID).
10/// Known slugs: en16931, peppol, pint, pint-my.
11/// `xml` must outlive the call. `err` is `err_len` writable bytes, UTF-8, always NUL-terminated if err_len > 0.
12#[allow(clippy::not_unsafe_ptr_arg_deref)]
13#[unsafe(no_mangle)]
14pub extern "C" fn core_invoice_validate_ubl(
15    xml: *const c_char,
16    profile: *const c_char,
17    err: *mut c_char,
18    err_len: usize,
19) -> c_int {
20    validate_any(xml, profile, err, err_len)
21}
22
23/// Same as [`core_invoice_validate_ubl`]: syntax is the document element (UBL or CII).
24#[allow(clippy::not_unsafe_ptr_arg_deref)]
25#[unsafe(no_mangle)]
26pub extern "C" fn core_invoice_validate(
27    xml: *const c_char,
28    profile: *const c_char,
29    err: *mut c_char,
30    err_len: usize,
31) -> c_int {
32    validate_any(xml, profile, err, err_len)
33}
34
35fn validate_any(
36    xml: *const c_char,
37    profile: *const c_char,
38    err: *mut c_char,
39    err_len: usize,
40) -> c_int {
41    if xml.is_null() {
42        return write_err(err, err_len, "xml is null", 2);
43    }
44    let xml = unsafe { std::ffi::CStr::from_ptr(xml) }.to_string_lossy();
45    let forced = match parse_profile(profile, err, err_len) {
46        Ok(p) => p,
47        Err(code) => return code,
48    };
49    match validate_xml(xml.as_ref(), forced) {
50        Ok(report) if report.ok() => 0,
51        Ok(report) => write_err(err, err_len, &report.to_string(), 1),
52        Err(e) => write_err(err, err_len, &e.to_string(), 2),
53    }
54}
55
56fn parse_profile(
57    profile: *const c_char,
58    err: *mut c_char,
59    err_len: usize,
60) -> Result<Option<Profile>, c_int> {
61    if profile.is_null() {
62        return Ok(None);
63    }
64    let s = unsafe { std::ffi::CStr::from_ptr(profile) }.to_string_lossy();
65    if s.is_empty() {
66        return Ok(None);
67    }
68    match Profile::parse(s.as_ref()) {
69        Some(p) => Ok(Some(p)),
70        None => Err(write_err(
71            err,
72            err_len,
73            &format!("unknown profile {s}; known: {}", Profile::known_slugs()),
74            2,
75        )),
76    }
77}
78
79/// Convert through the semantic model. `to` is `ubl` or `cii`.
80/// On success writes XML into `out` (NUL-terminated). Same 0/1/2 as CLI convert.
81#[allow(clippy::not_unsafe_ptr_arg_deref)]
82#[unsafe(no_mangle)]
83pub extern "C" fn core_invoice_convert(
84    xml: *const c_char,
85    to: *const c_char,
86    profile: *const c_char,
87    out: *mut c_char,
88    out_len: usize,
89    err: *mut c_char,
90    err_len: usize,
91) -> c_int {
92    if xml.is_null() || to.is_null() {
93        return write_err(err, err_len, "xml or to is null", 2);
94    }
95    let xml = unsafe { std::ffi::CStr::from_ptr(xml) }.to_string_lossy();
96    let to = unsafe { std::ffi::CStr::from_ptr(to) }.to_string_lossy();
97    let Some(syntax) = Syntax::parse(to.as_ref()) else {
98        return write_err(err, err_len, "to must be ubl or cii", 2);
99    };
100    let forced = match parse_profile(profile, err, err_len) {
101        Ok(p) => p,
102        Err(code) => return code,
103    };
104    match convert_with_profile(xml.as_ref(), syntax, forced) {
105        Ok(s) => write_out(out, out_len, &s),
106        Err(core_invoice_formats::FormatError::Semantic(rej)) => {
107            write_err(err, err_len, &rej.0.to_string(), 1)
108        }
109        Err(e) => write_err(err, err_len, &e.to_string(), 2),
110    }
111}
112
113/// Semantic diff. 0 identical, 1 differ, 2 unreadable. Writes the report into `out`.
114#[allow(clippy::not_unsafe_ptr_arg_deref)]
115#[unsafe(no_mangle)]
116pub extern "C" fn core_invoice_diff(
117    left: *const c_char,
118    right: *const c_char,
119    out: *mut c_char,
120    out_len: usize,
121    err: *mut c_char,
122    err_len: usize,
123) -> c_int {
124    if left.is_null() || right.is_null() {
125        return write_err(err, err_len, "left or right is null", 2);
126    }
127    let left = unsafe { std::ffi::CStr::from_ptr(left) }.to_string_lossy();
128    let right = unsafe { std::ffi::CStr::from_ptr(right) }.to_string_lossy();
129    match diff(left.as_ref(), right.as_ref()) {
130        Ok(s) if s == "no semantic difference" => write_out(out, out_len, &s),
131        Ok(s) => {
132            let _ = write_out(out, out_len, &s);
133            1
134        }
135        Err(e) => write_err(err, err_len, &e.to_string(), 2),
136    }
137}
138
139/// Writes crate version into `out`. Always 0 unless `out` is null (2).
140#[allow(clippy::not_unsafe_ptr_arg_deref)]
141#[unsafe(no_mangle)]
142pub extern "C" fn core_invoice_version(out: *mut c_char, out_len: usize) -> c_int {
143    write_out(out, out_len, env!("CARGO_PKG_VERSION"))
144}
145
146fn write_out(out: *mut c_char, out_len: usize, msg: &str) -> c_int {
147    if out.is_null() || out_len == 0 {
148        return 2;
149    }
150    let max = out_len - 1;
151    let mut end = msg.len().min(max);
152    let bytes = msg.as_bytes();
153    while end > 0 && end < bytes.len() && (bytes[end] & 0b1100_0000) == 0b1000_0000 {
154        end -= 1;
155    }
156    end = end.min(bytes.len()).min(max);
157    unsafe {
158        std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), out, end);
159        *out.add(end) = 0;
160    }
161    0
162}
163
164fn write_err(err: *mut c_char, err_len: usize, msg: &str, code: c_int) -> c_int {
165    if err.is_null() || err_len == 0 {
166        return code;
167    }
168    let max = err_len - 1;
169    let mut end = msg.len().min(max);
170    let bytes = msg.as_bytes();
171    while end > 0 && end < bytes.len() && (bytes[end] & 0b1100_0000) == 0b1000_0000 {
172        end -= 1;
173    }
174    end = end.min(bytes.len()).min(max);
175    unsafe {
176        std::ptr::copy_nonoverlapping(bytes.as_ptr().cast::<c_char>(), err, end);
177        *err.add(end) = 0;
178    }
179    code
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use std::ffi::CString;
186
187    #[test]
188    fn unknown_profile_is_unreadable() {
189        let xml = CString::new("<Invoice/>").unwrap();
190        let profile = CString::new("xrechnung").unwrap();
191        let mut buf = vec![0 as c_char; 128];
192        let code =
193            core_invoice_validate_ubl(xml.as_ptr(), profile.as_ptr(), buf.as_mut_ptr(), buf.len());
194        assert_eq!(code, 2);
195        let msg = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
196        assert!(msg.contains("unknown profile"), "{msg}");
197        assert!(msg.contains("en16931"), "{msg}");
198    }
199
200    #[test]
201    fn validate_peppol_ok_is_0() {
202        let xml = core_invoice_formats::write_unchecked(
203            &core_invoice_fixtures::peppol_vat(),
204            core_invoice_formats::Syntax::Ubl,
205        )
206        .unwrap();
207        let xml = CString::new(xml).unwrap();
208        let profile = CString::new("peppol").unwrap();
209        let mut buf = vec![0 as c_char; 256];
210        let code =
211            core_invoice_validate(xml.as_ptr(), profile.as_ptr(), buf.as_mut_ptr(), buf.len());
212        assert_eq!(code, 0);
213    }
214
215    #[test]
216    fn validate_empty_number_is_1() {
217        let mut inv = core_invoice_fixtures::peppol_vat();
218        inv.number.clear();
219        let xml =
220            core_invoice_formats::write_unchecked(&inv, core_invoice_formats::Syntax::Ubl).unwrap();
221        let xml = CString::new(xml).unwrap();
222        let profile = CString::new("peppol").unwrap();
223        let mut buf = vec![0 as c_char; 512];
224        let code =
225            core_invoice_validate(xml.as_ptr(), profile.as_ptr(), buf.as_mut_ptr(), buf.len());
226        assert_eq!(code, 1);
227    }
228
229    #[test]
230    fn version_is_nonzero() {
231        let mut buf = vec![0 as c_char; 32];
232        let code = core_invoice_version(buf.as_mut_ptr(), buf.len());
233        assert_eq!(code, 0);
234        let msg = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }.to_string_lossy();
235        assert!(!msg.is_empty());
236    }
237}