#!/usr/bin/env python3 # Copyright 2020 The Pigweed Authors # # 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. """Tests the ELF reader Python module.""" import io import os import re import unittest from pw_tokenizer import elf_reader # Output from the following command: # # readelf -WS elf_reader_test_binary.elf # TEST_READELF_OUTPUT = (""" There are 33 section headers, starting at offset 0x1758: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al [ 0] NULL 0000000000000000 000000 000000 00 0 0 0 [ 1] .interp PROGBITS 0000000000000238 000238 00001c 00 A 0 0 1 [ 2] .note.ABI-tag NOTE 0000000000000254 000254 000020 00 A 0 0 4 [ 3] .note.gnu.build-id NOTE 0000000000000274 000274 000024 00 A 0 0 4 [ 4] .dynsym DYNSYM 0000000000000298 000298 0000a8 18 A 5 1 8 [ 5] .dynstr STRTAB 0000000000000340 000340 00009b 00 A 0 0 1 [ 6] .gnu.hash GNU_HASH 00000000000003e0 0003e0 00001c 00 A 4 0 8 [ 7] .gnu.version VERSYM 00000000000003fc 0003fc 00000e 02 A 4 0 2 [ 8] .gnu.version_r VERNEED 000000000000040c 00040c 000020 00 A 5 1 4 [ 9] .rela.dyn RELA 0000000000000430 000430 0000d8 18 A 4 0 8 [10] .rela.plt RELA 0000000000000508 000508 000018 18 AI 4 12 8 [11] .init PROGBITS 0000000000000520 000520 000017 00 AX 0 0 4 [12] .plt PROGBITS 0000000000000540 000540 000020 10 AX 0 0 16 [13] .text PROGBITS 0000000000000560 000560 000151 00 AX 0 0 16 [14] .fini PROGBITS 00000000000006b4 0006b4 000009 00 AX 0 0 4 [15] .rodata PROGBITS 00000000000006c0 0006c0 000004 04 AM 0 0 4 [16] .test_section_1 PROGBITS 00000000000006d0 0006d0 000010 00 A 0 0 16 [17] .test_section_2 PROGBITS 00000000000006e0 0006e0 000004 00 A 0 0 4 [18] .eh_frame X86_64_UNWIND 00000000000006e8 0006e8 0000d4 00 A 0 0 8 [19] .eh_frame_hdr X86_64_UNWIND 00000000000007bc 0007bc 00002c 00 A 0 0 4 [20] .fini_array FINI_ARRAY 0000000000001d80 000d80 000008 08 WA 0 0 8 [21] .init_array INIT_ARRAY 0000000000001d88 000d88 000008 08 WA 0 0 8 [22] .dynamic DYNAMIC 0000000000001d90 000d90 000220 10 WA 5 0 8 [23] .got PROGBITS 0000000000001fb0 000fb0 000030 00 WA 0 0 8 [24] .got.plt PROGBITS 0000000000001fe0 000fe0 000020 00 WA 0 0 8 [25] .data PROGBITS 0000000000002000 001000 000010 00 WA 0 0 8 [26] .tm_clone_table PROGBITS 0000000000002010 001010 000000 00 WA 0 0 8 [27] .bss NOBITS 0000000000002010 001010 000001 00 WA 0 0 1 [28] .comment PROGBITS 0000000000000000 001010 00001d 01 MS 0 0 1 [29] .note.gnu.gold-version NOTE 0000000000000000 001030 00001c 00 0 0 4 [30] .symtab SYMTAB 0000000000000000 001050 000390 18 31 21 8 [31] .strtab STRTAB 0000000000000000 0013e0 000227 00 0 0 1 [32] .shstrtab STRTAB 0000000000000000 001607 00014a 00 0 0 1 Key to Flags: W (write), A (alloc), X (execute), M (merge), S (strings), I (info), L (link order), O (extra OS processing required), G (group), T (TLS), C (compressed), x (unknown), o (OS specific), E (exclude), l (large), p (processor specific) """) TEST_ELF_PATH = os.path.join(os.path.dirname(__file__), 'elf_reader_test_binary.elf') class ElfReaderTest(unittest.TestCase): """Tests the elf_reader.Elf class.""" def setUp(self): super().setUp() self._elf_file = open(TEST_ELF_PATH, 'rb') self._elf = elf_reader.Elf(self._elf_file) def tearDown(self): super().tearDown() self._elf_file.close() def _section(self, name): return next(self._elf.sections_with_name(name)) def test_readelf_comparison_using_the_readelf_binary(self): """Compares elf_reader to readelf's output.""" parse_readelf_output = re.compile(r'\s+' r'\[\s*(?P\d+)\]\s+' r'(?P\.\S*)?\s+' r'(?P\S+)\s+' r'(?P[0-9a-fA-F]+)\s+' r'(?P[0-9a-fA-F]+)\s+' r'(?P[0-9a-fA-F]+)\s+') readelf_sections = [] for number, name, _, addr, offset, size in parse_readelf_output.findall( TEST_READELF_OUTPUT): readelf_sections.append(( int(number), name or '', int(addr, 16), int(offset, 16), int(size, 16), )) self.assertEqual(len(readelf_sections), 33) self.assertEqual(len(readelf_sections), len(self._elf.sections)) for (index, section), readelf_section in zip(enumerate(self._elf.sections), readelf_sections): readelf_index, name, address, offset, size = readelf_section self.assertEqual(index, readelf_index) self.assertEqual(section.name, name) self.assertEqual(section.address, address) self.assertEqual(section.offset, offset) self.assertEqual(section.size, size) def test_dump_single_section(self): self.assertEqual(self._elf.dump_section_contents(r'\.test_section_1'), b'You cannot pass\0') self.assertEqual(self._elf.dump_section_contents(r'\.test_section_2'), b'\xef\xbe\xed\xfe') def test_dump_multiple_sections(self): if (self._section('.test_section_1').address < self._section('.test_section_2').address): contents = b'You cannot pass\0\xef\xbe\xed\xfe' else: contents = b'\xef\xbe\xed\xfeYou cannot pass\0' self.assertIn(self._elf.dump_section_contents(r'.test_section_\d'), contents) def test_read_values(self): address = self._section('.test_section_1').address self.assertEqual(self._elf.read_value(address), b'You cannot pass') int32_address = self._section('.test_section_2').address self.assertEqual(self._elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe') def test_read_string(self): bytes_io = io.BytesIO( b'This is a null-terminated string\0No terminator!') self.assertEqual(elf_reader.read_c_string(bytes_io), b'This is a null-terminated string') self.assertEqual(elf_reader.read_c_string(bytes_io), b'No terminator!') self.assertEqual(elf_reader.read_c_string(bytes_io), b'') def test_compatible_file_for_elf(self): self.assertTrue(elf_reader.compatible_file(self._elf_file)) self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'\x7fELF'))) def test_compatible_file_for_elf_start_at_offset(self): self._elf_file.seek(13) # Seek ahead to get out of sync self.assertTrue(elf_reader.compatible_file(self._elf_file)) self.assertEqual(13, self._elf_file.tell()) def test_compatible_file_for_invalid_elf(self): self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'\x7fELVESF'))) def _archive_file(data: bytes) -> bytes: return ('FILE ID 90123456' 'MODIFIED 012' 'OWNER ' 'GROUP ' 'MODE 678' f'{len(data):10}' # File size -- the only part that's needed. '`\n'.encode() + data) class ArchiveTest(unittest.TestCase): """Tests reading from archive files.""" def setUp(self): super().setUp() with open(TEST_ELF_PATH, 'rb') as fd: self._elf_data = fd.read() self._archive_entries = b'blah', b'hello', self._elf_data self._archive_data = elf_reader.ARCHIVE_MAGIC + b''.join( _archive_file(f) for f in self._archive_entries) self._archive = io.BytesIO(self._archive_data) def test_compatible_file_for_archive(self): self.assertTrue(elf_reader.compatible_file(io.BytesIO(b'!\n'))) self.assertTrue(elf_reader.compatible_file(self._archive)) def test_compatible_file_for_invalid_archive(self): self.assertFalse(elf_reader.compatible_file(io.BytesIO(b'!'))) def test_iterate_over_files(self): for expected, size in zip(self._archive_entries, elf_reader.files_in_archive(self._archive)): self.assertEqual(expected, self._archive.read(size)) def test_iterate_over_empty_archive(self): with self.assertRaises(StopIteration): next(iter(elf_reader.files_in_archive(io.BytesIO(b'!\n')))) def test_iterate_over_invalid_archive(self): with self.assertRaises(elf_reader.FileDecodeError): for _ in elf_reader.files_in_archive( io.BytesIO(b'!blah blahblah')): pass def test_extra_newline_after_entry_is_ignored(self): archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC + _archive_file(self._elf_data) + b'\n' + _archive_file(self._elf_data)) for size in elf_reader.files_in_archive(archive): self.assertEqual(self._elf_data, archive.read(size)) def test_two_extra_newlines_parsing_fails(self): archive = io.BytesIO(elf_reader.ARCHIVE_MAGIC + _archive_file(self._elf_data) + b'\n\n' + _archive_file(self._elf_data)) with self.assertRaises(elf_reader.FileDecodeError): for size in elf_reader.files_in_archive(archive): self.assertEqual(self._elf_data, archive.read(size)) def test_iterate_over_archive_with_invalid_size(self): data = elf_reader.ARCHIVE_MAGIC + _archive_file(b'$' * 3210) file = io.BytesIO(data) # Iterate over the file normally. for size in elf_reader.files_in_archive(file): self.assertEqual(b'$' * 3210, file.read(size)) # Replace the size with a hex number, which is not valid. with self.assertRaises(elf_reader.FileDecodeError): for _ in elf_reader.files_in_archive( io.BytesIO(data.replace(b'3210', b'0x99'))): pass def test_elf_reader_dump_single_section(self): elf = elf_reader.Elf(self._archive) self.assertEqual(elf.dump_section_contents(r'\.test_section_1'), b'You cannot pass\0') self.assertEqual(elf.dump_section_contents(r'\.test_section_2'), b'\xef\xbe\xed\xfe') def test_elf_reader_read_values(self): elf = elf_reader.Elf(self._archive) address = next(elf.sections_with_name('.test_section_1')).address self.assertEqual(elf.read_value(address), b'You cannot pass') int32_address = next(elf.sections_with_name('.test_section_2')).address self.assertEqual(elf.read_value(int32_address, 4), b'\xef\xbe\xed\xfe') if __name__ == '__main__': unittest.main()