From 524e9bbb17b75dfbcee6b1e19b299792930995d2 Mon Sep 17 00:00:00 2001 From: Workbench IA Date: Wed, 30 Sep 2026 16:25:06 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Corriger=20les=20acc=C3=A8s=20GPIO=20et=20f?= =?UTF-8?q?iabiliser=20le=20tactile=20au=20r=C3=A9veil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contrib/ipod-emu/.gitignore | 2 + contrib/ipod-emu/apps/TouchProbe/Info.plist | 18 ++ contrib/ipod-emu/apps/TouchProbe/Makefile | 10 + contrib/ipod-emu/apps/TouchProbe/main.m | 30 +++ contrib/ipod-emu/tests/run-multitouch | 42 ++++ contrib/ipod-emu/tests/test-multitouch.c | 200 ++++++++++++++++++++ hw/arm/ipod_touch_lcd.c | 2 - hw/arm/ipod_touch_multitouch.c | 139 ++++++++++---- hw/arm/ipod_touch_spi.c | 12 +- hw/arm/ipod_touch_sysic.c | 16 +- include/hw/arm/ipod_touch_gpio.h | 2 +- include/hw/arm/ipod_touch_multitouch.h | 13 +- 12 files changed, 438 insertions(+), 48 deletions(-) create mode 100644 contrib/ipod-emu/apps/TouchProbe/Info.plist create mode 100644 contrib/ipod-emu/apps/TouchProbe/Makefile create mode 100644 contrib/ipod-emu/apps/TouchProbe/main.m create mode 100755 contrib/ipod-emu/tests/run-multitouch create mode 100644 contrib/ipod-emu/tests/test-multitouch.c diff --git a/contrib/ipod-emu/.gitignore b/contrib/ipod-emu/.gitignore index 431f3a74e0..1d7d5c6564 100644 --- a/contrib/ipod-emu/.gitignore +++ b/contrib/ipod-emu/.gitignore @@ -20,3 +20,5 @@ __pycache__/ /qemu-ios-generate-nor/ !/patches/ !/patches/*.patch + +/apps/TouchProbe/TouchProbe.app/ diff --git a/contrib/ipod-emu/apps/TouchProbe/Info.plist b/contrib/ipod-emu/apps/TouchProbe/Info.plist new file mode 100644 index 0000000000..2821543cdd --- /dev/null +++ b/contrib/ipod-emu/apps/TouchProbe/Info.plist @@ -0,0 +1,18 @@ + + + + + CFBundleExecutable + TouchProbe + CFBundleIdentifier + com.ioslegacy.touchprobe + CFBundleName + TouchProbe + CFBundlePackageType + APPL + CFBundleVersion + 1.0 + MinimumOSVersion + 2.0 + + diff --git a/contrib/ipod-emu/apps/TouchProbe/Makefile b/contrib/ipod-emu/apps/TouchProbe/Makefile new file mode 100644 index 0000000000..3cbc31865c --- /dev/null +++ b/contrib/ipod-emu/apps/TouchProbe/Makefile @@ -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 diff --git a/contrib/ipod-emu/apps/TouchProbe/main.m b/contrib/ipod-emu/apps/TouchProbe/main.m new file mode 100644 index 0000000000..521a922cfe --- /dev/null +++ b/contrib/ipod-emu/apps/TouchProbe/main.m @@ -0,0 +1,30 @@ +#import +@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 { 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; } diff --git a/contrib/ipod-emu/tests/run-multitouch b/contrib/ipod-emu/tests/run-multitouch new file mode 100755 index 0000000000..26d601391a --- /dev/null +++ b/contrib/ipod-emu/tests/run-multitouch @@ -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) diff --git a/contrib/ipod-emu/tests/test-multitouch.c b/contrib/ipod-emu/tests/test-multitouch.c new file mode 100644 index 0000000000..e01b0d9d00 --- /dev/null +++ b/contrib/ipod-emu/tests/test-multitouch.c @@ -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(); +} diff --git a/hw/arm/ipod_touch_lcd.c b/hw/arm/ipod_touch_lcd.c index fb5ac19a4e..f4b25b6139 100644 --- a/hw/arm/ipod_touch_lcd.c +++ b/hw/arm/ipod_touch_lcd.c @@ -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; diff --git a/hw/arm/ipod_touch_multitouch.c b/hw/arm/ipod_touch_multitouch.c index 3f57ff526f..eefe0f7a60 100644 --- a/hw/arm/ipod_touch_multitouch.c +++ b/hw/arm/ipod_touch_multitouch.c @@ -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) diff --git a/hw/arm/ipod_touch_spi.c b/hw/arm/ipod_touch_spi.c index 49924b8e53..0d3614a037 100644 --- a/hw/arm/ipod_touch_spi.c +++ b/hw/arm/ipod_touch_spi.c @@ -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); diff --git a/hw/arm/ipod_touch_sysic.c b/hw/arm/ipod_touch_sysic.c index d1e8ebfb58..c1ffbb3398 100644 --- a/hw/arm/ipod_touch_sysic.c +++ b/hw/arm/ipod_touch_sysic.c @@ -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; diff --git a/include/hw/arm/ipod_touch_gpio.h b/include/hw/arm/ipod_touch_gpio.h index c5edafabf9..1459c1246b 100644 --- a/include/hw/arm/ipod_touch_gpio.h +++ b/include/hw/arm/ipod_touch_gpio.h @@ -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) diff --git a/include/hw/arm/ipod_touch_multitouch.h b/include/hw/arm/ipod_touch_multitouch.h index f48972d4b5..a517af464f 100644 --- a/include/hw/arm/ipod_touch_multitouch.h +++ b/include/hw/arm/ipod_touch_multitouch.h @@ -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); -- 2.54.0 From 5f65af21fcd90da31da87b28db369d1cf0b61635 Mon Sep 17 00:00:00 2001 From: Workbench IA Date: Wed, 30 Sep 2026 16:25:51 +0000 Subject: [PATCH 2/2] Fiabiliser les transferts et automatiser les tests d'applications --- contrib/ipod-emu/HANDOFF.md | 27 ++++ contrib/ipod-emu/README.md | 72 ++++++++- contrib/ipod-emu/agent/emuagent.c | 76 ++++++---- contrib/ipod-emu/bin/ipod-agent | 76 +++++++--- contrib/ipod-emu/bin/ipod-emu | 2 +- contrib/ipod-emu/bin/ipod-test-app | 147 ++++++++++++++++++ contrib/ipod-emu/tests/test-transfers.py | 180 +++++++++++++++++++++++ 7 files changed, 526 insertions(+), 54 deletions(-) create mode 100755 contrib/ipod-emu/bin/ipod-test-app create mode 100644 contrib/ipod-emu/tests/test-transfers.py diff --git a/contrib/ipod-emu/HANDOFF.md b/contrib/ipod-emu/HANDOFF.md index e9db8228eb..e20e182ef8 100644 --- a/contrib/ipod-emu/HANDOFF.md +++ b/contrib/ipod-emu/HANDOFF.md @@ -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 : diff --git a/contrib/ipod-emu/README.md b/contrib/ipod-emu/README.md index 41b7d0a8e0..17ca517249 100644 --- a/contrib/ipod-emu/README.md +++ b/contrib/ipod-emu/README.md @@ -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/-/`, 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`). diff --git a/contrib/ipod-emu/agent/emuagent.c b/contrib/ipod-emu/agent/emuagent.c index a9d17060b9..ca0211d218 100644 --- a/contrib/ipod-emu/agent/emuagent.c +++ b/contrib/ipod-emu/agent/emuagent.c @@ -67,7 +67,7 @@ struct rt_msghdr { #include #include -#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: putcheminmode octaltaille + 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: getchemin"); 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) diff --git a/contrib/ipod-emu/bin/ipod-agent b/contrib/ipod-emu/bin/ipod-agent index fa6dc78b93..377aacc01d 100755 --- a/contrib/ipod-emu/bin/ipod-agent +++ b/contrib/ipod-emu/bin/ipod-agent @@ -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}") diff --git a/contrib/ipod-emu/bin/ipod-emu b/contrib/ipod-emu/bin/ipod-emu index 41988316f4..1368f10c8b 100755 --- a/contrib/ipod-emu/bin/ipod-emu +++ b/contrib/ipod-emu/bin/ipod-emu @@ -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 diff --git a/contrib/ipod-emu/bin/ipod-test-app b/contrib/ipod-emu/bin/ipod-test-app new file mode 100755 index 0000000000..ded2bb1678 --- /dev/null +++ b/contrib/ipod-emu/bin/ipod-test-app @@ -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()) diff --git a/contrib/ipod-emu/tests/test-transfers.py b/contrib/ipod-emu/tests/test-transfers.py new file mode 100644 index 0000000000..c5d5fc03f7 --- /dev/null +++ b/contrib/ipod-emu/tests/test-transfers.py @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +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.54.0