Files
ls2tp/tools/tp_parse.py
Hermes 6b42945985 Decodage du header TP: magic, structure, encodage du nom
Decouvertes confirmees sur les 6 fichiers binaires:
- Magic signature: FE EF 00 01 (format TP Fanuc V9.x)
- Header fixe 13 octets: magic + version + champ taille + separateurs
- Encodage du nom: [4 chars][controle][8 chars][controle][8 chars]...
  avec octets de controle >= 0x80 aux positions 4, 13, 22...
  terminaison par 0xDC
- Champ 0x06-0x07 = probablement la taille decompressee

Nouvel outil tools/tp_parse.py:
- Decode le nom du programme (valide sur les 6 fichiers)
- Parse le header complet (magic, version, taille, owner)
- Extraction des chaines lisibles

Documentation mise a jour dans docs/TP_FORMAT_NOTES.md
2026-08-14 12:26:35 +00:00

206 lines
6.0 KiB
Python

#!/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()