commit 5dc58c04ae38ff141170a9c530678272f85d66db Author: snow flurry Date: Wed Jan 22 20:27:19 2025 -0800 Initial commit diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..3a02422 --- /dev/null +++ b/.clang-format @@ -0,0 +1,7 @@ +--- +BasedOnStyle: Mozilla +AlignAfterOpenBracket: Align +AlignArrayOfStructures: Left +AlwaysBreakAfterDefinitionReturnType: All +IndentWidth: 4 +IndentCaseLabels: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..054a2a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# subproject directories +/subprojects/* +!/subprojects/*.wrap + +# Meson Directories +meson-logs +meson-private + +# Meson Files +meson_benchmark_setup.dat +meson_test_setup.dat +sanitycheckcpp.cc # C++ specific +sanitycheckcpp.exe # C++ specific + +# Ninja +build.ninja +.ninja_deps +.ninja_logs + +# Misc +compile_commands.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8b604e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, snow flurry + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/NOTICES b/NOTICES new file mode 100644 index 0000000..684ce1a --- /dev/null +++ b/NOTICES @@ -0,0 +1,34 @@ +This program uses code from the NetBSD C library and mkdir(1) application. The +license is as follows: + +/* + * Copyright (c) 1989, 1991, 1993, 1995 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Jan-Simon Pendry. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc3b449 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# iasync - sync files to iOS app folders + +**iasync** uses [libimobiledevice] to sync a local directory tree with an iOS device. This is intended as a replacement to pairing ifuse and rsync, where users might have issues with fuse (or be unable to use fuse at all). + +## Building + +This project requires the following: + +- [libimobiledevice] for the actual iOS bits. +- [libbsd] on non-BSD/macOS systems. +- [meson] and [ninja] as the build system. + +To build in the directory `build`, run: + +``` +meson setup build/ +ninja -C build/ +# To install: +ninja -C build/ install +``` + +## Usage + +See the manpage for more details (`man 1 iasync`). + +Primarily, if you have more than one iOS device connected to your system, you'll need the name or UDID of your device: + +``` +$ iasync lsdevs +Name Conn. UDID +My Iphone USB 00000000-0000000 +``` + +Then, get the Bundle ID for the app you want to sync: + +``` +$ iasync lsapps +App Name Bundle ID +[...] +Doppler co.brushedtype.doppler-ios +``` + +To sync a directory tree to the app, you could run: + +``` +$ iasync sync ~/Music co.brushedtype.doppler-ios +``` + +If you want to delete files that exist on the device but not locally, use the `--allow-delete` flag: + +``` +$ iasync sync --allow-delete ~/Music co.brushedtype.doppler-ios +``` + +## License + +iasync is provided under the BSD 3-Clause license. For more information, please see the LICENSE file. + +Multiple functions (`idevfs_canonpath()` and `idevfs_mkpath()`) use code from [NetBSD]. These licenses are included in the NOTICES file. diff --git a/config.h.meson b/config.h.meson new file mode 100644 index 0000000..1775c4c --- /dev/null +++ b/config.h.meson @@ -0,0 +1,3 @@ +#define PROJECT_NAME "@name@" + +#define PROJECT_VER "@version@" \ No newline at end of file diff --git a/doc/iasync.1 b/doc/iasync.1 new file mode 100644 index 0000000..a3e51f6 --- /dev/null +++ b/doc/iasync.1 @@ -0,0 +1,124 @@ +.Dd January 22, 2025 +.Dt IASYNC 1 +.Os +.Sh NAME +.Nm iasync +.Nd sync files to iOS app folders +.Sh SYNOPSIS +.Nm iasync +.Op Ar options +.Ar command +.Op Ar command_options +.Nm +.Ar lsdevs +.Op command_options +.Nm +.Ar lsapps +.Op command_options +.Nm +.Ar ls +.Op command_options +.Nm +.Ar sync +.Op command_options +.Ar source +.Ar target +.Sh DESCRIPTION +.Nm +syncs files to Documents folders for iOS apps that support file sharing. +.Ss Global Options +The following options largely affect all commands, and should be provided before +the command name. +.Bl -tag -width XXXX +.It Fl n, -name Ar name +.It Fl u, -udid Ar udid +Connect to the device with the provided name or UDID. This can be discovered +with the +.Ar lsdevs +command if the device is already connected. +.It Fl v, -verbose +Increases the verbosity. This can be used multiple times. This is mutally exclusive with +.Ns Fl -quiet . +.It Fl q, -quiet +Lowers the verbosity. This is mutually exclusive with +.Ns Fl -verbose . +.El +.Ss Commands +.Bl -tag +.It Xo +.Nm +.Ic lsdevs +.Op Fl n | -no-headers +.Xc +Lists devices connected to the computer as a table. For each connected device, +this shows the name, whether it's connected over USB or Network (Wi-Fi), and its +UDID. +.Bl -tag +.It Fl n, -no-headers +Suppress the table headers. +.El +.It Xo +.Nm +.Ic lsapps +.Op Fl n | -no-headers +.Xc +Lists apps that support file sharing for the connected device. +.Bl -tag +.It Fl n, -no-headers +Suppress the table headers. +.El +.It Xo +.Nm +.Ic ls +.Op Fl a | -all +.Ar app_id Ns Op Ar :path +.Xc +Lists all files in the directory provided by the +.Ar path +argument, relative to the root of the app's Documents folder. +.Bl -tag +.It Fl a, -all +Display entries with names starting with a dot (`.'). +.El +.It Xo +.Nm +.Ic sync +.Op Fl Dnp +.Ar source +.Ar app_id Ns Op Ar :path +.Xc +Copies the files from the +.Ar source +directory to the path provided by the +.Ar app_id +and +.Ar path +arguments. +.Bl -tag +.It Fl D, -allow-delete +Allow the sync operation to delete remote files to ensure the remote directory +tree matches the local tree. +.It Fl n, -dry-run +Explain what would have been done, instead of performing the operations. +.It Fl p, -progress +Print progress information for files being copied. +.El +.El +.Sh EXIT STATUS +.Nm +exits with either 1 or 2 if an error occurred. +.Sh SEE ALSO +.Xr ifuse 1 , +.Xr idevicepair 1 +.Sh BUGS +.Nm +is still heavily in development, and bugs are to be expected. While efforts are +made to avoid loss of data, users should ensure their files are backed up before +performing a sync if they have any important data that could be lost. +.Pp +Bugs can be reported on GitHub at: +.Pp +.D1 Lk https://github.com/snowkat/iasync +.Pp +or by sending an e-mail to +.Ns Mt snow@datagirl.xyz . \ No newline at end of file diff --git a/doc/iasync.1.html b/doc/iasync.1.html new file mode 100644 index 0000000..995835f --- /dev/null +++ b/doc/iasync.1.html @@ -0,0 +1,190 @@ + + + + + + + IASYNC(1) + + + + + + + + +
IASYNC(1)General Commands ManualIASYNC(1)
+
+
+

+

iasyncsync + files to iOS app folders

+
+
+

+ + + + + +
iasync[options] command + [command_options]
+
+ + + + + +
iasynclsdevs [command_options]
+
+ + + + + +
iasynclsapps [command_options]
+
+ + + + + +
iasyncls [command_options]
+
+ + + + + +
iasyncsync [command_options] + source target
+
+
+

+

iasync syncs files to Documents folders + for iOS apps that support file sharing.

+
+

+

The following options largely affect all commands, and should be + provided before the command name.

+
+
+ --name name
+
 
+
+ --udid udid
+
Connect to the device with the provided name or UDID. This can be + discovered with the lsdevs command if the device is + already connected.
+
+ --verbose
+
Increases the verbosity. This can be used multiple times. This is mutally + exclusive with --quiet.
+
+ --quiet
+
Lowers the verbosity. This is mutually exclusive with + --verbose.
+
+
+
+

+
+
iasync lsdevs + [-n | --no-headers]
+
Lists devices connected to the computer as a table. For each connected + device, this shows the name, whether it's connected over USB or Network + (Wi-Fi), and its UDID. +
+
+ --no-headers
+
Suppress the table headers.
+
+
+
iasync lsapps + [-n | --no-headers]
+
Lists apps that support file sharing for the connected device. +
+
+ --no-headers
+
Suppress the table headers.
+
+
+
iasync ls + [-a | --all] + app_id[:path]
+
Lists all files in the directory provided by the + path argument, relative to the root of the app's + Documents folder. +
+
+ --all
+
Display entries with names starting with a dot (`.').
+
+
+
iasync sync + [-Dnp] source + app_id[:path]
+
Copies the files from the source directory to the + path provided by the app_id and + path arguments. +
+
+ --allow-delete
+
Allow the sync operation to delete remote files to ensure the remote + directory tree matches the local tree.
+
+ --dry-run
+
Explain what would have been done, instead of performing the + operations.
+
+ --progress
+
Print progress information for files being copied.
+
+
+
+
+
+
+

+

iasync exits with either 1 or 2 if an + error occurred.

+
+
+

+

ifuse(1), idevicepair(1)

+
+
+

+

iasync is still heavily in development, + and bugs are to be expected. While efforts are made to avoid loss of data, + users should ensure their files are backed up before performing a sync if + they have any important data that could be lost.

+

Bugs can be reported on GitHub at:

+

+ +

or by sending an e-mail to + snow@datagirl.xyz.

+
+
+ + + + + +
January 22, 2025 
+ + diff --git a/doc/iasync.1.md b/doc/iasync.1.md new file mode 100644 index 0000000..8ae7f36 --- /dev/null +++ b/doc/iasync.1.md @@ -0,0 +1,145 @@ +IASYNC(1) - General Commands Manual + +# NAME + +**iasync** - sync files to iOS app folders + +# SYNOPSIS + +**iasync** +\[*options*] +*command* +\[*command\_options*] +**iasync** +*lsdevs* +\[command\_options] +**iasync** +*lsapps* +\[command\_options] +**iasync** +*ls* +\[command\_options] +**iasync** +*sync* +\[command\_options] +*source* +*target* + +# DESCRIPTION + +**iasync** +syncs files to Documents folders for iOS apps that support file sharing. + +## Global Options + +The following options largely affect all commands, and should be provided before +the command name. + +**-n,** **--name** *name* + +**-u,** **--udid** *udid* + +> Connect to the device with the provided name or UDID. This can be discovered +> with the +> *lsdevs* +> command if the device is already connected. + +**-v,** **--verbose** + +> Increases the verbosity. This can be used multiple times. This is mutally exclusive with +> **--quiet**. + +**-q,** **--quiet** + +> Lowers the verbosity. This is mutually exclusive with +> **--verbose**. + +## Commands + +**iasync** +**lsdevs** +\[**-n** | **--no-headers**] + +> Lists devices connected to the computer as a table. For each connected device, +> this shows the name, whether it's connected over USB or Network (Wi-Fi), and its +> UDID. + +> **-n,** **--no-headers** + +> > Suppress the table headers. + +**iasync** +**lsapps** +\[**-n** | **--no-headers**] + +> Lists apps that support file sharing for the connected device. + +> **-n,** **--no-headers** + +> > Suppress the table headers. + +**iasync** +**ls** +\[**-a** | **--all**] +*app\_id*\[*:path*] + +> Lists all files in the directory provided by the +> *path* +> argument, relative to the root of the app's Documents folder. + +> **-a,** **--all** + +> > Display entries with names starting with a dot (\`.'). + +**iasync** +**sync** +\[**-Dnp**] +*source* +*app\_id*\[*:path*] + +> Copies the files from the +> *source* +> directory to the path provided by the +> *app\_id* +> and +> *path* +> arguments. + +> **-D,** **--allow-delete** + +> > Allow the sync operation to delete remote files to ensure the remote directory +> > tree matches the local tree. + +> **-n,** **--dry-run** + +> > Explain what would have been done, instead of performing the operations. + +> **-p,** **--progress** + +> > Print progress information for files being copied. + +# EXIT STATUS + +**iasync** +exits with either 1 or 2 if an error occurred. + +# SEE ALSO + +ifuse(1), +idevicepair(1) + +# BUGS + +**iasync** +is still heavily in development, and bugs are to be expected. While efforts are +made to avoid loss of data, users should ensure their files are backed up before +performing a sync if they have any important data that could be lost. + +Bugs can be reported on GitHub at: + +> [https://github.com/snowkat/iasync](https://github.com/snowkat/iasync) + +or by sending an e-mail to +[snow@datagirl.xyz](mailto:snow@datagirl.xyz). + +Linux 6.12.7-gentoo-dist - January 22, 2025 diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..d218115 --- /dev/null +++ b/meson.build @@ -0,0 +1,65 @@ +project( + 'iasync', + 'c', + version: '0.1', + default_options: ['warning_level=2', 'c_std=c11'], +) + +imobiledevice = dependency('libimobiledevice-1.0', version: '>=1.3.0') + +common_srcs = [ + 'src' / 'idevfs.c', + 'src' / 'log.c', + 'src' / 'strlist.c', + 'src' / 'util.c', + 'src' / 'cmd_lsdev.c', + 'src' / 'cmd_lsapps.c', + 'src' / 'cmd_ls.c', + 'src' / 'cmd_sync.c', + 'src' / 'sync_lock.c', +] +deps = [imobiledevice] + +cdata = configuration_data( + { + 'name': meson.project_name(), + 'version': meson.project_version(), + }, +) +cc = meson.get_compiler('c') + +if not cc.has_function('setprogname') + libbsd = dependency('libbsd-overlay') + deps += libbsd +endif + +configure_file(input: 'config.h.meson', output: 'config.h', configuration: cdata) +config_inc = include_directories('.') +src_inc = include_directories('src') +incpath = [config_inc, src_inc] + +main_srcs = common_srcs + ['src' / 'main.c'] + +executable('iasync', main_srcs, dependencies: deps, include_directories: config_inc) + +unity_subproject = subproject('unity') +unity_gen_runner = unity_subproject.get_variable('gen_test_runner') +unity_dep = unity_subproject.get_variable('unity_dep') +testdeps = deps + [unity_dep] + +test( + 'strlist', + executable( + 'strlist_test', + sources: [ + 'src' / 'strlist.c', + 'src' / 'log.c', + 'tests' / 'strlist.c', + unity_gen_runner.process('tests' / 'strlist.c'), + ], + include_directories: incpath, + dependencies: testdeps, + ), +) + +install_man('doc' / 'iasync.1') \ No newline at end of file diff --git a/src/cmd_ls.c b/src/cmd_ls.c new file mode 100644 index 0000000..5417041 --- /dev/null +++ b/src/cmd_ls.c @@ -0,0 +1,85 @@ +// cmd_ls.c: List files for a given application +#include "config.h" + +#include +#include +#include + +#include "cmds.h" +#include "idevfs.h" +#include "log.h" +#include "util.h" + +#include +#include +#include +#include +#include + +int +cmd_ls(int argc, char* argv[], globargs_t* ga) +{ + int res = 0, ch; + afc_error_t afc_err; + idevfs_t* idfs = NULL; + char** filents = NULL; + char** curent = NULL; + char *app_id, *ls_path; + bool list_all = false; + + struct option longopts[] = { + { "all", no_argument, NULL, 'a' }, + { NULL, 0, NULL, 0 }, + }; + + while ((ch = getopt_long(argc, argv, "a", longopts, NULL)) != -1) { + switch (ch) { + case 'a': + list_all = true; + break; + default: + return -1; + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) + die("App Bundle ID not provided.\n"); + + if (split_appid_path(argv[0], &app_id, &ls_path) < 0) { + die("Unable to parse provided remote path.\n"); + /* NOTREACHED */ + } + + if ((idfs = idevfs_setup(ga, app_id)) == NULL) { + res = 2; + goto done; + } + + afc_err = afc_read_directory(idfs->afc, ls_path, &filents); + if (afc_err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't read '%s' (app %s, error %d)\n", + ls_path, + app_id, + afc_err); + res = 2; + goto done; + } + + curent = filents; + while (*curent != NULL) { + if (list_all || *curent[0] != '.') + printf("%s\n", *curent); + curent++; + } + +done: + if (filents) + afc_dictionary_free(filents); + if (idfs) + idevfs_free(idfs); + return res; +} \ No newline at end of file diff --git a/src/cmd_lsapps.c b/src/cmd_lsapps.c new file mode 100644 index 0000000..de83ba1 --- /dev/null +++ b/src/cmd_lsapps.c @@ -0,0 +1,144 @@ +// cmd_apps.c: Lists available apps for a given device. + +#include "config.h" + +#include "cmds.h" +#include "log.h" +#include "util.h" + +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + * Format string used for the table format. + * Columns, in order, are ["Name", "Bundle ID", and "Supports File Sharing"] + */ +#define APP_TABLE_ROW "%-*s %-*s\n" + +static char* +plist_dict_item_val(plist_t node, const char* key) +{ + plist_t item_node; + char* val; + if (node == NULL || key == NULL) + return NULL; + if (plist_get_node_type(node) != PLIST_DICT) + return NULL; + + item_node = plist_dict_get_item(node, key); + if (item_node == NULL || plist_get_node_type(item_node) != PLIST_STRING) + return NULL; + plist_get_string_val(item_node, &val); + + return val; +} + +struct app_row +{ + char* disp_name; + char* bundle_id; + int sharing_enabled; +}; + +int +cmd_lsapps(int argc, char* argv[], globargs_t* ga) +{ + idevice_t dev = get_idevice_by_globargs(ga); + instproxy_client_t proxy_client = NULL; + instproxy_error_t err; + plist_t apps, opts; + struct app_row* rows = NULL; + size_t count; + int bundle_max = 0, name_max = 0, ch; + bool print_hdrs = true; + + struct option longopts[] = { + { "no-headers", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } + }; + + while ((ch = getopt_long(argc, argv, "n", longopts, NULL)) != -1) { + switch (ch) { + case 'n': + print_hdrs = false; + break; + default: + return -1; + } + } + + if (dev == NULL) + die("Couldn't find device!\n"); + + err = instproxy_client_start_service(dev, &proxy_client, PROJECT_NAME); + if (err != INSTPROXY_E_SUCCESS) { + idevice_free(dev); + die("Installation proxy connection failed (error %d)\n", err); + /* NOTREACHED */ + } + + opts = instproxy_client_options_new(); + instproxy_client_options_add(opts, "ApplicationType", "Any", NULL); + instproxy_client_options_set_return_attributes(opts, + "CFBundleDisplayName", + "CFBundleIdentifier", + "UIFileSharingEnabled", + NULL); + err = instproxy_browse(proxy_client, opts, &apps); + instproxy_client_options_free(opts); + if (err != INSTPROXY_E_SUCCESS) { + die("Unable to get programs from installation proxy (error %d)\n", err); + /* NOTREACHED */ + } + + count = plist_array_get_size(apps); + + if (count > 0) { + rows = calloc(count, sizeof(struct app_row)); + if (rows == NULL) { + perror("memory allocation error"); + plist_free(apps); + instproxy_client_free(proxy_client); + return 2; + } + } + + for (size_t i = 0; i < count; i++) { + plist_t cur_app = plist_array_get_item(apps, i); + rows[i].bundle_id = plist_dict_item_val(cur_app, "CFBundleIdentifier"); + if (rows[i].bundle_id != NULL) + bundle_max = MAX(bundle_max, (int)strlen(rows[i].bundle_id)); + + rows[i].disp_name = plist_dict_item_val(cur_app, "CFBundleDisplayName"); + if (rows[i].disp_name != NULL) + name_max = MAX(name_max, (int)strlen(rows[i].disp_name)); + + rows[i].sharing_enabled = + plist_dict_get_bool(cur_app, "UIFileSharingEnabled"); + } + + if (print_hdrs) + printf(APP_TABLE_ROW, name_max, "App Name", bundle_max, "Bundle ID"); + for (size_t i = 0; i < count; i++) { + if (!rows[i].sharing_enabled) + continue; + printf(APP_TABLE_ROW, + name_max, + STRING_OR_UNKNOWN(rows[i].disp_name), + bundle_max, + STRING_OR_UNKNOWN(rows[i].bundle_id)); + } + + plist_free(apps); + instproxy_client_free(proxy_client); + idevice_free(dev); + + return 0; +} \ No newline at end of file diff --git a/src/cmd_lsdev.c b/src/cmd_lsdev.c new file mode 100644 index 0000000..2426572 --- /dev/null +++ b/src/cmd_lsdev.c @@ -0,0 +1,114 @@ +// cmd_lsdev.c: Lists available devices. +#include "config.h" + +#include "cmds.h" +#include "log.h" +#include "util.h" + +#include +#include + +#include +#include +#include +#include + +struct device_row +{ + char* udid; + const char* type; + char* name; +}; + +/* + * Format string used for the table format. + * Columns, in order, are ["Name", "Conn.", and "UDID"] + */ +#define DEVICE_TABLE_ROW "%-*s %-8s %s\n" + +int +cmd_lsdev(int argc, char* argv[], globargs_t* ga) +{ + idevice_info_t* devices = NULL; + idevice_error_t err; + struct device_row* rows = NULL; + int name_max = 0, udid_max = 0, ch, count; + bool print_hdrs = true; + + // None of the global args are useful to us + UNUSED(ga); + + struct option longopts[] = { + { "no-headers", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } + }; + + while ((ch = getopt_long(argc, argv, "n", longopts, NULL)) != -1) { + switch (ch) { + case 'n': + print_hdrs = false; + break; + } + } + + err = idevice_get_device_list_extended(&devices, &count); + + if (err != IDEVICE_E_SUCCESS) { + die("Couldn't get device list! (is usbmuxd running?)\n"); + /* NOTREACHED */ + } + + /* Don't bother allocating if we have no rows */ + if (count > 0) { + rows = calloc(count, sizeof(struct device_row)); + if (rows == NULL) { + perror("memory allocation error"); + idevice_device_list_extended_free(devices); + return 2; + } + } + + /* make the table rows */ + for (int i = 0; i < count; i++) { + int is_usb = (devices[i]->conn_type == CONNECTION_USBMUXD); + idevice_t device = NULL; + + rows[i].type = is_usb ? " USB " : "Network"; + rows[i].udid = devices[i]->udid; + udid_max = MAX(udid_max, (int)strlen(rows[i].udid)); + + /* Open the device proper to get more info */ + err = idevice_new_with_options(&device, + devices[i]->udid, + is_usb ? IDEVICE_LOOKUP_USBMUX + : IDEVICE_LOOKUP_NETWORK); + + if (err == IDEVICE_E_SUCCESS) { + rows[i].name = get_idevice_name(device); + idevice_free(device); + + if (rows[i].name != NULL) + name_max = MAX(name_max, (int)strlen(rows[i].name)); + } + } + + /* and once more, for stdout */ + if (print_hdrs) + printf(DEVICE_TABLE_ROW, name_max, "Name", "Conn.", "UDID"); + for (int i = 0; i < count; i++) { + printf(DEVICE_TABLE_ROW, + name_max, + STRING_OR_UNKNOWN(rows[i].name), + rows[i].type, + STRING_OR_UNKNOWN(rows[i].udid)); + if (rows[i].name != NULL) + free(rows[i].name); + } + + idevice_device_list_extended_free(devices); + + if (rows != NULL) + free(rows); + + return 0; +} \ No newline at end of file diff --git a/src/cmd_sync.c b/src/cmd_sync.c new file mode 100644 index 0000000..4f4a906 --- /dev/null +++ b/src/cmd_sync.c @@ -0,0 +1,633 @@ +#include "config.h" + +#include "cmds.h" +#include "idevfs.h" +#include "log.h" +#include "strlist.h" +#include "sync_lock.h" +#include "util.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include +#include + +#include + +/// @brief Structure for keeping track of what to do with given paths. +struct sync_tasks +{ + /// @brief Relative paths to copy to the iOS device. + strlist_t to_copy; + /// @brief List of relative paths to files specifically to ignore. + strlist_t to_ignore; + /// @brief Remote paths to delete from the iOS device. + strlist_t to_delete; + /// @brief List of relative directories to be created. + strlist_t make_dirs; +}; + +static atomic_bool should_print_progress; + +/* + * Signal handler for timer events. + */ +static void +timer_handler(int sig, siginfo_t* si, void* uc) +{ + UNUSED(si); + UNUSED(uc); + + atomic_store(&should_print_progress, true); + + signal(sig, SIG_IGN); +} + +/// @brief Converts a `struct timespec` to `uint64_t`. +/// @return An unsigned 64-bit integer containing the timestamp, with +/// microsecond precision. +static inline uint64_t +tm2uint(struct timespec ts) +{ + /* + * We're dealing with timespec structs (nsec), but iOS/AFC seems to only + * have usec precision (backwards compatibility?). In most cases it's not + * the end of the world, so we'll just lop off the last thousand. + */ + uint64_t usec = (ts.tv_nsec / 1000); + return (ts.tv_sec * 1000000000) + (usec * 1000); +} + +/* + * Figures out what files and folders need to be copied/deleted/ignored/etc. + */ +int +enumerate_sync(idevfs_t* idfs, char* local_dir, struct sync_tasks* tasks) +{ + char** raw_paths; + strlist_t paths, dirs; + afc_error_t err; + int len, rroot_len, lroot_len; + + if (idfs == NULL || local_dir == NULL || tasks == NULL) + return -1; + + atomic_init(&should_print_progress, false); + + tasks->to_copy = strlist_new(NULL, 0); + tasks->to_ignore = strlist_new(NULL, 0); + tasks->to_delete = strlist_new(NULL, 0); + tasks->make_dirs = strlist_new(NULL, 0); + ALLOC_ASSERT(tasks->to_copy); + ALLOC_ASSERT(tasks->to_ignore); + ALLOC_ASSERT(tasks->to_delete); + ALLOC_ASSERT(tasks->make_dirs); + + /* + * Get the length of the "root" dirs so we can parse out the subfolders + * later. + */ + rroot_len = strlen(idfs->cwd) + 1; + lroot_len = strlen(local_dir) + 1; + + err = idevfs_readdir(idfs, "", &raw_paths, &len); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, "Couldn't read root: %s\n", afc_strerror(err)); + return -1; + } + + paths = strlist_new(raw_paths, len); + ALLOC_ASSERT(paths); + free(raw_paths); + + dirs = strlist_new(NULL, 0); + ALLOC_ASSERT(dirs); + + /* + * Iterate through every remote path first, and see which ones we care + * about. This process is going to be sloowwwww, so try to accomplish as + * much as possible at once. + */ + for (size_t i = 0; i < paths->len; i++) { + struct stat64 st; + struct stat local_st; + char* path = paths->begin[i]; + const char* fname; + char* local_path; + + if (path == NULL) + continue; + + fname = basename(path); + + if (fname[0] == '.') { + /* + * TODO: We skip all dotfiles so important metadata doesn't get + * deleted. We should allow this with an option though. + */ + continue; +#if 0 + // Skip "." and ".." + if (fname[1] == '\0') + continue; + if (fname[1] == '.' && fname[2] == '\0') + continue; +#endif + } + + /* + * First, check if we have this locally. If not, don't bother checking + * anything else, just mark it to be deleted (or at least skip looking + * further) + */ + local_path = join_path(local_dir, path + rroot_len); + ALLOC_ASSERT(local_path); + if (lstat(local_path, &local_st) == -1) { + if (errno == ENOENT) { + /* Remote has the file but we don't. Add to delete list */ + char* full_path = strlist_claim(paths, i); + char* rel_path = substr(full_path, rroot_len, -1); + + ALLOC_ASSERT(rel_path); + strlist_push(tasks->to_delete, rel_path); + + free(full_path); + } else { + log_printf(IA_ERROR, + "Unable to stat local %s: %s\n", + local_path, + strerror(errno)); + } + free(local_path); + continue; + } + + err = idevfs_stat(idfs, path, &st); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Unable to stat remote %s: %s\n", + path, + afc_strerror(err)); + continue; + } + + free(local_path); + + char* rel_path = substr(path, rroot_len, -1); + ALLOC_ASSERT(rel_path); + + if ((st.st_mode & S_IFDIR) != (local_st.st_mode & S_IFDIR)) { + /* + * If one side is a directory isn't, something's already wrong. Add + * it to the delete list + */ + strlist_push(tasks->to_delete, rel_path); + } else if ((st.st_mode & S_IFDIR) != 0) { + /* Another directory to traverse! */ + char** inner_paths; + int inner_len; + + strlist_push(dirs, rel_path); + + err = idevfs_readdir(idfs, path, &inner_paths, &inner_len); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Unable to read remote dir %s: %s\n", + paths[i], + afc_strerror(err)); + continue; + } + + /* + * Copy the paths to the end of the list, so we can keep iterating. + */ + strlist_pushall(paths, inner_paths, inner_len); + + /* !!! Only free the "outer" ptr, not the whole list !!! */ + free(inner_paths); + } else { + /* All other files, do our best to compare */ + if ((st.st_size != local_st.st_size) || + (tm2uint(st.st_mtim) != tm2uint(local_st.st_mtim))) { + /* Not a match, overwrite */ + strlist_push(tasks->to_copy, rel_path); + } else { + /* File match! Add it to the ignore list */ + strlist_push(tasks->to_ignore, rel_path); + } + } + } + + /* + * Now we can iterate through the local filesystem. Since whatever the user + * has is likely to be faster (and we have a lot more POSIX than AFC), let's + * use fts(3). + */ + { + char* path_argv[] = { local_dir, NULL }; + FTS* fts = fts_open(path_argv, FTS_PHYSICAL, NULL); + FTSENT* node = NULL; + + if (fts == NULL) { + log_printf(IA_ERROR, + "Couldn't open local %s for traversing: %s\n", + local_dir, + strerror(errno)); + return -1; + } + + while ((node = fts_read(fts)) != NULL) { + char* rel_path; + bool should_ignore = false; + + /* Skip the local root */ + if (node->fts_pathlen == (lroot_len - 1) && + !strncmp(local_dir, node->fts_path, lroot_len)) + continue; + + rel_path = substr(node->fts_path, lroot_len, node->fts_pathlen); + ALLOC_ASSERT(rel_path); + + /* Should we ignore this one? */ + strlist_foreach(p, tasks->to_ignore) + { + if (strcmp(rel_path, p) == 0) { + should_ignore = true; + break; + } + } + + if (should_ignore) { + free(rel_path); + continue; + } + + if ((node->fts_info & FTS_D) != 0) { + /* + * If this directory doesn't exist on the device, we need to + * make it + */ + bool dir_exists = false; + strlist_foreach(dir, dirs) + { + if (strcmp(rel_path, dir) == 0) { + dir_exists = true; + break; + } + } + + if (!dir_exists) { + strlist_push(tasks->make_dirs, rel_path); + } else { + free(rel_path); + } + } else if ((node->fts_info & FTS_F) != 0) { + /* + * Any files we're not ignoring, add to the list. + */ + strlist_push(tasks->to_copy, rel_path); + } else { + /* + * There shouldn't be any cases we care about that get us here + * (symlinks?) + */ + free(rel_path); + } + } + + fts_close(fts); + } + + strlist_free(dirs); + strlist_free(paths); + + return 0; +} + +int +do_copy(idevfs_t* idfs, const char* from_path, const char* to_path) +{ + struct stat st; + off_t flen = -1; + int lfd, res = -1; + afc_error_t err; + uint64_t rfd = UINT64_MAX; + size_t total_wr = 0; + bool newline = false; + + err = afc_file_open(idfs->afc, to_path, AFC_FOPEN_WRONLY, &rfd); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't open remote %s: %s\n", + to_path, + afc_strerror(err)); + rfd = UINT64_MAX; + goto out; + } + + lfd = open(from_path, O_RDONLY); + if (lfd == -1) { + log_printf( + IA_ERROR, "Couldn't open local %s: %s\n", from_path, strerror(errno)); + goto out; + } + + if (fstat(lfd, &st) != 0) { + log_printf(IA_ERROR, + "Unable to stat local file %s: %s\n", + from_path, + strerror(errno)); + + /* Try to get file size manually */ + flen = lseek(lfd, 0, SEEK_END); + lseek(lfd, 0, SEEK_SET); + } else { + flen = st.st_size; + } + + while (true) { + const size_t MAX_BLK_LEN = 4096 * 1024; + char block[MAX_BLK_LEN]; + ssize_t blklen; + + blklen = read(lfd, block, MAX_BLK_LEN); + if (blklen == 0) { + /* Done! */ + res = 0; + if (newline) { + if (flen <= 0) { + printf("\r\x1b[2K Wrote %zu/??? bytes\n", total_wr); + } else { + float completeness = ((float)total_wr / (float)flen) * 100; + printf("\r\x1b[2K Wrote %zu/%zu bytes (%.2f%%)\n", + total_wr, + flen, + completeness); + } + } + break; + } else if (blklen < 0) { + log_printf(IA_ERROR, + "Error reading local %s: %s\n", + from_path, + strerror(errno)); + goto out; + } + for (size_t written = 0; written < (size_t)blklen;) { + uint32_t wrlen; + + err = + afc_file_write(idfs->afc, rfd, block, blklen - written, &wrlen); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Error writing to remote %s: %s\n", + to_path, + afc_strerror(err)); + goto out; + } + + written += wrlen; + total_wr += wrlen; + } + + if (atomic_exchange(&should_print_progress, false)) { + if (flen <= 0) { + printf("\x1b[2K\r Written %zu/??? bytes", total_wr); + } else { + float completeness = ((float)total_wr / (float)flen) * 100; + printf("\x1b[2K\r Written %zu/%zu bytes (%.2f%%)", + total_wr, + flen, + completeness); + } + fflush(stdout); + } + newline = true; + } + +out: + if (rfd != UINT64_MAX) { + afc_file_close(idfs->afc, rfd); + if (res != 0) { + /* If we had an open fd and failed, clean up the mess */ + afc_remove_path(idfs->afc, to_path); + } else { + err = afc_set_file_time(idfs->afc, to_path, tm2uint(st.st_mtim)); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "WARNING: Couldn't update mtime for %s!\n", + to_path); + } + } + } + + if (lfd != -1) + close(lfd); + + return res; +} + +int +cmd_sync(int argc, char* argv[], globargs_t* ga) +{ + int ch, res = 0; + char *app_id, *path; + afc_error_t err; + idevfs_t* idfs = NULL; + struct sync_tasks tasks = { 0 }; + bool progress = false, dry_run = false, allow_delete = false, + timer_set = true; + timer_t timerid; + struct sigaction sa; + struct sync_lock lock = SYNCLOCK_INIT; + + struct option longopts[] = { + { "allow-delete", no_argument, NULL, 'D' }, + { "dry-run", no_argument, NULL, 'n' }, + { "progress", no_argument, NULL, 'p' }, + }; + + while ((ch = getopt_long(argc, argv, "Dnp", longopts, NULL)) != -1) { + switch (ch) { + case 'D': + allow_delete = true; + break; + case 'n': + dry_run = true; + break; + case 'p': + progress = true; + break; + } + } + + argc -= optind; + argv += optind; + + // TODO: usage + if (argc < 2) + die("Bad arguments.\n"); + + /* Truncate paths ending with '/' */ + { + char* local = argv[0]; + int len = strlen(argv[0]); + if (local[len - 1] == '/') { + local[len - 1] = '\0'; + } + } + + if (split_appid_path(argv[1], &app_id, &path) < 0) { + die("Unable to parse provided remote path.\n"); + /* NOTREACHED */ + } + if ((idfs = idevfs_setup(ga, app_id)) == NULL) { + res = 2; + goto done; + } + + /* Announce our intent to begin the sync process */ + if (sync_lock_start(idfs, &lock) < 0) { + res = 2; + goto done; + } + + log_printf(IA_INFO, ">> Enumerating files to sync. Please wait...\n"); + if (enumerate_sync(idfs, argv[0], &tasks) < 0) { + log_printf(IA_ERROR, "Enumerating files for syncing failed!\n"); + res = 2; + goto done; + } + + if (dry_run) { + strlist_foreach(dir, tasks.make_dirs) + { + printf("MKD %s\n", dir); + } + + strlist_foreach(ent, tasks.to_delete) + { + printf("DEL %s\n", ent); + } + + strlist_foreach(ent, tasks.to_copy) + { + printf("CPY %s\n", ent); + } + goto done; + } + + log_printf(IA_INFO, ">> Syncing %zu files.\n", tasks.to_copy->len); + + /* Setup signal handler to print progress */ + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = timer_handler; + sigemptyset(&sa.sa_mask); + if ((sigaction(SIGUSR1, &sa, NULL) == -1)) { + log_printf(IA_ERROR, + "Can't set signal handler? (%s) Continuing anyway...\n", + strerror(errno)); + } else if (progress) { + struct sigevent sev; + + sev.sigev_notify = SIGEV_SIGNAL; + sev.sigev_signo = SIGUSR1; + sev.sigev_value.sival_ptr = &timerid; + if (timer_create(CLOCK_MONOTONIC, &sev, &timerid) == -1) { + log_printf( + IA_ERROR, "Can't setup progress timer: %s\n", strerror(errno)); + } else { + struct itimerspec its; + + its.it_value.tv_sec = 1; + its.it_value.tv_nsec = 0; + its.it_interval.tv_sec = its.it_value.tv_sec; + its.it_interval.tv_nsec = its.it_value.tv_nsec; + + if (timer_settime(timerid, 0, &its, NULL) == -1) { + log_printf(IA_ERROR, + "Can't start progress timer: %s\n", + strerror(errno)); + } else { + timer_set = true; + } + } + } + + strlist_foreach(dir, tasks.make_dirs) + { + char* full_path = idevfs_canonpath(idfs, dir); + ALLOC_ASSERT(full_path); + log_printf(IA_DEBUG, "mkd %s\n", full_path); + err = idevfs_mkpath(idfs, full_path); + if (err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Can't make remote dir %s: %s\n", + full_path, + afc_strerror(err)); + } + free(full_path); + } + + if (allow_delete) { + strlist_foreach(ent, tasks.to_delete) + { + char* full_path = idevfs_canonpath(idfs, ent); + ALLOC_ASSERT(full_path); + log_printf(IA_DEBUG, "rm %s\n", full_path); + err = afc_remove_path_and_contents(idfs->afc, full_path); + if (err != AFC_E_SUCCESS) { + log_printf( + IA_ERROR, "Can't delete remote %s: %s\n", ent, afc_strerror); + } + free(full_path); + } + } + + strlist_foreach(ent, tasks.to_copy) + { + char* rem_path = idevfs_canonpath(idfs, ent); + char* local_path = join_path(argv[0], ent); + + ALLOC_ASSERT(rem_path); + ALLOC_ASSERT(local_path); + + log_printf(IA_VERBOSE, "%s\n", ent); + do_copy(idfs, local_path, rem_path); + + free(local_path); + free(rem_path); + } +done: + sync_lock_end(&lock); + + if (timer_set) + timer_delete(timerid); + if (tasks.make_dirs) + strlist_free(tasks.make_dirs); + if (tasks.to_copy) + strlist_free(tasks.to_copy); + if (tasks.to_delete) + strlist_free(tasks.to_delete); + if (tasks.to_ignore) + strlist_free(tasks.to_ignore); + if (idfs) + idevfs_free(idfs); + return res; +} \ No newline at end of file diff --git a/src/cmds.h b/src/cmds.h new file mode 100644 index 0000000..8f93448 --- /dev/null +++ b/src/cmds.h @@ -0,0 +1,24 @@ +// cmds.h: Declares subcommand functions +// The actual commands can be found in their respective .c files. + +#ifndef __IASYNC_CMDS_H +#define __IASYNC_CMDS_H + +typedef struct _global_args +{ + /// @brief Device name, or NULL if not provided + char* name; + /// @brief Device UDID, or NULL if not provided + char* udid; +} globargs_t; + +typedef int (*cmd_func_t)(int, char*[], globargs_t*); + +#define __DECLARE_CMD(cn) int cmd_##cn(int, char*[], globargs_t*) + +__DECLARE_CMD(lsdev); +__DECLARE_CMD(lsapps); +__DECLARE_CMD(ls); +__DECLARE_CMD(sync); + +#endif /* !__IASYNC_CMDS_H */ \ No newline at end of file diff --git a/src/idevfs.c b/src/idevfs.c new file mode 100644 index 0000000..b9a105d --- /dev/null +++ b/src/idevfs.c @@ -0,0 +1,575 @@ +// idevfs.c +#include "config.h" + +#include "idevfs.h" +#include "log.h" +#include "util.h" + +#include +#include +#include + +#define HA_CMD_DOCUMENTS "VendDocuments" +#define DOCUMENTS_ROOT "/Documents" + +idevfs_t* +idevfs_setup(globargs_t* ga, const char* app_id) +{ + house_arrest_error_t ha_err; + lockdownd_error_t ld_err; + afc_error_t afc_err; + idevfs_t* idfs; + plist_t res, err_node; + + if (ga == NULL || app_id == NULL) + return NULL; + + idfs = calloc(1, sizeof(idevfs_t)); + if (idfs == NULL) { + log_printf(IA_ERROR, "Memory allocation error!\n"); + return NULL; + } + + idfs->dev = get_idevice_by_globargs(ga); + if (idfs->dev == NULL) { + log_printf(IA_ERROR, "Couldn't find a device to connect to.\n"); + idevfs_free(idfs); + return NULL; + } + + ld_err = lockdownd_client_new_with_handshake( + idfs->dev, &(idfs->lockdown), PROJECT_NAME); + if (ld_err != LOCKDOWN_E_SUCCESS) { + switch (ld_err) { + case LOCKDOWN_E_PASSWORD_PROTECTED: + log_printf(IA_ERROR, + "Your device appears to be locked. Please unlock it to " + "continue pairing.\n"); + break; + case LOCKDOWN_E_PAIRING_DIALOG_RESPONSE_PENDING: + log_printf( + IA_ERROR, + "Please accept the pairing dialog to continue pairing.\n"); + break; + case LOCKDOWN_E_USER_DENIED_PAIRING: + log_printf(IA_ERROR, "Pairing was denied from the device.\n"); + break; + default: + log_printf(IA_ERROR, "Pairing failed: Error %d\n", ld_err); + } + idevfs_free(idfs); + return NULL; + } + + /* TODO: Why does house_arrest_client_start_service not work, but this does? + */ + ld_err = lockdownd_start_service( + idfs->lockdown, HOUSE_ARREST_SERVICE_NAME, &(idfs->ha_svc)); + if (ld_err != LOCKDOWN_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't start document sharing service! Error %d\n", + ld_err); + idevfs_free(idfs); + return NULL; + } else if (idfs->ha_svc == NULL) { + log_printf(IA_ERROR, + "Document sharing service was started, but we weren't given " + "access?\n"); + idevfs_free(idfs); + return NULL; + } + + ha_err = house_arrest_client_new(idfs->dev, idfs->ha_svc, &(idfs->ha)); + if (ha_err != HOUSE_ARREST_E_SUCCESS) { + log_printf( + IA_ERROR, + "Couldn't connect to the document transfer service! Error %d\n", + ha_err); + idevfs_free(idfs); + return NULL; + } + + ha_err = house_arrest_send_command(idfs->ha, HA_CMD_DOCUMENTS, app_id); + if (ha_err != HOUSE_ARREST_E_SUCCESS) { + log_printf(IA_ERROR, + "Error requesting access to documents for %s! Error %d\n", + app_id, + ha_err); + idevfs_free(idfs); + return NULL; + } + + ha_err = house_arrest_get_result(idfs->ha, &res); + if (ha_err != HOUSE_ARREST_E_SUCCESS) { + log_printf( + IA_ERROR, + "Couldn't get result from document sharing service! Error %d\n", + ha_err); + idevfs_free(idfs); + return NULL; + } + + err_node = plist_dict_get_item(res, "Error"); + if (err_node != NULL) { + char* val; + plist_get_string_val(err_node, &val); + log_printf(IA_ERROR, "Document sharing service error: %s\n", val); + free(val); + } + plist_free(res); + + afc_err = afc_client_new_from_house_arrest_client(idfs->ha, &(idfs->afc)); + if (afc_err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Error loading file sharing service: %s\n", + afc_strerror(afc_err)); + idevfs_free(idfs); + return NULL; + } + + strncpy(idfs->cwd, DOCUMENTS_ROOT, strlen(DOCUMENTS_ROOT) + 1); + + return idfs; +} + +void +idevfs_free(idevfs_t* idfs) +{ + if (idfs == NULL) + return; + + if (idfs->afc != NULL) { + afc_client_free(idfs->afc); + idfs->afc = NULL; + } + + if (idfs->ha != NULL) { + house_arrest_client_free(idfs->ha); + idfs->ha = NULL; + } + + if (idfs->ha_svc != NULL) { + lockdownd_service_descriptor_free(idfs->ha_svc); + idfs->ha_svc = NULL; + } + + if (idfs->lockdown != NULL) { + lockdownd_client_free(idfs->lockdown); + idfs->lockdown = NULL; + } + + if (idfs->dev != NULL) { + idevice_free(idfs->dev); + idfs->dev = NULL; + } + + free(idfs); + + return; +} + +char* +make_docs_path(const char* path) +{ + char* final; + int final_len; + const char* fmt = "/Documents/%s"; + + if (path == NULL) + return NULL; + + final_len = snprintf(NULL, 0, fmt, path); + if (final_len < 0) + return NULL; + + final = calloc(final_len + 1, sizeof(char)); + if (final == NULL) + return NULL; + + snprintf(final, final_len + 1, fmt, path); + + return final; +} + +/* + * Largely taken from NetBSD libc's realpath(), with the exclusion of lstat. + */ +char* +idevfs_canonpath(idevfs_t* idfs, const char* path) +{ + char *p, *resolved; + const char* q; + + if (idfs == NULL || path == NULL) + return NULL; + + resolved = malloc(IOS_PATH_MAX); + if (resolved == NULL) + return NULL; + + if (*path == '\0') { + strncpy(resolved, idfs->cwd, IOS_PATH_MAX); + return resolved; + } + + /* + * `p' is where we'll put a new component with prepending + * a delimiter. + */ + p = resolved; + + if (*path != '/') { + p = stpncpy(resolved, idfs->cwd, IOS_PATH_MAX); + } + +loop: + /* Skip any slash. */ + while (*path == '/') + path++; + + if (*path == '\0') { + if (p == resolved) + *p = '\0'; + return resolved; + } + + q = path; + do + q++; + while (*q != '/' && *q != '\0'); + + /* Test . or .. */ + if (path[0] == '.') { + if (q - path == 1) { + path = q; + goto loop; + } + if (path[1] == '.' && q - path == 2) { + /* Trim the last component. */ + if (p != resolved) + while (*--p != '/') + ; + path = q; + goto loop; + } + } + + /* Append this component. */ + if (p - resolved + 1 + q - path + 1 > IOS_PATH_MAX) { + if (p == resolved) + *p++ = '/'; + *p = '\0'; + goto out; + } + p[0] = '/'; + memcpy(&p[1], + path, + /* LINTED We know q > path. */ + q - path); + p[1 + q - path] = '\0'; + + /* Advance both resolved and unresolved path. */ + p += 1 + q - path; + path = q; + goto loop; +out: + free(resolved); + return NULL; +} + +afc_error_t +idevfs_chdir(idevfs_t* idfs, const char* dir) +{ + char** info = NULL; + char* new_dir = NULL; + afc_error_t err; + struct stat64 st; + + if (idfs == NULL || dir == NULL) + return AFC_E_INVALID_ARG; + + new_dir = idevfs_canonpath(idfs, dir); + if (new_dir == NULL) + return AFC_E_NO_MEM; + + err = idevfs_stat(idfs, idfs->cwd, &st); + if (err != AFC_E_SUCCESS) { + goto done; + } + + if (S_ISDIR(st.st_mode)) { + /* OK, this is a dir! */ + afc_dictionary_free(info); + strncpy(idfs->cwd, new_dir, IOS_PATH_MAX); + return AFC_E_SUCCESS; + } else { + /* + * AFC_E_NOT_A_DIR doesn't seem to exist, so I guess this is the + * best we can do? + */ + err = AFC_E_INVALID_ARG; + } +done: + if (new_dir) + free(new_dir); + + if (info) + afc_dictionary_free(info); + + return err; +} + +/// @brief Performs `strtoull` in a way that tries to avoid overflows. +/// @param in The variable to store the integer. +/// @param str The numeric string to convert to an integer. +#define CHECKED_STRTOU(in, str) \ + { \ + const uint64_t _len = sizeof(in) * 8; \ + const uint64_t _max_int = \ + (sizeof(in) >= 8) ? UINT64_MAX : (uint64_t)((1 << (_len + 1)) - 1); \ + uint64_t _res = strtoull(str, NULL, 10); \ + in = (_res > _max_int) ? _max_int : _res; \ + } + +afc_error_t +idevfs_stat(idevfs_t* idfs, const char* path, struct stat64* st) +{ + char* real_path; + char** info; + afc_error_t err; + + if (idfs == NULL || path == NULL || st == NULL) + return AFC_E_INVALID_ARG; + + real_path = idevfs_canonpath(idfs, path); + if (real_path == NULL) + return AFC_E_NO_MEM; + + err = afc_get_file_info(idfs->afc, real_path, &info); + if (err != AFC_E_SUCCESS) { + free(real_path); + return err; + } + + memset(st, 0, sizeof(struct stat64)); + + for (int i = 0; info[i] != NULL; i += 2) { + const char *key = info[i], *val = info[i + 1]; + if (!strcmp(key, "st_dev")) { + CHECKED_STRTOU(st->st_dev, val); + } else if (!strcmp(key, "st_ifmt")) { + if (!strcmp(val, "S_IFIFO")) { + st->st_mode = S_IFIFO; + } else if (!strcmp(val, "S_IFCHR")) { + st->st_mode = S_IFCHR; + } else if (!strcmp(val, "S_IFDIR")) { + st->st_mode = S_IFDIR; + } else if (!strcmp(val, "S_IFBLK")) { + st->st_mode = S_IFBLK; + } else if (!strcmp(val, "S_IFREG")) { + st->st_mode = S_IFREG; + } else if (!strcmp(val, "S_IFLNK")) { + st->st_mode = S_IFLNK; + } else if (!strcmp(val, "S_IFSOCK")) { + st->st_mode = S_IFSOCK; + } + /* + * S_IFWHT not supported on Linux and a pretty rare case, so + * we're skipping it here + */ + } else if (!strcmp(key, "st_nlink")) { + CHECKED_STRTOU(st->st_nlink, val); + } else if (!strcmp(key, "st_ino")) { + CHECKED_STRTOU(st->st_ino, val); + } else if (!strcmp(key, "st_uid")) { + CHECKED_STRTOU(st->st_uid, val); + } else if (!strcmp(key, "st_gid")) { + CHECKED_STRTOU(st->st_gid, val); + } else if (!strcmp(key, "st_size")) { + CHECKED_STRTOU(st->st_size, val); + } else if (!strcmp(key, "st_blocks")) { + CHECKED_STRTOU(st->st_blocks, val); + } else if (!strcmp(key, "st_blksize")) { + CHECKED_STRTOU(st->st_blksize, val); + } else if (!strcmp(key, "st_mtime")) { + uint64_t ts = strtoull(val, NULL, 10); + st->st_mtim.tv_sec = ts / 1000000000; + st->st_mtim.tv_nsec = ts % 1000000000; + } + } + + afc_dictionary_free(info); + free(real_path); + + return AFC_E_SUCCESS; +} + +/* + * Highly adapted from mkpath() in NetBSD src/bin/mkdir/mkdir.c + */ +afc_error_t +idevfs_mkpath(idevfs_t* idfs, const char* p) +{ + struct stat64 st; + char path[IOS_PATH_MAX] = { 0 }; + char* slash; + afc_error_t err; + + if (idfs == NULL || p == NULL) + return AFC_E_INVALID_ARG; + + strncpy(path, p, IOS_PATH_MAX); + + slash = path; + + for (;;) { + int done; + + slash += strspn(slash, "/"); + slash += strcspn(slash, "/"); + + done = (*(slash + strspn(slash, "/")) == '\0'); + *slash = '\0'; + + err = afc_make_directory(idfs->afc, path); + if (err != AFC_E_SUCCESS) { + /* + * Can't create; path exists or no perms. + * stat() path to determine what's there now. + */ + afc_error_t stat_err = idevfs_stat(idfs, path, &st); + if (stat_err != AFC_E_SUCCESS) { + /* Not there; use make_dir's error */ + return err; + } + if (!S_ISDIR(st.st_mode)) { + /* Is there, but isn't a directory */ + return AFC_E_OBJECT_NOT_FOUND; + } + } + + if (done) { + break; + } + *slash = '/'; + } + + return AFC_E_SUCCESS; +} + +void +idevfs_list_free(char** list) +{ + for (int i = 0; list[i] != NULL; i++) { + free(list[i]); + } + free(list); + return; +} + +afc_error_t +idevfs_readdir(idevfs_t* idfs, const char* dir, char*** list, int* len) +{ + char** info = NULL; + char** lp = NULL; + char* real_path = NULL; + int ilen; + afc_error_t err; + + if (idfs == NULL || dir == NULL || list == NULL) + return AFC_E_INVALID_ARG; + + *list = NULL; + + real_path = idevfs_canonpath(idfs, dir); + if (real_path == NULL) + return AFC_E_NO_MEM; + + err = afc_read_directory(idfs->afc, real_path, &info); + if (err != AFC_E_SUCCESS) { + goto out; + } + + /* Pass 1: Get the length of the info list */ + for (ilen = 0; info[ilen] != NULL; ilen++) + ; + + lp = calloc(ilen + 1, sizeof(char*)); + if (lp == NULL) { + err = AFC_E_NO_MEM; + goto out; + } + + /* Pass 2: Make a list with all "absolute" paths */ + for (int i = 0; i < ilen; i++) { + lp[i] = join_path(real_path, info[i]); + if (lp[i] == NULL) { + err = AFC_E_NO_MEM; + goto out; + } + } + + *list = lp; + + if (len != NULL) { + *len = ilen; + } +out: + if (err != AFC_E_SUCCESS && lp) { + idevfs_list_free(lp); + } + + if (info) + afc_dictionary_free(info); + + if (real_path) + free(real_path); + + return err; +} + +int +split_appid_path(const char* arg, char** app_id, char** path) +{ + const char* path_part = NULL; + + if (arg == NULL || app_id == NULL || path == NULL) + return -1; + + *path = *app_id = NULL; + + if ((path_part = strchr(arg, ':')) != NULL) { + /* Split the path and path parts. */ + int app_id_len = path_part - arg; + *path = make_docs_path(path_part + 1); + if (*path == NULL) + return -1; + + *app_id = calloc(app_id_len + 1, sizeof(char)); + if (*app_id == NULL) { + free(*path); + *path = NULL; + return -1; + } + + strncpy(*app_id, arg, app_id_len); + } else { + /* + * No path portion. Just copy the app ID and use the "root" Documents + * dir. + */ + int app_id_len = strlen(arg); + + /* + * the alloc is entirely unnecessary here, but makes it easier for the + * caller (they can always free()) + */ + *app_id = calloc(strlen(arg) + 1, sizeof(char)); + if (*app_id == NULL) + return -1; + strncpy(*app_id, arg, app_id_len + 1); + *path = make_docs_path(""); + } + + return 0; +} \ No newline at end of file diff --git a/src/idevfs.h b/src/idevfs.h new file mode 100644 index 0000000..60dbeb1 --- /dev/null +++ b/src/idevfs.h @@ -0,0 +1,102 @@ +// idevfs.h: Common tasks for idevice/house_arrest/afc + +#ifndef __IASYNC_IDEVFS_H +#define __IASYNC_IDEVFS_H + +#define _XOPEN_SOURCE 700 + +#ifndef _LARGEFILE64_SOURCE +#define _LARGEFILE64_SOURCE +#endif + +#include "cmds.h" + +#include +#include +#include + +struct stat64; + +#include +#include + +#define IOS_PATH_MAX 4096 + +typedef struct _idevfs_t +{ + idevice_t dev; + lockdownd_client_t lockdown; + lockdownd_service_descriptor_t ha_svc; + house_arrest_client_t ha; + afc_client_t afc; + char cwd[IOS_PATH_MAX]; +} idevfs_t; + +/// @brief Perform usual setup of an idevice, configuring HA/AFC on the given +/// Bundle ID. +/// @param ga Global args passed from `main()`. +/// @param app_id The application ID to connect to. +/// @return The `idevfs_t` structure for the device on success, or NULL +/// otherwise. +idevfs_t* +idevfs_setup(globargs_t* ga, const char* app_id); + +/// @brief Frees an `idevfs_t` structure created by `idevfs_setup`. +void +idevfs_free(idevfs_t* idfs); + +/// @brief Creates an absolute path from the given path string. +/// @param idfs The `idevfs_t` structure that will provide the current working +/// directory. +/// @param path The path to turn into an absolute path. +/// @return An absolute path that may or may not exist on the remote device, or +/// NULL if an error occurred. +char* +idevfs_canonpath(idevfs_t* idfs, const char* path); + +/// @brief "Changes" the directory of the idevfs device. +/// @param idfs The `idevfs_t` structure. +/// @param dir The path to "change" to. +/// @return `AFC_E_SUCCESS` on success, or an error otherwise. +afc_error_t +idevfs_chdir(idevfs_t* idfs, const char* dir); + +/// @brief Reads the given directory on the idevfs device. +/// @param idfs The `idevfs_t` structure. +/// @param dir The path to read, relative to the current directory. +/// @param list A pointer to populate with the list of absolute paths. +/// @param len If not NULL, a pointer to a `size_t` where the list size will be +/// stored. +/// @return `AFC_E_SUCCESS` on success, or an error otherwise. +afc_error_t +idevfs_readdir(idevfs_t* idfs, const char* dir, char*** list, int* len); + +afc_error_t +idevfs_stat(idevfs_t* idfs, const char* path, struct stat64* st); + +/// @brief Makes the given directory and all parents. +/// @param idfs The `idevfs_t` structure. +/// @param p The path to create. All parent directories will also be created. +/// @return `AFC_E_SUCCESS` on success, or an error otherwise. +afc_error_t +idevfs_mkpath(idevfs_t* idfs, const char* p); + +void +idevfs_list_free(char** list); + +/// @brief Makes a path relative to the common app Documents folder. +char* +make_docs_path(const char* path); + +/// @brief Takes a potential remote path and splits it into the App Bundle ID +/// and absolute app path. +/// @param arg The potential remote path argument. +/// @param app_id Pointer where the App ID string will be stored. This must be +/// freed by the caller. +/// @param path Pointer where the remote path string will be stored. This must +/// be freed by the caller. +/// @return 0 on success, -1 otherwise. +int +split_appid_path(const char* arg, char** app_id, char** path); + +#endif /* !__IASYNC_IDEVFS_H */ diff --git a/src/log.c b/src/log.c new file mode 100644 index 0000000..d9e26b6 --- /dev/null +++ b/src/log.c @@ -0,0 +1,29 @@ +// log.c + +#include "log.h" + +#include +#include +#include + +static enum log_level max_level = IA_INFO; + +void +log_printf(enum log_level level, const char* fmt, ...) +{ + if (level <= max_level) { + va_list ap; + + fprintf(stderr, "%s: ", getprogname()); + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + } +} + +void +set_log_level(enum log_level level) +{ + max_level = level; +} \ No newline at end of file diff --git a/src/log.h b/src/log.h new file mode 100644 index 0000000..2dcfc72 --- /dev/null +++ b/src/log.h @@ -0,0 +1,35 @@ +// log.h: Logging routines + +#ifndef __IASYNC_LOG_H +#define __IASYNC_LOG_H + +/// @brief Dies loudly (writes the error to stdout and exits with return value +/// 2). +#define die(...) \ + { \ + log_printf(IA_CRITICAL, __VA_ARGS__); \ + exit(2); \ + } + +enum log_level +{ + IA_CRITICAL = 0, + IA_ERROR = 1, + IA_WARNING = 2, + IA_INFO = 3, + IA_VERBOSE = 4, + IA_DEBUG = 5, + IA_TRACE = 6, +}; + +/// @brief Writes a `printf`-like formatted string to stdout, based on the given +/// verbosity. +/// @param level The desired log level for the text. +void +log_printf(enum log_level level, const char* fmt, ...); + +/// @brief Sets the highest permitted log level. +void +set_log_level(enum log_level level); + +#endif /* !__IASYNC_LOG_H */ \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..d004742 --- /dev/null +++ b/src/main.c @@ -0,0 +1,135 @@ +#include "config.h" + +#include +#include +#include +#include + +#include + +#include +#include + +#include "cmds.h" +#include "log.h" + +typedef struct _cmd_meta +{ + const char* name; + const char* desc; + const char* usage_text; + cmd_func_t fn; +} cmd_meta_t; + +static cmd_meta_t cmd_meta[] = { + { .name = "lsdevs", .usage_text = "[-n | --no-headers]", .fn = cmd_lsdev }, + { .name = "lsapps", .usage_text = "[-n | --no-headers]", .fn = cmd_lsapps }, + { .name = "ls", .usage_text = "[-a | --all] app_id[:path]", .fn = cmd_ls }, + { .name = "sync", + .usage_text = "[-DNP] local_path app_id[:path]", + .fn = cmd_sync }, +}; +const int cmd_count = (sizeof(cmd_meta) / sizeof(cmd_meta_t)); + +void +usage(const char* err) +{ + int i; + int res = 1; + + if (err != NULL) { + log_printf(IA_CRITICAL, "%s: %s\n", getprogname(), err); + res = 2; + } + + fprintf(stderr, + "Usage: %s [-vq] [-n name | -u udid] command\n" + "Commands:\n", + getprogname()); + + for (i = 0; i < cmd_count; i++) { + fprintf(stderr, "\t%s", cmd_meta[i].name); + if (cmd_meta[i].usage_text) { + fprintf(stderr, " %s", cmd_meta[i].usage_text); + } + fprintf(stderr, "\n"); + } + + exit(res); +} + +int +main(int argc, char* argv[]) +{ + int i, ch; + globargs_t ga = { 0 }; + enum log_level verbosity = IA_INFO; + bool q_set = false, v_set = false; + + setprogname(argv[0]); + + struct option longopts[] = { + { "name", required_argument, NULL, 'n' }, + { "udid", required_argument, NULL, 'u' }, + { "verbose", no_argument, NULL, 'v' }, + { "quiet", no_argument, NULL, 'q' }, + { "help", no_argument, NULL, 'h' }, + { NULL, 0, NULL, 0 } + }; + + while ((ch = getopt_long(argc, argv, "+vqn:u:h", longopts, NULL)) != -1) { + switch (ch) { + case 'n': + if (ga.udid != NULL) { + usage("Only device UDID or name should be set."); + } + ga.name = optarg; + break; + case 'u': + if (ga.name != NULL) { + usage("Only device UDID or name should be set."); + } + ga.udid = optarg; + break; + case 'v': + if (q_set) + usage("-q and -v are mutually exclusive."); + if (verbosity < IA_TRACE) + verbosity++; + v_set = true; + break; + case 'q': + if (v_set) + usage("-q and -v are mutually exclusive."); + if (verbosity > IA_CRITICAL) + verbosity--; + q_set = true; + break; + case '?': + default: + usage(NULL); + } + } + + argc -= optind; + argv += optind; + + if (argc < 1) { + usage(NULL); + } + + set_log_level(verbosity); + + for (i = 0; i < cmd_count; i++) { + if (!strcmp(cmd_meta[i].name, argv[0])) { + int res = (cmd_meta[i].fn)(argc, argv, &ga); + if (res == -1) + usage(NULL); + else + return res; + } + } + + usage("Unknown command."); + /* NOTREACHED */ +} diff --git a/src/strlist.c b/src/strlist.c new file mode 100644 index 0000000..5b50419 --- /dev/null +++ b/src/strlist.c @@ -0,0 +1,172 @@ +// strlist.c +#include "config.h" + +#ifndef _XOPEN_SOURCE +#define _XOPEN_SOURCE 600 +#endif + +#include "log.h" +#include "strlist.h" +#include "util.h" + +#include + +#include +#include +#include + +// How big of a chunk to allocate by default. +static const size_t DEFAULT_ALLOC = 8; + +// Grows the strlist, if needed, to avoid constant reallocs +static void +strlist_grow(strlist_t list, size_t add) +{ + char** p; + size_t new_cap; + + /* If we have capacity, we don't need to realloc */ + if (list->len + add < list->capacity) + return; + + /* Ensure we don't have a length overflow */ + if (add > 0 && list->len > SIZE_MAX - add) { + log_printf(IA_CRITICAL, "%s: List overflow!\n", __func__); + abort(); + } + + /* Try to avoid an overflow by maxing out at SIZE_MAX */ + new_cap = MIN(list->capacity, SSIZE_MAX / 2) * 2; + new_cap = MAX(new_cap, list->len + add); + new_cap = MAX(new_cap, DEFAULT_ALLOC); + + p = realloc(list->begin, new_cap * sizeof(char*)); + ALLOC_ASSERT(p); + list->capacity = new_cap; + if (p != list->begin) { + list->begin = p; + } +} + +strlist_t +strlist_new(char** list, int len) +{ + strlist_t slist = calloc(1, sizeof(struct _strlist)); + size_t ulen = 0; + ALLOC_ASSERT(slist); + if (list != NULL && len != 0) { + if (len < 0) { + /* Get len by traversing through the source list */ + for (ulen = 0; list[ulen] != NULL; ulen++) + ; + } else { + ulen = len; + } + } + + slist->len = ulen; + strlist_grow(slist, ulen); + + if (list != NULL && len != 0) { + /* slist->begin MUST be allocated appropriately by this point */ + memcpy(slist->begin, list, slist->len * sizeof(char*)); + } + + return slist; +} + +size_t +strlist_push(strlist_t list, char* str) +{ + if (list == NULL) + return 0; + + strlist_grow(list, 1); + list->begin[list->len] = str; + list->len++; + + return list->len; +} + +size_t +strlist_pushall(strlist_t list, char** strs, int count) +{ + size_t ulen; + + if (list == NULL) + return 0; + + if (strs == NULL || count == 0) + return list->len; + + if (count < 0) { + /* Get len by traversing the list */ + for (ulen = 0; strs[ulen] != NULL; ulen++) + ; + } else { + ulen = count; + } + + strlist_grow(list, ulen); + + memcpy(list->begin + list->len, strs, ulen * sizeof(char*)); + list->len += ulen; + + return list->len; +} + +size_t +strlist_cat(strlist_t dest, strlist_t src) +{ + size_t res; + + if (dest == NULL) + return 0; + if (src == NULL) + return dest->len; + + res = strlist_pushall(dest, src->begin, src->len); + free(src); + + return res; +} + +char* +strlist_pop(strlist_t list) +{ + char* p; + + if (list == NULL) + return NULL; + if (list->len == 0) + return NULL; + + p = list->begin[--list->len]; + + return p; +} + +char* +strlist_claim(strlist_t list, size_t idx) +{ + char *p, **ent; + if (list == NULL || list->len < idx) + return NULL; + + ent = list->begin + idx; + p = *ent; + *ent = NULL; + + return p; +} + +void +strlist_free(strlist_t list) +{ + for (size_t i = 0; i < list->len; i++) { + if (list->begin[i] != NULL) + free(list->begin[i]); + } + + free(list); +} \ No newline at end of file diff --git a/src/strlist.h b/src/strlist.h new file mode 100644 index 0000000..55b8d9a --- /dev/null +++ b/src/strlist.h @@ -0,0 +1,66 @@ +// strlist.h: Dynamically-allocated lists of strings. + +#include + +typedef struct _strlist +{ + /// @brief Pointer to the start of the array. + char** begin; + /// @brief The allocated capacity of the array. This must be >= len. + size_t capacity; + /// @brief The amount of strings stored in the array. + size_t len; +}* strlist_t; + +/// @brief Creates a new strlist. +/// @param list The initial list to use, or NULL. +/// @param count The initial list's count. If `list` is NULL, this is unused. +/// @return The newly-allocated `strlist_t`, or NULL if there was an error. +strlist_t +strlist_new(char** list, int len); + +/// @brief Pushes a string to the end of the array. +/// @param list The list to push to. +/// @param str The string to push. +/// @return The new length of the list. +size_t +strlist_push(strlist_t list, char* str); + +/// @brief Pushes all strings in a given array to this list. +/// @param list The list to push to. +/// @param strs The source list of strings. +/// @param count The number of entries to push. If this is < 0, the array will +/// be assumed to be null-terminated. +/// @return The new length of the list. +size_t +strlist_pushall(strlist_t list, char** strs, int count); + +/// @brief Combines two strlists by appending `src` to `dest`. +/// @param dest The strlist to append to. +/// @param src The strlist to take entries from. This list will be freed once +/// consumed. +/// @return The new length of `dest`. +size_t +strlist_cat(strlist_t dest, strlist_t src); + +/// @brief Pops a string off the end of the array. +/// @return The last string of the array, or NULL if no string was available. +char* +strlist_pop(strlist_t list); + +/// @brief Claims the given index, setting it to NULL. +/// @param list The list to claim from. +/// @param idx The index of the string to claim. +/// @return The claimed string, or NULL. +char* +strlist_claim(strlist_t list, size_t idx); + +/// @brief Frees a strlist created by `strlist_new`. +/// @param arr The strlist to free. +void +strlist_free(strlist_t list); + +#define strlist_foreach(ent, list) \ + for (char **ent##_p = (list)->begin, *ent = *ent##_p; \ + ent##_p < ((list)->begin + (list)->len); \ + ent = *(++ent##_p)) diff --git a/src/sync_lock.c b/src/sync_lock.c new file mode 100644 index 0000000..11c61bf --- /dev/null +++ b/src/sync_lock.c @@ -0,0 +1,144 @@ +#include "config.h" + +#include +#include +#include + +#include "log.h" +#include "sync_lock.h" + +#define SYNC_LOCK_FILE "/com.apple.itunes.lock_sync" + +#define NP_POST(np, msg) \ + { \ + np_error_t _np_err = np_post_notification((np), (msg)); \ + if (_np_err != NP_E_SUCCESS) { \ + log_printf(IA_ERROR, \ + "Sending notification %s failed. Error %d\n", \ + (msg), \ + _np_err); \ + goto out; \ + } \ + } + +int +sync_lock_start(idevfs_t* idfs, struct sync_lock* lock) +{ + lockdownd_error_t ld_err; + np_error_t np_err; + afc_error_t afc_err; + int res = -1; + + if (idfs == NULL) + return -1; + + if (lock->np != NULL || lock->np_svc != NULL || lock->afc != NULL || + lock->afc_svc != NULL) { + log_printf(IA_ERROR, "BUG: %s called more than once!\n", __func__); + return -1; + } + + ld_err = + lockdownd_start_service(idfs->lockdown, NP_SERVICE_NAME, &(lock->np_svc)); + if (ld_err != LOCKDOWN_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't start sync notification service! Error %d\n", + ld_err); + lock->np_svc = NULL; + goto out; + } else if (lock->np_svc == NULL) { + log_printf( + IA_ERROR, + "Sync notification sharing service was started, but we weren't " + "given access?\n"); + goto out; + } + + np_err = np_client_new(idfs->dev, lock->np_svc, &(lock->np)); + if (np_err != NP_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't connect to sync notification service! Error %d\n", + np_err); + goto out; + } + + ld_err = lockdownd_start_service( + idfs->lockdown, AFC_SERVICE_NAME, &(lock->afc_svc)); + if (ld_err != LOCKDOWN_E_SUCCESS) { + log_printf( + IA_ERROR, + "Couldn't access AFC client for sync notifications! Error %d\n", + ld_err); + goto out; + } + + /* + * TODO: I guess we have to wait for AFC to start before we can start a + * client? Why only this? + */ + sleep(1); + + afc_err = afc_client_new(idfs->dev, lock->afc_svc, &(lock->afc)); + if (afc_err != AFC_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't connect to AFC client: %s\n", + afc_strerror(afc_err)); + goto out; + } + + NP_POST(lock->np, NP_SYNC_WILL_START); + + NP_POST(lock->np, NP_SYNC_LOCK_REQUEST); + + afc_err = + afc_file_open(lock->afc, SYNC_LOCK_FILE, AFC_FOPEN_RW, &(lock->lockfd)); + if (afc_err != AFC_E_SUCCESS) { + log_printf( + IA_ERROR, "Error getting sync lock: %s\n", afc_strerror(afc_err)); + goto out; + } + + NP_POST(lock->np, NP_SYNC_DID_START); + + res = 0; +out: + if (res != 0) + sync_lock_end(lock); + return res; +} + +void +sync_lock_end(struct sync_lock* lock) +{ + + if (lock == NULL) + return; + + if (lock->lockfd != 0) { + afc_file_close(lock->afc, lock->lockfd); + lock->lockfd = -1; + } + + if (lock->afc != NULL) { + afc_client_free(lock->afc); + lock->afc = NULL; + } + + if (lock->afc_svc != NULL) { + lockdownd_service_descriptor_free(lock->afc_svc); + lock->afc_svc = NULL; + } + + if (lock->np != NULL) { + np_post_notification(lock->np, NP_SYNC_DID_FINISH); + np_client_free(lock->np); + lock->np = NULL; + } + + if (lock->np_svc != NULL) { + lockdownd_service_descriptor_free(lock->np_svc); + lock->np_svc = NULL; + } + + return; +} \ No newline at end of file diff --git a/src/sync_lock.h b/src/sync_lock.h new file mode 100644 index 0000000..2189265 --- /dev/null +++ b/src/sync_lock.h @@ -0,0 +1,32 @@ +// sync_lock.h: Functions for holding an exclusive lock on sync + +#ifndef __IASYNC_SYNC_LOCK_H +#define __IASYNC_SYNC_LOCK_H + +#include "idevfs.h" +#include +#include +#include +#include + +struct sync_lock +{ + afc_client_t afc; + lockdownd_service_descriptor_t afc_svc; + np_client_t np; + lockdownd_service_descriptor_t np_svc; + uint64_t lockfd; +}; + +#define SYNCLOCK_INIT \ + { \ + NULL, NULL, NULL, NULL, -1, \ + } + +int +sync_lock_start(idevfs_t* idfs, struct sync_lock* lock); + +void +sync_lock_end(struct sync_lock* lock); + +#endif /* !__IASYNC_SYNC_LOCK_H */ \ No newline at end of file diff --git a/src/util.c b/src/util.c new file mode 100644 index 0000000..408da98 --- /dev/null +++ b/src/util.c @@ -0,0 +1,150 @@ +// util.c: Utility functions +#include "config.h" + +#include + +#include +#include +#include +#include + +#include "cmds.h" +#include "log.h" +#include "util.h" + +char* +get_idevice_name(idevice_t device) +{ + lockdownd_client_t client = NULL; + lockdownd_error_t err; + char* name = NULL; + + if (device == NULL) + return NULL; + + err = lockdownd_client_new_with_handshake(device, &client, PROJECT_NAME); + if (err != LOCKDOWN_E_SUCCESS) { + return NULL; + } + + lockdownd_get_device_name(client, &name); + lockdownd_client_free(client); + + return name; +} + +idevice_t +get_idevice_by_globargs(globargs_t* ga) +{ + idevice_error_t err; + idevice_t dev; + enum idevice_options opts = IDEVICE_LOOKUP_NETWORK | IDEVICE_LOOKUP_USBMUX; + + if (ga == NULL) + return NULL; + + if (ga->name != NULL) { + /* + * The only way to find a device by name is to iterate through every + * UDID, pair with it, and see if it's the one we want. Not pretty, but + * I'm taking the assumption most people don't have a dozen iOS devices + * paired with their computer... + */ + char** udids = NULL; + int count; + + err = idevice_get_device_list(&udids, &count); + if (err != IDEVICE_E_SUCCESS) { + log_printf(IA_ERROR, + "Couldn't get device list! (is usbmux running?)\n"); + return NULL; + } + + for (int i = 0; i < count; i++) { + char* name; + if (idevice_new_with_options(&dev, udids[i], opts) != + IDEVICE_E_SUCCESS) + continue; + name = get_idevice_name(dev); + if (name != NULL) { + if (!strcmp(name, ga->name)) { + free(name); + break; + } else { + // Not our device + free(name); + idevice_free(dev); + dev = NULL; + } + } + } + } else { + if (idevice_new_with_options(&dev, ga->udid, opts) != + IDEVICE_E_SUCCESS) { + return NULL; + } + } + + return dev; +} + +char* +join_path(const char* p1, const char* p2) +{ + char* ccat; + size_t ccat_len; + if (p1 == NULL || p2 == NULL) + return NULL; + + ccat_len = strlen(p1) + strlen(p2) + 2; + + ccat = calloc(ccat_len, sizeof(char)); + if (ccat == NULL) + return NULL; + + snprintf(ccat, ccat_len, "%s/%s", p1, p2); + + return ccat; +} + +/** + * @brief Provides an allocated version of the new substring. + * + * @param root The main string to splice. + * @param begin The index to begin the splice. + * @param end The index to end the splice, or -1 to go until the string ends. + * @return An allocated string representing the substring. + */ +char* +substr(const char* root, int begin, int end) +{ + char* result; + int len; + if (root == NULL) + return NULL; + begin = MAX(begin, 0); + + if (end < 0) { + end = strlen(root); + } + + /* + * XXX: If end > strlen(root), this could be an issue!! But we provide + * length in case a string is potentially not NULL-terminated (fts_read?), + * so we'll have to trust our math. + */ + + if (end < begin) { + log_printf(IA_ERROR, "substr: end (%d) < begin (%d)?\n", end, begin); + return NULL; + } + + /* Includes null terminator */ + len = end - begin + 1; + result = calloc(len, sizeof(char)); + ALLOC_ASSERT(result); + + strncpy(result, root + begin, len); + + return result; +} \ No newline at end of file diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..ef4158d --- /dev/null +++ b/src/util.h @@ -0,0 +1,59 @@ +// util.h: Shared utility functions + +#ifndef __IASYNC_UTIL_H +#define __IASYNC_UTIL_H + +#include +#include +#include + +#define UNUSED(x) (void)(x) + +/// @brief Gets the lesser of the two integers. +#define MIN(a, b) ((/*CONSTCOND*/ (a) < (b)) ? (a) : (b)) +/// @brief Gets the greater of the two integers. +#define MAX(a, b) ((/*CONSTCOND*/ (a) > (b)) ? (a) : (b)) + +#define UNKNOWN_ID "???" +// Allows for printing strings that might be NULL. +#define STRING_OR_UNKNOWN(x) ((x) != NULL) ? (x) : UNKNOWN_ID + +// Dies if ptr is NULL. +#define ALLOC_ASSERT(ptr) \ + if ((ptr) == NULL) { \ + log_printf(IA_CRITICAL, "%s: Memory allocation error!\n", __func__); \ + abort(); \ + } + +#include + +struct _global_args; + +/// @brief Gets an `idevice_t` using the global args provided by the user. +/// @param ga Global argument struct provided to cmd function. +/// @return `idevice_t` if the device is available, or NULL. +idevice_t +get_idevice_by_globargs(struct _global_args* ga); + +/// @brief Gets the name of the provided device. +/// @param device The device to get the name of. +/// @return The name as a string, or NULL if unavailable. The return value must +/// be freed by the caller. +char* +get_idevice_name(idevice_t device); + +char* +join_path(const char* p1, const char* p2); + +/** + * @brief Provides an allocated version of the new substring. + * + * @param root The main string to splice. + * @param begin The index to begin the splice. + * @param end The index to end the splice, or -1 to go until the string ends. + * @return An allocated string representing the substring. + */ +char* +substr(const char* root, int begin, int end); + +#endif /* !__IASYNC_UTIL_H */ \ No newline at end of file diff --git a/subprojects/unity.wrap b/subprojects/unity.wrap new file mode 100644 index 0000000..d7724bf --- /dev/null +++ b/subprojects/unity.wrap @@ -0,0 +1,3 @@ +[wrap-git] +url = https://github.com/ThrowTheSwitch/Unity.git +revision = head \ No newline at end of file diff --git a/tests/strlist.c b/tests/strlist.c new file mode 100644 index 0000000..965b1bb --- /dev/null +++ b/tests/strlist.c @@ -0,0 +1,110 @@ +#include "unity.h" + +#include "strlist.h" + +#include +#include + +void +setUp(void) +{ +} + +void +tearDown(void) +{ +} + +void +test_strlist_init(void) +{ + strlist_t list = strlist_new(NULL, 512); + TEST_ASSERT_NOT_NULL(list); + + TEST_ASSERT_EQUAL(list->capacity, 8); + TEST_ASSERT_EQUAL(list->len, 0); + + strlist_free(list); +} + +void +test_strlist_pushpop(void) +{ + strlist_t list = strlist_new(NULL, 0); + TEST_ASSERT_NOT_NULL(list); + + for (int i = 1; i < 51; i++) { + char* str = calloc(16, sizeof(char)); + TEST_ASSERT_NOT_NULL_MESSAGE(str, + "memory error -- likely not a test fail"); + snprintf(str, 16, "hello%d", i); + TEST_ASSERT_EQUAL(i, strlist_push(list, str)); + } + + for (int i = 50; i > 0; i--) { + char* cmp = calloc(16, sizeof(char)); + TEST_ASSERT_NOT_NULL_MESSAGE(cmp, + "memory error -- likely not a test fail"); + snprintf(cmp, 16, "hello%d", i); + char* str = strlist_pop(list); + TEST_ASSERT_NOT_NULL(str); + TEST_ASSERT_EQUAL_STRING(cmp, str); + free(str); + free(cmp); + } + + strlist_free(list); +} + +void +test_strlist_cat(void) +{ + strlist_t list = strlist_new(NULL, 0); + TEST_ASSERT_NOT_NULL(list); + strlist_t list2 = strlist_new(NULL, 0); + TEST_ASSERT_NOT_NULL(list2); + + for (int i = 1; i < 20; i++) { + char* str = calloc(16, sizeof(char)); + TEST_ASSERT_NOT_NULL_MESSAGE(str, + "memory error -- likely not a test fail"); + snprintf(str, 16, "hello%d", i); + strlist_push(list, str); + } + + for (int i = 20; i < 41; i++) { + char* str = calloc(16, sizeof(char)); + TEST_ASSERT_NOT_NULL_MESSAGE(str, + "memory error -- likely not a test fail"); + snprintf(str, 16, "hello%d", i); + strlist_push(list2, str); + } + + TEST_ASSERT_EQUAL(strlist_cat(list, list2), 40); + + for (int i = 40; i > 0; i--) { + char* cmp = calloc(16, sizeof(char)); + TEST_ASSERT_NOT_NULL_MESSAGE(cmp, + "memory error -- likely not a test fail"); + snprintf(cmp, 16, "hello%d", i); + char* str = strlist_pop(list); + TEST_ASSERT_NOT_NULL(str); + TEST_ASSERT_EQUAL_STRING(cmp, str); + } + + strlist_free(list); +} + +void +test_strlist_claim(void) +{ + char* my_vals[] = { "hello", "world", "hi", NULL }; + strlist_t list = strlist_new(my_vals, -1); + char* val = strlist_claim(list, 1); + + TEST_ASSERT_NOT_NULL(val); + TEST_ASSERT_EQUAL_STRING("world", val); + TEST_ASSERT_NULL(list->begin[1]); + + free(list); +} \ No newline at end of file