1use crate::error::{Result, XfaError};
9use pdf_syntax::object::dict::keys::{ACRO_FORM, XFA};
10use pdf_syntax::object::{Array, Dict, Object, Stream};
11use pdf_syntax::{Filter, Pdf};
12
13fn is_image_only_stream(stream: &Stream<'_>) -> bool {
23 stream.filters().iter().any(|f| {
24 matches!(
25 f,
26 Filter::JpxDecode | Filter::DctDecode | Filter::Jbig2Decode | Filter::CcittFaxDecode
27 )
28 })
29}
30#[derive(Debug, Clone, Default)]
33pub struct XfaPackets {
34 pub full_xml: Option<String>,
36 pub packets: Vec<(String, String)>,
38}
39
40impl XfaPackets {
41 pub fn get_packet(&self, name: &str) -> Option<&str> {
43 self.packets
44 .iter()
45 .find(|(n, _)| n == name)
46 .map(|(_, v)| v.as_str())
47 }
48 pub fn template(&self) -> Option<&str> {
50 self.get_packet("template")
51 }
52 pub fn datasets(&self) -> Option<&str> {
54 self.packets
58 .iter()
59 .filter(|(n, _)| n == "datasets")
60 .max_by_key(|(_, v)| v.len())
61 .map(|(_, v)| v.as_str())
62 }
63 pub fn config(&self) -> Option<&str> {
65 self.get_packet("config")
66 }
67 pub fn locale_set(&self) -> Option<&str> {
69 self.get_packet("localeSet")
70 }
71}
72#[derive(Debug)]
91enum AcroformProbe {
92 XfaFound(XfaPackets),
94 NoAcroform,
100 AcroformNoXfa,
104 Unreadable,
108}
109
110pub fn extract_xfa(pdf: &Pdf) -> Result<XfaPackets> {
112 match probe_acroform_for_xfa(pdf) {
113 AcroformProbe::XfaFound(mut p) => {
114 let current_ds_len = p.datasets().map(|s| s.len()).unwrap_or(0);
118 if current_ds_len < 200 {
119 if let Some(better_ds) = scan_for_datasets(pdf, current_ds_len) {
120 p.packets.push(("datasets".to_string(), better_ds));
121 }
122 }
123 Ok(p)
124 }
125 AcroformProbe::NoAcroform | AcroformProbe::AcroformNoXfa => {
126 Err(XfaError::PacketNotFound("no XFA content found".to_string()))
134 }
135 AcroformProbe::Unreadable => {
136 scan_for_xfa(pdf)
140 }
141 }
142}
143
144fn probe_acroform_for_xfa(pdf: &Pdf) -> AcroformProbe {
150 let xref = pdf.xref();
151 let Some(catalog): Option<Dict<'_>> = xref.get(xref.root_id()) else {
152 return AcroformProbe::Unreadable;
153 };
154 let Some(acroform): Option<Dict<'_>> = catalog.get(ACRO_FORM) else {
155 return AcroformProbe::NoAcroform;
156 };
157 if let Some(stream) = acroform.get::<Stream<'_>>(XFA) {
158 if let Some(decoded) = decode_stream(&stream) {
159 return AcroformProbe::XfaFound(parse_xfa_xml(&decoded));
160 }
161 return AcroformProbe::Unreadable;
164 }
165 if let Some(array) = acroform.get::<Array<'_>>(XFA) {
166 return AcroformProbe::XfaFound(extract_from_array(&array));
167 }
168 AcroformProbe::AcroformNoXfa
169}
170
171fn is_datasets_packet(xml: &str) -> bool {
175 let mut rest = xml.trim_start_matches('\u{feff}').trim_start();
176 loop {
178 if let Some(r) = rest.strip_prefix("<?") {
179 match r.find("?>") {
180 Some(end) => rest = r[end + 2..].trim_start(),
181 None => return false,
182 }
183 } else if let Some(r) = rest.strip_prefix("<!--") {
184 match r.find("-->") {
185 Some(end) => rest = r[end + 3..].trim_start(),
186 None => return false,
187 }
188 } else {
189 break;
190 }
191 }
192 let Some(tag) = rest.strip_prefix('<') else {
193 return false;
194 };
195 let name_end = tag
196 .find(|c: char| c.is_whitespace() || c == '>' || c == '/')
197 .unwrap_or(tag.len());
198 let qname = &tag[..name_end];
199 qname.rsplit(':').next() == Some("datasets")
200}
201
202fn scan_for_datasets(pdf: &Pdf, min_len: usize) -> Option<String> {
205 let mut best: Option<String> = None;
206 for obj in pdf.objects() {
207 if let Object::Stream(s) = obj {
208 if is_image_only_stream(&s) {
210 continue;
211 }
212 if let Some(d) = decode_stream(&s) {
213 if d.len() > min_len
214 && is_datasets_packet(&d)
215 && best.as_ref().is_none_or(|b| d.len() > b.len())
216 {
217 best = Some(d);
218 }
219 }
220 }
221 }
222 best
223}
224pub fn extract_xfa_from_bytes(data: impl Into<pdf_syntax::PdfData>) -> Result<XfaPackets> {
226 let pdf = Pdf::new(data).map_err(|e| XfaError::LoadFailed(format!("{e:?}")))?;
227 extract_xfa(&pdf)
228}
229pub fn extract_xfa_from_acroform(pdf: &Pdf) -> Option<XfaPackets> {
231 let xref = pdf.xref();
232 let catalog: Dict<'_> = xref.get(xref.root_id())?;
233 let acroform: Dict<'_> = catalog.get(ACRO_FORM)?;
234 if let Some(stream) = acroform.get::<Stream<'_>>(XFA) {
235 return Some(parse_xfa_xml(&decode_stream(&stream)?));
236 }
237 if let Some(array) = acroform.get::<Array<'_>>(XFA) {
238 return Some(extract_from_array(&array));
239 }
240 None
241}
242
243fn extract_from_array(array: &Array<'_>) -> XfaPackets {
244 let mut packets = XfaPackets::default();
245 let items: Vec<Object<'_>> = array.iter::<Object<'_>>().collect();
246 let mut i = 0;
247 while i + 1 < items.len() {
248 let name = match &items[i] {
249 Object::String(s) => std::string::String::from_utf8_lossy(s.as_bytes()).to_string(),
250 Object::Name(n) => std::string::String::from_utf8_lossy(n.as_ref()).to_string(),
251 _ => {
252 i += 1;
253 continue;
254 }
255 };
256 if let Some(c) = match &items[i + 1] {
257 Object::Stream(s) => decode_stream(s),
258 Object::String(s) => {
259 Some(std::string::String::from_utf8_lossy(s.as_bytes()).to_string())
260 }
261 _ => None,
262 } {
263 packets.packets.push((name, c));
264 }
265 i += 2;
266 }
267 packets
268}
269
270fn scan_for_xfa(pdf: &Pdf) -> Result<XfaPackets> {
271 let mut streams_checked = 0u32;
281 for obj in pdf.objects() {
282 if let Object::Stream(s) = obj {
283 if is_image_only_stream(&s) {
284 continue;
285 }
286 streams_checked += 1;
287 if streams_checked > 2000 {
288 break;
289 }
290 if let Some(d) = decode_stream(&s) {
291 if d.contains("<xdp:xdp") {
292 return Ok(parse_xfa_xml(&d));
293 }
294 }
295 }
296 }
297 Err(XfaError::PacketNotFound("no XFA content found".to_string()))
298}
299
300fn decode_stream(stream: &Stream<'_>) -> Option<String> {
301 std::string::String::from_utf8(stream.decoded().ok()?).ok()
302}
303
304fn parse_xfa_xml(xml: &str) -> XfaPackets {
305 let mut packets = XfaPackets {
306 full_xml: Some(xml.to_string()),
307 packets: Vec::new(),
308 };
309 let t = xml.trim();
310 let c = t.find("?>").map(|p| &t[p + 2..]).unwrap_or(t).trim();
311 let inner = match c.find('>') {
312 Some(s) => {
313 let rest = &c[s + 1..];
314 rest.rfind("</xdp:xdp>")
315 .map(|e| &rest[..e])
316 .or_else(|| rest.rfind("</xdp>").map(|e| &rest[..e]))
317 .unwrap_or(rest)
318 }
319 None => return packets,
320 };
321 let mut pos = 0;
322 let bytes = inner.as_bytes();
323 while pos < bytes.len() {
324 while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
325 pos += 1;
326 }
327 if pos >= bytes.len() {
328 break;
329 }
330 if bytes[pos] != b'<' {
331 pos += 1;
332 continue;
333 }
334 if inner[pos..].starts_with("<!--") {
335 if let Some(e) = inner[pos..].find("-->") {
336 pos += e + 3;
337 continue;
338 }
339 }
340 if inner[pos..].starts_with("<?") {
341 if let Some(e) = inner[pos..].find("?>") {
342 pos += e + 2;
343 continue;
344 }
345 }
346 let ts = pos;
347 pos += 1;
348 let ns = pos;
349 while pos < bytes.len() && bytes[pos] != b'>' && bytes[pos] != b' ' && bytes[pos] != b'/' {
350 pos += 1;
351 }
352 let ft = &inner[ns..pos];
353 let pn = ft.split(':').next_back().unwrap_or(ft);
354 let ct = format!("</{ft}>");
355 let at = format!("</xfa:{pn}>");
356 if let Some(cp) = inner[ts..].find(ct.as_str()) {
357 let ee = ts + cp + ct.len();
358 packets
359 .packets
360 .push((pn.to_string(), inner[ts..ee].to_string()));
361 pos = ee;
362 } else if let Some(cp) = inner[ts..].find(at.as_str()) {
363 let ee = ts + cp + at.len();
364 packets
365 .packets
366 .push((pn.to_string(), inner[ts..ee].to_string()));
367 pos = ee;
368 } else {
369 while pos < bytes.len() && bytes[pos] != b'>' {
370 pos += 1;
371 }
372 pos += 1;
373 }
374 }
375 packets
376}
377
378#[derive(Debug, Clone, Default)]
385pub struct PacketValidation {
386 pub has_template: bool,
388 pub has_datasets: bool,
390 pub has_config: bool,
392 pub template_bytes: usize,
394 pub datasets_bytes: usize,
396 pub packet_names: Vec<String>,
398 pub warnings: Vec<String>,
400}
401
402pub fn validate_xfa_packets(packets: &XfaPackets) -> PacketValidation {
407 let has_template = packets.template().is_some();
408 let has_datasets = packets.datasets().is_some();
409 let has_config = packets.config().is_some();
410
411 let template_bytes = packets.template().map(|s| s.len()).unwrap_or(0);
412 let datasets_bytes = packets.datasets().map(|s| s.len()).unwrap_or(0);
413 let packet_names = packets.packets.iter().map(|(n, _)| n.clone()).collect();
414
415 let mut warnings = Vec::new();
416
417 if !has_template {
418 warnings.push("No template packet found".to_string());
419 } else if template_bytes < 100 {
420 warnings.push(format!(
421 "Template packet is empty (< 100 bytes) — only {template_bytes} bytes"
422 ));
423 }
424
425 if !has_datasets {
426 warnings.push("No datasets packet".to_string());
427 } else if datasets_bytes < 50 {
428 warnings.push(format!(
429 "Datasets packet is suspiciously small (< 50 bytes) — only {datasets_bytes} bytes"
430 ));
431 }
432
433 PacketValidation {
434 has_template,
435 has_datasets,
436 has_config,
437 template_bytes,
438 datasets_bytes,
439 packet_names,
440 warnings,
441 }
442}
443pub fn extract_embedded_fonts(pdf: &Pdf) -> Vec<(String, Vec<u8>)> {
461 use pdf_syntax::object::dict::keys::{FONT_FILE, FONT_FILE2, FONT_FILE3, FONT_NAME, TYPE};
462 use pdf_syntax::object::Name;
463 let mut fonts = Vec::new();
464 for obj in pdf.objects() {
465 let dict = match &obj {
466 Object::Dict(d) => d.clone(),
467 Object::Stream(s) => s.dict().clone(),
468 _ => continue,
469 };
470 if dict
471 .get::<Name>(TYPE)
472 .is_none_or(|n| n.as_ref() != b"FontDescriptor")
473 {
474 continue;
475 }
476 let name = dict
477 .get::<Name>(FONT_NAME)
478 .map(|n| std::string::String::from_utf8_lossy(n.as_ref()).to_string())
479 .unwrap_or_default();
480 for key in [FONT_FILE2, FONT_FILE, FONT_FILE3] {
481 if let Some(s) = dict.get::<Stream<'_>>(key) {
482 if let Ok(d) = s.decoded() {
483 if !d.is_empty() {
484 fonts.push((name.clone(), d));
485 break;
486 }
487 }
488 }
489 }
490 }
491 fonts
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 #[test]
498 fn parse_xfa_packets() {
499 let xml = r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="f1"><field name="T1"/></subform></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><f1><T1>Hi</T1></f1></xfa:data></xfa:datasets></xdp:xdp>"#;
500 let p = parse_xfa_xml(xml);
501 assert_eq!(p.packets.len(), 2);
502 assert!(p.template().is_some());
503 assert!(p.datasets().is_some());
504 }
505 #[test]
506 fn empty_xfa() {
507 let p = parse_xfa_xml(r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#);
508 assert_eq!(p.packets.len(), 0);
509 }
510
511 #[test]
512 fn get_packet_missing_returns_none() {
513 let p = parse_xfa_xml(r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#);
514 assert!(p.get_packet("template").is_none());
515 assert!(p.get_packet("nonexistent").is_none());
516 assert!(p.config().is_none());
517 assert!(p.locale_set().is_none());
518 }
519
520 #[test]
521 fn full_xml_preserved() {
522 let xml =
524 r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#;
525 let p = parse_xfa_xml(xml);
526 let stored = p.full_xml.as_deref().unwrap_or("");
527 assert!(stored.contains("xdp:xdp"));
528 }
529
530 #[test]
531 fn config_packet_parsed() {
532 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><config xmlns="http://www.xfa.org/schema/xci/3.1/"><present><xdp><packets>*</packets></xdp></present></config></xdp:xdp>"#;
533 let p = parse_xfa_xml(xml);
534 assert_eq!(p.packets.len(), 1);
535 assert!(p.config().is_some());
536 assert!(p.template().is_none());
537 }
538
539 #[test]
540 fn multiple_packets_order_preserved() {
541 let xml = r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"/></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data/></xfa:datasets></xdp:xdp>"#;
543 let p = parse_xfa_xml(xml);
544 assert_eq!(p.packets.len(), 2);
545 assert_eq!(p.packets[0].0, "template");
546 assert_eq!(p.packets[1].0, "datasets");
547 assert!(p.template().is_some());
548 assert!(p.datasets().is_some());
549 }
550
551 #[test]
554 fn validate_complete_packets_no_warnings() {
555 let xml = r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"><field name="firstName" xmlns:ui="http://www.xfa.org/schema/xfa-template/3.3/"><ui><textEdit/></ui></field></subform></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><root><firstName>Alice</firstName></root></xfa:data></xfa:datasets></xdp:xdp>"#;
556 let p = parse_xfa_xml(xml);
557 let v = validate_xfa_packets(&p);
558 assert!(v.has_template);
559 assert!(v.has_datasets);
560 assert!(v.template_bytes > 0);
561 assert!(v.datasets_bytes > 0);
562 assert!(
563 v.warnings.is_empty(),
564 "expected no warnings, got: {:?}",
565 v.warnings
566 );
567 }
568
569 #[test]
570 fn validate_missing_template_produces_warning() {
571 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data/></xfa:datasets></xdp:xdp>"#;
572 let p = parse_xfa_xml(xml);
573 let v = validate_xfa_packets(&p);
574 assert!(!v.has_template);
575 assert!(v
576 .warnings
577 .iter()
578 .any(|w| w.contains("No template packet found")));
579 }
580
581 #[test]
582 fn validate_missing_datasets_produces_warning() {
583 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"><field name="x"/><field name="y"/><field name="z"/><field name="w"/></subform></template></xdp:xdp>"#;
584 let p = parse_xfa_xml(xml);
585 let v = validate_xfa_packets(&p);
586 assert!(!v.has_datasets);
587 assert!(v.warnings.iter().any(|w| w.contains("No datasets packet")));
588 }
589
590 #[test]
591 fn validate_tiny_template_produces_warning() {
592 let mut p = XfaPackets::default();
593 p.packets.push(("template".to_string(), "<t/>".to_string()));
594 p.packets.push((
595 "datasets".to_string(),
596 "<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data/></xfa:datasets>".to_string(),
597 ));
598 let v = validate_xfa_packets(&p);
599 assert!(v.warnings.iter().any(|w| w.contains("< 100 bytes")));
600 }
601
602 #[test]
603 fn validate_tiny_datasets_produces_warning() {
604 let mut p = XfaPackets::default();
605 p.packets.push((
607 "template".to_string(),
608 "<template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"root\"><field name=\"a\"/><field name=\"b\"/><field name=\"c\"/></subform></template>".to_string(),
609 ));
610 p.packets
611 .push(("datasets".to_string(), "<ds/>".to_string()));
612 let v = validate_xfa_packets(&p);
613 assert!(v.warnings.iter().any(|w| w.contains("< 50 bytes")));
614 }
615
616 #[test]
617 fn validate_packet_names_list() {
618 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><config xmlns="http://www.xfa.org/schema/xci/3.1/"><present/></config><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"><field name="f1"/><field name="f2"/><field name="f3"/></subform></template></xdp:xdp>"#;
619 let p = parse_xfa_xml(xml);
620 let v = validate_xfa_packets(&p);
621 assert!(v.packet_names.contains(&"config".to_string()));
622 assert!(v.packet_names.contains(&"template".to_string()));
623 assert!(v.has_config);
624 }
625
626 #[test]
632 fn corpus_01_static_xfa_form_detection() {
633 use crate::classify::{detect_xfa_type_from_packets, XfaType};
634 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="Page1"><field name="LastName"/><field name="FirstName"/></subform></template></xdp:xdp>"#;
635 let p = parse_xfa_xml(xml);
636 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
637 }
638
639 #[test]
641 fn corpus_02_dynamic_xfa_form_detection() {
642 use crate::classify::{detect_xfa_type_from_packets, XfaType};
643 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"/><field name="item"/></subform></template></xdp:xdp>"#;
644 let p = parse_xfa_xml(xml);
645 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
646 }
647
648 #[test]
650 fn corpus_03_multiple_packets() {
651 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><config xmlns="http://www.xfa.org/schema/xci/3.1/"><present><xdp><packets>*</packets></xdp></present></config><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"/></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data/></xfa:datasets></xdp:xdp>"#;
652 let p = parse_xfa_xml(xml);
653 assert_eq!(p.packets.len(), 3, "should have config, template, datasets");
654 assert!(p.config().is_some());
655 assert!(p.template().is_some());
656 assert!(p.datasets().is_some());
657 }
658
659 #[test]
661 fn corpus_04_template_only_no_datasets() {
662 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"><field name="LastName"/><field name="FirstName"/><field name="DOB"/></subform></template></xdp:xdp>"#;
663 let p = parse_xfa_xml(xml);
664 assert!(p.template().is_some());
665 assert!(p.datasets().is_none());
666 let v = validate_xfa_packets(&p);
667 assert!(!v.has_datasets);
668 assert!(v.warnings.iter().any(|w| w.contains("No datasets")));
669 }
670
671 #[test]
673 fn corpus_05_xfa_with_image_data_in_datasets() {
674 let b64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
676 let xml = format!(
677 r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"><field name="photo"><ui><imageEdit/></ui></field></subform></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><root><photo contentType="image/png" href="">{b64_image}</photo></root></xfa:data></xfa:datasets></xdp:xdp>"#
678 );
679 let p = parse_xfa_xml(&xml);
680 assert!(p.template().is_some());
681 assert!(p.datasets().is_some());
682 let ds = p.datasets().unwrap();
683 assert!(ds.contains(b64_image), "datasets should contain image data");
684 }
685
686 #[test]
688 fn corpus_06_non_xfa_pdf_returns_none() {
689 use crate::classify::{detect_xfa_type, XfaType};
690 let not_xfa: &[u8] = b"%PDF-1.4\n%%EOF";
692 assert_eq!(detect_xfa_type(not_xfa), XfaType::None);
693 }
694
695 #[test]
697 fn corpus_07_xfa_with_config_packet() {
698 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><config xmlns="http://www.xfa.org/schema/xci/3.1/"><present><xdp><packets>*</packets></xdp></present><pdf><version>1.6</version></pdf></config><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"/></template></xdp:xdp>"#;
699 let p = parse_xfa_xml(xml);
700 assert!(p.config().is_some());
701 let cfg = p.config().unwrap();
702 assert!(cfg.contains("packets"));
703 let v = validate_xfa_packets(&p);
704 assert!(v.has_config);
705 }
706
707 #[test]
709 fn corpus_08_empty_datasets_incremental_save_pattern() {
710 let mut p = XfaPackets::default();
712 p.packets.push((
713 "template".to_string(),
714 "<template xmlns=\"http://www.xfa.org/schema/xfa-template/3.3/\"><subform name=\"root\"><field name=\"qty\"/><field name=\"price\"/><field name=\"total\"/></subform></template>".to_string(),
715 ));
716 p.packets.push((
718 "datasets".to_string(),
719 "<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"/>".to_string(),
720 ));
721 p.packets.push(("datasets".to_string(), "<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data><root><qty>3</qty><price>9.99</price><total>29.97</total></root></xfa:data></xfa:datasets>".to_string()));
723 let ds = p.datasets().expect("datasets should exist");
725 assert!(
726 ds.contains("29.97"),
727 "should return the larger/filled datasets"
728 );
729 }
730
731 #[test]
733 fn corpus_09_large_template_many_fields() {
734 let fields: String = (1..=20)
736 .map(|i| format!("<field name=\"field{i}\"><ui><textEdit/></ui></field>"))
737 .collect();
738 let xml = format!(
739 r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root">{fields}</subform></template><xfa:datasets xmlns:xfa="http://www.xfa.org/schema/xfa-data/1.0/"><xfa:data><root>{}</root></xfa:data></xfa:datasets></xdp:xdp>"#,
740 (1..=20)
741 .map(|i| format!("<field{i}>val{i}</field{i}>"))
742 .collect::<String>()
743 );
744 let p = parse_xfa_xml(&xml);
745 let v = validate_xfa_packets(&p);
746 assert!(v.has_template);
747 assert!(v.has_datasets);
748 assert!(
749 v.template_bytes >= 100,
750 "large template should exceed 100 bytes"
751 );
752 assert!(
753 v.warnings.is_empty(),
754 "no warnings expected: {:?}",
755 v.warnings
756 );
757 }
758
759 #[test]
761 fn corpus_10_xfa_with_locale_set_packet() {
762 let xml = r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><localeSet xmlns="http://www.xfa.org/schema/xfa-locale-set/2.7/"><locale name="en_US" desc="English (United States)"><calendarSymbols name="gregorian"/></locale></localeSet><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="root"><field name="date"/></subform></template></xdp:xdp>"#;
763 let p = parse_xfa_xml(xml);
764 assert!(
765 p.locale_set().is_some(),
766 "localeSet packet should be accessible"
767 );
768 assert!(p.template().is_some());
769 let ls = p.locale_set().unwrap();
770 assert!(ls.contains("en_US"));
771 }
772}