commit 5315a9e61b6bbacaa9e0278c3d83f4fdd1621612 Author: Anton Tarasenko Date: Tue Nov 17 13:05:42 2020 +0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..53eaa21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b0eb9bf --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "avarice" +version = "0.1.0" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e8facc0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "avarice" +version = "0.1.0" +authors = ["Anton Tarasenko "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8f664c9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +use std::env; +use std::path::Path; +mod unreal_config; + +fn main() { + let args: Vec = env::args().collect(); + let filename = &args[1]; + let config = unreal_config::load_file(Path::new(filename)); + match config { + Ok(config) => print!("{}", config), + _ => (), + } +} diff --git a/src/unreal_config/mod.rs b/src/unreal_config/mod.rs new file mode 100644 index 0000000..71dd32f --- /dev/null +++ b/src/unreal_config/mod.rs @@ -0,0 +1,184 @@ +use std::error::Error; +use std::fmt; +use std::fs; +use std::path::Path; + +mod reader; + +#[cfg(windows)] +const LINE_ENDING: &str = "\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &str = "\n"; + +#[derive(PartialEq, Copy, Clone)] +enum CommentStyle { + Semicolon, + Hash, +} + +type CommentLine = (CommentStyle, String); + +struct CommentBlock { + style: CommentStyle, + lines: Vec, +} + +const COMMENT_STYLE_TO_PREFIX_MAP: [(CommentStyle, &str); 2] = + [(CommentStyle::Semicolon, ";"), (CommentStyle::Hash, "#")]; + +enum Value { + Unknown(String), + UnknownArray(Vec), +} + +struct Variable { + name: String, + value: Value, + comments: Option>, + inline_comment: Option, +} + +enum SectionTitle { + Class { package: String, class: String }, + PerObject { object_name: String, class: String }, +} + +struct Section { + title: SectionTitle, + variables: Vec, + comments: Option>, + inline_comment: Option, +} + +pub struct ConfigFile { + name: String, + sections: Vec
, +} + +impl fmt::Display for CommentBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let prefix = match &self.style { + CommentStyle::Semicolon => ";", + CommentStyle::Hash => "#", + }; + if !self.lines.is_empty() { + write!(f, "{}{}", prefix, self.lines[0])?; + } + for line in self.lines.iter().skip(1) { + write!(f, "{}{}{}", LINE_ENDING, prefix, line)?; + } + Ok(()) + } +} + +impl fmt::Display for Value { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Value::Unknown(raw_value) => write!(f, "{}", raw_value)?, + Value::UnknownArray(raw_values) => { + write!(f, "[")?; + if !raw_values.is_empty() { + write!(f, "{}", raw_values[0])?; + } + for raw_value in raw_values.iter().skip(1) { + write!(f, ", {}", raw_value)?; + } + write!(f, "]")?; + } + }; + Ok(()) + } +} + +impl fmt::Display for Variable { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(comment_blocks) = &self.comments { + format_comment_blocks(f, &comment_blocks)?; + } + match &self.value { + Value::Unknown(raw_value) => write!(f, "{}={}", self.name, raw_value)?, + Value::UnknownArray(raw_values) => { + if !raw_values.is_empty() { + write!(f, "{}={}", self.name, raw_values[0])?; + } + for raw_value in raw_values.iter().skip(1) { + write!(f, "{}{}={}", LINE_ENDING, self.name, raw_value)?; + } + } + } + if let Some(comment) = &self.inline_comment { + format_comment_line(f, &comment)?; + } + Ok(()) + } +} + +impl fmt::Display for Section { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(comment_blocks) = &self.comments { + format_comment_blocks(f, &comment_blocks)?; + } + match &self.title { + SectionTitle::Class { package, class } => { + write!(f, "[{}.{}]", package, class)?; + } + SectionTitle::PerObject { object_name, class } => { + write!(f, "[{} {}]", object_name, class)?; + } + } + if let Some(comment) = &self.inline_comment { + format_comment_line(f, &comment)?; + } + writeln!(f, "")?; + for variable in self.variables.iter() { + writeln!(f, "{}", variable)?; + } + Ok(()) + } +} + +impl fmt::Display for ConfigFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, r#"Config file: "{}""#, self.name)?; + if self.sections.len() > 0 { + write!(f, "{}", self.sections[0])?; + } + for section in self.sections.iter().skip(1) { + write!(f, "{}{}", LINE_ENDING, section)?; + } + Ok(()) + } +} + +fn format_comment_line(f: &mut fmt::Formatter<'_>, (style, content): &CommentLine) -> fmt::Result { + match style { + CommentStyle::Semicolon => write!(f, " ;")?, + CommentStyle::Hash => write!(f, " #")?, + }; + write!(f, "{}", content)?; + Ok(()) +} + +fn format_comment_blocks(f: &mut fmt::Formatter<'_>, blocks: &Vec) -> fmt::Result { + for block in blocks.iter() { + if block.style == CommentStyle::Hash { + write!(f, "{}", LINE_ENDING)?; + } + write!(f, "{}{}", block, LINE_ENDING)?; + } + Ok(()) +} + +pub fn load_file(file_path: &Path) -> Result> { + let config_name = file_path + .file_stem() + .and_then(|x| x.to_str()) + .unwrap_or_default() + .to_string(); + Ok(fs::read_to_string(file_path)? + .lines() + .fold(reader::Reader::new(config_name), |reader, line| { + reader.add_line(line.to_string()) + }) + .return_config()) +} diff --git a/src/unreal_config/reader.rs b/src/unreal_config/reader.rs new file mode 100644 index 0000000..7da8348 --- /dev/null +++ b/src/unreal_config/reader.rs @@ -0,0 +1,253 @@ +use super::CommentBlock; +use super::CommentLine; +use super::CommentStyle; +use super::ConfigFile; +use super::Section; +use super::SectionTitle; +use super::Value; +use super::Variable; +use super::COMMENT_STYLE_TO_PREFIX_MAP; + +type NameValuePair = (String, String); + +const OPEN_SECTION: &str = "["; +const CLOSE_SECTION: &str = "]"; +const EQUAL_SIGN: &str = "="; +const CLASS_NAME_SEPARATOR: &str = "."; + +const ERROR_MSG_INVALID_SECTION: &str = "Invalid section definition"; +const ERROR_MSG_INVALID_VARIABLE: &str = "Invalid variable definition"; +const ERROR_MSG_EARLY_VARIABLE: &str = "Variable defined before first section"; + +pub struct Reader { + name: String, + lines_consumed: usize, + sections: Vec
, + current_section: Option
, + // Comment building + pending_comment_blocks: Option>, + current_comment_block: Option, + // Error collection + errors: Vec<(usize, String)>, +} + +// Consumes input as lines in config file, in order; +// Returns parsed config. +impl Reader { + pub fn new(name: String) -> Reader { + Reader { + name, + // For error reporting + lines_consumed: 0, + sections: Vec::new(), + // Keeps track of section we're currently filling + current_section: None, + // These two variables keep track of comment blocks that we are filling: + // those will be dumped into definition of first section / variable + // that would follow them + pending_comment_blocks: None, + current_comment_block: None, + errors: Vec::new(), + } + } + + pub fn add_line(mut self, line: String) -> Self { + self.lines_consumed += 1; + let (line, mut comment) = separate_comments(line); + if line.is_empty() { + match comment { + Some(comment) => self.record_comment(comment), + _ => self.flush_current_comment_block(), + } + return self; + } + if self.try_opening_section(&line, &mut comment) { + return self; + } + match parse_variable(line.clone()) { + Some(name_value_pair) => self.record_variable(name_value_pair, comment), + _ => { + self.record_reading_error(ERROR_MSG_INVALID_VARIABLE); + self.record_comment((CommentStyle::Semicolon, line)); + } + }; + self + } + + pub fn return_config(mut self) -> ConfigFile { + if self.current_section.is_some() { + self.sections.push(self.current_section.unwrap()); + } + ConfigFile { + name: self.name, + sections: self.sections, + } + } + + fn return_comment_blocks(&mut self) -> Option> { + self.flush_current_comment_block(); + self.pending_comment_blocks.take() + } + + // Tries to interpret given line as a section definition. + // If it looks like a section (correct or ill-defined) - returns `false`. + // If not - does nothing and returns 'false'. + // + // New section is only open if it is correctly defined, + // otherwise error is reported and line is added as a comment. + fn try_opening_section(&mut self, line: &String, comment: &mut Option) -> bool { + if !line.starts_with(OPEN_SECTION) { + return false; + } + if !line.ends_with(CLOSE_SECTION) { + self.record_reading_error(ERROR_MSG_INVALID_SECTION); + self.record_comment((CommentStyle::Semicolon, line.clone())); + // This still counts as a section definition, even if erroneous one + return true; + } + match self.current_section.take() { + Some(s) => self.sections.push(s), + _ => (), + } + // This should be safe, since we know that `line` has a form of `[...]` + let title = line[1..line.len() - 1].to_string(); + self.current_section = Some(Section { + title: parse_title(title), + variables: Vec::new(), + comments: self.return_comment_blocks(), + inline_comment: comment.take(), + }); + true + } + + // Records parsed variable (defines as a name~value pair) into currently open section. + // Reports an error if no section is open yet. + fn record_variable( + &mut self, + (name, value): NameValuePair, + comment: Option, + ) -> () { + let comment_blocks = self.return_comment_blocks(); + match self.current_section.as_mut() { + None => self.record_reading_error(ERROR_MSG_EARLY_VARIABLE), + Some(section) => { + let position = section.variables.iter().position(|x| x.name.eq(&name)); + match position { + None => section.variables.push(Variable { + name, + value: Value::Unknown(value), + comments: comment_blocks, + inline_comment: comment, + }), + Some(position) => { + let updated_value = + add_raw_value(section.variables.remove(position), value); + section.variables.push(updated_value); + } + } + } + } + } + + fn record_comment(&mut self, (style, content): CommentLine) -> () { + if let Some(current_comment_block) = &self.current_comment_block { + if current_comment_block.style != style { + self.flush_current_comment_block(); + } + } + match self.current_comment_block.as_mut() { + Some(current_comment_block) => current_comment_block.lines.push(content), + None => { + self.current_comment_block = Some(CommentBlock { + style, + lines: vec![content], + }) + } + } + } + + fn record_reading_error(&mut self, error: &str) -> () { + self.errors.push((self.lines_consumed, error.to_string())) + } + + fn flush_current_comment_block(&mut self) -> () { + if self.current_comment_block.is_none() { + return; + } + let block = self + .current_comment_block + .take() + .expect("[INI Reader]`None` after explicit check"); + match self.pending_comment_blocks.as_mut() { + Some(blocks) => blocks.push(block), + None => { + self.pending_comment_blocks = Some(vec![block]); + } + }; + } +} + +fn separate_comments(line: String) -> (String, Option) { + let comment_index = COMMENT_STYLE_TO_PREFIX_MAP + .iter() + .map(|x| line.find(x.1)) + .filter(|x| x.is_some()) + .map(|x| x.expect("[INI Reader]`None` after filtering")) + .min(); + match comment_index { + Some(index) => { + let (line, comment) = line.split_at(index); + (line.trim().to_string(), parse_comment(comment)) + } + _ => (line, None), + } +} + +// Breaks a string into two, acting like `split()`, but throwing away pointed at character +fn separate_at(string: &String, index: usize) -> (String, String) { + let (left, right) = string.split_at(index); + (left.to_string(), right[1..].to_string()) +} + +fn parse_comment(comment: &str) -> Option { + for pair in COMMENT_STYLE_TO_PREFIX_MAP.iter() { + if comment.starts_with(pair.1) { + return Some((pair.0, comment[1..].to_string())); + } + } + None +} + +fn parse_variable(line: String) -> Option { + match line.find(EQUAL_SIGN) { + None => None, + Some(index) => Some(separate_at(&line, index)), + } +} + +fn parse_title(title: String) -> SectionTitle { + let index = title.find(char::is_whitespace); + match index { + Some(index) => { + let (object_name, class) = separate_at(&title, index); + SectionTitle::PerObject { object_name, class } + } + _ => { + let (package, class) = match title.find(CLASS_NAME_SEPARATOR) { + Some(sep_index) => separate_at(&title, sep_index), + _ => (String::new(), title), + }; + SectionTitle::Class { package, class } + } + } +} + +fn add_raw_value(mut variable: Variable, raw_value: String) -> Variable { + let mut values = match variable.value { + Value::Unknown(value) => vec![value], + Value::UnknownArray(values) => values, + }; + values.push(raw_value); + variable.value = Value::UnknownArray(values); + variable +}