sgf-parse 2.0.4

A parser for the SGF file format for Go games
Documentation
use std::ptr::NonNull;

use super::errors::SgfParseError;
use super::lexer::{Lexer, Token};
use super::props::SgfProp;
use super::sgf_node::{SgfNode, SgfNodeBuilder};

/// Returns a Vector of the root `SgfNodes` parsed from the provided text.
///
/// Any `SgfNode` returned by this function should be valid according to the SGF
/// specification.
///
/// # Errors
/// If the text isn't a valid SGF FF[4] collection, then an error is returned.
///
/// # Examples
/// ```
/// use sgf_parse::parse;
///
/// // Prints the all the properties for the two root nodes in the SGF
/// let sgf = "(;SZ[9]C[Some comment];B[de];W[fe])(;B[de];W[ff])";
/// for node in parse(&sgf).unwrap().iter() {
///     for prop in node.properties() {
///         println!("{:?}", prop);
///     }
/// }
/// ```
pub fn parse(text: &str) -> Result<Vec<SgfNode>, SgfParseError> {
    let mut collection: Vec<SgfNodeBuilder> = vec![];
    // Pointer to the `Vec` of children we're currently building.
    let mut current_node_list_ptr = NonNull::new(&mut collection).unwrap();
    // Stack of pointers to incomplete `Vec`s of children.
    let mut incomplete_child_lists: Vec<NonNull<Vec<SgfNodeBuilder>>> = vec![];
    // Using pointers involves some unsafe calls, but should be ok here.
    // Since pointers are always initialized from real structs, and thos structs
    // live for the whole function body, our only safety concern is dangling pointers.
    //
    // Since we build the tree traversing depth-first those structs shouldn't be
    // modified while the pointer is live. Heap-allocated contents of their
    // `children` may be modified, but that shouldn't change anything.

    let mut lexer = Lexer::new(text).peekable();
    while let Some(result) = lexer.next() {
        let (token, _span) = result?;
        match token {
            Token::StartGameTree => {
                // SGF game trees must have a root node.
                if let Some(node_list_ptr) = incomplete_child_lists.last() {
                    let node_list = unsafe { node_list_ptr.as_ref() };
                    if node_list.is_empty() {
                        return Err(SgfParseError::ParseError(
                            "Unexpected start of game tree".to_string(),
                        ));
                    }
                }
                incomplete_child_lists.push(current_node_list_ptr);
            }
            Token::EndGameTree => match incomplete_child_lists.pop() {
                Some(node_list) => current_node_list_ptr = node_list,
                None => {
                    return Err(SgfParseError::ParseError(
                        "Unexpected end of game tree".to_string(),
                    ))
                }
            },
            Token::StartNode => {
                let mut new_node = SgfNodeBuilder::new();
                let mut prop_tokens = vec![];
                while let Some(Ok((Token::Property(_), _))) = lexer.peek() {
                    prop_tokens.push(lexer.next().unwrap()?);
                }
                for token in prop_tokens {
                    match token {
                        (Token::Property((identifier, values)), _) => {
                            new_node.properties.push(SgfProp::new(identifier, values))
                        }
                        _ => unreachable!(),
                    }
                }
                let node_list = unsafe { current_node_list_ptr.as_mut() };
                node_list.push(new_node);
                current_node_list_ptr =
                    NonNull::new(&mut node_list.last_mut().unwrap().children).unwrap();
            }
            Token::Property(_) => {
                return Err(SgfParseError::ParseError("Unexpected property".to_string()))
            }
        }
    }
    if !incomplete_child_lists.is_empty() {
        return Err(SgfParseError::ParseError(
            "Unexpected end of data".to_string(),
        ));
    }

    collection
        .into_iter()
        .map(|mut node| {
            node.is_root = true;
            node.build()
        })
        .collect::<Result<_, _>>()
}

#[cfg(test)]
mod test {
    use super::super::props::*;
    use super::super::serialize;
    use super::{parse, SgfNode};

    fn load_test_sgf() -> Result<Vec<SgfNode>, Box<dyn std::error::Error>> {
        // See https://www.red-bean.com/sgf/examples/
        let mut sgf_path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        sgf_path.push("resources/test/ff4_ex.sgf");
        let data = std::fs::read_to_string(sgf_path)?;

        Ok(parse(&data)?)
    }

    fn node_depth(mut sgf_node: &SgfNode) -> u64 {
        let mut depth = 1;
        while sgf_node.children().count() > 0 {
            depth += 1;
            sgf_node = sgf_node.children().next().unwrap();
        }
        depth
    }

    #[test]
    fn sgf_has_two_gametrees() {
        let sgf_nodes = load_test_sgf().unwrap();
        assert_eq!(sgf_nodes.len(), 2);
    }

    #[test]
    fn gametree_one_has_five_variations() {
        let sgf_nodes = load_test_sgf().unwrap();
        assert_eq!(sgf_nodes[0].children().count(), 5);
    }

    #[test]
    fn gametree_one_has_size_19() {
        let sgf_nodes = load_test_sgf().unwrap();
        match sgf_nodes[0].get_property("SZ") {
            Some(SgfProp::SZ(size)) => assert_eq!(size, &(19, 19)),
            _ => unreachable!("Expected size property"),
        }
    }

    #[test]
    fn gametree_variation_depths() {
        let sgf_nodes = load_test_sgf().unwrap();
        let children: Vec<_> = sgf_nodes[0].children().collect();
        assert_eq!(node_depth(children[0]), 13);
        assert_eq!(node_depth(children[1]), 4);
        assert_eq!(node_depth(children[2]), 4);
    }

    #[test]
    fn gametree_two_has_one_variation() {
        let sgf_nodes = load_test_sgf().unwrap();
        assert_eq!(sgf_nodes[1].children().count(), 1);
    }

    #[test]
    fn serialize_then_parse() {
        let sgf_nodes = load_test_sgf().unwrap();
        let text = serialize(&sgf_nodes);
        assert_eq!(sgf_nodes, parse(&text).unwrap());
    }

    #[test]
    fn invalid_property() {
        let input = "(;GM[1]W[rp.pmonpoqprpsornqmpm])";
        let result = parse(&input).unwrap();
        let expected = vec![
            SgfProp::GM(1),
            SgfProp::Invalid("W".to_string(), vec!["rp.pmonpoqprpsornqmpm".to_string()]),
        ];
        assert_eq!(result.len(), 1);
        assert_eq!(
            result[0].properties().cloned().collect::<Vec<_>>(),
            expected
        );
    }

    #[test]
    fn stack_overflow() {
        // This input generated a stack overflow with the old code
        let input = "(;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;)";
        let result = parse(&input);
        assert!(result.is_ok());
    }
}