use crate::Configuration;
use crate::JavaError::{IllegalAccessError, InaccessibleObjectException};
use crate::Result;
use ahash::{AHashMap, AHashSet};
use ristretto_classloader::Value;
use ristretto_classloader::module::{
AccessCheck, ModuleFinder, ModuleFinderChain, ModulePathFinder, ModuleSource,
ResolvedConfiguration, Resolver, SystemModuleFinder,
};
use ristretto_gc::sync::RwLock;
use ristretto_types::ModuleAccess;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, OnceLock};
use tracing::debug;
pub use ristretto_classloader::module::AccessCheckResult;
pub use ristretto_types::DefinedModule;
pub const ALL_UNNAMED: &str = "ALL-UNNAMED";
pub const ALL_MODULES: &str = "ALL";
pub const JAVA_BASE_MODULE: &str = "java.base";
type ResolvedConfigurationCell = tokio::sync::OnceCell<Arc<ResolvedConfiguration>>;
type ResolvedConfigurationCache = RwLock<HashMap<PathBuf, Arc<ResolvedConfigurationCell>>>;
fn resolved_configuration_cache() -> &'static ResolvedConfigurationCache {
static CACHE: OnceLock<ResolvedConfigurationCache> = OnceLock::new();
CACHE.get_or_init(|| RwLock::new(HashMap::new()))
}
fn resolved_configuration_cell(jimage_path: &Path) -> Arc<ResolvedConfigurationCell> {
if let Some(cell) = resolved_configuration_cache().read().get(jimage_path) {
return Arc::clone(cell);
}
let mut cache = resolved_configuration_cache().write();
Arc::clone(
cache
.entry(jimage_path.to_path_buf())
.or_insert_with(|| Arc::new(ResolvedConfigurationCell::new())),
)
}
#[derive(Debug)]
pub struct ModuleSystem {
resolved_configuration: Arc<ResolvedConfiguration>,
exports: RwLock<AHashMap<String, AHashMap<String, AHashSet<String>>>>,
opens: RwLock<AHashMap<String, AHashMap<String, AHashSet<String>>>>,
reads: RwLock<AHashMap<String, AHashSet<String>>>,
modules: RwLock<AHashMap<String, DefinedModule>>,
boot_unnamed_module: RwLock<Option<Value>>,
}
impl ModuleSystem {
pub async fn new(
configuration: &Configuration,
java_home: &Path,
java_major_version: u16,
) -> Result<Self> {
let mut module_system = Self::empty();
let resolver = Self::build_resolver(configuration, &mut module_system);
if java_major_version <= 8 {
debug!("Java 8 or earlier; module system disabled");
module_system.resolved_configuration =
Arc::new(Self::create_fallback_configuration(configuration));
return Ok(module_system);
}
if Self::is_cacheable_classpath_app(configuration) {
let jimage_path = java_home.join("lib").join("modules");
let cell = resolved_configuration_cell(&jimage_path);
module_system.resolved_configuration = Arc::clone(
cell.get_or_init(|| async {
Arc::new(Self::resolve_configuration(configuration, java_home, &resolver).await)
})
.await,
);
return Ok(module_system);
}
module_system.resolved_configuration =
Arc::new(Self::resolve_configuration(configuration, java_home, &resolver).await);
Ok(module_system)
}
async fn resolve_configuration(
configuration: &Configuration,
java_home: &Path,
resolver: &Resolver,
) -> ResolvedConfiguration {
let finder_chain = Self::build_finder_chain(configuration, java_home).await;
if finder_chain.find("java.base").is_none() {
debug!("No system modules found, using fallback configuration");
return Self::create_fallback_configuration(configuration);
}
let root_modules = Self::determine_root_modules(configuration, &finder_chain);
debug!("Resolving modules with roots: {root_modules:?}");
match resolver.resolve(&root_modules, &finder_chain) {
Ok(config) => {
debug!(
"Resolved {} modules with {} packages",
config.len(),
config
.modules()
.map(|m| m.descriptor().packages.len())
.sum::<usize>()
);
config
}
Err(error) => {
debug!("Module resolution failed: {error}, using fallback configuration");
Self::create_fallback_configuration(configuration)
}
}
}
fn is_cacheable_classpath_app(configuration: &Configuration) -> bool {
configuration.module_path().is_empty()
&& configuration.main_module().is_none()
&& configuration.limit_modules().is_empty()
&& configuration.upgrade_module_path().is_empty()
&& configuration.add_modules().is_empty()
&& configuration.add_reads().is_empty()
&& configuration.add_exports().is_empty()
&& configuration.add_opens().is_empty()
&& configuration.patch_modules().is_empty()
}
#[must_use]
pub fn empty() -> Self {
Self {
resolved_configuration: Arc::new(ResolvedConfiguration::empty()),
exports: RwLock::new(AHashMap::default()),
opens: RwLock::new(AHashMap::default()),
reads: RwLock::new(AHashMap::default()),
modules: RwLock::new(AHashMap::default()),
boot_unnamed_module: RwLock::new(None),
}
}
#[must_use]
pub fn resolved_configuration(&self) -> &ResolvedConfiguration {
self.resolved_configuration.as_ref()
}
#[must_use]
pub fn resolved_configuration_arc(&self) -> Arc<ResolvedConfiguration> {
Arc::clone(&self.resolved_configuration)
}
fn build_resolver(configuration: &Configuration, module_system: &mut Self) -> Resolver {
let mut resolver = Resolver::new();
for read in configuration.add_reads() {
resolver.add_read(read.source.clone(), read.target.clone());
module_system.add_read(&read.source, &read.target);
}
for export in configuration.add_exports() {
resolver.add_export(
export.source.clone(),
export.package.clone(),
export.target.clone(),
);
module_system.add_export(&export.source, &export.package, Some(&export.target));
}
for opens in configuration.add_opens() {
resolver.add_opens(
opens.source.clone(),
opens.package.clone(),
opens.target.clone(),
);
module_system.add_opens(&opens.source, &opens.package, Some(&opens.target));
}
if !configuration.limit_modules().is_empty() {
resolver.set_limit_modules(configuration.limit_modules().clone());
}
resolver
}
async fn build_finder_chain(
configuration: &Configuration,
java_home: &Path,
) -> ModuleFinderChain {
let mut finder_chain = ModuleFinderChain::new();
Self::add_upgrade_module_path(&mut finder_chain, configuration).await;
Self::add_system_modules(&mut finder_chain, java_home).await;
Self::add_module_path(&mut finder_chain, configuration).await;
finder_chain
}
async fn add_upgrade_module_path(
finder_chain: &mut ModuleFinderChain,
configuration: &Configuration,
) {
if configuration.upgrade_module_path().is_empty() {
return;
}
let upgrade_paths: Vec<PathBuf> = configuration
.upgrade_module_path()
.iter()
.map(PathBuf::from)
.collect();
match ModulePathFinder::new(&upgrade_paths).await {
Ok(upgrade_finder) => {
debug!(
"Loaded {} modules from upgrade module path",
upgrade_finder.find_all().len()
);
finder_chain.add(Box::new(upgrade_finder));
}
Err(error) => {
debug!("Failed to load upgrade module path: {error}");
}
}
}
async fn add_system_modules(finder_chain: &mut ModuleFinderChain, java_home: &Path) {
let jimage_path = java_home.join("lib").join("modules");
if !jimage_path.exists() {
debug!("No jimage found at {}", jimage_path.display());
return;
}
match SystemModuleFinder::new(&jimage_path).await {
Ok(system_finder) => {
debug!(
"Loaded {} system modules from jimage",
system_finder.find_all().len()
);
finder_chain.add(Box::new(system_finder));
}
Err(error) => {
debug!("Failed to load system modules from jimage: {error}");
}
}
}
async fn add_module_path(finder_chain: &mut ModuleFinderChain, configuration: &Configuration) {
if configuration.module_path().is_empty() {
return;
}
let module_paths: Vec<PathBuf> = configuration
.module_path()
.iter()
.map(PathBuf::from)
.collect();
match ModulePathFinder::new(&module_paths).await {
Ok(module_path_finder) => {
debug!(
"Loaded {} modules from module path",
module_path_finder.find_all().len()
);
finder_chain.add(Box::new(module_path_finder));
}
Err(error) => {
debug!("Failed to load module path: {error}");
}
}
}
fn determine_root_modules(
configuration: &Configuration,
finder_chain: &ModuleFinderChain,
) -> Vec<String> {
let mut root_modules: Vec<String> = Vec::new();
root_modules.push("java.base".to_string());
if let Some(main_module) = configuration.main_module()
&& !root_modules.contains(&main_module.name)
{
root_modules.push(main_module.name.clone());
}
Self::add_modules_from_config(&mut root_modules, configuration, finder_chain);
Self::add_default_modules(&mut root_modules, finder_chain);
root_modules
}
fn add_modules_from_config(
root_modules: &mut Vec<String>,
configuration: &Configuration,
finder_chain: &ModuleFinderChain,
) {
for module in configuration.add_modules() {
if module == "ALL-SYSTEM" {
for reference in finder_chain.find_all() {
if !matches!(
reference.source(),
ModuleSource::System | ModuleSource::Upgrade
) {
continue;
}
let name = reference.name().to_string();
if !root_modules.contains(&name) {
root_modules.push(name);
}
}
} else if module == "ALL-MODULE-PATH" {
for reference in finder_chain.find_all() {
if !matches!(
reference.source(),
ModuleSource::ModulePath | ModuleSource::Automatic
) {
continue;
}
let name = reference.name().to_string();
if !root_modules.contains(&name) {
root_modules.push(name);
}
}
} else if module == "ALL-DEFAULT" {
for reference in finder_chain.find_all() {
let name = reference.name().to_string();
if name.starts_with("jdk.incubator.") {
continue;
}
let has_unqualified_export = reference
.descriptor()
.exports
.iter()
.any(|e| e.targets.is_none());
if has_unqualified_export && !root_modules.contains(&name) {
root_modules.push(name);
}
}
} else if !root_modules.contains(module) {
root_modules.push(module.clone());
}
}
}
fn add_default_modules(root_modules: &mut Vec<String>, finder_chain: &ModuleFinderChain) {
if root_modules.len() > 1 {
return;
}
for reference in finder_chain.find_all() {
let name = reference.name().to_string();
if root_modules.contains(&name) {
continue;
}
if name.starts_with("jdk.incubator.") {
continue;
}
let desc = reference.descriptor();
let has_unqualified_exports = desc.exports.iter().any(|e| e.targets.is_none());
if has_unqualified_exports {
root_modules.push(name);
}
}
}
fn create_fallback_configuration(configuration: &Configuration) -> ResolvedConfiguration {
let add_exports: AHashMap<String, AHashMap<String, AHashSet<String>>> = configuration
.add_exports()
.iter()
.fold(AHashMap::default(), |mut acc, export| {
acc.entry(export.source.clone())
.or_default()
.entry(export.package.clone())
.or_default()
.insert(export.target.clone());
acc
});
let add_opens: AHashMap<String, AHashMap<String, AHashSet<String>>> = configuration
.add_opens()
.iter()
.fold(AHashMap::default(), |mut acc, opens| {
acc.entry(opens.source.clone())
.or_default()
.entry(opens.package.clone())
.or_default()
.insert(opens.target.clone());
acc
});
ResolvedConfiguration::new(
std::collections::BTreeMap::new(),
AHashMap::default(),
add_exports,
add_opens,
)
}
#[must_use]
pub fn check_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> AccessCheckResult {
let from = from_module.unwrap_or(ALL_UNNAMED);
let to = to_module.unwrap_or(ALL_UNNAMED);
let package = Self::package_from_class_name(to_class_name);
let static_checker = AccessCheck::new(&self.resolved_configuration);
let static_result = static_checker.check_access(from, to, package);
if static_result.is_allowed() {
return static_result;
}
self.check_dynamic_access(from, to, package)
}
#[must_use]
pub fn check_reflection_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> AccessCheckResult {
let from = from_module.unwrap_or(ALL_UNNAMED);
let to = to_module.unwrap_or(ALL_UNNAMED);
let package = Self::package_from_class_name(to_class_name);
let static_checker = AccessCheck::new(&self.resolved_configuration);
let static_result = static_checker.check_reflection_access(from, to, package);
if static_result.is_allowed() {
return static_result;
}
self.check_dynamic_reflection_access(from, to, package)
}
pub fn add_export(&self, source_module: &str, package: &str, target_module: Option<&str>) {
let target = target_module.unwrap_or(ALL_UNNAMED);
let mut exports = self.exports.write();
exports
.entry(source_module.to_string())
.or_default()
.entry(package.to_string())
.or_default()
.insert(target.to_string());
}
pub fn add_export_to_all(&self, source_module: &str, package: &str) {
self.add_export(source_module, package, Some("ALL"));
}
pub fn add_export_to_all_unnamed(&self, source_module: &str, package: &str) {
self.add_export(source_module, package, Some(ALL_UNNAMED));
}
#[must_use]
pub fn is_exported(&self, source_module: &str, package: &str, target_module: &str) -> bool {
if self
.resolved_configuration
.exports(source_module, package, target_module)
{
return true;
}
let exports = self.exports.read();
if let Some(module_exports) = exports.get(source_module)
&& let Some(targets) = module_exports.get(package)
{
return targets.contains(target_module)
|| targets.contains("ALL")
|| (target_module == ALL_UNNAMED && targets.contains(ALL_UNNAMED));
}
false
}
pub fn add_opens(&self, source_module: &str, package: &str, target_module: Option<&str>) {
let target = target_module.unwrap_or(ALL_UNNAMED);
let mut opens = self.opens.write();
opens
.entry(source_module.to_string())
.or_default()
.entry(package.to_string())
.or_default()
.insert(target.to_string());
}
pub fn add_opens_to_all(&self, source_module: &str, package: &str) {
self.add_opens(source_module, package, Some("ALL"));
}
pub fn add_opens_to_all_unnamed(&self, source_module: &str, package: &str) {
self.add_opens(source_module, package, Some(ALL_UNNAMED));
}
#[must_use]
pub fn is_opened(&self, source_module: &str, package: &str, target_module: &str) -> bool {
if self.is_module_open(source_module) {
return true;
}
if self
.resolved_configuration
.opens(source_module, package, target_module)
{
return true;
}
let opens = self.opens.read();
if let Some(module_opens) = opens.get(source_module)
&& let Some(targets) = module_opens.get(package)
{
return targets.contains(target_module)
|| targets.contains("ALL")
|| (target_module == ALL_UNNAMED && targets.contains(ALL_UNNAMED));
}
false
}
pub fn add_read(&self, source_module: &str, target_module: &str) {
let mut reads = self.reads.write();
reads
.entry(source_module.to_string())
.or_default()
.insert(target_module.to_string());
}
#[must_use]
pub fn can_read(&self, source_module: &str, target_module: &str) -> bool {
if source_module == target_module {
return true;
}
if target_module == "java.base" {
return true;
}
if self
.resolved_configuration
.reads(source_module, target_module)
{
return true;
}
let reads = self.reads.read();
reads
.get(source_module)
.is_some_and(|targets| targets.contains(target_module))
}
pub fn define_module(&self, module: DefinedModule) {
let mut modules = self.modules.write();
modules.insert(module.name.clone(), module);
}
#[must_use]
pub fn get_module(&self, name: &str) -> Option<DefinedModule> {
let modules = self.modules.read();
modules.get(name).cloned()
}
#[must_use]
pub fn is_module_open(&self, name: &str) -> bool {
let modules = self.modules.read();
modules.get(name).is_some_and(|m| m.is_open)
}
#[must_use]
pub fn all_defined_packages(&self) -> Vec<String> {
let modules = self.modules.read();
let capacity: usize = modules.values().map(|m| m.packages.len()).sum();
let mut packages = Vec::with_capacity(capacity);
for module in modules.values() {
packages.extend(module.packages.iter().cloned());
}
packages
}
#[must_use]
pub fn get_all_exports(&self) -> AHashMap<String, AHashMap<String, AHashSet<String>>> {
self.exports.read().clone()
}
#[must_use]
pub fn get_all_opens(&self) -> AHashMap<String, AHashMap<String, AHashSet<String>>> {
self.opens.read().clone()
}
#[must_use]
pub fn get_all_reads(&self) -> AHashMap<String, AHashSet<String>> {
self.reads.read().clone()
}
#[must_use]
pub fn boot_unnamed_module(&self) -> Option<Value> {
let guard = self.boot_unnamed_module.read();
guard.clone()
}
#[must_use]
pub fn get_module_for_package(&self, package: &str) -> Option<Value> {
let dot_package = package.replace('/', ".");
let modules = self.modules.read();
for module in modules.values() {
if module.packages.contains(&dot_package) {
return module.module_object.clone();
}
}
if let Some(module_name) = self.resolved_configuration.find_module_for_package(package)
&& let Some(module) = modules.get(module_name)
{
return module.module_object.clone();
}
None
}
pub fn set_boot_unnamed_module(&self, module: Value) {
let mut guard = self.boot_unnamed_module.write();
*guard = Some(module);
}
#[must_use]
pub fn check_dynamic_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> AccessCheckResult {
if from_module == to_module {
return AccessCheckResult::Allowed;
}
if from_module == ALL_UNNAMED {
return self.check_unnamed_module_access(to_module, package);
}
if !self.can_read(from_module, to_module) {
return AccessCheckResult::NotReadable;
}
if !self.is_exported(to_module, package, from_module) {
return AccessCheckResult::NotExported;
}
AccessCheckResult::Allowed
}
#[must_use]
pub fn check_dynamic_reflection_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> AccessCheckResult {
if from_module == to_module {
return AccessCheckResult::Allowed;
}
if from_module == ALL_UNNAMED {
return self.check_unnamed_module_reflection(to_module, package);
}
if !self.can_read(from_module, to_module) {
return AccessCheckResult::NotReadable;
}
if !self.is_opened(to_module, package, from_module) {
return AccessCheckResult::NotOpened;
}
AccessCheckResult::Allowed
}
fn check_unnamed_module_access(&self, to_module: &str, package: &str) -> AccessCheckResult {
if self.is_exported(to_module, package, ALL_UNNAMED) {
return AccessCheckResult::Allowed;
}
AccessCheckResult::NotExported
}
fn check_unnamed_module_reflection(&self, to_module: &str, package: &str) -> AccessCheckResult {
if self.is_opened(to_module, package, ALL_UNNAMED) {
return AccessCheckResult::Allowed;
}
AccessCheckResult::NotOpened
}
#[must_use]
pub fn package_from_class_name(class_name: &str) -> &str {
if let Some(last_slash) = class_name.rfind('/') {
&class_name[..last_slash]
} else {
"" }
}
#[must_use]
pub fn illegal_access_error(
from_module: &str,
to_module: &str,
class_name: &str,
result: AccessCheckResult,
) -> String {
let package = Self::package_from_class_name(class_name);
let class_display = class_name.replace('/', ".");
let from_display = if from_module == ALL_UNNAMED {
"unnamed module".to_string()
} else {
format!("module {from_module}")
};
let package_display = package.replace('/', ".");
match result {
AccessCheckResult::Allowed => {
format!("access to {class_display} allowed") }
AccessCheckResult::NotReadable => {
format!(
"{from_display} cannot access class {class_display} \
(in module {to_module}) because {from_display} does not read module {to_module}"
)
}
AccessCheckResult::NotExported => {
format!(
"{from_display} cannot access class {class_display} \
(in module {to_module}) because module {to_module} does not export {package_display} to {from_display}"
)
}
AccessCheckResult::NotOpened => {
format!("module {to_module} does not \"opens {package_display}\" to {from_display}")
}
}
}
pub fn require_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> Result<()> {
let result = self.check_access(from_module, to_module, to_class_name);
if result.is_allowed() {
return Ok(());
}
let from = from_module.unwrap_or(ALL_UNNAMED);
let to = to_module.unwrap_or(ALL_UNNAMED);
let error_msg = Self::illegal_access_error(from, to, to_class_name, result);
Err(IllegalAccessError(error_msg).into())
}
pub fn require_reflection_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> Result<()> {
let result = self.check_reflection_access(from_module, to_module, to_class_name);
if result.is_allowed() {
return Ok(());
}
let from = from_module.unwrap_or(ALL_UNNAMED);
let to = to_module.unwrap_or(ALL_UNNAMED);
let error_msg = Self::illegal_access_error(from, to, to_class_name, result);
Err(InaccessibleObjectException(error_msg).into())
}
}
impl ModuleAccess for ModuleSystem {
fn add_export(&self, source_module: &str, package: &str, target_module: Option<&str>) {
ModuleSystem::add_export(self, source_module, package, target_module);
}
fn add_export_to_all(&self, source_module: &str, package: &str) {
ModuleSystem::add_export_to_all(self, source_module, package);
}
fn add_export_to_all_unnamed(&self, source_module: &str, package: &str) {
ModuleSystem::add_export_to_all_unnamed(self, source_module, package);
}
fn add_opens(&self, source_module: &str, package: &str, target_module: Option<&str>) {
ModuleSystem::add_opens(self, source_module, package, target_module);
}
fn add_opens_to_all(&self, source_module: &str, package: &str) {
ModuleSystem::add_opens_to_all(self, source_module, package);
}
fn add_opens_to_all_unnamed(&self, source_module: &str, package: &str) {
ModuleSystem::add_opens_to_all_unnamed(self, source_module, package);
}
fn add_read(&self, source_module: &str, target_module: &str) {
ModuleSystem::add_read(self, source_module, target_module);
}
fn define_module(&self, module: DefinedModule) {
ModuleSystem::define_module(self, module);
}
fn get_module(&self, name: &str) -> Option<DefinedModule> {
ModuleSystem::get_module(self, name)
}
fn is_module_open(&self, name: &str) -> bool {
ModuleSystem::is_module_open(self, name)
}
fn check_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> AccessCheckResult {
ModuleSystem::check_access(self, from_module, to_module, to_class_name)
}
fn check_reflection_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> AccessCheckResult {
ModuleSystem::check_reflection_access(self, from_module, to_module, to_class_name)
}
fn require_reflection_access(
&self,
from_module: Option<&str>,
to_module: Option<&str>,
to_class_name: &str,
) -> Result<()> {
ModuleSystem::require_reflection_access(self, from_module, to_module, to_class_name)
}
fn set_boot_unnamed_module(&self, module: Value) {
ModuleSystem::set_boot_unnamed_module(self, module);
}
fn boot_unnamed_module(&self) -> Option<Value> {
ModuleSystem::boot_unnamed_module(self)
}
fn get_module_for_package(&self, package: &str) -> Option<Value> {
ModuleSystem::get_module_for_package(self, package)
}
fn resolved_configuration(&self) -> &ResolvedConfiguration {
ModuleSystem::resolved_configuration(self)
}
fn resolved_configuration_arc(&self) -> Arc<ResolvedConfiguration> {
ModuleSystem::resolved_configuration_arc(self)
}
fn all_defined_packages(&self) -> Vec<String> {
ModuleSystem::all_defined_packages(self)
}
}
impl Default for ModuleSystem {
fn default() -> Self {
Self::empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ModuleRead;
use crate::configuration::{ConfigurationBuilder, ModuleExport, ModuleOpens};
#[test]
fn test_new_module_system() {
let module_system = ModuleSystem::empty();
assert!(!module_system.is_exported("java.base", "java/lang", "my.module"));
assert!(!module_system.is_opened("java.base", "java/lang", "my.module"));
assert!(!module_system.can_read("my.module", "java.sql"));
}
#[cfg(not(target_os = "wasi"))]
#[tokio::test]
async fn test_classpath_configuration_is_reused() {
let configuration = ConfigurationBuilder::new().build().unwrap();
let java_home = tempfile::tempdir().unwrap();
let first = ModuleSystem::new(&configuration, java_home.path(), 21)
.await
.unwrap();
let second = ModuleSystem::new(&configuration, java_home.path(), 21)
.await
.unwrap();
assert!(Arc::ptr_eq(
&first.resolved_configuration_arc(),
&second.resolved_configuration_arc()
));
}
#[test]
fn test_add_export() {
let module_system = ModuleSystem::empty();
module_system.add_export("java.base", "java/lang", Some("my.module"));
assert!(module_system.is_exported("java.base", "java/lang", "my.module"));
assert!(!module_system.is_exported("java.base", "java/lang", "other.module"));
}
#[test]
fn test_add_export_to_all() {
let module_system = ModuleSystem::empty();
module_system.add_export_to_all("java.base", "java/lang");
assert!(module_system.is_exported("java.base", "java/lang", "my.module"));
assert!(module_system.is_exported("java.base", "java/lang", "other.module"));
}
#[test]
fn test_add_export_to_all_unnamed() {
let module_system = ModuleSystem::empty();
module_system.add_export_to_all_unnamed("java.base", "java/lang");
assert!(module_system.is_exported("java.base", "java/lang", ALL_UNNAMED));
assert!(!module_system.is_exported("java.base", "java/lang", "my.module"));
}
#[test]
fn test_add_opens() {
let module_system = ModuleSystem::empty();
module_system.add_opens("java.base", "java/lang", Some("my.module"));
assert!(module_system.is_opened("java.base", "java/lang", "my.module"));
assert!(!module_system.is_opened("java.base", "java/lang", "other.module"));
}
#[test]
fn test_add_opens_to_all() {
let module_system = ModuleSystem::empty();
module_system.add_opens_to_all("java.base", "java/lang");
assert!(module_system.is_opened("java.base", "java/lang", "my.module"));
assert!(module_system.is_opened("java.base", "java/lang", "other.module"));
}
#[test]
fn test_add_opens_to_all_unnamed() {
let module_system = ModuleSystem::empty();
module_system.add_opens_to_all_unnamed("java.base", "java/lang");
assert!(module_system.is_opened("java.base", "java/lang", ALL_UNNAMED));
assert!(!module_system.is_opened("java.base", "java/lang", "my.module"));
}
#[test]
fn test_open_module() {
let module_system = ModuleSystem::empty();
let mut open_module = DefinedModule::new("my.open.module".to_string(), true);
open_module.packages.insert("my/pkg".to_string());
module_system.define_module(open_module);
assert!(module_system.is_opened("my.open.module", "my/pkg", "any.module"));
assert!(module_system.is_opened("my.open.module", "other/pkg", "any.module"));
}
#[test]
fn test_add_read() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "java.sql");
assert!(module_system.can_read("my.module", "java.sql"));
assert!(!module_system.can_read("my.module", "java.xml"));
}
#[test]
fn test_can_read_same_module() {
let module_system = ModuleSystem::empty();
assert!(module_system.can_read("my.module", "my.module"));
}
#[test]
fn test_can_read_java_base() {
let module_system = ModuleSystem::empty();
assert!(module_system.can_read("my.module", "java.base"));
}
#[test]
fn test_define_module() {
let module_system = ModuleSystem::empty();
let mut defined_module = DefinedModule::new("my.module".to_string(), false);
defined_module.version = Some("1.0".to_string());
defined_module.location = Some("/path/to/module.jar".to_string());
defined_module.packages.insert("my/pkg".to_string());
module_system.define_module(defined_module);
let retrieved = module_system.get_module("my.module");
assert!(retrieved.is_some());
let retrieved_module = retrieved.unwrap();
assert_eq!(retrieved_module.name, "my.module");
assert!(!retrieved_module.is_open);
assert_eq!(retrieved_module.version, Some("1.0".to_string()));
assert!(retrieved_module.packages.contains("my/pkg"));
}
#[test]
fn test_get_module_not_found() {
let module_system = ModuleSystem::empty();
assert!(module_system.get_module("nonexistent").is_none());
}
#[test]
fn test_get_all_exports() {
let module_system = ModuleSystem::empty();
module_system.add_export("java.base", "java/lang", Some("my.module"));
module_system.add_export("java.sql", "java/sql", Some("my.module"));
let exports = module_system.get_all_exports();
assert_eq!(exports.len(), 2);
assert!(exports.contains_key("java.base"));
assert!(exports.contains_key("java.sql"));
}
#[test]
fn test_get_all_opens() {
let module_system = ModuleSystem::empty();
module_system.add_opens("java.base", "java/lang", Some("my.module"));
let opens = module_system.get_all_opens();
assert_eq!(opens.len(), 1);
}
#[test]
fn test_get_all_reads() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "java.sql");
module_system.add_read("my.module", "java.xml");
let reads = module_system.get_all_reads();
assert_eq!(reads.len(), 1);
assert!(reads.get("my.module").unwrap().contains("java.sql"));
assert!(reads.get("my.module").unwrap().contains("java.xml"));
}
#[test]
fn test_default() {
let module_system = ModuleSystem::default();
assert!(module_system.get_all_exports().is_empty());
}
#[test]
fn test_access_check_result_allowed() {
let result = AccessCheckResult::Allowed;
assert!(result.is_allowed());
assert!(!result.is_denied());
assert!(result.denial_reason().is_none());
assert_eq!(format!("{result}"), "access allowed");
}
#[test]
fn test_access_check_result_not_readable() {
let result = AccessCheckResult::NotReadable;
assert!(!result.is_allowed());
assert!(result.is_denied());
let denial_message = result.denial_reason();
assert!(denial_message.is_some());
assert!(denial_message.unwrap().contains("does not read"));
assert_eq!(format!("{result}"), "module not readable");
}
#[test]
fn test_access_check_result_not_exported() {
let result = AccessCheckResult::NotExported;
assert!(!result.is_allowed());
assert!(result.is_denied());
let denial_message = result.denial_reason();
assert!(denial_message.is_some());
assert!(denial_message.unwrap().contains("not exported"));
assert_eq!(format!("{result}"), "package not exported");
}
#[test]
fn test_access_check_result_not_opened() {
let result = AccessCheckResult::NotOpened;
assert!(!result.is_allowed());
assert!(result.is_denied());
let denial_message = result.denial_reason();
assert!(denial_message.is_some());
assert!(denial_message.unwrap().contains("not opened"));
assert_eq!(format!("{result}"), "package not opened for reflection");
}
#[test]
fn test_check_dynamic_access_same_module() {
let module_system = ModuleSystem::empty();
let result = module_system.check_dynamic_access("my.module", "my.module", "my/pkg");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_access_not_readable() {
let module_system = ModuleSystem::empty();
let result = module_system.check_dynamic_access("my.module", "other.module", "other/pkg");
assert_eq!(result, AccessCheckResult::NotReadable);
}
#[test]
fn test_check_dynamic_access_reads_but_not_exported() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
let result =
module_system.check_dynamic_access("my.module", "other.module", "other/internal");
assert_eq!(result, AccessCheckResult::NotExported);
}
#[test]
fn test_check_dynamic_access_allowed_with_export() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("my.module"));
let result = module_system.check_dynamic_access("my.module", "other.module", "other/api");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_access_java_base_always_readable() {
let module_system = ModuleSystem::empty();
module_system.add_export_to_all("java.base", "java/lang");
let result = module_system.check_dynamic_access("my.module", "java.base", "java/lang");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_access_unnamed_module_exported() {
let module_system = ModuleSystem::empty();
module_system.add_export("java.base", "java/lang", Some(ALL_UNNAMED));
let result = module_system.check_dynamic_access(ALL_UNNAMED, "java.base", "java/lang");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_access_unnamed_module_not_exported() {
let module_system = ModuleSystem::empty();
let result =
module_system.check_dynamic_access(ALL_UNNAMED, "java.base", "java/lang/internal");
assert_eq!(result, AccessCheckResult::NotExported);
}
#[test]
fn test_check_dynamic_reflection_access_same_module() {
let module_system = ModuleSystem::empty();
let result =
module_system.check_dynamic_reflection_access("my.module", "my.module", "my/pkg");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_reflection_access_not_readable() {
let module_system = ModuleSystem::empty();
let result =
module_system.check_dynamic_reflection_access("my.module", "other.module", "other/pkg");
assert_eq!(result, AccessCheckResult::NotReadable);
}
#[test]
fn test_check_dynamic_reflection_access_not_opened() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("my.module"));
let result =
module_system.check_dynamic_reflection_access("my.module", "other.module", "other/api");
assert_eq!(result, AccessCheckResult::NotOpened);
}
#[test]
fn test_check_dynamic_reflection_access_allowed_with_opens() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_opens("other.module", "other/internal", Some("my.module"));
let result = module_system.check_dynamic_reflection_access(
"my.module",
"other.module",
"other/internal",
);
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_dynamic_reflection_access_open_module() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "open.module");
let open_module = DefinedModule::new("open.module".to_string(), true);
module_system.define_module(open_module);
let result =
module_system.check_dynamic_reflection_access("my.module", "open.module", "any/pkg");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_reflection_unnamed_module_opened() {
let module_system = ModuleSystem::empty();
module_system.add_opens("java.base", "java/lang", Some(ALL_UNNAMED));
let result =
module_system.check_dynamic_reflection_access(ALL_UNNAMED, "java.base", "java/lang");
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_check_reflection_unnamed_module_not_opened() {
let module_system = ModuleSystem::empty();
let result =
module_system.check_dynamic_reflection_access(ALL_UNNAMED, "java.base", "java/lang");
assert_eq!(result, AccessCheckResult::NotOpened);
}
#[test]
fn test_package_from_class_name() {
assert_eq!(
ModuleSystem::package_from_class_name("java/lang/String"),
"java/lang"
);
assert_eq!(
ModuleSystem::package_from_class_name("com/example/MyClass"),
"com/example"
);
assert_eq!(ModuleSystem::package_from_class_name("MyClass"), "");
assert_eq!(ModuleSystem::package_from_class_name("a/b/c/D"), "a/b/c");
}
#[test]
fn test_illegal_access_error_not_readable() {
let error_message = ModuleSystem::illegal_access_error(
"my.module",
"other.module",
"other/internal/Secret",
AccessCheckResult::NotReadable,
);
assert!(error_message.contains("my.module"));
assert!(error_message.contains("other.module"));
assert!(error_message.contains("does not read"));
}
#[test]
fn test_illegal_access_error_not_exported() {
let error_message = ModuleSystem::illegal_access_error(
"my.module",
"other.module",
"other/internal/Secret",
AccessCheckResult::NotExported,
);
assert!(error_message.contains("my.module"));
assert!(error_message.contains("other.module"));
assert!(error_message.contains("does not export"));
}
#[test]
fn test_illegal_access_error_not_opened() {
let error_message = ModuleSystem::illegal_access_error(
"my.module",
"other.module",
"other/internal/Secret",
AccessCheckResult::NotOpened,
);
assert!(error_message.contains("does not \"opens"));
}
#[test]
fn test_illegal_access_error_unnamed_module() {
let error_message = ModuleSystem::illegal_access_error(
ALL_UNNAMED,
"java.base",
"java/lang/internal/Secret",
AccessCheckResult::NotExported,
);
assert!(error_message.contains("unnamed module"));
assert!(error_message.contains("java.base"));
}
#[tokio::test]
async fn test_module_system_new_with_add_exports() {
let configuration = ConfigurationBuilder::new()
.add_export(ModuleExport::new("java.base", "java.lang", "my.module"))
.add_export(ModuleExport::new("java.sql", "java.sql", "ALL-UNNAMED"))
.build()
.unwrap();
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await;
assert!(module_system.is_ok());
let module_system = module_system.unwrap();
let add_exports = module_system.resolved_configuration().add_exports();
assert!(add_exports.contains_key("java.base"));
assert!(add_exports.contains_key("java.sql"));
let java_base_exports = add_exports.get("java.base").unwrap();
assert!(
java_base_exports
.get("java.lang")
.unwrap()
.contains("my.module")
);
let java_sql_exports = add_exports.get("java.sql").unwrap();
assert!(
java_sql_exports
.get("java.sql")
.unwrap()
.contains("ALL-UNNAMED")
);
}
#[tokio::test]
async fn test_module_system_new_with_add_opens() {
let configuration = ConfigurationBuilder::new()
.add_opens(ModuleOpens::new(
"java.base",
"java.lang.reflect",
"my.module",
))
.add_opens(ModuleOpens::new("java.base", "sun.reflect", "ALL-UNNAMED"))
.build()
.unwrap();
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await;
assert!(module_system.is_ok());
let module_system = module_system.unwrap();
let add_opens = module_system.resolved_configuration().add_opens();
assert!(add_opens.contains_key("java.base"));
let java_base_opens = add_opens.get("java.base").unwrap();
assert!(
java_base_opens
.get("java.lang.reflect")
.unwrap()
.contains("my.module")
);
assert!(
java_base_opens
.get("sun.reflect")
.unwrap()
.contains("ALL-UNNAMED")
);
}
#[tokio::test]
async fn test_module_system_new_java8_fallback() {
let configuration = ConfigurationBuilder::new().build().unwrap();
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 8).await;
assert!(module_system.is_ok());
let module_system = module_system.unwrap();
assert!(module_system.resolved_configuration().is_empty());
}
#[tokio::test]
async fn test_module_system_check_access() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("my.module"));
let result = module_system.check_access(
Some("my.module"),
Some("other.module"),
"other/api/SomeClass",
);
assert_eq!(result, AccessCheckResult::Allowed);
}
#[tokio::test]
async fn test_module_system_check_reflection_access() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_opens("other.module", "other/internal", Some("my.module"));
let result = module_system.check_reflection_access(
Some("my.module"),
Some("other.module"),
"other/internal/Secret",
);
assert_eq!(result, AccessCheckResult::Allowed);
}
#[test]
fn test_resolved_configuration_getter() {
let module_system = ModuleSystem::empty();
let config = module_system.resolved_configuration();
assert!(config.is_empty());
let first = module_system.resolved_configuration_arc();
let second = module_system.resolved_configuration_arc();
assert!(Arc::ptr_eq(&first, &second));
}
#[tokio::test]
async fn test_create_module_system_with_add_reads() -> Result<()> {
let configuration = ConfigurationBuilder::new()
.add_read(ModuleRead::new("my.module", "java.sql"))
.build()?;
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await?;
assert!(module_system.can_read("my.module", "java.sql"));
assert!(!module_system.can_read("my.module", "java.xml"));
Ok(())
}
#[tokio::test]
async fn test_create_module_system_with_add_exports() -> Result<()> {
let configuration = ConfigurationBuilder::new()
.add_export(ModuleExport::new("java.base", "java.lang", "my.module"))
.build()?;
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await?;
assert!(module_system.is_exported("java.base", "java.lang", "my.module"));
assert!(!module_system.is_exported("java.base", "java.lang", "other.module"));
Ok(())
}
#[tokio::test]
async fn test_create_module_system_with_add_opens() -> Result<()> {
let configuration = ConfigurationBuilder::new()
.add_opens(ModuleOpens::new(
"java.base",
"java.lang.reflect",
"my.module",
))
.build()?;
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await?;
assert!(module_system.is_opened("java.base", "java.lang.reflect", "my.module"));
assert!(!module_system.is_opened("java.base", "java.lang.reflect", "other.module"));
Ok(())
}
#[tokio::test]
async fn test_create_module_system_combined() -> Result<()> {
let configuration = ConfigurationBuilder::new()
.add_read(ModuleRead::new("my.module", "java.sql"))
.add_read(ModuleRead::new("my.module", "java.xml"))
.add_export(ModuleExport::new("java.base", "java.lang", "ALL-UNNAMED"))
.add_opens(ModuleOpens::new(
"java.base",
"java.lang.reflect",
"ALL-UNNAMED",
))
.build()?;
let module_system = ModuleSystem::new(&configuration, &PathBuf::new(), 21).await?;
assert!(module_system.can_read("my.module", "java.sql"));
assert!(module_system.can_read("my.module", "java.xml"));
assert!(module_system.is_exported("java.base", "java.lang", "ALL-UNNAMED"));
assert!(module_system.is_opened("java.base", "java.lang.reflect", "ALL-UNNAMED"));
Ok(())
}
#[test]
fn test_require_access_same_module_success() {
let module_system = ModuleSystem::empty();
let result =
module_system.require_access(Some("my.module"), Some("my.module"), "my/pkg/MyClass");
assert!(result.is_ok());
}
#[test]
fn test_require_access_with_export_success() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("my.module"));
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/api/SomeClass",
);
assert!(result.is_ok());
}
#[test]
fn test_require_access_not_readable_failure() {
let module_system = ModuleSystem::empty();
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/pkg/SomeClass",
);
assert!(result.is_err());
let error = result.unwrap_err();
let error_msg = error.to_string();
assert!(error_msg.contains("does not read"));
}
#[test]
fn test_require_access_not_exported_failure() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/internal/Secret",
);
assert!(result.is_err());
let error = result.unwrap_err();
let error_msg = error.to_string();
assert!(error_msg.contains("does not export"));
}
#[test]
fn test_require_access_unnamed_to_named_module_success() {
let module_system = ModuleSystem::empty();
module_system.add_export_to_all_unnamed("java.base", "java/lang");
let result = module_system.require_access(None, Some("java.base"), "java/lang/String");
assert!(result.is_ok());
}
#[test]
fn test_require_access_unnamed_to_named_module_failure() {
let module_system = ModuleSystem::empty();
let result =
module_system.require_access(None, Some("java.base"), "java/lang/internal/Secret");
assert!(result.is_err());
}
#[test]
fn test_require_access_both_unnamed_success() {
let module_system = ModuleSystem::empty();
let result = module_system.require_access(None, None, "com/example/MyClass");
assert!(result.is_ok());
}
#[test]
fn test_require_access_to_java_base_success() {
let module_system = ModuleSystem::empty();
module_system.add_export_to_all("java.base", "java/lang");
let result =
module_system.require_access(Some("my.module"), Some("java.base"), "java/lang/String");
assert!(result.is_ok());
}
#[test]
fn test_require_access_export_to_all_success() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export_to_all("other.module", "other/public");
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/public/PublicClass",
);
assert!(result.is_ok());
}
#[test]
fn test_require_access_wrong_target_module_failure() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("different.module"));
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/api/SomeClass",
);
assert!(result.is_err());
}
#[test]
fn test_require_access_multiple_packages_partial_export() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export("other.module", "other/api", Some("my.module"));
let result1 = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/api/PublicClass",
);
assert!(result1.is_ok());
let result2 = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/internal/InternalClass",
);
assert!(result2.is_err());
}
#[test]
fn test_require_access_default_package_class() {
let module_system = ModuleSystem::empty();
let result = module_system.require_access(None, None, "DefaultPackageClass");
assert!(result.is_ok());
}
#[test]
fn test_require_access_deeply_nested_package() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_export(
"other.module",
"com/example/deep/nested/api",
Some("my.module"),
);
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"com/example/deep/nested/api/DeepClass",
);
assert!(result.is_ok());
}
#[test]
fn test_require_access_error_message_contains_module_names() {
let module_system = ModuleSystem::empty();
let result = module_system.require_access(
Some("requesting.module"),
Some("target.module"),
"target/pkg/TargetClass",
);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("requesting.module"));
assert!(error_msg.contains("target.module"));
}
#[test]
fn test_require_access_error_message_contains_class_name() {
let module_system = ModuleSystem::empty();
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"com/example/MyClass",
);
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("com.example.MyClass"));
}
#[test]
fn test_require_access_opened_package_requires_export_for_regular_access() {
let module_system = ModuleSystem::empty();
module_system.add_read("my.module", "other.module");
module_system.add_opens("other.module", "other/internal", Some("my.module"));
let result = module_system.require_access(
Some("my.module"),
Some("other.module"),
"other/internal/InternalClass",
);
assert!(result.is_err());
}
#[test]
fn test_require_access_unnamed_module_cannot_normal_access_opened_package() {
let module_system = ModuleSystem::empty();
module_system.add_opens_to_all_unnamed("java.base", "java/lang/internal");
let result =
module_system.require_access(None, Some("java.base"), "java/lang/internal/Unsafe");
assert!(result.is_err());
}
#[test]
fn test_require_reflection_access_unnamed_module_can_access_opened_package() {
let module_system = ModuleSystem::empty();
module_system.add_opens_to_all_unnamed("java.base", "java/lang/internal");
let result = module_system.require_reflection_access(
None,
Some("java.base"),
"java/lang/internal/Unsafe",
);
assert!(result.is_ok());
}
}