use crate::ffi::{Handle, HandleStore};
use std::ffi::{CStr, CString, c_char};
use std::ptr;
use std::sync::LazyLock;
type ContextHandle = Handle;
type DocumentHandle = Handle;
type BufferHandle = Handle;
pub const PDF_NOT_ZUGFERD: i32 = 0;
pub const PDF_ZUGFERD_COMFORT: i32 = 1;
pub const PDF_ZUGFERD_BASIC: i32 = 2;
pub const PDF_ZUGFERD_EXTENDED: i32 = 3;
pub const PDF_ZUGFERD_BASIC_WL: i32 = 4;
pub const PDF_ZUGFERD_MINIMUM: i32 = 5;
pub const PDF_ZUGFERD_XRECHNUNG: i32 = 6;
pub const PDF_ZUGFERD_UNKNOWN: i32 = 7;
pub const PDF_FACTURX_MINIMUM: i32 = PDF_ZUGFERD_MINIMUM;
pub const PDF_FACTURX_BASIC_WL: i32 = PDF_ZUGFERD_BASIC_WL;
pub const PDF_FACTURX_BASIC: i32 = PDF_ZUGFERD_BASIC;
pub const PDF_FACTURX_EN16931: i32 = PDF_ZUGFERD_COMFORT;
pub const PDF_FACTURX_EXTENDED: i32 = PDF_ZUGFERD_EXTENDED;
#[derive(Debug, Clone)]
pub struct ZugferdInfo {
pub profile: i32,
pub version: f32,
pub conformance: String,
pub xml_filename: String,
pub has_xmp: bool,
}
impl Default for ZugferdInfo {
fn default() -> Self {
Self::new()
}
}
impl ZugferdInfo {
pub fn new() -> Self {
Self {
profile: PDF_NOT_ZUGFERD,
version: 0.0,
conformance: String::new(),
xml_filename: String::new(),
has_xmp: false,
}
}
pub fn is_zugferd(&self) -> bool {
self.profile != PDF_NOT_ZUGFERD
}
}
#[derive(Debug, Clone)]
pub struct InvoiceData {
pub xml: Vec<u8>,
pub mime_type: String,
pub filename: String,
pub created: i64,
pub modified: i64,
}
impl Default for InvoiceData {
fn default() -> Self {
Self::new()
}
}
impl InvoiceData {
pub fn new() -> Self {
Self {
xml: Vec::new(),
mime_type: "text/xml".to_string(),
filename: "factur-x.xml".to_string(),
created: 0,
modified: 0,
}
}
pub fn with_xml(mut self, xml: &[u8]) -> Self {
self.xml = xml.to_vec();
self
}
pub fn with_filename(mut self, filename: &str) -> Self {
self.filename = filename.to_string();
self
}
}
pub struct ZugferdContext {
pub document: DocumentHandle,
pub info: Option<ZugferdInfo>,
pub xml_data: Option<Vec<u8>>,
pub validation: Option<ZugferdValidation>,
}
impl ZugferdContext {
pub fn new(document: DocumentHandle) -> Self {
Self {
document,
info: None,
xml_data: None,
validation: None,
}
}
}
pub static ZUGFERD_CONTEXTS: LazyLock<HandleStore<ZugferdContext>> =
LazyLock::new(HandleStore::new);
#[unsafe(no_mangle)]
pub extern "C" fn pdf_new_zugferd_context(_ctx: ContextHandle, doc: DocumentHandle) -> Handle {
let context = ZugferdContext::new(doc);
ZUGFERD_CONTEXTS.insert(context)
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_drop_zugferd_context(_ctx: ContextHandle, zugferd: Handle) {
ZUGFERD_CONTEXTS.remove(zugferd);
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_profile(
_ctx: ContextHandle,
zugferd: Handle,
version_out: *mut f32,
) -> i32 {
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let mut zctx = zctx.lock().unwrap();
if let Some(ref info) = zctx.info {
if !version_out.is_null() {
unsafe {
*version_out = info.version;
}
}
return info.profile;
}
let mut info = ZugferdInfo::new();
if let Some(doc_arc) = crate::ffi::DOCUMENTS.get(zctx.document) {
if let Ok(doc_guard) = doc_arc.lock() {
let data = doc_guard.data();
let text = String::from_utf8_lossy(data);
let has_embedded_files = text.contains("/EmbeddedFiles");
let has_facturx_xml = text.contains("factur-x.xml")
|| text.contains("Factur-X.xml")
|| text.contains("FACTUR-X.XML");
let has_zugferd_xml =
text.contains("zugferd-invoice.xml") || text.contains("ZUGFeRD-invoice.xml");
if has_embedded_files && (has_facturx_xml || has_zugferd_xml) {
info.xml_filename = if has_facturx_xml {
"factur-x.xml".to_string()
} else {
"ZUGFeRD-invoice.xml".to_string()
};
info.has_xmp = text.contains("<x:xmpmeta") || text.contains("xpacket");
let profile_and_version = detect_zugferd_profile_from_metadata(&text);
info.profile = profile_and_version.0;
info.version = profile_and_version.1;
info.conformance = profile_and_version.2;
} else if has_facturx_xml || has_zugferd_xml {
info.xml_filename = if has_facturx_xml {
"factur-x.xml".to_string()
} else {
"ZUGFeRD-invoice.xml".to_string()
};
let profile_and_version = detect_zugferd_profile_from_metadata(&text);
info.profile = profile_and_version.0;
info.version = profile_and_version.1;
info.conformance = profile_and_version.2;
}
}
}
if info.profile == PDF_NOT_ZUGFERD {
if let Some(ref xml) = zctx.xml_data {
let xml_text = String::from_utf8_lossy(xml);
if xml_text.contains("CrossIndustryInvoice")
|| xml_text.contains("CrossIndustryDocument")
{
info.profile = PDF_ZUGFERD_UNKNOWN;
info.version = 2.0;
info.xml_filename = "factur-x.xml".to_string();
}
}
}
let profile = info.profile;
let version = info.version;
zctx.info = Some(info);
if !version_out.is_null() {
unsafe {
*version_out = version;
}
}
return profile;
}
PDF_NOT_ZUGFERD
}
fn detect_zugferd_profile_from_metadata(text: &str) -> (i32, f32, String) {
let conformance = extract_xml_value(text, "ConformanceLevel")
.or_else(|| extract_xml_value(text, "fx:ConformanceLevel"))
.or_else(|| extract_xml_value(text, "zf:ConformanceLevel"))
.unwrap_or_default()
.to_uppercase();
let version_str = extract_xml_value(text, "fx:Version")
.or_else(|| extract_xml_value(text, "zf:Version"))
.unwrap_or_default();
let version = version_str.parse::<f32>().unwrap_or(2.0);
let profile = match conformance.as_str() {
"MINIMUM" => PDF_ZUGFERD_MINIMUM,
"BASIC WL" | "BASICWL" | "BASIC_WL" => PDF_ZUGFERD_BASIC_WL,
"BASIC" => PDF_ZUGFERD_BASIC,
"EN 16931" | "EN16931" | "COMFORT" => PDF_ZUGFERD_COMFORT,
"EXTENDED" => PDF_ZUGFERD_EXTENDED,
"XRECHNUNG" => PDF_ZUGFERD_XRECHNUNG,
"" => {
if text.contains("urn:factur-x") || text.contains("factur-x.xml") {
PDF_ZUGFERD_UNKNOWN
} else if text.contains("ZUGFeRD") || text.contains("zugferd") {
PDF_ZUGFERD_UNKNOWN
} else {
PDF_NOT_ZUGFERD
}
}
_ => PDF_ZUGFERD_UNKNOWN,
};
(profile, version, conformance)
}
fn extract_xml_value(text: &str, tag: &str) -> Option<String> {
let open = format!("<{}>", tag);
let close = format!("</{}>", tag);
if let Some(start) = text.find(&open) {
let val_start = start + open.len();
if let Some(end) = text[val_start..].find(&close) {
let value = text[val_start..val_start + end].trim().to_string();
if !value.is_empty() {
return Some(value);
}
}
}
None
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_is_zugferd(_ctx: ContextHandle, zugferd: Handle) -> i32 {
let mut version: f32 = 0.0;
let profile = pdf_zugferd_profile(_ctx, zugferd, &mut version);
if profile != PDF_NOT_ZUGFERD { 1 } else { 0 }
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_version(_ctx: ContextHandle, zugferd: Handle) -> f32 {
let mut version: f32 = 0.0;
pdf_zugferd_profile(_ctx, zugferd, &mut version);
version
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_xml(
_ctx: ContextHandle,
zugferd: Handle,
len_out: *mut usize,
) -> *const u8 {
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let zctx = zctx.lock().unwrap();
if let Some(ref xml_data) = zctx.xml_data {
if !len_out.is_null() {
unsafe {
*len_out = xml_data.len();
}
}
return xml_data.as_ptr();
}
}
if !len_out.is_null() {
unsafe {
*len_out = 0;
}
}
ptr::null()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_set_xml(
_ctx: ContextHandle,
zugferd: Handle,
xml: *const u8,
len: usize,
) -> i32 {
if xml.is_null() || len == 0 {
return 0;
}
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let mut zctx = zctx.lock().unwrap();
unsafe {
let data = std::slice::from_raw_parts(xml, len);
zctx.xml_data = Some(data.to_vec());
}
return 1;
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_profile_to_string(_ctx: ContextHandle, profile: i32) -> *mut c_char {
let s = match profile {
PDF_NOT_ZUGFERD => "Not ZUGFeRD",
PDF_ZUGFERD_COMFORT => "ZUGFeRD Comfort (EN16931)",
PDF_ZUGFERD_BASIC => "ZUGFeRD Basic",
PDF_ZUGFERD_EXTENDED => "ZUGFeRD Extended",
PDF_ZUGFERD_BASIC_WL => "ZUGFeRD Basic WL",
PDF_ZUGFERD_MINIMUM => "ZUGFeRD Minimum",
PDF_ZUGFERD_XRECHNUNG => "ZUGFeRD XRechnung",
PDF_ZUGFERD_UNKNOWN => "ZUGFeRD Unknown",
_ => "Invalid Profile",
};
if let Ok(cstr) = CString::new(s) {
return cstr.into_raw();
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_free_string(s: *mut c_char) {
if !s.is_null() {
unsafe {
drop(CString::from_raw(s));
}
}
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct ZugferdEmbedParams {
pub profile: i32,
pub version: f32,
pub filename: *const c_char,
pub add_checksum: i32,
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_default_embed_params() -> ZugferdEmbedParams {
ZugferdEmbedParams {
profile: PDF_ZUGFERD_COMFORT,
version: 2.2,
filename: ptr::null(),
add_checksum: 1,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_embed(
_ctx: ContextHandle,
zugferd: Handle,
xml: *const u8,
xml_len: usize,
params: *const ZugferdEmbedParams,
) -> i32 {
if xml.is_null() || xml_len == 0 {
return 0;
}
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let mut zctx = zctx.lock().unwrap();
unsafe {
let data = std::slice::from_raw_parts(xml, xml_len);
zctx.xml_data = Some(data.to_vec());
}
let profile = if !params.is_null() {
unsafe { (*params).profile }
} else {
PDF_ZUGFERD_COMFORT
};
let version = if !params.is_null() {
unsafe { (*params).version }
} else {
2.2
};
let filename = if !params.is_null() && !unsafe { (*params).filename }.is_null() {
unsafe {
CStr::from_ptr((*params).filename)
.to_string_lossy()
.to_string()
}
} else {
"factur-x.xml".to_string()
};
zctx.info = Some(ZugferdInfo {
profile,
version,
conformance: String::new(),
xml_filename: filename,
has_xmp: true,
});
return 1;
}
0
}
#[derive(Debug, Clone, Default)]
pub struct ZugferdValidation {
pub is_valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_validate(_ctx: ContextHandle, zugferd: Handle) -> i32 {
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let mut zctx = zctx.lock().unwrap();
let xml = match zctx.xml_data {
Some(ref data) => data.clone(),
None => {
zctx.validation = Some(ZugferdValidation {
is_valid: false,
errors: vec!["No XML data available".to_string()],
warnings: Vec::new(),
});
return 0;
}
};
let mut errors = Vec::new();
let mut warnings = Vec::new();
let xml_text = String::from_utf8_lossy(&xml);
let has_xml_decl = xml.starts_with(b"<?xml");
let has_rsm_root = xml_text.contains("<rsm:")
|| xml_text.contains("<CrossIndustryInvoice")
|| xml_text.contains("<CrossIndustryDocument");
if !has_xml_decl && !has_rsm_root {
errors.push(
"XML does not start with <?xml declaration or recognized root element".to_string(),
);
}
let has_invoice_root =
xml_text.contains("CrossIndustryInvoice") || xml_text.contains("CrossIndustryDocument");
if !has_invoice_root && !xml_text.contains("<rsm:") {
if has_xml_decl {
warnings.push(
"Missing CrossIndustryInvoice or CrossIndustryDocument root element"
.to_string(),
);
} else {
errors.push(
"Missing CrossIndustryInvoice or CrossIndustryDocument root element"
.to_string(),
);
}
}
let required_elements = [
(
"ExchangedDocumentContext",
"ExchangedDocumentContext element is required",
),
("ExchangedDocument", "ExchangedDocument element is required"),
(
"SupplyChainTradeTransaction",
"SupplyChainTradeTransaction element is required",
),
];
for (element, message) in &required_elements {
if !xml_text.contains(element) {
warnings.push(message.to_string());
}
}
if !xml_text.contains("ID") && !xml_text.contains("TypeCode") {
warnings.push("Missing ID or TypeCode elements in invoice".to_string());
}
let is_valid = errors.is_empty();
zctx.validation = Some(ZugferdValidation {
is_valid,
errors,
warnings,
});
return if is_valid { 1 } else { 0 };
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_error_count(_ctx: ContextHandle, zugferd: Handle) -> i32 {
if let Some(zctx) = ZUGFERD_CONTEXTS.get(zugferd) {
let zctx = zctx.lock().unwrap();
if let Some(ref validation) = zctx.validation {
return validation.errors.len() as i32;
}
}
0
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_standard_filename(_ctx: ContextHandle, profile: i32) -> *mut c_char {
let filename = match profile {
PDF_ZUGFERD_COMFORT | PDF_ZUGFERD_BASIC | PDF_ZUGFERD_EXTENDED => "ZUGFeRD-invoice.xml",
PDF_ZUGFERD_BASIC_WL | PDF_ZUGFERD_MINIMUM | PDF_ZUGFERD_XRECHNUNG => "factur-x.xml",
_ => "invoice.xml",
};
if let Ok(cstr) = CString::new(filename) {
return cstr.into_raw();
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_mime_type(_ctx: ContextHandle) -> *mut c_char {
if let Ok(cstr) = CString::new("text/xml") {
return cstr.into_raw();
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub extern "C" fn pdf_zugferd_af_relationship(_ctx: ContextHandle) -> *mut c_char {
if let Ok(cstr) = CString::new("Alternative") {
return cstr.into_raw();
}
ptr::null_mut()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_profile_constants() {
assert_eq!(PDF_NOT_ZUGFERD, 0);
assert_eq!(PDF_ZUGFERD_COMFORT, 1);
assert_eq!(PDF_ZUGFERD_BASIC, 2);
assert_eq!(PDF_ZUGFERD_EXTENDED, 3);
assert_eq!(PDF_ZUGFERD_BASIC_WL, 4);
assert_eq!(PDF_ZUGFERD_MINIMUM, 5);
assert_eq!(PDF_ZUGFERD_XRECHNUNG, 6);
assert_eq!(PDF_ZUGFERD_UNKNOWN, 7);
}
#[test]
fn test_facturx_aliases() {
assert_eq!(PDF_FACTURX_MINIMUM, PDF_ZUGFERD_MINIMUM);
assert_eq!(PDF_FACTURX_BASIC_WL, PDF_ZUGFERD_BASIC_WL);
assert_eq!(PDF_FACTURX_BASIC, PDF_ZUGFERD_BASIC);
assert_eq!(PDF_FACTURX_EN16931, PDF_ZUGFERD_COMFORT);
assert_eq!(PDF_FACTURX_EXTENDED, PDF_ZUGFERD_EXTENDED);
}
#[test]
fn test_zugferd_info() {
let info = ZugferdInfo::new();
assert!(!info.is_zugferd());
assert_eq!(info.profile, PDF_NOT_ZUGFERD);
assert_eq!(info.version, 0.0);
}
#[test]
fn test_invoice_data() {
let data = InvoiceData::new()
.with_xml(b"<?xml version=\"1.0\"?>")
.with_filename("test.xml");
assert_eq!(data.xml, b"<?xml version=\"1.0\"?>");
assert_eq!(data.filename, "test.xml");
assert_eq!(data.mime_type, "text/xml");
}
#[test]
fn test_ffi_context() {
let ctx = 0;
let doc = 1;
let zugferd = pdf_new_zugferd_context(ctx, doc);
assert!(zugferd > 0);
assert_eq!(pdf_is_zugferd(ctx, zugferd), 0);
pdf_drop_zugferd_context(ctx, zugferd);
}
#[test]
fn test_ffi_profile_detection() {
let ctx = 0;
let doc = 1;
let zugferd = pdf_new_zugferd_context(ctx, doc);
let mut version: f32 = 0.0;
let profile = pdf_zugferd_profile(ctx, zugferd, &mut version);
assert_eq!(profile, PDF_NOT_ZUGFERD);
pdf_drop_zugferd_context(ctx, zugferd);
}
#[test]
fn test_ffi_profile_to_string() {
let ctx = 0;
let s = pdf_zugferd_profile_to_string(ctx, PDF_ZUGFERD_COMFORT);
assert!(!s.is_null());
unsafe {
let str = CStr::from_ptr(s).to_string_lossy();
assert!(str.contains("Comfort"));
pdf_zugferd_free_string(s);
}
let s = pdf_zugferd_profile_to_string(ctx, PDF_NOT_ZUGFERD);
assert!(!s.is_null());
unsafe {
let str = CStr::from_ptr(s).to_string_lossy();
assert_eq!(str, "Not ZUGFeRD");
pdf_zugferd_free_string(s);
}
}
#[test]
fn test_ffi_xml_handling() {
let ctx = 0;
let doc = 1;
let zugferd = pdf_new_zugferd_context(ctx, doc);
let xml = b"<?xml version=\"1.0\"?><invoice/>";
let result = pdf_zugferd_set_xml(ctx, zugferd, xml.as_ptr(), xml.len());
assert_eq!(result, 1);
let mut len: usize = 0;
let ptr = pdf_zugferd_xml(ctx, zugferd, &mut len);
assert!(!ptr.is_null());
assert_eq!(len, xml.len());
pdf_drop_zugferd_context(ctx, zugferd);
}
#[test]
fn test_ffi_embed() {
let ctx = 0;
let doc = 1;
let zugferd = pdf_new_zugferd_context(ctx, doc);
let xml = b"<?xml version=\"1.0\"?><rsm:CrossIndustryInvoice/>";
let params = pdf_zugferd_default_embed_params();
let result = pdf_zugferd_embed(ctx, zugferd, xml.as_ptr(), xml.len(), ¶ms);
assert_eq!(result, 1);
let mut len: usize = 0;
let ptr = pdf_zugferd_xml(ctx, zugferd, &mut len);
assert!(!ptr.is_null());
assert_eq!(len, xml.len());
pdf_drop_zugferd_context(ctx, zugferd);
}
#[test]
fn test_ffi_validation() {
let ctx = 0;
let doc = 1;
let zugferd = pdf_new_zugferd_context(ctx, doc);
assert_eq!(pdf_zugferd_validate(ctx, zugferd), 0);
let xml = b"<?xml version=\"1.0\"?>";
pdf_zugferd_set_xml(ctx, zugferd, xml.as_ptr(), xml.len());
assert_eq!(pdf_zugferd_validate(ctx, zugferd), 1);
pdf_drop_zugferd_context(ctx, zugferd);
}
#[test]
fn test_ffi_standard_filename() {
let ctx = 0;
let s = pdf_zugferd_standard_filename(ctx, PDF_ZUGFERD_COMFORT);
assert!(!s.is_null());
unsafe {
let str = CStr::from_ptr(s).to_string_lossy();
assert!(str.contains("ZUGFeRD"));
pdf_zugferd_free_string(s);
}
let s = pdf_zugferd_standard_filename(ctx, PDF_ZUGFERD_XRECHNUNG);
assert!(!s.is_null());
unsafe {
let str = CStr::from_ptr(s).to_string_lossy();
assert_eq!(str, "factur-x.xml");
pdf_zugferd_free_string(s);
}
}
#[test]
fn test_ffi_utility() {
let ctx = 0;
let mime = pdf_zugferd_mime_type(ctx);
assert!(!mime.is_null());
unsafe {
let str = CStr::from_ptr(mime).to_string_lossy();
assert_eq!(str, "text/xml");
pdf_zugferd_free_string(mime);
}
let af = pdf_zugferd_af_relationship(ctx);
assert!(!af.is_null());
unsafe {
let str = CStr::from_ptr(af).to_string_lossy();
assert_eq!(str, "Alternative");
pdf_zugferd_free_string(af);
}
}
}