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), - } + }*/ }