1use std::borrow::Cow;
2use std::path::Path;
3use concat_string::concat_string;
6use eval::eval;
7use itertools::Itertools;
8use thiserror::Error;
9
10#[cfg(feature = "vfs")]
11use vfs::VfsPath;
12
13#[cfg(not(feature = "vfs"))]
14type FeatPath = std::path::Path;
15#[cfg(feature = "vfs")]
16type FeatPath = vfs::VfsPath;
17
18fn read_to_string(input_file: &FeatPath) -> Result<String> {
19 #[cfg(not(feature = "vfs"))] {
20 return read_to_string_std(input_file);
21 }
22 #[cfg(feature = "vfs")] {
23 let mut result = String::new();
24 input_file.open_file()?.read_to_string(&mut result)
25 .map_err(|err| Error::IOError(err, input_file.as_str().into()))?;
26 return Ok(result);
27 }
28}
29
30fn read_to_string_std(input_file: &Path) -> Result<String> {
31 return std::fs::read_to_string(input_file)
32 .map_err(|err| Error::IOError(err, input_file.to_path_buf()));
33}
34
35#[derive(Error, Debug)]
36pub enum Error {
37 #[error("Invalid macro `{}` on line {}", .0, .1)]
38 InvalidMacro(String, usize),
39 #[error("Found extra parameters in #param macro on line {}", .0)]
40 ExtraParamsInMacro(usize, &'static str),
41 #[error("Not enough parameters passed to template file")]
42 NotEnoughParameters,
43 #[error("Unused parameters passed to template file")]
44 UnusedParameters,
45 #[error("Not enough parameters passed to function-like macro `{}`", .0)]
46 NotEnoughParametersMacro(String),
47 #[error("Too many parameters passed to function-like macro `{}`", .0)]
48 UnusedParametersMacro(String),
49 #[error("Invalid parameter name {} on line {}", .0, .1)]
50 InvalidParameterName(String, usize),
51 #[error("First parameter of #include should be a string on line {}", .0)]
52 FirstParamOfIncludeNotString(usize),
53 #[error("Invalid pragma '{}'", .0)]
54 InvalidPragma(String),
55 #[error("IOError while reading {}: {}", .1.display(), .0)]
56 IOError(std::io::Error, std::path::PathBuf),
57 #[error("Couldn't evaluate `#if` expression condition: {}", .0)]
58 ConditionEvaluationError(#[from] eval::Error),
59 #[error("`#if` condition doesn't evaluate to bool '{}'", .0)]
60 NonBooleanConditionResult(eval::Value),
61 #[error("Elif specified after else")]
62 ElifAfterElse,
63 #[cfg(feature = "vfs")]
64 #[error("VfsError: {}", .0)]
65 VfsError(#[from] vfs::VfsError),
66}
67
68type Result<T> = std::result::Result<T, Error>;
69
70pub fn parse<'a>(
80 input_file: impl AsRef<Path>,
81 base_dir: impl AsRef<Path>,
82 parameters: impl Iterator<Item = &'a str>,
83) -> Result<String> {
84 return parse_cow(input_file, base_dir, parameters);
85}
86
87#[cfg(feature = "vfs")]
102pub fn parse_vfs<'a>(
103 input_file: impl Into<VfsPath>,
104 base_dir: impl Into<VfsPath>,
105 parameters: impl Iterator<Item = &'a str>
106) -> Result<String> {
107 return parse_vfs_cow(input_file.into(), base_dir.into(), parameters);
108}
109
110pub fn parse_cow<'a, Iter, C>(
113 input_file: impl AsRef<Path>,
114 base_dir: impl AsRef<Path>,
115 parameters: Iter
116) -> Result<String>
117 where
118 Iter: Iterator<Item = C>,
119 C: Into<Cow<'a, str>>
120{
121 let content = read_to_string_std(input_file.as_ref())?;
122
123 return parse_string_cow(&content, base_dir, parameters);
124}
125
126#[cfg(feature = "vfs")]
127pub fn parse_vfs_cow<'a, Iter, C>(
128 input_file: impl Into<VfsPath>,
129 base_dir: impl Into<VfsPath>,
130 parameters: Iter
131) -> Result<String>
132 where
133 Iter: Iterator<Item = C>,
134 C: Into<Cow<'a, str>>
135{
136 let content = read_to_string(&input_file.into())?;
137
138 return parse_string_vfs_cow(&content, base_dir, parameters);
139}
140
141pub fn parse_string<'a>(
163 input: &str,
164 base_dir: impl AsRef<Path>,
165 parameters: impl Iterator<Item = &'a str>
166) -> Result<String> {
167 parse_string_cow(input, base_dir, parameters)
168}
169
170#[cfg(feature = "vfs")]
171pub fn parse_string_vfs<'a>(
172 input: &str,
173 base_dir: impl Into<VfsPath>,
174 parameters: impl Iterator<Item = &'a str>
175) -> Result<String> {
176 parse_string_vfs_cow(input, base_dir, parameters)
177}
178
179pub fn parse_string_cow<'a, Iter, C>(
182 input: &str,
183 base_dir: impl AsRef<Path>,
184 parameters: Iter
185) -> Result<String>
186 where
187 Iter: Iterator<Item = C>,
188 C: Into<Cow<'a, str>>
189{
190 #[cfg(not(feature = "vfs"))]
191 let base_dir = base_dir.as_ref();
192 #[cfg(feature = "vfs")]
193 let base_dir = VfsPath::from(vfs::PhysicalFS::new(base_dir));
194 parse_string_cow_impl(input, &base_dir, &mut parameters.map(|v| v.into()))
195}
196
197#[cfg(feature = "vfs")]
198pub fn parse_string_vfs_cow<'a, Iter, C>(
199 input: &str,
200 base_dir: impl Into<VfsPath>,
201 parameters: Iter
202) -> Result<String>
203 where
204 Iter: Iterator<Item = C>,
205 C: Into<Cow<'a, str>>
206{
207 parse_string_cow_impl(input, &base_dir.into(), &mut parameters.map(|v| v.into()))
208}
209
210fn parse_string_cow_impl<'a>(
211 input: &str,
212 base_dir: &FeatPath,
213 parameters: &mut dyn Iterator<Item = Cow<'a, str>>
214) -> Result<String> {
215 let mut replacements: Vec<(String, Cow<str>)> = vec![];
216 let mut fn_replacements: Vec<(String, Vec<String>, String)> = vec![];
217 let mut visited_sources: Vec<String> = vec![];
218
219 parse_string_cow_rec(input, None, base_dir, parameters, &mut replacements, &mut fn_replacements, &mut visited_sources)
220 .map(|str| str.unwrap())
221}
222
223fn parse_string_cow_rec<'a>(
224 input: &str,
225 path: Option<&str>,
226 base_dir: &FeatPath,
227 parameters: &mut dyn Iterator<Item = Cow<'a, str>>,
228 replacements: &mut Vec<(String, Cow<'a, str>)>,
229 fn_replacements: &mut Vec<(String, Vec<String>, String)>,
230 visited_sources: &mut Vec<String>,
231) -> Result<Option<String>> {
232 let mut out = String::new();
233
234 let mut cur_fn_replacement: Option<(String, Vec<String>, String)> = None;
235 let mut if_condition: Vec<(bool, bool, bool)> = vec![];
236
237 let max_lines = input.chars()
238 .filter(|c| *c == '\n')
239 .count();
240
241 for (line_num, line) in input.lines().enumerate() {
242 let mut line_chars = line.chars().skip_while(char::is_ascii_whitespace);
243 let start_char = line_chars.by_ref().next();
244
245 let mut macro_name = None;
246
247 if let Some((include, _has_included, _is_end)) = if_condition.last() {
248 if !include {
249 if start_char == Some('#') {
250 macro_name = Some(line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>());
251 let macro_name = macro_name.as_ref().unwrap();
252 if macro_name == "endif" {
253 if_condition.pop();
254 continue;
255 } else if !(macro_name == "else" || macro_name == "elif") {
256 continue;
257 }
258 } else {
259 continue;
260 }
261 }
262 }
263
264 if let Some(cur_fn_repl) = cur_fn_replacement {
265 if line.ends_with("\\") {
266 cur_fn_replacement = Some((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + &line[..line.len()-1]))
267 } else {
268 fn_replacements.push((cur_fn_repl.0, cur_fn_repl.1, cur_fn_repl.2 + line));
269 cur_fn_replacement = None;
270 }
271 continue;
272 }
273
274 match start_char {
275 Some('#') => {
276 if macro_name.is_none() {
277 macro_name = Some(line_chars.by_ref().take_while(|c| c.is_ascii_alphanumeric()).collect::<String>());
278 }
279 match macro_name.as_ref().unwrap().as_str() {
280 "define" => {
281 let mut is_last_bracket = false;
282 let name = line_chars.by_ref()
283 .skip_while(char::is_ascii_whitespace)
284 .take_while(|c| {
285 if *c == '(' {
286 is_last_bracket = true;
287 }
288 !c.is_ascii_whitespace() && *c != '('
289 })
290 .collect::<String>();
291
292 if is_last_bracket {
293 let params = line_chars.by_ref()
294 .take_while(|c| *c != ')')
295 .chunk_by(|c| *c == ',');
296 let params = params
297 .into_iter()
298 .filter(|(b, _)| !b)
299 .map(|(_, i)| i
300 .skip_while(char::is_ascii_whitespace)
301 .take_while(|c| !c.is_ascii_whitespace())
302 .collect::<String>())
303 .collect::<Vec<String>>();
304
305 let check_param_name = params.iter().find(|param| !param.chars().all(|c| c.is_alphanumeric() || c == '_'))
306 .or(params.iter().find(|param| param.len() == 0 || param.chars().next().unwrap().is_numeric()));
307 if let Some(param_name) = check_param_name {
308 return Err(Error::InvalidParameterName(param_name.clone(), line_num).into())
309 }
310
311 let replacement = line_chars.by_ref().collect::<String>();
312
313 if replacement.ends_with("\\") {
314 cur_fn_replacement = Some((name, params, replacement[..replacement.len()-1].to_string()));
315 } else {
316 fn_replacements.push((name, params, replacement));
317 }
318 } else {
319 let replacement = line_chars.collect::<Cow<str>>();
320 replacements.push((name, replacement))
321 }
322 }, "include" => {
323 let path = line_chars.by_ref()
324 .skip_while(char::is_ascii_whitespace)
325 .take_while(|c| !c.is_ascii_whitespace())
326 .collect::<String>();
327
328 if !(path.starts_with('"') && path.ends_with('"')) {
329 return Err(Error::FirstParamOfIncludeNotString(line_num).into());
330 }
331
332 let path = &path[1..path.len()-1];
333
334 let params = line_chars.by_ref()
335 .chunk_by(|c| *c == ',');
336 let mut params = params
337 .into_iter()
338 .filter(|(b, _)| !b)
339 .map(|(_, i)| Cow::Owned(i.collect::<String>()));
340
341 #[cfg(not(feature = "vfs"))]
342 let file_path = base_dir.join(path);
343
344 #[cfg(feature = "vfs")]
345 let file_path = base_dir.join(path)?;
346
347 let content = read_to_string(&file_path)?;
348
349 match parse_string_cow_rec(&content, Some(path), &base_dir, &mut params, replacements, fn_replacements, visited_sources) {
350 Ok(Some(res)) => {
351 out += res.as_str();
352 visited_sources.push(path.to_string());
353 },
354 Ok(None) => {},
355 Err(err) => return Err(err),
356 };
357 }, "param" => {
358 let param_name = line_chars.by_ref()
359 .skip_while(|c| c.is_ascii_whitespace())
360 .take_while(|c| !c.is_ascii_whitespace())
361 .collect::<String>();
362
363 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
364 return Err(Error::ExtraParamsInMacro(line_num, "param"));
365 }
366
367 let Some(param_value) = parameters.next() else {
368 return Err(Error::NotEnoughParameters);
369 };
370
371 replacements.push((param_name, param_value));
372 }, "pragma" => {
373 let param_name = line_chars.by_ref()
374 .skip_while(|c| c.is_ascii_whitespace())
375 .take_while(|c| !c.is_ascii_whitespace())
376 .collect::<String>();
377
378 if !line_chars.by_ref().all(|c| c.is_ascii_whitespace()) {
379 return Err(Error::ExtraParamsInMacro(line_num, "pragma"));
380 }
381
382 if param_name != "once" {
383 return Err(Error::InvalidPragma(param_name));
384 }
385
386 if let Some(path) = path {
387 if visited_sources.iter().find(|p| p.as_str() == path).is_some() {
388 return Ok(None);
389 }
390 }
391 }, "if" => {
392 let condition = line_chars.collect::<String>();
393 let condition = fn_replace(replace(&condition, &replacements), &fn_replacements)?;
394 let res = eval(&condition)?;
395
396 let Some(res) = res.is_boolean().then(|| res.as_bool().unwrap())
397 .or_else(|| res.is_string().then(|| res.as_str().unwrap() == "true"))
398 .or_else(|| res.is_i64().then(|| res.as_i64().unwrap() == 1))
399 .or_else(|| res.is_u64().then(|| res.as_u64().unwrap() == 1))
400 else {
401 return Err(Error::NonBooleanConditionResult(res));
402 };
403
404 if_condition.push((res, res, false));
405 }, "elif" => {
406 if if_condition.last().map(|r| r.2).unwrap_or(false) {
407 return Err(Error::ElifAfterElse);
408 }
409
410 let condition = line_chars.collect::<String>();
411 let condition = fn_replace(replace(&condition, &replacements), &fn_replacements)?;
412 let res = eval(&condition)?;
413
414 let Some(res) = res.as_bool() else {
415 return Err(Error::NonBooleanConditionResult(res));
416 };
417
418 let last_idx = if_condition.len() - 1;
419 if_condition[last_idx].0 = res;
420 if_condition[last_idx].1 |= res;
421 }, "else" => {
422 let last_idx = if_condition.len() - 1;
423 if_condition[last_idx].0 = !if_condition[last_idx].1;
424 if_condition[last_idx].2 = true;
425 }, "endif" => {
426 if_condition.pop();
427 },
428 _ => return Err(Error::InvalidMacro(macro_name.unwrap(), line_num)),
429 }
430 },
431 Some('\\') if (line_chars.next() == Some('#')) => {
432 out += fn_replace(replace(&line.replacen("\\#", "#", 1), &replacements), &fn_replacements)?.as_ref();
433 if line_num != max_lines {
434 out += "\n";
435 }
436 },
437 _ => {
438 out += fn_replace(replace(line, &replacements), &fn_replacements)?.as_ref();
439 if line_num != max_lines {
440 out += "\n";
441 }
442 },
443 }
444 }
445
446 if parameters.count() != 0 {
447 return Err(Error::UnusedParameters);
448 }
449
450 return Ok(Some(out));
451}
452
453fn ident_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
455 if (start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true))
456 && str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true)
457 {
458 return Some((start, end));
459 } else {
460 return None;
461 }
462}
463
464fn ident_or_paste_range(str: &str, start: usize, end: usize) -> Option<(usize, usize)> {
465 if start >= 2 && str.chars().skip(start - 2).take(2).all(|c| c == '#') {
466 let mut iter = str.chars().skip(end).take(2).tee();
467 if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
469 Some((start - 2, end + 2))
470 } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
472 Some((start - 2, end))
473 } else {
474 None
475 }
476 } else if start == 0 || str.chars().nth(start - 1).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
477 let mut iter = str.chars().skip(end).take(2).tee();
478 if iter.0.count() == 2 && iter.1.all(|c| c == '#') {
480 Some((start, end + 2))
481 } else if str.chars().nth(end).map(|c| !(c.is_alphanumeric() || c == '_')).unwrap_or(true) {
483 Some((start, end))
484 } else {
485 None
486 }
487 } else {
488 None
489 }
490}
491
492fn replace<'a>(line: &'a str, replacements: &Vec<(String, Cow<str>)>) -> Cow<'a, str> {
493 let mut out: Cow<str> = line.into();
494
495 for replacement in replacements {
496 out = replace_all(out, &replacement.0, replacement.1.as_ref(), ident_range);
497 }
498
499 return out;
500}
501
502fn fn_replace<'a>(line: Cow<'a, str>, replacements: &Vec<(String, Vec<String>, String)>) -> Result<Cow<'a, str>> {
503 let mut out: Cow<str> = line;
504
505 for replacement in replacements {
506 out = replace_all_fn(out, replacement.0.as_str(), replacement.2.as_str(), &replacement.1, ident_or_paste_range)?;
507 }
508
509 return Ok(out);
510}
511
512fn replace_all<'a>(str: Cow<'a, str>, to_match: &str, replacement: &str, predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>) -> Cow<'a, str> {
513 let matches = str.match_indices(to_match).collect::<Vec<_>>();
514
515 let mut out: Option<Cow<str>> = None;
516 let mut end_idx = str.len();
517
518 for (idx, _) in matches.into_iter().rev() {
519 if let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_match.len()) {
520 let following_str = &str[end..end_idx];
521 end_idx = start;
522 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
523 .or(Some(concat_string!(replacement, following_str).into()));
524 }
525 }
526
527 if end_idx != 0 {
528 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into())
529 }
530
531 return out.unwrap_or(str);
532}
533
534fn replace_all_fn<'a>(
535 str: Cow<'a, str>,
536 name: &str,
537 replacement: &str,
538 param_names: &Vec<String>,
539 predicate: impl Fn(&str, usize, usize) -> Option<(usize, usize)>,
540) -> Result<Cow<'a, str>> {
541 let matches = str.match_indices(name).collect::<Vec<_>>();
542
543 let mut out: Option<Cow<str>> = None;
544 let mut end_idx = str.len();
545
546 for (idx, _) in matches.into_iter().rev() {
547 let mut iter = str.chars();
548 if iter.by_ref().nth(idx + name.len()) != Some('(') {
549 continue;
550 }
551
552 let mut parens = 0;
553 let mut params = Vec::new();
554 let mut cur = String::new();
555 let mut param_len = 0;
556 for (i, c) in iter.enumerate() {
557 if c == ')' {
558 parens -= 1;
559 if parens == -1 {
560 params.push(cur);
561 cur = String::new();
562 param_len = i;
563 break;
564 }
565 }
566
567 if c == ',' && parens == 0 {
569 params.push(cur);
570 cur = String::new();
571 continue;
572 }
573
574 if c == '(' {
575 parens += 1;
576 }
577
578 cur.push(c);
579 }
580
581 let to_replace_len = name.len() + 2 + param_len;
582
583 let Some((start, end)) = predicate(str.as_ref(), idx, idx + to_replace_len) else {
584 continue;
585 };
586
587 if params.len() < param_names.len() {
588 return Err(Error::NotEnoughParametersMacro(name.to_string()));
589 } else if params.len() > param_names.len() {
590 return Err(Error::UnusedParametersMacro(name.to_string()))
591 }
592
593 let params = param_names.iter()
594 .zip(params.iter().map(|p| p.trim()));
595
596 let mut replacement = Cow::Borrowed(replacement);
597 for param in params {
598 replacement = replace_all(replacement, param.0, param.1, ident_or_paste_range);
599 }
600
601 let following_str = &str[end..end_idx];
602 end_idx = start;
603 out = out.map(|m| concat_string!(replacement, following_str, m.as_ref()).into())
604 .or(Some(concat_string!(replacement, following_str).into()));
605 }
606
607 if end_idx != 0 {
608 out = out.map(|m| concat_string!(&str[0..end_idx], m.as_ref()).into());
609 }
610
611 return Ok(out.unwrap_or(str));
612}