Skip to main content

teamcraft/simmesh/
vtk_xml.rs

1//! VTK XML: `UnstructuredGrid` (`.vtu`) and `PolyData` (`.vtp`), ascii, inline binary and appended data, optional zlib.
2
3use 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    /// Verts, Lines, Strips, Polys
46    pub(super) poly_counts: [usize; 4],
47    pub(super) point_array: Option<XmlArray<'a>>,
48    /// (section, array): connectivity, offsets, types
49    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    /// Start of the appended data (after the `_`) and its encoding.
61    pub(super) appended: Option<(usize, bool)>,
62    /// End of the appended data.
63    pub(super) appended_end: usize,
64    /// Offsets of all appended arrays, sorted (end of one array = next offset).
65    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
83/// Base64 that may consist of several separately encoded blocks (VTK encodes
84/// the header of compressed data on its own, so `=` padding can appear in
85/// the middle).
86pub(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    /// The data bytes behind a header (uncompressed or zlib blocks); only the
127    /// first `need` bytes are decompressed.
128    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        // the header may lie: reserve no more than zlib can expand the
170        // bytes at hand to (at most 1032 : 1), the rest grows when it comes
171        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    /// Calls `f` for the first `count` numbers of an array.
201    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                // offsets are sorted: binary search (files may have many arrays)
234                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    /// Cells of one topology section (`Cells`, `Polys`, …).
276    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
333/// Merges the fields of one piece into the grid's fields (by name; values
334/// missing in a piece are NaN). `first`: no values before this piece – the
335/// fields of empty pieces before it (parallel writers emit those) are
336/// replaced, so they do not hide the fields of the first real piece.
337pub(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
353/// VTK XML `UnstructuredGrid` (`.vtu`) and `PolyData` (`.vtp`) files.
354pub fn vtk_xml(data: &[u8]) -> Result<Mesh> {
355    use quick_xml::events::Event;
356    // Raw appended data is binary and not XML: only the part before it is parsed.
357    let appended_at = find(data, b"<AppendedData");
358    let xml = appended_at.and_then(|at| data.get(..at)).unwrap_or(data);
359    // the data from byte `i` on (empty past the end)
360    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        // point and cell data
617        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        // VTK writes the header and the data as separately padded blocks
666        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        // one unpadded block, and nothing at all
670        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        // the first piece decides which fields there are
685        merge_fields(
686            &mut fields,
687            vec![field("T", &[1.0]), field("P", &[2.0])],
688            true,
689            1,
690        );
691        // a later piece: values are appended; a field it lacks gets NaN for
692        // its points; a field only it has is left out
693        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        // `first` starts over
705        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 &amp; 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        // the local name counts, a namespace prefix does not
722        assert_eq!(xml_attr(&tag, "format").as_deref(), Some("ascii"));
723        assert_eq!(xml_attr(&tag, "NumberOfComponents"), None);
724    }
725}