#!/usr/bin/env python3 """ tp_parse — Décodeur du header de fichier TP binaire Fanuc. Rétro-ingénierie du format binaire TP (R-30iB, V9.x) à partir de fichiers authentiques trouvés sur GitHub. CONNAISSANCES ÉTABLIES : - Magic signature : FE EF 00 01 (commune à tous les fichiers) - Header fixe de 13 octets (0x00-0x0C) - Champ taille sur 2 octets à 0x06-0x07 - Nom du programme à partir de 0x0D, encodé en groupes [4 chars][1 contrôle][8 chars][1 contrôle][8 chars]... - Le nom se termine par l'octet de contrôle 0xDC (généralement) - Les chaînes (nom, owner, commentaire) sont entrecoupées d'octets de contrôle (>= 0x80 ou == 0xFF) toutes les 8 positions Usage: tp_parse fichier.tp tp_parse fichier.tp --verbose """ import sys import argparse from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) MAGIC = b'\xfe\xef' VERSION = b'\x00\x01' def is_control(b: int) -> bool: """Un octet de contrôle est >= 0x80 (high bit set).""" return b >= 0x80 def is_printable(b: int) -> bool: """Caractère ASCII imprimable (espace à ~).""" return 0x20 <= b < 0x7F def decode_name(data: bytes) -> str: """ Décode le nom du programme à partir de 0x0D. Structure : [4 chars][1 contrôle][8 chars][1 contrôle][8 chars]... """ if len(data) < 0x0E: return "" name_chars = [] i = 0x0D # début du nom # Premier groupe de 4 caractères for _ in range(4): if i < len(data) and is_printable(data[i]): name_chars.append(chr(data[i])) i += 1 # Contrôle if i < len(data): i += 1 # Groupes de 8 caractères suivis d'un contrôle while i < len(data): # Lire jusqu'à 8 caractères group_end = min(i + 8, len(data)) for j in range(i, group_end): if is_printable(data[j]): name_chars.append(chr(data[j])) else: # Octet non-imprimable → fin du nom return ''.join(name_chars) i = group_end # Contrôle if i < len(data): # Si le contrôle est 0xDC (terminateur courant), arrêter if data[i] == 0xDC: return ''.join(name_chars) i += 1 else: break return ''.join(name_chars) def decode_string_field(data: bytes, start: int) -> tuple[str, int]: """ Décode un champ texte encodé (même schéma que le nom) à partir de start. Retourne (texte, offset_de_fin). """ chars = [] i = start # Premier groupe de 4 caractères for _ in range(4): if i < len(data) and is_printable(data[i]): chars.append(chr(data[i])) i += 1 if i < len(data): i += 1 # contrôle # Groupes de 8 while i < len(data): group_end = min(i + 8, len(data)) for j in range(i, group_end): if is_printable(data[j]): chars.append(chr(data[j])) else: return ''.join(chars), j i = group_end if i < len(data): if data[i] == 0xDC: return ''.join(chars), i i += 1 else: break return ''.join(chars), i def parse_header(data: bytes) -> dict: """Parse le header complet du fichier TP.""" result = { 'magic': data[0:2].hex(), 'version': data[2:4].hex(), 'size_field': int.from_bytes(data[6:8], 'big'), 'name': '', 'owner': '', 'file_size': len(data), } if data[:2] != MAGIC: result['error'] = f"Magic invalide: {data[:2].hex()} (attendu {MAGIC.hex()})" return result # Nom du programme result['name'] = decode_name(data) # Chercher l'owner (souvent "MNEDITOR" ou "LMNEDITOR" après le nom) # L'owner apparaît après le nom + quelques octets de métadonnées # Heuristique : chercher des chaînes connues text = data.decode('latin-1', errors='ignore') for known in ['LMNEDITOR', 'MNEDITOR']: idx = text.find(known) if idx >= 0: result['owner'] = known break return result def extract_readable_strings(data: bytes, min_len: int = 4) -> list[str]: """Extrait toutes les chaînes ASCII lisibles (runs de caractères).""" import re runs = re.findall(rb'[\x20-\x7E]{%d,}' % min_len, data) return [r.decode('ascii') for r in runs] def main(): parser = argparse.ArgumentParser(description="Décodeur de header TP binaire Fanuc") parser.add_argument('file', type=Path, help='Fichier TP à analyser') parser.add_argument('--verbose', action='store_true', help='Affichage détaillé') parser.add_argument('--strings', action='store_true', help='Extraire les chaînes lisibles') args = parser.parse_args() if not args.file.exists(): print(f"Erreur: fichier '{args.file}' introuvable.", file=sys.stderr) sys.exit(1) data = args.file.read_bytes() print(f"═══ TP Header — {args.file.name} ({len(data)} bytes) ═══") header = parse_header(data) print(f"Magic: {header['magic'].upper()}") print(f"Version: {header['version'].upper()}") print(f"Size field: 0x{header['size_field']:04X} ({header['size_field']})") print(f"Nom: '{header['name']}'") if header.get('owner'): print(f"Owner: '{header['owner']}'") if header.get('error'): print(f"⚠ {header['error']}") if args.verbose: print(f"\n── 32 premiers octets ──") for off in range(0, 32, 16): chunk = data[off:off + 16] hexstr = ' '.join(f'{b:02X}' for b in chunk) ascii_v = ''.join(chr(b) if 0x20 <= b < 0x7F else '.' for b in chunk) print(f" 0x{off:04X}: {hexstr:<48s} {ascii_v}") if args.strings: strings = extract_readable_strings(data) print(f"\n── Chaînes lisibles ({len(strings)}) ──") for s in strings: print(f" '{s}'") if __name__ == '__main__': main()