use crate::emulation::{
memory::HeapObject,
runtime::hook::{Hook, HookContext, HookManager, PreHookResult},
thread::EmulationThread,
EmValue,
};
use crate::metadata::typesystem::CilFlavor;
pub fn register(manager: &mut HookManager) {
manager.register(
Hook::new("System.String.Concat")
.match_name("System", "String", "Concat")
.pre(string_concat_pre),
);
manager.register(
Hook::new("System.String.IsNullOrEmpty")
.match_name("System", "String", "IsNullOrEmpty")
.pre(string_is_null_or_empty_pre),
);
manager.register(
Hook::new("System.String.Join")
.match_name("System", "String", "Join")
.pre(string_join_pre),
);
manager.register(
Hook::new("System.String.Format")
.match_name("System", "String", "Format")
.pre(string_format_pre),
);
manager.register(
Hook::new("System.String.Intern")
.match_name("System", "String", "Intern")
.pre(string_intern_pre),
);
manager.register(
Hook::new("System.String.IsInterned")
.match_name("System", "String", "IsInterned")
.pre(string_is_interned_pre),
);
manager.register(
Hook::new("System.String.get_Length")
.match_name("System", "String", "get_Length")
.pre(string_get_length_pre),
);
manager.register(
Hook::new("System.String.get_Chars")
.match_name("System", "String", "get_Chars")
.pre(string_get_chars_pre),
);
manager.register(
Hook::new("System.String.Substring")
.match_name("System", "String", "Substring")
.pre(string_substring_pre),
);
manager.register(
Hook::new("System.String.ToCharArray")
.match_name("System", "String", "ToCharArray")
.pre(string_to_char_array_pre),
);
manager.register(
Hook::new("System.String.ToUpper")
.match_name("System", "String", "ToUpper")
.pre(string_to_upper_pre),
);
manager.register(
Hook::new("System.String.ToLower")
.match_name("System", "String", "ToLower")
.pre(string_to_lower_pre),
);
manager.register(
Hook::new("System.String.Trim")
.match_name("System", "String", "Trim")
.pre(string_trim_pre),
);
manager.register(
Hook::new("System.String.Replace")
.match_name("System", "String", "Replace")
.pre(string_replace_pre),
);
manager.register(
Hook::new("System.String.Split")
.match_name("System", "String", "Split")
.pre(string_split_pre),
);
manager.register(
Hook::new("System.String.Contains")
.match_name("System", "String", "Contains")
.pre(string_contains_pre),
);
manager.register(
Hook::new("System.String.StartsWith")
.match_name("System", "String", "StartsWith")
.pre(string_starts_with_pre),
);
manager.register(
Hook::new("System.String.EndsWith")
.match_name("System", "String", "EndsWith")
.pre(string_ends_with_pre),
);
manager.register(
Hook::new("System.String.IndexOf")
.match_name("System", "String", "IndexOf")
.pre(string_index_of_pre),
);
manager.register(
Hook::new("System.String.PadLeft")
.match_name("System", "String", "PadLeft")
.pre(string_pad_left_pre),
);
manager.register(
Hook::new("System.String.PadRight")
.match_name("System", "String", "PadRight")
.pre(string_pad_right_pre),
);
}
fn string_concat_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let mut result = String::new();
for arg in ctx.args {
match arg {
EmValue::ObjectRef(href) => {
if let Ok(s) = thread.heap().get_string(*href) {
result.push_str(&s);
}
}
EmValue::Null => {}
EmValue::I32(v) => result.push_str(&v.to_string()),
EmValue::I64(v) => result.push_str(&v.to_string()),
EmValue::Bool(v) => result.push_str(if *v { "True" } else { "False" }),
EmValue::Char(c) => result.push(*c),
_ => result.push_str("[object]"),
}
}
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_get_length_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::I32(0)));
};
match thread.heap().get_string(*href) {
Ok(s) => {
let len = i32::try_from(s.chars().count()).unwrap_or(i32::MAX);
PreHookResult::Bypass(Some(EmValue::I32(len)))
}
Err(_) => PreHookResult::Bypass(Some(EmValue::I32(0))),
}
}
fn string_get_chars_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Char('\0')));
};
let index = ctx
.args
.first()
.map(usize::try_from)
.and_then(Result::ok)
.unwrap_or(0);
match thread.heap().get_string(*href) {
Ok(s) => match s.chars().nth(index) {
Some(c) => PreHookResult::Bypass(Some(EmValue::Char(c))),
None => PreHookResult::Bypass(Some(EmValue::Char('\0'))),
},
Err(_) => PreHookResult::Bypass(Some(EmValue::Char('\0'))),
}
}
fn string_substring_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let start = ctx
.args
.first()
.map(usize::try_from)
.and_then(Result::ok)
.unwrap_or(0);
let substring: String = if ctx.args.len() > 1 {
let length = usize::try_from(&ctx.args[1]).unwrap_or(0);
s.chars().skip(start).take(length).collect()
} else {
s.chars().skip(start).collect()
};
match thread.heap_mut().alloc_string(&substring) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_to_char_array_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let chars: Vec<EmValue> = s.chars().map(EmValue::Char).collect();
match thread
.heap_mut()
.alloc_array_with_values(CilFlavor::Char, chars)
{
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_to_upper_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
match thread.heap().get_string(*href) {
Ok(s) => {
let upper = s.to_uppercase();
match thread.heap_mut().alloc_string(&upper) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_to_lower_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
match thread.heap().get_string(*href) {
Ok(s) => {
let lower = s.to_lowercase();
match thread.heap_mut().alloc_string(&lower) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_trim_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
match thread.heap().get_string(*href) {
Ok(s) => {
let trimmed = s.trim().to_string();
match thread.heap_mut().alloc_string(&trimmed) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_replace_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
if ctx.args.len() < 2 {
return PreHookResult::Bypass(Some(EmValue::ObjectRef(*href)));
}
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let result = match (&ctx.args[0], &ctx.args[1]) {
(EmValue::Char(old), EmValue::Char(new)) => s.replace(*old, &new.to_string()),
(EmValue::ObjectRef(old_ref), EmValue::ObjectRef(new_ref)) => {
let old_str = thread
.heap()
.get_string(*old_ref)
.map(|s| s.to_string())
.unwrap_or_default();
let new_str = thread
.heap()
.get_string(*new_ref)
.map(|s| s.to_string())
.unwrap_or_default();
s.replace(&old_str, &new_str)
}
_ => s,
};
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_split_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let separator = match ctx.args.first() {
Some(EmValue::Char(c)) => *c,
Some(EmValue::ObjectRef(href)) => {
if let Ok(HeapObject::Array { elements, .. }) = thread.heap().get(*href) {
elements
.first()
.and_then(|e| {
if let EmValue::Char(c) = e {
Some(*c)
} else {
None
}
})
.unwrap_or(' ')
} else {
' '
}
}
_ => ' ',
};
let parts: Vec<&str> = s.split(separator).collect();
let mut string_refs = Vec::with_capacity(parts.len());
for part in parts {
match thread.heap_mut().alloc_string(part) {
Ok(string_ref) => string_refs.push(EmValue::ObjectRef(string_ref)),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
}
}
match thread
.heap_mut()
.alloc_array_with_values(CilFlavor::String, string_refs)
{
Ok(array_ref) => PreHookResult::Bypass(Some(EmValue::ObjectRef(array_ref))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_contains_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Bool(false)));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Bool(false))),
};
let contains = match ctx.args.first() {
Some(EmValue::Char(c)) => s.contains(*c),
Some(EmValue::ObjectRef(href)) => thread
.heap()
.get_string(*href)
.map(|substr| s.contains(&*substr))
.unwrap_or(false),
_ => false,
};
PreHookResult::Bypass(Some(EmValue::Bool(contains)))
}
fn string_starts_with_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Bool(false)));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Bool(false))),
};
let starts_with = match ctx.args.first() {
Some(EmValue::Char(c)) => s.starts_with(*c),
Some(EmValue::ObjectRef(href)) => thread
.heap()
.get_string(*href)
.map(|prefix| s.starts_with(&*prefix))
.unwrap_or(false),
_ => false,
};
PreHookResult::Bypass(Some(EmValue::Bool(starts_with)))
}
fn string_ends_with_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Bool(false)));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Bool(false))),
};
let ends_with = match ctx.args.first() {
Some(EmValue::Char(c)) => s.ends_with(*c),
Some(EmValue::ObjectRef(href)) => thread
.heap()
.get_string(*href)
.map(|suffix| s.ends_with(&*suffix))
.unwrap_or(false),
_ => false,
};
PreHookResult::Bypass(Some(EmValue::Bool(ends_with)))
}
fn string_index_of_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::I32(-1)));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::I32(-1))),
};
let index = match ctx.args.first() {
Some(EmValue::Char(c)) => s
.find(*c)
.map_or(-1, |i| i32::try_from(i).unwrap_or(i32::MAX)),
Some(EmValue::ObjectRef(href)) => thread
.heap()
.get_string(*href)
.map(|substr| {
s.find(&*substr)
.map_or(-1, |i| i32::try_from(i).unwrap_or(i32::MAX))
})
.unwrap_or(-1),
_ => -1,
};
PreHookResult::Bypass(Some(EmValue::I32(index)))
}
fn string_pad_left_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let total_width = ctx
.args
.first()
.map(usize::try_from)
.and_then(Result::ok)
.unwrap_or(0);
let pad_char = ctx
.args
.get(1)
.and_then(|v| {
if let EmValue::Char(c) = v {
Some(*c)
} else {
None
}
})
.unwrap_or(' ');
let result = if s.len() >= total_width {
s
} else {
let padding: String = std::iter::repeat_n(pad_char, total_width - s.len()).collect();
format!("{padding}{s}")
};
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_pad_right_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
let Some(EmValue::ObjectRef(href)) = ctx.this else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let s = match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let total_width = ctx
.args
.first()
.map(usize::try_from)
.and_then(Result::ok)
.unwrap_or(0);
let pad_char = ctx
.args
.get(1)
.and_then(|v| {
if let EmValue::Char(c) = v {
Some(*c)
} else {
None
}
})
.unwrap_or(' ');
let result = if s.len() >= total_width {
s
} else {
let padding: String = std::iter::repeat_n(pad_char, total_width - s.len()).collect();
format!("{s}{padding}")
};
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_is_null_or_empty_pre(
ctx: &HookContext<'_>,
thread: &mut EmulationThread,
) -> PreHookResult {
let is_null_or_empty = if let Some(EmValue::ObjectRef(href)) = ctx.args.first() {
thread
.heap()
.get_string(*href)
.map(|s| s.is_empty())
.unwrap_or(true)
} else {
true
};
PreHookResult::Bypass(Some(EmValue::Bool(is_null_or_empty)))
}
fn string_join_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.len() < 2 {
return PreHookResult::Bypass(Some(EmValue::Null));
}
let separator = if let EmValue::ObjectRef(href) = &ctx.args[0] {
thread
.heap()
.get_string(*href)
.map(|s| s.to_string())
.unwrap_or_default()
} else {
String::new()
};
let array_ref = match &ctx.args[1] {
EmValue::ObjectRef(r) => *r,
_ => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let Ok(obj) = thread.heap().get(array_ref) else {
return PreHookResult::Bypass(Some(EmValue::Null));
};
let parts: Vec<String> = match obj {
HeapObject::Array { elements, .. } => elements
.iter()
.filter_map(|e| match e {
EmValue::ObjectRef(href) => {
thread.heap().get_string(*href).ok().map(|s| s.to_string())
}
EmValue::Null => Some(String::new()),
_ => None,
})
.collect(),
_ => Vec::new(),
};
let result = parts.join(&separator);
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_format_pre(ctx: &HookContext<'_>, thread: &mut EmulationThread) -> PreHookResult {
if ctx.args.is_empty() {
return PreHookResult::Bypass(Some(EmValue::Null));
}
let format_str = match &ctx.args[0] {
EmValue::ObjectRef(href) => match thread.heap().get_string(*href) {
Ok(s) => s.to_string(),
Err(_) => return PreHookResult::Bypass(Some(EmValue::Null)),
},
_ => return PreHookResult::Bypass(Some(EmValue::Null)),
};
let mut result = format_str;
for (i, arg) in ctx.args.iter().skip(1).enumerate() {
let placeholder = format!("{{{i}}}");
let value_str = match arg {
EmValue::ObjectRef(href) => thread
.heap()
.get_string(*href)
.map_or_else(|_| "[object]".to_string(), |s| s.to_string()),
EmValue::Null => String::new(),
EmValue::I32(v) => v.to_string(),
EmValue::I64(v) => v.to_string(),
EmValue::F32(v) => v.to_string(),
EmValue::F64(v) => v.to_string(),
EmValue::Bool(v) => if *v { "True" } else { "False" }.to_string(),
EmValue::Char(c) => c.to_string(),
_ => "[value]".to_string(),
};
result = result.replace(&placeholder, &value_str);
}
match thread.heap_mut().alloc_string(&result) {
Ok(href) => PreHookResult::Bypass(Some(EmValue::ObjectRef(href))),
Err(_) => PreHookResult::Bypass(Some(EmValue::Null)),
}
}
fn string_intern_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if let Some(s) = ctx.args.first() {
PreHookResult::Bypass(Some(s.clone()))
} else {
PreHookResult::Bypass(Some(EmValue::Null))
}
}
fn string_is_interned_pre(ctx: &HookContext<'_>, _thread: &mut EmulationThread) -> PreHookResult {
if let Some(s) = ctx.args.first() {
PreHookResult::Bypass(Some(s.clone()))
} else {
PreHookResult::Bypass(Some(EmValue::Null))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
emulation::runtime::hook::HookManager,
metadata::{token::Token, typesystem::PointerSize},
test::emulation::create_test_thread,
};
#[test]
fn test_register_hooks() {
let mut manager = HookManager::new();
register(&mut manager);
assert_eq!(manager.len(), 21);
}
#[test]
fn test_string_concat_hook() {
let mut thread = create_test_thread();
let s1 = thread.heap_mut().alloc_string("Hello ").unwrap();
let s2 = thread.heap_mut().alloc_string("World").unwrap();
let args = [EmValue::ObjectRef(s1), EmValue::ObjectRef(s2)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"Concat",
PointerSize::Bit64,
)
.with_args(&args);
let result = string_concat_pre(&ctx, &mut thread);
if let PreHookResult::Bypass(Some(EmValue::ObjectRef(r))) = result {
assert_eq!(&*thread.heap().get_string(r).unwrap(), "Hello World");
} else {
panic!("Expected Bypass with ObjectRef");
}
}
#[test]
fn test_string_length_hook() {
let mut thread = create_test_thread();
let s = thread.heap_mut().alloc_string("Hello").unwrap();
let this = EmValue::ObjectRef(s);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"get_Length",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = string_get_length_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::I32(5)))
));
}
#[test]
fn test_string_to_upper_hook() {
let mut thread = create_test_thread();
let s = thread.heap_mut().alloc_string("hello").unwrap();
let this = EmValue::ObjectRef(s);
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"ToUpper",
PointerSize::Bit64,
)
.with_this(Some(&this));
let result = string_to_upper_pre(&ctx, &mut thread);
if let PreHookResult::Bypass(Some(EmValue::ObjectRef(r))) = result {
assert_eq!(&*thread.heap().get_string(r).unwrap(), "HELLO");
} else {
panic!("Expected Bypass with ObjectRef");
}
}
#[test]
fn test_string_is_null_or_empty_hook() {
let mut thread = create_test_thread();
let empty = thread.heap_mut().alloc_string("").unwrap();
let non_empty = thread.heap_mut().alloc_string("test").unwrap();
let args = [EmValue::Null];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"IsNullOrEmpty",
PointerSize::Bit64,
)
.with_args(&args);
let result = string_is_null_or_empty_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(true)))
));
let args = [EmValue::ObjectRef(empty)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"IsNullOrEmpty",
PointerSize::Bit64,
)
.with_args(&args);
let result = string_is_null_or_empty_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(true)))
));
let args = [EmValue::ObjectRef(non_empty)];
let ctx = HookContext::new(
Token::new(0x0A000001),
"System",
"String",
"IsNullOrEmpty",
PointerSize::Bit64,
)
.with_args(&args);
let result = string_is_null_or_empty_pre(&ctx, &mut thread);
assert!(matches!(
result,
PreHookResult::Bypass(Some(EmValue::Bool(false)))
));
}
}