Files
ls2tp/tools/tp_dump.py
Hermes 9e997327e9 Initial commit: lexer, parser, tp_dump tool
- LSLexer: tokenisation complete du format LS Fanuc
- LSParser: AST pour programmes LS (mouvements, CALL, JMP, WAIT, IF)
- tp_dump: analyseur hexa TP avec header parsing et comparaison
- tp_format: structures du format binaire TP (V9.30)
- CLI: interface en ligne de commande (lex, parse, compile)
- Tests unitaires pour lexer et parser
2026-07-01 17:41:28 +00:00

329 lines
12 KiB
Python

#!/usr/bin/env python3
"""
tp_dump — Analyseur hexadécimal de fichiers TP Fanuc
Outil d'analyse et de visualisation du format binaire TP Fanuc.
Utile pour la rétro-ingénierie du format en comparant des paires
de fichiers LS/TP.
Usage:
tp_dump fichier.tp # Analyse complète
tp_dump fichier.tp --header-only # Header uniquement
tp_dump fichier.tp --instructions # Décodage des instructions
tp_dump fichier.tp --positions # Données de positions
tp_dump fichier.tp --hex # Dump hexadécimal complet
tp_dump fichier.tp --compare f2.tp # Comparaison de deux TP
"""
import sys
import struct
import argparse
from pathlib import Path
from datetime import datetime
# Ajouter le répertoire parent au path pour l'import
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from ls2tp.tp_format import (
TPHeader, TPInstruction, TPPosition, TPFile,
TPOpcode, PositionType, ConfigFlags,
identify_tp_version, is_valid_tp,
)
# ═══════════════════════════════════════════════════════════════
# Affichage formaté
# ═══════════════════════════════════════════════════════════════
class Colors:
"""Codes ANSI pour la coloration."""
HEADER = '\033[1;36m' # Cyan bold
SECTION = '\033[1;33m' # Jaune bold
VALUE = '\033[1;32m' # Vert bold
WARN = '\033[1;31m' # Rouge bold
DIM = '\033[2m' # Dim
RESET = '\033[0m'
HEX_ADDR = '\033[2;34m' # Bleu dim
HEX_ASCII = '\033[2;37m' # Gris dim
def color(c: str, text: str) -> str:
"""Applique une couleur si le terminal le supporte."""
return f"{c}{text}{Colors.RESET}"
def hexdump(data: bytes, offset: int = 0, length: int | None = None,
show_header: bool = True) -> str:
"""Affiche un dump hexadécimal formaté (style xxd)."""
if length is None:
length = len(data)
end = min(offset + length, len(data))
lines = []
if show_header:
lines.append(color(Colors.DIM,
" Offset │ 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F │ ASCII"))
for pos in range(offset, end, 16):
chunk = data[pos:min(pos + 16, end)]
hex_parts = []
ascii_parts = []
for i in range(16):
if i < len(chunk):
b = chunk[i]
hex_parts.append(f"{b:02X}")
ascii_parts.append(chr(b) if 32 <= b <= 126 else '.')
else:
hex_parts.append(' ')
ascii_parts.append(' ')
hex_str = ' '.join(hex_parts[:8]) + ' ' + ' '.join(hex_parts[8:])
ascii_str = ''.join(ascii_parts)
addr = color(Colors.HEX_ADDR, f"{pos:08X}")
text = color(Colors.DIM, ascii_str)
lines.append(f" {addr} │ {hex_str} │ {text}")
return '\n'.join(lines)
def analyze_header(data: bytes) -> str:
"""Analyse et affiche l'en-tête TP."""
lines = []
lines.append(color(Colors.SECTION, "\n═══ TP HEADER ═══"))
try:
header = TPHeader.unpack(data)
except Exception as e:
lines.append(color(Colors.WARN, f" Erreur parsing header: {e}"))
# Fallback : dump brut des 64 premiers octets
lines.append(hexdump(data, 0, 64))
return '\n'.join(lines)
lines.append(f" Magic: {header.magic.hex(' ').upper()}")
lines.append(f" Version: {header.version_str()} (raw: 0x{header.version:04X})")
lines.append(f" Programme: '{header.name}'")
ts_dt = header.timestamp_dt()
ts_str = ts_dt.strftime('%Y-%m-%d %H:%M:%S') if ts_dt else 'N/A'
lines.append(f" Timestamp: {header.timestamp} ({ts_str})")
lines.append(f" Instructions: {header.instr_count}")
lines.append(f" Positions: {header.pos_count}")
lines.append(f" Instr offset: 0x{header.instr_offset:04X}")
lines.append(f" Pos offset: 0x{header.pos_offset:04X}")
lines.append(f" Taille tot.: {header.total_size} bytes")
# Version détectée
detected = identify_tp_version(data)
if detected:
lines.append(color(Colors.DIM, f" Détecté: {detected}"))
return '\n'.join(lines)
def analyze_instructions(data: bytes, header: TPHeader) -> str:
"""Tente d'analyser la section instructions."""
lines = []
lines.append(color(Colors.SECTION, "\n═══ INSTRUCTIONS ═══"))
if header.instr_offset == 0 or header.instr_offset >= len(data):
# Essayer de trouver les instructions après le header
start = header.HEADER_SIZE
lines.append(color(Colors.DIM,
f" Offset table non trouvé. Scan depuis offset 0x{start:04X}..."))
# Afficher les 256 premiers bytes après le header comme instructions potentielles
scan_len = min(512, len(data) - start)
lines.append(hexdump(data, start, scan_len))
return '\n'.join(lines)
# Lire la table d'offsets
lines.append(f" Offset table: 0x{header.instr_offset:04X}")
lines.append(f" Nombre d'instructions: {header.instr_count}")
# Chaque entrée de la table fait probablement 4 bytes (offset)
table_start = header.instr_offset
for i in range(min(header.instr_count, 50)): # Max 50 pour lisibilité
entry_off = table_start + i * 4
if entry_off + 4 > len(data):
break
instr_off = struct.unpack('<I', data[entry_off:entry_off + 4])[0]
lines.append(f" [{i:4d}] offset=0x{instr_off:06X}")
return '\n'.join(lines)
def analyze_positions(data: bytes, header: TPHeader) -> str:
"""Tente d'analyser la section positions."""
lines = []
lines.append(color(Colors.SECTION, "\n═══ POSITIONS ═══"))
if header.pos_count == 0:
lines.append(" Aucune position dans ce fichier.")
return '\n'.join(lines)
lines.append(f" Nombre de positions: {header.pos_count}")
if header.pos_offset > 0 and header.pos_offset < len(data):
lines.append(f" Offset données: 0x{header.pos_offset:06X}")
# Essayer d'interpréter les positions
pos_data = data[header.pos_offset:]
lines.append(f" Taille données positions: {len(pos_data)} bytes")
# Afficher un échantillon
sample_len = min(256, len(pos_data))
lines.append(hexdump(data, header.pos_offset, sample_len))
return '\n'.join(lines)
def compare_files(data1: bytes, data2: bytes, name1: str, name2: str) -> str:
"""Compare deux fichiers TP octet par octet."""
lines = []
lines.append(color(Colors.SECTION, "\n═══ COMPARAISON ═══"))
lines.append(f" Fichier 1: {name1} ({len(data1)} bytes)")
lines.append(f" Fichier 2: {name2} ({len(data2)} bytes)")
if len(data1) != len(data2):
lines.append(color(Colors.WARN,
f" ⚠ Tailles différentes: Δ = {abs(len(data1) - len(data2))} bytes"))
# Comparer les headers
try:
h1 = TPHeader.unpack(data1)
h2 = TPHeader.unpack(data2)
lines.append("\n Différences header:")
if h1.version != h2.version:
lines.append(f" Version: {h1.version_str()} vs {h2.version_str()}")
if h1.instr_count != h2.instr_count:
lines.append(f" Instructions: {h1.instr_count} vs {h2.instr_count}")
if h1.pos_count != h2.pos_count:
lines.append(f" Positions: {h1.pos_count} vs {h2.pos_count}")
except Exception:
pass
# Comparer octet par octet
diff_count = 0
min_len = min(len(data1), len(data2))
for i in range(min_len):
if data1[i] != data2[i]:
diff_count += 1
lines.append(f"\n Octets différents: {diff_count} sur {min_len} "
f"({diff_count / min_len * 100:.1f}%)")
# Afficher les 20 premières différences
shown = 0
for i in range(min_len):
if data1[i] != data2[i] and shown < 20:
if shown == 0:
lines.append("\n Premières différences:")
lines.append(f" 0x{i:06X}: {data1[i]:02X} → {data2[i]:02X}")
shown += 1
if diff_count > 20:
lines.append(f" ... et {diff_count - 20} autres")
return '\n'.join(lines)
def full_analysis(filepath: Path) -> str:
"""Analyse complète d'un fichier TP."""
data = filepath.read_bytes()
size = len(data)
lines = []
lines.append(color(Colors.HEADER, f"\n{'═' * 60}"))
lines.append(color(Colors.HEADER, f" TP DUMP — {filepath.name}"))
lines.append(color(Colors.HEADER, f"{'═' * 60}"))
lines.append(f" Taille: {size} bytes ({size / 1024:.1f} KB)")
is_valid = is_valid_tp(data)
status = color(Colors.VALUE, "✓ Valide") if is_valid else color(Colors.WARN, "⚠ Format non reconnu")
lines.append(f" Status: {status}")
# Header
lines.append(analyze_header(data))
# Essayer de parser le header pour la suite
try:
header = TPHeader.unpack(data)
except Exception:
header = TPHeader()
# Instructions
lines.append(analyze_instructions(data, header))
# Positions
lines.append(analyze_positions(data, header))
# Dump hexadécimal des 256 derniers bytes (footer potentiel)
if size > 256:
lines.append(color(Colors.SECTION, "\n═══ FOOTER (derniers 128 bytes) ═══"))
lines.append(hexdump(data, max(0, size - 128), 128))
return '\n'.join(lines)
# ═══════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════
def main():
parser = argparse.ArgumentParser(
description="Analyseur de fichiers TP Fanuc — rétro-ingénierie du format binaire",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Exemples:
tp_dump MONPROG.TP Analyse complète
tp_dump MONPROG.TP --header-only Header uniquement
tp_dump MONPROG.TP --hex Dump hexadécimal complet
tp_dump PROG1.TP --compare PROG2.TP Comparer deux TP
"""
)
parser.add_argument('file', type=Path, help='Fichier TP à analyser')
parser.add_argument('--header-only', action='store_true',
help='Afficher uniquement le header')
parser.add_argument('--instructions', action='store_true',
help='Analyser les instructions')
parser.add_argument('--positions', action='store_true',
help='Analyser les positions')
parser.add_argument('--hex', action='store_true',
help='Dump hexadécimal complet')
parser.add_argument('--hex-offset', type=lambda x: int(x, 0), default=0,
help='Offset de départ pour le dump hex')
parser.add_argument('--hex-length', type=lambda x: int(x, 0), default=512,
help='Longueur du dump hex (défaut: 512)')
parser.add_argument('--compare', type=Path,
help='Comparer avec un autre fichier TP')
parser.add_argument('--raw-offsets', action='store_true',
help='Afficher les offsets bruts des sections')
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()
if args.compare:
if not args.compare.exists():
print(f"Erreur: fichier de comparaison '{args.compare}' introuvable.",
file=sys.stderr)
sys.exit(1)
data2 = args.compare.read_bytes()
print(compare_files(data, data2, args.file.name, args.compare.name))
elif args.header_only:
print(analyze_header(data))
elif args.hex:
print(hexdump(data, args.hex_offset, args.hex_length))
else:
print(full_analysis(args.file))
if __name__ == '__main__':
main()