Skip to main content

sqry_classpath/kotlin/
decoder.rs

1//! Protobuf wire-format reader and Kotlin metadata extraction.
2//!
3//! This module implements a minimal protobuf wire-format decoder that reads
4//! the binary blob stored in the `d1` field of `@kotlin.Metadata` annotations.
5//! It extracts Kotlin-specific semantic information that the JVM bytecode
6//! representation erases: extension receivers, nullable types, companion
7//! objects, class kind (object/data/sealed), and visibility.
8//!
9//! # Wire format
10//!
11//! Protobuf encodes each field as `(field_number << 3) | wire_type`:
12//! - Wire type 0: varint (7 bits per byte, MSB = continuation)
13//! - Wire type 1: 64-bit fixed
14//! - Wire type 2: length-delimited (varint length prefix + bytes)
15//! - Wire type 5: 32-bit fixed
16//!
17//! We only read the specific field numbers defined in `kotlin.metadata.jvm.proto`
18//! and skip everything else, making this forward-compatible with new fields.
19
20// Kotlin protobuf metadata indices fit in u32; casts are intentional
21#![allow(clippy::cast_possible_truncation)]
22
23use crate::stub::model::KotlinMetadataStub;
24
25// ---------------------------------------------------------------------------
26// Public types
27// ---------------------------------------------------------------------------
28
29/// Decoded Kotlin metadata for a class.
30///
31/// Contains the Kotlin-specific information that is erased during bytecode
32/// compilation. This is used to enrich graph nodes with accurate visibility,
33/// class kind, and relationship edges (extension receivers, companion objects).
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct KotlinClassMetadata {
36    /// Kotlin class kind (Class, Object, `CompanionObject`, Interface, etc.).
37    pub kind: KotlinClassKind,
38    /// Kotlin visibility (public, private, protected, internal).
39    pub visibility: KotlinVisibility,
40    /// Whether this is a data class (has `data` modifier).
41    pub is_data: bool,
42    /// Whether this is a sealed class (has `sealed` modifier — modality=3).
43    pub is_sealed: bool,
44    /// Companion object name (if this class has one).
45    ///
46    /// Most companion objects are named `"Companion"`, but Kotlin allows
47    /// custom names: `companion object Factory { ... }`.
48    pub companion_object_name: Option<String>,
49    /// Extension functions declared in this class with their receiver types.
50    pub extension_functions: Vec<KotlinExtensionFunction>,
51    /// Property names whose types are nullable (`T?`).
52    pub nullable_properties: Vec<String>,
53}
54
55/// Kotlin class kind as encoded in the metadata flags (bits 9-11).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum KotlinClassKind {
58    /// Regular class.
59    Class,
60    /// Interface (including functional interfaces).
61    Interface,
62    /// Enum class.
63    EnumClass,
64    /// Enum entry (individual constant).
65    EnumEntry,
66    /// Annotation class.
67    AnnotationClass,
68    /// Singleton `object` declaration.
69    Object,
70    /// Companion object.
71    CompanionObject,
72}
73
74/// Kotlin visibility as encoded in the metadata flags (bits 3-5).
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
76pub enum KotlinVisibility {
77    /// Visible within the module.
78    Internal,
79    /// Visible only within the declaring class.
80    Private,
81    /// Visible to subclasses.
82    Protected,
83    /// Visible everywhere.
84    Public,
85    /// `private` scoped to `this` (backing field access).
86    PrivateToThis,
87    /// Local declaration (inside a function body).
88    Local,
89}
90
91/// An extension function with its receiver type.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct KotlinExtensionFunction {
94    /// Function name.
95    pub name: String,
96    /// Fully qualified receiver type (e.g., `"kotlin/String"`).
97    pub receiver_type: String,
98    /// Whether the receiver type is nullable.
99    pub receiver_nullable: bool,
100}
101
102// ---------------------------------------------------------------------------
103// Wire format constants
104// ---------------------------------------------------------------------------
105
106/// Protobuf wire types.
107const WIRE_VARINT: u8 = 0;
108const WIRE_64BIT: u8 = 1;
109const WIRE_LENGTH_DELIMITED: u8 = 2;
110const WIRE_32BIT: u8 = 5;
111
112/// Class message field numbers (from `kotlin.metadata.jvm.proto`).
113const CLASS_FLAGS: u32 = 1;
114const CLASS_FUNCTIONS: u32 = 14;
115const CLASS_PROPERTIES: u32 = 15;
116const CLASS_COMPANION_OBJECT_NAME: u32 = 20;
117
118/// Function message field numbers.
119const FUNCTION_FLAGS: u32 = 1;
120const FUNCTION_NAME: u32 = 2;
121const FUNCTION_RECEIVER_TYPE: u32 = 6;
122
123/// Property message field numbers.
124const PROPERTY_FLAGS: u32 = 1;
125const PROPERTY_NAME: u32 = 2;
126const PROPERTY_RETURN_TYPE: u32 = 5;
127
128/// Type message field numbers.
129const TYPE_FLAGS: u32 = 1;
130const TYPE_CLASS_NAME: u32 = 6;
131
132// ---------------------------------------------------------------------------
133// Flag extraction helpers
134// ---------------------------------------------------------------------------
135
136/// Extract visibility from a flags varint (bits 3-5, zero-indexed).
137fn extract_visibility(flags: u64) -> KotlinVisibility {
138    #[allow(clippy::match_same_arms)] // Arms separated for documentation clarity
139    match (flags >> 3) & 0x7 {
140        #[allow(clippy::match_same_arms)] // Kotlin metadata flag arms separated by semantic meaning
141        0 => KotlinVisibility::Internal,
142        1 => KotlinVisibility::Private,
143        2 => KotlinVisibility::Protected,
144        3 => KotlinVisibility::Public,
145        4 => KotlinVisibility::PrivateToThis,
146        5 => KotlinVisibility::Local,
147        _ => KotlinVisibility::Public, // unknown → default to public
148    }
149}
150
151/// Extract modality from a flags varint (bits 6-8, zero-indexed).
152fn extract_modality(flags: u64) -> u8 {
153    ((flags >> 6) & 0x7) as u8
154}
155
156/// Extract class kind from a flags varint (bits 9-11, zero-indexed).
157#[allow(clippy::match_same_arms)] // Arms separated for documentation clarity
158fn extract_class_kind(flags: u64) -> KotlinClassKind {
159    match (flags >> 9) & 0x7 {
160        0 => KotlinClassKind::Class,
161        1 => KotlinClassKind::Interface,
162        2 => KotlinClassKind::EnumClass,
163        3 => KotlinClassKind::EnumEntry,
164        4 => KotlinClassKind::AnnotationClass,
165        5 => KotlinClassKind::Object,
166        6 => KotlinClassKind::CompanionObject,
167        _ => KotlinClassKind::Class, // unknown → default to class
168    }
169}
170
171// ---------------------------------------------------------------------------
172// Wire format reader
173// ---------------------------------------------------------------------------
174
175/// Minimal protobuf wire-format reader.
176///
177/// Supports varint, length-delimited, and fixed-width field types. Does not
178/// allocate — all returned byte slices borrow from the input buffer.
179struct WireReader<'a> {
180    data: &'a [u8],
181    pos: usize,
182}
183
184impl<'a> WireReader<'a> {
185    /// Create a new reader over the given byte slice.
186    fn new(data: &'a [u8]) -> Self {
187        Self { data, pos: 0 }
188    }
189
190    /// Returns `true` if there are more bytes to read.
191    fn has_remaining(&self) -> bool {
192        self.pos < self.data.len()
193    }
194
195    /// Read a varint (up to 64 bits, 10 bytes max).
196    ///
197    /// Returns `None` if the input is truncated or the varint exceeds 10 bytes
198    /// (malformed data).
199    fn read_varint(&mut self) -> Option<u64> {
200        let mut result: u64 = 0;
201        let mut shift: u32 = 0;
202
203        for _ in 0..10 {
204            if self.pos >= self.data.len() {
205                return None;
206            }
207            let byte = self.data[self.pos];
208            self.pos += 1;
209
210            result |= u64::from(byte & 0x7F) << shift;
211            if byte & 0x80 == 0 {
212                return Some(result);
213            }
214            shift += 7;
215        }
216
217        // Varint exceeds 10 bytes — malformed.
218        None
219    }
220
221    /// Read a field tag and decompose it into `(field_number, wire_type)`.
222    ///
223    /// Returns `None` at EOF or on malformed input.
224    fn read_tag(&mut self) -> Option<(u32, u8)> {
225        let raw = self.read_varint()?;
226        let wire_type = (raw & 0x7) as u8;
227        let field_number = (raw >> 3) as u32;
228        if field_number == 0 {
229            return None; // field number 0 is invalid
230        }
231        Some((field_number, wire_type))
232    }
233
234    /// Read a length-delimited field (varint length prefix + payload bytes).
235    ///
236    /// Returns the payload as a borrowed byte slice, or `None` if truncated.
237    fn read_length_delimited(&mut self) -> Option<&'a [u8]> {
238        let len = self.read_varint()? as usize;
239        if self.pos + len > self.data.len() {
240            return None;
241        }
242        let slice = &self.data[self.pos..self.pos + len];
243        self.pos += len;
244        Some(slice)
245    }
246
247    /// Skip a field of the given wire type.
248    ///
249    /// Returns `false` if the data is malformed (truncated or unknown wire type).
250    fn skip_field(&mut self, wire_type: u8) -> bool {
251        match wire_type {
252            WIRE_VARINT => self.read_varint().is_some(),
253            WIRE_64BIT => {
254                if self.pos + 8 > self.data.len() {
255                    return false;
256                }
257                self.pos += 8;
258                true
259            }
260            WIRE_LENGTH_DELIMITED => self.read_length_delimited().is_some(),
261            WIRE_32BIT => {
262                if self.pos + 4 > self.data.len() {
263                    return false;
264                }
265                self.pos += 4;
266                true
267            }
268            _ => false, // unknown wire type
269        }
270    }
271}
272
273// ---------------------------------------------------------------------------
274// String table
275// ---------------------------------------------------------------------------
276
277/// Look up a string in the `d2` string table by index.
278///
279/// Returns `None` if the index is out of bounds.
280fn string_table_lookup(d2: &[String], index: u64) -> Option<&str> {
281    let idx = index as usize;
282    d2.get(idx).map(String::as_str)
283}
284
285// ---------------------------------------------------------------------------
286// Type message decoder
287// ---------------------------------------------------------------------------
288
289/// Decoded information from a `Type` protobuf message.
290struct DecodedType {
291    /// Whether the type is nullable (`T?`).
292    nullable: bool,
293    /// Class name index into the string table.
294    class_name_index: Option<u64>,
295}
296
297/// Decode a `Type` message from its raw protobuf bytes.
298///
299/// Extracts the nullable flag (bit 1 of field 1) and the class name index
300/// (field 6).
301fn decode_type(data: &[u8]) -> Option<DecodedType> {
302    let mut reader = WireReader::new(data);
303    let mut flags: u64 = 0;
304    let mut class_name_index: Option<u64> = None;
305
306    while reader.has_remaining() {
307        let (field_number, wire_type) = reader.read_tag()?;
308
309        match (field_number, wire_type) {
310            (TYPE_FLAGS, WIRE_VARINT) => {
311                flags = reader.read_varint()?;
312            }
313            (TYPE_CLASS_NAME, WIRE_VARINT) => {
314                class_name_index = Some(reader.read_varint()?);
315            }
316            _ => {
317                if !reader.skip_field(wire_type) {
318                    return None;
319                }
320            }
321        }
322    }
323
324    Some(DecodedType {
325        nullable: (flags >> 1) & 1 == 1,
326        class_name_index,
327    })
328}
329
330// ---------------------------------------------------------------------------
331// Function message decoder
332// ---------------------------------------------------------------------------
333
334/// Decoded information from a `Function` message.
335struct DecodedFunction {
336    /// Function name index into the string table.
337    name_index: u64,
338    /// Receiver type bytes (present only for extension functions).
339    receiver_type: Option<DecodedType>,
340}
341
342/// Decode a `Function` message from its raw protobuf bytes.
343///
344/// Extracts the function name index and, if present, the receiver type
345/// (which marks this as an extension function).
346fn decode_function(data: &[u8]) -> Option<DecodedFunction> {
347    let mut reader = WireReader::new(data);
348    let mut name_index: u64 = 0;
349    let mut receiver_type: Option<DecodedType> = None;
350
351    while reader.has_remaining() {
352        let (field_number, wire_type) = reader.read_tag()?;
353
354        match (field_number, wire_type) {
355            (FUNCTION_FLAGS, WIRE_VARINT) => {
356                // Read and discard flags — we extract name and receiver only.
357                let _flags = reader.read_varint()?;
358            }
359            (FUNCTION_NAME, WIRE_VARINT) => {
360                name_index = reader.read_varint()?;
361            }
362            (FUNCTION_RECEIVER_TYPE, WIRE_LENGTH_DELIMITED) => {
363                let type_data = reader.read_length_delimited()?;
364                receiver_type = decode_type(type_data);
365            }
366            _ => {
367                if !reader.skip_field(wire_type) {
368                    return None;
369                }
370            }
371        }
372    }
373
374    Some(DecodedFunction {
375        name_index,
376        receiver_type,
377    })
378}
379
380// ---------------------------------------------------------------------------
381// Property message decoder
382// ---------------------------------------------------------------------------
383
384/// Decoded information from a `Property` message.
385struct DecodedProperty {
386    /// Property name index into the string table.
387    name_index: u64,
388    /// Whether the return type is nullable.
389    return_type_nullable: bool,
390}
391
392/// Decode a `Property` message from its raw protobuf bytes.
393///
394/// Extracts the property name index and whether its return type is nullable.
395fn decode_property(data: &[u8]) -> Option<DecodedProperty> {
396    let mut reader = WireReader::new(data);
397    let mut name_index: u64 = 0;
398    let mut return_type_nullable = false;
399
400    while reader.has_remaining() {
401        let (field_number, wire_type) = reader.read_tag()?;
402
403        match (field_number, wire_type) {
404            (PROPERTY_FLAGS, WIRE_VARINT) => {
405                let _flags = reader.read_varint()?;
406            }
407            (PROPERTY_NAME, WIRE_VARINT) => {
408                name_index = reader.read_varint()?;
409            }
410            (PROPERTY_RETURN_TYPE, WIRE_LENGTH_DELIMITED) => {
411                let type_data = reader.read_length_delimited()?;
412                if let Some(decoded) = decode_type(type_data) {
413                    return_type_nullable = decoded.nullable;
414                }
415            }
416            _ => {
417                if !reader.skip_field(wire_type) {
418                    return None;
419                }
420            }
421        }
422    }
423
424    Some(DecodedProperty {
425        name_index,
426        return_type_nullable,
427    })
428}
429
430// ---------------------------------------------------------------------------
431// Class message decoder (top-level)
432// ---------------------------------------------------------------------------
433
434/// Decode the top-level `Class` message from `d1[0]` protobuf bytes.
435///
436/// Extracts class flags, companion object name, functions (with extension
437/// receiver detection), and properties (with nullable type detection).
438fn decode_class_message(data: &[u8], string_table: &[String]) -> Option<KotlinClassMetadata> {
439    let mut reader = WireReader::new(data);
440    let mut flags: u64 = 0;
441    let mut companion_name_index: Option<u64> = None;
442    let mut extension_functions = Vec::new();
443    let mut nullable_properties = Vec::new();
444
445    while reader.has_remaining() {
446        let (field_number, wire_type) = reader.read_tag()?;
447
448        match (field_number, wire_type) {
449            (CLASS_FLAGS, WIRE_VARINT) => {
450                flags = reader.read_varint()?;
451            }
452            (CLASS_FUNCTIONS, WIRE_LENGTH_DELIMITED) => {
453                let func_data = reader.read_length_delimited()?;
454                if let Some(func) = decode_function(func_data)
455                    && let Some(ref recv_type) = func.receiver_type
456                {
457                    // This is an extension function — resolve names.
458                    let fn_name = string_table_lookup(string_table, func.name_index)
459                        .unwrap_or("<unknown>")
460                        .to_owned();
461
462                    let receiver_name = recv_type
463                        .class_name_index
464                        .and_then(|idx| string_table_lookup(string_table, idx))
465                        .unwrap_or("<unknown>")
466                        .to_owned();
467
468                    extension_functions.push(KotlinExtensionFunction {
469                        name: fn_name,
470                        receiver_type: receiver_name,
471                        receiver_nullable: recv_type.nullable,
472                    });
473                }
474            }
475            (CLASS_PROPERTIES, WIRE_LENGTH_DELIMITED) => {
476                let prop_data = reader.read_length_delimited()?;
477                if let Some(prop) = decode_property(prop_data)
478                    && prop.return_type_nullable
479                    && let Some(name) = string_table_lookup(string_table, prop.name_index)
480                {
481                    nullable_properties.push(name.to_owned());
482                }
483            }
484            (CLASS_COMPANION_OBJECT_NAME, WIRE_VARINT) => {
485                companion_name_index = Some(reader.read_varint()?);
486            }
487            _ => {
488                if !reader.skip_field(wire_type) {
489                    return None;
490                }
491            }
492        }
493    }
494
495    let class_kind = extract_class_kind(flags);
496    let visibility = extract_visibility(flags);
497    let modality = extract_modality(flags);
498
499    // Data class: class kind is Class (0) and has the `data` modifier.
500    // The data flag is bit 12 in Kotlin metadata class flags.
501    let is_data = class_kind == KotlinClassKind::Class && (flags >> 12) & 1 == 1;
502    let is_sealed = modality == 3; // modality 3 = sealed
503
504    let companion_object_name = companion_name_index
505        .and_then(|idx| string_table_lookup(string_table, idx))
506        .map(str::to_owned);
507
508    Some(KotlinClassMetadata {
509        kind: class_kind,
510        visibility,
511        is_data,
512        is_sealed,
513        companion_object_name,
514        extension_functions,
515        nullable_properties,
516    })
517}
518
519// ---------------------------------------------------------------------------
520// Public API
521// ---------------------------------------------------------------------------
522
523/// Decode Kotlin metadata from a [`KotlinMetadataStub`].
524///
525/// Returns `None` if the metadata kind is not supported (only kind=1 Class is
526/// decoded in Tier 1) or if decoding fails. When `None` is returned, callers
527/// should fall back to bytecode-only analysis.
528///
529/// # Errors
530///
531/// This function never panics. Malformed protobuf data, unsupported metadata
532/// versions, and out-of-bounds string table references all result in `None`.
533#[must_use]
534pub fn decode_kotlin_metadata(stub: &KotlinMetadataStub) -> Option<KotlinClassMetadata> {
535    // Only kind=1 (Class) is supported in Tier 1.
536    if stub.kind != 1 {
537        log::debug!(
538            "skipping Kotlin metadata kind {} (only kind=1 Class is supported)",
539            stub.kind,
540        );
541        return None;
542    }
543
544    // Validate metadata version — we support 1.x.
545    if let Some(&major) = stub.metadata_version.first()
546        && !(1..=2).contains(&major)
547    {
548        log::warn!(
549            "unsupported Kotlin metadata version {:?}, skipping",
550            stub.metadata_version,
551        );
552        return None;
553    }
554
555    // d1 must contain at least one protobuf chunk.
556    let d1_combined = combine_d1_chunks(&stub.data1)?;
557
558    // An empty protobuf message is valid — all fields take default values.
559    decode_class_message(&d1_combined, &stub.data2)
560}
561
562/// Combine `d1` string chunks into raw protobuf bytes.
563///
564/// In JVM bytecode, `d1` entries are `String[]` where each character's
565/// Unicode code point represents a raw byte value (Latin-1 / ISO-8859-1
566/// encoding). Kotlin uses code points 0-255 to encode arbitrary protobuf
567/// bytes in JVM strings. We iterate over chars and extract the low byte of
568/// each code point.
569///
570/// Returns `None` if `d1` is empty.
571fn combine_d1_chunks(d1: &[String]) -> Option<Vec<u8>> {
572    if d1.is_empty() {
573        return None;
574    }
575
576    let total_chars: usize = d1.iter().map(|s| s.chars().count()).sum();
577    let mut bytes = Vec::with_capacity(total_chars);
578    for chunk in d1 {
579        for ch in chunk.chars() {
580            // Each char's code point is a protobuf byte (0-255).
581            // Code points > 255 should not appear in valid metadata, but
582            // we mask to the low byte defensively.
583            bytes.push((ch as u32 & 0xFF) as u8);
584        }
585    }
586    Some(bytes)
587}
588
589// ---------------------------------------------------------------------------
590// Tests
591// ---------------------------------------------------------------------------
592
593#[cfg(test)]
594mod tests {
595    use super::*;
596
597    // -- Wire format helpers ------------------------------------------------
598
599    /// Encode a varint into bytes.
600    fn encode_varint(mut value: u64) -> Vec<u8> {
601        let mut buf = Vec::new();
602        loop {
603            let mut byte = (value & 0x7F) as u8;
604            value >>= 7;
605            if value != 0 {
606                byte |= 0x80;
607            }
608            buf.push(byte);
609            if value == 0 {
610                break;
611            }
612        }
613        buf
614    }
615
616    /// Encode a protobuf tag.
617    fn encode_tag(field_number: u32, wire_type: u8) -> Vec<u8> {
618        encode_varint(u64::from(field_number) << 3 | u64::from(wire_type))
619    }
620
621    /// Encode a varint field (tag + varint value).
622    fn encode_varint_field(field_number: u32, value: u64) -> Vec<u8> {
623        let mut buf = encode_tag(field_number, WIRE_VARINT);
624        buf.extend(encode_varint(value));
625        buf
626    }
627
628    /// Encode a length-delimited field (tag + length + bytes).
629    fn encode_length_delimited_field(field_number: u32, data: &[u8]) -> Vec<u8> {
630        let mut buf = encode_tag(field_number, WIRE_LENGTH_DELIMITED);
631        buf.extend(encode_varint(data.len() as u64));
632        buf.extend(data);
633        buf
634    }
635
636    /// Build class flags from visibility, modality, and class kind.
637    fn build_class_flags(visibility: u64, modality: u64, class_kind: u64, is_data: bool) -> u64 {
638        let mut flags = 0u64;
639        flags |= visibility << 3;
640        flags |= modality << 6;
641        flags |= class_kind << 9;
642        if is_data {
643            flags |= 1 << 12;
644        }
645        flags
646    }
647
648    /// Build a Type message with optional nullable flag and class name index.
649    fn build_type_message(nullable: bool, class_name_index: Option<u64>) -> Vec<u8> {
650        let mut buf = Vec::new();
651        let mut flags: u64 = 0;
652        if nullable {
653            flags |= 1 << 1;
654        }
655        if flags != 0 {
656            buf.extend(encode_varint_field(TYPE_FLAGS, flags));
657        }
658        if let Some(idx) = class_name_index {
659            buf.extend(encode_varint_field(TYPE_CLASS_NAME, idx));
660        }
661        buf
662    }
663
664    /// Build a Function message.
665    fn build_function_message(name_index: u64, receiver_type: Option<&[u8]>) -> Vec<u8> {
666        let mut buf = Vec::new();
667        // flags (field 1) — just set to 0 for tests.
668        buf.extend(encode_varint_field(FUNCTION_FLAGS, 0));
669        // name (field 2)
670        buf.extend(encode_varint_field(FUNCTION_NAME, name_index));
671        // receiver_type (field 6) — only for extension functions
672        if let Some(rt) = receiver_type {
673            buf.extend(encode_length_delimited_field(FUNCTION_RECEIVER_TYPE, rt));
674        }
675        buf
676    }
677
678    /// Build a Property message.
679    fn build_property_message(name_index: u64, return_type: Option<&[u8]>) -> Vec<u8> {
680        let mut buf = Vec::new();
681        buf.extend(encode_varint_field(PROPERTY_FLAGS, 0));
682        buf.extend(encode_varint_field(PROPERTY_NAME, name_index));
683        if let Some(rt) = return_type {
684            buf.extend(encode_length_delimited_field(PROPERTY_RETURN_TYPE, rt));
685        }
686        buf
687    }
688
689    #[allow(clippy::needless_continue)] // Continue at end of loop for clarity
690    /// Create a `KotlinMetadataStub` from raw d1 bytes and a string table.
691    ///
692    /// Uses Latin-1 encoding (byte → char code point) to match how Kotlin
693    /// stores protobuf bytes in JVM string constants.
694    #[allow(clippy::needless_pass_by_value)] // Convenience for callers
695    fn make_stub(d1_bytes: Vec<u8>, string_table: Vec<&str>) -> KotlinMetadataStub {
696        let d1_string: String = d1_bytes.iter().map(|&b| b as char).collect();
697        KotlinMetadataStub {
698            kind: 1,
699            metadata_version: vec![1, 9, 0],
700            data1: vec![d1_string],
701            data2: string_table.into_iter().map(str::to_owned).collect(),
702            extra_string: None,
703            package_name: None,
704            extra_int: None,
705        }
706    }
707
708    // -- WireReader tests ---------------------------------------------------
709
710    #[test]
711    fn wire_reader_varint_single_byte() {
712        let data = [0x05]; // value = 5
713        let mut reader = WireReader::new(&data);
714        assert_eq!(reader.read_varint(), Some(5));
715        assert!(!reader.has_remaining());
716    }
717
718    #[test]
719    fn wire_reader_varint_multi_byte() {
720        // 300 = 0b100101100
721        // byte 0: 0b10101100 = 0xAC
722        // byte 1: 0b00000010 = 0x02
723        let data = [0xAC, 0x02];
724        let mut reader = WireReader::new(&data);
725        assert_eq!(reader.read_varint(), Some(300));
726    }
727
728    #[test]
729    fn wire_reader_varint_max_bytes() {
730        // u64::MAX requires 10 bytes.
731        let encoded = encode_varint(u64::MAX);
732        assert_eq!(encoded.len(), 10);
733        let mut reader = WireReader::new(&encoded);
734        assert_eq!(reader.read_varint(), Some(u64::MAX));
735    }
736
737    #[test]
738    fn wire_reader_varint_truncated() {
739        // Continuation bit set but no next byte.
740        let data = [0x80];
741        let mut reader = WireReader::new(&data);
742        assert_eq!(reader.read_varint(), None);
743    }
744
745    #[test]
746    fn wire_reader_varint_exceeds_10_bytes() {
747        // 11 bytes all with continuation bit — malformed.
748        let data = [0x80; 11];
749        let mut reader = WireReader::new(&data);
750        assert_eq!(reader.read_varint(), None);
751    }
752
753    #[test]
754    fn wire_reader_tag_decomposition() {
755        // Field 3, wire type 2 (length-delimited): (3 << 3) | 2 = 26 = 0x1A
756        let data = [0x1A];
757        let mut reader = WireReader::new(&data);
758        assert_eq!(reader.read_tag(), Some((3, 2)));
759    }
760
761    #[test]
762    fn wire_reader_tag_field_zero_invalid() {
763        // Field 0 is invalid in protobuf.
764        let data = [0x02]; // (0 << 3) | 2 = 2 → field 0, wire type 2
765        let mut reader = WireReader::new(&data);
766        assert_eq!(reader.read_tag(), None);
767    }
768
769    #[test]
770    fn wire_reader_length_delimited() {
771        // Tag for field 1, wire type 2: (1 << 3) | 2 = 10 = 0x0A
772        // Length = 3, payload = [0x01, 0x02, 0x03]
773        let data = [0x0A, 0x03, 0x01, 0x02, 0x03];
774        let mut reader = WireReader::new(&data);
775        let (field, wire) = reader.read_tag().unwrap();
776        assert_eq!((field, wire), (1, 2));
777        let payload = reader.read_length_delimited().unwrap();
778        assert_eq!(payload, &[0x01, 0x02, 0x03]);
779    }
780
781    #[test]
782    fn wire_reader_length_delimited_truncated() {
783        // Claims length 5 but only 2 bytes follow.
784        let data = [0x05, 0x01, 0x02];
785        let mut reader = WireReader::new(&data);
786        assert_eq!(reader.read_length_delimited(), None);
787    }
788
789    #[test]
790    fn wire_reader_skip_varint() {
791        let mut data = encode_tag(99, WIRE_VARINT);
792        data.extend(encode_varint(42));
793        data.extend(encode_tag(1, WIRE_VARINT));
794        data.extend(encode_varint(7));
795
796        let mut reader = WireReader::new(&data);
797        let (field, wire) = reader.read_tag().unwrap();
798        assert_eq!(field, 99);
799        assert!(reader.skip_field(wire));
800
801        let (field2, _) = reader.read_tag().unwrap();
802        assert_eq!(field2, 1);
803    }
804
805    #[test]
806    fn wire_reader_skip_32bit() {
807        let mut data = vec![];
808        data.extend(encode_tag(5, WIRE_32BIT));
809        data.extend(&[0x00, 0x00, 0x00, 0x00]); // 4 bytes
810        data.extend(encode_tag(1, WIRE_VARINT));
811        data.extend(encode_varint(99));
812
813        let mut reader = WireReader::new(&data);
814        let (_, wire) = reader.read_tag().unwrap();
815        assert!(reader.skip_field(wire));
816        let (field, _) = reader.read_tag().unwrap();
817        assert_eq!(field, 1);
818    }
819
820    #[test]
821    fn wire_reader_skip_64bit() {
822        let mut data = vec![];
823        data.extend(encode_tag(5, WIRE_64BIT));
824        data.extend(&[0u8; 8]); // 8 bytes
825        data.extend(encode_tag(1, WIRE_VARINT));
826        data.extend(encode_varint(99));
827
828        let mut reader = WireReader::new(&data);
829        let (_, wire) = reader.read_tag().unwrap();
830        assert!(reader.skip_field(wire));
831        let (field, _) = reader.read_tag().unwrap();
832        assert_eq!(field, 1);
833    }
834
835    #[test]
836    fn wire_reader_skip_unknown_wire_type() {
837        let mut reader = WireReader::new(&[]);
838        assert!(!reader.skip_field(3)); // wire type 3 is deprecated/unknown
839    }
840
841    // -- String table tests -------------------------------------------------
842
843    #[test]
844    fn string_table_valid_lookup() {
845        let table = vec!["kotlin/String".to_owned(), "isEmail".to_owned()];
846        assert_eq!(string_table_lookup(&table, 0), Some("kotlin/String"));
847        assert_eq!(string_table_lookup(&table, 1), Some("isEmail"));
848    }
849
850    #[test]
851    fn string_table_out_of_bounds() {
852        let table = vec!["only_one".to_owned()];
853        assert_eq!(string_table_lookup(&table, 1), None);
854        assert_eq!(string_table_lookup(&table, 999), None);
855    }
856
857    // -- Extension receiver detection ---------------------------------------
858
859    #[test]
860    fn extension_receiver_detection() {
861        // String table: [0]="kotlin/String", [1]="isEmail"
862        let receiver_type = build_type_message(false, Some(0));
863        let func = build_function_message(1, Some(&receiver_type));
864
865        let mut d1 = Vec::new();
866        // Class flags: public, final, class
867        d1.extend(encode_varint_field(
868            CLASS_FLAGS,
869            build_class_flags(3, 0, 0, false),
870        ));
871        // Function with extension receiver
872        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
873
874        let stub = make_stub(d1, vec!["kotlin/String", "isEmail"]);
875        let meta = decode_kotlin_metadata(&stub).unwrap();
876
877        assert_eq!(meta.extension_functions.len(), 1);
878        assert_eq!(meta.extension_functions[0].name, "isEmail");
879        assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
880        assert!(!meta.extension_functions[0].receiver_nullable);
881    }
882
883    #[test]
884    fn extension_receiver_nullable() {
885        // Test nullable receiver: `fun String?.isNullOrEmail()`
886        let receiver_type = build_type_message(true, Some(0));
887        let func = build_function_message(1, Some(&receiver_type));
888
889        let mut d1 = Vec::new();
890        d1.extend(encode_varint_field(
891            CLASS_FLAGS,
892            build_class_flags(3, 0, 0, false),
893        ));
894        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
895
896        let stub = make_stub(d1, vec!["kotlin/String", "isNullOrEmail"]);
897        let meta = decode_kotlin_metadata(&stub).unwrap();
898
899        assert_eq!(meta.extension_functions.len(), 1);
900        assert_eq!(meta.extension_functions[0].name, "isNullOrEmail");
901        assert!(meta.extension_functions[0].receiver_nullable);
902    }
903
904    #[test]
905    fn regular_function_not_treated_as_extension() {
906        // Function without receiver_type should NOT appear in extension_functions.
907        let func = build_function_message(0, None);
908
909        let mut d1 = Vec::new();
910        d1.extend(encode_varint_field(
911            CLASS_FLAGS,
912            build_class_flags(3, 0, 0, false),
913        ));
914        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
915
916        let stub = make_stub(d1, vec!["regularFunction"]);
917        let meta = decode_kotlin_metadata(&stub).unwrap();
918
919        assert!(meta.extension_functions.is_empty());
920    }
921
922    // -- Nullable parameter detection ---------------------------------------
923
924    #[test]
925    fn nullable_property_detection() {
926        // Property with nullable return type
927        let nullable_type = build_type_message(true, Some(0));
928        let prop = build_property_message(1, Some(&nullable_type));
929
930        let mut d1 = Vec::new();
931        d1.extend(encode_varint_field(
932            CLASS_FLAGS,
933            build_class_flags(3, 0, 0, false),
934        ));
935        d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop));
936
937        let stub = make_stub(d1, vec!["kotlin/String", "name"]);
938        let meta = decode_kotlin_metadata(&stub).unwrap();
939
940        assert_eq!(meta.nullable_properties, vec!["name"]);
941    }
942
943    #[test]
944    fn non_nullable_property_not_included() {
945        let non_nullable_type = build_type_message(false, Some(0));
946        let prop = build_property_message(1, Some(&non_nullable_type));
947
948        let mut d1 = Vec::new();
949        d1.extend(encode_varint_field(
950            CLASS_FLAGS,
951            build_class_flags(3, 0, 0, false),
952        ));
953        d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop));
954
955        let stub = make_stub(d1, vec!["kotlin/String", "name"]);
956        let meta = decode_kotlin_metadata(&stub).unwrap();
957
958        assert!(meta.nullable_properties.is_empty());
959    }
960
961    // -- Companion object detection -----------------------------------------
962
963    #[test]
964    fn companion_object_detection() {
965        let mut d1 = Vec::new();
966        d1.extend(encode_varint_field(
967            CLASS_FLAGS,
968            build_class_flags(3, 0, 0, false),
969        ));
970        // companion_object_name = string table index 0 = "Companion"
971        d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 0));
972
973        let stub = make_stub(d1, vec!["Companion"]);
974        let meta = decode_kotlin_metadata(&stub).unwrap();
975
976        assert_eq!(meta.companion_object_name, Some("Companion".to_owned()));
977    }
978
979    #[test]
980    fn companion_object_custom_name() {
981        let mut d1 = Vec::new();
982        d1.extend(encode_varint_field(
983            CLASS_FLAGS,
984            build_class_flags(3, 0, 0, false),
985        ));
986        d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 0));
987
988        let stub = make_stub(d1, vec!["Factory"]);
989        let meta = decode_kotlin_metadata(&stub).unwrap();
990
991        assert_eq!(meta.companion_object_name, Some("Factory".to_owned()));
992    }
993
994    #[test]
995    fn no_companion_object() {
996        let mut d1 = Vec::new();
997        d1.extend(encode_varint_field(
998            CLASS_FLAGS,
999            build_class_flags(3, 0, 0, false),
1000        ));
1001
1002        let stub = make_stub(d1, vec![]);
1003        let meta = decode_kotlin_metadata(&stub).unwrap();
1004
1005        assert_eq!(meta.companion_object_name, None);
1006    }
1007
1008    // -- Object declaration (singleton) -------------------------------------
1009
1010    #[test]
1011    fn object_declaration_kind() {
1012        let mut d1 = Vec::new();
1013        // class kind 5 = object
1014        d1.extend(encode_varint_field(
1015            CLASS_FLAGS,
1016            build_class_flags(3, 0, 5, false),
1017        ));
1018
1019        let stub = make_stub(d1, vec![]);
1020        let meta = decode_kotlin_metadata(&stub).unwrap();
1021
1022        assert_eq!(meta.kind, KotlinClassKind::Object);
1023    }
1024
1025    #[test]
1026    fn companion_object_kind() {
1027        let mut d1 = Vec::new();
1028        // class kind 6 = companion object
1029        d1.extend(encode_varint_field(
1030            CLASS_FLAGS,
1031            build_class_flags(3, 0, 6, false),
1032        ));
1033
1034        let stub = make_stub(d1, vec![]);
1035        let meta = decode_kotlin_metadata(&stub).unwrap();
1036
1037        assert_eq!(meta.kind, KotlinClassKind::CompanionObject);
1038    }
1039
1040    // -- Data class flag detection ------------------------------------------
1041
1042    #[test]
1043    fn data_class_detection() {
1044        let mut d1 = Vec::new();
1045        // class kind 0 = class, is_data = true
1046        d1.extend(encode_varint_field(
1047            CLASS_FLAGS,
1048            build_class_flags(3, 0, 0, true),
1049        ));
1050
1051        let stub = make_stub(d1, vec![]);
1052        let meta = decode_kotlin_metadata(&stub).unwrap();
1053
1054        assert!(meta.is_data);
1055        assert_eq!(meta.kind, KotlinClassKind::Class);
1056    }
1057
1058    #[test]
1059    fn non_data_class() {
1060        let mut d1 = Vec::new();
1061        d1.extend(encode_varint_field(
1062            CLASS_FLAGS,
1063            build_class_flags(3, 0, 0, false),
1064        ));
1065
1066        let stub = make_stub(d1, vec![]);
1067        let meta = decode_kotlin_metadata(&stub).unwrap();
1068
1069        assert!(!meta.is_data);
1070    }
1071
1072    // -- Sealed class flag detection ----------------------------------------
1073
1074    #[test]
1075    fn sealed_class_detection() {
1076        let mut d1 = Vec::new();
1077        // modality 3 = sealed
1078        d1.extend(encode_varint_field(
1079            CLASS_FLAGS,
1080            build_class_flags(3, 3, 0, false),
1081        ));
1082
1083        let stub = make_stub(d1, vec![]);
1084        let meta = decode_kotlin_metadata(&stub).unwrap();
1085
1086        assert!(meta.is_sealed);
1087    }
1088
1089    #[test]
1090    fn non_sealed_class() {
1091        let mut d1 = Vec::new();
1092        // modality 0 = final
1093        d1.extend(encode_varint_field(
1094            CLASS_FLAGS,
1095            build_class_flags(3, 0, 0, false),
1096        ));
1097
1098        let stub = make_stub(d1, vec![]);
1099        let meta = decode_kotlin_metadata(&stub).unwrap();
1100
1101        assert!(!meta.is_sealed);
1102    }
1103
1104    // -- Visibility extraction ----------------------------------------------
1105
1106    #[test]
1107    fn visibility_public() {
1108        let mut d1 = Vec::new();
1109        d1.extend(encode_varint_field(
1110            CLASS_FLAGS,
1111            build_class_flags(3, 0, 0, false), // visibility 3 = public
1112        ));
1113
1114        let stub = make_stub(d1, vec![]);
1115        let meta = decode_kotlin_metadata(&stub).unwrap();
1116        assert_eq!(meta.visibility, KotlinVisibility::Public);
1117    }
1118
1119    #[test]
1120    fn visibility_private() {
1121        let mut d1 = Vec::new();
1122        d1.extend(encode_varint_field(
1123            CLASS_FLAGS,
1124            build_class_flags(1, 0, 0, false), // visibility 1 = private
1125        ));
1126
1127        let stub = make_stub(d1, vec![]);
1128        let meta = decode_kotlin_metadata(&stub).unwrap();
1129        assert_eq!(meta.visibility, KotlinVisibility::Private);
1130    }
1131
1132    #[test]
1133    fn visibility_internal() {
1134        let mut d1 = Vec::new();
1135        d1.extend(encode_varint_field(
1136            CLASS_FLAGS,
1137            build_class_flags(0, 0, 0, false), // visibility 0 = internal
1138        ));
1139
1140        let stub = make_stub(d1, vec![]);
1141        let meta = decode_kotlin_metadata(&stub).unwrap();
1142        assert_eq!(meta.visibility, KotlinVisibility::Internal);
1143    }
1144
1145    #[test]
1146    fn visibility_protected() {
1147        let mut d1 = Vec::new();
1148        d1.extend(encode_varint_field(
1149            CLASS_FLAGS,
1150            build_class_flags(2, 0, 0, false), // visibility 2 = protected
1151        ));
1152
1153        let stub = make_stub(d1, vec![]);
1154        let meta = decode_kotlin_metadata(&stub).unwrap();
1155        assert_eq!(meta.visibility, KotlinVisibility::Protected);
1156    }
1157
1158    // -- Malformed protobuf → None ------------------------------------------
1159
1160    #[test]
1161    fn malformed_protobuf_returns_none() {
1162        // Random garbage bytes — should not panic, should return None or
1163        // degrade gracefully.
1164        let stub = make_stub(
1165            vec![
1166                0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
1167            ],
1168            vec![],
1169        );
1170        // This may return Some with defaults or None — either is acceptable
1171        // as long as it doesn't panic.
1172        let _result = decode_kotlin_metadata(&stub);
1173    }
1174
1175    #[test]
1176    fn empty_d1_returns_none() {
1177        let stub = KotlinMetadataStub {
1178            kind: 1,
1179            metadata_version: vec![1, 9, 0],
1180            data1: vec![],
1181            data2: vec![],
1182            extra_string: None,
1183            package_name: None,
1184            extra_int: None,
1185        };
1186        assert_eq!(decode_kotlin_metadata(&stub), None);
1187    }
1188
1189    #[test]
1190    fn truncated_varint_returns_none() {
1191        // Tag byte with continuation bit but no subsequent byte.
1192        let stub = make_stub(vec![0x80], vec![]);
1193        assert_eq!(decode_kotlin_metadata(&stub), None);
1194    }
1195
1196    // -- Unsupported kind → None --------------------------------------------
1197
1198    #[test]
1199    fn unsupported_kind_returns_none() {
1200        let stub = KotlinMetadataStub {
1201            kind: 2, // file facade — not supported in Tier 1
1202            metadata_version: vec![1, 9, 0],
1203            data1: vec!["data".to_owned()],
1204            data2: vec![],
1205            extra_string: None,
1206            package_name: None,
1207            extra_int: None,
1208        };
1209        assert_eq!(decode_kotlin_metadata(&stub), None);
1210    }
1211
1212    #[test]
1213    fn unsupported_kind_synthetic() {
1214        let stub = KotlinMetadataStub {
1215            kind: 3,
1216            metadata_version: vec![1, 9, 0],
1217            data1: vec![],
1218            data2: vec![],
1219            extra_string: None,
1220            package_name: None,
1221            extra_int: None,
1222        };
1223        assert_eq!(decode_kotlin_metadata(&stub), None);
1224    }
1225
1226    #[test]
1227    fn unsupported_metadata_version() {
1228        let stub = KotlinMetadataStub {
1229            kind: 1,
1230            metadata_version: vec![99, 0, 0], // far future version
1231            data1: vec!["data".to_owned()],
1232            data2: vec![],
1233            extra_string: None,
1234            package_name: None,
1235            extra_int: None,
1236        };
1237        assert_eq!(decode_kotlin_metadata(&stub), None);
1238    }
1239
1240    // -- Comprehensive scenario: all features combined ----------------------
1241
1242    #[test]
1243    fn comprehensive_class_decoding() {
1244        // String table:
1245        //   [0] = "kotlin/String"
1246        //   [1] = "isEmail"
1247        //   [2] = "name"
1248        //   [3] = "Companion"
1249        //   [4] = "toString"
1250        let string_table = vec!["kotlin/String", "isEmail", "name", "Companion", "toString"];
1251
1252        // Build an extension function: fun String.isEmail()
1253        let receiver_type = build_type_message(false, Some(0));
1254        let ext_func = build_function_message(1, Some(&receiver_type));
1255
1256        // Build a regular function: fun toString()
1257        let regular_func = build_function_message(4, None);
1258
1259        // Build a nullable property: var name: String?
1260        let nullable_type = build_type_message(true, Some(0));
1261        let nullable_prop = build_property_message(2, Some(&nullable_type));
1262
1263        // Assemble class message
1264        let mut d1 = Vec::new();
1265        // public, final, class, data=false
1266        d1.extend(encode_varint_field(
1267            CLASS_FLAGS,
1268            build_class_flags(3, 0, 0, false),
1269        ));
1270        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &ext_func));
1271        d1.extend(encode_length_delimited_field(
1272            CLASS_FUNCTIONS,
1273            &regular_func,
1274        ));
1275        d1.extend(encode_length_delimited_field(
1276            CLASS_PROPERTIES,
1277            &nullable_prop,
1278        ));
1279        d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 3));
1280
1281        let stub = make_stub(d1, string_table);
1282        let meta = decode_kotlin_metadata(&stub).unwrap();
1283
1284        assert_eq!(meta.kind, KotlinClassKind::Class);
1285        assert_eq!(meta.visibility, KotlinVisibility::Public);
1286        assert!(!meta.is_data);
1287        assert!(!meta.is_sealed);
1288        assert_eq!(meta.companion_object_name, Some("Companion".to_owned()));
1289        assert_eq!(meta.extension_functions.len(), 1);
1290        assert_eq!(meta.extension_functions[0].name, "isEmail");
1291        assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
1292        assert_eq!(meta.nullable_properties, vec!["name"]);
1293    }
1294
1295    // -- Interface kind -----------------------------------------------------
1296
1297    #[test]
1298    fn interface_kind_detection() {
1299        let mut d1 = Vec::new();
1300        // class kind 1 = interface
1301        d1.extend(encode_varint_field(
1302            CLASS_FLAGS,
1303            build_class_flags(3, 2, 1, false), // public, abstract, interface
1304        ));
1305
1306        let stub = make_stub(d1, vec![]);
1307        let meta = decode_kotlin_metadata(&stub).unwrap();
1308
1309        assert_eq!(meta.kind, KotlinClassKind::Interface);
1310    }
1311
1312    // -- Enum class kind ----------------------------------------------------
1313
1314    #[test]
1315    fn enum_class_kind_detection() {
1316        let mut d1 = Vec::new();
1317        // class kind 2 = enum class
1318        d1.extend(encode_varint_field(
1319            CLASS_FLAGS,
1320            build_class_flags(3, 0, 2, false),
1321        ));
1322
1323        let stub = make_stub(d1, vec![]);
1324        let meta = decode_kotlin_metadata(&stub).unwrap();
1325
1326        assert_eq!(meta.kind, KotlinClassKind::EnumClass);
1327    }
1328
1329    // -- Annotation class kind ----------------------------------------------
1330
1331    #[test]
1332    fn annotation_class_kind_detection() {
1333        let mut d1 = Vec::new();
1334        // class kind 4 = annotation class
1335        d1.extend(encode_varint_field(
1336            CLASS_FLAGS,
1337            build_class_flags(3, 0, 4, false),
1338        ));
1339
1340        let stub = make_stub(d1, vec![]);
1341        let meta = decode_kotlin_metadata(&stub).unwrap();
1342
1343        assert_eq!(meta.kind, KotlinClassKind::AnnotationClass);
1344    }
1345
1346    // -- Multiple extension functions ---------------------------------------
1347
1348    #[test]
1349    fn multiple_extension_functions() {
1350        let recv_string = build_type_message(false, Some(0));
1351        let recv_list = build_type_message(false, Some(2));
1352
1353        let func1 = build_function_message(1, Some(&recv_string));
1354        let func2 = build_function_message(3, Some(&recv_list));
1355
1356        let mut d1 = Vec::new();
1357        d1.extend(encode_varint_field(
1358            CLASS_FLAGS,
1359            build_class_flags(3, 0, 0, false),
1360        ));
1361        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func1));
1362        d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func2));
1363
1364        let stub = make_stub(
1365            d1,
1366            vec!["kotlin/String", "isEmail", "kotlin/List", "firstOrNull"],
1367        );
1368        let meta = decode_kotlin_metadata(&stub).unwrap();
1369
1370        assert_eq!(meta.extension_functions.len(), 2);
1371        assert_eq!(meta.extension_functions[0].name, "isEmail");
1372        assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
1373        assert_eq!(meta.extension_functions[1].name, "firstOrNull");
1374        assert_eq!(meta.extension_functions[1].receiver_type, "kotlin/List");
1375    }
1376
1377    // -- Multiple nullable properties ---------------------------------------
1378
1379    #[test]
1380    fn multiple_nullable_properties() {
1381        let nullable_type = build_type_message(true, Some(0));
1382        let prop1 = build_property_message(1, Some(&nullable_type));
1383        let prop2 = build_property_message(2, Some(&nullable_type));
1384
1385        let mut d1 = Vec::new();
1386        d1.extend(encode_varint_field(
1387            CLASS_FLAGS,
1388            build_class_flags(3, 0, 0, false),
1389        ));
1390        d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop1));
1391        d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop2));
1392
1393        let stub = make_stub(d1, vec!["kotlin/String", "name", "email"]);
1394        let meta = decode_kotlin_metadata(&stub).unwrap();
1395
1396        assert_eq!(meta.nullable_properties.len(), 2);
1397        assert!(meta.nullable_properties.contains(&"name".to_owned()));
1398        assert!(meta.nullable_properties.contains(&"email".to_owned()));
1399    }
1400
1401    // -- combine_d1_chunks --------------------------------------------------
1402
1403    #[test]
1404    fn combine_d1_multiple_chunks() {
1405        let d1 = vec!["hel".to_owned(), "lo".to_owned()];
1406        let combined = combine_d1_chunks(&d1).unwrap();
1407        assert_eq!(combined, b"hello");
1408    }
1409
1410    #[test]
1411    fn combine_d1_empty() {
1412        let d1: Vec<String> = vec![];
1413        assert_eq!(combine_d1_chunks(&d1), None);
1414    }
1415
1416    // -- Edge case: data class with all features ----------------------------
1417
1418    #[test]
1419    fn data_class_with_sealed_is_not_data() {
1420        // A sealed data class has both is_data and is_sealed set.
1421        let mut d1 = Vec::new();
1422        // modality 3 = sealed, is_data = true
1423        d1.extend(encode_varint_field(
1424            CLASS_FLAGS,
1425            build_class_flags(3, 3, 0, true),
1426        ));
1427
1428        let stub = make_stub(d1, vec![]);
1429        let meta = decode_kotlin_metadata(&stub).unwrap();
1430
1431        assert!(meta.is_data);
1432        assert!(meta.is_sealed);
1433    }
1434
1435    // -- Default flags when field 1 is absent --------------------------------
1436
1437    #[test]
1438    fn missing_flags_defaults_to_internal_final_class() {
1439        // A class message with no flags field.
1440        let d1 = Vec::new();
1441        let stub = make_stub(d1, vec![]);
1442        let meta = decode_kotlin_metadata(&stub).unwrap();
1443
1444        // flags=0 → visibility=internal, modality=final, kind=class
1445        assert_eq!(meta.kind, KotlinClassKind::Class);
1446        assert_eq!(meta.visibility, KotlinVisibility::Internal);
1447        assert!(!meta.is_data);
1448        assert!(!meta.is_sealed);
1449        assert!(meta.companion_object_name.is_none());
1450        assert!(meta.extension_functions.is_empty());
1451        assert!(meta.nullable_properties.is_empty());
1452    }
1453
1454    // -- PrivateToThis and Local visibility ----------------------------------
1455
1456    #[test]
1457    fn visibility_private_to_this() {
1458        let mut d1 = Vec::new();
1459        d1.extend(encode_varint_field(
1460            CLASS_FLAGS,
1461            build_class_flags(4, 0, 0, false),
1462        ));
1463
1464        let stub = make_stub(d1, vec![]);
1465        let meta = decode_kotlin_metadata(&stub).unwrap();
1466        assert_eq!(meta.visibility, KotlinVisibility::PrivateToThis);
1467    }
1468
1469    #[test]
1470    fn visibility_local() {
1471        let mut d1 = Vec::new();
1472        d1.extend(encode_varint_field(
1473            CLASS_FLAGS,
1474            build_class_flags(5, 0, 0, false),
1475        ));
1476
1477        let stub = make_stub(d1, vec![]);
1478        let meta = decode_kotlin_metadata(&stub).unwrap();
1479        assert_eq!(meta.visibility, KotlinVisibility::Local);
1480    }
1481
1482    // -- Type decoder tests -------------------------------------------------
1483
1484    #[test]
1485    fn decode_type_nullable() {
1486        let data = build_type_message(true, Some(5));
1487        let decoded = decode_type(&data).unwrap();
1488        assert!(decoded.nullable);
1489        assert_eq!(decoded.class_name_index, Some(5));
1490    }
1491
1492    #[test]
1493    fn decode_type_non_nullable() {
1494        let data = build_type_message(false, Some(3));
1495        let decoded = decode_type(&data).unwrap();
1496        assert!(!decoded.nullable);
1497        assert_eq!(decoded.class_name_index, Some(3));
1498    }
1499
1500    #[test]
1501    fn decode_type_no_class_name() {
1502        let data = build_type_message(true, None);
1503        let decoded = decode_type(&data).unwrap();
1504        assert!(decoded.nullable);
1505        assert_eq!(decoded.class_name_index, None);
1506    }
1507
1508    #[test]
1509    fn decode_type_empty() {
1510        let decoded = decode_type(&[]).unwrap();
1511        assert!(!decoded.nullable);
1512        assert_eq!(decoded.class_name_index, None);
1513    }
1514}