#![allow(clippy::cast_possible_truncation)]
use cafebabe::attributes::AttributeData;
use crate::ClasspathResult;
use crate::stub::model::{
AccessFlags, ModuleExports, ModuleOpens, ModuleProvides, ModuleRequires, ModuleStub,
};
use super::constants::class_name_to_fqn;
#[allow(clippy::missing_errors_doc)] pub fn extract_module(class: &cafebabe::ClassFile<'_>) -> ClasspathResult<Option<ModuleStub>> {
let module_data = class.attributes.iter().find_map(|attr| match &attr.data {
AttributeData::Module(data) => Some(data),
_ => None,
});
let Some(data) = module_data else {
return Ok(None);
};
let stub = convert_module_data(data)?;
Ok(Some(stub))
}
fn convert_module_data(data: &cafebabe::attributes::ModuleData<'_>) -> ClasspathResult<ModuleStub> {
let name = class_name_to_fqn(&data.name);
let access = AccessFlags::new(data.access_flags.bits());
let version = data.version.as_ref().map(std::string::ToString::to_string);
let requires = data
.requires
.iter()
.map(convert_requires_entry)
.collect::<ClasspathResult<Vec<_>>>()?;
let exports = data
.exports
.iter()
.map(convert_exports_entry)
.collect::<ClasspathResult<Vec<_>>>()?;
let opens = data
.opens
.iter()
.map(convert_opens_entry)
.collect::<ClasspathResult<Vec<_>>>()?;
let provides = data
.provides
.iter()
.map(convert_provides_entry)
.collect::<ClasspathResult<Vec<_>>>()?;
let uses = data
.uses
.iter()
.map(|class_name| class_name_to_fqn(class_name))
.collect();
Ok(ModuleStub {
name,
access,
version,
requires,
exports,
opens,
provides,
uses,
})
}
#[allow(clippy::unnecessary_wraps)] fn convert_requires_entry(
entry: &cafebabe::attributes::ModuleRequireEntry<'_>,
) -> ClasspathResult<ModuleRequires> {
Ok(ModuleRequires {
module_name: class_name_to_fqn(&entry.name),
access: AccessFlags::new(entry.flags.bits()),
version: entry.version.as_ref().map(std::string::ToString::to_string),
})
}
#[allow(clippy::unnecessary_wraps)] fn convert_exports_entry(
entry: &cafebabe::attributes::ModuleExportsEntry<'_>,
) -> ClasspathResult<ModuleExports> {
let to_modules = entry
.exports_to
.iter()
.map(|m| class_name_to_fqn(m))
.collect();
Ok(ModuleExports {
package: class_name_to_fqn(&entry.package_name),
access: AccessFlags::new(entry.flags.bits()),
to_modules,
})
}
#[allow(clippy::unnecessary_wraps)] fn convert_opens_entry(
entry: &cafebabe::attributes::ModuleOpensEntry<'_>,
) -> ClasspathResult<ModuleOpens> {
let to_modules = entry
.opens_to
.iter()
.map(|m| class_name_to_fqn(m))
.collect();
Ok(ModuleOpens {
package: class_name_to_fqn(&entry.package_name),
access: AccessFlags::new(entry.flags.bits()),
to_modules,
})
}
#[allow(clippy::unnecessary_wraps)] fn convert_provides_entry(
entry: &cafebabe::attributes::ModuleProvidesEntry<'_>,
) -> ClasspathResult<ModuleProvides> {
let implementations = entry
.provides_with
.iter()
.map(|c| class_name_to_fqn(c))
.collect();
Ok(ModuleProvides {
service: class_name_to_fqn(&entry.service_interface_name),
implementations,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ClasspathError;
use cafebabe::ParseOptions;
struct ModuleBuilder {
cp_entries: Vec<Vec<u8>>,
module_name_idx: u16,
module_flags: u16,
module_version_idx: u16,
requires: Vec<(u16, u16, u16)>,
exports: Vec<(u16, u16, Vec<u16>)>,
opens: Vec<(u16, u16, Vec<u16>)>,
uses: Vec<u16>,
provides: Vec<(u16, Vec<u16>)>,
}
impl ModuleBuilder {
fn new(module_name: &str) -> Self {
let mut builder = Self {
cp_entries: Vec::new(),
module_name_idx: 0,
module_flags: 0,
module_version_idx: 0,
requires: Vec::new(),
exports: Vec::new(),
opens: Vec::new(),
uses: Vec::new(),
provides: Vec::new(),
};
builder.add_utf8("module-info");
builder.add_class(1);
builder.add_utf8("java/lang/Object");
builder.add_class(3);
builder.add_utf8("Module");
builder.add_utf8(module_name);
builder.module_name_idx = builder.add_module(6);
builder
}
fn module_flags(mut self, flags: u16) -> Self {
self.module_flags = flags;
self
}
fn module_version(mut self, version: &str) -> Self {
self.module_version_idx = self.add_utf8(version);
self
}
fn add_utf8(&mut self, s: &str) -> u16 {
let mut entry = vec![1u8]; let bytes = s.as_bytes();
entry.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
entry.extend_from_slice(bytes);
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_class(&mut self, name_idx: u16) -> u16 {
let mut entry = vec![7u8]; entry.extend_from_slice(&name_idx.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_module(&mut self, name_idx: u16) -> u16 {
let mut entry = vec![19u8]; entry.extend_from_slice(&name_idx.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_package(&mut self, name_idx: u16) -> u16 {
let mut entry = vec![20u8]; entry.extend_from_slice(&name_idx.to_be_bytes());
self.cp_entries.push(entry);
self.cp_entries.len() as u16
}
fn add_requires(
&mut self,
module_name: &str,
flags: u16,
version: Option<&str>,
) -> &mut Self {
let name_idx = self.add_utf8(module_name);
let module_idx = self.add_module(name_idx);
let version_idx = version.map_or(0, |v| self.add_utf8(v));
self.requires.push((module_idx, flags, version_idx));
self
}
fn add_exports(
&mut self,
package_name: &str,
flags: u16,
to_modules: &[&str],
) -> &mut Self {
let pkg_name_idx = self.add_utf8(package_name);
let pkg_idx = self.add_package(pkg_name_idx);
let to_indices: Vec<u16> = to_modules
.iter()
.map(|m| {
let name_idx = self.add_utf8(m);
self.add_module(name_idx)
})
.collect();
self.exports.push((pkg_idx, flags, to_indices));
self
}
fn add_opens(&mut self, package_name: &str, flags: u16, to_modules: &[&str]) -> &mut Self {
let pkg_name_idx = self.add_utf8(package_name);
let pkg_idx = self.add_package(pkg_name_idx);
let to_indices: Vec<u16> = to_modules
.iter()
.map(|m| {
let name_idx = self.add_utf8(m);
self.add_module(name_idx)
})
.collect();
self.opens.push((pkg_idx, flags, to_indices));
self
}
fn add_uses(&mut self, class_name: &str) -> &mut Self {
let name_idx = self.add_utf8(class_name);
let class_idx = self.add_class(name_idx);
self.uses.push(class_idx);
self
}
fn add_provides(&mut self, service_class: &str, impl_classes: &[&str]) -> &mut Self {
let svc_name_idx = self.add_utf8(service_class);
let svc_idx = self.add_class(svc_name_idx);
let impl_indices: Vec<u16> = impl_classes
.iter()
.map(|c| {
let name_idx = self.add_utf8(c);
self.add_class(name_idx)
})
.collect();
self.provides.push((svc_idx, impl_indices));
self
}
fn build(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&53u16.to_be_bytes());
let cp_count = self.cp_entries.len() as u16 + 1;
bytes.extend_from_slice(&cp_count.to_be_bytes());
for entry in &self.cp_entries {
bytes.extend_from_slice(entry);
}
bytes.extend_from_slice(&0x8000u16.to_be_bytes());
bytes.extend_from_slice(&2u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&1u16.to_be_bytes());
let attr_data = self.build_module_attr_data();
bytes.extend_from_slice(&5u16.to_be_bytes());
bytes.extend_from_slice(&(attr_data.len() as u32).to_be_bytes());
bytes.extend_from_slice(&attr_data);
bytes
}
fn build_module_attr_data(&self) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&self.module_name_idx.to_be_bytes());
data.extend_from_slice(&self.module_flags.to_be_bytes());
data.extend_from_slice(&self.module_version_idx.to_be_bytes());
data.extend_from_slice(&(self.requires.len() as u16).to_be_bytes());
for &(module_idx, flags, version_idx) in &self.requires {
data.extend_from_slice(&module_idx.to_be_bytes());
data.extend_from_slice(&flags.to_be_bytes());
data.extend_from_slice(&version_idx.to_be_bytes());
}
data.extend_from_slice(&(self.exports.len() as u16).to_be_bytes());
for (pkg_idx, flags, to_indices) in &self.exports {
data.extend_from_slice(&pkg_idx.to_be_bytes());
data.extend_from_slice(&flags.to_be_bytes());
data.extend_from_slice(&(to_indices.len() as u16).to_be_bytes());
for idx in to_indices {
data.extend_from_slice(&idx.to_be_bytes());
}
}
data.extend_from_slice(&(self.opens.len() as u16).to_be_bytes());
for (pkg_idx, flags, to_indices) in &self.opens {
data.extend_from_slice(&pkg_idx.to_be_bytes());
data.extend_from_slice(&flags.to_be_bytes());
data.extend_from_slice(&(to_indices.len() as u16).to_be_bytes());
for idx in to_indices {
data.extend_from_slice(&idx.to_be_bytes());
}
}
data.extend_from_slice(&(self.uses.len() as u16).to_be_bytes());
for idx in &self.uses {
data.extend_from_slice(&idx.to_be_bytes());
}
data.extend_from_slice(&(self.provides.len() as u16).to_be_bytes());
for (svc_idx, impl_indices) in &self.provides {
data.extend_from_slice(&svc_idx.to_be_bytes());
data.extend_from_slice(&(impl_indices.len() as u16).to_be_bytes());
for idx in impl_indices {
data.extend_from_slice(&idx.to_be_bytes());
}
}
data
}
}
fn parse_and_extract(bytes: &[u8]) -> ClasspathResult<Option<ModuleStub>> {
let mut opts = ParseOptions::default();
opts.parse_bytecode(false);
let class_file = cafebabe::parse_class_with_options(bytes, &opts).map_err(|e| {
ClasspathError::BytecodeParseError {
class_name: String::from("<test>"),
reason: e.to_string(),
}
})?;
extract_module(&class_file)
}
#[test]
fn test_java_base_module_exports() {
let mut builder = ModuleBuilder::new("java.base");
builder.add_exports("java/lang", 0, &[]);
builder.add_exports("java/util", 0, &[]);
builder.add_requires("java.base", 0x8000, Some("17"));
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.name, "java.base");
assert_eq!(stub.exports.len(), 2);
assert_eq!(stub.exports[0].package, "java.lang");
assert!(stub.exports[0].to_modules.is_empty()); assert_eq!(stub.exports[1].package, "java.util");
assert_eq!(stub.requires.len(), 1);
assert_eq!(stub.requires[0].module_name, "java.base");
assert!(stub.requires[0].access.contains(0x8000)); assert_eq!(stub.requires[0].version.as_deref(), Some("17"));
}
#[test]
fn test_requires_transitive() {
let mut builder = ModuleBuilder::new("com.example.app");
builder.add_requires("java.base", 0x8000, Some("17")); builder.add_requires("java.logging", 0x0020, None);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.name, "com.example.app");
assert_eq!(stub.requires.len(), 2);
let java_base = &stub.requires[0];
assert_eq!(java_base.module_name, "java.base");
assert!(java_base.access.contains(0x8000));
let java_logging = &stub.requires[1];
assert_eq!(java_logging.module_name, "java.logging");
assert!(java_logging.access.contains(0x0020)); assert!(java_logging.version.is_none());
}
#[test]
fn test_provides_service() {
let mut builder = ModuleBuilder::new("com.example.provider");
builder.add_provides(
"com/example/api/Service",
&[
"com/example/impl/ServiceImpl",
"com/example/impl/ServiceImpl2",
],
);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.provides.len(), 1);
assert_eq!(stub.provides[0].service, "com.example.api.Service");
assert_eq!(stub.provides[0].implementations.len(), 2);
assert_eq!(
stub.provides[0].implementations[0],
"com.example.impl.ServiceImpl"
);
assert_eq!(
stub.provides[0].implementations[1],
"com.example.impl.ServiceImpl2"
);
}
#[test]
fn test_opens_for_reflection() {
let mut builder = ModuleBuilder::new("com.example.reflective");
builder.add_opens("com/example/internal", 0, &[]);
builder.add_opens(
"com/example/private",
0,
&["com.example.framework", "com.example.test"],
);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.opens.len(), 2);
let open_all = &stub.opens[0];
assert_eq!(open_all.package, "com.example.internal");
assert!(open_all.to_modules.is_empty());
let open_qualified = &stub.opens[1];
assert_eq!(open_qualified.package, "com.example.private");
assert_eq!(open_qualified.to_modules.len(), 2);
assert_eq!(open_qualified.to_modules[0], "com.example.framework");
assert_eq!(open_qualified.to_modules[1], "com.example.test");
}
#[test]
fn test_uses_declarations() {
let mut builder = ModuleBuilder::new("com.example.consumer");
builder.add_uses("com/example/api/Service");
builder.add_uses("java/sql/Driver");
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.uses.len(), 2);
assert_eq!(stub.uses[0], "com.example.api.Service");
assert_eq!(stub.uses[1], "java.sql.Driver");
}
#[test]
fn test_no_module_attribute_returns_none() {
let mut bytes = Vec::new();
bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&52u16.to_be_bytes());
bytes.extend_from_slice(&5u16.to_be_bytes());
bytes.push(1);
let name = b"com/example/Foo";
bytes.extend_from_slice(&(name.len() as u16).to_be_bytes());
bytes.extend_from_slice(name);
bytes.push(7);
bytes.extend_from_slice(&1u16.to_be_bytes());
bytes.push(1);
let obj = b"java/lang/Object";
bytes.extend_from_slice(&(obj.len() as u16).to_be_bytes());
bytes.extend_from_slice(obj);
bytes.push(7);
bytes.extend_from_slice(&3u16.to_be_bytes());
bytes.extend_from_slice(&0x0021u16.to_be_bytes());
bytes.extend_from_slice(&2u16.to_be_bytes());
bytes.extend_from_slice(&4u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
bytes.extend_from_slice(&0u16.to_be_bytes());
let result = parse_and_extract(&bytes).unwrap();
assert!(result.is_none());
}
#[test]
fn test_module_version_and_open_flag() {
let builder = ModuleBuilder::new("com.example.open")
.module_flags(0x0020) .module_version("1.0.0");
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.name, "com.example.open");
assert!(stub.access.contains(0x0020)); assert_eq!(stub.version.as_deref(), Some("1.0.0"));
}
#[test]
fn test_qualified_exports() {
let mut builder = ModuleBuilder::new("com.example.lib");
builder.add_exports(
"com/example/internal",
0,
&["com.example.app", "com.example.test"],
);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.exports.len(), 1);
assert_eq!(stub.exports[0].package, "com.example.internal");
assert_eq!(stub.exports[0].to_modules.len(), 2);
assert_eq!(stub.exports[0].to_modules[0], "com.example.app");
assert_eq!(stub.exports[0].to_modules[1], "com.example.test");
}
#[test]
fn test_comprehensive_module() {
let mut builder = ModuleBuilder::new("com.example.full");
builder
.add_requires("java.base", 0x8000, Some("17"))
.add_requires("java.logging", 0x0020, None)
.add_exports("com/example/api", 0, &[])
.add_exports("com/example/spi", 0, &["com.example.impl"])
.add_opens("com/example/internal", 0, &[])
.add_uses("com/example/spi/Plugin")
.add_provides(
"com/example/spi/Plugin",
&["com/example/impl/DefaultPlugin"],
);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.name, "com.example.full");
assert_eq!(stub.requires.len(), 2);
assert_eq!(stub.exports.len(), 2);
assert_eq!(stub.opens.len(), 1);
assert_eq!(stub.uses.len(), 1);
assert_eq!(stub.uses[0], "com.example.spi.Plugin");
assert_eq!(stub.provides.len(), 1);
assert_eq!(stub.provides[0].service, "com.example.spi.Plugin");
assert_eq!(
stub.provides[0].implementations,
vec!["com.example.impl.DefaultPlugin"]
);
}
#[test]
fn test_empty_module() {
let builder = ModuleBuilder::new("com.example.empty");
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.name, "com.example.empty");
assert!(stub.requires.is_empty());
assert!(stub.exports.is_empty());
assert!(stub.opens.is_empty());
assert!(stub.uses.is_empty());
assert!(stub.provides.is_empty());
assert!(stub.version.is_none());
}
#[test]
fn test_requires_static_phase() {
let mut builder = ModuleBuilder::new("com.example.compile");
builder.add_requires("org.checkerframework.checker.qual", 0x0040, None);
let bytes = builder.build();
let stub = parse_and_extract(&bytes).unwrap().unwrap();
assert_eq!(stub.requires.len(), 1);
assert_eq!(
stub.requires[0].module_name,
"org.checkerframework.checker.qual"
);
assert!(stub.requires[0].access.contains(0x0040)); }
}