Initial commit

This commit is contained in:
snow flurry 2025-01-22 20:27:19 -08:00
commit 5dc58c04ae
28 changed files with 3288 additions and 0 deletions

7
.clang-format Normal file
View file

@ -0,0 +1,7 @@
---
BasedOnStyle: Mozilla
AlignAfterOpenBracket: Align
AlignArrayOfStructures: Left
AlwaysBreakAfterDefinitionReturnType: All
IndentWidth: 4
IndentCaseLabels: false

21
.gitignore vendored Normal file
View file

@ -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

28
LICENSE Normal file
View file

@ -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.

34
NOTICES Normal file
View file

@ -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.
*/

59
README.md Normal file
View file

@ -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.

3
config.h.meson Normal file
View file

@ -0,0 +1,3 @@
#define PROJECT_NAME "@name@"
#define PROJECT_VER "@version@"

124
doc/iasync.1 Normal file
View file

@ -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 .

190
doc/iasync.1.html Normal file
View file

@ -0,0 +1,190 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<style>
table.head, table.foot { width: 100%; }
td.head-rtitle, td.foot-os { text-align: right; }
td.head-vol { text-align: center; }
.Nd, .Bf, .Op { display: inline; }
.Pa, .Ad { font-style: italic; }
.Ms { font-weight: bold; }
.Bl-diag > dt { font-weight: bold; }
code.Nm, .Fl, .Cm, .Ic, code.In, .Fd, .Fn, .Cd { font-weight: bold;
font-family: inherit; }
</style>
<title>IASYNC(1)</title>
</head>
<body>
<table class="head">
<tr>
<td class="head-ltitle">IASYNC(1)</td>
<td class="head-vol">General Commands Manual</td>
<td class="head-rtitle">IASYNC(1)</td>
</tr>
</table>
<div class="manual-text">
<section class="Sh">
<h1 class="Sh" id="NAME"><a class="permalink" href="#NAME">NAME</a></h1>
<p class="Pp"><code class="Nm">iasync</code> &#x2014; <span class="Nd">sync
files to iOS app folders</span></p>
</section>
<section class="Sh">
<h1 class="Sh" id="SYNOPSIS"><a class="permalink" href="#SYNOPSIS">SYNOPSIS</a></h1>
<table class="Nm">
<tr>
<td><code class="Nm">iasync</code></td>
<td>[<var class="Ar">options</var>] <var class="Ar">command</var>
[<var class="Ar">command_options</var>]</td>
</tr>
</table>
<br/>
<table class="Nm">
<tr>
<td><code class="Nm">iasync</code></td>
<td><var class="Ar">lsdevs</var> [command_options]</td>
</tr>
</table>
<br/>
<table class="Nm">
<tr>
<td><code class="Nm">iasync</code></td>
<td><var class="Ar">lsapps</var> [command_options]</td>
</tr>
</table>
<br/>
<table class="Nm">
<tr>
<td><code class="Nm">iasync</code></td>
<td><var class="Ar">ls</var> [command_options]</td>
</tr>
</table>
<br/>
<table class="Nm">
<tr>
<td><code class="Nm">iasync</code></td>
<td><var class="Ar">sync</var> [command_options]
<var class="Ar">source</var> <var class="Ar">target</var></td>
</tr>
</table>
</section>
<section class="Sh">
<h1 class="Sh" id="DESCRIPTION"><a class="permalink" href="#DESCRIPTION">DESCRIPTION</a></h1>
<p class="Pp"><code class="Nm">iasync</code> syncs files to Documents folders
for iOS apps that support file sharing.</p>
<section class="Ss">
<h2 class="Ss" id="Global_Options"><a class="permalink" href="#Global_Options">Global
Options</a></h2>
<p class="Pp">The following options largely affect all commands, and should be
provided before the command name.</p>
<dl class="Bl-tag">
<dt id="n,"><a class="permalink" href="#n,"><code class="Fl">-n,</code></a>
<code class="Fl">--name</code> <var class="Ar">name</var></dt>
<dd style="width: auto;">&#x00A0;</dd>
<dt id="u,"><a class="permalink" href="#u,"><code class="Fl">-u,</code></a>
<code class="Fl">--udid</code> <var class="Ar">udid</var></dt>
<dd>Connect to the device with the provided name or UDID. This can be
discovered with the <var class="Ar">lsdevs</var> command if the device is
already connected.</dd>
<dt id="v,"><a class="permalink" href="#v,"><code class="Fl">-v,</code></a>
<code class="Fl">--verbose</code></dt>
<dd>Increases the verbosity. This can be used multiple times. This is mutally
exclusive with <code class="Fl">--quiet</code>.</dd>
<dt id="q,"><a class="permalink" href="#q,"><code class="Fl">-q,</code></a>
<code class="Fl">--quiet</code></dt>
<dd>Lowers the verbosity. This is mutually exclusive with
<code class="Fl">--verbose</code>.</dd>
</dl>
</section>
<section class="Ss">
<h2 class="Ss" id="Commands"><a class="permalink" href="#Commands">Commands</a></h2>
<dl class="Bl-tag">
<dt><code class="Nm">iasync</code> <code class="Ic">lsdevs</code>
[<code class="Fl">-n</code> | <code class="Fl">--no-headers</code>]</dt>
<dd>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.
<dl class="Bl-tag">
<dt id="n,~2"><a class="permalink" href="#n,~2"><code class="Fl">-n,</code></a>
<code class="Fl">--no-headers</code></dt>
<dd>Suppress the table headers.</dd>
</dl>
</dd>
<dt><code class="Nm">iasync</code> <code class="Ic">lsapps</code>
[<code class="Fl">-n</code> | <code class="Fl">--no-headers</code>]</dt>
<dd>Lists apps that support file sharing for the connected device.
<dl class="Bl-tag">
<dt id="n,~3"><a class="permalink" href="#n,~3"><code class="Fl">-n,</code></a>
<code class="Fl">--no-headers</code></dt>
<dd>Suppress the table headers.</dd>
</dl>
</dd>
<dt><code class="Nm">iasync</code> <code class="Ic">ls</code>
[<code class="Fl">-a</code> | <code class="Fl">--all</code>]
<var class="Ar">app_id</var>[<var class="Ar">:path</var>]</dt>
<dd>Lists all files in the directory provided by the
<var class="Ar">path</var> argument, relative to the root of the app's
Documents folder.
<dl class="Bl-tag">
<dt id="a,"><a class="permalink" href="#a,"><code class="Fl">-a,</code></a>
<code class="Fl">--all</code></dt>
<dd>Display entries with names starting with a dot (`.').</dd>
</dl>
</dd>
<dt><code class="Nm">iasync</code> <code class="Ic">sync</code>
[<code class="Fl">-Dnp</code>] <var class="Ar">source</var>
<var class="Ar">app_id</var>[<var class="Ar">:path</var>]</dt>
<dd>Copies the files from the <var class="Ar">source</var> directory to the
path provided by the <var class="Ar">app_id</var> and
<var class="Ar">path</var> arguments.
<dl class="Bl-tag">
<dt id="D,"><a class="permalink" href="#D,"><code class="Fl">-D,</code></a>
<code class="Fl">--allow-delete</code></dt>
<dd>Allow the sync operation to delete remote files to ensure the remote
directory tree matches the local tree.</dd>
<dt id="n,~4"><a class="permalink" href="#n,~4"><code class="Fl">-n,</code></a>
<code class="Fl">--dry-run</code></dt>
<dd>Explain what would have been done, instead of performing the
operations.</dd>
<dt id="p,"><a class="permalink" href="#p,"><code class="Fl">-p,</code></a>
<code class="Fl">--progress</code></dt>
<dd>Print progress information for files being copied.</dd>
</dl>
</dd>
</dl>
</section>
</section>
<section class="Sh">
<h1 class="Sh" id="EXIT_STATUS"><a class="permalink" href="#EXIT_STATUS">EXIT
STATUS</a></h1>
<p class="Pp"><code class="Nm">iasync</code> exits with either 1 or 2 if an
error occurred.</p>
</section>
<section class="Sh">
<h1 class="Sh" id="SEE_ALSO"><a class="permalink" href="#SEE_ALSO">SEE
ALSO</a></h1>
<p class="Pp"><a class="Xr">ifuse(1)</a>, <a class="Xr">idevicepair(1)</a></p>
</section>
<section class="Sh">
<h1 class="Sh" id="BUGS"><a class="permalink" href="#BUGS">BUGS</a></h1>
<p class="Pp"><code class="Nm">iasync</code> 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.</p>
<p class="Pp">Bugs can be reported on GitHub at:</p>
<p class="Pp"></p>
<div class="Bd
Bd-indent"><a class="Lk" href="https://github.com/snowkat/iasync">https://github.com/snowkat/iasync</a></div>
<p class="Pp">or by sending an e-mail to
<a class="Mt" href="mailto:snow@datagirl.xyz">snow@datagirl.xyz</a>.</p>
</section>
</div>
<table class="foot">
<tr>
<td class="foot-date">January 22, 2025</td>
<td class="foot-os">&nbsp;</td>
</tr>
</table>
</body>
</html>

145
doc/iasync.1.md Normal file
View file

@ -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

65
meson.build Normal file
View file

@ -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')

85
src/cmd_ls.c Normal file
View file

@ -0,0 +1,85 @@
// cmd_ls.c: List files for a given application
#include "config.h"
#include <libimobiledevice/afc.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/libimobiledevice.h>
#include "cmds.h"
#include "idevfs.h"
#include "log.h"
#include "util.h"
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}

144
src/cmd_lsapps.c Normal file
View file

@ -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 <libimobiledevice/installation_proxy.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*
* 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;
}

114
src/cmd_lsdev.c Normal file
View file

@ -0,0 +1,114 @@
// cmd_lsdev.c: Lists available devices.
#include "config.h"
#include "cmds.h"
#include "log.h"
#include "util.h"
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <getopt.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
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;
}

633
src/cmd_sync.c Normal file
View file

@ -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 <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <signal.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <getopt.h>
#include <libimobiledevice/afc.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fts.h>
/// @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;
}

24
src/cmds.h Normal file
View file

@ -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 */

575
src/idevfs.c Normal file
View file

@ -0,0 +1,575 @@
// idevfs.c
#include "config.h"
#include "idevfs.h"
#include "log.h"
#include "util.h"
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#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;
}

102
src/idevfs.h Normal file
View file

@ -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 <libimobiledevice/afc.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/libimobiledevice.h>
struct stat64;
#include <sys/stat.h>
#include <sys/types.h>
#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 */

29
src/log.c Normal file
View file

@ -0,0 +1,29 @@
// log.c
#include "log.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
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;
}

35
src/log.h Normal file
View file

@ -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 */

135
src/main.c Normal file
View file

@ -0,0 +1,135 @@
#include "config.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#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 */
}

172
src/strlist.c Normal file
View file

@ -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 <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 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);
}

66
src/strlist.h Normal file
View file

@ -0,0 +1,66 @@
// strlist.h: Dynamically-allocated lists of strings.
#include <stddef.h>
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))

144
src/sync_lock.c Normal file
View file

@ -0,0 +1,144 @@
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#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;
}

32
src/sync_lock.h Normal file
View file

@ -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 <libimobiledevice/afc.h>
#include <libimobiledevice/libimobiledevice.h>
#include <libimobiledevice/lockdown.h>
#include <libimobiledevice/notification_proxy.h>
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 */

150
src/util.c Normal file
View file

@ -0,0 +1,150 @@
// util.c: Utility functions
#include "config.h"
#include <libimobiledevice/lockdown.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}

59
src/util.h Normal file
View file

@ -0,0 +1,59 @@
// util.h: Shared utility functions
#ifndef __IASYNC_UTIL_H
#define __IASYNC_UTIL_H
#include <libimobiledevice/afc.h>
#include <libimobiledevice/house_arrest.h>
#include <libimobiledevice/libimobiledevice.h>
#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 <libimobiledevice/libimobiledevice.h>
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 */

3
subprojects/unity.wrap Normal file
View file

@ -0,0 +1,3 @@
[wrap-git]
url = https://github.com/ThrowTheSwitch/Unity.git
revision = head

110
tests/strlist.c Normal file
View file

@ -0,0 +1,110 @@
#include "unity.h"
#include "strlist.h"
#include <stdlib.h>
#include <string.h>
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);
}