use crate::stub::model::KotlinMetadataStub;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KotlinClassMetadata {
pub kind: KotlinClassKind,
pub visibility: KotlinVisibility,
pub is_data: bool,
pub is_sealed: bool,
pub companion_object_name: Option<String>,
pub extension_functions: Vec<KotlinExtensionFunction>,
pub nullable_properties: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KotlinClassKind {
Class,
Interface,
EnumClass,
EnumEntry,
AnnotationClass,
Object,
CompanionObject,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KotlinVisibility {
Internal,
Private,
Protected,
Public,
PrivateToThis,
Local,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KotlinExtensionFunction {
pub name: String,
pub receiver_type: String,
pub receiver_nullable: bool,
}
const WIRE_VARINT: u8 = 0;
const WIRE_64BIT: u8 = 1;
const WIRE_LENGTH_DELIMITED: u8 = 2;
const WIRE_32BIT: u8 = 5;
const CLASS_FLAGS: u32 = 1;
const CLASS_FUNCTIONS: u32 = 14;
const CLASS_PROPERTIES: u32 = 15;
const CLASS_COMPANION_OBJECT_NAME: u32 = 20;
const FUNCTION_FLAGS: u32 = 1;
const FUNCTION_NAME: u32 = 2;
const FUNCTION_RECEIVER_TYPE: u32 = 6;
const PROPERTY_FLAGS: u32 = 1;
const PROPERTY_NAME: u32 = 2;
const PROPERTY_RETURN_TYPE: u32 = 5;
const TYPE_FLAGS: u32 = 1;
const TYPE_CLASS_NAME: u32 = 6;
fn extract_visibility(flags: u64) -> KotlinVisibility {
match (flags >> 3) & 0x7 {
0 => KotlinVisibility::Internal,
1 => KotlinVisibility::Private,
2 => KotlinVisibility::Protected,
3 => KotlinVisibility::Public,
4 => KotlinVisibility::PrivateToThis,
5 => KotlinVisibility::Local,
_ => KotlinVisibility::Public, }
}
fn extract_modality(flags: u64) -> u8 {
((flags >> 6) & 0x7) as u8
}
fn extract_class_kind(flags: u64) -> KotlinClassKind {
match (flags >> 9) & 0x7 {
0 => KotlinClassKind::Class,
1 => KotlinClassKind::Interface,
2 => KotlinClassKind::EnumClass,
3 => KotlinClassKind::EnumEntry,
4 => KotlinClassKind::AnnotationClass,
5 => KotlinClassKind::Object,
6 => KotlinClassKind::CompanionObject,
_ => KotlinClassKind::Class, }
}
struct WireReader<'a> {
data: &'a [u8],
pos: usize,
}
impl<'a> WireReader<'a> {
fn new(data: &'a [u8]) -> Self {
Self { data, pos: 0 }
}
fn has_remaining(&self) -> bool {
self.pos < self.data.len()
}
fn read_varint(&mut self) -> Option<u64> {
let mut result: u64 = 0;
let mut shift: u32 = 0;
for _ in 0..10 {
if self.pos >= self.data.len() {
return None;
}
let byte = self.data[self.pos];
self.pos += 1;
result |= u64::from(byte & 0x7F) << shift;
if byte & 0x80 == 0 {
return Some(result);
}
shift += 7;
}
None
}
fn read_tag(&mut self) -> Option<(u32, u8)> {
let raw = self.read_varint()?;
let wire_type = (raw & 0x7) as u8;
let field_number = (raw >> 3) as u32;
if field_number == 0 {
return None; }
Some((field_number, wire_type))
}
fn read_length_delimited(&mut self) -> Option<&'a [u8]> {
let len = self.read_varint()? as usize;
if self.pos + len > self.data.len() {
return None;
}
let slice = &self.data[self.pos..self.pos + len];
self.pos += len;
Some(slice)
}
fn skip_field(&mut self, wire_type: u8) -> bool {
match wire_type {
WIRE_VARINT => self.read_varint().is_some(),
WIRE_64BIT => {
if self.pos + 8 > self.data.len() {
return false;
}
self.pos += 8;
true
}
WIRE_LENGTH_DELIMITED => self.read_length_delimited().is_some(),
WIRE_32BIT => {
if self.pos + 4 > self.data.len() {
return false;
}
self.pos += 4;
true
}
_ => false, }
}
}
fn string_table_lookup(d2: &[String], index: u64) -> Option<&str> {
let idx = index as usize;
d2.get(idx).map(String::as_str)
}
struct DecodedType {
nullable: bool,
class_name_index: Option<u64>,
}
fn decode_type(data: &[u8]) -> Option<DecodedType> {
let mut reader = WireReader::new(data);
let mut flags: u64 = 0;
let mut class_name_index: Option<u64> = None;
while reader.has_remaining() {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(TYPE_FLAGS, WIRE_VARINT) => {
flags = reader.read_varint()?;
}
(TYPE_CLASS_NAME, WIRE_VARINT) => {
class_name_index = Some(reader.read_varint()?);
}
_ => {
if !reader.skip_field(wire_type) {
return None;
}
}
}
}
Some(DecodedType {
nullable: (flags >> 1) & 1 == 1,
class_name_index,
})
}
struct DecodedFunction {
name_index: u64,
receiver_type: Option<DecodedType>,
}
fn decode_function(data: &[u8]) -> Option<DecodedFunction> {
let mut reader = WireReader::new(data);
let mut name_index: u64 = 0;
let mut receiver_type: Option<DecodedType> = None;
while reader.has_remaining() {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(FUNCTION_FLAGS, WIRE_VARINT) => {
let _flags = reader.read_varint()?;
}
(FUNCTION_NAME, WIRE_VARINT) => {
name_index = reader.read_varint()?;
}
(FUNCTION_RECEIVER_TYPE, WIRE_LENGTH_DELIMITED) => {
let type_data = reader.read_length_delimited()?;
receiver_type = decode_type(type_data);
}
_ => {
if !reader.skip_field(wire_type) {
return None;
}
}
}
}
Some(DecodedFunction {
name_index,
receiver_type,
})
}
struct DecodedProperty {
name_index: u64,
return_type_nullable: bool,
}
fn decode_property(data: &[u8]) -> Option<DecodedProperty> {
let mut reader = WireReader::new(data);
let mut name_index: u64 = 0;
let mut return_type_nullable = false;
while reader.has_remaining() {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(PROPERTY_FLAGS, WIRE_VARINT) => {
let _flags = reader.read_varint()?;
}
(PROPERTY_NAME, WIRE_VARINT) => {
name_index = reader.read_varint()?;
}
(PROPERTY_RETURN_TYPE, WIRE_LENGTH_DELIMITED) => {
let type_data = reader.read_length_delimited()?;
if let Some(decoded) = decode_type(type_data) {
return_type_nullable = decoded.nullable;
}
}
_ => {
if !reader.skip_field(wire_type) {
return None;
}
}
}
}
Some(DecodedProperty {
name_index,
return_type_nullable,
})
}
fn decode_class_message(data: &[u8], string_table: &[String]) -> Option<KotlinClassMetadata> {
let mut reader = WireReader::new(data);
let mut flags: u64 = 0;
let mut companion_name_index: Option<u64> = None;
let mut extension_functions = Vec::new();
let mut nullable_properties = Vec::new();
while reader.has_remaining() {
let (field_number, wire_type) = reader.read_tag()?;
match (field_number, wire_type) {
(CLASS_FLAGS, WIRE_VARINT) => {
flags = reader.read_varint()?;
}
(CLASS_FUNCTIONS, WIRE_LENGTH_DELIMITED) => {
let func_data = reader.read_length_delimited()?;
if let Some(func) = decode_function(func_data)
&& let Some(ref recv_type) = func.receiver_type
{
let fn_name = string_table_lookup(string_table, func.name_index)
.unwrap_or("<unknown>")
.to_owned();
let receiver_name = recv_type
.class_name_index
.and_then(|idx| string_table_lookup(string_table, idx))
.unwrap_or("<unknown>")
.to_owned();
extension_functions.push(KotlinExtensionFunction {
name: fn_name,
receiver_type: receiver_name,
receiver_nullable: recv_type.nullable,
});
}
}
(CLASS_PROPERTIES, WIRE_LENGTH_DELIMITED) => {
let prop_data = reader.read_length_delimited()?;
if let Some(prop) = decode_property(prop_data)
&& prop.return_type_nullable
&& let Some(name) = string_table_lookup(string_table, prop.name_index)
{
nullable_properties.push(name.to_owned());
}
}
(CLASS_COMPANION_OBJECT_NAME, WIRE_VARINT) => {
companion_name_index = Some(reader.read_varint()?);
}
_ => {
if !reader.skip_field(wire_type) {
return None;
}
}
}
}
let class_kind = extract_class_kind(flags);
let visibility = extract_visibility(flags);
let modality = extract_modality(flags);
let is_data = class_kind == KotlinClassKind::Class && (flags >> 12) & 1 == 1;
let is_sealed = modality == 3;
let companion_object_name = companion_name_index
.and_then(|idx| string_table_lookup(string_table, idx))
.map(str::to_owned);
Some(KotlinClassMetadata {
kind: class_kind,
visibility,
is_data,
is_sealed,
companion_object_name,
extension_functions,
nullable_properties,
})
}
pub fn decode_kotlin_metadata(stub: &KotlinMetadataStub) -> Option<KotlinClassMetadata> {
if stub.kind != 1 {
log::debug!(
"skipping Kotlin metadata kind {} (only kind=1 Class is supported)",
stub.kind,
);
return None;
}
if let Some(&major) = stub.metadata_version.first()
&& !(1..=2).contains(&major)
{
log::warn!(
"unsupported Kotlin metadata version {:?}, skipping",
stub.metadata_version,
);
return None;
}
let d1_combined = combine_d1_chunks(&stub.data1)?;
decode_class_message(&d1_combined, &stub.data2)
}
fn combine_d1_chunks(d1: &[String]) -> Option<Vec<u8>> {
if d1.is_empty() {
return None;
}
let total_chars: usize = d1.iter().map(|s| s.chars().count()).sum();
let mut bytes = Vec::with_capacity(total_chars);
for chunk in d1 {
for ch in chunk.chars() {
bytes.push((ch as u32 & 0xFF) as u8);
}
}
Some(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
fn encode_varint(mut value: u64) -> Vec<u8> {
let mut buf = Vec::new();
loop {
let mut byte = (value & 0x7F) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
buf.push(byte);
if value == 0 {
break;
}
}
buf
}
fn encode_tag(field_number: u32, wire_type: u8) -> Vec<u8> {
encode_varint(u64::from(field_number) << 3 | u64::from(wire_type))
}
fn encode_varint_field(field_number: u32, value: u64) -> Vec<u8> {
let mut buf = encode_tag(field_number, WIRE_VARINT);
buf.extend(encode_varint(value));
buf
}
fn encode_length_delimited_field(field_number: u32, data: &[u8]) -> Vec<u8> {
let mut buf = encode_tag(field_number, WIRE_LENGTH_DELIMITED);
buf.extend(encode_varint(data.len() as u64));
buf.extend(data);
buf
}
fn build_class_flags(visibility: u64, modality: u64, class_kind: u64, is_data: bool) -> u64 {
let mut flags = 0u64;
flags |= visibility << 3;
flags |= modality << 6;
flags |= class_kind << 9;
if is_data {
flags |= 1 << 12;
}
flags
}
fn build_type_message(nullable: bool, class_name_index: Option<u64>) -> Vec<u8> {
let mut buf = Vec::new();
let mut flags: u64 = 0;
if nullable {
flags |= 1 << 1;
}
if flags != 0 {
buf.extend(encode_varint_field(TYPE_FLAGS, flags));
}
if let Some(idx) = class_name_index {
buf.extend(encode_varint_field(TYPE_CLASS_NAME, idx));
}
buf
}
fn build_function_message(name_index: u64, receiver_type: Option<&[u8]>) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend(encode_varint_field(FUNCTION_FLAGS, 0));
buf.extend(encode_varint_field(FUNCTION_NAME, name_index));
if let Some(rt) = receiver_type {
buf.extend(encode_length_delimited_field(FUNCTION_RECEIVER_TYPE, rt));
}
buf
}
fn build_property_message(name_index: u64, return_type: Option<&[u8]>) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend(encode_varint_field(PROPERTY_FLAGS, 0));
buf.extend(encode_varint_field(PROPERTY_NAME, name_index));
if let Some(rt) = return_type {
buf.extend(encode_length_delimited_field(PROPERTY_RETURN_TYPE, rt));
}
buf
}
fn make_stub(d1_bytes: Vec<u8>, string_table: Vec<&str>) -> KotlinMetadataStub {
let d1_string: String = d1_bytes.iter().map(|&b| b as char).collect();
KotlinMetadataStub {
kind: 1,
metadata_version: vec![1, 9, 0],
data1: vec![d1_string],
data2: string_table.into_iter().map(str::to_owned).collect(),
extra_string: None,
package_name: None,
extra_int: None,
}
}
#[test]
fn wire_reader_varint_single_byte() {
let data = [0x05]; let mut reader = WireReader::new(&data);
assert_eq!(reader.read_varint(), Some(5));
assert!(!reader.has_remaining());
}
#[test]
fn wire_reader_varint_multi_byte() {
let data = [0xAC, 0x02];
let mut reader = WireReader::new(&data);
assert_eq!(reader.read_varint(), Some(300));
}
#[test]
fn wire_reader_varint_max_bytes() {
let encoded = encode_varint(u64::MAX);
assert_eq!(encoded.len(), 10);
let mut reader = WireReader::new(&encoded);
assert_eq!(reader.read_varint(), Some(u64::MAX));
}
#[test]
fn wire_reader_varint_truncated() {
let data = [0x80];
let mut reader = WireReader::new(&data);
assert_eq!(reader.read_varint(), None);
}
#[test]
fn wire_reader_varint_exceeds_10_bytes() {
let data = [0x80; 11];
let mut reader = WireReader::new(&data);
assert_eq!(reader.read_varint(), None);
}
#[test]
fn wire_reader_tag_decomposition() {
let data = [0x1A];
let mut reader = WireReader::new(&data);
assert_eq!(reader.read_tag(), Some((3, 2)));
}
#[test]
fn wire_reader_tag_field_zero_invalid() {
let data = [0x02]; let mut reader = WireReader::new(&data);
assert_eq!(reader.read_tag(), None);
}
#[test]
fn wire_reader_length_delimited() {
let data = [0x0A, 0x03, 0x01, 0x02, 0x03];
let mut reader = WireReader::new(&data);
let (field, wire) = reader.read_tag().unwrap();
assert_eq!((field, wire), (1, 2));
let payload = reader.read_length_delimited().unwrap();
assert_eq!(payload, &[0x01, 0x02, 0x03]);
}
#[test]
fn wire_reader_length_delimited_truncated() {
let data = [0x05, 0x01, 0x02];
let mut reader = WireReader::new(&data);
assert_eq!(reader.read_length_delimited(), None);
}
#[test]
fn wire_reader_skip_varint() {
let mut data = encode_tag(99, WIRE_VARINT);
data.extend(encode_varint(42));
data.extend(encode_tag(1, WIRE_VARINT));
data.extend(encode_varint(7));
let mut reader = WireReader::new(&data);
let (field, wire) = reader.read_tag().unwrap();
assert_eq!(field, 99);
assert!(reader.skip_field(wire));
let (field2, _) = reader.read_tag().unwrap();
assert_eq!(field2, 1);
}
#[test]
fn wire_reader_skip_32bit() {
let mut data = vec![];
data.extend(encode_tag(5, WIRE_32BIT));
data.extend(&[0x00, 0x00, 0x00, 0x00]); data.extend(encode_tag(1, WIRE_VARINT));
data.extend(encode_varint(99));
let mut reader = WireReader::new(&data);
let (_, wire) = reader.read_tag().unwrap();
assert!(reader.skip_field(wire));
let (field, _) = reader.read_tag().unwrap();
assert_eq!(field, 1);
}
#[test]
fn wire_reader_skip_64bit() {
let mut data = vec![];
data.extend(encode_tag(5, WIRE_64BIT));
data.extend(&[0u8; 8]); data.extend(encode_tag(1, WIRE_VARINT));
data.extend(encode_varint(99));
let mut reader = WireReader::new(&data);
let (_, wire) = reader.read_tag().unwrap();
assert!(reader.skip_field(wire));
let (field, _) = reader.read_tag().unwrap();
assert_eq!(field, 1);
}
#[test]
fn wire_reader_skip_unknown_wire_type() {
let mut reader = WireReader::new(&[]);
assert!(!reader.skip_field(3)); }
#[test]
fn string_table_valid_lookup() {
let table = vec!["kotlin/String".to_owned(), "isEmail".to_owned()];
assert_eq!(string_table_lookup(&table, 0), Some("kotlin/String"));
assert_eq!(string_table_lookup(&table, 1), Some("isEmail"));
}
#[test]
fn string_table_out_of_bounds() {
let table = vec!["only_one".to_owned()];
assert_eq!(string_table_lookup(&table, 1), None);
assert_eq!(string_table_lookup(&table, 999), None);
}
#[test]
fn extension_receiver_detection() {
let receiver_type = build_type_message(false, Some(0));
let func = build_function_message(1, Some(&receiver_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
let stub = make_stub(d1, vec!["kotlin/String", "isEmail"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.extension_functions.len(), 1);
assert_eq!(meta.extension_functions[0].name, "isEmail");
assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
assert!(!meta.extension_functions[0].receiver_nullable);
}
#[test]
fn extension_receiver_nullable() {
let receiver_type = build_type_message(true, Some(0));
let func = build_function_message(1, Some(&receiver_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
let stub = make_stub(d1, vec!["kotlin/String", "isNullOrEmail"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.extension_functions.len(), 1);
assert_eq!(meta.extension_functions[0].name, "isNullOrEmail");
assert!(meta.extension_functions[0].receiver_nullable);
}
#[test]
fn regular_function_not_treated_as_extension() {
let func = build_function_message(0, None);
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func));
let stub = make_stub(d1, vec!["regularFunction"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(meta.extension_functions.is_empty());
}
#[test]
fn nullable_property_detection() {
let nullable_type = build_type_message(true, Some(0));
let prop = build_property_message(1, Some(&nullable_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop));
let stub = make_stub(d1, vec!["kotlin/String", "name"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.nullable_properties, vec!["name"]);
}
#[test]
fn non_nullable_property_not_included() {
let non_nullable_type = build_type_message(false, Some(0));
let prop = build_property_message(1, Some(&non_nullable_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop));
let stub = make_stub(d1, vec!["kotlin/String", "name"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(meta.nullable_properties.is_empty());
}
#[test]
fn companion_object_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 0));
let stub = make_stub(d1, vec!["Companion"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.companion_object_name, Some("Companion".to_owned()));
}
#[test]
fn companion_object_custom_name() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 0));
let stub = make_stub(d1, vec!["Factory"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.companion_object_name, Some("Factory".to_owned()));
}
#[test]
fn no_companion_object() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.companion_object_name, None);
}
#[test]
fn object_declaration_kind() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 5, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::Object);
}
#[test]
fn companion_object_kind() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 6, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::CompanionObject);
}
#[test]
fn data_class_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, true),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(meta.is_data);
assert_eq!(meta.kind, KotlinClassKind::Class);
}
#[test]
fn non_data_class() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(!meta.is_data);
}
#[test]
fn sealed_class_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 3, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(meta.is_sealed);
}
#[test]
fn non_sealed_class() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(!meta.is_sealed);
}
#[test]
fn visibility_public() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false), ));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::Public);
}
#[test]
fn visibility_private() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(1, 0, 0, false), ));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::Private);
}
#[test]
fn visibility_internal() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(0, 0, 0, false), ));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::Internal);
}
#[test]
fn visibility_protected() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(2, 0, 0, false), ));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::Protected);
}
#[test]
fn malformed_protobuf_returns_none() {
let stub = make_stub(
vec![
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
],
vec![],
);
let _result = decode_kotlin_metadata(&stub);
}
#[test]
fn empty_d1_returns_none() {
let stub = KotlinMetadataStub {
kind: 1,
metadata_version: vec![1, 9, 0],
data1: vec![],
data2: vec![],
extra_string: None,
package_name: None,
extra_int: None,
};
assert_eq!(decode_kotlin_metadata(&stub), None);
}
#[test]
fn truncated_varint_returns_none() {
let stub = make_stub(vec![0x80], vec![]);
assert_eq!(decode_kotlin_metadata(&stub), None);
}
#[test]
fn unsupported_kind_returns_none() {
let stub = KotlinMetadataStub {
kind: 2, metadata_version: vec![1, 9, 0],
data1: vec!["data".to_owned()],
data2: vec![],
extra_string: None,
package_name: None,
extra_int: None,
};
assert_eq!(decode_kotlin_metadata(&stub), None);
}
#[test]
fn unsupported_kind_synthetic() {
let stub = KotlinMetadataStub {
kind: 3,
metadata_version: vec![1, 9, 0],
data1: vec![],
data2: vec![],
extra_string: None,
package_name: None,
extra_int: None,
};
assert_eq!(decode_kotlin_metadata(&stub), None);
}
#[test]
fn unsupported_metadata_version() {
let stub = KotlinMetadataStub {
kind: 1,
metadata_version: vec![99, 0, 0], data1: vec!["data".to_owned()],
data2: vec![],
extra_string: None,
package_name: None,
extra_int: None,
};
assert_eq!(decode_kotlin_metadata(&stub), None);
}
#[test]
fn comprehensive_class_decoding() {
let string_table = vec!["kotlin/String", "isEmail", "name", "Companion", "toString"];
let receiver_type = build_type_message(false, Some(0));
let ext_func = build_function_message(1, Some(&receiver_type));
let regular_func = build_function_message(4, None);
let nullable_type = build_type_message(true, Some(0));
let nullable_prop = build_property_message(2, Some(&nullable_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &ext_func));
d1.extend(encode_length_delimited_field(
CLASS_FUNCTIONS,
®ular_func,
));
d1.extend(encode_length_delimited_field(
CLASS_PROPERTIES,
&nullable_prop,
));
d1.extend(encode_varint_field(CLASS_COMPANION_OBJECT_NAME, 3));
let stub = make_stub(d1, string_table);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::Class);
assert_eq!(meta.visibility, KotlinVisibility::Public);
assert!(!meta.is_data);
assert!(!meta.is_sealed);
assert_eq!(meta.companion_object_name, Some("Companion".to_owned()));
assert_eq!(meta.extension_functions.len(), 1);
assert_eq!(meta.extension_functions[0].name, "isEmail");
assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
assert_eq!(meta.nullable_properties, vec!["name"]);
}
#[test]
fn interface_kind_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 2, 1, false), ));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::Interface);
}
#[test]
fn enum_class_kind_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 2, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::EnumClass);
}
#[test]
fn annotation_class_kind_detection() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 4, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::AnnotationClass);
}
#[test]
fn multiple_extension_functions() {
let recv_string = build_type_message(false, Some(0));
let recv_list = build_type_message(false, Some(2));
let func1 = build_function_message(1, Some(&recv_string));
let func2 = build_function_message(3, Some(&recv_list));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func1));
d1.extend(encode_length_delimited_field(CLASS_FUNCTIONS, &func2));
let stub = make_stub(
d1,
vec!["kotlin/String", "isEmail", "kotlin/List", "firstOrNull"],
);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.extension_functions.len(), 2);
assert_eq!(meta.extension_functions[0].name, "isEmail");
assert_eq!(meta.extension_functions[0].receiver_type, "kotlin/String");
assert_eq!(meta.extension_functions[1].name, "firstOrNull");
assert_eq!(meta.extension_functions[1].receiver_type, "kotlin/List");
}
#[test]
fn multiple_nullable_properties() {
let nullable_type = build_type_message(true, Some(0));
let prop1 = build_property_message(1, Some(&nullable_type));
let prop2 = build_property_message(2, Some(&nullable_type));
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 0, 0, false),
));
d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop1));
d1.extend(encode_length_delimited_field(CLASS_PROPERTIES, &prop2));
let stub = make_stub(d1, vec!["kotlin/String", "name", "email"]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.nullable_properties.len(), 2);
assert!(meta.nullable_properties.contains(&"name".to_owned()));
assert!(meta.nullable_properties.contains(&"email".to_owned()));
}
#[test]
fn combine_d1_multiple_chunks() {
let d1 = vec!["hel".to_owned(), "lo".to_owned()];
let combined = combine_d1_chunks(&d1).unwrap();
assert_eq!(combined, b"hello");
}
#[test]
fn combine_d1_empty() {
let d1: Vec<String> = vec![];
assert_eq!(combine_d1_chunks(&d1), None);
}
#[test]
fn data_class_with_sealed_is_not_data() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(3, 3, 0, true),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert!(meta.is_data);
assert!(meta.is_sealed);
}
#[test]
fn missing_flags_defaults_to_internal_final_class() {
let d1 = Vec::new();
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.kind, KotlinClassKind::Class);
assert_eq!(meta.visibility, KotlinVisibility::Internal);
assert!(!meta.is_data);
assert!(!meta.is_sealed);
assert!(meta.companion_object_name.is_none());
assert!(meta.extension_functions.is_empty());
assert!(meta.nullable_properties.is_empty());
}
#[test]
fn visibility_private_to_this() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(4, 0, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::PrivateToThis);
}
#[test]
fn visibility_local() {
let mut d1 = Vec::new();
d1.extend(encode_varint_field(
CLASS_FLAGS,
build_class_flags(5, 0, 0, false),
));
let stub = make_stub(d1, vec![]);
let meta = decode_kotlin_metadata(&stub).unwrap();
assert_eq!(meta.visibility, KotlinVisibility::Local);
}
#[test]
fn decode_type_nullable() {
let data = build_type_message(true, Some(5));
let decoded = decode_type(&data).unwrap();
assert!(decoded.nullable);
assert_eq!(decoded.class_name_index, Some(5));
}
#[test]
fn decode_type_non_nullable() {
let data = build_type_message(false, Some(3));
let decoded = decode_type(&data).unwrap();
assert!(!decoded.nullable);
assert_eq!(decoded.class_name_index, Some(3));
}
#[test]
fn decode_type_no_class_name() {
let data = build_type_message(true, None);
let decoded = decode_type(&data).unwrap();
assert!(decoded.nullable);
assert_eq!(decoded.class_name_index, None);
}
#[test]
fn decode_type_empty() {
let decoded = decode_type(&[]).unwrap();
assert!(!decoded.nullable);
assert_eq!(decoded.class_name_index, None);
}
}