Compare commits
No commits in common. 'feature_database' and 'master' have entirely different histories.
feature_da
...
master
10 changed files with 6 additions and 1322 deletions
@ -1,13 +0,0 @@ |
|||||||
{ |
|
||||||
"76561198025127722": { |
|
||||||
"allowed_ips": ["127.0.0.1", "192.168.0.100"], |
|
||||||
"groups": ["admin"], |
|
||||||
"ip_lock": true, |
|
||||||
"password_hash": "fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d" |
|
||||||
}, |
|
||||||
"76561198044316328": { |
|
||||||
"groups": ["admin"], |
|
||||||
"ip_lock": false, |
|
||||||
"password_hash": "fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d" |
|
||||||
} |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
{ |
|
||||||
"76561198025127722": { |
|
||||||
"walked": 1073, |
|
||||||
"dosh_thrown": 483482, |
|
||||||
"achievements": ["kf:LabCleaner", "kf:ChickenFarmer", "scrn:playedscrn"] |
|
||||||
}, |
|
||||||
"76561198044316328": { |
|
||||||
"walked": 1693, |
|
||||||
"dosh_thrown": 527624, |
|
||||||
"achievements": ["kf:PubCrawl", "kf:FascistDietitian", "kf:GimliThatAxe!", "scrn:playedscrn"] |
|
||||||
} |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
{ |
|
||||||
"76561198025127722": { |
|
||||||
"headshots": 582, |
|
||||||
"assault_rifle_damage": 9067, |
|
||||||
"stalker_kills": 143 |
|
||||||
}, |
|
||||||
"76561198044316328": { |
|
||||||
"explosive_damage": 19674, |
|
||||||
"shotgun_damage": 3835, |
|
||||||
"welded_amount": 1 |
|
||||||
} |
|
||||||
} |
|
@ -1,220 +0,0 @@ |
|||||||
use serde_json; |
|
||||||
use serde_json::json; |
|
||||||
use std::error::Error; |
|
||||||
|
|
||||||
extern crate custom_error; |
|
||||||
use custom_error::custom_error; |
|
||||||
|
|
||||||
const JSON_POINTER_SEPARATOR: &str = "/"; |
|
||||||
|
|
||||||
custom_error! { pub IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}" } |
|
||||||
|
|
||||||
/// This is a enum that used internally to refer to values inside of
|
|
||||||
/// JSON (their serde implementation) objects and arrays.
|
|
||||||
/// This enum helps to simplify module's code.
|
|
||||||
///
|
|
||||||
/// For values inside of JSON object it stores object's
|
|
||||||
/// `Map<String, serde_json::Value>` and name of referred value.
|
|
||||||
///
|
|
||||||
/// For values inside JSON arrays it stores array's
|
|
||||||
/// `Vec<serde_json::Value>` and referred index.
|
|
||||||
///
|
|
||||||
/// `Invalid` can be used to return a failed state.
|
|
||||||
enum ValueReference<'a> { |
|
||||||
Object(&'a mut serde_json::Map<String, serde_json::Value>, String), |
|
||||||
Array(&'a mut Vec<serde_json::Value>, usize), |
|
||||||
Invalid, |
|
||||||
} |
|
||||||
|
|
||||||
/// Implements database's file by wrapping JSON value (`serde_json::Value`)
|
|
||||||
/// and providing several convenient accessor methods.
|
|
||||||
#[derive(Debug)] |
|
||||||
pub struct File { |
|
||||||
/// File's full contents, normally a JSON object.
|
|
||||||
contents: serde_json::Value, |
|
||||||
} |
|
||||||
|
|
||||||
impl ToString for File { |
|
||||||
fn to_string(&self) -> String { |
|
||||||
self.contents.to_string() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
impl File { |
|
||||||
/// Creates an empty file that will contain an empty JSON object.
|
|
||||||
pub fn empty() -> File { |
|
||||||
File { |
|
||||||
contents: json!({}), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Loads JSON value from the specified file.
|
|
||||||
pub fn load(file_contents: String) -> Result<File, Box<dyn Error>> { |
|
||||||
Ok(File { |
|
||||||
contents: serde_json::from_str(&file_contents)?, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns file's "root", - JSON value contained inside it.
|
|
||||||
pub fn root(&self) -> &serde_json::Value { |
|
||||||
&self.contents |
|
||||||
} |
|
||||||
|
|
||||||
/// Attempts to return JSON value, corresponding to the given JSON pointer.
|
|
||||||
/// `None` if the value is missing.
|
|
||||||
pub fn get(&self, pointer: &str) -> Option<&serde_json::Value> { |
|
||||||
self.contents.pointer(pointer) |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if values at a given JSON pointer exists.
|
|
||||||
/// Returns `true` if it does.
|
|
||||||
pub fn contains(&self, pointer: &str) -> bool { |
|
||||||
self.get(pointer) != None |
|
||||||
} |
|
||||||
|
|
||||||
/// Inserts new JSON value inside this file
|
|
||||||
/// (possibly in some sub-object/array).
|
|
||||||
///
|
|
||||||
/// Given pointer must point at new value:
|
|
||||||
/// 1. If it already exists, - it will be overwritten.
|
|
||||||
/// 2. If it does not exist, but it's parent object/array does -
|
|
||||||
/// it will be added.
|
|
||||||
/// 3. Otherwise an error will be raise.
|
|
||||||
///
|
|
||||||
/// If array needs to be expanded, - missing values will be filled
|
|
||||||
/// with `json!(null)`, i.e. inserting `7` at index `5` in array `[1, 2, 3]`
|
|
||||||
/// will produce `[1, 2, 3, null, null, 7]`.
|
|
||||||
pub fn insert( |
|
||||||
&mut self, |
|
||||||
pointer: &str, |
|
||||||
new_value: serde_json::Value, |
|
||||||
) -> Result<(), IncorrectPointer> { |
|
||||||
self.touch(pointer)?; |
|
||||||
match self.contents.pointer_mut(pointer) { |
|
||||||
Some(v) => *v = new_value, |
|
||||||
_ => { |
|
||||||
// If after `touch()` call we still don't have an existing value -
|
|
||||||
// something is wrong with the `pointer`
|
|
||||||
return Err(IncorrectPointer { |
|
||||||
pointer: pointer.to_owned(), |
|
||||||
}); |
|
||||||
} |
|
||||||
}; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Removes (and returns) value specified by theJSON pointer.
|
|
||||||
/// If it did not exist - returns `None`.
|
|
||||||
pub fn remove(&mut self, pointer: &str) -> Option<serde_json::Value> { |
|
||||||
match self.pointer_to_reference(pointer) { |
|
||||||
ValueReference::Object(map, variable_name) => map.remove(&variable_name), |
|
||||||
ValueReference::Array(vec, variable_index) => { |
|
||||||
if variable_index < vec.len() { |
|
||||||
return Some(vec.remove(variable_index)); |
|
||||||
} |
|
||||||
None |
|
||||||
} |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Helper method to create a value if missing (as `json!(null)`).
|
|
||||||
/// Can only be done if parent container already exists.
|
|
||||||
///
|
|
||||||
/// For specifics refer to `insert()` method.
|
|
||||||
fn touch(&mut self, pointer: &str) -> Result<(), IncorrectPointer> { |
|
||||||
// If value is present - we're done
|
|
||||||
if pointer.is_empty() || self.contents.pointer_mut(pointer).is_some() { |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
// Otherwise - try to create it
|
|
||||||
match self.pointer_to_reference(pointer) { |
|
||||||
ValueReference::Object(map, variable_name) => { |
|
||||||
map.insert(variable_name, json!(null)); |
|
||||||
} |
|
||||||
ValueReference::Array(vec, variable_index) => { |
|
||||||
// We've checked at the beginning of this method that value
|
|
||||||
// at `variable_index` does not exist, which guarantees
|
|
||||||
// that array is to short and we won't shrink it
|
|
||||||
vec.resize(variable_index + 1, json!(null)); |
|
||||||
} |
|
||||||
_ => { |
|
||||||
return Err(IncorrectPointer { |
|
||||||
pointer: pointer.to_owned(), |
|
||||||
}) |
|
||||||
} |
|
||||||
}; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Helper method, - converts JSON pointer into auxiliary `ValueReference` enum.
|
|
||||||
fn pointer_to_reference<'a>(&'a mut self, pointer: &str) -> ValueReference<'a> { |
|
||||||
if pointer.is_empty() { |
|
||||||
return ValueReference::Invalid; |
|
||||||
} |
|
||||||
// Extract variable name (that `pointer` points to)
|
|
||||||
// and reference to it's container
|
|
||||||
//
|
|
||||||
// i.e. given file with '{"obj":{"arr":[1,3,5,2,4]}}',
|
|
||||||
// for pointer `/obj/arr/5`,
|
|
||||||
// it will return, basically, `(&[1,3,5,2,4], "5")`
|
|
||||||
let container_variable_pair = |
|
||||||
pop_json_pointer(pointer).and_then(move |(path, variable_name)| { |
|
||||||
match self.contents.pointer_mut(&path) { |
|
||||||
Some(v) => Some((v, variable_name)), |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
}); |
|
||||||
let (json_container, variable_name) = match container_variable_pair { |
|
||||||
Some(v) => v, |
|
||||||
_ => return ValueReference::Invalid, |
|
||||||
}; |
|
||||||
// For arrays we also need to confirm validity of the variable name
|
|
||||||
// and convert it into `usize`
|
|
||||||
match json_container { |
|
||||||
serde_json::Value::Object(map) => ValueReference::Object(map, variable_name), |
|
||||||
serde_json::Value::Array(vec) => { |
|
||||||
let index: usize = match variable_name.parse() { |
|
||||||
Ok(v) => v, |
|
||||||
_ => return ValueReference::Invalid, |
|
||||||
}; |
|
||||||
ValueReference::Array(vec, index) |
|
||||||
} |
|
||||||
_ => ValueReference::Invalid, |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Helper function to disassemble JSON path.
|
|
||||||
fn pop_json_pointer(pointer: &str) -> Option<(String, String)> { |
|
||||||
let mut pointer = pointer.to_string(); |
|
||||||
let last_separator_index = match pointer.rfind(JSON_POINTER_SEPARATOR) { |
|
||||||
Some(v) => v, |
|
||||||
_ => { |
|
||||||
return None; |
|
||||||
} |
|
||||||
}; |
|
||||||
if last_separator_index >= pointer.len() { |
|
||||||
pointer.pop(); |
|
||||||
return Some((pointer, String::new())); |
|
||||||
} |
|
||||||
let var_name = pointer.split_off(last_separator_index + 1); |
|
||||||
pointer.pop(); |
|
||||||
Some((pointer, var_name)) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_pop_json_pointer() { |
|
||||||
assert_eq!( |
|
||||||
pop_json_pointer("/a/b/c/d"), |
|
||||||
Some(("/a/b/c".to_owned(), "d".to_owned())) |
|
||||||
); |
|
||||||
assert_eq!(pop_json_pointer("/"), Some(("".to_owned(), "".to_owned()))); |
|
||||||
assert_eq!( |
|
||||||
pop_json_pointer("/a/b/"), |
|
||||||
Some(("/a/b".to_owned(), "".to_owned())) |
|
||||||
); |
|
||||||
assert_eq!(pop_json_pointer(""), None); |
|
||||||
// This pointer is incorrect
|
|
||||||
assert_eq!(pop_json_pointer("var"), None); |
|
||||||
} |
|
@ -1,240 +0,0 @@ |
|||||||
use super::*; |
|
||||||
use log::{error, info, warn}; |
|
||||||
use std::collections::HashMap; |
|
||||||
use std::error::Error; |
|
||||||
use std::fs; |
|
||||||
use std::path; |
|
||||||
use std::path::Path; |
|
||||||
|
|
||||||
extern crate custom_error; |
|
||||||
use custom_error::custom_error; |
|
||||||
|
|
||||||
const JSON_EXTENSION: &str = "json"; |
|
||||||
|
|
||||||
custom_error! { pub IOError |
|
||||||
NotDirectory{path: String} = "Path to the database should point at a directory: {path}", |
|
||||||
} |
|
||||||
|
|
||||||
/// Reads database data from a directory at the specified path
|
|
||||||
/// as a vector of `Group`s.
|
|
||||||
pub fn read(db_path: &path::Path) -> Result<Vec<Group>, Box<dyn Error>> { |
|
||||||
if !db_path.is_dir() { |
|
||||||
error!( |
|
||||||
"Loading database from a non-directory {} was attempted.", |
|
||||||
db_path.display() |
|
||||||
); |
|
||||||
return Err(Box::new(IOError::NotDirectory { |
|
||||||
path: db_path.display().to_string(), |
|
||||||
})); |
|
||||||
} |
|
||||||
info!("Loading database from {}.", db_path.display()); |
|
||||||
let mut groups = Vec::new(); |
|
||||||
for entry in fs::read_dir(db_path)? { |
|
||||||
let path = match entry { |
|
||||||
Ok(r) => r, |
|
||||||
_ => continue, |
|
||||||
} |
|
||||||
.path(); |
|
||||||
if !check_valid_group_dir(path.as_path()) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
match read_group(&path)? { |
|
||||||
Some(g) => groups.push(g), |
|
||||||
_ => (), |
|
||||||
} |
|
||||||
} |
|
||||||
info!("Correctly finished leading database."); |
|
||||||
Ok(groups) |
|
||||||
} |
|
||||||
|
|
||||||
/// Writes data of the given database into the directory specified by the path.
|
|
||||||
/// Does not clear it from any previously existing files,
|
|
||||||
/// you can use `clear_dir()` for that.
|
|
||||||
pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box<dyn Error>> { |
|
||||||
if db_path.exists() && !db_path.is_dir() { |
|
||||||
error!( |
|
||||||
"Cannot write database into a non-directory {}", |
|
||||||
db_path.display() |
|
||||||
); |
|
||||||
return Err(Box::new(IOError::NotDirectory { |
|
||||||
path: db_path.display().to_string(), |
|
||||||
})); |
|
||||||
} |
|
||||||
fs::create_dir(db_path)?; |
|
||||||
for group in db.group_names().iter() { |
|
||||||
let group_path = db_path.join(group); |
|
||||||
if !group_path.exists() || !group_path.is_dir() { |
|
||||||
fs::create_dir(group_path.clone())?; |
|
||||||
} |
|
||||||
for file in db.file_names_in(group).iter() { |
|
||||||
let file_path = group_path.join(format!("{}.{}", file, JSON_EXTENSION)); |
|
||||||
match db.file(group, file) { |
|
||||||
Some(file) => fs::write(file_path, file.to_string())?, |
|
||||||
_ => (), |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Given a path to directory that was used for database storage -
|
|
||||||
/// clears it's data.
|
|
||||||
///
|
|
||||||
/// This means removing any '.json' files from all immediate subdirectories and
|
|
||||||
/// then removing any subdirectories that were or became empty.
|
|
||||||
pub fn clear_dir(db_path: &path::Path) -> Result<(), Box<dyn Error>> { |
|
||||||
info!( |
|
||||||
"Clearing directory {} from database files.", |
|
||||||
db_path.display() |
|
||||||
); |
|
||||||
if !db_path.exists() { |
|
||||||
info!("Directory not found, nothing to do."); |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
for entry in fs::read_dir(db_path)? { |
|
||||||
let dir_path = match entry { |
|
||||||
Ok(r) => r, |
|
||||||
_ => continue, |
|
||||||
} |
|
||||||
.path(); |
|
||||||
if !check_valid_group_dir(dir_path.as_path()) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
for entry in fs::read_dir(dir_path.clone())? { |
|
||||||
let file_path = match entry { |
|
||||||
Ok(r) => r, |
|
||||||
_ => continue, |
|
||||||
} |
|
||||||
.path(); |
|
||||||
if !check_valid_data_file(file_path.as_path()) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
fs::remove_file(file_path)?; |
|
||||||
} |
|
||||||
let _ = fs::remove_dir(dir_path); |
|
||||||
} |
|
||||||
let _ = fs::remove_dir(db_path); |
|
||||||
info!("Correctly finished clearing database files."); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// helper function to read a group from a given subdirectory:
|
|
||||||
/// loads data from all containing '.json' files.
|
|
||||||
fn read_group(group_path: &path::Path) -> Result<Option<Group>, Box<dyn Error>> { |
|
||||||
let mut files = HashMap::new(); |
|
||||||
for entry in fs::read_dir(group_path)? { |
|
||||||
let path = match entry { |
|
||||||
Ok(r) => r, |
|
||||||
_ => continue, |
|
||||||
} |
|
||||||
.path(); |
|
||||||
if !check_valid_data_file(path.as_path()) { |
|
||||||
continue; |
|
||||||
} |
|
||||||
let file_name = get_file_name(path.as_path()); |
|
||||||
let file_contents = fs::read_to_string(&path)?; |
|
||||||
files.insert(file_name, File::load(file_contents)?); |
|
||||||
} |
|
||||||
if files.len() > 0 { |
|
||||||
return Ok(Some(Group { |
|
||||||
name: get_file_name(group_path), |
|
||||||
files, |
|
||||||
})); |
|
||||||
} |
|
||||||
Ok(None) |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if given path points at a directory that can represent
|
|
||||||
/// a database group (has a valid name).
|
|
||||||
fn check_valid_group_dir(dir_path: &path::Path) -> bool { |
|
||||||
if !dir_path.is_dir() { |
|
||||||
warn!( |
|
||||||
r#"Skipping {}, because only directories are expected in database's root."#, |
|
||||||
dir_path.display() |
|
||||||
); |
|
||||||
return false; |
|
||||||
} |
|
||||||
if !is_name_valid(&get_file_name(dir_path)) { |
|
||||||
warn!( |
|
||||||
r#"Skipping directory {}, because it does not have a valid name."#, |
|
||||||
dir_path.display() |
|
||||||
); |
|
||||||
return false; |
|
||||||
} |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if given path points at a file that can represent
|
|
||||||
/// a database group (has a valid name).
|
|
||||||
fn check_valid_data_file(file_path: &path::Path) -> bool { |
|
||||||
if file_path.is_dir() { |
|
||||||
warn!( |
|
||||||
r#"Skipping directory {}, because group directories are only\ |
|
||||||
supposed to contain files."#, |
|
||||||
file_path.display() |
|
||||||
); |
|
||||||
return false; |
|
||||||
} |
|
||||||
let name = get_file_name(file_path); |
|
||||||
if !is_name_valid(&name) { |
|
||||||
warn!( |
|
||||||
r#"Skipping file {}, because it does not have a valid name."#, |
|
||||||
file_path.display() |
|
||||||
); |
|
||||||
return false; |
|
||||||
} |
|
||||||
let extension = get_file_extension(file_path); |
|
||||||
if !JSON_EXTENSION.eq_ignore_ascii_case(&extension) { |
|
||||||
warn!( |
|
||||||
r#"Skipping file {}, because it does not have "json" extension."#, |
|
||||||
file_path.display() |
|
||||||
); |
|
||||||
return false; |
|
||||||
} |
|
||||||
true |
|
||||||
} |
|
||||||
|
|
||||||
fn get_file_name(path: &path::Path) -> String { |
|
||||||
path.file_stem() |
|
||||||
.and_then(|x| x.to_str()) |
|
||||||
.unwrap_or_default() |
|
||||||
.to_string() |
|
||||||
} |
|
||||||
|
|
||||||
fn get_file_extension(path: &path::Path) -> String { |
|
||||||
path.extension() |
|
||||||
.and_then(|x| x.to_str()) |
|
||||||
.unwrap_or_default() |
|
||||||
.to_string() |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn test_file_name_extension_extraction() { |
|
||||||
assert_eq!(get_file_name(Path::new("/dir/file")), "file".to_owned()); |
|
||||||
assert_eq!( |
|
||||||
get_file_name(Path::new("/dir/sub_dir/some.ext")), |
|
||||||
"some".to_owned() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
get_file_name(Path::new("/dir/sub_dir/.ext")), |
|
||||||
".ext".to_owned() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
get_file_name(Path::new("/dir/sub_dir/thing.")), |
|
||||||
"thing".to_owned() |
|
||||||
); |
|
||||||
|
|
||||||
assert_eq!(get_file_extension(Path::new("/dir/file")), "".to_owned()); |
|
||||||
assert_eq!( |
|
||||||
get_file_extension(Path::new("/dir/sub_dir/some.ext")), |
|
||||||
"ext".to_owned() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
get_file_extension(Path::new("/dir/sub_dir/.ext")), |
|
||||||
"".to_owned() |
|
||||||
); |
|
||||||
assert_eq!( |
|
||||||
get_file_extension(Path::new("/dir/sub_dir/thing.")), |
|
||||||
"".to_owned() |
|
||||||
); |
|
||||||
} |
|
@ -1,263 +0,0 @@ |
|||||||
#[cfg(test)] |
|
||||||
mod tests; |
|
||||||
|
|
||||||
use serde_json; |
|
||||||
use std::collections::HashMap; |
|
||||||
use std::error::Error; |
|
||||||
use std::path; |
|
||||||
|
|
||||||
extern crate custom_error; |
|
||||||
use custom_error::custom_error; |
|
||||||
|
|
||||||
pub mod file; |
|
||||||
pub use file::File; |
|
||||||
|
|
||||||
pub mod io; |
|
||||||
|
|
||||||
custom_error! { pub DBError |
|
||||||
InvalidEntityName{entity_name: String} = r#"Cannot use {entity_name} for file or group"#, |
|
||||||
NoGroup{group_name: String} = r#"Group "{group_name}" does not exist"#, |
|
||||||
NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group\ |
|
||||||
"{group_name}""#, |
|
||||||
GroupAlreadyExists{group_name: String} = r#"Group "{group_name}" already exists"#, |
|
||||||
FileAlreadyExists{group_name: String, file_name: String} = r#"File "{file_name}" already exists\ |
|
||||||
in the group "{group_name}""#, |
|
||||||
} |
|
||||||
|
|
||||||
/// Avarice database is a collection of named JSON values (by default objects),
|
|
||||||
/// separated into different names groups. Names of such groups and JSON values must only contain
|
|
||||||
/// numbers and latin letters (ASCII subset).
|
|
||||||
///
|
|
||||||
/// This database is only supposed to hold a relatively small amount of data
|
|
||||||
/// that:
|
|
||||||
///
|
|
||||||
/// 1. can be freely and full loaded into memory;
|
|
||||||
/// 2. then saved all at once.
|
|
||||||
///
|
|
||||||
/// Database is loaded and saved on the disk as a directory,
|
|
||||||
/// that contains subdirectories (with valid names) for each group;
|
|
||||||
/// those subdirectories in turn must contain "*.json" files (with valid names)
|
|
||||||
/// that correspond to the stored JSON values.
|
|
||||||
///
|
|
||||||
/// Database directory should not contain any other files, but their presence
|
|
||||||
/// should not prevent database from loading (such files should be ignored).
|
|
||||||
pub struct Database { |
|
||||||
/// Path to the database's directory
|
|
||||||
storage_path: path::PathBuf, |
|
||||||
/// Collection of groups (of JSON values) inside of database
|
|
||||||
groups: Vec<Group>, |
|
||||||
} |
|
||||||
|
|
||||||
/// Represents a database's group: a ste of named files
|
|
||||||
pub struct Group { |
|
||||||
/// Name of the group
|
|
||||||
name: String, |
|
||||||
/// Maps file names with their contents (as file::File structures)
|
|
||||||
files: HashMap<String, File>, |
|
||||||
} |
|
||||||
|
|
||||||
impl Database { |
|
||||||
/// Creates new database by loading it from the specified directory.
|
|
||||||
/// Directory must contain valid database and be readable.
|
|
||||||
pub fn load(storage_path: &path::Path) -> Result<Database, Box<dyn Error>> { |
|
||||||
Ok(Database { |
|
||||||
storage_path: storage_path.to_path_buf(), |
|
||||||
groups: io::read(storage_path)?, |
|
||||||
}) |
|
||||||
} |
|
||||||
|
|
||||||
/// Removes all data from the database.
|
|
||||||
pub fn clear(&mut self) { |
|
||||||
self.groups = Vec::new(); |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns path from which this database was loaded (can be changed with `change_path()`).
|
|
||||||
pub fn path(&mut self) -> path::PathBuf { |
|
||||||
self.storage_path.clone() |
|
||||||
} |
|
||||||
|
|
||||||
/// Changes current path of this database. All operations with files will use this path,
|
|
||||||
/// unless stated otherwise.
|
|
||||||
/// Directory must contain valid database and be readable.
|
|
||||||
///
|
|
||||||
/// This method will also remove all data from the current database's path
|
|
||||||
/// and fail if it can't.
|
|
||||||
pub fn change_path(&mut self, new_path: &path::Path) -> Result<(), Box<dyn Error>> { |
|
||||||
self.write_copy(new_path)?; |
|
||||||
io::clear_dir(&self.storage_path)?; |
|
||||||
self.storage_path = new_path.to_path_buf(); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Erases database's data on disk.
|
|
||||||
pub fn erase(self) -> Result<(), Box<dyn Error>> { |
|
||||||
io::clear_dir(&self.storage_path)?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Writes copy of the current database into specified directory.
|
|
||||||
/// Erases any preexisting database files.
|
|
||||||
///
|
|
||||||
/// Empty group won't be saved.
|
|
||||||
pub fn write_copy(&self, new_path: &path::Path) -> Result<(), Box<dyn Error>> { |
|
||||||
io::clear_dir(new_path)?; |
|
||||||
io::write(new_path, &self)?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Writes current state of the database on the disk.
|
|
||||||
///
|
|
||||||
/// Empty group won't be saved.
|
|
||||||
pub fn save(&self) -> Result<(), Box<dyn Error>> { |
|
||||||
self.write_copy(&self.storage_path)?; |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns names of all the groups in the database.
|
|
||||||
pub fn group_names(&self) -> Vec<String> { |
|
||||||
self.groups.iter().map(|x| x.name.clone()).collect() |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns names of all the files in a particular group.
|
|
||||||
pub fn file_names_in(&self, group_name: &str) -> Vec<String> { |
|
||||||
match self.groups.iter().find(|x| x.name.eq(group_name)) { |
|
||||||
Some(group) => group.files.keys().map(|x| x.clone()).collect(), |
|
||||||
None => Vec::new(), |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if specified group exists in the database
|
|
||||||
pub fn contains_group(&self, group_name: &str) -> bool { |
|
||||||
self.group_index(group_name).is_ok() |
|
||||||
} |
|
||||||
|
|
||||||
/// Creates a new empty group.
|
|
||||||
/// Will produce error if group already exists.
|
|
||||||
pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> { |
|
||||||
assert_name_is_valid(group_name)?; |
|
||||||
self.assert_no_group(group_name)?; |
|
||||||
self.groups.push(Group { |
|
||||||
name: group_name.to_owned(), |
|
||||||
files: HashMap::new(), |
|
||||||
}); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Removes specified group.
|
|
||||||
/// Will produce error if group does not exist.
|
|
||||||
pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> { |
|
||||||
let index = self.group_index(group_name)?; |
|
||||||
self.groups.remove(index); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Checks if specified file (in a specified group) is contained in the database.
|
|
||||||
pub fn contains_file(&self, group_name: &str, file_name: &str) -> bool { |
|
||||||
self.group_files(group_name) |
|
||||||
.and_then(|x| Ok(x.contains_key(&file_name.to_owned()))) |
|
||||||
.unwrap_or(false) |
|
||||||
} |
|
||||||
|
|
||||||
/// Creates new file in the specified group that will contain an empty JSON object.
|
|
||||||
/// Will produce error if file already exists.
|
|
||||||
pub fn create_file(&mut self, group_name: &str, file_name: &str) -> Result<&mut File, DBError> { |
|
||||||
assert_name_is_valid(&file_name)?; |
|
||||||
let files = self.group_files_mut(group_name)?; |
|
||||||
if files.contains_key(&file_name.to_owned()) { |
|
||||||
return Err(DBError::FileAlreadyExists { |
|
||||||
group_name: group_name.to_owned(), |
|
||||||
file_name: file_name.to_owned(), |
|
||||||
}); |
|
||||||
} |
|
||||||
let new_file = File::empty(); |
|
||||||
files.insert(file_name.to_owned(), new_file); |
|
||||||
Ok(files |
|
||||||
.get_mut(file_name) |
|
||||||
.expect("Missing value that was just inserted.")) |
|
||||||
} |
|
||||||
|
|
||||||
/// Removes specified file (in a specified group).
|
|
||||||
/// Will produce error if file does not exist.
|
|
||||||
pub fn remove_file(&mut self, group_name: &str, file_name: &str) -> Result<(), DBError> { |
|
||||||
if self |
|
||||||
.group_files_mut(group_name)? |
|
||||||
.remove(file_name) |
|
||||||
.is_none() |
|
||||||
{ |
|
||||||
return Err(DBError::NoFile { |
|
||||||
group_name: group_name.to_owned(), |
|
||||||
file_name: file_name.to_owned(), |
|
||||||
}); |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns immutable reference to the specified file (in a specified group) as `file::File`.
|
|
||||||
/// `None` if file does not exist.
|
|
||||||
pub fn file_mut(&mut self, group_name: &str, file_name: &str) -> Option<&mut File> { |
|
||||||
match self.group_files_mut(group_name) { |
|
||||||
Ok(files) => files.get_mut(&file_name.to_owned()), |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns mutable reference to the specified file (in a specified group) as `file::File`.
|
|
||||||
/// `None` if file does not exist.
|
|
||||||
pub fn file(&self, group_name: &str, file_name: &str) -> Option<&File> { |
|
||||||
match self.group_files(group_name) { |
|
||||||
Ok(files) => files.get(&file_name.to_owned()), |
|
||||||
_ => None, |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Helper method that raises error if specified group exists.
|
|
||||||
fn assert_no_group(&self, group_name: &str) -> Result<(), DBError> { |
|
||||||
if self.group_index(group_name).is_ok() { |
|
||||||
return Err(DBError::GroupAlreadyExists { |
|
||||||
group_name: group_name.to_owned(), |
|
||||||
}); |
|
||||||
} |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Returns current index of the specified group in`groups` vector.
|
|
||||||
fn group_index(&self, group_name: &str) -> Result<usize, DBError> { |
|
||||||
match self.groups.iter().position(|x| x.name.eq(group_name)) { |
|
||||||
Some(index) => Ok(index), |
|
||||||
_ => { |
|
||||||
return Err(DBError::NoGroup { |
|
||||||
group_name: group_name.to_owned(), |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Helper methods that return (im)mutable reference to `HashMap`
|
|
||||||
// (of 'file_name -> file' map) for a particular group.
|
|
||||||
fn group_files_mut(&mut self, group_name: &str) -> Result<&mut HashMap<String, File>, DBError> { |
|
||||||
let group_index = self.group_index(group_name)?; |
|
||||||
Ok(&mut (&mut self.groups[group_index]).files) |
|
||||||
} |
|
||||||
|
|
||||||
fn group_files(&self, group_name: &str) -> Result<&HashMap<String, File>, DBError> { |
|
||||||
let group_index = self.group_index(group_name)?; |
|
||||||
Ok(&self.groups[group_index].files) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/// Name validity check (for groups and files)
|
|
||||||
fn is_name_valid(entity_name: &str) -> bool { |
|
||||||
entity_name |
|
||||||
.chars() |
|
||||||
.all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()) |
|
||||||
} |
|
||||||
|
|
||||||
/// Helper function that raises error if passed name is invalid
|
|
||||||
fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> { |
|
||||||
if is_name_valid(entity_name) { |
|
||||||
return Ok(()); |
|
||||||
} |
|
||||||
return Err(DBError::InvalidEntityName { |
|
||||||
entity_name: entity_name.to_owned(), |
|
||||||
}); |
|
||||||
} |
|
@ -1,380 +0,0 @@ |
|||||||
use super::*; |
|
||||||
use serde_json::json; |
|
||||||
use std::fs; |
|
||||||
use std::path; |
|
||||||
|
|
||||||
const TEST_DB_PATH: &str = "./fixtures/database"; |
|
||||||
const TEST_DB_MOVED_PATH: &str = "./fixtures/moved"; |
|
||||||
|
|
||||||
const NO_DB_MESSAGE: &str = "Can not find/load test database"; |
|
||||||
|
|
||||||
struct TestCleanup { |
|
||||||
path: String, |
|
||||||
clear_moved: bool, |
|
||||||
} |
|
||||||
|
|
||||||
impl Drop for TestCleanup { |
|
||||||
fn drop(&mut self) { |
|
||||||
let _ = fs::remove_dir_all(&self.path); |
|
||||||
if self.clear_moved { |
|
||||||
let _ = fs::remove_dir_all(TEST_DB_MOVED_PATH); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fn prepare_db_copy(copy_id: &str, clear_moved: bool) -> (String, TestCleanup) { |
|
||||||
let path = format!("{}_{}", TEST_DB_PATH, copy_id); |
|
||||||
let original_db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
original_db |
|
||||||
.write_copy(path::Path::new(&path)) |
|
||||||
.expect("Should be able to create a new copy of the fixture database."); |
|
||||||
( |
|
||||||
path.clone(), |
|
||||||
TestCleanup { |
|
||||||
path: path.to_owned(), |
|
||||||
clear_moved: clear_moved, |
|
||||||
}, |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_path() { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
assert!(db.path() == path::Path::new(TEST_DB_PATH)); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_clear() { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
db.clear(); |
|
||||||
assert!(!db.contains_group("game")); |
|
||||||
assert!(!db.contains_file("administration", "registered")); |
|
||||||
let names = db.group_names(); |
|
||||||
assert_eq!(names.len(), 0); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_group_names() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
|
|
||||||
let names = db.group_names(); |
|
||||||
assert!(names.contains(&"administration".to_owned())); |
|
||||||
assert!(names.contains(&"game".to_owned())); |
|
||||||
assert_eq!(names.len(), 2); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_file_names() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
|
|
||||||
let names_admin = db.file_names_in("administration"); |
|
||||||
let names_game = db.file_names_in("game"); |
|
||||||
assert!(names_admin.contains(&"registered".to_owned())); |
|
||||||
assert!(names_game.contains(&"general".to_owned())); |
|
||||||
assert!(names_game.contains(&"perks".to_owned())); |
|
||||||
assert_eq!(names_admin.len(), 1); |
|
||||||
assert_eq!(names_game.len(), 2); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_group_check() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
|
|
||||||
assert!(db.contains_group("game")); |
|
||||||
assert!(db.contains_group("administration")); |
|
||||||
assert!(!db.contains_group("perks")); |
|
||||||
assert!(!db.contains_group("7random7")); |
|
||||||
assert!(!db.contains_group("")); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_group_remove() -> Result<(), Box<dyn Error>> { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Success
|
|
||||||
db.remove_group("administration")?; |
|
||||||
db.remove_group("game")?; |
|
||||||
assert!(!db.contains_group("administration")); |
|
||||||
assert!(!db.contains_group("game")); |
|
||||||
// Failure
|
|
||||||
db.remove_group("test") |
|
||||||
.expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors."); |
|
||||||
db.remove_group("administration") |
|
||||||
.expect_err("Testing whether removing non-existent groups with incorrect ASCII characters causes errors."); |
|
||||||
db.remove_group("game group").expect_err( |
|
||||||
"Testing whether removing non-existent groups with whitespace characters causes errors.", |
|
||||||
); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_group_create() -> Result<(), Box<dyn Error>> { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Success
|
|
||||||
db.create_group("7random7")?; |
|
||||||
assert!(db.contains_group("7random7")); |
|
||||||
assert!(db.contains_group("game")); |
|
||||||
// Failure
|
|
||||||
db.create_group("my_group").expect_err( |
|
||||||
"Testing whether creating groups with incorrect ASCII characters causes errors.", |
|
||||||
); |
|
||||||
db.create_group("my group") |
|
||||||
.expect_err("Testing whether creating groups with whitespace characters causes errors."); |
|
||||||
db.create_group("Жgroup") |
|
||||||
.expect_err("Testing whether creating groups with non-ASCII characters causes errors."); |
|
||||||
// Create after removal
|
|
||||||
db.remove_group("game")?; |
|
||||||
assert!(!db.contains_group("game")); |
|
||||||
db.create_group("game")?; |
|
||||||
assert!(db.contains_group("game")); |
|
||||||
assert!(db.file_names_in("game").is_empty()); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_file_check() { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Success
|
|
||||||
assert!(db.contains_file("administration", "registered")); |
|
||||||
assert!(db.file("game", "general").is_some()); |
|
||||||
assert!(db.file_mut("game", "perks").is_some()); |
|
||||||
// Failure
|
|
||||||
assert!(!db.contains_file("game", "perk")); |
|
||||||
assert!(db.file("games", "perks").is_none()); |
|
||||||
assert!(db.file_mut("random", "rnd_file").is_none()); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_file_create() -> Result<(), Box<dyn Error>> { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Success
|
|
||||||
let file = db.create_file("administration", "secrets")?; |
|
||||||
assert_eq!(file.to_string(), "{}"); |
|
||||||
assert!(db.contains_file("administration", "secrets")); |
|
||||||
assert!(db.contains_file("game", "perks")); |
|
||||||
// Failure
|
|
||||||
db.create_file("administration", "secrets") |
|
||||||
.expect_err("Testing whether creating existing file causes errors."); |
|
||||||
db.create_file("none", "secrets") |
|
||||||
.expect_err("Testing whether creating existing file in non-existent group causes errors."); |
|
||||||
db.create_file("game", "sec_rets") |
|
||||||
.expect_err("Testing whether creating existing file with invalid name causes errors."); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_file_remove() -> Result<(), Box<dyn Error>> { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Success
|
|
||||||
db.remove_file("administration", "registered")?; |
|
||||||
assert!(!db.contains_file("administration", "registered")); |
|
||||||
assert!(db.contains_group("administration")); |
|
||||||
db.remove_file("game", "perks")?; |
|
||||||
assert_eq!(db.file_names_in("game").len(), 1); |
|
||||||
// Failure
|
|
||||||
db.remove_file("administration", "registered") |
|
||||||
.expect_err("Testing whether removing non-existent files causes errors."); |
|
||||||
db.remove_file("administration", "never") |
|
||||||
.expect_err("Testing whether removing non-existent files causes errors."); |
|
||||||
db.remove_file("never", "file") |
|
||||||
.expect_err("Testing whether removing non-existent files causes errors."); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn file_json_contents() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
let registered = db.file("administration", "registered").unwrap().root(); |
|
||||||
let user_map = registered |
|
||||||
.as_object() |
|
||||||
.expect("Read value is not an object."); |
|
||||||
assert_eq!(user_map.len(), 2); |
|
||||||
assert!(user_map.contains_key("76561198025127722")); |
|
||||||
let user_record = user_map |
|
||||||
.get("76561198044316328") |
|
||||||
.unwrap() |
|
||||||
.as_object() |
|
||||||
.unwrap(); |
|
||||||
assert_eq!(user_record.len(), 3); |
|
||||||
assert_eq!(user_record.get("ip_lock").unwrap().as_bool(), Some(false)); |
|
||||||
assert_eq!( |
|
||||||
user_record.get("password_hash").unwrap().as_str(), |
|
||||||
Some("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") |
|
||||||
); |
|
||||||
|
|
||||||
let groups_arrays = user_record.get("groups").unwrap().as_array().unwrap(); |
|
||||||
assert_eq!(groups_arrays.len(), 1); |
|
||||||
assert_eq!(groups_arrays[0], "admin"); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn file_json_get() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
// Test empty path
|
|
||||||
let file = db.file("administration", "registered").unwrap(); |
|
||||||
assert_eq!(file.root(), file.get("").unwrap()); |
|
||||||
// Test complex path
|
|
||||||
let expected = file.get("/76561198025127722/allowed_ips/1").unwrap(); |
|
||||||
assert_eq!(expected.as_str().unwrap(), "192.168.0.100"); |
|
||||||
// Test bad paths
|
|
||||||
assert!(file.get("/777") == None); |
|
||||||
assert!(file.get("/76561198025127722/allowed_ips/2") == None); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn file_contains_check() { |
|
||||||
let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
let registered_file = db.file("administration", "registered").unwrap(); |
|
||||||
let perks_file = db.file("game", "perks").unwrap(); |
|
||||||
// These exist
|
|
||||||
assert!(registered_file.contains("/76561198025127722/password_hash")); |
|
||||||
assert!(registered_file.contains("/76561198044316328/groups/0")); |
|
||||||
assert!(perks_file.contains("/76561198025127722/headshots")); |
|
||||||
assert!(perks_file.contains("/76561198044316328")); |
|
||||||
// These do not exist
|
|
||||||
assert!(!registered_file.contains("/76561198025127722/password/")); |
|
||||||
assert!(!registered_file.contains("/76561198044316328/groups/2")); |
|
||||||
assert!(!perks_file.contains("/76561198025127722/assault_rifle_damage/9067")); |
|
||||||
assert!(!perks_file.contains("/76561198044316328/headshots")); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_insert_success() -> Result<(), Box<dyn Error>> { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
let registered_file = db.file_mut("administration", "registered").unwrap(); |
|
||||||
// Modify existing
|
|
||||||
registered_file.insert("/76561198025127722/ip_lock", json!(false))?; |
|
||||||
assert_eq!( |
|
||||||
registered_file |
|
||||||
.get("/76561198025127722/ip_lock") |
|
||||||
.unwrap() |
|
||||||
.to_string(), |
|
||||||
"false" |
|
||||||
); |
|
||||||
registered_file.insert("/76561198044316328/password_hash", json!({"var":13524}))?; |
|
||||||
assert_eq!( |
|
||||||
registered_file |
|
||||||
.get("/76561198044316328/password_hash") |
|
||||||
.unwrap() |
|
||||||
.to_string(), |
|
||||||
r#"{"var":13524}"# |
|
||||||
); |
|
||||||
// Reset whole file
|
|
||||||
registered_file.insert("", json!({}))?; |
|
||||||
assert_eq!(registered_file.root().to_string(), "{}"); |
|
||||||
// Add new values
|
|
||||||
registered_file.insert("/new_var", json!([42, {"word":"life"}, null]))?; |
|
||||||
assert_eq!( |
|
||||||
registered_file.root().to_string(), |
|
||||||
r#"{"new_var":[42,{"word":"life"},null]}"# |
|
||||||
); |
|
||||||
let general_file = db.file_mut("game", "general").unwrap(); |
|
||||||
general_file.insert("/76561198025127722/achievements/5", json!("kf:bugged"))?; |
|
||||||
assert_eq!( |
|
||||||
general_file |
|
||||||
.get("/76561198025127722/achievements") |
|
||||||
.unwrap() |
|
||||||
.to_string(), |
|
||||||
r#"["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn",null,null,"kf:bugged"]"# |
|
||||||
); |
|
||||||
Ok(()) |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_set_failure() { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
let file = db.file_mut("administration", "registered").unwrap(); |
|
||||||
file.insert("/76561198025127722/dir/var", json!(null)) |
|
||||||
.expect_err("Testing panic at trying to set a value in non-existing object/array."); |
|
||||||
file.insert("/76561198044316328/groups/d", json!(null)) |
|
||||||
.expect_err("Testing panic at trying to set a value at non-numeric index in an array."); |
|
||||||
file.insert("/76561198044316328/groups/-1", json!(null)) |
|
||||||
.expect_err("Testing panic at trying to set a value at negative index in an array."); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_remove_value() { |
|
||||||
let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
let file = db.file_mut("administration", "registered").unwrap(); |
|
||||||
// Removing non-existent value
|
|
||||||
assert_eq!(file.remove("/76561198025127722/something"), None); |
|
||||||
// Remove simple value
|
|
||||||
assert_eq!( |
|
||||||
file.remove("/76561198025127722/password_hash").unwrap(), |
|
||||||
json!("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") |
|
||||||
); |
|
||||||
assert!(!file.contains("/76561198025127722/password_hash")); |
|
||||||
// Remove complex value (array)
|
|
||||||
assert_eq!( |
|
||||||
file.remove("/76561198044316328/groups").unwrap(), |
|
||||||
json!(["admin"]) |
|
||||||
); |
|
||||||
assert!(!file.contains("/76561198044316328/groups/0")); |
|
||||||
assert!(!file.contains("/76561198044316328/groups")); |
|
||||||
// Remove array elements
|
|
||||||
assert_eq!( |
|
||||||
file.remove("/76561198025127722/allowed_ips/0").unwrap(), |
|
||||||
json!("127.0.0.1") |
|
||||||
); |
|
||||||
assert!(file.contains("/76561198025127722/allowed_ips/0")); |
|
||||||
assert!(!file.contains("/76561198025127722/allowed_ips/1")); |
|
||||||
assert_eq!( |
|
||||||
file.remove("/76561198025127722/allowed_ips/0").unwrap(), |
|
||||||
json!("192.168.0.100") |
|
||||||
); |
|
||||||
assert!(file.contains("/76561198025127722/allowed_ips")); |
|
||||||
assert!(!file.contains("/76561198025127722/allowed_ips/0")); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_save() { |
|
||||||
let (path, _cleanup) = prepare_db_copy("db_save", false); |
|
||||||
// Change something up and save
|
|
||||||
let mut db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE); |
|
||||||
db.remove_group("administration") |
|
||||||
.expect(r#"Should be able to remove "administration" group"#); |
|
||||||
db.save() |
|
||||||
.expect("Should be able to save copy of the database."); |
|
||||||
// Reload and check changes
|
|
||||||
let db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE); |
|
||||||
assert_eq!(db.group_names().len(), 1); |
|
||||||
assert_eq!(db.group_names().get(0), Some(&"game".to_owned())); |
|
||||||
assert_eq!(db.file_names_in("game").len(), 2); |
|
||||||
assert!(db.contains_file("game", "general")); |
|
||||||
assert!(db.contains_file("game", "perks")); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_change_path() { |
|
||||||
let (path, _cleanup) = prepare_db_copy("db_change_path", true); |
|
||||||
// Change something up and move
|
|
||||||
let mut db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE); |
|
||||||
db.remove_group("administration") |
|
||||||
.expect(r#"Should be able to remove "administration" group"#); |
|
||||||
db.file_mut("game", "perks") |
|
||||||
.unwrap() |
|
||||||
.insert("", json!({"var":7})) |
|
||||||
.expect("Should be able to insert into root."); |
|
||||||
db.change_path(path::Path::new(TEST_DB_MOVED_PATH)) |
|
||||||
.expect("Should be able to change database's path."); |
|
||||||
assert!(!path::Path::new(&path).exists()); |
|
||||||
assert!(path::Path::new(TEST_DB_MOVED_PATH).exists()); |
|
||||||
// Reload and check the changes
|
|
||||||
let db = Database::load(path::Path::new(TEST_DB_MOVED_PATH)).expect(NO_DB_MESSAGE); |
|
||||||
assert_eq!(db.group_names().len(), 1); |
|
||||||
assert_eq!(db.group_names().get(0), Some(&"game".to_owned())); |
|
||||||
assert_eq!(db.file_names_in("game").len(), 2); |
|
||||||
assert!(db.contains_file("game", "general")); |
|
||||||
assert!(db.contains_file("game", "perks")); |
|
||||||
assert_eq!( |
|
||||||
db.file("game", "perks").unwrap().root().to_string(), |
|
||||||
r#"{"var":7}"#.to_owned() |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
|
||||||
fn db_erase() { |
|
||||||
let (path, _cleanup) = prepare_db_copy("db_erase", false); |
|
||||||
let db = Database::load(path::Path::new(&path)).expect(NO_DB_MESSAGE); |
|
||||||
db.erase().expect("Should be able to erase data."); |
|
||||||
assert!(!path::Path::new(&path).exists()); |
|
||||||
} |
|
@ -1,16 +1,13 @@ |
|||||||
use std::env; |
use std::env; |
||||||
use std::path::Path; |
use std::path::Path; |
||||||
mod database; |
mod unreal_config; |
||||||
|
|
||||||
use simplelog::{Config, LevelFilter, SimpleLogger}; |
|
||||||
|
|
||||||
fn main() { |
fn main() { |
||||||
let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); |
|
||||||
let args: Vec<String> = env::args().collect(); |
let args: Vec<String> = env::args().collect(); |
||||||
let filename = &args[1]; |
let filename = &args[1]; |
||||||
let db = database::Database::load(Path::new(filename)); |
let config = unreal_config::load_file(Path::new(filename)); |
||||||
/*match db {
|
match config { |
||||||
Ok(db) => print!("{}", db), |
Ok(config) => print!("{}", config), |
||||||
Err(error) => println!("OH NO: {}", error), |
_ => (), |
||||||
}*/ |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue