Skip to main content

valve_keyvalue/
parse.rs

1/*
2   Copyright 2026 Gerg0Vagyok
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17use crate::{ValveKeyValue, ValveKeyValueType};
18use crate::error::{Error, Result};
19
20#[derive(Clone, Debug)]
21pub struct TrackedChars<'a> {
22	inner: std::iter::Peekable<std::str::Chars<'a>>,
23	line: usize,
24	col: usize,
25}
26
27impl<'a> TrackedChars<'a> {
28	pub fn new(input: &'a str) -> Self {
29		Self { inner: input.chars().peekable(), line: 0, col: 0 }
30	}
31
32	pub fn get_pos(&self) -> (usize, usize) {
33		(self.line, self.col)
34	}
35
36	#[allow(clippy::should_implement_trait)]
37	pub fn next(&mut self) -> Option<char> {
38		let ch = self.inner.next();
39		if let Some(c) = ch {
40			match c {
41				'\n' => {
42					self.line += 1;
43					self.col = 0;
44				},
45				'\r' => self.col = 0,
46				_ => self.col += 1
47			}
48		}
49		ch
50	}
51
52	pub fn peek(&mut self) -> Option<&char> {
53		self.inner.peek()
54	}
55
56	pub fn peek_2(&mut self) -> Option<char> {
57		let mut next = self.inner.clone();
58		next.next()?;
59		next.next()
60	}
61}
62
63// getting serde vibes with the iterators
64
65pub fn parse_string( // If it starts with " it has to end with a ", if it doesnt start with a " it
66					 // ends at the next whitespace
67	input: &mut TrackedChars,
68	use_escape_sequences: bool
69) -> Result<String> {
70	let mut output_string = String::new();
71
72	let is_quote = Some(&'"') == input.peek();
73	if is_quote {
74		input.next();
75	}
76
77	let is_end = |inp: char| -> bool {
78		if is_quote {
79			inp == '"'
80		} else {
81			inp.is_whitespace() || inp == '{' || inp == '}' || inp == '"'
82		}
83	};
84
85	while let Some(ch) = input.peek() {
86		if use_escape_sequences {
87			match ch {
88				'\\' => match input.peek_2() {
89					Some(ch) => {
90						match ch {
91							'\\' => {
92								output_string.push('\\');
93								input.next();
94								input.next();
95							},
96							'"' => {
97								output_string.push('"');
98								input.next();
99								input.next();
100							},
101							'n' => {
102								output_string.push('\n');
103								input.next();
104								input.next();
105							},
106							't' => {
107								output_string.push('\t');
108								input.next();
109								input.next();
110							},
111							_ => {input.next();}
112						}
113					},
114					None => return Err(Error::UnexpectedEndOfFile)
115				},
116				_ => if is_end(*ch) {
117					if *ch == '"' && is_quote {input.next();}
118					return Ok(output_string);
119				} else {
120					output_string.push(*ch);
121					input.next();
122				}
123			}
124		} else {
125			if is_end(*ch) {
126				if *ch == '"' && is_quote {input.next();}
127				return Ok(output_string);
128			} else {
129				output_string.push(*ch);
130				input.next();
131			}
132		}
133	}
134
135	if is_quote {
136		Err(Error::UnexpectedEndOfFile)
137	} else {
138		Ok(output_string)
139	}
140}
141
142pub fn skip_comments(input: &mut TrackedChars) {
143	while let Some(ch) = input.peek() {
144		if ch == &'\n' {
145			return;
146		} else {
147			input.next();
148		}
149	}
150}
151
152pub fn parse_object(
153	input: &mut TrackedChars,
154	use_escape_sequences: bool,
155	depth: usize
156) -> Result<Vec<ValveKeyValue>> {
157	let mut keypairs: Vec<ValveKeyValue> = Vec::new();
158	let mut current_key: Option<String> = None;
159
160	while let Some(ch) = input.peek() {
161		if ch == &'{' {
162			let pos = input.get_pos();
163			input.next();
164			let parsed_object = parse_object(input, use_escape_sequences, depth + 1)?;
165			if let Some(curr_key) = &current_key {
166				keypairs.push(ValveKeyValue::new(curr_key.clone(), ValveKeyValueType::Object(parsed_object)));
167				current_key = None;
168			} else {
169				return Err(Error::UnexpectedObjectAsKey { line: pos.0, col: pos.1 });
170			}
171		} else if ch == &'}' {
172			let pos = input.get_pos();
173			if depth == 0 {
174				return Err(Error::UnexpectedEndOfObject { line: pos.0, col: pos.1 });
175			}
176			if current_key.is_some() {
177				return Err(Error::MissingValue { line: pos.0, col: pos.1 });
178			}
179			input.next();
180			return Ok(keypairs);
181		} else if ch.is_whitespace() {
182			input.next();
183		} else if ch == &'/' && input.peek_2() == Some('/') {
184			input.next();
185			skip_comments(input);
186		} else {
187			let parsed_string = parse_string(input, use_escape_sequences)?;
188			if let Some(curr_key) = &current_key {
189				keypairs.push(ValveKeyValue::new(curr_key.clone(), ValveKeyValueType::String(parsed_string)));
190				current_key = None;
191			} else {
192				current_key = Some(parsed_string);
193			}
194		}
195	}
196
197	if current_key.is_some() {
198		let pos = input.get_pos();
199		Err(Error::MissingValue { line: pos.0, col: pos.1 })
200	} else if depth == 0 {
201		Ok(keypairs)
202	} else {
203		Err(Error::UnexpectedEndOfFile)
204	}
205}
206
207
208// This is more of a wrapper
209pub fn parse(input: String, use_escape_sequences: bool) -> Result<Vec<ValveKeyValue>> {
210	parse_object(&mut TrackedChars::new(&input), use_escape_sequences, 0)
211}