1use std::borrow::Cow;
4use std::io::Read;
5
6use anyhow::{anyhow, bail, Context, Result};
7use base64::Engine;
8
9use crate::mesh::{Field, Mesh, MAX_FIELDS};
10
11use super::*;
12
13#[derive(Clone, Copy, PartialEq, Debug)]
14pub(super) enum Section {
15 Points,
16 Cells,
17 Verts,
18 Lines,
19 Strips,
20 Polys,
21 PointData,
22 CellData,
23}
24
25#[derive(Clone, Copy, PartialEq, Debug)]
26pub(super) enum Format {
27 Ascii,
28 Binary,
29 Appended,
30}
31
32pub(super) struct XmlArray<'a> {
33 pub(super) name: String,
34 pub(super) ty: Num,
35 pub(super) components: usize,
36 pub(super) format: Format,
37 pub(super) offset: usize,
38 pub(super) text: Cow<'a, [u8]>,
39}
40
41#[derive(Default)]
42pub(super) struct XmlPiece<'a> {
43 pub(super) points: usize,
44 pub(super) cells: usize,
45 pub(super) poly_counts: [usize; 4],
47 pub(super) point_array: Option<XmlArray<'a>>,
48 pub(super) topology: Vec<(Section, XmlArray<'a>)>,
50 pub(super) point_data: Vec<XmlArray<'a>>,
51 pub(super) cell_data: Vec<XmlArray<'a>>,
52 pub(super) active_scalars: Option<String>,
53}
54
55pub(super) struct XmlFile<'a> {
56 pub(super) data: &'a [u8],
57 pub(super) big_endian: bool,
58 pub(super) header64: bool,
59 pub(super) zlib: bool,
60 pub(super) appended: Option<(usize, bool)>,
62 pub(super) appended_end: usize,
64 pub(super) offsets: Vec<usize>,
66}
67
68pub(super) fn xml_attr(e: &quick_xml::events::BytesStart<'_>, name: &str) -> Option<String> {
69 e.attributes()
70 .flatten()
71 .find(|a| a.key.local_name().as_ref() == name)
72 .and_then(|a| {
73 a.normalized_value(quick_xml::XmlVersion::Implicit1_0)
74 .ok()
75 .map(|v| v.into_owned())
76 })
77}
78
79pub(super) fn find(hay: &[u8], needle: &[u8]) -> Option<usize> {
80 hay.windows(needle.len()).position(|w| w == needle)
81}
82
83pub(super) fn base64_blocks(text: &[u8]) -> Result<Vec<u8>> {
87 let clean: Vec<u8> = text
88 .iter()
89 .copied()
90 .filter(|b| !b.is_ascii_whitespace())
91 .collect();
92 let mut out = Vec::with_capacity(clean.len() / 4 * 3);
93 let mut start = 0;
94 while start < clean.len() {
95 let mut end = start;
96 while clean
97 .get(end..end + 4)
98 .is_some_and(|quad| !quad.contains(&b'='))
99 {
100 end += 4;
101 }
102 end = (end + 4).min(clean.len());
103 base64::engine::general_purpose::STANDARD
104 .decode_vec(clean.get(start..end).unwrap_or_default(), &mut out)
105 .map_err(|e| anyhow!("invalid base64 data: {e}"))?;
106 start = end;
107 }
108 Ok(out)
109}
110
111impl<'a> XmlFile<'a> {
112 pub(super) fn header(&self, bytes: &[u8], i: usize) -> Result<usize> {
113 let size = if self.header64 { 8 } else { 4 };
114 let at = i.checked_mul(size).context("invalid data header")?;
115 let b = bytes
116 .get(at..at + size)
117 .context("the data header is truncated")?;
118 let v = Num::get(
119 if self.header64 { Num::U64 } else { Num::U32 },
120 b,
121 self.big_endian,
122 );
123 usize::try_from(v as u64).map_err(|_| anyhow!("invalid data header"))
124 }
125
126 pub(super) fn payload<'b>(&self, bytes: Cow<'b, [u8]>, need: usize) -> Result<Cow<'b, [u8]>> {
129 let hs = if self.header64 { 8 } else { 4 };
130 if !self.zlib {
131 let n = self.header(&bytes, 0)?;
132 let end = n.checked_add(hs).context("invalid data header")?;
133 let truncated =
134 "the result file is truncated (data block shorter than its header says)";
135 if end > bytes.len() {
136 bail!(truncated);
137 }
138 return Ok(match bytes {
139 Cow::Borrowed(b) => Cow::Borrowed(b.get(hs..end).context(truncated)?),
140 Cow::Owned(mut v) => {
141 v.truncate(end);
142 v.drain(..hs);
143 Cow::Owned(v)
144 }
145 });
146 }
147 let blocks = self.header(&bytes, 0)?;
148 let block_size = self.header(&bytes, 1)?;
149 let last = self.header(&bytes, 2)?;
150 if blocks > bytes.len() / hs {
151 bail!("invalid compressed data header");
152 }
153 let total = if blocks == 0 {
154 0
155 } else {
156 block_size
157 .checked_mul(blocks - 1)
158 .and_then(|t| t.checked_add(if last == 0 { block_size } else { last }))
159 .context("invalid compressed data header")?
160 };
161 if total > MAX_ARRAY_BYTES {
162 bail!("a data array is too large for the preview");
163 }
164 let mut at = (3 + blocks) * hs;
165 if at > bytes.len() {
166 bail!("the compressed data header is truncated");
167 }
168 let want = need.min(total);
169 let mut out = Vec::with_capacity(want.min(bytes.len().saturating_mul(1032)));
172 for i in 0..blocks {
173 if out.len() >= want {
174 break;
175 }
176 let size = self.header(&bytes, 3 + i)?;
177 let end = at.checked_add(size).context("invalid compressed block")?;
178 let block = bytes
179 .get(at..end)
180 .context("the result file is truncated (compressed block)")?;
181 at = end;
182 let full = if i + 1 == blocks && last != 0 {
183 last
184 } else {
185 block_size
186 };
187 let take = full.min(want - out.len());
188 let before = out.len();
189 flate2::read::ZlibDecoder::new(block)
190 .take(take as u64)
191 .read_to_end(&mut out)
192 .map_err(|e| anyhow!("corrupt compressed data: {e}"))?;
193 if out.len() - before != take {
194 bail!("corrupt compressed data (block shorter than declared)");
195 }
196 }
197 Ok(Cow::Owned(out))
198 }
199
200 pub(super) fn values(
202 &self,
203 a: &XmlArray<'_>,
204 count: usize,
205 f: &mut dyn FnMut(f64) -> Result<()>,
206 ) -> Result<()> {
207 let need = count
208 .checked_mul(a.ty.size())
209 .context("data array too large")?;
210 if need > MAX_ARRAY_BYTES {
211 bail!("a data array is too large for the preview");
212 }
213 match a.format {
214 Format::Ascii => {
215 if count > a.text.len() / 2 + 1 {
216 bail!("the result file is truncated (fewer values than declared)");
217 }
218 each_ascii(&a.text, count, f)
219 }
220 Format::Binary => {
221 let bytes = base64_blocks(&a.text)?;
222 let data = self.payload(Cow::Owned(bytes), need)?;
223 each_binary(&data, a.ty, self.big_endian, count, f)
224 }
225 Format::Appended => {
226 let (base, raw) = self
227 .appended
228 .context("a DataArray refers to missing appended data")?;
229 let start = base
230 .checked_add(a.offset)
231 .filter(|&s| s <= self.appended_end)
232 .context("a DataArray offset points outside the appended data")?;
233 let end = self
235 .offsets
236 .get(self.offsets.partition_point(|&o| o <= a.offset))
237 .and_then(|&o| base.checked_add(o))
238 .unwrap_or(self.appended_end)
239 .min(self.appended_end);
240 let region = self.data.get(start..end.max(start)).unwrap_or_default();
241 let data = if raw {
242 self.payload(Cow::Borrowed(region), need)?
243 } else {
244 self.payload(Cow::Owned(base64_blocks(region)?), need)?
245 };
246 each_binary(&data, a.ty, self.big_endian, count, f)
247 }
248 }
249 }
250
251 pub(super) fn tuples(&self, a: &XmlArray<'_>, tuples: usize) -> Result<Field> {
252 let components = a.components;
253 if components == 0 || components > MAX_COMPONENTS {
254 bail!("unsupported number of components ({components})");
255 }
256 let total = tuples.checked_mul(components).context("data too large")?;
257 let mut t = Tuples::new(tuples, components, MAX_NODES);
258 self.values(a, total, &mut |v| t.push(v))?;
259 Ok(Field {
260 name: Reduce::of(components).label(&a.name),
261 unit: String::new(),
262 values: t.out,
263 })
264 }
265
266 pub(super) fn indices(&self, a: &XmlArray<'_>, count: usize) -> Result<Vec<usize>> {
267 let mut out = Vec::new();
268 self.values(a, count, &mut |v| {
269 out.push(index(v)? as usize);
270 Ok(())
271 })?;
272 Ok(out)
273 }
274
275 pub(super) fn topology(
277 &self,
278 grid: &mut Grid,
279 piece: &XmlPiece<'_>,
280 section: Section,
281 cells: usize,
282 base: u32,
283 poly_type: Option<u8>,
284 ) -> Result<()> {
285 if cells == 0 {
286 return Ok(());
287 }
288 let array = |name: &str| {
289 piece
290 .topology
291 .iter()
292 .find(|(s, a)| *s == section && a.name == name)
293 .map(|(_, a)| a)
294 .with_context(|| format!("the {section:?} section has no '{name}' array"))
295 };
296 grid.check_cells(cells, 0)?;
297 let offsets = self.indices(array("offsets")?, cells)?;
298 let total = offsets.last().copied().unwrap_or(0);
299 grid.check_cells(cells, total)?;
300 let start = grid.conn.len();
301 let conn = &mut grid.conn;
302 self.values(array("connectivity")?, total, &mut |v| {
303 let i = index(v)?.checked_add(base).context("too many points")?;
304 conn.push(i);
305 Ok(())
306 })?;
307 let mut prev = 0;
308 for &o in &offsets {
309 if o < prev || o > total {
310 bail!("invalid cell offsets");
311 }
312 prev = o;
313 grid.ends.push(start + o);
314 }
315 match poly_type {
316 Some(ty) => grid.types.extend(std::iter::repeat_n(ty, cells)),
317 None => {
318 let types = &mut grid.types;
319 self.values(array("types")?, cells, &mut |v| {
320 types.push(if (0.0..=255.0).contains(&v) {
321 v as u8
322 } else {
323 0
324 });
325 Ok(())
326 })?;
327 }
328 }
329 Ok(())
330 }
331}
332
333pub(super) fn merge_fields(into: &mut Vec<Field>, mut new: Vec<Field>, first: bool, count: usize) {
338 if first {
339 into.clear();
340 for f in new {
341 Grid::add_field(into, f);
342 }
343 return;
344 }
345 for f in into.iter_mut() {
346 match new.iter_mut().find(|n| n.name == f.name) {
347 Some(n) => f.values.append(&mut n.values),
348 None => f.values.extend(std::iter::repeat_n(f32::NAN, count)),
349 }
350 }
351}
352
353pub fn vtk_xml(data: &[u8]) -> Result<Mesh> {
355 use quick_xml::events::Event;
356 let appended_at = find(data, b"<AppendedData");
358 let xml = appended_at.and_then(|at| data.get(..at)).unwrap_or(data);
359 let from = |i: usize| data.get(i..).unwrap_or_default();
361 let mut file = XmlFile {
362 data,
363 big_endian: false,
364 header64: false,
365 zlib: false,
366 appended: None,
367 appended_end: data.len(),
368 offsets: Vec::new(),
369 };
370 if let Some(at) = appended_at {
371 let tag_end = from(at)
372 .iter()
373 .position(|&b| b == b'>')
374 .map(|p| at + p)
375 .context("unterminated AppendedData tag")?;
376 let tag = String::from_utf8_lossy(data.get(at..tag_end).unwrap_or_default());
377 let raw = !tag.contains("base64");
378 let underscore = from(tag_end)
379 .iter()
380 .position(|&b| b == b'_')
381 .map(|p| tag_end + p + 1)
382 .context("AppendedData without the '_' marker")?;
383 file.appended = Some((underscore, raw));
384 if let Some(end) = from(underscore)
385 .windows(b"</AppendedData".len())
386 .rposition(|w| w == b"</AppendedData")
387 {
388 file.appended_end = underscore + end;
389 }
390 }
391
392 let mut reader = quick_xml::Reader::from_reader(xml);
393 let mut kind: Option<String> = None;
394 let mut pieces: Vec<XmlPiece<'_>> = Vec::new();
395 let mut section: Option<Section> = None;
396 let mut array: Option<(Section, XmlArray<'_>)> = None;
397 let mut nested = 0usize;
398 loop {
399 let event = reader.read_event().context("invalid VTK XML")?;
400 match event {
401 Event::Start(ref e) | Event::Empty(ref e) => {
402 let empty = matches!(event, Event::Empty(_));
403 if array.is_some() {
404 if !empty {
405 nested += 1;
406 }
407 continue;
408 }
409 match e.local_name().as_ref() {
410 "VTKFile" => {
411 kind = xml_attr(e, "type");
412 file.big_endian = xml_attr(e, "byte_order").as_deref() == Some("BigEndian");
413 file.header64 = xml_attr(e, "header_type").as_deref() == Some("UInt64");
414 match xml_attr(e, "compressor").as_deref() {
415 None | Some("") => {}
416 Some("vtkZLibDataCompressor") => file.zlib = true,
417 Some(other) => bail!(
418 "The 3D preview reads uncompressed and zlib-compressed VTK files, not {}.",
419 short(other)
420 ),
421 }
422 }
423 "Piece" => {
424 if pieces.len() >= 100_000 {
425 bail!("too many pieces");
426 }
427 let n = |name: &str| -> Result<usize> {
428 match xml_attr(e, name) {
429 None => Ok(0),
430 Some(v) => {
431 v.trim().parse().map_err(|_| anyhow!("invalid piece size"))
432 }
433 }
434 };
435 pieces.push(XmlPiece {
436 points: n("NumberOfPoints")?,
437 cells: n("NumberOfCells")?,
438 poly_counts: [
439 n("NumberOfVerts")?,
440 n("NumberOfLines")?,
441 n("NumberOfStrips")?,
442 n("NumberOfPolys")?,
443 ],
444 ..XmlPiece::default()
445 });
446 }
447 name => {
448 let s = match name {
449 "Points" => Some(Section::Points),
450 "Cells" => Some(Section::Cells),
451 "Verts" => Some(Section::Verts),
452 "Lines" => Some(Section::Lines),
453 "Strips" => Some(Section::Strips),
454 "Polys" => Some(Section::Polys),
455 "PointData" => Some(Section::PointData),
456 "CellData" => Some(Section::CellData),
457 _ => None,
458 };
459 if let Some(s) = s {
460 if s == Section::PointData {
461 if let Some(p) = pieces.last_mut() {
462 p.active_scalars = xml_attr(e, "Scalars");
463 }
464 }
465 if !empty {
466 section = Some(s);
467 }
468 } else if name == "DataArray" {
469 if let (Some(s), false) = (section, pieces.is_empty()) {
470 let format = match xml_attr(e, "format").as_deref() {
471 Some("ascii") => Format::Ascii,
472 Some("binary") => Format::Binary,
473 Some("appended") => Format::Appended,
474 other => bail!(
475 "unknown DataArray format '{}'",
476 short(other.unwrap_or(""))
477 ),
478 };
479 let a = XmlArray {
480 name: xml_attr(e, "Name").unwrap_or_default(),
481 ty: Num::xml(xml_attr(e, "type").as_deref())?,
482 components: match xml_attr(e, "NumberOfComponents") {
483 None => 1,
484 Some(v) => v
485 .trim()
486 .parse()
487 .map_err(|_| anyhow!("invalid NumberOfComponents"))?,
488 },
489 format,
490 offset: match xml_attr(e, "offset") {
491 None => 0,
492 Some(v) => v
493 .trim()
494 .parse()
495 .map_err(|_| anyhow!("invalid DataArray offset"))?,
496 },
497 text: Cow::Borrowed(&[]),
498 };
499 if format == Format::Appended {
500 file.offsets.push(a.offset);
501 }
502 if empty {
503 store_array(&mut pieces, s, a);
504 } else {
505 array = Some((s, a));
506 nested = 0;
507 }
508 }
509 }
510 }
511 }
512 }
513 Event::Text(t) => {
514 if let (Some((_, a)), 0) = (array.as_mut(), nested) {
515 let bytes = match t.into_inner() {
516 std::borrow::Cow::Borrowed(s) => std::borrow::Cow::Borrowed(s.as_bytes()),
517 std::borrow::Cow::Owned(s) => std::borrow::Cow::Owned(s.into_bytes()),
518 };
519 if a.text.is_empty() {
520 a.text = bytes;
521 } else {
522 let text = a.text.to_mut();
523 text.push(b' ');
524 text.extend_from_slice(&bytes);
525 }
526 }
527 }
528 Event::CData(t) => {
529 if let (Some((_, a)), 0) = (array.as_mut(), nested) {
530 a.text.to_mut().extend_from_slice(t.into_inner().as_bytes());
531 }
532 }
533 Event::End(ref e) => {
534 if array.is_some() {
535 if nested > 0 {
536 nested -= 1;
537 } else if let Some((s, a)) = array.take() {
538 store_array(&mut pieces, s, a);
539 }
540 continue;
541 }
542 let name = e.local_name();
543 if matches!(
544 name.as_ref(),
545 "Points"
546 | "Cells"
547 | "Verts"
548 | "Lines"
549 | "Strips"
550 | "Polys"
551 | "PointData"
552 | "CellData"
553 | "Piece"
554 ) {
555 section = None;
556 }
557 }
558 Event::Eof => break,
559 _ => {}
560 }
561 }
562 let poly = match kind.as_deref() {
563 Some("UnstructuredGrid") => false,
564 Some("PolyData") => true,
565 Some(other) => bail!(
566 "The 3D preview reads VTK UnstructuredGrid (.vtu) and PolyData (.vtp) files, not {}.",
567 short(other)
568 ),
569 None => bail!("Not a VTK XML file (no VTKFile element)."),
570 };
571 if pieces.is_empty() {
572 bail!("The VTK file contains no Piece.");
573 }
574 file.offsets.sort_unstable();
575 file.offsets.dedup();
576
577 let mut grid = Grid::default();
578 for piece in &pieces {
579 let n = piece.points;
580 if n.saturating_add(grid.point_count()) > MAX_NODES {
581 bail!("The result has more than {MAX_NODES} points – too many for the preview.");
582 }
583 let base = grid.point_count() as u32;
584 let cells_before = grid.cell_count();
585 if n > 0 {
586 let a = piece
587 .point_array
588 .as_ref()
589 .context("a Piece has no Points")?;
590 if a.components != 3 {
591 bail!("Points must have three components");
592 }
593 let points = &mut grid.points;
594 points.reserve(n.min(file.data.len() / 3) * 3);
595 file.values(a, n * 3, &mut |v| {
596 points.push(v as f32);
597 Ok(())
598 })?;
599 }
600 if poly {
601 for ((s, ty), count) in [
602 (Section::Verts, POLY_VERTEX),
603 (Section::Lines, POLY_LINE),
604 (Section::Strips, TRIANGLE_STRIP),
605 (Section::Polys, POLYGON),
606 ]
607 .into_iter()
608 .zip(piece.poly_counts)
609 {
610 file.topology(&mut grid, piece, s, count, base, Some(ty))?;
611 }
612 } else {
613 file.topology(&mut grid, piece, Section::Cells, piece.cells, base, None)?;
614 }
615 let cells = grid.cell_count() - cells_before;
616 let mut point_fields = Vec::new();
618 let mut ordered: Vec<&XmlArray<'_>> = piece.point_data.iter().collect();
619 if let Some(active) = &piece.active_scalars {
620 ordered.sort_by_key(|a| a.name != *active);
621 }
622 for a in ordered {
623 if point_fields.len() < MAX_FIELDS {
624 point_fields.push(file.tuples(a, n)?);
625 }
626 }
627 let mut cell_fields = Vec::new();
628 for a in &piece.cell_data {
629 if cell_fields.len() < MAX_FIELDS {
630 cell_fields.push(file.tuples(a, cells)?);
631 }
632 }
633 merge_fields(&mut grid.point_fields, point_fields, base == 0, n);
634 merge_fields(&mut grid.cell_fields, cell_fields, cells_before == 0, cells);
635 }
636 grid.into_mesh()
637}
638
639pub(super) fn store_array<'a>(pieces: &mut [XmlPiece<'a>], section: Section, a: XmlArray<'a>) {
640 let Some(p) = pieces.last_mut() else {
641 return;
642 };
643 match section {
644 Section::Points => {
645 if p.point_array.is_none() {
646 p.point_array = Some(a);
647 }
648 }
649 Section::PointData => p.point_data.push(a),
650 Section::CellData => p.cell_data.push(a),
651 s => p.topology.push((s, a)),
652 }
653}
654
655#[cfg(test)]
656mod unit_tests {
657 use super::*;
658
659 fn b64(bytes: &[u8]) -> String {
660 base64::engine::general_purpose::STANDARD.encode(bytes)
661 }
662
663 #[test]
664 fn base64_blocks_are_decoded_one_after_the_other() {
665 let text = format!("{} \n{}\n", b64(b"ab"), b64(b"cdef"));
667 assert!(b64(b"ab").ends_with('='), "the first block is padded");
668 assert_eq!(base64_blocks(text.as_bytes()).unwrap(), b"abcdef");
669 assert_eq!(base64_blocks(b64(b"abc").as_bytes()).unwrap(), b"abc");
671 assert!(base64_blocks(b"").unwrap().is_empty());
672 let err = base64_blocks(b"@@@@").unwrap_err().to_string();
673 assert!(err.contains("invalid base64"), "{err}");
674 }
675
676 #[test]
677 fn fields_of_several_pieces_are_joined() {
678 let field = |name: &str, values: &[f32]| Field {
679 name: name.into(),
680 unit: String::new(),
681 values: values.to_vec(),
682 };
683 let mut fields = Vec::new();
684 merge_fields(
686 &mut fields,
687 vec![field("T", &[1.0]), field("P", &[2.0])],
688 true,
689 1,
690 );
691 merge_fields(
694 &mut fields,
695 vec![field("T", &[3.0, 4.0]), field("Q", &[9.0])],
696 false,
697 2,
698 );
699 assert_eq!(fields.len(), 2);
700 assert_eq!(fields[0].values, [1.0, 3.0, 4.0]);
701 assert_eq!(fields[1].values[0], 2.0);
702 assert!(fields[1].values[1..].iter().all(|v| v.is_nan()));
703 assert_eq!(fields[1].values.len(), 3);
704 merge_fields(&mut fields, vec![field("Z", &[0.0])], true, 1);
706 assert_eq!(fields.len(), 1);
707 }
708
709 #[test]
710 fn bytes_and_attributes() {
711 assert_eq!(find(b"<VTKFile><AppendedData>", b"<AppendedData"), Some(9));
712 assert_eq!(find(b"<VTKFile>", b"<AppendedData"), None);
713 assert_eq!(find(b"ab", b"abc"), None, "needle longer than the text");
714
715 let tag = quick_xml::events::BytesStart::from_content(
716 r#"DataArray type="Float32" Name="Stress & strain" vtk:format="ascii""#,
717 9,
718 );
719 assert_eq!(xml_attr(&tag, "type").as_deref(), Some("Float32"));
720 assert_eq!(xml_attr(&tag, "Name").as_deref(), Some("Stress & strain"));
721 assert_eq!(xml_attr(&tag, "format").as_deref(), Some("ascii"));
723 assert_eq!(xml_attr(&tag, "NumberOfComponents"), None);
724 }
725}