Skip to main content

sqry_classpath/bytecode/
modules.rs

1//! Java 9+ module attribute parser (JVMS 4.7.25).
2//!
3//! Extracts the `Module` attribute from `module-info.class` files and converts
4//! the parsed representation into our [`ModuleStub`] model type. This module
5//! bridges cafebabe's `ModuleData` to our stub types, converting all internal
6//! JVM names (`/`-separated) to fully-qualified names (`.`-separated).
7//!
8//! The `Module` attribute is only present on `module-info.class` files produced
9//! by `javac` for Java 9+ `module-info.java` source files.
10
11// JVM module_info attributes are spec-bounded to u16; casts are intentional
12#![allow(clippy::cast_possible_truncation)]
13
14use cafebabe::attributes::AttributeData;
15
16use crate::ClasspathResult;
17use crate::stub::model::{
18    AccessFlags, ModuleExports, ModuleOpens, ModuleProvides, ModuleRequires, ModuleStub,
19};
20
21use super::constants::class_name_to_fqn;
22
23// ---------------------------------------------------------------------------
24// Public API
25// ---------------------------------------------------------------------------
26
27/// Extract module information from a parsed class file.
28///
29/// Searches the class-level attributes for a `Module` attribute and converts
30/// it into a [`ModuleStub`]. Module names, package names, and class names are
31/// all converted from JVM internal form (`/` separator) to FQN form (`.`
32/// separator).
33///
34/// Returns `Ok(None)` if the class file does not contain a `Module` attribute
35/// (i.e., it is not a `module-info.class`). Returns an error if the `Module`
36/// attribute is present but cannot be converted.
37#[allow(clippy::missing_errors_doc)] // Internal helper function
38pub fn extract_module(class: &cafebabe::ClassFile<'_>) -> ClasspathResult<Option<ModuleStub>> {
39    let module_data = class.attributes.iter().find_map(|attr| match &attr.data {
40        AttributeData::Module(data) => Some(data),
41        _ => None,
42    });
43
44    let Some(data) = module_data else {
45        return Ok(None);
46    };
47
48    let stub = convert_module_data(data)?;
49    Ok(Some(stub))
50}
51
52// ---------------------------------------------------------------------------
53// Internal conversion
54// ---------------------------------------------------------------------------
55
56/// Convert cafebabe's `ModuleData` into our `ModuleStub`.
57fn convert_module_data(data: &cafebabe::attributes::ModuleData<'_>) -> ClasspathResult<ModuleStub> {
58    let name = class_name_to_fqn(&data.name);
59    let access = AccessFlags::new(data.access_flags.bits());
60    let version = data.version.as_ref().map(std::string::ToString::to_string);
61
62    let requires = data
63        .requires
64        .iter()
65        .map(convert_requires_entry)
66        .collect::<ClasspathResult<Vec<_>>>()?;
67
68    let exports = data
69        .exports
70        .iter()
71        .map(convert_exports_entry)
72        .collect::<ClasspathResult<Vec<_>>>()?;
73
74    let opens = data
75        .opens
76        .iter()
77        .map(convert_opens_entry)
78        .collect::<ClasspathResult<Vec<_>>>()?;
79
80    let provides = data
81        .provides
82        .iter()
83        .map(convert_provides_entry)
84        .collect::<ClasspathResult<Vec<_>>>()?;
85
86    let uses = data
87        .uses
88        .iter()
89        .map(|class_name| class_name_to_fqn(class_name))
90        .collect();
91
92    Ok(ModuleStub {
93        name,
94        access,
95        version,
96        requires,
97        exports,
98        opens,
99        provides,
100        uses,
101    })
102}
103
104/// Convert a cafebabe `ModuleRequireEntry` to our `ModuleRequires`.
105#[allow(clippy::unnecessary_wraps)] // Result for API consistency
106fn convert_requires_entry(
107    entry: &cafebabe::attributes::ModuleRequireEntry<'_>,
108) -> ClasspathResult<ModuleRequires> {
109    Ok(ModuleRequires {
110        module_name: class_name_to_fqn(&entry.name),
111        access: AccessFlags::new(entry.flags.bits()),
112        version: entry.version.as_ref().map(std::string::ToString::to_string),
113    })
114}
115
116/// Convert a cafebabe `ModuleExportsEntry` to our `ModuleExports`.
117#[allow(clippy::unnecessary_wraps)] // Result for API consistency
118fn convert_exports_entry(
119    entry: &cafebabe::attributes::ModuleExportsEntry<'_>,
120) -> ClasspathResult<ModuleExports> {
121    let to_modules = entry
122        .exports_to
123        .iter()
124        .map(|m| class_name_to_fqn(m))
125        .collect();
126
127    Ok(ModuleExports {
128        package: class_name_to_fqn(&entry.package_name),
129        access: AccessFlags::new(entry.flags.bits()),
130        to_modules,
131    })
132}
133
134/// Convert a cafebabe `ModuleOpensEntry` to our `ModuleOpens`.
135#[allow(clippy::unnecessary_wraps)] // Result for API consistency
136fn convert_opens_entry(
137    entry: &cafebabe::attributes::ModuleOpensEntry<'_>,
138) -> ClasspathResult<ModuleOpens> {
139    let to_modules = entry
140        .opens_to
141        .iter()
142        .map(|m| class_name_to_fqn(m))
143        .collect();
144
145    Ok(ModuleOpens {
146        package: class_name_to_fqn(&entry.package_name),
147        access: AccessFlags::new(entry.flags.bits()),
148        to_modules,
149    })
150}
151
152/// Convert a cafebabe `ModuleProvidesEntry` to our `ModuleProvides`.
153#[allow(clippy::unnecessary_wraps)] // Result for API consistency
154fn convert_provides_entry(
155    entry: &cafebabe::attributes::ModuleProvidesEntry<'_>,
156) -> ClasspathResult<ModuleProvides> {
157    let implementations = entry
158        .provides_with
159        .iter()
160        .map(|c| class_name_to_fqn(c))
161        .collect();
162
163    Ok(ModuleProvides {
164        service: class_name_to_fqn(&entry.service_interface_name),
165        implementations,
166    })
167}
168
169// ---------------------------------------------------------------------------
170// Tests
171// ---------------------------------------------------------------------------
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176    use crate::ClasspathError;
177    use cafebabe::ParseOptions;
178
179    // -----------------------------------------------------------------------
180    // Module class file builder for tests
181    // -----------------------------------------------------------------------
182
183    /// Builds minimal `module-info.class` bytecode with a Module attribute.
184    ///
185    /// Constructs valid JVM class file bytes containing the necessary constant
186    /// pool entries (UTF-8, Class, Module, Package) and a complete Module
187    /// attribute with requires, exports, opens, uses, and provides directives.
188    struct ModuleBuilder {
189        /// Raw constant pool entries (each entry is tag + data bytes).
190        cp_entries: Vec<Vec<u8>>,
191        /// Constant pool index of the `CONSTANT_Module_info` for this module.
192        module_name_idx: u16,
193        /// Module-level access flags (`ACC_OPEN`, `ACC_SYNTHETIC`, `ACC_MANDATED`).
194        module_flags: u16,
195        /// Constant pool index for the module version UTF-8 string (0 = none).
196        module_version_idx: u16,
197        /// Pending requires directives: (`module_cp_idx`, flags, `version_cp_idx`).
198        requires: Vec<(u16, u16, u16)>,
199        /// Pending exports directives: (`package_cp_idx`, flags, `to_module_cp_indices`).
200        exports: Vec<(u16, u16, Vec<u16>)>,
201        /// Pending opens directives: (`package_cp_idx`, flags, `to_module_cp_indices`).
202        opens: Vec<(u16, u16, Vec<u16>)>,
203        /// Pending uses directives: `class_cp_indices`.
204        uses: Vec<u16>,
205        /// Pending provides directives: (`service_class_cp_idx`, `impl_class_cp_indices`).
206        provides: Vec<(u16, Vec<u16>)>,
207    }
208
209    impl ModuleBuilder {
210        /// Create a builder for a module with the given name.
211        ///
212        /// Pre-populates the constant pool with entries needed for the class
213        /// file structure (`this_class`, `super_class`) and the Module attribute
214        /// name and module name.
215        fn new(module_name: &str) -> Self {
216            let mut builder = Self {
217                cp_entries: Vec::new(),
218                module_name_idx: 0,
219                module_flags: 0,
220                module_version_idx: 0,
221                requires: Vec::new(),
222                exports: Vec::new(),
223                opens: Vec::new(),
224                uses: Vec::new(),
225                provides: Vec::new(),
226            };
227
228            // CP#1: UTF-8 "module-info"
229            builder.add_utf8("module-info");
230            // CP#2: CONSTANT_Class -> #1
231            builder.add_class(1);
232            // CP#3: UTF-8 "java/lang/Object"
233            builder.add_utf8("java/lang/Object");
234            // CP#4: CONSTANT_Class -> #3
235            builder.add_class(3);
236            // CP#5: UTF-8 "Module"
237            builder.add_utf8("Module");
238            // CP#6: UTF-8 module_name
239            builder.add_utf8(module_name);
240            // CP#7: CONSTANT_Module -> #6
241            builder.module_name_idx = builder.add_module(6);
242
243            builder
244        }
245
246        /// Set module access flags (`ACC_OPEN=0x0020`, `ACC_SYNTHETIC=0x1000`,
247        /// `ACC_MANDATED=0x8000`).
248        fn module_flags(mut self, flags: u16) -> Self {
249            self.module_flags = flags;
250            self
251        }
252
253        /// Set the module version string.
254        fn module_version(mut self, version: &str) -> Self {
255            self.module_version_idx = self.add_utf8(version);
256            self
257        }
258
259        /// Add a `CONSTANT_Utf8` entry. Returns 1-based constant pool index.
260        fn add_utf8(&mut self, s: &str) -> u16 {
261            let mut entry = vec![1u8]; // tag
262            let bytes = s.as_bytes();
263            entry.extend_from_slice(&(bytes.len() as u16).to_be_bytes());
264            entry.extend_from_slice(bytes);
265            self.cp_entries.push(entry);
266            self.cp_entries.len() as u16
267        }
268
269        /// Add a `CONSTANT_Class` entry. Returns 1-based constant pool index.
270        fn add_class(&mut self, name_idx: u16) -> u16 {
271            let mut entry = vec![7u8]; // tag
272            entry.extend_from_slice(&name_idx.to_be_bytes());
273            self.cp_entries.push(entry);
274            self.cp_entries.len() as u16
275        }
276
277        /// Add a `CONSTANT_Module_info` entry (tag 19). Returns 1-based index.
278        fn add_module(&mut self, name_idx: u16) -> u16 {
279            let mut entry = vec![19u8]; // tag
280            entry.extend_from_slice(&name_idx.to_be_bytes());
281            self.cp_entries.push(entry);
282            self.cp_entries.len() as u16
283        }
284
285        /// Add a `CONSTANT_Package_info` entry (tag 20). Returns 1-based index.
286        fn add_package(&mut self, name_idx: u16) -> u16 {
287            let mut entry = vec![20u8]; // tag
288            entry.extend_from_slice(&name_idx.to_be_bytes());
289            self.cp_entries.push(entry);
290            self.cp_entries.len() as u16
291        }
292
293        /// Add a `requires` directive.
294        fn add_requires(
295            &mut self,
296            module_name: &str,
297            flags: u16,
298            version: Option<&str>,
299        ) -> &mut Self {
300            let name_idx = self.add_utf8(module_name);
301            let module_idx = self.add_module(name_idx);
302            let version_idx = version.map_or(0, |v| self.add_utf8(v));
303            self.requires.push((module_idx, flags, version_idx));
304            self
305        }
306
307        /// Add an `exports` directive.
308        fn add_exports(
309            &mut self,
310            package_name: &str,
311            flags: u16,
312            to_modules: &[&str],
313        ) -> &mut Self {
314            let pkg_name_idx = self.add_utf8(package_name);
315            let pkg_idx = self.add_package(pkg_name_idx);
316            let to_indices: Vec<u16> = to_modules
317                .iter()
318                .map(|m| {
319                    let name_idx = self.add_utf8(m);
320                    self.add_module(name_idx)
321                })
322                .collect();
323            self.exports.push((pkg_idx, flags, to_indices));
324            self
325        }
326
327        /// Add an `opens` directive.
328        fn add_opens(&mut self, package_name: &str, flags: u16, to_modules: &[&str]) -> &mut Self {
329            let pkg_name_idx = self.add_utf8(package_name);
330            let pkg_idx = self.add_package(pkg_name_idx);
331            let to_indices: Vec<u16> = to_modules
332                .iter()
333                .map(|m| {
334                    let name_idx = self.add_utf8(m);
335                    self.add_module(name_idx)
336                })
337                .collect();
338            self.opens.push((pkg_idx, flags, to_indices));
339            self
340        }
341
342        /// Add a `uses` directive (service interface consumed).
343        fn add_uses(&mut self, class_name: &str) -> &mut Self {
344            let name_idx = self.add_utf8(class_name);
345            let class_idx = self.add_class(name_idx);
346            self.uses.push(class_idx);
347            self
348        }
349
350        /// Add a `provides` directive (service interface + implementations).
351        fn add_provides(&mut self, service_class: &str, impl_classes: &[&str]) -> &mut Self {
352            let svc_name_idx = self.add_utf8(service_class);
353            let svc_idx = self.add_class(svc_name_idx);
354            let impl_indices: Vec<u16> = impl_classes
355                .iter()
356                .map(|c| {
357                    let name_idx = self.add_utf8(c);
358                    self.add_class(name_idx)
359                })
360                .collect();
361            self.provides.push((svc_idx, impl_indices));
362            self
363        }
364
365        /// Serialize the complete class file to bytes.
366        fn build(&self) -> Vec<u8> {
367            let mut bytes = Vec::new();
368
369            // Magic number
370            bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
371            // Minor version
372            bytes.extend_from_slice(&0u16.to_be_bytes());
373            // Major version: 53 (Java 9, the minimum for modules)
374            bytes.extend_from_slice(&53u16.to_be_bytes());
375
376            // Constant pool (count = entries + 1)
377            let cp_count = self.cp_entries.len() as u16 + 1;
378            bytes.extend_from_slice(&cp_count.to_be_bytes());
379            for entry in &self.cp_entries {
380                bytes.extend_from_slice(entry);
381            }
382
383            // Access flags: ACC_MODULE (0x8000)
384            bytes.extend_from_slice(&0x8000u16.to_be_bytes());
385            // this_class: CP#2 (Class -> "module-info")
386            bytes.extend_from_slice(&2u16.to_be_bytes());
387            // super_class: 0 (module-info.class has no superclass per JVMS 4.1)
388            bytes.extend_from_slice(&0u16.to_be_bytes());
389            // interfaces_count: 0
390            bytes.extend_from_slice(&0u16.to_be_bytes());
391            // fields_count: 0
392            bytes.extend_from_slice(&0u16.to_be_bytes());
393            // methods_count: 0
394            bytes.extend_from_slice(&0u16.to_be_bytes());
395
396            // attributes_count: 1 (the Module attribute)
397            bytes.extend_from_slice(&1u16.to_be_bytes());
398
399            // Module attribute
400            let attr_data = self.build_module_attr_data();
401            // attribute_name_index: CP#5 ("Module")
402            bytes.extend_from_slice(&5u16.to_be_bytes());
403            // attribute_length
404            bytes.extend_from_slice(&(attr_data.len() as u32).to_be_bytes());
405            bytes.extend_from_slice(&attr_data);
406
407            bytes
408        }
409
410        /// Build the Module attribute data payload (JVMS 4.7.25).
411        fn build_module_attr_data(&self) -> Vec<u8> {
412            let mut data = Vec::new();
413
414            // module_name_index (CONSTANT_Module_info)
415            data.extend_from_slice(&self.module_name_idx.to_be_bytes());
416            // module_flags
417            data.extend_from_slice(&self.module_flags.to_be_bytes());
418            // module_version_index (CONSTANT_Utf8 or 0)
419            data.extend_from_slice(&self.module_version_idx.to_be_bytes());
420
421            // requires
422            data.extend_from_slice(&(self.requires.len() as u16).to_be_bytes());
423            for &(module_idx, flags, version_idx) in &self.requires {
424                data.extend_from_slice(&module_idx.to_be_bytes());
425                data.extend_from_slice(&flags.to_be_bytes());
426                data.extend_from_slice(&version_idx.to_be_bytes());
427            }
428
429            // exports
430            data.extend_from_slice(&(self.exports.len() as u16).to_be_bytes());
431            for (pkg_idx, flags, to_indices) in &self.exports {
432                data.extend_from_slice(&pkg_idx.to_be_bytes());
433                data.extend_from_slice(&flags.to_be_bytes());
434                data.extend_from_slice(&(to_indices.len() as u16).to_be_bytes());
435                for idx in to_indices {
436                    data.extend_from_slice(&idx.to_be_bytes());
437                }
438            }
439
440            // opens
441            data.extend_from_slice(&(self.opens.len() as u16).to_be_bytes());
442            for (pkg_idx, flags, to_indices) in &self.opens {
443                data.extend_from_slice(&pkg_idx.to_be_bytes());
444                data.extend_from_slice(&flags.to_be_bytes());
445                data.extend_from_slice(&(to_indices.len() as u16).to_be_bytes());
446                for idx in to_indices {
447                    data.extend_from_slice(&idx.to_be_bytes());
448                }
449            }
450
451            // uses
452            data.extend_from_slice(&(self.uses.len() as u16).to_be_bytes());
453            for idx in &self.uses {
454                data.extend_from_slice(&idx.to_be_bytes());
455            }
456
457            // provides
458            data.extend_from_slice(&(self.provides.len() as u16).to_be_bytes());
459            for (svc_idx, impl_indices) in &self.provides {
460                data.extend_from_slice(&svc_idx.to_be_bytes());
461                data.extend_from_slice(&(impl_indices.len() as u16).to_be_bytes());
462                for idx in impl_indices {
463                    data.extend_from_slice(&idx.to_be_bytes());
464                }
465            }
466
467            data
468        }
469    }
470
471    /// Helper: parse raw bytes with cafebabe and run `extract_module`.
472    fn parse_and_extract(bytes: &[u8]) -> ClasspathResult<Option<ModuleStub>> {
473        let mut opts = ParseOptions::default();
474        opts.parse_bytecode(false);
475        let class_file = cafebabe::parse_class_with_options(bytes, &opts).map_err(|e| {
476            ClasspathError::BytecodeParseError {
477                class_name: String::from("<test>"),
478                reason: e.to_string(),
479            }
480        })?;
481        extract_module(&class_file)
482    }
483
484    // -----------------------------------------------------------------------
485    // Test 1: java.base-like module with exports
486    // -----------------------------------------------------------------------
487
488    #[test]
489    fn test_java_base_module_exports() {
490        let mut builder = ModuleBuilder::new("java.base");
491        builder.add_exports("java/lang", 0, &[]);
492        builder.add_exports("java/util", 0, &[]);
493        builder.add_requires("java.base", 0x8000, Some("17")); // ACC_MANDATED
494
495        let bytes = builder.build();
496        let stub = parse_and_extract(&bytes).unwrap().unwrap();
497
498        assert_eq!(stub.name, "java.base");
499        assert_eq!(stub.exports.len(), 2);
500        assert_eq!(stub.exports[0].package, "java.lang");
501        assert!(stub.exports[0].to_modules.is_empty()); // unqualified export
502        assert_eq!(stub.exports[1].package, "java.util");
503        assert_eq!(stub.requires.len(), 1);
504        assert_eq!(stub.requires[0].module_name, "java.base");
505        assert!(stub.requires[0].access.contains(0x8000)); // ACC_MANDATED
506        assert_eq!(stub.requires[0].version.as_deref(), Some("17"));
507    }
508
509    // -----------------------------------------------------------------------
510    // Test 2: requires transitive flag
511    // -----------------------------------------------------------------------
512
513    #[test]
514    fn test_requires_transitive() {
515        let mut builder = ModuleBuilder::new("com.example.app");
516        builder.add_requires("java.base", 0x8000, Some("17")); // ACC_MANDATED
517        builder.add_requires("java.logging", 0x0020, None); // ACC_TRANSITIVE
518
519        let bytes = builder.build();
520        let stub = parse_and_extract(&bytes).unwrap().unwrap();
521
522        assert_eq!(stub.name, "com.example.app");
523        assert_eq!(stub.requires.len(), 2);
524
525        let java_base = &stub.requires[0];
526        assert_eq!(java_base.module_name, "java.base");
527        assert!(java_base.access.contains(0x8000)); // mandated
528
529        let java_logging = &stub.requires[1];
530        assert_eq!(java_logging.module_name, "java.logging");
531        assert!(java_logging.access.contains(0x0020)); // transitive
532        assert!(java_logging.version.is_none());
533    }
534
535    // -----------------------------------------------------------------------
536    // Test 3: provides with service implementations
537    // -----------------------------------------------------------------------
538
539    #[test]
540    fn test_provides_service() {
541        let mut builder = ModuleBuilder::new("com.example.provider");
542        builder.add_provides(
543            "com/example/api/Service",
544            &[
545                "com/example/impl/ServiceImpl",
546                "com/example/impl/ServiceImpl2",
547            ],
548        );
549
550        let bytes = builder.build();
551        let stub = parse_and_extract(&bytes).unwrap().unwrap();
552
553        assert_eq!(stub.provides.len(), 1);
554        assert_eq!(stub.provides[0].service, "com.example.api.Service");
555        assert_eq!(stub.provides[0].implementations.len(), 2);
556        assert_eq!(
557            stub.provides[0].implementations[0],
558            "com.example.impl.ServiceImpl"
559        );
560        assert_eq!(
561            stub.provides[0].implementations[1],
562            "com.example.impl.ServiceImpl2"
563        );
564    }
565
566    // -----------------------------------------------------------------------
567    // Test 4: opens for reflection
568    // -----------------------------------------------------------------------
569
570    #[test]
571    fn test_opens_for_reflection() {
572        let mut builder = ModuleBuilder::new("com.example.reflective");
573        // Unqualified open (to all modules)
574        builder.add_opens("com/example/internal", 0, &[]);
575        // Qualified open (to specific modules)
576        builder.add_opens(
577            "com/example/private",
578            0,
579            &["com.example.framework", "com.example.test"],
580        );
581
582        let bytes = builder.build();
583        let stub = parse_and_extract(&bytes).unwrap().unwrap();
584
585        assert_eq!(stub.opens.len(), 2);
586
587        let open_all = &stub.opens[0];
588        assert_eq!(open_all.package, "com.example.internal");
589        assert!(open_all.to_modules.is_empty());
590
591        let open_qualified = &stub.opens[1];
592        assert_eq!(open_qualified.package, "com.example.private");
593        assert_eq!(open_qualified.to_modules.len(), 2);
594        assert_eq!(open_qualified.to_modules[0], "com.example.framework");
595        assert_eq!(open_qualified.to_modules[1], "com.example.test");
596    }
597
598    // -----------------------------------------------------------------------
599    // Test 5: uses declarations
600    // -----------------------------------------------------------------------
601
602    #[test]
603    fn test_uses_declarations() {
604        let mut builder = ModuleBuilder::new("com.example.consumer");
605        builder.add_uses("com/example/api/Service");
606        builder.add_uses("java/sql/Driver");
607
608        let bytes = builder.build();
609        let stub = parse_and_extract(&bytes).unwrap().unwrap();
610
611        assert_eq!(stub.uses.len(), 2);
612        assert_eq!(stub.uses[0], "com.example.api.Service");
613        assert_eq!(stub.uses[1], "java.sql.Driver");
614    }
615
616    // -----------------------------------------------------------------------
617    // Test 6: class without Module attribute returns None
618    // -----------------------------------------------------------------------
619
620    #[test]
621    fn test_no_module_attribute_returns_none() {
622        // Build a minimal regular class file (no Module attribute).
623        let mut bytes = Vec::new();
624
625        // Magic
626        bytes.extend_from_slice(&0xCAFE_BABEu32.to_be_bytes());
627        // Minor version
628        bytes.extend_from_slice(&0u16.to_be_bytes());
629        // Major version: 52 (Java 8)
630        bytes.extend_from_slice(&52u16.to_be_bytes());
631
632        // Constant pool: 4 entries => cp_count = 5
633        bytes.extend_from_slice(&5u16.to_be_bytes());
634
635        // CP#1: UTF-8 "com/example/Foo"
636        bytes.push(1);
637        let name = b"com/example/Foo";
638        bytes.extend_from_slice(&(name.len() as u16).to_be_bytes());
639        bytes.extend_from_slice(name);
640
641        // CP#2: CONSTANT_Class -> #1
642        bytes.push(7);
643        bytes.extend_from_slice(&1u16.to_be_bytes());
644
645        // CP#3: UTF-8 "java/lang/Object"
646        bytes.push(1);
647        let obj = b"java/lang/Object";
648        bytes.extend_from_slice(&(obj.len() as u16).to_be_bytes());
649        bytes.extend_from_slice(obj);
650
651        // CP#4: CONSTANT_Class -> #3
652        bytes.push(7);
653        bytes.extend_from_slice(&3u16.to_be_bytes());
654
655        // Access flags: ACC_PUBLIC | ACC_SUPER
656        bytes.extend_from_slice(&0x0021u16.to_be_bytes());
657        // this_class: CP#2
658        bytes.extend_from_slice(&2u16.to_be_bytes());
659        // super_class: CP#4
660        bytes.extend_from_slice(&4u16.to_be_bytes());
661        // interfaces_count: 0
662        bytes.extend_from_slice(&0u16.to_be_bytes());
663        // fields_count: 0
664        bytes.extend_from_slice(&0u16.to_be_bytes());
665        // methods_count: 0
666        bytes.extend_from_slice(&0u16.to_be_bytes());
667        // attributes_count: 0
668        bytes.extend_from_slice(&0u16.to_be_bytes());
669
670        let result = parse_and_extract(&bytes).unwrap();
671        assert!(result.is_none());
672    }
673
674    // -----------------------------------------------------------------------
675    // Test 7: module version and ACC_OPEN flag
676    // -----------------------------------------------------------------------
677
678    #[test]
679    fn test_module_version_and_open_flag() {
680        let builder = ModuleBuilder::new("com.example.open")
681            .module_flags(0x0020) // ACC_OPEN
682            .module_version("1.0.0");
683
684        let bytes = builder.build();
685        let stub = parse_and_extract(&bytes).unwrap().unwrap();
686
687        assert_eq!(stub.name, "com.example.open");
688        assert!(stub.access.contains(0x0020)); // ACC_OPEN
689        assert_eq!(stub.version.as_deref(), Some("1.0.0"));
690    }
691
692    // -----------------------------------------------------------------------
693    // Test 8: qualified exports (to specific modules)
694    // -----------------------------------------------------------------------
695
696    #[test]
697    fn test_qualified_exports() {
698        let mut builder = ModuleBuilder::new("com.example.lib");
699        builder.add_exports(
700            "com/example/internal",
701            0,
702            &["com.example.app", "com.example.test"],
703        );
704
705        let bytes = builder.build();
706        let stub = parse_and_extract(&bytes).unwrap().unwrap();
707
708        assert_eq!(stub.exports.len(), 1);
709        assert_eq!(stub.exports[0].package, "com.example.internal");
710        assert_eq!(stub.exports[0].to_modules.len(), 2);
711        assert_eq!(stub.exports[0].to_modules[0], "com.example.app");
712        assert_eq!(stub.exports[0].to_modules[1], "com.example.test");
713    }
714
715    // -----------------------------------------------------------------------
716    // Test 9: comprehensive module with all directive types
717    // -----------------------------------------------------------------------
718
719    #[test]
720    fn test_comprehensive_module() {
721        let mut builder = ModuleBuilder::new("com.example.full");
722        builder
723            .add_requires("java.base", 0x8000, Some("17"))
724            .add_requires("java.logging", 0x0020, None)
725            .add_exports("com/example/api", 0, &[])
726            .add_exports("com/example/spi", 0, &["com.example.impl"])
727            .add_opens("com/example/internal", 0, &[])
728            .add_uses("com/example/spi/Plugin")
729            .add_provides(
730                "com/example/spi/Plugin",
731                &["com/example/impl/DefaultPlugin"],
732            );
733
734        let bytes = builder.build();
735        let stub = parse_and_extract(&bytes).unwrap().unwrap();
736
737        assert_eq!(stub.name, "com.example.full");
738        assert_eq!(stub.requires.len(), 2);
739        assert_eq!(stub.exports.len(), 2);
740        assert_eq!(stub.opens.len(), 1);
741        assert_eq!(stub.uses.len(), 1);
742        assert_eq!(stub.uses[0], "com.example.spi.Plugin");
743        assert_eq!(stub.provides.len(), 1);
744        assert_eq!(stub.provides[0].service, "com.example.spi.Plugin");
745        assert_eq!(
746            stub.provides[0].implementations,
747            vec!["com.example.impl.DefaultPlugin"]
748        );
749    }
750
751    // -----------------------------------------------------------------------
752    // Test 10: empty module (no directives)
753    // -----------------------------------------------------------------------
754
755    #[test]
756    fn test_empty_module() {
757        let builder = ModuleBuilder::new("com.example.empty");
758
759        let bytes = builder.build();
760        let stub = parse_and_extract(&bytes).unwrap().unwrap();
761
762        assert_eq!(stub.name, "com.example.empty");
763        assert!(stub.requires.is_empty());
764        assert!(stub.exports.is_empty());
765        assert!(stub.opens.is_empty());
766        assert!(stub.uses.is_empty());
767        assert!(stub.provides.is_empty());
768        assert!(stub.version.is_none());
769    }
770
771    // -----------------------------------------------------------------------
772    // Test 11: requires with ACC_STATIC_PHASE
773    // -----------------------------------------------------------------------
774
775    #[test]
776    fn test_requires_static_phase() {
777        let mut builder = ModuleBuilder::new("com.example.compile");
778        // ACC_STATIC_PHASE = 0x0040
779        builder.add_requires("org.checkerframework.checker.qual", 0x0040, None);
780
781        let bytes = builder.build();
782        let stub = parse_and_extract(&bytes).unwrap().unwrap();
783
784        assert_eq!(stub.requires.len(), 1);
785        assert_eq!(
786            stub.requires[0].module_name,
787            "org.checkerframework.checker.qual"
788        );
789        assert!(stub.requires[0].access.contains(0x0040)); // ACC_STATIC_PHASE
790    }
791}