28 Commits
Author SHA1 Message Date
132nd-Professor 13187c9d9b version bump to 0.3.2 2023-01-18 14:55:50 +01:00
132nd-Professor 1a6ff6fe6f remove anyhow backtrace feature 2023-01-18 14:54:04 +01:00
132nd-Professor 8026ef1a04 add blanket implementation for StringWriter 2022-11-07 17:51:34 +01:00
132nd-Professor a6acdbac4f clean up save_to_disk by cloning OutputData 2022-11-07 17:35:06 +01:00
132nd-Professor 61cfbd3e13 use auto-deref for Arcs 2022-11-07 16:58:08 +01:00
132nd-Professor e51e57505e propagate error from sanity_check 2022-11-03 14:31:21 +01:00
132nd-Professor ea81556898 print program name and version on start 2022-11-03 14:28:30 +01:00
132nd-Professor cfb9b4e8d6 rename divide_body_by_coalition 2022-08-15 20:40:39 +02:00
132nd-Professor 24d50c2c62 clean up import 2022-08-15 20:35:10 +02:00
132nd-Professor 126ef3708f introduce anyhow, test 2022-08-15 20:24:21 +02:00
132nd-Professor 2bf2352c67 refactor create_writer 2022-08-12 22:06:37 +02:00
132nd-Professor d944942d62 remove debug output 2022-08-12 22:06:23 +02:00
132nd-Professor ac873d385e replace purple with violet 2022-08-12 22:06:01 +02:00
132nd-Professor 89a79e3015 update build scripts 2022-08-12 21:55:21 +02:00
132nd-Professor 1cbf11a468 handle arbitrary data in files 2022-08-12 21:55:13 +02:00
132nd-Professor 43b1bacaab updated build scripts 2022-08-12 20:38:03 +02:00
132nd-Professor f94db99810 add error messages for threads 2022-08-12 20:22:45 +02:00
132nd-Professor 5eccad6517 remove rayon, manual parallelization 2022-08-12 20:02:44 +02:00
132nd-Professor 8c46e2bbcc parallelize with rayon 2022-08-12 18:47:59 +02:00
132nd-Professor cda33692cf fix coalition assigning 2022-08-12 17:47:48 +02:00
132nd-Professor c1ef22f3a3 fix output filename generation 2022-08-12 17:23:16 +02:00
132nd-Professor fd800ce394 make crate constants locale to module 2022-08-12 16:52:06 +02:00
132nd-Professor a35ff8b362 fix some lints 2022-08-12 16:48:43 +02:00
132nd-Professor d7db52fc2d finish error handling 2022-08-12 16:39:38 +02:00
132nd-Professor f87a3c9e06 remove lib.rs 2022-08-12 16:20:06 +02:00
132nd-Professor b82c40d4ab more error handling 2022-08-12 16:19:42 +02:00
132nd-Professor 7d66b1ff31 start adding error handling (WIP) 2022-08-11 20:56:49 +02:00
132nd-Professor 84f1bf7efe refactor 2022-08-11 20:29:17 +02:00
9 changed files with 578 additions and 383 deletions
+4 -2
View File
@@ -1,12 +1,14 @@
[package] [package]
name = "tacview-splitter" name = "tacview-splitter"
version = "0.3.1" version = "0.3.2"
authors = ["132nd-Professor <132nd-Professor>"] authors = ["132nd-Professor <132nd-Professor>"]
edition = "2021" edition = "2021"
[dependencies] [dependencies]
zip = ">= 0.6.2" zip = {version = ">= 0.6.2", default-features = false, features = ["deflate"]}
anyhow = {version = ">= 1.0"}
[profile.release] [profile.release]
lto = true lto = true
opt-level = 3 opt-level = 3
strip = "symbols"
+5 -9
View File
@@ -10,25 +10,21 @@ TEST_FILES=Tacview-20210606-222650-DCS-ATRM_2.7.0.443.txt.acmi Tacview-20210606-
default: release default: release
.built: src/lib.rs src/main.rs Cargo.toml .built: src/*.rs
cargo build --release --target x86_64-unknown-linux-gnu cargo build --release --target x86_64-unknown-linux-gnu
cargo build --release --target x86_64-pc-windows-gnu cargo build --release --target x86_64-pc-windows-gnu
touch .built touch .built
build: .built build: .built
.stripped: .built .tested: .built
strip ${BIN_LINUX} ${BIN_WINDOWS}
touch .stripped
strip: .stripped
.tested: .stripped
for file in ${TEST_FILES}; do \ for file in ${TEST_FILES}; do \
mkdir -p test; \ mkdir -p test; \
cp testfiles/$$file test; \ cp testfiles/$$file test; \
if [[ $$? != 0 ]]; then exit 1; fi;\
cd test; \ cd test; \
../${BIN_LINUX}; \ ../${BIN_LINUX}; \
if [[ $$? != 0 ]]; then exit 1; fi;\
cd ../ ; \ cd ../ ; \
rm -rf test ; \ rm -rf test ; \
done done
@@ -49,4 +45,4 @@ test: .tested
release: .released release: .released
clean: clean:
rm -rf release target test .released .tested .stripped .built rm -rf release target test .released .tested .built
+2
View File
@@ -0,0 +1,2 @@
pub const EXTENSION_ZIP: &str = ".zip.acmi";
pub const EXTENSION_TXT: &str = ".txt.acmi";
-147
View File
@@ -1,147 +0,0 @@
pub mod lib {
use std::collections::HashSet;
use std::fs;
use std::io::Write;
use zip;
const ERR_CANNOT_WRITE_DATA: &str = "Could not write data";
const ERR_CANNOT_OPEN_OUTPUT: &str = "Could not open output file";
const ERR_CANNOT_BEGIN_FILE: &str = "Could not begin file in zip archive";
pub struct IDs<'a> {
pub blue: HashSet<&'a str>,
pub red: HashSet<&'a str>,
pub violet: HashSet<&'a str>,
pub unknown: HashSet<&'a str>,
}
pub struct Descriptors<T: Write> {
blue: T,
red: T,
violet: T,
}
pub struct DescriptorsTxt {
pub blue: fs::File,
pub red: fs::File,
pub violet: fs::File,
}
pub struct OutputFilenames {
pub txt: FilenamesVariant,
pub zip: FilenamesVariant,
}
pub struct FilenamesVariant {
pub blue: String,
pub red: String,
pub violet: String,
}
pub struct BodiesByCoalition<'a> {
pub blue: Vec<&'a str>,
pub red: Vec<&'a str>,
pub violet: Vec<&'a str>,
}
pub trait Handling {
fn write(&mut self, header: Vec<String>, bodies_by_coalition: BodiesByCoalition);
}
impl<T: Write> Handling for Descriptors<T> {
fn write(&mut self, header: Vec<String>, bodies_by_coalition: BodiesByCoalition) {
for line in &header {
writeln!(self.blue, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
writeln!(self.red, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
writeln!(self.violet, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
}
for line in &bodies_by_coalition.blue {
writeln!(self.blue, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
}
for line in &bodies_by_coalition.red {
writeln!(self.red, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
}
for line in &bodies_by_coalition.violet {
writeln!(self.violet, "{}", line).expect(ERR_CANNOT_WRITE_DATA);
}
}
}
impl Descriptors<fs::File> {
pub fn new(filenames: OutputFilenames) -> Descriptors<fs::File> {
let blue = fs::File::create(&filenames.txt.blue).expect(ERR_CANNOT_OPEN_OUTPUT);
let red = fs::File::create(&filenames.txt.red).expect(ERR_CANNOT_OPEN_OUTPUT);
let violet = fs::File::create(&filenames.txt.violet).expect(ERR_CANNOT_OPEN_OUTPUT);
Descriptors { blue, red, violet }
}
}
impl Descriptors<zip::ZipWriter<fs::File>> {
pub fn new(filenames: OutputFilenames) -> Descriptors<zip::ZipWriter<fs::File>> {
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
let file = fs::File::create(&filenames.zip.blue).expect(ERR_CANNOT_OPEN_OUTPUT);
let mut blue = zip::ZipWriter::new(file);
blue.start_file(&filenames.txt.blue, options)
.expect(ERR_CANNOT_BEGIN_FILE);
let file = fs::File::create(&filenames.zip.red).expect(ERR_CANNOT_OPEN_OUTPUT);
let mut red = zip::ZipWriter::new(file);
red.start_file(&filenames.txt.red, options)
.expect(ERR_CANNOT_BEGIN_FILE);
let file = fs::File::create(&filenames.zip.violet).expect(ERR_CANNOT_OPEN_OUTPUT);
let mut violet = zip::ZipWriter::new(file);
violet
.start_file(&filenames.txt.violet, options)
.expect(ERR_CANNOT_BEGIN_FILE);
Descriptors { blue, red, violet }
}
}
pub fn sanity_check_output_filenames(
input_filename: &String,
output_filenames: &FilenamesVariant,
) {
if input_filename == &output_filenames.blue
|| input_filename == &output_filenames.red
|| input_filename == &output_filenames.violet
{
panic!("Output filenames were the same as input filenames")
}
}
pub fn get_output_filenames_individual(
input_filename: &str,
old_extension: &str,
new_extension: &str,
coalition: &str,
) -> String {
let mut output_extension = coalition.to_owned();
output_extension.push_str(new_extension);
input_filename.replace(old_extension, &output_extension)
}
}
#[cfg(test)]
mod tests {
use crate::lib::*;
#[test]
fn test_get_output_filenames_individual() {
let input_filename = "something.txt.acmi".to_string();
let extension = ".txt.acmi";
let coalition = "_blue";
let result =
get_output_filenames_individual(&input_filename, extension, extension, coalition);
let correct_result = "something_blue.txt.acmi";
assert_eq!(result, correct_result);
let extension_wrong = ".tXT.acmi";
let result =
get_output_filenames_individual(&input_filename, extension_wrong, extension, coalition);
assert_ne!(result, correct_result);
}
}
+22 -225
View File
@@ -1,231 +1,28 @@
use std::collections::HashSet; mod constants;
use std::io::{BufRead, BufReader}; mod processor;
use std::{fs, str}; mod reader;
mod tacview;
mod writer;
use tacview_splitter::lib; use anyhow::Result;
use tacview_splitter::lib::Handling;
const COMMENT: char = '#'; fn main() -> Result<()> {
const MINUS: char = '-'; println!(
"{}, version {}",
const EXTENSION_ZIP: &str = ".zip.acmi"; env!("CARGO_PKG_NAME"),
const EXTENSION_TXT: &str = ".txt.acmi"; env!("CARGO_PKG_VERSION")
#[derive(PartialEq)]
enum LineType {
Unknown,
Timestamp,
Destruction,
Telemetry,
}
fn main() {
let (input_filename, is_zip) = find_input_file();
println!("Processing {}", input_filename);
let lines = read_data(&input_filename, is_zip);
let (header, body) = split_into_header_and_body(lines);
let bodies_by_coalition = divide_body_by_coalition(&body);
let output_filenames = get_output_filenames(&input_filename, is_zip);
if is_zip {
let mut descriptors = lib::Descriptors::<zip::ZipWriter<fs::File>>::new(output_filenames);
descriptors.write(header, bodies_by_coalition);
} else {
let mut descriptors = lib::Descriptors::<fs::File>::new(output_filenames);
descriptors.write(header, bodies_by_coalition);
}
}
fn split_into_header_and_body(lines: Vec<String>) -> (Vec<String>, Vec<String>) {
let mut i = 0;
for line in &lines {
if line.chars().next().expect("malformed line") == COMMENT {
break;
}
i += 1;
}
(lines[..i].to_vec(), lines[i..].to_vec())
}
fn divide_body_by_coalition(body: &Vec<String>) -> lib::BodiesByCoalition {
let mut bbc = lib::BodiesByCoalition {
blue: Vec::new(),
red: Vec::new(),
violet: Vec::new(),
};
let mut continued = false;
let mut line_type = LineType::Unknown;
let mut coalitions = lib::IDs {
blue: HashSet::new(),
red: HashSet::new(),
violet: HashSet::new(),
unknown: HashSet::new(),
};
for line in body {
let result = process_line(continued, &mut coalitions, line, line_type);
line_type = result.0;
continued = result.1;
let id = result.2;
if line_type == LineType::Timestamp {
bbc.blue.push(line);
bbc.red.push(line);
bbc.violet.push(line);
} else {
// destruction or telemetry
if coalitions.blue.contains(&id) {
bbc.blue.push(line);
} else if coalitions.red.contains(&id) {
bbc.red.push(line);
} else if coalitions.violet.contains(&id) {
bbc.violet.push(line);
}
}
}
bbc
}
fn process_line<'a>(
continued: bool,
coalitions: &mut lib::IDs<'a>,
line: &'a str,
last_line_type: LineType,
) -> (LineType, bool, &'a str) {
let mut id = "";
let line_type;
if !continued {
line_type = determine_line_type(line);
if line_type == LineType::Telemetry {
id = get_id_from_line(line);
assign_id_to_coalitions(coalitions, line, id)
}
} else {
line_type = last_line_type;
}
let line_will_continue = will_line_continue(line);
(line_type, line_will_continue, id)
}
fn will_line_continue(line: &str) -> bool {
line.ends_with('\\')
}
fn get_id_from_line(line: &str) -> &str {
let result = line.split_once(',');
let split = match result {
Some(t) => t,
None => panic!("Could not get ID from line!"),
};
split.0 as _
}
fn assign_id_to_coalitions<'a>(coalitions: &mut lib::IDs<'a>, line: &'a str, id: &'a str) {
if line.contains("Color=") {
if line.contains("Color=Blue") {
coalitions.blue.insert(id);
} else if line.contains("Color=Red") {
coalitions.red.insert(id);
} else if line.contains("Color=Violet") {
coalitions.violet.insert(id);
} else {
coalitions.unknown.insert(id);
}
}
}
fn determine_line_type(line: &str) -> LineType {
let first_char = line.chars().next().expect("malformed line");
if first_char == COMMENT {
LineType::Timestamp
} else if first_char == MINUS {
LineType::Destruction
} else {
LineType::Telemetry
}
}
fn get_output_filenames(input_filename: &String, is_zip: bool) -> lib::OutputFilenames {
let output_filenames_zip: lib::FilenamesVariant;
let output_filenames_txt: lib::FilenamesVariant;
if is_zip {
output_filenames_zip =
get_output_filenames_for_extension(input_filename, EXTENSION_ZIP, EXTENSION_ZIP);
output_filenames_txt =
get_output_filenames_for_extension(input_filename, EXTENSION_ZIP, EXTENSION_TXT);
} else {
output_filenames_zip = get_output_filenames_dummy();
output_filenames_txt =
get_output_filenames_for_extension(input_filename, EXTENSION_TXT, EXTENSION_TXT);
}
lib::OutputFilenames {
txt: output_filenames_txt,
zip: output_filenames_zip,
}
}
fn get_output_filenames_dummy() -> lib::FilenamesVariant {
let blue = "".to_string();
let red = "".to_string();
let violet = "".to_string();
lib::FilenamesVariant { blue, red, violet }
}
fn get_output_filenames_for_extension(
input_filename: &String,
old_extension: &str,
new_extension: &str,
) -> lib::FilenamesVariant {
let blue =
lib::get_output_filenames_individual(input_filename, old_extension, new_extension, "_blue");
let red =
lib::get_output_filenames_individual(input_filename, old_extension, new_extension, "_red");
let violet = lib::get_output_filenames_individual(
input_filename,
old_extension,
new_extension,
"_violet",
); );
let output_filenames_zip = lib::FilenamesVariant { blue, red, violet }; let (input_filename, is_zip) = reader::find_input_file()?;
lib::sanity_check_output_filenames(input_filename, &output_filenames_zip); println!("Processing {}", input_filename);
output_filenames_zip
}
fn find_input_file() -> (String, bool) { let lines = reader::read_data(&input_filename, is_zip)?;
let read_dir = fs::read_dir(".").expect("Could not read current directory");
for entry_result in read_dir {
let entry = entry_result.expect("Could not parse DirEntry");
let path_buf = entry.path();
let filename = path_buf.to_string_lossy().to_string();
if filename.ends_with(EXTENSION_TXT) {
return (filename, false);
} else if filename.ends_with(EXTENSION_ZIP) {
return (filename, true);
}
}
println!("No tacview input file found in current directory.");
std::process::exit(1);
}
fn read_data(filename: &String, is_zip: bool) -> Vec<String> { let (header, body) = processor::split_into_header_and_body(lines)?;
let file = fs::File::open(filename).expect("Could not read from input file"); let coalition_per_line = processor::get_coalition_per_line(&body)?;
let buf = BufReader::new(file);
return if is_zip { let output_data =
let mut archive = zip::ZipArchive::new(buf).expect("Could not read zip data"); writer::OutputData::new(input_filename, is_zip, header, body, coalition_per_line);
let inner_file = archive output_data.save_to_disk()?;
.by_index(0)
.expect("Could not read telemetry file from zip archive"); Ok(())
let inner_buf = BufReader::new(inner_file);
let lines: Vec<String> = inner_buf
.lines()
.map(|l| l.expect("Could not parse line"))
.collect();
lines
} else {
let lines: Vec<String> = buf
.lines()
.map(|l| l.expect("Could not parse line"))
.collect();
lines
};
} }
+238
View File
@@ -0,0 +1,238 @@
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<S>(mut lines: Vec<S>) -> Result<(Vec<S>, Vec<S>)>
where
S: AsRef<str> + From<String>,
{
let mut split_idx = 0;
for line in lines.iter() {
if line
.as_ref()
.chars()
.find(|c| *c != UTF8_BOM)
.with_context(|| "found an empty line")?
== COMMENT
{
break;
}
split_idx += 1;
}
let body = lines.split_off(split_idx);
Ok((lines, body))
}
pub fn get_coalition_per_line<S>(body: &[S]) -> Result<Vec<Coalition>>
where
S: AsRef<str>,
{
let mut result = vec![];
let mut coalition_ids: CoalitionIDs = CoalitionIDs::new();
let mut old_line = Line::default();
for line_s in body {
let processed = Line::process_line(old_line, line_s, &mut coalition_ids)?;
result.push(processed.coalition.clone());
old_line = processed;
}
sanity_check(&result, body)?;
Ok(result)
}
fn sanity_check<S, T>(v1: &[S], v2: &[T]) -> Result<()> {
if v1.len() != v2.len() {
bail!("Warning: data is inconsistent\n{}\n{}", v1.len(), v2.len())
}
Ok(())
}
struct Line {
line_type: LineType,
continued: bool,
coalition: Coalition,
}
impl Line {
fn process_line<'a, S: AsRef<str>>(
old_line: Line,
current_line: &'a S,
coalition_ids: &mut CoalitionIDs<'a>,
) -> Result<Self> {
let line_type = match old_line.continued {
true => old_line.line_type,
false => LineType::find_type(current_line)
.with_context(|| "could not get line type for this line: {current_line}")?,
};
match line_type {
LineType::ArbitraryData => Ok(Line::new(
line_type,
Self::will_line_continue(current_line),
Coalition::All,
)),
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)
.with_context(|| {
format!(
"could not parse this destruction line: {}",
current_line.as_ref()
)
})?),
LineType::Unknown => bail!("unknown line type"),
}
}
fn from_content<'a, S: AsRef<str>>(
line_type: LineType,
current_line: &'a S,
coalition_ids: &mut CoalitionIDs<'a>,
) -> Result<Self> {
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)
.unwrap_or(Coalition::Unknown); // happens for the authentication line at the end of
// a file that has verification enabled
Ok(Line::new(line_type, continued, coalition))
}
fn assign_id_to_coalitions<'a, S: AsRef<str>>(
coalition_ids: &mut CoalitionIDs<'a>,
line: &S,
id: &'a str,
) -> Option<Coalition> {
if Coalition::line_contains_coalition(line) {
let coalition = Coalition::from_line(line);
coalition_ids.insert(id, &coalition);
Some(coalition)
} else {
coalition_ids.get(id)
}
}
fn will_line_continue<S: AsRef<str>>(current_line: &S) -> bool {
current_line.as_ref().ends_with('\\')
}
fn get_id_from_line<'a, S>(line: &'a S, line_type: &LineType) -> Result<&'a str>
where
S: AsRef<str>,
{
let local_line = line.as_ref();
let result = if line_type == &LineType::Destruction {
&local_line[1..local_line.len()]
} else {
local_line
.split_once(',')
.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)
}
}
fn new(line_type: LineType, continued: bool, coalition: Coalition) -> Self {
Self {
line_type,
continued,
coalition,
}
}
}
impl Default for Line {
fn default() -> Self {
Self::new(LineType::Unknown, false, Coalition::Unknown)
}
}
#[derive(PartialEq, Clone, Debug)]
enum LineType {
Unknown,
Timestamp,
Destruction,
Telemetry,
ArbitraryData,
}
impl LineType {
fn find_type<S: AsRef<str>>(line: &S) -> Result<Self> {
let first_char = line
.as_ref()
.chars()
.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)
}
}
}
#[cfg(test)]
mod test {
use super::{Line, LineType};
#[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
);
}
#[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
)
}
}
}
+51
View File
@@ -0,0 +1,51 @@
use std::fs::{self, File};
use std::io::{BufRead, BufReader};
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
.with_context(|| "error while iterating over directory")?
.path();
let filename = path_buf.to_string_lossy().to_string();
if filename.ends_with(EXTENSION_TXT) {
return Ok((filename, false));
} else if filename.ends_with(EXTENSION_ZIP) {
return Ok((filename, true));
}
}
bail!("No tacview input file found in current directory.")
}
pub fn read_data(filename: &str, is_zip: bool) -> Result<Vec<String>> {
let file = fs::File::open(filename).with_context(|| "could not open {filename}")?;
let buf = BufReader::new(file);
if is_zip {
Ok(read_zip(buf)?)
} else {
Ok(read_txt(buf)?)
}
}
fn read_zip(buf: BufReader<File>) -> Result<Vec<String>> {
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<Vec<String>> {
let mut lines = vec![];
for line in buf.lines() {
lines.push(line.with_context(|| "could not read line from file, is it valid UTF-8?")?);
}
Ok(lines)
}
+80
View File
@@ -0,0 +1,80 @@
use std::collections::HashSet;
use std::fmt::Display;
#[derive(PartialEq, Clone)]
pub enum Coalition {
Blue,
Red,
Violet,
All,
Unknown,
}
impl Coalition {
pub fn from_line<S: AsRef<str>>(line: &S) -> Self {
let line = line.as_ref();
if line.contains("Color=Blue") {
Self::Blue
} else if line.contains("Color=Red") {
Self::Red
} else if line.contains("Color=Violet") {
Self::Violet
} else {
Self::Unknown
}
}
pub fn line_contains_coalition<S: AsRef<str>>(line: &S) -> bool {
line.as_ref().contains("Color=")
}
}
impl Display for Coalition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Blue => "blue".to_string(),
Self::Red => "red".to_string(),
Self::Violet => "violet".to_string(),
Self::All => "all".to_string(),
Self::Unknown => "unknown".to_string(),
}
)
}
}
pub struct CoalitionIDs<'a> {
pub blue: HashSet<&'a str>,
pub red: HashSet<&'a str>,
pub violet: HashSet<&'a str>,
}
impl<'a> CoalitionIDs<'a> {
pub fn new() -> Self {
let (blue, red, violet) = (HashSet::new(), HashSet::new(), HashSet::new());
Self { blue, red, violet }
}
pub fn insert(&mut self, id: &'a str, coalition: &Coalition) {
match coalition {
Coalition::Blue => self.blue.insert(id),
Coalition::Red => self.red.insert(id),
Coalition::Violet => self.violet.insert(id),
Coalition::All | Coalition::Unknown => false,
};
}
pub fn get(&self, id: &'a str) -> Option<Coalition> {
if self.blue.contains(id) {
Some(Coalition::Blue)
} else if self.red.contains(id) {
Some(Coalition::Red)
} else if self.violet.contains(id) {
Some(Coalition::Violet)
} else {
None
}
}
}
+176
View File
@@ -0,0 +1,176 @@
use std::fs::File;
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<dyn Write>> {
let base_name = remove_extension(filename, is_zip);
let txt_name = format!("{base_name}_{coalition}{EXTENSION_TXT}");
println!("writing {coalition}");
if is_zip {
let zip_name = format!("{base_name}_{coalition}{EXTENSION_ZIP}");
Ok(Box::new(create_zipwriter(&zip_name, &txt_name)?))
} else {
Ok(Box::new(create_textwriter(&txt_name)?))
}
}
fn remove_extension(filename: &str, is_zip: bool) -> &str {
if is_zip {
&filename[..(filename.len() - EXTENSION_ZIP.len())]
} else {
&filename[..(filename.len() - EXTENSION_TXT.len())]
}
}
fn create_textwriter(filename: &str) -> Result<File> {
File::create(filename).with_context(|| "could not create text file")
}
fn create_zipwriter(outer_name: &str, inner_name: &str) -> Result<ZipWriter<File>> {
let mut writer = ZipWriter::new(create_textwriter(outer_name)?);
writer.start_file(
inner_name,
FileOptions::default().compression_method(zip::CompressionMethod::Deflated),
)?;
Ok(writer)
}
pub trait StringWriter
where
Self: Write,
{
fn write_line<S>(&mut self, line: &S) -> Result<()>
where
S: AsRef<str>,
{
writeln!(self, "{}", line.as_ref()).with_context(|| "could not write line")
}
fn write_strings<S: AsRef<str>>(&mut self, lines: &[S]) -> Result<()> {
for line in lines {
self.write_line(line)?;
}
Ok(())
}
fn write_for_coalition<S: AsRef<str>>(
&mut self,
lines: &[S],
coalition_per_line: &[Coalition],
coalition: &Coalition,
) -> Result<()>;
}
impl<T> StringWriter for T
where
T: Write,
{
fn write_for_coalition<S: AsRef<str>>(
&mut self,
lines: &[S],
coalition_per_line: &[Coalition],
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)
.map(|(l, _)| l);
for line in iter {
self.write_line(line)?;
}
Ok(())
}
}
#[derive(Clone)]
pub struct OutputData {
input_filename: Arc<String>,
is_zip: bool,
header: Arc<Vec<String>>,
body: Arc<Vec<String>>,
coalition_per_line: Arc<Vec<Coalition>>,
}
impl OutputData {
pub fn new(
input_filename: String,
is_zip: bool,
header: Vec<String>,
body: Vec<String>,
coalition_per_line: Vec<Coalition>,
) -> 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 thread_self = self.clone();
let thread_coalition = coalition.clone();
thread::spawn(move || {
let mut writer = create_writer(
thread_self.is_zip,
&thread_self.input_filename,
&thread_coalition,
)
.with_context(|| format!("could not create writer for {thread_coalition}"))
.unwrap();
writer
.write_strings(&thread_self.header)
.with_context(|| format!("could not write header for {thread_coalition}"))
.unwrap();
writer
.write_for_coalition(
&thread_self.body,
&thread_self.coalition_per_line,
&thread_coalition,
)
.with_context(|| format!("could not write body for {thread_coalition}"))
.unwrap();
})
})
.collect();
handles.into_iter().zip(coalitions).for_each(|(h, c)| {
h.join()
.unwrap_or_else(|_| println!("could not write data for {c}"))
});
Ok(())
}
}