vmf_forge/
parser.rs

1//! This module provides the VMF parser implementation using the `pest` parsing library.
2
3use indexmap::IndexMap;
4use pest::Parser;
5use pest::iterators::Pair;
6use pest_derive::Parser;
7use std::mem;
8
9use crate::errors::{VmfError, VmfResult};
10
11use crate::VmfBlock;
12use crate::prelude::{Cameras, Entity, VersionInfo, ViewSettings, VisGroups, VmfFile, World};
13use crate::vmf::regions::{Cordon, Cordons};
14
15/// The VMF parser.
16#[derive(Parser)]
17#[grammar = "vmf.pest"]
18struct VmfParser;
19
20/// Parses a VMF string into a `VmfFile` struct.
21///
22/// # Arguments
23///
24/// * `input` - The VMF string to parse.
25///
26/// # Returns
27///
28/// A `VmfResult` containing the parsed `VmfFile` or a `VmfError` if parsing fails.
29pub fn parse_vmf(input: &str) -> VmfResult<VmfFile> {
30    let parsed = VmfParser::parse(Rule::file, input)
31        .map_err(|e| VmfError::Parse(Box::new(e)))?
32        .next()
33        .unwrap(); // ok_or_else(|| VmfError::InvalidFormat("Input string did not contain a valid VMF file structure.".to_string()))?
34    let mut vmf_file = VmfFile::default();
35
36    for pair in parsed.into_inner() {
37        if pair.as_rule() == Rule::block {
38            let block: VmfBlock = parse_block(pair)?;
39
40            match block.name.to_lowercase().as_str() {
41                // -- metadatas
42                "versioninfo" => vmf_file.versioninfo = VersionInfo::try_from(block)?,
43                "visgroups" => vmf_file.visgroups = VisGroups::try_from(block)?,
44                "viewsettings" => vmf_file.viewsettings = ViewSettings::try_from(block)?,
45
46                // world
47                "world" => vmf_file.world = World::try_from(block)?,
48
49                // -- entities
50                "entity" => vmf_file.entities.push(Entity::try_from(block)?),
51                "hidden" => {
52                    let mut blocks = block.blocks;
53                    if !blocks.is_empty() {
54                        // Take the first block out of the vector to avoid cloning
55                        let first_block = mem::take(&mut blocks[0]);
56                        let mut ent = Entity::try_from(first_block)?;
57                        ent.is_hidden = true;
58                        vmf_file.hiddens.push(ent)
59                    }
60                }
61
62                // -- regions
63                "cameras" => vmf_file.cameras = Cameras::try_from(block)?,
64                "cordons" => vmf_file.cordons = Cordons::try_from(block)?,
65                // for old version of VMF
66                "cordon" => vmf_file.cordons.push(Cordon::try_from(block)?),
67                // ....
68                _ => {
69                    #[cfg(feature = "debug_assert_info")]
70                    debug_assert!(false, "Unexpected block name: {}", block.name);
71                }
72            }
73        }
74    }
75
76    Ok(vmf_file)
77}
78
79/// Parses a `Pair` representing a VMF block into a `VmfBlock` struct.
80///
81/// # Arguments
82///
83/// * `pair` - The `Pair` representing the VMF block.
84///
85/// # Returns
86///
87/// A `VmfResult` containing the parsed `VmfBlock` or a `VmfError` if parsing fails.
88fn parse_block(pair: Pair<Rule>) -> VmfResult<VmfBlock> {
89    let mut inner = pair.into_inner();
90    let block_name_pair = inner
91        .next()
92        .ok_or_else(|| VmfError::InvalidFormat("block name not found".to_string()))?;
93
94    let name = block_name_pair.as_str().to_string();
95
96    // Pre-allocate with reasonable capacity to avoid reallocations
97    let mut key_values = IndexMap::with_capacity(8);
98    let mut blocks = Vec::with_capacity(16);
99
100    for item in inner {
101        match item.as_rule() {
102            Rule::key_value => {
103                let mut kv_inner = item.into_inner();
104                let key_pair = kv_inner
105                    .next()
106                    .ok_or_else(|| VmfError::InvalidFormat("key not found".to_string()))?;
107                let value_pair = kv_inner
108                    .next()
109                    .ok_or_else(|| VmfError::InvalidFormat("value not found".to_string()))?;
110
111                let key = strip_quotes(key_pair.as_str());
112                let value = strip_quotes(value_pair.as_str());
113
114                key_values
115                    .entry(key)
116                    .and_modify(|existing_value: &mut String| {
117                        existing_value.push('\r');
118                        existing_value.push_str(&value);
119                    })
120                    .or_insert(value);
121            }
122            Rule::block => {
123                blocks.push(parse_block(item)?);
124            }
125            _ => {}
126        }
127    }
128
129    Ok(VmfBlock {
130        name,
131        key_values,
132        blocks,
133    })
134}
135
136/// Removes the leading and trailing quotes from a string.
137///
138/// # Arguments
139///
140/// * `s` - The string to strip quotes from.
141///
142/// # Returns
143///
144/// The string with quotes removed.
145#[inline]
146fn strip_quotes(s: &str) -> String {
147    if s.starts_with('"') && s.ends_with('"') {
148        s[1..s.len() - 1].to_string()
149    } else {
150        s.to_string()
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    #[test]
158    fn parse_block_valid_block() {
159        let input = "entity { \"classname\" \"logic_relay\" }";
160        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
161        let block = parse_block(parsed.next().unwrap()).unwrap();
162
163        assert_eq!(block.name, "entity");
164        assert_eq!(
165            block.key_values.get("classname"),
166            Some(&"logic_relay".to_string())
167        );
168        assert!(block.blocks.is_empty());
169    }
170
171    #[test]
172    fn parse_block_nested_blocks() {
173        let input = "entity { \"classname\" \"logic_relay\" solid { \"id\" \"1\" } }";
174        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
175        let block = parse_block(parsed.next().unwrap()).unwrap();
176
177        assert_eq!(block.name, "entity");
178        assert_eq!(
179            block.key_values.get("classname"),
180            Some(&"logic_relay".to_string())
181        );
182        assert_eq!(block.blocks.len(), 1);
183        assert_eq!(block.blocks[0].name, "solid");
184        assert_eq!(block.blocks[0].key_values.get("id"), Some(&"1".to_string()));
185    }
186
187    #[test]
188    fn parse_block_empty_block() {
189        let input = "entity { }";
190        let mut parsed = VmfParser::parse(Rule::block, input).unwrap();
191        let block = parse_block(parsed.next().unwrap()).unwrap();
192
193        assert_eq!(block.name, "entity");
194        assert!(block.key_values.is_empty());
195        assert!(block.blocks.is_empty());
196    }
197}