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)?)
            }))
        }
    }
}

// TODO: line and position information for certain errors?
#[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),
}