// File-server-Loop für MobileBackup2 Restore-Mode. // // Während Restore drives das iPhone das gespräch: // - Es schickt DLMessageDownloadFiles mit Filenamen // - Es erwartet Datei-Content via custom binary chunks (NICHT DLMessage!) // // Wire-Format für File-Transfer (auf demselben Socket wie DLMessage): // // → [4-byte filename-length big-endian][filename UTF-8] // → [4-byte code=12 DATA][4-byte 0 length] (start) // → [4-byte code=12 DATA][4-byte chunk-length][chunk-data] (repeat) // → [4-byte code=0 DONE][4-byte 0] // // Nach allen Files: // → [4-byte 0 — no more files] // → DLMessageStatusResponse [0, "___EmptyParameterString___", {}] // // Codes: // // CODE_FILE_DONE = 0 (end-of-file marker) // CODE_FILE_DATA = 12 (data chunk) // CODE_FILE_ERROR = 6 (error) package mobilebackup2 import ( "encoding/binary" "errors" "fmt" "io" "strconv" "github.com/raynis/rebreak-supervise-magic/internal/dlmessage" ) // 1-byte chunk-codes (matched libimobiledevice's CODE_FILE_DATA etc). const ( fileCodeDone byte = 0x00 // CODE_SUCCESS — end of file fileCodeData byte = 0x0c // CODE_FILE_DATA — chunk fileCodeError byte = 0x06 // CODE_ERROR_LOCAL — file not found / error // Apple sentinel: status-response "no error". emptyParameterString = "___EmptyParameterString___" ) // FileProvider liefert Datei-Content für einen relativen Pfad. // Returnt (content, true) wenn vorhanden, ([], false) sonst. type FileProvider func(relpath string) ([]byte, bool) // ServeFiles fängt das iPhone-driven file-serve-loop ab. // Loopt bis DLMessageDisconnect oder Error. // // Files werden via `provider` geliefert. Wenn provider eine angefragte // Datei nicht hat: Server returnt CODE_FILE_ERROR. // // onProgress wird bei jedem Mess-event aufgerufen (für UI/Log). func (c *Client) ServeFiles(provider FileProvider, onProgress func(event string, info string)) error { if onProgress == nil { onProgress = func(string, string) {} } // Wenn iPhone uns ein ProcessMessage:Response mit ErrorCode=0 schickt, // signalisiert das: Restore-Operation completed successfully. Danach // trennt das iPhone die USB-Verbindung um in den Restore-Mode zu booten → // wir bekommen ein EOF. Das ist KEIN Fehler — es ist erwartetes Verhalten. successConfirmed := false for { t, args, err := c.dl.Receive() if err != nil { if successConfirmed { // iPhone hat success-Response geschickt + disconnected um zu rebooten. onProgress("ServeFiles", "device disconnected after successful Response — reboot expected") return nil } return fmt.Errorf("serve: receive: %w", err) } switch t { case dlmessage.TypeProcessMessage: if len(args) == 0 { continue } dict, ok := args[0].(map[string]interface{}) if !ok { continue } msgName, _ := dict["MessageName"].(string) // 2026-05-28 DEBUG: log full dict to understand what iPhone sends onProgress("ProcessMessage", fmt.Sprintf("%s | full=%v", msgName, dict)) if msgName == "DLMessageDisconnect" || msgName == "Disconnect" { return nil } // "Response" = iPhone signals operation completed — NICHT antworten, // nur weiter loopen + auf nächste Message warten. // Wenn ErrorCode=0 → success → nachfolgender EOF ist erwartet. if msgName == "Response" { ed, _ := dict["ErrorDescription"].(string) if ec, ok := dict["ErrorCode"]; ok { code, parsed := parseErrorCode(ec) if !parsed { return fmt.Errorf("serve: response error (unknown code type %T): %s", ec, ed) } if code == 0 { successConfirmed = true } else { return fmt.Errorf("serve: response error code=%d: %s", code, ed) } } else { if ed != "" { return fmt.Errorf("serve: response without error code: %s", ed) } } continue } // Andere Sub-Messages — Status OK respond. if err := c.sendStatusOK(); err != nil { return err } case dlmessage.TypeStatusResponse: // Device-Status — wir loggen + machen weiter onProgress("StatusResponse", fmt.Sprintf("%v", args)) // Falls error: abbrechen if len(args) > 0 { if code, ok := args[0].(uint64); ok && code != 0 { return fmt.Errorf("serve: device error code=%d: %v", code, args) } } case dlmessage.TypeGetFreeDiskSpace: // Device fragt nach freiem Speicher — wir behaupten Schubladen voll Platz onProgress("GetFreeDiskSpace", "") // Antwort: [0, errStr, freeSpaceUint64] if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, uint64(1<<40)); err != nil { return err } case dlmessage.TypeContentsOfDirectory: // Device möchte Verzeichnis listen — wir antworten mit empty dict dirname := "" if len(args) > 0 { dirname, _ = args[0].(string) } onProgress("ContentsOfDirectory", dirname) // Antwort: status [0, "", {}] if err := c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}); err != nil { return err } case dlmessage.TypeCreateDirectory, dlmessage.TypeMoveItems, dlmessage.TypeRemoveItems, dlmessage.TypeCopyItem: onProgress(t, "") if err := c.sendStatusOK(); err != nil { return err } case dlmessage.TypeDownloadFiles: // Device fragt files an — args[0] ist []string var fileList []string if len(args) > 0 { if arr, ok := args[0].([]interface{}); ok { for _, item := range arr { if s, ok := item.(string); ok { fileList = append(fileList, s) } } } } onProgress("DownloadFiles", fmt.Sprintf("count=%d", len(fileList))) if err := c.sendFiles(fileList, provider, onProgress); err != nil { return fmt.Errorf("serve: send files: %w", err) } case dlmessage.TypeUploadFiles: // iPhone will UNS files uploaden (sein current state). // Wir ACK + receive raw upload-stream. onProgress("UploadFiles", "device uploading") if err := c.sendStatusOK(); err != nil { return err } if err := c.receiveFiles(onProgress); err != nil { return fmt.Errorf("serve: receive files: %w", err) } case dlmessage.TypeDisconnect: onProgress("Disconnect", "explicit") return nil default: // unbekannter Typ — log + status OK onProgress("Unknown", t) if err := c.sendStatusOK(); err != nil { return err } } } } func (c *Client) sendStatusOK() error { return c.dl.Send(dlmessage.TypeStatusResponse, uint64(0), emptyParameterString, map[string]interface{}{}) } func parseErrorCode(v interface{}) (int64, bool) { switch n := v.(type) { case uint64: return int64(n), true case int64: return n, true case int: return int64(n), true case uint32: return int64(n), true case int32: return int64(n), true case float64: return int64(n), true case string: parsed, err := strconv.ParseInt(n, 10, 64) if err != nil { return 0, false } return parsed, true default: return 0, false } } // sendFiles antwortet auf DLMessageDownloadFiles. Wire-format aus libimobiledevice // mb2_handle_send_file verifiziert: // // per file: // [4-byte BE filename-length][filename UTF-8] // if file exists: // per chunk: [4-byte BE (1+chunk-size)][1-byte 0x0c CODE_DATA][chunk-data] // end-of-file: [4-byte BE 1][1-byte 0x00 CODE_SUCCESS] // else: // [4-byte BE 1][1-byte 0x06 CODE_ERROR_LOCAL] // // after all files: // [4-byte BE 0] (terminator = "no more files") // DLMessageStatusResponse [0, "___EmptyParameterString___", {}] func (c *Client) sendFiles(fileList []string, provider FileProvider, onProgress func(string, string)) error { for _, fname := range fileList { content, ok := provider(fname) onProgress("send-file", fmt.Sprintf("%s (%d bytes, found=%v)", fname, len(content), ok)) // filename header if err := c.writeRawU32(uint32(len(fname))); err != nil { return err } if err := c.deviceConn.Send([]byte(fname)); err != nil { return fmt.Errorf("write filename: %w", err) } if !ok { // ERROR: [length=1][CODE_ERROR_LOCAL] if err := c.writeRawU32(1); err != nil { return err } if err := c.deviceConn.Send([]byte{fileCodeError}); err != nil { return err } continue } // DATA chunks const maxChunk = 32 * 1024 for offset := 0; offset < len(content); offset += maxChunk { end := offset + maxChunk if end > len(content) { end = len(content) } chunk := content[offset:end] // [length=1+chunk][CODE_DATA][chunk] if err := c.writeRawU32(uint32(1 + len(chunk))); err != nil { return err } if err := c.deviceConn.Send([]byte{fileCodeData}); err != nil { return err } if err := c.deviceConn.Send(chunk); err != nil { return err } } // END-OF-FILE: [length=1][CODE_SUCCESS] if err := c.writeRawU32(1); err != nil { return err } if err := c.deviceConn.Send([]byte{fileCodeDone}); err != nil { return err } } // Terminator: [length=0] if err := c.writeRawU32(0); err != nil { return err } // Final status return c.sendStatusOK() } func (c *Client) writeRawU32(v uint32) error { buf := make([]byte, 4) binary.BigEndian.PutUint32(buf, v) return c.deviceConn.Send(buf) } // receiveFiles empfängt iPhone's file-upload-stream nach DLMessageUploadFiles. // Wire-format mirror von sendFiles (gleiche frames, andere Richtung): // // per file: // [4-byte BE path-len][path] // per chunk: [4-byte BE (1+chunk-size)][1-byte code][data] // end-of-file: [4-byte BE 1][1-byte 0x00 SUCCESS] // terminator: [4-byte BE 0] // // Wir discardieren den Inhalt — iPhone's "backup-of-backup" interessiert uns nicht, // nur dass wir den Stream sauber durchlesen damit das Protokoll weitergeht. func (c *Client) receiveFiles(onProgress func(string, string)) error { reader := c.deviceConn.Reader() totalFiles := 0 totalBytes := uint64(0) for { // path-length-prefix var pathLenBuf [4]byte if _, err := io.ReadFull(reader, pathLenBuf[:]); err != nil { return fmt.Errorf("read path-len: %w", err) } pathLen := binary.BigEndian.Uint32(pathLenBuf[:]) if pathLen == 0 { // terminator — no more files onProgress("upload-done", fmt.Sprintf("%d files, %d bytes total", totalFiles, totalBytes)) return nil } if pathLen > 65536 { return fmt.Errorf("upload-path too large: %d", pathLen) } // path-bytes pathBuf := make([]byte, pathLen) if _, err := io.ReadFull(reader, pathBuf); err != nil { return fmt.Errorf("read path: %w", err) } path := string(pathBuf) totalFiles++ // chunks fileBytes := uint64(0) for { var chunkLenBuf [4]byte if _, err := io.ReadFull(reader, chunkLenBuf[:]); err != nil { return fmt.Errorf("read chunk-len for %s: %w", path, err) } chunkLen := binary.BigEndian.Uint32(chunkLenBuf[:]) if chunkLen == 0 { // some implementations send zero-length as terminator break } // 1-byte code var code [1]byte if _, err := io.ReadFull(reader, code[:]); err != nil { return fmt.Errorf("read chunk-code: %w", err) } dataLen := chunkLen - 1 if dataLen > 0 { discard := make([]byte, dataLen) if _, err := io.ReadFull(reader, discard); err != nil { return fmt.Errorf("read chunk-data: %w", err) } fileBytes += uint64(dataLen) } // code 0x00 (CODE_SUCCESS) = end-of-file if code[0] == fileCodeDone { break } } totalBytes += fileBytes onProgress("upload-file", fmt.Sprintf("%s (%d bytes)", path, fileBytes)) } } // ErrServeAborted ist ein Sentinel den FileProvider zurückgeben kann um Loop // zu beenden (z.B. bei Disconnect-Trigger). var ErrServeAborted = errors.New("serve aborted")