forked from dkanus/Avarice
Initial commit
This commit is contained in:
commit
5315a9e61b
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
**/*.rs.bk
|
6
Cargo.lock
generated
Normal file
6
Cargo.lock
generated
Normal file
@ -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
Normal file
9
Cargo.toml
Normal file
@ -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
Normal file
13
src/main.rs
Normal file
@ -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
Normal file
184
src/unreal_config/mod.rs
Normal file
@ -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
Normal file
253
src/unreal_config/reader.rs
Normal file
@ -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
Block a user