1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
use std::io::{BufRead, BufReader, Lines, Read};
use std::str::FromStr;
use zip::read::ZipFile;
use zip::result::ZipError;
use crate::record::{self, Record};
pub struct Parser<R> {
lines: Lines<BufReader<R>>,
}
impl<R> Parser<R> {
pub fn new(rd: R) -> Result<Self, ParseError>
where
R: Read,
{
let mut lines = BufReader::new(rd).lines();
let file_type = lines.next().ok_or(ParseError::InvalidFileType)??;
if file_type != "FileType=text/acmi/tacview"
&& file_type != "\u{feff}FileType=text/acmi/tacview"
{
return Err(ParseError::InvalidFileType);
}
let version = lines.next().ok_or(ParseError::InvalidVersion)??;
if version.get(..version.len() - 1) != Some("FileVersion=2.")
|| !version
.get(version.len() - 1..)
.map(|s| s.chars().all(|c| c.is_ascii_digit()))
.unwrap_or(false)
{
return Err(ParseError::InvalidVersion);
}
Ok(Parser { lines })
}
pub fn new_compressed(rd: &mut R) -> Result<Parser<ZipFile<'_>>, ParseError>
where
R: Read,
{
let file = zip::read::read_zipfile_from_stream(rd)?
.ok_or(ParseError::Zip(ZipError::FileNotFound))?;
dbg!(file.name());
Parser::new(file)
}
}
impl<R> Iterator for Parser<R>
where
R: Read,
{
type Item = Result<Record, ParseError>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let next = self
.lines
.next()
.filter(|r| r.as_ref().map(|l| !l.is_empty()).unwrap_or(true))?
.map_err(ParseError::Io)
.and_then(parse_line)
.transpose();
if next.is_some() {
return next;
}
}
}
}
fn parse_line(line: String) -> Result<Option<Record>, ParseError> {
let mut chars = line.chars();
match chars.next().ok_or(ParseError::Eol)? {
'-' => {
let id = u64::from_str_radix(&line[1..], 16)?;
Ok(Some(Record::Remove(id)))
}
'#' => {
let id = f64::from_str(&line[1..])?;
Ok(Some(Record::Frame(id)))
}
'/' if chars.next() == Some('/') => Ok(None),
_ => {
let (id, rest) = line.split_once(',').ok_or(ParseError::Eol)?;
Ok(Some(if id == "0" {
let (name, value) = rest
.split_once('=')
.ok_or(ParseError::MissingDelimiter('='))?;
if name == "Event" {
Record::Event(record::Event::from_str(value)?)
} else {
Record::GlobalProperty(record::GlobalProperty::from_str(rest)?)
}
} else {
Record::Update(record::Update::from_str(&line)?)
}))
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("input is not a ACMI file")]
InvalidFileType,
#[error("invalid version, expected ACMI v2.x")]
InvalidVersion,
#[error("error reading input")]
Io(#[from] std::io::Error),
#[error("unexpected end of line")]
Eol,
#[error("object id is not a u64")]
InvalidId(#[from] std::num::ParseIntError),
#[error("expected numeric")]
InvalidNumeric(#[from] std::num::ParseFloatError),
#[error("could not find expected delimiter `{0}`")]
MissingDelimiter(char),
#[error("failed to parse event")]
InvalidEvent,
#[error("encountered invalid coordinate format")]
InvalidCoordinateFormat,
#[error("error reading zip compressed input")]
Zip(#[from] zip::result::ZipError),
}