use crate::module::error::ModuleError;
use crate::module::resolution::ResolvedConfiguration;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AccessCheckResult {
Allowed,
NotReadable,
NotExported,
NotOpened,
}
impl AccessCheckResult {
#[must_use]
pub fn is_allowed(&self) -> bool {
matches!(self, Self::Allowed)
}
#[must_use]
pub fn is_denied(&self) -> bool {
!self.is_allowed()
}
#[must_use]
pub fn denial_reason(&self) -> Option<&'static str> {
match self {
Self::Allowed => None,
Self::NotReadable => Some("module does not read target module"),
Self::NotExported => Some("package is not exported to this module"),
Self::NotOpened => Some("package is not opened for deep reflection"),
}
}
}
impl std::fmt::Display for AccessCheckResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Allowed => write!(f, "access allowed"),
Self::NotReadable => write!(f, "module not readable"),
Self::NotExported => write!(f, "package not exported"),
Self::NotOpened => write!(f, "package not opened for reflection"),
}
}
}
pub const UNNAMED_MODULE: &str = "ALL-UNNAMED";
pub const JAVA_BASE_MODULE: &str = "java.base";
#[derive(Debug)]
pub struct AccessCheck<'a> {
config: &'a ResolvedConfiguration,
}
impl<'a> AccessCheck<'a> {
#[must_use]
pub fn new(config: &'a ResolvedConfiguration) -> Self {
Self { config }
}
#[must_use]
pub fn check_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> AccessCheckResult {
if from_module == to_module {
return AccessCheckResult::Allowed;
}
if from_module == UNNAMED_MODULE {
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_reflection_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> AccessCheckResult {
if from_module == to_module {
return AccessCheckResult::Allowed;
}
if from_module == UNNAMED_MODULE {
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
}
#[must_use]
pub fn can_read(&self, from_module: &str, to_module: &str) -> bool {
if from_module == to_module {
return true;
}
if to_module == JAVA_BASE_MODULE {
return true;
}
self.config.reads(from_module, to_module)
}
#[must_use]
pub fn is_exported(&self, to_module: &str, package: &str, from_module: &str) -> bool {
self.config.exports(to_module, package, from_module)
}
#[must_use]
pub fn is_opened(&self, to_module: &str, package: &str, from_module: &str) -> bool {
self.config.opens(to_module, package, from_module)
}
fn check_unnamed_module_access(&self, to_module: &str, package: &str) -> AccessCheckResult {
if let Some(module) = self.config.get(to_module) {
let descriptor = module.descriptor();
if module.reference().is_automatic() {
if descriptor.packages.contains(package) {
return AccessCheckResult::Allowed;
}
return AccessCheckResult::NotExported;
}
if descriptor.exports_package(package, None) {
return AccessCheckResult::Allowed;
}
if descriptor.exports_package(package, Some(UNNAMED_MODULE)) {
return AccessCheckResult::Allowed;
}
if let Some(module_exports) = self.config.add_exports().get(to_module)
&& let Some(targets) = module_exports.get(package)
&& targets.contains(UNNAMED_MODULE)
{
return AccessCheckResult::Allowed;
}
}
AccessCheckResult::NotExported
}
fn check_unnamed_module_reflection(&self, to_module: &str, package: &str) -> AccessCheckResult {
if let Some(module) = self.config.get(to_module) {
let descriptor = module.descriptor();
if descriptor.is_open() && descriptor.packages.contains(package) {
return AccessCheckResult::Allowed;
}
if module.reference().is_automatic() {
if descriptor.packages.contains(package) {
return AccessCheckResult::Allowed;
}
return AccessCheckResult::NotOpened;
}
if descriptor.opens_package(package, None) {
return AccessCheckResult::Allowed;
}
if descriptor.opens_package(package, Some(UNNAMED_MODULE)) {
return AccessCheckResult::Allowed;
}
if let Some(module_opens) = self.config.add_opens().get(to_module)
&& let Some(targets) = module_opens.get(package)
&& targets.contains(UNNAMED_MODULE)
{
return AccessCheckResult::Allowed;
}
}
AccessCheckResult::NotOpened
}
#[must_use]
pub fn to_error(
&self,
result: AccessCheckResult,
from_module: &str,
to_module: &str,
package: &str,
) -> ModuleError {
match result {
AccessCheckResult::Allowed => {
ModuleError::InternalError("to_error called for allowed access".to_string())
}
AccessCheckResult::NotReadable | AccessCheckResult::NotExported => {
ModuleError::AccessDenied {
from_module: from_module.to_string(),
to_module: to_module.to_string(),
package: package.to_string(),
}
}
AccessCheckResult::NotOpened => ModuleError::ReflectionAccessDenied {
from_module: from_module.to_string(),
to_module: to_module.to_string(),
package: package.to_string(),
},
}
}
pub fn require_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> Result<(), ModuleError> {
let result = self.check_access(from_module, to_module, package);
if result.is_allowed() {
Ok(())
} else {
Err(self.to_error(result, from_module, to_module, package))
}
}
pub fn require_reflection_access(
&self,
from_module: &str,
to_module: &str,
package: &str,
) -> Result<(), ModuleError> {
let result = self.check_reflection_access(from_module, to_module, package);
if result.is_allowed() {
Ok(())
} else {
Err(self.to_error(result, from_module, to_module, package))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::module::descriptor::{Exports, ModuleDescriptor, Opens};
use crate::module::reference::ModuleReference;
use crate::module::resolution::{ResolvedConfiguration, ResolvedModule};
use ahash::{AHashMap, AHashSet};
use std::collections::BTreeMap;
fn create_descriptor(name: &str) -> ModuleDescriptor {
let mut descriptor = ModuleDescriptor::new(name.to_string());
descriptor.packages.insert(format!("{name}/internal"));
descriptor.packages.insert(format!("{name}/api"));
descriptor
}
fn create_descriptor_with_exports(
name: &str,
exports: Vec<(&str, Option<Vec<&str>>)>,
) -> ModuleDescriptor {
let mut descriptor = create_descriptor(name);
for (package, targets) in exports {
descriptor.packages.insert(package.to_string());
descriptor.exports.push(Exports {
package: package.to_string(),
targets: targets.map(|t| t.iter().map(|s| (*s).to_string()).collect()),
});
}
descriptor
}
fn create_descriptor_with_opens(
name: &str,
opens: Vec<(&str, Option<Vec<&str>>)>,
) -> ModuleDescriptor {
let mut descriptor = create_descriptor(name);
for (package, targets) in opens {
descriptor.packages.insert(package.to_string());
descriptor.opens.push(Opens {
package: package.to_string(),
targets: targets.map(|t| t.iter().map(|s| (*s).to_string()).collect()),
});
}
descriptor
}
fn create_resolved_module(descriptor: ModuleDescriptor, reads: Vec<&str>) -> ResolvedModule {
let reference = ModuleReference::system(descriptor);
let mut resolved = ResolvedModule::new(reference);
for read in reads {
resolved.add_read(read.to_string());
}
resolved
}
fn create_test_config(modules: Vec<ResolvedModule>) -> ResolvedConfiguration {
let mut resolved = BTreeMap::new();
let mut package_to_module = BTreeMap::new();
for module in modules {
let name = module.name().to_string();
for package in &module.descriptor().packages {
package_to_module.insert(package.clone(), name.clone());
}
resolved.insert(name, module);
}
ResolvedConfiguration::new(
resolved,
package_to_module,
AHashMap::default(),
AHashMap::default(),
)
}
fn create_config_with_exports(
modules: Vec<ResolvedModule>,
add_exports: AHashMap<String, AHashMap<String, AHashSet<String>>>,
) -> ResolvedConfiguration {
let mut resolved = BTreeMap::new();
let mut package_to_module = BTreeMap::new();
for module in modules {
let name = module.name().to_string();
for package in &module.descriptor().packages {
package_to_module.insert(package.clone(), name.clone());
}
resolved.insert(name, module);
}
ResolvedConfiguration::new(
resolved,
package_to_module,
add_exports,
AHashMap::default(),
)
}
fn create_config_with_opens(
modules: Vec<ResolvedModule>,
add_opens: AHashMap<String, AHashMap<String, AHashSet<String>>>,
) -> ResolvedConfiguration {
let mut resolved = BTreeMap::new();
let mut package_to_module = BTreeMap::new();
for module in modules {
let name = module.name().to_string();
for package in &module.descriptor().packages {
package_to_module.insert(package.clone(), name.clone());
}
resolved.insert(name, module);
}
ResolvedConfiguration::new(resolved, package_to_module, AHashMap::default(), add_opens)
}
#[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());
assert_eq!(
result.denial_reason(),
Some("module does not read target module")
);
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());
assert_eq!(
result.denial_reason(),
Some("package is not exported to this module")
);
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());
assert_eq!(
result.denial_reason(),
Some("package is not opened for deep reflection")
);
assert_eq!(format!("{result}"), "package not opened for reflection");
}
#[test]
fn test_same_module_access_always_allowed() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]);
let config = create_test_config(vec![module_a]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.a", "module.a/internal"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_same_module_reflection_always_allowed() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]);
let config = create_test_config(vec![module_a]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access("module.a", "module.a", "module.a/internal"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_readable_module_exported_package() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_not_readable_module_denied() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]); let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::NotReadable
);
}
#[test]
fn test_can_read_checks_readability() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert!(checker.can_read("module.a", "module.b"));
assert!(!checker.can_read("module.b", "module.a"));
}
#[test]
fn test_can_read_same_module() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]);
let config = create_test_config(vec![module_a]);
let checker = AccessCheck::new(&config);
assert!(checker.can_read("module.a", "module.a"));
}
#[test]
fn test_java_base_implicitly_readable() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]);
let config = create_test_config(vec![module_a]);
let checker = AccessCheck::new(&config);
assert!(checker.can_read("module.a", JAVA_BASE_MODULE));
}
#[test]
fn test_non_exported_package_denied() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]); let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/internal"),
AccessCheckResult::NotExported
);
}
#[test]
fn test_qualified_export_to_specific_module() {
let descriptor_b = create_descriptor_with_exports(
"module.b",
vec![("module.b/internal", Some(vec!["module.a"]))],
);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let module_c = create_resolved_module(create_descriptor("module.c"), vec!["module.b"]);
let config = create_test_config(vec![module_a, module_b, module_c]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/internal"),
AccessCheckResult::Allowed
);
assert_eq!(
checker.check_access("module.c", "module.b", "module.b/internal"),
AccessCheckResult::NotExported
);
}
#[test]
fn test_add_exports_override() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]);
let mut add_exports: AHashMap<String, AHashMap<String, AHashSet<String>>> =
AHashMap::default();
let mut package_map = AHashMap::default();
let mut targets = AHashSet::default();
targets.insert("module.a".to_string());
package_map.insert("module.b/internal".to_string(), targets);
add_exports.insert("module.b".to_string(), package_map);
let config = create_config_with_exports(vec![module_a, module_b], add_exports);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/internal"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_exported_but_not_opened_package() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
assert_eq!(
checker.check_reflection_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::NotOpened
);
}
#[test]
fn test_opened_package_allows_reflection() {
let mut descriptor_b =
create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
descriptor_b.opens.push(Opens {
package: "module.b/api".to_string(),
targets: None,
});
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_qualified_opens() {
let descriptor_b = create_descriptor_with_opens(
"module.b",
vec![("module.b/api", Some(vec!["module.a"]))],
);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let module_c = create_resolved_module(create_descriptor("module.c"), vec!["module.b"]);
let config = create_test_config(vec![module_a, module_b, module_c]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
assert_eq!(
checker.check_reflection_access("module.c", "module.b", "module.b/api"),
AccessCheckResult::NotOpened
);
}
#[test]
fn test_add_opens_override() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let mut add_opens: AHashMap<String, AHashMap<String, AHashSet<String>>> =
AHashMap::default();
let mut package_map = AHashMap::default();
let mut targets = AHashSet::default();
targets.insert("module.a".to_string());
package_map.insert("module.b/api".to_string(), targets);
add_opens.insert("module.b".to_string(), package_map);
let config = create_config_with_opens(vec![module_a, module_b], add_opens);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access("module.a", "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_unnamed_module_access_exported_package() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access(UNNAMED_MODULE, "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_unnamed_module_cannot_access_non_exported() {
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]);
let config = create_test_config(vec![module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access(UNNAMED_MODULE, "module.b", "module.b/internal"),
AccessCheckResult::NotExported
);
}
#[test]
fn test_unnamed_module_with_add_exports() {
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]);
let mut add_exports: AHashMap<String, AHashMap<String, AHashSet<String>>> =
AHashMap::default();
let mut package_map = AHashMap::default();
let mut targets = AHashSet::default();
targets.insert(UNNAMED_MODULE.to_string());
package_map.insert("module.b/internal".to_string(), targets);
add_exports.insert("module.b".to_string(), package_map);
let config = create_config_with_exports(vec![module_b], add_exports);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_access(UNNAMED_MODULE, "module.b", "module.b/internal"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_unnamed_module_reflection_opened_package() {
let descriptor_b = create_descriptor_with_opens("module.b", vec![("module.b/api", None)]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access(UNNAMED_MODULE, "module.b", "module.b/api"),
AccessCheckResult::Allowed
);
}
#[test]
fn test_unnamed_module_cannot_reflect_non_opened() {
let descriptor_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_b = create_resolved_module(descriptor_b, vec![]);
let config = create_test_config(vec![module_b]);
let checker = AccessCheck::new(&config);
assert_eq!(
checker.check_reflection_access(UNNAMED_MODULE, "module.b", "module.b/api"),
AccessCheckResult::NotOpened
);
}
#[test]
fn test_to_error_not_readable() {
let config = create_test_config(vec![]);
let checker = AccessCheck::new(&config);
let error = checker.to_error(
AccessCheckResult::NotReadable,
"module.a",
"module.b",
"pkg",
);
assert!(matches!(error, ModuleError::AccessDenied { .. }));
}
#[test]
fn test_to_error_not_exported() {
let config = create_test_config(vec![]);
let checker = AccessCheck::new(&config);
let error = checker.to_error(
AccessCheckResult::NotExported,
"module.a",
"module.b",
"pkg",
);
assert!(matches!(error, ModuleError::AccessDenied { .. }));
}
#[test]
fn test_to_error_not_opened() {
let config = create_test_config(vec![]);
let checker = AccessCheck::new(&config);
let error = checker.to_error(AccessCheckResult::NotOpened, "module.a", "module.b", "pkg");
assert!(matches!(error, ModuleError::ReflectionAccessDenied { .. }));
}
#[test]
fn test_require_access_success() {
let desc_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(desc_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert!(
checker
.require_access("module.a", "module.b", "module.b/api")
.is_ok()
);
}
#[test]
fn test_require_access_failure() {
let module_a = create_resolved_module(create_descriptor("module.a"), vec![]);
let module_b = create_resolved_module(create_descriptor("module.b"), vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
let result = checker.require_access("module.a", "module.b", "module.b/internal");
assert!(result.is_err());
}
#[test]
fn test_require_reflection_access_success() {
let desc_b = create_descriptor_with_opens("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(desc_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
assert!(
checker
.require_reflection_access("module.a", "module.b", "module.b/api")
.is_ok()
);
}
#[test]
fn test_require_reflection_access_failure() {
let desc_b = create_descriptor_with_exports("module.b", vec![("module.b/api", None)]);
let module_a = create_resolved_module(create_descriptor("module.a"), vec!["module.b"]);
let module_b = create_resolved_module(desc_b, vec![]);
let config = create_test_config(vec![module_a, module_b]);
let checker = AccessCheck::new(&config);
let result = checker.require_reflection_access("module.a", "module.b", "module.b/api");
assert!(result.is_err());
}
}