Browse Source

Initial commit

master
Anton Tarasenko 4 years ago
commit
5315a9e61b
  1. 2
      .gitignore
  2. 6
      Cargo.lock
  3. 9
      Cargo.toml
  4. 13
      src/main.rs
  5. 184
      src/unreal_config/mod.rs
  6. 253
      src/unreal_config/reader.rs

2
.gitignore vendored

@ -0,0 +1,2 @@
/target
**/*.rs.bk

6
Cargo.lock generated

@ -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"

9
Cargo.toml

@ -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]

13
src/main.rs

@ -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),
_ => (),
}
}

184
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<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())
}

253
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<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…
Cancel
Save