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 scan_for_datasets(pdf: &Pdf, min_len: usize) -> Option<String> {
174 let mut best: Option<String> = None;
175 for obj in pdf.objects() {
176 if let Object::Stream(s) = obj {
177 if is_image_only_stream(&s) {
179 continue;
180 }
181 if let Some(d) = decode_stream(&s) {
182 if d.len() > min_len
183 && d.contains("<xfa:datasets")
184 && best.as_ref().is_none_or(|b| d.len() > b.len())
185 {
186 best = Some(d);
187 }
188 }
189 }
190 }
191 best
192}
193pub fn extract_xfa_from_bytes(data: impl Into<pdf_syntax::PdfData>) -> Result<XfaPackets> {
195 let pdf = Pdf::new(data).map_err(|e| XfaError::LoadFailed(format!("{e:?}")))?;
196 extract_xfa(&pdf)
197}
198pub fn extract_xfa_from_acroform(pdf: &Pdf) -> Option<XfaPackets> {
200 let xref = pdf.xref();
201 let catalog: Dict<'_> = xref.get(xref.root_id())?;
202 let acroform: Dict<'_> = catalog.get(ACRO_FORM)?;
203 if let Some(stream) = acroform.get::<Stream<'_>>(XFA) {
204 return Some(parse_xfa_xml(&decode_stream(&stream)?));
205 }
206 if let Some(array) = acroform.get::<Array<'_>>(XFA) {
207 return Some(extract_from_array(&array));
208 }
209 None
210}
211
212fn extract_from_array(array: &Array<'_>) -> XfaPackets {
213 let mut packets = XfaPackets::default();
214 let items: Vec<Object<'_>> = array.iter::<Object<'_>>().collect();
215 let mut i = 0;
216 while i + 1 < items.len() {
217 let name = match &items[i] {
218 Object::String(s) => std::string::String::from_utf8_lossy(s.as_bytes()).to_string(),
219 Object::Name(n) => std::string::String::from_utf8_lossy(n.as_ref()).to_string(),
220 _ => {
221 i += 1;
222 continue;
223 }
224 };
225 if let Some(c) = match &items[i + 1] {
226 Object::Stream(s) => decode_stream(s),
227 Object::String(s) => {
228 Some(std::string::String::from_utf8_lossy(s.as_bytes()).to_string())
229 }
230 _ => None,
231 } {
232 packets.packets.push((name, c));
233 }
234 i += 2;
235 }
236 packets
237}
238
239fn scan_for_xfa(pdf: &Pdf) -> Result<XfaPackets> {
240 let mut streams_checked = 0u32;
250 for obj in pdf.objects() {
251 if let Object::Stream(s) = obj {
252 if is_image_only_stream(&s) {
253 continue;
254 }
255 streams_checked += 1;
256 if streams_checked > 2000 {
257 break;
258 }
259 if let Some(d) = decode_stream(&s) {
260 if d.contains("<xdp:xdp") {
261 return Ok(parse_xfa_xml(&d));
262 }
263 }
264 }
265 }
266 Err(XfaError::PacketNotFound("no XFA content found".to_string()))
267}
268
269fn decode_stream(stream: &Stream<'_>) -> Option<String> {
270 std::string::String::from_utf8(stream.decoded().ok()?).ok()
271}
272
273fn parse_xfa_xml(xml: &str) -> XfaPackets {
274 let mut packets = XfaPackets {
275 full_xml: Some(xml.to_string()),
276 packets: Vec::new(),
277 };
278 let t = xml.trim();
279 let c = t.find("?>").map(|p| &t[p + 2..]).unwrap_or(t).trim();
280 let inner = match c.find('>') {
281 Some(s) => {
282 let rest = &c[s + 1..];
283 rest.rfind("</xdp:xdp>")
284 .map(|e| &rest[..e])
285 .or_else(|| rest.rfind("</xdp>").map(|e| &rest[..e]))
286 .unwrap_or(rest)
287 }
288 None => return packets,
289 };
290 let mut pos = 0;
291 let bytes = inner.as_bytes();
292 while pos < bytes.len() {
293 while pos < bytes.len() && bytes[pos].is_ascii_whitespace() {
294 pos += 1;
295 }
296 if pos >= bytes.len() {
297 break;
298 }
299 if bytes[pos] != b'<' {
300 pos += 1;
301 continue;
302 }
303 if inner[pos..].starts_with("<!--") {
304 if let Some(e) = inner[pos..].find("-->") {
305 pos += e + 3;
306 continue;
307 }
308 }
309 if inner[pos..].starts_with("<?") {
310 if let Some(e) = inner[pos..].find("?>") {
311 pos += e + 2;
312 continue;
313 }
314 }
315 let ts = pos;
316 pos += 1;
317 let ns = pos;
318 while pos < bytes.len() && bytes[pos] != b'>' && bytes[pos] != b' ' && bytes[pos] != b'/' {
319 pos += 1;
320 }
321 let ft = &inner[ns..pos];
322 let pn = ft.split(':').next_back().unwrap_or(ft);
323 let ct = format!("</{ft}>");
324 let at = format!("</xfa:{pn}>");
325 if let Some(cp) = inner[ts..].find(ct.as_str()) {
326 let ee = ts + cp + ct.len();
327 packets
328 .packets
329 .push((pn.to_string(), inner[ts..ee].to_string()));
330 pos = ee;
331 } else if let Some(cp) = inner[ts..].find(at.as_str()) {
332 let ee = ts + cp + at.len();
333 packets
334 .packets
335 .push((pn.to_string(), inner[ts..ee].to_string()));
336 pos = ee;
337 } else {
338 while pos < bytes.len() && bytes[pos] != b'>' {
339 pos += 1;
340 }
341 pos += 1;
342 }
343 }
344 packets
345}
346
347#[derive(Debug, Clone, Default)]
354pub struct PacketValidation {
355 pub has_template: bool,
357 pub has_datasets: bool,
359 pub has_config: bool,
361 pub template_bytes: usize,
363 pub datasets_bytes: usize,
365 pub packet_names: Vec<String>,
367 pub warnings: Vec<String>,
369}
370
371pub fn validate_xfa_packets(packets: &XfaPackets) -> PacketValidation {
376 let has_template = packets.template().is_some();
377 let has_datasets = packets.datasets().is_some();
378 let has_config = packets.config().is_some();
379
380 let template_bytes = packets.template().map(|s| s.len()).unwrap_or(0);
381 let datasets_bytes = packets.datasets().map(|s| s.len()).unwrap_or(0);
382 let packet_names = packets.packets.iter().map(|(n, _)| n.clone()).collect();
383
384 let mut warnings = Vec::new();
385
386 if !has_template {
387 warnings.push("No template packet found".to_string());
388 } else if template_bytes < 100 {
389 warnings.push(format!(
390 "Template packet is empty (< 100 bytes) — only {template_bytes} bytes"
391 ));
392 }
393
394 if !has_datasets {
395 warnings.push("No datasets packet".to_string());
396 } else if datasets_bytes < 50 {
397 warnings.push(format!(
398 "Datasets packet is suspiciously small (< 50 bytes) — only {datasets_bytes} bytes"
399 ));
400 }
401
402 PacketValidation {
403 has_template,
404 has_datasets,
405 has_config,
406 template_bytes,
407 datasets_bytes,
408 packet_names,
409 warnings,
410 }
411}
412pub fn extract_embedded_fonts(pdf: &Pdf) -> Vec<(String, Vec<u8>)> {
430 use pdf_syntax::object::dict::keys::{FONT_FILE, FONT_FILE2, FONT_FILE3, FONT_NAME, TYPE};
431 use pdf_syntax::object::Name;
432 let mut fonts = Vec::new();
433 for obj in pdf.objects() {
434 let dict = match &obj {
435 Object::Dict(d) => d.clone(),
436 Object::Stream(s) => s.dict().clone(),
437 _ => continue,
438 };
439 if dict
440 .get::<Name>(TYPE)
441 .is_none_or(|n| n.as_ref() != b"FontDescriptor")
442 {
443 continue;
444 }
445 let name = dict
446 .get::<Name>(FONT_NAME)
447 .map(|n| std::string::String::from_utf8_lossy(n.as_ref()).to_string())
448 .unwrap_or_default();
449 for key in [FONT_FILE2, FONT_FILE, FONT_FILE3] {
450 if let Some(s) = dict.get::<Stream<'_>>(key) {
451 if let Ok(d) = s.decoded() {
452 if !d.is_empty() {
453 fonts.push((name.clone(), d));
454 break;
455 }
456 }
457 }
458 }
459 }
460 fonts
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466 #[test]
467 fn parse_xfa_packets() {
468 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>"#;
469 let p = parse_xfa_xml(xml);
470 assert_eq!(p.packets.len(), 2);
471 assert!(p.template().is_some());
472 assert!(p.datasets().is_some());
473 }
474 #[test]
475 fn empty_xfa() {
476 let p = parse_xfa_xml(r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#);
477 assert_eq!(p.packets.len(), 0);
478 }
479
480 #[test]
481 fn get_packet_missing_returns_none() {
482 let p = parse_xfa_xml(r#"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#);
483 assert!(p.get_packet("template").is_none());
484 assert!(p.get_packet("nonexistent").is_none());
485 assert!(p.config().is_none());
486 assert!(p.locale_set().is_none());
487 }
488
489 #[test]
490 fn full_xml_preserved() {
491 let xml =
493 r#"<?xml version="1.0"?><xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"></xdp:xdp>"#;
494 let p = parse_xfa_xml(xml);
495 let stored = p.full_xml.as_deref().unwrap_or("");
496 assert!(stored.contains("xdp:xdp"));
497 }
498
499 #[test]
500 fn config_packet_parsed() {
501 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>"#;
502 let p = parse_xfa_xml(xml);
503 assert_eq!(p.packets.len(), 1);
504 assert!(p.config().is_some());
505 assert!(p.template().is_none());
506 }
507
508 #[test]
509 fn multiple_packets_order_preserved() {
510 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>"#;
512 let p = parse_xfa_xml(xml);
513 assert_eq!(p.packets.len(), 2);
514 assert_eq!(p.packets[0].0, "template");
515 assert_eq!(p.packets[1].0, "datasets");
516 assert!(p.template().is_some());
517 assert!(p.datasets().is_some());
518 }
519
520 #[test]
523 fn validate_complete_packets_no_warnings() {
524 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>"#;
525 let p = parse_xfa_xml(xml);
526 let v = validate_xfa_packets(&p);
527 assert!(v.has_template);
528 assert!(v.has_datasets);
529 assert!(v.template_bytes > 0);
530 assert!(v.datasets_bytes > 0);
531 assert!(
532 v.warnings.is_empty(),
533 "expected no warnings, got: {:?}",
534 v.warnings
535 );
536 }
537
538 #[test]
539 fn validate_missing_template_produces_warning() {
540 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>"#;
541 let p = parse_xfa_xml(xml);
542 let v = validate_xfa_packets(&p);
543 assert!(!v.has_template);
544 assert!(v
545 .warnings
546 .iter()
547 .any(|w| w.contains("No template packet found")));
548 }
549
550 #[test]
551 fn validate_missing_datasets_produces_warning() {
552 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>"#;
553 let p = parse_xfa_xml(xml);
554 let v = validate_xfa_packets(&p);
555 assert!(!v.has_datasets);
556 assert!(v.warnings.iter().any(|w| w.contains("No datasets packet")));
557 }
558
559 #[test]
560 fn validate_tiny_template_produces_warning() {
561 let mut p = XfaPackets::default();
562 p.packets.push(("template".to_string(), "<t/>".to_string()));
563 p.packets.push((
564 "datasets".to_string(),
565 "<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"><xfa:data/></xfa:datasets>".to_string(),
566 ));
567 let v = validate_xfa_packets(&p);
568 assert!(v.warnings.iter().any(|w| w.contains("< 100 bytes")));
569 }
570
571 #[test]
572 fn validate_tiny_datasets_produces_warning() {
573 let mut p = XfaPackets::default();
574 p.packets.push((
576 "template".to_string(),
577 "<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(),
578 ));
579 p.packets
580 .push(("datasets".to_string(), "<ds/>".to_string()));
581 let v = validate_xfa_packets(&p);
582 assert!(v.warnings.iter().any(|w| w.contains("< 50 bytes")));
583 }
584
585 #[test]
586 fn validate_packet_names_list() {
587 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>"#;
588 let p = parse_xfa_xml(xml);
589 let v = validate_xfa_packets(&p);
590 assert!(v.packet_names.contains(&"config".to_string()));
591 assert!(v.packet_names.contains(&"template".to_string()));
592 assert!(v.has_config);
593 }
594
595 #[test]
601 fn corpus_01_static_xfa_form_detection() {
602 use crate::classify::{detect_xfa_type_from_packets, XfaType};
603 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>"#;
604 let p = parse_xfa_xml(xml);
605 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Static);
606 }
607
608 #[test]
610 fn corpus_02_dynamic_xfa_form_detection() {
611 use crate::classify::{detect_xfa_type_from_packets, XfaType};
612 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>"#;
613 let p = parse_xfa_xml(xml);
614 assert_eq!(detect_xfa_type_from_packets(&p), XfaType::Dynamic);
615 }
616
617 #[test]
619 fn corpus_03_multiple_packets() {
620 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>"#;
621 let p = parse_xfa_xml(xml);
622 assert_eq!(p.packets.len(), 3, "should have config, template, datasets");
623 assert!(p.config().is_some());
624 assert!(p.template().is_some());
625 assert!(p.datasets().is_some());
626 }
627
628 #[test]
630 fn corpus_04_template_only_no_datasets() {
631 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>"#;
632 let p = parse_xfa_xml(xml);
633 assert!(p.template().is_some());
634 assert!(p.datasets().is_none());
635 let v = validate_xfa_packets(&p);
636 assert!(!v.has_datasets);
637 assert!(v.warnings.iter().any(|w| w.contains("No datasets")));
638 }
639
640 #[test]
642 fn corpus_05_xfa_with_image_data_in_datasets() {
643 let b64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==";
645 let xml = format!(
646 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>"#
647 );
648 let p = parse_xfa_xml(&xml);
649 assert!(p.template().is_some());
650 assert!(p.datasets().is_some());
651 let ds = p.datasets().unwrap();
652 assert!(ds.contains(b64_image), "datasets should contain image data");
653 }
654
655 #[test]
657 fn corpus_06_non_xfa_pdf_returns_none() {
658 use crate::classify::{detect_xfa_type, XfaType};
659 let not_xfa: &[u8] = b"%PDF-1.4\n%%EOF";
661 assert_eq!(detect_xfa_type(not_xfa), XfaType::None);
662 }
663
664 #[test]
666 fn corpus_07_xfa_with_config_packet() {
667 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>"#;
668 let p = parse_xfa_xml(xml);
669 assert!(p.config().is_some());
670 let cfg = p.config().unwrap();
671 assert!(cfg.contains("packets"));
672 let v = validate_xfa_packets(&p);
673 assert!(v.has_config);
674 }
675
676 #[test]
678 fn corpus_08_empty_datasets_incremental_save_pattern() {
679 let mut p = XfaPackets::default();
681 p.packets.push((
682 "template".to_string(),
683 "<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(),
684 ));
685 p.packets.push((
687 "datasets".to_string(),
688 "<xfa:datasets xmlns:xfa=\"http://www.xfa.org/schema/xfa-data/1.0/\"/>".to_string(),
689 ));
690 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()));
692 let ds = p.datasets().expect("datasets should exist");
694 assert!(
695 ds.contains("29.97"),
696 "should return the larger/filled datasets"
697 );
698 }
699
700 #[test]
702 fn corpus_09_large_template_many_fields() {
703 let fields: String = (1..=20)
705 .map(|i| format!("<field name=\"field{i}\"><ui><textEdit/></ui></field>"))
706 .collect();
707 let xml = format!(
708 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>"#,
709 (1..=20)
710 .map(|i| format!("<field{i}>val{i}</field{i}>"))
711 .collect::<String>()
712 );
713 let p = parse_xfa_xml(&xml);
714 let v = validate_xfa_packets(&p);
715 assert!(v.has_template);
716 assert!(v.has_datasets);
717 assert!(
718 v.template_bytes >= 100,
719 "large template should exceed 100 bytes"
720 );
721 assert!(
722 v.warnings.is_empty(),
723 "no warnings expected: {:?}",
724 v.warnings
725 );
726 }
727
728 #[test]
730 fn corpus_10_xfa_with_locale_set_packet() {
731 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>"#;
732 let p = parse_xfa_xml(xml);
733 assert!(
734 p.locale_set().is_some(),
735 "localeSet packet should be accessible"
736 );
737 assert!(p.template().is_some());
738 let ls = p.locale_set().unwrap();
739 assert!(ls.contains("en_US"));
740 }
741}