Skip to main content

msh_rw/
parser.rs

1use crate::*;
2
3use thiserror::Error;
4
5use nom::*;
6use nom::branch::alt;
7use nom::bytes::complete::{tag, take, take_until};
8use nom::combinator::{map_res, cut, peek};
9use nom::character::complete::{anychar, char, line_ending, digit1, one_of, space0, space1};
10use nom::multi::{count, many_till};
11use nom::number::complete::double;
12use nom::error::context;
13use nom::sequence::{delimited, preceded, terminated};
14
15use bstr::ByteSlice;
16
17use std::path::Path;
18use std::str::FromStr;
19
20#[derive(Error, Debug)]
21pub enum MshError {
22    #[error("IO error ({source})")]
23    Io {
24        #[from]
25        source: std::io::Error,
26    },
27    #[error("parse error:\n{source}")]
28    Parse {
29        #[from]
30        source: nom::Err<(String, nom::error::ErrorKind)>,
31    }
32}
33
34pub type MshResult<T> = std::result::Result<T, MshError>;
35
36impl Msh {
37    fn from_file<P: AsRef<Path>>(path: P) -> MshResult<Msh> {
38        let (input, header) = msh_header(&first_four_lines(path)?.as_bytes()).unwrap();
39        //match header {
40        //    todo!()
41        //}
42        todo!();
43    }
44}
45
46#[derive(Debug, Copy, Clone, PartialEq, Eq)]
47pub enum Msh2Section {
48    Nodes,
49    Elements,
50    PhysicalGroups,
51    Unknown,
52}
53
54fn parse_msh2_ascii(input: &str) -> IResult<&str, Msh> {
55    let (input, _) = msh2_ascii_header(input)?;
56    let mut msh = Msh::new();
57    let mut msh_input = input;
58    while let Ok((input, section)) = peek_section(msh_input) {
59        let (rest, _) = add_section(&mut msh, section, input)?;
60        msh_input = rest;
61    }
62    Ok((msh_input, msh))
63}
64
65#[derive(Debug, Copy, Clone, PartialEq, Eq)]
66pub enum MshVersion {
67    AsciiV22,
68    AsciiV41,
69    BinaryLeV22,
70    BinaryLeV41,
71}
72
73// careful about input types here! might need to take a &[u8] slice and convert
74// to string for ascii formats.
75pub fn parse_single_msh(input: &str, header: MshVersion) -> IResult<&str, Msh> {
76    match header {
77        MshVersion::AsciiV22 => parse_msh2_ascii(input),
78        MshVersion::AsciiV41 => todo!(),
79        MshVersion::BinaryLeV22 => todo!(),
80        MshVersion::BinaryLeV41 => todo!(),
81    }
82}
83
84/// Returns a vector of meshes, since two or more concatenated `msh` files are also a valid `msh` file.
85pub fn parse_msh_file(input: &str) -> MshResult<Vec<Msh>> {
86    let mut msh_input = input;
87    let mut meshes = Vec::new();
88
89    while let Ok((input, header)) = peek_header(msh_input) {
90        match parse_single_msh(input, header) {
91            Ok((rest, msh)) => { meshes.push(msh); msh_input = rest },
92            Err(err) => return Err(err.to_owned().into()),
93        }
94    }
95
96    Ok(meshes)
97}
98
99fn peek_header(input: &str) -> IResult<&str, MshVersion> {
100    peek(alt((
101        msh2_ascii_header,
102        msh4_ascii_header,
103    )))(input)
104}
105
106fn peek_section(input: &str) -> IResult<&str, Msh2Section> {
107    peek(alt((
108        nodes_header,
109        elements_header,
110        physical_groups_header,
111    )))(input)
112}
113
114fn add_section<'a>(mesh: &mut Msh, section: Msh2Section, input: &'a str) -> IResult<&'a str, ()> {
115    use Msh2Section::*;
116    match section {
117        Nodes => {
118            let (rest, nodes) = parse_node_section_msh2(input)?;
119            mesh.nodes = nodes;
120            Ok((rest, ()))
121        },
122        Elements => {
123            let (rest, elts) = parse_elements_section_msh2(input)?;
124            mesh.elts = elts;
125            Ok((rest, ()))
126        }
127        PhysicalGroups => {
128            let (rest, pgs) = parse_physical_groups_msh2(input)?;
129            mesh.physical_groups = pgs;
130            Ok((rest, ()))
131        }
132        _ => todo!(),
133    }
134}
135
136#[test]
137fn peek_sections() {
138    assert!(peek_section("$Nodes").unwrap().1 == Msh2Section::Nodes);
139    assert!(peek_section("$Elements").unwrap().1 == Msh2Section::Elements);
140}
141
142fn nodes_header(input: &str) -> IResult<&str, Msh2Section> {
143    let (input, _) = terminated(tag("$Nodes"), end_of_line)(input)?;
144    Ok((input, Msh2Section::Nodes))
145}
146
147fn elements_header(input: &str) -> IResult<&str, Msh2Section> {
148    let (input, _) = terminated(tag("$Elements"), end_of_line)(input)?;
149    Ok((input, Msh2Section::Elements))
150}
151
152fn physical_groups_header(input: &str) -> IResult<&str, Msh2Section> {
153    let (input, _) = terminated(tag("$PhysicalNames"), end_of_line)(input)?;
154    Ok((input, Msh2Section::PhysicalGroups))
155}
156
157#[test]
158fn headers() {
159    assert!(nodes_header("$Nodes").unwrap().1 == Msh2Section::Nodes);
160    assert!(elements_header("$Elements").unwrap().1 == Msh2Section::Elements);
161}
162
163fn first_four_lines<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
164    use std::io::BufRead;
165    // examine first 3-4 lines, instead of reading the whole file
166    // because binary files aren't utf8
167    let file = std::fs::File::open(path)?;
168    let mut reader = std::io::BufReader::new(file);
169    let mut buffer = String::new();
170    for _ in 0..=3 {
171        reader.read_line(&mut buffer)?;
172    }
173    Ok(buffer)
174}
175
176// -- helper parsers
177
178pub fn sp(input: &str) -> IResult<&str, &str> {
179    use nom::character::complete::multispace0;
180    multispace0(input)
181    //let (input, nodes) = count(parse_u64_sp, num_info as usize)(input)?;
182    //tag(" ")(input)
183}
184
185pub fn tab(input: &str) -> IResult<&str, &str> {
186    tag("\t")(input)
187}
188
189pub fn whitespace(input: &str) -> IResult<&str, &str> {
190    alt((sp, tab))(input)
191}
192
193// TODO: add many spaces terminated by eol
194pub fn end_of_line(input: &str) -> IResult<&str, &str> {
195    if input.is_empty() {
196        Ok((input, input))
197    } else {
198        let (input, (_, eol)) = many_till(whitespace, line_ending)(input)?;
199        Ok((input, eol))
200    }
201}
202
203pub fn format_header(input: &str) -> IResult<&str, &str> {
204    terminated(tag("$MeshFormat"), end_of_line)(input)
205}
206
207pub fn format_footer(input: &str) -> IResult<&str, &str> {
208    terminated(tag("$EndMeshFormat"), end_of_line)(input)
209}
210
211pub fn msh2_ascii_header(input: &str) -> IResult<&str, MshVersion> {
212    let ((input, _)) = format_header(input)?;
213    let ((input, _)) = terminated(tag("2.2 0 8"), end_of_line)(input)?;
214    let ((input, _)) = format_footer(input)?;
215    Ok((input, MshVersion::AsciiV22))
216}
217
218pub fn msh4_ascii_header(input: &str) -> IResult<&str, MshVersion> {
219    let ((input, _)) = format_header(input)?;
220    let ((input, _)) = terminated(tag("4.1 0 8"), end_of_line)(input)?;
221    let ((input, _)) = format_footer(input)?;
222    Ok((input, MshVersion::AsciiV41))
223}
224
225pub fn msh_header(input: &[u8]) -> IResult<&[u8], MshVersion> {
226    let (input, _) = terminated(tag("$MeshFormat"), line_ending)(input)?;
227    let (input, version) = terminated(alt((tag("2.2"), tag("4.1"))), space1)(input)?;
228    let (input, binary) = terminated(one_of("01"), space1)(input)?;
229    let (mut input, _size_t) = terminated(char('8'), terminated(space0, line_ending))(input)?;
230    if binary == '1' {
231        let (inner_input, endianness) = terminated(take(4usize), line_ending)(input)?;
232        match endianness {
233            [1, 0, 0, 0] => (),
234            [0, 0, 0, 1] => {
235                eprintln!("big-endian files are not supported");
236                return Err(Err::Error((input, nom::error::ErrorKind::Tag)));
237            },
238            _ => panic!("bad endianness byte sequence"),
239        }
240        // update parser position in outer scope
241        input = inner_input;
242    }
243    let (input, _) = terminated(tag("$EndMeshFormat"), line_ending)(input)?;
244    match (version, binary) {
245        (b"2.2", '0') => Ok((input, MshVersion::AsciiV22)),
246        (b"2.2", '1') => Ok((input, MshVersion::BinaryLeV22)),
247        (b"4.1", '0') => Ok((input, MshVersion::AsciiV41)),
248        (b"4.1", '1') => Ok((input, MshVersion::BinaryLeV41)),
249        _ => Err(Err::Error((input, nom::error::ErrorKind::Tag))),
250    }
251}
252
253fn parse_node_section_msh2(input: &str) -> IResult<&str, Vec<Node>> {
254    let (input, _) = terminated(tag("$Nodes"), end_of_line)(input)?;
255    let (input, num_nodes) = cut(terminated(parse_u64, end_of_line))(input)?;
256    let (input, (nodes, _)) = many_till(parse_node_msh2, terminated(tag("$EndNodes"), end_of_line))(input)?;
257    if num_nodes != nodes.len() as u64 {
258        // we don't really care if the number doesn't line up for msh2
259        eprintln!("warning: node header says {} nodes, but parsed {}", num_nodes, nodes.len());
260    }
261    Ok((input, nodes))
262}
263
264fn parse_unknown_section(input: &str) -> IResult<&str, ()> {
265    let (input, section_name) = delimited(char('$'), take_until("\n"), end_of_line)(input)?;
266    eprintln!("skipping unknown section ${}", section_name);
267    // fast-forward to ending line
268    let (input, _) = take_until("$End")(input)?;
269    // skip the ending line and continue
270    let (input, _) = delimited(char('$'), take_until("\n"), end_of_line)(input)?;
271    Ok((input, ()))
272}
273
274fn parse_node_msh2(input: &str) -> IResult<&str, Node> {
275    do_parse!(input,
276        tag: parse_u64 >> sp >>
277        x: double >> sp >>
278        y: double >> sp >>
279        z: double >> sp >>
280        ( Node { tag, x, y, z } )
281    )
282}
283
284fn parse_physical_groups_msh2(input: &str) -> IResult<&str, Vec<PhysicalGroup>> {
285    let (input, _) = terminated(tag("$PhysicalNames"), end_of_line)(input)?;
286    let (input, num_groups) = cut(terminated(parse_u64, end_of_line))(input)?;
287    let (input, (groups, _)) = many_till(parse_physical_group_msh2, terminated(tag("$EndPhysicalNames"), end_of_line))(input)?;
288    if num_groups != groups.len() as u64 {
289        // we don't really care if the number doesn't line up for msh2
290        eprintln!("warning: header says {} physical groups, but read {}", num_groups, groups.len());
291    }
292    Ok((input, groups))
293}
294
295fn parse_physical_group_msh2(input: &str) -> IResult<&str, PhysicalGroup> {
296    let (input, dim) = parse_dimension(input)?;
297    let (input, _) = sp(input)?;
298    let (input, tag) = parse_u64(input)?;
299    let (input, _) = sp(input)?;
300    let (input, name) = delimited(char('"'), take_until("\""), char('"'))(input)?;
301    let (input, _) = end_of_line(input)?;
302    Ok((input, PhysicalGroup { dim, tag, name: name.to_string() }))
303}
304
305fn parse_dimension(input: &str) -> IResult<&str, Dim> {
306    let (input, dim) = one_of("0123")(input)?;
307    Ok((input, Dim::from_u8_unchecked(u8::from_str(&dim.to_string()).unwrap())))
308}
309
310fn parse_u64(input: &str) -> IResult<&str, u64> {
311    map_res(digit1, u64::from_str)(input)
312}
313
314fn parse_u64_sp(input: &str) ->  IResult<&str, u64> {
315    terminated(map_res(digit1, u64::from_str), sp)(input)
316}
317
318fn parse_elements_section_msh2(input: &str) -> IResult<&str, Vec<MeshElt>> {
319    let (input, _) = terminated(tag("$Elements"), end_of_line)(input)?;
320    let (input, num_elts) = cut(terminated(parse_u64, end_of_line))(input)?;
321    let (input, (elts, _)) = many_till(parse_element_msh2, terminated(tag("$EndElements"), end_of_line))(input)?;
322    if num_elts != elts.len() as u64 {
323        // we don't really care if the number doesn't line up for msh2
324        eprintln!("warning: header says {} elements, but read {}", num_elts, elts.len());
325    }
326    Ok((input, elts))
327}
328
329fn parse_element_msh2(input: &str) -> IResult<&str, MeshElt> {
330    let (input, tag) = terminated(parse_u64, sp)(input)?;
331    let (input, label) = terminated(digit1, sp)(input)?;
332    let elt_type = match MeshShape::from_gmsh_label(label) {
333        Some(ty) => ty,
334        None => panic!(format!("unknown mesh element type: {}", label)),
335    };
336
337    let (input, elt_info) = parse_elt_info(input)?;
338    let (input, nodes) = count(parse_u64_sp, elt_type.num_nodes() as usize)(input)?;
339
340    let uint_to_tag = |uint| if uint != 0 { Some(uint) } else { None };
341    Ok((input, MeshElt {
342        tag,
343        ty: elt_type,
344        nodes,
345        // multiple physical groups are handled by duplicate shapes
346        physical_group: uint_to_tag(elt_info.physical_group),
347        geometry: uint_to_tag(elt_info.geometry),
348    }))
349}
350
351struct EltInfo {
352    pub physical_group: Tag,
353    pub geometry: Tag,
354    // lots of options here:
355    // https://gitlab.onelab.info/gmsh/gmsh/-/blob/master/Geo/GModelIO_MSH2.cpp#L370
356    //pub mesh_partition: Option<Tag>,
357    //pub ghost_elements: Option<Vec<Tag>>,
358    //pub domain: Option<(Tag, Tag)>,
359    //pub parent_elt: Option<Tag>,
360}
361
362fn parse_elt_info(input: &str) -> IResult<&str, EltInfo> {
363    let (input, num_info) = parse_u64_sp(input)?;
364    if num_info > 2 {
365        eprintln!("warning: only reading physical group and geometry information and skipping partitions, ghost elements...");
366    }
367    let (input, elt_info) = count(parse_u64_sp, num_info as usize)(input)?;
368    Ok((input, EltInfo { physical_group: elt_info[0], geometry: elt_info[1] }))
369}
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374    use std::path::PathBuf;
375    use insta::{assert_debug_snapshot, assert_display_snapshot};
376
377    #[test]
378    fn trailing_spaces() {
379        let inp = "101 0 1 100.0      \t\n";
380        assert_debug_snapshot!(parse_node_msh2(inp).unwrap().1);
381    }
382
383mod msh2 {
384
385    use super::*;
386
387    #[test]
388    fn msh2_ascii() {
389        let msh = std::fs::read_to_string("props/v2/basic.msh").unwrap();
390        assert_debug_snapshot!(parse_msh2_ascii(&msh).unwrap().1);
391    }
392
393    #[test]
394    fn concatenated_mesh_files() {
395        // add basic mesh to itself and make sure the copies match
396        let mut msh = std::fs::read_to_string("props/v2/basic.msh").unwrap();
397        let msh = msh.clone() + &msh;
398        assert_debug_snapshot!(parse_msh_file(&msh).unwrap());
399    }
400
401    #[test]
402    fn unknown_section() {
403        assert_debug_snapshot!(parse_unknown_section("$Comments\nhi there\nfinished in 10.2seconds\n$EndComments\n").unwrap().1);
404    }
405
406    #[test]
407    fn section_failure() {
408        // no elt number before listing elts
409        let input =
410            "$Elements\n\
411             1 15 2 0 0 5\n\
412             500 1 2 1 2 30 31\n\
413             $EndElements";
414        let mut msh_input = input;
415        let res = parse_msh2_ascii(msh_input);
416        assert!(res.is_err());
417    }
418
419    #[test]
420    fn node_elt() {
421        assert_debug_snapshot!(parse_element_msh2("1 15 2 0 0 5\n").unwrap().1);
422    }
423
424    #[test]
425    fn line_elt() {
426        assert_debug_snapshot!(parse_element_msh2("500 1 2 1 2 30 31\n").unwrap().1);
427    }
428
429    #[test]
430    fn tri_elt() {
431        assert_debug_snapshot!(parse_element_msh2("10 2 2 5 1 1 2 3\n").unwrap().1);
432    }
433
434    #[test]
435    fn tetra_elt() {
436        assert_debug_snapshot!(parse_element_msh2("41 4 2 0 1 1 2 3 4\n").unwrap().1);
437    }
438
439    #[test]
440    fn elt_extra_fields() {
441        assert_debug_snapshot!(parse_element_msh2("41 4 5 0 1 1 2 3 41 42 43 44\n").unwrap().1);
442    }
443
444    #[test]
445    fn node_msh2() {
446        let i = "1201 0 0. 1.";
447        assert_debug_snapshot!(parse_node_msh2(i).unwrap().1);
448    }
449
450    #[test]
451    fn pgroup_msh2() {
452        let i = r#"3 1 "Water cube""#;
453        assert_debug_snapshot!(parse_physical_group_msh2(i).unwrap().1);
454    }
455
456    #[test]
457    fn pgroups_section() {
458        assert_debug_snapshot!(parse_physical_groups_msh2(
459            &r#"$PhysicalNames
4604
4610 1 "a point"
4620 2 "hi"
4633 3 "Water-cube"
4642 4 "fuselage"
465$EndPhysicalNames"#).unwrap().1);
466    }
467
468    #[test]
469    fn bad_physical_group_dimension() {
470        let res = parse_physical_group_msh2(&r#"4 1 "Water cube"#);
471        assert!(res.is_err());
472        if let Err(trace) = res {
473            assert_display_snapshot!(trace);
474        }
475    }
476
477    #[test]
478    fn some_nodes() {
479        let i = "$Nodes\n3\n1 0. 0. 1.\n2 1. 1 1\n100 1 1 1\n$EndNodes\n";
480        assert_debug_snapshot!(parse_node_section_msh2(i).unwrap().1);
481    }
482
483    #[test]
484    fn nodes_len_mismatch() {
485        let inp = "$Nodes\n0\n1 0. 0. 1.\n2 1. 1 1\n100 1 1 1\n$EndNodes\n";
486        assert_debug_snapshot!(parse_node_section_msh2(inp).unwrap().1);
487    }
488
489    #[test]
490    fn empty_nodes() {
491        let inp = "$Nodes\n0\n$EndNodes\n";
492        assert_debug_snapshot!(parse_node_section_msh2(inp).unwrap().1);
493    }
494
495    #[test]
496    fn msh_header_test() {
497        let header = "$MeshFormat\n2.2 0 8\n$EndMeshFormat\n";
498        match msh_header(header.as_bytes()) {
499            Ok((_, MshVersion::AsciiV22)) => (),
500            _else => {std::dbg!(_else); panic!("bad mesh header")},
501        };
502        let header = "$MeshFormat\n2.2 1 8\n\u{1}\u{0}\u{0}\u{0}\n$EndMeshFormat\n";
503        match msh_header(header.as_bytes()) {
504            Ok((_, MshVersion::BinaryLeV22)) => (),
505            _else => {std::dbg!(_else); panic!("bad mesh header")},
506        };
507    }
508
509    #[test]
510    fn msh2_ascii_header() {
511        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
512        path.push("props");
513        path.push("v2");
514        path.push("empty.msh");
515        assert_debug_snapshot!(msh_header(&first_four_lines(path).unwrap().as_bytes()).unwrap());
516    }
517
518    #[test]
519    fn unix_line_endings() {
520        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
521        path.push("props");
522        path.push("v2");
523        path.push("unix-empty.msh");
524        assert_debug_snapshot!(msh_header(&first_four_lines(path).unwrap().as_bytes()).unwrap());
525    }
526
527    #[test]
528    fn msh2_binary_header() {
529        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
530        path.push("props");
531        path.push("v2");
532        path.push("empty-bin.msh");
533        assert_debug_snapshot!(msh_header(&first_four_lines(path).unwrap().as_bytes()).unwrap());
534    }
535
536    #[test]
537    fn bad_header() {
538        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
539        path.push("props");
540        path.push("v2");
541        path.push("bad-header.msh");
542        let header_str = first_four_lines(path).unwrap();
543        let res = msh_header(&header_str.as_bytes());
544        assert!(res.is_err());
545        if let Err(trace) = res {
546            assert_display_snapshot!(trace);
547        }
548    }
549}
550
551mod msh4 {
552    use super::*;
553
554    #[test]
555    fn msh4_ascii_header() {
556        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
557        path.push("props");
558        path.push("v4");
559        path.push("empty.msh");
560        assert_debug_snapshot!(msh_header(&first_four_lines(path).unwrap().as_bytes()).unwrap());
561    }
562
563    #[test]
564    fn msh4_binary_header() {
565        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
566        path.push("props");
567        path.push("v4");
568        path.push("empty-bin.msh");
569        assert_debug_snapshot!(msh_header(&first_four_lines(path).unwrap().as_bytes()).unwrap());
570    }
571}
572}