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
206 lines
6.0 KiB
Python
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()
|