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