Xiao Han

36 papers A* 8A 2B 1Journal 20Unranked 5
YearRankTypeTitle / Venue / Authors
2026 A* conf
WWW
Mengyang Ma, Xiaopeng Li, Wanyu Wang, Zhaocheng Du, Jingtong Gao, Pengyue Jia, Yuyang Ye, Yiqi Wang, Yunpeng Weng, Weihong Luo, Xiao Han, Xiangyu Zhao
2026 J jnl
Neurocomputing
Linan Zhu, Jianlin Chen, Xiao Han, Xiangfan Chen, Mengfei Xu, Guojiang Shen, Xiangjie Kong
2026 J jnl
CoRR
Yimin Deng, Yuqing Fu, Derong Xu, Yejing Wang, Wei Ni, Jingtong Gao, Xiaopeng Li, Chengxu Liu, Xiao Han, Guoshuai Zhao, Xiangyu Zhao, Li Zhu, Xueming Qian
2026 J jnl
CoRR
Bowen Liu, Pengyue Jia, Wanyu Wang, Derong Xu, Jiawei Cheng, Jiancheng Dong, Xiao Han, Zimo Zhao, Chao Zhang, Bowen Yu, Fangyu Hong, Xiangyu Zhao
2026 A* conf
WWW
Zhi Liu, Xuyuan Hu, Xiao Han, Zhehao Dai, Zhaolin Deng, Guojiang Shen, Xiangjie Kong
2025 J jnl
CoRR
Xiao Han, Dayan Pan, Xiangyu Zhao, Xuyuan Hu, Zhaolin Deng, Xiangjie Kong, Guojiang Shen
2025 J jnl
CoRR
Mengyang Ma, Xiaopeng Li, Wanyu Wang, Zhaocheng Du, Jingtong Gao, Pengyue Jia, Yuyang Ye, Yiqi Wang, Yunpeng Weng, Weihong Luo, Xiao Han, Xiangyu Zhao
2025 A conf
CIKM
Dayan Pan, Zhaoyang Fu, Jingyuan Wang, Xiao Han, Yue Zhu, Xiangyu Zhao
2025 J jnl
CoRR
Dayan Pan, Zhaoyang Fu, Jingyuan Wang, Xiao Han, Yue Zhu, Xiangyu Zhao
2025 J jnl
CoRR
Xiao Han, Zimo Zhao, Wanyu Wang, Maolin Wang, Zitao Liu, Yi Chang, Xiangyu Zhao
2025 A conf
CIKM
Zhehao Dai, Guojiang Shen, Yuyue Hu, Jiaxin Du, Xiao Han, Xiangjie Kong
2025 A* conf
AAAI
Xiao Han, Zijian Zhang, Xiangyu Zhao, Yuanshao Zhu, Guojiang Shen, Xiangjie Kong, Xuetao Wei, Liqiang Nie, Jieping Ye
2025 conf
KDD (2)
Yuanshao Zhu, James Jianqiao Yu, Xiangyu Zhao, Xiao Han, Qidong Liu, Xuetao Wei, Yuxuan Liang
2025 J jnl
CoRR
Yuanshao Zhu, James Jianqiao Yu, Xiangyu Zhao, Xiao Han, Qidong Liu, Xuetao Wei, Yuxuan Liang
2025 B conf
SSTD
Zijian Zhang, Xiao Han, Xiangyu Zhao, Chenjuan Guo, Bin Yang
2025 A* conf
IJCAI
Wenchao Weng, Hanyu Jiang, Mei Wu, Xiao Han, Haidong Gao, Guojiang Shen, Xiangjie Kong
2025 J jnl
Knowl. Based Syst.
Qihong Pan, Limin Yao, Guojiang Shen, Xiao Han, Yichuan Chen, Xiangjie Kong
2025 conf
MMAsia
Jiajie Deng, Haokun Wen, Xiao Han, Xuemeng Song, Xiangyu Zhao
2025 J jnl
CoRR
Zhi Liu, Xuyuan Hu, Xiao Han, Zhehao Dai, Zhaolin Deng, Guojiang Shen, Xiangjie Kong
2025 conf
KDD (2)
Xiao Han, Chen Zhu, Hengshu Zhu, Xiangyu Zhao
2025 conf
WWW (Companion Volume)
Zijian Zhang, Hao Miao, Yuxuan Liang, Yan Zhao, Xiao Han, Pengyue Jia, Bin Yang, Christian S. Jensen
2024 A* conf
KDD
Xiao Han, Chen Zhu, Xiao Hu, Chuan Qin, Xiangyu Zhao, Hengshu Zhu
2024 J jnl
CoRR
Xiao Han, Chen Zhu, Xiao Hu, Chuan Qin, Xiangyu Zhao, Hengshu Zhu
2024 J jnl
IEEE Trans. Veh. Technol.
Xiao Han, Ding-Xuan Zhou, Guojiang Shen, Xiangjie Kong, Yulong Zhao
2024 A* conf
NeurIPS
Pengyue Jia, Yiding Liu, Xiaopeng Li, Xiangyu Zhao, Yuhao Wang, Yantong Du, Xiao Han, Xuetao Wei, Shuaiqiang Wang, Dawei Yin
2024 J jnl
CoRR
Pengyue Jia, Yiding Liu, Xiaopeng Li, Xiangyu Zhao, Yuhao Wang, Yantong Du, Xiao Han, Xuetao Wei, Shuaiqiang Wang, Dawei Yin
2024 J jnl
CoRR
Xiao Han, Zijian Zhang, Xiangyu Zhao, Guojiang Shen, Xiangjie Kong, Xuetao Wei, Liqiang Nie, Jieping Ye
2024 A* conf
IJCAI
Yinghui Liu, Guojiang Shen, Chengyong Cui, Zhenzhen Zhao, Xiao Han, Jiaxin Du, Xiangyu Zhao, Xiangjie Kong
2024 J jnl
CoRR
Xiao Han, Chen Zhu, Xiangyu Zhao, Hengshu Zhu
2024 J jnl
PeerJ Comput. Sci.
Yinghui Liu, Guojiang Shen, Nali Liu, Xiao Han, Zhenhui Xu, Junjie Zhou, Xiangjie Kong
2023 conf
ITSC
Xiaolin Luo, Jingtong Gao, Yang Zhang, Xiao Han, HongJie Liu
2023 A* conf
KDD
Xiao Han, Xiangyu Zhao, Liang Zhang, Wanyu Wang
2023 J jnl
IEEE Trans. Ind. Informatics
Yan Zhang, Guojiang Shen, Xiao Han, Wei Wang, Xiangjie Kong
2022 J jnl
IEEE Trans. Intell. Transp. Syst.
Guojiang Shen, Xiao Han, Kwai-Sang Chin, Xiangjie Kong
2019 J jnl
Sensors
Duanyang Liu, Longfeng Tang, Guojiang Shen, Xiao Han
2018 J jnl
IEEE Access
Guojiang Shen, Xiao Han, Junjie Zhou, Zhongyuan Ruan, Qihong Pan
tests/unit/test_decompile_binja_extractor.py
← Index tests/unit/test_decompile_binja_extractor.py python
"""Unit tests for DecompileBinja (top-level extractor) and prepare_export_data."""
import hashlib
import json
import os
import sys
import pytest
from unittest.mock import MagicMock, patch, PropertyMock
from datetime import datetime

# Install binaryninja stubs
from tests.unit.conftest_binja_stubs import install_binja_stubs
install_binja_stubs()

from redb.extractors.enum import Tag


# ============================================================================
# 7a. DecompileBinja — methods that don't require a live Binary Ninja session
# ============================================================================


class TestDecompileBinjaCalculateMD5:
    def test_calculate_md5(self):
        """calculate_md5("test") returns expected MD5."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = object.__new__(DecompileBinja)
            obj.log = MagicMock()
            result = obj.calculate_md5("test")
            expected = hashlib.md5(b"test").hexdigest()
            assert result == expected


class TestDecompileBinjaTimeouts:
    def _make_extractor(self, env_vars=None):
        """Create a DecompileBinja with mocked Extractor.__init__."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            if env_vars:
                with patch.dict(os.environ, env_vars, clear=False):
                    obj = DecompileBinja.__new__(DecompileBinja)
                    obj.log = MagicMock()
                    obj.analysis_results = None
                    obj.binja_decompiler = None
                    obj.filetype = None
                    obj.goresym_data = None
                    obj.goresym_output_path = None
                    # Re-run timeout logic
                    try:
                        obj.BINJA_TIMEOUT = int(os.getenv("BINJA_TIMEOUT", "1200"))
                    except ValueError:
                        obj.BINJA_TIMEOUT = 1200
                    try:
                        obj.DECOMPILE_EXTRACTOR_TIMEOUT = int(
                            os.getenv("DECOMPILE_EXTRACTOR_TIMEOUT", "2580")
                        )
                    except ValueError:
                        obj.DECOMPILE_EXTRACTOR_TIMEOUT = 2580
                    return obj
            else:
                obj = DecompileBinja.__new__(DecompileBinja)
                obj.log = MagicMock()
                obj.analysis_results = None
                obj.binja_decompiler = None
                obj.filetype = None
                obj.goresym_data = None
                obj.goresym_output_path = None
                obj.BINJA_TIMEOUT = 1200
                obj.DECOMPILE_EXTRACTOR_TIMEOUT = 2580
                return obj

    def test_timeout_defaults(self):
        obj = self._make_extractor()
        assert obj.BINJA_TIMEOUT == 1200
        assert obj.DECOMPILE_EXTRACTOR_TIMEOUT == 2580

    def test_timeout_from_env(self):
        obj = self._make_extractor(env_vars={"BINJA_TIMEOUT": "600"})
        assert obj.BINJA_TIMEOUT == 600

    def test_timeout_invalid_env(self):
        obj = self._make_extractor(env_vars={"BINJA_TIMEOUT": "not_a_number"})
        assert obj.BINJA_TIMEOUT == 1200


class TestDecompileBinjaDotnet:
    def _make_extractor(self, filetype=None):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.filetype = filetype
            obj.filepath = "/tmp/fake.bin"
            obj.binary = b"\x00" * 100
            obj.hash = MagicMock()
            obj.hash.sha256 = "a" * 64
            return obj

    def test_is_dotnet_non_pe(self):
        obj = self._make_extractor(filetype="elf")
        assert obj.is_dotnet() is False

    def test_is_dotnet_pe_without_com(self):
        """PE without .NET COM descriptor -> False."""
        obj = self._make_extractor(filetype="pebin")
        # Mock magic to not have .Net
        with patch("redb.extractors.decompiler.DecompileBinja.magic") as mock_magic:
            mock_magic.from_buffer.return_value = "PE32 executable"
            with patch("redb.extractors.decompiler.DecompileBinja.pefile") as mock_pefile:
                mock_pe = MagicMock()
                entry = MagicMock()
                entry.name = "IMAGE_DIRECTORY_ENTRY_IMPORT"
                entry.Size = 100
                mock_pe.OPTIONAL_HEADER.DATA_DIRECTORY = [entry]
                mock_pefile.PE.return_value = mock_pe
                assert obj.is_dotnet() is False

    def test_is_dotnet_pe_with_com_descriptor(self):
        """PE with COM descriptor -> True."""
        obj = self._make_extractor(filetype="pebin")
        with patch("redb.extractors.decompiler.DecompileBinja.magic") as mock_magic:
            mock_magic.from_buffer.return_value = "PE32 executable"
            with patch("redb.extractors.decompiler.DecompileBinja.pefile") as mock_pefile:
                mock_pe = MagicMock()
                entry = MagicMock()
                entry.name = "IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR"
                entry.Size = 72
                mock_pe.OPTIONAL_HEADER.DATA_DIRECTORY = [entry]
                mock_pefile.PE.return_value = mock_pe
                assert obj.is_dotnet() is True


class TestDecompileBinjaGolang:
    def _make_extractor(self, filetype=None):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.filetype = filetype
            obj.filepath = "/tmp/fake.bin"
            obj.hash = MagicMock()
            obj.hash.sha256 = "a" * 64
            return obj

    def test_is_golang_non_elf_pe(self):
        obj = self._make_extractor(filetype=None)
        assert obj.is_golang() is False

    def test_is_golang_elf_with_go_sections(self):
        """ELF with .gopclntab section -> True."""
        obj = self._make_extractor(filetype="elf")
        with patch("builtins.open", MagicMock()):
            with patch("redb.extractors.decompiler.DecompileBinja.ELFFile") as mock_elf_cls:
                mock_elf = MagicMock()
                sec1 = MagicMock()
                sec1.name = ".text"
                sec1.__getitem__ = lambda self, k: 0x2 if k == "sh_flags" else 0
                sec1.data.return_value = b""
                sec2 = MagicMock()
                sec2.name = ".gopclntab"
                sec2.__getitem__ = lambda self, k: 0x2 if k == "sh_flags" else 0
                sec2.data.return_value = b""
                mock_elf.iter_sections.return_value = [sec1, sec2]
                mock_elf_cls.return_value = mock_elf
                assert obj.is_golang() is True

    def test_is_golang_elf_without_go(self):
        obj = self._make_extractor(filetype="elf")
        with patch("builtins.open", MagicMock()):
            with patch("redb.extractors.decompiler.DecompileBinja.ELFFile") as mock_elf_cls:
                mock_elf = MagicMock()
                sec = MagicMock()
                sec.name = ".text"
                sec.__getitem__ = lambda self, k: 0x2 if k == "sh_flags" else 0
                sec.data.return_value = b"no go signatures here"
                mock_elf.iter_sections.return_value = [sec]
                mock_elf_cls.return_value = mock_elf
                assert obj.is_golang() is False


class TestDecompileBinjaTag:
    def test_tag_returns_decompiled(self):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            assert obj.tag() == Tag.DECOMPILED.value


class TestDecompileBinjaContextManager:
    def test_context_manager_protocol(self):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.binja_decompiler = None
            obj.goresym_output_path = None
            assert obj.__enter__() is obj
            # __exit__ calls cleanup_run
            obj.__exit__(None, None, None)

    def test_cleanup_run_clears_state(self):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.binja_decompiler = MagicMock()
            obj.goresym_output_path = None
            obj.cleanup_run()
            assert obj.binja_decompiler is None


class TestDecompileBinjaExtract:
    def test_extract_timeout_handling(self):
        """Thread timeout -> returns None and calls cleanup."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.analysis_results = None
            obj.binja_decompiler = None
            obj.goresym_output_path = None
            obj.DECOMPILE_EXTRACTOR_TIMEOUT = 0  # Immediate timeout
            obj.sha256 = "a" * 64
            obj.sha1 = "b" * 40
            obj.md5 = "c" * 32
            # Mock analyze_binary to block
            import threading
            obj.analyze_binary = lambda: None
            result = obj.extract()
            assert result is None

    def test_analyze_binary_skips_dotnet(self):
        """Dotnet binary -> analyze_binary returns None."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.filetype = "pebin"
            obj.filepath = "/fake"
            obj.hash = MagicMock()
            obj.hash.sha256 = "a" * 64
            obj.binja_decompiler = None
            obj.goresym_output_path = None
            obj.goresym_data = None
            obj.BINJA_TIMEOUT = 60
            obj.exporters = []
            obj.index_prefix = None
            with patch.object(obj, "is_dotnet", return_value=True):
                result = obj.analyze_binary()
                assert result is None


# ============================================================================
# 7b. prepare_export_data (ClickHouse schema)
# ============================================================================


class TestPrepareExportData:
    def _make_extractor_with_results(self, goresym_data=None):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.goresym_data = goresym_data
            obj.analysis_results = {
                "sha256": "a" * 64,
                "sha1": "b" * 40,
                "md5": "c" * 32,
                "decompiled": [
                    {
                        "decompiled_function_hash": "h" * 64,
                        "decompiled_function": "void test() {}",
                        "decompiled_function_name": "test",
                        "decompiled_function_prototype": "void test()",
                        "decompiled_function_address": 0x1000,
                        "function_type": "USER",
                        "functions_caller": [],
                        "functions_call": [],
                        "disassembled_function_hash": "d" * 64,
                        "flattened_score": 0.0,
                        "mba_score": 0.0,
                    }
                ],
                "disassembled": [
                    {
                        "disassembled_function_hash": "d" * 64,
                        "disassembled_function": "0x1000: push rbp",
                        "disassembled_function_no_addresses": "push rbp",
                        "disassembled_function_name": "test",
                        "disassembled_function_address": 0x1000,
                        "function_type": "USER",
                        "instructions_count": 5,
                        "instructions_types": ["DATA_MOVEMENT"],
                        "control_flow_count": 1,
                        "memory_access_pattern": ["MEM_STACK"],
                        "register_usage": ["GPR"],
                        "data_references_count": 0,
                        "max_block_size": 5,
                        "num_calls": 1,
                        "stack_size": -8,
                        "decompiled_function_hash": "h" * 64,
                        "tlsh_disassembly": None,
                        "tlsh_llil": None,
                        "minhash": [1, 2, 3],
                        "cyclomatic_complexity": 2,
                    }
                ],
                "cfg": [
                    {
                        "function_address": 0x1000,
                        "disassembled_function_hash": "d" * 64,
                        "cfg_topology_hash": b'\x00' * 16,
                        "block_count": 3,
                        "edge_count": 4,
                        "llil_total_operations": 20,
                        "call_count": 1,
                        "cyclomatic_complexity": 3,
                        "loop_count": 1,
                        "max_depth": 2,
                        "max_fan_out": 2,
                        "md_index_topdown": 12345,
                        "md_index_bottomup": 67890,
                        "prime_product_llil": 99999,
                        "cfg_feature_tlsh": None,
                        "wl_minhash": [0] * 128,
                        "bb_features": [[5, 1, 0, 2, 0, 1, 1, 2]] * 3,
                        "cfg_adjacency": [(0 << 16) | 1, (0 << 16) | 2, (1 << 16) | 2],
                    }
                ],
                "llil": [
                    {
                        "sha256_llil": "l" * 64,
                        "function_type": "USER",
                        "instructions_types_llil": [],
                        "control_flow_count_llil": 0,
                        "memory_access_pattern_llil": [],
                        "register_usage": {},
                        "total_reg_reads": 0,
                        "total_reg_written": 0,
                        "data_references_count": 0,
                        "max_block_size": 5,
                        "num_calls": 0,
                        "stack_size": -8,
                        "body_llil_vector": [],
                        "function_address": 0x1000,
                        "disassembled_function_hash": "d" * 64,
                        "tlsh_disassembly": None,
                        "tlsh_llil": None,
                    }
                ],
                "errors": [],
                "strings": [
                    {
                        "string": "Hello",
                        "string_raw": "Hello",
                        "string_encoding": "Utf8String",
                        "string_offset": 0,
                        "string_length": 5,
                        "string_raw_length": 5,
                        "string_entropy": 2.32,
                    }
                ],
            }
            return obj

    def test_prepare_export_returns_none_no_results(self):
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.analysis_results = None
            result = obj.prepare_export_data("ClickHouseExporter")
            assert result is None

    def test_prepare_export_multi_table_flag(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        assert result["multi_table"] is True

    def test_prepare_export_all_tables_present(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        expected_tables = [
            "decompiled_content",
            "decompiled_refs",
            "disassembled_content",
            "disassembled_refs",
            "llil_content",
            "llil_refs",
            "cfg_functions",
            "function_similarity_metrics",
            "strings_raw",
        ]
        for table in expected_tables:
            assert table in result, f"Missing table: {table}"

    def test_prepare_export_decompiled_content_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["decompiled_content"]
        assert len(table["column_names"]) == len(table["column_type_names"])
        assert "decompiled_function_hash" in table["column_names"]

    def test_prepare_export_decompiled_refs_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["decompiled_refs"]
        assert len(table["column_names"]) == len(table["column_type_names"])
        assert "sha256" in table["column_names"]

    def test_prepare_export_disassembled_content_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["disassembled_content"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_disassembled_refs_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["disassembled_refs"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_llil_content_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["llil_content"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_llil_refs_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["llil_refs"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_cfg_functions_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["cfg_functions"]
        assert len(table["column_names"]) == len(table["column_type_names"])
        assert table["table"] == "code_binja_cfg_functions"
        assert "cfg_topology_hash" in table["column_names"]
        assert "wl_minhash" in table["column_names"]
        assert "bb_features" in table["column_names"]

    def test_prepare_export_function_similarity_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["function_similarity_metrics"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_strings_raw_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["strings_raw"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_errors_schema(self):
        obj = self._make_extractor_with_results()
        result = obj.prepare_export_data("ClickHouseExporter")
        table = result["function_analysis_errors"]
        assert len(table["column_names"]) == len(table["column_type_names"])

    def test_prepare_export_golang_metadata_present(self):
        obj = self._make_extractor_with_results(goresym_data={"UserFunctions": []})
        result = obj.prepare_export_data("ClickHouseExporter")
        assert "golang_metadata" in result

    def test_prepare_export_golang_metadata_absent(self):
        obj = self._make_extractor_with_results(goresym_data=None)
        result = obj.prepare_export_data("ClickHouseExporter")
        assert "golang_metadata" not in result


class TestPrepareHelperFunctions:
    """Test the helper functions inside prepare_export_data."""

    def test_prepare_array_field_none(self):
        # The prepare_array_field function converts None -> []
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.goresym_data = None
            obj.analysis_results = {
                "sha256": "a" * 64, "sha1": "b" * 40, "md5": "c" * 32,
                "decompiled": [], "disassembled": [], "cfg": [], "llil": [],
                "errors": [], "strings": [],
            }
            result = obj.prepare_export_data("ClickHouseExporter")
            # Verify the export succeeds (function works even with empty lists)
            assert result is not None

    def test_prepare_array_field_list(self):
        """List values pass through."""
        # Verified through the export data containing arrays
        pass

    def test_prepare_register_usage_map(self):
        """Dict converted to {reg: (reads, writes)} tuples."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.goresym_data = None
            obj.analysis_results = {
                "sha256": "a" * 64, "sha1": "b" * 40, "md5": "c" * 32,
                "decompiled": [], "disassembled": [],
                "cfg": [],
                "llil": [{
                    "sha256_llil": "l" * 64, "function_type": "USER",
                    "instructions_types_llil": [], "control_flow_count_llil": 0,
                    "memory_access_pattern_llil": [],
                    "register_usage": {"rbx": {"reads": 3, "writes": 1}},
                    "total_reg_reads": 3, "total_reg_written": 1,
                    "data_references_count": 0, "max_block_size": 5,
                    "num_calls": 0, "stack_size": 0,
                    "body_llil_vector": [],
                    "function_address": 0x1000,
                    "disassembled_function_hash": "d" * 64,
                    "tlsh_disassembly": None,
                    "tlsh_llil": None,
                }],
                "errors": [], "strings": [],
            }
            result = obj.prepare_export_data("ClickHouseExporter")
            # The LLIL content data should have register_usage_llil as a map
            llil_data = result["llil_content"]["data"]
            assert len(llil_data) == 1
            # Column index 5 is register_usage_llil
            reg_map = llil_data[0][5]
            assert reg_map == {"rbx": (3, 1)}

    def test_prepare_register_usage_map_empty(self):
        """Empty/None register usage -> {}."""
        with patch("redb.extractors.decompiler.DecompileBinja.Extractor.__init__", return_value=None):
            from redb.extractors.decompiler.DecompileBinja import DecompileBinja
            obj = DecompileBinja.__new__(DecompileBinja)
            obj.log = MagicMock()
            obj.goresym_data = None
            obj.analysis_results = {
                "sha256": "a" * 64, "sha1": "b" * 40, "md5": "c" * 32,
                "decompiled": [], "disassembled": [],
                "cfg": [],
                "llil": [{
                    "sha256_llil": "l" * 64, "function_type": "USER",
                    "instructions_types_llil": [], "control_flow_count_llil": 0,
                    "memory_access_pattern_llil": [],
                    "register_usage": None,
                    "total_reg_reads": 0, "total_reg_written": 0,
                    "data_references_count": 0, "max_block_size": 0,
                    "num_calls": 0, "stack_size": 0,
                    "body_llil_vector": [],
                    "function_address": 0x1000,
                    "disassembled_function_hash": "d" * 64,
                    "tlsh_disassembly": None,
                    "tlsh_llil": None,
                }],
                "errors": [], "strings": [],
            }
            result = obj.prepare_export_data("ClickHouseExporter")
            llil_data = result["llil_content"]["data"]
            reg_map = llil_data[0][5]
            assert reg_map == {}