Valider les boutons, le tactile et les outils du simulateur

Relecture et validations locales terminées : iPhone OS 2.2.1 et 2.1.1, gestes, HTTP, saisie, installation et persistance.
This commit was merged in pull request #2.
This commit is contained in:
Mahtan committed 2026-10-01 09:15:58 +02:00
commit 436bdc536c
19 files changed
+964 -102

No files matched your search

+2
View File
@@ -20,3 +20,5 @@ __pycache__/
/qemu-ios-generate-nor/
!/patches/
!/patches/*.patch
/apps/TouchProbe/TouchProbe.app/
+27
View File
@@ -2,6 +2,33 @@
État au 2026-09-30. Environnement : conteneur Arch Linux, utilisateur `dev` avec sudo, sans `CAP_NET_ADMIN`, sans `/dev/ppp` ni TUN.
## Dernière mise à jour — fiabilisation GPIO/SPI et agent v12
Les sections de diagnostic plus anciennes ci-dessous décrivent les étapes précédentes.
Le workflow `push-app` puis déverrouillage/lancement a été validé avec l'agent v11.
Cette suite corrige les débordements GPIO/SYSIC, les commandes tactiles invalides,
les trames instables et les transactions SPI abandonnées à la reprise de l'écran.
Huit cycles veille/réveil alternant Home et Power ont passé sur 5H11a sans perte
du tactile. La calibration TouchProbe retrouve les coordonnées écran sur sept
points ; la vitesse verticale et le calcul signé des vitesses sont corrigés.
L'agent v12 conserve le protocole et transfère par blocs de 64 Kio avec fichiers
temporaires distincts. `ipod-test-app` prépare une NAND indépendante, vérifie le
premier plan, récupère journal et capture, puis arrête sa propre session.
Voir le README pour les commandes et les tests reproductibles ASan/UBSan.
Validation finale : huit régressions matérielles avec ASan/UBSan et sept tests
sur les transferts ; HelloEmu réussit le workflow automatique, l'application
absente échoue avec capture et arrêt de la VM. Un fichier de 8 460 608 octets
fait l'aller-retour via l'agent v12 avec SHA-256 identique. Trois cycles de
veille/réveil supplémentaires avec la calibration finale et l'agent v12 passent,
puis HelloEmu se lance au premier plan avec son journal. Diagnostics locaux :
`work/fiabilisation-20260930/` (dont `test-hello`, `test-echec`, `hello-final.png`).
Les sessions de validation sont arrêtées.
Les fichiers générés, images et diagnostics restent hors du dépôt. Les images
5H11a et les NAND d'origine sont conservées ; le RTC, le Wi-Fi matériel, l'audio
et la veille profonde restent à implémenter. Les nouveaux correctifs ne sont
pas encore validés sur 5F138.
## Objectif
On fournit un simulateur d'**iPod Touch 2G sous iPhone OS 2.2.1** qui sert surtout à un agent IA pour tester les applications iOS 2.x d'AlloVoisins. Les apps sont compilées en armv6 avec `/opt/cctools` et `/opt/theos/sdks/iPhoneOS2.2.1.sdk`. L'agent doit pouvoir faire les choses suivantes, sans intervention humaine :
+65 -7
View File
@@ -27,7 +27,7 @@ redirection. `stop` nettoie le relais suivi par PID même si QEMU est déjà arr
## Utilisation
```bash
ipod-emu start -W # agent v11 : conserve la NAND, attend SpringBoard et déverrouille (~35–50 s)
ipod-emu start -w 5H11a-agent-v12 -W # attend SpringBoard et déverrouille (~25–50 s)
# Ajouter -f uniquement pour remettre la NAND à zéro.
ipod-qmp screenshot ecran.png # capture 320x480
ipod-qmp tap 160 326 # touche (coordonnées écran), ipod-qmp tap X Y 1.5 = appui long
@@ -37,14 +37,13 @@ ipod-emu log # console du kernel + syslog iOS
ipod-emu stop
```
- Pas de touch avant que `ipod-emu wait` / `start -W` ait rendu la main : le contrôleur tactile plante QEMU s'il est
touché pendant son démarrage.
- Attendre `ipod-emu wait` / `start -W` avant les gestes : le pilote tactile invité doit avoir terminé son démarrage.
- VNC sur `127.0.0.1:5900` pour interagir à la main (tunnel SSH : `ssh -L 5900:127.0.0.1:5900 …`).
- Le lancement d'une app prend 10-20 s.
## Déverrouillage après redémarrage
Avec l'agent **v11**, `start -W` et `wait` vérifient le verrouillage, réveillent
Avec l'agent **v11 ou ultérieur**, `start -W` et `wait` vérifient le verrouillage, réveillent
l'écran et effectuent le glissement normal avant de rendre la main.
`ipod-agent screen` retourne `locked=0 passcode=0` une fois déverrouillé ;
`ipod-agent unlock` permet de refaire ce parcours après démarrage.
@@ -89,6 +88,65 @@ Les `.app`/`.ipa` vont dans `/Applications` (re-signés avec `ldid -S` ; le kern
la toolchain de l'environnement (`arm-apple-darwin9-gcc -isysroot /opt/theos/sdks/iPhoneOS2.2.1.sdk`), voir
`apps/HelloEmu`. Environ 230 Mo libres dans l'image 2.2.1.
## Test automatique et diagnostics
```bash
# Construire d'abord l'agent v12 et une image dédiée depuis la base inchangée :
PATH=/opt/cctools/bin:$PATH make -C agent
ipod-mkimage -b 5H11a -o 5H11a-agent-v12 agent/overlay
# Après avoir arrêté sa session, tester dans une NAND indépendante :
ipod-test-app MonApp.app
ipod-test-app MonApp.ipa --output /chemin/nouveau-dossier
ipod-test-app paquet.deb --bundle-id com.exemple.monapp
```
Les chemins `agent/` ci-dessus sont relatifs au dossier des outils ; dans le dépôt,
utiliser `contrib/ipod-emu/agent`. Choisir un nom d'image absent lors de sa création.
`--base` permet de choisir une autre image contenant l'agent v11 ou ultérieur.
Le bundle-id est lu dans le plist des `.app` et `.ipa` ; il doit être fourni pour
les `.deb` et overlays. Le runner refuse de démarrer si les ports VNC ou agent
sont déjà occupés. Chaque test crée une image et une session dans son propre
répertoire ; la NAND de `run/` et l'image de base restent conservées.
Le code de sortie vaut zéro uniquement si l'app apparaît puis reste au premier
plan pendant `--settle` secondes (3 par défaut), si les diagnostics sont collectés
et si la VM est arrêtée. Ce contrôle ne remplace pas les tests fonctionnels de l'app.
Les résultats vont dans `work/tests/<date>-<identifiant>/`, ou dans un nouveau dossier
`--output` : `result.json`, `workflow.log`, `app.log`, `screen.png` et les logs QEMU,
série et réseau dans `session/run/`. En cas d'échec, les diagnostics disponibles
sont conservés et la session de test est arrêtée.
## Tactile et transferts
Home/Power ne doivent plus écrire hors du tableau GPIO : le port C est inclus.
Les trous des registres SYSIC ne débordent plus dans le groupe voisin.
Le tactile traite séparément les requêtes de longueur et de données, accepte
EA/EB, conserve une trame stable, et abandonne proprement une commande lors de
la réinitialisation SPI. Les commandes et rapports inconnus ne font plus arrêter
QEMU. Le découpage des requêtes est documenté également dans
[OpeniBoot Z2](https://github.com/iDroid-Project/openiBoot/blob/master/plat-s5l8900/multitouch-z2.c).
La vitesse verticale utilise `y`, les vitesses conservent leur signe et sont
bornées à 16 bits. `apps/TouchProbe` permet de mesurer la calibration : compiler
avec son Makefile, lancer avec `--log`, puis comparer les gestes QMP au marqueur
bleu et aux coordonnées du journal (ajouter 20 pixels à l'ordonnée locale de la vue).
La calibration a été vérifiée sur 5H11a, sur sept points de `(20,40)` à `(300,460)`.
Les modifications n'ont pas encore été validées sur 5F138.
Agent **v12** : `put` et `get` utilisent des blocs de 64 Kio au lieu d'allouer
le fichier entier dans l'iPod. Un upload incomplet supprime son temporaire et
conserve le fichier précédent ; le client fait de même pour les téléchargements.
Le protocole reste compatible avec l'agent v11. Les téléchargements vers stdout
peuvent naturellement afficher un préfixe avant une erreur de transport.
Depuis le dépôt, les tests sans firmware sont :
```bash
contrib/ipod-emu/tests/run-multitouch # build QEMU arm existant, Ninja, compilateur avec ASan/UBSan
python3 contrib/ipod-emu/tests/test-transfers.py # Python et compilateur C, sockets locales
```
## Limites
- **Persistance NAND** : les pages et métadonnées écrites par iOS, ainsi que les effacements de blocs, sont conservés
@@ -103,9 +161,9 @@ la toolchain de l'environnement (`arm-apple-darwin9-gcc -isysroot /opt/theos/sdk
L'image de base est en français, région France, fuseau Europe/Paris.
- **Relance de SpringBoard** : `respring` peut bloquer la reconstruction du catalogue d'apps. `push-app` redémarre
donc l'émulateur sans reconstruire l'image ni réinitialiser la NAND. Avec l'agent v11, le déverrouillage puis
le lancement de HelloEmu au premier plan sont validés après cette installation. Les gestes Home/Power restent
susceptibles de déclencher un défaut du pilote lors de la reprise de l'écran ; le workflow automatique utilise
le réveil par l'API d'affichage et vérifie le verrouillage.
le lancement de HelloEmu au premier plan sont validés après cette installation. Les corrections GPIO/SPI
ont permis huit cycles veille/réveil alternant Home et Power sur 5H11a. Le déverrouillage automatique
continue à vérifier l'état de l'écran.
- **Veille** : l'agent v10 pose `SleepDisabled` sur `IOPMrootDomain`, le SoC ne sachant pas reprendre une veille profonde.
- Pas de son, pas d'accélération graphique (SpringBoard tourne avec `LK_ENABLE_MBX2D=0`).
+51 -25
View File
@@ -67,7 +67,7 @@ struct rt_msghdr {
#include <notify.h>
#include <unistd.h>
#define VERSION "11"
#define VERSION "12"
#define PORT 7788
#define PPP_TTY "/dev/tty.bluetooth"
#define GUEST_IP "10.0.2.15"
@@ -236,7 +236,7 @@ static int send_all(int fd, const void *b, size_t n)
const char *p = b;
while (n) {
ssize_t w = write(fd, p, n);
if (w < 0) { if (errno == EINTR) continue; return -1; }
if (w <= 0) { if (w < 0 && errno == EINTR) continue; if (!w) errno = EIO; return -1; }
p += w; n -= w;
}
return 0;
@@ -569,49 +569,75 @@ static int read_full(int fd, void *b, size_t n)
char *p = b;
while (n) {
ssize_t r = read(fd, p, n);
if (r <= 0) { if (r < 0 && errno == EINTR) continue; return -1; }
if (r <= 0) { if (r < 0 && errno == EINTR) continue; if (!r) errno = EIO; return -1; }
p += r; n -= r;
}
return 0;
}
/* Transferts par blocs : ne pas allouer la taille du fichier sur l'iPod. */
static void cmd_put(int fd, char **args, int nargs)
{
if (nargs < 3) { reply_str(fd, 0, "usage: put<TAB>chemin<TAB>mode octal<TAB>taille + données"); return; }
size_t size = strtoul(args[2], NULL, 10);
char *data = malloc(size ? size : 1);
if (read_full(fd, data, size)) { free(data); return; }
char *end;
errno = 0;
unsigned long size = strtoul(args[2], &end, 10);
if (errno || !args[2][0] || args[2][0] == '-' || *end) {
reply_str(fd, 0, "taille invalide"); return;
}
char dir[1024];
strncpy(dir, args[0], sizeof dir - 1);
dir[sizeof dir - 1] = 0;
if (strlen(args[0]) >= sizeof dir) { reply_str(fd, 0, "chemin trop long"); return; }
strcpy(dir, args[0]);
char *slash = strrchr(dir, '/');
if (slash && slash != dir) { *slash = 0; mkdirs(dir); }
char tmp[1100];
snprintf(tmp, sizeof tmp, "%s.emuagent-tmp", args[0]);
int f = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, strtoul(args[1], NULL, 8));
if (f < 0 || send_all(f, data, size) || fsync(f)) {
reply_str(fd, 0, "écriture de %s : %s", args[0], strerror(errno));
if (f >= 0) close(f);
free(data);
snprintf(tmp, sizeof tmp, "%s.emuagent-XXXXXX", args[0]);
int f = mkstemp(tmp);
if (f < 0) { reply_str(fd, 0, "création : %s", strerror(errno)); return; }
char data[65536];
size_t remaining = size;
int failed = 0;
while (remaining) {
size_t chunk = remaining < sizeof data ? remaining : sizeof data;
if (read_full(fd, data, chunk) || send_all(f, data, chunk)) { failed = 1; break; }
remaining -= chunk;
}
if (!failed && (fchmod(f, strtoul(args[1], NULL, 8)) || fsync(f))) failed = 1;
int saved = errno;
if (close(f) && !failed) { failed = 1; saved = errno; }
if (!failed && rename(tmp, args[0])) { failed = 1; saved = errno; }
if (failed) {
unlink(tmp);
reply_str(fd, 0, "écriture de %s : %s", args[0], strerror(saved));
return;
}
close(f);
chmod(tmp, strtoul(args[1], NULL, 8));
rename(tmp, args[0]);
free(data);
reply_str(fd, 1, "%zu octets écrits", size);
reply_str(fd, 1, "%lu octets écrits", size);
}
static void cmd_get(int fd, char **args, int nargs)
{
if (nargs < 1) { reply_str(fd, 0, "usage: get<TAB>chemin"); return; }
int f = open(args[0], O_RDONLY | O_NONBLOCK);
struct stat st;
if (nargs < 1 || stat(args[0], &st) || !S_ISREG(st.st_mode)) { reply_str(fd, 0, "%s : %s", nargs ? args[0] : "?", strerror(errno)); return; }
int f = open(args[0], O_RDONLY);
char *data = malloc(st.st_size ? st.st_size : 1);
if (f < 0 || read_full(f, data, st.st_size)) { reply_str(fd, 0, "lecture de %s : %s", args[0], strerror(errno)); free(data); if (f >= 0) close(f); return; }
if (f < 0 || fstat(f, &st) || !S_ISREG(st.st_mode)) {
reply_str(fd, 0, "lecture de %s impossible", args[0]);
if (f >= 0) close(f);
return;
}
char hdr[128], data[65536];
snprintf(hdr, sizeof hdr, "OK %llu\n", (unsigned long long)st.st_size);
if (send_all(fd, hdr, strlen(hdr))) { close(f); return; }
off_t remaining = st.st_size;
while (remaining) {
size_t chunk = remaining < (off_t)sizeof data ? (size_t)remaining : sizeof data;
/* Après l'en-tête, une erreur impose de fermer la connexion :
le client détecte la réponse incomplète et conserve son ancien fichier. */
if (read_full(f, data, chunk) || send_all(fd, data, chunk)) {
close(f); shutdown(fd, SHUT_RDWR); return;
}
remaining -= chunk;
}
close(f);
reply(fd, 1, NULL, data, st.st_size);
free(data);
}
static void cmd_ls(int fd, char **args, int nargs)
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>TouchProbe</string>
<key>CFBundleIdentifier</key>
<string>com.ioslegacy.touchprobe</string>
<key>CFBundleName</key>
<string>TouchProbe</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>2.0</string>
</dict>
</plist>
+10
View File
@@ -0,0 +1,10 @@
# Sonde de calibration tactile pour iPhone OS 2.
SDK := /opt/theos/sdks/iPhoneOS2.2.1.sdk
CC := /opt/cctools/bin/arm-apple-darwin9-gcc
all: TouchProbe.app/TouchProbe
TouchProbe.app/TouchProbe: main.m Info.plist
mkdir -p TouchProbe.app
$(CC) -isysroot $(SDK) -arch armv6 -miphoneos-version-min=2.0 -O2 -o $@ main.m -framework UIKit -framework Foundation
cp Info.plist TouchProbe.app/
ldid -S $@
.PHONY: all
+30
View File
@@ -0,0 +1,30 @@
#import <UIKit/UIKit.h>
@interface ProbeView : UIView
@end
@implementation ProbeView
- (void)record:(NSSet *)touches phase:(NSString *)phase {
CGPoint p=[[touches anyObject] locationInView:self];
UIView *marker=[self viewWithTag:99];
if (!marker) {
marker=[[[UIView alloc] initWithFrame:CGRectMake(0,0,0,0)] autorelease];
marker.tag=99; marker.backgroundColor=[UIColor blueColor]; marker.userInteractionEnabled=NO;
[self addSubview:marker];
}
marker.frame=CGRectMake(p.x-2,p.y-2,5,5);
NSLog(@"TOUCH %@ %.1f %.1f",phase,p.x,p.y);
}
- (void)touchesBegan:(NSSet *)t withEvent:(UIEvent *)e { [self record:t phase:@"begin"]; }
- (void)touchesMoved:(NSSet *)t withEvent:(UIEvent *)e { [self record:t phase:@"move"]; }
- (void)touchesEnded:(NSSet *)t withEvent:(UIEvent *)e { [self record:t phase:@"end"]; }
@end
@interface ProbeApp : NSObject <UIApplicationDelegate> { UIWindow *window; }
@end
@implementation ProbeApp
- (void)applicationDidFinishLaunching:(UIApplication *)app {
window=[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
ProbeView *v=[[ProbeView alloc] initWithFrame:CGRectMake(0,20,320,460)];
v.backgroundColor=[UIColor whiteColor];[window addSubview:v];[v release];[window makeKeyAndVisible];
NSLog(@"TouchProbe ready window=%@ view=%@",NSStringFromCGRect(window.frame),NSStringFromCGRect(v.frame));
}
@end
int main(int argc,char **argv) { NSAutoreleasePool *p=[[NSAutoreleasePool alloc] init];int rc=UIApplicationMain(argc,argv,nil,@"ProbeApp");[p release];return rc; }
+55 -21
View File
@@ -20,7 +20,7 @@
Code de sortie : 0 si OK, 1 si erreur, code de sortie du programme pour exec.
"""
import os, socket, subprocess, sys, time
import os, socket, subprocess, sys, tempfile, time
HOST = os.environ.get("IPOD_AGENT_HOST", "127.0.0.1")
PORT = int(os.environ.get("IPOD_AGENT_PORT", "7788"))
@@ -31,19 +31,41 @@ class Agent:
self.s = socket.create_connection((HOST, PORT), timeout=timeout)
self.f = self.s.makefile("rb")
def call(self, *fields, data=None):
def call(self, *fields, data=None, sink=None):
line = "\t".join(str(x) for x in fields) + "\n"
self.s.sendall(line.encode())
if data is not None:
self.s.sendall(data)
hdr = self.f.readline().decode().rstrip("\n")
if hasattr(data, "read"):
remaining = int(fields[-1])
while remaining:
chunk = data.read(min(65536, remaining))
if not chunk:
raise ConnectionError("fichier local tronqué pendant l'upload")
self.s.sendall(chunk)
remaining -= len(chunk)
else:
self.s.sendall(data)
hdr = self.f.readline(4096).decode().rstrip("\n")
if not hdr:
raise ConnectionError("connexion fermée par l'agent")
parts = hdr.split(" ", 2)
if len(parts) < 2:
raise ConnectionError("en-tête invalide")
n = int(parts[1])
body = self.f.read(n)
if len(body) != n:
raise ConnectionError(f"réponse tronquée : {len(body)} octets reçus sur {n}")
if n < 0 or parts[0] not in ("OK", "ERR"):
raise ConnectionError("en-tête invalide")
body = bytearray()
received = 0
while received < n:
chunk = self.f.read(min(65536, n - received))
if not chunk:
raise ConnectionError(f"réponse tronquée : {received} octets reçus sur {n}")
received += len(chunk)
if sink is not None and parts[0] == "OK":
sink.write(chunk)
else:
body.extend(chunk)
body = bytes(body)
return parts[0] == "OK", body, parts[2] if len(parts) > 2 else None
@@ -88,8 +110,7 @@ def unlock(a):
if not ok:
sys.exit(body.decode())
time.sleep(2) # laisser le pilote réactiver l'écran et le tactile
# La surface tactile émulée est légèrement plus petite que l'écran :
# le geste doit atteindre le bord droit pour terminer le déverrouillage.
# Terminer le geste au bord droit du curseur de déverrouillage.
subprocess.run([qmp, "-s", sock, "swipe", "60", "450", "319", "450"], check=True)
time.sleep(2)
state = screen()
@@ -175,20 +196,30 @@ def main():
unlock(a)
return
if cmd == "get":
ok, body, _ = a.call("get", rest[0])
if not ok:
sys.exit(body.decode())
if len(rest) > 1:
with open(rest[1], "wb") as fh:
fh.write(body)
else:
sys.stdout.buffer.write(body)
tmp = None
try:
if len(rest) > 1:
destination = os.path.abspath(rest[1])
fd, tmp = tempfile.mkstemp(prefix=".ipod-get-", dir=os.path.dirname(destination))
with os.fdopen(fd, "wb") as fh:
ok, body, _ = a.call("get", rest[0], sink=fh)
fh.flush()
os.fsync(fh.fileno())
if ok:
os.replace(tmp, destination)
tmp = None
else:
ok, body, _ = a.call("get", rest[0], sink=sys.stdout.buffer)
if not ok:
sys.exit(body.decode())
finally:
if tmp is not None:
os.unlink(tmp)
return
if cmd == "put":
with open(rest[0], "rb") as fh:
data = fh.read()
mode = rest[2] if len(rest) > 2 else ("755" if os.access(rest[0], os.X_OK) else "644")
ok, body, _ = a.call("put", rest[1], mode, len(data), data=data)
with open(rest[0], "rb") as fh:
ok, body, _ = a.call("put", rest[1], mode, os.fstat(fh.fileno()).st_size, data=fh)
elif cmd == "push-app":
push_app(a, rest[0])
return
@@ -207,4 +238,7 @@ def main():
if __name__ == "__main__":
main()
try:
main()
except (OSError, ValueError, ConnectionError) as e:
sys.exit(f"Erreur de transfert ou de protocole : {e}")
+1 -1
View File
@@ -20,7 +20,7 @@ Usage : ipod-emu start [-f] [-W] [-w FW] démarre (headless, VNC $VNC, QMP $RU
-W attend que SpringBoard soit prêt à recevoir des touches
-w FW firmware à utiliser (défaut : $FW ; dispo : $(ls "$EMU_HOME/images" 2>/dev/null | tr '\n' ' '))
ipod-emu wait [SECONDES] attend SpringBoard (défaut 300 s) — n'envoyez PAS de touch avant :
le contrôleur tactile plante s'il est touché pendant son boot
le pilote tactile doit avoir terminé son démarrage
ipod-emu stop | status | log | console
Pilotage (écran, touch, boutons) : ipod-qmp --help
USAGE
+147
View File
@@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Teste une application dans une NAND indépendante et conserve les diagnostics."""
import argparse
import json
import os
from pathlib import Path
import plistlib
import shutil
import socket
import subprocess
import tempfile
import time
import zipfile
def bundle_id(package):
if package.is_dir() and package.suffix == '.app':
with (package / 'Info.plist').open('rb') as f:
return plistlib.load(f)['CFBundleIdentifier']
if package.suffix == '.ipa':
with zipfile.ZipFile(package) as z:
plists = [n for n in z.namelist() if n.startswith('Payload/') and
n.endswith('.app/Info.plist') and n.count('/') == 2]
if len(plists) != 1:
raise ValueError('IPA ambiguë : préciser --bundle-id')
return plistlib.loads(z.read(plists[0]))['CFBundleIdentifier']
raise ValueError('Préciser --bundle-id pour un paquet .deb ou un overlay')
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('package', type=Path, help='.app, .ipa, .deb ou overlay')
p.add_argument('--bundle-id')
p.add_argument('--base', default=os.environ.get('IPOD_TEST_BASE', '5H11a-agent-v12'))
p.add_argument('--output', type=Path, help='nouveau dossier de résultats (doit être absent)')
p.add_argument('--settle', type=float, default=3, help='attente après apparition au premier plan')
args = p.parse_args()
package = args.package.resolve()
tools = Path(__file__).resolve().parent
default_home = tools.parent if (tools.parent / 'images').is_dir() else Path.home() / 'ipod-emu'
home = Path(os.environ.get('IPOD_EMU_HOME', default_home)).resolve()
try:
if not package.exists():
raise ValueError('Application introuvable')
identifier = args.bundle_id or bundle_id(package)
if not isinstance(identifier, str) or not identifier or any(c in identifier for c in '\t\r\n'):
raise ValueError('Bundle-id invalide')
if Path(args.base).name != args.base or args.base in ('.', '..'):
raise ValueError('Nom de firmware invalide')
if args.settle < 0:
raise ValueError('--settle doit être positif')
base = home / 'images' / args.base
if not (base / 'rootfs.img').is_file():
raise ValueError(f'Image de base introuvable : {base}')
# Les ports sont partagés par les sessions : ne jamais arrêter une VM tierce.
vnc = os.environ.get('IPOD_VNC', '127.0.0.1:0')
host, display = vnc.rsplit(':', 1)
for address, port in [(os.environ.get('IPOD_AGENT_HOST', '127.0.0.1'),
int(os.environ.get('IPOD_AGENT_PORT', '7788'))),
(host or '127.0.0.1', 5900 + int(display))]:
try:
connection = socket.create_connection((address, port), timeout=1)
except ConnectionRefusedError:
continue
else:
connection.close()
raise ValueError(f'Port {port} occupé : arrêter votre session avant ce test')
if args.output:
result = args.output.resolve()
result.mkdir(parents=True, exist_ok=False)
else:
parent = home / 'work' / 'tests'
parent.mkdir(parents=True, exist_ok=True)
result = Path(tempfile.mkdtemp(prefix=time.strftime('%Y%m%d-%H%M%S-'), dir=parent))
except (OSError, ValueError, KeyError, plistlib.InvalidFileException, zipfile.BadZipFile) as e:
p.exit(1, f'{e}\n')
print(f'Diagnostics : {result}', flush=True)
session = result / 'session'
(session / 'images').mkdir(parents=True)
(session / 'images' / args.base).symlink_to(base, target_is_directory=True)
for dependency in ('xpwn', 'gen-v1', 'netd'):
(session / dependency).symlink_to(home / dependency, target_is_directory=True)
env = dict(os.environ, IPOD_EMU_HOME=str(session),
IPOD_QEMU=os.environ.get('IPOD_QEMU', str(home.parent / 'qemu-ios/build/qemu-system-arm')))
# Un socket de session explicite ne doit pas viser la VM de l'utilisateur.
env.pop('IPOD_QMP', None)
image_name = 'test-app' if args.base != 'test-app' else 'test-app-result'
started = False
report = {'package': str(package), 'bundle_id': identifier, 'base': args.base, 'ok': False}
with (result / 'workflow.log').open('w') as workflow:
def run(tool, *argv, capture=False, timeout=360):
r = subprocess.run([str(tools / tool), *map(str, argv)], env=env,
stdout=subprocess.PIPE if capture else workflow,
stderr=workflow, text=True, timeout=timeout)
if capture:
workflow.write(r.stdout)
workflow.flush()
if r.returncode:
raise RuntimeError(f'{tool} {argv[0] if argv else ""} : code {r.returncode}')
return r.stdout if capture else None
try:
run('ipod-mkimage', '-b', args.base, '-o', image_name, package, timeout=900)
started = True # nettoie également un démarrage partiel
run('ipod-emu', 'start', '-w', image_name, '-W')
run('ipod-agent', 'launch', identifier, '--log', '/private/var/mobile/ipod-test-app.log')
deadline = time.monotonic() + 30
while run('ipod-agent', 'front', capture=True, timeout=20).strip() != identifier:
if time.monotonic() >= deadline:
raise RuntimeError('Application absente du premier plan après 30 s')
time.sleep(1)
time.sleep(args.settle)
front = run('ipod-agent', 'front', capture=True, timeout=20).strip()
report['front'] = front
if front != identifier:
raise RuntimeError('Application quittée pendant la période de stabilisation')
report['ok'] = True
except (OSError, RuntimeError, subprocess.TimeoutExpired) as e:
report['error'] = str(e)
finally:
if started:
for tool, argv in [('ipod-agent', ['get', '/private/var/mobile/ipod-test-app.log', result / 'app.log']),
('ipod-qmp', ['screenshot', result / 'screen.png'])]:
try:
run(tool, *argv, timeout=30)
except (OSError, RuntimeError, subprocess.TimeoutExpired) as e:
report.setdefault('diagnostic_errors', []).append(str(e))
try:
run('ipod-agent', 'sync', timeout=15)
except (OSError, RuntimeError, subprocess.TimeoutExpired):
pass
try:
run('ipod-emu', 'stop', timeout=30)
except (OSError, RuntimeError, subprocess.TimeoutExpired) as e:
report['ok'] = False
report['stop_error'] = str(e)
if report.get('diagnostic_errors'):
report['ok'] = False
(result / 'result.json').write_text(json.dumps(report, indent=2, ensure_ascii=False) + '\n')
print('Test réussi' if report['ok'] else 'Test échoué : consulter workflow.log et result.json')
return 0 if report['ok'] else 1
if __name__ == '__main__':
raise SystemExit(main())
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""Compile et exécute les régressions tactiles avec ASan/UBSan sur un build QEMU existant."""
import argparse
import json
import os
from pathlib import Path
import shlex
import subprocess
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--build', type=Path)
a = p.parse_args()
root = Path(__file__).resolve().parents[3]
build = (a.build or root / 'build').resolve()
source = Path(__file__).with_name('test-multitouch.c')
commands = json.loads((build / 'compile_commands.json').read_text())
c = next(c for c in commands if c['file'].endswith('ipod_touch_multitouch.c'))
old_root = str(Path(c['directory']).parent)
args = shlex.split(c['command'].replace(old_root, str(root)))
obj = str(build / 'test-ipod-multitouch.o')
args[args.index('-c') + 1] = str(source)
args[args.index('-o') + 1] = obj
args[args.index('-MF') + 1] = obj + '.d'
args += ['-fsanitize=address,undefined', '-fno-omit-frame-pointer']
subprocess.run(args, cwd=build, check=True)
links = json.loads(subprocess.check_output(
['ninja', '-C', str(build), '-t', 'compdb', '-x', 'c_LINKER_RSP', 'c_LINKER'], text=True))
link = next(c for c in links if c.get('output') == 'qemu-system-arm')
args = shlex.split(link['command'].replace(old_root, str(root)))
args = [x for x in args if x not in (
'qemu-system-arm.p/system_main.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_multitouch.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_gpio.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_sysic.c.o',
'libqemu-arm-softmmu.fa.p/hw_arm_ipod_touch_spi.c.o')]
out = str(build / 'test-ipod-multitouch')
args[args.index('-o') + 1] = out
args += [obj, '-fsanitize=address,undefined']
subprocess.run(args, cwd=build, check=True)
# QEMU conserve volontairement ses registres de types et sa boucle globale à la sortie.
env = dict(os.environ, ASAN_OPTIONS='detect_leaks=0', UBSAN_OPTIONS='halt_on_error=1')
subprocess.run([out], env=env, check=True)
+200
View File
@@ -0,0 +1,200 @@
/* Régressions du protocole tactile : commandes invalides et trames concurrentes. */
#include "qemu/osdep.h"
#include "qemu/main-loop.h"
#include "sysemu/cpus.h"
#include "hw/arm/ipod_touch_multitouch.c"
#include "hw/arm/ipod_touch_gpio.c"
#include "hw/arm/ipod_touch_sysic.c"
#include "hw/arm/ipod_touch_spi.c"
static IPodTouchMultitouchState *new_sensor(void)
{
IPodTouchMultitouchState *s = IPOD_TOUCH_MULTITOUCH(
object_new(TYPE_IPOD_TOUCH_MULTITOUCH));
ipod_touch_multitouch_realize(SSI_PERIPHERAL(s), &error_abort);
return s;
}
static void destroy_sensor(IPodTouchMultitouchState *s)
{
finish_transfer(s);
timer_free(s->touch_timer);
timer_free(s->touch_end_timer);
g_free(s->next_frame);
g_free(s->read_frame);
object_unref(OBJECT(s));
}
static uint8_t transfer(IPodTouchMultitouchState *s, uint8_t byte)
{
return ipod_touch_multitouch_transfer(SSI_PERIPHERAL(s), byte);
}
static void test_unknown(void)
{
IPodTouchMultitouchState *s = new_sensor();
for (int i = 0; i < 10000; i++) {
g_assert_cmpuint(transfer(s, 0xab), ==, 0xff);
g_assert_cmpuint(s->cur_cmd, ==, 0);
g_assert_null(s->in_buffer);
}
uint8_t out[16];
out[0] = transfer(s, MT_CMD_GET_INTERFACE_VERSION);
for (int i = 1; i < 16; i++) out[i] = transfer(s, 0);
g_assert_cmpuint(out[2], ==, MT_INTERFACE_VERSION);
destroy_sensor(s);
}
static void test_snapshot(void)
{
IPodTouchMultitouchState *s = new_sensor();
MTFrame expected = *s->next_frame;
uint8_t out[sizeof(MTFrame)];
out[0] = transfer(s, MT_CMD_FRAME_READ);
for (int i = 1; i < 16; i++) out[i] = transfer(s, 0);
g_free(s->next_frame);
s->next_frame = get_frame(s, MT_EVENT_TOUCH_START, .4, .6,
100, 660, 580, 150);
g_assert_cmpuint(s->cur_cmd, ==, 0);
out[16] = transfer(s, 0xeb);
out[17] = transfer(s, 0);
out[18] = transfer(s, 1);
for (size_t i = 19; i < sizeof out; i++) out[i] = transfer(s, 0);
expected.frame_packet.unused1 = expected.frame_length.length1;
expected.frame_packet.checksum_pad = -(expected.frame_packet.cmd +
expected.frame_packet.unused1 + expected.frame_packet.length1 +
expected.frame_packet.length2);
g_assert_cmpmem(out, sizeof out, &expected, sizeof expected);
g_assert_cmpuint(s->next_frame->finger_data.event, ==, MT_EVENT_TOUCH_START);
destroy_sensor(s);
}
static void test_reports(void)
{
IPodTouchMultitouchState *s = new_sensor();
uint8_t out[16];
for (int cmd = MT_CMD_GET_REPORT_INFO; cmd <= MT_CMD_SHORT_CONTROL_READ; cmd += 3) {
out[0] = transfer(s, cmd);
out[1] = transfer(s, 0xff);
for (int i = 2; i < 16; i++) out[i] = transfer(s, 0);
g_assert_cmpuint(out[2], ==, 1);
unsigned sum = 0;
for (int i = 0; i < 14; i++) sum += out[i];
g_assert_cmpuint(lduw_le_p(out + 14), ==, sum);
}
transfer(s, MT_CMD_SHORT_CONTROL_READ);
transfer(s, MT_REPORT_SENSOR_DIMENSIONS);
for (int i = 2; i < 16; i++) out[i] = transfer(s, 0);
g_assert_cmpuint(ldl_le_p(out + 3), ==, 5000);
g_assert_cmpuint(ldl_le_p(out + 7), ==, 7500);
destroy_sensor(s);
}
static void test_spi_abort(void)
{
IPodTouchSPIState *spi = IPOD_TOUCH_SPI(object_new(TYPE_IPOD_TOUCH_SPI));
DEVICE(spi)->id = g_strdup("spi-test");
set_spi_base(4);
ipod_touch_spi_realize(DEVICE(spi), &error_abort);
ipod_touch_spi_write(spi, R_TXCNT, 2, 4);
ipod_touch_spi_write(spi, R_RXCNT, 2, 4);
ipod_touch_spi_write(spi, R_TXDATA, MT_CMD_FRAME_READ, 4);
ipod_touch_spi_write(spi, R_TXDATA, 2, 4);
ipod_touch_spi_write(spi, R_CTRL, R_CTRL_RUN, 4);
g_assert_cmpuint(spi->mt->in_buffer_ind, ==, 2);
ipod_touch_spi_write(spi, R_CTRL, R_CTRL_TX_RESET | R_CTRL_RX_RESET, 4);
g_assert_cmpuint(spi->mt->cur_cmd, ==, 0);
g_assert_null(spi->mt->in_buffer);
uint8_t response[16];
response[0] = transfer(spi->mt, MT_CMD_GET_INTERFACE_VERSION);
for (int i = 1; i < 16; i++) response[i] = transfer(spi->mt, 0);
g_assert_cmpuint(response[2], ==, MT_INTERFACE_VERSION);
timer_free(spi->mt->touch_timer);
timer_free(spi->mt->touch_end_timer);
g_free(spi->mt->next_frame);
fifo8_destroy(&spi->tx_fifo);
fifo8_destroy(&spi->rx_fifo);
object_unref(OBJECT(spi));
}
static void test_hbpp(void)
{
IPodTouchMultitouchState *s = new_sensor();
uint8_t bad[10] = {MT_CMD_HBPP_DATA_PACKET, 0, 0, 4};
for (int i = 0; i < 10; i++) transfer(s, bad[i]);
g_assert_cmpuint(s->cur_cmd, ==, 0);
uint8_t good[10] = {MT_CMD_HBPP_DATA_PACKET, 0, 0, 4, 0, 0, 0, 0, 0, 4};
for (int i = 0; i < 10; i++) transfer(s, good[i]);
for (int i = 1; i < 21; i++) transfer(s, 0);
g_assert_cmpuint(s->cur_cmd, ==, 0);
destroy_sensor(s);
}
static void test_velocity(void)
{
IPodTouchMultitouchState *s = new_sensor();
MTFrame *f = get_frame(s, MT_EVENT_TOUCH_MOVED, .4, .6, 1, 1, 1, 1);
g_free(f);
f = get_frame(s, MT_EVENT_TOUCH_MOVED, .4, .7, 1, 1, 1, 1);
g_assert_cmpint(f->finger_data.velX, ==, 0);
g_assert_cmpint(f->finger_data.velY, >, 0);
g_free(f);
f = get_frame(s, MT_EVENT_TOUCH_MOVED, .4, .7, 1, 1, 1, 1);
g_assert_cmpint(f->finger_data.velX, ==, 0);
g_assert_cmpint(f->finger_data.velY, ==, 0);
g_free(f);
destroy_sensor(s);
}
static void test_buttons(void)
{
IPodTouchGPIOState *s = IPOD_TOUCH_GPIO(object_new(TYPE_IPOD_TOUCH_GPIO));
g_assert_cmpuint(GPIO2PAD(GPIO_BUTTON_HOME), <, NUM_GPIO_PADS);
gpio_set_on(s->gpio_state, GPIO_BUTTON_HOME);
gpio_set_on(s->gpio_state, GPIO_BUTTON_POWER);
g_assert_true(gpio_is_on(s->gpio_state, GPIO_BUTTON_HOME));
g_assert_true(gpio_is_on(s->gpio_state, GPIO_BUTTON_POWER));
gpio_set_off(s->gpio_state, GPIO_BUTTON_HOME);
g_assert_true(gpio_is_off(s->gpio_state, GPIO_BUTTON_HOME));
g_assert_true(gpio_is_on(s->gpio_state, GPIO_BUTTON_POWER));
object_unref(OBJECT(s));
}
static void test_gpio_ranges(void)
{
IPodTouchSYSICState *s = IPOD_TOUCH_SYSIC(object_new(TYPE_IPOD_TOUCH_SYSIC));
uint32_t arrays[4][GPIO_NUMINTGROUPS];
s->gpio_int_level[0] = 0x1234;
memcpy(arrays[0], s->gpio_int_level, sizeof arrays[0]);
memcpy(arrays[1], s->gpio_int_status, sizeof arrays[1]);
memcpy(arrays[2], s->gpio_int_enabled, sizeof arrays[2]);
memcpy(arrays[3], s->gpio_int_type, sizeof arrays[3]);
/* Les quatre trous entre groupes ne doivent pas accéder au tableau suivant. */
for (int addr = GPIO_INTLEVEL; addr <= GPIO_INTTYPE; addr += 0x20) {
ipod_touch_sysic_write(s, addr + GPIO_NUMINTGROUPS * 4, 0xffffffff, 4);
g_assert_cmpuint(ipod_touch_sysic_read(s, addr + GPIO_NUMINTGROUPS * 4, 4), ==, 0);
}
g_assert_cmpmem(arrays[0], sizeof arrays[0], s->gpio_int_level, sizeof arrays[0]);
g_assert_cmpmem(arrays[1], sizeof arrays[1], s->gpio_int_status, sizeof arrays[1]);
g_assert_cmpmem(arrays[2], sizeof arrays[2], s->gpio_int_enabled, sizeof arrays[2]);
g_assert_cmpmem(arrays[3], sizeof arrays[3], s->gpio_int_type, sizeof arrays[3]);
object_unref(OBJECT(s));
}
int main(int argc, char **argv)
{
g_test_init(&argc, &argv, NULL);
module_call_init(MODULE_INIT_QOM);
qemu_init_main_loop(&error_abort);
qemu_init_cpu_loop();
qemu_mutex_lock_iothread();
g_test_add_func("/ipod-multitouch/unknown", test_unknown);
g_test_add_func("/ipod-multitouch/snapshot", test_snapshot);
g_test_add_func("/ipod-multitouch/hbpp", test_hbpp);
g_test_add_func("/ipod-multitouch/spi-abort", test_spi_abort);
g_test_add_func("/ipod-multitouch/reports", test_reports);
g_test_add_func("/ipod-multitouch/velocity", test_velocity);
g_test_add_func("/ipod-input/buttons", test_buttons);
g_test_add_func("/ipod-input/gpio-ranges", test_gpio_ranges);
return g_test_run();
}
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
"""Teste les transferts réels de l'agent sur socket local, sans firmware Apple."""
import importlib.util
from importlib.machinery import SourceFileLoader
import io
import os
from pathlib import Path
import socket
import subprocess
import tempfile
import threading
import unittest
ROOT = Path(__file__).resolve().parents[1]
loader = SourceFileLoader('ipod_agent', str(ROOT / 'bin/ipod-agent'))
spec = importlib.util.spec_from_loader(loader.name, loader)
client = importlib.util.module_from_spec(spec)
loader.exec_module(client)
class Transfers(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.work = tempfile.TemporaryDirectory()
cls.binary = Path(cls.work.name) / 'transfer-agent'
source = (ROOT / 'agent/emuagent.c').read_text()
# Compile les fonctions de production, sans dépendances privées d'iOS.
functions = source[source.index('static int send_all('):source.index('/* tampon dynamique */')]
functions += source[source.index('static int read_full('):source.index('static void cmd_ls(')]
harness = '''#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <unistd.h>
static void mkdirs(const char *path) { (void)path; }
''' + functions + '''
int main(int argc, char **argv) {
if (argc < 4) return 2;
int fd = atoi(argv[1]);
if (!strcmp(argv[2], "get")) cmd_get(fd, argv+3, argc-3);
else cmd_put(fd, argv+3, argc-3);
char ignored[256]; read(fd, ignored, sizeof ignored);
close(fd); return 0;
}
'''
c = Path(cls.work.name) / 'transfer-agent.c'
c.write_text(harness)
subprocess.run(['cc', '-Wall', '-Wextra', '-Werror', '-fsanitize=undefined',
'-o', str(cls.binary), str(c)], check=True)
@classmethod
def tearDownClass(cls):
cls.work.cleanup()
def agent(self, command, *args):
a, b = socket.socketpair()
process = subprocess.Popen([str(self.binary), str(b.fileno()), command, *map(str, args)],
pass_fds=(b.fileno(),), env=dict(os.environ, UBSAN_OPTIONS='halt_on_error=1'))
b.close()
agent = client.Agent.__new__(client.Agent)
agent.s, agent.f = a, a.makefile('rb')
self.addCleanup(agent.f.close)
self.addCleanup(a.close)
self.addCleanup(lambda: self.assertEqual(process.wait(timeout=5), 0))
return agent
def test_large_round_trip(self):
data = os.urandom(8 * 1024 * 1024 + 37)
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'file'
a = self.agent('put', path, '640', len(data))
# Le harness reçoit les arguments hors bande, puis le contenu du protocole.
a.s.sendall(data)
ok, _, _ = a.call() # ligne vide ignorée par ce harness
self.assertTrue(ok)
self.assertEqual(path.read_bytes(), data)
self.assertEqual(path.stat().st_mode & 0o777, 0o640)
a = self.agent('get', path)
with (Path(d) / 'download').open('wb') as out:
ok, _, _ = a.call(sink=out)
self.assertTrue(ok)
self.assertEqual((Path(d) / 'download').read_bytes(), data)
def test_interrupted_put_preserves_existing_file(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'file'
path.write_bytes(b'ancienne version')
a = self.agent('put', path, '644', 100000)
a.s.sendall(b'court')
a.s.shutdown(socket.SHUT_WR)
response = a.f.read()
self.assertTrue(response.startswith(b'ERR '))
self.assertEqual(path.read_bytes(), b'ancienne version')
self.assertEqual(list(Path(d).iterdir()), [path])
def test_empty_file(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'empty'
a = self.agent('put', path, '644', 0)
self.assertTrue(a.call()[0])
self.assertEqual(path.read_bytes(), b'')
self.assertEqual(self.agent('get', path).call()[1], b'')
def test_download_failure_preserves_destination(self):
listener = socket.socket()
listener.bind(('127.0.0.1', 0)); listener.listen()
def server():
connection, _ = listener.accept()
connection.recv(1024)
connection.sendall(b'OK 100\npartiel')
connection.close()
thread = threading.Thread(target=server); thread.start()
try:
with tempfile.TemporaryDirectory() as d:
destination = Path(d) / 'download'
destination.write_bytes(b'original')
r = subprocess.run([str(ROOT / 'bin/ipod-agent'), 'get', '/file', str(destination)],
env=dict(os.environ, IPOD_AGENT_PORT=str(listener.getsockname()[1]),
IPOD_AGENT_HOST='127.0.0.1'), capture_output=True)
self.assertEqual(r.returncode, 1)
self.assertIn('tronquée', r.stderr.decode())
self.assertEqual(destination.read_bytes(), b'original')
self.assertEqual(list(Path(d).iterdir()), [destination])
finally:
thread.join(); listener.close()
def test_upload_respects_announced_size(self):
a, b = socket.socketpair()
agent = client.Agent.__new__(client.Agent)
agent.s, agent.f = a, a.makefile('rb')
received = []
def server():
f = b.makefile('rb')
received.append(f.readline())
received.append(f.read(3))
b.sendall(b'OK 0\n')
f.close(); b.close()
thread = threading.Thread(target=server); thread.start()
try:
data = io.BytesIO(b'abcEXTRA')
self.assertTrue(agent.call('put', '/file', '644', 3, data=data)[0])
self.assertEqual(data.tell(), 3)
self.assertEqual(received, [b'put\t/file\t644\t3\n', b'abc'])
finally:
agent.f.close(); a.close(); thread.join()
def test_get_rejects_fifo_without_blocking(self):
with tempfile.TemporaryDirectory() as d:
path = Path(d) / 'fifo'
os.mkfifo(path)
self.assertFalse(self.agent('get', path).call()[0])
def test_fragmented_and_truncated_reply(self):
for truncated in (False, True):
a, b = socket.socketpair()
agent = client.Agent.__new__(client.Agent)
agent.s, agent.f = a, a.makefile('rb')
def server():
b.recv(100)
for part in (b'OK ', b'10\n', b'123', b'4567', b'890' if not truncated else b''):
b.sendall(part)
b.close()
thread = threading.Thread(target=server)
thread.start()
try:
if truncated:
with self.assertRaisesRegex(ConnectionError, 'tronquée'):
agent.call('get', '/test')
else:
self.assertEqual(agent.call('get', '/test')[1], b'1234567890')
finally:
agent.f.close(); a.close(); thread.join()
if __name__ == '__main__':
unittest.main()
-2
View File
@@ -159,8 +159,6 @@ static void ipod_touch_lcd_mouse_event(void *opaque, int x, int y, int z, int bu
float fy = 1 - y / pow(2, 15);
IPodTouchLCDState *lcd = (IPodTouchLCDState *) opaque;
lcd->mt->prev_touch_x = lcd->mt->touch_x;
lcd->mt->prev_touch_y = lcd->mt->touch_y;
lcd->mt->touch_x = fx;
lcd->mt->touch_y = fy;
+107 -32
View File
@@ -1,4 +1,6 @@
#include "hw/arm/ipod_touch_multitouch.h"
#include "qemu/log.h"
#include "qemu/bswap.h"
static void prepare_interface_version_response(IPodTouchMultitouchState *s) {
memset(s->out_buffer + 1, 0, 15);
@@ -62,7 +64,9 @@ static void prepare_report_info_response(IPodTouchMultitouchState *s, uint8_t re
report_length = MT_REPORT_SENSOR_DIMENSIONS_SIZE;
}
else {
hw_error("Unknown report ID 0x%02x\n", report_id);
qemu_log_mask(LOG_GUEST_ERROR, "%s: rapport inconnu 0x%02x\n",
__func__, report_id);
s->out_buffer[2] = 1;
}
s->out_buffer[3] = (report_length & 0xFF);
@@ -98,12 +102,13 @@ static void prepare_short_control_response(IPodTouchMultitouchState *s, uint8_t
s->out_buffer[3] = MT_SENSOR_REGION_PARAM;
}
else if(report_id == MT_REPORT_SENSOR_DIMENSIONS) {
uint32_t *ob_int32 = (uint32_t *)&s->out_buffer[3];
ob_int32[0] = MT_SENSOR_SURFACE_WIDTH;
ob_int32[1] = MT_SENSOR_SURFACE_HEIGHT;
stl_le_p(&s->out_buffer[3], MT_SENSOR_SURFACE_WIDTH);
stl_le_p(&s->out_buffer[7], MT_SENSOR_SURFACE_HEIGHT);
}
else {
hw_error("Unknown report ID 0x%02x\n", report_id);
qemu_log_mask(LOG_GUEST_ERROR, "%s: rapport inconnu 0x%02x\n",
__func__, report_id);
s->out_buffer[2] = 1;
}
// compute and set the checksum
@@ -116,6 +121,19 @@ static void prepare_short_control_response(IPodTouchMultitouchState *s, uint8_t
s->out_buffer[15] = (checksum >> 8) & 0xFF;
}
static MTFrame *get_frame(IPodTouchMultitouchState *s, uint8_t event,
float x, float y, uint16_t radius1, uint16_t radius2,
uint16_t radius3, uint16_t contactDensity);
static void finish_transfer(IPodTouchMultitouchState *s)
{
g_clear_pointer(&s->in_buffer, g_free);
g_clear_pointer(&s->out_buffer, g_free);
s->cur_cmd = 0;
s->buf_size = s->buf_ind = s->in_buffer_ind = 0;
s->in_buffer_capacity = 0;
}
static uint32_t ipod_touch_multitouch_transfer(SSIPeripheral *dev, uint32_t value)
{
IPodTouchMultitouchState *s = IPOD_TOUCH_MULTITOUCH(dev);
@@ -126,10 +144,11 @@ static uint32_t ipod_touch_multitouch_transfer(SSIPeripheral *dev, uint32_t valu
//printf("Starting command 0x%02x\n", value);
// we're currently not in a command - start a new command
s->cur_cmd = value;
s->out_buffer = malloc(0x100);
s->out_buffer = g_malloc0(0x100);
s->out_buffer[0] = value; // the response header
s->buf_ind = 0;
s->in_buffer = malloc(0x100);
s->in_buffer = g_malloc0(0x100);
s->in_buffer_capacity = 0x100;
s->in_buffer_ind = 0;
if(value == 0x18) { // filler packet??
@@ -193,20 +212,56 @@ static uint32_t ipod_touch_multitouch_transfer(SSIPeripheral *dev, uint32_t valu
else if(value == MT_CMD_SHORT_CONTROL_READ) {
s->buf_size = 16;
}
else if(value == MT_CMD_FRAME_READ) {
s->buf_size = sizeof(MTFrame);
free(s->out_buffer);
s->out_buffer = (uint8_t *) s->next_frame;
else if(value == MT_CMD_FRAME_READ || value == MT_CMD_FRAME_READ_ALT) {
s->buf_size = sizeof(MTFrameLengthPacket);
g_free(s->out_buffer);
/* La trame doit rester stable entre sa longueur et ses données,
même si un événement hôte arrive pendant les lectures SPI. */
s->out_buffer = g_memdup2(s->read_frame ? s->read_frame : s->next_frame,
sizeof(MTFrame));
}
else {
printf("%s Unknown command 0x%02x!\n", __func__, value);
qemu_log_mask(LOG_GUEST_ERROR, "%s: commande inconnue 0x%02x\n",
__func__, value);
finish_transfer(s);
return 0xff;
}
}
if (s->buf_ind >= s->buf_size || s->in_buffer_ind >= s->in_buffer_capacity) {
finish_transfer(s);
return 0xff;
}
s->in_buffer[s->in_buffer_ind] = value;
s->in_buffer_ind++;
if(s->cur_cmd == MT_CMD_HBPP_DATA_PACKET && s->in_buffer_ind == 10) {
if ((s->cur_cmd == MT_CMD_FRAME_READ || s->cur_cmd == MT_CMD_FRAME_READ_ALT) &&
s->in_buffer_ind == 3) {
/* Le troisième octet distingue longueur (0) et données (1).
Chaque requête est une transaction complète : une lecture de
longueur abandonnée pendant la veille ne décale pas la suivante. */
if (value == 1) {
MTFrame *frame = (MTFrame *)s->out_buffer;
/* Le deuxième octet est déjà émis avant de connaître le sélecteur.
Il est ignoré dans les données, mais participe au checksum. */
uint8_t second = s->out_buffer[1];
frame->frame_packet.unused1 = second;
frame->frame_packet.checksum_pad = -(frame->frame_packet.cmd +
second + frame->frame_packet.length1 + frame->frame_packet.length2);
memmove(s->out_buffer, &frame->frame_packet,
sizeof(MTFrame) - sizeof(MTFrameLengthPacket));
s->buf_size = sizeof(MTFrame) - sizeof(MTFrameLengthPacket);
g_clear_pointer(&s->read_frame, g_free);
} else if (value == 0) {
g_free(s->read_frame);
s->read_frame = g_memdup2(s->next_frame, sizeof(MTFrame));
memcpy(s->out_buffer, s->read_frame, sizeof(MTFrame));
} else {
finish_transfer(s);
return 0xff;
}
}
else if(s->cur_cmd == MT_CMD_HBPP_DATA_PACKET && s->in_buffer_ind == 10) {
// verify the header checksum
uint32_t checksum = 0;
for(int i = 2; i < 8; i++) {
@@ -214,17 +269,22 @@ static uint32_t ipod_touch_multitouch_transfer(SSIPeripheral *dev, uint32_t valu
}
if(checksum != (s->in_buffer[8] << 8 | s->in_buffer[9])) {
hw_error("HBPP data header checksum doesn't match!");
qemu_log_mask(LOG_GUEST_ERROR, "%s: somme de contrôle HBPP invalide\n",
__func__);
finish_transfer(s);
return 0xff;
}
uint32_t data_len = (s->in_buffer[2] << 10) | (s->in_buffer[3] << 2) + 5;
// extend the lengths of the in/out buffers
free(s->in_buffer);
s->in_buffer = malloc(data_len + 0x10);
free(s->out_buffer);
s->out_buffer = malloc(data_len);
memset(s->out_buffer, 0, data_len);
uint32_t data_len = ((s->in_buffer[2] << 10) | (s->in_buffer[3] << 2)) + 5;
if (data_len < s->in_buffer_ind) {
finish_transfer(s);
return 0xff;
}
/* Conserver l'en-tête reçu et dimensionner aussi les petites trames. */
s->in_buffer_capacity = MAX(data_len + 10, 0x100);
s->in_buffer = g_realloc(s->in_buffer, s->in_buffer_capacity);
g_free(s->out_buffer);
s->out_buffer = g_malloc0(data_len);
s->buf_size = data_len;
s->buf_ind = 0;
}
@@ -255,17 +315,14 @@ static uint32_t ipod_touch_multitouch_transfer(SSIPeripheral *dev, uint32_t valu
}
// we're done with the command
s->cur_cmd = 0;
s->buf_size = 0;
//free(s->out_buffer);
//free(s->in_buffer);
finish_transfer(s);
}
return ret_val;
}
static MTFrame *get_frame(IPodTouchMultitouchState *s, uint8_t event, float x, float y, uint16_t radius1, uint16_t radius2, uint16_t radius3, uint16_t contactDensity) {
MTFrame *frame = calloc(sizeof(MTFrame), sizeof(uint8_t *));
MTFrame *frame = g_new0(MTFrame, 1);
uint16_t data_len = sizeof(MTFrameHeader) + sizeof(FingerData) + 2;
@@ -310,12 +367,17 @@ static MTFrame *get_frame(IPodTouchMultitouchState *s, uint8_t event, float x, f
// compute the velocity
int diff_x = (int)((x - s->prev_touch_x) * MT_INTERNAL_SENSOR_SURFACE_WIDTH);
int diff_y = (int)((x - s->prev_touch_y) * MT_INTERNAL_SENSOR_SURFACE_HEIGHT);
frame->finger_data.velX = diff_x / (elapsed_ns + 1 - s->last_frame_timestamp) * 1000;
frame->finger_data.velY = diff_y / (elapsed_ns + 1 - s->last_frame_timestamp) * 1000;
int diff_y = (int)((y - s->prev_touch_y) * MT_INTERNAL_SENSOR_SURFACE_HEIGHT);
uint64_t dt = MAX(elapsed_ns - s->last_frame_timestamp, 1);
frame->finger_data.velX = CLAMP((int64_t)diff_x * 1000 / (int64_t)dt,
INT16_MIN, INT16_MAX);
frame->finger_data.velY = CLAMP((int64_t)diff_y * 1000 / (int64_t)dt,
INT16_MIN, INT16_MAX);
s->prev_touch_x = x;
s->prev_touch_y = y;
frame->finger_data.x = (int)(x * MT_INTERNAL_SENSOR_SURFACE_WIDTH);
frame->finger_data.y = (int)(y * MT_INTERNAL_SENSOR_SURFACE_HEIGHT);
frame->finger_data.x = (int)(x * MT_INTERNAL_SENSOR_SURFACE_WIDTH) + MT_INTERNAL_SENSOR_ORIGIN_X;
frame->finger_data.y = (int)(y * MT_INTERNAL_SENSOR_SURFACE_HEIGHT) + MT_INTERNAL_SENSOR_ORIGIN_Y;
frame->finger_data.radius1 = radius1;
frame->finger_data.radius2 = radius2;
frame->finger_data.radius3 = radius3;
@@ -344,6 +406,7 @@ static void ipod_touch_multitouch_inform_frame_ready(IPodTouchMultitouchState *s
void ipod_touch_multitouch_on_touch(IPodTouchMultitouchState *s) {
s->touch_down = true;
g_free(s->next_frame);
s->next_frame = get_frame(s, MT_EVENT_TOUCH_START, s->touch_x, s->touch_y, 100, 660, 580, 150);
ipod_touch_multitouch_inform_frame_ready(s);
@@ -351,6 +414,7 @@ void ipod_touch_multitouch_on_touch(IPodTouchMultitouchState *s) {
}
void ipod_touch_multitouch_on_release(IPodTouchMultitouchState *s) {
g_free(s->next_frame);
s->next_frame = get_frame(s, MT_EVENT_TOUCH_ENDED, s->touch_x, s->touch_y, 0, 0, 0, 0);
s->touch_down = false;
ipod_touch_multitouch_inform_frame_ready(s);
@@ -363,6 +427,7 @@ static void touch_timer_tick(void *opaque)
{
IPodTouchMultitouchState *s = (IPodTouchMultitouchState *)opaque;
g_free(s->next_frame);
s->next_frame = get_frame(s, MT_EVENT_TOUCH_MOVED, s->touch_x, s->touch_y, 100, 660, 580, 150);
ipod_touch_multitouch_inform_frame_ready(s);
@@ -375,14 +440,22 @@ static void touch_timer_tick(void *opaque)
static void touch_end_timer_tick(void *opaque)
{
IPodTouchMultitouchState *s = (IPodTouchMultitouchState *)opaque;
g_free(s->next_frame);
s->next_frame = get_frame(s, MT_EVENT_TOUCH_FULL_END, s->touch_x, s->touch_y, 0, 0, 0, 0);
s->touch_down = false;
ipod_touch_multitouch_inform_frame_ready(s);
}
static void multitouch_cs(void *opaque, int pin, int level)
{
IPodTouchMultitouchState *s = opaque;
if (level) finish_transfer(s);
}
static void ipod_touch_multitouch_realize(SSIPeripheral *d, Error **errp)
{
IPodTouchMultitouchState *s = IPOD_TOUCH_MULTITOUCH(d);
qdev_init_gpio_in_named(DEVICE(d), multitouch_cs, SSI_GPIO_CS, 1);
memset(s->hbpp_atn_ack_response, 0, 2);
s->touch_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, touch_timer_tick, s);
s->touch_end_timer = timer_new_ns(QEMU_CLOCK_VIRTUAL, touch_end_timer_tick, s);
@@ -390,6 +463,8 @@ static void ipod_touch_multitouch_realize(SSIPeripheral *d, Error **errp)
s->prev_touch_x = 0;
s->prev_touch_y = 0;
s->last_frame_timestamp = 0;
/* Une lecture sans toucher doit renvoyer une trame de fin valide. */
s->next_frame = get_frame(s, MT_EVENT_TOUCH_FULL_END, 0, 0, 0, 0, 0, 0);
}
static void ipod_touch_multitouch_class_init(ObjectClass *klass, void *data)
+10 -2
View File
@@ -64,8 +64,11 @@ static void apple_spi_update_cs(IPodTouchSPIState *s)
BusState *b = BUS(s->spi);
BusChild *kid = QTAILQ_FIRST(&b->children);
if (kid) {
// TODO GPIO not properly setup yet
//qemu_set_irq(qdev_get_gpio_in_named(kid->child, SSI_GPIO_CS, 0), (REG(s, R_PIN) & R_PIN_CS) != 0);
if (object_dynamic_cast(OBJECT(kid->child), TYPE_IPOD_TOUCH_MULTITOUCH)) {
qemu_set_irq(qdev_get_gpio_in_named(kid->child, SSI_GPIO_CS, 0),
(REG(s, R_PIN) & R_PIN_CS) != 0);
}
/* La sélection de la NOR conserve son comportement historique. */
}
}
@@ -197,6 +200,11 @@ static void ipod_touch_spi_write(void *opaque, hwaddr addr, uint64_t data, unsig
case R_CTRL:
if (r & R_CTRL_TX_RESET) {
fifo8_reset(&s->tx_fifo);
/* Le pilote interrompt aussi les commandes au retour de veille.
Ne pas réutiliser les octets d'une transaction abandonnée. */
if (s->mt) {
qemu_set_irq(qdev_get_gpio_in_named(DEVICE(s->mt), SSI_GPIO_CS, 0), 1);
}
}
if (r & R_CTRL_RX_RESET) {
fifo8_reset(&s->rx_fifo);
+8 -8
View File
@@ -15,22 +15,22 @@ static uint64_t ipod_touch_sysic_read(void *opaque, hwaddr addr, unsigned size)
case 0x7a:
case 0x7c:
return 1;
case GPIO_INTLEVEL ... (GPIO_INTLEVEL + GPIO_NUMINTGROUPS * 4):
case GPIO_INTLEVEL ... (GPIO_INTLEVEL + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTLEVEL) / 4;
return s->gpio_int_level[group];
}
case GPIO_INTSTAT ... (GPIO_INTSTAT + GPIO_NUMINTGROUPS * 4):
case GPIO_INTSTAT ... (GPIO_INTSTAT + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTSTAT) / 4;
return s->gpio_int_status[group];
}
case GPIO_INTEN ... (GPIO_INTEN + GPIO_NUMINTGROUPS * 4):
case GPIO_INTEN ... (GPIO_INTEN + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTEN) / 4;
return s->gpio_int_enabled[group];
}
case GPIO_INTTYPE ... (GPIO_INTTYPE + GPIO_NUMINTGROUPS * 4):
case GPIO_INTTYPE ... (GPIO_INTTYPE + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTTYPE) / 4;
return s->gpio_int_type[group];
@@ -58,11 +58,11 @@ static void ipod_touch_sysic_write(void *opaque, hwaddr addr, uint64_t val, unsi
case POWER_OFFCTRL:
s->power_state = val;
break;
case GPIO_INTLEVEL ... (GPIO_INTLEVEL + GPIO_NUMINTGROUPS * 4):
case GPIO_INTLEVEL ... (GPIO_INTLEVEL + (GPIO_NUMINTGROUPS - 1) * 4):
{
break;
}
case GPIO_INTSTAT ... (GPIO_INTSTAT + GPIO_NUMINTGROUPS * 4):
case GPIO_INTSTAT ... (GPIO_INTSTAT + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTSTAT) / 4;
@@ -73,13 +73,13 @@ static void ipod_touch_sysic_write(void *opaque, hwaddr addr, uint64_t val, unsi
break;
}
case GPIO_INTEN ... (GPIO_INTEN + GPIO_NUMINTGROUPS * 4):
case GPIO_INTEN ... (GPIO_INTEN + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTEN) / 4;
s->gpio_int_enabled[group] = val;
break;
}
case GPIO_INTTYPE ... (GPIO_INTTYPE + GPIO_NUMINTGROUPS * 4):
case GPIO_INTTYPE ... (GPIO_INTTYPE + (GPIO_NUMINTGROUPS - 1) * 4):
{
uint8_t group = (addr - GPIO_INTTYPE) / 4;
s->gpio_int_type[group] = val;
+1 -1
View File
@@ -21,7 +21,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(IPodTouchGPIOState, IPOD_TOUCH_GPIO)
#define GPIO_BUTTON_VOLUP_IRQ 0x78
#define GPIO_BUTTON_VOLDOWN_IRQ 0x62
#define NUM_GPIO_PADS 0xC
#define NUM_GPIO_PADS 0xD /* Ports 0 à C inclus, dont Home et Power. */
#define NUM_GPIO_PINS 0x20
#define GPIO2PIN(gpio) ((gpio) & 7)
+10 -3
View File
@@ -25,9 +25,13 @@ OBJECT_DECLARE_SIMPLE_TYPE(IPodTouchMultitouchState, IPOD_TOUCH_MULTITOUCH)
#define MT_SENSOR_SURFACE_WIDTH 5000
#define MT_SENSOR_SURFACE_HEIGHT 7500
// internal surface width/height
#define MT_INTERNAL_SENSOR_SURFACE_WIDTH (9000 - MT_SENSOR_SURFACE_WIDTH) * 84 / 73
#define MT_INTERNAL_SENSOR_SURFACE_HEIGHT (13850 - MT_SENSOR_SURFACE_HEIGHT) * 84 / 73
/* Calibration mesurée sur les pixels 320x480 de 5H11a avec TouchProbe.
* La transformation du pilote nécessite une étendue et une origine propres
* au capteur ; les dimensions annoncées dans les rapports restent 5000x7500. */
#define MT_INTERNAL_SENSOR_SURFACE_WIDTH 4718
#define MT_INTERNAL_SENSOR_SURFACE_HEIGHT 7328
#define MT_INTERNAL_SENSOR_ORIGIN_X (-68)
#define MT_INTERNAL_SENSOR_ORIGIN_Y (-188)
// report IDs
#define MT_REPORT_UNKNOWN1 0x70
@@ -52,6 +56,7 @@ OBJECT_DECLARE_SIMPLE_TYPE(IPodTouchMultitouchState, IPOD_TOUCH_MULTITOUCH)
#define MT_CMD_SHORT_CONTROL_WRITE 0xE4
#define MT_CMD_SHORT_CONTROL_READ 0xE6
#define MT_CMD_FRAME_READ 0xEA
#define MT_CMD_FRAME_READ_ALT 0xEB
// frame types
#define MT_FRAME_TYPE_PATH 0x44
@@ -152,6 +157,8 @@ typedef struct IPodTouchMultitouchState {
float prev_touch_x;
float prev_touch_y;
uint64_t last_frame_timestamp;
uint32_t in_buffer_capacity;
MTFrame *read_frame;
} IPodTouchMultitouchState;
void ipod_touch_multitouch_on_touch(IPodTouchMultitouchState *s);