1use crate::extract::{extract_xfa_from_bytes, XfaPackets};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum XfaType {
34 None,
36 Static,
39 Dynamic,
41}
42
43impl std::fmt::Display for XfaType {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 XfaType::None => write!(f, "None"),
47 XfaType::Static => write!(f, "Static"),
48 XfaType::Dynamic => write!(f, "Dynamic"),
49 }
50 }
51}
52
53pub fn detect_xfa_type_from_packets(packets: &XfaPackets) -> XfaType {
57 if packets.packets.is_empty() && packets.full_xml.is_none() {
59 return XfaType::None;
60 }
61
62 let template_xml: Option<&str> = packets.template();
65 let full_xml: Option<&str> = packets.full_xml.as_deref();
66
67 let search_text: &str = template_xml.or(full_xml).unwrap_or("");
68
69 if search_text.contains(r#"baseProfile="interactiveForms""#) {
70 XfaType::Static
71 } else if !search_text.is_empty() || packets.packets.iter().any(|(name, _)| name == "template")
72 {
73 XfaType::Dynamic
74 } else {
75 XfaType::Dynamic
78 }
79}
80
81pub fn detect_xfa_type(pdf_bytes: &[u8]) -> XfaType {
86 match extract_xfa_from_bytes(pdf_bytes.to_vec()) {
88 Ok(packets) => detect_xfa_type_from_packets(&packets),
89 Err(_) => XfaType::None,
90 }
91}
92
93#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::extract::XfaPackets;
99
100 fn packets_from_xml(xml: &str) -> XfaPackets {
101 let mut p = XfaPackets {
104 full_xml: Some(xml.to_string()),
105 ..Default::default()
106 };
107 if xml.contains("<template") {
109 let start = xml.find("<template").unwrap();
110 let end = xml
112 .find("</template>")
113 .map(|i| i + "</template>".len())
114 .or({
115 Some(xml.len())
117 })
118 .unwrap();
119 p.packets
120 .push(("template".to_string(), xml[start..end].to_string()));
121 }
122 p
123 }
124
125 #[test]
126 fn empty_packets_returns_none() {
127 let p = XfaPackets::default();
128 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::None);
129 }
130
131 #[test]
132 fn static_form_detected_via_base_profile() {
133 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>"#;
134 let p = packets_from_xml(xml);
135 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
136 }
137
138 #[test]
139 fn dynamic_form_detected_when_no_base_profile() {
140 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>"#;
141 let p = packets_from_xml(xml);
142 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
143 }
144
145 #[test]
146 fn xfa_type_none_on_empty_pdf_bytes() {
147 assert_eq!(detect_xfa_type(&[]), XfaType::None);
149 }
150
151 #[test]
152 fn xfa_type_display() {
153 assert_eq!(XfaType::None.to_string(), "None");
154 assert_eq!(XfaType::Static.to_string(), "Static");
155 assert_eq!(XfaType::Dynamic.to_string(), "Dynamic");
156 }
157
158 #[test]
159 fn packets_with_only_full_xml_static() {
160 let xml = r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template baseProfile="interactiveForms"><subform/></template></xdp:xdp>"#;
162 let p = XfaPackets {
163 full_xml: Some(xml.to_string()),
164 ..Default::default()
165 };
166 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
168 }
169
170 #[test]
171 fn datasets_only_packet_treated_as_dynamic() {
172 let mut p = XfaPackets::default();
173 p.packets
174 .push(("datasets".to_string(), "<xfa:datasets/>".to_string()));
175 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
177 }
178}