#![allow(clippy::result_large_err)]
mod alignment_util;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use qlexpress::check_options::CheckOptions;
use qlexpress::exception::error_codes;
use qlexpress::exception::{QLException, QLExceptionKind};
use qlexpress::init_options::InitOptions;
use qlexpress::ql_options::QLOptions;
use qlexpress::runtime::native_object::NativeObject;
use qlexpress::runtime::value::DataValue;
use qlexpress::security::ql_security_strategy::QLSecurityStrategy;
use qlexpress::Express4Runner;
struct HostDesk;
impl NativeObject for HostDesk {
fn get_field(&self, name: &str) -> Option<DataValue> {
match name {
"book" | "book1" | "getBook1" => Some(DataValue::Str("Thinking in Rust".into())),
"book2" | "getBook2" => Some(DataValue::Str("Effective Rust".into())),
_ => None,
}
}
fn call_method(&mut self, name: &str, _args: &[DataValue]) -> Result<DataValue, QLException> {
match name {
"bookCount" => Ok(DataValue::Int(1)),
"getBook1" => Ok(DataValue::Str("Thinking in Rust".into())),
"getBook2" => Ok(DataValue::Str("Effective Rust".into())),
_ => Err(QLException::for_test(
QLExceptionKind::Runtime,
"method not found",
error_codes::METHOD_NOT_FOUND,
)),
}
}
fn native_type_name(&self) -> &str {
"com.example.HostDesk"
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
fn host_context() -> HashMap<String, DataValue> {
HashMap::from([(
"desk".to_string(),
DataValue::Object(Rc::new(RefCell::new(HostDesk))),
)])
}
#[test]
fn open_strategy_allows_builtin_method() {
let runner = Express4Runner::with_init_options(
InitOptions::builder()
.security_strategy(QLSecurityStrategy::open())
.build(),
);
let result = runner
.execute(
"'hello'.length()",
HashMap::new(),
&QLOptions::builder().build(),
)
.unwrap()
.into_result();
assert_eq!(result, DataValue::Int(5));
}
#[test]
fn isolation_blocks_native_object_field_and_method() {
let runner = Express4Runner::with_init_options(
InitOptions::builder()
.security_strategy(QLSecurityStrategy::isolation())
.build(),
);
let options = QLOptions::builder().build();
let field_error = runner
.execute("desk.book", host_context(), &options)
.expect_err("isolation must hide host fields");
assert_eq!(field_error.error_code(), error_codes::FIELD_NOT_FOUND);
let method_error = runner
.execute("desk.bookCount()", host_context(), &options)
.expect_err("isolation must hide host methods");
assert_eq!(method_error.error_code(), error_codes::METHOD_NOT_FOUND);
}
#[test]
fn runner_load_field_host_api_skips_script_security() {
let runner = Express4Runner::with_init_options(
InitOptions::builder()
.security_strategy(QLSecurityStrategy::isolation())
.build(),
);
let desk = DataValue::Object(Rc::new(RefCell::new(HostDesk)));
let loaded = runner
.load_field(&desk, "book")
.expect("host API mirrors Java skipSecurity=true");
assert_eq!(loaded.get(), DataValue::Str("Thinking in Rust".into()));
}
#[test]
fn white_list_allows_listed_members_only() {
use qlexpress::aparser::import_manager::QLImport;
use qlexpress::default_class_supplier::DefaultClassSupplier;
use qlexpress::runtime::native_type::NativeType;
use qlexpress::security::ql_security_strategy::NativeMember;
use std::collections::HashSet;
use std::rc::Rc;
let mut calc = NativeType::named("com.example.Calc");
calc.static_methods.insert(
"mul".to_string(),
Rc::new(|_, args| match args {
[DataValue::Int(a), DataValue::Int(b)] => Ok(DataValue::Int(a * b)),
_ => Ok(DataValue::Null),
}),
);
calc.static_methods.insert(
"add".to_string(),
Rc::new(|_, args| match args {
[DataValue::Int(a), DataValue::Int(b)] => Ok(DataValue::Int(a + b)),
_ => Ok(DataValue::Null),
}),
);
let mut allowed = HashSet::new();
allowed.insert(NativeMember::new("com.example.Calc", "mul"));
let mut supplier = DefaultClassSupplier::instance();
supplier.register("com.example.Calc");
let mut runner = Express4Runner::with_init_options(
InitOptions::builder()
.class_supplier(Rc::new(supplier))
.add_default_import(vec![QLImport::import_cls("com.example.Calc")])
.security_strategy(QLSecurityStrategy::white_list(allowed))
.build(),
);
runner.register_native_type(calc);
let r = runner
.execute(
"Calc.mul(3, 4)",
HashMap::new(),
&QLOptions::builder().build(),
)
.unwrap()
.into_result();
assert_eq!(r, DataValue::Int(12));
let r2 = runner.execute(
"Calc.add(1, 2)",
HashMap::new(),
&QLOptions::builder().build(),
);
assert!(r2.is_err());
}
#[test]
fn black_list_blocks_listed_members() {
use qlexpress::aparser::import_manager::QLImport;
use qlexpress::default_class_supplier::DefaultClassSupplier;
use qlexpress::runtime::native_type::NativeType;
use qlexpress::security::ql_security_strategy::NativeMember;
use std::collections::HashSet;
use std::rc::Rc;
let mut calc = NativeType::named("com.example.Calc");
calc.static_methods.insert(
"mul".to_string(),
Rc::new(|_, args| match args {
[DataValue::Int(a), DataValue::Int(b)] => Ok(DataValue::Int(a * b)),
_ => Ok(DataValue::Null),
}),
);
let mut blocked = HashSet::new();
blocked.insert(NativeMember::new("com.example.Calc", "mul"));
let mut supplier = DefaultClassSupplier::instance();
supplier.register("com.example.Calc");
let mut runner = Express4Runner::with_init_options(
InitOptions::builder()
.class_supplier(Rc::new(supplier))
.add_default_import(vec![QLImport::import_cls("com.example.Calc")])
.security_strategy(QLSecurityStrategy::black_list(blocked))
.build(),
);
runner.register_native_type(calc);
let r = runner.execute(
"Calc.mul(6, 7)",
HashMap::new(),
&QLOptions::builder().build(),
);
assert!(r.is_err());
}
fn desk_runner(strategy: QLSecurityStrategy) -> Express4Runner {
use qlexpress::runtime::native_type::NativeType;
let mut runner = Express4Runner::with_init_options(
InitOptions::builder().security_strategy(strategy).build(),
);
let mut desk_type = NativeType::named("com.example.HostDesk");
for (method, value) in [
("getBook1", "Thinking in Rust"),
("getBook2", "Effective Rust"),
] {
let field_value = value.to_string();
desk_type.fields.insert(
method.to_string(),
Rc::new(move |_bean| Some(DataValue::string(field_value.clone()))),
);
desk_type.field_aliases.insert(
method.to_string(),
vec![method.trim_start_matches("get").to_lowercase()],
);
let method_value = value.to_string();
desk_type.methods.insert(
method.to_string(),
Rc::new(move |_bean, args| {
assert!(args.is_empty());
Ok(DataValue::string(method_value.clone()))
}),
);
}
runner.register_native_type(desk_type);
runner
}
#[test]
fn java_express4_runner_security_strategy_test() {
use qlexpress::security::ql_security_strategy::NativeMember;
use std::collections::HashSet;
let isolation = desk_runner(QLSecurityStrategy::isolation());
assert_eq!(
isolation
.execute("desk.book1", host_context(), &QLOptions::default())
.expect_err("isolation field")
.error_code(),
error_codes::FIELD_NOT_FOUND
);
assert_eq!(
isolation
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect_err("isolation method")
.error_code(),
error_codes::METHOD_NOT_FOUND
);
let get_book2 = NativeMember::new("com.example.HostDesk", "getBook2");
let black = desk_runner(QLSecurityStrategy::black_list(HashSet::from([
get_book2.clone()
])));
assert_eq!(
black
.execute("desk.book2", host_context(), &QLOptions::default())
.expect_err("blacklisted getter property")
.error_code(),
error_codes::FIELD_NOT_FOUND
);
assert_eq!(
black
.execute("desk.book1", host_context(), &QLOptions::default())
.expect("non-blacklisted field")
.result(),
&DataValue::Str("Thinking in Rust".into())
);
let white = desk_runner(QLSecurityStrategy::white_list(HashSet::from([get_book2])));
assert_eq!(
white
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect("whitelisted method")
.result(),
&DataValue::Str("Effective Rust".into())
);
assert_eq!(
white
.execute("desk.getBook1()", host_context(), &QLOptions::default())
.expect_err("non-whitelisted method")
.error_code(),
error_codes::METHOD_NOT_FOUND
);
let open = desk_runner(QLSecurityStrategy::open());
assert_eq!(
open.execute("desk.book1", host_context(), &QLOptions::default())
.expect("open field")
.result(),
&DataValue::Str("Thinking in Rust".into())
);
assert_eq!(
open.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect("open method")
.result(),
&DataValue::Str("Effective Rust".into())
);
}
#[test]
fn custom_security_strategy_is_extensible_and_observes_shared_state() {
use qlexpress::security::ql_security_strategy::NativeMember;
use std::collections::HashSet;
let allowed = Rc::new(RefCell::new(HashSet::new()));
let captured = Rc::clone(&allowed);
let runner = desk_runner(QLSecurityStrategy::custom(move |member| {
captured.borrow().contains(member)
}));
let get_book2 = NativeMember::new("com.example.HostDesk", "getBook2");
assert_eq!(
runner
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect_err("custom strategy initially denies the member")
.error_code(),
error_codes::METHOD_NOT_FOUND
);
allowed.borrow_mut().insert(get_book2);
assert_eq!(
runner
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect("existing runner must observe the updated custom policy")
.result(),
&DataValue::Str("Effective Rust".into())
);
}
#[test]
fn shared_white_list_observes_backing_set_mutation_after_runner_creation() {
use qlexpress::security::ql_security_strategy::NativeMember;
use std::collections::HashSet;
let allowed = Rc::new(RefCell::new(HashSet::new()));
let runner = desk_runner(QLSecurityStrategy::shared_white_list(Rc::clone(&allowed)));
let get_book2 = NativeMember::new("com.example.HostDesk", "getBook2");
assert_eq!(
runner
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect_err("shared whitelist initially denies the member")
.error_code(),
error_codes::METHOD_NOT_FOUND
);
allowed.borrow_mut().insert(get_book2.clone());
assert_eq!(
runner
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect("existing runner must observe the updated whitelist")
.result(),
&DataValue::Str("Effective Rust".into())
);
allowed.borrow_mut().remove(&get_book2);
assert_eq!(
runner
.execute("desk.getBook2()", host_context(), &QLOptions::default())
.expect_err("removing the member must revoke later calls")
.error_code(),
error_codes::METHOD_NOT_FOUND
);
}
#[test]
fn check_rejects_disallowed_operator() {
let runner = Express4Runner::new();
let opts = CheckOptions::builder().build(); assert!(runner.check("a + b", &opts).is_ok());
let result = std::panic::catch_unwind(|| {
qlexpress::operator::operator_check_strategy::OperatorCheckStrategy::default()
});
assert!(result.is_ok());
}