1use crate::extract::{extract_xfa_from_bytes, XfaPackets};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum XfaType {
28 None,
30 Static,
33 Dynamic,
35}
36
37impl std::fmt::Display for XfaType {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 match self {
40 XfaType::None => write!(f, "None"),
41 XfaType::Static => write!(f, "Static"),
42 XfaType::Dynamic => write!(f, "Dynamic"),
43 }
44 }
45}
46
47pub fn detect_xfa_type_from_packets(packets: &XfaPackets) -> XfaType {
51 if packets.packets.is_empty() && packets.full_xml.is_none() {
53 return XfaType::None;
54 }
55
56 let template_xml: Option<&str> = packets.template();
59 let full_xml: Option<&str> = packets.full_xml.as_deref();
60
61 let search_text: &str = template_xml.or(full_xml).unwrap_or("");
62
63 if search_text.contains(r#"baseProfile="interactiveForms""#) {
64 XfaType::Static
65 } else if !search_text.is_empty() || packets.packets.iter().any(|(name, _)| name == "template")
66 {
67 XfaType::Dynamic
68 } else {
69 XfaType::Dynamic
72 }
73}
74
75pub fn detect_xfa_type(pdf_bytes: &[u8]) -> XfaType {
80 match extract_xfa_from_bytes(pdf_bytes.to_vec()) {
82 Ok(packets) => detect_xfa_type_from_packets(&packets),
83 Err(_) => XfaType::None,
84 }
85}
86
87#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::extract::XfaPackets;
93
94 fn packets_from_xml(xml: &str) -> XfaPackets {
95 let mut p = XfaPackets {
98 full_xml: Some(xml.to_string()),
99 ..Default::default()
100 };
101 if xml.contains("<template") {
103 let start = xml.find("<template").unwrap();
104 let end = xml
106 .find("</template>")
107 .map(|i| i + "</template>".len())
108 .or({
109 Some(xml.len())
111 })
112 .unwrap();
113 p.packets
114 .push(("template".to_string(), xml[start..end].to_string()));
115 }
116 p
117 }
118
119 #[test]
120 fn empty_packets_returns_none() {
121 let p = XfaPackets::default();
122 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::None);
123 }
124
125 #[test]
126 fn static_form_detected_via_base_profile() {
127 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/" baseProfile="interactiveForms"><subform name="root"/></template></xdp:xdp>"#;
128 let p = packets_from_xml(xml);
129 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
130 }
131
132 #[test]
133 fn dynamic_form_detected_when_no_base_profile() {
134 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"><occur min="0" max="-1"/></subform></template></xdp:xdp>"#;
135 let p = packets_from_xml(xml);
136 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
137 }
138
139 #[test]
140 fn xfa_type_none_on_empty_pdf_bytes() {
141 assert_eq!(detect_xfa_type(&[]), XfaType::None);
143 }
144
145 #[test]
146 fn xfa_type_display() {
147 assert_eq!(XfaType::None.to_string(), "None");
148 assert_eq!(XfaType::Static.to_string(), "Static");
149 assert_eq!(XfaType::Dynamic.to_string(), "Dynamic");
150 }
151
152 #[test]
153 fn packets_with_only_full_xml_static() {
154 let xml = r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template baseProfile="interactiveForms"><subform/></template></xdp:xdp>"#;
156 let p = XfaPackets {
157 full_xml: Some(xml.to_string()),
158 ..Default::default()
159 };
160 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
162 }
163
164 #[test]
165 fn datasets_only_packet_treated_as_dynamic() {
166 let mut p = XfaPackets::default();
167 p.packets
168 .push(("datasets".to_string(), "<xfa:datasets/>".to_string()));
169 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
171 }
172}