From 126ef3708fb1f779b1409b837b7b495508fb49a3 Mon Sep 17 00:00:00 2001 From: 132nd-Professor <132nd-Professor> Date: Mon, 15 Aug 2022 20:24:21 +0200 Subject: [PATCH] introduce anyhow, test --- Cargo.toml | 1 + src/main.rs | 54 +++--------------- src/processor.rs | 141 +++++++++++++++++++++++++++++++---------------- src/reader.rs | 36 ++++++------ src/writer.rs | 110 ++++++++++++++++++++++++++++++------ 5 files changed, 213 insertions(+), 129 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2c33aa4..4593e6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" [dependencies] zip = {version = ">= 0.6.2", default-features = false, features = ["deflate"]} +anyhow = {version = ">= 1.0", features = ["backtrace"]} [profile.release] lto = true diff --git a/src/main.rs b/src/main.rs index f736710..c52beb8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,61 +4,21 @@ mod reader; mod tacview; mod writer; -use std::sync::Arc; -use std::{process, thread}; +use anyhow::Result; -use crate::{tacview::Coalition, writer::StringWriter}; +use crate::writer::OutputData; -fn main() { - if let Err(err) = main_inner() { - eprintln!("{err}"); - process::exit(1); - } -} - -fn main_inner() -> Result<(), Box> { +fn main() -> Result<()> { let (input_filename, is_zip) = reader::find_input_file()?; println!("Processing {}", input_filename); let lines = reader::read_data(&input_filename, is_zip)?; + let (header, body) = processor::split_into_header_and_body(lines)?; + let coalition_per_line = processor::divide_body_by_coalition(&body)?; - let coalition_per_line = Arc::new(processor::divide_body_by_coalition(&body)?); - - let header = Arc::new(header); - let body = Arc::new(body); - let input_filename = Arc::new(input_filename); - - let coalitions = vec![Coalition::Blue, Coalition::Red, Coalition::Violet]; - - // clippy wants us to combine both ierators into one. this is not what we want, because then - // we would spawn a thread, join it, and only then spawn a new one. - #[allow(clippy::needless_collect)] - let handles: Vec<_> = coalitions - .iter() - .map(|coalition| { - let z = is_zip; - let b = body.clone(); - let cpl = coalition_per_line.clone(); - let c = coalition.clone(); - let h = header.clone(); - let i = input_filename.clone(); - thread::spawn(move || { - let mut writer = writer::create_writer(z, &*i.clone(), &c).unwrap(); - writer.write_strings(&*h).unwrap(); - writer.write_for_coalition(&*b, &*cpl, c).unwrap(); - }) - }) - .collect(); - - let _: Vec<_> = handles - .into_iter() - .zip(coalitions) - .map(|(h, c)| { - h.join() - .unwrap_or_else(|_| println!("could not write data for {c}")) - }) - .collect(); + let output_data = OutputData::new(input_filename, is_zip, header, body, coalition_per_line); + output_data.save_to_disk()?; Ok(()) } diff --git a/src/processor.rs b/src/processor.rs index 1cdb87c..570b8aa 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -1,24 +1,24 @@ -use std::error::Error; -use std::fmt::Display; -use std::hash::Hash; +use anyhow::{bail, Context, Result}; use crate::tacview::{Coalition, CoalitionIDs}; const COMMENT: char = '#'; const MINUS: char = '-'; const ZERO: char = '0'; +const UTF8_BOM: char = '\u{FEFF}'; // byte order mark. a String can begin with that, in which case + // we need to discard it. -pub fn split_into_header_and_body(mut lines: Vec) -> Result<(Vec, Vec), ProcessingError> +pub fn split_into_header_and_body(mut lines: Vec) -> Result<(Vec, Vec)> where - S: AsRef, + S: AsRef + From, { let mut split_idx = 0; - for line in &lines { + for line in lines.iter() { if line .as_ref() .chars() - .next() - .ok_or(ProcessingError::CannotSplitIntoHeaderAndBody)? + .find(|c| *c != UTF8_BOM) + .with_context(|| "found an empty line")? == COMMENT { break; @@ -29,9 +29,9 @@ where Ok((lines, body)) } -pub fn divide_body_by_coalition(body: &[S]) -> Result, Box> +pub fn divide_body_by_coalition(body: &[S]) -> Result> where - S: AsRef + Hash + Eq, + S: AsRef, { let mut result = vec![]; let mut coalition_ids: CoalitionIDs = CoalitionIDs::new(); @@ -43,9 +43,18 @@ where result.push(processed.coalition.clone()); old_line = processed; } + sanity_check(&result, body); Ok(result) } +fn sanity_check(v1: &[S], v2: &[T]) { + if v1.len() != v2.len() { + println!("Warning: data is inconsistent"); + println!("{}", v1.len()); + println!("{}", v2.len()); + } +} + struct Line { line_type: LineType, continued: bool, @@ -57,10 +66,11 @@ impl Line { old_line: Line, current_line: &'a S, coalition_ids: &mut CoalitionIDs<'a>, - ) -> Result { + ) -> Result { let line_type = match old_line.continued { true => old_line.line_type, - false => LineType::find_type(current_line)?, + false => LineType::find_type(current_line) + .with_context(|| "could not get line type for this line: {current_line}")?, }; match line_type { @@ -69,12 +79,17 @@ impl Line { Self::will_line_continue(current_line), Coalition::All, )), - LineType::Telemetry => Ok(Line::from_content(line_type, current_line, coalition_ids)?), + LineType::Telemetry => Ok(Line::from_content(line_type, current_line, coalition_ids) + .with_context(|| "could not parse telemetry line")?), LineType::Timestamp => Ok(Line::new(line_type, false, Coalition::All)), - LineType::Destruction => { - Ok(Line::from_content(line_type, current_line, coalition_ids)?) - } - LineType::Unknown => Err(ProcessingError::UnknownLineType), + LineType::Destruction => Ok(Line::from_content(line_type, current_line, coalition_ids) + .with_context(|| { + format!( + "could not parse this destruction line: {}", + current_line.as_ref() + ) + })?), + LineType::Unknown => bail!("unknown line type"), } } @@ -82,7 +97,7 @@ impl Line { line_type: LineType, current_line: &'a S, coalition_ids: &mut CoalitionIDs<'a>, - ) -> Result { + ) -> Result { let id = Self::get_id_from_line(current_line, &line_type)?; let continued = Self::will_line_continue(current_line); let coalition = Self::assign_id_to_coalitions(coalition_ids, current_line, id) @@ -109,21 +124,23 @@ impl Line { current_line.as_ref().ends_with('\\') } - fn get_id_from_line<'a, S>( - line: &'a S, - line_type: &LineType, - ) -> Result<&'a str, ProcessingError> + fn get_id_from_line<'a, S>(line: &'a S, line_type: &LineType) -> Result<&'a str> where S: AsRef, { let local_line = line.as_ref(); - if line_type == &LineType::Destruction { - Ok(&local_line[1..local_line.len()]) + let result = if line_type == &LineType::Destruction { + &local_line[1..local_line.len()] } else { - Ok(local_line + local_line .split_once(',') - .ok_or_else(|| ProcessingError::CannotGetIDFromLine(local_line.to_owned()))? - .0) + .with_context(|| format!("cannot get ID from line {local_line}"))? + .0 + }; + if result.is_empty() { + bail!("cannot get ID from this line\n{local_line}") + } else { + Ok(result) } } @@ -142,7 +159,7 @@ impl Default for Line { } } -#[derive(PartialEq, Clone)] +#[derive(PartialEq, Clone, Debug)] enum LineType { Unknown, Timestamp, @@ -152,43 +169,71 @@ enum LineType { } impl LineType { - fn find_type>(line: &S) -> Result { + fn find_type>(line: &S) -> Result { let first_char = line .as_ref() .chars() - .next() - .ok_or(ProcessingError::LineIsEmptyError)?; + .find(|c| *c != UTF8_BOM) + .with_context(|| "line in tacview file is empty")?; + let mut buffer: [u8; 4] = [0; 4]; + first_char.encode_utf8(&mut buffer); if first_char == COMMENT { Ok(Self::Timestamp) } else if first_char == MINUS { Ok(Self::Destruction) } else if first_char == ZERO { Ok(LineType::ArbitraryData) + } else if first_char.is_whitespace() { + bail!("found line beginning with whitespace") } else { Ok(LineType::Telemetry) } } } -#[derive(Debug)] -pub enum ProcessingError { - LineIsEmptyError, - UnknownLineType, - CannotSplitIntoHeaderAndBody, - CannotGetIDFromLine(String), -} +#[cfg(test)] +mod test { + use super::{Line, LineType}; -impl Error for ProcessingError {} + #[test] + fn test_find_type() { + assert_eq!(LineType::find_type(&"#0").unwrap(), LineType::Timestamp); + assert_eq!( + LineType::find_type(&"102,T=5.0785362|6.203").unwrap(), + LineType::Telemetry + ); + assert_eq!(LineType::find_type(&"-102").unwrap(), LineType::Destruction); + assert_eq!( + LineType::find_type(&"0,Authentication").unwrap(), + LineType::ArbitraryData + ); + } -impl Display for ProcessingError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::LineIsEmptyError => write!(f, "line in tacview file is empty"), - Self::UnknownLineType => write!(f, "unknown line type in tacview file"), - Self::CannotSplitIntoHeaderAndBody => { - write!(f, "cannot split the file into header and body") - } - Self::CannotGetIDFromLine(l) => write!(f, "cannot get unit ID from line: \n{l}"), + #[test] + #[should_panic] + fn test_find_type_for_space() { + LineType::find_type(&" ").unwrap(); + } + + #[test] + fn test_get_id_from_line() { + let lines = vec![ + ("102,T=5.0785362|6.203", "102"), + ("2d02,T=", "2d02"), + ("248b02,T=6.70283", "248b02"), + ]; + for l in lines { + assert_eq!( + &Line::get_id_from_line(&l.0, &LineType::Telemetry).unwrap(), + &l.1 + ) + } + let lines = vec![("-102", "102"), ("-2d02", "2d02"), ("-248b02", "248b02")]; + for l in lines { + assert_eq!( + &Line::get_id_from_line(&l.0, &LineType::Destruction).unwrap(), + &l.1 + ) } } } diff --git a/src/reader.rs b/src/reader.rs index f13a810..1f3ed85 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1,12 +1,17 @@ -use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP}; use std::fs::{self, File}; -use std::io::{BufRead, BufReader, Error, ErrorKind}; +use std::io::{BufRead, BufReader}; -pub fn find_input_file() -> Result<(String, bool), Error> { - let read_dir = fs::read_dir(".")?; +use anyhow::{bail, Context, Result}; + +use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP}; + +pub fn find_input_file() -> Result<(String, bool)> { + let read_dir = fs::read_dir(".").with_context(|| "could not open the current directory")?; for entry_result in read_dir { - let path_buf = entry_result?.path(); + let path_buf = entry_result + .with_context(|| "error while iterating over directory")? + .path(); let filename = path_buf.to_string_lossy().to_string(); if filename.ends_with(EXTENSION_TXT) { @@ -15,14 +20,11 @@ pub fn find_input_file() -> Result<(String, bool), Error> { return Ok((filename, true)); } } - Err(Error::new( - ErrorKind::NotFound, - "No tacview input file found in current directory.", - )) + bail!("No tacview input file found in current directory.") } -pub fn read_data(filename: &str, is_zip: bool) -> Result, Error> { - let file = fs::File::open(filename)?; +pub fn read_data(filename: &str, is_zip: bool) -> Result> { + let file = fs::File::open(filename).with_context(|| "could not open {filename}")?; let buf = BufReader::new(file); if is_zip { Ok(read_zip(buf)?) @@ -31,17 +33,19 @@ pub fn read_data(filename: &str, is_zip: bool) -> Result, Error> { } } -fn read_zip(buf: BufReader) -> Result, Error> { - let mut archive = zip::ZipArchive::new(buf)?; - let inner_file = archive.by_index(0)?; +fn read_zip(buf: BufReader) -> Result> { + let mut archive = zip::ZipArchive::new(buf).with_context(|| "could not open zip archive")?; + let inner_file = archive + .by_index(0) + .with_context(|| "could not get the txt file in the zip archive")?; let inner_buf = BufReader::new(inner_file); read_txt(inner_buf) } -fn read_txt(buf: impl BufRead) -> Result, Error> { +fn read_txt(buf: impl BufRead) -> Result> { let mut lines = vec![]; for line in buf.lines() { - lines.push(line?); + lines.push(line.with_context(|| "could not read line from file, is it valid UTF-8?")?); } Ok(lines) } diff --git a/src/writer.rs b/src/writer.rs index 1e3e533..9426977 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -1,16 +1,20 @@ -use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP}; -use crate::tacview::Coalition; -use std::error::Error as ErrTrait; use std::fs::File; -use std::io::{Error, Write}; +use std::io::Write; +use std::sync::Arc; +use std::thread; + +use anyhow::{Context, Result}; use zip::write::FileOptions; use zip::ZipWriter; +use crate::constants::{EXTENSION_TXT, EXTENSION_ZIP}; +use crate::tacview::Coalition; + pub fn create_writer( is_zip: bool, filename: &str, coalition: &Coalition, -) -> Result, Box> { +) -> Result> { let base_name = remove_extension(filename, is_zip); let txt_name = format!("{base_name}_{coalition}{EXTENSION_TXT}"); @@ -31,11 +35,11 @@ fn remove_extension(filename: &str, is_zip: bool) -> &str { } } -fn create_textwriter(filename: &str) -> Result { - File::create(filename) +fn create_textwriter(filename: &str) -> Result { + File::create(filename).with_context(|| "could not create text file") } -fn create_zipwriter(outer_name: &str, inner_name: &str) -> Result, Error> { +fn create_zipwriter(outer_name: &str, inner_name: &str) -> Result> { let mut writer = ZipWriter::new(create_textwriter(outer_name)?); writer.start_file( inner_name, @@ -45,27 +49,27 @@ fn create_zipwriter(outer_name: &str, inner_name: &str) -> Result>(&mut self, line: &S) -> Result<(), Error>; + fn write_line>(&mut self, line: &S) -> Result<()>; - fn write_strings>(&mut self, lines: &[S]) -> Result<(), Error>; + fn write_strings>(&mut self, lines: &[S]) -> Result<()>; fn write_for_coalition>( &mut self, lines: &[S], coalition_per_line: &[Coalition], - coalition: Coalition, - ) -> Result<(), Box>; + coalition: &Coalition, + ) -> Result<()>; } impl StringWriter for T where T: Write, { - fn write_line>(&mut self, line: &S) -> Result<(), Error> { - writeln!(self, "{}", line.as_ref()) + fn write_line>(&mut self, line: &S) -> Result<()> { + writeln!(self, "{}", line.as_ref()).with_context(|| "could not write line") } - fn write_strings>(&mut self, lines: &[S]) -> Result<(), Error> { + fn write_strings>(&mut self, lines: &[S]) -> Result<()> { for line in lines { self.write_line(line)?; } @@ -76,14 +80,14 @@ where &mut self, lines: &[S], coalition_per_line: &[Coalition], - coalition: Coalition, - ) -> Result<(), Box> { + coalition: &Coalition, + ) -> Result<()> { assert_eq!(lines.len(), coalition_per_line.len()); let iter = lines .iter() .zip(coalition_per_line) - .filter(|(_, c)| **c == coalition || **c == Coalition::All) + .filter(|(_, c)| *c == coalition || **c == Coalition::All) .map(|(l, _)| l); for line in iter { @@ -93,3 +97,73 @@ where Ok(()) } } + +pub struct OutputData { + input_filename: Arc, + is_zip: bool, + header: Arc>, + body: Arc>, + coalition_per_line: Arc>, +} + +impl OutputData { + pub fn new( + input_filename: String, + is_zip: bool, + header: Vec, + body: Vec, + coalition_per_line: Vec, + ) -> Self { + let body = Arc::new(body); + let coalition_per_line = Arc::new(coalition_per_line); + let header = Arc::new(header); + let input_filename = Arc::new(input_filename); + + Self { + is_zip, + body, + coalition_per_line, + header, + input_filename, + } + } + + // clippy wants us to combine both ierators into one. this is not what we want, because then + // we would spawn a thread, join it, and only then spawn a new one. + #[allow(clippy::needless_collect)] + pub fn save_to_disk(&self) -> Result<()> { + let coalitions = vec![Coalition::Blue, Coalition::Red, Coalition::Violet]; + + let handles: Vec<_> = coalitions + .iter() + .map(|coalition| { + let z = self.is_zip; + let b = self.body.clone(); + let cpl = self.coalition_per_line.clone(); + let c = coalition.clone(); + let h = self.header.clone(); + let i = self.input_filename.clone(); + + thread::spawn(move || { + let mut writer = create_writer(z, &*i.clone(), &c) + .with_context(|| format!("could not create writer for {c}")) + .unwrap(); + writer + .write_strings(&*h) + .with_context(|| format!("could not write header for {c}")) + .unwrap(); + writer + .write_for_coalition(&*b, &*cpl, &c) + .with_context(|| format!("could not write body for {c}")) + .unwrap(); + }) + }) + .collect(); + + handles.into_iter().zip(coalitions).for_each(|(h, c)| { + h.join() + .unwrap_or_else(|_| println!("could not write data for {c}")) + }); + Ok(()) + } +}