From 408757149c8f31b7e09da3043fcd08d481eeb633 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 17 Nov 2020 19:22:12 +0700 Subject: [PATCH 01/13] Add database skeleton --- Cargo.lock | 56 ++++++++++++++++++++++ Cargo.toml | 3 ++ src/database/mod.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 6 +-- 4 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 src/database/mod.rs diff --git a/Cargo.lock b/Cargo.lock index b0eb9bf..ba42506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,4 +3,60 @@ [[package]] name = "avarice" version = "0.1.0" +dependencies = [ + "custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", +] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "custom_error" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12" +"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +"checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +"checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" diff --git a/Cargo.toml b/Cargo.toml index e8facc0..e04d62e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,6 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +log = "0.4" +serde_json = "1.0" +custom_error = "1.8.0" \ No newline at end of file diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..19f78fa --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,112 @@ +use log::warn; +use serde_json; +use std::error::Error; +use std::fs; +use std::path; + +extern crate custom_error; +use custom_error::custom_error; + +custom_error! {DatabaseError + NotDirectory{path: String} = "Path to database should point at the directory: {path}", +} + +struct File { + name: String, + contents: serde_json::Value, +} + +struct Group { + name: String, + files: Vec, +} + +struct Category { + name: String, + groups: Vec, +} + +pub struct Database { + storage: path::PathBuf, + contents: Vec, +} + +impl Database { + pub fn new(storage: &path::Path) -> Result> { + if !storage.is_dir() { + return Err(Box::new(DatabaseError::NotDirectory { + path: storage.display().to_string(), + })); + } + let mut contents = Vec::new(); + for entry in fs::read_dir(storage)? { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() { + warn!( + r#"File {} found where only category directories are supposed to be"#, + path.display() + ); + } else { + let category = load_category(&path)?; + contents.push(category); + } + } + Ok(Database { + storage: storage.to_path_buf(), + contents, + }) + } +} + +fn load_category(category_path: &path::Path) -> Result> { + let mut groups = Vec::new(); + for entry in fs::read_dir(category_path)? { + let entry = entry?; + let path = entry.path(); + if !path.is_dir() { + warn!( + r#"File {} found where only group directories are supposed to be"#, + path.display() + ); + } else { + let group = load_group(&path)?; + groups.push(group); + } + } + Ok(Category { + name: get_file_name(category_path), + groups, + }) +} + +fn load_group(group_path: &path::Path) -> Result> { + let mut files = Vec::new(); + for entry in fs::read_dir(group_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_dir() { + warn!( + r#"Directory {} found where only data files are supposed to be"#, + path.display() + ); + } else { + let file_contents = fs::read_to_string(&path)?; + files.push(File { + name: get_file_name(path.as_path()), + contents: serde_json::from_str(&file_contents)?, + }); + } + } + Ok(Group { + name: get_file_name(group_path), + files, + }) +} + +fn get_file_name(path: &path::Path) -> String { + path.file_stem() + .and_then(|x| x.to_str()) + .unwrap_or_default() + .to_string() +} diff --git a/src/main.rs b/src/main.rs index 8f664c9..b78fc6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,13 @@ use std::env; use std::path::Path; -mod unreal_config; +mod database; fn main() { let args: Vec = env::args().collect(); let filename = &args[1]; - let config = unreal_config::load_file(Path::new(filename)); + let config = database::Database::new(Path::new(filename)); match config { - Ok(config) => print!("{}", config), + Ok(db) => print!("ok"), _ => (), } } From 185f7ced8de4a44400051f3976d6d826863acb69 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Tue, 17 Nov 2020 19:36:29 +0700 Subject: [PATCH 02/13] Add db output for testing --- src/database/mod.rs | 31 +++++++++++++++++++++++++++++++ src/main.rs | 4 ++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 19f78fa..43169df 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -2,6 +2,7 @@ use log::warn; use serde_json; use std::error::Error; use std::fs; +use std::fmt; use std::path; extern crate custom_error; @@ -31,6 +32,36 @@ pub struct Database { contents: Vec, } +impl fmt::Display for Group { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, " ({})", self.name)?; + for file in self.files.iter() { + writeln!(f, r#" File "{}": {}"#, file.name, file.contents.to_string())?; + } + Ok(()) + } +} + +impl fmt::Display for Category { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "[{}]", self.name)?; + for g in self.groups.iter() { + write!(f, "{}", g)?; + } + Ok(()) + } +} + +impl fmt::Display for Database { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "DB: {}", self.storage.display())?; + for c in self.contents.iter() { + writeln!(f, "{}", c)?; + } + Ok(()) + } +} + impl Database { pub fn new(storage: &path::Path) -> Result> { if !storage.is_dir() { diff --git a/src/main.rs b/src/main.rs index b78fc6d..9aff605 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,7 +7,7 @@ fn main() { let filename = &args[1]; let config = database::Database::new(Path::new(filename)); match config { - Ok(db) => print!("ok"), - _ => (), + Ok(db) => print!("{}", db), + Err(error) => println!("OH NO: {}", error), } } From 1572d7fb5a40a1a81125ea32f7fa017fe5ba6504 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 21 Nov 2020 23:53:03 +0700 Subject: [PATCH 03/13] Implement get/set functionality for database --- src/database/mod.rs | 285 +++++++++++++++++++++++++++++++++--------- src/database/tests.rs | 160 ++++++++++++++++++++++++ src/main.rs | 4 +- 3 files changed, 385 insertions(+), 64 deletions(-) create mode 100644 src/database/tests.rs diff --git a/src/database/mod.rs b/src/database/mod.rs index 43169df..541d991 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,52 +1,48 @@ +#[cfg(test)] +mod tests; + use log::warn; use serde_json; +use serde_json::json; +use std::collections::HashMap; use std::error::Error; -use std::fs; use std::fmt; +use std::fs; use std::path; extern crate custom_error; use custom_error::custom_error; -custom_error! {DatabaseError +const JSON_POINTER_SEPARATOR: &str = "/"; + +custom_error! {DBError NotDirectory{path: String} = "Path to database should point at the directory: {path}", + NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group "{group_name}"."#, + IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}.", } -struct File { - name: String, - contents: serde_json::Value, +enum ValueReference<'a> { + Object(&'a mut serde_json::Map, String), + Array(&'a mut Vec, usize), } -struct Group { - name: String, - files: Vec, -} +type FileID<'a> = (&'a str, &'a str); -struct Category { +pub struct Group { name: String, - groups: Vec, + files: HashMap, } pub struct Database { - storage: path::PathBuf, - contents: Vec, + storage_path: path::PathBuf, + groups: Vec, } impl fmt::Display for Group { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, " ({})", self.name)?; - for file in self.files.iter() { - writeln!(f, r#" File "{}": {}"#, file.name, file.contents.to_string())?; - } - Ok(()) - } -} - -impl fmt::Display for Category { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "[{}]", self.name)?; - for g in self.groups.iter() { - write!(f, "{}", g)?; + for (name, contents) in self.files.iter() { + writeln!(f, r#" File "{}": {}"#, name, contents.to_string())?; } Ok(()) } @@ -54,65 +50,153 @@ impl fmt::Display for Category { impl fmt::Display for Database { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "DB: {}", self.storage.display())?; - for c in self.contents.iter() { - writeln!(f, "{}", c)?; + writeln!(f, "DB: {}", self.storage_path.display())?; + for g in self.groups.iter() { + writeln!(f, "{}", g)?; } Ok(()) } } impl Database { - pub fn new(storage: &path::Path) -> Result> { - if !storage.is_dir() { - return Err(Box::new(DatabaseError::NotDirectory { - path: storage.display().to_string(), + pub fn new(storage_path: &path::Path) -> Result> { + if !storage_path.is_dir() { + return Err(Box::new(DBError::NotDirectory { + path: storage_path.display().to_string(), })); } - let mut contents = Vec::new(); - for entry in fs::read_dir(storage)? { + let mut groups = Vec::new(); + for entry in fs::read_dir(storage_path)? { let entry = entry?; let path = entry.path(); if !path.is_dir() { warn!( - r#"File {} found where only category directories are supposed to be"#, + r#"File {} found where only group directories are supposed to be"#, path.display() ); } else { - let category = load_category(&path)?; - contents.push(category); + let group = load_group(&path)?; + groups.push(group); } } Ok(Database { - storage: storage.to_path_buf(), - contents, + storage_path: storage_path.to_path_buf(), + groups, }) } -} -fn load_category(category_path: &path::Path) -> Result> { - let mut groups = Vec::new(); - for entry in fs::read_dir(category_path)? { - let entry = entry?; - let path = entry.path(); - if !path.is_dir() { - warn!( - r#"File {} found where only group directories are supposed to be"#, - path.display() - ); - } else { - let group = load_group(&path)?; - groups.push(group); + pub fn group_names(&self) -> Vec { + self.groups.iter().map(|x| x.name.clone()).collect() + } + + pub fn file_names_in(&self, group_name: &str) -> Vec { + match self.groups.iter().find(|x| x.name.eq(group_name)) { + Some(group) => group.files.keys().map(|x| x.clone()).collect(), + None => Vec::new(), } } - Ok(Category { - name: get_file_name(category_path), - groups, - }) + + fn group_index(&self, group_name: &str) -> Option { + self.groups.iter().position(|x| x.name.eq(group_name)) + } + + fn as_json_mut(&mut self, (group_name, file_name): FileID) -> Option<&mut serde_json::Value> { + match self.group_index(group_name) { + Some(index) => self.groups[index].files.get_mut(&file_name.to_owned()), + _ => None, + } + } + + pub fn as_json(&self, (group_name, file_name): FileID) -> Option<&serde_json::Value> { + match self.group_index(group_name) { + Some(index) => self.groups[index].files.get(&file_name.to_owned()), + _ => None, + } + } + + pub fn as_string(&self, file_id: FileID) -> Option { + self.as_json(file_id).map(|x| x.to_string()) + } + + pub fn get_json(&self, file_id: FileID, pointer: &str) -> Option<&serde_json::Value> { + match self.as_json(file_id) { + Some(v) => v.pointer(pointer), + _ => None, + } + } + + pub fn get_string(&self, file_id: FileID, pointer: &str) -> Option { + self.get_json(file_id, pointer).map(|x| x.to_string()) + } + + pub fn contains(&self, file_id: FileID, pointer: &str) -> bool { + self.get_json(file_id, pointer) != None + } + + pub fn set_json( + &mut self, + (group_name, file_name): FileID, + pointer: &str, + new_value: serde_json::Value, + ) -> Result<(), Box> { + let file_json = match self.as_json_mut((group_name, file_name)) { + Some(file_json) => file_json, + _ => { + return Err(Box::new(DBError::NoFile { + group_name: group_name.to_owned(), + file_name: file_name.to_owned(), + })) + } + }; + touch(file_json, pointer)?; + match file_json.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(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })); + } + }; + Ok(()) + } + + pub fn remove( + &mut self, + (group_name, file_name): FileID, + pointer: &str, + ) -> Result<(), Box> { + let file_json = match self.as_json_mut((group_name, file_name)) { + Some(file_json) => file_json, + _ => { + return Err(Box::new(DBError::NoFile { + group_name: group_name.to_owned(), + file_name: file_name.to_owned(), + })) + } + }; + match pointer_to_reference(file_json, pointer) { + Some(ValueReference::Object(map, variable_name)) => { + map.remove(&variable_name); + } + Some(ValueReference::Array(vec, variable_index)) => { + if variable_index < vec.len() { + vec.remove(variable_index); + } + } + _ => { + return Err(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })) + } + }; + Ok(()) + } } fn load_group(group_path: &path::Path) -> Result> { - let mut files = Vec::new(); + let mut files = HashMap::new(); for entry in fs::read_dir(group_path)? { let entry = entry?; let path = entry.path(); @@ -123,10 +207,9 @@ fn load_group(group_path: &path::Path) -> Result> { ); } else { let file_contents = fs::read_to_string(&path)?; - files.push(File { - name: get_file_name(path.as_path()), - contents: serde_json::from_str(&file_contents)?, - }); + let file_name = get_file_name(path.as_path()); + let file_contents = serde_json::from_str(&file_contents)?; + files.insert(file_name, file_contents); } } Ok(Group { @@ -135,9 +218,87 @@ fn load_group(group_path: &path::Path) -> Result> { }) } +fn touch(json_root: &mut serde_json::Value, pointer: &str) -> (Result<(), Box>) { + if pointer.is_empty() || json_root.pointer_mut(pointer).is_some() { + return Ok(()); + } + match pointer_to_reference(json_root, pointer) { + Some(ValueReference::Object(map, variable_name)) => { + map.insert(variable_name, json!(null)); + } + Some(ValueReference::Array(vec, variable_index)) => { + // Since values at the index does not exist - resize will increase the sizeof the array + vec.resize(variable_index + 1, json!(null)); + } + _ => { + return Err(Box::new(DBError::IncorrectPointer { + pointer: pointer.to_owned(), + })) + } + }; + Ok(()) +} + +fn pointer_to_reference<'a>( + json_root: &'a mut serde_json::Value, + pointer: &str, +) -> Option> { + if pointer.is_empty() { + return None; + } + let container_variable_pair = + pop_json_pointer(pointer).and_then(move |(path, variable_name)| { + match json_root.pointer_mut(&path) { + Some(v) => Some((v, variable_name)), + _ => None, + } + }); + let (json_container, variable_name) = match container_variable_pair { + Some(v) => v, + _ => return None, + }; + match json_container { + serde_json::Value::Object(map) => Some(ValueReference::Object(map, variable_name)), + serde_json::Value::Array(vec) => { + let index: usize = match variable_name.parse() { + Ok(v) => v, + _ => return None, + }; + Some(ValueReference::Array(vec, index)) + } + _ => None, + } +} + +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)) +} + fn get_file_name(path: &path::Path) -> String { path.file_stem() .and_then(|x| x.to_str()) .unwrap_or_default() .to_string() } +// TODO add tests for remove +// TODO add tests for panics (both add and remove) +// TODO add file addition/removal +// TODO add db saving + +// TODO make sure file's main value not being an object won't break anything +// TODO check that file name is appropriate +// TODO handle parsing errors differently +// TODO add logs diff --git a/src/database/tests.rs b/src/database/tests.rs new file mode 100644 index 0000000..5347949 --- /dev/null +++ b/src/database/tests.rs @@ -0,0 +1,160 @@ +use super::*; +use serde_json::json; +use std::path; + +const TEST_DB_PATH: &str = "./fixtures/database"; + +const NO_DB_MESSAGE: &str = "Can not find/load test database"; + +#[test] +fn group_names() { + let db = Database::new(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 file_names() { + let db = Database::new(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_json_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let registered = db.as_json(("administration", "registered")).unwrap(); + 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 db_string_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let general = db.as_string(("game", "general")).unwrap(); + assert!(general.contains(r#""dosh_thrown":527624"#)); + assert!(general + .contains(r#""achievements":["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn"]"#)); +} + +#[test] +fn db_json_sub_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Test empty path + let file_id = ("administration", "registered"); + assert_eq!(db.as_json(file_id), db.get_json(file_id, "")); + // Test complex path + let received = db + .get_json(file_id, "/76561198025127722/allowed_ips/1") + .unwrap(); + assert_eq!(received.as_str().unwrap(), "192.168.0.100"); + // Test bad paths + assert!(db.get_json(file_id, "/777") == None); + assert!(db.get_json(file_id, "/76561198025127722/allowed_ips/2") == None); +} + +#[test] +fn db_string_sub_contents() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Test empty path + let file_id = ("administration", "registered"); + assert_eq!(db.as_string(file_id), db.get_string(file_id, "")); + // Test complex path + let received = db + .get_string(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(); + assert_eq!(received, r#""127.0.0.1""#); +} + +#[test] +fn db_contains_check() { + let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let registered_id = ("administration", "registered"); + let perks_id = ("game", "perks"); + // These exist + assert!(db.contains(registered_id, "/76561198025127722/password_hash")); + assert!(db.contains(registered_id, "/76561198044316328/groups")); + assert!(db.contains(perks_id, "/76561198025127722/headshots")); + assert!(db.contains(perks_id, "/76561198044316328")); + // These do not exist + assert!(!db.contains(registered_id, "/76561198025127722/password/")); + assert!(!db.contains(registered_id, "/76561198044316328/groups/2")); + assert!(!db.contains(perks_id, "/76561198025127722/assault_rifle_damage/9067")); + assert!(!db.contains(perks_id, "/76561198044316328/headshots")); +} + +#[test] +fn db_set_success() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let file_id = ("administration", "registered"); + // Modify existing + db.set_json(file_id, "/76561198025127722/ip_lock", json!(false))?; + assert_eq!( + db.get_string(file_id, "/76561198025127722/ip_lock") + .unwrap(), + "false" + ); + db.set_json( + file_id, + "/76561198044316328/password_hash", + json!({"var":13524}), + )?; + assert_eq!( + db.get_string(file_id, "/76561198044316328/password_hash") + .unwrap(), + r#"{"var":13524}"# + ); + // Reset whole file + db.set_json(file_id, "", json!({}))?; + assert_eq!(db.as_json(file_id).unwrap().to_string(), "{}"); + // Add new values + db.set_json(file_id, "/new_var", json!([42, {"word":"life"}, null]))?; + assert_eq!( + db.as_json(file_id).unwrap().to_string(), + r#"{"new_var":[42,{"word":"life"},null]}"# + ); + Ok(()) +} + +#[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); +} diff --git a/src/main.rs b/src/main.rs index 9aff605..5a5bd11 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,8 +5,8 @@ mod database; fn main() { let args: Vec = env::args().collect(); let filename = &args[1]; - let config = database::Database::new(Path::new(filename)); - match config { + let db = database::Database::new(Path::new(filename)); + match db { Ok(db) => print!("{}", db), Err(error) => println!("OH NO: {}", error), } From 3432ea98578b0415f392600f57f36e7132992086 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 22 Nov 2020 19:45:43 +0700 Subject: [PATCH 04/13] Add group/file management methods --- src/database/mod.rs | 125 +++++++++++++++++++------ src/database/tests.rs | 209 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 289 insertions(+), 45 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 541d991..9fde7d8 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -15,10 +15,14 @@ use custom_error::custom_error; const JSON_POINTER_SEPARATOR: &str = "/"; -custom_error! {DBError - NotDirectory{path: String} = "Path to database should point at the directory: {path}", - NoFile{group_name: String, file_name: String} = r#"There is no "{file_name}" file in group "{group_name}"."#, - IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}.", +custom_error! { pub DBError + NotDirectory{path: String} = "Path to the database should point at a directory: {path}", + 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}""#, + IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}", } enum ValueReference<'a> { @@ -100,6 +104,77 @@ impl Database { self.groups.iter().position(|x| x.name.eq(group_name)) } + pub fn contains_group(&self, group_name: &str) -> bool { + self.group_index(group_name).is_some() + } + + pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> { + verify_name(group_name)?; + if self.group_index(group_name).is_some() { + return Err(DBError::GroupAlreadyExists { + group_name: group_name.to_owned(), + }); + } + self.groups.push(Group { + name: group_name.to_owned(), + files: HashMap::new(), + }); + Ok(()) + } + + pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> { + match self.group_index(group_name) { + Some(index) => self.groups.remove(index), + _ => { + return Err(DBError::NoGroup { + group_name: group_name.to_owned(), + }) + } + }; + Ok(()) + } + + pub fn contains_file(&self, (group_name, file_name): FileID) -> bool { + match self.group_index(group_name) { + Some(index) => self.groups[index].files.contains_key(&file_name.to_owned()), + _ => false, + } + } + + pub fn create_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { + let group_name = group_name.to_owned(); + let file_name = file_name.to_owned(); + verify_name(&file_name)?; + let group_index = match self.group_index(&group_name) { + Some(index) => index, + _ => return Err(DBError::NoGroup { group_name }), + }; + if self.groups[group_index].files.contains_key(&file_name) { + return Err(DBError::FileAlreadyExists { + group_name, + file_name, + }); + } + self.groups[group_index].files.insert(file_name, json!({})); + Ok(()) + } + + pub fn remove_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { + let group_name = group_name.to_owned(); + let file_name = file_name.to_owned(); + let group_index = match self.group_index(&group_name) { + Some(index) => index, + _ => return Err(DBError::NoGroup { group_name }), + }; + if self.groups[group_index].files.remove(&file_name).is_none() { + return Err(DBError::NoFile { + group_name, + file_name, + }); + } + Ok(()) + } + fn as_json_mut(&mut self, (group_name, file_name): FileID) -> Option<&mut serde_json::Value> { match self.group_index(group_name) { Some(index) => self.groups[index].files.get_mut(&file_name.to_owned()), @@ -129,7 +204,7 @@ impl Database { self.get_json(file_id, pointer).map(|x| x.to_string()) } - pub fn contains(&self, file_id: FileID, pointer: &str) -> bool { + pub fn contains_value(&self, file_id: FileID, pointer: &str) -> bool { self.get_json(file_id, pointer) != None } @@ -166,35 +241,36 @@ impl Database { &mut self, (group_name, file_name): FileID, pointer: &str, - ) -> Result<(), Box> { + ) -> Option { let file_json = match self.as_json_mut((group_name, file_name)) { Some(file_json) => file_json, - _ => { - return Err(Box::new(DBError::NoFile { - group_name: group_name.to_owned(), - file_name: file_name.to_owned(), - })) - } + _ => return None, }; match pointer_to_reference(file_json, pointer) { - Some(ValueReference::Object(map, variable_name)) => { - map.remove(&variable_name); - } + Some(ValueReference::Object(map, variable_name)) => map.remove(&variable_name), Some(ValueReference::Array(vec, variable_index)) => { if variable_index < vec.len() { - vec.remove(variable_index); + return Some(vec.remove(variable_index)); } + None } - _ => { - return Err(Box::new(DBError::IncorrectPointer { - pointer: pointer.to_owned(), - })) - } - }; - Ok(()) + _ => None, + } } } +fn verify_name(entity_name: &str) -> Result<(), DBError> { + let is_valid = entity_name + .chars() + .all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()); + if is_valid { + return Ok(()); + } + return Err(DBError::InvalidEntityName { + entity_name: entity_name.to_owned(), + }); +} + fn load_group(group_path: &path::Path) -> Result> { let mut files = HashMap::new(); for entry in fs::read_dir(group_path)? { @@ -293,9 +369,6 @@ fn get_file_name(path: &path::Path) -> String { .unwrap_or_default() .to_string() } -// TODO add tests for remove -// TODO add tests for panics (both add and remove) -// TODO add file addition/removal // TODO add db saving // TODO make sure file's main value not being an object won't break anything diff --git a/src/database/tests.rs b/src/database/tests.rs index 5347949..037b7bf 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -7,7 +7,7 @@ const TEST_DB_PATH: &str = "./fixtures/database"; const NO_DB_MESSAGE: &str = "Can not find/load test database"; #[test] -fn group_names() { +fn db_group_names() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names = db.group_names(); @@ -17,7 +17,7 @@ fn group_names() { } #[test] -fn file_names() { +fn db_file_names() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); let names_admin = db.file_names_in("administration"); @@ -29,6 +29,110 @@ fn file_names() { assert_eq!(names_game.len(), 2); } +#[test] +fn db_group_check() { + let db = Database::new(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> { + let mut db = Database::new(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> { + let mut db = Database::new(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 db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + assert!(db.contains_file(("administration", "registered"))); + assert!(db.contains_file(("game", "general"))); + assert!(db.contains_file(("game", "perks"))); + // Failure + assert!(!db.contains_file(("game", "perk"))); + assert!(!db.contains_file(("games", "perks"))); + assert!(!db.contains_file(("random", "rnd_file"))); +} + +#[test] +fn db_file_create() -> Result<(), Box> { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + // Success + db.create_file(("administration", "secrets"))?; + assert!(db.contains_file(("administration", "secrets"))); + assert_eq!(db.as_string(("administration", "secrets")).unwrap(), "{}"); + 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> { + let mut db = Database::new(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 db_json_contents() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); @@ -99,50 +203,117 @@ fn db_contains_check() { let registered_id = ("administration", "registered"); let perks_id = ("game", "perks"); // These exist - assert!(db.contains(registered_id, "/76561198025127722/password_hash")); - assert!(db.contains(registered_id, "/76561198044316328/groups")); - assert!(db.contains(perks_id, "/76561198025127722/headshots")); - assert!(db.contains(perks_id, "/76561198044316328")); + assert!(db.contains_value(registered_id, "/76561198025127722/password_hash")); + assert!(db.contains_value(registered_id, "/76561198044316328/groups/0")); + assert!(db.contains_value(perks_id, "/76561198025127722/headshots")); + assert!(db.contains_value(perks_id, "/76561198044316328")); // These do not exist - assert!(!db.contains(registered_id, "/76561198025127722/password/")); - assert!(!db.contains(registered_id, "/76561198044316328/groups/2")); - assert!(!db.contains(perks_id, "/76561198025127722/assault_rifle_damage/9067")); - assert!(!db.contains(perks_id, "/76561198044316328/headshots")); + assert!(!db.contains_value(registered_id, "/76561198025127722/password/")); + assert!(!db.contains_value(registered_id, "/76561198044316328/groups/2")); + assert!(!db.contains_value(perks_id, "/76561198025127722/assault_rifle_damage/9067")); + assert!(!db.contains_value(perks_id, "/76561198044316328/headshots")); } #[test] fn db_set_success() -> Result<(), Box> { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let file_id = ("administration", "registered"); + let registered_id = ("administration", "registered"); + let general_id = ("game", "general"); // Modify existing - db.set_json(file_id, "/76561198025127722/ip_lock", json!(false))?; + db.set_json(registered_id, "/76561198025127722/ip_lock", json!(false))?; assert_eq!( - db.get_string(file_id, "/76561198025127722/ip_lock") + db.get_string(registered_id, "/76561198025127722/ip_lock") .unwrap(), "false" ); db.set_json( - file_id, + registered_id, "/76561198044316328/password_hash", json!({"var":13524}), )?; assert_eq!( - db.get_string(file_id, "/76561198044316328/password_hash") + db.get_string(registered_id, "/76561198044316328/password_hash") .unwrap(), r#"{"var":13524}"# ); // Reset whole file - db.set_json(file_id, "", json!({}))?; - assert_eq!(db.as_json(file_id).unwrap().to_string(), "{}"); + db.set_json(registered_id, "", json!({}))?; + assert_eq!(db.as_json(registered_id).unwrap().to_string(), "{}"); // Add new values - db.set_json(file_id, "/new_var", json!([42, {"word":"life"}, null]))?; + db.set_json( + registered_id, + "/new_var", + json!([42, {"word":"life"}, null]), + )?; assert_eq!( - db.as_json(file_id).unwrap().to_string(), + db.as_json(registered_id).unwrap().to_string(), r#"{"new_var":[42,{"word":"life"},null]}"# ); + db.set_json( + general_id, + "/76561198025127722/achievements/5", + json!("kf:bugged"), + )?; + assert_eq!( + db.get_string(general_id, "/76561198025127722/achievements") + .unwrap(), + r#"["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn",null,null,"kf:bugged"]"# + ); Ok(()) } +#[test] +fn db_set_failure() { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let file_id = ("administration", "registered"); + let imaginary_file_id = ("general", "everything"); + db.set_json(imaginary_file_id, "", json!(null)) + .expect_err("Testing panic at missing file."); + db.set_json(file_id, "/76561198025127722/dir/var", json!(null)) + .expect_err("Testing panic at trying to set a value in non-existing object/array."); + db.set_json(file_id, "/76561198044316328/groups/d", json!(null)) + .expect_err("Testing panic at trying to set a value at non-numeric index in an array."); + db.set_json(file_id, "/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() { + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let file_id = ("administration", "registered"); + // Removing non-existent value + assert_eq!(db.remove(file_id, "/76561198025127722/something"), None); + // Remove simple value + assert_eq!( + db.remove(file_id, "/76561198025127722/password_hash") + .unwrap(), + json!("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") + ); + assert!(!db.contains_value(file_id, "/76561198025127722/password_hash")); + // Remove complex value (array) + assert_eq!( + db.remove(file_id, "/76561198044316328/groups").unwrap(), + json!(["admin"]) + ); + assert!(!db.contains_value(file_id, "/76561198044316328/groups/0")); + assert!(!db.contains_value(file_id, "/76561198044316328/groups")); + // Remove array elements + assert_eq!( + db.remove(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(), + json!("127.0.0.1") + ); + assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); + assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/1")); + assert_eq!( + db.remove(file_id, "/76561198025127722/allowed_ips/0") + .unwrap(), + json!("192.168.0.100") + ); + assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips")); + assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); +} + #[test] fn test_pop_json_pointer() { assert_eq!( From d66b8ab090e243fb28e37388f889ce49f3756abc Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Fri, 27 Nov 2020 20:50:48 +0700 Subject: [PATCH 05/13] asdf --- Cargo.lock | 113 ++++++++++++ Cargo.toml | 1 + src/database/file.rs | 179 ++++++++++++++++++ src/database/io.rs | 211 ++++++++++++++++++++++ src/database/mod.rs | 410 ++++++++++++------------------------------ src/database/tests.rs | 228 ++++++++++------------- src/main.rs | 7 +- 7 files changed, 721 insertions(+), 428 deletions(-) create mode 100644 src/database/file.rs create mode 100644 src/database/io.rs diff --git a/Cargo.lock b/Cargo.lock index ba42506..ec3ebc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,10 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "avarice" version = "0.1.0" @@ -7,6 +12,7 @@ dependencies = [ "custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -14,6 +20,18 @@ name = "cfg-if" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "custom_error" version = "1.8.0" @@ -24,6 +42,11 @@ name = "itoa" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "log" version = "0.4.11" @@ -32,6 +55,23 @@ dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "ryu" version = "1.0.5" @@ -52,11 +92,84 @@ dependencies = [ "serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "simplelog" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [metadata] +"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" "checksum custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12" "checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +"checksum libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" "checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +"checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +"checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" "checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" "checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" "checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +"checksum simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c" +"checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +"checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +"checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index e04d62e..e4b9c9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +simplelog = "0.8" log = "0.4" serde_json = "1.0" custom_error = "1.8.0" \ No newline at end of file diff --git a/src/database/file.rs b/src/database/file.rs new file mode 100644 index 0000000..a1e010d --- /dev/null +++ b/src/database/file.rs @@ -0,0 +1,179 @@ +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}" } + +enum ValueReference<'a> { + Object(&'a mut serde_json::Map, String), + Array(&'a mut Vec, usize), + Invalid, +} + +#[derive(Debug)] +pub struct File { + contents: serde_json::Value, +} + +impl ToString for File { + fn to_string(&self) -> String { + self.contents.to_string() + } +} + +impl File { + pub fn empty() -> File { + File { + contents: json!({}), + } + } + + pub fn new(file_contents: String) -> Result> { + Ok(File { + contents: serde_json::from_str(&file_contents)?, + }) + } + + pub fn root(&self) -> &serde_json::Value { + &self.contents + } + + pub fn get(&self, pointer: &str) -> Option<&serde_json::Value> { + self.contents.pointer(pointer) + } + + pub fn contains(&self, pointer: &str) -> bool { + self.get(pointer) != None + } + + 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(()) + } + + pub fn remove(&mut self, pointer: &str) -> Option { + 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, + } + } + + 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(()) + } + + 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, + } + } +} + +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); +} diff --git a/src/database/io.rs b/src/database/io.rs new file mode 100644 index 0000000..2b761f0 --- /dev/null +++ b/src/database/io.rs @@ -0,0 +1,211 @@ +use super::*; +use log::{info, error, 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}", +} + +pub fn read(db_path: &path::Path) -> Result, Box> { + 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) +} + +pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box> { + 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(file); + match db.file(group, file) { + Some(file) => fs::write(file_path, file.to_string())?, + _ => (), + } + } + } + Ok(()) +} + +pub fn clear_dir(db_path: &path::Path) -> Result<(), Box> { + info!("Clearing directory {} from database files.", db_path.display()); + 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(()) +} + +fn read_group(group_path: &path::Path) -> Result, Box> { + 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::new(file_contents)?); + } + if files.len() > 0 { + return Ok(Some(Group { + name: get_file_name(group_path), + files, + })); + } + Ok(None) +} + +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 +} + +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() + ); +} diff --git a/src/database/mod.rs b/src/database/mod.rs index 9fde7d8..7e90cd9 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -1,40 +1,30 @@ #[cfg(test)] mod tests; -use log::warn; use serde_json; -use serde_json::json; use std::collections::HashMap; use std::error::Error; -use std::fmt; -use std::fs; use std::path; extern crate custom_error; use custom_error::custom_error; -const JSON_POINTER_SEPARATOR: &str = "/"; +pub mod file; +pub use file::File; + +pub mod io; custom_error! { pub DBError - NotDirectory{path: String} = "Path to the database should point at a directory: {path}", 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}""#, - IncorrectPointer{pointer: String} = "Incorrect pointer is specified: {pointer}", } -enum ValueReference<'a> { - Object(&'a mut serde_json::Map, String), - Array(&'a mut Vec, usize), -} - -type FileID<'a> = (&'a str, &'a str); - pub struct Group { name: String, - files: HashMap, + files: HashMap, } pub struct Database { @@ -42,53 +32,40 @@ pub struct Database { groups: Vec, } -impl fmt::Display for Group { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "[{}]", self.name)?; - for (name, contents) in self.files.iter() { - writeln!(f, r#" File "{}": {}"#, name, contents.to_string())?; - } - Ok(()) - } -} - -impl fmt::Display for Database { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "DB: {}", self.storage_path.display())?; - for g in self.groups.iter() { - writeln!(f, "{}", g)?; - } - Ok(()) - } -} - impl Database { pub fn new(storage_path: &path::Path) -> Result> { - if !storage_path.is_dir() { - return Err(Box::new(DBError::NotDirectory { - path: storage_path.display().to_string(), - })); - } - let mut groups = Vec::new(); - for entry in fs::read_dir(storage_path)? { - let entry = entry?; - let path = entry.path(); - if !path.is_dir() { - warn!( - r#"File {} found where only group directories are supposed to be"#, - path.display() - ); - } else { - let group = load_group(&path)?; - groups.push(group); - } - } Ok(Database { storage_path: storage_path.to_path_buf(), - groups, + groups: io::read(storage_path)?, }) } + pub fn clear(&mut self) { + self.groups = Vec::new(); + } + + pub fn path(&mut self) -> path::PathBuf { + self.storage_path.clone() + } + + pub fn change_path(&mut self, new_path: &path::Path) -> Result<(), Box> { + self.write_copy(new_path)?; + io::clear_dir(&self.storage_path)?; + self.storage_path = new_path.to_path_buf(); + Ok(()) + } + + pub fn write_copy(&self, new_path: &path::Path) -> Result<(), Box> { + io::clear_dir(new_path)?; + io::write(new_path, &self)?; + Ok(()) + } + + pub fn erase(self) -> Result<(), Box> { + io::clear_dir(&self.storage_path)?; + Ok(()) + } + pub fn group_names(&self) -> Vec { self.groups.iter().map(|x| x.name.clone()).collect() } @@ -100,21 +77,13 @@ impl Database { } } - fn group_index(&self, group_name: &str) -> Option { - self.groups.iter().position(|x| x.name.eq(group_name)) - } - pub fn contains_group(&self, group_name: &str) -> bool { - self.group_index(group_name).is_some() + self.group_index(group_name).is_ok() } pub fn create_group(&mut self, group_name: &str) -> Result<(), DBError> { - verify_name(group_name)?; - if self.group_index(group_name).is_some() { - return Err(DBError::GroupAlreadyExists { - group_name: group_name.to_owned(), - }); - } + assert_name_is_valid(group_name)?; + self.assert_no_group(group_name)?; self.groups.push(Group { name: group_name.to_owned(), files: HashMap::new(), @@ -123,147 +92,100 @@ impl Database { } pub fn remove_group(&mut self, group_name: &str) -> Result<(), DBError> { - match self.group_index(group_name) { - Some(index) => self.groups.remove(index), + let index = self.group_index(group_name)?; + self.groups.remove(index); + Ok(()) + } + + 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) + } + + 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.")) + } + + 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(()) + } + + 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, + } + } + + 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, + } + } + + 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(()) + } + + fn group_index(&self, group_name: &str) -> Result { + 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(), }) } - }; - Ok(()) - } - - pub fn contains_file(&self, (group_name, file_name): FileID) -> bool { - match self.group_index(group_name) { - Some(index) => self.groups[index].files.contains_key(&file_name.to_owned()), - _ => false, } } - pub fn create_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { - let group_name = group_name.to_owned(); - let file_name = file_name.to_owned(); - verify_name(&file_name)?; - let group_index = match self.group_index(&group_name) { - Some(index) => index, - _ => return Err(DBError::NoGroup { group_name }), - }; - if self.groups[group_index].files.contains_key(&file_name) { - return Err(DBError::FileAlreadyExists { - group_name, - file_name, - }); - } - self.groups[group_index].files.insert(file_name, json!({})); - Ok(()) + fn group_files_mut(&mut self, group_name: &str) -> Result<&mut HashMap, DBError> { + let group_index = self.group_index(group_name)?; + Ok(&mut (&mut self.groups[group_index]).files) } - pub fn remove_file(&mut self, (group_name, file_name): FileID) -> Result<(), DBError> { - let group_name = group_name.to_owned(); - let file_name = file_name.to_owned(); - let group_index = match self.group_index(&group_name) { - Some(index) => index, - _ => return Err(DBError::NoGroup { group_name }), - }; - if self.groups[group_index].files.remove(&file_name).is_none() { - return Err(DBError::NoFile { - group_name, - file_name, - }); - } - Ok(()) - } - - fn as_json_mut(&mut self, (group_name, file_name): FileID) -> Option<&mut serde_json::Value> { - match self.group_index(group_name) { - Some(index) => self.groups[index].files.get_mut(&file_name.to_owned()), - _ => None, - } - } - - pub fn as_json(&self, (group_name, file_name): FileID) -> Option<&serde_json::Value> { - match self.group_index(group_name) { - Some(index) => self.groups[index].files.get(&file_name.to_owned()), - _ => None, - } - } - - pub fn as_string(&self, file_id: FileID) -> Option { - self.as_json(file_id).map(|x| x.to_string()) - } - - pub fn get_json(&self, file_id: FileID, pointer: &str) -> Option<&serde_json::Value> { - match self.as_json(file_id) { - Some(v) => v.pointer(pointer), - _ => None, - } - } - - pub fn get_string(&self, file_id: FileID, pointer: &str) -> Option { - self.get_json(file_id, pointer).map(|x| x.to_string()) - } - - pub fn contains_value(&self, file_id: FileID, pointer: &str) -> bool { - self.get_json(file_id, pointer) != None - } - - pub fn set_json( - &mut self, - (group_name, file_name): FileID, - pointer: &str, - new_value: serde_json::Value, - ) -> Result<(), Box> { - let file_json = match self.as_json_mut((group_name, file_name)) { - Some(file_json) => file_json, - _ => { - return Err(Box::new(DBError::NoFile { - group_name: group_name.to_owned(), - file_name: file_name.to_owned(), - })) - } - }; - touch(file_json, pointer)?; - match file_json.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(Box::new(DBError::IncorrectPointer { - pointer: pointer.to_owned(), - })); - } - }; - Ok(()) - } - - pub fn remove( - &mut self, - (group_name, file_name): FileID, - pointer: &str, - ) -> Option { - let file_json = match self.as_json_mut((group_name, file_name)) { - Some(file_json) => file_json, - _ => return None, - }; - match pointer_to_reference(file_json, pointer) { - Some(ValueReference::Object(map, variable_name)) => map.remove(&variable_name), - Some(ValueReference::Array(vec, variable_index)) => { - if variable_index < vec.len() { - return Some(vec.remove(variable_index)); - } - None - } - _ => None, - } + fn group_files(&self, group_name: &str) -> Result<&HashMap, DBError> { + let group_index = self.group_index(group_name)?; + Ok(&self.groups[group_index].files) } } -fn verify_name(entity_name: &str) -> Result<(), DBError> { - let is_valid = entity_name +fn is_name_valid(entity_name: &str) -> bool { + entity_name .chars() - .all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()); - if is_valid { + .all(|x| x.is_ascii_alphabetic() || x.is_ascii_digit()) +} + +fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> { + if is_name_valid(entity_name) { return Ok(()); } return Err(DBError::InvalidEntityName { @@ -271,107 +193,7 @@ fn verify_name(entity_name: &str) -> Result<(), DBError> { }); } -fn load_group(group_path: &path::Path) -> Result> { - let mut files = HashMap::new(); - for entry in fs::read_dir(group_path)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - warn!( - r#"Directory {} found where only data files are supposed to be"#, - path.display() - ); - } else { - let file_contents = fs::read_to_string(&path)?; - let file_name = get_file_name(path.as_path()); - let file_contents = serde_json::from_str(&file_contents)?; - files.insert(file_name, file_contents); - } - } - Ok(Group { - name: get_file_name(group_path), - files, - }) -} - -fn touch(json_root: &mut serde_json::Value, pointer: &str) -> (Result<(), Box>) { - if pointer.is_empty() || json_root.pointer_mut(pointer).is_some() { - return Ok(()); - } - match pointer_to_reference(json_root, pointer) { - Some(ValueReference::Object(map, variable_name)) => { - map.insert(variable_name, json!(null)); - } - Some(ValueReference::Array(vec, variable_index)) => { - // Since values at the index does not exist - resize will increase the sizeof the array - vec.resize(variable_index + 1, json!(null)); - } - _ => { - return Err(Box::new(DBError::IncorrectPointer { - pointer: pointer.to_owned(), - })) - } - }; - Ok(()) -} - -fn pointer_to_reference<'a>( - json_root: &'a mut serde_json::Value, - pointer: &str, -) -> Option> { - if pointer.is_empty() { - return None; - } - let container_variable_pair = - pop_json_pointer(pointer).and_then(move |(path, variable_name)| { - match json_root.pointer_mut(&path) { - Some(v) => Some((v, variable_name)), - _ => None, - } - }); - let (json_container, variable_name) = match container_variable_pair { - Some(v) => v, - _ => return None, - }; - match json_container { - serde_json::Value::Object(map) => Some(ValueReference::Object(map, variable_name)), - serde_json::Value::Array(vec) => { - let index: usize = match variable_name.parse() { - Ok(v) => v, - _ => return None, - }; - Some(ValueReference::Array(vec, index)) - } - _ => None, - } -} - -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)) -} - -fn get_file_name(path: &path::Path) -> String { - path.file_stem() - .and_then(|x| x.to_str()) - .unwrap_or_default() - .to_string() -} -// TODO add db saving - // TODO make sure file's main value not being an object won't break anything -// TODO check that file name is appropriate // TODO handle parsing errors differently // TODO add logs +// TODO add docs diff --git a/src/database/tests.rs b/src/database/tests.rs index 037b7bf..9bb2fc3 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -6,6 +6,22 @@ const TEST_DB_PATH: &str = "./fixtures/database"; const NO_DB_MESSAGE: &str = "Can not find/load test database"; +#[test] +fn db_path() { + let mut db = Database::new(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::new(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::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); @@ -85,31 +101,31 @@ fn db_group_create() -> Result<(), Box> { #[test] fn db_file_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success - assert!(db.contains_file(("administration", "registered"))); - assert!(db.contains_file(("game", "general"))); - assert!(db.contains_file(("game", "perks"))); + 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.contains_file(("games", "perks"))); - assert!(!db.contains_file(("random", "rnd_file"))); + 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> { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success - db.create_file(("administration", "secrets"))?; - assert!(db.contains_file(("administration", "secrets"))); - assert_eq!(db.as_string(("administration", "secrets")).unwrap(), "{}"); - assert!(db.contains_file(("game", "perks"))); + 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")) + db.create_file("administration", "secrets") .expect_err("Testing whether creating existing file causes errors."); - db.create_file(("none", "secrets")) + db.create_file("none", "secrets") .expect_err("Testing whether creating existing file in non-existent group causes errors."); - db.create_file(("game", "sec_rets")) + db.create_file("game", "sec_rets") .expect_err("Testing whether creating existing file with invalid name causes errors."); Ok(()) } @@ -118,25 +134,25 @@ fn db_file_create() -> Result<(), Box> { fn db_file_remove() -> Result<(), Box> { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success - db.remove_file(("administration", "registered"))?; - assert!(!db.contains_file(("administration", "registered"))); + db.remove_file("administration", "registered")?; + assert!(!db.contains_file("administration", "registered")); assert!(db.contains_group("administration")); - db.remove_file(("game", "perks"))?; + db.remove_file("game", "perks")?; assert_eq!(db.file_names_in("game").len(), 1); // Failure - db.remove_file(("administration", "registered")) + db.remove_file("administration", "registered") .expect_err("Testing whether removing non-existent files causes errors."); - db.remove_file(("administration", "never")) + db.remove_file("administration", "never") .expect_err("Testing whether removing non-existent files causes errors."); - db.remove_file(("never", "file")) + db.remove_file("never", "file") .expect_err("Testing whether removing non-existent files causes errors."); Ok(()) } #[test] -fn db_json_contents() { +fn file_json_contents() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let registered = db.as_json(("administration", "registered")).unwrap(); + let registered = db.file("administration", "registered").unwrap().root(); let user_map = registered .as_object() .expect("Read value is not an object."); @@ -160,103 +176,73 @@ fn db_json_contents() { } #[test] -fn db_string_contents() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let general = db.as_string(("game", "general")).unwrap(); - assert!(general.contains(r#""dosh_thrown":527624"#)); - assert!(general - .contains(r#""achievements":["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn"]"#)); -} - -#[test] -fn db_json_sub_contents() { +fn file_json_get() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Test empty path - let file_id = ("administration", "registered"); - assert_eq!(db.as_json(file_id), db.get_json(file_id, "")); + let file = db.file("administration", "registered").unwrap(); + assert_eq!(file.root(), file.get("").unwrap()); // Test complex path - let received = db - .get_json(file_id, "/76561198025127722/allowed_ips/1") - .unwrap(); - assert_eq!(received.as_str().unwrap(), "192.168.0.100"); + let expected = file.get("/76561198025127722/allowed_ips/1").unwrap(); + assert_eq!(expected.as_str().unwrap(), "192.168.0.100"); // Test bad paths - assert!(db.get_json(file_id, "/777") == None); - assert!(db.get_json(file_id, "/76561198025127722/allowed_ips/2") == None); + assert!(file.get("/777") == None); + assert!(file.get("/76561198025127722/allowed_ips/2") == None); } #[test] -fn db_string_sub_contents() { +fn file_contains_check() { let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - // Test empty path - let file_id = ("administration", "registered"); - assert_eq!(db.as_string(file_id), db.get_string(file_id, "")); - // Test complex path - let received = db - .get_string(file_id, "/76561198025127722/allowed_ips/0") - .unwrap(); - assert_eq!(received, r#""127.0.0.1""#); -} - -#[test] -fn db_contains_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let registered_id = ("administration", "registered"); - let perks_id = ("game", "perks"); + let registered_file = db.file("administration", "registered").unwrap(); + let perks_file = db.file("game", "perks").unwrap(); // These exist - assert!(db.contains_value(registered_id, "/76561198025127722/password_hash")); - assert!(db.contains_value(registered_id, "/76561198044316328/groups/0")); - assert!(db.contains_value(perks_id, "/76561198025127722/headshots")); - assert!(db.contains_value(perks_id, "/76561198044316328")); + 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!(!db.contains_value(registered_id, "/76561198025127722/password/")); - assert!(!db.contains_value(registered_id, "/76561198044316328/groups/2")); - assert!(!db.contains_value(perks_id, "/76561198025127722/assault_rifle_damage/9067")); - assert!(!db.contains_value(perks_id, "/76561198044316328/headshots")); + 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_set_success() -> Result<(), Box> { +fn db_insert_success() -> Result<(), Box> { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let registered_id = ("administration", "registered"); - let general_id = ("game", "general"); + let registered_file = db.file_mut("administration", "registered").unwrap(); // Modify existing - db.set_json(registered_id, "/76561198025127722/ip_lock", json!(false))?; + registered_file.insert("/76561198025127722/ip_lock", json!(false))?; assert_eq!( - db.get_string(registered_id, "/76561198025127722/ip_lock") - .unwrap(), + registered_file + .get("/76561198025127722/ip_lock") + .unwrap() + .to_string(), "false" ); - db.set_json( - registered_id, - "/76561198044316328/password_hash", - json!({"var":13524}), - )?; + registered_file.insert("/76561198044316328/password_hash", json!({"var":13524}))?; assert_eq!( - db.get_string(registered_id, "/76561198044316328/password_hash") - .unwrap(), + registered_file + .get("/76561198044316328/password_hash") + .unwrap() + .to_string(), r#"{"var":13524}"# ); // Reset whole file - db.set_json(registered_id, "", json!({}))?; - assert_eq!(db.as_json(registered_id).unwrap().to_string(), "{}"); + registered_file.insert("", json!({}))?; + assert_eq!(registered_file.root().to_string(), "{}"); // Add new values - db.set_json( - registered_id, - "/new_var", - json!([42, {"word":"life"}, null]), - )?; + registered_file.insert("/new_var", json!([42, {"word":"life"}, null]))?; assert_eq!( - db.as_json(registered_id).unwrap().to_string(), + registered_file.root().to_string(), r#"{"new_var":[42,{"word":"life"},null]}"# ); - db.set_json( - general_id, - "/76561198025127722/achievements/5", - json!("kf:bugged"), - )?; + let general_file = db.file_mut("game", "general").unwrap(); + general_file.insert("/76561198025127722/achievements/5", json!("kf:bugged"))?; assert_eq!( - db.get_string(general_id, "/76561198025127722/achievements") - .unwrap(), + general_file + .get("/76561198025127722/achievements") + .unwrap() + .to_string(), r#"["kf:LabCleaner","kf:ChickenFarmer","scrn:playedscrn",null,null,"kf:bugged"]"# ); Ok(()) @@ -265,67 +251,45 @@ fn db_set_success() -> Result<(), Box> { #[test] fn db_set_failure() { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let file_id = ("administration", "registered"); - let imaginary_file_id = ("general", "everything"); - db.set_json(imaginary_file_id, "", json!(null)) - .expect_err("Testing panic at missing file."); - db.set_json(file_id, "/76561198025127722/dir/var", json!(null)) + 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."); - db.set_json(file_id, "/76561198044316328/groups/d", json!(null)) + file.insert("/76561198044316328/groups/d", json!(null)) .expect_err("Testing panic at trying to set a value at non-numeric index in an array."); - db.set_json(file_id, "/76561198044316328/groups/-1", json!(null)) + 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() { +fn db_remove_value() { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); - let file_id = ("administration", "registered"); + let file = db.file_mut("administration", "registered").unwrap(); // Removing non-existent value - assert_eq!(db.remove(file_id, "/76561198025127722/something"), None); + assert_eq!(file.remove("/76561198025127722/something"), None); // Remove simple value assert_eq!( - db.remove(file_id, "/76561198025127722/password_hash") - .unwrap(), + file.remove("/76561198025127722/password_hash").unwrap(), json!("fce798e0804dfb217f929bdba26745024f37f6b6ba7406f3775176e20dd5089d") ); - assert!(!db.contains_value(file_id, "/76561198025127722/password_hash")); + assert!(!file.contains("/76561198025127722/password_hash")); // Remove complex value (array) assert_eq!( - db.remove(file_id, "/76561198044316328/groups").unwrap(), + file.remove("/76561198044316328/groups").unwrap(), json!(["admin"]) ); - assert!(!db.contains_value(file_id, "/76561198044316328/groups/0")); - assert!(!db.contains_value(file_id, "/76561198044316328/groups")); + assert!(!file.contains("/76561198044316328/groups/0")); + assert!(!file.contains("/76561198044316328/groups")); // Remove array elements assert_eq!( - db.remove(file_id, "/76561198025127722/allowed_ips/0") - .unwrap(), + file.remove("/76561198025127722/allowed_ips/0").unwrap(), json!("127.0.0.1") ); - assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); - assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/1")); + assert!(file.contains("/76561198025127722/allowed_ips/0")); + assert!(!file.contains("/76561198025127722/allowed_ips/1")); assert_eq!( - db.remove(file_id, "/76561198025127722/allowed_ips/0") - .unwrap(), + file.remove("/76561198025127722/allowed_ips/0").unwrap(), json!("192.168.0.100") ); - assert!(db.contains_value(file_id, "/76561198025127722/allowed_ips")); - assert!(!db.contains_value(file_id, "/76561198025127722/allowed_ips/0")); -} - -#[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); + assert!(file.contains("/76561198025127722/allowed_ips")); + assert!(!file.contains("/76561198025127722/allowed_ips/0")); } diff --git a/src/main.rs b/src/main.rs index 5a5bd11..bba9c73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,12 +2,15 @@ use std::env; use std::path::Path; mod database; +use simplelog::{Config, LevelFilter, SimpleLogger}; + fn main() { + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); let args: Vec = env::args().collect(); let filename = &args[1]; let db = database::Database::new(Path::new(filename)); - match db { + /*match db { Ok(db) => print!("{}", db), Err(error) => println!("OH NO: {}", error), - } + }*/ } From d97fc6cd4286512740240dd2cbc82e38c2ca5f71 Mon Sep 17 00:00:00 2001 From: g Date: Fri, 27 Nov 2020 22:52:14 +0700 Subject: [PATCH 06/13] add missing newline --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e4b9c9e..f9c8363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,4 @@ edition = "2018" simplelog = "0.8" log = "0.4" serde_json = "1.0" -custom_error = "1.8.0" \ No newline at end of file +custom_error = "1.8.0" From eb8cd516b24ac9a65f51e41dd2b96689ff16a4d6 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 28 Nov 2020 03:25:57 +0700 Subject: [PATCH 07/13] Add file tests --- Cargo.lock | 130 ++++++++++++++++++ Cargo.toml | 1 + .../database/administration/registered.json | 13 ++ fixtures/database/game/general.json | 12 ++ fixtures/database/game/perks.json | 12 ++ src/database/io.rs | 25 +++- src/database/mod.rs | 5 + src/database/tests.rs | 84 +++++++++++ 8 files changed, 276 insertions(+), 6 deletions(-) create mode 100644 fixtures/database/administration/registered.json create mode 100644 fixtures/database/game/general.json create mode 100644 fixtures/database/game/perks.json diff --git a/Cargo.lock b/Cargo.lock index ec3ebc1..d521002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12,9 +12,15 @@ dependencies = [ "custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", + "serial_test 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "cfg-if" version = "0.1.10" @@ -32,6 +38,14 @@ dependencies = [ "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "custom_error" version = "1.8.0" @@ -42,11 +56,24 @@ name = "itoa" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "libc" version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "log" version = "0.4.11" @@ -72,11 +99,59 @@ dependencies = [ "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot_core 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)", + "redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "ryu" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "serde" version = "1.0.117" @@ -92,6 +167,26 @@ dependencies = [ "serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "serial_test" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "serial_test_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serial_test_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "simplelog" version = "0.8.0" @@ -102,6 +197,21 @@ dependencies = [ "termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "smallvec" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "termcolor" version = "1.1.2" @@ -120,6 +230,11 @@ dependencies = [ "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -154,20 +269,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +"checksum bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" "checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" "checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" "checksum custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12" "checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +"checksum lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" "checksum libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +"checksum lock_api 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" "checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" "checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" "checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +"checksum parking_lot 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +"checksum parking_lot_core 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +"checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +"checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +"checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" "checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +"checksum scopeguard 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" "checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" "checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +"checksum serial_test 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fef5f7c7434b2f2c598adc6f9494648a1e41274a75c0ba4056f680ae0c117fd6" +"checksum serial_test_derive 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d08338d8024b227c62bd68a12c7c9883f5c66780abaef15c550dc56f46ee6515" "checksum simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c" +"checksum smallvec 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7acad6f34eb9e8a259d3283d1e8c1d34d7415943d4895f65cc73813c7396fc85" +"checksum syn 1.0.52 (registry+https://github.com/rust-lang/crates.io-index)" = "6c1e438504729046a5cfae47f97c30d6d083c7d91d94603efdae3477fc070d4c" "checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" "checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +"checksum unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" "checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" "checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" diff --git a/Cargo.toml b/Cargo.toml index f9c8363..6dfe7a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ simplelog = "0.8" log = "0.4" serde_json = "1.0" custom_error = "1.8.0" +serial_test = "0.4" diff --git a/fixtures/database/administration/registered.json b/fixtures/database/administration/registered.json new file mode 100644 index 0000000..aad23a4 --- /dev/null +++ b/fixtures/database/administration/registered.json @@ -0,0 +1,13 @@ +{ + "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" + } +} diff --git a/fixtures/database/game/general.json b/fixtures/database/game/general.json new file mode 100644 index 0000000..24cc980 --- /dev/null +++ b/fixtures/database/game/general.json @@ -0,0 +1,12 @@ +{ + "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"] + } +} diff --git a/fixtures/database/game/perks.json b/fixtures/database/game/perks.json new file mode 100644 index 0000000..a35c51d --- /dev/null +++ b/fixtures/database/game/perks.json @@ -0,0 +1,12 @@ +{ + "76561198025127722": { + "headshots": 582, + "assault_rifle_damage": 9067, + "stalker_kills": 143 + }, + "76561198044316328": { + "explosive_damage": 19674, + "shotgun_damage": 3835, + "welded_amount": 1 + } +} diff --git a/src/database/io.rs b/src/database/io.rs index 2b761f0..62decb1 100644 --- a/src/database/io.rs +++ b/src/database/io.rs @@ -1,5 +1,5 @@ use super::*; -use log::{info, error, warn}; +use log::{error, info, warn}; use std::collections::HashMap; use std::error::Error; use std::fs; @@ -9,7 +9,7 @@ use std::path::Path; extern crate custom_error; use custom_error::custom_error; -const JSON_EXTENSION: &str = "JSON"; +const JSON_EXTENSION: &str = "json"; custom_error! { pub IOError NotDirectory{path: String} = "Path to the database should point at a directory: {path}", @@ -17,7 +17,10 @@ custom_error! { pub IOError pub fn read(db_path: &path::Path) -> Result, Box> { if !db_path.is_dir() { - error!("Loading database from a non-directory {} was attempted.", db_path.display()); + error!( + "Loading database from a non-directory {} was attempted.", + db_path.display() + ); return Err(Box::new(IOError::NotDirectory { path: db_path.display().to_string(), })); @@ -44,7 +47,10 @@ pub fn read(db_path: &path::Path) -> Result, Box> { pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box> { if db_path.exists() && !db_path.is_dir() { - error!("Cannot write database into a non-directory {}", db_path.display()); + error!( + "Cannot write database into a non-directory {}", + db_path.display() + ); return Err(Box::new(IOError::NotDirectory { path: db_path.display().to_string(), })); @@ -56,7 +62,7 @@ pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box> fs::create_dir(group_path.clone())?; } for file in db.file_names_in(group).iter() { - let file_path = group_path.join(file); + let file_path = group_path.join(format!("{}.{}", file, JSON_EXTENSION)); match db.file(group, file) { Some(file) => fs::write(file_path, file.to_string())?, _ => (), @@ -67,7 +73,14 @@ pub fn write(db_path: &path::Path, db: &Database) -> Result<(), Box> } pub fn clear_dir(db_path: &path::Path) -> Result<(), Box> { - info!("Clearing directory {} from database files.", db_path.display()); + 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, diff --git a/src/database/mod.rs b/src/database/mod.rs index 7e90cd9..08e1b2d 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -61,6 +61,11 @@ impl Database { Ok(()) } + pub fn save(&self) -> Result<(), Box> { + self.write_copy(&self.storage_path)?; + Ok(()) + } + pub fn erase(self) -> Result<(), Box> { io::clear_dir(&self.storage_path)?; Ok(()) diff --git a/src/database/tests.rs b/src/database/tests.rs index 9bb2fc3..3dccc60 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -1,11 +1,38 @@ use super::*; use serde_json::json; +use std::fs; use std::path; +use serial_test::serial; + const TEST_DB_PATH: &str = "./fixtures/database"; +const TEST_DB_COPY_PATH: &str = "./fixtures/copy"; +const TEST_DB_MOVED_COPY_PATH: &str = "./fixtures/copy_moved"; const NO_DB_MESSAGE: &str = "Can not find/load test database"; +struct TestCleanup; + +impl Drop for TestCleanup { + fn drop(&mut self) { + clear_test_db(); + } +} + +fn prepare_db_copy() -> TestCleanup { + clear_test_db(); + let original_db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + original_db + .write_copy(path::Path::new(TEST_DB_COPY_PATH)) + .expect("Should be able to create a new copy of the fixture database."); + TestCleanup +} + +fn clear_test_db() { + let _ = fs::remove_dir_all(TEST_DB_COPY_PATH); + let _ = fs::remove_dir_all(TEST_DB_MOVED_COPY_PATH); +} + #[test] fn db_path() { let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); @@ -293,3 +320,60 @@ fn db_remove_value() { assert!(file.contains("/76561198025127722/allowed_ips")); assert!(!file.contains("/76561198025127722/allowed_ips/0")); } + +#[test] +#[serial] +fn db_save() { + let _cleanup = prepare_db_copy(); + // Change something up and save + let mut db = Database::new(path::Path::new(TEST_DB_COPY_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::new(path::Path::new(TEST_DB_COPY_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] +#[serial] +fn db_change_path() { + let _cleanup = prepare_db_copy(); + // Change something up and move + let mut db = Database::new(path::Path::new(TEST_DB_COPY_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_COPY_PATH)) + .expect("Should be able to change database's path."); + assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); + assert!(path::Path::new(TEST_DB_MOVED_COPY_PATH).exists()); + // Reload and check the changes + let db = Database::new(path::Path::new(TEST_DB_MOVED_COPY_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] +#[serial] +fn db_erase() { + let _cleanup = prepare_db_copy(); + let db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + db.erase().expect("Should be able to erase data."); + assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); +} From 64b55538638922e5ffb6641603d086e9f45779ae Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 28 Nov 2020 04:12:50 +0700 Subject: [PATCH 08/13] Document database's mod.rs --- src/database/mod.rs | 92 +++++++++++++++++++++++++++++++++++-------- src/database/tests.rs | 44 ++++++++++----------- 2 files changed, 97 insertions(+), 39 deletions(-) diff --git a/src/database/mod.rs b/src/database/mod.rs index 08e1b2d..659120b 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -19,35 +19,68 @@ custom_error! { pub DBError 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}""#, -} - -pub struct Group { - name: String, - files: HashMap, + 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, } +/// 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, +} + impl Database { - pub fn new(storage_path: &path::Path) -> Result> { + /// 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> { 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> { self.write_copy(new_path)?; io::clear_dir(&self.storage_path)?; @@ -55,26 +88,36 @@ impl Database { Ok(()) } + /// Erases database's data on disk. + pub fn erase(self) -> Result<(), Box> { + 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> { 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> { self.write_copy(&self.storage_path)?; Ok(()) } - pub fn erase(self) -> Result<(), Box> { - io::clear_dir(&self.storage_path)?; - Ok(()) - } - + /// Returns names of all the groups in the database. pub fn group_names(&self) -> Vec { 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 { match self.groups.iter().find(|x| x.name.eq(group_name)) { Some(group) => group.files.keys().map(|x| x.clone()).collect(), @@ -82,10 +125,13 @@ impl Database { } } + /// 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)?; @@ -96,18 +142,23 @@ impl Database { 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)?; @@ -124,6 +175,8 @@ impl Database { .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)? @@ -138,6 +191,8 @@ impl Database { 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()), @@ -145,6 +200,8 @@ impl Database { } } + /// 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()), @@ -152,6 +209,7 @@ impl Database { } } + /// 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 { @@ -161,6 +219,7 @@ impl Database { Ok(()) } + /// Returns current index of the specified group in`groups` vector. fn group_index(&self, group_name: &str) -> Result { match self.groups.iter().position(|x| x.name.eq(group_name)) { Some(index) => Ok(index), @@ -172,6 +231,8 @@ impl Database { } } + // 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, DBError> { let group_index = self.group_index(group_name)?; Ok(&mut (&mut self.groups[group_index]).files) @@ -183,12 +244,14 @@ impl Database { } } +/// 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(()); @@ -197,8 +260,3 @@ fn assert_name_is_valid(entity_name: &str) -> Result<(), DBError> { entity_name: entity_name.to_owned(), }); } - -// TODO make sure file's main value not being an object won't break anything -// TODO handle parsing errors differently -// TODO add logs -// TODO add docs diff --git a/src/database/tests.rs b/src/database/tests.rs index 3dccc60..974dc99 100644 --- a/src/database/tests.rs +++ b/src/database/tests.rs @@ -21,7 +21,7 @@ impl Drop for TestCleanup { fn prepare_db_copy() -> TestCleanup { clear_test_db(); - let original_db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let original_db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); original_db .write_copy(path::Path::new(TEST_DB_COPY_PATH)) .expect("Should be able to create a new copy of the fixture database."); @@ -35,13 +35,13 @@ fn clear_test_db() { #[test] fn db_path() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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")); @@ -51,7 +51,7 @@ fn db_clear() { #[test] fn db_group_names() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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())); @@ -61,7 +61,7 @@ fn db_group_names() { #[test] fn db_file_names() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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"); @@ -74,7 +74,7 @@ fn db_file_names() { #[test] fn db_group_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); assert!(db.contains_group("game")); assert!(db.contains_group("administration")); @@ -85,7 +85,7 @@ fn db_group_check() { #[test] fn db_group_remove() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); // Success db.remove_group("administration")?; db.remove_group("game")?; @@ -104,7 +104,7 @@ fn db_group_remove() -> Result<(), Box> { #[test] fn db_group_create() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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")); @@ -128,7 +128,7 @@ fn db_group_create() -> Result<(), Box> { #[test] fn db_file_check() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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()); @@ -141,7 +141,7 @@ fn db_file_check() { #[test] fn db_file_create() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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(), "{}"); @@ -159,7 +159,7 @@ fn db_file_create() -> Result<(), Box> { #[test] fn db_file_remove() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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")); @@ -178,7 +178,7 @@ fn db_file_remove() -> Result<(), Box> { #[test] fn file_json_contents() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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() @@ -204,7 +204,7 @@ fn file_json_contents() { #[test] fn file_json_get() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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()); @@ -218,7 +218,7 @@ fn file_json_get() { #[test] fn file_contains_check() { - let db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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 @@ -235,7 +235,7 @@ fn file_contains_check() { #[test] fn db_insert_success() -> Result<(), Box> { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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))?; @@ -277,7 +277,7 @@ fn db_insert_success() -> Result<(), Box> { #[test] fn db_set_failure() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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."); @@ -289,7 +289,7 @@ fn db_set_failure() { #[test] fn db_remove_value() { - let mut db = Database::new(path::Path::new(TEST_DB_PATH)).expect(NO_DB_MESSAGE); + 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); @@ -326,13 +326,13 @@ fn db_remove_value() { fn db_save() { let _cleanup = prepare_db_copy(); // Change something up and save - let mut db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_COPY_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::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_COPY_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); @@ -345,7 +345,7 @@ fn db_save() { fn db_change_path() { let _cleanup = prepare_db_copy(); // Change something up and move - let mut db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let mut db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); db.remove_group("administration") .expect(r#"Should be able to remove "administration" group"#); db.file_mut("game", "perks") @@ -357,7 +357,7 @@ fn db_change_path() { assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); assert!(path::Path::new(TEST_DB_MOVED_COPY_PATH).exists()); // Reload and check the changes - let db = Database::new(path::Path::new(TEST_DB_MOVED_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_MOVED_COPY_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); @@ -373,7 +373,7 @@ fn db_change_path() { #[serial] fn db_erase() { let _cleanup = prepare_db_copy(); - let db = Database::new(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); + let db = Database::load(path::Path::new(TEST_DB_COPY_PATH)).expect(NO_DB_MESSAGE); db.erase().expect("Should be able to erase data."); assert!(!path::Path::new(TEST_DB_COPY_PATH).exists()); } From 2701729dcf25b8aac9256ee6cf9c5ae49f927005 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 28 Nov 2020 04:42:26 +0700 Subject: [PATCH 09/13] Document database's file.rs --- src/database/file.rs | 43 ++++++++++++++++++++++++++++++++++++++++++- src/database/io.rs | 2 +- src/database/mod.rs | 3 ++- 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/database/file.rs b/src/database/file.rs index a1e010d..ccc0038 100644 --- a/src/database/file.rs +++ b/src/database/file.rs @@ -9,14 +9,28 @@ 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` and name of referred value. +/// +/// For values inside JSON arrays it stores array's +/// `Vec` and referred index. +/// +/// `Invalid` can be used to return a failed state. enum ValueReference<'a> { Object(&'a mut serde_json::Map, String), Array(&'a mut Vec, 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, } @@ -27,30 +41,49 @@ impl ToString for File { } impl File { + /// Creates an empty file that will contain an empty JSON object. pub fn empty() -> File { File { contents: json!({}), } } - pub fn new(file_contents: String) -> Result> { + /// Loads JSON value from the specified file. + pub fn load(file_contents: String) -> Result> { 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, @@ -70,6 +103,8 @@ impl File { Ok(()) } + /// Removes (and returns) value specified by theJSON pointer. + /// If it did not exist - returns `None`. pub fn remove(&mut self, pointer: &str) -> Option { match self.pointer_to_reference(pointer) { ValueReference::Object(map, variable_name) => map.remove(&variable_name), @@ -83,6 +118,10 @@ impl File { } } + /// 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() { @@ -108,6 +147,7 @@ impl File { 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; @@ -145,6 +185,7 @@ impl File { } } +// 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) { diff --git a/src/database/io.rs b/src/database/io.rs index 62decb1..c5a2f6f 100644 --- a/src/database/io.rs +++ b/src/database/io.rs @@ -121,7 +121,7 @@ fn read_group(group_path: &path::Path) -> Result, Box> } let file_name = get_file_name(path.as_path()); let file_contents = fs::read_to_string(&path)?; - files.insert(file_name, File::new(file_contents)?); + files.insert(file_name, File::load(file_contents)?); } if files.len() > 0 { return Ok(Some(Group { diff --git a/src/database/mod.rs b/src/database/mod.rs index 659120b..72b52b7 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -17,7 +17,8 @@ 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}""#, + 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}""#, From 2823504e84ca47a17b9c4bbd9dc3ab2687dc41f2 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 28 Nov 2020 04:43:36 +0700 Subject: [PATCH 10/13] Call cargo fmt --- src/database/file.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/database/file.rs b/src/database/file.rs index ccc0038..4856c98 100644 --- a/src/database/file.rs +++ b/src/database/file.rs @@ -12,13 +12,13 @@ custom_error! { pub IncorrectPointer{pointer: String} = "Incorrect pointer is sp /// 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` and name of referred value. -/// +/// /// For values inside JSON arrays it stores array's /// `Vec` and referred index. -/// +/// /// `Invalid` can be used to return a failed state. enum ValueReference<'a> { Object(&'a mut serde_json::Map, String), @@ -74,13 +74,13 @@ impl File { /// 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]`. @@ -120,7 +120,7 @@ impl File { /// 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 From 7c802e7056cbca09bceedef5433620cb29680cae Mon Sep 17 00:00:00 2001 From: g Date: Sat, 28 Nov 2020 12:18:54 +0700 Subject: [PATCH 11/13] Remove unneded parens --- src/database/file.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/file.rs b/src/database/file.rs index 4856c98..d1d9390 100644 --- a/src/database/file.rs +++ b/src/database/file.rs @@ -122,7 +122,7 @@ impl File { /// Can only be done if parent container already exists. /// /// For specifics refer to `insert()` method. - fn touch(&mut self, pointer: &str) -> (Result<(), IncorrectPointer>) { + 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(()); From 229840ac685d46c75f93f0e8cedc7b13dc466f00 Mon Sep 17 00:00:00 2001 From: g Date: Sat, 28 Nov 2020 12:27:17 +0700 Subject: [PATCH 12/13] Fix compile error --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index bba9c73..0c27ddf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ fn main() { let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); let args: Vec = env::args().collect(); let filename = &args[1]; - let db = database::Database::new(Path::new(filename)); + let db = database::Database::load(Path::new(filename)); /*match db { Ok(db) => print!("{}", db), Err(error) => println!("OH NO: {}", error), From 5602ee7c0aeb8b42f9fffbccb57e11c0b2c61a15 Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sat, 28 Nov 2020 23:35:27 +0700 Subject: [PATCH 13/13] Implement database feature A simple JSON-based database that works through loading full self-copy into the memory. Should be more than enough for the needs of kf modding. --- Cargo.lock | 169 ++++++++ Cargo.toml | 4 + .../database/administration/registered.json | 13 + fixtures/database/game/general.json | 12 + fixtures/database/game/perks.json | 12 + src/database/file.rs | 220 +++++++++++ src/database/io.rs | 240 +++++++++++ src/database/mod.rs | 263 ++++++++++++ src/database/tests.rs | 374 ++++++++++++++++++ src/main.rs | 15 +- 10 files changed, 1316 insertions(+), 6 deletions(-) create mode 100644 fixtures/database/administration/registered.json create mode 100644 fixtures/database/game/general.json create mode 100644 fixtures/database/game/perks.json create mode 100644 src/database/file.rs create mode 100644 src/database/io.rs create mode 100644 src/database/mod.rs create mode 100644 src/database/tests.rs diff --git a/Cargo.lock b/Cargo.lock index b0eb9bf..ec3ebc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,175 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "avarice" version = "0.1.0" +dependencies = [ + "custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)", + "simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", +] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)", + "num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "custom_error" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "log" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "1.0.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "simplelog" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)", + "wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +"checksum cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +"checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +"checksum custom_error 1.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "51ac5e99a7fea3ee8a03fa4721a47e2efd3fbb38358fc61192a54d4c6f866c12" +"checksum itoa 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +"checksum libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" +"checksum log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +"checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +"checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +"checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" +"checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" +"checksum simplelog 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2b2736f58087298a448859961d3f4a0850b832e72619d75adc69da7993c2cd3c" +"checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +"checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +"checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index e8facc0..f9c8363 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +simplelog = "0.8" +log = "0.4" +serde_json = "1.0" +custom_error = "1.8.0" diff --git a/fixtures/database/administration/registered.json b/fixtures/database/administration/registered.json new file mode 100644 index 0000000..aad23a4 --- /dev/null +++ b/fixtures/database/administration/registered.json @@ -0,0 +1,13 @@ +{ + "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" + } +} diff --git a/fixtures/database/game/general.json b/fixtures/database/game/general.json new file mode 100644 index 0000000..24cc980 --- /dev/null +++ b/fixtures/database/game/general.json @@ -0,0 +1,12 @@ +{ + "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"] + } +} diff --git a/fixtures/database/game/perks.json b/fixtures/database/game/perks.json new file mode 100644 index 0000000..a35c51d --- /dev/null +++ b/fixtures/database/game/perks.json @@ -0,0 +1,12 @@ +{ + "76561198025127722": { + "headshots": 582, + "assault_rifle_damage": 9067, + "stalker_kills": 143 + }, + "76561198044316328": { + "explosive_damage": 19674, + "shotgun_damage": 3835, + "welded_amount": 1 + } +} diff --git a/src/database/file.rs b/src/database/file.rs new file mode 100644 index 0000000..d1d9390 --- /dev/null +++ b/src/database/file.rs @@ -0,0 +1,220 @@ +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` and name of referred value. +/// +/// For values inside JSON arrays it stores array's +/// `Vec` and referred index. +/// +/// `Invalid` can be used to return a failed state. +enum ValueReference<'a> { + Object(&'a mut serde_json::Map, String), + Array(&'a mut Vec, 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> { + 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 { + 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); +} diff --git a/src/database/io.rs b/src/database/io.rs new file mode 100644 index 0000000..9cbb679 --- /dev/null +++ b/src/database/io.rs @@ -0,0 +1,240 @@ +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, Box> { + 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> { + 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> { + 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, Box> { + 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() + ); +} diff --git a/src/database/mod.rs b/src/database/mod.rs new file mode 100644 index 0000000..72b52b7 --- /dev/null +++ b/src/database/mod.rs @@ -0,0 +1,263 @@ +#[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, +} + +/// 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, +} + +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> { + 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> { + 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> { + 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> { + 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> { + self.write_copy(&self.storage_path)?; + Ok(()) + } + + /// Returns names of all the groups in the database. + pub fn group_names(&self) -> Vec { + 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 { + 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 { + 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, 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, 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(), + }); +} diff --git a/src/database/tests.rs b/src/database/tests.rs new file mode 100644 index 0000000..d47f73a --- /dev/null +++ b/src/database/tests.rs @@ -0,0 +1,374 @@ +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> { + 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> { + 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> { + 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> { + 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> { + 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()); +} diff --git a/src/main.rs b/src/main.rs index 8f664c9..0c27ddf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,16 @@ use std::env; use std::path::Path; -mod unreal_config; +mod database; + +use simplelog::{Config, LevelFilter, SimpleLogger}; fn main() { + let _ = SimpleLogger::init(LevelFilter::Info, Config::default()); let args: Vec = env::args().collect(); let filename = &args[1]; - let config = unreal_config::load_file(Path::new(filename)); - match config { - Ok(config) => print!("{}", config), - _ => (), - } + let db = database::Database::load(Path::new(filename)); + /*match db { + Ok(db) => print!("{}", db), + Err(error) => println!("OH NO: {}", error), + }*/ }