Initial commit
This commit is contained in:
commit
5dc58c04ae
28 changed files with 3288 additions and 0 deletions
7
.clang-format
Normal file
7
.clang-format
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
BasedOnStyle: Mozilla
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignArrayOfStructures: Left
|
||||
AlwaysBreakAfterDefinitionReturnType: All
|
||||
IndentWidth: 4
|
||||
IndentCaseLabels: false
|
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal 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
28
LICENSE
Normal 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
34
NOTICES
Normal 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
59
README.md
Normal 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
3
config.h.meson
Normal file
|
@ -0,0 +1,3 @@
|
|||
#define PROJECT_NAME "@name@"
|
||||
|
||||
#define PROJECT_VER "@version@"
|
124
doc/iasync.1
Normal file
124
doc/iasync.1
Normal 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
190
doc/iasync.1.html
Normal 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> — <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;"> </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"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
145
doc/iasync.1.md
Normal file
145
doc/iasync.1.md
Normal 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
65
meson.build
Normal 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
85
src/cmd_ls.c
Normal 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
144
src/cmd_lsapps.c
Normal 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
114
src/cmd_lsdev.c
Normal 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
633
src/cmd_sync.c
Normal 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
24
src/cmds.h
Normal 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
575
src/idevfs.c
Normal 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
102
src/idevfs.h
Normal 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
29
src/log.c
Normal 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
35
src/log.h
Normal 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
135
src/main.c
Normal 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
172
src/strlist.c
Normal 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
66
src/strlist.h
Normal 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
144
src/sync_lock.c
Normal 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
32
src/sync_lock.h
Normal 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
150
src/util.c
Normal 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
59
src/util.h
Normal 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
3
subprojects/unity.wrap
Normal file
|
@ -0,0 +1,3 @@
|
|||
[wrap-git]
|
||||
url = https://github.com/ThrowTheSwitch/Unity.git
|
||||
revision = head
|
110
tests/strlist.c
Normal file
110
tests/strlist.c
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue