""" Unit tests for the rocm_kpack.coff.surgery module. Tests CoffSurgery class for PE/COFF binary manipulation. """ import pytest from pathlib import Path import struct from rocm_kpack.coff import ( CoffSurgery, SectionInfo, IMAGE_SCN_MEM_READ, IMAGE_SCN_CNT_INITIALIZED_DATA, ) from rocm_kpack.coff.types import DOS_MAGIC class TestCoffSurgery: """Tests for the CoffSurgery class.""" def test_load_executable(self, test_assets_dir: Path): """Test loading a valid PE executable.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) assert surgery.coff_header is not None assert surgery.optional_header is not None # Should be an executable (not DLL) assert not surgery.is_dll def test_load_dll(self, test_assets_dir: Path): """Test loading a valid DLL.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.dll" ) surgery = CoffSurgery.load(binary) assert surgery.coff_header is not None assert surgery.optional_header is not None # Should be a DLL assert surgery.is_dll def test_find_section(self, test_assets_dir: Path): """Test finding sections by name.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) # Should find .hip_fat (PE uses 8-char names) section = surgery.find_section(".hip_fat") assert section is not None assert section.name == ".hip_fat" assert section.raw_size > 0 # Should not find nonexistent section assert surgery.find_section(".nonexistent") is None def test_find_hipfatb_segment(self, test_assets_dir: Path): """Test finding .hipFatB (wrapper) section.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) # Should find .hipFatB (wrapper section) section = surgery.find_section(".hipFatB") assert section is not None assert section.name == ".hipFatB" def test_iter_sections(self, test_assets_dir: Path): """Test iterating over all sections.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) sections = list(surgery.iter_sections()) assert len(sections) > 0 # Check we have expected sections names = [s.name for s in sections] assert ".text" in names assert ".hip_fat" in names assert ".hipFatB" in names def test_get_section_content(self, test_assets_dir: Path): """Test reading section content.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) section = surgery.find_section(".hip_fat") assert section is not None content = surgery.get_section_content(section) assert len(content) > 0 def test_rva_to_file_offset(self, test_assets_dir: Path): """Test RVA to file offset conversion.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) section = surgery.find_section(".text") assert section is not None # RVA should map to file offset offset = surgery.rva_to_file_offset(section.rva) assert offset is not None assert offset == section.file_offset def test_add_section(self, test_assets_dir: Path, tmp_path: Path): """Test adding a new section.""" binary = ( test_assets_dir / "bundled_binaries/windows/cov5/test_kernel_single.exe" ) surgery = CoffSurgery.load(binary) # Count original sections original_count = len(list(surgery.iter_sections())) # Add a new section test_data = b"Hello, test section!" result = surgery.add_section( name=".test", content=test_data, characteristics=IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA, ) assert result is not None assert result.section_index >= 0 # Verify section exists new_section = surgery.find_section(".test") assert new_section is not None # Verify content can be read back (slice to virtual_size, not padded raw_size) content = surgery.get_section_content(new_section) content = content[: new_section.virtual_size] assert content == test_data # Save and reload output_path = tmp_path / "with_section.exe" surgery.save(output_path) # Reload and verify surgery2 = CoffSurgery.load(output_path) new_count = len(list(surgery2.iter_sections())) assert new_count == original_count + 1 section = surgery2.find_section(".test") assert section is not None content = surgery2.get_section_content(section) content = content[: section.virtual_size] assert content == test_data class TestHostOnlyBinaries: """Tests for host-only binaries (negative tests).""" def test_host_only_exe_no_hip_fat(self, test_assets_dir: Path): """Test that host-only exe has no .hip_fat section.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.exe" surgery = CoffSurgery.load(binary) # Should NOT have .hip_fat section assert surgery.find_section(".hip_fat") is None assert surgery.find_section(".hipFatB") is None def test_host_only_dll_no_hip_fat(self, test_assets_dir: Path): """Test that host-only DLL has no .hip_fat section.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Should NOT have .hip_fat section assert surgery.find_section(".hip_fat") is None assert surgery.find_section(".hipFatB") is None class TestCoffSurgeryErrors: """Tests for error handling in CoffSurgery.""" def test_load_truncated_file_raises(self, tmp_path: Path): """Test that loading a truncated file raises an error.""" truncated = tmp_path / "truncated.dll" # Write just MZ header, not enough for full PE data = bytearray(64) struct.pack_into(" 0: # This is a BSS-like section offset = surgery.rva_to_file_offset(section.rva) assert offset is None return pytest.skip("No BSS section found in test binary") def test_va_conversion_roundtrip(self, test_assets_dir: Path): """Test VA to RVA and back conversion.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Get a section RVA section = surgery.find_section(".text") if section is None: pytest.skip("No .text section") rva = section.rva va = surgery.rva_to_va(rva) rva_back = surgery.va_to_rva(va) assert rva_back == rva assert va == surgery.image_base + rva class TestWriteOperations: """Tests for write operations.""" def test_write_bytes_at_offset(self, test_assets_dir: Path, tmp_path: Path): """Test writing bytes at a file offset.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Find a writable location (end of a section) section = surgery.find_section(".text") if section is None: pytest.skip("No .text section") # Write some bytes at a safe offset (within section) test_data = b"TEST" write_offset = section.file_offset + 10 surgery.write_bytes_at_offset(write_offset, test_data, "test write") # Verify the write assert surgery.data[write_offset : write_offset + len(test_data)] == test_data # Verify modification was tracked assert any(m.operation == "write_bytes" for m in surgery.modifications) def test_write_beyond_file_raises(self, test_assets_dir: Path): """Test that writing beyond file bounds raises error.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) file_size = len(surgery.data) with pytest.raises(ValueError, match="exceed file bounds"): surgery.write_bytes_at_offset(file_size, b"OVERFLOW") def test_zero_range(self, test_assets_dir: Path): """Test zeroing a range of bytes.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Find a section with data section = surgery.find_section(".text") if section is None: pytest.skip("No .text section") # Zero some bytes zero_offset = section.file_offset + 20 zero_size = 16 surgery.zero_range(zero_offset, zero_size) # Verify zeros assert ( surgery.data[zero_offset : zero_offset + zero_size] == b"\x00" * zero_size ) def test_write_pointer_at_rva(self, test_assets_dir: Path): """Test writing a pointer at an RVA.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Find a section section = surgery.find_section(".text") if section is None: pytest.skip("No .text section") # Write a pointer (8 bytes) rva = section.rva + 32 # Offset into section test_value = 0x140001000 surgery.write_pointer_at_rva(rva, test_value) # Read it back read_value = surgery.read_pointer_at_rva(rva) assert read_value == test_value class TestBaseRelocations: """Tests for base relocation operations.""" def test_iter_base_relocations(self, test_assets_dir: Path): """Test iterating base relocations.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Iterate relocations relocs = list(surgery.iter_base_relocations()) # DLLs typically have relocations # Just verify we can iterate without error for reloc in relocs: assert reloc.target_rva >= 0 assert reloc.entry is not None def test_find_relocation_at_rva(self, test_assets_dir: Path): """Test finding a relocation at a specific RVA.""" binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" surgery = CoffSurgery.load(binary) # Get first relocation relocs = list(surgery.iter_base_relocations()) if not relocs: pytest.skip("No relocations in test binary") first_reloc = relocs[0] # Find it found = surgery.find_relocation_at_rva(first_reloc.target_rva) assert found is not None assert found.target_rva == first_reloc.target_rva def test_no_relocation_directory(self, tmp_path: Path, test_assets_dir: Path): """Test iteration when no relocation directory exists.""" # Create a PE-like structure without relocation directory binary = test_assets_dir / "bundled_binaries/windows/cov5/host_only.dll" data = bytearray(binary.read_bytes()) # Find PE offset pe_offset = struct.unpack_from("