From ccc157af5d9fc25aebd4d5714b627bb1b4a90ed8 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Fri, 4 Jul 2025 13:22:45 +0200 Subject: [PATCH 01/13] implement new client for AFC --- ios/afc/client.go | 420 +++++++++++++++++++++++++++++++++++++++++ ios/afc/client_test.go | 221 ++++++++++++++++++++++ 2 files changed, 641 insertions(+) create mode 100644 ios/afc/client.go create mode 100644 ios/afc/client_test.go diff --git a/ios/afc/client.go b/ios/afc/client.go new file mode 100644 index 00000000..83efc558 --- /dev/null +++ b/ios/afc/client.go @@ -0,0 +1,420 @@ +package afc + +import ( + "bufio" + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/fs" + "path" + "strconv" + "strings" + "sync/atomic" + + "github.com/danielpaulus/go-ios/ios" + "golang.org/x/exp/slices" +) + +// WalkFunc is used by [Client.WalkDir] for traversing directories +// This function will be called for each entry in a directory +// Execution can be controlled by returning error values from this function: +// - [fs.SkipDir] will skip files of the current directory +// - [fs.SkipAll] will stop traversal and exit without an error +// - returning any other non-nil error will stop traversal and return this error from [Client.WalkDir] +type WalkFunc func(path string, info FileInfo, err error) error + +type Client struct { + connection io.ReadWriteCloser + packetNum atomic.Int64 +} + +func NewAfcConnection(d ios.DeviceEntry) (*Client, error) { + deviceConn, err := ios.ConnectToService(d, serviceName) + if err != nil { + return nil, fmt.Errorf("error connecting to service '%s': %w", serviceName, err) + } + return &Client{ + connection: deviceConn, + }, nil +} + +func (c *Client) Close() error { + err := c.connection.Close() + if err != nil { + return fmt.Errorf("error closing afc client: %w", err) + } + return nil +} + +// List all entries of the provided path +func (c *Client) List(p string) ([]string, error) { + err := c.sendPacket(Afc_operation_read_dir, []byte(p), nil) + if err != nil { + return nil, fmt.Errorf("error listing afc dir: %w", err) + } + pack, err := c.readPacket() + if err != nil { + return nil, fmt.Errorf("error listing afc dir: %w", err) + } + reader := bufio.NewReader(bytes.NewReader(pack.Payload)) + var list []string + for { + s, err := reader.ReadString('\x00') + if err != nil { + break + } + if len(s) == 0 { + continue + } + list = append(list, s[:len(s)-1]) + } + return list, nil +} + +func (c *Client) Open(p string, mode Mode) (*File, error) { + pathBytes := []byte(p) + pathBytes = append(pathBytes, 0) + headerLength := 8 + uint64(len(pathBytes)) + headerPayload := make([]byte, headerLength) + binary.LittleEndian.PutUint64(headerPayload, uint64(mode)) + copy(headerPayload[8:], pathBytes) + err := c.sendPacket(Afc_operation_file_open, headerPayload, nil) + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + resp, err := c.readPacket() + if err != nil { + return nil, fmt.Errorf("error opening file: %w", err) + } + fd := binary.LittleEndian.Uint64(resp.HeaderPayload) + + return &File{ + client: c, + handle: fd, + }, nil +} + +// CreateDir +func (c *Client) CreateDir(p string) error { + headerPayload := []byte(p) + headerPayload = append(headerPayload, 0) + + err := c.sendPacket(Afc_operation_make_dir, headerPayload, nil) + if err != nil { + return fmt.Errorf("error creating dir: %w", err) + } + _, err = c.readPacket() + if err != nil { + return fmt.Errorf("error creating dir: %w", err) + } + return nil +} + +func (c *Client) Delete(p string) error { + headerPayload := []byte(p) + err := c.sendPacket(Afc_operation_remove_path_and_contents, headerPayload, nil) + if err != nil { + return fmt.Errorf("error deleting file: %w", err) + } + _, err = c.readPacket() + if err != nil { + return fmt.Errorf("error deleting file: %w", err) + } + return nil +} + +func (c *Client) sendPacket(operation uint64, headerPayload []byte, payload []byte) error { + num := c.packetNum.Add(1) + + thisLen := Afc_header_size + uint64(len(headerPayload)) + p := packet{ + Header: header{ + Magic: Afc_magic, + EntireLen: thisLen + uint64(len(payload)), + ThisLen: thisLen, + PacketNum: uint64(num), + Operation: operation, + }, + HeaderPayload: headerPayload, + Payload: payload, + } + + err := binary.Write(c.connection, binary.LittleEndian, p.Header) + if err != nil { + return fmt.Errorf("error writing header: %w", err) + } + if len(headerPayload) > 0 { + _, err = c.connection.Write(headerPayload) + if err != nil { + return fmt.Errorf("error writing header payload: %w", err) + } + } + if len(payload) > 0 { + _, err = c.connection.Write(payload) + if err != nil { + return fmt.Errorf("error writing payload: %w", err) + } + } + return nil +} + +func (c *Client) readPacket() (packet, error) { + var h header + err := binary.Read(c.connection, binary.LittleEndian, &h) + if err != nil { + return packet{}, fmt.Errorf("error reading header: %w", err) + } + headerPayloadLen := h.ThisLen - Afc_header_size + payloadLen := h.EntireLen - h.ThisLen + + headerpayload := make([]byte, headerPayloadLen) + payload := make([]byte, payloadLen) + + p := packet{ + Header: h, + HeaderPayload: headerpayload, + Payload: payload, + } + + if headerPayloadLen > 0 { + _, err = io.ReadFull(c.connection, headerpayload) + if err != nil { + return packet{}, fmt.Errorf("error reading header: %w", err) + } + } + if payloadLen > 0 { + _, err = io.ReadFull(c.connection, payload) + if err != nil { + return packet{}, fmt.Errorf("error reading payload: %w", err) + } + } + + if p.Header.Operation == Afc_operation_status { + code := binary.LittleEndian.Uint64(p.HeaderPayload) + if code == Afc_Err_Success { + return p, nil + } + return p, fmt.Errorf("error processing afc status: %d", code) + } + + return p, nil +} + +type FileType string + +const ( + // S_IFDIR marks a directory + S_IFDIR FileType = "S_IFDIR" + // S_IFDIR marks a regular file + S_IFMT FileType = "S_IFMT" +) + +type FileInfo struct { + Name string + Type FileType + Mode uint32 + Size int64 +} + +func (c *Client) Stat(s string) (FileInfo, error) { + err := c.sendPacket(Afc_operation_file_info, []byte(s), nil) + if err != nil { + return FileInfo{}, fmt.Errorf("error getting file info: %w", err) + } + + resp, err := c.readPacket() + if err != nil { + return FileInfo{}, fmt.Errorf("error getting file info: %w", err) + } + + reader := bufio.NewReader(bytes.NewReader(resp.Payload)) + info := FileInfo{} + + // Parse the file info response which is a series of null-terminated strings + // in key-value pairs + for { + key, err := reader.ReadString('\x00') + if err != nil { + break + } + if len(key) <= 1 { + break + } + key = key[:len(key)-1] // Remove null terminator + + value, err := reader.ReadString('\x00') + if err != nil { + break + } + value = value[:len(value)-1] // Remove null terminator + + switch key { + case "st_ifmt": + info.Type = FileType(value) + case "st_size": + size, _ := strconv.ParseInt(value, 10, 64) + info.Size = size + case "st_mode": + mode, _ := strconv.ParseUint(value, 8, 32) + info.Mode = uint32(mode) + } + } + + // Set the name from the path + parts := strings.Split(s, "/") + if len(parts) > 0 { + info.Name = parts[len(parts)-1] + } + + return info, nil +} + +func (c *Client) WalkDir(p string, f WalkFunc) error { + files, err := c.List(p) + if err != nil { + return err + } + + slices.Sort(files) + for _, file := range files { + if file == "." || file == ".." { + continue + } + info, err := c.Stat(path.Join(p, file)) + if err != nil { + return err + } + fnErr := f(path.Join(p, file), info, nil) + if fnErr != nil { + if errors.Is(fnErr, fs.SkipDir) { + continue + } else if errors.Is(fnErr, fs.SkipAll) { + return nil + } else { + return fnErr + } + } + if info.Type == S_IFDIR { + walkErr := c.WalkDir(path.Join(p, file), f) + if walkErr != nil { + return walkErr + } + } + } + return nil +} + +func (c *Client) DeviceInfo() (AFCDeviceInfo, error) { + err := c.sendPacket(Afc_operation_device_info, nil, nil) + if err != nil { + return AFCDeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + } + resp, err := c.readPacket() + if err != nil { + return AFCDeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + } + + bs := bytes.Split(resp.Payload, []byte{0}) + + var info AFCDeviceInfo + for i := 0; i+1 < len(bs); i += 2 { + key := string(bs[i]) + if key == "Model" { + info.Model = string(bs[i+1]) + continue + } + value, intParseErr := strconv.ParseUint(string(bs[i+1]), 10, 64) + switch key { + case "FSTotalBytes": + if intParseErr != nil { + return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + } + info.TotalBytes = value + case "FSFreeBytes": + if intParseErr != nil { + return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + } + info.FreeBytes = value + case "FSBlockSize": + if intParseErr != nil { + return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + } + info.BlockSize = value + } + } + return info, nil +} + +type header struct { + Magic uint64 + EntireLen uint64 + ThisLen uint64 + PacketNum uint64 + Operation uint64 +} + +type packet struct { + Header header + HeaderPayload []byte + Payload []byte +} + +type File struct { + client *Client + handle uint64 +} + +func (f *File) Read(p []byte) (int, error) { + headerPayload := make([]byte, 16) + binary.LittleEndian.PutUint64(headerPayload, f.handle) + binary.LittleEndian.PutUint64(headerPayload[8:], uint64(len(p))) + + err := f.client.sendPacket(Afc_operation_file_read, headerPayload, nil) + if err != nil { + return 0, fmt.Errorf("error reading data: %w", err) + } + resp, err := f.client.readPacket() + copy(p, resp.Payload) + return len(resp.Payload), nil +} + +func (f *File) Write(p []byte) (int, error) { + headerPayload := make([]byte, 8) + binary.LittleEndian.PutUint64(headerPayload, f.handle) + err := f.client.sendPacket(Afc_operation_file_write, headerPayload, p) + if err != nil { + return 0, fmt.Errorf("error writing data: %w", err) + } + _, err = f.client.readPacket() + if err != nil { + return 0, fmt.Errorf("error reading data: %w", err) + } + return len(p), nil +} + +func (f *File) Close() error { + headerPayload := make([]byte, 8) + binary.LittleEndian.PutUint64(headerPayload, f.handle) + err := f.client.sendPacket(Afc_operation_file_close, headerPayload, nil) + if err != nil { + return fmt.Errorf("error closing file: %w", err) + } + _, err = f.client.readPacket() + if err != nil { + return fmt.Errorf("error closing file: %w", err) + } + return nil +} + +type Mode uint64 + +const ( + READ_ONLY = Mode(0x00000001) + READ_WRITE_CREATE = Mode(0x00000002) + WRITE_ONLY_CREATE_TRUNC = Mode(0x00000003) + READ_WRITE_CREATE_TRUNC = Mode(0x00000004) + WRITE_ONLY_CREATE_APPEND = Mode(0x00000005) + READ_WRITE_CREATE_APPEND = Mode(0x00000006) +) diff --git a/ios/afc/client_test.go b/ios/afc/client_test.go new file mode 100644 index 00000000..d323a2d8 --- /dev/null +++ b/ios/afc/client_test.go @@ -0,0 +1,221 @@ +package afc + +import ( + "errors" + "fmt" + "io/fs" + "path" + "slices" + "strings" + "testing" + + "github.com/danielpaulus/go-ios/ios" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestAfc(t *testing.T) { + devices, err := ios.ListDevices() + if err != nil { + t.Skipf("failed to list devices: %s", err) + return + } + + if len(devices.DeviceList) == 0 { + t.Skipf("no devices connected") + return + } + + for _, device := range devices.DeviceList { + + t.Run(fmt.Sprintf("device %s", device.Properties.SerialNumber), func(t *testing.T) { + + client, err := NewAfcConnection(device) + assert.NoError(t, err) + + defer client.Close() + + t.Run("list /tmp", func(t *testing.T) { + _, err := client.List("/") + assert.NoError(t, err) + }) + + t.Run("list invalid folder returns error", func(t *testing.T) { + _, err := client.List("/invalid123") + assert.Error(t, err) + }) + + t.Run("create file", func(t *testing.T) { + files, err := client.List(".") + assert.NoError(t, err) + hasFile := slices.ContainsFunc(files, func(s string) bool { + return strings.Contains(s, "test-file") + }) + if hasFile { + err = client.Delete("./test-file") + assert.NoError(t, err) + } + f, err := client.Open("./test-file", READ_WRITE_CREATE_TRUNC) + assert.NoError(t, err) + + err = f.Close() + assert.NoError(t, err) + + err = client.Delete("./test-file") + assert.NoError(t, err) + }) + + t.Run("write to file and check size", func(t *testing.T) { + f, err := client.Open("./test-file", READ_WRITE_CREATE_TRUNC) + assert.NoError(t, err) + + n, err := f.Write([]byte("test")) + assert.NoError(t, err) + assert.Equal(t, 4, n) + + err = f.Close() + assert.NoError(t, err) + + info, err := client.Stat("./test-file") + + assert.EqualValues(t, 4, info.Size) + + err = client.Delete("./test-file") + assert.NoError(t, err) + }) + + t.Run("write to file and read it back", func(t *testing.T) { + f, err := client.Open("./test-file", READ_WRITE_CREATE_TRUNC) + assert.NoError(t, err) + + n, err := f.Write([]byte("test")) + assert.NoError(t, err) + assert.Equal(t, 4, n) + + err = f.Close() + assert.NoError(t, err) + + f, err = client.Open("./test-file", READ_ONLY) + assert.NoError(t, err) + + b := make([]byte, 8) + n, err = f.Read(b) + assert.NoError(t, err) + assert.Equal(t, []byte("test"), b[:n]) + + err = client.Delete("./test-file") + assert.NoError(t, err) + }) + + t.Run("created and delete nested directory", func(t *testing.T) { + err = client.CreateDir("./some/nested/directory") + assert.NoError(t, err) + + var info FileInfo + info, err = client.Stat("./some") + assert.NoError(t, err) + assert.Equal(t, S_IFDIR, info.Type) + + info, err = client.Stat("./some/nested") + assert.NoError(t, err) + assert.Equal(t, S_IFDIR, info.Type) + + info, err = client.Stat("./some/nested/directory") + assert.NoError(t, err) + assert.Equal(t, S_IFDIR, info.Type) + + err = client.Delete("./some") + assert.NoError(t, err) + + _, err = client.Stat("./some") + assert.Error(t, err) + }) + + t.Run("walk dir", func(t *testing.T) { + basePath := path.Join("./", uuid.New().String()) + mustCreateDir(client, basePath) + mustCreateDir(client, path.Join(basePath, "a-dir")) + mustCreateDir(client, path.Join(basePath, "a-dir", "subdir")) + mustCreateFile(client, path.Join(basePath, "a-dir", "file")) + mustCreateDir(client, path.Join(basePath, "c-dir")) + + t.Run("visit all", func(t *testing.T) { + var visited []string + err = client.WalkDir(basePath, func(path string, info FileInfo, err error) error { + visited = append(visited, path) + return nil + }) + + assert.NoError(t, err) + assert.Equal(t, []string{ + path.Join(basePath, "a-dir"), + path.Join(basePath, "a-dir/file"), + path.Join(basePath, "a-dir/subdir"), + path.Join(basePath, "c-dir"), + }, visited) + }) + + t.Run("skip dir", func(t *testing.T) { + var visited []string + err = client.WalkDir(basePath, func(p string, info FileInfo, err error) error { + visited = append(visited, p) + if path.Base(p) == "a-dir" { + return fs.SkipDir + } + return nil + }) + + assert.NoError(t, err) + assert.Equal(t, []string{ + path.Join(basePath, "a-dir"), + path.Join(basePath, "c-dir"), + }, visited) + }) + + t.Run("skip all", func(t *testing.T) { + var visited []string + err = client.WalkDir(basePath, func(p string, info FileInfo, err error) error { + visited = append(visited, p) + return fs.SkipAll + }) + + assert.NoError(t, err) + assert.Equal(t, []string{ + path.Join(basePath, "a-dir"), + }, visited) + }) + + t.Run("return error stops walkdir", func(t *testing.T) { + var visited []string + walkDirErr := errors.New("stop walkdir") + err = client.WalkDir(basePath, func(p string, info FileInfo, err error) error { + visited = append(visited, p) + return walkDirErr + }) + assert.Len(t, visited, 1) + assert.Equal(t, walkDirErr, err) + }) + + t.Run("device info", func(t *testing.T) { + _, err := client.DeviceInfo() + assert.NoError(t, err) + }) + }) + }) + } +} + +func mustCreateDir(c *Client, dir string) { + err := c.CreateDir(dir) + if err != nil { + panic(err) + } +} + +func mustCreateFile(c *Client, path string) { + f, err := c.Open(path, READ_WRITE_CREATE) + if err != nil { + panic(err) + } + _ = f.Close() +} From 35b37db9bf9dfc2fdf37f88666bca153e1e00fd7 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Wed, 17 Sep 2025 14:29:32 +0200 Subject: [PATCH 02/13] remove afc prefix from status codes --- ios/afc/afc.go | 110 +++++++++++++++++++++++----------------------- ios/afc/client.go | 2 +- ios/afc/fsync.go | 2 +- 3 files changed, 57 insertions(+), 57 deletions(-) diff --git a/ios/afc/afc.go b/ios/afc/afc.go index 91728953..0fd25ea7 100644 --- a/ios/afc/afc.go +++ b/ios/afc/afc.go @@ -35,34 +35,34 @@ const ( ) const ( - Afc_Err_Success = 0 - Afc_Err_UnknownError = 1 - Afc_Err_OperationHeaderInvalid = 2 - Afc_Err_NoResources = 3 - Afc_Err_ReadError = 4 - Afc_Err_WriteError = 5 - Afc_Err_UnknownPacketType = 6 - Afc_Err_InvalidArgument = 7 - Afc_Err_ObjectNotFound = 8 - Afc_Err_ObjectIsDir = 9 - Afc_Err_PermDenied = 10 - Afc_Err_ServiceNotConnected = 11 - Afc_Err_OperationTimeout = 12 - Afc_Err_TooMuchData = 13 - Afc_Err_EndOfData = 14 - Afc_Err_OperationNotSupported = 15 - Afc_Err_ObjectExists = 16 - Afc_Err_ObjectBusy = 17 - Afc_Err_NoSpaceLeft = 18 - Afc_Err_OperationWouldBlock = 19 - Afc_Err_IoError = 20 - Afc_Err_OperationInterrupted = 21 - Afc_Err_OperationInProgress = 22 - Afc_Err_InternalError = 23 - Afc_Err_MuxError = 30 - Afc_Err_NoMemory = 31 - Afc_Err_NotEnoughData = 32 - Afc_Err_DirNotEmpty = 33 + errSuccess = 0 + errUnknown = 1 + errOperationHeaderInvalid = 2 + errNoResources = 3 + errReadError = 4 + errWriteError = 5 + errUnknownPacketType = 6 + errInvalidArgument = 7 + errObjectNotFound = 8 + errObjectIsDir = 9 + errPermDenied = 10 + errServiceNotConnected = 11 + errOperationTimeout = 12 + errTooMuchData = 13 + errEndOfData = 14 + errOperationNotSupported = 15 + errObjectExists = 16 + errObjectBusy = 17 + errNoSpaceLeft = 18 + errOperationWouldBlock = 19 + errIoError = 20 + errOperationInterrupted = 21 + errOperationInProgress = 22 + errInternalError = 23 + errMuxError = 30 + errNoMemory = 31 + errNotEnoughData = 32 + errDirNotEmpty = 33 ) type AFCDeviceInfo struct { @@ -74,59 +74,59 @@ type AFCDeviceInfo struct { func getError(errorCode uint64) error { switch errorCode { - case Afc_Err_UnknownError: + case errUnknown: return errors.New("UnknownError") - case Afc_Err_OperationHeaderInvalid: + case errOperationHeaderInvalid: return errors.New("OperationHeaderInvalid") - case Afc_Err_NoResources: + case errNoResources: return errors.New("NoResources") - case Afc_Err_ReadError: + case errReadError: return errors.New("ReadError") - case Afc_Err_WriteError: + case errWriteError: return errors.New("WriteError") - case Afc_Err_UnknownPacketType: + case errUnknownPacketType: return errors.New("UnknownPacketType") - case Afc_Err_InvalidArgument: + case errInvalidArgument: return errors.New("InvalidArgument") - case Afc_Err_ObjectNotFound: + case errObjectNotFound: return errors.New("ObjectNotFound") - case Afc_Err_ObjectIsDir: + case errObjectIsDir: return errors.New("ObjectIsDir") - case Afc_Err_PermDenied: + case errPermDenied: return errors.New("PermDenied") - case Afc_Err_ServiceNotConnected: + case errServiceNotConnected: return errors.New("ServiceNotConnected") - case Afc_Err_OperationTimeout: + case errOperationTimeout: return errors.New("OperationTimeout") - case Afc_Err_TooMuchData: + case errTooMuchData: return errors.New("TooMuchData") - case Afc_Err_EndOfData: + case errEndOfData: return errors.New("EndOfData") - case Afc_Err_OperationNotSupported: + case errOperationNotSupported: return errors.New("OperationNotSupported") - case Afc_Err_ObjectExists: + case errObjectExists: return errors.New("ObjectExists") - case Afc_Err_ObjectBusy: + case errObjectBusy: return errors.New("ObjectBusy") - case Afc_Err_NoSpaceLeft: + case errNoSpaceLeft: return errors.New("NoSpaceLeft") - case Afc_Err_OperationWouldBlock: + case errOperationWouldBlock: return errors.New("OperationWouldBlock") - case Afc_Err_IoError: + case errIoError: return errors.New("IoError") - case Afc_Err_OperationInterrupted: + case errOperationInterrupted: return errors.New("OperationInterrupted") - case Afc_Err_OperationInProgress: + case errOperationInProgress: return errors.New("OperationInProgress") - case Afc_Err_InternalError: + case errInternalError: return errors.New("InternalError") - case Afc_Err_MuxError: + case errMuxError: return errors.New("MuxError") - case Afc_Err_NoMemory: + case errNoMemory: return errors.New("NoMemory") - case Afc_Err_NotEnoughData: + case errNotEnoughData: return errors.New("NotEnoughData") - case Afc_Err_DirNotEmpty: + case errDirNotEmpty: return errors.New("DirNotEmpty") default: return nil diff --git a/ios/afc/client.go b/ios/afc/client.go index 83efc558..6f5a9e39 100644 --- a/ios/afc/client.go +++ b/ios/afc/client.go @@ -193,7 +193,7 @@ func (c *Client) readPacket() (packet, error) { if p.Header.Operation == Afc_operation_status { code := binary.LittleEndian.Uint64(p.HeaderPayload) - if code == Afc_Err_Success { + if code == errSuccess { return p, nil } return p, fmt.Errorf("error processing afc status: %d", code) diff --git a/ios/afc/fsync.go b/ios/afc/fsync.go index eff493aa..bcc6a8b9 100644 --- a/ios/afc/fsync.go +++ b/ios/afc/fsync.go @@ -127,7 +127,7 @@ func (conn *Connection) sendAfcPacketAndAwaitResponse(packet AfcPacket) (AfcPack func (conn *Connection) checkOperationStatus(packet AfcPacket) error { if packet.Header.Operation == Afc_operation_status { errorCode := binary.LittleEndian.Uint64(packet.HeaderPayload) - if errorCode != Afc_Err_Success { + if errorCode != errSuccess { return getError(errorCode) } } From 072598f91fa2910dcc27fdf437bd460aaac8b3c9 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Mon, 29 Sep 2025 13:54:51 +0200 Subject: [PATCH 03/13] use afc client for crash reports --- ios/crashreport/crashreport.go | 110 ++++++++++++++++----------------- 1 file changed, 54 insertions(+), 56 deletions(-) diff --git a/ios/crashreport/crashreport.go b/ios/crashreport/crashreport.go index e77aa4e3..ef501f23 100644 --- a/ios/crashreport/crashreport.go +++ b/ios/crashreport/crashreport.go @@ -2,8 +2,10 @@ package crashreport import ( "fmt" + "io" "os" "path" + "path/filepath" "github.com/danielpaulus/go-ios/ios" "github.com/danielpaulus/go-ios/ios/afc" @@ -30,55 +32,35 @@ func DownloadReports(device ios.DeviceEntry, pattern string, targetdir string) e if err != nil { return err } - afc := afc.NewFromConn(deviceConn) - return copyReports(afc, ".", pattern, targetdir) -} - -func copyReports(afc *afc.Connection, cwd string, pattern string, targetDir string) error { - log.WithFields(log.Fields{"dir": cwd, "pattern": pattern, "to": targetDir}).Info("downloading") - targetDirInfo, err := os.Stat(targetDir) - if err != nil { - return err - } - files, err := afc.ListFiles(cwd, pattern) - if err != nil { - return err - } - - log.Debugf("files:%+v", files) - for _, f := range files { - if f == "." || f == ".." { - continue - } - devicePath := path.Join(cwd, f) - targetFilePath := path.Join(targetDir, f) - log.WithFields(log.Fields{"from": devicePath, "to": targetFilePath}).Info("downloading") - info, err := afc.Stat(devicePath) + afcConn := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + err = afcConn.WalkDir(".", func(p string, info afc.FileInfo, err error) error { + matched, err := filepath.Match(pattern, p) if err != nil { - log.Warnf("failed getting info for file: %s, skipping", f) - continue + return err + } + if !matched { + return nil } - log.Debugf("%+v", info) - if info.IsDir() { - err := os.Mkdir(targetFilePath, targetDirInfo.Mode().Perm()) - if err != nil { - return err - } - err = copyReports(afc, devicePath, "*", targetFilePath) - if err != nil { - return err - } - continue + f, err := afcConn.Open(p, afc.READ_ONLY) + if err != nil { + return err } + defer f.Close() + _, filename := path.Split(p) + + targetPath := path.Join(targetdir, filename) - err = afc.PullSingleFile(devicePath, targetFilePath) + dst, err := os.Create(targetPath) if err != nil { return err } - log.WithFields(log.Fields{"from": devicePath, "to": targetFilePath}).Info("done") - } - return nil + defer dst.Close() + + _, err = io.Copy(dst, f) + return err + }) + return err } func RemoveReports(device ios.DeviceEntry, cwd string, pattern string) error { @@ -94,23 +76,20 @@ func RemoveReports(device ios.DeviceEntry, cwd string, pattern string) error { if err != nil { return err } - afc := afc.NewFromConn(deviceConn) - files, err := afc.ListFiles(cwd, pattern) - if err != nil { - return err - } - for _, f := range files { - if f == "." || f == ".." { - continue + afcClient := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + return afcClient.WalkDir(".", func(path string, info afc.FileInfo, err error) error { + if info.Type == afc.S_IFDIR { + return nil } - log.WithFields(log.Fields{"path": path.Join(cwd, f)}).Info("delete") - err := afc.Remove(path.Join(cwd, f)) + matched, err := filepath.Match(pattern, path) if err != nil { return err } - } - log.WithFields(log.Fields{"cwd": cwd, "pattern": pattern}).Info("done deleting") - return nil + if !matched { + return nil + } + return afcClient.Delete(path) + }) } func ListReports(device ios.DeviceEntry, pattern string) ([]string, error) { @@ -122,8 +101,27 @@ func ListReports(device ios.DeviceEntry, pattern string) ([]string, error) { if err != nil { return []string{}, err } - afc := afc.NewFromConn(deviceConn) - return afc.ListFiles(".", pattern) + afcClient := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + + var files []string + err = afcClient.WalkDir(".", func(path string, info afc.FileInfo, err error) error { + if info.Type == afc.S_IFDIR { + return nil + } + matched, err := filepath.Match(pattern, path) + if err != nil { + return err + } + if !matched { + return nil + } + files = append(files, path) + return nil + }) + if err != nil { + return []string{}, err + } + return files, nil } func moveReports(device ios.DeviceEntry) error { From 70385d5bafc5fa70a2ae61e2b29c87710c97a214 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Mon, 29 Sep 2025 13:55:10 +0200 Subject: [PATCH 04/13] use afc client for house arrest --- ios/house_arrest/house_arrest.go | 113 ++++++++++--------------------- 1 file changed, 36 insertions(+), 77 deletions(-) diff --git a/ios/house_arrest/house_arrest.go b/ios/house_arrest/house_arrest.go index 605d5c0c..daf266db 100644 --- a/ios/house_arrest/house_arrest.go +++ b/ios/house_arrest/house_arrest.go @@ -1,11 +1,12 @@ package house_arrest import ( - "encoding/binary" + "bytes" "fmt" - "strings" "github.com/danielpaulus/go-ios/ios/afc" + "github.com/pkg/errors" + "howett.net/plist" "github.com/danielpaulus/go-ios/ios" ) @@ -17,16 +18,16 @@ type Connection struct { packageNumber uint64 } -func New(device ios.DeviceEntry, bundleID string) (*Connection, error) { +func New(device ios.DeviceEntry, bundleID string) (*afc.Client, error) { deviceConn, err := ios.ConnectToService(device, serviceName) if err != nil { - return &Connection{}, err + return nil, err } - err = afc.VendContainer(deviceConn, bundleID) + err = VendContainer(deviceConn, bundleID) if err != nil { - return &Connection{}, err + return nil, err } - return &Connection{deviceConn: deviceConn}, nil + return afc.NewAfcConnectionWithDeviceConnection(deviceConn), nil } func (c Connection) Close() { @@ -35,93 +36,51 @@ func (c Connection) Close() { } } -func (conn *Connection) SendFile(fileContents []byte, filePath string) error { - handle, err := conn.openFileForWriting(filePath) +func VendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { + plistCodec := ios.NewPlistCodec() + vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} + msg, err := plistCodec.Encode(vendContainer) if err != nil { - return err + return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) } - err = conn.sendFileContents(fileContents, handle) + err = deviceConn.Send(msg) if err != nil { return err } - return conn.closeHandle(handle) -} - -func (conn *Connection) ListFiles(filePath string) ([]string, error) { - headerPayload := []byte(filePath) - headerLength := uint64(len(headerPayload)) - - this_length := afc.Afc_header_size + headerLength - header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_read_dir, This_length: this_length, Entire_length: this_length} - conn.packageNumber++ - packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) + reader := deviceConn.Reader() + response, err := plistCodec.Decode(reader) if err != nil { - return []string{}, err + return err } - fileList := string(response.Payload) - return strings.Split(fileList, string([]byte{0})), nil + return checkResponse(response) } -func (conn *Connection) openFileForWriting(filePath string) (byte, error) { - pathBytes := []byte(filePath) - headerLength := 8 + uint64(len(pathBytes)) - headerPayload := make([]byte, headerLength) - binary.LittleEndian.PutUint64(headerPayload, afc.Afc_Mode_WRONLY) - copy(headerPayload[8:], pathBytes) - this_length := afc.Afc_header_size + headerLength - header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_open, This_length: this_length, Entire_length: this_length} - conn.packageNumber++ - packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) +func checkResponse(vendContainerResponseBytes []byte) error { + response, err := plistFromBytes(vendContainerResponseBytes) if err != nil { - return 0, err + return err } - if response.Header.Operation != afc.Afc_operation_file_open_result { - return 0, fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + if "Complete" == response.Status { + return nil } - return response.HeaderPayload[0], nil -} - -func (conn *Connection) sendAfcPacketAndAwaitResponse(packet afc.AfcPacket) (afc.AfcPacket, error) { - err := afc.Encode(packet, conn.deviceConn.Writer()) - if err != nil { - return afc.AfcPacket{}, err + if response.Error != "" { + return errors.New(response.Error) } - return afc.Decode(conn.deviceConn.Reader()) + return errors.New("unknown error during vendcontainer") } -func (conn *Connection) sendFileContents(fileContents []byte, handle byte) error { - headerPayload := make([]byte, 8) - headerPayload[0] = handle - header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_write, This_length: 8 + afc.Afc_header_size, Entire_length: 8 + afc.Afc_header_size + uint64(len(fileContents))} - conn.packageNumber++ - packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: fileContents} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) +func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { + var vendResponse vendContainerResponse + decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) + + err := decoder.Decode(&vendResponse) if err != nil { - return err - } - if response.Header.Operation != afc.Afc_operation_status { - return fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + return vendResponse, err } - return nil + return vendResponse, nil } -func (conn *Connection) closeHandle(handle byte) error { - headerPayload := make([]byte, 8) - headerPayload[0] = handle - this_length := 8 + afc.Afc_header_size - header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_close, This_length: this_length, Entire_length: this_length} - conn.packageNumber++ - packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if response.Header.Operation != afc.Afc_operation_status { - return fmt.Errorf("unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) - } - return nil +type vendContainerResponse struct { + Status string + Error string } From 392ebed348c8fb8c66a68922835d997968cb7c25 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Mon, 29 Sep 2025 13:55:38 +0200 Subject: [PATCH 05/13] use afc client for xcuitests --- ios/testmanagerd/xcuitestrunner.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ios/testmanagerd/xcuitestrunner.go b/ios/testmanagerd/xcuitestrunner.go index d9e955c7..fc73e82b 100644 --- a/ios/testmanagerd/xcuitestrunner.go +++ b/ios/testmanagerd/xcuitestrunner.go @@ -1,6 +1,7 @@ package testmanagerd import ( + "bytes" "context" "errors" "fmt" @@ -10,8 +11,8 @@ import ( "strings" "github.com/Masterminds/semver" + "github.com/danielpaulus/go-ios/ios/afc" "github.com/danielpaulus/go-ios/ios/appservice" - "github.com/danielpaulus/go-ios/ios/house_arrest" "github.com/danielpaulus/go-ios/ios" @@ -563,7 +564,7 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s return testSessionID, testConfigPath, testConfig, info, nil } -func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (string, nskeyedarchiver.XCTestConfiguration, error) { +func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *afc.Client, xctestConfigFileName string, testsToRun []string, testsToSkip []string, isXCTest bool, version *semver.Version) (string, nskeyedarchiver.XCTestConfiguration, error) { relativeXcTestConfigPath := path.Join("tmp", testSessionID.String()+".xctestconfiguration") xctestConfigPath := path.Join(info.testApp.homePath, relativeXcTestConfigPath) @@ -576,7 +577,15 @@ func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArres return "", nskeyedarchiver.XCTestConfiguration{}, err } - err = houseArrestService.SendFile([]byte(result), relativeXcTestConfigPath) + remoteFile, err := houseArrestService.Open(relativeXcTestConfigPath, afc.WRITE_ONLY_CREATE_TRUNC) + if err != nil { + return "", nskeyedarchiver.XCTestConfiguration{}, err + } + defer remoteFile.Close() + _, err = io.Copy(remoteFile, bytes.NewReader([]byte(result))) + if err != nil { + return "", nskeyedarchiver.XCTestConfiguration{}, err + } if err != nil { return "", nskeyedarchiver.XCTestConfiguration{}, err } From b27829896e64ba142aed04c1f7bd6e760b7f83fb Mon Sep 17 00:00:00 2001 From: David Missmann Date: Mon, 29 Sep 2025 14:54:08 +0200 Subject: [PATCH 06/13] use the afc client for all cli commands --- main.go | 52 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/main.go b/main.go index ea240dc6..cf0d039d 100644 --- a/main.go +++ b/main.go @@ -20,6 +20,7 @@ import ( "github.com/danielpaulus/go-ios/ios/debugproxy" "github.com/danielpaulus/go-ios/ios/deviceinfo" + "github.com/danielpaulus/go-ios/ios/house_arrest" "github.com/danielpaulus/go-ios/ios/tunnel" "github.com/danielpaulus/go-ios/ios/amfi" @@ -1087,11 +1088,11 @@ The commands work as following: b, _ = arguments.Bool("fsync") if b { containerBundleId, _ := arguments.String("--app") - var afcService *afc.Connection + var afcService *afc.Client if containerBundleId == "" { - afcService, err = afc.New(device) + afcService, err = afc.NewAfcConnection(device) } else { - afcService, err = afc.NewContainer(device, containerBundleId) + afcService, err = house_arrest.New(device, containerBundleId) } exitIfError("fsync: connect afc service failed", err) b, _ = arguments.Bool("rm") @@ -1099,9 +1100,9 @@ The commands work as following: path, _ := arguments.String("--path") isRecursive, _ := arguments.Bool("--r") if isRecursive { - err = afcService.RemoveAll(path) + err = afcService.DeleteRecursive(path) } else { - err = afcService.Remove(path) + err = afcService.Delete(path) } exitIfError("fsync: remove failed", err) } @@ -1109,14 +1110,26 @@ The commands work as following: b, _ = arguments.Bool("tree") if b { path, _ := arguments.String("--path") - err = afcService.TreeView(path, "", true) + err := afcService.WalkDir(path, func(path string, info afc.FileInfo, err error) error { + s := strings.Split(path, string(os.PathSeparator)) + _, f := filepath.Split(path) + prefix := strings.Repeat("| ", len(s)-1) + + suffix := "" + if info.Type == afc.S_IFDIR { + suffix = "/" + } + + fmt.Printf("%s|-%s%s\n", prefix, f, suffix) + return nil + }) exitIfError("fsync: tree view failed", err) } b, _ = arguments.Bool("mkdir") if b { path, _ := arguments.String("--path") - err = afcService.MkDir(path) + err = afcService.CreateDir(path) exitIfError("fsync: mkdir failed", err) } @@ -1131,15 +1144,30 @@ The commands work as following: exitIfError("mkdir failed", err) } } - dp = path.Join(dp, filepath.Base(sp)) - err = afcService.Pull(sp, dp) + + local, err := os.Create(path.Join(dp, filepath.Base(sp))) + exitIfError("failed to open local file", err) + defer local.Close() + remote, err := afcService.Open(sp, afc.READ_ONLY) + exitIfError("failed to open remote file", err) + defer remote.Close() + + _, err = io.Copy(local, remote) exitIfError("fsync: pull failed", err) } b, _ = arguments.Bool("push") if b { sp, _ := arguments.String("--srcPath") dp, _ := arguments.String("--dstPath") - err = afcService.Push(sp, dp) + + local, err := os.Open(sp) + exitIfError("failed to open local file", err) + defer local.Close() + remote, err := afcService.Open(dp, afc.WRITE_ONLY_CREATE_TRUNC) + exitIfError("failed to create remote file", err) + defer remote.Close() + + _, err = io.Copy(remote, local) exitIfError("fsync: push failed", err) } afcService.Close() @@ -1148,9 +1176,9 @@ The commands work as following: b, _ = arguments.Bool("diskspace") if b { - afcService, err := afc.New(device) + afcService, err := afc.NewAfcConnection(device) exitIfError("connect afc service failed", err) - info, err := afcService.GetSpaceInfo() + info, err := afcService.DeviceInfo() if err != nil { exitIfError("get device info push failed", err) } From 65bf4f49b6953c8d49a7f8cca8ed7a6ecce3e2a2 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Mon, 29 Sep 2025 21:58:01 +0200 Subject: [PATCH 07/13] use the afc client for device preparation --- ios/mcinstall/prepare.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ios/mcinstall/prepare.go b/ios/mcinstall/prepare.go index ade4be30..30b8e3b8 100644 --- a/ios/mcinstall/prepare.go +++ b/ios/mcinstall/prepare.go @@ -195,11 +195,12 @@ func setupSkipSetup(device ios.DeviceEntry) error { if err != nil { return err } - err = afcConn.RemovePathAndContents(skipSetupFilePath) + defer afcConn.Close() + err = afcConn.DeleteRecursive(skipSetupFilePath) if err != nil { log.Debug("skip setup: nothing to remove") } - err = afcConn.MkDir(skipSetupDirPath) + err = afcConn.CreateDir(skipSetupDirPath) if err != nil { log.Warn("error creating dir") } @@ -208,7 +209,7 @@ func setupSkipSetup(device ios.DeviceEntry) error { return err } if log.GetLevel() == log.DebugLevel { - f, _ := afcConn.ListFiles(skipSetupDirPath, "*") + f, _ := afcConn.List(skipSetupDirPath) log.Debugf("list of files %v", f) } return nil From f972d7bb1fa1b84e209a12777d1811553d376e5b Mon Sep 17 00:00:00 2001 From: David Missmann Date: Tue, 30 Sep 2025 10:10:56 +0200 Subject: [PATCH 08/13] return EOF if we don't get any data back --- ios/afc/client.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ios/afc/client.go b/ios/afc/client.go index 6f5a9e39..cbb3ac44 100644 --- a/ios/afc/client.go +++ b/ios/afc/client.go @@ -367,6 +367,9 @@ type File struct { } func (f *File) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } headerPayload := make([]byte, 16) binary.LittleEndian.PutUint64(headerPayload, f.handle) binary.LittleEndian.PutUint64(headerPayload[8:], uint64(len(p))) @@ -377,7 +380,11 @@ func (f *File) Read(p []byte) (int, error) { } resp, err := f.client.readPacket() copy(p, resp.Payload) - return len(resp.Payload), nil + l := len(resp.Payload) + if l == 0 { + return 0, io.EOF + } + return l, nil } func (f *File) Write(p []byte) (int, error) { From 4deb8455eb5335f31e82ed4f8e75aa573ac29e8f Mon Sep 17 00:00:00 2001 From: David Missmann Date: Tue, 30 Sep 2025 11:46:00 +0200 Subject: [PATCH 09/13] add some docs --- ios/afc/client.go | 60 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 55 insertions(+), 5 deletions(-) diff --git a/ios/afc/client.go b/ios/afc/client.go index cbb3ac44..90ff68a0 100644 --- a/ios/afc/client.go +++ b/ios/afc/client.go @@ -30,16 +30,23 @@ type Client struct { packetNum atomic.Int64 } +// NewAfcConnection creates a connection to the afc service func NewAfcConnection(d ios.DeviceEntry) (*Client, error) { deviceConn, err := ios.ConnectToService(d, serviceName) if err != nil { return nil, fmt.Errorf("error connecting to service '%s': %w", serviceName, err) } + return NewAfcConnectionWithDeviceConnection(deviceConn), nil +} + +// NewAfcConnectionWithDeviceConnection establishes a new AFC client connection from an existing device connection +func NewAfcConnectionWithDeviceConnection(d ios.DeviceConnectionInterface) *Client { return &Client{ - connection: deviceConn, - }, nil + connection: d, + } } +// Close the afc client func (c *Client) Close() error { err := c.connection.Close() if err != nil { @@ -73,6 +80,7 @@ func (c *Client) List(p string) ([]string, error) { return list, nil } +// Open opens a file with the specified name in the given mode func (c *Client) Open(p string, mode Mode) (*File, error) { pathBytes := []byte(p) pathBytes = append(pathBytes, 0) @@ -96,7 +104,7 @@ func (c *Client) Open(p string, mode Mode) (*File, error) { }, nil } -// CreateDir +// CreateDir creates a directory at the specified path func (c *Client) CreateDir(p string) error { headerPayload := []byte(p) headerPayload = append(headerPayload, 0) @@ -112,9 +120,25 @@ func (c *Client) CreateDir(p string) error { return nil } +// Delete deletes the file at the given path +// If the path is a non-empty directory, an error will be returned func (c *Client) Delete(p string) error { + return c.delete(p, false) +} + +// DeleteRecursive deletes the file at the given path +// If the path is a non-empty directory, the directory and its contents will be deleted +func (c *Client) DeleteRecursive(p string) error { + return c.delete(p, true) +} + +func (c *Client) delete(p string, recursive bool) error { headerPayload := []byte(p) - err := c.sendPacket(Afc_operation_remove_path_and_contents, headerPayload, nil) + var opcode = Afc_operation_remove_path + if recursive { + opcode = Afc_operation_remove_path_and_contents + } + err := c.sendPacket(opcode, headerPayload, nil) if err != nil { return fmt.Errorf("error deleting file: %w", err) } @@ -196,7 +220,9 @@ func (c *Client) readPacket() (packet, error) { if code == errSuccess { return p, nil } - return p, fmt.Errorf("error processing afc status: %d", code) + return p, afcError{ + code: int(code), + } } return p, nil @@ -218,6 +244,7 @@ type FileInfo struct { Size int64 } +// Stat retrieves information about a given file path func (c *Client) Stat(s string) (FileInfo, error) { err := c.sendPacket(Afc_operation_file_info, []byte(s), nil) if err != nil { @@ -271,9 +298,15 @@ func (c *Client) Stat(s string) (FileInfo, error) { return info, nil } +// WalkDir traverses the filesystem starting at the provided path +// It calls the WalkFunc for each file, and if the file is a directory, +// it recursively traverses the directory func (c *Client) WalkDir(p string, f WalkFunc) error { files, err := c.List(p) if err != nil { + if isPermissionDeniedError(err) { + return nil + } return err } @@ -284,6 +317,9 @@ func (c *Client) WalkDir(p string, f WalkFunc) error { } info, err := c.Stat(path.Join(p, file)) if err != nil { + if isPermissionDeniedError(err) { + continue + } return err } fnErr := f(path.Join(p, file), info, nil) @@ -306,6 +342,7 @@ func (c *Client) WalkDir(p string, f WalkFunc) error { return nil } +// DeviceInfo retrieves information about the filesystem of the device func (c *Client) DeviceInfo() (AFCDeviceInfo, error) { err := c.sendPacket(Afc_operation_device_info, nil, nil) if err != nil { @@ -425,3 +462,16 @@ const ( WRITE_ONLY_CREATE_APPEND = Mode(0x00000005) READ_WRITE_CREATE_APPEND = Mode(0x00000006) ) + +type afcError struct { + code int +} + +func (a afcError) Error() string { + return fmt.Sprintf("afc error code: %d", a.code) +} + +func isPermissionDeniedError(err error) bool { + var aError afcError + return errors.As(err, &aError) && aError.code == errPermDenied +} From d861090ca80c26254f82d272935537e4f10df036 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Wed, 1 Oct 2025 20:44:11 +0200 Subject: [PATCH 10/13] remove old code --- ios/afc/afc.go | 2 +- ios/afc/client.go | 64 +++-- ios/afc/client_test.go | 18 +- ios/afc/fsync.go | 503 ++------------------------------- ios/crashreport/crashreport.go | 2 +- ios/mcinstall/prepare.go | 4 +- main.go | 10 +- 7 files changed, 74 insertions(+), 529 deletions(-) diff --git a/ios/afc/afc.go b/ios/afc/afc.go index 0fd25ea7..519dddd9 100644 --- a/ios/afc/afc.go +++ b/ios/afc/afc.go @@ -65,7 +65,7 @@ const ( errDirNotEmpty = 33 ) -type AFCDeviceInfo struct { +type DeviceInfo struct { Model string TotalBytes uint64 FreeBytes uint64 diff --git a/ios/afc/client.go b/ios/afc/client.go index 90ff68a0..e4f9cec5 100644 --- a/ios/afc/client.go +++ b/ios/afc/client.go @@ -30,17 +30,17 @@ type Client struct { packetNum atomic.Int64 } -// NewAfcConnection creates a connection to the afc service -func NewAfcConnection(d ios.DeviceEntry) (*Client, error) { +// New creates a connection to the afc service +func New(d ios.DeviceEntry) (*Client, error) { deviceConn, err := ios.ConnectToService(d, serviceName) if err != nil { return nil, fmt.Errorf("error connecting to service '%s': %w", serviceName, err) } - return NewAfcConnectionWithDeviceConnection(deviceConn), nil + return NewFromConn(deviceConn), nil } -// NewAfcConnectionWithDeviceConnection establishes a new AFC client connection from an existing device connection -func NewAfcConnectionWithDeviceConnection(d ios.DeviceConnectionInterface) *Client { +// NewFromConn establishes a new AFC client connection from an existing device connection +func NewFromConn(d ios.DeviceConnectionInterface) *Client { return &Client{ connection: d, } @@ -104,8 +104,8 @@ func (c *Client) Open(p string, mode Mode) (*File, error) { }, nil } -// CreateDir creates a directory at the specified path -func (c *Client) CreateDir(p string) error { +// MkDir creates a directory at the specified path +func (c *Client) MkDir(p string) error { headerPayload := []byte(p) headerPayload = append(headerPayload, 0) @@ -120,19 +120,19 @@ func (c *Client) CreateDir(p string) error { return nil } -// Delete deletes the file at the given path +// Remove deletes the file at the given path // If the path is a non-empty directory, an error will be returned -func (c *Client) Delete(p string) error { - return c.delete(p, false) +func (c *Client) Remove(p string) error { + return c.remove(p, false) } -// DeleteRecursive deletes the file at the given path +// RemoveAll deletes the file at the given path // If the path is a non-empty directory, the directory and its contents will be deleted -func (c *Client) DeleteRecursive(p string) error { - return c.delete(p, true) +func (c *Client) RemoveAll(p string) error { + return c.remove(p, true) } -func (c *Client) delete(p string, recursive bool) error { +func (c *Client) remove(p string, recursive bool) error { headerPayload := []byte(p) var opcode = Afc_operation_remove_path if recursive { @@ -234,14 +234,24 @@ const ( // S_IFDIR marks a directory S_IFDIR FileType = "S_IFDIR" // S_IFDIR marks a regular file - S_IFMT FileType = "S_IFMT" + S_IFMT FileType = "S_IFMT" + S_IFLNK FileType = "S_IFLNK" ) type FileInfo struct { - Name string - Type FileType - Mode uint32 - Size int64 + Name string + Type FileType + Mode uint32 + Size int64 + LinkTarget string +} + +func (f FileInfo) IsDir() bool { + return f.Type == S_IFDIR +} + +func (f FileInfo) IsLink() bool { + return f.Type == S_IFLNK } // Stat retrieves information about a given file path @@ -286,6 +296,8 @@ func (c *Client) Stat(s string) (FileInfo, error) { case "st_mode": mode, _ := strconv.ParseUint(value, 8, 32) info.Mode = uint32(mode) + case "st_linktarget": + info.LinkTarget = value } } @@ -343,19 +355,19 @@ func (c *Client) WalkDir(p string, f WalkFunc) error { } // DeviceInfo retrieves information about the filesystem of the device -func (c *Client) DeviceInfo() (AFCDeviceInfo, error) { +func (c *Client) DeviceInfo() (DeviceInfo, error) { err := c.sendPacket(Afc_operation_device_info, nil, nil) if err != nil { - return AFCDeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + return DeviceInfo{}, fmt.Errorf("error getting device info: %w", err) } resp, err := c.readPacket() if err != nil { - return AFCDeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + return DeviceInfo{}, fmt.Errorf("error getting device info: %w", err) } bs := bytes.Split(resp.Payload, []byte{0}) - var info AFCDeviceInfo + var info DeviceInfo for i := 0; i+1 < len(bs); i += 2 { key := string(bs[i]) if key == "Model" { @@ -366,17 +378,17 @@ func (c *Client) DeviceInfo() (AFCDeviceInfo, error) { switch key { case "FSTotalBytes": if intParseErr != nil { - return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + return DeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) } info.TotalBytes = value case "FSFreeBytes": if intParseErr != nil { - return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + return DeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) } info.FreeBytes = value case "FSBlockSize": if intParseErr != nil { - return AFCDeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + return DeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) } info.BlockSize = value } diff --git a/ios/afc/client_test.go b/ios/afc/client_test.go index d323a2d8..9cf89305 100644 --- a/ios/afc/client_test.go +++ b/ios/afc/client_test.go @@ -30,7 +30,7 @@ func TestAfc(t *testing.T) { t.Run(fmt.Sprintf("device %s", device.Properties.SerialNumber), func(t *testing.T) { - client, err := NewAfcConnection(device) + client, err := New(device) assert.NoError(t, err) defer client.Close() @@ -52,7 +52,7 @@ func TestAfc(t *testing.T) { return strings.Contains(s, "test-file") }) if hasFile { - err = client.Delete("./test-file") + err = client.Remove("./test-file") assert.NoError(t, err) } f, err := client.Open("./test-file", READ_WRITE_CREATE_TRUNC) @@ -61,7 +61,7 @@ func TestAfc(t *testing.T) { err = f.Close() assert.NoError(t, err) - err = client.Delete("./test-file") + err = client.Remove("./test-file") assert.NoError(t, err) }) @@ -80,7 +80,7 @@ func TestAfc(t *testing.T) { assert.EqualValues(t, 4, info.Size) - err = client.Delete("./test-file") + err = client.Remove("./test-file") assert.NoError(t, err) }) @@ -103,12 +103,12 @@ func TestAfc(t *testing.T) { assert.NoError(t, err) assert.Equal(t, []byte("test"), b[:n]) - err = client.Delete("./test-file") + err = client.Remove("./test-file") assert.NoError(t, err) }) - t.Run("created and delete nested directory", func(t *testing.T) { - err = client.CreateDir("./some/nested/directory") + t.Run("create and delete nested directory", func(t *testing.T) { + err = client.MkDir("./some/nested/directory") assert.NoError(t, err) var info FileInfo @@ -124,7 +124,7 @@ func TestAfc(t *testing.T) { assert.NoError(t, err) assert.Equal(t, S_IFDIR, info.Type) - err = client.Delete("./some") + err = client.RemoveAll("./some") assert.NoError(t, err) _, err = client.Stat("./some") @@ -206,7 +206,7 @@ func TestAfc(t *testing.T) { } func mustCreateDir(c *Client, dir string) { - err := c.CreateDir(dir) + err := c.MkDir(dir) if err != nil { panic(err) } diff --git a/ios/afc/fsync.go b/ios/afc/fsync.go index bcc6a8b9..584b9fff 100644 --- a/ios/afc/fsync.go +++ b/ios/afc/fsync.go @@ -1,452 +1,30 @@ package afc import ( - "bytes" - "encoding/binary" - "errors" "fmt" "io" "os" "path" "path/filepath" - "strconv" - "strings" "github.com/danielpaulus/go-ios/ios" - log "github.com/sirupsen/logrus" - "howett.net/plist" ) const serviceName = "com.apple.afc" -type Connection struct { - deviceConn ios.DeviceConnectionInterface - packageNumber uint64 -} - -type statInfo struct { - stSize int64 - stBlocks int64 - stCtime int64 - stMtime int64 - stNlink string - stIfmt string - stLinktarget string -} - -func (s *statInfo) IsDir() bool { - return s.stIfmt == "S_IFDIR" -} - -func (s *statInfo) IsLink() bool { - return s.stIfmt == "S_IFLNK" -} - -func New(device ios.DeviceEntry) (*Connection, error) { - deviceConn, err := ios.ConnectToService(device, serviceName) - if err != nil { - return nil, err - } - return &Connection{deviceConn: deviceConn}, nil -} - -func NewContainer(device ios.DeviceEntry, bundleID string) (*Connection, error) { - deviceConn, err := ios.ConnectToService(device, "com.apple.mobile.house_arrest") - if err != nil { - return nil, err - } - err = VendContainer(deviceConn, bundleID) - if err != nil { - return nil, err - } - return &Connection{deviceConn: deviceConn}, nil -} - -func VendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { - plistCodec := ios.NewPlistCodec() - vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} - msg, err := plistCodec.Encode(vendContainer) - if err != nil { - return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) - } - err = deviceConn.Send(msg) - if err != nil { - return err - } - reader := deviceConn.Reader() - response, err := plistCodec.Decode(reader) - if err != nil { - return err - } - return checkResponse(response) -} - -func checkResponse(vendContainerResponseBytes []byte) error { - response, err := plistFromBytes(vendContainerResponseBytes) - if err != nil { - return err - } - if "Complete" == response.Status { - return nil - } - if response.Error != "" { - return errors.New(response.Error) - } - return errors.New("unknown error during vendcontainer") -} - -func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { - var vendResponse vendContainerResponse - decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) - - err := decoder.Decode(&vendResponse) - if err != nil { - return vendResponse, err - } - return vendResponse, nil -} - -type vendContainerResponse struct { - Status string - Error string -} - -// NewFromConn allows to use AFC on a DeviceConnectionInterface, see crashreport for an example -func NewFromConn(deviceConn ios.DeviceConnectionInterface) *Connection { - return &Connection{deviceConn: deviceConn} -} - -func (conn *Connection) sendAfcPacketAndAwaitResponse(packet AfcPacket) (AfcPacket, error) { - err := Encode(packet, conn.deviceConn.Writer()) - if err != nil { - return AfcPacket{}, err - } - return Decode(conn.deviceConn.Reader()) -} - -func (conn *Connection) checkOperationStatus(packet AfcPacket) error { - if packet.Header.Operation == Afc_operation_status { - errorCode := binary.LittleEndian.Uint64(packet.HeaderPayload) - if errorCode != errSuccess { - return getError(errorCode) - } - } - return nil -} - -func (conn *Connection) Remove(path string) error { - headerPayload := []byte(path) - headerLength := uint64(len(headerPayload)) - thisLength := Afc_header_size + headerLength - - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_remove_path, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("remove: unexpected afc status: %v", err) - } - return nil -} - -func (conn *Connection) RemovePathAndContents(path string) error { - headerPayload := []byte(path) - headerPayload = append(headerPayload, 0) - headerLength := uint64(len(headerPayload)) - thisLength := Afc_header_size + headerLength - - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_remove_path_and_contents, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("remove: unexpected afc status: %v", err) - } - return nil -} - -func (conn *Connection) RemoveAll(srcPath string) error { - fileInfo, err := conn.Stat(srcPath) - if err != nil { - return err - } - if fileInfo.IsDir() { - fileList, err := conn.listDir(srcPath) - if err != nil { - return err - } - for _, v := range fileList { - sp := path.Join(srcPath, v) - err = conn.RemoveAll(sp) - if err != nil { - return err - } - } - } - return conn.Remove(srcPath) -} - -func (conn *Connection) MkDir(path string) error { - headerPayload := []byte(path) - headerPayload = append(headerPayload, 0) - headerLength := uint64(len(headerPayload)) - thisLength := Afc_header_size + headerLength - - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_make_dir, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("mkdir: unexpected afc status: %v", err) - } - return nil -} - -func (conn *Connection) Stat(path string) (*statInfo, error) { - headerPayload := []byte(path) - headerLength := uint64(len(headerPayload)) - thisLength := Afc_header_size + headerLength - - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_info, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return nil, err - } - if err = conn.checkOperationStatus(response); err != nil { - return nil, fmt.Errorf("stat: unexpected afc status: %v", err) - } - ret := bytes.Split(response.Payload, []byte{0}) - retLen := len(ret) - if retLen%2 != 0 { - retLen = retLen - 1 - } - statInfoMap := make(map[string]string) - for i := 0; i <= retLen-2; i = i + 2 { - k := string(ret[i]) - v := string(ret[i+1]) - statInfoMap[k] = v - } - - var si statInfo - si.stSize, _ = strconv.ParseInt(statInfoMap["st_size"], 10, 64) - si.stBlocks, _ = strconv.ParseInt(statInfoMap["st_blocks"], 10, 64) - si.stCtime, _ = strconv.ParseInt(statInfoMap["st_birthtime"], 10, 64) - si.stMtime, _ = strconv.ParseInt(statInfoMap["st_mtime"], 10, 64) - si.stNlink = statInfoMap["st_nlink"] - si.stIfmt = statInfoMap["st_ifmt"] - si.stLinktarget = statInfoMap["st_linktarget"] - return &si, nil -} - -func (conn *Connection) listDir(path string) ([]string, error) { - headerPayload := []byte(path) - headerLength := uint64(len(headerPayload)) - thisLength := Afc_header_size + headerLength - - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_read_dir, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return nil, err - } - if err = conn.checkOperationStatus(response); err != nil { - return nil, fmt.Errorf("list dir: unexpected afc status: %v", err) - } - ret := bytes.Split(response.Payload, []byte{0}) - var fileList []string - for _, v := range ret { - if string(v) != "." && string(v) != ".." && string(v) != "" { - fileList = append(fileList, string(v)) - } - } - return fileList, nil -} - -func (conn *Connection) GetSpaceInfo() (*AFCDeviceInfo, error) { - thisLength := Afc_header_size - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_device_info, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: nil, Payload: nil} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return nil, err - } - if err = conn.checkOperationStatus(response); err != nil { - return nil, fmt.Errorf("mkdir: unexpected afc status: %v", err) - } - - bs := bytes.Split(response.Payload, []byte{0}) - strs := make([]string, len(bs)-1) - for i := 0; i < len(strs); i++ { - strs[i] = string(bs[i]) - } - m := make(map[string]string) - if strs != nil { - for i := 0; i < len(strs); i += 2 { - m[strs[i]] = strs[i+1] - } - } - - totalBytes, err := strconv.ParseUint(m["FSTotalBytes"], 10, 64) - if err != nil { - return nil, err - } - freeBytes, err := strconv.ParseUint(m["FSFreeBytes"], 10, 64) - if err != nil { - return nil, err - } - blockSize, err := strconv.ParseUint(m["FSBlockSize"], 10, 64) - if err != nil { - return nil, err - } - - return &AFCDeviceInfo{ - Model: m["Model"], - TotalBytes: totalBytes, - FreeBytes: freeBytes, - BlockSize: blockSize, - }, nil -} - -// ListFiles returns all files in the given directory, matching the pattern. -// Example: ListFiles(".", "*") returns all files and dirs in the current path the afc connection is in -func (conn *Connection) ListFiles(cwd string, matchPattern string) ([]string, error) { - headerPayload := []byte(cwd) - headerLength := uint64(len(headerPayload)) - - thisLength := Afc_header_size + headerLength - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_read_dir, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return nil, err - } - fileList := string(response.Payload) - files := strings.Split(fileList, string([]byte{0})) - var filteredFiles []string - for _, f := range files { - if f == "" { - continue - } - matches, err := filepath.Match(matchPattern, f) - if err != nil { - log.Warn("error while matching pattern", err) - } - if matches { - filteredFiles = append(filteredFiles, f) - } - } - return filteredFiles, nil -} - -func (conn *Connection) TreeView(dpath string, prefix string, treePoint bool) error { - fileInfo, err := conn.Stat(dpath) - if err != nil { - return err - } - namePrefix := "`--" - if !treePoint { - namePrefix = "|--" - } - tPrefix := prefix + namePrefix - if fileInfo.IsDir() { - fmt.Printf("%s %s/\n", tPrefix, filepath.Base(dpath)) - fileList, err := conn.listDir(dpath) - if err != nil { - return err - } - for i, v := range fileList { - tp := false - if i == len(fileList)-1 { - tp = true - } - rp := prefix + " " - if !treePoint { - rp = prefix + "| " - } - nPath := path.Join(dpath, v) - err = conn.TreeView(nPath, rp, tp) - if err != nil { - return err - } - } - } else { - fmt.Printf("%s %s\n", tPrefix, filepath.Base(dpath)) - } - return nil -} - -func (conn *Connection) OpenFile(path string, mode uint64) (uint64, error) { - pathBytes := []byte(path) - pathBytes = append(pathBytes, 0) - headerLength := 8 + uint64(len(pathBytes)) - headerPayload := make([]byte, headerLength) - binary.LittleEndian.PutUint64(headerPayload, mode) - copy(headerPayload[8:], pathBytes) - thisLength := Afc_header_size + headerLength - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_open, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return 0, err - } - if err = conn.checkOperationStatus(response); err != nil { - return 0, fmt.Errorf("open file: unexpected afc status: %v", err) - } - fd := binary.LittleEndian.Uint64(response.HeaderPayload) - if fd == 0 { - return 0, fmt.Errorf("file descriptor should not be zero") - } - - return fd, nil -} - -func (conn *Connection) CloseFile(fd uint64) error { - headerPayload := make([]byte, 8) - binary.LittleEndian.PutUint64(headerPayload, fd) - thisLength := 8 + Afc_header_size - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_close, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("close file: unexpected afc status: %v", err) - } - return nil -} - -func (conn *Connection) PullSingleFile(srcPath, dstPath string) error { - fileInfo, err := conn.Stat(srcPath) +func (c *Client) PullSingleFile(srcPath, dstPath string) error { + fileInfo, err := c.Stat(srcPath) if err != nil { return err } if fileInfo.IsLink() { - srcPath = fileInfo.stLinktarget + srcPath = fileInfo.LinkTarget } - fd, err := conn.OpenFile(srcPath, Afc_Mode_RDONLY) + fd, err := c.Open(srcPath, READ_ONLY) if err != nil { return err } - defer conn.CloseFile(fd) + defer fd.Close() f, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) if err != nil { @@ -454,30 +32,11 @@ func (conn *Connection) PullSingleFile(srcPath, dstPath string) error { } defer f.Close() - leftSize := fileInfo.stSize - maxReadSize := 64 * 1024 - for leftSize > 0 { - headerPayload := make([]byte, 16) - binary.LittleEndian.PutUint64(headerPayload, fd) - thisLength := Afc_header_size + 16 - binary.LittleEndian.PutUint64(headerPayload[8:], uint64(maxReadSize)) - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_read, This_length: thisLength, Entire_length: thisLength} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("read file: unexpected afc status: %v", err) - } - leftSize = leftSize - int64(len(response.Payload)) - f.Write(response.Payload) - } - return nil + _, err = io.Copy(f, fd) + return err } -func (conn *Connection) Pull(srcPath, dstPath string) error { +func (conn *Client) Pull(srcPath, dstPath string) error { fileInfo, err := conn.Stat(srcPath) if err != nil { return err @@ -490,7 +49,7 @@ func (conn *Connection) Pull(srcPath, dstPath string) error { return err } } - fileList, err := conn.listDir(srcPath) + fileList, err := conn.List(srcPath) if err != nil { return err } @@ -508,7 +67,7 @@ func (conn *Connection) Pull(srcPath, dstPath string) error { return nil } -func (conn *Connection) Push(srcPath, dstPath string) error { +func (conn *Client) Push(srcPath, dstPath string) error { ret, _ := ios.PathExists(srcPath) if !ret { return fmt.Errorf("%s: no such file.", srcPath) @@ -520,7 +79,7 @@ func (conn *Connection) Push(srcPath, dstPath string) error { } defer f.Close() - if fileInfo, _ := conn.Stat(dstPath); fileInfo != nil { + if fileInfo, err := conn.Stat(dstPath); err == nil { if fileInfo.IsDir() { dstPath = path.Join(dstPath, filepath.Base(srcPath)) } @@ -529,48 +88,22 @@ func (conn *Connection) Push(srcPath, dstPath string) error { return conn.WriteToFile(f, dstPath) } -func (conn *Connection) WriteToFile(reader io.Reader, dstPath string) error { - if fileInfo, _ := conn.Stat(dstPath); fileInfo != nil { +func (conn *Client) WriteToFile(reader io.Reader, dstPath string) error { + if fileInfo, err := conn.Stat(dstPath); err == nil { if fileInfo.IsDir() { return fmt.Errorf("%s is a directory, cannot write to it as file", dstPath) } } - fd, err := conn.OpenFile(dstPath, Afc_Mode_WR) + fd, err := conn.Open(dstPath, WRITE_ONLY_CREATE_TRUNC) if err != nil { return err } - defer conn.CloseFile(fd) - - maxWriteSize := 64 * 1024 - chunk := make([]byte, maxWriteSize) - for { - n, err := reader.Read(chunk) - if err != nil && err != io.EOF { - return err - } - if n == 0 { - break - } - bytesRead := chunk[:n] - headerPayload := make([]byte, 8) - binary.LittleEndian.PutUint64(headerPayload, fd) - thisLength := Afc_header_size + 8 - header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_write, This_length: thisLength, Entire_length: thisLength + uint64(n)} - conn.packageNumber++ - packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: bytesRead} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if err = conn.checkOperationStatus(response); err != nil { - return fmt.Errorf("write file: unexpected afc status: %v", err) - } + defer fd.Close() + _, err = io.Copy(fd, reader) + if err != nil { + return err } return nil } - -func (conn *Connection) Close() { - conn.deviceConn.Close() -} diff --git a/ios/crashreport/crashreport.go b/ios/crashreport/crashreport.go index ef501f23..74c56c38 100644 --- a/ios/crashreport/crashreport.go +++ b/ios/crashreport/crashreport.go @@ -88,7 +88,7 @@ func RemoveReports(device ios.DeviceEntry, cwd string, pattern string) error { if !matched { return nil } - return afcClient.Delete(path) + return afcClient.Remove(path) }) } diff --git a/ios/mcinstall/prepare.go b/ios/mcinstall/prepare.go index 30b8e3b8..889e36f6 100644 --- a/ios/mcinstall/prepare.go +++ b/ios/mcinstall/prepare.go @@ -196,11 +196,11 @@ func setupSkipSetup(device ios.DeviceEntry) error { return err } defer afcConn.Close() - err = afcConn.DeleteRecursive(skipSetupFilePath) + err = afcConn.RemoveAll(skipSetupFilePath) if err != nil { log.Debug("skip setup: nothing to remove") } - err = afcConn.CreateDir(skipSetupDirPath) + err = afcConn.MkDir(skipSetupDirPath) if err != nil { log.Warn("error creating dir") } diff --git a/main.go b/main.go index cf0d039d..835ee321 100644 --- a/main.go +++ b/main.go @@ -1090,7 +1090,7 @@ The commands work as following: containerBundleId, _ := arguments.String("--app") var afcService *afc.Client if containerBundleId == "" { - afcService, err = afc.NewAfcConnection(device) + afcService, err = afc.New(device) } else { afcService, err = house_arrest.New(device, containerBundleId) } @@ -1100,9 +1100,9 @@ The commands work as following: path, _ := arguments.String("--path") isRecursive, _ := arguments.Bool("--r") if isRecursive { - err = afcService.DeleteRecursive(path) + err = afcService.RemoveAll(path) } else { - err = afcService.Delete(path) + err = afcService.Remove(path) } exitIfError("fsync: remove failed", err) } @@ -1129,7 +1129,7 @@ The commands work as following: b, _ = arguments.Bool("mkdir") if b { path, _ := arguments.String("--path") - err = afcService.CreateDir(path) + err = afcService.MkDir(path) exitIfError("fsync: mkdir failed", err) } @@ -1176,7 +1176,7 @@ The commands work as following: b, _ = arguments.Bool("diskspace") if b { - afcService, err := afc.NewAfcConnection(device) + afcService, err := afc.New(device) exitIfError("connect afc service failed", err) info, err := afcService.DeviceInfo() if err != nil { From f494634e6945b3382e3680cd3bd4758e995c3862 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Wed, 1 Oct 2025 21:39:08 +0200 Subject: [PATCH 11/13] cleanup --- ios/house_arrest/house_arrest.go | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/ios/house_arrest/house_arrest.go b/ios/house_arrest/house_arrest.go index daf266db..8e4de821 100644 --- a/ios/house_arrest/house_arrest.go +++ b/ios/house_arrest/house_arrest.go @@ -13,35 +13,24 @@ import ( const serviceName = "com.apple.mobile.house_arrest" -type Connection struct { - deviceConn ios.DeviceConnectionInterface - packageNumber uint64 -} - func New(device ios.DeviceEntry, bundleID string) (*afc.Client, error) { deviceConn, err := ios.ConnectToService(device, serviceName) if err != nil { return nil, err } - err = VendContainer(deviceConn, bundleID) + err = vendContainer(deviceConn, bundleID) if err != nil { return nil, err } - return afc.NewAfcConnectionWithDeviceConnection(deviceConn), nil -} - -func (c Connection) Close() { - if c.deviceConn != nil { - c.deviceConn.Close() - } + return afc.NewFromConn(deviceConn), nil } -func VendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { +func vendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { plistCodec := ios.NewPlistCodec() - vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} + vendContainer := map[string]interface{}{"Command": "vendContainer", "Identifier": bundleID} msg, err := plistCodec.Encode(vendContainer) if err != nil { - return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) + return fmt.Errorf("vendContainer Encoding cannot fail unless the encoder is broken: %v", err) } err = deviceConn.Send(msg) if err != nil { From 0a88bd7b2264be31250f1063cfb549410c94273b Mon Sep 17 00:00:00 2001 From: David Missmann Date: Thu, 2 Oct 2025 09:34:11 +0200 Subject: [PATCH 12/13] use correct initializers --- ios/afc/errors.go | 5 +++++ ios/crashreport/crashreport.go | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 ios/afc/errors.go diff --git a/ios/afc/errors.go b/ios/afc/errors.go new file mode 100644 index 00000000..79058077 --- /dev/null +++ b/ios/afc/errors.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/ios/crashreport/crashreport.go b/ios/crashreport/crashreport.go index 74c56c38..c3fef115 100644 --- a/ios/crashreport/crashreport.go +++ b/ios/crashreport/crashreport.go @@ -32,7 +32,7 @@ func DownloadReports(device ios.DeviceEntry, pattern string, targetdir string) e if err != nil { return err } - afcConn := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + afcConn := afc.NewFromConn(deviceConn) err = afcConn.WalkDir(".", func(p string, info afc.FileInfo, err error) error { matched, err := filepath.Match(pattern, p) if err != nil { @@ -76,7 +76,7 @@ func RemoveReports(device ios.DeviceEntry, cwd string, pattern string) error { if err != nil { return err } - afcClient := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + afcClient := afc.NewFromConn(deviceConn) return afcClient.WalkDir(".", func(path string, info afc.FileInfo, err error) error { if info.Type == afc.S_IFDIR { return nil @@ -101,7 +101,7 @@ func ListReports(device ios.DeviceEntry, pattern string) ([]string, error) { if err != nil { return []string{}, err } - afcClient := afc.NewAfcConnectionWithDeviceConnection(deviceConn) + afcClient := afc.NewFromConn(deviceConn) var files []string err = afcClient.WalkDir(".", func(path string, info afc.FileInfo, err error) error { From e6388a0f56a9e79939418fd3eb1997d0a6696900 Mon Sep 17 00:00:00 2001 From: David Missmann Date: Thu, 2 Oct 2025 09:36:19 +0200 Subject: [PATCH 13/13] create own type for opcodes --- ios/afc/afc.go | 196 ++++------------------------------------------ ios/afc/client.go | 46 ++++------- ios/afc/errors.go | 117 ++++++++++++++++++++++++++- 3 files changed, 147 insertions(+), 212 deletions(-) diff --git a/ios/afc/afc.go b/ios/afc/afc.go index 519dddd9..56fb7b09 100644 --- a/ios/afc/afc.go +++ b/ios/afc/afc.go @@ -1,189 +1,23 @@ package afc -import ( - "encoding/binary" - "errors" - "fmt" - "io" -) - const ( - Afc_magic uint64 = 0x4141504c36414643 - Afc_header_size uint64 = 40 - Afc_operation_status uint64 = 0x00000001 - Afc_operation_data uint64 = 0x00000002 - Afc_operation_read_dir uint64 = 0x00000003 - Afc_operation_remove_path uint64 = 0x00000008 - Afc_operation_make_dir uint64 = 0x00000009 - Afc_operation_file_info uint64 = 0x0000000A - Afc_operation_device_info uint64 = 0x0000000B - Afc_operation_file_open uint64 = 0x0000000D - Afc_operation_file_close uint64 = 0x00000014 - Afc_operation_file_write uint64 = 0x00000010 - Afc_operation_file_open_result uint64 = 0x0000000E - Afc_operation_file_read uint64 = 0x0000000F - Afc_operation_remove_path_and_contents uint64 = 0x00000022 + magic uint64 = 0x4141504c36414643 + headerSize uint64 = 40 ) -const ( - Afc_Mode_RDONLY uint64 = 0x00000001 - Afc_Mode_RW uint64 = 0x00000002 - Afc_Mode_WRONLY uint64 = 0x00000003 - Afc_Mode_WR uint64 = 0x00000004 - Afc_Mode_APPEND uint64 = 0x00000005 - Afc_Mode_RDAPPEND uint64 = 0x00000006 -) +type opcode uint64 const ( - errSuccess = 0 - errUnknown = 1 - errOperationHeaderInvalid = 2 - errNoResources = 3 - errReadError = 4 - errWriteError = 5 - errUnknownPacketType = 6 - errInvalidArgument = 7 - errObjectNotFound = 8 - errObjectIsDir = 9 - errPermDenied = 10 - errServiceNotConnected = 11 - errOperationTimeout = 12 - errTooMuchData = 13 - errEndOfData = 14 - errOperationNotSupported = 15 - errObjectExists = 16 - errObjectBusy = 17 - errNoSpaceLeft = 18 - errOperationWouldBlock = 19 - errIoError = 20 - errOperationInterrupted = 21 - errOperationInProgress = 22 - errInternalError = 23 - errMuxError = 30 - errNoMemory = 31 - errNotEnoughData = 32 - errDirNotEmpty = 33 + status opcode = 0x00000001 + readDir opcode = 0x00000003 + removePath opcode = 0x00000008 + makeDir opcode = 0x00000009 + fileInfo opcode = 0x0000000A + deviceInfo opcode = 0x0000000B + fileOpen opcode = 0x0000000D + fileClose opcode = 0x00000014 + fileWrite opcode = 0x00000010 + fileOpenResult opcode = 0x0000000E + fileRead opcode = 0x0000000F + removePathAndContents opcode = 0x00000022 ) - -type DeviceInfo struct { - Model string - TotalBytes uint64 - FreeBytes uint64 - BlockSize uint64 -} - -func getError(errorCode uint64) error { - switch errorCode { - case errUnknown: - return errors.New("UnknownError") - case errOperationHeaderInvalid: - return errors.New("OperationHeaderInvalid") - case errNoResources: - return errors.New("NoResources") - case errReadError: - return errors.New("ReadError") - case errWriteError: - return errors.New("WriteError") - case errUnknownPacketType: - return errors.New("UnknownPacketType") - case errInvalidArgument: - return errors.New("InvalidArgument") - case errObjectNotFound: - return errors.New("ObjectNotFound") - case errObjectIsDir: - return errors.New("ObjectIsDir") - case errPermDenied: - return errors.New("PermDenied") - case errServiceNotConnected: - return errors.New("ServiceNotConnected") - case errOperationTimeout: - return errors.New("OperationTimeout") - case errTooMuchData: - return errors.New("TooMuchData") - case errEndOfData: - return errors.New("EndOfData") - case errOperationNotSupported: - return errors.New("OperationNotSupported") - case errObjectExists: - return errors.New("ObjectExists") - case errObjectBusy: - return errors.New("ObjectBusy") - case errNoSpaceLeft: - return errors.New("NoSpaceLeft") - case errOperationWouldBlock: - return errors.New("OperationWouldBlock") - case errIoError: - return errors.New("IoError") - case errOperationInterrupted: - return errors.New("OperationInterrupted") - case errOperationInProgress: - return errors.New("OperationInProgress") - case errInternalError: - return errors.New("InternalError") - case errMuxError: - return errors.New("MuxError") - case errNoMemory: - return errors.New("NoMemory") - case errNotEnoughData: - return errors.New("NotEnoughData") - case errDirNotEmpty: - return errors.New("DirNotEmpty") - default: - return nil - } -} - -type AfcPacketHeader struct { - Magic uint64 - Entire_length uint64 - This_length uint64 - Packet_num uint64 - Operation uint64 -} - -type AfcPacket struct { - Header AfcPacketHeader - HeaderPayload []byte - Payload []byte -} - -func Decode(reader io.Reader) (AfcPacket, error) { - var header AfcPacketHeader - err := binary.Read(reader, binary.LittleEndian, &header) - if err != nil { - return AfcPacket{}, err - } - if header.Magic != Afc_magic { - return AfcPacket{}, fmt.Errorf("Wrong magic:%x expected: %x", header.Magic, Afc_magic) - } - headerPayloadLength := header.This_length - Afc_header_size - headerPayload := make([]byte, headerPayloadLength) - _, err = io.ReadFull(reader, headerPayload) - if err != nil { - return AfcPacket{}, err - } - contentPayloadLength := header.Entire_length - header.This_length - payload := make([]byte, contentPayloadLength) - _, err = io.ReadFull(reader, payload) - if err != nil { - return AfcPacket{}, err - } - return AfcPacket{header, headerPayload, payload}, nil -} - -func Encode(packet AfcPacket, writer io.Writer) error { - err := binary.Write(writer, binary.LittleEndian, packet.Header) - if err != nil { - return err - } - _, err = writer.Write(packet.HeaderPayload) - if err != nil { - return err - } - - _, err = writer.Write(packet.Payload) - if err != nil { - return err - } - return nil -} diff --git a/ios/afc/client.go b/ios/afc/client.go index e4f9cec5..983c7603 100644 --- a/ios/afc/client.go +++ b/ios/afc/client.go @@ -57,7 +57,7 @@ func (c *Client) Close() error { // List all entries of the provided path func (c *Client) List(p string) ([]string, error) { - err := c.sendPacket(Afc_operation_read_dir, []byte(p), nil) + err := c.sendPacket(readDir, []byte(p), nil) if err != nil { return nil, fmt.Errorf("error listing afc dir: %w", err) } @@ -88,7 +88,7 @@ func (c *Client) Open(p string, mode Mode) (*File, error) { headerPayload := make([]byte, headerLength) binary.LittleEndian.PutUint64(headerPayload, uint64(mode)) copy(headerPayload[8:], pathBytes) - err := c.sendPacket(Afc_operation_file_open, headerPayload, nil) + err := c.sendPacket(fileOpen, headerPayload, nil) if err != nil { return nil, fmt.Errorf("error opening file: %w", err) } @@ -109,7 +109,7 @@ func (c *Client) MkDir(p string) error { headerPayload := []byte(p) headerPayload = append(headerPayload, 0) - err := c.sendPacket(Afc_operation_make_dir, headerPayload, nil) + err := c.sendPacket(makeDir, headerPayload, nil) if err != nil { return fmt.Errorf("error creating dir: %w", err) } @@ -134,9 +134,9 @@ func (c *Client) RemoveAll(p string) error { func (c *Client) remove(p string, recursive bool) error { headerPayload := []byte(p) - var opcode = Afc_operation_remove_path + var opcode = removePath if recursive { - opcode = Afc_operation_remove_path_and_contents + opcode = removePathAndContents } err := c.sendPacket(opcode, headerPayload, nil) if err != nil { @@ -149,13 +149,13 @@ func (c *Client) remove(p string, recursive bool) error { return nil } -func (c *Client) sendPacket(operation uint64, headerPayload []byte, payload []byte) error { +func (c *Client) sendPacket(operation opcode, headerPayload []byte, payload []byte) error { num := c.packetNum.Add(1) - thisLen := Afc_header_size + uint64(len(headerPayload)) + thisLen := headerSize + uint64(len(headerPayload)) p := packet{ Header: header{ - Magic: Afc_magic, + Magic: magic, EntireLen: thisLen + uint64(len(payload)), ThisLen: thisLen, PacketNum: uint64(num), @@ -190,7 +190,7 @@ func (c *Client) readPacket() (packet, error) { if err != nil { return packet{}, fmt.Errorf("error reading header: %w", err) } - headerPayloadLen := h.ThisLen - Afc_header_size + headerPayloadLen := h.ThisLen - headerSize payloadLen := h.EntireLen - h.ThisLen headerpayload := make([]byte, headerPayloadLen) @@ -215,7 +215,7 @@ func (c *Client) readPacket() (packet, error) { } } - if p.Header.Operation == Afc_operation_status { + if p.Header.Operation == status { code := binary.LittleEndian.Uint64(p.HeaderPayload) if code == errSuccess { return p, nil @@ -256,7 +256,7 @@ func (f FileInfo) IsLink() bool { // Stat retrieves information about a given file path func (c *Client) Stat(s string) (FileInfo, error) { - err := c.sendPacket(Afc_operation_file_info, []byte(s), nil) + err := c.sendPacket(fileInfo, []byte(s), nil) if err != nil { return FileInfo{}, fmt.Errorf("error getting file info: %w", err) } @@ -356,7 +356,7 @@ func (c *Client) WalkDir(p string, f WalkFunc) error { // DeviceInfo retrieves information about the filesystem of the device func (c *Client) DeviceInfo() (DeviceInfo, error) { - err := c.sendPacket(Afc_operation_device_info, nil, nil) + err := c.sendPacket(deviceInfo, nil, nil) if err != nil { return DeviceInfo{}, fmt.Errorf("error getting device info: %w", err) } @@ -401,7 +401,7 @@ type header struct { EntireLen uint64 ThisLen uint64 PacketNum uint64 - Operation uint64 + Operation opcode } type packet struct { @@ -410,6 +410,7 @@ type packet struct { Payload []byte } +// File is a reference to a file on the devices filesystem type File struct { client *Client handle uint64 @@ -423,7 +424,7 @@ func (f *File) Read(p []byte) (int, error) { binary.LittleEndian.PutUint64(headerPayload, f.handle) binary.LittleEndian.PutUint64(headerPayload[8:], uint64(len(p))) - err := f.client.sendPacket(Afc_operation_file_read, headerPayload, nil) + err := f.client.sendPacket(fileRead, headerPayload, nil) if err != nil { return 0, fmt.Errorf("error reading data: %w", err) } @@ -439,7 +440,7 @@ func (f *File) Read(p []byte) (int, error) { func (f *File) Write(p []byte) (int, error) { headerPayload := make([]byte, 8) binary.LittleEndian.PutUint64(headerPayload, f.handle) - err := f.client.sendPacket(Afc_operation_file_write, headerPayload, p) + err := f.client.sendPacket(fileWrite, headerPayload, p) if err != nil { return 0, fmt.Errorf("error writing data: %w", err) } @@ -453,7 +454,7 @@ func (f *File) Write(p []byte) (int, error) { func (f *File) Close() error { headerPayload := make([]byte, 8) binary.LittleEndian.PutUint64(headerPayload, f.handle) - err := f.client.sendPacket(Afc_operation_file_close, headerPayload, nil) + err := f.client.sendPacket(fileClose, headerPayload, nil) if err != nil { return fmt.Errorf("error closing file: %w", err) } @@ -474,16 +475,3 @@ const ( WRITE_ONLY_CREATE_APPEND = Mode(0x00000005) READ_WRITE_CREATE_APPEND = Mode(0x00000006) ) - -type afcError struct { - code int -} - -func (a afcError) Error() string { - return fmt.Sprintf("afc error code: %d", a.code) -} - -func isPermissionDeniedError(err error) bool { - var aError afcError - return errors.As(err, &aError) && aError.code == errPermDenied -} diff --git a/ios/afc/errors.go b/ios/afc/errors.go index 79058077..ab821e85 100644 --- a/ios/afc/errors.go +++ b/ios/afc/errors.go @@ -1,5 +1,118 @@ -package main +package afc -func main() { +import ( + "errors" + "fmt" +) +const ( + errSuccess = 0 + errUnknown = 1 + errOperationHeaderInvalid = 2 + errNoResources = 3 + errReadError = 4 + errWriteError = 5 + errUnknownPacketType = 6 + errInvalidArgument = 7 + errObjectNotFound = 8 + errObjectIsDir = 9 + errPermDenied = 10 + errServiceNotConnected = 11 + errOperationTimeout = 12 + errTooMuchData = 13 + errEndOfData = 14 + errOperationNotSupported = 15 + errObjectExists = 16 + errObjectBusy = 17 + errNoSpaceLeft = 18 + errOperationWouldBlock = 19 + errIoError = 20 + errOperationInterrupted = 21 + errOperationInProgress = 22 + errInternalError = 23 + errMuxError = 30 + errNoMemory = 31 + errNotEnoughData = 32 + errDirNotEmpty = 33 +) + +type DeviceInfo struct { + Model string + TotalBytes uint64 + FreeBytes uint64 + BlockSize uint64 +} + +func getError(errorCode uint64) error { + switch errorCode { + case errUnknown: + return errors.New("UnknownError") + case errOperationHeaderInvalid: + return errors.New("OperationHeaderInvalid") + case errNoResources: + return errors.New("NoResources") + case errReadError: + return errors.New("ReadError") + case errWriteError: + return errors.New("WriteError") + case errUnknownPacketType: + return errors.New("UnknownPacketType") + case errInvalidArgument: + return errors.New("InvalidArgument") + case errObjectNotFound: + return errors.New("ObjectNotFound") + case errObjectIsDir: + return errors.New("ObjectIsDir") + case errPermDenied: + return errors.New("PermDenied") + case errServiceNotConnected: + return errors.New("ServiceNotConnected") + case errOperationTimeout: + return errors.New("OperationTimeout") + case errTooMuchData: + return errors.New("TooMuchData") + case errEndOfData: + return errors.New("EndOfData") + case errOperationNotSupported: + return errors.New("OperationNotSupported") + case errObjectExists: + return errors.New("ObjectExists") + case errObjectBusy: + return errors.New("ObjectBusy") + case errNoSpaceLeft: + return errors.New("NoSpaceLeft") + case errOperationWouldBlock: + return errors.New("OperationWouldBlock") + case errIoError: + return errors.New("IoError") + case errOperationInterrupted: + return errors.New("OperationInterrupted") + case errOperationInProgress: + return errors.New("OperationInProgress") + case errInternalError: + return errors.New("InternalError") + case errMuxError: + return errors.New("MuxError") + case errNoMemory: + return errors.New("NoMemory") + case errNotEnoughData: + return errors.New("NotEnoughData") + case errDirNotEmpty: + return errors.New("DirNotEmpty") + default: + return nil + } +} + +type afcError struct { + code int +} + +func (a afcError) Error() string { + return fmt.Sprintf("afc error code: %d", a.code) +} + +func isPermissionDeniedError(err error) bool { + var aError afcError + return errors.As(err, &aError) && aError.code == errPermDenied }