Skip to main content

sqry_classpath/scala/
signature.rs

1//! Binary format reader for the Scala 2 signature format.
2//!
3//! The `@ScalaSignature` annotation embeds a binary-encoded table of symbol
4//! entries containing Scala-specific type information. This module parses the
5//! entry table from the raw bytes and provides accessors for individual entries.
6//!
7//! # Format overview
8//!
9//! The Scala 2 signature format (version 5.x) consists of:
10//! 1. A 2-byte header: major version (1 byte) + minor version (1 byte)
11//! 2. A `Nat`-encoded entry count
12//! 3. A sequence of tagged entries: `tag (1 byte) | length (Nat) | data[length]`
13//!
14//! `Nat` is a variable-length encoding where each byte contributes 7 bits and
15//! the high bit indicates continuation (similar to LEB128 unsigned).
16
17// Scala signature table indices fit in u32; casts are intentional
18#![allow(clippy::cast_possible_truncation)]
19
20use log::warn;
21
22// ---------------------------------------------------------------------------
23// Entry tags (Scala 2 signature format)
24// ---------------------------------------------------------------------------
25
26/// Name entry containing UTF-8 bytes (term-level name).
27pub const TAG_TERM_NAME: u8 = 1;
28/// Name entry containing UTF-8 bytes (type-level name).
29pub const TAG_TYPE_NAME: u8 = 2;
30/// Empty symbol (no data).
31pub const TAG_NONE_SYM: u8 = 3;
32/// Type symbol.
33pub const TAG_TYPE_SYM: u8 = 4;
34/// Type alias symbol.
35pub const TAG_ALIAS_SYM: u8 = 5;
36/// Class symbol.
37pub const TAG_CLASS_SYM: u8 = 6;
38/// Module (object) symbol.
39pub const TAG_MODULE_SYM: u8 = 7;
40/// Value symbol.
41pub const TAG_VAL_SYM: u8 = 8;
42/// External reference (name + optional owner).
43pub const TAG_EXT_REF: u8 = 9;
44/// External module-class reference (name + optional owner).
45pub const TAG_EXT_MOD_CLASS_REF: u8 = 10;
46
47// ---------------------------------------------------------------------------
48// Symbol flags (relevant bits for Tier 1)
49// ---------------------------------------------------------------------------
50
51/// Bit 2: `private` visibility.
52pub const FLAG_PRIVATE: u64 = 1 << 2;
53/// Bit 3: `protected` visibility.
54pub const FLAG_PROTECTED: u64 = 1 << 3;
55/// Bit 5: `sealed` modifier.
56pub const FLAG_SEALED: u64 = 1 << 5;
57/// Bit 7: `case` modifier (case class / case object).
58pub const FLAG_CASE: u64 = 1 << 7;
59/// Bit 8: `abstract` modifier.
60pub const FLAG_ABSTRACT: u64 = 1 << 8;
61/// Bit 11: module (object) flag.
62pub const FLAG_MODULE: u64 = 1 << 11;
63/// Bit 13: interface flag (trait from JVM perspective).
64pub const FLAG_INTERFACE: u64 = 1 << 13;
65/// Bit 36: actual trait flag.
66pub const FLAG_TRAIT: u64 = 1 << 36;
67
68// ---------------------------------------------------------------------------
69// SignatureEntry
70// ---------------------------------------------------------------------------
71
72/// A single entry in the Scala signature entry table.
73#[derive(Debug, Clone)]
74pub struct SignatureEntry {
75    /// Tag byte identifying the entry kind.
76    pub tag: u8,
77    /// Raw data bytes for this entry (excluding tag and length).
78    pub data: Vec<u8>,
79}
80
81// ---------------------------------------------------------------------------
82// ScalaSymbolInfo
83// ---------------------------------------------------------------------------
84
85/// Decoded symbol info from a `CLASSsym` or `MODULEsym` entry.
86#[derive(Debug, Clone)]
87pub struct ScalaSymbolInfo {
88    /// Index of the name entry in the entry table.
89    pub name_index: usize,
90    /// Index of the owning symbol entry.
91    pub owner_index: usize,
92    /// Symbol flags (visibility, modifiers, etc.).
93    pub flags: u64,
94    /// Index of the type-info entry.
95    pub info_index: usize,
96}
97
98// ---------------------------------------------------------------------------
99// ScalaSignatureReader
100// ---------------------------------------------------------------------------
101
102/// Reader for the Scala 2 signature binary format.
103///
104/// Parses the binary entry table from raw `@ScalaSignature` bytes and provides
105/// accessors for individual entries, name resolution, and symbol info extraction.
106#[derive(Debug)]
107pub struct ScalaSignatureReader {
108    /// Parsed entry table.
109    entries: Vec<SignatureEntry>,
110}
111
112impl ScalaSignatureReader {
113    /// Parse a Scala signature from raw bytes.
114    ///
115    /// Returns `None` if the bytes are too short, the version is unsupported,
116    /// or the entry table is malformed.
117    #[must_use]
118    pub fn parse(bytes: &[u8]) -> Option<Self> {
119        // Need at least 2 bytes for the version header.
120        if bytes.len() < 2 {
121            warn!("scala signature too short ({} bytes)", bytes.len());
122            return None;
123        }
124
125        let major = bytes[0];
126        let minor = bytes[1];
127
128        // We support major version 5 (Scala 2.10+).
129        if major != 5 {
130            warn!("unsupported Scala signature version {major}.{minor} (expected 5.x)");
131            return None;
132        }
133
134        let mut pos = 2;
135
136        // Read the entry count.
137        let entry_count = read_nat(bytes, &mut pos)? as usize;
138
139        let mut entries = Vec::with_capacity(entry_count);
140        for _ in 0..entry_count {
141            let entry = read_entry(bytes, &mut pos)?;
142            entries.push(entry);
143        }
144
145        Some(Self { entries })
146    }
147
148    /// Return the number of entries in the table.
149    #[must_use]
150    pub fn entry_count(&self) -> usize {
151        self.entries.len()
152    }
153
154    /// Get entry at the given index.
155    #[must_use]
156    pub fn entry(&self, index: usize) -> Option<&SignatureEntry> {
157        self.entries.get(index)
158    }
159
160    /// Read a name from a `TERMname` or `TYPEname` entry.
161    ///
162    /// Returns `None` if the index is out of bounds or the entry is not a name.
163    #[must_use]
164    pub fn read_name(&self, index: usize) -> Option<String> {
165        let entry = self.entry(index)?;
166        if entry.tag != TAG_TERM_NAME && entry.tag != TAG_TYPE_NAME {
167            return None;
168        }
169        String::from_utf8(entry.data.clone()).ok()
170    }
171
172    /// Read symbol info from a `CLASSsym` or `MODULEsym` entry.
173    ///
174    /// The symbol info format is:
175    /// ```text
176    /// name_ref (Nat) | owner_ref (Nat) | flags (LongNat) | [private_within_ref (Nat)] | info_ref (Nat)
177    /// ```
178    ///
179    /// Returns `None` if the entry data is malformed.
180    #[must_use]
181    pub fn read_symbol_info(&self, entry: &SignatureEntry) -> Option<ScalaSymbolInfo> {
182        if entry.tag != TAG_CLASS_SYM && entry.tag != TAG_MODULE_SYM {
183            return None;
184        }
185        parse_symbol_info(&entry.data)
186    }
187
188    /// Resolve the fully qualified name of a symbol by walking the owner chain.
189    ///
190    /// Returns `None` if the chain cannot be resolved (e.g., missing entries).
191    #[must_use]
192    #[allow(clippy::items_after_statements)] // Items near usage for clarity
193    #[allow(clippy::match_same_arms)] // Arms separated for documentation clarity
194    #[allow(clippy::manual_let_else)] // Match for error handling clarity
195    pub fn resolve_qualified_name(&self, sym_index: usize) -> Option<String> {
196        let entry = self.entry(sym_index)?;
197        let info = self.read_symbol_info(entry)?;
198        let name = self.read_name(info.name_index)?;
199
200        // Walk the owner chain to build segments.
201        let mut segments = vec![name];
202        let mut current_owner = info.owner_index;
203
204        // Limit depth to prevent infinite loops on malformed data.
205        const MAX_DEPTH: usize = 128;
206        for _ in 0..MAX_DEPTH {
207            let owner_entry = match self.entry(current_owner) {
208                Some(e) => e,
209                None => break,
210            };
211
212            match owner_entry.tag {
213                TAG_CLASS_SYM | TAG_MODULE_SYM => {
214                    if let Some(owner_info) = self.read_symbol_info(owner_entry) {
215                        if let Some(owner_name) = self.read_name(owner_info.name_index) {
216                            segments.push(owner_name);
217                            current_owner = owner_info.owner_index;
218                        } else {
219                            break;
220                        }
221                    } else {
222                        break;
223                    }
224                }
225                TAG_EXT_REF | TAG_EXT_MOD_CLASS_REF => {
226                    if let Some(ext_name) = self.read_ext_ref_name(owner_entry) {
227                        // Skip the root `<empty>` package marker.
228                        if ext_name != "<empty>" {
229                            segments.push(ext_name);
230                        }
231                    }
232                    break;
233                }
234                TAG_NONE_SYM => break,
235                _ => break,
236            }
237        }
238
239        segments.reverse();
240        Some(segments.join("."))
241    }
242
243    /// Read the name from an `EXTref` or `EXTMODCLASSref` entry.
244    ///
245    /// Format: `name_ref (Nat) [owner_ref (Nat)]`
246    #[must_use]
247    fn read_ext_ref_name(&self, entry: &SignatureEntry) -> Option<String> {
248        if entry.tag != TAG_EXT_REF && entry.tag != TAG_EXT_MOD_CLASS_REF {
249            return None;
250        }
251        let mut pos = 0;
252        let name_index = read_nat(&entry.data, &mut pos)? as usize;
253        self.read_name(name_index)
254    }
255
256    /// Read the owner index from an `EXTref` or `EXTMODCLASSref` entry.
257    ///
258    /// Returns `None` if there is no owner reference (only a name reference)
259    /// or the entry is not an external reference.
260    #[must_use]
261    pub fn read_ext_ref_owner(&self, entry: &SignatureEntry) -> Option<usize> {
262        if entry.tag != TAG_EXT_REF && entry.tag != TAG_EXT_MOD_CLASS_REF {
263            return None;
264        }
265        let mut pos = 0;
266        let _name_index = read_nat(&entry.data, &mut pos)?;
267        // Owner is optional — only present if there are remaining bytes.
268        if pos < entry.data.len() {
269            Some(read_nat(&entry.data, &mut pos)? as usize)
270        } else {
271            None
272        }
273    }
274
275    /// Find all `CLASSsym` and `MODULEsym` entries and their indices.
276    #[must_use]
277    pub fn class_and_module_symbols(&self) -> Vec<(usize, &SignatureEntry)> {
278        self.entries
279            .iter()
280            .enumerate()
281            .filter(|(_, e)| e.tag == TAG_CLASS_SYM || e.tag == TAG_MODULE_SYM)
282            .collect()
283    }
284
285    /// Find all `EXTref` and `EXTMODCLASSref` entries and their indices.
286    #[must_use]
287    pub fn ext_refs(&self) -> Vec<(usize, &SignatureEntry)> {
288        self.entries
289            .iter()
290            .enumerate()
291            .filter(|(_, e)| e.tag == TAG_EXT_REF || e.tag == TAG_EXT_MOD_CLASS_REF)
292            .collect()
293    }
294}
295
296// ---------------------------------------------------------------------------
297// Nat / LongNat encoding
298// ---------------------------------------------------------------------------
299
300/// Read a `Nat` (variable-length natural number) from a byte slice.
301///
302/// Each byte contributes 7 bits of value. The high bit (0x80) indicates that
303/// more bytes follow. Returns `None` if the data is truncated.
304pub fn read_nat(data: &[u8], pos: &mut usize) -> Option<u64> {
305    let mut result: u64 = 0;
306    let mut shift: u32 = 0;
307
308    loop {
309        if *pos >= data.len() {
310            return None;
311        }
312        let byte = data[*pos];
313        *pos += 1;
314
315        // Accumulate the low 7 bits.
316        let value = u64::from(byte & 0x7F);
317
318        // Guard against overflow (Nat should fit in u64 with up to 10 bytes).
319        result = result.checked_add(value.checked_shl(shift)?)?;
320        shift += 7;
321
322        // If the high bit is clear, we're done.
323        if byte & 0x80 == 0 {
324            return Some(result);
325        }
326
327        // Safety: 10 bytes can encode at most 70 bits; u64 is 64 bits.
328        if shift > 63 {
329            return None;
330        }
331    }
332}
333
334/// Read a `LongNat` (variable-length long natural) from a byte slice.
335///
336/// This is identical to `read_nat` in wire format but semantically represents
337/// a `Long`-sized value. The encoding is the same.
338pub fn read_long_nat(data: &[u8], pos: &mut usize) -> Option<u64> {
339    read_nat(data, pos)
340}
341
342// ---------------------------------------------------------------------------
343// Internal helpers
344// ---------------------------------------------------------------------------
345
346/// Read a single entry from the byte stream.
347fn read_entry(data: &[u8], pos: &mut usize) -> Option<SignatureEntry> {
348    if *pos >= data.len() {
349        return None;
350    }
351    let tag = data[*pos];
352    *pos += 1;
353
354    let length = read_nat(data, pos)? as usize;
355
356    // Bounds check.
357    if *pos + length > data.len() {
358        return None;
359    }
360
361    let entry_data = data[*pos..*pos + length].to_vec();
362    *pos += length;
363
364    Some(SignatureEntry {
365        tag,
366        data: entry_data,
367    })
368}
369
370/// Parse symbol info fields from raw entry data.
371///
372/// Format: `name_ref (Nat) | owner_ref (Nat) | flags (LongNat) | [private_within_ref (Nat)] | info_ref (Nat)`
373///
374/// The `private_within_ref` field is present when the PRIVATE or PROTECTED flag
375/// has a qualifier (e.g., `private[pkg]`). We detect its presence by reading
376/// all remaining Nats after flags and taking the last one as `info_ref`.
377fn parse_symbol_info(data: &[u8]) -> Option<ScalaSymbolInfo> {
378    let mut pos = 0;
379    let name_index = read_nat(data, &mut pos)? as usize;
380    let owner_index = read_nat(data, &mut pos)? as usize;
381    let flags = read_long_nat(data, &mut pos)?;
382
383    // After flags, the remaining data contains either:
384    //   info_ref                        (no private_within qualifier)
385    //   private_within_ref | info_ref   (with qualifier)
386    //
387    // We read all remaining Nats and pick the last one as info_ref.
388    let mut remaining_nats = Vec::new();
389    while pos < data.len() {
390        match read_nat(data, &mut pos) {
391            Some(v) => remaining_nats.push(v as usize),
392            None => break,
393        }
394    }
395
396    // At minimum we need info_ref.
397    let info_index = remaining_nats.pop().unwrap_or(0);
398
399    Some(ScalaSymbolInfo {
400        name_index,
401        owner_index,
402        flags,
403        info_index,
404    })
405}
406
407// ---------------------------------------------------------------------------
408// Tests
409// ---------------------------------------------------------------------------
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414
415    // -- Nat encoding helpers -----------------------------------------------
416
417    /// Encode a u64 as a Nat (variable-length).
418    fn encode_nat(mut value: u64) -> Vec<u8> {
419        let mut bytes = Vec::new();
420        loop {
421            let mut byte = (value & 0x7F) as u8;
422            value >>= 7;
423            if value != 0 {
424                byte |= 0x80;
425            }
426            bytes.push(byte);
427            if value == 0 {
428                break;
429            }
430        }
431        bytes
432    }
433
434    /// Build a minimal entry: tag + Nat-encoded length + data.
435    fn build_entry(tag: u8, data: &[u8]) -> Vec<u8> {
436        let mut entry = vec![tag];
437        entry.extend(encode_nat(data.len() as u64));
438        entry.extend_from_slice(data);
439        entry
440    }
441
442    /// Build a complete signature with version header + entry count + entries.
443    fn build_signature(entries: Vec<Vec<u8>>) -> Vec<u8> {
444        let mut buf = vec![5, 0]; // version 5.0
445        buf.extend(encode_nat(entries.len() as u64));
446        for entry in entries {
447            buf.extend(entry);
448        }
449        buf
450    }
451
452    // -- Nat encoding/decoding tests ----------------------------------------
453
454    #[test]
455    fn nat_single_byte() {
456        let data = [42];
457        let mut pos = 0;
458        assert_eq!(read_nat(&data, &mut pos), Some(42));
459        assert_eq!(pos, 1);
460    }
461
462    #[test]
463    fn nat_zero() {
464        let data = [0];
465        let mut pos = 0;
466        assert_eq!(read_nat(&data, &mut pos), Some(0));
467        assert_eq!(pos, 1);
468    }
469
470    #[test]
471    fn nat_max_single_byte() {
472        let data = [127];
473        let mut pos = 0;
474        assert_eq!(read_nat(&data, &mut pos), Some(127));
475        assert_eq!(pos, 1);
476    }
477
478    #[test]
479    fn nat_two_bytes() {
480        // 128 = 0x80 → encoded as [0x80, 0x01]
481        // byte 0: 0x80 → low 7 bits = 0, continuation
482        // byte 1: 0x01 → 1 << 7 = 128
483        let data = [0x80, 0x01];
484        let mut pos = 0;
485        assert_eq!(read_nat(&data, &mut pos), Some(128));
486        assert_eq!(pos, 2);
487    }
488
489    #[test]
490    fn nat_multi_byte_300() {
491        // 300 = 0x12C → low 7 bits = 0x2C (44), remaining = 2
492        // encoded as [0xAC, 0x02]
493        let data = [0xAC, 0x02];
494        let mut pos = 0;
495        assert_eq!(read_nat(&data, &mut pos), Some(300));
496        assert_eq!(pos, 2);
497    }
498
499    #[test]
500    fn nat_round_trip() {
501        for value in [0, 1, 127, 128, 255, 300, 16383, 16384, 65535, 1_000_000] {
502            let encoded = encode_nat(value);
503            let mut pos = 0;
504            assert_eq!(
505                read_nat(&encoded, &mut pos),
506                Some(value),
507                "round-trip failed for {value}"
508            );
509            assert_eq!(pos, encoded.len());
510        }
511    }
512
513    #[test]
514    fn nat_truncated_returns_none() {
515        // Continuation byte with no follow-up.
516        let data = [0x80];
517        let mut pos = 0;
518        assert_eq!(read_nat(&data, &mut pos), None);
519    }
520
521    #[test]
522    fn nat_empty_returns_none() {
523        let data: [u8; 0] = [];
524        let mut pos = 0;
525        assert_eq!(read_nat(&data, &mut pos), None);
526    }
527
528    // -- Entry table parsing tests ------------------------------------------
529
530    #[test]
531    fn parse_empty_signature() {
532        let sig = build_signature(vec![]);
533        let reader = ScalaSignatureReader::parse(&sig).unwrap();
534        assert_eq!(reader.entry_count(), 0);
535    }
536
537    #[test]
538    fn parse_name_entries() {
539        let name_data = b"MyClass".to_vec();
540        let name_entry = build_entry(TAG_TYPE_NAME, &name_data);
541
542        let sig = build_signature(vec![name_entry]);
543        let reader = ScalaSignatureReader::parse(&sig).unwrap();
544
545        assert_eq!(reader.entry_count(), 1);
546        assert_eq!(reader.read_name(0), Some("MyClass".to_string()));
547    }
548
549    #[test]
550    fn parse_term_name() {
551        let name_data = b"myVal".to_vec();
552        let name_entry = build_entry(TAG_TERM_NAME, &name_data);
553
554        let sig = build_signature(vec![name_entry]);
555        let reader = ScalaSignatureReader::parse(&sig).unwrap();
556
557        assert_eq!(reader.read_name(0), Some("myVal".to_string()));
558    }
559
560    #[test]
561    fn read_name_wrong_tag_returns_none() {
562        let entry = build_entry(TAG_CLASS_SYM, b"data");
563        let sig = build_signature(vec![entry]);
564        let reader = ScalaSignatureReader::parse(&sig).unwrap();
565
566        assert_eq!(reader.read_name(0), None);
567    }
568
569    #[test]
570    fn read_name_out_of_bounds_returns_none() {
571        let sig = build_signature(vec![]);
572        let reader = ScalaSignatureReader::parse(&sig).unwrap();
573        assert_eq!(reader.read_name(0), None);
574    }
575
576    #[test]
577    fn parse_class_sym_entry() {
578        // Build: name entry at 0, owner (NONEsym) at 1, CLASSsym at 2.
579        let name = build_entry(TAG_TYPE_NAME, b"Point");
580        let owner = build_entry(TAG_NONE_SYM, &[]);
581
582        // CLASSsym data: name_ref=0, owner_ref=1, flags=CASE(bit7)=128, info_ref=0
583        let mut sym_data = Vec::new();
584        sym_data.extend(encode_nat(0)); // name_ref
585        sym_data.extend(encode_nat(1)); // owner_ref
586        sym_data.extend(encode_nat(FLAG_CASE)); // flags
587        sym_data.extend(encode_nat(0)); // info_ref
588        let class_sym = build_entry(TAG_CLASS_SYM, &sym_data);
589
590        let sig = build_signature(vec![name, owner, class_sym]);
591        let reader = ScalaSignatureReader::parse(&sig).unwrap();
592
593        let entry = reader.entry(2).unwrap();
594        assert_eq!(entry.tag, TAG_CLASS_SYM);
595
596        let info = reader.read_symbol_info(entry).unwrap();
597        assert_eq!(info.name_index, 0);
598        assert_eq!(info.owner_index, 1);
599        assert_eq!(info.flags & FLAG_CASE, FLAG_CASE);
600        assert_eq!(reader.read_name(info.name_index), Some("Point".to_string()));
601    }
602
603    #[test]
604    fn parse_module_sym_entry() {
605        let name = build_entry(TAG_TERM_NAME, b"Config");
606        let owner = build_entry(TAG_NONE_SYM, &[]);
607
608        let mut sym_data = Vec::new();
609        sym_data.extend(encode_nat(0)); // name_ref
610        sym_data.extend(encode_nat(1)); // owner_ref
611        sym_data.extend(encode_nat(FLAG_MODULE)); // flags (MODULE)
612        sym_data.extend(encode_nat(0)); // info_ref
613        let mod_sym = build_entry(TAG_MODULE_SYM, &sym_data);
614
615        let sig = build_signature(vec![name, owner, mod_sym]);
616        let reader = ScalaSignatureReader::parse(&sig).unwrap();
617
618        let entry = reader.entry(2).unwrap();
619        assert_eq!(entry.tag, TAG_MODULE_SYM);
620
621        let info = reader.read_symbol_info(entry).unwrap();
622        assert_eq!(info.flags & FLAG_MODULE, FLAG_MODULE);
623    }
624
625    #[test]
626    fn class_and_module_symbols_finds_all() {
627        let name1 = build_entry(TAG_TYPE_NAME, b"A");
628        let name2 = build_entry(TAG_TERM_NAME, b"B");
629        let owner = build_entry(TAG_NONE_SYM, &[]);
630
631        let mut cls_data = Vec::new();
632        cls_data.extend(encode_nat(0));
633        cls_data.extend(encode_nat(2));
634        cls_data.extend(encode_nat(0));
635        cls_data.extend(encode_nat(0));
636        let cls = build_entry(TAG_CLASS_SYM, &cls_data);
637
638        let mut mod_data = Vec::new();
639        mod_data.extend(encode_nat(1));
640        mod_data.extend(encode_nat(2));
641        mod_data.extend(encode_nat(0));
642        mod_data.extend(encode_nat(0));
643        let module = build_entry(TAG_MODULE_SYM, &mod_data);
644
645        let sig = build_signature(vec![name1, name2, owner, cls, module]);
646        let reader = ScalaSignatureReader::parse(&sig).unwrap();
647
648        let symbols = reader.class_and_module_symbols();
649        assert_eq!(symbols.len(), 2);
650        assert_eq!(symbols[0].0, 3); // CLASSsym
651        assert_eq!(symbols[1].0, 4); // MODULEsym
652    }
653
654    #[test]
655    fn ext_ref_name_resolution() {
656        let name = build_entry(TAG_TERM_NAME, b"scala");
657        let mut ext_data = Vec::new();
658        ext_data.extend(encode_nat(0)); // name_ref
659        let ext = build_entry(TAG_EXT_REF, &ext_data);
660
661        let sig = build_signature(vec![name, ext]);
662        let reader = ScalaSignatureReader::parse(&sig).unwrap();
663
664        let ext_entry = reader.entry(1).unwrap();
665        assert_eq!(
666            reader.read_ext_ref_name(ext_entry),
667            Some("scala".to_string())
668        );
669    }
670
671    #[test]
672    fn ext_ref_with_owner() {
673        let name = build_entry(TAG_TERM_NAME, b"Option");
674        let owner_name = build_entry(TAG_TERM_NAME, b"scala");
675        let mut owner_ext_data = Vec::new();
676        owner_ext_data.extend(encode_nat(1)); // name_ref → "scala"
677        let owner_ext = build_entry(TAG_EXT_MOD_CLASS_REF, &owner_ext_data);
678
679        let mut ext_data = Vec::new();
680        ext_data.extend(encode_nat(0)); // name_ref → "Option"
681        ext_data.extend(encode_nat(2)); // owner_ref → ext at index 2
682        let ext = build_entry(TAG_EXT_REF, &ext_data);
683
684        let sig = build_signature(vec![name, owner_name, owner_ext, ext]);
685        let reader = ScalaSignatureReader::parse(&sig).unwrap();
686
687        let entry = reader.entry(3).unwrap();
688        assert_eq!(reader.read_ext_ref_owner(entry), Some(2));
689    }
690
691    // -- Version handling ---------------------------------------------------
692
693    #[test]
694    fn unsupported_major_version_returns_none() {
695        let mut sig = build_signature(vec![]);
696        sig[0] = 4; // wrong major version
697        assert!(ScalaSignatureReader::parse(&sig).is_none());
698    }
699
700    #[test]
701    fn too_short_returns_none() {
702        assert!(ScalaSignatureReader::parse(&[5]).is_none());
703        assert!(ScalaSignatureReader::parse(&[]).is_none());
704    }
705
706    // -- Malformed data handling --------------------------------------------
707
708    #[test]
709    fn truncated_entry_returns_none() {
710        // Header + entry count = 1, but the entry data is truncated.
711        let mut data = vec![5, 0]; // version
712        data.extend(encode_nat(1)); // 1 entry
713        data.push(TAG_TYPE_NAME); // tag
714        data.extend(encode_nat(100)); // length = 100 (but no data follows)
715        assert!(ScalaSignatureReader::parse(&data).is_none());
716    }
717
718    #[test]
719    fn symbol_info_from_non_symbol_returns_none() {
720        let name = build_entry(TAG_TYPE_NAME, b"Foo");
721        let sig = build_signature(vec![name]);
722        let reader = ScalaSignatureReader::parse(&sig).unwrap();
723
724        let entry = reader.entry(0).unwrap();
725        assert!(reader.read_symbol_info(entry).is_none());
726    }
727
728    // -- Flags tests --------------------------------------------------------
729
730    #[test]
731    fn trait_flag_detection() {
732        let name = build_entry(TAG_TYPE_NAME, b"Functor");
733        let owner = build_entry(TAG_NONE_SYM, &[]);
734
735        // Traits have both INTERFACE and TRAIT flags set, plus ABSTRACT.
736        let flags = FLAG_TRAIT | FLAG_INTERFACE | FLAG_ABSTRACT;
737        let mut sym_data = Vec::new();
738        sym_data.extend(encode_nat(0)); // name_ref
739        sym_data.extend(encode_nat(1)); // owner_ref
740        sym_data.extend(encode_nat(flags)); // flags
741        sym_data.extend(encode_nat(0)); // info_ref
742        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
743
744        let sig = build_signature(vec![name, owner, cls]);
745        let reader = ScalaSignatureReader::parse(&sig).unwrap();
746
747        let entry = reader.entry(2).unwrap();
748        let info = reader.read_symbol_info(entry).unwrap();
749        assert_ne!(info.flags & FLAG_TRAIT, 0);
750        assert_ne!(info.flags & FLAG_INTERFACE, 0);
751        assert_ne!(info.flags & FLAG_ABSTRACT, 0);
752    }
753
754    #[test]
755    fn sealed_flag_detection() {
756        let name = build_entry(TAG_TYPE_NAME, b"Expr");
757        let owner = build_entry(TAG_NONE_SYM, &[]);
758
759        let flags = FLAG_SEALED | FLAG_ABSTRACT | FLAG_TRAIT | FLAG_INTERFACE;
760        let mut sym_data = Vec::new();
761        sym_data.extend(encode_nat(0));
762        sym_data.extend(encode_nat(1));
763        sym_data.extend(encode_nat(flags));
764        sym_data.extend(encode_nat(0));
765        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
766
767        let sig = build_signature(vec![name, owner, cls]);
768        let reader = ScalaSignatureReader::parse(&sig).unwrap();
769
770        let entry = reader.entry(2).unwrap();
771        let info = reader.read_symbol_info(entry).unwrap();
772        assert_ne!(info.flags & FLAG_SEALED, 0);
773    }
774
775    #[test]
776    fn private_and_protected_flags() {
777        // Private class.
778        let name = build_entry(TAG_TYPE_NAME, b"Inner");
779        let owner = build_entry(TAG_NONE_SYM, &[]);
780
781        let mut sym_data = Vec::new();
782        sym_data.extend(encode_nat(0));
783        sym_data.extend(encode_nat(1));
784        sym_data.extend(encode_nat(FLAG_PRIVATE));
785        sym_data.extend(encode_nat(0));
786        let cls = build_entry(TAG_CLASS_SYM, &sym_data);
787
788        let sig = build_signature(vec![name, owner, cls]);
789        let reader = ScalaSignatureReader::parse(&sig).unwrap();
790        let entry = reader.entry(2).unwrap();
791        let info = reader.read_symbol_info(entry).unwrap();
792        assert_ne!(info.flags & FLAG_PRIVATE, 0);
793        assert_eq!(info.flags & FLAG_PROTECTED, 0);
794    }
795
796    #[test]
797    fn large_nat_flag_value() {
798        // Test that FLAG_TRAIT (bit 36) survives encoding/decoding.
799        let encoded = encode_nat(FLAG_TRAIT);
800        let mut pos = 0;
801        let decoded = read_nat(&encoded, &mut pos).unwrap();
802        assert_eq!(decoded, FLAG_TRAIT);
803        assert_eq!(decoded, 1 << 36);
804    }
805}