diff --git a/ios/afc/afc.go b/ios/afc/afc.go index 91728953..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 ( - 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 + 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 AFCDeviceInfo struct { - Model string - TotalBytes uint64 - FreeBytes uint64 - BlockSize uint64 -} - -func getError(errorCode uint64) error { - switch errorCode { - case Afc_Err_UnknownError: - return errors.New("UnknownError") - case Afc_Err_OperationHeaderInvalid: - return errors.New("OperationHeaderInvalid") - case Afc_Err_NoResources: - return errors.New("NoResources") - case Afc_Err_ReadError: - return errors.New("ReadError") - case Afc_Err_WriteError: - return errors.New("WriteError") - case Afc_Err_UnknownPacketType: - return errors.New("UnknownPacketType") - case Afc_Err_InvalidArgument: - return errors.New("InvalidArgument") - case Afc_Err_ObjectNotFound: - return errors.New("ObjectNotFound") - case Afc_Err_ObjectIsDir: - return errors.New("ObjectIsDir") - case Afc_Err_PermDenied: - return errors.New("PermDenied") - case Afc_Err_ServiceNotConnected: - return errors.New("ServiceNotConnected") - case Afc_Err_OperationTimeout: - return errors.New("OperationTimeout") - case Afc_Err_TooMuchData: - return errors.New("TooMuchData") - case Afc_Err_EndOfData: - return errors.New("EndOfData") - case Afc_Err_OperationNotSupported: - return errors.New("OperationNotSupported") - case Afc_Err_ObjectExists: - return errors.New("ObjectExists") - case Afc_Err_ObjectBusy: - return errors.New("ObjectBusy") - case Afc_Err_NoSpaceLeft: - return errors.New("NoSpaceLeft") - case Afc_Err_OperationWouldBlock: - return errors.New("OperationWouldBlock") - case Afc_Err_IoError: - return errors.New("IoError") - case Afc_Err_OperationInterrupted: - return errors.New("OperationInterrupted") - case Afc_Err_OperationInProgress: - return errors.New("OperationInProgress") - case Afc_Err_InternalError: - return errors.New("InternalError") - case Afc_Err_MuxError: - return errors.New("MuxError") - case Afc_Err_NoMemory: - return errors.New("NoMemory") - case Afc_Err_NotEnoughData: - return errors.New("NotEnoughData") - case Afc_Err_DirNotEmpty: - 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 new file mode 100644 index 00000000..983c7603 --- /dev/null +++ b/ios/afc/client.go @@ -0,0 +1,477 @@ +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 +} + +// 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 NewFromConn(deviceConn), nil +} + +// NewFromConn establishes a new AFC client connection from an existing device connection +func NewFromConn(d ios.DeviceConnectionInterface) *Client { + return &Client{ + connection: d, + } +} + +// Close the afc client +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(readDir, []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 +} + +// 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) + headerLength := 8 + uint64(len(pathBytes)) + headerPayload := make([]byte, headerLength) + binary.LittleEndian.PutUint64(headerPayload, uint64(mode)) + copy(headerPayload[8:], pathBytes) + err := c.sendPacket(fileOpen, 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 +} + +// MkDir creates a directory at the specified path +func (c *Client) MkDir(p string) error { + headerPayload := []byte(p) + headerPayload = append(headerPayload, 0) + + err := c.sendPacket(makeDir, 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 +} + +// Remove deletes the file at the given path +// If the path is a non-empty directory, an error will be returned +func (c *Client) Remove(p string) error { + return c.remove(p, false) +} + +// 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) RemoveAll(p string) error { + return c.remove(p, true) +} + +func (c *Client) remove(p string, recursive bool) error { + headerPayload := []byte(p) + var opcode = removePath + if recursive { + opcode = removePathAndContents + } + err := c.sendPacket(opcode, 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 opcode, headerPayload []byte, payload []byte) error { + num := c.packetNum.Add(1) + + thisLen := headerSize + uint64(len(headerPayload)) + p := packet{ + Header: header{ + Magic: 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 - headerSize + 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 == status { + code := binary.LittleEndian.Uint64(p.HeaderPayload) + if code == errSuccess { + return p, nil + } + return p, afcError{ + code: int(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" + S_IFLNK FileType = "S_IFLNK" +) + +type FileInfo struct { + 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 +func (c *Client) Stat(s string) (FileInfo, error) { + err := c.sendPacket(fileInfo, []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) + case "st_linktarget": + info.LinkTarget = value + } + } + + // Set the name from the path + parts := strings.Split(s, "/") + if len(parts) > 0 { + info.Name = parts[len(parts)-1] + } + + 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 + } + + slices.Sort(files) + for _, file := range files { + if file == "." || file == ".." { + continue + } + 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) + 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 +} + +// DeviceInfo retrieves information about the filesystem of the device +func (c *Client) DeviceInfo() (DeviceInfo, error) { + err := c.sendPacket(deviceInfo, nil, nil) + if err != nil { + return DeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + } + resp, err := c.readPacket() + if err != nil { + return DeviceInfo{}, fmt.Errorf("error getting device info: %w", err) + } + + bs := bytes.Split(resp.Payload, []byte{0}) + + var info DeviceInfo + 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 DeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + } + info.TotalBytes = value + case "FSFreeBytes": + if intParseErr != nil { + return DeviceInfo{}, fmt.Errorf("error parsing %s: %w", key, intParseErr) + } + info.FreeBytes = value + case "FSBlockSize": + if intParseErr != nil { + return DeviceInfo{}, 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 opcode +} + +type packet struct { + Header header + HeaderPayload []byte + Payload []byte +} + +// File is a reference to a file on the devices filesystem +type File struct { + client *Client + handle uint64 +} + +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))) + + err := f.client.sendPacket(fileRead, headerPayload, nil) + if err != nil { + return 0, fmt.Errorf("error reading data: %w", err) + } + resp, err := f.client.readPacket() + copy(p, resp.Payload) + l := len(resp.Payload) + if l == 0 { + return 0, io.EOF + } + return l, nil +} + +func (f *File) Write(p []byte) (int, error) { + headerPayload := make([]byte, 8) + binary.LittleEndian.PutUint64(headerPayload, f.handle) + err := f.client.sendPacket(fileWrite, 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(fileClose, 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..9cf89305 --- /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 := New(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.Remove("./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.Remove("./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.Remove("./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.Remove("./test-file") + assert.NoError(t, err) + }) + + t.Run("create and delete nested directory", func(t *testing.T) { + err = client.MkDir("./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.RemoveAll("./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.MkDir(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() +} diff --git a/ios/afc/errors.go b/ios/afc/errors.go new file mode 100644 index 00000000..ab821e85 --- /dev/null +++ b/ios/afc/errors.go @@ -0,0 +1,118 @@ +package afc + +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 +} diff --git a/ios/afc/fsync.go b/ios/afc/fsync.go index eff493aa..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 != Afc_Err_Success { - 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 e77aa4e3..c3fef115 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.NewFromConn(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.NewFromConn(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.Remove(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.NewFromConn(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 { diff --git a/ios/house_arrest/house_arrest.go b/ios/house_arrest/house_arrest.go index 605d5c0c..8e4de821 100644 --- a/ios/house_arrest/house_arrest.go +++ b/ios/house_arrest/house_arrest.go @@ -1,127 +1,75 @@ 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" ) const serviceName = "com.apple.mobile.house_arrest" -type Connection struct { - deviceConn ios.DeviceConnectionInterface - 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.NewFromConn(deviceConn), nil } -func (c Connection) Close() { - if c.deviceConn != nil { - c.deviceConn.Close() +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) } -} - -func (conn *Connection) SendFile(fileContents []byte, filePath string) error { - handle, err := conn.openFileForWriting(filePath) + err = deviceConn.Send(msg) if err != nil { return err } - err = conn.sendFileContents(fileContents, handle) + reader := deviceConn.Reader() + response, err := plistCodec.Decode(reader) if err != nil { return err } - return conn.closeHandle(handle) + return checkResponse(response) } -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) +func checkResponse(vendContainerResponseBytes []byte) error { + response, err := plistFromBytes(vendContainerResponseBytes) if err != nil { - return []string{}, err + return err } - fileList := string(response.Payload) - return strings.Split(fileList, string([]byte{0})), nil -} - -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) - if err != nil { - return 0, err + if "Complete" == response.Status { + return nil } - 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 response.Error != "" { + return errors.New(response.Error) } - return response.HeaderPayload[0], nil + return errors.New("unknown error during vendcontainer") } -func (conn *Connection) sendAfcPacketAndAwaitResponse(packet afc.AfcPacket) (afc.AfcPacket, error) { - err := afc.Encode(packet, conn.deviceConn.Writer()) - if err != nil { - return afc.AfcPacket{}, err - } - return afc.Decode(conn.deviceConn.Reader()) -} +func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { + var vendResponse vendContainerResponse + decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) -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) + err := decoder.Decode(&vendResponse) if err != nil { - return err + return vendResponse, 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 + 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 } diff --git a/ios/mcinstall/prepare.go b/ios/mcinstall/prepare.go index ade4be30..889e36f6 100644 --- a/ios/mcinstall/prepare.go +++ b/ios/mcinstall/prepare.go @@ -195,7 +195,8 @@ func setupSkipSetup(device ios.DeviceEntry) error { if err != nil { return err } - err = afcConn.RemovePathAndContents(skipSetupFilePath) + defer afcConn.Close() + err = afcConn.RemoveAll(skipSetupFilePath) if err != nil { log.Debug("skip setup: nothing to remove") } @@ -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 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 } diff --git a/main.go b/main.go index ea240dc6..835ee321 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) } 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") @@ -1109,7 +1110,19 @@ 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) } @@ -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() @@ -1150,7 +1178,7 @@ The commands work as following: if b { afcService, err := afc.New(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) }