mirror of
https://github.com/loot/loot-condition-interpreter.git
synced 2026-07-27 14:16:09 -07:00
Cache Function evaluation results, and use cached results
This commit is contained in:
+34
-2
@@ -193,7 +193,13 @@ fn evaluate_version(
|
|||||||
|
|
||||||
impl Function {
|
impl Function {
|
||||||
pub fn eval(&self, state: &State) -> Result<bool, Error> {
|
pub fn eval(&self, state: &State) -> Result<bool, Error> {
|
||||||
match *self {
|
if let Ok(reader) = state.condition_cache.read() {
|
||||||
|
if let Some(cached_result) = reader.get(self) {
|
||||||
|
return Ok(*cached_result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match *self {
|
||||||
Function::FilePath(ref f) => evaluate_file_path(state, f),
|
Function::FilePath(ref f) => evaluate_file_path(state, f),
|
||||||
Function::FileRegex(ref p, ref r) => evaluate_file_regex(state, p, r),
|
Function::FileRegex(ref p, ref r) => evaluate_file_regex(state, p, r),
|
||||||
Function::ActivePath(ref p) => evaluate_active_path(state, p),
|
Function::ActivePath(ref p) => evaluate_active_path(state, p),
|
||||||
@@ -202,15 +208,23 @@ impl Function {
|
|||||||
Function::ManyActive(ref r) => evaluate_many_active(state, r),
|
Function::ManyActive(ref r) => evaluate_many_active(state, r),
|
||||||
Function::Checksum(ref path, ref crc) => evaluate_checksum(state, path, *crc),
|
Function::Checksum(ref path, ref crc) => evaluate_checksum(state, path, *crc),
|
||||||
Function::Version(ref p, ref v, ref c) => evaluate_version(state, p, v, *c),
|
Function::Version(ref p, ref v, ref c) => evaluate_version(state, p, v, *c),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(function_result) = result {
|
||||||
|
if let Ok(mut writer) = state.condition_cache.write() {
|
||||||
|
writer.insert(self.clone(), function_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
use std::fs::{copy, create_dir};
|
use std::fs::{copy, create_dir, remove_file};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
|
|
||||||
use regex::RegexBuilder;
|
use regex::RegexBuilder;
|
||||||
@@ -261,6 +275,7 @@ mod tests {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(p, v)| (p.to_lowercase(), v.to_string()))
|
.map(|(p, v)| (p.to_lowercase(), v.to_string()))
|
||||||
.collect(),
|
.collect(),
|
||||||
|
condition_cache: RwLock::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,6 +588,23 @@ mod tests {
|
|||||||
assert!(function.eval(&state).unwrap());
|
assert!(function.eval(&state).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_eval_should_cache_results_and_use_cached_results() {
|
||||||
|
let tmp_dir = tempdir().unwrap();
|
||||||
|
let data_path = tmp_dir.path().join("Data");
|
||||||
|
let state = state(data_path);
|
||||||
|
|
||||||
|
copy(Path::new("Cargo.toml"), &state.data_path.join("Cargo.toml")).unwrap();
|
||||||
|
|
||||||
|
let function = Function::FilePath(PathBuf::from("Cargo.toml"));
|
||||||
|
|
||||||
|
assert!(function.eval(&state).unwrap());
|
||||||
|
|
||||||
|
remove_file(&state.data_path.join("Cargo.toml")).unwrap();
|
||||||
|
|
||||||
|
assert!(function.eval(&state).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
|
fn function_version_eval_should_be_true_if_the_path_does_not_exist_and_comparator_is_ne() {
|
||||||
let function =
|
let function =
|
||||||
|
|||||||
+289
-1
@@ -1,4 +1,6 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
use std::mem::discriminant;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
@@ -7,7 +9,7 @@ use unicase::eq;
|
|||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod parse;
|
pub mod parse;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum ComparisonOperator {
|
pub enum ComparisonOperator {
|
||||||
Equal,
|
Equal,
|
||||||
NotEqual,
|
NotEqual,
|
||||||
@@ -86,6 +88,45 @@ impl PartialEq for Function {
|
|||||||
|
|
||||||
impl Eq for Function {}
|
impl Eq for Function {}
|
||||||
|
|
||||||
|
impl Hash for Function {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
use Function::*;
|
||||||
|
match self {
|
||||||
|
FilePath(p) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
FileRegex(p, r) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
r.as_str().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
ActivePath(p) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
ActiveRegex(r) => {
|
||||||
|
r.as_str().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
Many(p, r) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
r.as_str().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
ManyActive(r) => {
|
||||||
|
r.as_str().to_lowercase().hash(state);
|
||||||
|
}
|
||||||
|
Checksum(p, c) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
c.hash(state);
|
||||||
|
}
|
||||||
|
Version(p, v, c) => {
|
||||||
|
p.to_string_lossy().to_lowercase().hash(state);
|
||||||
|
v.to_lowercase().hash(state);
|
||||||
|
c.hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
discriminant(self).hash(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -94,6 +135,9 @@ mod tests {
|
|||||||
Regex::new(string).unwrap()
|
Regex::new(string).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod fmt {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_fmt_for_file_path_should_format_correctly() {
|
fn function_fmt_for_file_path_should_format_correctly() {
|
||||||
let function = Function::FilePath("subdir/Blank.esm".into());
|
let function = Function::FilePath("subdir/Blank.esm".into());
|
||||||
@@ -159,6 +203,10 @@ mod tests {
|
|||||||
&format!("{}", function)
|
&format!("{}", function)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod eq {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn function_eq_for_file_path_should_check_pathbuf() {
|
fn function_eq_for_file_path_should_check_pathbuf() {
|
||||||
@@ -372,3 +420,243 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod hash {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
|
||||||
|
fn hash(function: Function) -> u64 {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
function.hash(&mut hasher);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_path_should_hash_pathbuf() {
|
||||||
|
let function1 = Function::FilePath("Blank.esm".into());
|
||||||
|
let function2 = Function::FilePath("Blank.esm".into());
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::FilePath("Blank.esm".into());
|
||||||
|
let function2 = Function::FilePath("Blank.esp".into());
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_path_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::FilePath("Blank.esm".into());
|
||||||
|
let function2 = Function::FilePath("blank.esm".into());
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_regex_should_hash_pathbuf_and_regex() {
|
||||||
|
let function1 = Function::FileRegex("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::FileRegex("subdir".into(), regex(".*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::FileRegex("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::FileRegex("other".into(), regex(".*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::FileRegex("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::FileRegex("subdir".into(), regex("Blank.*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_regex_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::FileRegex("Subdir".into(), regex("Blank.*"));
|
||||||
|
let function2 = Function::FileRegex("subdir".into(), regex("blank.*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_active_path_should_hash_pathbuf() {
|
||||||
|
let function1 = Function::ActivePath("Blank.esm".into());
|
||||||
|
let function2 = Function::ActivePath("Blank.esm".into());
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::ActivePath("Blank.esm".into());
|
||||||
|
let function2 = Function::ActivePath("Blank.esp".into());
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_active_path_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::ActivePath("Blank.esm".into());
|
||||||
|
let function2 = Function::ActivePath("blank.esm".into());
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_path_and_active_path_should_not_have_equal_hashes() {
|
||||||
|
let function1 = Function::FilePath("Blank.esm".into());
|
||||||
|
let function2 = Function::ActivePath("Blank.esm".into());
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_active_regex_should_hash_pathbuf_and_regex() {
|
||||||
|
let function1 = Function::ActiveRegex(regex(".*"));
|
||||||
|
let function2 = Function::ActiveRegex(regex(".*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::ActiveRegex(regex(".*"));
|
||||||
|
let function2 = Function::ActiveRegex(regex("Blank.*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_active_regex_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::ActiveRegex(regex("Blank.*"));
|
||||||
|
let function2 = Function::ActiveRegex(regex("blank.*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_many_should_hash_pathbuf_and_regex() {
|
||||||
|
let function1 = Function::Many("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::Many("subdir".into(), regex(".*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::Many("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::Many("other".into(), regex(".*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::Many("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::Many("subdir".into(), regex("Blank.*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_many_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::Many("Subdir".into(), regex("Blank.*"));
|
||||||
|
let function2 = Function::Many("subdir".into(), regex("blank.*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_file_regex_and_many_should_not_have_equal_hashes() {
|
||||||
|
let function1 = Function::FileRegex("subdir".into(), regex(".*"));
|
||||||
|
let function2 = Function::Many("subdir".into(), regex(".*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_many_active_should_hash_pathbuf_and_regex() {
|
||||||
|
let function1 = Function::ManyActive(regex(".*"));
|
||||||
|
let function2 = Function::ManyActive(regex(".*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::ManyActive(regex(".*"));
|
||||||
|
let function2 = Function::ManyActive(regex("Blank.*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_many_active_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::ManyActive(regex("Blank.*"));
|
||||||
|
let function2 = Function::ManyActive(regex("blank.*"));
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_active_regex_and_many_active_should_not_have_equal_hashes() {
|
||||||
|
let function1 = Function::ActiveRegex(regex(".*"));
|
||||||
|
let function2 = Function::ManyActive(regex(".*"));
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_checksum_should_hash_pathbuf_and_regex() {
|
||||||
|
let function1 = Function::Checksum("subdir".into(), 1);
|
||||||
|
let function2 = Function::Checksum("subdir".into(), 1);
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::Checksum("subdir".into(), 1);
|
||||||
|
let function2 = Function::Checksum("other".into(), 1);
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 = Function::Checksum("subdir".into(), 1);
|
||||||
|
let function2 = Function::Checksum("subdir".into(), 2);
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_checksum_should_be_case_insensitive() {
|
||||||
|
let function1 = Function::Checksum("Blank.esm".into(), 1);
|
||||||
|
let function2 = Function::Checksum("Blank.esm".into(), 1);
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_version_should_hash_pathbuf_and_version_and_comparator() {
|
||||||
|
let function1 =
|
||||||
|
Function::Version("Blank.esm".into(), "1.2a".into(), ComparisonOperator::Equal);
|
||||||
|
let function2 =
|
||||||
|
Function::Version("Blank.esm".into(), "1.2a".into(), ComparisonOperator::Equal);
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 =
|
||||||
|
Function::Version("Blank.esm".into(), "1".into(), ComparisonOperator::Equal);
|
||||||
|
let function2 =
|
||||||
|
Function::Version("Blank.esp".into(), "1".into(), ComparisonOperator::Equal);
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 =
|
||||||
|
Function::Version("Blank.esm".into(), "1".into(), ComparisonOperator::Equal);
|
||||||
|
let function2 =
|
||||||
|
Function::Version("Blank.esm".into(), "2".into(), ComparisonOperator::Equal);
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
|
||||||
|
let function1 =
|
||||||
|
Function::Version("Blank.esm".into(), "1".into(), ComparisonOperator::Equal);
|
||||||
|
let function2 =
|
||||||
|
Function::Version("Blank.esm".into(), "1".into(), ComparisonOperator::NotEqual);
|
||||||
|
|
||||||
|
assert_ne!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn function_hash_version_should_be_case_insensitive() {
|
||||||
|
let function1 =
|
||||||
|
Function::Version("Blank.esm".into(), "1.2a".into(), ComparisonOperator::Equal);
|
||||||
|
let function2 =
|
||||||
|
Function::Version("Blank.esm".into(), "1.2A".into(), ComparisonOperator::Equal);
|
||||||
|
|
||||||
|
assert_eq!(hash(function1), hash(function2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+16
-8
@@ -86,15 +86,22 @@ impl GameType {
|
|||||||
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
game_type: GameType,
|
game_type: GameType,
|
||||||
|
/// Game Data folder path.
|
||||||
data_path: PathBuf,
|
data_path: PathBuf,
|
||||||
|
/// Path to the LOOT executable, used to resolve conditions that use the "LOOT" path.
|
||||||
loot_path: PathBuf,
|
loot_path: PathBuf,
|
||||||
active_plugins: HashSet<String>, // Lowercased plugin filenames.
|
/// Lowercased plugin filenames.
|
||||||
crc_cache: RwLock<HashMap<String, u32>>, // Lowercased paths.
|
active_plugins: HashSet<String>,
|
||||||
plugin_versions: HashMap<String, String>, // Lowercased plugin filenames and their versions as found in description fields.
|
/// Lowercased paths.
|
||||||
|
crc_cache: RwLock<HashMap<String, u32>>,
|
||||||
|
/// Lowercased plugin filenames and their versions as found in description fields.
|
||||||
|
plugin_versions: HashMap<String, String>,
|
||||||
|
/// Conditions that have already been evaluated, and their results.
|
||||||
|
condition_cache: RwLock<HashMap<Function, bool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compound conditions joined by 'or'
|
/// Compound conditions joined by 'or'
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
pub struct Expression(Vec<CompoundCondition>);
|
pub struct Expression(Vec<CompoundCondition>);
|
||||||
|
|
||||||
impl Expression {
|
impl Expression {
|
||||||
@@ -124,8 +131,8 @@ impl fmt::Display for Expression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Conditions joined by 'and'
|
/// Conditions joined by 'and'
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
|
||||||
struct CompoundCondition(Vec<Condition>);
|
struct CompoundCondition(Vec<Condition>);
|
||||||
|
|
||||||
impl CompoundCondition {
|
impl CompoundCondition {
|
||||||
@@ -154,7 +161,7 @@ impl fmt::Display for CompoundCondition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
enum Condition {
|
enum Condition {
|
||||||
Function(Function),
|
Function(Function),
|
||||||
InvertedFunction(Function),
|
InvertedFunction(Function),
|
||||||
@@ -219,6 +226,7 @@ mod tests {
|
|||||||
active_plugins: HashSet::new(),
|
active_plugins: HashSet::new(),
|
||||||
crc_cache: RwLock::default(),
|
crc_cache: RwLock::default(),
|
||||||
plugin_versions: HashMap::default(),
|
plugin_versions: HashMap::default(),
|
||||||
|
condition_cache: RwLock::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user