/* Original code from Yara dotnet.c, forked in 2017-2025 by pancake for radare2 */

/*
Copyright(c) 2015. The YARA Authors. All Rights Reserved.

Licensed under the Apache License, Version 2.0(the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

#define _GNU_SOURCE

#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>
#include <ctype.h>
#include <time.h>
#include <r_types.h>
#include <r_util.h>
#include <r_list.h>

#include "dotnet.h"

typedef struct _PE {
	const uint8_t *data;
	size_t data_size;
	void *object;
} PE;

static char *pe_get_dotnet_string(PE *pe, const uint8_t *string_offset, ut32 string_index) {
	// Start of string must be within boundary
	if (! (string_offset + string_index >= pe->data &&
		string_offset + string_index < pe->data + pe->data_size)) {
		return NULL;
	}
	// Calculate how much until end of boundary, don't scan past that.
	size_t remaining = (pe->data + pe->data_size) - (string_offset + string_index);
	// Search for a NULL terminator from start of string, up to remaining.
	char *start = (char *) (string_offset + string_index);
	char *eos = (char *)r_mem_mem ((void *)start, remaining, (void *)"\0", 1);
	return eos? start: NULL;
}

static uint32_t max_rows(int count, ...) {
	va_list ap;
	int i;
	uint32_t biggest;
	uint32_t x;

	if (count == 0) {
		return 0;
	}

	va_start (ap, count);
	biggest = va_arg (ap, uint32_t);

	for (i = 1; i < count; i++) {
		x = va_arg (ap, uint32_t);
		if (x > biggest) {
			biggest = x;
		}
	}

	va_end (ap);
	return biggest;
}

static ut64 dotnet_max_stream_headers(PE *pe, ut64 offset) {
	if (offset >= pe->data_size) {
		return 0;
	}
	ut64 remaining_size = pe->data_size - offset;
	return remaining_size / (sizeof (STREAM_HEADER) + 4);
}

static ut32 dotnet_max_rows_at(PE *pe, const ut8 *row_ptr, ut32 row_size) {
	const ut8 *data = (const ut8 *)pe->data;
	const ut8 *end = data + pe->data_size;
	if (!row_size || row_ptr < data || row_ptr > end) {
		return 0;
	}
	ut64 remaining_size = end - row_ptr;
	return (ut32)(remaining_size / row_size);
}

static bool dotnet_parse_tilde_rows(PE *pe, PTILDE_HEADER tilde_header, R_OUT ROWS *rows, R_OUT INDEX_SIZES *index_sizes) {
	int bit_check, matched_bits = 0;

	if (!pe || !rows || !index_sizes || !struct_fits_in_pe (pe, tilde_header, TILDE_HEADER)) {
		return false;
	}

	memset (rows, 0, sizeof (ROWS));
	memset (index_sizes, 2, sizeof (INDEX_SIZES));

	if (tilde_header->HeapSizes & 0x01) {
		index_sizes->string = 4;
	}
	if (tilde_header->HeapSizes & 0x02) {
		index_sizes->guid = 4;
	}
	if (tilde_header->HeapSizes & 0x04) {
		index_sizes->blob = 4;
	}

	ut32 *row_offset = (ut32 *)(tilde_header + 1);
	for (bit_check = 0; bit_check < 64; bit_check++) {
		ut32 *row = NULL;
		ut8 *index_size = NULL;

		if (!((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}
		if (!fits_in_pe (pe, row_offset, (matched_bits + 1) * sizeof (ut32))) {
			return false;
		}
		ut32 row_count = *(row_offset + matched_bits);
		switch (bit_check) {
		case BIT_MODULE:
			row = &rows->module;
			break;
		case BIT_MODULEREF:
			row = &rows->moduleref;
			index_size = &index_sizes->moduleref;
			break;
		case BIT_ASSEMBLYREF:
			row = &rows->assemblyref;
			index_size = &index_sizes->assemblyref;
			break;
		case BIT_TYPEREF:
			row = &rows->typeref;
			break;
		case BIT_METHODDEF:
			row = &rows->methoddef;
			index_size = &index_sizes->methoddef;
			break;
		case BIT_MEMBERREF:
			row = &rows->memberref;
			index_size = &index_sizes->memberref;
			break;
		case BIT_TYPEDEF:
			row = &rows->typedef_;
			index_size = &index_sizes->typedef_;
			break;
		case BIT_TYPESPEC:
			row = &rows->typespec;
			break;
		case BIT_FIELD:
			row = &rows->field;
			index_size = &index_sizes->field;
			break;
		case BIT_PARAM:
			row = &rows->param;
			index_size = &index_sizes->param;
			break;
		case BIT_PROPERTY:
			row = &rows->property;
			index_size = &index_sizes->property;
			break;
		case BIT_INTERFACEIMPL:
			row = &rows->interfaceimpl;
			break;
		case BIT_EVENT:
			row = &rows->event;
			index_size = &index_sizes->event;
			break;
		case BIT_STANDALONESIG:
			row = &rows->standalonesig;
			break;
		case BIT_ASSEMBLY:
			row = &rows->assembly;
			break;
		case BIT_FILE:
			row = &rows->file;
			break;
		case BIT_EXPORTEDTYPE:
			row = &rows->exportedtype;
			break;
		case BIT_MANIFESTRESOURCE:
			row = &rows->manifestresource;
			break;
		case BIT_GENERICPARAM:
			row = &rows->genericparam;
			index_size = &index_sizes->genericparam;
			break;
		case BIT_GENERICPARAMCONSTRAINT:
			row = &rows->genericparamconstraint;
			break;
		case BIT_METHODSPEC:
			row = &rows->methodspec;
			break;
		case BIT_ASSEMBLYREFPROCESSOR:
			row = &rows->assemblyrefprocessor;
			index_size = &index_sizes->assemblyrefprocessor;
			break;
		default:
			break;
		}

		if (row) {
			*row = row_count;
			if (index_size && row_count > 0xFFFF) {
				*index_size = 4;
			}
		}
		matched_bits++;
	}
	return true;
}

static bool dotnet_tilde_table_size(R_IN const ROWS *rows, R_IN const INDEX_SIZES *index_sizes, int bit_check, ut32 num_rows, R_OUT ut32 *table_size) {
	ut32 row_count = 0;
	ut64 table_size64 = 0;

	if (!rows || !index_sizes || !table_size) {
		return false;
	}

	switch (bit_check) {
	case BIT_MODULE:
		table_size64 = (ut64)(2 + index_sizes->string + (index_sizes->guid * 3)) * num_rows;
		break;
	case BIT_TYPEREF:
		row_count = max_rows (3, rows->module, rows->moduleref, rows->assemblyref);
		table_size64 = (ut64)((row_count > (0xFFFF >> 0x02)? 4: 2) + (index_sizes->string * 2)) * num_rows;
		break;
	case BIT_TYPEDEF:
		row_count = max_rows (3, rows->typedef_, rows->typeref, rows->typespec);
		table_size64 = (ut64)(4 + (index_sizes->string * 2) + (row_count > (0xFFFF >> 0x02)? 4: 2) +
			index_sizes->field + index_sizes->methoddef) * num_rows;
		break;
	case BIT_FIELDPTR:
		table_size64 = (ut64)(index_sizes->field) * num_rows;
		break;
	case BIT_FIELD:
		table_size64 = (ut64)(2 + index_sizes->string + index_sizes->blob) * num_rows;
		break;
	case BIT_METHODDEFPTR:
		table_size64 = (ut64)(index_sizes->methoddef) * num_rows;
		break;
	case BIT_METHODDEF:
		table_size64 = (ut64)(4 + 2 + 2 + index_sizes->string + index_sizes->blob + index_sizes->param) * num_rows;
		break;
	case BIT_PARAMPTR:
		table_size64 = (ut64)(index_sizes->param) * num_rows;
		break;
	case BIT_PARAM:
		table_size64 = (ut64)(2 + 2 + index_sizes->string) * num_rows;
		break;
	case BIT_INTERFACEIMPL:
		row_count = max_rows (3, rows->typedef_, rows->typeref, rows->typespec);
		table_size64 = (ut64)(index_sizes->typedef_ + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
		break;
	case BIT_MEMBERREF:
		row_count = max_rows (4, rows->methoddef, rows->memberref, rows->typeref, rows->typespec);
		table_size64 = (ut64)((row_count > (0xFFFF >> 0x03)? 4: 2) + index_sizes->string + index_sizes->blob) * num_rows;
		break;
	case BIT_CONSTANT:
		row_count = max_rows (3, rows->field, rows->param, rows->property);
		table_size64 = (ut64)(2 + (row_count > (0xFFFF >> 0x02)? 4: 2) + index_sizes->blob) * num_rows;
		break;
	case BIT_CUSTOMATTRIBUTE:
		row_count = max_rows (3, rows->methoddef, rows->field, rows->param);
		table_size64 = (ut64)((row_count > (0xFFFF >> 0x05)? 4: 2) + index_sizes->memberref + index_sizes->blob) * num_rows;
		break;
	case BIT_FIELDMARSHAL:
		row_count = max_rows (2, rows->field, rows->param);
		table_size64 = (ut64)((row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes->blob) * num_rows;
		break;
	case BIT_DECLSECURITY:
		row_count = max_rows (3, rows->typedef_, rows->methoddef, rows->assembly);
		table_size64 = (ut64)(2 + (row_count > (0xFFFF >> 0x02)? 4: 2) + index_sizes->blob) * num_rows;
		break;
	case BIT_CLASSLAYOUT:
		table_size64 = (ut64)(2 + 4 + index_sizes->typedef_) * num_rows;
		break;
	case BIT_FIELDLAYOUT:
		table_size64 = (ut64)(4 + index_sizes->field) * num_rows;
		break;
	case BIT_STANDALONESIG:
		table_size64 = (ut64)(index_sizes->blob) * num_rows;
		break;
	case BIT_EVENTMAP:
		table_size64 = (ut64)(index_sizes->typedef_ + index_sizes->event) * num_rows;
		break;
	case BIT_EVENTPTR:
		table_size64 = (ut64)(index_sizes->event) * num_rows;
		break;
	case BIT_EVENT:
		table_size64 = (ut64)(2 + index_sizes->string + index_sizes->typedef_) * num_rows;
		break;
	case BIT_PROPERTYMAP:
		table_size64 = (ut64)(index_sizes->typedef_ + index_sizes->property) * num_rows;
		break;
	case BIT_PROPERTYPTR:
		table_size64 = (ut64)(index_sizes->property) * num_rows;
		break;
	case BIT_PROPERTY:
		table_size64 = (ut64)(2 + index_sizes->string + index_sizes->blob) * num_rows;
		break;
	case BIT_METHODSEMANTICS:
		row_count = max_rows (2, rows->event, rows->property);
		table_size64 = (ut64)(2 + index_sizes->methoddef + (row_count > (0xFFFF >> 0x01)? 4: 2)) * num_rows;
		break;
	case BIT_METHODIMPL:
		row_count = max_rows (2, rows->methoddef, rows->memberref);
		table_size64 = (ut64)(index_sizes->typedef_ + index_sizes->methoddef + (row_count > (0xFFFF >> 0x01)? 4: 2)) * num_rows;
		break;
	case BIT_MODULEREF:
		table_size64 = (ut64)(index_sizes->string) * num_rows;
		break;
	case BIT_TYPESPEC:
		table_size64 = (ut64)(index_sizes->blob) * num_rows;
		break;
	case BIT_IMPLMAP:
		row_count = max_rows (3, rows->field, rows->methoddef, rows->typedef_);
		table_size64 = (ut64)(2 + (row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes->string + index_sizes->moduleref) * num_rows;
		break;
	case BIT_FIELDRVA:
		table_size64 = (ut64)(4 + index_sizes->field) * num_rows;
		break;
	case BIT_ENCLOG:
		table_size64 = (ut64)(4) * num_rows;
		break;
	case BIT_ENCMAP:
		table_size64 = (ut64)(4) * num_rows;
		break;
	case BIT_ASSEMBLY:
		table_size64 = (ut64)(2 + 2 + 2 + 2 + 4 + index_sizes->blob + (index_sizes->string * 2)) * num_rows;
		break;
	case BIT_ASSEMBLYPROCESSOR:
		table_size64 = (ut64)(4) * num_rows;
		break;
	case BIT_ASSEMBLYOS:
		table_size64 = (ut64)(4 + 4 + 4) * num_rows;
		break;
	case BIT_ASSEMBLYREF:
		table_size64 = (ut64)(2 + 2 + 2 + 2 + 4 + (index_sizes->blob * 2) + (index_sizes->string * 2)) * num_rows;
		break;
	case BIT_ASSEMBLYREFPROCESSOR:
		table_size64 = (ut64)(4 + index_sizes->assemblyrefprocessor) * num_rows;
		break;
	case BIT_ASSEMBLYREFOS:
		table_size64 = (ut64)(4 + 4 + 4 + index_sizes->assemblyref) * num_rows;
		break;
	case BIT_FILE:
		table_size64 = (ut64)(4 + index_sizes->string + index_sizes->blob) * num_rows;
		break;
	case BIT_EXPORTEDTYPE:
		row_count = max_rows (3, rows->file, rows->assemblyref, rows->exportedtype);
		table_size64 = (ut64)(4 + 4 + (index_sizes->string * 2) + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
		break;
	case BIT_MANIFESTRESOURCE:
		row_count = max_rows (2, rows->file, rows->assemblyref);
		table_size64 = (ut64)(4 + 4 + index_sizes->string + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
		break;
	case BIT_NESTEDCLASS:
		table_size64 = (ut64)(index_sizes->typedef_ * 2) * num_rows;
		break;
	case BIT_GENERICPARAM:
		row_count = max_rows (2, rows->typedef_, rows->methoddef);
		table_size64 = (ut64)(2 + 2 + (row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes->string) * num_rows;
		break;
	case BIT_METHODSPEC:
		row_count = max_rows (2, rows->methoddef, rows->memberref);
		table_size64 = (ut64)((row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes->blob) * num_rows;
		break;
	case BIT_GENERICPARAMCONSTRAINT:
		row_count = max_rows (3, rows->typedef_, rows->typeref, rows->typespec);
		table_size64 = (ut64)(index_sizes->genericparam + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
		break;
	default:
		return false;
	}

	if (table_size64 > UT32_MAX) {
		return false;
	}
	*table_size = (ut32)table_size64;
	return true;
}

static const ut8 *dotnet_tilde_table_offset(PE *pe, PTILDE_HEADER tilde_header, R_IN const ROWS *rows, R_IN const INDEX_SIZES *index_sizes, int target_bit, R_OUT ut32 *target_rows) {
	int bit_check, matched_bits = 0, valid_tables = 0;

	if (!pe || !rows || !index_sizes || !target_rows || !struct_fits_in_pe (pe, tilde_header, TILDE_HEADER)) {
		return NULL;
	}

	ut32 *row_offset = (ut32 *)(tilde_header + 1);
	for (bit_check = 0; bit_check < 64; bit_check++) {
		valid_tables += ((tilde_header->Valid >> bit_check) & 0x01);
	}
	ut64 table_offset = (const ut8 *)row_offset - pe->data;
	table_offset += (ut64)sizeof (ut32) * valid_tables;

	for (bit_check = 0; bit_check < 64; bit_check++) {
		ut32 num_rows;
		ut32 table_size;

		if (!((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}
		if (!fits_in_pe (pe, row_offset, (matched_bits + 1) * sizeof (ut32))) {
			return NULL;
		}

		num_rows = *(row_offset + matched_bits);
		if (!dotnet_tilde_table_size (rows, index_sizes, bit_check, num_rows, &table_size)) {
			return NULL;
		}
		if (table_offset > pe->data_size || table_size > pe->data_size - table_offset) {
			return NULL;
		}
		if (bit_check == target_bit) {
			*target_rows = num_rows;
			return pe->data + table_offset;
		}

		table_offset += table_size;
		matched_bits++;
	}
	return NULL;
}

static STREAMS dotnet_parse_stream_headers(PE *pe, ut64 offset, ut64 metadata_root, ut32 num_streams) {
	char stream_name[DOTNET_STREAM_NAME_SIZE + 1];
	STREAMS headers = { 0 };
	unsigned int i;

	if (offset >= pe->data_size || num_streams > dotnet_max_stream_headers (pe, offset)) {
		return headers;
	}
	PSTREAM_HEADER stream_header = (PSTREAM_HEADER) (pe->data + offset);

	for (i = 0; i < num_streams; i++) {
		if (!struct_fits_in_pe (pe, stream_header, STREAM_HEADER)) {
			break;
		}

		char *start = (char *)stream_header->Name;

		if (!fits_in_pe (pe, start, DOTNET_STREAM_NAME_SIZE)) {
			break;
		}

		char *eos = (char *)r_mem_mem ((void *)start, DOTNET_STREAM_NAME_SIZE, (void *)"\0", 1);

		if (!eos) {
			break;
		}

		size_t name_len = eos - start;
		r_str_ncpy (stream_name, start, R_MIN (name_len + 1, DOTNET_STREAM_NAME_SIZE + 1));

		// Store necessary bits to parse these later
		if (r_str_startswith (stream_name, "#GUID")) {
			headers.guid = stream_header;
		} else if (r_str_startswith (stream_name, "#~") && !headers.tilde) {
			headers.tilde = stream_header;
		} else if (r_str_startswith (stream_name, "#Strings") && !headers.string) {
			headers.string = stream_header;
		} else if (r_str_startswith (stream_name, "#Blob")) {
			headers.blob = stream_header;
		} else if (r_str_startswith (stream_name, "#US") && !headers.us) {
			headers.us = stream_header;
		}

		// Stream name is padded to a multiple of 4.
		stream_header = (PSTREAM_HEADER) ((uint8_t *)stream_header +
			sizeof (STREAM_HEADER) +
			strlen (stream_name) +
			4 - (strlen (stream_name) % 4));
	}

	return headers;
}

static void dotnet_parse_tilde_assemblyref(
	PE *pe,
	PTILDE_HEADER tilde_header,
	ut64 metadata_root,
	ROWS rows,
	INDEX_SIZES index_sizes,
	PSTREAMS streams,
	RList *libraries) {

	int bit_check, matched_bits = 0;
	uint32_t num_rows = 0;
	uint8_t *row_ptr = NULL;
	int i;
	char *name;

	// Validate we have the required streams
	if (!streams->tilde || !streams->string) {
		return;
	}

	// Number of rows is the number of bits set to 1 in Valid
	for (i = 0; i < 64; i++) {
		matched_bits += ((tilde_header->Valid >> i) & 0x01);
	}

	uint32_t *row_offset = (uint32_t *) (tilde_header + 1);
	uint8_t *table_offset = (uint8_t *)row_offset;
	table_offset += sizeof (uint32_t) * matched_bits;

	if (metadata_root + streams->string->Offset >= pe->data_size) {
		return;
	}
	const uint8_t *string_offset = pe->data + metadata_root + streams->string->Offset;

	matched_bits = 0;

	// Parse AssemblyRef table
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

		if (!fits_in_pe (pe, table_offset, 1)) {
			return;
		}

		if (!fits_in_pe (pe, (uint8_t *)row_offset, (matched_bits + 1) * sizeof (uint32_t))) {
			return;
		}
		num_rows = *(row_offset + matched_bits);

		if (bit_check == BIT_ASSEMBLYREF) {
			// AssemblyRef structure: MajorVersion (2) MinorVersion (2) BuildNumber (2) RevisionNumber (2)
			// Flags (4) PublicKeyOrToken (blob) Name (string) Culture (string)
			row_ptr = table_offset;
			ut32 row_size = 2 + 2 + 2 + 2 + 4 + (index_sizes.blob * 2) + (index_sizes.string * 2);
			ut32 max_rows = dotnet_max_rows_at (pe, row_ptr, row_size);
			if (num_rows > max_rows) {
				return;
			}
			for (i = 0; i < num_rows; i++) {
				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}
				ut16 major = r_read_le16 (row_ptr);
				ut16 minor = r_read_le16 (row_ptr + 2);
				ut16 build = r_read_le16 (row_ptr + 4);
				ut16 revision = r_read_le16 (row_ptr + 6);

				// Get assembly name from string stream
				// Skip flags (4) and PublicKeyOrToken (blob) to get to Name
				uint8_t *name_ptr = row_ptr + 4 + 4;
				if (index_sizes.blob == 4) {
					name_ptr += 4;
				} else {
					name_ptr += 2;
				}
				if (index_sizes.blob == 4) {
					name_ptr += 4;
				} else {
					name_ptr += 2;
				}

				if (index_sizes.string == 4) {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (name_ptr));
				} else {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (name_ptr));
				}

				if (R_STR_ISNOTEMPTY (name)) {
					DotNetLibrary *lib = R_NEW0 (DotNetLibrary);
					lib->name = strdup (name);
					lib->major_version = major;
					lib->minor_version = minor;
					lib->build_number = build;
					lib->revision_number = revision;
					lib->version = r_str_newf ("%d.%d.%d.%d", major, minor, build, revision);
					r_list_append (libraries, lib);
				}

				row_ptr += row_size;
			}
			return;
		} else {
			switch (bit_check) {
			case BIT_MODULE:
				table_offset += (2 + index_sizes.string + (index_sizes.guid * 3)) * num_rows;
				break;
			default:
				// Cannot compute table size for unknown table, bail out
				return;
			}
		}
		matched_bits++;
	}
}

// Structure to hold TypeDef metadata for method and field matching
typedef struct {
	char *class_name;
	char *namespace;
	uint32_t method_list_start;
	uint32_t method_list_end;
	uint32_t field_list_start;
	uint32_t field_list_end;
} DotNetTypeDefInfo;

// Free function for DotNetField
static void dotnet_field_free(void *f) {
	if (f) {
		DotNetField *field = f;
		free (field->name);
		free (field);
	}
}

static void dotnet_symbol_free(void *s) {
	if (s) {
		DotNetSymbol *sym = s;
		free (sym->name);
		free (sym->namespace);
		free (sym->type);
		r_list_free (sym->methods);
		r_list_free (sym->fields);
		free (sym);
	}
}

// Helper to find class name for a method index
static DotNetTypeDefInfo *dotnet_find_typedef_for_method_index(RList *typedefs, uint32_t method_idx) {
	RListIter *iter;
	DotNetTypeDefInfo *td;
	r_list_foreach (typedefs, iter, td) {
		// method_idx is 1-based in the MethodDef table
		// method_list_start/end are the ranges from TypeDef.MethodList field
		// Check if method_idx falls in this typedef's method range
		if (method_idx >= td->method_list_start && method_idx < td->method_list_end) {
			return td;
		}
	}
	return NULL;
}

// Parse Field table to collect field information for types
static void dotnet_parse_tilde_field(
	PE *pe,
	PTILDE_HEADER tilde_header,
	ut64 metadata_root,
	ROWS rows,
	INDEX_SIZES index_sizes,
	PSTREAMS streams,
	RList *typedef_info,
	RList *symbols) {

	int bit_check;
	uint32_t num_rows = 0;
	const uint8_t *string_offset = NULL;
	uint8_t *row_ptr = NULL;
	int i;
	char *name;

	// Validate we have the required streams
	if (!streams->tilde || !streams->string) {
		return;
	}
	int matched_bits = 0;

	// Number of rows is the number of bits set to 1 in Valid
	for (i = 0; i < 64; i++) {
		matched_bits += ((tilde_header->Valid >> i) & 0x01);
	}

	uint32_t *row_offset = (uint32_t *) (tilde_header + 1);
	uint8_t *table_offset = (uint8_t *)row_offset;
	table_offset += sizeof (uint32_t) * matched_bits;

	matched_bits = 0;

	if (metadata_root + streams->string->Offset >= pe->data_size) {
		return;
	}
	string_offset = pe->data + metadata_root + streams->string->Offset;

	// Iterate through tables, looking for Field
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

		if (!fits_in_pe (pe, table_offset, 1)) {
			return;
		}

		num_rows = *(row_offset + matched_bits);

		if (bit_check == BIT_FIELD) {
			// Parse Field table
			// Structure: Flags (2) Name (string) Signature (blob)
			row_ptr = table_offset;
			uint32_t field_idx = 1; // Field indices are 1-based
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = 2 + index_sizes.string + index_sizes.blob;
				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}
				ut16 flags = r_read_le16 (row_ptr);
				// Get field name from string stream
				if (index_sizes.string == 4) {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 2));
				} else {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 2));
				}

				if (R_STR_ISNOTEMPTY (name)) {
					// Find which typedef this field belongs to
					RListIter *iter;
					DotNetTypeDefInfo *td;
					r_list_foreach (typedef_info, iter, td) {
						if (field_idx >= td->field_list_start && field_idx < td->field_list_end) {
							// Find the corresponding DotNetSymbol
							char *full_name;
							if (R_STR_ISNOTEMPTY (td->namespace)) {
								full_name = r_str_newf ("%s.%s", td->namespace, td->class_name);
							} else {
								full_name = strdup (td->class_name);
							}
							RListIter *sym_iter;
							DotNetSymbol *sym;
							r_list_foreach (symbols, sym_iter, sym) {
								if (sym->type && !strcmp (sym->type, "typedef")) {
									char *sym_full_name;
									const char *ns = sym->namespace;
									if (R_STR_ISNOTEMPTY (ns)) {
										sym_full_name = r_str_newf ("%s.%s", ns, sym->name);
									} else {
										sym_full_name = strdup (sym->name);
									}
									if (!strcmp (sym_full_name, full_name)) {
										DotNetField *field = R_NEW0 (DotNetField);
										field->name = strdup (name);
										field->flags = flags;
										if (!sym->fields) {
											sym->fields = r_list_newf (dotnet_field_free);
										}
										r_list_append (sym->fields, field);
										free (sym_full_name);
										break;
									}
									free (sym_full_name);
								}
							}
							free (full_name);
							break;
						}
					}
				}

				row_ptr += row_size;
				field_idx++;
			}
			return;
		} else if (bit_check < BIT_FIELD) {
			// Skip tables before Field
			switch (bit_check) {
			case BIT_MODULE:
				table_offset += (2 + index_sizes.string + (index_sizes.guid * 3)) * num_rows;
				break;
			case BIT_TYPEREF:
				{
					// ResolutionScope is a coded index (module | moduleref | assemblyref)
					uint32_t resolution_scope_row_count = max_rows (3, rows.module, rows.moduleref, rows.assemblyref);
					uint8_t resolution_scope_size = (resolution_scope_row_count > (0xFFFF >> 0x02))? 4: 2;
					table_offset += (resolution_scope_size + (index_sizes.string * 2)) * num_rows;
				}
				break;
			case BIT_TYPEDEF:
				{
					uint32_t row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
					uint8_t extends_index_size = (row_count > (0xFFFF >> 0x02))? 4: 2;
					uint8_t field_index_size = (rows.field > 0xFFFF)? 4: 2;
					uint8_t methoddef_index_size = (rows.methoddef > 0xFFFF)? 4: 2;
					table_offset += (4 + (index_sizes.string * 2) + extends_index_size + field_index_size + methoddef_index_size) * num_rows;
				}
				break;
			case BIT_FIELDPTR:
				table_offset += index_sizes.field * num_rows;
				break;
			default:
				break;
			}
		} else {
			// We've passed Field table
			return;
		}

		matched_bits++;
	}
}

static void dotnet_parse_tilde_typedef(
	PE *pe,
	PTILDE_HEADER tilde_header,
	ut64 metadata_root,
	ROWS rows,
	INDEX_SIZES index_sizes,
	PSTREAMS streams,
	RList *symbols) {

	uint8_t *table_offset = NULL;
	uint32_t *row_offset = NULL;
	int bit_check, matched_bits = 0;
	uint32_t num_rows = 0;
	const uint8_t *string_offset = NULL;
	uint8_t *row_ptr = NULL;
	int i;
	char *name, *namespace;

	// Validate we have the required streams
	if (!streams->tilde || !streams->string) {
		return;
	}

	// Number of rows is the number of bits set to 1 in Valid
	for (i = 0; i < 64; i++) {
		matched_bits += ((tilde_header->Valid >> i) & 0x01);
	}

	row_offset = (uint32_t *) (tilde_header + 1);
	table_offset = (uint8_t *)row_offset;
	table_offset += sizeof (uint32_t) * matched_bits;

	if (metadata_root + streams->string->Offset >= pe->data_size) {
		return;
	}
	string_offset = pe->data + metadata_root + streams->string->Offset;

	matched_bits = 0;

	// Parse TypeDef table
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

		if (!fits_in_pe (pe, table_offset, 1)) {
			return;
		}

		num_rows = *(row_offset + matched_bits);

		if (bit_check == BIT_TYPEDEF) {
			// TypeDef structure: Flags (4) Name (string) Namespace (string) Extends (coded_idx) FieldList (field) MethodList (methoddef)
			uint32_t row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
			uint8_t extends_index_size = (row_count > (0xFFFF >> 0x02))? 4: 2;
			uint8_t field_index_size = (rows.field > 0xFFFF)? 4: 2;
			uint8_t methoddef_index_size = (rows.methoddef > 0xFFFF)? 4: 2;
			row_ptr = table_offset;
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = 4 + (index_sizes.string * 2) + extends_index_size +
					field_index_size + methoddef_index_size;

				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}

				uint32_t flags = r_read_le32 (row_ptr);

				// Get type name from string stream
				if (index_sizes.string == 4) {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 4));
					namespace = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 8));
				} else {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 4));
					namespace = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 6));
				}

				if (R_STR_ISNOTEMPTY (name)) {
					DotNetSymbol *sym = R_NEW0 (DotNetSymbol);
					sym->name = strdup (name);
					sym->namespace = strdup (r_str_get (namespace));
					sym->type = strdup ("typedef");
					sym->flags = flags;
					sym->vaddr = 0; // TypeDefs don't have direct RVAs
					sym->size = 0;
					sym->fields = r_list_newf (dotnet_field_free);
					r_list_append (symbols, sym);
				}

				row_ptr += row_size;
			}
			return;
		} else if (bit_check < BIT_TYPEDEF) {
			// Skip tables that come before TypeDef
			num_rows = *(row_offset + matched_bits);
			switch (bit_check) {
			case BIT_MODULE:
				table_offset += (2 + index_sizes.string + (index_sizes.guid * 3)) * num_rows;
				break;
			case BIT_TYPEREF:
				{
					// TypeRef: ResolutionScope (coded index) + Name (string) + Namespace (string)
					// ResolutionScope is a ResolutionScope coded index (module | moduleref | assemblyref)
					uint32_t resolution_scope_row_count = max_rows (3, rows.module, rows.moduleref, rows.assemblyref);
					uint8_t resolution_scope_size = (resolution_scope_row_count > (0xFFFF >> 0x02))? 4: 2;
					table_offset += (resolution_scope_size + (index_sizes.string * 2)) * num_rows;
				}
				break;
			default:
				// Other tables before typedef (FieldPtr, Field, MethodDefPtr) - skip for now
				// This is incomplete but matches the original logic
				break;
			}
		} else if (bit_check > BIT_TYPEDEF) {
			// We've passed TypeDef table, stop looking
			return;
		}

		matched_bits++;
	}
}

// ECMA-335 II.23.2 compressed-uint. Returns the value and advances *p past it.
// Returns 0 on malformed input (and leaves *p untouched).
static ut32 dotnet_read_compressed_uint(const ut8 **p, const ut8 *end) {
	if (*p >= end) {
		return 0;
	}
	ut8 b0 = **p;
	if ((b0 & 0x80) == 0) {
		(*p)++;
		return b0 & 0x7f;
	}
	if ((b0 & 0xc0) == 0x80) {
		if (*p + 1 >= end) {
			return 0;
		}
		ut32 v = ((ut32)(b0 & 0x3f) << 8) | (*p)[1];
		*p += 2;
		return v;
	}
	if ((b0 & 0xe0) == 0xc0) {
		if (*p + 3 >= end) {
			return 0;
		}
		ut32 v = ((ut32)(b0 & 0x1f) << 24) | ((ut32)(*p)[1] << 16)
				| ((ut32)(*p)[2] << 8) | (*p)[3];
		*p += 4;
		return v;
	}
	return 0;
}

static bool dotnet_read_ret_count(const ut8 **p, const ut8 *end, ut16 *ret_count) {
	if (ret_count) {
		*ret_count = 1;
	}
	while (*p < end && (**p == ELEMENT_TYPE_CMOD_REQD || **p == ELEMENT_TYPE_CMOD_OPT)) {
		(*p)++;
		if (!dotnet_read_compressed_uint (p, end)) {
			return false;
		}
	}
	if (*p >= end) {
		return false;
	}
	if (ret_count && **p == ELEMENT_TYPE_VOID) {
		*ret_count = 0;
	}
	(*p)++;
	return true;
}

// Decode the MethodDefSig blob at blob_index and return the total argument
// slot count (paramCount + 1 if HASTHIS). 0 means "unknown / no info".
// ECMA-335 II.23.2.1: header byte, optional GenParamCount, ParamCount, ret, params.
static ut32 dotnet_method_param_count(PE *pe, PSTREAM_HEADER blob_hdr, ut64 metadata_root, ut32 blob_index, bool *has_this, ut16 *ret_count) {
	if (has_this) {
		*has_this = false;
	}
	if (ret_count) {
		*ret_count = 1;
	}
	if (!blob_hdr || blob_index == 0) {
		return 0;
	}
	ut64 blob_heap_off = metadata_root + blob_hdr->Offset;
	if (blob_heap_off >= pe->data_size) {
		return 0;
	}
	const ut8 *blob_heap = pe->data + blob_heap_off;
	const ut8 *end = pe->data + pe->data_size;
	if (blob_index >= (ut32)(end - blob_heap)) {
		return 0;
	}
	const ut8 *p = blob_heap + blob_index;
	ut32 sig_size = dotnet_read_compressed_uint (&p, end);
	if (!sig_size || (ut64)(end - p) < sig_size) {
		return 0;
	}
	const ut8 *sig_end = p + sig_size;
	if (p >= sig_end) {
		return 0;
	}
	ut8 hdr = *p++;
	ut32 implicit_this = (hdr & 0x20)? 1: 0; // HASTHIS
	if (has_this) {
		*has_this = implicit_this != 0;
	}
	if (hdr & 0x10) { // GENERIC: skip GenParamCount
		const ut8 *gen_param = p;
		dotnet_read_compressed_uint (&p, sig_end);
		if (p == gen_param) {
			return 0;
		}
	}
	const ut8 *param = p;
	ut32 param_count = dotnet_read_compressed_uint (&p, sig_end);
	if (p == param) {
		return 0;
	}
	if (!dotnet_read_ret_count (&p, sig_end, ret_count)) {
		return 0;
	}
	return implicit_this + param_count;
}

static void dotnet_parse_tilde_methoddef(
	PE *pe,
	PTILDE_HEADER tilde_header,
	ut64 metadata_root,
	ROWS rows,
	INDEX_SIZES index_sizes,
	PSTREAMS streams,
	RList *symbols,
	RList *typedef_info) {

	uint8_t *table_offset = NULL;
	uint32_t *row_offset = NULL;
	int bit_check, matched_bits = 0;
	uint32_t num_rows = 0;
	const uint8_t *string_offset = NULL;
	uint8_t *row_ptr = NULL;
	int i;
	char *name;
	ut32 rva;
	uint32_t row_count;

	// Validate we have the required streams
	if (!streams->tilde || !streams->string) {
		return;
	}

	// Number of rows is the number of bits set to 1 in Valid
	for (i = 0; i < 64; i++) {
		matched_bits += ((tilde_header->Valid >> i) & 0x01);
	}

	row_offset = (uint32_t *) (tilde_header + 1);
	table_offset = (uint8_t *)row_offset;
	table_offset += sizeof (uint32_t) * matched_bits;

	if (metadata_root + streams->string->Offset >= pe->data_size) {
		return;
	}
	string_offset = pe->data + metadata_root + streams->string->Offset;

	matched_bits = 0;

	// Iterate through tables in order, looking for MethodDef
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

		if (!fits_in_pe (pe, table_offset, 1)) {
			return;
		}

		num_rows = *(row_offset + matched_bits);
		if (bit_check == BIT_METHODDEF) {
			// Parse MethodDef table
			// Structure: RVA (4) ImplFlags (2) Flags (2) Name (string) Signature (blob) ParamList (param)
			row_count = max_rows (3, rows.methoddef, rows.memberref, rows.typedef_);
			uint8_t param_index_size = (row_count > (0xFFFF >> 0x01))? 4: 2;

			row_ptr = table_offset;
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = 4 + 2 + 2 + index_sizes.string + index_sizes.blob + param_index_size;
				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}
				rva = r_read_le32 (row_ptr);
				ut16 impl_flags = r_read_le16 (row_ptr + 4);

				// Get method name from string stream
				// Offset: RVA (4) + ImplFlags (2) + Flags (2) = 8
				if (index_sizes.string == 4) {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 8));
				} else {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 8));
				}
				// Signature blob index: Offset 8 + string-index-size
				ut32 sig_blob_off = 8 + index_sizes.string;
				ut32 sig_idx = (index_sizes.blob == 4)
					? r_read_le32 (row_ptr + sig_blob_off)
					: r_read_le16 (row_ptr + sig_blob_off);
				if (R_STR_ISNOTEMPTY (name)) {
					DotNetSymbol *sym = R_NEW0 (DotNetSymbol);
					// Methods are 1-based, the method index is relative to MethodDef table start
					// So method 1 is the first row (i = 0), method 2 is the second row (i = 1), etc.
					uint32_t method_idx = i + 1;
					bool has_this = false;
					ut16 ret_count = 1;
					ut32 args = dotnet_method_param_count (pe, streams->blob, metadata_root, sig_idx, &has_this, &ret_count);
					sym->param_count = (args > 0xffff)? 0xffff: (ut16)args;
					sym->ret_count = ret_count;
					sym->is_instance = has_this;
					DotNetTypeDefInfo *parent_typedef = dotnet_find_typedef_for_method_index (typedef_info, method_idx);
					if (parent_typedef) {
						// Create fully qualified name: namespace.classname.methodname
						const char *ns = parent_typedef->namespace;
						if (R_STR_ISNOTEMPTY (ns)) {
							sym->name = r_str_newf ("%s.%s.%s", ns, parent_typedef->class_name, name);
						} else {
							sym->name = r_str_newf ("%s.%s", parent_typedef->class_name, name);
						}
					} else {
						sym->name = strdup (name);
					}
					sym->vaddr = rva; // RVA from the method table
					sym->size = 0;
					sym->type = strdup ("methoddef");
					sym->token = 0x06000000 | method_idx;
					// Set is_native based on ImplFlags
					// IL = 0x0000, Native = 0x0001, OPTIL = 0x0002, Runtime = 0x0003
					sym->is_native = (impl_flags & 0x0003) == 0x0001;
					r_list_append (symbols, sym);
				}

				row_ptr += row_size;
			}
			// Successfully parsed MethodDef, continue to process other tables
			table_offset += (4 + 2 + 2 + index_sizes.string + index_sizes.blob + param_index_size) * num_rows;
			matched_bits++;
			continue;
		} else if (bit_check == BIT_MEMBERREF) {
			// Parse MemberRef table
			// Structure: Class (coded_idx) Name (string) Signature (blob)
			row_count = max_rows (4, rows.methoddef, rows.memberref, rows.typeref, rows.typespec);
			uint8_t class_index_size = (row_count > (0xFFFF >> 0x03))? 4: 2;

			row_ptr = table_offset;
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = class_index_size + index_sizes.string + index_sizes.blob;

				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}

				// Name
				if (index_sizes.string == 4) {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + class_index_size));
				} else {
					name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + class_index_size));
				}

				if (R_STR_ISNOTEMPTY (name)) {
					DotNetSymbol *sym = R_NEW0 (DotNetSymbol);
					sym->name = strdup (name);
					sym->vaddr = 0; // MemberRef don't have RVA
					sym->size = 0;
					sym->type = strdup ("memberref");
					sym->token = 0x0A000000 | (i + 1);
					r_list_append (symbols, sym);
				}

				row_ptr += row_size;
			}
			// Successfully parsed MemberRef
			table_offset += (class_index_size + index_sizes.string + index_sizes.blob) * num_rows;
			matched_bits++;
			continue;
		} else if (bit_check == BIT_TYPEDEF) {
			// Parse TypeDef table
			// TypeDef structure: Flags (4) Name (string) Namespace (string) Extends (coded_idx) FieldList (field) MethodList (methoddef)
			row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
			uint8_t extends_index_size = (row_count > (0xFFFF >> 0x02))? 4: 2;

			row_ptr = table_offset;
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = 4 + (index_sizes.string * 2) + extends_index_size +
					index_sizes.field + index_sizes.methoddef;

				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}

				uint32_t flags = r_read_le32 (row_ptr);

				// Get type name from string stream
				// Offset: Flags (4)
				char *type_name, *namespace;
				if (index_sizes.string == 4) {
					type_name = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 4));
					namespace = pe_get_dotnet_string (pe, string_offset, r_read_le32 (row_ptr + 8));
				} else {
					type_name = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 4));
					namespace = pe_get_dotnet_string (pe, string_offset, r_read_le16 (row_ptr + 6));
				}

				if (R_STR_ISNOTEMPTY (type_name)) {
					DotNetSymbol *sym = R_NEW0 (DotNetSymbol);
					sym->name = strdup (type_name);
					sym->namespace = strdup (r_str_get (namespace));
					sym->vaddr = 0; // TypeDefs don't have direct RVAs
					sym->type = strdup ("typedef");
					sym->flags = flags;

					// Extract MethodList index - stored in size field for now
					// MethodList is at offset: Flags (4) + Name (string) + Namespace (string) + Extends (coded_idx) + FieldList (field)
					uint8_t *method_list_ptr = row_ptr + 4 + (index_sizes.string * 2) + extends_index_size + index_sizes.field;
					if (index_sizes.methoddef == 4) {
						sym->size = r_read_le32 (method_list_ptr);
					} else {
						sym->size = r_read_le16 (method_list_ptr);
					}
					r_list_append (symbols, sym);
				}

				row_ptr += row_size;
			}
			// Successfully parsed TypeDef, continue to process other tables
			table_offset += (4 + (index_sizes.string * 2) +
						((max_rows (3, rows.typedef_, rows.typeref, rows.typespec) > (0xFFFF >> 0x02))? 4: 2) +
						index_sizes.field + index_sizes.methoddef) *
				num_rows;
			matched_bits++;
			continue;
		} else {
			// Calculate table size to skip this table
			uint32_t table_size = 0;
			switch (bit_check) {
			case BIT_MODULE:
				table_size = (2 + index_sizes.string + (index_sizes.guid * 3)) * num_rows;
				break;
			case BIT_TYPEREF:
				// ResolutionScope is a coded index (module | moduleref | assemblyref)
				row_count = max_rows (3, rows.module, rows.moduleref, rows.assemblyref);
				table_size = ((row_count > (0xFFFF >> 0x02)? 4: 2) + (index_sizes.string * 2)) * num_rows;
				break;
#if 0
			case BIT_TYPEDEF:
				row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
				table_size = (4 + (index_sizes.string * 2) + (row_count > (0xFFFF >> 0x02)? 4: 2) +
						index_sizes.field + index_sizes.methoddef) *
					num_rows;
				break;
#endif
			case BIT_FIELDPTR:
				table_size = (index_sizes.field) * num_rows;
				break;
			case BIT_FIELD:
				table_size = (2 + index_sizes.string + index_sizes.blob) * num_rows;
				break;
			case BIT_METHODDEFPTR:
				table_size = (index_sizes.methoddef) * num_rows;
				break;
			case BIT_PARAM:
				table_size = (2 + 2 + index_sizes.string) * num_rows;
				break;
			case BIT_INTERFACEIMPL:
				row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
				table_size = (index_sizes.typedef_ + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
				break;
#if 0
			case BIT_MEMBERREF:
				row_count = max_rows (4, rows.methoddef, rows.memberref, rows.typeref, rows.typespec);
				table_size = ((row_count > (0xFFFF >> 0x03)? 4: 2) + index_sizes.string + index_sizes.blob) * num_rows;
				break;
#endif
			case BIT_CONSTANT:
				row_count = max_rows (3, rows.field, rows.param, rows.property);
				table_size = (2 + (row_count > (0xFFFF >> 0x02)? 4: 2) + index_sizes.blob) * num_rows;
				break;
			case BIT_CUSTOMATTRIBUTE:
				row_count = max_rows (3, rows.methoddef, rows.field, rows.param);
				table_size = ((row_count > (0xFFFF >> 0x05)? 4: 2) + index_sizes.memberref + index_sizes.blob) * num_rows;
				break;
			case BIT_FIELDMARSHAL:
				row_count = max_rows (2, rows.field, rows.param);
				table_size = ((row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes.blob) * num_rows;
				break;
			case BIT_DECLSECURITY:
				row_count = max_rows (3, rows.typedef_, rows.methoddef, rows.assembly);
				table_size = (2 + (row_count > (0xFFFF >> 0x02)? 4: 2) + index_sizes.blob) * num_rows;
				break;
			case BIT_CLASSLAYOUT:
				table_size = (2 + 4 + index_sizes.typedef_) * num_rows;
				break;
			case BIT_FIELDLAYOUT:
				table_size = (4 + index_sizes.field) * num_rows;
				break;
			case BIT_STANDALONESIG:
				table_size = (index_sizes.blob) * num_rows;
				break;
			case BIT_EVENTMAP:
				table_size = (index_sizes.typedef_ + index_sizes.event) * num_rows;
				break;
			case BIT_EVENTPTR:
				table_size = (index_sizes.event) * num_rows;
				break;
			case BIT_EVENT:
				table_size = (2 + index_sizes.string + index_sizes.typedef_) * num_rows;
				break;
			case BIT_PROPERTYMAP:
				table_size = (index_sizes.typedef_ + index_sizes.property) * num_rows;
				break;
			case BIT_PROPERTYPTR:
				table_size = (index_sizes.property) * num_rows;
				break;
			case BIT_PROPERTY:
				table_size = (2 + index_sizes.string + index_sizes.blob) * num_rows;
				break;
			case BIT_METHODSEMANTICS:
				row_count = max_rows (2, rows.event, rows.property);
				table_size = (2 + index_sizes.methoddef + (row_count > (0xFFFF >> 0x01)? 4: 2)) * num_rows;
				break;
			case BIT_METHODIMPL:
				row_count = max_rows (2, rows.methoddef, rows.memberref);
				table_size = (index_sizes.typedef_ + index_sizes.methoddef + (row_count > (0xFFFF >> 0x01)? 4: 2)) * num_rows;
				break;
			case BIT_MODULEREF:
				table_size = (index_sizes.string) * num_rows;
				break;
			case BIT_TYPESPEC:
				table_size = (index_sizes.blob) * num_rows;
				break;
			case BIT_IMPLMAP:
				row_count = max_rows (3, rows.field, rows.methoddef, rows.typedef_);
				table_size = (2 + (row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes.string + index_sizes.moduleref) * num_rows;
				break;
			case BIT_FIELDRVA:
				table_size = (4 + index_sizes.field) * num_rows;
				break;
			case BIT_ENCLOG:
				table_size = (4) * num_rows;
				break;
			case BIT_ENCMAP:
				table_size = (4) * num_rows;
				break;
			case BIT_ASSEMBLY:
				table_size = (2 + 2 + 2 + 2 + 4 + index_sizes.blob + (index_sizes.string * 2)) * num_rows;
				break;
			case BIT_ASSEMBLYPROCESSOR:
				table_size = (4) * num_rows;
				break;
			case BIT_ASSEMBLYOS:
				table_size = (4 + 4 + 4) * num_rows;
				break;
			case BIT_ASSEMBLYREF:
				table_size = (2 + 2 + 2 + 2 + 4 + (index_sizes.blob * 2) + (index_sizes.string * 2)) * num_rows;
				break;
			case BIT_ASSEMBLYREFPROCESSOR:
				table_size = (4 + index_sizes.assemblyrefprocessor) * num_rows;
				break;
			case BIT_ASSEMBLYREFOS:
				table_size = (4 + 4 + 4 + index_sizes.assemblyref) * num_rows;
				break;
			case BIT_FILE:
				table_size = (4 + index_sizes.string + index_sizes.blob) * num_rows;
				break;
			case BIT_EXPORTEDTYPE:
				row_count = max_rows (3, rows.file, rows.assemblyref, rows.exportedtype);
				table_size = (4 + 4 + (index_sizes.string * 2) + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
				break;
			case BIT_MANIFESTRESOURCE:
				row_count = max_rows (2, rows.file, rows.assemblyref);
				table_size = (4 + 4 + index_sizes.string + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
				break;
			case BIT_NESTEDCLASS:
				table_size = (index_sizes.typedef_ * 2) * num_rows;
				break;
			case BIT_GENERICPARAM:
				row_count = max_rows (2, rows.typedef_, rows.methoddef);
				table_size = (2 + 2 + (row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes.string) * num_rows;
				break;
			case BIT_METHODSPEC:
				row_count = max_rows (2, rows.methoddef, rows.memberref);
				table_size = ((row_count > (0xFFFF >> 0x01)? 4: 2) + index_sizes.blob) * num_rows;
				break;
			case BIT_GENERICPARAMCONSTRAINT:
				row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
				table_size = (index_sizes.genericparam + (row_count > (0xFFFF >> 0x02)? 4: 2)) * num_rows;
				break;
			default:
				return;
			}
			table_offset += table_size;
		}
		matched_bits++;
	}
}

// Helper function to collect typedef metadata for method association
static void dotnet_typedef_free(void *p) {
	if (p) {
		DotNetTypeDefInfo *td = (DotNetTypeDefInfo *)p;
		free (td->class_name);
		free (td->namespace);
		free (td);
	}
}

static RList *dotnet_collect_typedefs(PE *pe, ut64 metadata_root, PSTREAMS streams, ROWS rows, INDEX_SIZES index_sizes) {
	if (metadata_root + streams->tilde->Offset >= pe->data_size) {
		return r_list_newf ((RListFree)dotnet_typedef_free);
	}
	PTILDE_HEADER tilde_header = (PTILDE_HEADER) (pe->data + metadata_root + streams->tilde->Offset);
	if (!struct_fits_in_pe (pe, tilde_header, TILDE_HEADER)) {
		return r_list_newf ((RListFree)dotnet_typedef_free);
	}
	uint32_t *row_offset = (uint32_t *) (tilde_header + 1);
	if (metadata_root + streams->string->Offset >= pe->data_size) {
		return r_list_newf ((RListFree)dotnet_typedef_free);
	}
	const uint8_t *string_offset = pe->data + metadata_root + streams->string->Offset;
	uint8_t *table_offset = (uint8_t *)row_offset;
	int j, bit_check, matched_bits = 0;
	uint32_t num_rows;
	RList *typedef_info = r_list_newf ((RListFree)dotnet_typedef_free);

	// Calculate offset to TypeDef table
	// First count how many tables are present so we can skip the row-counts array
	for (j = 0; j < 64; j++) {
		matched_bits += ((tilde_header->Valid >> j) & 0x01);
	}
	// Advance past the row-count array (one uint32_t per present table)
	table_offset += sizeof (uint32_t) * matched_bits;
	matched_bits = 0;
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

		if (bit_check == BIT_TYPEDEF) {
			// Found TypeDef table, parse it
			uint32_t row_count = max_rows (3, rows.typedef_, rows.typeref, rows.typespec);
			uint8_t extends_index_size = (row_count > (0xFFFF >> 0x02))? 4: 2;
			uint8_t field_index_size = (rows.field > 0xFFFF)? 4: 2;
			uint8_t methoddef_index_size = (rows.methoddef > 0xFFFF)? 4: 2;
			if (!fits_in_pe (pe, row_offset, (matched_bits + 1) * sizeof (uint32_t))) {
				return typedef_info;
			}
			num_rows = *(row_offset + matched_bits);

			uint8_t *row_ptr = table_offset;

			int i;
			for (i = 0; i < num_rows; i++) {
				uint32_t row_size = 4 + (index_sizes.string * 2) + extends_index_size + field_index_size + methoddef_index_size;

				if (!fits_in_pe (pe, row_ptr, row_size)) {
					break;
				}

				uint32_t name_idx = 0, ns_idx = 0;
				char *type_name, *namespace;
				if (index_sizes.string == 4) {
					name_idx = r_read_le32 (row_ptr + 4);
					ns_idx = r_read_le32 (row_ptr + 8);
				} else {
					name_idx = r_read_le16 (row_ptr + 4);
					ns_idx = r_read_le16 (row_ptr + 6);
				}
				type_name = pe_get_dotnet_string (pe, string_offset, name_idx);
				namespace = pe_get_dotnet_string (pe, string_offset, ns_idx);

				// Extract FieldList and MethodList indices
				// Layout: Flags (4) + Name (string) + Namespace (string) + Extends (coded_idx) + FieldList (field) + MethodList (methoddef)
				uint8_t *field_list_ptr = row_ptr + 4 + (index_sizes.string * 2) + extends_index_size;
				uint8_t *method_list_ptr = field_list_ptr + index_sizes.field;

				uint32_t field_list_idx, method_list_idx;
				if (index_sizes.field == 4) {
					field_list_idx = r_read_le32 (field_list_ptr);
				} else {
					field_list_idx = r_read_le16 (field_list_ptr);
				}
				if (index_sizes.methoddef == 4) {
					method_list_idx = r_read_le32 (method_list_ptr);
				} else {
					method_list_idx = r_read_le16 (method_list_ptr);
				}

				// Find next typedef's lists to know the range for this class
				uint32_t next_field_list_idx = rows.field + 1; // Default to end
				uint32_t next_method_list_idx = rows.methoddef + 1; // Default to end
				uint8_t *next_row_ptr = row_ptr + row_size;
				if (i + 1 < num_rows && fits_in_pe (pe, next_row_ptr, row_size)) {
					uint8_t *next_field_list_ptr = next_row_ptr + 4 + (index_sizes.string * 2) + extends_index_size;
					uint8_t *next_method_list_ptr = next_field_list_ptr + index_sizes.field;
					if (index_sizes.field == 4) {
						next_field_list_idx = r_read_le32 (next_field_list_ptr);
					} else {
						next_field_list_idx = r_read_le16 (next_field_list_ptr);
					}
					if (index_sizes.methoddef == 4) {
						next_method_list_idx = r_read_le32 (next_method_list_ptr);
					} else {
						next_method_list_idx = r_read_le16 (next_method_list_ptr);
					}
				}

				DotNetTypeDefInfo *td = R_NEW0 (DotNetTypeDefInfo);
				td->class_name = strdup (r_str_get_fail (type_name, "<unnamed>"));
				td->namespace = strdup (r_str_get (namespace));
				td->field_list_start = field_list_idx;
				td->field_list_end = next_field_list_idx;
				td->method_list_start = method_list_idx;
				td->method_list_end = next_method_list_idx;
				r_list_append (typedef_info, td);
				// Next Row
				row_ptr += row_size;
			}
			break;
		} else if (bit_check < BIT_TYPEDEF) {
			// Skip tables before TypeDef
			if (!fits_in_pe (pe, row_offset, (matched_bits + 1) * sizeof (uint32_t))) {
				return typedef_info;
			}
			num_rows = *(row_offset + matched_bits);
			// Calculate row size for this table and skip it
			switch (bit_check) {
			case BIT_MODULE:
				table_offset += (2 + index_sizes.string + (index_sizes.guid * 3)) * num_rows;
				break;
			case BIT_TYPEREF:
				{
					// TypeRef: ResolutionScope (coded index) + Name (string) + Namespace (string)
					// ResolutionScope is a ResolutionScope coded index (module | moduleref | assemblyref)
					uint32_t resolution_scope_row_count = max_rows (3, rows.module, rows.moduleref, rows.assemblyref);
					uint8_t resolution_scope_size = (resolution_scope_row_count > (0xFFFF >> 0x02))? 4: 2;
					table_offset += (resolution_scope_size + (index_sizes.string * 2)) * num_rows;
				}
				break;
#if 0
			case BIT_FIELDPTR:
				table_offset += index_sizes.field * num_rows;
				break;
#endif
#if 0
			case BIT_FIELD:
				table_offset += (2 + index_sizes.string + index_sizes.blob) * num_rows;
				break;
#endif
#if 0
			case BIT_METHODDEFPTR:
				table_offset += index_sizes.methoddef * num_rows;
				break;
#endif
			default:
				// Other tables shouldn't appear before TypeDef in standard order
				break;
			}
		}

		matched_bits++;
	}

	return typedef_info;
}

static void dotnet_parse_tilde(PE *pe, ut64 metadata_root, PSTREAMS streams, RList *symbols) {
	PTILDE_HEADER tilde_header;
	uint32_t *row_offset = NULL;
	int bit_check;
	int matched_bits = 0;
	ROWS rows;
	INDEX_SIZES index_sizes;

	if (!streams->tilde || !streams->string) {
		return;
	}

	// Default all rows to 0
	memset (&rows, '\0', sizeof (ROWS));

	// Default index sizes are 2. Will be bumped to 4 if necessary.
	memset (&index_sizes, 2, sizeof (index_sizes));

	if (metadata_root + streams->tilde->Offset >= pe->data_size) {
		return;
	}
	tilde_header = (PTILDE_HEADER) (pe->data +
		metadata_root +
		streams->tilde->Offset);

	if (!struct_fits_in_pe (pe, tilde_header, TILDE_HEADER)) {
		return;
	}

	// Set index sizes for various heaps.
	if (tilde_header->HeapSizes & 0x01) {
		index_sizes.string = 4;
	}
	if (tilde_header->HeapSizes & 0x02) {
		index_sizes.guid = 4;
	}
	if (tilde_header->HeapSizes & 0x04) {
		index_sizes.blob = 4;
	}

	row_offset = (uint32_t *) (tilde_header + 1);

	// Walk all the bits first to collect row counts
	for (bit_check = 0; bit_check < 64; bit_check++) {
		if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
			continue;
		}

#define ROW_CHECK(name) \
	if (fits_in_pe (pe, row_offset, (matched_bits + 1) * sizeof (uint32_t))) \
		rows.name = *(row_offset + matched_bits);

#define ROW_CHECK_WITH_INDEX(name) \
	ROW_CHECK (name); \
	if (rows.name > 0xFFFF) \
		index_sizes.name = 4;

		switch (bit_check) {
		case BIT_MODULE:
			ROW_CHECK (module);
			break;
		case BIT_MODULEREF:
			ROW_CHECK_WITH_INDEX (moduleref);
			break;
		case BIT_ASSEMBLYREF:
			ROW_CHECK_WITH_INDEX (assemblyref);
			break;
		case BIT_TYPEREF:
			ROW_CHECK (typeref);
			break;
		case BIT_METHODDEF:
			ROW_CHECK_WITH_INDEX (methoddef);
			break;
		case BIT_MEMBERREF:
			ROW_CHECK_WITH_INDEX (memberref);
			break;
		case BIT_TYPEDEF:
			ROW_CHECK_WITH_INDEX (typedef_);
			break;
		case BIT_TYPESPEC:
			ROW_CHECK (typespec);
			break;
		case BIT_FIELD:
			ROW_CHECK_WITH_INDEX (field);
			break;
		case BIT_PARAM:
			ROW_CHECK_WITH_INDEX (param);
			break;
		case BIT_PROPERTY:
			ROW_CHECK_WITH_INDEX (property);
			break;
		case BIT_INTERFACEIMPL:
			ROW_CHECK (interfaceimpl);
			break;
		case BIT_EVENT:
			ROW_CHECK_WITH_INDEX (event);
			break;
		case BIT_STANDALONESIG:
			ROW_CHECK (standalonesig);
			break;
		case BIT_ASSEMBLY:
			ROW_CHECK (assembly);
			break;
		case BIT_FILE:
			ROW_CHECK (file);
			break;
		case BIT_EXPORTEDTYPE:
			ROW_CHECK (exportedtype);
			break;
		case BIT_MANIFESTRESOURCE:
			ROW_CHECK (manifestresource);
			break;
		case BIT_GENERICPARAM:
			ROW_CHECK_WITH_INDEX (genericparam);
			break;
		case BIT_GENERICPARAMCONSTRAINT:
			ROW_CHECK (genericparamconstraint);
			break;
		case BIT_METHODSPEC:
			ROW_CHECK (methodspec);
			break;
		default:
			break;
		}

		matched_bits++;
	}

	// Now parse the tables with the row information we collected
	// Collect typedef metadata for method and field association
	RList *typedef_info = dotnet_collect_typedefs (pe, metadata_root, streams, rows, index_sizes);

	// Parse typedef first to establish class definitions
	dotnet_parse_tilde_typedef (pe, tilde_header, metadata_root, rows, index_sizes, streams, symbols);
	// Parse fields to associate them with classes
	dotnet_parse_tilde_field (pe, tilde_header, metadata_root, rows, index_sizes, streams, typedef_info, symbols);
	// Then parse methoddef to assign methods to classes with full qualified names
	dotnet_parse_tilde_methoddef (pe, tilde_header, metadata_root, rows, index_sizes, streams, symbols, typedef_info);

	// Clean up typedef info
	r_list_free (typedef_info);
}

static RList *dotnet_parse_com(PE *pe, ut64 baddr) {
	PNET_METADATA metadata;
	ut64 metadata_root;
	STREAMS headers;
	ut16 num_streams;
	RList *symbols = NULL;
	int i;
	st64 metadata_offset = -1;

	symbols = r_list_newf ((RListFree)dotnet_symbol_free);
	if (!symbols) {
		return NULL;
	}

	// Find the .NET metadata by searching for the magic number (BSJB = 0x424a5342)
	// The metadata can be anywhere in the file, so scan the entire file
	if (pe->data_size > 0x100) {
		for (i = 0x40; i < (int)pe->data_size - (int)sizeof (NET_METADATA); i++) {
			PNET_METADATA test_metadata = (PNET_METADATA) (pe->data + i);
			if (test_metadata->Magic == NET_METADATA_MAGIC) {
				metadata_offset = i;
				break;
			}
		}
	}

	if (metadata_offset < 0) {
		return symbols;
	}

	metadata_root = metadata_offset;

	if (!struct_fits_in_pe (pe, pe->data + metadata_root, NET_METADATA)) {
		return symbols;
	}

	metadata = (PNET_METADATA) (pe->data + metadata_root);

	if (metadata->Magic != NET_METADATA_MAGIC) {
		return symbols;
	}

	// Version length must be between 1 and 255, and be a multiple of 4.
	if (metadata->Length == 0 ||
		metadata->Length > 255 ||
		metadata->Length % 4 != 0 ||
		!fits_in_pe (pe, pe->data + metadata_root, metadata->Length)) {
		return symbols;
	}

	// The metadata structure has some variable length records after the version.
	st64 stream_offset = metadata_root + sizeof (NET_METADATA) + metadata->Length + 2;

	// 2 bytes for Streams.
	if (!fits_in_pe (pe, pe->data + stream_offset, 2)) {
		return symbols;
	}

	num_streams = r_read_le16 (pe->data + stream_offset);
	stream_offset += 2;
	if (num_streams > dotnet_max_stream_headers (pe, stream_offset)) {
		return symbols;
	}

	headers = dotnet_parse_stream_headers (pe, stream_offset, metadata_root, num_streams);

	// Parse the #~ stream which contains the metadata tables
	if (headers.tilde && headers.string && headers.blob) {
		dotnet_parse_tilde (pe, metadata_root, &headers, symbols);
	}

	return symbols;
}

// entrypoint - returns a list of DotNetSymbol pointers
RList *dotnet_parse(const ut8 *buf, int size, ut64 baddr) {
	if (!buf || size < 1) {
		return NULL;
	}
	PE pe = { buf, (ut32)size, NULL };
	return dotnet_parse_com (&pe, baddr);
}

RList *dotnet_parse_libs(const ut8 *buf, int size) {
	PNET_METADATA metadata;
	ut64 metadata_root;
	STREAMS headers;
	ut16 num_streams;
	RList *libraries = NULL;
	int i;
	st64 metadata_offset = -1;
	if (!buf || size < 1) {
		return NULL;
	}
	PE pe_struct = { buf, (ut32)size, NULL };
	PE *pe = &pe_struct;

	libraries = r_list_newf ((RListFree)free);
	if (!libraries) {
		return NULL;
	}

	// Find the .NET metadata by searching for the magic number
	if (pe->data_size > 0x100) {
		for (i = 0x40; i < (int)pe->data_size - (int)sizeof (NET_METADATA); i++) {
			PNET_METADATA test_metadata = (PNET_METADATA) (pe->data + i);
			if (test_metadata->Magic == NET_METADATA_MAGIC) {
				metadata_offset = i;
				break;
			}
		}
	}

	if (metadata_offset < 0) {
		return libraries;
	}

	metadata_root = metadata_offset;

	if (! (fits_in_pe (pe, pe->data + metadata_root, sizeof (NET_METADATA)))) {
		return libraries;
	}

	metadata = (PNET_METADATA) (pe->data + metadata_root);

	if (metadata->Magic != NET_METADATA_MAGIC) {
		return libraries;
	}

	if (metadata->Length == 0 ||
		metadata->Length > 255 ||
		metadata->Length % 4 != 0 ||
		! (fits_in_pe (pe, pe->data + metadata_root, metadata->Length))) {
		return libraries;
	}

	st64 stream_offset = metadata_root + sizeof (NET_METADATA) + metadata->Length + 2;

	if (! (fits_in_pe (pe, pe->data + stream_offset, 2))) {
		return libraries;
	}

	num_streams = r_read_le16 (pe->data + stream_offset);
	stream_offset += 2;
	if (num_streams > dotnet_max_stream_headers (pe, stream_offset)) {
		return libraries;
	}

	headers = dotnet_parse_stream_headers (pe, stream_offset, metadata_root, num_streams);

	// Parse the #~ stream which contains the metadata tables for libraries
	if (headers.tilde && headers.string) {
		PTILDE_HEADER tilde_header;
		uint32_t *row_offset = NULL;
		int bit_check;
		int matched_bits = 0;
		ROWS rows;
		INDEX_SIZES index_sizes;

		memset (&rows, '\0', sizeof (ROWS));
		memset (&index_sizes, 2, sizeof (index_sizes));

		if (metadata_root + headers.tilde->Offset >= pe->data_size) {
			return libraries;
		}
		tilde_header = (PTILDE_HEADER) (pe->data + metadata_root + headers.tilde->Offset);

		if (fits_in_pe (pe, (uint8_t *)tilde_header, sizeof (TILDE_HEADER))) {
			if (tilde_header->HeapSizes & 0x01) {
				index_sizes.string = 4;
			}
			if (tilde_header->HeapSizes & 0x02) {
				index_sizes.guid = 4;
			}
			if (tilde_header->HeapSizes & 0x04) {
				index_sizes.blob = 4;
			}

			row_offset = (uint32_t *) (tilde_header + 1);

			// Walk all the bits first to collect row counts
			for (bit_check = 0; bit_check < 64; bit_check++) {
				if (! ((tilde_header->Valid >> bit_check) & 0x01)) {
					continue;
				}

				if (fits_in_pe (pe, (uint8_t *)row_offset, (matched_bits + 1) * sizeof (uint32_t))) {
					rows.assemblyref = *(row_offset + matched_bits);
				}

				matched_bits++;
			}

			// Parse AssemblyRef table
			dotnet_parse_tilde_assemblyref (pe, tilde_header, metadata_root, rows, index_sizes, &headers, libraries);
		}
	}

	return libraries;
}

RList *dotnet_parse_imports(const ut8 *buf, int size) {
	if (!buf || size < 1) {
		return NULL;
	}
	// TODO: Parse ImplMap table for P/Invoke declarations
	// This would extract native library imports from .NET assemblies
	RList *imports = r_list_newf ((RListFree)free);
	return imports;
}

// Extract .NET runtime version and assembly version from MSIL headers
DotNetVersionInfo *dotnet_parse_version_info(const ut8 *buf, int size) {
	st64 cli_offset = -1;
	int i;
	st64 metadata_offset = -1;
	if (!buf || size < 1) {
		return NULL;
	}
	PE pe_struct = { buf, (ut32)size, NULL };
	PE *pe = &pe_struct;

	// First, find the CLI header by scanning for its signature near the start of the file
	// CLI headers are typically within the first 0x1000 bytes of the .text section
	if (pe->data_size > 0x100) {
		int search_limit = (pe->data_size > 0x2000)? 0x2000: (int)pe->data_size;
		for (i = 0x100; i < search_limit - (int)sizeof (CLI_HEADER); i++) {
			PCLI_HEADER cli = (PCLI_HEADER) (pe->data + i);
			if ((cli->Size == 0x48 || cli->Size == 0x44) &&
				cli->MajorRuntimeVersion >= 1 && cli->MajorRuntimeVersion <= 5 &&
				cli->MinorRuntimeVersion <= 10) {
				cli_offset = i;
				break;
			}
		}
	}

	// Find the metadata magic
	if (pe->data_size > 0x100) {
		for (i = 0x40; i < (int)pe->data_size - (int)sizeof (NET_METADATA); i++) {
			PNET_METADATA test_metadata = (PNET_METADATA) (pe->data + i);
			if (test_metadata->Magic == NET_METADATA_MAGIC) {
				metadata_offset = i;
				break;
			}
		}
	}

	if (cli_offset < 0 && metadata_offset < 0) {
		return NULL;
	}

	// Allocate version info structure
	DotNetVersionInfo *version_info = R_NEW0 (DotNetVersionInfo);
	// Get CLI header version if we found it
	if (cli_offset >= 0 && struct_fits_in_pe (pe, pe->data + cli_offset, CLI_HEADER)) {
		PCLI_HEADER cli = (PCLI_HEADER) (pe->data + cli_offset);
		version_info->cli_major = cli->MajorRuntimeVersion;
		version_info->cli_minor = cli->MinorRuntimeVersion;
	}

	if (metadata_offset < 0) {
		return version_info;
	}

	// Try to parse Assembly table, but don't fail if we can't
	// The CLR version alone is valuable
	PNET_METADATA metadata;
	ut64 metadata_root = metadata_offset;

	if (struct_fits_in_pe (pe, pe->data + metadata_root, NET_METADATA)) {
		metadata = (PNET_METADATA) (pe->data + metadata_root);

		if (metadata->Magic == NET_METADATA_MAGIC &&
			metadata->Length > 0 &&
			metadata->Length <= 255 &&
			metadata->Length % 4 == 0 &&
			fits_in_pe (pe, pe->data + metadata_root, metadata->Length)) {

			st64 offset_2 = metadata_root + sizeof (NET_METADATA) + metadata->Length + 2;
			if (fits_in_pe (pe, pe->data + offset_2, 2)) {
				ut16 num_streams = r_read_le16 (pe->data + offset_2);
				offset_2 += 2;
				if (num_streams > dotnet_max_stream_headers (pe, offset_2)) {
					return version_info;
				}
				STREAMS headers = dotnet_parse_stream_headers (pe, offset_2, metadata_root, num_streams);

				// Try to parse Assembly table
				if (headers.tilde && headers.string) {
					PTILDE_HEADER tilde_header;
					const ut8 *assembly_row;
					ROWS rows;
					INDEX_SIZES index_sizes;
					ut32 num_rows = 0;

					if (metadata_root + headers.tilde->Offset >= pe->data_size) {
						return version_info;
					}
					tilde_header = (PTILDE_HEADER) (pe->data + metadata_root + headers.tilde->Offset);

					if (dotnet_parse_tilde_rows (pe, tilde_header, &rows, &index_sizes) &&
						((tilde_header->Valid >> BIT_ASSEMBLY) & 0x01)) {
						assembly_row = dotnet_tilde_table_offset (pe, tilde_header, &rows, &index_sizes, BIT_ASSEMBLY, &num_rows);
						if (assembly_row && num_rows > 0 && fits_in_pe (pe, assembly_row, 12)) {
							version_info->asm_major = r_read_le16 (assembly_row + 4);
							version_info->asm_minor = r_read_le16 (assembly_row + 6);
							version_info->asm_build = r_read_le16 (assembly_row + 8);
							version_info->asm_revision = r_read_le16 (assembly_row + 10);
						}
					}
				}
			}
		}
	}
	return version_info;
}
