1use core_invoice::Profile;
4use core_invoice_formats::{Syntax, convert_with_profile, diff, validate_xml};
5use std::os::raw::{c_char, c_int};
6
7#[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#[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#[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#[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#[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}