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