conspire 0.7.0

The Rust interface to conspire.
Documentation
#[cfg(test)]
mod test;

use crate::{
    geometry::{
        Coordinates,
        mesh::{Connectivity, Mesh},
    },
    math::Scalar,
};
use std::{
    fs::read_to_string,
    io::{Error, ErrorKind, Result},
    path::Path,
};

pub trait ReadVtu<P>
where
    P: AsRef<Path>,
    Self: Sized,
{
    fn read_vtu(input: P) -> Result<Self>;
}

impl<const D: usize, P> ReadVtu<P> for Mesh<D>
where
    P: AsRef<Path>,
{
    fn read_vtu(input: P) -> Result<Self> {
        if D != 2 && D != 3 {
            return Err(unsupported("VTU supports only 2D or 3D meshes"));
        }
        let text = match read_to_string(input) {
            Ok(text) => text,
            Err(error) if error.kind() == ErrorKind::InvalidData => {
                return Err(unsupported(
                    "raw appended VTU is not supported (use ascii or binary)",
                ));
            }
            Err(error) => return Err(error),
        };
        let header = tag(&text, "<VTKFile")?;
        if attribute(header, "compressor").is_some() {
            return Err(unsupported("compressed VTU is not supported"));
        }
        if attribute(header, "type") != Some("UnstructuredGrid") {
            return Err(invalid("VTU is not an UnstructuredGrid".into()));
        }
        if matches!(attribute(header, "byte_order"), Some(order) if order != "LittleEndian") {
            return Err(unsupported("big-endian VTU is not supported"));
        }
        let header_bytes = match attribute(header, "header_type") {
            Some("UInt32") | None => 4,
            Some("UInt64") => 8,
            Some(other) => return Err(invalid(format!("unsupported header_type {other}"))),
        };

        let points_region = region(&text, "Points")?;
        let points = floats(&data_array(points_region, None)?, header_bytes)?;
        let components = attribute(tag(points_region, "<DataArray")?, "NumberOfComponents")
            .and_then(|n| n.parse().ok())
            .unwrap_or(3);

        let cells_region = region(&text, "Cells")?;
        let connectivity = integers(
            &data_array(cells_region, Some("connectivity"))?,
            header_bytes,
        )?;
        let offsets = integers(&data_array(cells_region, Some("offsets"))?, header_bytes)?;
        let types = integers(&data_array(cells_region, Some("types"))?, header_bytes)?;

        let coordinates: Coordinates<D> = points
            .chunks(components)
            .map(|point| std::array::from_fn(|i| point[i]).into())
            .collect();
        Ok((blocks(&connectivity, &offsets, &types)?, coordinates).into())
    }
}

#[derive(Clone, Copy)]
struct DataArray<'a> {
    data_type: &'a str,
    format: &'a str,
    text: &'a str,
}

fn blocks(connectivity: &[i64], offsets: &[i64], types: &[i64]) -> Result<Vec<Connectivity>> {
    let mut cells: Vec<(i64, &[i64])> = Vec::with_capacity(types.len());
    let mut start = 0;
    for (cell, &end) in offsets.iter().enumerate() {
        cells.push((types[cell], &connectivity[start..end as usize]));
        start = end as usize;
    }
    let mut blocks = Vec::new();
    let mut from = 0;
    while from < cells.len() {
        let mut to = from;
        while to < cells.len() && cells[to].0 == cells[from].0 {
            to += 1;
        }
        blocks.push(block(cells[from].0, &cells[from..to])?);
        from = to;
    }
    Ok(blocks)
}

fn block(cell_type: i64, cells: &[(i64, &[i64])]) -> Result<Connectivity> {
    Ok(match cell_type {
        5 => Connectivity::Triangular(arrays::<3>(cells)?.into()),
        9 => Connectivity::Quadrilateral(arrays::<4>(cells)?.into()),
        10 => Connectivity::Tetrahedral(arrays::<4>(cells)?.into()),
        12 => Connectivity::Hexahedral(arrays::<8>(cells)?.into()),
        13 => Connectivity::Wedge(arrays::<6>(cells)?.into()),
        14 => Connectivity::Pyramidal(arrays::<5>(cells)?.into()),
        other => return Err(invalid(format!("unsupported VTK cell type: {other}"))),
    })
}

fn arrays<const N: usize>(cells: &[(i64, &[i64])]) -> Result<Vec<[usize; N]>> {
    cells
        .iter()
        .map(|(_, nodes)| {
            if nodes.len() != N {
                return Err(invalid(format!(
                    "cell has {} nodes, expected {N}",
                    nodes.len()
                )));
            }
            Ok(std::array::from_fn(|i| nodes[i] as usize))
        })
        .collect()
}

fn floats(array: &DataArray, header_bytes: usize) -> Result<Vec<Scalar>> {
    if array.format == "ascii" {
        return array.text.split_whitespace().map(parse).collect();
    }
    let bytes = decode(array, header_bytes)?;
    Ok(match array.data_type {
        "Float64" => bytes.chunks(8).map(le_f64).collect(),
        "Float32" => bytes.chunks(4).map(le_f32).collect(),
        other => return Err(invalid(format!("unsupported point type {other}"))),
    })
}

fn integers(array: &DataArray, header_bytes: usize) -> Result<Vec<i64>> {
    if array.format == "ascii" {
        return array.text.split_whitespace().map(parse).collect();
    }
    let bytes = decode(array, header_bytes)?;
    Ok(match array.data_type {
        "Int64" | "UInt64" => bytes.chunks(8).map(le_i64).collect(),
        "Int32" | "UInt32" => bytes.chunks(4).map(le_i32).collect(),
        "Int8" | "UInt8" => bytes.iter().map(|&b| b as i64).collect(),
        other => return Err(invalid(format!("unsupported cell-data type {other}"))),
    })
}

fn decode(array: &DataArray, header_bytes: usize) -> Result<Vec<u8>> {
    if array.format != "binary" {
        return Err(unsupported(
            "only ascii and inline binary DataArrays are supported",
        ));
    }
    let mut bytes = unbase64(array.text);
    if bytes.len() < header_bytes {
        return Err(invalid("binary DataArray shorter than its header".into()));
    }
    Ok(bytes.split_off(header_bytes))
}

fn le_f64(b: &[u8]) -> Scalar {
    f64::from_le_bytes(b.try_into().unwrap())
}
fn le_f32(b: &[u8]) -> Scalar {
    f32::from_le_bytes(b.try_into().unwrap()) as Scalar
}
fn le_i64(b: &[u8]) -> i64 {
    i64::from_le_bytes(b.try_into().unwrap())
}
fn le_i32(b: &[u8]) -> i64 {
    i32::from_le_bytes(b.try_into().unwrap()) as i64
}

fn parse<T: std::str::FromStr>(token: &str) -> Result<T> {
    token
        .parse()
        .map_err(|_| invalid(format!("could not parse '{token}' in VTU")))
}

fn unbase64(text: &str) -> Vec<u8> {
    let value = |c: u8| match c {
        b'A'..=b'Z' => (c - b'A') as u32,
        b'a'..=b'z' => (c - b'a' + 26) as u32,
        b'0'..=b'9' => (c - b'0' + 52) as u32,
        b'+' => 62,
        b'/' => 63,
        _ => 0,
    };
    let chars: Vec<u8> = text
        .bytes()
        .filter(|b| !b.is_ascii_whitespace() && *b != b'=')
        .collect();
    let mut out = Vec::with_capacity(chars.len() / 4 * 3);
    for chunk in chars.chunks(4) {
        let mut block = 0u32;
        for &c in chunk {
            block = (block << 6) | value(c);
        }
        block <<= 6 * (4 - chunk.len());
        for shift in (0..chunk.len().saturating_sub(1)).map(|i| 16 - 8 * i) {
            out.push((block >> shift) as u8);
        }
    }
    out
}

fn tag<'a>(text: &'a str, open: &str) -> Result<&'a str> {
    let start = text
        .find(open)
        .ok_or_else(|| invalid(format!("missing {open} in VTU")))?;
    let end = text[start..]
        .find('>')
        .ok_or_else(|| invalid(format!("unterminated {open} in VTU")))?;
    Ok(&text[start..start + end])
}

fn region<'a>(text: &'a str, name: &str) -> Result<&'a str> {
    let open = format!("<{name}>");
    let close = format!("</{name}>");
    let start = text
        .find(&open)
        .ok_or_else(|| invalid(format!("missing <{name}> in VTU")))?;
    let end = text
        .find(&close)
        .ok_or_else(|| invalid(format!("missing </{name}> in VTU")))?;
    Ok(&text[start..end])
}

fn data_array<'a>(region: &'a str, name: Option<&str>) -> Result<DataArray<'a>> {
    let mut rest = region;
    loop {
        let open = rest
            .find("<DataArray")
            .ok_or_else(|| invalid("missing DataArray in VTU".into()))?;
        let attributes_end = rest[open..]
            .find('>')
            .ok_or_else(|| invalid("unterminated DataArray in VTU".into()))?
            + open;
        let attributes = &rest[open..attributes_end];
        if name.is_none() || attribute(attributes, "Name") == name {
            let close = rest[attributes_end..]
                .find("</DataArray>")
                .ok_or_else(|| invalid("unclosed DataArray in VTU".into()))?
                + attributes_end;
            return Ok(DataArray {
                data_type: attribute(attributes, "type")
                    .ok_or_else(|| invalid("DataArray without type".into()))?,
                format: attribute(attributes, "format").unwrap_or("ascii"),
                text: rest[attributes_end + 1..close].trim(),
            });
        }
        rest = &rest[attributes_end..];
    }
}

fn attribute<'a>(tag: &'a str, name: &str) -> Option<&'a str> {
    let key = format!("{name}=\"");
    let start = tag.find(&key)? + key.len();
    let end = tag[start..].find('"')? + start;
    Some(&tag[start..end])
}

fn invalid(message: String) -> Error {
    Error::new(ErrorKind::InvalidData, message)
}

fn unsupported(message: &str) -> Error {
    Error::new(ErrorKind::Unsupported, message.to_string())
}