ck3_history_extractor/parser/
section_reader.rs

1use std::{
2    error,
3    fmt::{self, Debug, Display},
4};
5
6use derive_more::From;
7use jomini::{
8    binary::{ReaderError as BinaryReaderError, Token as BinaryToken, TokenResolver},
9    text::{Operator, ReaderError as TextReaderError, Token as TextToken},
10};
11
12use super::{
13    section::Section,
14    tokens::TOKEN_TRANSLATOR,
15    types::{Tape, Token},
16};
17
18/// An error that occurred while reading sections from a tape.
19#[derive(Debug, From)]
20pub enum SectionReaderError<'err> {
21    /// An unexpected token was encountered.
22    UnexpectedToken(usize, Token<'err>, &'static str),
23    /// An unknown token was encountered.
24    UnknownToken(u16),
25    TextReaderError(TextReaderError),
26    BinaryReaderError(BinaryReaderError),
27}
28
29impl<'err> Display for SectionReaderError<'err> {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        match self {
32            Self::UnexpectedToken(pos, tok, desc) => {
33                write!(
34                    f,
35                    "reader encountered an unexpected token {:?} at {}: {}",
36                    tok, pos, desc
37                )
38            }
39            Self::UnknownToken(token) => {
40                write!(f, "reader encountered an unknown token {}", token)
41            }
42            Self::TextReaderError(e) => {
43                write!(f, "text reader encountered an error: {}", e)
44            }
45            Self::BinaryReaderError(e) => {
46                write!(f, "binary reader encountered an error: {}", e)
47            }
48        }
49    }
50}
51
52impl<'err> error::Error for SectionReaderError<'err> {
53    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
54        match self {
55            Self::TextReaderError(e) => Some(e),
56            Self::BinaryReaderError(e) => Some(e),
57            _ => None,
58        }
59    }
60}
61
62/// Essentially an iterator over sections in a tape.
63/// Previously a struct, but this is simpler, and makes the borrow checker happy.
64/// Returns None if there are no more sections. Otherwise, returns the next section, or reports an error.
65pub fn yield_section<'tape, 'data: 'tape>(
66    tape: &'tape mut Tape<'data>,
67) -> Option<Result<Section<'tape, 'data>, SectionReaderError<'data>>> {
68    let mut potential_key = None;
69    let mut past_eq = false;
70    match tape {
71        Tape::Text(text) => {
72            while let Some(res) = text.next().transpose() {
73                match res {
74                    Err(e) => {
75                        return Some(Err(e.into()));
76                    }
77                    Ok(tok) => match tok {
78                        TextToken::Open => {
79                            if past_eq {
80                                if let Some(key) = potential_key {
81                                    return Some(Ok(Section::new(tape, key)));
82                                }
83                            }
84                        }
85                        TextToken::Close => {
86                            return Some(Err(SectionReaderError::UnexpectedToken(
87                                text.position(),
88                                TextToken::Close.into(),
89                                "unexpected close token",
90                            )))
91                        }
92                        TextToken::Operator(op) => {
93                            if op == Operator::Equal {
94                                past_eq = true;
95                            } else {
96                                past_eq = false;
97                            }
98                        }
99                        TextToken::Unquoted(scalar) => {
100                            potential_key = Some(scalar.to_string());
101                        }
102                        _ => {
103                            past_eq = false;
104                            potential_key = None;
105                        }
106                    },
107                }
108            }
109        }
110        Tape::Binary(binary) => {
111            while let Some(res) = binary.next().transpose() {
112                match res {
113                    Err(e) => {
114                        return Some(Err(e.into()));
115                    }
116                    Ok(tok) => match tok {
117                        BinaryToken::Open => {
118                            if past_eq {
119                                if let Some(key) = potential_key {
120                                    return Some(Ok(Section::new(tape, key)));
121                                }
122                            }
123                        }
124                        BinaryToken::Close => {
125                            return Some(Err(SectionReaderError::UnexpectedToken(
126                                tape.position(),
127                                BinaryToken::Close.into(),
128                                "unexpected close token",
129                            )))
130                        }
131                        BinaryToken::Unquoted(token) => potential_key = Some(token.to_string()),
132                        BinaryToken::Id(token) => match TOKEN_TRANSLATOR.resolve(token) {
133                            Some(key) => {
134                                potential_key = Some(key.to_string());
135                            }
136                            None => {
137                                return Some(Err(SectionReaderError::UnknownToken(token)));
138                            }
139                        },
140                        BinaryToken::Equal => {
141                            past_eq = true;
142                        }
143                        _ => {
144                            past_eq = false;
145                            potential_key = None;
146                        }
147                    },
148                }
149            }
150        }
151    }
152    return None;
153}
154
155#[cfg(test)]
156mod tests {
157    use jomini::text::TokenReader;
158
159    use super::*;
160
161    #[test]
162    fn test_empty() {
163        let mut tape = Tape::Text(TokenReader::from_slice(b""));
164        assert!(yield_section(&mut tape).is_none());
165    }
166
167    #[test]
168    fn test_single_section() {
169        let mut tape = Tape::Text(TokenReader::from_slice(b"test={a=1}"));
170        let section = yield_section(&mut tape).unwrap().unwrap();
171        assert_eq!(section.get_name(), "test");
172    }
173
174    #[test]
175    fn test_single_section_messy() {
176        let mut tape = Tape::Text(TokenReader::from_slice(b" \t\r   test={a=1}   \t\r "));
177        let mut section = yield_section(&mut tape).unwrap().unwrap();
178        assert_eq!(section.get_name(), "test");
179        section.skip().unwrap();
180        assert!(yield_section(&mut tape).is_none());
181    }
182
183    #[test]
184    fn test_multiple_sections() {
185        let mut tape = Tape::Text(TokenReader::from_slice(b"test={a=1}test2={b=2}test3={c=3}"));
186        let mut section = yield_section(&mut tape).unwrap().unwrap();
187        assert_eq!(section.get_name(), "test");
188        section.skip().unwrap();
189        let mut section = yield_section(&mut tape).unwrap().unwrap();
190        assert_eq!(section.get_name(), "test2");
191        section.skip().unwrap();
192        let mut section = yield_section(&mut tape).unwrap().unwrap();
193        assert_eq!(section.get_name(), "test3");
194        section.skip().unwrap();
195        assert!(yield_section(&mut tape).is_none());
196    }
197
198    #[test]
199    fn test_non_ascii_key() {
200        let mut tape = Tape::Text(TokenReader::from_slice(b"test={\x80=1}"));
201        let section = yield_section(&mut tape).unwrap().unwrap();
202        assert_eq!(section.get_name(), "test");
203    }
204
205    #[test]
206    fn test_mixed() {
207        let mut tape = Tape::Text(TokenReader::from_slice(
208            b"a\na=b\ntest={a=1}test2={b=2}test3={c=3}",
209        ));
210        let mut section = yield_section(&mut tape).unwrap().unwrap();
211        assert_eq!(section.get_name(), "test");
212        section.skip().unwrap();
213        let mut section = yield_section(&mut tape).unwrap().unwrap();
214        assert_eq!(section.get_name(), "test2");
215        section.skip().unwrap();
216        let mut section = yield_section(&mut tape).unwrap().unwrap();
217        assert_eq!(section.get_name(), "test3");
218        section.skip().unwrap();
219    }
220}