forked from dkanus/Avarice
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