acf_parser/
parser.rs

1use crate::errors::*;
2use chumsky::prelude::*;
3use std::fs;
4use std::collections::HashMap;
5
6// Error handling
7type Result<T> = std::result::Result<T, AcfError>;
8
9/// Representation of an ACF's file content
10/// 
11/// Results are returned in the form of a hash map. Valve ACF files are expected
12/// to have a root level entry (`AppState`) containing the app's ID, path, name,
13/// and filesystem specific information
14#[derive(Clone, Debug, PartialEq, Eq, Default)]
15pub struct Acf {
16    /// A list of entries. Valve ACF files should have at least `AppState`
17    pub entries: Vec<Entry>,
18}
19
20/// Representation of an individual ACF entry
21#[derive(Clone, Debug, PartialEq, Eq, Default)]
22pub struct Entry {
23    /// Name of the entry
24    pub name: String,
25
26    // A list of expressions
27    pub expressions: HashMap<String, String>,
28
29    // A list of sub-entries
30    pub entries: Vec<Entry>,
31}
32
33/// Representation of an individual ACF expression (of form "*."\s+"*.")
34/// 
35/// > NOTE: This is an internal representation that is not shown to the user
36#[derive(Clone, Debug, PartialEq, Eq)]
37struct Expr {
38    /// Name of the expression
39    name: String,
40
41    /// Value of the expression
42    value: String,
43}
44
45/// ACF file parser
46///
47/// An ACF file is just a list of ACF entries. The current implementation returns a vector of
48/// entries, but expects a single root entry. It will not parse files that have additional entries given
49pub fn parse_acf(path: &str) -> Result<Acf> {
50    let contents = match fs::read_to_string(path) {
51        Ok(val) => val,
52        Err(_) => return Err(AcfError::Read(path.into())),
53    };
54
55    let entries = match acf_parser().parse(&contents).into_result() {
56        Ok(val) => val,
57        Err(e) => {
58            e.into_iter()
59                .for_each(|err| println!("Parse error: {}", err));
60            return Err(AcfError::Parse(ParseError::Unknown));
61        }
62    };
63
64    Ok(Acf { entries })
65}
66
67/// ACF parser
68///
69/// A wrapper for the entry parser that allows multiple entries to be defined within the file.
70/// Will parse until the end of the file is reached
71fn acf_parser<'src>() -> impl Parser<'src, &'src str, Vec<Entry>> {
72    entry_parser()
73        .padded()
74        .repeated()
75        .collect::<Vec<_>>()
76        .then_ignore(end())
77        .map(|entries| entries)
78}
79
80/// Entry parser
81///
82/// Entries start with a string literal followed by an opening brace (i.e., '{'). Entries are
83/// expected to have a list of expressions, followed by a list of sub-entries. This ordering
84/// is currently enforced
85fn entry_parser<'src>() -> impl Parser<'src, &'src str, Entry> {
86    recursive(|rec_parser| {
87        str_parser()
88            .padded()
89            .then_ignore(just("{").padded())
90            .then(
91                expr_parser().padded().repeated().collect::<Vec<_>>()
92            )
93            .then(rec_parser.padded().repeated().collect::<Vec<_>>())
94            .then_ignore(just("}").padded())
95            .map(|((name, expressions), entries)| Entry {
96                name,
97                expressions: {
98                    let names = expressions.iter().map(|expr| expr.name.clone());
99                    let values = expressions.iter().map(|expr| expr.value.clone());
100
101                    names.zip(values).collect()
102                },
103                entries,
104            })
105            .boxed()
106    })
107}
108
109/// Expression parser
110///
111/// Expressions are formed by two string literals delimited by some whitespace. There are no
112/// constraints as to what may form entries (will match up until next quote), so you may get
113/// strange resulting expressions if the input file is incorrectly formatted
114fn expr_parser<'src>() -> impl Parser<'src, &'src str, Expr> {
115    str_parser()
116        .padded()
117        .then(str_parser())
118        .padded()
119        .map(|(str1, str2)| Expr {
120            name: str1,
121            value: str2,
122        })
123}
124
125/// String literal parser
126fn str_parser<'src>() -> impl Parser<'src, &'src str, String> {
127    just('"')
128        .ignore_then(none_of('"').repeated().to_slice())
129        .then_ignore(just('"'))
130        .padded()
131        .map(|val: &str| val.to_owned())
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn does_run() {
140        let result = parse_acf("./acfs/simple.acf");
141        assert!(result.is_ok());
142    }
143
144    #[test]
145    fn simple() {
146        let result = parse_acf("./acfs/simple.acf");
147        assert!(result.is_ok());
148        let result = result.unwrap();
149        let root_entry = &result.entries[0];
150        assert_eq!(root_entry.name, "AppState");
151        let expressions = &root_entry.expressions;
152        assert_eq!(expressions["appid"], "730");
153    }
154
155    #[test]
156    fn full() {
157        let result = parse_acf("./acfs/appmanifest_730.acf");
158        assert!(result.is_ok());
159        let result = result.unwrap();
160        let root_entry = &result.entries[0];
161        assert_eq!(root_entry.name, "AppState");
162        let expressions = &root_entry.expressions;
163        assert_eq!(expressions["appid"], "730");
164        assert_eq!(expressions["universe"], "1");
165        assert_eq!(expressions["LauncherPath"], "C:\\\\Program Files (x86)\\\\Steam\\\\steam.exe");
166        assert_eq!(expressions["name"], "Counter-Strike 2");
167    }
168}