#!/usr/bin/env python3 import hashlib import os import struct import subprocess import shutil import tempfile import time from collections import defaultdict from dataclasses import dataclass from elftools.elf.elffile import ELFFile from elftools.elf.relocation import RelocationSection from elftools.elf.sections import SymbolTableSection from fbt.sdk.hashes import gnu_sym_hash from flipper.app import App VERSION = 1 def replace_file_with_retry(source: str, target: str, attempts: int = 50) -> None: for attempt in range(attempts): try: os.replace(source, target) return except PermissionError: if attempt == attempts - 1: raise time.sleep(0.1) except OSError: shutil.copy2(source, target) os.remove(source) return @dataclass class RelData: section: int section_value: int type: int offset: int name: str @dataclass(frozen=True) class UniqueRelData: section: int section_value: int type: int name: str @dataclass class RelSection: name: str oringinal_name: str data: dict[UniqueRelData, list[int]] def serialize_relsection_data(data: dict[UniqueRelData, list[int]]) -> bytes: result = struct.pack(" 0: result += struct.pack("> 8) & 0xFF, (offset >> 16) & 0xFF ) return result class Main(App): def init(self): self.parser.add_argument("fap_src_path", help="App file to upload") self.parser.add_argument("objcopy_path", help="Objcopy path") self.parser.set_defaults(func=self.process) def process(self): fap_path = self.args.fap_src_path objcopy_path = self.args.objcopy_path sections: list[RelSection] = [] with open(fap_path, "rb") as f: elf_file = ELFFile(f) relocation_sections: list[RelocationSection] = [] symtab_section: SymbolTableSection | None = None for section in elf_file.iter_sections(): if isinstance(section, RelocationSection): relocation_sections.append(section) if isinstance(section, SymbolTableSection): symtab_section = section if not symtab_section: self.logger.error("No symbol table found") return 1 if not relocation_sections: self.logger.info("No relocation sections found") return 0 for section in relocation_sections: section_relocations: list[RelData] = [] for relocation in section.iter_relocations(): symbol_id: int = relocation.entry["r_info_sym"] offset: int = relocation.entry["r_offset"] type: int = relocation.entry["r_info_type"] symbol = symtab_section.get_symbol(symbol_id) section_index: int = symbol["st_shndx"] section_value: int = symbol["st_value"] if section_index == "SHN_UNDEF": section_index = 0 section_relocations.append( RelData(section_index, section_value, type, offset, symbol.name) ) unique_relocations: dict[UniqueRelData, list[int]] = defaultdict(list) for relocation in section_relocations: unique = UniqueRelData( relocation.section, relocation.section_value, relocation.type, relocation.name, ) unique_relocations[unique].append(relocation.offset) section_name = section.name if section_name.startswith(".rel"): section_name = ".fast.rel" + section_name[4:] else: self.logger.error( "Unknown relocation section name: %s", section_name ) return 1 sections.append( RelSection(section_name, section.name, unique_relocations) ) with tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.abspath(fap_path))) as temp_dir: current_fap_path = fap_path for section_index, section in enumerate(sections): data = serialize_relsection_data(section.data) hash_name = hashlib.md5(section.name.encode()).hexdigest() filename = os.path.join(temp_dir, f"{section_index}_{hash_name}.bin") patched_fap_path = os.path.join( temp_dir, f"{section_index}_{hash_name}.fap" ) if os.path.isfile(filename): self.logger.error(f"File {filename} already exists") return 1 with open(filename, "wb") as f: f.write(data) exit_code = subprocess.run( [ objcopy_path, "--add-section", f"{section.name}={filename}", current_fap_path, patched_fap_path, ], check=True, ) if exit_code.returncode != 0: self.logger.error("objcopy failed") return 1 current_fap_path = patched_fap_path if current_fap_path != fap_path: replace_file_with_retry(current_fap_path, fap_path) return 0 if __name__ == "__main__": Main()()