Anton Tarasenko
4 years ago
commit
5315a9e61b
6 changed files with 467 additions and 0 deletions
@ -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" |
||||||
|
|
@ -0,0 +1,9 @@ |
|||||||
|
[package] |
||||||
|
name = "avarice" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Anton Tarasenko <dkanus@gmail.com>"] |
||||||
|
edition = "2018" |
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html |
||||||
|
|
||||||
|
[dependencies] |
@ -0,0 +1,13 @@ |
|||||||
|
use std::env; |
||||||
|
use std::path::Path; |
||||||
|
mod unreal_config; |
||||||
|
|
||||||
|
fn main() { |
||||||
|
let args: Vec<String> = env::args().collect(); |
||||||
|
let filename = &args[1]; |
||||||
|
let config = unreal_config::load_file(Path::new(filename)); |
||||||
|
match config { |
||||||
|
Ok(config) => print!("{}", config), |
||||||
|
_ => (), |
||||||
|
} |
||||||
|
} |
@ -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<String>, |
||||||
|
} |
||||||
|
|
||||||
|
const COMMENT_STYLE_TO_PREFIX_MAP: [(CommentStyle, &str); 2] = |
||||||
|
[(CommentStyle::Semicolon, ";"), (CommentStyle::Hash, "#")]; |
||||||
|
|
||||||
|
enum Value { |
||||||
|
Unknown(String), |
||||||
|
UnknownArray(Vec<String>), |
||||||
|
} |
||||||
|
|
||||||
|
struct Variable { |
||||||
|
name: String, |
||||||
|
value: Value, |
||||||
|
comments: Option<Vec<CommentBlock>>, |
||||||
|
inline_comment: Option<CommentLine>, |
||||||
|
} |
||||||
|
|
||||||
|
enum SectionTitle { |
||||||
|
Class { package: String, class: String }, |
||||||
|
PerObject { object_name: String, class: String }, |
||||||
|
} |
||||||
|
|
||||||
|
struct Section { |
||||||
|
title: SectionTitle, |
||||||
|
variables: Vec<Variable>, |
||||||
|
comments: Option<Vec<CommentBlock>>, |
||||||
|
inline_comment: Option<CommentLine>, |
||||||
|
} |
||||||
|
|
||||||
|
pub struct ConfigFile { |
||||||
|
name: String, |
||||||
|
sections: Vec<Section>, |
||||||
|
} |
||||||
|
|
||||||
|
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<CommentBlock>) -> 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<ConfigFile, Box<dyn Error>> { |
||||||
|
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()) |
||||||
|
} |
@ -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<Section>, |
||||||
|
current_section: Option<Section>, |
||||||
|
// Comment building
|
||||||
|
pending_comment_blocks: Option<Vec<CommentBlock>>, |
||||||
|
current_comment_block: Option<CommentBlock>, |
||||||
|
// 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<Vec<CommentBlock>> { |
||||||
|
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<CommentLine>) -> 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<CommentLine>, |
||||||
|
) -> () { |
||||||
|
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<CommentLine>) { |
||||||
|
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<CommentLine> { |
||||||
|
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<NameValuePair> { |
||||||
|
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 |
||||||
|
} |
Loading…
Reference in new issue