mirror of
https://github.com/flatpak/flatpak.git
synced 2025-12-30 03:18:00 -05:00
9304 lines
290 KiB
C
9304 lines
290 KiB
C
/* vi:set et sw=2 sts=2 cin cino=t0,f0,(0,{s,>2s,n-s,^-s,e-s:
|
||
* Copyright © 1995-1998 Free Software Foundation, Inc.
|
||
* Copyright © 2014-2019 Red Hat, Inc
|
||
*
|
||
* This program is free software; you can redistribute it and/or
|
||
* modify it under the terms of the GNU Lesser General Public
|
||
* License as published by the Free Software Foundation; either
|
||
* version 2.1 of the License, or (at your option) any later version.
|
||
*
|
||
* This library is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||
* Lesser General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU Lesser General Public
|
||
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
||
*
|
||
* Authors:
|
||
* Alexander Larsson <alexl@redhat.com>
|
||
*/
|
||
|
||
#include "config.h"
|
||
|
||
#include <glib/gi18n-lib.h>
|
||
|
||
#include <string.h>
|
||
#include <stdlib.h>
|
||
#include <ctype.h>
|
||
#include <stdio.h>
|
||
#include <errno.h>
|
||
#include <unistd.h>
|
||
#include <fcntl.h>
|
||
#include <string.h>
|
||
#include <sys/stat.h>
|
||
#include <sys/file.h>
|
||
#include <sys/mman.h>
|
||
#include <sys/types.h>
|
||
#include <sys/utsname.h>
|
||
#include <sys/ioctl.h>
|
||
#include <termios.h>
|
||
|
||
#include <glib.h>
|
||
#include <gio/gunixoutputstream.h>
|
||
#include <gio/gunixinputstream.h>
|
||
|
||
#include "flatpak-dir-private.h"
|
||
#include "flatpak-error.h"
|
||
#include "flatpak-oci-registry-private.h"
|
||
#include "flatpak-progress-private.h"
|
||
#include "flatpak-run-private.h"
|
||
#include "flatpak-utils-base-private.h"
|
||
#include "flatpak-utils-private.h"
|
||
#include "flatpak-variant-impl-private.h"
|
||
#include "libglnx.h"
|
||
#include "valgrind-private.h"
|
||
|
||
/* This is also here so the common code can report these errors to the lib */
|
||
static const GDBusErrorEntry flatpak_error_entries[] = {
|
||
{FLATPAK_ERROR_ALREADY_INSTALLED, "org.freedesktop.Flatpak.Error.AlreadyInstalled"},
|
||
{FLATPAK_ERROR_NOT_INSTALLED, "org.freedesktop.Flatpak.Error.NotInstalled"},
|
||
{FLATPAK_ERROR_ONLY_PULLED, "org.freedesktop.Flatpak.Error.OnlyPulled"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_DIFFERENT_REMOTE, "org.freedesktop.Flatpak.Error.DifferentRemote"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_ABORTED, "org.freedesktop.Flatpak.Error.Aborted"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_SKIPPED, "org.freedesktop.Flatpak.Error.Skipped"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_NEED_NEW_FLATPAK, "org.freedesktop.Flatpak.Error.NeedNewFlatpak"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_REMOTE_NOT_FOUND, "org.freedesktop.Flatpak.Error.RemoteNotFound"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_RUNTIME_NOT_FOUND, "org.freedesktop.Flatpak.Error.RuntimeNotFound"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_DOWNGRADE, "org.freedesktop.Flatpak.Error.Downgrade"}, /* Since: 1.0 */
|
||
{FLATPAK_ERROR_INVALID_REF, "org.freedesktop.Flatpak.Error.InvalidRef"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_INVALID_DATA, "org.freedesktop.Flatpak.Error.InvalidData"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_UNTRUSTED, "org.freedesktop.Flatpak.Error.Untrusted"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_SETUP_FAILED, "org.freedesktop.Flatpak.Error.SetupFailed"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_EXPORT_FAILED, "org.freedesktop.Flatpak.Error.ExportFailed"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_REMOTE_USED, "org.freedesktop.Flatpak.Error.RemoteUsed"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_RUNTIME_USED, "org.freedesktop.Flatpak.Error.RuntimeUsed"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_INVALID_NAME, "org.freedesktop.Flatpak.Error.InvalidName"}, /* Since: 1.0.3 */
|
||
{FLATPAK_ERROR_OUT_OF_SPACE, "org.freedesktop.Flatpak.Error.OutOfSpace"}, /* Since: 1.2.0 */
|
||
{FLATPAK_ERROR_WRONG_USER, "org.freedesktop.Flatpak.Error.WrongUser"}, /* Since: 1.2.0 */
|
||
{FLATPAK_ERROR_NOT_CACHED, "org.freedesktop.Flatpak.Error.NotCached"}, /* Since: 1.3.3 */
|
||
{FLATPAK_ERROR_REF_NOT_FOUND, "org.freedesktop.Flatpak.Error.RefNotFound"}, /* Since: 1.4.0 */
|
||
{FLATPAK_ERROR_PERMISSION_DENIED, "org.freedesktop.Flatpak.Error.PermissionDenied"}, /* Since: 1.5.1 */
|
||
{FLATPAK_ERROR_AUTHENTICATION_FAILED, "org.freedesktop.Flatpak.Error.AuthenticationFailed"}, /* Since: 1.7.3 */
|
||
{FLATPAK_ERROR_NOT_AUTHORIZED, "org.freedesktop.Flatpak.Error.NotAuthorized"}, /* Since: 1.7.3 */
|
||
};
|
||
|
||
typedef struct archive FlatpakAutoArchiveRead;
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakAutoArchiveRead, archive_read_free)
|
||
|
||
static void
|
||
propagate_libarchive_error (GError **error,
|
||
struct archive *a)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||
"%s", archive_error_string (a));
|
||
}
|
||
|
||
GQuark
|
||
flatpak_error_quark (void)
|
||
{
|
||
static volatile gsize quark_volatile = 0;
|
||
|
||
g_dbus_error_register_error_domain ("flatpak-error-quark",
|
||
&quark_volatile,
|
||
flatpak_error_entries,
|
||
G_N_ELEMENTS (flatpak_error_entries));
|
||
return (GQuark) quark_volatile;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_fail_error (GError **error, FlatpakError code, const char *fmt, ...)
|
||
{
|
||
if (error == NULL)
|
||
return FALSE;
|
||
|
||
va_list args;
|
||
va_start (args, fmt);
|
||
GError *new = g_error_new_valist (FLATPAK_ERROR, code, fmt, args);
|
||
va_end (args);
|
||
g_propagate_error (error, g_steal_pointer (&new));
|
||
return FALSE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_write_update_checksum (GOutputStream *out,
|
||
gconstpointer data,
|
||
gsize len,
|
||
gsize *out_bytes_written,
|
||
GChecksum *checksum,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
if (out)
|
||
{
|
||
if (!g_output_stream_write_all (out, data, len, out_bytes_written,
|
||
cancellable, error))
|
||
return FALSE;
|
||
}
|
||
else if (out_bytes_written)
|
||
{
|
||
*out_bytes_written = len;
|
||
}
|
||
|
||
if (checksum)
|
||
g_checksum_update (checksum, data, len);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_splice_update_checksum (GOutputStream *out,
|
||
GInputStream *in,
|
||
GChecksum *checksum,
|
||
FlatpakLoadUriProgress progress,
|
||
gpointer progress_data,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gsize bytes_read, bytes_written;
|
||
char buf[32 * 1024];
|
||
guint64 downloaded_bytes = 0;
|
||
gint64 progress_start;
|
||
|
||
progress_start = g_get_monotonic_time ();
|
||
do
|
||
{
|
||
if (!g_input_stream_read_all (in, buf, sizeof buf, &bytes_read, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_write_update_checksum (out, buf, bytes_read, &bytes_written, checksum,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
downloaded_bytes += bytes_read;
|
||
|
||
if (progress &&
|
||
g_get_monotonic_time () - progress_start > 5 * 1000000)
|
||
{
|
||
progress (downloaded_bytes, progress_data);
|
||
progress_start = g_get_monotonic_time ();
|
||
}
|
||
}
|
||
while (bytes_read > 0);
|
||
|
||
if (progress)
|
||
progress (downloaded_bytes, progress_data);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
GBytes *
|
||
flatpak_zlib_compress_bytes (GBytes *bytes,
|
||
int level,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GZlibCompressor) compressor = NULL;
|
||
g_autoptr(GOutputStream) out = NULL;
|
||
g_autoptr(GOutputStream) mem = NULL;
|
||
|
||
mem = g_memory_output_stream_new_resizable ();
|
||
|
||
compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, level);
|
||
out = g_converter_output_stream_new (mem, G_CONVERTER (compressor));
|
||
|
||
if (!g_output_stream_write_all (out, g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes),
|
||
NULL, NULL, error))
|
||
return NULL;
|
||
|
||
if (!g_output_stream_close (out, NULL, error))
|
||
return NULL;
|
||
|
||
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem));
|
||
}
|
||
|
||
GBytes *
|
||
flatpak_zlib_decompress_bytes (GBytes *bytes,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GZlibDecompressor) decompressor = NULL;
|
||
g_autoptr(GOutputStream) out = NULL;
|
||
g_autoptr(GOutputStream) mem = NULL;
|
||
|
||
mem = g_memory_output_stream_new_resizable ();
|
||
|
||
decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);
|
||
out = g_converter_output_stream_new (mem, G_CONVERTER (decompressor));
|
||
|
||
if (!g_output_stream_write_all (out, g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes),
|
||
NULL, NULL, error))
|
||
return NULL;
|
||
|
||
if (!g_output_stream_close (out, NULL, error))
|
||
return NULL;
|
||
|
||
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem));
|
||
}
|
||
|
||
GBytes *
|
||
flatpak_read_stream (GInputStream *in,
|
||
gboolean null_terminate,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GOutputStream) mem_stream = NULL;
|
||
|
||
mem_stream = g_memory_output_stream_new_resizable ();
|
||
if (g_output_stream_splice (mem_stream, in,
|
||
0, NULL, error) < 0)
|
||
return NULL;
|
||
|
||
if (null_terminate)
|
||
{
|
||
if (!g_output_stream_write (G_OUTPUT_STREAM (mem_stream), "\0", 1, NULL, error))
|
||
return NULL;
|
||
}
|
||
|
||
if (!g_output_stream_close (G_OUTPUT_STREAM (mem_stream), NULL, error))
|
||
return NULL;
|
||
|
||
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem_stream));
|
||
}
|
||
|
||
gint
|
||
flatpak_strcmp0_ptr (gconstpointer a,
|
||
gconstpointer b)
|
||
{
|
||
return g_strcmp0 (*(char * const *) a, *(char * const *) b);
|
||
}
|
||
|
||
/* Sometimes this is /var/run which is a symlink, causing weird issues when we pass
|
||
* it as a path into the sandbox */
|
||
char *
|
||
flatpak_get_real_xdg_runtime_dir (void)
|
||
{
|
||
return realpath (g_get_user_runtime_dir (), NULL);
|
||
}
|
||
|
||
/* Compares if str has a specific path prefix. This differs
|
||
from a regular prefix in two ways. First of all there may
|
||
be multiple slashes separating the path elements, and
|
||
secondly, if a prefix is matched that has to be en entire
|
||
path element. For instance /a/prefix matches /a/prefix/foo/bar,
|
||
but not /a/prefixfoo/bar. */
|
||
gboolean
|
||
flatpak_has_path_prefix (const char *str,
|
||
const char *prefix)
|
||
{
|
||
while (TRUE)
|
||
{
|
||
/* Skip consecutive slashes to reach next path
|
||
element */
|
||
while (*str == '/')
|
||
str++;
|
||
while (*prefix == '/')
|
||
prefix++;
|
||
|
||
/* No more prefix path elements? Done! */
|
||
if (*prefix == 0)
|
||
return TRUE;
|
||
|
||
/* Compare path element */
|
||
while (*prefix != 0 && *prefix != '/')
|
||
{
|
||
if (*str != *prefix)
|
||
return FALSE;
|
||
str++;
|
||
prefix++;
|
||
}
|
||
|
||
/* Matched prefix path element,
|
||
must be entire str path element */
|
||
if (*str != '/' && *str != 0)
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
/* Returns end of matching path prefix, or NULL if no match */
|
||
const char *
|
||
flatpak_path_match_prefix (const char *pattern,
|
||
const char *string)
|
||
{
|
||
char c, test;
|
||
const char *tmp;
|
||
|
||
while (*pattern == '/')
|
||
pattern++;
|
||
|
||
while (*string == '/')
|
||
string++;
|
||
|
||
while (TRUE)
|
||
{
|
||
switch (c = *pattern++)
|
||
{
|
||
case 0:
|
||
if (*string == '/' || *string == 0)
|
||
return string;
|
||
return NULL;
|
||
|
||
case '?':
|
||
if (*string == '/' || *string == 0)
|
||
return NULL;
|
||
string++;
|
||
break;
|
||
|
||
case '*':
|
||
c = *pattern;
|
||
|
||
while (c == '*')
|
||
c = *++pattern;
|
||
|
||
/* special case * at end */
|
||
if (c == 0)
|
||
{
|
||
tmp = strchr (string, '/');
|
||
if (tmp != NULL)
|
||
return tmp;
|
||
return string + strlen (string);
|
||
}
|
||
else if (c == '/')
|
||
{
|
||
string = strchr (string, '/');
|
||
if (string == NULL)
|
||
return NULL;
|
||
break;
|
||
}
|
||
|
||
while ((test = *string) != 0)
|
||
{
|
||
tmp = flatpak_path_match_prefix (pattern, string);
|
||
if (tmp != NULL)
|
||
return tmp;
|
||
if (test == '/')
|
||
break;
|
||
string++;
|
||
}
|
||
return NULL;
|
||
|
||
default:
|
||
if (c != *string)
|
||
return NULL;
|
||
string++;
|
||
break;
|
||
}
|
||
}
|
||
return NULL; /* Should not be reached */
|
||
}
|
||
|
||
static const char *
|
||
flatpak_get_kernel_arch (void)
|
||
{
|
||
static struct utsname buf;
|
||
static char *arch = NULL;
|
||
char *m;
|
||
|
||
if (arch != NULL)
|
||
return arch;
|
||
|
||
if (uname (&buf))
|
||
{
|
||
arch = "unknown";
|
||
return arch;
|
||
}
|
||
|
||
/* By default, just pass on machine, good enough for most arches */
|
||
arch = buf.machine;
|
||
|
||
/* Override for some arches */
|
||
|
||
m = buf.machine;
|
||
/* i?86 */
|
||
if (strlen (m) == 4 && m[0] == 'i' && m[2] == '8' && m[3] == '6')
|
||
{
|
||
arch = "i386";
|
||
}
|
||
else if (g_str_has_prefix (m, "arm"))
|
||
{
|
||
if (g_str_has_suffix (m, "b"))
|
||
arch = "armeb";
|
||
else
|
||
arch = "arm";
|
||
}
|
||
else if (strcmp (m, "mips") == 0)
|
||
{
|
||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||
arch = "mipsel";
|
||
#endif
|
||
}
|
||
else if (strcmp (m, "mips64") == 0)
|
||
{
|
||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||
arch = "mips64el";
|
||
#endif
|
||
}
|
||
|
||
return arch;
|
||
}
|
||
|
||
/* This maps the kernel-reported uname to a single string representing
|
||
* the cpu family, in the sense that all members of this family would
|
||
* be able to understand and link to a binary file with such cpu
|
||
* opcodes. That doesn't necessarily mean that all members of the
|
||
* family can run all opcodes, for instance for modern 32bit intel we
|
||
* report "i386", even though they support instructions that the
|
||
* original i386 cpu cannot run. Still, such an executable would
|
||
* at least try to execute a 386, whereas an arm binary would not.
|
||
*/
|
||
const char *
|
||
flatpak_get_arch (void)
|
||
{
|
||
/* Avoid using uname on multiarch machines, because uname reports the kernels
|
||
* arch, and that may be different from userspace. If e.g. the kernel is 64bit and
|
||
* the userspace is 32bit we want to use 32bit by default. So, we take the current build
|
||
* arch as the default. */
|
||
#if defined(__i386__)
|
||
return "i386";
|
||
#elif defined(__x86_64__)
|
||
return "x86_64";
|
||
#elif defined(__aarch64__)
|
||
return "aarch64";
|
||
#elif defined(__arm__)
|
||
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
|
||
return "arm";
|
||
#else
|
||
return "armeb";
|
||
#endif
|
||
#else
|
||
return flatpak_get_kernel_arch ();
|
||
#endif
|
||
}
|
||
|
||
gboolean
|
||
flatpak_is_linux32_arch (const char *arch)
|
||
{
|
||
const char *kernel_arch = flatpak_get_kernel_arch ();
|
||
|
||
if (strcmp (kernel_arch, "x86_64") == 0 &&
|
||
strcmp (arch, "i386") == 0)
|
||
return TRUE;
|
||
|
||
if (strcmp (kernel_arch, "aarch64") == 0 &&
|
||
strcmp (arch, "arm") == 0)
|
||
return TRUE;
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static struct
|
||
{
|
||
const char *kernel_arch;
|
||
const char *compat_arch;
|
||
} compat_arches[] = {
|
||
{ "x86_64", "i386" },
|
||
{ "aarch64", "arm" },
|
||
};
|
||
|
||
static const char *
|
||
flatpak_get_compat_arch (const char *kernel_arch)
|
||
{
|
||
int i;
|
||
|
||
/* Also add all other arches that are compatible with the kernel arch */
|
||
for (i = 0; i < G_N_ELEMENTS (compat_arches); i++)
|
||
{
|
||
if (strcmp (compat_arches[i].kernel_arch, kernel_arch) == 0)
|
||
return compat_arches[i].compat_arch;
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
const char *
|
||
flatpak_get_compat_arch_reverse (const char *compat_arch)
|
||
{
|
||
int i;
|
||
|
||
/* Also add all other arches that are compatible with the kernel arch */
|
||
for (i = 0; i < G_N_ELEMENTS (compat_arches); i++)
|
||
{
|
||
if (strcmp (compat_arches[i].compat_arch, compat_arch) == 0)
|
||
return compat_arches[i].kernel_arch;
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
/* Get all compatible arches for this host in order of priority */
|
||
const char **
|
||
flatpak_get_arches (void)
|
||
{
|
||
static gsize arches = 0;
|
||
|
||
if (g_once_init_enter (&arches))
|
||
{
|
||
gsize new_arches = 0;
|
||
const char *main_arch = flatpak_get_arch ();
|
||
const char *kernel_arch = flatpak_get_kernel_arch ();
|
||
const char *compat_arch;
|
||
GPtrArray *array = g_ptr_array_new ();
|
||
|
||
/* This is the userspace arch, i.e. the one flatpak itself was
|
||
build for. It's always first. */
|
||
g_ptr_array_add (array, (char *) main_arch);
|
||
|
||
compat_arch = flatpak_get_compat_arch (kernel_arch);
|
||
if (g_strcmp0 (compat_arch, main_arch) != 0)
|
||
g_ptr_array_add (array, (char *) compat_arch);
|
||
|
||
g_ptr_array_add (array, NULL);
|
||
new_arches = (gsize) g_ptr_array_free (array, FALSE);
|
||
|
||
g_once_init_leave (&arches, new_arches);
|
||
}
|
||
|
||
return (const char **) arches;
|
||
}
|
||
|
||
const char **
|
||
flatpak_get_gl_drivers (void)
|
||
{
|
||
static gsize drivers = 0;
|
||
|
||
if (g_once_init_enter (&drivers))
|
||
{
|
||
gsize new_drivers;
|
||
char **new_drivers_c = 0;
|
||
const char *env = g_getenv ("FLATPAK_GL_DRIVERS");
|
||
if (env != NULL && *env != 0)
|
||
new_drivers_c = g_strsplit (env, ":", -1);
|
||
else
|
||
{
|
||
g_autofree char *nvidia_version = NULL;
|
||
char *dot;
|
||
GPtrArray *array = g_ptr_array_new ();
|
||
|
||
if (g_file_get_contents ("/sys/module/nvidia/version",
|
||
&nvidia_version, NULL, NULL))
|
||
{
|
||
g_strstrip (nvidia_version);
|
||
/* Convert dots to dashes */
|
||
while ((dot = strchr (nvidia_version, '.')) != NULL)
|
||
*dot = '-';
|
||
g_ptr_array_add (array, g_strconcat ("nvidia-", nvidia_version, NULL));
|
||
}
|
||
|
||
g_ptr_array_add (array, (char *) "default");
|
||
g_ptr_array_add (array, (char *) "host");
|
||
|
||
g_ptr_array_add (array, NULL);
|
||
new_drivers_c = (char **) g_ptr_array_free (array, FALSE);
|
||
}
|
||
|
||
new_drivers = (gsize) new_drivers_c;
|
||
g_once_init_leave (&drivers, new_drivers);
|
||
}
|
||
|
||
return (const char **) drivers;
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_get_have_intel_gpu (void)
|
||
{
|
||
static int have_intel = -1;
|
||
|
||
if (have_intel == -1)
|
||
have_intel = g_file_test ("/sys/module/i915", G_FILE_TEST_EXISTS);
|
||
|
||
return have_intel;
|
||
}
|
||
|
||
static GHashTable *
|
||
load_kernel_module_list (void)
|
||
{
|
||
GHashTable *modules = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||
g_autofree char *modules_data = NULL;
|
||
g_autoptr(GError) error = NULL;
|
||
char *start, *end;
|
||
|
||
if (!g_file_get_contents ("/proc/modules", &modules_data, NULL, &error))
|
||
{
|
||
g_info ("Failed to read /proc/modules: %s", error->message);
|
||
return modules;
|
||
}
|
||
|
||
/* /proc/modules is a table of modules.
|
||
* Columns are split by spaces and rows by newlines.
|
||
* The first column is the name. */
|
||
start = modules_data;
|
||
while (TRUE)
|
||
{
|
||
end = strchr (start, ' ');
|
||
if (end == NULL)
|
||
break;
|
||
|
||
g_hash_table_add (modules, g_strndup (start, (end - start)));
|
||
|
||
start = strchr (end, '\n');
|
||
if (start == NULL)
|
||
break;
|
||
|
||
start++;
|
||
}
|
||
|
||
return modules;
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_get_have_kernel_module (const char *module_name)
|
||
{
|
||
static GHashTable *kernel_modules = NULL;
|
||
|
||
if (g_once_init_enter (&kernel_modules))
|
||
g_once_init_leave (&kernel_modules, load_kernel_module_list ());
|
||
|
||
return g_hash_table_contains (kernel_modules, module_name);
|
||
}
|
||
|
||
static const char *
|
||
flatpak_get_gtk_theme (void)
|
||
{
|
||
static char *gtk_theme;
|
||
|
||
if (g_once_init_enter (>k_theme))
|
||
{
|
||
/* The schema may not be installed so check first */
|
||
GSettingsSchemaSource *source = g_settings_schema_source_get_default ();
|
||
g_autoptr(GSettingsSchema) schema = NULL;
|
||
|
||
if (source == NULL)
|
||
g_once_init_leave (>k_theme, g_strdup (""));
|
||
else
|
||
{
|
||
schema = g_settings_schema_source_lookup (source,
|
||
"org.gnome.desktop.interface", TRUE);
|
||
|
||
if (schema == NULL)
|
||
g_once_init_leave (>k_theme, g_strdup (""));
|
||
else
|
||
{
|
||
/* GSettings is used to store the theme if you use Wayland or GNOME.
|
||
* TODO: Check XSettings Net/ThemeName for other desktops.
|
||
* We don't care about any other method (like settings.ini) because they
|
||
* aren't passed through the sandbox anyway. */
|
||
g_autoptr(GSettings) settings = g_settings_new ("org.gnome.desktop.interface");
|
||
g_once_init_leave (>k_theme, g_settings_get_string (settings, "gtk-theme"));
|
||
}
|
||
}
|
||
}
|
||
|
||
return (const char *) gtk_theme;
|
||
}
|
||
|
||
static int fancy_output = -1;
|
||
|
||
void
|
||
flatpak_disable_fancy_output (void)
|
||
{
|
||
fancy_output = FALSE;
|
||
}
|
||
|
||
void
|
||
flatpak_enable_fancy_output (void)
|
||
{
|
||
fancy_output = TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_fancy_output (void)
|
||
{
|
||
static gsize fancy_output_once = 0;
|
||
enum {
|
||
PLAIN_OUTPUT = 1,
|
||
FANCY_OUTPUT = 2
|
||
};
|
||
|
||
if (fancy_output != -1)
|
||
return fancy_output;
|
||
|
||
if (g_once_init_enter (&fancy_output_once))
|
||
{
|
||
if (g_strcmp0 (g_getenv ("FLATPAK_FANCY_OUTPUT"), "0") == 0)
|
||
g_once_init_leave (&fancy_output_once, PLAIN_OUTPUT);
|
||
else if (getenv ("G_MESSAGES_DEBUG"))
|
||
g_once_init_leave (&fancy_output_once, PLAIN_OUTPUT);
|
||
else if (!isatty (STDOUT_FILENO))
|
||
g_once_init_leave (&fancy_output_once, PLAIN_OUTPUT);
|
||
else
|
||
g_once_init_leave (&fancy_output_once, FANCY_OUTPUT);
|
||
}
|
||
|
||
return fancy_output_once == FANCY_OUTPUT;
|
||
}
|
||
|
||
const char *
|
||
flatpak_get_bwrap (void)
|
||
{
|
||
const char *e = g_getenv ("FLATPAK_BWRAP");
|
||
|
||
if (e != NULL)
|
||
return e;
|
||
return HELPER;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_bwrap_is_unprivileged (void)
|
||
{
|
||
const char *path = g_find_program_in_path (flatpak_get_bwrap ());
|
||
struct stat st;
|
||
|
||
/* Various features are supported only if bwrap exists and is not setuid */
|
||
return
|
||
path != NULL &&
|
||
stat (path, &st) == 0 &&
|
||
(st.st_mode & S_ISUID) == 0;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_get_allowed_exports (const char *source_path,
|
||
const char *app_id,
|
||
FlatpakContext *context,
|
||
char ***allowed_extensions_out,
|
||
char ***allowed_prefixes_out,
|
||
gboolean *require_exact_match_out)
|
||
{
|
||
g_autoptr(GPtrArray) allowed_extensions = g_ptr_array_new_with_free_func (g_free);
|
||
g_autoptr(GPtrArray) allowed_prefixes = g_ptr_array_new_with_free_func (g_free);
|
||
gboolean require_exact_match = FALSE;
|
||
|
||
g_ptr_array_add (allowed_prefixes, g_strdup_printf ("%s.*", app_id));
|
||
|
||
if (strcmp (source_path, "share/applications") == 0)
|
||
{
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".desktop"));
|
||
}
|
||
else if (flatpak_has_path_prefix (source_path, "share/icons"))
|
||
{
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".svgz"));
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".png"));
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".svg"));
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".ico"));
|
||
}
|
||
else if (strcmp (source_path, "share/dbus-1/services") == 0)
|
||
{
|
||
g_auto(GStrv) owned_dbus_names = flatpak_context_get_session_bus_policy_allowed_own_names (context);
|
||
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".service"));
|
||
|
||
for (GStrv iter = owned_dbus_names; *iter != NULL; ++iter)
|
||
g_ptr_array_add (allowed_prefixes, g_strdup (*iter));
|
||
|
||
/* We need an exact match with no extra garbage, because the filename refers to busnames
|
||
* and we can *only* match exactly these */
|
||
require_exact_match = TRUE;
|
||
}
|
||
else if (strcmp (source_path, "share/gnome-shell/search-providers") == 0)
|
||
{
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".ini"));
|
||
}
|
||
else if (strcmp (source_path, "share/mime/packages") == 0)
|
||
{
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".xml"));
|
||
}
|
||
else if (strcmp (source_path, "share/metainfo") == 0 ||
|
||
strcmp (source_path, "share/appdata") == 0)
|
||
{
|
||
g_ptr_array_add (allowed_extensions, g_strdup (".xml"));
|
||
}
|
||
else
|
||
return FALSE;
|
||
|
||
g_ptr_array_add (allowed_extensions, NULL);
|
||
g_ptr_array_add (allowed_prefixes, NULL);
|
||
|
||
if (allowed_extensions_out)
|
||
*allowed_extensions_out = (char **) g_ptr_array_free (g_steal_pointer (&allowed_extensions), FALSE);
|
||
|
||
if (allowed_prefixes_out)
|
||
*allowed_prefixes_out = (char **) g_ptr_array_free (g_steal_pointer (&allowed_prefixes), FALSE);
|
||
|
||
if (require_exact_match_out)
|
||
*require_exact_match_out = require_exact_match;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
static char *
|
||
line_get_word (char **line)
|
||
{
|
||
char *word = NULL;
|
||
|
||
while (g_ascii_isspace (**line))
|
||
(*line)++;
|
||
|
||
if (**line == 0)
|
||
return NULL;
|
||
|
||
word = *line;
|
||
|
||
while (**line && !g_ascii_isspace (**line))
|
||
(*line)++;
|
||
|
||
if (**line)
|
||
{
|
||
**line = 0;
|
||
(*line)++;
|
||
}
|
||
|
||
return word;
|
||
}
|
||
|
||
char *
|
||
flatpak_filter_glob_to_regexp (const char *glob,
|
||
gboolean runtime_only,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GString) regexp = g_string_new ("");
|
||
int parts = 1;
|
||
gboolean empty_part;
|
||
|
||
if (g_str_has_prefix (glob, "app/"))
|
||
{
|
||
if (runtime_only)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Glob can't match apps"));
|
||
return NULL;
|
||
}
|
||
else
|
||
{
|
||
glob += strlen ("app/");
|
||
g_string_append (regexp, "app/");
|
||
}
|
||
}
|
||
else if (g_str_has_prefix (glob, "runtime/"))
|
||
{
|
||
glob += strlen ("runtime/");
|
||
g_string_append (regexp, "runtime/");
|
||
}
|
||
else
|
||
{
|
||
if (runtime_only)
|
||
g_string_append (regexp, "runtime/");
|
||
else
|
||
g_string_append (regexp, "(app|runtime)/");
|
||
}
|
||
|
||
/* We really need an id part, the rest is optional */
|
||
if (*glob == 0)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Empty glob"));
|
||
return NULL;
|
||
}
|
||
|
||
empty_part = TRUE;
|
||
while (*glob != 0)
|
||
{
|
||
char c = *glob;
|
||
glob++;
|
||
|
||
if (c == '/')
|
||
{
|
||
if (empty_part)
|
||
g_string_append (regexp, "[.\\-_a-zA-Z0-9]*");
|
||
empty_part = TRUE;
|
||
parts++;
|
||
g_string_append (regexp, "/");
|
||
if (parts > 3)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Too many segments in glob"));
|
||
return NULL;
|
||
}
|
||
}
|
||
else if (c == '*')
|
||
{
|
||
empty_part = FALSE;
|
||
g_string_append (regexp, "[.\\-_a-zA-Z0-9]*");
|
||
}
|
||
else if (c == '.')
|
||
{
|
||
empty_part = FALSE;
|
||
g_string_append (regexp, "\\.");
|
||
}
|
||
else if (g_ascii_isalnum (c) || c == '-' || c == '_')
|
||
{
|
||
empty_part = FALSE;
|
||
g_string_append_c (regexp, c);
|
||
}
|
||
else
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid glob character '%c'"), c);
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
while (parts < 3)
|
||
{
|
||
parts++;
|
||
g_string_append (regexp, "/[.\\-_a-zA-Z0-9]*");
|
||
}
|
||
|
||
return g_string_free (g_steal_pointer (®exp), FALSE);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_parse_filters (const char *data,
|
||
GRegex **allow_refs_out,
|
||
GRegex **deny_refs_out,
|
||
GError **error)
|
||
{
|
||
g_auto(GStrv) lines = NULL;
|
||
int i;
|
||
g_autoptr(GString) allow_regexp = g_string_new ("^(");
|
||
g_autoptr(GString) deny_regexp = g_string_new ("^(");
|
||
gboolean has_allow = FALSE;
|
||
gboolean has_deny = FALSE;
|
||
g_autoptr(GRegex) allow_refs = NULL;
|
||
g_autoptr(GRegex) deny_refs = NULL;
|
||
|
||
lines = g_strsplit (data, "\n", -1);
|
||
for (i = 0; lines[i] != NULL; i++)
|
||
{
|
||
char *line = lines[i];
|
||
char *comment, *command;
|
||
|
||
/* Ignore shell-style comments */
|
||
comment = strchr (line, '#');
|
||
if (comment != NULL)
|
||
*comment = 0;
|
||
|
||
command = line_get_word (&line);
|
||
/* Ignore empty lines */
|
||
if (command == NULL)
|
||
continue;
|
||
|
||
if (strcmp (command, "allow") == 0 || strcmp (command, "deny") == 0)
|
||
{
|
||
char *glob, *next;
|
||
g_autofree char *ref_regexp = NULL;
|
||
GString *command_regexp;
|
||
gboolean *has_type = NULL;
|
||
|
||
glob = line_get_word (&line);
|
||
if (glob == NULL)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Missing glob on line %d"), i + 1);
|
||
|
||
next = line_get_word (&line);
|
||
if (next != NULL)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Trailing text on line %d"), i + 1);
|
||
|
||
ref_regexp = flatpak_filter_glob_to_regexp (glob, FALSE, error);
|
||
if (ref_regexp == NULL)
|
||
return glnx_prefix_error (error, _("on line %d"), i + 1);
|
||
|
||
if (strcmp (command, "allow") == 0)
|
||
{
|
||
command_regexp = allow_regexp;
|
||
has_type = &has_allow;
|
||
}
|
||
else
|
||
{
|
||
command_regexp = deny_regexp;
|
||
has_type = &has_deny;
|
||
}
|
||
|
||
if (*has_type)
|
||
g_string_append (command_regexp, "|");
|
||
else
|
||
*has_type = TRUE;
|
||
|
||
g_string_append (command_regexp, ref_regexp);
|
||
}
|
||
else
|
||
{
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Unexpected word '%s' on line %d"), command, i + 1);
|
||
}
|
||
}
|
||
|
||
g_string_append (allow_regexp, ")$");
|
||
g_string_append (deny_regexp, ")$");
|
||
|
||
if (allow_regexp)
|
||
{
|
||
allow_refs = g_regex_new (allow_regexp->str, G_REGEX_DOLLAR_ENDONLY|G_REGEX_RAW|G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, error);
|
||
if (allow_refs == NULL)
|
||
return FALSE;
|
||
}
|
||
|
||
if (deny_regexp)
|
||
{
|
||
deny_refs = g_regex_new (deny_regexp->str, G_REGEX_DOLLAR_ENDONLY|G_REGEX_RAW|G_REGEX_OPTIMIZE, G_REGEX_MATCH_ANCHORED, error);
|
||
if (deny_refs == NULL)
|
||
return FALSE;
|
||
}
|
||
|
||
*allow_refs_out = g_steal_pointer (&allow_refs);
|
||
*deny_refs_out = g_steal_pointer (&deny_refs);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_filters_allow_ref (GRegex *allow_refs,
|
||
GRegex *deny_refs,
|
||
const char *ref)
|
||
{
|
||
if (deny_refs == NULL)
|
||
return TRUE; /* All refs are allowed by default */
|
||
|
||
if (!g_regex_match (deny_refs, ref, G_REGEX_MATCH_ANCHORED, NULL))
|
||
return TRUE; /* Not denied */
|
||
|
||
if (allow_refs && g_regex_match (allow_refs, ref, G_REGEX_MATCH_ANCHORED, NULL))
|
||
return TRUE; /* Explicitly allowed */
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
char **
|
||
flatpak_list_deployed_refs (const char *type,
|
||
const char *name_prefix,
|
||
const char *arch,
|
||
const char *branch,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gchar **ret = NULL;
|
||
g_autoptr(GPtrArray) names = NULL;
|
||
g_autoptr(GHashTable) hash = NULL;
|
||
g_autoptr(FlatpakDir) user_dir = NULL;
|
||
g_autoptr(GPtrArray) system_dirs = NULL;
|
||
int i;
|
||
|
||
hash = g_hash_table_new_full ((GHashFunc)flatpak_decomposed_hash, (GEqualFunc)flatpak_decomposed_equal, (GDestroyNotify)flatpak_decomposed_unref, NULL);
|
||
|
||
user_dir = flatpak_dir_get_user ();
|
||
system_dirs = flatpak_dir_get_system_list (cancellable, error);
|
||
if (system_dirs == NULL)
|
||
goto out;
|
||
|
||
if (!flatpak_dir_collect_deployed_refs (user_dir, type, name_prefix,
|
||
arch, branch, hash, cancellable,
|
||
error))
|
||
goto out;
|
||
|
||
for (i = 0; i < system_dirs->len; i++)
|
||
{
|
||
FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i);
|
||
if (!flatpak_dir_collect_deployed_refs (system_dir, type, name_prefix,
|
||
arch, branch, hash, cancellable,
|
||
error))
|
||
goto out;
|
||
}
|
||
|
||
names = g_ptr_array_new ();
|
||
|
||
GLNX_HASH_TABLE_FOREACH (hash, FlatpakDecomposed *, ref)
|
||
{
|
||
g_ptr_array_add (names, flatpak_decomposed_dup_id (ref));
|
||
}
|
||
|
||
g_ptr_array_sort (names, flatpak_strcmp0_ptr);
|
||
g_ptr_array_add (names, NULL);
|
||
|
||
ret = (char **) g_ptr_array_free (names, FALSE);
|
||
names = NULL;
|
||
|
||
out:
|
||
return ret;
|
||
}
|
||
|
||
char **
|
||
flatpak_list_unmaintained_refs (const char *name_prefix,
|
||
const char *arch,
|
||
const char *branch,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gchar **ret = NULL;
|
||
g_autoptr(GPtrArray) names = NULL;
|
||
g_autoptr(GHashTable) hash = NULL;
|
||
g_autoptr(FlatpakDir) user_dir = NULL;
|
||
const char *key;
|
||
GHashTableIter iter;
|
||
g_autoptr(GPtrArray) system_dirs = NULL;
|
||
int i;
|
||
|
||
hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||
|
||
user_dir = flatpak_dir_get_user ();
|
||
|
||
if (!flatpak_dir_collect_unmaintained_refs (user_dir, name_prefix,
|
||
arch, branch, hash, cancellable,
|
||
error))
|
||
return NULL;
|
||
|
||
system_dirs = flatpak_dir_get_system_list (cancellable, error);
|
||
if (system_dirs == NULL)
|
||
return NULL;
|
||
|
||
for (i = 0; i < system_dirs->len; i++)
|
||
{
|
||
FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i);
|
||
|
||
if (!flatpak_dir_collect_unmaintained_refs (system_dir, name_prefix,
|
||
arch, branch, hash, cancellable,
|
||
error))
|
||
return NULL;
|
||
}
|
||
|
||
names = g_ptr_array_new ();
|
||
g_hash_table_iter_init (&iter, hash);
|
||
while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL))
|
||
g_ptr_array_add (names, g_strdup (key));
|
||
|
||
g_ptr_array_sort (names, flatpak_strcmp0_ptr);
|
||
g_ptr_array_add (names, NULL);
|
||
|
||
ret = (char **) g_ptr_array_free (names, FALSE);
|
||
names = NULL;
|
||
|
||
return ret;
|
||
}
|
||
|
||
GFile *
|
||
flatpak_find_deploy_dir_for_ref (FlatpakDecomposed *ref,
|
||
FlatpakDir **dir_out,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(FlatpakDir) user_dir = NULL;
|
||
g_autoptr(GPtrArray) system_dirs = NULL;
|
||
FlatpakDir *dir = NULL;
|
||
g_autoptr(GFile) deploy = NULL;
|
||
|
||
user_dir = flatpak_dir_get_user ();
|
||
system_dirs = flatpak_dir_get_system_list (cancellable, error);
|
||
if (system_dirs == NULL)
|
||
return NULL;
|
||
|
||
dir = user_dir;
|
||
deploy = flatpak_dir_get_if_deployed (dir, ref, NULL, cancellable);
|
||
if (deploy == NULL)
|
||
{
|
||
int i;
|
||
for (i = 0; deploy == NULL && i < system_dirs->len; i++)
|
||
{
|
||
dir = g_ptr_array_index (system_dirs, i);
|
||
deploy = flatpak_dir_get_if_deployed (dir, ref, NULL, cancellable);
|
||
if (deploy != NULL)
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (deploy == NULL)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED, _("%s not installed"), flatpak_decomposed_get_ref (ref));
|
||
return NULL;
|
||
}
|
||
|
||
if (dir_out)
|
||
*dir_out = g_object_ref (dir);
|
||
return g_steal_pointer (&deploy);
|
||
}
|
||
|
||
GFile *
|
||
flatpak_find_files_dir_for_ref (FlatpakDecomposed *ref,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFile) deploy = NULL;
|
||
|
||
deploy = flatpak_find_deploy_dir_for_ref (ref, NULL, cancellable, error);
|
||
if (deploy == NULL)
|
||
return NULL;
|
||
|
||
return g_file_get_child (deploy, "files");
|
||
}
|
||
|
||
GFile *
|
||
flatpak_find_unmaintained_extension_dir_if_exists (const char *name,
|
||
const char *arch,
|
||
const char *branch,
|
||
GCancellable *cancellable)
|
||
{
|
||
g_autoptr(FlatpakDir) user_dir = NULL;
|
||
g_autoptr(GFile) extension_dir = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
user_dir = flatpak_dir_get_user ();
|
||
|
||
extension_dir = flatpak_dir_get_unmaintained_extension_dir_if_exists (user_dir, name, arch, branch, cancellable);
|
||
if (extension_dir == NULL)
|
||
{
|
||
g_autoptr(GPtrArray) system_dirs = NULL;
|
||
int i;
|
||
|
||
system_dirs = flatpak_dir_get_system_list (cancellable, &local_error);
|
||
if (system_dirs == NULL)
|
||
{
|
||
g_warning ("Could not get the system installations: %s", local_error->message);
|
||
return NULL;
|
||
}
|
||
|
||
for (i = 0; i < system_dirs->len; i++)
|
||
{
|
||
FlatpakDir *system_dir = g_ptr_array_index (system_dirs, i);
|
||
extension_dir = flatpak_dir_get_unmaintained_extension_dir_if_exists (system_dir, name, arch, branch, cancellable);
|
||
if (extension_dir != NULL)
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (extension_dir == NULL)
|
||
return NULL;
|
||
|
||
return g_steal_pointer (&extension_dir);
|
||
}
|
||
|
||
FlatpakDecomposed *
|
||
flatpak_find_current_ref (const char *app_id,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(FlatpakDecomposed) current_ref = NULL;
|
||
g_autoptr(FlatpakDir) user_dir = flatpak_dir_get_user ();
|
||
int i;
|
||
|
||
current_ref = flatpak_dir_current_ref (user_dir, app_id, NULL);
|
||
if (current_ref == NULL)
|
||
{
|
||
g_autoptr(GPtrArray) system_dirs = NULL;
|
||
|
||
system_dirs = flatpak_dir_get_system_list (cancellable, error);
|
||
if (system_dirs == NULL)
|
||
return FALSE;
|
||
|
||
for (i = 0; i < system_dirs->len; i++)
|
||
{
|
||
FlatpakDir *dir = g_ptr_array_index (system_dirs, i);
|
||
current_ref = flatpak_dir_current_ref (dir, app_id, cancellable);
|
||
if (current_ref != NULL)
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (current_ref)
|
||
return g_steal_pointer (¤t_ref);
|
||
|
||
flatpak_fail_error (error, FLATPAK_ERROR_NOT_INSTALLED, _("%s not installed"), app_id);
|
||
return NULL;
|
||
}
|
||
|
||
FlatpakDeploy *
|
||
flatpak_find_deploy_for_ref_in (GPtrArray *dirs,
|
||
const char *ref_str,
|
||
const char *commit,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
FlatpakDeploy *deploy = NULL;
|
||
int i;
|
||
g_autoptr(GError) my_error = NULL;
|
||
|
||
g_autoptr(FlatpakDecomposed) ref = flatpak_decomposed_new_from_ref (ref_str, error);
|
||
if (ref == NULL)
|
||
return NULL;
|
||
|
||
for (i = 0; deploy == NULL && i < dirs->len; i++)
|
||
{
|
||
FlatpakDir *dir = g_ptr_array_index (dirs, i);
|
||
|
||
flatpak_log_dir_access (dir);
|
||
g_clear_error (&my_error);
|
||
deploy = flatpak_dir_load_deployed (dir, ref, commit, cancellable, &my_error);
|
||
}
|
||
|
||
if (deploy == NULL)
|
||
g_propagate_error (error, g_steal_pointer (&my_error));
|
||
|
||
return deploy;
|
||
}
|
||
|
||
FlatpakDeploy *
|
||
flatpak_find_deploy_for_ref (const char *ref,
|
||
const char *commit,
|
||
FlatpakDir *opt_user_dir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GPtrArray) dirs = NULL;
|
||
|
||
dirs = flatpak_dir_get_system_list (cancellable, error);
|
||
if (dirs == NULL)
|
||
return NULL;
|
||
|
||
/* If an custom dir was passed, use that instead of the user dir.
|
||
* This is used when running apply-extra-data where if the target
|
||
* is a custom installation location the regular user one may not
|
||
* have the (possibly just installed in this transaction) runtime.
|
||
*/
|
||
if (opt_user_dir)
|
||
g_ptr_array_insert (dirs, 0, g_object_ref (opt_user_dir));
|
||
else
|
||
g_ptr_array_insert (dirs, 0, flatpak_dir_get_user ());
|
||
|
||
return flatpak_find_deploy_for_ref_in (dirs, ref, commit, cancellable, error);
|
||
}
|
||
|
||
static gboolean
|
||
remove_dangling_symlinks (int parent_fd,
|
||
const char *name,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gboolean ret = FALSE;
|
||
struct dirent *dent;
|
||
g_auto(GLnxDirFdIterator) iter = { 0 };
|
||
|
||
if (!glnx_dirfd_iterator_init_at (parent_fd, name, FALSE, &iter, error))
|
||
goto out;
|
||
|
||
while (TRUE)
|
||
{
|
||
if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent, cancellable, error))
|
||
goto out;
|
||
|
||
if (dent == NULL)
|
||
break;
|
||
|
||
if (dent->d_type == DT_DIR)
|
||
{
|
||
if (!remove_dangling_symlinks (iter.fd, dent->d_name, cancellable, error))
|
||
goto out;
|
||
}
|
||
else if (dent->d_type == DT_LNK)
|
||
{
|
||
struct stat stbuf;
|
||
if (fstatat (iter.fd, dent->d_name, &stbuf, 0) != 0 && errno == ENOENT)
|
||
{
|
||
if (unlinkat (iter.fd, dent->d_name, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
goto out;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
ret = TRUE;
|
||
out:
|
||
|
||
return ret;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_remove_dangling_symlinks (GFile *dir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gboolean ret = FALSE;
|
||
|
||
/* The fd is closed by this call */
|
||
if (!remove_dangling_symlinks (AT_FDCWD, flatpak_file_get_path_cached (dir),
|
||
cancellable, error))
|
||
goto out;
|
||
|
||
ret = TRUE;
|
||
|
||
out:
|
||
return ret;
|
||
}
|
||
|
||
/* This atomically replaces a symlink with a new value, removing the
|
||
* existing symlink target, if it exstis and is different from
|
||
* @target. This is atomic in the sense that we're guaranteed to
|
||
* remove any existing symlink target (once), independent of how many
|
||
* processes do the same operation in parallele. However, it is still
|
||
* possible that we remove the old and then fail to create the new
|
||
* symlink for some reason, ending up with neither the old or the new
|
||
* target. That is fine if the reason for the symlink is keeping a
|
||
* cache though.
|
||
*/
|
||
gboolean
|
||
flatpak_switch_symlink_and_remove (const char *symlink_path,
|
||
const char *target,
|
||
GError **error)
|
||
{
|
||
g_autofree char *symlink_dir = g_path_get_dirname (symlink_path);
|
||
int try;
|
||
|
||
for (try = 0; try < 100; try++)
|
||
{
|
||
g_autofree char *tmp_path = NULL;
|
||
int fd;
|
||
|
||
/* Try to atomically create the symlink */
|
||
if (TEMP_FAILURE_RETRY (symlink (target, symlink_path)) == 0)
|
||
return TRUE;
|
||
|
||
if (errno != EEXIST)
|
||
{
|
||
/* Unexpected failure, bail */
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
/* The symlink existed, move it to a temporary name atomically, and remove target
|
||
if that succeeded. */
|
||
tmp_path = g_build_filename (symlink_dir, ".switched-symlink-XXXXXX", NULL);
|
||
|
||
fd = g_mkstemp_full (tmp_path, O_RDWR, 0644);
|
||
if (fd == -1)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
close (fd);
|
||
|
||
if (TEMP_FAILURE_RETRY (rename (symlink_path, tmp_path)) == 0)
|
||
{
|
||
/* The move succeeded, now we can remove the old target */
|
||
g_autofree char *old_target = flatpak_readlink (tmp_path, error);
|
||
if (old_target == NULL)
|
||
return FALSE;
|
||
if (strcmp (old_target, target) != 0) /* Don't remove old file if its the same as the new one */
|
||
{
|
||
g_autofree char *old_target_path = g_build_filename (symlink_dir, old_target, NULL);
|
||
unlink (old_target_path);
|
||
}
|
||
}
|
||
else if (errno != ENOENT)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
unlink (tmp_path);
|
||
return -1;
|
||
}
|
||
unlink (tmp_path);
|
||
|
||
/* An old target was removed, try again */
|
||
}
|
||
|
||
return flatpak_fail (error, "flatpak_switch_symlink_and_remove looped too many times");
|
||
}
|
||
|
||
gboolean
|
||
flatpak_argument_needs_quoting (const char *arg)
|
||
{
|
||
if (*arg == '\0')
|
||
return TRUE;
|
||
|
||
while (*arg != 0)
|
||
{
|
||
char c = *arg;
|
||
if (!g_ascii_isalnum (c) &&
|
||
!(c == '-' || c == '/' || c == '~' ||
|
||
c == ':' || c == '.' || c == '_' ||
|
||
c == '=' || c == '@'))
|
||
return TRUE;
|
||
arg++;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
char *
|
||
flatpak_quote_argv (const char *argv[],
|
||
gssize len)
|
||
{
|
||
GString *res = g_string_new ("");
|
||
int i;
|
||
|
||
if (len == -1)
|
||
len = g_strv_length ((char **) argv);
|
||
|
||
for (i = 0; i < len; i++)
|
||
{
|
||
if (i != 0)
|
||
g_string_append_c (res, ' ');
|
||
|
||
if (flatpak_argument_needs_quoting (argv[i]))
|
||
{
|
||
g_autofree char *quoted = g_shell_quote (argv[i]);
|
||
g_string_append (res, quoted);
|
||
}
|
||
else
|
||
g_string_append (res, argv[i]);
|
||
}
|
||
|
||
return g_string_free (res, FALSE);
|
||
}
|
||
|
||
/* This is useful, because it handles escaped characters in uris, and ? arguments at the end of the uri */
|
||
gboolean
|
||
flatpak_file_arg_has_suffix (const char *arg, const char *suffix)
|
||
{
|
||
g_autoptr(GFile) file = g_file_new_for_commandline_arg (arg);
|
||
g_autofree char *basename = g_file_get_basename (file);
|
||
|
||
return g_str_has_suffix (basename, suffix);
|
||
}
|
||
|
||
GFile *
|
||
flatpak_build_file_va (GFile *base,
|
||
va_list args)
|
||
{
|
||
g_autoptr(GFile) res = g_object_ref (base);
|
||
const gchar *arg;
|
||
|
||
while ((arg = va_arg (args, const gchar *)))
|
||
{
|
||
g_autoptr(GFile) child = g_file_resolve_relative_path (res, arg);
|
||
g_set_object (&res, child);
|
||
}
|
||
|
||
return g_steal_pointer (&res);
|
||
}
|
||
|
||
GFile *
|
||
flatpak_build_file (GFile *base, ...)
|
||
{
|
||
GFile *res;
|
||
va_list args;
|
||
|
||
va_start (args, base);
|
||
res = flatpak_build_file_va (base, args);
|
||
va_end (args);
|
||
|
||
return res;
|
||
}
|
||
|
||
const char *
|
||
flatpak_file_get_path_cached (GFile *file)
|
||
{
|
||
const char *path;
|
||
static GQuark _file_path_quark = 0;
|
||
|
||
if (G_UNLIKELY (_file_path_quark == 0))
|
||
_file_path_quark = g_quark_from_static_string ("flatpak-file-path");
|
||
|
||
do
|
||
{
|
||
path = g_object_get_qdata ((GObject *) file, _file_path_quark);
|
||
if (path == NULL)
|
||
{
|
||
g_autofree char *new_path = NULL;
|
||
new_path = g_file_get_path (file);
|
||
if (new_path == NULL)
|
||
return NULL;
|
||
|
||
if (g_object_replace_qdata ((GObject *) file, _file_path_quark,
|
||
NULL, new_path, g_free, NULL))
|
||
path = g_steal_pointer (&new_path);
|
||
}
|
||
}
|
||
while (path == NULL);
|
||
|
||
return path;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_openat_noatime (int dfd,
|
||
const char *name,
|
||
int *ret_fd,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
int fd;
|
||
int flags = O_RDONLY | O_CLOEXEC;
|
||
|
||
#ifdef O_NOATIME
|
||
do
|
||
fd = openat (dfd, name, flags | O_NOATIME, 0);
|
||
while (G_UNLIKELY (fd == -1 && errno == EINTR));
|
||
/* Only the owner or superuser may use O_NOATIME; so we may get
|
||
* EPERM. EINVAL may happen if the kernel is really old...
|
||
*/
|
||
if (fd == -1 && (errno == EPERM || errno == EINVAL))
|
||
#endif
|
||
do
|
||
fd = openat (dfd, name, flags, 0);
|
||
while (G_UNLIKELY (fd == -1 && errno == EINTR));
|
||
|
||
if (fd == -1)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
else
|
||
{
|
||
*ret_fd = fd;
|
||
return TRUE;
|
||
}
|
||
}
|
||
|
||
gboolean
|
||
flatpak_cp_a (GFile *src,
|
||
GFile *dest,
|
||
FlatpakCpFlags flags,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gboolean ret = FALSE;
|
||
GFileEnumerator *enumerator = NULL;
|
||
GFileInfo *src_info = NULL;
|
||
GFile *dest_child = NULL;
|
||
int dest_dfd = -1;
|
||
gboolean merge = (flags & FLATPAK_CP_FLAGS_MERGE) != 0;
|
||
gboolean no_chown = (flags & FLATPAK_CP_FLAGS_NO_CHOWN) != 0;
|
||
gboolean move = (flags & FLATPAK_CP_FLAGS_MOVE) != 0;
|
||
g_autoptr(GFileInfo) child_info = NULL;
|
||
GError *temp_error = NULL;
|
||
int r;
|
||
|
||
enumerator = g_file_enumerate_children (src, "standard::type,standard::name,unix::uid,unix::gid,unix::mode",
|
||
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
|
||
cancellable, error);
|
||
if (!enumerator)
|
||
goto out;
|
||
|
||
src_info = g_file_query_info (src, "standard::name,unix::mode,unix::uid,unix::gid," \
|
||
"time::modified,time::modified-usec,time::access,time::access-usec",
|
||
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
|
||
cancellable, error);
|
||
if (!src_info)
|
||
goto out;
|
||
|
||
do
|
||
r = mkdir (flatpak_file_get_path_cached (dest), 0755);
|
||
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
||
if (r == -1 &&
|
||
(!merge || errno != EEXIST))
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
goto out;
|
||
}
|
||
|
||
if (!glnx_opendirat (AT_FDCWD, flatpak_file_get_path_cached (dest), TRUE,
|
||
&dest_dfd, error))
|
||
goto out;
|
||
|
||
if (!no_chown)
|
||
{
|
||
do
|
||
r = fchown (dest_dfd,
|
||
g_file_info_get_attribute_uint32 (src_info, "unix::uid"),
|
||
g_file_info_get_attribute_uint32 (src_info, "unix::gid"));
|
||
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
||
if (r == -1)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
goto out;
|
||
}
|
||
}
|
||
|
||
do
|
||
r = fchmod (dest_dfd, g_file_info_get_attribute_uint32 (src_info, "unix::mode"));
|
||
while (G_UNLIKELY (r == -1 && errno == EINTR));
|
||
|
||
if (dest_dfd != -1)
|
||
{
|
||
(void) close (dest_dfd);
|
||
dest_dfd = -1;
|
||
}
|
||
|
||
while ((child_info = g_file_enumerator_next_file (enumerator, cancellable, &temp_error)))
|
||
{
|
||
const char *name = g_file_info_get_name (child_info);
|
||
g_autoptr(GFile) src_child = g_file_get_child (src, name);
|
||
|
||
if (dest_child)
|
||
g_object_unref (dest_child);
|
||
dest_child = g_file_get_child (dest, name);
|
||
|
||
if (g_file_info_get_file_type (child_info) == G_FILE_TYPE_DIRECTORY)
|
||
{
|
||
if (!flatpak_cp_a (src_child, dest_child, flags,
|
||
cancellable, error))
|
||
goto out;
|
||
}
|
||
else
|
||
{
|
||
(void) unlink (flatpak_file_get_path_cached (dest_child));
|
||
GFileCopyFlags copyflags = G_FILE_COPY_OVERWRITE | G_FILE_COPY_NOFOLLOW_SYMLINKS;
|
||
if (!no_chown)
|
||
copyflags |= G_FILE_COPY_ALL_METADATA;
|
||
if (move)
|
||
{
|
||
if (!g_file_move (src_child, dest_child, copyflags,
|
||
cancellable, NULL, NULL, error))
|
||
goto out;
|
||
}
|
||
else
|
||
{
|
||
if (!g_file_copy (src_child, dest_child, copyflags,
|
||
cancellable, NULL, NULL, error))
|
||
goto out;
|
||
}
|
||
}
|
||
|
||
g_clear_object (&child_info);
|
||
}
|
||
|
||
if (temp_error != NULL)
|
||
{
|
||
g_propagate_error (error, temp_error);
|
||
goto out;
|
||
}
|
||
|
||
if (move &&
|
||
!g_file_delete (src, NULL, error))
|
||
goto out;
|
||
|
||
ret = TRUE;
|
||
out:
|
||
if (dest_dfd != -1)
|
||
(void) close (dest_dfd);
|
||
g_clear_object (&src_info);
|
||
g_clear_object (&enumerator);
|
||
g_clear_object (&dest_child);
|
||
return ret;
|
||
}
|
||
|
||
static gboolean
|
||
_flatpak_canonicalize_permissions (int parent_dfd,
|
||
const char *rel_path,
|
||
gboolean toplevel,
|
||
int uid,
|
||
int gid,
|
||
GError **error)
|
||
{
|
||
struct stat stbuf;
|
||
gboolean res = TRUE;
|
||
|
||
/* Note, in order to not leave non-canonical things around in case
|
||
* of error, this continues after errors, but returns the first
|
||
* error. */
|
||
|
||
if (TEMP_FAILURE_RETRY (fstatat (parent_dfd, rel_path, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
if ((uid != -1 && uid != stbuf.st_uid) || (gid != -1 && gid != stbuf.st_gid))
|
||
{
|
||
if (TEMP_FAILURE_RETRY (fchownat (parent_dfd, rel_path, uid, gid, AT_SYMLINK_NOFOLLOW)) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
/* Re-read st_mode for new owner */
|
||
if (TEMP_FAILURE_RETRY (fstatat (parent_dfd, rel_path, &stbuf, AT_SYMLINK_NOFOLLOW)) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
if (S_ISDIR (stbuf.st_mode))
|
||
{
|
||
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
|
||
|
||
/* For the toplevel we set to 0700 so we can modify it, but not
|
||
expose any non-canonical files to any other user, then we set
|
||
it to 0755 afterwards. */
|
||
if (fchmodat (parent_dfd, rel_path, toplevel ? 0700 : 0755, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
error = NULL;
|
||
res = FALSE;
|
||
}
|
||
|
||
if (glnx_dirfd_iterator_init_at (parent_dfd, rel_path, FALSE, &dfd_iter, NULL))
|
||
{
|
||
while (TRUE)
|
||
{
|
||
struct dirent *dent;
|
||
|
||
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, NULL, NULL) || dent == NULL)
|
||
break;
|
||
|
||
if (!_flatpak_canonicalize_permissions (dfd_iter.fd, dent->d_name, FALSE, uid, gid, error))
|
||
{
|
||
error = NULL;
|
||
res = FALSE;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (toplevel &&
|
||
fchmodat (parent_dfd, rel_path, 0755, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
error = NULL;
|
||
res = FALSE;
|
||
}
|
||
|
||
return res;
|
||
}
|
||
else if (S_ISREG (stbuf.st_mode))
|
||
{
|
||
mode_t mode;
|
||
|
||
/* If use can execute, make executable by all */
|
||
if (stbuf.st_mode & S_IXUSR)
|
||
mode = 0755;
|
||
else /* otherwise executable by none */
|
||
mode = 0644;
|
||
|
||
if (fchmodat (parent_dfd, rel_path, mode, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
res = FALSE;
|
||
}
|
||
}
|
||
else if (S_ISLNK (stbuf.st_mode))
|
||
{
|
||
/* symlinks have no permissions */
|
||
}
|
||
else
|
||
{
|
||
/* some weird non-canonical type, lets delete it */
|
||
if (unlinkat (parent_dfd, rel_path, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
res = FALSE;
|
||
}
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
/* Canonicalizes files to the same permissions as bare-user-only checkouts */
|
||
gboolean
|
||
flatpak_canonicalize_permissions (int parent_dfd,
|
||
const char *rel_path,
|
||
int uid,
|
||
int gid,
|
||
GError **error)
|
||
{
|
||
return _flatpak_canonicalize_permissions (parent_dfd, rel_path, TRUE, uid, gid, error);
|
||
}
|
||
|
||
/* Make a directory, and its parent. Don't error if it already exists.
|
||
* If you want a failure mode with EEXIST, use g_file_make_directory_with_parents. */
|
||
gboolean
|
||
flatpak_mkdir_p (GFile *dir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
return glnx_shutil_mkdir_p_at (AT_FDCWD,
|
||
flatpak_file_get_path_cached (dir),
|
||
0777,
|
||
cancellable,
|
||
error);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_rm_rf (GFile *dir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
return glnx_shutil_rm_rf_at (AT_FDCWD,
|
||
flatpak_file_get_path_cached (dir),
|
||
cancellable, error);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_file_rename (GFile *from,
|
||
GFile *to,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
if (g_cancellable_set_error_if_cancelled (cancellable, error))
|
||
return FALSE;
|
||
|
||
if (rename (flatpak_file_get_path_cached (from),
|
||
flatpak_file_get_path_cached (to)) < 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
/* If memfd_create() is available, generate a sealed memfd with contents of
|
||
* @str. Otherwise use an O_TMPFILE @tmpf in anonymous mode, write @str to
|
||
* @tmpf, and lseek() back to the start. See also similar uses in e.g.
|
||
* rpm-ostree for running dracut.
|
||
*/
|
||
gboolean
|
||
flatpak_buffer_to_sealed_memfd_or_tmpfile (GLnxTmpfile *tmpf,
|
||
const char *name,
|
||
const char *str,
|
||
size_t len,
|
||
GError **error)
|
||
{
|
||
if (len == -1)
|
||
len = strlen (str);
|
||
glnx_autofd int memfd = memfd_create (name, MFD_CLOEXEC | MFD_ALLOW_SEALING);
|
||
int fd; /* Unowned */
|
||
if (memfd != -1)
|
||
{
|
||
fd = memfd;
|
||
}
|
||
else
|
||
{
|
||
/* We use an anonymous fd (i.e. O_EXCL) since we don't want
|
||
* the target container to potentially be able to re-link it.
|
||
*/
|
||
if (!G_IN_SET (errno, ENOSYS, EOPNOTSUPP))
|
||
return glnx_throw_errno_prefix (error, "memfd_create");
|
||
if (!glnx_open_anonymous_tmpfile (O_RDWR | O_CLOEXEC, tmpf, error))
|
||
return FALSE;
|
||
fd = tmpf->fd;
|
||
}
|
||
if (ftruncate (fd, len) < 0)
|
||
return glnx_throw_errno_prefix (error, "ftruncate");
|
||
if (glnx_loop_write (fd, str, len) < 0)
|
||
return glnx_throw_errno_prefix (error, "write");
|
||
if (lseek (fd, 0, SEEK_SET) < 0)
|
||
return glnx_throw_errno_prefix (error, "lseek");
|
||
if (memfd != -1)
|
||
{
|
||
/* Valgrind doesn't currently handle G_ADD_SEALS, so lets not seal when debugging... */
|
||
if ((!RUNNING_ON_VALGRIND) &&
|
||
fcntl (memfd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE | F_SEAL_SEAL) < 0)
|
||
return glnx_throw_errno_prefix (error, "fcntl(F_ADD_SEALS)");
|
||
/* The other values can stay default */
|
||
tmpf->fd = glnx_steal_fd (&memfd);
|
||
tmpf->initialized = TRUE;
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_open_in_tmpdir_at (int tmpdir_fd,
|
||
int mode,
|
||
char *tmpl,
|
||
GOutputStream **out_stream,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
const int max_attempts = 128;
|
||
int i;
|
||
int fd;
|
||
|
||
/* 128 attempts seems reasonable... */
|
||
for (i = 0; i < max_attempts; i++)
|
||
{
|
||
glnx_gen_temp_name (tmpl);
|
||
|
||
do
|
||
fd = openat (tmpdir_fd, tmpl, O_WRONLY | O_CREAT | O_EXCL, mode);
|
||
while (fd == -1 && errno == EINTR);
|
||
if (fd < 0 && errno != EEXIST)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
else if (fd != -1)
|
||
break;
|
||
}
|
||
if (i == max_attempts)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
|
||
"Exhausted attempts to open temporary file");
|
||
return FALSE;
|
||
}
|
||
|
||
if (out_stream)
|
||
*out_stream = g_unix_output_stream_new (fd, TRUE);
|
||
else
|
||
(void) close (fd);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_bytes_save (GFile *dest,
|
||
GBytes *bytes,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GOutputStream) out = NULL;
|
||
|
||
out = (GOutputStream *) g_file_replace (dest, NULL, FALSE,
|
||
G_FILE_CREATE_REPLACE_DESTINATION,
|
||
cancellable, error);
|
||
if (out == NULL)
|
||
return FALSE;
|
||
|
||
if (!g_output_stream_write_all (out,
|
||
g_bytes_get_data (bytes, NULL),
|
||
g_bytes_get_size (bytes),
|
||
NULL,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
|
||
if (!g_output_stream_close (out, cancellable, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_variant_save (GFile *dest,
|
||
GVariant *variant,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GOutputStream) out = NULL;
|
||
gsize bytes_written;
|
||
|
||
out = (GOutputStream *) g_file_replace (dest, NULL, FALSE,
|
||
G_FILE_CREATE_REPLACE_DESTINATION,
|
||
cancellable, error);
|
||
if (out == NULL)
|
||
return FALSE;
|
||
|
||
if (!g_output_stream_write_all (out,
|
||
g_variant_get_data (variant),
|
||
g_variant_get_size (variant),
|
||
&bytes_written,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
|
||
if (!g_output_stream_close (out, cancellable, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
/* This special cases the ref lookup which by doing a
|
||
bsearch since the array is sorted */
|
||
gboolean
|
||
flatpak_var_ref_map_lookup_ref (VarRefMapRef ref_map,
|
||
const char *ref,
|
||
VarRefInfoRef *out_info)
|
||
{
|
||
gsize imax, imin;
|
||
gsize imid;
|
||
gsize n;
|
||
|
||
g_return_val_if_fail (out_info != NULL, FALSE);
|
||
|
||
n = var_ref_map_get_length (ref_map);
|
||
if (n == 0)
|
||
return FALSE;
|
||
|
||
imax = n - 1;
|
||
imin = 0;
|
||
while (imax >= imin)
|
||
{
|
||
VarRefMapEntryRef entry;
|
||
const char *cur;
|
||
int cmp;
|
||
|
||
imid = (imin + imax) / 2;
|
||
|
||
entry = var_ref_map_get_at (ref_map, imid);
|
||
cur = var_ref_map_entry_get_ref (entry);
|
||
|
||
cmp = strcmp (cur, ref);
|
||
if (cmp < 0)
|
||
{
|
||
imin = imid + 1;
|
||
}
|
||
else if (cmp > 0)
|
||
{
|
||
if (imid == 0)
|
||
break;
|
||
imax = imid - 1;
|
||
}
|
||
else
|
||
{
|
||
*out_info = var_ref_map_entry_get_info (entry);
|
||
return TRUE;
|
||
}
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* Find the list of refs which belong to the given @collection_id in @summary.
|
||
* If @collection_id is %NULL, the main refs list from the summary will be
|
||
* returned. If @collection_id doesn’t match any collection IDs in the summary
|
||
* file, %FALSE will be returned. */
|
||
gboolean
|
||
flatpak_summary_find_ref_map (VarSummaryRef summary,
|
||
const char *collection_id,
|
||
VarRefMapRef *refs_out)
|
||
{
|
||
VarMetadataRef metadata = var_summary_get_metadata (summary);
|
||
const char *summary_collection_id;
|
||
|
||
summary_collection_id = var_metadata_lookup_string (metadata, "ostree.summary.collection-id", NULL);
|
||
|
||
if (collection_id == NULL || g_strcmp0 (collection_id, summary_collection_id) == 0)
|
||
{
|
||
if (refs_out)
|
||
*refs_out = var_summary_get_ref_map (summary);
|
||
return TRUE;
|
||
}
|
||
else if (collection_id != NULL)
|
||
{
|
||
VarVariantRef collection_map_v;
|
||
if (var_metadata_lookup (metadata, "ostree.summary.collection-map", NULL, &collection_map_v))
|
||
{
|
||
VarCollectionMapRef collection_map = var_collection_map_from_variant (collection_map_v);
|
||
return var_collection_map_lookup (collection_map, collection_id, NULL, refs_out);
|
||
}
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* This matches all refs from @collection_id that have ref, followed by '.' as prefix */
|
||
GPtrArray *
|
||
flatpak_summary_match_subrefs (GVariant *summary_v,
|
||
const char *collection_id,
|
||
FlatpakDecomposed *ref)
|
||
{
|
||
GPtrArray *res = g_ptr_array_new_with_free_func ((GDestroyNotify)flatpak_decomposed_unref);
|
||
gsize n, i;
|
||
g_autofree char *parts_prefix = NULL;
|
||
g_autofree char *ref_prefix = NULL;
|
||
g_autofree char *ref_suffix = NULL;
|
||
VarSummaryRef summary;
|
||
VarRefMapRef ref_map;
|
||
|
||
summary = var_summary_from_gvariant (summary_v);
|
||
|
||
/* Work out which refs list to use, based on the @collection_id. */
|
||
if (flatpak_summary_find_ref_map (summary, collection_id, &ref_map))
|
||
{
|
||
/* Match against the refs. */
|
||
g_autofree char *id = flatpak_decomposed_dup_id (ref);
|
||
g_autofree char *arch = flatpak_decomposed_dup_arch (ref);
|
||
g_autofree char *branch = flatpak_decomposed_dup_branch (ref);
|
||
parts_prefix = g_strconcat (id, ".", NULL);
|
||
|
||
ref_prefix = g_strconcat (flatpak_decomposed_get_kind_str (ref), "/", NULL);
|
||
ref_suffix = g_strconcat ("/", arch, "/", branch, NULL);
|
||
|
||
n = var_ref_map_get_length (ref_map);
|
||
for (i = 0; i < n; i++)
|
||
{
|
||
VarRefMapEntryRef entry = var_ref_map_get_at (ref_map, i);
|
||
const char *cur;
|
||
const char *id_start;
|
||
const char *id_suffix;
|
||
const char *id_end;
|
||
|
||
cur = var_ref_map_entry_get_ref (entry);
|
||
|
||
/* Must match type */
|
||
if (!g_str_has_prefix (cur, ref_prefix))
|
||
continue;
|
||
|
||
/* Must match arch & branch */
|
||
if (!g_str_has_suffix (cur, ref_suffix))
|
||
continue;
|
||
|
||
id_start = strchr (cur, '/');
|
||
if (id_start == NULL)
|
||
continue;
|
||
id_start += 1;
|
||
|
||
id_end = strchr (id_start, '/');
|
||
if (id_end == NULL)
|
||
continue;
|
||
|
||
/* But only prefix of id */
|
||
if (!g_str_has_prefix (id_start, parts_prefix))
|
||
continue;
|
||
|
||
/* And no dots (we want to install prefix.$ID, but not prefix.$ID.Sources) */
|
||
id_suffix = id_start + strlen (parts_prefix);
|
||
if (memchr (id_suffix, '.', id_end - id_suffix) != NULL)
|
||
continue;
|
||
|
||
FlatpakDecomposed *d = flatpak_decomposed_new_from_ref (cur, NULL);
|
||
if (d)
|
||
g_ptr_array_add (res, d);
|
||
}
|
||
}
|
||
|
||
return g_steal_pointer (&res);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_summary_lookup_ref (GVariant *summary_v,
|
||
const char *collection_id,
|
||
const char *ref,
|
||
char **out_checksum,
|
||
VarRefInfoRef *out_info)
|
||
{
|
||
VarSummaryRef summary;
|
||
VarRefMapRef ref_map;
|
||
VarRefInfoRef info;
|
||
const guchar *checksum_bytes;
|
||
gsize checksum_bytes_len;
|
||
|
||
summary = var_summary_from_gvariant (summary_v);
|
||
|
||
/* Work out which refs list to use, based on the @collection_id. */
|
||
if (!flatpak_summary_find_ref_map (summary, collection_id, &ref_map))
|
||
return FALSE;
|
||
|
||
if (!flatpak_var_ref_map_lookup_ref (ref_map, ref, &info))
|
||
return FALSE;
|
||
|
||
checksum_bytes = var_ref_info_peek_checksum (info, &checksum_bytes_len);
|
||
if (G_UNLIKELY (checksum_bytes_len != OSTREE_SHA256_DIGEST_LEN))
|
||
return FALSE;
|
||
|
||
if (out_checksum)
|
||
*out_checksum = ostree_checksum_from_bytes (checksum_bytes);
|
||
|
||
if (out_info)
|
||
*out_info = info;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
char *
|
||
flatpak_keyfile_get_string_non_empty (GKeyFile *keyfile,
|
||
const char *group,
|
||
const char *key)
|
||
{
|
||
g_autofree char *value = NULL;
|
||
|
||
value = g_key_file_get_string (keyfile, group, key, NULL);
|
||
if (value != NULL && *value == '\0')
|
||
g_clear_pointer (&value, g_free);
|
||
|
||
return g_steal_pointer (&value);
|
||
}
|
||
|
||
GKeyFile *
|
||
flatpak_parse_repofile (const char *remote_name,
|
||
gboolean from_ref,
|
||
GKeyFile *keyfile,
|
||
GBytes **gpg_data_out,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GBytes) gpg_data = NULL;
|
||
g_autofree char *uri = NULL;
|
||
g_autofree char *title = NULL;
|
||
g_autofree char *gpg_key = NULL;
|
||
g_autofree char *collection_id = NULL;
|
||
g_autofree char *default_branch = NULL;
|
||
g_autofree char *comment = NULL;
|
||
g_autofree char *description = NULL;
|
||
g_autofree char *icon = NULL;
|
||
g_autofree char *homepage = NULL;
|
||
g_autofree char *filter = NULL;
|
||
g_autofree char *subset = NULL;
|
||
g_autofree char *authenticator_name = NULL;
|
||
gboolean nodeps;
|
||
const char *source_group;
|
||
g_autofree char *version = NULL;
|
||
|
||
if (from_ref)
|
||
source_group = FLATPAK_REF_GROUP;
|
||
else
|
||
source_group = FLATPAK_REPO_GROUP;
|
||
|
||
GKeyFile *config = g_key_file_new ();
|
||
g_autofree char *group = g_strdup_printf ("remote \"%s\"", remote_name);
|
||
|
||
if (!g_key_file_has_group (keyfile, source_group))
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid %s: Missing group ‘%s’"),
|
||
from_ref ? ".flatpakref" : ".flatpakrepo", source_group);
|
||
return NULL;
|
||
}
|
||
|
||
uri = g_key_file_get_string (keyfile, source_group,
|
||
FLATPAK_REPO_URL_KEY, NULL);
|
||
if (uri == NULL)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid %s: Missing key ‘%s’"),
|
||
from_ref ? ".flatpakref" : ".flatpakrepo", FLATPAK_REPO_URL_KEY);
|
||
return NULL;
|
||
}
|
||
|
||
version = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_VERSION_KEY, NULL);
|
||
if (version != NULL && strcmp (version, "1") != 0)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
|
||
_("Invalid version %s, only 1 supported"), version);
|
||
return NULL;
|
||
}
|
||
|
||
g_key_file_set_string (config, group, "url", uri);
|
||
|
||
subset = g_key_file_get_locale_string (keyfile, source_group,
|
||
FLATPAK_REPO_SUBSET_KEY, NULL, NULL);
|
||
if (subset != NULL)
|
||
g_key_file_set_string (config, group, "xa.subset", subset);
|
||
|
||
/* Don't use the title from flatpakref files; that's the title of the app */
|
||
if (!from_ref)
|
||
title = g_key_file_get_locale_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_TITLE_KEY, NULL, NULL);
|
||
if (title != NULL)
|
||
g_key_file_set_string (config, group, "xa.title", title);
|
||
|
||
default_branch = g_key_file_get_locale_string (keyfile, source_group,
|
||
FLATPAK_REPO_DEFAULT_BRANCH_KEY, NULL, NULL);
|
||
if (default_branch != NULL)
|
||
g_key_file_set_string (config, group, "xa.default-branch", default_branch);
|
||
|
||
nodeps = g_key_file_get_boolean (keyfile, source_group,
|
||
FLATPAK_REPO_NODEPS_KEY, NULL);
|
||
if (nodeps)
|
||
g_key_file_set_boolean (config, group, "xa.nodeps", TRUE);
|
||
|
||
gpg_key = g_key_file_get_string (keyfile, source_group,
|
||
FLATPAK_REPO_GPGKEY_KEY, NULL);
|
||
if (gpg_key != NULL)
|
||
{
|
||
guchar *decoded;
|
||
gsize decoded_len;
|
||
|
||
gpg_key = g_strstrip (gpg_key);
|
||
decoded = g_base64_decode (gpg_key, &decoded_len);
|
||
if (decoded_len < 10) /* Check some minimal size so we don't get crap */
|
||
{
|
||
g_free (decoded);
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid gpg key"));
|
||
return NULL;
|
||
}
|
||
|
||
gpg_data = g_bytes_new_take (decoded, decoded_len);
|
||
g_key_file_set_boolean (config, group, "gpg-verify", TRUE);
|
||
}
|
||
else
|
||
{
|
||
g_key_file_set_boolean (config, group, "gpg-verify", FALSE);
|
||
}
|
||
|
||
/* We have a hierarchy of keys for setting the collection ID, which all have
|
||
* the same effect. The only difference is which versions of Flatpak support
|
||
* them, and therefore what P2P implementation is enabled by them:
|
||
* DeploySideloadCollectionID: supported by Flatpak >= 1.12.8 (1.7.1
|
||
* introduced sideload support but this key was added late)
|
||
* DeployCollectionID: supported by Flatpak >= 1.0.6 (but fully supported in
|
||
* >= 1.2.0)
|
||
* CollectionID: supported by Flatpak >= 0.9.8
|
||
*/
|
||
collection_id = flatpak_keyfile_get_string_non_empty (keyfile, source_group,
|
||
FLATPAK_REPO_DEPLOY_SIDELOAD_COLLECTION_ID_KEY);
|
||
if (collection_id == NULL)
|
||
collection_id = flatpak_keyfile_get_string_non_empty (keyfile, source_group,
|
||
FLATPAK_REPO_DEPLOY_COLLECTION_ID_KEY);
|
||
if (collection_id == NULL)
|
||
collection_id = flatpak_keyfile_get_string_non_empty (keyfile, source_group,
|
||
FLATPAK_REPO_COLLECTION_ID_KEY);
|
||
if (collection_id != NULL)
|
||
{
|
||
if (gpg_key == NULL)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Collection ID requires GPG key to be provided"));
|
||
return NULL;
|
||
}
|
||
|
||
g_key_file_set_string (config, group, "collection-id", collection_id);
|
||
}
|
||
|
||
g_key_file_set_boolean (config, group, "gpg-verify-summary",
|
||
(gpg_key != NULL));
|
||
|
||
authenticator_name = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_AUTHENTICATOR_NAME_KEY, NULL);
|
||
if (authenticator_name)
|
||
g_key_file_set_string (config, group, "xa.authenticator-name", authenticator_name);
|
||
|
||
if (g_key_file_has_key (keyfile, FLATPAK_REPO_GROUP, FLATPAK_REPO_AUTHENTICATOR_INSTALL_KEY, NULL))
|
||
{
|
||
gboolean authenticator_install = g_key_file_get_boolean (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_AUTHENTICATOR_INSTALL_KEY, NULL);
|
||
g_key_file_set_boolean (config, group, "xa.authenticator-install", authenticator_install);
|
||
}
|
||
|
||
comment = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_COMMENT_KEY, NULL);
|
||
if (comment)
|
||
g_key_file_set_string (config, group, "xa.comment", comment);
|
||
|
||
description = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_DESCRIPTION_KEY, NULL);
|
||
if (description)
|
||
g_key_file_set_string (config, group, "xa.description", description);
|
||
|
||
icon = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_ICON_KEY, NULL);
|
||
if (icon)
|
||
g_key_file_set_string (config, group, "xa.icon", icon);
|
||
|
||
homepage = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_HOMEPAGE_KEY, NULL);
|
||
if (homepage)
|
||
g_key_file_set_string (config, group, "xa.homepage", homepage);
|
||
|
||
filter = g_key_file_get_string (keyfile, FLATPAK_REPO_GROUP,
|
||
FLATPAK_REPO_FILTER_KEY, NULL);
|
||
if (filter)
|
||
g_key_file_set_string (config, group, "xa.filter", filter);
|
||
else
|
||
g_key_file_set_string (config, group, "xa.filter", ""); /* Default to override any pre-existing filters */
|
||
|
||
*gpg_data_out = g_steal_pointer (&gpg_data);
|
||
|
||
return g_steal_pointer (&config);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_title (OstreeRepo *repo,
|
||
const char *title,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (title)
|
||
g_key_file_set_string (config, "flatpak", "title", title);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "title", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_comment (OstreeRepo *repo,
|
||
const char *comment,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (comment)
|
||
g_key_file_set_string (config, "flatpak", "comment", comment);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "comment", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_description (OstreeRepo *repo,
|
||
const char *description,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (description)
|
||
g_key_file_set_string (config, "flatpak", "description", description);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "description", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
gboolean
|
||
flatpak_repo_set_icon (OstreeRepo *repo,
|
||
const char *icon,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (icon)
|
||
g_key_file_set_string (config, "flatpak", "icon", icon);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "icon", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_homepage (OstreeRepo *repo,
|
||
const char *homepage,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (homepage)
|
||
g_key_file_set_string (config, "flatpak", "homepage", homepage);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "homepage", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_redirect_url (OstreeRepo *repo,
|
||
const char *redirect_url,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (redirect_url)
|
||
g_key_file_set_string (config, "flatpak", "redirect-url", redirect_url);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "redirect-url", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_authenticator_name (OstreeRepo *repo,
|
||
const char *authenticator_name,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (authenticator_name)
|
||
g_key_file_set_string (config, "flatpak", "authenticator-name", authenticator_name);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "authenticator-name", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_authenticator_install (OstreeRepo *repo,
|
||
gboolean authenticator_install,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
g_key_file_set_boolean (config, "flatpak", "authenticator-install", authenticator_install);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_authenticator_option (OstreeRepo *repo,
|
||
const char *key,
|
||
const char *value,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
g_autofree char *full_key = g_strdup_printf ("authenticator-options.%s", key);
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (value)
|
||
g_key_file_set_string (config, "flatpak", full_key, value);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", full_key, NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_deploy_collection_id (OstreeRepo *repo,
|
||
gboolean deploy_collection_id,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
g_key_file_set_boolean (config, "flatpak", "deploy-collection-id", deploy_collection_id);
|
||
return ostree_repo_write_config (repo, config, error);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_deploy_sideload_collection_id (OstreeRepo *repo,
|
||
gboolean deploy_collection_id,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
g_key_file_set_boolean (config, "flatpak", "deploy-sideload-collection-id", deploy_collection_id);
|
||
return ostree_repo_write_config (repo, config, error);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_gpg_keys (OstreeRepo *repo,
|
||
GBytes *bytes,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
g_autofree char *value_base64 = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
value_base64 = g_base64_encode (g_bytes_get_data (bytes, NULL), g_bytes_get_size (bytes));
|
||
|
||
g_key_file_set_string (config, "flatpak", "gpg-keys", value_base64);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_default_branch (OstreeRepo *repo,
|
||
const char *branch,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (branch)
|
||
g_key_file_set_string (config, "flatpak", "default-branch", branch);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "default-branch", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_collection_id (OstreeRepo *repo,
|
||
const char *collection_id,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
if (!ostree_repo_set_collection_id (repo, collection_id, error))
|
||
return FALSE;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_set_summary_history_length (OstreeRepo *repo,
|
||
guint length,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GKeyFile) config = NULL;
|
||
|
||
config = ostree_repo_copy_config (repo);
|
||
|
||
if (length)
|
||
g_key_file_set_integer (config, "flatpak", "summary-history-length", length);
|
||
else
|
||
g_key_file_remove_key (config, "flatpak", "summary-history-length", NULL);
|
||
|
||
if (!ostree_repo_write_config (repo, config, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
guint
|
||
flatpak_repo_get_summary_history_length (OstreeRepo *repo)
|
||
{
|
||
GKeyFile *config = ostree_repo_get_config (repo);
|
||
int length;
|
||
|
||
length = g_key_file_get_integer (config, "flatpak", "sumary-history-length", NULL);
|
||
|
||
if (length <= 0)
|
||
return FLATPAK_SUMMARY_HISTORY_LENGTH_DEFAULT;
|
||
|
||
return length;
|
||
}
|
||
|
||
GVariant *
|
||
flatpak_commit_get_extra_data_sources (GVariant *commitv,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariant) commit_metadata = NULL;
|
||
g_autoptr(GVariant) extra_data_sources = NULL;
|
||
|
||
commit_metadata = g_variant_get_child_value (commitv, 0);
|
||
extra_data_sources = g_variant_lookup_value (commit_metadata,
|
||
"xa.extra-data-sources",
|
||
G_VARIANT_TYPE ("a(ayttays)"));
|
||
|
||
if (extra_data_sources == NULL)
|
||
{
|
||
g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND,
|
||
_("No extra data sources"));
|
||
return NULL;
|
||
}
|
||
|
||
return g_steal_pointer (&extra_data_sources);
|
||
}
|
||
|
||
|
||
GVariant *
|
||
flatpak_repo_get_extra_data_sources (OstreeRepo *repo,
|
||
const char *rev,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariant) commitv = NULL;
|
||
|
||
if (!ostree_repo_load_variant (repo,
|
||
OSTREE_OBJECT_TYPE_COMMIT,
|
||
rev, &commitv, error))
|
||
return NULL;
|
||
|
||
return flatpak_commit_get_extra_data_sources (commitv, error);
|
||
}
|
||
|
||
void
|
||
flatpak_repo_parse_extra_data_sources (GVariant *extra_data_sources,
|
||
int index,
|
||
const char **name,
|
||
guint64 *download_size,
|
||
guint64 *installed_size,
|
||
const guchar **sha256,
|
||
const char **uri)
|
||
{
|
||
g_autoptr(GVariant) sha256_v = NULL;
|
||
g_variant_get_child (extra_data_sources, index, "(^aytt@ay&s)",
|
||
name,
|
||
download_size,
|
||
installed_size,
|
||
&sha256_v,
|
||
uri);
|
||
|
||
if (download_size)
|
||
*download_size = GUINT64_FROM_BE (*download_size);
|
||
|
||
if (installed_size)
|
||
*installed_size = GUINT64_FROM_BE (*installed_size);
|
||
|
||
if (sha256)
|
||
*sha256 = ostree_checksum_bytes_peek (sha256_v);
|
||
}
|
||
|
||
#define OSTREE_GIO_FAST_QUERYINFO ("standard::name,standard::type,standard::size,standard::is-symlink,standard::symlink-target," \
|
||
"unix::device,unix::inode,unix::mode,unix::uid,unix::gid,unix::rdev")
|
||
|
||
static gboolean
|
||
_flatpak_repo_collect_sizes (OstreeRepo *repo,
|
||
GFile *file,
|
||
GFileInfo *file_info,
|
||
guint64 *installed_size,
|
||
guint64 *download_size,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFileEnumerator) dir_enum = NULL;
|
||
GFileInfo *child_info_tmp;
|
||
g_autoptr(GError) temp_error = NULL;
|
||
|
||
if (file_info != NULL && g_file_info_get_file_type (file_info) == G_FILE_TYPE_REGULAR)
|
||
{
|
||
const char *checksum = ostree_repo_file_get_checksum (OSTREE_REPO_FILE (file));
|
||
guint64 obj_size;
|
||
guint64 file_size = g_file_info_get_size (file_info);
|
||
|
||
if (installed_size)
|
||
*installed_size += ((file_size + 511) / 512) * 512;
|
||
|
||
if (download_size)
|
||
{
|
||
g_autoptr(GInputStream) input = NULL;
|
||
GInputStream *base_input;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
if (!ostree_repo_query_object_storage_size (repo,
|
||
OSTREE_OBJECT_TYPE_FILE, checksum,
|
||
&obj_size, cancellable, &local_error))
|
||
{
|
||
int fd;
|
||
struct stat stbuf;
|
||
|
||
/* Ostree does not look at the staging directory when querying storage
|
||
size, so may return a NOT_FOUND error here. We work around this
|
||
by loading the object and walking back until we find the original
|
||
fd which we can fstat(). */
|
||
if (!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
|
||
return FALSE;
|
||
|
||
if (!ostree_repo_load_file (repo, checksum, &input, NULL, NULL, NULL, error))
|
||
return FALSE;
|
||
|
||
base_input = input;
|
||
while (G_IS_FILTER_INPUT_STREAM (base_input))
|
||
base_input = g_filter_input_stream_get_base_stream (G_FILTER_INPUT_STREAM (base_input));
|
||
|
||
if (!G_IS_UNIX_INPUT_STREAM (base_input))
|
||
return flatpak_fail (error, "Unable to find size of commit %s, not an unix stream", checksum);
|
||
|
||
fd = g_unix_input_stream_get_fd (G_UNIX_INPUT_STREAM (base_input));
|
||
|
||
if (fstat (fd, &stbuf) != 0)
|
||
return glnx_throw_errno_prefix (error, "Can't find commit size: ");
|
||
|
||
obj_size = stbuf.st_size;
|
||
}
|
||
|
||
*download_size += obj_size;
|
||
}
|
||
}
|
||
|
||
if (file_info == NULL || g_file_info_get_file_type (file_info) == G_FILE_TYPE_DIRECTORY)
|
||
{
|
||
dir_enum = g_file_enumerate_children (file, OSTREE_GIO_FAST_QUERYINFO,
|
||
G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
|
||
cancellable, error);
|
||
if (!dir_enum)
|
||
return FALSE;
|
||
|
||
|
||
while ((child_info_tmp = g_file_enumerator_next_file (dir_enum, cancellable, &temp_error)))
|
||
{
|
||
g_autoptr(GFileInfo) child_info = child_info_tmp;
|
||
const char *name = g_file_info_get_name (child_info);
|
||
g_autoptr(GFile) child = g_file_get_child (file, name);
|
||
|
||
if (!_flatpak_repo_collect_sizes (repo, child, child_info, installed_size, download_size, cancellable, error))
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_repo_collect_sizes (OstreeRepo *repo,
|
||
GFile *root,
|
||
guint64 *installed_size,
|
||
guint64 *download_size,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
/* Initialize the sums */
|
||
if (installed_size)
|
||
*installed_size = 0;
|
||
if (download_size)
|
||
*download_size = 0;
|
||
return _flatpak_repo_collect_sizes (repo, root, NULL, installed_size, download_size, cancellable, error);
|
||
}
|
||
|
||
|
||
static void
|
||
flatpak_repo_collect_extra_data_sizes (OstreeRepo *repo,
|
||
const char *rev,
|
||
guint64 *installed_size,
|
||
guint64 *download_size)
|
||
{
|
||
g_autoptr(GVariant) extra_data_sources = NULL;
|
||
gsize n_extra_data;
|
||
int i;
|
||
|
||
extra_data_sources = flatpak_repo_get_extra_data_sources (repo, rev, NULL, NULL);
|
||
if (extra_data_sources == NULL)
|
||
return;
|
||
|
||
n_extra_data = g_variant_n_children (extra_data_sources);
|
||
if (n_extra_data == 0)
|
||
return;
|
||
|
||
for (i = 0; i < n_extra_data; i++)
|
||
{
|
||
guint64 extra_download_size;
|
||
guint64 extra_installed_size;
|
||
|
||
flatpak_repo_parse_extra_data_sources (extra_data_sources, i,
|
||
NULL,
|
||
&extra_download_size,
|
||
&extra_installed_size,
|
||
NULL, NULL);
|
||
if (installed_size)
|
||
*installed_size += extra_installed_size;
|
||
if (download_size)
|
||
*download_size += extra_download_size;
|
||
}
|
||
}
|
||
|
||
/* Loads the old compat summary file from a local repo */
|
||
GVariant *
|
||
flatpak_repo_load_summary (OstreeRepo *repo,
|
||
GError **error)
|
||
{
|
||
glnx_autofd int fd = -1;
|
||
g_autoptr(GMappedFile) mfile = NULL;
|
||
g_autoptr(GBytes) bytes = NULL;
|
||
|
||
fd = openat (ostree_repo_get_dfd (repo), "summary", O_RDONLY | O_CLOEXEC);
|
||
if (fd < 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return NULL;
|
||
}
|
||
|
||
mfile = g_mapped_file_new_from_fd (fd, FALSE, error);
|
||
if (!mfile)
|
||
return NULL;
|
||
|
||
bytes = g_mapped_file_get_bytes (mfile);
|
||
|
||
return g_variant_ref_sink (g_variant_new_from_bytes (OSTREE_SUMMARY_GVARIANT_FORMAT, bytes, TRUE));
|
||
}
|
||
|
||
GVariant *
|
||
flatpak_repo_load_summary_index (OstreeRepo *repo,
|
||
GError **error)
|
||
{
|
||
glnx_autofd int fd = -1;
|
||
g_autoptr(GMappedFile) mfile = NULL;
|
||
g_autoptr(GBytes) bytes = NULL;
|
||
|
||
fd = openat (ostree_repo_get_dfd (repo), "summary.idx", O_RDONLY | O_CLOEXEC);
|
||
if (fd < 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return NULL;
|
||
}
|
||
|
||
mfile = g_mapped_file_new_from_fd (fd, FALSE, error);
|
||
if (!mfile)
|
||
return NULL;
|
||
|
||
bytes = g_mapped_file_get_bytes (mfile);
|
||
|
||
return g_variant_ref_sink (g_variant_new_from_bytes (FLATPAK_SUMMARY_INDEX_GVARIANT_FORMAT, bytes, TRUE));
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_repo_save_compat_summary (OstreeRepo *repo,
|
||
GVariant *summary,
|
||
time_t *out_old_sig_mtime,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
int repo_dfd = ostree_repo_get_dfd (repo);
|
||
struct stat stbuf;
|
||
time_t old_sig_mtime = 0;
|
||
GLnxFileReplaceFlags flags;
|
||
|
||
flags = GLNX_FILE_REPLACE_INCREASING_MTIME;
|
||
if (ostree_repo_get_disable_fsync (repo))
|
||
flags |= GLNX_FILE_REPLACE_NODATASYNC;
|
||
else
|
||
flags |= GLNX_FILE_REPLACE_DATASYNC_NEW;
|
||
|
||
if (!glnx_file_replace_contents_at (repo_dfd, "summary",
|
||
g_variant_get_data (summary),
|
||
g_variant_get_size (summary),
|
||
flags,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
if (fstatat (repo_dfd, "summary.sig", &stbuf, AT_SYMLINK_NOFOLLOW) == 0)
|
||
old_sig_mtime = stbuf.st_mtime;
|
||
|
||
if (unlinkat (repo_dfd, "summary.sig", 0) != 0 &&
|
||
G_UNLIKELY (errno != ENOENT))
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
*out_old_sig_mtime = old_sig_mtime;
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_repo_save_summary_index (OstreeRepo *repo,
|
||
GVariant *index,
|
||
const char *index_digest,
|
||
GBytes *index_sig,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
int repo_dfd = ostree_repo_get_dfd (repo);
|
||
GLnxFileReplaceFlags flags;
|
||
|
||
if (index == NULL)
|
||
{
|
||
if (unlinkat (repo_dfd, "summary.idx", 0) != 0 &&
|
||
G_UNLIKELY (errno != ENOENT))
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
if (unlinkat (repo_dfd, "summary.idx.sig", 0) != 0 &&
|
||
G_UNLIKELY (errno != ENOENT))
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
flags = GLNX_FILE_REPLACE_INCREASING_MTIME;
|
||
if (ostree_repo_get_disable_fsync (repo))
|
||
flags |= GLNX_FILE_REPLACE_NODATASYNC;
|
||
else
|
||
flags |= GLNX_FILE_REPLACE_DATASYNC_NEW;
|
||
|
||
if (index_sig)
|
||
{
|
||
g_autofree char *path = g_strconcat ("summaries/", index_digest, ".idx.sig", NULL);
|
||
|
||
if (!glnx_shutil_mkdir_p_at (repo_dfd, "summaries",
|
||
0775, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (!glnx_file_replace_contents_at (repo_dfd, path,
|
||
g_bytes_get_data (index_sig, NULL),
|
||
g_bytes_get_size (index_sig),
|
||
flags,
|
||
cancellable, error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (!glnx_file_replace_contents_at (repo_dfd, "summary.idx",
|
||
g_variant_get_data (index),
|
||
g_variant_get_size (index),
|
||
flags,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
/* Update the non-indexed summary.idx.sig file that was introduced in 1.9.1 but
|
||
* was made unnecessary in 1.9.3. Lets keep it for a while until everyone updates
|
||
*/
|
||
if (index_sig)
|
||
{
|
||
if (!glnx_file_replace_contents_at (repo_dfd, "summary.idx.sig",
|
||
g_bytes_get_data (index_sig, NULL),
|
||
g_bytes_get_size (index_sig),
|
||
flags,
|
||
cancellable, error))
|
||
return FALSE;
|
||
}
|
||
else
|
||
{
|
||
if (unlinkat (repo_dfd, "summary.idx.sig", 0) != 0 &&
|
||
G_UNLIKELY (errno != ENOENT))
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
GVariant *
|
||
flatpak_repo_load_digested_summary (OstreeRepo *repo,
|
||
const char *digest,
|
||
GError **error)
|
||
{
|
||
glnx_autofd int fd = -1;
|
||
g_autoptr(GMappedFile) mfile = NULL;
|
||
g_autoptr(GBytes) bytes = NULL;
|
||
g_autoptr(GBytes) compressed_bytes = NULL;
|
||
g_autofree char *path = NULL;
|
||
g_autofree char *filename = NULL;
|
||
|
||
filename = g_strconcat (digest, ".gz", NULL);
|
||
path = g_build_filename ("summaries", filename, NULL);
|
||
|
||
fd = openat (ostree_repo_get_dfd (repo), path, O_RDONLY | O_CLOEXEC);
|
||
if (fd < 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return NULL;
|
||
}
|
||
|
||
mfile = g_mapped_file_new_from_fd (fd, FALSE, error);
|
||
if (!mfile)
|
||
return NULL;
|
||
|
||
compressed_bytes = g_mapped_file_get_bytes (mfile);
|
||
bytes = flatpak_zlib_decompress_bytes (compressed_bytes, error);
|
||
if (bytes == NULL)
|
||
return NULL;
|
||
|
||
return g_variant_ref_sink (g_variant_new_from_bytes (OSTREE_SUMMARY_GVARIANT_FORMAT, bytes, TRUE));
|
||
}
|
||
|
||
static char *
|
||
flatpak_repo_save_digested_summary (OstreeRepo *repo,
|
||
const char *name,
|
||
GVariant *summary,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
int repo_dfd = ostree_repo_get_dfd (repo);
|
||
g_autofree char *digest = NULL;
|
||
g_autofree char *filename = NULL;
|
||
g_autofree char *path = NULL;
|
||
g_autoptr(GBytes) data = NULL;
|
||
g_autoptr(GBytes) compressed_data = NULL;
|
||
struct stat stbuf;
|
||
|
||
if (!glnx_shutil_mkdir_p_at (repo_dfd, "summaries",
|
||
0775,
|
||
cancellable,
|
||
error))
|
||
return NULL;
|
||
|
||
digest = g_compute_checksum_for_data (G_CHECKSUM_SHA256,
|
||
g_variant_get_data (summary),
|
||
g_variant_get_size (summary));
|
||
filename = g_strconcat (digest, ".gz", NULL);
|
||
|
||
path = g_build_filename ("summaries", filename, NULL);
|
||
|
||
/* Check for pre-existing (non-truncated) copy and avoid re-writing it */
|
||
if (fstatat (repo_dfd, path, &stbuf, 0) == 0 &&
|
||
stbuf.st_size != 0)
|
||
{
|
||
g_info ("Reusing digested summary at %s for %s", path, name);
|
||
return g_steal_pointer (&digest);
|
||
}
|
||
|
||
data = g_variant_get_data_as_bytes (summary);
|
||
compressed_data = flatpak_zlib_compress_bytes (data, -1, error);
|
||
if (compressed_data == NULL)
|
||
return NULL;
|
||
|
||
if (!glnx_file_replace_contents_at (repo_dfd, path,
|
||
g_bytes_get_data (compressed_data, NULL),
|
||
g_bytes_get_size (compressed_data),
|
||
ostree_repo_get_disable_fsync (repo) ? GLNX_FILE_REPLACE_NODATASYNC : GLNX_FILE_REPLACE_DATASYNC_NEW,
|
||
cancellable, error))
|
||
return NULL;
|
||
|
||
g_info ("Wrote digested summary at %s for %s", path, name);
|
||
return g_steal_pointer (&digest);
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_repo_save_digested_summary_delta (OstreeRepo *repo,
|
||
const char *from_digest,
|
||
const char *to_digest,
|
||
GBytes *delta,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
int repo_dfd = ostree_repo_get_dfd (repo);
|
||
g_autofree char *path = NULL;
|
||
g_autofree char *filename = g_strconcat (from_digest, "-", to_digest, ".delta", NULL);
|
||
struct stat stbuf;
|
||
|
||
if (!glnx_shutil_mkdir_p_at (repo_dfd, "summaries",
|
||
0775,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
|
||
path = g_build_filename ("summaries", filename, NULL);
|
||
|
||
/* Check for pre-existing copy of same size and avoid re-writing it */
|
||
if (fstatat (repo_dfd, path, &stbuf, 0) == 0 &&
|
||
stbuf.st_size == g_bytes_get_size (delta))
|
||
{
|
||
g_info ("Reusing digested summary-diff for %s", filename);
|
||
return TRUE;
|
||
}
|
||
|
||
if (!glnx_file_replace_contents_at (repo_dfd, path,
|
||
g_bytes_get_data (delta, NULL),
|
||
g_bytes_get_size (delta),
|
||
ostree_repo_get_disable_fsync (repo) ? GLNX_FILE_REPLACE_NODATASYNC : GLNX_FILE_REPLACE_DATASYNC_NEW,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
g_info ("Wrote digested summary delta at %s", path);
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
gboolean
|
||
flatpak_is_app_runtime_or_appstream_ref (const char *ref)
|
||
{
|
||
return
|
||
g_str_has_prefix (ref, "appstream/") ||
|
||
g_str_has_prefix (ref, "appstream2/") ||
|
||
g_str_has_prefix (ref, "app/") ||
|
||
g_str_has_prefix (ref, "runtime/");
|
||
}
|
||
|
||
typedef struct
|
||
{
|
||
guint64 installed_size;
|
||
guint64 download_size;
|
||
char *metadata_contents;
|
||
GPtrArray *subsets;
|
||
GVariant *sparse_data;
|
||
gsize commit_size;
|
||
guint64 commit_timestamp;
|
||
} CommitData;
|
||
|
||
static void
|
||
commit_data_free (gpointer data)
|
||
{
|
||
CommitData *rev_data = data;
|
||
|
||
if (rev_data->subsets)
|
||
g_ptr_array_unref (rev_data->subsets);
|
||
g_free (rev_data->metadata_contents);
|
||
if (rev_data->sparse_data)
|
||
g_variant_unref (rev_data->sparse_data);
|
||
g_free (rev_data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (CommitData, commit_data_free);
|
||
|
||
static GHashTable *
|
||
commit_data_cache_new (void)
|
||
{
|
||
return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, commit_data_free);
|
||
}
|
||
|
||
static GHashTable *
|
||
populate_commit_data_cache (OstreeRepo *repo,
|
||
GVariant *index_v)
|
||
{
|
||
|
||
VarSummaryIndexRef index = var_summary_index_from_gvariant (index_v);
|
||
VarMetadataRef index_metadata = var_summary_index_get_metadata (index);
|
||
VarSummaryIndexSubsummariesRef subsummaries = var_summary_index_get_subsummaries (index);
|
||
gsize n_subsummaries = var_summary_index_subsummaries_get_length (subsummaries);
|
||
guint32 cache_version;
|
||
g_autoptr(GHashTable) commit_data_cache = commit_data_cache_new ();
|
||
|
||
cache_version = GUINT32_FROM_LE (var_metadata_lookup_uint32 (index_metadata, "xa.cache-version", 0));
|
||
if (cache_version < FLATPAK_XA_CACHE_VERSION)
|
||
{
|
||
/* Need to re-index to get all data */
|
||
g_info ("Old summary cache version %d, not using cache", cache_version);
|
||
return NULL;
|
||
}
|
||
|
||
for (gsize i = 0; i < n_subsummaries; i++)
|
||
{
|
||
VarSummaryIndexSubsummariesEntryRef entry = var_summary_index_subsummaries_get_at (subsummaries, i);
|
||
const char *name = var_summary_index_subsummaries_entry_get_key (entry);
|
||
const char *s;
|
||
g_autofree char *subset = NULL;
|
||
VarSubsummaryRef subsummary = var_summary_index_subsummaries_entry_get_value (entry);
|
||
gsize checksum_bytes_len;
|
||
const guchar *checksum_bytes;
|
||
g_autofree char *digest = NULL;
|
||
g_autoptr(GVariant) summary_v = NULL;
|
||
VarSummaryRef summary;
|
||
VarRefMapRef ref_map;
|
||
gsize n_refs;
|
||
|
||
checksum_bytes = var_subsummary_peek_checksum (subsummary, &checksum_bytes_len);
|
||
if (G_UNLIKELY (checksum_bytes_len != OSTREE_SHA256_DIGEST_LEN))
|
||
{
|
||
g_info ("Invalid checksum for digested summary, not using cache");
|
||
return NULL;
|
||
}
|
||
digest = ostree_checksum_from_bytes (checksum_bytes);
|
||
|
||
s = strrchr (name, '-');
|
||
if (s != NULL)
|
||
subset = g_strndup (name, s - name);
|
||
else
|
||
subset = g_strdup ("");
|
||
|
||
summary_v = flatpak_repo_load_digested_summary (repo, digest, NULL);
|
||
if (summary_v == NULL)
|
||
{
|
||
g_info ("Failed to load digested summary %s, not using cache", digest);
|
||
return NULL;
|
||
}
|
||
|
||
/* Note that all summaries refered to by the index is in new format */
|
||
summary = var_summary_from_gvariant (summary_v);
|
||
ref_map = var_summary_get_ref_map (summary);
|
||
n_refs = var_ref_map_get_length (ref_map);
|
||
for (gsize j = 0; j < n_refs; j++)
|
||
{
|
||
VarRefMapEntryRef e = var_ref_map_get_at (ref_map, j);
|
||
const char *ref = var_ref_map_entry_get_ref (e);
|
||
VarRefInfoRef info = var_ref_map_entry_get_info (e);
|
||
VarMetadataRef commit_metadata = var_ref_info_get_metadata (info);
|
||
guint64 commit_size = var_ref_info_get_commit_size (info);
|
||
const guchar *commit_bytes;
|
||
gsize commit_bytes_len;
|
||
g_autofree char *rev = NULL;
|
||
CommitData *rev_data;
|
||
VarVariantRef xa_data_v;
|
||
VarCacheDataRef xa_data;
|
||
|
||
if (!flatpak_is_app_runtime_or_appstream_ref (ref))
|
||
continue;
|
||
|
||
commit_bytes = var_ref_info_peek_checksum (info, &commit_bytes_len);
|
||
if (G_UNLIKELY (commit_bytes_len != OSTREE_SHA256_DIGEST_LEN))
|
||
continue;
|
||
|
||
if (!var_metadata_lookup (commit_metadata, "xa.data", NULL, &xa_data_v) ||
|
||
!var_variant_is_type (xa_data_v, G_VARIANT_TYPE ("(tts)")))
|
||
{
|
||
g_info ("Missing xa.data for ref %s, not using cache", ref);
|
||
return NULL;
|
||
}
|
||
|
||
xa_data = var_cache_data_from_variant (xa_data_v);
|
||
|
||
rev = ostree_checksum_from_bytes (commit_bytes);
|
||
rev_data = g_hash_table_lookup (commit_data_cache, rev);
|
||
if (rev_data == NULL)
|
||
{
|
||
g_auto(GVariantBuilder) sparse_builder = FLATPAK_VARIANT_BUILDER_INITIALIZER;
|
||
g_variant_builder_init (&sparse_builder, G_VARIANT_TYPE_VARDICT);
|
||
gboolean has_sparse = FALSE;
|
||
|
||
rev_data = g_new0 (CommitData, 1);
|
||
rev_data->installed_size = var_cache_data_get_installed_size (xa_data);
|
||
rev_data->download_size = var_cache_data_get_download_size (xa_data);
|
||
rev_data->metadata_contents = g_strdup (var_cache_data_get_metadata (xa_data));
|
||
rev_data->commit_size = commit_size;
|
||
rev_data->commit_timestamp = GUINT64_FROM_BE (var_metadata_lookup_uint64 (commit_metadata, OSTREE_COMMIT_TIMESTAMP2, 0));
|
||
|
||
/* Get sparse data */
|
||
gsize len = var_metadata_get_length (commit_metadata);
|
||
for (gsize k = 0; k < len; k++)
|
||
{
|
||
VarMetadataEntryRef m = var_metadata_get_at (commit_metadata, k);
|
||
const char *m_key = var_metadata_entry_get_key (m);
|
||
if (!g_str_has_prefix (m_key, "ot.") &&
|
||
!g_str_has_prefix (m_key, "ostree.") &&
|
||
strcmp (m_key, "xa.data") != 0)
|
||
{
|
||
VarVariantRef v = var_metadata_entry_get_value (m);
|
||
GVariant *vv = var_variant_dup_to_gvariant (v);
|
||
g_variant_builder_add (&sparse_builder, "{sv}", m_key, g_variant_get_child_value (vv, 0));
|
||
has_sparse = TRUE;
|
||
}
|
||
}
|
||
|
||
if (has_sparse)
|
||
rev_data->sparse_data = g_variant_ref_sink (g_variant_builder_end (&sparse_builder));
|
||
|
||
g_hash_table_insert (commit_data_cache, g_strdup (rev), (CommitData *)rev_data);
|
||
}
|
||
|
||
if (*subset != 0)
|
||
{
|
||
if (rev_data->subsets == NULL)
|
||
rev_data->subsets = g_ptr_array_new_with_free_func (g_free);
|
||
|
||
if (!flatpak_g_ptr_array_contains_string (rev_data->subsets, subset))
|
||
g_ptr_array_add (rev_data->subsets, g_strdup (subset));
|
||
}
|
||
}
|
||
}
|
||
|
||
return g_steal_pointer (&commit_data_cache);
|
||
}
|
||
|
||
static CommitData *
|
||
read_commit_data (OstreeRepo *repo,
|
||
const char *ref,
|
||
const char *rev,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFile) root = NULL;
|
||
g_autoptr(GFile) metadata = NULL;
|
||
guint64 installed_size = 0;
|
||
guint64 download_size = 0;
|
||
g_autofree char *metadata_contents = NULL;
|
||
g_autofree char *commit = NULL;
|
||
g_autoptr(GVariant) commit_v = NULL;
|
||
g_autoptr(GVariant) commit_metadata = NULL;
|
||
g_autoptr(GPtrArray) subsets = NULL;
|
||
CommitData *rev_data;
|
||
const char *eol = NULL;
|
||
const char *eol_rebase = NULL;
|
||
int token_type = -1;
|
||
g_autoptr(GVariant) extra_data_sources = NULL;
|
||
guint32 n_extra_data = 0;
|
||
guint64 total_extra_data_download_size = 0;
|
||
g_autoptr(GVariantIter) subsets_iter = NULL;
|
||
|
||
if (!ostree_repo_read_commit (repo, rev, &root, &commit, NULL, error))
|
||
return NULL;
|
||
|
||
if (!ostree_repo_load_commit (repo, commit, &commit_v, NULL, error))
|
||
return NULL;
|
||
|
||
commit_metadata = g_variant_get_child_value (commit_v, 0);
|
||
if (!g_variant_lookup (commit_metadata, "xa.metadata", "s", &metadata_contents))
|
||
{
|
||
metadata = g_file_get_child (root, "metadata");
|
||
if (!g_file_load_contents (metadata, cancellable, &metadata_contents, NULL, NULL, NULL))
|
||
metadata_contents = g_strdup ("");
|
||
}
|
||
|
||
if (g_variant_lookup (commit_metadata, "xa.installed-size", "t", &installed_size) &&
|
||
g_variant_lookup (commit_metadata, "xa.download-size", "t", &download_size))
|
||
{
|
||
installed_size = GUINT64_FROM_BE (installed_size);
|
||
download_size = GUINT64_FROM_BE (download_size);
|
||
}
|
||
else
|
||
{
|
||
if (!flatpak_repo_collect_sizes (repo, root, &installed_size, &download_size, cancellable, error))
|
||
return NULL;
|
||
}
|
||
|
||
if (g_variant_lookup (commit_metadata, "xa.subsets", "as", &subsets_iter))
|
||
{
|
||
const char *subset;
|
||
subsets = g_ptr_array_new_with_free_func (g_free);
|
||
while (g_variant_iter_next (subsets_iter, "&s", &subset))
|
||
g_ptr_array_add (subsets, g_strdup (subset));
|
||
}
|
||
|
||
flatpak_repo_collect_extra_data_sizes (repo, rev, &installed_size, &download_size);
|
||
|
||
rev_data = g_new0 (CommitData, 1);
|
||
rev_data->installed_size = installed_size;
|
||
rev_data->download_size = download_size;
|
||
rev_data->metadata_contents = g_steal_pointer (&metadata_contents);
|
||
rev_data->subsets = g_steal_pointer (&subsets);
|
||
rev_data->commit_size = g_variant_get_size (commit_v);
|
||
rev_data->commit_timestamp = ostree_commit_get_timestamp (commit_v);
|
||
|
||
g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE, "&s", &eol);
|
||
g_variant_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE, "&s", &eol_rebase);
|
||
if (g_variant_lookup (commit_metadata, "xa.token-type", "i", &token_type))
|
||
token_type = GINT32_FROM_LE(token_type);
|
||
|
||
extra_data_sources = flatpak_commit_get_extra_data_sources (commit_v, NULL);
|
||
if (extra_data_sources)
|
||
{
|
||
n_extra_data = g_variant_n_children (extra_data_sources);
|
||
for (int i = 0; i < n_extra_data; i++)
|
||
{
|
||
guint64 extra_download_size;
|
||
flatpak_repo_parse_extra_data_sources (extra_data_sources, i,
|
||
NULL,
|
||
&extra_download_size,
|
||
NULL,
|
||
NULL,
|
||
NULL);
|
||
total_extra_data_download_size += extra_download_size;
|
||
}
|
||
}
|
||
|
||
if (eol || eol_rebase || token_type >= 0 || n_extra_data > 0)
|
||
{
|
||
g_auto(GVariantBuilder) sparse_builder = FLATPAK_VARIANT_BUILDER_INITIALIZER;
|
||
g_variant_builder_init (&sparse_builder, G_VARIANT_TYPE_VARDICT);
|
||
if (eol)
|
||
g_variant_builder_add (&sparse_builder, "{sv}", FLATPAK_SPARSE_CACHE_KEY_ENDOFLINE, g_variant_new_string (eol));
|
||
if (eol_rebase)
|
||
g_variant_builder_add (&sparse_builder, "{sv}", FLATPAK_SPARSE_CACHE_KEY_ENDOFLINE_REBASE, g_variant_new_string (eol_rebase));
|
||
if (token_type >= 0)
|
||
g_variant_builder_add (&sparse_builder, "{sv}", FLATPAK_SPARSE_CACHE_KEY_TOKEN_TYPE, g_variant_new_int32 (GINT32_TO_LE(token_type)));
|
||
if (n_extra_data > 0)
|
||
g_variant_builder_add (&sparse_builder, "{sv}", FLATPAK_SPARSE_CACHE_KEY_EXTRA_DATA_SIZE,
|
||
g_variant_new ("(ut)", GUINT32_TO_LE(n_extra_data), GUINT64_TO_LE(total_extra_data_download_size)));
|
||
|
||
rev_data->sparse_data = g_variant_ref_sink (g_variant_builder_end (&sparse_builder));
|
||
}
|
||
|
||
return rev_data;
|
||
}
|
||
|
||
static void
|
||
_ostree_parse_delta_name (const char *delta_name,
|
||
char **out_from,
|
||
char **out_to)
|
||
{
|
||
g_auto(GStrv) parts = g_strsplit (delta_name, "-", 2);
|
||
|
||
if (parts[0] && parts[1])
|
||
{
|
||
*out_from = g_steal_pointer (&parts[0]);
|
||
*out_to = g_steal_pointer (&parts[1]);
|
||
}
|
||
else
|
||
{
|
||
*out_from = NULL;
|
||
*out_to = g_steal_pointer (&parts[0]);
|
||
}
|
||
}
|
||
|
||
static GString *
|
||
static_delta_path_base (const char *dir,
|
||
const char *from,
|
||
const char *to)
|
||
{
|
||
guint8 csum_to[OSTREE_SHA256_DIGEST_LEN];
|
||
char to_b64[44];
|
||
guint8 csum_to_copy[OSTREE_SHA256_DIGEST_LEN];
|
||
GString *ret = g_string_new (dir);
|
||
|
||
ostree_checksum_inplace_to_bytes (to, csum_to);
|
||
ostree_checksum_b64_inplace_from_bytes (csum_to, to_b64);
|
||
ostree_checksum_b64_inplace_to_bytes (to_b64, csum_to_copy);
|
||
|
||
g_assert (memcmp (csum_to, csum_to_copy, OSTREE_SHA256_DIGEST_LEN) == 0);
|
||
|
||
if (from != NULL)
|
||
{
|
||
guint8 csum_from[OSTREE_SHA256_DIGEST_LEN];
|
||
char from_b64[44];
|
||
|
||
ostree_checksum_inplace_to_bytes (from, csum_from);
|
||
ostree_checksum_b64_inplace_from_bytes (csum_from, from_b64);
|
||
|
||
g_string_append_c (ret, from_b64[0]);
|
||
g_string_append_c (ret, from_b64[1]);
|
||
g_string_append_c (ret, '/');
|
||
g_string_append (ret, from_b64 + 2);
|
||
g_string_append_c (ret, '-');
|
||
}
|
||
|
||
g_string_append_c (ret, to_b64[0]);
|
||
g_string_append_c (ret, to_b64[1]);
|
||
if (from == NULL)
|
||
g_string_append_c (ret, '/');
|
||
g_string_append (ret, to_b64 + 2);
|
||
|
||
return ret;
|
||
}
|
||
|
||
static char *
|
||
_ostree_get_relative_static_delta_path (const char *from,
|
||
const char *to,
|
||
const char *target)
|
||
{
|
||
GString *ret = static_delta_path_base ("deltas/", from, to);
|
||
|
||
if (target != NULL)
|
||
{
|
||
g_string_append_c (ret, '/');
|
||
g_string_append (ret, target);
|
||
}
|
||
|
||
return g_string_free (ret, FALSE);
|
||
}
|
||
|
||
static char *
|
||
_ostree_get_relative_static_delta_superblock_path (const char *from,
|
||
const char *to)
|
||
{
|
||
return _ostree_get_relative_static_delta_path (from, to, "superblock");
|
||
}
|
||
|
||
static GVariant *
|
||
_ostree_repo_static_delta_superblock_digest (OstreeRepo *repo,
|
||
const char *from,
|
||
const char *to,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autofree char *superblock = _ostree_get_relative_static_delta_superblock_path ((from && from[0]) ? from : NULL, to);
|
||
glnx_autofd int fd = -1;
|
||
guint8 digest[OSTREE_SHA256_DIGEST_LEN];
|
||
gsize len;
|
||
|
||
if (!glnx_openat_rdonly (ostree_repo_get_dfd (repo), superblock, TRUE, &fd, error))
|
||
return NULL;
|
||
|
||
g_autoptr(GBytes) superblock_content = glnx_fd_readall_bytes (fd, cancellable, error);
|
||
if (!superblock_content)
|
||
return NULL;
|
||
|
||
g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256);
|
||
g_checksum_update (checksum, g_bytes_get_data (superblock_content, NULL), g_bytes_get_size (superblock_content));
|
||
len = sizeof digest;
|
||
g_checksum_get_digest (checksum, digest, &len);
|
||
|
||
return g_variant_new_from_data (G_VARIANT_TYPE ("ay"),
|
||
g_memdup2 (digest, len), len,
|
||
FALSE, g_free, FALSE);
|
||
}
|
||
|
||
static char *
|
||
appstream_ref_get_subset (const char *ref)
|
||
{
|
||
if (!g_str_has_prefix (ref, "appstream2/"))
|
||
return NULL;
|
||
|
||
const char *rest = ref + strlen ("appstream2/");
|
||
const char *dash = strrchr (rest, '-');
|
||
if (dash == NULL)
|
||
return NULL;
|
||
|
||
return g_strndup (rest, dash - rest);
|
||
}
|
||
|
||
char *
|
||
flatpak_get_arch_for_ref (const char *ref)
|
||
{
|
||
if (g_str_has_prefix (ref, "appstream/") ||
|
||
g_str_has_prefix (ref, "appstream2/"))
|
||
{
|
||
const char *rest = strchr (ref, '/') + 1; /* Guaranteed to exist per above check */
|
||
const char *dash = strrchr (rest, '-'); /* Subset appstream refs are appstream2/$subset-$arch */
|
||
if (dash != NULL)
|
||
rest = dash + 1;
|
||
return g_strdup (rest);
|
||
}
|
||
else if (g_str_has_prefix (ref, "app/") ||
|
||
g_str_has_prefix (ref, "runtime/"))
|
||
{
|
||
const char *slash;
|
||
const char *arch;
|
||
|
||
slash = strchr (ref, '/') + 1; /* Guaranteed to exist per above check */
|
||
slash = strchr (slash, '/'); /* Skip id */
|
||
if (slash == NULL)
|
||
return NULL;
|
||
arch = slash + 1;
|
||
|
||
slash = strchr (arch, '/'); /* skip to end arch */
|
||
if (slash == NULL)
|
||
return NULL;
|
||
|
||
return g_strndup (arch, slash - arch);
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
typedef enum {
|
||
DIFF_OP_KIND_RESUSE_OLD,
|
||
DIFF_OP_KIND_SKIP_OLD,
|
||
DIFF_OP_KIND_DATA,
|
||
} DiffOpKind;
|
||
|
||
typedef struct {
|
||
DiffOpKind kind;
|
||
gsize size;
|
||
} DiffOp;
|
||
|
||
typedef struct {
|
||
const guchar *old_data;
|
||
const guchar *new_data;
|
||
|
||
GArray *ops;
|
||
GArray *data;
|
||
|
||
gsize last_old_offset;
|
||
gsize last_new_offset;
|
||
} DiffData;
|
||
|
||
static gsize
|
||
match_bytes_at_start (const guchar *data1,
|
||
gsize data1_len,
|
||
const guchar *data2,
|
||
gsize data2_len)
|
||
{
|
||
gsize len = 0;
|
||
gsize max_len = MIN (data1_len, data2_len);
|
||
|
||
while (len < max_len)
|
||
{
|
||
if (*data1 != *data2)
|
||
break;
|
||
data1++;
|
||
data2++;
|
||
len++;
|
||
}
|
||
return len;
|
||
}
|
||
|
||
static gsize
|
||
match_bytes_at_end (const guchar *data1,
|
||
gsize data1_len,
|
||
const guchar *data2,
|
||
gsize data2_len)
|
||
{
|
||
gsize len = 0;
|
||
gsize max_len = MIN (data1_len, data2_len);
|
||
|
||
data1 += data1_len - 1;
|
||
data2 += data2_len - 1;
|
||
|
||
while (len < max_len)
|
||
{
|
||
if (*data1 != *data2)
|
||
break;
|
||
data1--;
|
||
data2--;
|
||
len++;
|
||
}
|
||
return len;
|
||
}
|
||
|
||
static DiffOp *
|
||
diff_ensure_op (DiffData *data,
|
||
DiffOpKind kind)
|
||
{
|
||
if (data->ops->len == 0 ||
|
||
g_array_index (data->ops, DiffOp, data->ops->len-1).kind != kind)
|
||
{
|
||
DiffOp op = {kind, 0};
|
||
g_array_append_val (data->ops, op);
|
||
}
|
||
|
||
return &g_array_index (data->ops, DiffOp, data->ops->len-1);
|
||
}
|
||
|
||
static void
|
||
diff_emit_reuse (DiffData *data,
|
||
gsize size)
|
||
{
|
||
DiffOp *op;
|
||
|
||
if (size == 0)
|
||
return;
|
||
|
||
op = diff_ensure_op (data, DIFF_OP_KIND_RESUSE_OLD);
|
||
op->size += size;
|
||
}
|
||
|
||
static void
|
||
diff_emit_skip (DiffData *data,
|
||
gsize size)
|
||
{
|
||
DiffOp *op;
|
||
|
||
if (size == 0)
|
||
return;
|
||
|
||
op = diff_ensure_op (data, DIFF_OP_KIND_SKIP_OLD);
|
||
op->size += size;
|
||
}
|
||
|
||
static void
|
||
diff_emit_data (DiffData *data,
|
||
gsize size,
|
||
const guchar *new_data)
|
||
{
|
||
DiffOp *op;
|
||
|
||
if (size == 0)
|
||
return;
|
||
|
||
op = diff_ensure_op (data, DIFF_OP_KIND_DATA);
|
||
op->size += size;
|
||
|
||
g_array_append_vals (data->data, new_data, size);
|
||
}
|
||
|
||
static GBytes *
|
||
diff_encode (DiffData *data, GError **error)
|
||
{
|
||
g_autoptr(GOutputStream) mem = g_memory_output_stream_new_resizable ();
|
||
g_autoptr(GDataOutputStream) out = g_data_output_stream_new (mem);
|
||
gsize ops_count = 0;
|
||
|
||
g_data_output_stream_set_byte_order (out, G_DATA_STREAM_BYTE_ORDER_LITTLE_ENDIAN);
|
||
|
||
/* Header */
|
||
if (!g_output_stream_write_all (G_OUTPUT_STREAM (out),
|
||
FLATPAK_SUMMARY_DIFF_HEADER, 4,
|
||
NULL, NULL, error))
|
||
return NULL;
|
||
|
||
/* Write the ops count placeholder */
|
||
if (!g_data_output_stream_put_uint32 (out, 0, NULL, error))
|
||
return NULL;
|
||
|
||
for (gsize i = 0; i < data->ops->len; i++)
|
||
{
|
||
DiffOp *op = &g_array_index (data->ops, DiffOp, i);
|
||
gsize size = op->size;
|
||
|
||
while (size > 0)
|
||
{
|
||
/* We leave a nibble at the top for the op */
|
||
guint32 opdata = (guint64)size & 0x0fffffff;
|
||
size -= opdata;
|
||
|
||
opdata = opdata | ((0xf & op->kind) << 28);
|
||
|
||
if (!g_data_output_stream_put_uint32 (out, opdata, NULL, error))
|
||
return NULL;
|
||
ops_count++;
|
||
}
|
||
}
|
||
|
||
/* Then add the data */
|
||
if (data->data->len > 0 &&
|
||
!g_output_stream_write_all (G_OUTPUT_STREAM (out),
|
||
data->data->data, data->data->len,
|
||
NULL, NULL, error))
|
||
return NULL;
|
||
|
||
/* Back-patch in the ops count */
|
||
if (!g_seekable_seek (G_SEEKABLE(out), 4, G_SEEK_SET, NULL, error))
|
||
return NULL;
|
||
|
||
if (!g_data_output_stream_put_uint32 (out, ops_count, NULL, error))
|
||
return NULL;
|
||
|
||
if (!g_output_stream_close (G_OUTPUT_STREAM (out), NULL, error))
|
||
return NULL;
|
||
|
||
return g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (mem));
|
||
}
|
||
|
||
static void
|
||
diff_consume_block2 (DiffData *data,
|
||
gsize consume_old_offset,
|
||
gsize consume_old_size,
|
||
gsize produce_new_offset,
|
||
gsize produce_new_size)
|
||
{
|
||
/* We consumed $consume_old_size bytes from $consume_old_offset to
|
||
produce $produce_new_size bytes at $produce_new_size */
|
||
|
||
/* First we copy old data for any matching prefix of the block */
|
||
|
||
gsize prefix_len = match_bytes_at_start (data->old_data + consume_old_offset, consume_old_size,
|
||
data->new_data + produce_new_offset, produce_new_size);
|
||
diff_emit_reuse (data, prefix_len);
|
||
|
||
consume_old_size -= prefix_len;
|
||
consume_old_offset += prefix_len;
|
||
|
||
produce_new_size -= prefix_len;
|
||
produce_new_offset += prefix_len;
|
||
|
||
/* Then we find the matching suffix for the rest */
|
||
gsize suffix_len = match_bytes_at_end (data->old_data + consume_old_offset, consume_old_size,
|
||
data->new_data + produce_new_offset, produce_new_size);
|
||
|
||
/* Skip source data until suffix match */
|
||
diff_emit_skip (data, consume_old_size - suffix_len);
|
||
|
||
/* Copy new data until suffix match */
|
||
diff_emit_data (data, produce_new_size - suffix_len, data->new_data + produce_new_offset);
|
||
|
||
diff_emit_reuse (data, suffix_len);
|
||
}
|
||
|
||
static void
|
||
diff_consume_block (DiffData *data,
|
||
gssize consume_old_offset,
|
||
gsize consume_old_size,
|
||
gssize produce_new_offset,
|
||
gsize produce_new_size)
|
||
{
|
||
if (consume_old_offset == -1)
|
||
consume_old_offset = data->last_old_offset;
|
||
if (produce_new_offset == -1)
|
||
produce_new_offset = data->last_new_offset;
|
||
|
||
/* We consumed $consume_old_size bytes from $consume_old_offset to
|
||
* produce $produce_new_size bytes at $produce_new_size, however
|
||
* while the emitted blocks are in order they may not cover the
|
||
* every byte, so we emit the inbetwen blocks separately. */
|
||
|
||
if (consume_old_offset != data->last_old_offset ||
|
||
produce_new_offset != data->last_new_offset)
|
||
diff_consume_block2 (data,
|
||
data->last_old_offset, consume_old_offset - data->last_old_offset ,
|
||
data->last_new_offset, produce_new_offset - data->last_new_offset);
|
||
|
||
diff_consume_block2 (data,
|
||
consume_old_offset, consume_old_size,
|
||
produce_new_offset, produce_new_size);
|
||
|
||
data->last_old_offset = consume_old_offset + consume_old_size;
|
||
data->last_new_offset = produce_new_offset + produce_new_size;
|
||
}
|
||
|
||
GBytes *
|
||
flatpak_summary_apply_diff (GBytes *old,
|
||
GBytes *diff,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GBytes) uncompressed = NULL;
|
||
const guchar *diffdata;
|
||
gsize diff_size;
|
||
guint32 *ops;
|
||
guint32 n_ops;
|
||
gsize data_offset;
|
||
gsize data_size;
|
||
const guchar *data;
|
||
const guchar *old_data = g_bytes_get_data (old, NULL);
|
||
gsize old_size = g_bytes_get_size (old);
|
||
g_autoptr(GByteArray) res = g_byte_array_new ();
|
||
|
||
uncompressed = flatpak_zlib_decompress_bytes (diff, error);
|
||
if (uncompressed == NULL)
|
||
{
|
||
g_prefix_error (error, "Invalid summary diff: ");
|
||
return NULL;
|
||
}
|
||
|
||
diffdata = g_bytes_get_data (uncompressed, NULL);
|
||
diff_size = g_bytes_get_size (uncompressed);
|
||
|
||
if (diff_size < 8 ||
|
||
memcmp (diffdata, FLATPAK_SUMMARY_DIFF_HEADER, 4) != 0)
|
||
{
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
|
||
n_ops = GUINT32_FROM_LE (*(guint32 *)(diffdata+4));
|
||
ops = (guint32 *)(diffdata+8);
|
||
|
||
data_offset = 4 + 4 + 4 * n_ops;
|
||
|
||
/* All ops must fit in diff, and avoid wrapping the multiply */
|
||
if (data_offset > diff_size ||
|
||
(data_offset - 4 - 4) / 4 != n_ops)
|
||
{
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
|
||
data = diffdata + data_offset;
|
||
data_size = diff_size - data_offset;
|
||
|
||
for (gsize i = 0; i < n_ops; i++)
|
||
{
|
||
guint32 opdata = GUINT32_FROM_LE (ops[i]);
|
||
guint32 kind = (opdata & 0xf0000000) >> 28;
|
||
guint32 size = opdata & 0x0fffffff;
|
||
|
||
switch (kind)
|
||
{
|
||
case DIFF_OP_KIND_RESUSE_OLD:
|
||
if (size > old_size)
|
||
{
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
g_byte_array_append (res, old_data, size);
|
||
old_data += size;
|
||
old_size -= size;
|
||
break;
|
||
case DIFF_OP_KIND_SKIP_OLD:
|
||
if (size > old_size)
|
||
{
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
old_data += size;
|
||
old_size -= size;
|
||
break;
|
||
case DIFF_OP_KIND_DATA:
|
||
if (size > data_size)
|
||
{
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
g_byte_array_append (res, data, size);
|
||
data += size;
|
||
data_size -= size;
|
||
break;
|
||
default:
|
||
flatpak_fail (error, "Invalid summary diff");
|
||
return NULL;
|
||
}
|
||
}
|
||
|
||
return g_byte_array_free_to_bytes (g_steal_pointer (&res));
|
||
}
|
||
|
||
|
||
static GBytes *
|
||
flatpak_summary_generate_diff (GVariant *old_v,
|
||
GVariant *new_v,
|
||
GError **error)
|
||
{
|
||
VarSummaryRef new, old;
|
||
VarRefMapRef new_refs, old_refs;
|
||
VarRefMapEntryRef new_entry, old_entry;
|
||
gsize new_len, old_len;
|
||
int new_i, old_i;
|
||
const char *old_ref, *new_ref;
|
||
g_autoptr(GArray) ops = g_array_new (FALSE, TRUE, sizeof (DiffOp));
|
||
g_autoptr(GArray) data_bytes = g_array_new (FALSE, TRUE, 1);
|
||
g_autoptr(GBytes) diff_uncompressed = NULL;
|
||
g_autoptr(GBytes) diff_compressed = NULL;
|
||
DiffData data = {
|
||
g_variant_get_data (old_v),
|
||
g_variant_get_data (new_v),
|
||
ops,
|
||
data_bytes,
|
||
};
|
||
|
||
new = var_summary_from_gvariant (new_v);
|
||
old = var_summary_from_gvariant (old_v);
|
||
|
||
new_refs = var_summary_get_ref_map (new);
|
||
old_refs = var_summary_get_ref_map (old);
|
||
|
||
new_len = var_ref_map_get_length (new_refs);
|
||
old_len = var_ref_map_get_length (old_refs);
|
||
|
||
new_i = old_i = 0;
|
||
while (new_i < new_len && old_i < old_len)
|
||
{
|
||
if (new_i == new_len)
|
||
{
|
||
/* Just old left */
|
||
old_entry = var_ref_map_get_at (old_refs, old_i);
|
||
old_ref = var_ref_map_entry_get_ref (old_entry);
|
||
old_i++;
|
||
diff_consume_block (&data,
|
||
-1, 0,
|
||
(const guchar *)new_entry.base - (const guchar *)new.base, new_entry.size);
|
||
}
|
||
else if (old_i == old_len)
|
||
{
|
||
/* Just new left */
|
||
new_entry = var_ref_map_get_at (new_refs, new_i);
|
||
new_ref = var_ref_map_entry_get_ref (new_entry);
|
||
diff_consume_block (&data,
|
||
(const guchar *)old_entry.base - (const guchar *)old.base, old_entry.size,
|
||
-1, 0);
|
||
|
||
new_i++;
|
||
}
|
||
else
|
||
{
|
||
new_entry = var_ref_map_get_at (new_refs, new_i);
|
||
new_ref = var_ref_map_entry_get_ref (new_entry);
|
||
|
||
old_entry = var_ref_map_get_at (old_refs, old_i);
|
||
old_ref = var_ref_map_entry_get_ref (old_entry);
|
||
|
||
int cmp = strcmp (new_ref, old_ref);
|
||
if (cmp == 0)
|
||
{
|
||
/* same ref */
|
||
diff_consume_block (&data,
|
||
(const guchar *)old_entry.base - (const guchar *)old.base, old_entry.size,
|
||
(const guchar *)new_entry.base - (const guchar *)new.base, new_entry.size);
|
||
old_i++;
|
||
new_i++;
|
||
}
|
||
else if (cmp < 0)
|
||
{
|
||
/* new added */
|
||
diff_consume_block (&data,
|
||
-1, 0,
|
||
(const guchar *)new_entry.base - (const guchar *)new.base, new_entry.size);
|
||
new_i++;
|
||
}
|
||
else
|
||
{
|
||
/* old removed */
|
||
diff_consume_block (&data,
|
||
(const guchar *)old_entry.base - (const guchar *)old.base, old_entry.size,
|
||
-1, 0);
|
||
old_i++;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Flush till the end */
|
||
diff_consume_block2 (&data,
|
||
data.last_old_offset, old.size - data.last_old_offset,
|
||
data.last_new_offset, new.size - data.last_new_offset);
|
||
|
||
diff_uncompressed = diff_encode (&data, error);
|
||
if (diff_uncompressed == NULL)
|
||
return NULL;
|
||
|
||
diff_compressed = flatpak_zlib_compress_bytes (diff_uncompressed, 9, error);
|
||
if (diff_compressed == NULL)
|
||
return NULL;
|
||
|
||
#ifdef VALIDATE_DIFF
|
||
{
|
||
g_autoptr(GError) apply_error = NULL;
|
||
g_autoptr(GBytes) old_bytes = g_variant_get_data_as_bytes (old_v);
|
||
g_autoptr(GBytes) new_bytes = g_variant_get_data_as_bytes (new_v);
|
||
g_autoptr(GBytes) applied = flatpak_summary_apply_diff (old_bytes, diff_compressed, &apply_error);
|
||
g_assert (applied != NULL);
|
||
g_assert (g_bytes_equal (applied, new_bytes));
|
||
}
|
||
#endif
|
||
|
||
return g_steal_pointer (&diff_compressed);
|
||
}
|
||
|
||
static void
|
||
variant_dict_merge (GVariantDict *dict,
|
||
GVariant *to_merge)
|
||
{
|
||
GVariantIter iter;
|
||
gchar *key;
|
||
GVariant *value;
|
||
|
||
if (to_merge)
|
||
{
|
||
g_variant_iter_init (&iter, to_merge);
|
||
while (g_variant_iter_next (&iter, "{sv}", &key, &value))
|
||
{
|
||
g_variant_dict_insert_value (dict, key, value);
|
||
g_variant_unref (value);
|
||
g_free (key);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void
|
||
add_summary_metadata (OstreeRepo *repo,
|
||
GVariantBuilder *metadata_builder)
|
||
{
|
||
GKeyFile *config;
|
||
g_autofree char *title = NULL;
|
||
g_autofree char *comment = NULL;
|
||
g_autofree char *description = NULL;
|
||
g_autofree char *homepage = NULL;
|
||
g_autofree char *icon = NULL;
|
||
g_autofree char *redirect_url = NULL;
|
||
g_autofree char *default_branch = NULL;
|
||
g_autofree char *remote_mode_str = NULL;
|
||
g_autofree char *authenticator_name = NULL;
|
||
g_autofree char *gpg_keys = NULL;
|
||
g_auto(GStrv) config_keys = NULL;
|
||
int authenticator_install = -1;
|
||
const char *collection_id;
|
||
gboolean deploy_collection_id = FALSE;
|
||
gboolean deploy_sideload_collection_id = FALSE;
|
||
gboolean tombstone_commits = FALSE;
|
||
|
||
config = ostree_repo_get_config (repo);
|
||
|
||
if (config)
|
||
{
|
||
remote_mode_str = g_key_file_get_string (config, "core", "mode", NULL);
|
||
tombstone_commits = g_key_file_get_boolean (config, "core", "tombstone-commits", NULL);
|
||
|
||
title = g_key_file_get_string (config, "flatpak", "title", NULL);
|
||
comment = g_key_file_get_string (config, "flatpak", "comment", NULL);
|
||
description = g_key_file_get_string (config, "flatpak", "description", NULL);
|
||
homepage = g_key_file_get_string (config, "flatpak", "homepage", NULL);
|
||
icon = g_key_file_get_string (config, "flatpak", "icon", NULL);
|
||
default_branch = g_key_file_get_string (config, "flatpak", "default-branch", NULL);
|
||
gpg_keys = g_key_file_get_string (config, "flatpak", "gpg-keys", NULL);
|
||
redirect_url = g_key_file_get_string (config, "flatpak", "redirect-url", NULL);
|
||
deploy_sideload_collection_id = g_key_file_get_boolean (config, "flatpak", "deploy-sideload-collection-id", NULL);
|
||
deploy_collection_id = g_key_file_get_boolean (config, "flatpak", "deploy-collection-id", NULL);
|
||
authenticator_name = g_key_file_get_string (config, "flatpak", "authenticator-name", NULL);
|
||
if (g_key_file_has_key (config, "flatpak", "authenticator-install", NULL))
|
||
authenticator_install = g_key_file_get_boolean (config, "flatpak", "authenticator-install", NULL);
|
||
|
||
config_keys = g_key_file_get_keys (config, "flatpak", NULL, NULL);
|
||
}
|
||
|
||
collection_id = ostree_repo_get_collection_id (repo);
|
||
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.summary.mode",
|
||
g_variant_new_string (remote_mode_str ? remote_mode_str : "bare"));
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.summary.tombstone-commits",
|
||
g_variant_new_boolean (tombstone_commits));
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.summary.indexed-deltas",
|
||
g_variant_new_boolean (TRUE));
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.summary.last-modified",
|
||
g_variant_new_uint64 (GUINT64_TO_BE (g_get_real_time () / G_USEC_PER_SEC)));
|
||
|
||
if (collection_id)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.summary.collection-id",
|
||
g_variant_new_string (collection_id));
|
||
|
||
if (title)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.title",
|
||
g_variant_new_string (title));
|
||
|
||
if (comment)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.comment",
|
||
g_variant_new_string (comment));
|
||
|
||
if (description)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.description",
|
||
g_variant_new_string (description));
|
||
|
||
if (homepage)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.homepage",
|
||
g_variant_new_string (homepage));
|
||
|
||
if (icon)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.icon",
|
||
g_variant_new_string (icon));
|
||
|
||
if (redirect_url)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.redirect-url",
|
||
g_variant_new_string (redirect_url));
|
||
|
||
if (default_branch)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.default-branch",
|
||
g_variant_new_string (default_branch));
|
||
|
||
if (deploy_collection_id && collection_id != NULL)
|
||
g_variant_builder_add (metadata_builder, "{sv}", OSTREE_META_KEY_DEPLOY_COLLECTION_ID,
|
||
g_variant_new_string (collection_id));
|
||
else if (deploy_sideload_collection_id && collection_id != NULL)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.deploy-collection-id",
|
||
g_variant_new_string (collection_id));
|
||
else if (deploy_collection_id)
|
||
g_info ("Ignoring deploy-collection-id=true because no collection ID is set.");
|
||
|
||
if (authenticator_name)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.authenticator-name",
|
||
g_variant_new_string (authenticator_name));
|
||
|
||
if (authenticator_install != -1)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.authenticator-install",
|
||
g_variant_new_boolean (authenticator_install));
|
||
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.cache-version",
|
||
g_variant_new_uint32 (GUINT32_TO_LE (FLATPAK_XA_CACHE_VERSION)));
|
||
|
||
if (config_keys != NULL)
|
||
{
|
||
for (int i = 0; config_keys[i] != NULL; i++)
|
||
{
|
||
const char *key = config_keys[i];
|
||
g_autofree char *xa_key = NULL;
|
||
g_autofree char *value = NULL;
|
||
|
||
if (!g_str_has_prefix (key, "authenticator-options."))
|
||
continue;
|
||
|
||
value = g_key_file_get_string (config, "flatpak", key, NULL);
|
||
if (value == NULL)
|
||
continue;
|
||
|
||
xa_key = g_strconcat ("xa.", key, NULL);
|
||
g_variant_builder_add (metadata_builder, "{sv}", xa_key,
|
||
g_variant_new_string (value));
|
||
}
|
||
}
|
||
|
||
if (gpg_keys)
|
||
{
|
||
guchar *decoded;
|
||
gsize decoded_len;
|
||
|
||
gpg_keys = g_strstrip (gpg_keys);
|
||
decoded = g_base64_decode (gpg_keys, &decoded_len);
|
||
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.gpg-keys",
|
||
g_variant_new_from_data (G_VARIANT_TYPE ("ay"), decoded, decoded_len,
|
||
TRUE, (GDestroyNotify) g_free, decoded));
|
||
}
|
||
}
|
||
|
||
static GVariant *
|
||
generate_summary (OstreeRepo *repo,
|
||
gboolean compat_format,
|
||
GHashTable *refs,
|
||
GHashTable *commit_data_cache,
|
||
GPtrArray *delta_names,
|
||
const char *subset,
|
||
const char **summary_arches,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
|
||
g_autoptr(GVariantBuilder) ref_data_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{s(tts)}"));
|
||
g_autoptr(GVariantBuilder) ref_sparse_data_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sa{sv}}"));
|
||
g_autoptr(GVariantBuilder) refs_builder = g_variant_builder_new (G_VARIANT_TYPE ("a(s(taya{sv}))"));
|
||
g_autoptr(GVariantBuilder) summary_builder = g_variant_builder_new (OSTREE_SUMMARY_GVARIANT_FORMAT);
|
||
g_autoptr(GHashTable) summary_arches_ht = NULL;
|
||
g_autoptr(GHashTable) commits = NULL;
|
||
g_autoptr(GList) ordered_keys = NULL;
|
||
GList *l = NULL;
|
||
|
||
/* In the new format this goes in the summary index instead */
|
||
if (compat_format)
|
||
add_summary_metadata (repo, metadata_builder);
|
||
|
||
ordered_keys = g_hash_table_get_keys (refs);
|
||
ordered_keys = g_list_sort (ordered_keys, (GCompareFunc) strcmp);
|
||
|
||
if (summary_arches)
|
||
{
|
||
summary_arches_ht = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);
|
||
for (int i = 0; summary_arches[i] != NULL; i++)
|
||
{
|
||
const char *arch = summary_arches[i];
|
||
|
||
g_hash_table_add (summary_arches_ht, (char *)arch);
|
||
}
|
||
}
|
||
|
||
/* Compute which commits to keep */
|
||
commits = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); /* strings owned by ref */
|
||
for (l = ordered_keys; l; l = l->next)
|
||
{
|
||
const char *ref = l->data;
|
||
const char *rev = g_hash_table_lookup (refs, ref);
|
||
g_autofree char *arch = NULL;
|
||
const CommitData *rev_data = NULL;
|
||
|
||
if (summary_arches)
|
||
{
|
||
/* NOTE: Non-arched (unknown) refs get into all summary versions */
|
||
arch = flatpak_get_arch_for_ref (ref);
|
||
if (arch != NULL && !g_hash_table_contains (summary_arches_ht, arch))
|
||
continue; /* Filter this ref by arch */
|
||
}
|
||
|
||
rev_data = g_hash_table_lookup (commit_data_cache, rev);
|
||
if (*subset != 0)
|
||
{
|
||
/* Subset summaries keep the appstream2/$subset-$arch, and have no appstream/ compat branch */
|
||
|
||
if (g_str_has_prefix (ref, "appstream/"))
|
||
{
|
||
continue; /* No compat branch in subsets */
|
||
}
|
||
else if (g_str_has_prefix (ref, "appstream2/"))
|
||
{
|
||
g_autofree char *ref_subset = appstream_ref_get_subset (ref);
|
||
if (ref_subset == NULL)
|
||
continue; /* Non-subset, ignore */
|
||
|
||
if (strcmp (subset, ref_subset) != 0)
|
||
continue; /* Different subset, ignore */
|
||
|
||
/* Otherwise, keep */
|
||
}
|
||
else if (rev_data)
|
||
{
|
||
if (rev_data->subsets == NULL ||
|
||
!flatpak_g_ptr_array_contains_string (rev_data->subsets, subset))
|
||
continue; /* Ref is not in this subset */
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* non-subset, keep everything but subset appstream refs */
|
||
|
||
g_autofree char *ref_subset = appstream_ref_get_subset (ref);
|
||
if (ref_subset != NULL)
|
||
continue; /* Subset appstream ref, ignore */
|
||
}
|
||
|
||
g_hash_table_add (commits, (char *)rev);
|
||
}
|
||
|
||
/* Create refs list, metadata and sparse_data */
|
||
for (l = ordered_keys; l; l = l->next)
|
||
{
|
||
const char *ref = l->data;
|
||
const char *rev = g_hash_table_lookup (refs, ref);
|
||
const CommitData *rev_data = NULL;
|
||
g_auto(GVariantDict) commit_metadata_builder = FLATPAK_VARIANT_BUILDER_INITIALIZER;
|
||
guint64 commit_size;
|
||
guint64 commit_timestamp;
|
||
|
||
if (!g_hash_table_contains (commits, rev))
|
||
continue; /* Filter out commit (by arch & subset) */
|
||
|
||
if (flatpak_is_app_runtime_or_appstream_ref (ref))
|
||
rev_data = g_hash_table_lookup (commit_data_cache, rev);
|
||
|
||
if (rev_data != NULL)
|
||
{
|
||
commit_size = rev_data->commit_size;
|
||
commit_timestamp = rev_data->commit_timestamp;
|
||
}
|
||
else
|
||
{
|
||
g_autoptr(GVariant) commit_obj = NULL;
|
||
if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, rev, &commit_obj, error))
|
||
return NULL;
|
||
commit_size = g_variant_get_size (commit_obj);
|
||
commit_timestamp = ostree_commit_get_timestamp (commit_obj);
|
||
}
|
||
|
||
g_variant_dict_init (&commit_metadata_builder, NULL);
|
||
if (!compat_format && rev_data)
|
||
{
|
||
g_variant_dict_insert (&commit_metadata_builder, "xa.data", "(tts)",
|
||
GUINT64_TO_BE (rev_data->installed_size),
|
||
GUINT64_TO_BE (rev_data->download_size),
|
||
rev_data->metadata_contents);
|
||
variant_dict_merge (&commit_metadata_builder, rev_data->sparse_data);
|
||
}
|
||
|
||
/* For the new format summary we use a shorter name for the timestamp to save space */
|
||
g_variant_dict_insert_value (&commit_metadata_builder,
|
||
compat_format ? OSTREE_COMMIT_TIMESTAMP : OSTREE_COMMIT_TIMESTAMP2,
|
||
g_variant_new_uint64 (GUINT64_TO_BE (commit_timestamp)));
|
||
|
||
g_variant_builder_add_value (refs_builder,
|
||
g_variant_new ("(s(t@ay@a{sv}))", ref,
|
||
commit_size,
|
||
ostree_checksum_to_bytes_v (rev),
|
||
g_variant_dict_end (&commit_metadata_builder)));
|
||
|
||
if (compat_format && rev_data)
|
||
{
|
||
g_variant_builder_add (ref_data_builder, "{s(tts)}",
|
||
ref,
|
||
GUINT64_TO_BE (rev_data->installed_size),
|
||
GUINT64_TO_BE (rev_data->download_size),
|
||
rev_data->metadata_contents);
|
||
if (rev_data->sparse_data)
|
||
g_variant_builder_add (ref_sparse_data_builder, "{s@a{sv}}",
|
||
ref, rev_data->sparse_data);
|
||
}
|
||
}
|
||
|
||
if (delta_names)
|
||
{
|
||
g_auto(GVariantDict) deltas_builder = FLATPAK_VARIANT_BUILDER_INITIALIZER;
|
||
|
||
g_variant_dict_init (&deltas_builder, NULL);
|
||
for (guint i = 0; i < delta_names->len; i++)
|
||
{
|
||
g_autofree char *from = NULL;
|
||
g_autofree char *to = NULL;
|
||
GVariant *digest;
|
||
|
||
_ostree_parse_delta_name (delta_names->pdata[i], &from, &to);
|
||
|
||
/* Only keep deltas going to a ref that is in the summary
|
||
* (i.e. not arch filtered or random) */
|
||
if (!g_hash_table_contains (commits, to))
|
||
continue;
|
||
|
||
digest = _ostree_repo_static_delta_superblock_digest (repo,
|
||
(from && from[0]) ? from : NULL,
|
||
to, cancellable, error);
|
||
if (digest == NULL)
|
||
return FALSE;
|
||
|
||
g_variant_dict_insert_value (&deltas_builder, delta_names->pdata[i], digest);
|
||
}
|
||
|
||
if (delta_names->len > 0)
|
||
g_variant_builder_add (metadata_builder, "{sv}", "ostree.static-deltas", g_variant_dict_end (&deltas_builder));
|
||
}
|
||
|
||
if (compat_format)
|
||
{
|
||
/* Note: xa.cache doesn’t need to support collection IDs for the refs listed
|
||
* in it, because the xa.cache metadata is stored on the ostree-metadata ref,
|
||
* which is itself strongly bound to a collection ID — so that collection ID
|
||
* is bound to all the refs in xa.cache. If a client is using the xa.cache
|
||
* data from a summary file (rather than an ostree-metadata branch), they are
|
||
* too old to care about collection IDs anyway. */
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.cache",
|
||
g_variant_new_variant (g_variant_builder_end (ref_data_builder)));
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.sparse-cache",
|
||
g_variant_builder_end (ref_sparse_data_builder));
|
||
}
|
||
else
|
||
{
|
||
g_variant_builder_add (metadata_builder, "{sv}", "xa.summary-version",
|
||
g_variant_new_uint32 (GUINT32_TO_LE (FLATPAK_XA_SUMMARY_VERSION)));
|
||
}
|
||
|
||
g_variant_builder_add_value (summary_builder, g_variant_builder_end (refs_builder));
|
||
g_variant_builder_add_value (summary_builder, g_variant_builder_end (metadata_builder));
|
||
|
||
return g_variant_ref_sink (g_variant_builder_end (summary_builder));
|
||
}
|
||
|
||
static GVariant *
|
||
read_digested_summary (OstreeRepo *repo,
|
||
const char *digest,
|
||
GHashTable *digested_summary_cache,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
GVariant *cached;
|
||
g_autoptr(GVariant) loaded = NULL;
|
||
|
||
cached = g_hash_table_lookup (digested_summary_cache, digest);
|
||
if (cached)
|
||
return g_variant_ref (cached);
|
||
|
||
loaded = flatpak_repo_load_digested_summary (repo, digest, error);
|
||
if (loaded == NULL)
|
||
return NULL;
|
||
|
||
g_hash_table_insert (digested_summary_cache, g_strdup (digest), g_variant_ref (loaded));
|
||
|
||
return g_steal_pointer (&loaded);
|
||
}
|
||
|
||
static gboolean
|
||
add_to_history (OstreeRepo *repo,
|
||
GVariantBuilder *history_builder,
|
||
VarChecksumRef old_digest_vv,
|
||
GVariant *current_digest_v,
|
||
GVariant *current_content,
|
||
GHashTable *digested_summary_cache,
|
||
guint *history_len,
|
||
guint max_history_length,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariant) old_digest_v = g_variant_ref_sink (var_checksum_dup_to_gvariant (old_digest_vv));
|
||
g_autofree char *old_digest = NULL;
|
||
g_autoptr(GVariant) old_content = NULL;
|
||
g_autofree char *current_digest = NULL;
|
||
g_autoptr(GBytes) subsummary_diff = NULL;
|
||
|
||
/* Limit history length */
|
||
if (*history_len >= max_history_length)
|
||
return TRUE;
|
||
|
||
/* Avoid repeats in the history (in case nothing changed in subsummary) */
|
||
if (g_variant_equal (old_digest_v, current_digest_v))
|
||
return TRUE;
|
||
|
||
old_digest = ostree_checksum_from_bytes_v (old_digest_v);
|
||
old_content = read_digested_summary (repo, old_digest, digested_summary_cache, cancellable, NULL);
|
||
if (old_content == NULL)
|
||
return TRUE; /* Only add parents that still exist */
|
||
|
||
subsummary_diff = flatpak_summary_generate_diff (old_content, current_content, error);
|
||
if (subsummary_diff == NULL)
|
||
return FALSE;
|
||
|
||
current_digest = ostree_checksum_from_bytes_v (current_digest_v);
|
||
|
||
if (!flatpak_repo_save_digested_summary_delta (repo, old_digest, current_digest,
|
||
subsummary_diff, cancellable, error))
|
||
return FALSE;
|
||
|
||
*history_len += 1;
|
||
g_variant_builder_add_value (history_builder, old_digest_v);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static GVariant *
|
||
generate_summary_index (OstreeRepo *repo,
|
||
GVariant *old_index_v,
|
||
GHashTable *summaries,
|
||
GHashTable *digested_summaries,
|
||
GHashTable *digested_summary_cache,
|
||
const char **gpg_key_ids,
|
||
const char *gpg_homedir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
|
||
g_autoptr(GVariantBuilder) subsummary_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{s(ayaaya{sv})}"));
|
||
g_autoptr(GVariantBuilder) index_builder = g_variant_builder_new (FLATPAK_SUMMARY_INDEX_GVARIANT_FORMAT);
|
||
g_autoptr(GVariant) index = NULL;
|
||
g_autoptr(GList) ordered_summaries = NULL;
|
||
guint max_history_length = flatpak_repo_get_summary_history_length (repo);
|
||
GList *l;
|
||
|
||
add_summary_metadata (repo, metadata_builder);
|
||
|
||
ordered_summaries = g_hash_table_get_keys (summaries);
|
||
ordered_summaries = g_list_sort (ordered_summaries, (GCompareFunc) strcmp);
|
||
for (l = ordered_summaries; l; l = l->next)
|
||
{
|
||
g_auto(GVariantDict) subsummary_metadata_builder = FLATPAK_VARIANT_BUILDER_INITIALIZER;
|
||
const char *subsummary = l->data;
|
||
const char *digest = g_hash_table_lookup (summaries, subsummary);
|
||
g_autoptr(GVariant) digest_v = g_variant_ref_sink (ostree_checksum_to_bytes_v (digest));
|
||
g_autoptr(GVariantBuilder) history_builder = g_variant_builder_new (G_VARIANT_TYPE ("aay"));
|
||
g_autoptr(GVariant) subsummary_content = NULL;
|
||
|
||
subsummary_content = read_digested_summary (repo, digest, digested_summary_cache, cancellable, error);
|
||
if (subsummary_content == NULL)
|
||
return NULL; /* This really should always be there as we're supposed to index it */
|
||
|
||
if (old_index_v)
|
||
{
|
||
VarSummaryIndexRef old_index = var_summary_index_from_gvariant (old_index_v);
|
||
VarSummaryIndexSubsummariesRef old_subsummaries = var_summary_index_get_subsummaries (old_index);
|
||
VarSubsummaryRef old_subsummary;
|
||
guint history_len = 0;
|
||
|
||
if (var_summary_index_subsummaries_lookup (old_subsummaries, subsummary, NULL, &old_subsummary))
|
||
{
|
||
VarChecksumRef parent = var_subsummary_get_checksum (old_subsummary);
|
||
|
||
/* Add current as first in history */
|
||
if (!add_to_history (repo, history_builder, parent, digest_v, subsummary_content, digested_summary_cache,
|
||
&history_len, max_history_length, cancellable, error))
|
||
return FALSE;
|
||
|
||
/* Add previous history */
|
||
VarArrayofChecksumRef history = var_subsummary_get_history (old_subsummary);
|
||
gsize len = var_arrayof_checksum_get_length (history);
|
||
for (gsize i = 0; i < len; i++)
|
||
{
|
||
VarChecksumRef c = var_arrayof_checksum_get_at (history, i);
|
||
if (!add_to_history (repo, history_builder, c, digest_v, subsummary_content, digested_summary_cache,
|
||
&history_len, max_history_length, cancellable, error))
|
||
return FALSE;
|
||
}
|
||
}
|
||
}
|
||
|
||
g_variant_dict_init (&subsummary_metadata_builder, NULL);
|
||
g_variant_builder_add (subsummary_builder, "{s(@ay@aay@a{sv})}",
|
||
subsummary,
|
||
digest_v,
|
||
g_variant_builder_end (history_builder),
|
||
g_variant_dict_end (&subsummary_metadata_builder));
|
||
}
|
||
|
||
g_variant_builder_add_value (index_builder, g_variant_builder_end (subsummary_builder));
|
||
g_variant_builder_add_value (index_builder, g_variant_builder_end (metadata_builder));
|
||
|
||
index = g_variant_ref_sink (g_variant_builder_end (index_builder));
|
||
|
||
return g_steal_pointer (&index);
|
||
}
|
||
|
||
static gboolean
|
||
flatpak_repo_gc_digested_summaries (OstreeRepo *repo,
|
||
const char *index_digest, /* The digest of the current (new) index (if any) */
|
||
const char *old_index_digest, /* The digest of the previous index (if any) */
|
||
GHashTable *digested_summaries, /* generated */
|
||
GHashTable *digested_summary_cache, /* generated + referenced */
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_auto(GLnxDirFdIterator) iter = {0};
|
||
int repo_fd = ostree_repo_get_dfd (repo);
|
||
struct dirent *dent;
|
||
const char *ext;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
if (!glnx_dirfd_iterator_init_at (repo_fd, "summaries", FALSE, &iter, &local_error))
|
||
{
|
||
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
|
||
return TRUE;
|
||
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
return FALSE;
|
||
}
|
||
|
||
while (TRUE)
|
||
{
|
||
gboolean remove = FALSE;
|
||
|
||
if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (dent == NULL)
|
||
break;
|
||
|
||
if (dent->d_type != DT_REG)
|
||
continue;
|
||
|
||
/* Keep it if its an unexpected type */
|
||
ext = strchr (dent->d_name, '.');
|
||
if (ext != NULL)
|
||
{
|
||
if (strcmp (ext, ".gz") == 0 && strlen (dent->d_name) == 64 + 3)
|
||
{
|
||
char *sha256 = g_strndup (dent->d_name, 64);
|
||
|
||
/* Keep all the referenced summaries */
|
||
if (g_hash_table_contains (digested_summary_cache, sha256))
|
||
{
|
||
g_info ("Keeping referenced summary %s", dent->d_name);
|
||
continue;
|
||
}
|
||
/* Remove rest */
|
||
remove = TRUE;
|
||
}
|
||
else if (strcmp (ext, ".delta") == 0)
|
||
{
|
||
const char *dash = strchr (dent->d_name, '-');
|
||
if (dash != NULL && dash < ext && (ext - dash) == 1 + 64)
|
||
{
|
||
char *to_sha256 = g_strndup (dash + 1, 64);
|
||
|
||
/* Only keep deltas going to a generated summary */
|
||
if (g_hash_table_contains (digested_summaries, to_sha256))
|
||
{
|
||
g_info ("Keeping delta to generated summary %s", dent->d_name);
|
||
continue;
|
||
}
|
||
/* Remove rest */
|
||
remove = TRUE;
|
||
}
|
||
}
|
||
else if (strcmp (ext, ".idx.sig") == 0)
|
||
{
|
||
g_autofree char *digest = g_strndup (dent->d_name, strlen (dent->d_name) - strlen (".idx.sig"));
|
||
|
||
if (g_strcmp0 (digest, index_digest) == 0)
|
||
continue; /* Always keep current */
|
||
|
||
if (g_strcmp0 (digest, old_index_digest) == 0)
|
||
continue; /* Always keep previous one, to avoid some races */
|
||
|
||
/* Remove the rest */
|
||
remove = TRUE;
|
||
}
|
||
}
|
||
|
||
if (remove)
|
||
{
|
||
g_info ("Removing old digested summary file %s", dent->d_name);
|
||
if (unlinkat (iter.fd, dent->d_name, 0) != 0)
|
||
{
|
||
glnx_set_error_from_errno (error);
|
||
return FALSE;
|
||
}
|
||
}
|
||
else
|
||
g_info ("Keeping unexpected summary file %s", dent->d_name);
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
/* Update the metadata in the summary file for @repo, and then re-sign the file.
|
||
* If the repo has a collection ID set, additionally store the metadata on a
|
||
* contentless commit in a well-known branch, which is the preferred way of
|
||
* broadcasting per-repo metadata (putting it in the summary file is deprecated,
|
||
* but kept for backwards compatibility).
|
||
*
|
||
* Note that there are two keys for the collection ID: collection-id, and
|
||
* ostree.deploy-collection-id. If a client does not currently have a
|
||
* collection ID configured for this remote, it will *only* update its
|
||
* configuration from ostree.deploy-collection-id. This allows phased
|
||
* deployment of collection-based repositories. Clients will only update their
|
||
* configuration from an unset to a set collection ID once (otherwise the
|
||
* security properties of collection IDs are broken). */
|
||
gboolean
|
||
flatpak_repo_update (OstreeRepo *repo,
|
||
FlatpakRepoUpdateFlags flags,
|
||
const char **gpg_key_ids,
|
||
const char *gpg_homedir,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GHashTable) commit_data_cache = NULL;
|
||
g_autoptr(GVariant) compat_summary = NULL;
|
||
g_autoptr(GVariant) summary_index = NULL;
|
||
g_autoptr(GVariant) old_index = NULL;
|
||
g_autoptr(GPtrArray) delta_names = NULL;
|
||
g_auto(GStrv) summary_arches = NULL;
|
||
g_autoptr(GHashTable) refs = NULL;
|
||
g_autoptr(GHashTable) arches = NULL;
|
||
g_autoptr(GHashTable) subsets = NULL;
|
||
g_autoptr(GHashTable) summaries = NULL;
|
||
g_autoptr(GHashTable) digested_summaries = NULL;
|
||
g_autoptr(GHashTable) digested_summary_cache = NULL;
|
||
g_autoptr(GBytes) index_sig = NULL;
|
||
time_t old_compat_sig_mtime;
|
||
GKeyFile *config;
|
||
gboolean disable_index = (flags & FLATPAK_REPO_UPDATE_FLAG_DISABLE_INDEX) != 0;
|
||
g_autofree char *index_digest = NULL;
|
||
g_autofree char *old_index_digest = NULL;
|
||
|
||
config = ostree_repo_get_config (repo);
|
||
|
||
if (!ostree_repo_list_refs_ext (repo, NULL, &refs,
|
||
OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_REMOTES | OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_MIRRORS,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
old_index = flatpak_repo_load_summary_index (repo, NULL);
|
||
if (old_index)
|
||
commit_data_cache = populate_commit_data_cache (repo, old_index);
|
||
|
||
if (commit_data_cache == NULL) /* No index or failed to load it */
|
||
commit_data_cache = commit_data_cache_new ();
|
||
|
||
if (!ostree_repo_list_static_delta_names (repo, &delta_names, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (config)
|
||
summary_arches = g_key_file_get_string_list (config, "flatpak", "summary-arches", NULL, NULL);
|
||
|
||
summaries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||
/* These are the ones we generated */
|
||
digested_summaries = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
|
||
/* These are the ones generated or references */
|
||
digested_summary_cache = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)g_variant_unref);
|
||
|
||
arches = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||
subsets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
|
||
g_hash_table_add (subsets, g_strdup ("")); /* Always have everything subset */
|
||
|
||
GLNX_HASH_TABLE_FOREACH_KV (refs, const char *, ref, const char *, rev)
|
||
{
|
||
g_autofree char *arch = flatpak_get_arch_for_ref (ref);
|
||
CommitData *rev_data = NULL;
|
||
|
||
if (arch != NULL &&
|
||
!g_hash_table_contains (arches, arch))
|
||
g_hash_table_add (arches, g_steal_pointer (&arch));
|
||
|
||
/* Add CommitData for flatpak refs that we didn't already pre-populate */
|
||
if (flatpak_is_app_runtime_or_appstream_ref (ref))
|
||
{
|
||
rev_data = g_hash_table_lookup (commit_data_cache, rev);
|
||
if (rev_data == NULL)
|
||
{
|
||
rev_data = read_commit_data (repo, ref, rev, cancellable, error);
|
||
if (rev_data == NULL)
|
||
return FALSE;
|
||
|
||
g_hash_table_insert (commit_data_cache, g_strdup (rev), (CommitData *)rev_data);
|
||
}
|
||
|
||
for (int i = 0; rev_data->subsets != NULL && i < rev_data->subsets->len; i++)
|
||
{
|
||
const char *subset = g_ptr_array_index (rev_data->subsets, i);
|
||
if (!g_hash_table_contains (subsets, subset))
|
||
g_hash_table_add (subsets, g_strdup (subset));
|
||
}
|
||
}
|
||
}
|
||
|
||
compat_summary = generate_summary (repo, TRUE, refs, commit_data_cache, delta_names,
|
||
"", (const char **)summary_arches,
|
||
cancellable, error);
|
||
if (compat_summary == NULL)
|
||
return FALSE;
|
||
|
||
if (!disable_index)
|
||
{
|
||
GLNX_HASH_TABLE_FOREACH (subsets, const char *, subset)
|
||
{
|
||
GLNX_HASH_TABLE_FOREACH (arches, const char *, arch)
|
||
{
|
||
const char *arch_v[] = { arch, NULL };
|
||
g_autofree char *name = NULL;
|
||
g_autofree char *digest = NULL;
|
||
|
||
if (*subset == 0)
|
||
name = g_strdup (arch);
|
||
else
|
||
name = g_strconcat (subset, "-", arch, NULL);
|
||
|
||
g_autoptr(GVariant) arch_summary = generate_summary (repo, FALSE, refs, commit_data_cache, NULL, subset, arch_v,
|
||
cancellable, error);
|
||
if (arch_summary == NULL)
|
||
return FALSE;
|
||
|
||
digest = flatpak_repo_save_digested_summary (repo, name, arch_summary, cancellable, error);
|
||
if (digest == NULL)
|
||
return FALSE;
|
||
|
||
g_hash_table_insert (digested_summaries, g_strdup (digest), g_variant_ref (arch_summary));
|
||
/* Prime summary cache with generated summaries */
|
||
g_hash_table_insert (digested_summary_cache, g_strdup (digest), g_variant_ref (arch_summary));
|
||
g_hash_table_insert (summaries, g_steal_pointer (&name), g_steal_pointer (&digest));
|
||
}
|
||
}
|
||
|
||
summary_index = generate_summary_index (repo, old_index, summaries, digested_summaries, digested_summary_cache,
|
||
gpg_key_ids, gpg_homedir,
|
||
cancellable, error);
|
||
if (summary_index == NULL)
|
||
return FALSE;
|
||
}
|
||
|
||
if (!ostree_repo_static_delta_reindex (repo, 0, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (summary_index && gpg_key_ids)
|
||
{
|
||
g_autoptr(GBytes) index_bytes = g_variant_get_data_as_bytes (summary_index);
|
||
|
||
if (!ostree_repo_gpg_sign_data (repo, index_bytes,
|
||
NULL,
|
||
gpg_key_ids,
|
||
gpg_homedir,
|
||
&index_sig,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (summary_index)
|
||
index_digest = g_compute_checksum_for_data (G_CHECKSUM_SHA256,
|
||
g_variant_get_data (summary_index),
|
||
g_variant_get_size (summary_index));
|
||
if (old_index)
|
||
old_index_digest = g_compute_checksum_for_data (G_CHECKSUM_SHA256,
|
||
g_variant_get_data (old_index),
|
||
g_variant_get_size (old_index));
|
||
|
||
if (!flatpak_repo_save_summary_index (repo, summary_index, index_digest, index_sig, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_repo_save_compat_summary (repo, compat_summary, &old_compat_sig_mtime, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (gpg_key_ids)
|
||
{
|
||
if (!ostree_repo_add_gpg_signature_summary (repo,
|
||
gpg_key_ids,
|
||
gpg_homedir,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
|
||
|
||
if (old_compat_sig_mtime != 0)
|
||
{
|
||
int repo_dfd = ostree_repo_get_dfd (repo);
|
||
struct stat stbuf;
|
||
|
||
/* Ensure we increase (in sec precision) */
|
||
if (fstatat (repo_dfd, "summary.sig", &stbuf, AT_SYMLINK_NOFOLLOW) == 0 &&
|
||
stbuf.st_mtime <= old_compat_sig_mtime)
|
||
{
|
||
struct timespec ts[2] = { {0, UTIME_OMIT}, {old_compat_sig_mtime + 1, 0} };
|
||
(void) utimensat (repo_dfd, "summary.sig", ts, AT_SYMLINK_NOFOLLOW);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!disable_index &&
|
||
!flatpak_repo_gc_digested_summaries (repo, index_digest, old_index_digest, digested_summaries, digested_summary_cache, cancellable, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_mtree_create_dir (OstreeRepo *repo,
|
||
OstreeMutableTree *parent,
|
||
const char *name,
|
||
OstreeMutableTree **dir_out,
|
||
GError **error)
|
||
{
|
||
g_autoptr(OstreeMutableTree) dir = NULL;
|
||
|
||
if (!ostree_mutable_tree_ensure_dir (parent, name, &dir, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_mtree_ensure_dir_metadata (repo, dir, NULL, error))
|
||
return FALSE;
|
||
|
||
*dir_out = g_steal_pointer (&dir);
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_mtree_create_symlink (OstreeRepo *repo,
|
||
OstreeMutableTree *parent,
|
||
const char *filename,
|
||
const char *target,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFileInfo) file_info = g_file_info_new ();
|
||
g_autoptr(GInputStream) content_stream = NULL;
|
||
g_autofree guchar *raw_checksum = NULL;
|
||
g_autofree char *checksum = NULL;
|
||
guint64 length;
|
||
|
||
g_file_info_set_name (file_info, filename);
|
||
g_file_info_set_file_type (file_info, G_FILE_TYPE_SYMBOLIC_LINK);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::uid", 0);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::gid", 0);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::mode", S_IFLNK | 0777);
|
||
|
||
g_file_info_set_attribute_boolean (file_info, "standard::is-symlink", TRUE);
|
||
g_file_info_set_attribute_byte_string (file_info, "standard::symlink-target", target);
|
||
|
||
if (!ostree_raw_file_to_content_stream (NULL, file_info, NULL,
|
||
&content_stream, &length,
|
||
NULL, error))
|
||
return FALSE;
|
||
|
||
if (!ostree_repo_write_content (repo, NULL, content_stream, length,
|
||
&raw_checksum, NULL, error))
|
||
return FALSE;
|
||
|
||
checksum = ostree_checksum_from_bytes (raw_checksum);
|
||
|
||
if (!ostree_mutable_tree_replace_file (parent, filename, checksum, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_mtree_add_file_from_bytes (OstreeRepo *repo,
|
||
GBytes *bytes,
|
||
OstreeMutableTree *parent,
|
||
const char *filename,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFileInfo) info = g_file_info_new ();
|
||
g_autoptr(GInputStream) memstream = NULL;
|
||
g_autoptr(GInputStream) content_stream = NULL;
|
||
g_autofree guchar *raw_checksum = NULL;
|
||
g_autofree char *checksum = NULL;
|
||
guint64 length;
|
||
|
||
g_file_info_set_attribute_uint32 (info, "standard::type", G_FILE_TYPE_REGULAR);
|
||
g_file_info_set_attribute_uint64 (info, "standard::size", g_bytes_get_size (bytes));
|
||
g_file_info_set_attribute_uint32 (info, "unix::uid", 0);
|
||
g_file_info_set_attribute_uint32 (info, "unix::gid", 0);
|
||
g_file_info_set_attribute_uint32 (info, "unix::mode", S_IFREG | 0644);
|
||
|
||
memstream = g_memory_input_stream_new_from_bytes (bytes);
|
||
|
||
if (!ostree_raw_file_to_content_stream (memstream, info, NULL,
|
||
&content_stream, &length,
|
||
cancellable, error))
|
||
return FALSE;
|
||
|
||
if (!ostree_repo_write_content (repo, NULL, content_stream, length,
|
||
&raw_checksum, cancellable, error))
|
||
return FALSE;
|
||
|
||
checksum = ostree_checksum_from_bytes (raw_checksum);
|
||
|
||
if (!ostree_mutable_tree_replace_file (parent, filename, checksum, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_mtree_ensure_dir_metadata (OstreeRepo *repo,
|
||
OstreeMutableTree *mtree,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariant) dirmeta = NULL;
|
||
g_autoptr(GFileInfo) file_info = g_file_info_new ();
|
||
g_autofree guchar *csum = NULL;
|
||
g_autofree char *checksum = NULL;
|
||
|
||
g_file_info_set_name (file_info, "/");
|
||
g_file_info_set_file_type (file_info, G_FILE_TYPE_DIRECTORY);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::uid", 0);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::gid", 0);
|
||
g_file_info_set_attribute_uint32 (file_info, "unix::mode", 040755);
|
||
|
||
dirmeta = ostree_create_directory_metadata (file_info, NULL);
|
||
if (!ostree_repo_write_metadata (repo, OSTREE_OBJECT_TYPE_DIR_META, NULL,
|
||
dirmeta, &csum, cancellable, error))
|
||
return FALSE;
|
||
|
||
checksum = ostree_checksum_from_bytes (csum);
|
||
ostree_mutable_tree_set_metadata_checksum (mtree, checksum);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
validate_component (FlatpakXml *component,
|
||
const char *ref,
|
||
const char *id,
|
||
char **tags,
|
||
const char *runtime,
|
||
const char *sdk)
|
||
{
|
||
FlatpakXml *bundle, *text, *prev, *id_node, *id_text_node, *metadata, *value;
|
||
g_autofree char *id_text = NULL;
|
||
int i;
|
||
|
||
if (g_strcmp0 (component->element_name, "component") != 0)
|
||
return FALSE;
|
||
|
||
id_node = flatpak_xml_find (component, "id", NULL);
|
||
if (id_node == NULL)
|
||
return FALSE;
|
||
|
||
id_text_node = flatpak_xml_find (id_node, NULL, NULL);
|
||
if (id_text_node == NULL || id_text_node->text == NULL)
|
||
return FALSE;
|
||
|
||
id_text = g_strstrip (g_strdup (id_text_node->text));
|
||
|
||
/* Drop .desktop file suffix (unless the actual app id ends with .desktop) */
|
||
if (g_str_has_suffix (id_text, ".desktop") &&
|
||
!g_str_has_suffix (id, ".desktop"))
|
||
id_text[strlen (id_text) - strlen (".desktop")] = 0;
|
||
|
||
if (!g_str_has_prefix (id_text, id))
|
||
{
|
||
g_warning ("Invalid id %s", id_text);
|
||
return FALSE;
|
||
}
|
||
|
||
while ((bundle = flatpak_xml_find (component, "bundle", &prev)) != NULL)
|
||
flatpak_xml_free (flatpak_xml_unlink (bundle, prev));
|
||
|
||
bundle = flatpak_xml_new ("bundle");
|
||
bundle->attribute_names = g_new0 (char *, 2 * 4);
|
||
bundle->attribute_values = g_new0 (char *, 2 * 4);
|
||
bundle->attribute_names[0] = g_strdup ("type");
|
||
bundle->attribute_values[0] = g_strdup ("flatpak");
|
||
|
||
i = 1;
|
||
if (runtime && !g_str_has_prefix (runtime, "runtime/"))
|
||
{
|
||
bundle->attribute_names[i] = g_strdup ("runtime");
|
||
bundle->attribute_values[i] = g_strdup (runtime);
|
||
i++;
|
||
}
|
||
|
||
if (sdk)
|
||
{
|
||
bundle->attribute_names[i] = g_strdup ("sdk");
|
||
bundle->attribute_values[i] = g_strdup (sdk);
|
||
i++;
|
||
}
|
||
|
||
text = flatpak_xml_new (NULL);
|
||
text->text = g_strdup (ref);
|
||
flatpak_xml_add (bundle, text);
|
||
|
||
flatpak_xml_add (component, flatpak_xml_new_text (" "));
|
||
flatpak_xml_add (component, bundle);
|
||
flatpak_xml_add (component, flatpak_xml_new_text ("\n "));
|
||
|
||
if (tags != NULL && tags[0] != NULL)
|
||
{
|
||
metadata = flatpak_xml_find (component, "metadata", NULL);
|
||
if (metadata == NULL)
|
||
{
|
||
metadata = flatpak_xml_new ("metadata");
|
||
metadata->attribute_names = g_new0 (char *, 1);
|
||
metadata->attribute_values = g_new0 (char *, 1);
|
||
|
||
flatpak_xml_add (component, flatpak_xml_new_text (" "));
|
||
flatpak_xml_add (component, metadata);
|
||
flatpak_xml_add (component, flatpak_xml_new_text ("\n "));
|
||
}
|
||
|
||
value = flatpak_xml_new ("value");
|
||
value->attribute_names = g_new0 (char *, 2);
|
||
value->attribute_values = g_new0 (char *, 2);
|
||
value->attribute_names[0] = g_strdup ("key");
|
||
value->attribute_values[0] = g_strdup ("X-Flatpak-Tags");
|
||
flatpak_xml_add (metadata, flatpak_xml_new_text ("\n "));
|
||
flatpak_xml_add (metadata, value);
|
||
flatpak_xml_add (metadata, flatpak_xml_new_text ("\n "));
|
||
|
||
text = flatpak_xml_new (NULL);
|
||
text->text = g_strjoinv (",", tags);
|
||
flatpak_xml_add (value, text);
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_appstream_xml_migrate (FlatpakXml *source,
|
||
FlatpakXml *dest,
|
||
const char *ref,
|
||
const char *id,
|
||
GKeyFile *metadata)
|
||
{
|
||
FlatpakXml *source_components;
|
||
FlatpakXml *dest_components;
|
||
FlatpakXml *component;
|
||
FlatpakXml *prev_component;
|
||
gboolean migrated = FALSE;
|
||
g_auto(GStrv) tags = NULL;
|
||
g_autofree const char *runtime = NULL;
|
||
g_autofree const char *sdk = NULL;
|
||
const char *group;
|
||
|
||
if (source->first_child == NULL ||
|
||
source->first_child->next_sibling != NULL ||
|
||
g_strcmp0 (source->first_child->element_name, "components") != 0)
|
||
return FALSE;
|
||
|
||
if (g_str_has_prefix (ref, "app/"))
|
||
group = FLATPAK_METADATA_GROUP_APPLICATION;
|
||
else
|
||
group = FLATPAK_METADATA_GROUP_RUNTIME;
|
||
|
||
tags = g_key_file_get_string_list (metadata, group, FLATPAK_METADATA_KEY_TAGS,
|
||
NULL, NULL);
|
||
runtime = g_key_file_get_string (metadata, group,
|
||
FLATPAK_METADATA_KEY_RUNTIME, NULL);
|
||
sdk = g_key_file_get_string (metadata, group, FLATPAK_METADATA_KEY_SDK, NULL);
|
||
|
||
source_components = source->first_child;
|
||
dest_components = dest->first_child;
|
||
|
||
component = source_components->first_child;
|
||
prev_component = NULL;
|
||
while (component != NULL)
|
||
{
|
||
FlatpakXml *next = component->next_sibling;
|
||
|
||
if (validate_component (component, ref, id, tags, runtime, sdk))
|
||
{
|
||
flatpak_xml_add (dest_components,
|
||
flatpak_xml_unlink (component, prev_component));
|
||
migrated = TRUE;
|
||
}
|
||
else
|
||
{
|
||
prev_component = component;
|
||
}
|
||
|
||
component = next;
|
||
}
|
||
|
||
return migrated;
|
||
}
|
||
|
||
static gboolean
|
||
copy_icon (const char *id,
|
||
GFile *icons_dir,
|
||
OstreeRepo *repo,
|
||
OstreeMutableTree *size_mtree,
|
||
const char *size,
|
||
GError **error)
|
||
{
|
||
g_autofree char *icon_name = g_strconcat (id, ".png", NULL);
|
||
g_autoptr(GFile) size_dir = g_file_get_child (icons_dir, size);
|
||
g_autoptr(GFile) icon_file = g_file_get_child (size_dir, icon_name);
|
||
const char *checksum;
|
||
|
||
if (!ostree_repo_file_ensure_resolved (OSTREE_REPO_FILE(icon_file), NULL))
|
||
{
|
||
g_info ("No icon at size %s for %s", size, id);
|
||
return TRUE;
|
||
}
|
||
|
||
checksum = ostree_repo_file_get_checksum (OSTREE_REPO_FILE(icon_file));
|
||
if (!ostree_mutable_tree_replace_file (size_mtree, icon_name, checksum, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
extract_appstream (OstreeRepo *repo,
|
||
FlatpakXml *appstream_root,
|
||
FlatpakDecomposed *ref,
|
||
const char *id,
|
||
OstreeMutableTree *size1_mtree,
|
||
OstreeMutableTree *size2_mtree,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GFile) root = NULL;
|
||
g_autoptr(GFile) app_info_dir = NULL;
|
||
g_autoptr(GFile) xmls_dir = NULL;
|
||
g_autoptr(GFile) icons_dir = NULL;
|
||
g_autoptr(GFile) appstream_file = NULL;
|
||
g_autoptr(GFile) metadata = NULL;
|
||
g_autofree char *appstream_basename = NULL;
|
||
g_autoptr(GInputStream) in = NULL;
|
||
g_autoptr(FlatpakXml) xml_root = NULL;
|
||
g_autoptr(GKeyFile) keyfile = NULL;
|
||
|
||
if (!ostree_repo_read_commit (repo, flatpak_decomposed_get_ref (ref), &root, NULL, NULL, error))
|
||
return FALSE;
|
||
|
||
keyfile = g_key_file_new ();
|
||
metadata = g_file_get_child (root, "metadata");
|
||
if (g_file_query_exists (metadata, cancellable))
|
||
{
|
||
g_autofree char *content = NULL;
|
||
gsize len;
|
||
|
||
if (!g_file_load_contents (metadata, cancellable, &content, &len, NULL, error))
|
||
return FALSE;
|
||
|
||
if (!g_key_file_load_from_data (keyfile, content, len, G_KEY_FILE_NONE, error))
|
||
return FALSE;
|
||
}
|
||
|
||
appstream_file = g_file_resolve_relative_path (root, "files/share/swcatalog/xml/flatpak.xml.gz");
|
||
if (g_file_test (g_file_get_path (appstream_file), G_FILE_TEST_EXISTS))
|
||
app_info_dir = g_file_resolve_relative_path (root, "files/share/swcatalog");
|
||
else
|
||
{
|
||
app_info_dir = g_file_resolve_relative_path (root, "files/share/app-info");
|
||
xmls_dir = g_file_resolve_relative_path (app_info_dir, "xmls");
|
||
appstream_basename = g_strconcat (id, ".xml.gz", NULL);
|
||
appstream_file = g_file_get_child (xmls_dir, appstream_basename);
|
||
}
|
||
|
||
icons_dir = g_file_resolve_relative_path (app_info_dir, "icons/flatpak");
|
||
|
||
in = (GInputStream *) g_file_read (appstream_file, cancellable, error);
|
||
if (!in)
|
||
return FALSE;
|
||
|
||
xml_root = flatpak_xml_parse (in, TRUE, cancellable, error);
|
||
if (xml_root == NULL)
|
||
return FALSE;
|
||
|
||
if (flatpak_appstream_xml_migrate (xml_root, appstream_root,
|
||
flatpak_decomposed_get_ref (ref), id, keyfile))
|
||
{
|
||
g_autoptr(GError) my_error = NULL;
|
||
FlatpakXml *components = appstream_root->first_child;
|
||
FlatpakXml *component = components->first_child;
|
||
|
||
while (component != NULL)
|
||
{
|
||
FlatpakXml *component_id, *component_id_text_node;
|
||
g_autofree char *component_id_text = NULL;
|
||
char *component_id_suffix;
|
||
|
||
if (g_strcmp0 (component->element_name, "component") != 0)
|
||
{
|
||
component = component->next_sibling;
|
||
continue;
|
||
}
|
||
|
||
component_id = flatpak_xml_find (component, "id", NULL);
|
||
component_id_text_node = flatpak_xml_find (component_id, NULL, NULL);
|
||
|
||
component_id_text = g_strstrip (g_strdup (component_id_text_node->text));
|
||
|
||
/* We're looking for a component that matches the app-id (id), but it
|
||
may have some further elements (separated by dot) and can also have
|
||
".desktop" at the end which we need to strip out. Further complicating
|
||
things, some actual app ids ends in .desktop, such as org.telegram.desktop. */
|
||
|
||
component_id_suffix = component_id_text + strlen (id); /* Don't deref before we check for prefix match! */
|
||
if (!g_str_has_prefix (component_id_text, id) ||
|
||
(component_id_suffix[0] != 0 && component_id_suffix[0] != '.'))
|
||
{
|
||
component = component->next_sibling;
|
||
continue;
|
||
}
|
||
|
||
if (g_str_has_suffix (component_id_suffix, ".desktop"))
|
||
component_id_suffix[strlen (component_id_suffix) - strlen (".desktop")] = 0;
|
||
|
||
if (!copy_icon (component_id_text, icons_dir, repo, size1_mtree, "64x64", &my_error))
|
||
{
|
||
g_print (_("Error copying 64x64 icon for component %s: %s\n"), component_id_text, my_error->message);
|
||
g_clear_error (&my_error);
|
||
}
|
||
|
||
if (!copy_icon (component_id_text, icons_dir, repo, size2_mtree, "128x128", &my_error))
|
||
{
|
||
g_print (_("Error copying 128x128 icon for component %s: %s\n"), component_id_text, my_error->message);
|
||
g_clear_error (&my_error);
|
||
}
|
||
|
||
|
||
/* We might match other prefixes, so keep on going */
|
||
component = component->next_sibling;
|
||
}
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
FlatpakXml *
|
||
flatpak_appstream_xml_new (void)
|
||
{
|
||
FlatpakXml *appstream_root = NULL;
|
||
FlatpakXml *appstream_components;
|
||
|
||
appstream_root = flatpak_xml_new ("root");
|
||
appstream_components = flatpak_xml_new ("components");
|
||
flatpak_xml_add (appstream_root, appstream_components);
|
||
flatpak_xml_add (appstream_components, flatpak_xml_new_text ("\n "));
|
||
|
||
appstream_components->attribute_names = g_new0 (char *, 3);
|
||
appstream_components->attribute_values = g_new0 (char *, 3);
|
||
appstream_components->attribute_names[0] = g_strdup ("version");
|
||
appstream_components->attribute_values[0] = g_strdup ("0.8");
|
||
appstream_components->attribute_names[1] = g_strdup ("origin");
|
||
appstream_components->attribute_values[1] = g_strdup ("flatpak");
|
||
|
||
return appstream_root;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_appstream_xml_root_to_data (FlatpakXml *appstream_root,
|
||
GBytes **uncompressed,
|
||
GBytes **compressed,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GString) xml = NULL;
|
||
g_autoptr(GZlibCompressor) compressor = NULL;
|
||
g_autoptr(GOutputStream) out2 = NULL;
|
||
g_autoptr(GOutputStream) out = NULL;
|
||
|
||
flatpak_xml_add (appstream_root->first_child, flatpak_xml_new_text ("\n"));
|
||
|
||
xml = g_string_new ("");
|
||
flatpak_xml_to_string (appstream_root, xml);
|
||
|
||
if (compressed)
|
||
{
|
||
compressor = g_zlib_compressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP, -1);
|
||
out = g_memory_output_stream_new_resizable ();
|
||
out2 = g_converter_output_stream_new (out, G_CONVERTER (compressor));
|
||
if (!g_output_stream_write_all (out2, xml->str, xml->len,
|
||
NULL, NULL, error))
|
||
return FALSE;
|
||
if (!g_output_stream_close (out2, NULL, error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (uncompressed)
|
||
*uncompressed = g_string_free_to_bytes (g_steal_pointer (&xml));
|
||
|
||
if (compressed)
|
||
*compressed = g_memory_output_stream_steal_as_bytes (G_MEMORY_OUTPUT_STREAM (out));
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
void
|
||
flatpak_appstream_xml_filter (FlatpakXml *appstream,
|
||
GRegex *allow_refs,
|
||
GRegex *deny_refs)
|
||
{
|
||
FlatpakXml *components;
|
||
FlatpakXml *component;
|
||
FlatpakXml *prev_component, *old;
|
||
|
||
for (components = appstream->first_child;
|
||
components != NULL;
|
||
components = components->next_sibling)
|
||
{
|
||
if (g_strcmp0 (components->element_name, "components") != 0)
|
||
continue;
|
||
|
||
|
||
prev_component = NULL;
|
||
component = components->first_child;
|
||
while (component != NULL)
|
||
{
|
||
FlatpakXml *bundle;
|
||
gboolean allow = FALSE;
|
||
|
||
if (g_strcmp0 (component->element_name, "component") == 0)
|
||
{
|
||
bundle = flatpak_xml_find (component, "bundle", NULL);
|
||
if (bundle && bundle->first_child && bundle->first_child->text)
|
||
allow = flatpak_filters_allow_ref (allow_refs, deny_refs, bundle->first_child->text);
|
||
}
|
||
|
||
if (allow)
|
||
{
|
||
prev_component = component;
|
||
component = component->next_sibling;
|
||
}
|
||
else
|
||
{
|
||
old = component;
|
||
|
||
/* prev_component is same as before */
|
||
component = component->next_sibling;
|
||
|
||
flatpak_xml_unlink (old, prev_component);
|
||
flatpak_xml_free (old);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/* This is similar to ostree_repo_list_refs(), but returns only valid flatpak
|
||
* refs, as FlatpakDecomposed. */
|
||
static GHashTable *
|
||
flatpak_repo_list_flatpak_refs (OstreeRepo *repo,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GHashTable) refspecs = NULL;
|
||
g_autoptr(GHashTable) refs = NULL;
|
||
GHashTableIter iter;
|
||
gpointer key, value;
|
||
|
||
if (!ostree_repo_list_refs_ext (repo, NULL, &refspecs,
|
||
OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_REMOTES | OSTREE_REPO_LIST_REFS_EXT_EXCLUDE_MIRRORS,
|
||
cancellable, error))
|
||
return NULL;
|
||
|
||
refs = g_hash_table_new_full ((GHashFunc)flatpak_decomposed_hash, (GEqualFunc)flatpak_decomposed_equal, (GDestroyNotify)flatpak_decomposed_unref, g_free);
|
||
|
||
g_hash_table_iter_init (&iter, refspecs);
|
||
while (g_hash_table_iter_next (&iter, &key, &value))
|
||
{
|
||
const char *refstr = key;
|
||
const char *checksum = value;
|
||
FlatpakDecomposed *ref = NULL;
|
||
|
||
ref = flatpak_decomposed_new_from_ref_take ((char *)refstr, NULL);
|
||
if (ref)
|
||
{
|
||
g_hash_table_iter_steal (&iter);
|
||
g_hash_table_insert (refs, ref, (char *)checksum);
|
||
}
|
||
}
|
||
|
||
return g_steal_pointer (&refs);
|
||
}
|
||
|
||
static gboolean
|
||
_flatpak_repo_generate_appstream (OstreeRepo *repo,
|
||
const char **gpg_key_ids,
|
||
const char *gpg_homedir,
|
||
FlatpakDecomposed **all_refs_keys,
|
||
guint n_keys,
|
||
GHashTable *all_commits,
|
||
const char *arch,
|
||
const char *subset,
|
||
guint64 timestamp,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(FlatpakXml) appstream_root = NULL;
|
||
g_autoptr(GBytes) xml_data = NULL;
|
||
g_autoptr(GBytes) xml_gz_data = NULL;
|
||
g_autoptr(OstreeMutableTree) mtree = ostree_mutable_tree_new ();
|
||
g_autoptr(OstreeMutableTree) icons_mtree = NULL;
|
||
g_autoptr(OstreeMutableTree) icons_flatpak_mtree = NULL;
|
||
g_autoptr(OstreeMutableTree) size1_mtree = NULL;
|
||
g_autoptr(OstreeMutableTree) size2_mtree = NULL;
|
||
const char *compat_arch;
|
||
compat_arch = flatpak_get_compat_arch (arch);
|
||
const char *branch_names[] = { "appstream", "appstream2" };
|
||
const char *collection_id;
|
||
|
||
if (subset != NULL && *subset != 0)
|
||
g_info ("Generating appstream for %s, subset %s", arch, subset);
|
||
else
|
||
g_info ("Generating appstream for %s", arch);
|
||
|
||
collection_id = ostree_repo_get_collection_id (repo);
|
||
|
||
if (!flatpak_mtree_ensure_dir_metadata (repo, mtree, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_mtree_create_dir (repo, mtree, "icons", &icons_mtree, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_mtree_create_dir (repo, icons_mtree, "64x64", &size1_mtree, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_mtree_create_dir (repo, icons_mtree, "128x128", &size2_mtree, error))
|
||
return FALSE;
|
||
|
||
/* For compatibility with libappstream we create a $origin ("flatpak") subdirectory with symlinks
|
||
* to the size directories thus matching the standard merged appstream layout if we assume the
|
||
* appstream has origin=flatpak, which flatpak-builder creates.
|
||
*
|
||
* See https://github.com/ximion/appstream/pull/224 for details.
|
||
*/
|
||
if (!flatpak_mtree_create_dir (repo, icons_mtree, "flatpak", &icons_flatpak_mtree, error))
|
||
return FALSE;
|
||
if (!flatpak_mtree_create_symlink (repo, icons_flatpak_mtree, "64x64", "../64x64", error))
|
||
return FALSE;
|
||
if (!flatpak_mtree_create_symlink (repo, icons_flatpak_mtree, "128x128", "../128x128", error))
|
||
return FALSE;
|
||
|
||
appstream_root = flatpak_appstream_xml_new ();
|
||
|
||
for (int i = 0; i < n_keys; i++)
|
||
{
|
||
FlatpakDecomposed *ref = all_refs_keys[i];
|
||
GVariant *commit_v = NULL;
|
||
VarMetadataRef commit_metadata;
|
||
g_autoptr(GError) my_error = NULL;
|
||
g_autofree char *id = NULL;
|
||
|
||
if (!flatpak_decomposed_is_arch (ref, arch))
|
||
{
|
||
g_autoptr(FlatpakDecomposed) main_ref = NULL;
|
||
|
||
/* Include refs that don't match the main arch (e.g. x86_64), if they match
|
||
the compat arch (e.g. i386) and the main arch version is not in the repo */
|
||
if (compat_arch != NULL && flatpak_decomposed_is_arch (ref, compat_arch))
|
||
main_ref = flatpak_decomposed_new_from_decomposed (ref, 0, NULL, compat_arch, NULL, NULL);
|
||
|
||
if (main_ref == NULL ||
|
||
g_hash_table_lookup (all_commits, main_ref))
|
||
continue;
|
||
}
|
||
|
||
commit_v = g_hash_table_lookup (all_commits, ref);
|
||
g_assert (commit_v != NULL);
|
||
|
||
commit_metadata = var_commit_get_metadata (var_commit_from_gvariant (commit_v));
|
||
if (var_metadata_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE, NULL, NULL) ||
|
||
var_metadata_lookup (commit_metadata, OSTREE_COMMIT_META_KEY_ENDOFLIFE_REBASE, NULL, NULL))
|
||
{
|
||
g_info (_("%s is end-of-life, ignoring for appstream"), flatpak_decomposed_get_ref (ref));
|
||
continue;
|
||
}
|
||
|
||
if (*subset != 0)
|
||
{
|
||
VarVariantRef xa_subsets_v;
|
||
gboolean in_subset = FALSE;
|
||
|
||
if (var_metadata_lookup (commit_metadata, "xa.subsets", NULL, &xa_subsets_v))
|
||
{
|
||
VarArrayofstringRef xa_subsets = var_arrayofstring_from_variant (xa_subsets_v);
|
||
gsize len = var_arrayofstring_get_length (xa_subsets);
|
||
|
||
for (gsize j = 0; j < len; j++)
|
||
{
|
||
const char *xa_subset = var_arrayofstring_get_at (xa_subsets, j);
|
||
if (strcmp (subset, xa_subset) == 0)
|
||
{
|
||
in_subset = TRUE;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!in_subset)
|
||
continue;
|
||
}
|
||
|
||
id = flatpak_decomposed_dup_id (ref);
|
||
if (!extract_appstream (repo, appstream_root,
|
||
ref, id, size1_mtree, size2_mtree,
|
||
cancellable, &my_error))
|
||
{
|
||
if (flatpak_decomposed_is_app (ref))
|
||
g_print (_("No appstream data for %s: %s\n"), flatpak_decomposed_get_ref (ref), my_error->message);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if (!flatpak_appstream_xml_root_to_data (appstream_root, &xml_data, &xml_gz_data, error))
|
||
return FALSE;
|
||
|
||
for (int i = 0; i < G_N_ELEMENTS (branch_names); i++)
|
||
{
|
||
gboolean skip_commit = FALSE;
|
||
const char *branch_prefix = branch_names[i];
|
||
g_autoptr(GFile) root = NULL;
|
||
g_autofree char *branch = NULL;
|
||
g_autofree char *parent = NULL;
|
||
g_autofree char *commit_checksum = NULL;
|
||
|
||
if (*subset != 0 && i == 0)
|
||
continue; /* No old-style branch for subsets */
|
||
|
||
if (*subset != 0)
|
||
branch = g_strdup_printf ("%s/%s-%s", branch_prefix, subset, arch);
|
||
else
|
||
branch = g_strdup_printf ("%s/%s", branch_prefix, arch);
|
||
|
||
if (!flatpak_repo_resolve_rev (repo, collection_id, NULL, branch, TRUE,
|
||
&parent, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (i == 0)
|
||
{
|
||
if (!flatpak_mtree_add_file_from_bytes (repo, xml_gz_data, mtree, "appstream.xml.gz", cancellable, error))
|
||
return FALSE;
|
||
}
|
||
else
|
||
{
|
||
if (!ostree_mutable_tree_remove (mtree, "appstream.xml.gz", TRUE, error))
|
||
return FALSE;
|
||
|
||
if (!flatpak_mtree_add_file_from_bytes (repo, xml_data, mtree, "appstream.xml", cancellable, error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (!ostree_repo_write_mtree (repo, mtree, &root, cancellable, error))
|
||
return FALSE;
|
||
|
||
/* No need to commit if nothing changed */
|
||
if (parent)
|
||
{
|
||
g_autoptr(GFile) parent_root = NULL;
|
||
|
||
if (!ostree_repo_read_commit (repo, parent, &parent_root, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (g_file_equal (root, parent_root))
|
||
{
|
||
skip_commit = TRUE;
|
||
g_info ("Not updating %s, no change", branch);
|
||
}
|
||
}
|
||
|
||
if (!skip_commit)
|
||
{
|
||
g_autoptr(GVariantDict) metadata_dict = NULL;
|
||
g_autoptr(GVariant) metadata = NULL;
|
||
|
||
/* Add bindings to the metadata. Do this even if P2P support is not
|
||
* enabled, as it might be enable for other flatpak builds. */
|
||
metadata_dict = g_variant_dict_new (NULL);
|
||
g_variant_dict_insert (metadata_dict, "ostree.collection-binding",
|
||
"s", (collection_id != NULL) ? collection_id : "");
|
||
g_variant_dict_insert_value (metadata_dict, "ostree.ref-binding",
|
||
g_variant_new_strv ((const gchar * const *) &branch, 1));
|
||
metadata = g_variant_ref_sink (g_variant_dict_end (metadata_dict));
|
||
|
||
if (timestamp > 0)
|
||
{
|
||
if (!ostree_repo_write_commit_with_time (repo, parent, "Update", NULL, metadata,
|
||
OSTREE_REPO_FILE (root),
|
||
timestamp,
|
||
&commit_checksum,
|
||
cancellable, error))
|
||
return FALSE;
|
||
}
|
||
else
|
||
{
|
||
if (!ostree_repo_write_commit (repo, parent, "Update", NULL, metadata,
|
||
OSTREE_REPO_FILE (root),
|
||
&commit_checksum, cancellable, error))
|
||
return FALSE;
|
||
}
|
||
|
||
if (gpg_key_ids)
|
||
{
|
||
for (int j = 0; gpg_key_ids[j] != NULL; j++)
|
||
{
|
||
const char *keyid = gpg_key_ids[j];
|
||
|
||
if (!ostree_repo_sign_commit (repo,
|
||
commit_checksum,
|
||
keyid,
|
||
gpg_homedir,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
g_info ("Creating appstream branch %s", branch);
|
||
if (collection_id != NULL)
|
||
{
|
||
const OstreeCollectionRef collection_ref = { (char *) collection_id, branch };
|
||
ostree_repo_transaction_set_collection_ref (repo, &collection_ref, commit_checksum);
|
||
}
|
||
else
|
||
{
|
||
ostree_repo_transaction_set_ref (repo, NULL, branch, commit_checksum);
|
||
}
|
||
}
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
gboolean
|
||
flatpak_repo_generate_appstream (OstreeRepo *repo,
|
||
const char **gpg_key_ids,
|
||
const char *gpg_homedir,
|
||
guint64 timestamp,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GHashTable) all_refs = NULL;
|
||
g_autoptr(GHashTable) all_commits = NULL;
|
||
g_autofree FlatpakDecomposed **all_refs_keys = NULL;
|
||
guint n_keys;
|
||
g_autoptr(GPtrArray) arches = NULL; /* (element-type utf8 utf8) */
|
||
g_autoptr(GPtrArray) subsets = NULL; /* (element-type utf8 utf8) */
|
||
g_autoptr(FlatpakRepoTransaction) transaction = NULL;
|
||
OstreeRepoTransactionStats stats;
|
||
|
||
arches = g_ptr_array_new_with_free_func (g_free);
|
||
subsets = g_ptr_array_new_with_free_func (g_free);
|
||
|
||
g_ptr_array_add (subsets, g_strdup (""));
|
||
|
||
all_refs = flatpak_repo_list_flatpak_refs (repo, cancellable, error);
|
||
if (all_refs == NULL)
|
||
return FALSE;
|
||
|
||
all_commits = g_hash_table_new_full ((GHashFunc)flatpak_decomposed_hash, (GEqualFunc)flatpak_decomposed_equal, (GDestroyNotify)flatpak_decomposed_unref, (GDestroyNotify)g_variant_unref);
|
||
|
||
GLNX_HASH_TABLE_FOREACH_KV (all_refs, FlatpakDecomposed *, ref, const char *, commit)
|
||
{
|
||
VarMetadataRef commit_metadata;
|
||
VarVariantRef xa_subsets_v;
|
||
const char *reverse_compat_arch;
|
||
char *new_arch = NULL;
|
||
g_autoptr(GVariant) commit_v = NULL;
|
||
|
||
if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, commit, &commit_v, NULL))
|
||
{
|
||
g_warning ("Couldn't load commit %s (ref %s)", commit, flatpak_decomposed_get_ref (ref));
|
||
continue;
|
||
}
|
||
|
||
g_hash_table_insert (all_commits, flatpak_decomposed_ref (ref), g_variant_ref (commit_v));
|
||
|
||
/* Compute list of subsets */
|
||
commit_metadata = var_commit_get_metadata (var_commit_from_gvariant (commit_v));
|
||
if (var_metadata_lookup (commit_metadata, "xa.subsets", NULL, &xa_subsets_v))
|
||
{
|
||
VarArrayofstringRef xa_subsets = var_arrayofstring_from_variant (xa_subsets_v);
|
||
gsize len = var_arrayofstring_get_length (xa_subsets);
|
||
for (gsize j = 0; j < len; j++)
|
||
{
|
||
const char *subset = var_arrayofstring_get_at (xa_subsets, j);
|
||
|
||
if (!flatpak_g_ptr_array_contains_string (subsets, subset))
|
||
g_ptr_array_add (subsets, g_strdup (subset));
|
||
}
|
||
}
|
||
|
||
/* Compute list of arches */
|
||
if (!flatpak_decomposed_is_arches (ref, arches->len, (const char **) arches->pdata))
|
||
{
|
||
new_arch = flatpak_decomposed_dup_arch (ref);
|
||
g_ptr_array_add (arches, new_arch);
|
||
|
||
/* If repo contains e.g. i386, also generated x86-64 appdata */
|
||
reverse_compat_arch = flatpak_get_compat_arch_reverse (new_arch);
|
||
if (reverse_compat_arch != NULL &&
|
||
!flatpak_g_ptr_array_contains_string (arches, reverse_compat_arch))
|
||
g_ptr_array_add (arches, g_strdup (reverse_compat_arch));
|
||
}
|
||
}
|
||
|
||
g_ptr_array_sort (subsets, flatpak_strcmp0_ptr);
|
||
g_ptr_array_sort (arches, flatpak_strcmp0_ptr);
|
||
|
||
all_refs_keys = (FlatpakDecomposed **) g_hash_table_get_keys_as_array (all_refs, &n_keys);
|
||
|
||
/* Sort refs so that appdata order is stable for e.g. deltas */
|
||
g_qsort_with_data (all_refs_keys, n_keys, sizeof (FlatpakDecomposed *), (GCompareDataFunc) flatpak_decomposed_strcmp_p, NULL);
|
||
|
||
transaction = flatpak_repo_transaction_start (repo, cancellable, error);
|
||
if (transaction == NULL)
|
||
return FALSE;
|
||
|
||
for (int l = 0; l < subsets->len; l++)
|
||
{
|
||
const char *subset = g_ptr_array_index (subsets, l);
|
||
|
||
for (int k = 0; k < arches->len; k++)
|
||
{
|
||
const char *arch = g_ptr_array_index (arches, k);
|
||
|
||
if (!_flatpak_repo_generate_appstream (repo,
|
||
gpg_key_ids,
|
||
gpg_homedir,
|
||
all_refs_keys,
|
||
n_keys,
|
||
all_commits,
|
||
arch,
|
||
subset,
|
||
timestamp,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
if (!ostree_repo_commit_transaction (repo, &stats, cancellable, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
void
|
||
flatpak_extension_free (FlatpakExtension *extension)
|
||
{
|
||
g_free (extension->id);
|
||
g_free (extension->installed_id);
|
||
g_free (extension->commit);
|
||
flatpak_decomposed_unref (extension->ref);
|
||
g_free (extension->directory);
|
||
g_free (extension->files_path);
|
||
g_free (extension->add_ld_path);
|
||
g_free (extension->subdir_suffix);
|
||
g_strfreev (extension->merge_dirs);
|
||
g_free (extension);
|
||
}
|
||
|
||
static int
|
||
flatpak_extension_compare (gconstpointer _a,
|
||
gconstpointer _b)
|
||
{
|
||
const FlatpakExtension *a = _a;
|
||
const FlatpakExtension *b = _b;
|
||
|
||
return b->priority - a->priority;
|
||
}
|
||
|
||
static FlatpakExtension *
|
||
flatpak_extension_new (const char *id,
|
||
const char *extension,
|
||
FlatpakDecomposed *ref,
|
||
const char *directory,
|
||
const char *add_ld_path,
|
||
const char *subdir_suffix,
|
||
char **merge_dirs,
|
||
GFile *files,
|
||
GFile *deploy_dir,
|
||
gboolean is_unmaintained,
|
||
OstreeRepo *repo)
|
||
{
|
||
FlatpakExtension *ext = g_new0 (FlatpakExtension, 1);
|
||
g_autoptr(GBytes) deploy_data = NULL;
|
||
|
||
ext->id = g_strdup (id);
|
||
ext->installed_id = g_strdup (extension);
|
||
ext->ref = flatpak_decomposed_ref (ref);
|
||
ext->directory = g_strdup (directory);
|
||
ext->files_path = g_file_get_path (files);
|
||
ext->add_ld_path = g_strdup (add_ld_path);
|
||
ext->subdir_suffix = g_strdup (subdir_suffix);
|
||
ext->merge_dirs = g_strdupv (merge_dirs);
|
||
ext->is_unmaintained = is_unmaintained;
|
||
|
||
/* Unmaintained extensions won't have a deploy or commit; see
|
||
* https://github.com/flatpak/flatpak/issues/167 */
|
||
if (deploy_dir && !is_unmaintained)
|
||
{
|
||
deploy_data = flatpak_load_deploy_data (deploy_dir, ref, repo, FLATPAK_DEPLOY_VERSION_ANY, NULL, NULL);
|
||
if (deploy_data)
|
||
ext->commit = g_strdup (flatpak_deploy_data_get_commit (deploy_data));
|
||
}
|
||
|
||
if (is_unmaintained)
|
||
ext->priority = 1000;
|
||
else
|
||
{
|
||
g_autoptr(GKeyFile) keyfile = g_key_file_new ();
|
||
g_autofree char *metadata_path = g_build_filename (ext->files_path, "../metadata", NULL);
|
||
|
||
if (g_key_file_load_from_file (keyfile, metadata_path, G_KEY_FILE_NONE, NULL))
|
||
ext->priority = g_key_file_get_integer (keyfile,
|
||
FLATPAK_METADATA_GROUP_EXTENSION_OF,
|
||
FLATPAK_METADATA_KEY_PRIORITY,
|
||
NULL);
|
||
}
|
||
|
||
return ext;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_extension_matches_reason (const char *extension_id,
|
||
const char *reasons,
|
||
gboolean default_value)
|
||
{
|
||
const char *extension_basename;
|
||
g_auto(GStrv) reason_list = NULL;
|
||
size_t i;
|
||
|
||
if (reasons == NULL || *reasons == 0)
|
||
return default_value;
|
||
|
||
extension_basename = strrchr (extension_id, '.');
|
||
if (extension_basename == NULL)
|
||
return FALSE;
|
||
extension_basename += 1;
|
||
|
||
reason_list = g_strsplit (reasons, ";", -1);
|
||
|
||
for (i = 0; reason_list[i]; ++i)
|
||
{
|
||
const char *reason = reason_list[i];
|
||
|
||
if (strcmp (reason, "active-gl-driver") == 0)
|
||
{
|
||
/* handled below */
|
||
const char **gl_drivers = flatpak_get_gl_drivers ();
|
||
size_t j;
|
||
|
||
for (j = 0; gl_drivers[j]; j++)
|
||
{
|
||
if (strcmp (gl_drivers[j], extension_basename) == 0)
|
||
return TRUE;
|
||
}
|
||
}
|
||
else if (strcmp (reason, "active-gtk-theme") == 0)
|
||
{
|
||
const char *gtk_theme = flatpak_get_gtk_theme ();
|
||
if (strcmp (gtk_theme, extension_basename) == 0)
|
||
return TRUE;
|
||
}
|
||
else if (strcmp (reason, "have-intel-gpu") == 0)
|
||
{
|
||
/* Used for Intel VAAPI driver extension */
|
||
if (flatpak_get_have_intel_gpu ())
|
||
return TRUE;
|
||
}
|
||
else if (g_str_has_prefix (reason, "have-kernel-module-"))
|
||
{
|
||
const char *module_name = reason + strlen ("have-kernel-module-");
|
||
|
||
if (flatpak_get_have_kernel_module (module_name))
|
||
return TRUE;
|
||
}
|
||
else if (g_str_has_prefix (reason, "on-xdg-desktop-"))
|
||
{
|
||
const char *desktop_name = reason + strlen ("on-xdg-desktop-");
|
||
const char *current_desktop_var = g_getenv ("XDG_CURRENT_DESKTOP");
|
||
g_auto(GStrv) current_desktop_names = NULL;
|
||
size_t j;
|
||
|
||
if (!current_desktop_var)
|
||
continue;
|
||
|
||
current_desktop_names = g_strsplit (current_desktop_var, ":", -1);
|
||
|
||
for (j = 0; current_desktop_names[j]; ++j)
|
||
{
|
||
if (g_ascii_strcasecmp (desktop_name, current_desktop_names[j]) == 0)
|
||
return TRUE;
|
||
}
|
||
}
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static GList *
|
||
add_extension (GKeyFile *metakey,
|
||
const char *group,
|
||
const char *extension,
|
||
const char *arch,
|
||
const char *branch,
|
||
GList *res)
|
||
{
|
||
FlatpakExtension *ext;
|
||
g_autofree char *directory = g_key_file_get_string (metakey, group,
|
||
FLATPAK_METADATA_KEY_DIRECTORY,
|
||
NULL);
|
||
g_autofree char *add_ld_path = g_key_file_get_string (metakey, group,
|
||
FLATPAK_METADATA_KEY_ADD_LD_PATH,
|
||
NULL);
|
||
g_auto(GStrv) merge_dirs = g_key_file_get_string_list (metakey, group,
|
||
FLATPAK_METADATA_KEY_MERGE_DIRS,
|
||
NULL, NULL);
|
||
g_autofree char *enable_if = g_key_file_get_string (metakey, group,
|
||
FLATPAK_METADATA_KEY_ENABLE_IF,
|
||
NULL);
|
||
g_autofree char *subdir_suffix = g_key_file_get_string (metakey, group,
|
||
FLATPAK_METADATA_KEY_SUBDIRECTORY_SUFFIX,
|
||
NULL);
|
||
g_autoptr(FlatpakDecomposed) ref = NULL;
|
||
gboolean is_unmaintained = FALSE;
|
||
g_autoptr(GFile) files = NULL;
|
||
g_autoptr(GFile) deploy_dir = NULL;
|
||
g_autoptr(FlatpakDir) dir = NULL;
|
||
|
||
if (directory == NULL)
|
||
return res;
|
||
|
||
ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_RUNTIME, extension, arch, branch, NULL);
|
||
if (ref == NULL)
|
||
return res;
|
||
|
||
files = flatpak_find_unmaintained_extension_dir_if_exists (extension, arch, branch, NULL);
|
||
|
||
if (files == NULL)
|
||
{
|
||
deploy_dir = flatpak_find_deploy_dir_for_ref (ref, &dir, NULL, NULL);
|
||
if (deploy_dir)
|
||
files = g_file_get_child (deploy_dir, "files");
|
||
}
|
||
else
|
||
is_unmaintained = TRUE;
|
||
|
||
/* Prefer a full extension (org.freedesktop.Locale) over subdirectory ones (org.freedesktop.Locale.sv) */
|
||
if (files != NULL)
|
||
{
|
||
if (flatpak_extension_matches_reason (extension, enable_if, TRUE))
|
||
{
|
||
ext = flatpak_extension_new (extension, extension, ref, directory,
|
||
add_ld_path, subdir_suffix, merge_dirs,
|
||
files, deploy_dir, is_unmaintained,
|
||
is_unmaintained ? NULL : flatpak_dir_get_repo (dir));
|
||
res = g_list_prepend (res, ext);
|
||
}
|
||
}
|
||
else if (g_key_file_get_boolean (metakey, group,
|
||
FLATPAK_METADATA_KEY_SUBDIRECTORIES, NULL))
|
||
{
|
||
g_autofree char *prefix = g_strconcat (extension, ".", NULL);
|
||
g_auto(GStrv) ids = NULL;
|
||
g_auto(GStrv) unmaintained_refs = NULL;
|
||
int j;
|
||
|
||
ids = flatpak_list_deployed_refs ("runtime", prefix, arch, branch,
|
||
NULL, NULL);
|
||
for (j = 0; ids != NULL && ids[j] != NULL; j++)
|
||
{
|
||
const char *id = ids[j];
|
||
g_autofree char *extended_dir = g_build_filename (directory, id + strlen (prefix), NULL);
|
||
g_autoptr(FlatpakDecomposed) dir_ref = NULL;
|
||
g_autoptr(GFile) subdir_deploy_dir = NULL;
|
||
g_autoptr(GFile) subdir_files = NULL;
|
||
g_autoptr(FlatpakDir) subdir_dir = NULL;
|
||
|
||
dir_ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_RUNTIME, id, arch, branch, NULL);
|
||
if (dir_ref == NULL)
|
||
continue;
|
||
|
||
subdir_deploy_dir = flatpak_find_deploy_dir_for_ref (dir_ref, &subdir_dir, NULL, NULL);
|
||
if (subdir_deploy_dir)
|
||
subdir_files = g_file_get_child (subdir_deploy_dir, "files");
|
||
|
||
if (subdir_files && flatpak_extension_matches_reason (id, enable_if, TRUE))
|
||
{
|
||
ext = flatpak_extension_new (extension, id, dir_ref, extended_dir,
|
||
add_ld_path, subdir_suffix, merge_dirs,
|
||
subdir_files, subdir_deploy_dir, FALSE,
|
||
flatpak_dir_get_repo (subdir_dir));
|
||
ext->needs_tmpfs = TRUE;
|
||
res = g_list_prepend (res, ext);
|
||
}
|
||
}
|
||
|
||
unmaintained_refs = flatpak_list_unmaintained_refs (prefix, arch, branch,
|
||
NULL, NULL);
|
||
for (j = 0; unmaintained_refs != NULL && unmaintained_refs[j] != NULL; j++)
|
||
{
|
||
g_autofree char *extended_dir = g_build_filename (directory, unmaintained_refs[j] + strlen (prefix), NULL);
|
||
g_autoptr(FlatpakDecomposed) dir_ref = NULL;
|
||
g_autoptr(GFile) subdir_files = flatpak_find_unmaintained_extension_dir_if_exists (unmaintained_refs[j], arch, branch, NULL);
|
||
|
||
dir_ref = flatpak_decomposed_new_from_parts (FLATPAK_KINDS_RUNTIME, unmaintained_refs[j], arch, branch, NULL);
|
||
if (dir_ref == NULL)
|
||
continue;
|
||
|
||
if (subdir_files && flatpak_extension_matches_reason (unmaintained_refs[j], enable_if, TRUE))
|
||
{
|
||
ext = flatpak_extension_new (extension, unmaintained_refs[j], dir_ref,
|
||
extended_dir, add_ld_path, subdir_suffix,
|
||
merge_dirs, subdir_files, NULL, TRUE, NULL);
|
||
ext->needs_tmpfs = TRUE;
|
||
res = g_list_prepend (res, ext);
|
||
}
|
||
}
|
||
}
|
||
|
||
return res;
|
||
}
|
||
|
||
void
|
||
flatpak_parse_extension_with_tag (const char *extension,
|
||
char **name,
|
||
char **tag)
|
||
{
|
||
const char *tag_chr = strchr (extension, '@');
|
||
|
||
if (tag_chr)
|
||
{
|
||
if (name != NULL)
|
||
*name = g_strndup (extension, tag_chr - extension);
|
||
|
||
/* Everything after the @ */
|
||
if (tag != NULL)
|
||
*tag = g_strdup (tag_chr + 1);
|
||
|
||
return;
|
||
}
|
||
|
||
if (name != NULL)
|
||
*name = g_strdup (extension);
|
||
|
||
if (tag != NULL)
|
||
*tag = NULL;
|
||
}
|
||
|
||
GList *
|
||
flatpak_list_extensions (GKeyFile *metakey,
|
||
const char *arch,
|
||
const char *default_branch)
|
||
{
|
||
g_auto(GStrv) groups = NULL;
|
||
int i, j;
|
||
GList *res;
|
||
|
||
res = NULL;
|
||
|
||
if (arch == NULL)
|
||
arch = flatpak_get_arch ();
|
||
|
||
groups = g_key_file_get_groups (metakey, NULL);
|
||
for (i = 0; groups[i] != NULL; i++)
|
||
{
|
||
char *extension;
|
||
|
||
if (g_str_has_prefix (groups[i], FLATPAK_METADATA_GROUP_PREFIX_EXTENSION) &&
|
||
*(extension = (groups[i] + strlen (FLATPAK_METADATA_GROUP_PREFIX_EXTENSION))) != 0)
|
||
{
|
||
g_autofree char *version = g_key_file_get_string (metakey, groups[i],
|
||
FLATPAK_METADATA_KEY_VERSION,
|
||
NULL);
|
||
g_auto(GStrv) versions = g_key_file_get_string_list (metakey, groups[i],
|
||
FLATPAK_METADATA_KEY_VERSIONS,
|
||
NULL, NULL);
|
||
g_autofree char *name = NULL;
|
||
const char *default_branches[] = { default_branch, NULL};
|
||
const char **branches;
|
||
|
||
flatpak_parse_extension_with_tag (extension, &name, NULL);
|
||
|
||
if (versions)
|
||
branches = (const char **) versions;
|
||
else
|
||
{
|
||
if (version)
|
||
default_branches[0] = version;
|
||
branches = default_branches;
|
||
}
|
||
|
||
for (j = 0; branches[j] != NULL; j++)
|
||
res = add_extension (metakey, groups[i], name, arch, branches[j], res);
|
||
}
|
||
}
|
||
|
||
return g_list_sort (g_list_reverse (res), flatpak_extension_compare);
|
||
}
|
||
|
||
typedef struct
|
||
{
|
||
FlatpakXml *current;
|
||
} XmlData;
|
||
|
||
FlatpakXml *
|
||
flatpak_xml_new (const gchar *element_name)
|
||
{
|
||
FlatpakXml *node = g_new0 (FlatpakXml, 1);
|
||
|
||
node->element_name = g_strdup (element_name);
|
||
return node;
|
||
}
|
||
|
||
FlatpakXml *
|
||
flatpak_xml_new_text (const gchar *text)
|
||
{
|
||
FlatpakXml *node = g_new0 (FlatpakXml, 1);
|
||
|
||
node->text = g_strdup (text);
|
||
return node;
|
||
}
|
||
|
||
void
|
||
flatpak_xml_add (FlatpakXml *parent, FlatpakXml *node)
|
||
{
|
||
node->parent = parent;
|
||
|
||
if (parent->first_child == NULL)
|
||
parent->first_child = node;
|
||
else
|
||
parent->last_child->next_sibling = node;
|
||
parent->last_child = node;
|
||
}
|
||
|
||
static void
|
||
xml_start_element (GMarkupParseContext *context,
|
||
const gchar *element_name,
|
||
const gchar **attribute_names,
|
||
const gchar **attribute_values,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
XmlData *data = user_data;
|
||
FlatpakXml *node;
|
||
|
||
node = flatpak_xml_new (element_name);
|
||
node->attribute_names = g_strdupv ((char **) attribute_names);
|
||
node->attribute_values = g_strdupv ((char **) attribute_values);
|
||
|
||
flatpak_xml_add (data->current, node);
|
||
data->current = node;
|
||
}
|
||
|
||
static void
|
||
xml_end_element (GMarkupParseContext *context,
|
||
const gchar *element_name,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
XmlData *data = user_data;
|
||
|
||
data->current = data->current->parent;
|
||
}
|
||
|
||
static void
|
||
xml_text (GMarkupParseContext *context,
|
||
const gchar *text,
|
||
gsize text_len,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
XmlData *data = user_data;
|
||
FlatpakXml *node;
|
||
|
||
node = flatpak_xml_new (NULL);
|
||
node->text = g_strndup (text, text_len);
|
||
flatpak_xml_add (data->current, node);
|
||
}
|
||
|
||
static void
|
||
xml_passthrough (GMarkupParseContext *context,
|
||
const gchar *passthrough_text,
|
||
gsize text_len,
|
||
gpointer user_data,
|
||
GError **error)
|
||
{
|
||
}
|
||
|
||
static GMarkupParser xml_parser = {
|
||
xml_start_element,
|
||
xml_end_element,
|
||
xml_text,
|
||
xml_passthrough,
|
||
NULL
|
||
};
|
||
|
||
void
|
||
flatpak_xml_free (FlatpakXml *node)
|
||
{
|
||
FlatpakXml *child;
|
||
|
||
if (node == NULL)
|
||
return;
|
||
|
||
child = node->first_child;
|
||
while (child != NULL)
|
||
{
|
||
FlatpakXml *next = child->next_sibling;
|
||
flatpak_xml_free (child);
|
||
child = next;
|
||
}
|
||
|
||
g_free (node->element_name);
|
||
g_free (node->text);
|
||
g_strfreev (node->attribute_names);
|
||
g_strfreev (node->attribute_values);
|
||
g_free (node);
|
||
}
|
||
|
||
|
||
void
|
||
flatpak_xml_to_string (FlatpakXml *node, GString *res)
|
||
{
|
||
int i;
|
||
FlatpakXml *child;
|
||
|
||
if (node->parent == NULL)
|
||
g_string_append (res, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||
|
||
if (node->element_name)
|
||
{
|
||
if (node->parent != NULL)
|
||
{
|
||
g_string_append (res, "<");
|
||
g_string_append (res, node->element_name);
|
||
if (node->attribute_names)
|
||
{
|
||
for (i = 0; node->attribute_names[i] != NULL; i++)
|
||
{
|
||
g_string_append_printf (res, " %s=\"%s\"",
|
||
node->attribute_names[i],
|
||
node->attribute_values[i]);
|
||
}
|
||
}
|
||
if (node->first_child == NULL)
|
||
g_string_append (res, "/>");
|
||
else
|
||
g_string_append (res, ">");
|
||
}
|
||
|
||
child = node->first_child;
|
||
while (child != NULL)
|
||
{
|
||
flatpak_xml_to_string (child, res);
|
||
child = child->next_sibling;
|
||
}
|
||
if (node->parent != NULL)
|
||
{
|
||
if (node->first_child != NULL)
|
||
g_string_append_printf (res, "</%s>", node->element_name);
|
||
}
|
||
}
|
||
else if (node->text)
|
||
{
|
||
g_autofree char *escaped = g_markup_escape_text (node->text, -1);
|
||
g_string_append (res, escaped);
|
||
}
|
||
}
|
||
|
||
FlatpakXml *
|
||
flatpak_xml_unlink (FlatpakXml *node,
|
||
FlatpakXml *prev_sibling)
|
||
{
|
||
FlatpakXml *parent = node->parent;
|
||
|
||
if (parent == NULL)
|
||
return node;
|
||
|
||
if (parent->first_child == node)
|
||
parent->first_child = node->next_sibling;
|
||
|
||
if (parent->last_child == node)
|
||
parent->last_child = prev_sibling;
|
||
|
||
if (prev_sibling)
|
||
prev_sibling->next_sibling = node->next_sibling;
|
||
|
||
node->parent = NULL;
|
||
node->next_sibling = NULL;
|
||
|
||
return node;
|
||
}
|
||
|
||
FlatpakXml *
|
||
flatpak_xml_find (FlatpakXml *node,
|
||
const char *type,
|
||
FlatpakXml **prev_child_out)
|
||
{
|
||
FlatpakXml *child = NULL;
|
||
FlatpakXml *prev_child = NULL;
|
||
|
||
child = node->first_child;
|
||
prev_child = NULL;
|
||
while (child != NULL)
|
||
{
|
||
FlatpakXml *next = child->next_sibling;
|
||
|
||
if (g_strcmp0 (child->element_name, type) == 0)
|
||
{
|
||
if (prev_child_out)
|
||
*prev_child_out = prev_child;
|
||
return child;
|
||
}
|
||
|
||
prev_child = child;
|
||
child = next;
|
||
}
|
||
|
||
return NULL;
|
||
}
|
||
|
||
|
||
FlatpakXml *
|
||
flatpak_xml_parse (GInputStream *in,
|
||
gboolean compressed,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GInputStream) real_in = NULL;
|
||
g_autoptr(FlatpakXml) xml_root = NULL;
|
||
XmlData data = { 0 };
|
||
char buffer[32 * 1024];
|
||
gssize len;
|
||
g_autoptr(GMarkupParseContext) ctx = NULL;
|
||
|
||
if (compressed)
|
||
{
|
||
g_autoptr(GZlibDecompressor) decompressor = NULL;
|
||
decompressor = g_zlib_decompressor_new (G_ZLIB_COMPRESSOR_FORMAT_GZIP);
|
||
real_in = g_converter_input_stream_new (in, G_CONVERTER (decompressor));
|
||
}
|
||
else
|
||
{
|
||
real_in = g_object_ref (in);
|
||
}
|
||
|
||
xml_root = flatpak_xml_new ("root");
|
||
data.current = xml_root;
|
||
|
||
ctx = g_markup_parse_context_new (&xml_parser,
|
||
G_MARKUP_PREFIX_ERROR_POSITION,
|
||
&data,
|
||
NULL);
|
||
|
||
while ((len = g_input_stream_read (real_in, buffer, sizeof (buffer),
|
||
cancellable, error)) > 0)
|
||
{
|
||
if (!g_markup_parse_context_parse (ctx, buffer, len, error))
|
||
return NULL;
|
||
}
|
||
|
||
if (len < 0)
|
||
return NULL;
|
||
|
||
return g_steal_pointer (&xml_root);
|
||
}
|
||
|
||
#define OSTREE_STATIC_DELTA_META_ENTRY_FORMAT "(uayttay)"
|
||
#define OSTREE_STATIC_DELTA_FALLBACK_FORMAT "(yaytt)"
|
||
#define OSTREE_STATIC_DELTA_SUPERBLOCK_FORMAT "(a{sv}tayay" OSTREE_COMMIT_GVARIANT_STRING "aya" OSTREE_STATIC_DELTA_META_ENTRY_FORMAT "a" OSTREE_STATIC_DELTA_FALLBACK_FORMAT ")"
|
||
|
||
static inline guint64
|
||
maybe_swap_endian_u64 (gboolean swap,
|
||
guint64 v)
|
||
{
|
||
if (!swap)
|
||
return v;
|
||
return GUINT64_SWAP_LE_BE (v);
|
||
}
|
||
|
||
static guint64
|
||
flatpak_bundle_get_installed_size (GVariant *bundle, gboolean byte_swap)
|
||
{
|
||
guint64 total_usize = 0;
|
||
g_autoptr(GVariant) meta_entries = NULL;
|
||
guint i, n_parts;
|
||
|
||
g_variant_get_child (bundle, 6, "@a" OSTREE_STATIC_DELTA_META_ENTRY_FORMAT, &meta_entries);
|
||
n_parts = g_variant_n_children (meta_entries);
|
||
|
||
for (i = 0; i < n_parts; i++)
|
||
{
|
||
guint32 version;
|
||
guint64 size, usize;
|
||
g_autoptr(GVariant) objects = NULL;
|
||
|
||
g_variant_get_child (meta_entries, i, "(u@aytt@ay)",
|
||
&version, NULL, &size, &usize, &objects);
|
||
|
||
total_usize += maybe_swap_endian_u64 (byte_swap, usize);
|
||
}
|
||
|
||
return total_usize;
|
||
}
|
||
|
||
GVariant *
|
||
flatpak_bundle_load (GFile *file,
|
||
char **commit,
|
||
FlatpakDecomposed **ref,
|
||
char **origin,
|
||
char **runtime_repo,
|
||
char **app_metadata,
|
||
guint64 *installed_size,
|
||
GBytes **gpg_keys,
|
||
char **collection_id,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GVariant) delta = NULL;
|
||
g_autoptr(GVariant) metadata = NULL;
|
||
g_autoptr(GBytes) bytes = NULL;
|
||
g_autoptr(GBytes) copy = NULL;
|
||
g_autoptr(GVariant) to_csum_v = NULL;
|
||
|
||
guint8 endianness_char;
|
||
gboolean byte_swap = FALSE;
|
||
|
||
GMappedFile *mfile = g_mapped_file_new (flatpak_file_get_path_cached (file), FALSE, error);
|
||
|
||
if (mfile == NULL)
|
||
return NULL;
|
||
|
||
bytes = g_mapped_file_get_bytes (mfile);
|
||
g_mapped_file_unref (mfile);
|
||
|
||
delta = g_variant_new_from_bytes (G_VARIANT_TYPE (OSTREE_STATIC_DELTA_SUPERBLOCK_FORMAT), bytes, FALSE);
|
||
g_variant_ref_sink (delta);
|
||
|
||
to_csum_v = g_variant_get_child_value (delta, 3);
|
||
if (!ostree_validate_structureof_csum_v (to_csum_v, error))
|
||
return NULL;
|
||
|
||
metadata = g_variant_get_child_value (delta, 0);
|
||
|
||
if (g_variant_lookup (metadata, "ostree.endianness", "y", &endianness_char))
|
||
{
|
||
int file_byte_order = G_BYTE_ORDER;
|
||
switch (endianness_char)
|
||
{
|
||
case 'l':
|
||
file_byte_order = G_LITTLE_ENDIAN;
|
||
break;
|
||
|
||
case 'B':
|
||
file_byte_order = G_BIG_ENDIAN;
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
byte_swap = (G_BYTE_ORDER != file_byte_order);
|
||
}
|
||
|
||
if (commit)
|
||
*commit = ostree_checksum_from_bytes_v (to_csum_v);
|
||
|
||
if (installed_size)
|
||
*installed_size = flatpak_bundle_get_installed_size (delta, byte_swap);
|
||
|
||
if (ref != NULL)
|
||
{
|
||
FlatpakDecomposed *the_ref = NULL;
|
||
g_autofree char *ref_str = NULL;
|
||
|
||
if (!g_variant_lookup (metadata, "ref", "s", &ref_str))
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid bundle, no ref in metadata"));
|
||
return NULL;
|
||
}
|
||
|
||
the_ref = flatpak_decomposed_new_from_ref (ref_str, error);
|
||
if (the_ref == NULL)
|
||
return NULL;
|
||
|
||
g_clear_pointer (ref, flatpak_decomposed_unref);
|
||
*ref = the_ref;
|
||
}
|
||
|
||
if (origin != NULL)
|
||
{
|
||
if (!g_variant_lookup (metadata, "origin", "s", origin))
|
||
*origin = NULL;
|
||
}
|
||
|
||
if (runtime_repo != NULL)
|
||
{
|
||
if (!g_variant_lookup (metadata, "runtime-repo", "s", runtime_repo))
|
||
*runtime_repo = NULL;
|
||
}
|
||
|
||
if (collection_id != NULL)
|
||
{
|
||
if (!g_variant_lookup (metadata, "collection-id", "s", collection_id))
|
||
{
|
||
*collection_id = NULL;
|
||
}
|
||
else if (**collection_id == '\0')
|
||
{
|
||
g_free (*collection_id);
|
||
*collection_id = NULL;
|
||
}
|
||
}
|
||
|
||
if (app_metadata != NULL)
|
||
{
|
||
if (!g_variant_lookup (metadata, "metadata", "s", app_metadata))
|
||
*app_metadata = NULL;
|
||
}
|
||
|
||
if (gpg_keys != NULL)
|
||
{
|
||
g_autoptr(GVariant) gpg_value = g_variant_lookup_value (metadata, "gpg-keys",
|
||
G_VARIANT_TYPE ("ay"));
|
||
if (gpg_value)
|
||
{
|
||
gsize n_elements;
|
||
const char *data = g_variant_get_fixed_array (gpg_value, &n_elements, 1);
|
||
*gpg_keys = g_bytes_new (data, n_elements);
|
||
}
|
||
else
|
||
{
|
||
*gpg_keys = NULL;
|
||
}
|
||
}
|
||
|
||
/* Make a copy of the data so we can return it after freeing the file */
|
||
copy = g_bytes_new (g_variant_get_data (metadata),
|
||
g_variant_get_size (metadata));
|
||
return g_variant_ref_sink (g_variant_new_from_bytes (g_variant_get_type (metadata),
|
||
copy,
|
||
FALSE));
|
||
}
|
||
|
||
gboolean
|
||
flatpak_pull_from_bundle (OstreeRepo *repo,
|
||
GFile *file,
|
||
const char *remote,
|
||
const char *ref,
|
||
gboolean require_gpg_signature,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gsize metadata_size = 0;
|
||
g_autofree char *metadata_contents = NULL;
|
||
g_autofree char *to_checksum = NULL;
|
||
g_autoptr(GFile) root = NULL;
|
||
g_autoptr(GFile) metadata_file = NULL;
|
||
g_autoptr(GInputStream) in = NULL;
|
||
g_autoptr(OstreeGpgVerifyResult) gpg_result = NULL;
|
||
g_autoptr(GError) my_error = NULL;
|
||
g_autoptr(GVariant) metadata = NULL;
|
||
gboolean metadata_valid;
|
||
g_autofree char *remote_collection_id = NULL;
|
||
g_autofree char *collection_id = NULL;
|
||
|
||
metadata = flatpak_bundle_load (file, &to_checksum, NULL, NULL, NULL, &metadata_contents, NULL, NULL, &collection_id, error);
|
||
if (metadata == NULL)
|
||
return FALSE;
|
||
|
||
if (metadata_contents != NULL)
|
||
metadata_size = strlen (metadata_contents);
|
||
|
||
if (!ostree_repo_get_remote_option (repo, remote, "collection-id", NULL,
|
||
&remote_collection_id, NULL))
|
||
remote_collection_id = NULL;
|
||
|
||
if (remote_collection_id != NULL && collection_id != NULL &&
|
||
strcmp (remote_collection_id, collection_id) != 0)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Collection ‘%s’ of bundle doesn’t match collection ‘%s’ of remote"),
|
||
collection_id, remote_collection_id);
|
||
|
||
if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
/* Don’t need to set the collection ID here, since the remote binds this ref to the collection. */
|
||
ostree_repo_transaction_set_ref (repo, remote, ref, to_checksum);
|
||
|
||
if (!ostree_repo_static_delta_execute_offline (repo,
|
||
file,
|
||
FALSE,
|
||
cancellable,
|
||
error))
|
||
return FALSE;
|
||
|
||
gpg_result = ostree_repo_verify_commit_ext (repo, to_checksum,
|
||
NULL, NULL, cancellable, &my_error);
|
||
if (gpg_result == NULL)
|
||
{
|
||
/* no gpg signature, we ignore this *if* there is no gpg key
|
||
* specified in the bundle or by the user */
|
||
if (g_error_matches (my_error, OSTREE_GPG_ERROR, OSTREE_GPG_ERROR_NO_SIGNATURE) &&
|
||
!require_gpg_signature)
|
||
{
|
||
g_clear_error (&my_error);
|
||
}
|
||
else
|
||
{
|
||
g_propagate_error (error, g_steal_pointer (&my_error));
|
||
return FALSE;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
/* If there is no valid gpg signature we fail, unless there is no gpg
|
||
key specified (on the command line or in the file) because then we
|
||
trust the source bundle. */
|
||
if (ostree_gpg_verify_result_count_valid (gpg_result) == 0 &&
|
||
require_gpg_signature)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_UNTRUSTED, _("GPG signatures found, but none are in trusted keyring"));
|
||
}
|
||
|
||
if (!ostree_repo_read_commit (repo, to_checksum, &root, NULL, NULL, error))
|
||
return FALSE;
|
||
|
||
if (!ostree_repo_commit_transaction (repo, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
/* We ensure that the actual installed metadata matches the one in the
|
||
header, because you may have made decisions on whether to install it or not
|
||
based on that data. */
|
||
metadata_file = g_file_resolve_relative_path (root, "metadata");
|
||
in = (GInputStream *) g_file_read (metadata_file, cancellable, NULL);
|
||
if (in != NULL)
|
||
{
|
||
g_autoptr(GMemoryOutputStream) data_stream = (GMemoryOutputStream *) g_memory_output_stream_new_resizable ();
|
||
|
||
if (g_output_stream_splice (G_OUTPUT_STREAM (data_stream), in,
|
||
G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE,
|
||
cancellable, error) < 0)
|
||
return FALSE;
|
||
|
||
metadata_valid =
|
||
metadata_contents != NULL &&
|
||
metadata_size == g_memory_output_stream_get_data_size (data_stream) &&
|
||
memcmp (metadata_contents, g_memory_output_stream_get_data (data_stream), metadata_size) == 0;
|
||
}
|
||
else
|
||
{
|
||
metadata_valid = (metadata_contents == NULL);
|
||
}
|
||
|
||
if (!metadata_valid)
|
||
{
|
||
/* Immediately remove this broken commit */
|
||
ostree_repo_set_ref_immediate (repo, remote, ref, NULL, cancellable, error);
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Metadata in header and app are inconsistent"));
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
typedef struct
|
||
{
|
||
FlatpakOciPullProgress progress_cb;
|
||
gpointer progress_user_data;
|
||
guint64 total_size;
|
||
guint64 previous_layers_size;
|
||
guint32 n_layers;
|
||
guint32 pulled_layers;
|
||
} FlatpakOciPullProgressData;
|
||
|
||
static void
|
||
oci_layer_progress (guint64 downloaded_bytes,
|
||
gpointer user_data)
|
||
{
|
||
FlatpakOciPullProgressData *progress_data = user_data;
|
||
|
||
if (progress_data->progress_cb)
|
||
progress_data->progress_cb (progress_data->total_size, progress_data->previous_layers_size + downloaded_bytes,
|
||
progress_data->n_layers, progress_data->pulled_layers,
|
||
progress_data->progress_user_data);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_mirror_image_from_oci (FlatpakOciRegistry *dst_registry,
|
||
FlatpakOciRegistry *registry,
|
||
const char *oci_repository,
|
||
const char *digest,
|
||
const char *remote,
|
||
const char *ref,
|
||
const char *delta_url,
|
||
OstreeRepo *repo,
|
||
FlatpakOciPullProgress progress_cb,
|
||
gpointer progress_user_data,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
FlatpakOciPullProgressData progress_data = { progress_cb, progress_user_data };
|
||
g_autoptr(FlatpakOciVersioned) versioned = NULL;
|
||
FlatpakOciManifest *manifest = NULL;
|
||
g_autoptr(FlatpakOciDescriptor) manifest_desc = NULL;
|
||
g_autoptr(FlatpakOciManifest) delta_manifest = NULL;
|
||
g_autofree char *old_checksum = NULL;
|
||
g_autoptr(GVariant) old_commit = NULL;
|
||
g_autoptr(GFile) old_root = NULL;
|
||
OstreeRepoCommitState old_state = 0;
|
||
g_autofree char *old_diffid = NULL;
|
||
gsize versioned_size;
|
||
g_autoptr(FlatpakOciIndex) index = NULL;
|
||
g_autoptr(FlatpakOciImage) image_config = NULL;
|
||
int n_layers;
|
||
int i;
|
||
|
||
if (!flatpak_oci_registry_mirror_blob (dst_registry, registry, oci_repository, TRUE, digest, NULL, NULL, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
versioned = flatpak_oci_registry_load_versioned (dst_registry, NULL, digest, NULL, &versioned_size, cancellable, error);
|
||
if (versioned == NULL)
|
||
return FALSE;
|
||
|
||
if (!FLATPAK_IS_OCI_MANIFEST (versioned))
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest"));
|
||
|
||
manifest = FLATPAK_OCI_MANIFEST (versioned);
|
||
|
||
if (manifest->config.digest == NULL)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Image is not a manifest"));
|
||
|
||
if (!flatpak_oci_registry_mirror_blob (dst_registry, registry, oci_repository, FALSE, manifest->config.digest, (const char **)manifest->config.urls, NULL, NULL, cancellable, error))
|
||
return FALSE;
|
||
|
||
image_config = flatpak_oci_registry_load_image_config (dst_registry, NULL,
|
||
manifest->config.digest, NULL,
|
||
NULL, cancellable, error);
|
||
if (image_config == NULL)
|
||
return FALSE;
|
||
|
||
/* For deltas we ensure that the diffid and regular layers exists and match up */
|
||
n_layers = flatpak_oci_manifest_get_n_layers (manifest);
|
||
if (n_layers == 0 || n_layers != flatpak_oci_image_get_n_layers (image_config))
|
||
return flatpak_fail (error, _("Invalid OCI image config"));
|
||
|
||
/* Look for delta manifest, and if it exists, the current (old) commit and its recorded diffid */
|
||
if (flatpak_repo_resolve_rev (repo, NULL, remote, ref, FALSE, &old_checksum, NULL, NULL) &&
|
||
ostree_repo_load_commit (repo, old_checksum, &old_commit, &old_state, NULL) &&
|
||
(old_state == OSTREE_REPO_COMMIT_STATE_NORMAL) &&
|
||
ostree_repo_read_commit (repo, old_checksum, &old_root, NULL, NULL, NULL))
|
||
{
|
||
delta_manifest = flatpak_oci_registry_find_delta_manifest (registry, oci_repository, digest, delta_url, cancellable);
|
||
if (delta_manifest)
|
||
{
|
||
VarMetadataRef commit_metadata = var_commit_get_metadata (var_commit_from_gvariant (old_commit));
|
||
const char *raw_old_diffid = var_metadata_lookup_string (commit_metadata, "xa.diff-id", NULL);
|
||
if (raw_old_diffid != NULL)
|
||
old_diffid = g_strconcat ("sha256:", raw_old_diffid, NULL);
|
||
}
|
||
}
|
||
|
||
for (i = 0; manifest->layers[i] != NULL; i++)
|
||
{
|
||
FlatpakOciDescriptor *layer = manifest->layers[i];
|
||
FlatpakOciDescriptor *delta_layer = NULL;
|
||
|
||
if (delta_manifest)
|
||
delta_layer = flatpak_oci_manifest_find_delta_for (delta_manifest, old_diffid, image_config->rootfs.diff_ids[i]);
|
||
|
||
if (delta_layer)
|
||
progress_data.total_size += delta_layer->size;
|
||
else
|
||
progress_data.total_size += layer->size;
|
||
progress_data.n_layers++;
|
||
}
|
||
|
||
if (progress_cb)
|
||
progress_cb (progress_data.total_size, 0,
|
||
progress_data.n_layers, progress_data.pulled_layers,
|
||
progress_user_data);
|
||
|
||
for (i = 0; manifest->layers[i] != NULL; i++)
|
||
{
|
||
FlatpakOciDescriptor *layer = manifest->layers[i];
|
||
FlatpakOciDescriptor *delta_layer = NULL;
|
||
|
||
if (delta_manifest)
|
||
delta_layer = flatpak_oci_manifest_find_delta_for (delta_manifest, old_diffid, image_config->rootfs.diff_ids[i]);
|
||
|
||
if (delta_layer)
|
||
{
|
||
g_info ("Using OCI delta %s for layer %s", delta_layer->digest, layer->digest);
|
||
g_autofree char *delta_digest = NULL;
|
||
glnx_autofd int delta_fd = flatpak_oci_registry_download_blob (registry, oci_repository, FALSE,
|
||
delta_layer->digest, (const char **)delta_layer->urls,
|
||
oci_layer_progress, &progress_data,
|
||
cancellable, error);
|
||
if (delta_fd == -1)
|
||
return FALSE;
|
||
|
||
delta_digest = flatpak_oci_registry_apply_delta_to_blob (dst_registry, delta_fd, old_root, cancellable, error);
|
||
if (delta_digest == NULL)
|
||
return FALSE;
|
||
|
||
if (g_strcmp0 (delta_digest, image_config->rootfs.diff_ids[i]) != 0)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Wrong layer checksum, expected %s, was %s"), image_config->rootfs.diff_ids[i], delta_digest);
|
||
}
|
||
else
|
||
{
|
||
if (!flatpak_oci_registry_mirror_blob (dst_registry, registry, oci_repository, FALSE, layer->digest, (const char **)layer->urls,
|
||
oci_layer_progress, &progress_data,
|
||
cancellable, error))
|
||
return FALSE;
|
||
}
|
||
|
||
progress_data.pulled_layers++;
|
||
progress_data.previous_layers_size += delta_layer ? delta_layer->size : layer->size;
|
||
}
|
||
|
||
index = flatpak_oci_registry_load_index (dst_registry, NULL, NULL);
|
||
if (index == NULL)
|
||
index = flatpak_oci_index_new ();
|
||
|
||
manifest_desc = flatpak_oci_descriptor_new (versioned->mediatype, digest, versioned_size);
|
||
|
||
flatpak_oci_index_add_manifest (index, ref, manifest_desc);
|
||
|
||
if (!flatpak_oci_registry_save_index (dst_registry, index, cancellable, error))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
char *
|
||
flatpak_pull_from_oci (OstreeRepo *repo,
|
||
FlatpakOciRegistry *registry,
|
||
const char *oci_repository,
|
||
const char *digest,
|
||
const char *delta_url,
|
||
FlatpakOciManifest *manifest,
|
||
FlatpakOciImage *image_config,
|
||
const char *remote,
|
||
const char *ref,
|
||
FlatpakPullFlags flags,
|
||
FlatpakOciPullProgress progress_cb,
|
||
gpointer progress_user_data,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gboolean force_disable_deltas = (flags & FLATPAK_PULL_FLAGS_NO_STATIC_DELTAS) != 0;
|
||
g_autoptr(OstreeMutableTree) archive_mtree = NULL;
|
||
g_autoptr(GFile) archive_root = NULL;
|
||
g_autoptr(FlatpakOciManifest) delta_manifest = NULL;
|
||
g_autofree char *old_checksum = NULL;
|
||
g_autoptr(GVariant) old_commit = NULL;
|
||
g_autoptr(GFile) old_root = NULL;
|
||
OstreeRepoCommitState old_state = 0;
|
||
g_autofree char *old_diffid = NULL;
|
||
g_autofree char *commit_checksum = NULL;
|
||
const char *parent = NULL;
|
||
g_autofree char *subject = NULL;
|
||
g_autofree char *body = NULL;
|
||
g_autofree char *manifest_ref = NULL;
|
||
g_autofree char *full_ref = NULL;
|
||
const char *diffid;
|
||
guint64 timestamp = 0;
|
||
FlatpakOciPullProgressData progress_data = { progress_cb, progress_user_data };
|
||
g_autoptr(GVariantBuilder) metadata_builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}"));
|
||
g_autoptr(GVariant) metadata = NULL;
|
||
GHashTable *labels;
|
||
int n_layers;
|
||
int i;
|
||
|
||
g_assert (ref != NULL);
|
||
g_assert (g_str_has_prefix (digest, "sha256:"));
|
||
|
||
labels = flatpak_oci_image_get_labels (image_config);
|
||
if (labels)
|
||
flatpak_oci_parse_commit_labels (labels, ×tamp,
|
||
&subject, &body,
|
||
&manifest_ref, NULL, NULL,
|
||
metadata_builder);
|
||
|
||
if (manifest_ref == NULL)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("No ref specified for OCI image %s"), digest);
|
||
return NULL;
|
||
}
|
||
|
||
if (strcmp (manifest_ref, ref) != 0)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Wrong ref (%s) specified for OCI image %s, expected %s"), manifest_ref, digest, ref);
|
||
return NULL;
|
||
}
|
||
|
||
g_variant_builder_add (metadata_builder, "{s@v}", "xa.alt-id",
|
||
g_variant_new_variant (g_variant_new_string (digest + strlen ("sha256:"))));
|
||
|
||
/* For deltas we ensure that the diffid and regular layers exists and match up */
|
||
n_layers = flatpak_oci_manifest_get_n_layers (manifest);
|
||
if (n_layers == 0 || n_layers != flatpak_oci_image_get_n_layers (image_config))
|
||
{
|
||
flatpak_fail (error, _("Invalid OCI image config"));
|
||
return NULL;
|
||
}
|
||
|
||
/* Assuming everyting looks good, we record the uncompressed checksum (the diff-id) of the last layer,
|
||
because that is what we can read back easily from the deploy dir, and thus is easy to use for applying deltas */
|
||
diffid = image_config->rootfs.diff_ids[n_layers-1];
|
||
if (diffid != NULL && g_str_has_prefix (diffid, "sha256:"))
|
||
g_variant_builder_add (metadata_builder, "{s@v}", "xa.diff-id",
|
||
g_variant_new_variant (g_variant_new_string (diffid + strlen ("sha256:"))));
|
||
|
||
/* Look for delta manifest, and if it exists, the current (old) commit and its recorded diffid */
|
||
if (!force_disable_deltas &&
|
||
!flatpak_oci_registry_is_local (registry) &&
|
||
flatpak_repo_resolve_rev (repo, NULL, remote, ref, FALSE, &old_checksum, NULL, NULL) &&
|
||
ostree_repo_load_commit (repo, old_checksum, &old_commit, &old_state, NULL) &&
|
||
(old_state == OSTREE_REPO_COMMIT_STATE_NORMAL) &&
|
||
ostree_repo_read_commit (repo, old_checksum, &old_root, NULL, NULL, NULL))
|
||
{
|
||
delta_manifest = flatpak_oci_registry_find_delta_manifest (registry, oci_repository, digest, delta_url, cancellable);
|
||
if (delta_manifest)
|
||
{
|
||
VarMetadataRef commit_metadata = var_commit_get_metadata (var_commit_from_gvariant (old_commit));
|
||
const char *raw_old_diffid = var_metadata_lookup_string (commit_metadata, "xa.diff-id", NULL);
|
||
if (raw_old_diffid != NULL)
|
||
old_diffid = g_strconcat ("sha256:", raw_old_diffid, NULL);
|
||
}
|
||
}
|
||
|
||
if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error))
|
||
return NULL;
|
||
|
||
/* There is no way to write a subset of the archive to a mtree, so instead
|
||
we write all of it and then build a new mtree with the subset */
|
||
archive_mtree = ostree_mutable_tree_new ();
|
||
|
||
for (i = 0; manifest->layers[i] != NULL; i++)
|
||
{
|
||
FlatpakOciDescriptor *layer = manifest->layers[i];
|
||
FlatpakOciDescriptor *delta_layer = NULL;
|
||
|
||
if (delta_manifest)
|
||
delta_layer = flatpak_oci_manifest_find_delta_for (delta_manifest, old_diffid, image_config->rootfs.diff_ids[i]);
|
||
|
||
if (delta_layer)
|
||
progress_data.total_size += delta_layer->size;
|
||
else
|
||
progress_data.total_size += layer->size;
|
||
|
||
progress_data.n_layers++;
|
||
}
|
||
|
||
if (progress_cb)
|
||
progress_cb (progress_data.total_size, 0,
|
||
progress_data.n_layers, progress_data.pulled_layers,
|
||
progress_user_data);
|
||
|
||
for (i = 0; manifest->layers[i] != NULL; i++)
|
||
{
|
||
FlatpakOciDescriptor *layer = manifest->layers[i];
|
||
FlatpakOciDescriptor *delta_layer = NULL;
|
||
OstreeRepoImportArchiveOptions opts = { 0, };
|
||
g_autoptr(FlatpakAutoArchiveRead) a = NULL;
|
||
glnx_autofd int layer_fd = -1;
|
||
glnx_autofd int blob_fd = -1;
|
||
g_autoptr(GChecksum) checksum = g_checksum_new (G_CHECKSUM_SHA256);
|
||
g_autoptr(GError) local_error = NULL;
|
||
const char *layer_checksum;
|
||
const char *expected_digest;
|
||
|
||
if (delta_manifest)
|
||
delta_layer = flatpak_oci_manifest_find_delta_for (delta_manifest, old_diffid, image_config->rootfs.diff_ids[i]);
|
||
|
||
opts.autocreate_parents = TRUE;
|
||
opts.ignore_unsupported_content = TRUE;
|
||
|
||
if (delta_layer)
|
||
{
|
||
g_info ("Using OCI delta %s for layer %s", delta_layer->digest, layer->digest);
|
||
expected_digest = image_config->rootfs.diff_ids[i]; /* The delta recreates the uncompressed tar so use that digest */
|
||
}
|
||
else
|
||
{
|
||
layer_fd = glnx_steal_fd (&blob_fd);
|
||
expected_digest = layer->digest;
|
||
}
|
||
|
||
blob_fd = flatpak_oci_registry_download_blob (registry, oci_repository, FALSE,
|
||
delta_layer ? delta_layer->digest : layer->digest,
|
||
(const char **)(delta_layer ? delta_layer->urls : layer->urls),
|
||
oci_layer_progress, &progress_data,
|
||
cancellable, &local_error);
|
||
|
||
if (blob_fd == -1 && delta_layer == NULL &&
|
||
flatpak_oci_registry_is_local (registry) &&
|
||
g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
|
||
{
|
||
/* Pulling regular layer from local repo and its not there, try the uncompressed version.
|
||
* This happens when we deploy via system helper using oci deltas */
|
||
expected_digest = image_config->rootfs.diff_ids[i];
|
||
blob_fd = flatpak_oci_registry_download_blob (registry, oci_repository, FALSE,
|
||
image_config->rootfs.diff_ids[i], NULL,
|
||
oci_layer_progress, &progress_data,
|
||
cancellable, NULL); /* No error here, we report the first error if this failes */
|
||
}
|
||
|
||
if (blob_fd == -1)
|
||
{
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
goto error;
|
||
}
|
||
|
||
g_clear_error (&local_error);
|
||
|
||
if (delta_layer)
|
||
{
|
||
layer_fd = flatpak_oci_registry_apply_delta (registry, blob_fd, old_root, cancellable, error);
|
||
if (layer_fd == -1)
|
||
goto error;
|
||
}
|
||
else
|
||
{
|
||
layer_fd = glnx_steal_fd (&blob_fd);
|
||
}
|
||
|
||
a = archive_read_new ();
|
||
#ifdef HAVE_ARCHIVE_READ_SUPPORT_FILTER_ALL
|
||
archive_read_support_filter_all (a);
|
||
#else
|
||
archive_read_support_compression_all (a);
|
||
#endif
|
||
archive_read_support_format_all (a);
|
||
|
||
if (!flatpak_archive_read_open_fd_with_checksum (a, layer_fd, checksum, error))
|
||
goto error;
|
||
|
||
if (!ostree_repo_import_archive_to_mtree (repo, &opts, a, archive_mtree, NULL, cancellable, error))
|
||
goto error;
|
||
|
||
if (archive_read_close (a) != ARCHIVE_OK)
|
||
{
|
||
propagate_libarchive_error (error, a);
|
||
goto error;
|
||
}
|
||
|
||
layer_checksum = g_checksum_get_string (checksum);
|
||
if (!g_str_has_prefix (expected_digest, "sha256:") ||
|
||
strcmp (expected_digest + strlen ("sha256:"), layer_checksum) != 0)
|
||
{
|
||
flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Wrong layer checksum, expected %s, was %s"), expected_digest, layer_checksum);
|
||
goto error;
|
||
}
|
||
|
||
progress_data.pulled_layers++;
|
||
progress_data.previous_layers_size += delta_layer ? delta_layer->size : layer->size;
|
||
}
|
||
|
||
if (!ostree_repo_write_mtree (repo, archive_mtree, &archive_root, cancellable, error))
|
||
goto error;
|
||
|
||
if (!ostree_repo_file_ensure_resolved ((OstreeRepoFile *) archive_root, error))
|
||
goto error;
|
||
|
||
metadata = g_variant_ref_sink (g_variant_builder_end (metadata_builder));
|
||
if (!ostree_repo_write_commit_with_time (repo,
|
||
parent,
|
||
subject,
|
||
body,
|
||
metadata,
|
||
OSTREE_REPO_FILE (archive_root),
|
||
timestamp,
|
||
&commit_checksum,
|
||
cancellable, error))
|
||
goto error;
|
||
|
||
if (remote)
|
||
full_ref = g_strdup_printf ("%s:%s", remote, ref);
|
||
else
|
||
full_ref = g_strdup (ref);
|
||
|
||
/* Don’t need to set the collection ID here, since the ref is bound to a
|
||
* collection via its remote. */
|
||
ostree_repo_transaction_set_ref (repo, NULL, full_ref, commit_checksum);
|
||
|
||
if (!ostree_repo_commit_transaction (repo, NULL, cancellable, error))
|
||
return NULL;
|
||
|
||
return g_steal_pointer (&commit_checksum);
|
||
|
||
error:
|
||
|
||
ostree_repo_abort_transaction (repo, cancellable, NULL);
|
||
return NULL;
|
||
}
|
||
|
||
/* This allocates and locks a subdir of the tmp dir, using an existing
|
||
* one with the same prefix if it is not in use already. */
|
||
gboolean
|
||
flatpak_allocate_tmpdir (int tmpdir_dfd,
|
||
const char *tmpdir_relpath,
|
||
const char *tmpdir_prefix,
|
||
char **tmpdir_name_out,
|
||
int *tmpdir_fd_out,
|
||
GLnxLockFile *file_lock_out,
|
||
gboolean *reusing_dir_out,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
gboolean reusing_dir = FALSE;
|
||
g_autofree char *tmpdir_name = NULL;
|
||
glnx_autofd int tmpdir_fd = -1;
|
||
g_auto(GLnxDirFdIterator) dfd_iter = { 0, };
|
||
|
||
/* Look for existing tmpdir (with same prefix) to reuse */
|
||
if (!glnx_dirfd_iterator_init_at (tmpdir_dfd, tmpdir_relpath ? tmpdir_relpath : ".", FALSE, &dfd_iter, error))
|
||
return FALSE;
|
||
|
||
while (tmpdir_name == NULL)
|
||
{
|
||
struct dirent *dent;
|
||
glnx_autofd int existing_tmpdir_fd = -1;
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autofree char *lock_name = NULL;
|
||
|
||
if (!glnx_dirfd_iterator_next_dent (&dfd_iter, &dent, cancellable, error))
|
||
return FALSE;
|
||
|
||
if (dent == NULL)
|
||
break;
|
||
|
||
if (!g_str_has_prefix (dent->d_name, tmpdir_prefix))
|
||
continue;
|
||
|
||
/* Quickly skip non-dirs, if unknown we ignore ENOTDIR when opening instead */
|
||
if (dent->d_type != DT_UNKNOWN &&
|
||
dent->d_type != DT_DIR)
|
||
continue;
|
||
|
||
if (!glnx_opendirat (dfd_iter.fd, dent->d_name, FALSE,
|
||
&existing_tmpdir_fd, &local_error))
|
||
{
|
||
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_DIRECTORY))
|
||
{
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
lock_name = g_strconcat (dent->d_name, "-lock", NULL);
|
||
|
||
/* We put the lock outside the dir, so we can hold the lock
|
||
* until the directory is fully removed */
|
||
if (!glnx_make_lock_file (dfd_iter.fd, lock_name, LOCK_EX | LOCK_NB,
|
||
file_lock_out, &local_error))
|
||
{
|
||
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
|
||
{
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
/* Touch the reused directory so that we don't accidentally
|
||
* remove it due to being old when cleaning up the tmpdir
|
||
*/
|
||
(void) futimens (existing_tmpdir_fd, NULL);
|
||
|
||
/* We found an existing tmpdir which we managed to lock */
|
||
tmpdir_name = g_strdup (dent->d_name);
|
||
tmpdir_fd = glnx_steal_fd (&existing_tmpdir_fd);
|
||
reusing_dir = TRUE;
|
||
}
|
||
|
||
while (tmpdir_name == NULL)
|
||
{
|
||
g_autofree char *tmpdir_name_template = g_strconcat (tmpdir_prefix, "XXXXXX", NULL);
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autofree char *lock_name = NULL;
|
||
g_auto(GLnxTmpDir) new_tmpdir = { 0, };
|
||
/* No existing tmpdir found, create a new */
|
||
|
||
if (!glnx_mkdtempat (dfd_iter.fd, tmpdir_name_template, 0777,
|
||
&new_tmpdir, error))
|
||
return FALSE;
|
||
|
||
lock_name = g_strconcat (new_tmpdir.path, "-lock", NULL);
|
||
|
||
/* Note, at this point we can race with another process that picks up this
|
||
* new directory. If that happens we need to retry, making a new directory. */
|
||
if (!glnx_make_lock_file (dfd_iter.fd, lock_name, LOCK_EX | LOCK_NB,
|
||
file_lock_out, &local_error))
|
||
{
|
||
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
|
||
{
|
||
glnx_tmpdir_unset (&new_tmpdir); /* Don't delete */
|
||
continue;
|
||
}
|
||
else
|
||
{
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
tmpdir_name = g_strdup (new_tmpdir.path);
|
||
tmpdir_fd = dup (new_tmpdir.fd);
|
||
glnx_tmpdir_unset (&new_tmpdir); /* Don't delete */
|
||
}
|
||
|
||
if (tmpdir_name_out)
|
||
*tmpdir_name_out = g_steal_pointer (&tmpdir_name);
|
||
|
||
if (tmpdir_fd_out)
|
||
*tmpdir_fd_out = glnx_steal_fd (&tmpdir_fd);
|
||
|
||
if (reusing_dir_out)
|
||
*reusing_dir_out = reusing_dir;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_allow_fuzzy_matching (const char *term)
|
||
{
|
||
if (strchr (term, '/') != NULL || strchr (term, '.') != NULL)
|
||
return FALSE;
|
||
|
||
/* This env var is used by the unit tests and only skips the tty test not the
|
||
* check above.
|
||
*/
|
||
if (g_strcmp0 (g_getenv ("FLATPAK_FORCE_ALLOW_FUZZY_MATCHING"), "1") == 0)
|
||
return TRUE;
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
return FALSE;
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
char *
|
||
flatpak_prompt (gboolean allow_empty,
|
||
const char *prompt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
while (TRUE)
|
||
{
|
||
g_print ("%s: ", s);
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
{
|
||
g_print ("n\n");
|
||
return NULL;
|
||
}
|
||
|
||
if (fgets (buf, sizeof (buf), stdin) == NULL)
|
||
return NULL;
|
||
|
||
g_strstrip (buf);
|
||
|
||
if (buf[0] != 0 || allow_empty)
|
||
return g_strdup (buf);
|
||
}
|
||
}
|
||
|
||
char *
|
||
flatpak_password_prompt (const char *prompt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
gboolean was_echo;
|
||
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
while (TRUE)
|
||
{
|
||
g_print ("%s: ", s);
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
return NULL;
|
||
|
||
was_echo = flatpak_set_tty_echo (FALSE);
|
||
|
||
if (fgets (buf, sizeof (buf), stdin) == NULL)
|
||
return NULL;
|
||
|
||
flatpak_set_tty_echo (was_echo);
|
||
|
||
g_strstrip (buf);
|
||
|
||
/* We stole the return, so manual new line */
|
||
g_print ("\n");
|
||
return g_strdup (buf);
|
||
}
|
||
}
|
||
|
||
|
||
gboolean
|
||
flatpak_yes_no_prompt (gboolean default_yes, const char *prompt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
while (TRUE)
|
||
{
|
||
g_print ("%s %s: ", s, default_yes ? "[Y/n]" : "[y/n]");
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
{
|
||
g_print ("n\n");
|
||
return FALSE;
|
||
}
|
||
|
||
if (fgets (buf, sizeof (buf), stdin) == NULL)
|
||
return FALSE;
|
||
|
||
g_strstrip (buf);
|
||
|
||
if (default_yes && strlen (buf) == 0)
|
||
return TRUE;
|
||
|
||
if (g_ascii_strcasecmp (buf, "y") == 0 ||
|
||
g_ascii_strcasecmp (buf, "yes") == 0)
|
||
return TRUE;
|
||
|
||
if (g_ascii_strcasecmp (buf, "n") == 0 ||
|
||
g_ascii_strcasecmp (buf, "no") == 0)
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
is_number (const char *s)
|
||
{
|
||
if (*s == '\0')
|
||
return FALSE;
|
||
|
||
while (*s != 0)
|
||
{
|
||
if (!g_ascii_isdigit (*s))
|
||
return FALSE;
|
||
s++;
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
long
|
||
flatpak_number_prompt (gboolean default_yes, int min, int max, const char *prompt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
while (TRUE)
|
||
{
|
||
g_print ("%s [%d-%d]: ", s, min, max);
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
{
|
||
g_print ("0\n");
|
||
return 0;
|
||
}
|
||
|
||
if (fgets (buf, sizeof (buf), stdin) == NULL)
|
||
return 0;
|
||
|
||
g_strstrip (buf);
|
||
|
||
if (default_yes && strlen (buf) == 0 &&
|
||
max - min == 1 && min == 0)
|
||
return 1;
|
||
|
||
if (is_number (buf))
|
||
{
|
||
long res = strtol (buf, NULL, 10);
|
||
|
||
if (res >= min && res <= max)
|
||
return res;
|
||
}
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
parse_range (const char *s, int *a, int *b)
|
||
{
|
||
char *p;
|
||
|
||
p = strchr (s, '-');
|
||
if (!p)
|
||
return FALSE;
|
||
|
||
p++;
|
||
p[-1] = '\0';
|
||
|
||
if (is_number (s) && is_number (p))
|
||
{
|
||
*a = (int) strtol (s, NULL, 10);
|
||
*b = (int) strtol (p, NULL, 10);
|
||
p[-1] = '-';
|
||
return TRUE;
|
||
}
|
||
|
||
p[-1] = '-';
|
||
return FALSE;
|
||
}
|
||
|
||
static void
|
||
add_number (GArray *numbers,
|
||
int num)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < numbers->len; i++)
|
||
{
|
||
if (g_array_index (numbers, int, i) == num)
|
||
return;
|
||
}
|
||
|
||
g_array_append_val (numbers, num);
|
||
}
|
||
|
||
int *
|
||
flatpak_parse_numbers (const char *buf,
|
||
int min,
|
||
int max)
|
||
{
|
||
g_autoptr(GArray) numbers = g_array_new (FALSE, FALSE, sizeof (int));
|
||
g_auto(GStrv) parts = g_strsplit_set (buf, " ,", 0);
|
||
int i, j;
|
||
|
||
for (i = 0; parts[i]; i++)
|
||
{
|
||
int a, b;
|
||
|
||
g_strstrip (parts[i]);
|
||
|
||
if (parse_range (parts[i], &a, &b) &&
|
||
min <= a && a <= max &&
|
||
min <= b && b <= max)
|
||
{
|
||
for (j = a; j <= b; j++)
|
||
add_number (numbers, j);
|
||
}
|
||
else if (is_number (parts[i]))
|
||
{
|
||
int res = (int) strtol (parts[i], NULL, 10);
|
||
if (min <= res && res <= max)
|
||
add_number (numbers, res);
|
||
else
|
||
return NULL;
|
||
}
|
||
else
|
||
return NULL;
|
||
}
|
||
|
||
j = 0;
|
||
g_array_append_val (numbers, j);
|
||
|
||
return (int *) g_array_free (g_steal_pointer (&numbers), FALSE);
|
||
}
|
||
|
||
/* Returns a 0-terminated array of ints. Free with g_free */
|
||
int *
|
||
flatpak_numbers_prompt (gboolean default_yes, int min, int max, const char *prompt, ...)
|
||
{
|
||
char buf[512];
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
g_autofree int *choice = g_new0 (int, 2);
|
||
int *numbers;
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
while (TRUE)
|
||
{
|
||
g_print ("%s [%d-%d]: ", s, min, max);
|
||
|
||
if (!isatty (STDIN_FILENO) || !isatty (STDOUT_FILENO))
|
||
{
|
||
g_print ("0\n");
|
||
choice[0] = 0;
|
||
return g_steal_pointer (&choice);
|
||
}
|
||
|
||
if (fgets (buf, sizeof (buf), stdin) == NULL)
|
||
{
|
||
choice[0] = 0;
|
||
return g_steal_pointer (&choice);
|
||
}
|
||
|
||
g_strstrip (buf);
|
||
|
||
if (default_yes && strlen (buf) == 0 &&
|
||
max - min == 1 && min == 0)
|
||
{
|
||
choice[0] = 0;
|
||
return g_steal_pointer (&choice);
|
||
}
|
||
|
||
numbers = flatpak_parse_numbers (buf, min, max);
|
||
if (numbers)
|
||
return numbers;
|
||
}
|
||
}
|
||
|
||
void
|
||
flatpak_format_choices (const char **choices,
|
||
const char *prompt,
|
||
...)
|
||
{
|
||
va_list var_args;
|
||
g_autofree char *s = NULL;
|
||
int i;
|
||
|
||
va_start (var_args, prompt);
|
||
s = g_strdup_vprintf (prompt, var_args);
|
||
va_end (var_args);
|
||
|
||
g_print ("%s\n\n", s);
|
||
for (i = 0; choices[i]; i++)
|
||
g_print (" %2d) %s\n", i + 1, choices[i]);
|
||
g_print ("\n");
|
||
}
|
||
|
||
static gint
|
||
string_length_compare_func (gconstpointer a,
|
||
gconstpointer b)
|
||
{
|
||
return strlen (*(char * const *) a) - strlen (*(char * const *) b);
|
||
}
|
||
|
||
/* Sort a string array by decreasing length */
|
||
char **
|
||
flatpak_strv_sort_by_length (const char * const *strv)
|
||
{
|
||
GPtrArray *array;
|
||
int i;
|
||
|
||
if (strv == NULL)
|
||
return NULL;
|
||
|
||
/* Combine both */
|
||
array = g_ptr_array_new ();
|
||
|
||
for (i = 0; strv[i] != NULL; i++)
|
||
g_ptr_array_add (array, g_strdup (strv[i]));
|
||
|
||
g_ptr_array_sort (array, string_length_compare_func);
|
||
|
||
g_ptr_array_add (array, NULL);
|
||
return (char **) g_ptr_array_free (array, FALSE);
|
||
}
|
||
|
||
char **
|
||
flatpak_strv_merge (char **strv1,
|
||
char **strv2)
|
||
{
|
||
GPtrArray *array;
|
||
int i;
|
||
|
||
/* Maybe either (or both) is unspecified */
|
||
if (strv1 == NULL)
|
||
return g_strdupv (strv2);
|
||
if (strv2 == NULL)
|
||
return g_strdupv (strv1);
|
||
|
||
/* Combine both */
|
||
array = g_ptr_array_new ();
|
||
|
||
for (i = 0; strv1[i] != NULL; i++)
|
||
{
|
||
if (!flatpak_g_ptr_array_contains_string (array, strv1[i]))
|
||
g_ptr_array_add (array, g_strdup (strv1[i]));
|
||
}
|
||
|
||
for (i = 0; strv2[i] != NULL; i++)
|
||
{
|
||
if (!flatpak_g_ptr_array_contains_string (array, strv2[i]))
|
||
g_ptr_array_add (array, g_strdup (strv2[i]));
|
||
}
|
||
|
||
g_ptr_array_add (array, NULL);
|
||
return (char **) g_ptr_array_free (array, FALSE);
|
||
}
|
||
|
||
/* In this NULL means don't care about these paths, while
|
||
an empty array means match anything */
|
||
char **
|
||
flatpak_subpaths_merge (char **subpaths1,
|
||
char **subpaths2)
|
||
{
|
||
char **res;
|
||
|
||
if (subpaths1 != NULL && subpaths1[0] == NULL)
|
||
return g_strdupv (subpaths1);
|
||
if (subpaths2 != NULL && subpaths2[0] == NULL)
|
||
return g_strdupv (subpaths2);
|
||
|
||
res = flatpak_strv_merge (subpaths1, subpaths2);
|
||
if (res)
|
||
qsort (res, g_strv_length (res), sizeof (const char *), flatpak_strcmp0_ptr);
|
||
|
||
return res;
|
||
}
|
||
|
||
const char * const *
|
||
flatpak_get_locale_categories (void)
|
||
{
|
||
/* See locale(7) for these categories */
|
||
static const char * const categories[] = {
|
||
"LANG", "LC_ALL", "LC_MESSAGES", "LC_ADDRESS", "LC_COLLATE", "LC_CTYPE",
|
||
"LC_IDENTIFICATION", "LC_MONETARY", "LC_MEASUREMENT", "LC_NAME", "LC_NUMERIC",
|
||
"LC_PAPER", "LC_TELEPHONE", "LC_TIME",
|
||
NULL
|
||
};
|
||
|
||
return categories;
|
||
}
|
||
|
||
char *
|
||
flatpak_get_lang_from_locale (const char *locale)
|
||
{
|
||
g_autofree char *lang = g_strdup (locale);
|
||
char *c;
|
||
|
||
c = strchr (lang, '@');
|
||
if (c != NULL)
|
||
*c = 0;
|
||
c = strchr (lang, '_');
|
||
if (c != NULL)
|
||
*c = 0;
|
||
c = strchr (lang, '.');
|
||
if (c != NULL)
|
||
*c = 0;
|
||
|
||
if (strcmp (lang, "C") == 0)
|
||
return NULL;
|
||
|
||
return g_steal_pointer (&lang);
|
||
}
|
||
|
||
gboolean
|
||
flatpak_g_ptr_array_contains_string (GPtrArray *array, const char *str)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < array->len; i++)
|
||
{
|
||
if (strcmp (g_ptr_array_index (array, i), str) == 0)
|
||
return TRUE;
|
||
}
|
||
return FALSE;
|
||
}
|
||
|
||
#if !GLIB_CHECK_VERSION (2, 58, 0)
|
||
/* All this code is backported directly from glib 2.66 */
|
||
|
||
typedef struct _GLanguageNamesCache GLanguageNamesCache;
|
||
|
||
struct _GLanguageNamesCache {
|
||
gchar *languages;
|
||
gchar **language_names;
|
||
};
|
||
|
||
static void
|
||
language_names_cache_free (gpointer data)
|
||
{
|
||
GLanguageNamesCache *cache = data;
|
||
g_free (cache->languages);
|
||
g_strfreev (cache->language_names);
|
||
g_free (cache);
|
||
}
|
||
|
||
/* read an alias file for the locales */
|
||
static void
|
||
read_aliases (const gchar *file,
|
||
GHashTable *alias_table)
|
||
{
|
||
FILE *fp;
|
||
char buf[256];
|
||
|
||
fp = fopen (file,"r");
|
||
if (!fp)
|
||
return;
|
||
while (fgets (buf, 256, fp))
|
||
{
|
||
char *p, *q;
|
||
|
||
g_strstrip (buf);
|
||
|
||
/* Line is a comment */
|
||
if ((buf[0] == '#') || (buf[0] == '\0'))
|
||
continue;
|
||
|
||
/* Reads first column */
|
||
for (p = buf, q = NULL; *p; p++) {
|
||
if ((*p == '\t') || (*p == ' ') || (*p == ':')) {
|
||
*p = '\0';
|
||
q = p+1;
|
||
while ((*q == '\t') || (*q == ' ')) {
|
||
q++;
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
/* The line only had one column */
|
||
if (!q || *q == '\0')
|
||
continue;
|
||
|
||
/* Read second column */
|
||
for (p = q; *p; p++) {
|
||
if ((*p == '\t') || (*p == ' ')) {
|
||
*p = '\0';
|
||
break;
|
||
}
|
||
}
|
||
|
||
/* Add to alias table if necessary */
|
||
if (!g_hash_table_lookup (alias_table, buf)) {
|
||
g_hash_table_insert (alias_table, g_strdup (buf), g_strdup (q));
|
||
}
|
||
}
|
||
fclose (fp);
|
||
}
|
||
|
||
static char *
|
||
unalias_lang (char *lang)
|
||
{
|
||
static GHashTable *alias_table = NULL;
|
||
char *p;
|
||
int i;
|
||
|
||
if (g_once_init_enter (&alias_table))
|
||
{
|
||
GHashTable *table = g_hash_table_new (g_str_hash, g_str_equal);
|
||
read_aliases ("/usr/share/locale/locale.alias", table);
|
||
g_once_init_leave (&alias_table, table);
|
||
}
|
||
|
||
i = 0;
|
||
while ((p = g_hash_table_lookup (alias_table, lang)) && (strcmp (p, lang) != 0))
|
||
{
|
||
lang = p;
|
||
if (i++ == 30)
|
||
{
|
||
static gboolean said_before = FALSE;
|
||
if (!said_before)
|
||
g_warning ("Too many alias levels for a locale, "
|
||
"may indicate a loop");
|
||
said_before = TRUE;
|
||
return lang;
|
||
}
|
||
}
|
||
return lang;
|
||
}
|
||
|
||
/* Mask for components of locale spec. The ordering here is from
|
||
* least significant to most significant
|
||
*/
|
||
enum
|
||
{
|
||
COMPONENT_CODESET = 1 << 0,
|
||
COMPONENT_TERRITORY = 1 << 1,
|
||
COMPONENT_MODIFIER = 1 << 2
|
||
};
|
||
|
||
/* Break an X/Open style locale specification into components
|
||
*/
|
||
static guint
|
||
explode_locale (const gchar *locale,
|
||
gchar **language,
|
||
gchar **territory,
|
||
gchar **codeset,
|
||
gchar **modifier)
|
||
{
|
||
const gchar *uscore_pos;
|
||
const gchar *at_pos;
|
||
const gchar *dot_pos;
|
||
|
||
guint mask = 0;
|
||
|
||
uscore_pos = strchr (locale, '_');
|
||
dot_pos = strchr (uscore_pos ? uscore_pos : locale, '.');
|
||
at_pos = strchr (dot_pos ? dot_pos : (uscore_pos ? uscore_pos : locale), '@');
|
||
|
||
if (at_pos)
|
||
{
|
||
mask |= COMPONENT_MODIFIER;
|
||
*modifier = g_strdup (at_pos);
|
||
}
|
||
else
|
||
at_pos = locale + strlen (locale);
|
||
|
||
if (dot_pos)
|
||
{
|
||
mask |= COMPONENT_CODESET;
|
||
*codeset = g_strndup (dot_pos, at_pos - dot_pos);
|
||
}
|
||
else
|
||
dot_pos = at_pos;
|
||
|
||
if (uscore_pos)
|
||
{
|
||
mask |= COMPONENT_TERRITORY;
|
||
*territory = g_strndup (uscore_pos, dot_pos - uscore_pos);
|
||
}
|
||
else
|
||
uscore_pos = dot_pos;
|
||
|
||
*language = g_strndup (locale, uscore_pos - locale);
|
||
|
||
return mask;
|
||
}
|
||
|
||
/*
|
||
* Compute all interesting variants for a given locale name -
|
||
* by stripping off different components of the value.
|
||
*
|
||
* For simplicity, we assume that the locale is in
|
||
* X/Open format: language[_territory][.codeset][@modifier]
|
||
*
|
||
* TODO: Extend this to handle the CEN format (see the GNUlibc docs)
|
||
* as well. We could just copy the code from glibc wholesale
|
||
* but it is big, ugly, and complicated, so I'm reluctant
|
||
* to do so when this should handle 99% of the time...
|
||
*/
|
||
static void
|
||
append_locale_variants (GPtrArray *array,
|
||
const gchar *locale)
|
||
{
|
||
gchar *language = NULL;
|
||
gchar *territory = NULL;
|
||
gchar *codeset = NULL;
|
||
gchar *modifier = NULL;
|
||
|
||
guint mask;
|
||
guint i, j;
|
||
|
||
g_return_if_fail (locale != NULL);
|
||
|
||
mask = explode_locale (locale, &language, &territory, &codeset, &modifier);
|
||
|
||
/* Iterate through all possible combinations, from least attractive
|
||
* to most attractive.
|
||
*/
|
||
for (j = 0; j <= mask; ++j)
|
||
{
|
||
i = mask - j;
|
||
|
||
if ((i & ~mask) == 0)
|
||
{
|
||
gchar *val = g_strconcat (language,
|
||
(i & COMPONENT_TERRITORY) ? territory : "",
|
||
(i & COMPONENT_CODESET) ? codeset : "",
|
||
(i & COMPONENT_MODIFIER) ? modifier : "",
|
||
NULL);
|
||
g_ptr_array_add (array, val);
|
||
}
|
||
}
|
||
|
||
g_free (language);
|
||
if (mask & COMPONENT_CODESET)
|
||
g_free (codeset);
|
||
if (mask & COMPONENT_TERRITORY)
|
||
g_free (territory);
|
||
if (mask & COMPONENT_MODIFIER)
|
||
g_free (modifier);
|
||
}
|
||
|
||
/* The following is (partly) taken from the gettext package.
|
||
Copyright (C) 1995, 1996, 1997, 1998 Free Software Foundation, Inc. */
|
||
|
||
static const gchar *
|
||
guess_category_value (const gchar *category_name)
|
||
{
|
||
const gchar *retval;
|
||
|
||
/* The highest priority value is the 'LANGUAGE' environment
|
||
variable. This is a GNU extension. */
|
||
retval = g_getenv ("LANGUAGE");
|
||
if ((retval != NULL) && (retval[0] != '\0'))
|
||
return retval;
|
||
|
||
/* 'LANGUAGE' is not set. So we have to proceed with the POSIX
|
||
methods of looking to 'LC_ALL', 'LC_xxx', and 'LANG'. On some
|
||
systems this can be done by the 'setlocale' function itself. */
|
||
|
||
/* Setting of LC_ALL overwrites all other. */
|
||
retval = g_getenv ("LC_ALL");
|
||
if ((retval != NULL) && (retval[0] != '\0'))
|
||
return retval;
|
||
|
||
/* Next comes the name of the desired category. */
|
||
retval = g_getenv (category_name);
|
||
if ((retval != NULL) && (retval[0] != '\0'))
|
||
return retval;
|
||
|
||
/* Last possibility is the LANG environment variable. */
|
||
retval = g_getenv ("LANG");
|
||
if ((retval != NULL) && (retval[0] != '\0'))
|
||
return retval;
|
||
|
||
#ifdef G_PLATFORM_WIN32
|
||
/* g_win32_getlocale() first checks for LC_ALL, LC_MESSAGES and
|
||
* LANG, which we already did above. Oh well. The main point of
|
||
* calling g_win32_getlocale() is to get the thread's locale as used
|
||
* by Windows and the Microsoft C runtime (in the "English_United
|
||
* States" format) translated into the Unixish format.
|
||
*/
|
||
{
|
||
char *locale = g_win32_getlocale ();
|
||
retval = g_intern_string (locale);
|
||
g_free (locale);
|
||
return retval;
|
||
}
|
||
#endif
|
||
|
||
return NULL;
|
||
}
|
||
|
||
static const gchar * const *
|
||
g_get_language_names_with_category (const gchar *category_name)
|
||
{
|
||
static GPrivate cache_private = G_PRIVATE_INIT ((void (*)(gpointer)) g_hash_table_unref);
|
||
GHashTable *cache = g_private_get (&cache_private);
|
||
const gchar *languages;
|
||
GLanguageNamesCache *name_cache;
|
||
|
||
g_return_val_if_fail (category_name != NULL, NULL);
|
||
|
||
if (!cache)
|
||
{
|
||
cache = g_hash_table_new_full (g_str_hash, g_str_equal,
|
||
g_free, language_names_cache_free);
|
||
g_private_set (&cache_private, cache);
|
||
}
|
||
|
||
languages = guess_category_value (category_name);
|
||
if (!languages)
|
||
languages = "C";
|
||
|
||
name_cache = (GLanguageNamesCache *) g_hash_table_lookup (cache, category_name);
|
||
if (!(name_cache && name_cache->languages &&
|
||
strcmp (name_cache->languages, languages) == 0))
|
||
{
|
||
GPtrArray *array;
|
||
gchar **alist, **a;
|
||
|
||
g_hash_table_remove (cache, category_name);
|
||
|
||
array = g_ptr_array_sized_new (8);
|
||
|
||
alist = g_strsplit (languages, ":", 0);
|
||
for (a = alist; *a; a++)
|
||
append_locale_variants (array, unalias_lang (*a));
|
||
g_strfreev (alist);
|
||
g_ptr_array_add (array, g_strdup ("C"));
|
||
g_ptr_array_add (array, NULL);
|
||
|
||
name_cache = g_new0 (GLanguageNamesCache, 1);
|
||
name_cache->languages = g_strdup (languages);
|
||
name_cache->language_names = (gchar **) g_ptr_array_free (array, FALSE);
|
||
g_hash_table_insert (cache, g_strdup (category_name), name_cache);
|
||
}
|
||
|
||
return (const gchar * const *) name_cache->language_names;
|
||
}
|
||
|
||
#endif
|
||
|
||
char **
|
||
flatpak_get_current_locale_langs (void)
|
||
{
|
||
const char * const *categories = flatpak_get_locale_categories ();
|
||
GPtrArray *langs = g_ptr_array_new ();
|
||
int i;
|
||
|
||
for (; categories != NULL && *categories != NULL; categories++)
|
||
{
|
||
const gchar * const *locales = g_get_language_names_with_category (*categories);
|
||
|
||
for (i = 0; locales[i] != NULL; i++)
|
||
{
|
||
g_autofree char *lang = flatpak_get_lang_from_locale (locales[i]);
|
||
if (lang != NULL && !flatpak_g_ptr_array_contains_string (langs, lang))
|
||
g_ptr_array_add (langs, g_steal_pointer (&lang));
|
||
}
|
||
}
|
||
|
||
g_ptr_array_sort (langs, flatpak_strcmp0_ptr);
|
||
g_ptr_array_add (langs, NULL);
|
||
|
||
return (char **) g_ptr_array_free (langs, FALSE);
|
||
}
|
||
|
||
void
|
||
flatpak_log_dir_access (FlatpakDir *dir)
|
||
{
|
||
if (dir != NULL)
|
||
{
|
||
GFile *dir_path = NULL;
|
||
g_autofree char *dir_path_str = NULL;
|
||
g_autofree char *dir_name = NULL;
|
||
|
||
dir_path = flatpak_dir_get_path (dir);
|
||
if (dir_path != NULL)
|
||
dir_path_str = g_file_get_path (dir_path);
|
||
dir_name = flatpak_dir_get_name (dir);
|
||
g_info ("Opening %s flatpak installation at path %s", dir_name, dir_path_str);
|
||
}
|
||
}
|
||
|
||
gboolean
|
||
flatpak_check_required_version (const char *ref,
|
||
GKeyFile *metakey,
|
||
GError **error)
|
||
{
|
||
g_auto(GStrv) required_versions = NULL;
|
||
const char *group;
|
||
int max_required_major = 0, max_required_minor = 0;
|
||
const char *max_required_version = "0.0";
|
||
int i;
|
||
|
||
if (g_str_has_prefix (ref, "app/"))
|
||
group = "Application";
|
||
else
|
||
group = "Runtime";
|
||
|
||
/* We handle handle multiple version requirements here. Each requirement must
|
||
* be in the form major.minor.micro, and if the flatpak version matches the
|
||
* major.minor part, t must be equal or later in the micro. If the major.minor part
|
||
* doesn't exactly match any of the specified requirements it must be larger
|
||
* than the maximum specified requirement.
|
||
*
|
||
* For example, specifying
|
||
* required-flatpak=1.6.2;1.4.2;1.0.2;
|
||
* would allow flatpak versions:
|
||
* 1.7.0, 1.6.2, 1.6.3, 1.4.2, 1.4.3, 1.0.2, 1.0.3
|
||
* but not:
|
||
* 1.6.1, 1.4.1 or 1.2.100.
|
||
*
|
||
* The goal here is to be able to specify a version (like 1.6.2 above) where a feature
|
||
* was introduced, but also allow backports of said feature to earlier version series.
|
||
*
|
||
* Earlier versions that only support specifying one version will only look at the first
|
||
* element in the list, so put the largest version first.
|
||
*/
|
||
required_versions = g_key_file_get_string_list (metakey, group, "required-flatpak", NULL, NULL);
|
||
if (required_versions == 0 || required_versions[0] == NULL)
|
||
return TRUE;
|
||
|
||
for (i = 0; required_versions[i] != NULL; i++)
|
||
{
|
||
int required_major, required_minor, required_micro;
|
||
const char *required_version = required_versions[i];
|
||
|
||
if (sscanf (required_version, "%d.%d.%d", &required_major, &required_minor, &required_micro) != 3)
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA,
|
||
_("Invalid require-flatpak argument %s"), required_version);
|
||
else
|
||
{
|
||
/* If flatpak is in the same major.minor series as the requirement, do a micro check */
|
||
if (required_major == PACKAGE_MAJOR_VERSION && required_minor == PACKAGE_MINOR_VERSION)
|
||
{
|
||
if (required_micro <= PACKAGE_MICRO_VERSION)
|
||
return TRUE;
|
||
else
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_NEED_NEW_FLATPAK,
|
||
_("%s needs a later flatpak version (%s)"),
|
||
ref, required_version);
|
||
}
|
||
|
||
/* Otherwise, keep track of the largest major.minor that is required */
|
||
if ((required_major > max_required_major) ||
|
||
(required_major == max_required_major &&
|
||
required_minor > max_required_minor))
|
||
{
|
||
max_required_major = required_major;
|
||
max_required_minor = required_minor;
|
||
max_required_version = required_version;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (max_required_major > PACKAGE_MAJOR_VERSION ||
|
||
(max_required_major == PACKAGE_MAJOR_VERSION && max_required_minor > PACKAGE_MINOR_VERSION))
|
||
return flatpak_fail_error (error, FLATPAK_ERROR_NEED_NEW_FLATPAK,
|
||
_("%s needs a later flatpak version (%s)"),
|
||
ref, max_required_version);
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
str_has_sign (const gchar *str)
|
||
{
|
||
return str[0] == '-' || str[0] == '+';
|
||
}
|
||
|
||
static gboolean
|
||
str_has_hex_prefix (const gchar *str)
|
||
{
|
||
return str[0] == '0' && g_ascii_tolower (str[1]) == 'x';
|
||
}
|
||
|
||
/* Copied from glib-2.54.0 to avoid the Glib's version bump.
|
||
* Function name in glib: g_ascii_string_to_unsigned
|
||
* If this is being dropped(migration to g_ascii_string_to_unsigned)
|
||
* make sure to remove str_has_hex_prefix and str_has_sign helpers too.
|
||
*/
|
||
gboolean
|
||
flatpak_utils_ascii_string_to_unsigned (const gchar *str,
|
||
guint base,
|
||
guint64 min,
|
||
guint64 max,
|
||
guint64 *out_num,
|
||
GError **error)
|
||
{
|
||
guint64 number;
|
||
const gchar *end_ptr = NULL;
|
||
gint saved_errno = 0;
|
||
|
||
g_return_val_if_fail (str != NULL, FALSE);
|
||
g_return_val_if_fail (base >= 2 && base <= 36, FALSE);
|
||
g_return_val_if_fail (min <= max, FALSE);
|
||
g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
|
||
|
||
if (str[0] == '\0')
|
||
{
|
||
g_set_error_literal (error,
|
||
G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
_("Empty string is not a number"));
|
||
return FALSE;
|
||
}
|
||
|
||
errno = 0;
|
||
number = g_ascii_strtoull (str, (gchar **) &end_ptr, base);
|
||
saved_errno = errno;
|
||
|
||
if (/* We do not allow leading whitespace, but g_ascii_strtoull
|
||
* accepts it and just skips it, so we need to check for it
|
||
* ourselves.
|
||
*/
|
||
g_ascii_isspace (str[0]) ||
|
||
/* Unsigned number should have no sign.
|
||
*/
|
||
str_has_sign (str) ||
|
||
/* We don't support hexadecimal numbers prefixed with 0x or
|
||
* 0X.
|
||
*/
|
||
(base == 16 && str_has_hex_prefix (str)) ||
|
||
(saved_errno != 0 && saved_errno != ERANGE) ||
|
||
end_ptr == NULL ||
|
||
*end_ptr != '\0')
|
||
{
|
||
g_set_error (error,
|
||
G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
_("“%s” is not an unsigned number"), str);
|
||
return FALSE;
|
||
}
|
||
if (saved_errno == ERANGE || number < min || number > max)
|
||
{
|
||
gchar *min_str = g_strdup_printf ("%" G_GUINT64_FORMAT, min);
|
||
gchar *max_str = g_strdup_printf ("%" G_GUINT64_FORMAT, max);
|
||
|
||
g_set_error (error,
|
||
G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
|
||
_("Number “%s” is out of bounds [%s, %s]"),
|
||
str, min_str, max_str);
|
||
g_free (min_str);
|
||
g_free (max_str);
|
||
return FALSE;
|
||
}
|
||
if (out_num != NULL)
|
||
*out_num = number;
|
||
return TRUE;
|
||
}
|
||
|
||
static int
|
||
dist (const char *s, int ls, const char *t, int lt, int i, int j, int *d)
|
||
{
|
||
int x, y;
|
||
|
||
if (d[i * (lt + 1) + j] >= 0)
|
||
return d[i * (lt + 1) + j];
|
||
|
||
if (i == ls)
|
||
x = lt - j;
|
||
else if (j == lt)
|
||
x = ls - i;
|
||
else if (s[i] == t[j])
|
||
x = dist (s, ls, t, lt, i + 1, j + 1, d);
|
||
else
|
||
{
|
||
x = dist (s, ls, t, lt, i + 1, j + 1, d);
|
||
y = dist (s, ls, t, lt, i, j + 1, d);
|
||
if (y < x)
|
||
x = y;
|
||
y = dist (s, ls, t, lt, i + 1, j, d);
|
||
if (y < x)
|
||
x = y;
|
||
x++;
|
||
}
|
||
|
||
d[i * (lt + 1) + j] = x;
|
||
|
||
return x;
|
||
}
|
||
|
||
int
|
||
flatpak_levenshtein_distance (const char *s,
|
||
gssize ls,
|
||
const char *t,
|
||
gssize lt)
|
||
{
|
||
int i, j;
|
||
int *d;
|
||
|
||
if (ls < 0)
|
||
ls = strlen (s);
|
||
|
||
if (lt < 0)
|
||
lt = strlen (t);
|
||
|
||
d = alloca (sizeof (int) * (ls + 1) * (lt + 1));
|
||
|
||
for (i = 0; i <= ls; i++)
|
||
for (j = 0; j <= lt; j++)
|
||
d[i * (lt + 1) + j] = -1;
|
||
|
||
return dist (s, ls, t, lt, 0, 0, d);
|
||
}
|
||
|
||
void
|
||
flatpak_get_window_size (int *rows, int *cols)
|
||
{
|
||
struct winsize w;
|
||
|
||
if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &w) == 0)
|
||
{
|
||
/* For whatever reason, in buildbot this returns 0, 0 so add a fallback */
|
||
if (w.ws_row == 0)
|
||
w.ws_row = 24;
|
||
if (w.ws_col == 0)
|
||
w.ws_col = 80;
|
||
*rows = w.ws_row;
|
||
*cols = w.ws_col;
|
||
}
|
||
else
|
||
{
|
||
*rows = 24;
|
||
*cols = 80;
|
||
}
|
||
}
|
||
|
||
gboolean
|
||
flatpak_set_tty_echo (gboolean echo)
|
||
{
|
||
struct termios term;
|
||
gboolean was;
|
||
|
||
tcgetattr (STDIN_FILENO, &term);
|
||
was = (term.c_lflag & ECHO) != 0;
|
||
|
||
if (echo)
|
||
term.c_lflag |= ECHO;
|
||
else
|
||
term.c_lflag &= ~ECHO;
|
||
tcsetattr (STDIN_FILENO, TCSANOW, &term);
|
||
|
||
return was;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_get_cursor_pos (int * row, int *col)
|
||
{
|
||
fd_set readset;
|
||
struct timeval time;
|
||
struct termios term, initial_term;
|
||
int res = 0;
|
||
|
||
tcgetattr (STDIN_FILENO, &initial_term);
|
||
term = initial_term;
|
||
term.c_lflag &= ~ICANON;
|
||
term.c_lflag &= ~ECHO;
|
||
tcsetattr (STDIN_FILENO, TCSANOW, &term);
|
||
|
||
printf ("\033[6n");
|
||
fflush (stdout);
|
||
|
||
FD_ZERO (&readset);
|
||
FD_SET (STDIN_FILENO, &readset);
|
||
time.tv_sec = 0;
|
||
time.tv_usec = 100000;
|
||
|
||
if (select (STDIN_FILENO + 1, &readset, NULL, NULL, &time) == 1)
|
||
res = scanf ("\033[%d;%dR", row, col);
|
||
|
||
tcsetattr (STDIN_FILENO, TCSADRAIN, &initial_term);
|
||
|
||
return res == 2;
|
||
}
|
||
|
||
void
|
||
flatpak_hide_cursor (void)
|
||
{
|
||
const size_t flatpak_hide_cursor_len = strlen (FLATPAK_ANSI_HIDE_CURSOR);
|
||
const ssize_t write_ret = write (STDOUT_FILENO, FLATPAK_ANSI_HIDE_CURSOR,
|
||
flatpak_hide_cursor_len);
|
||
|
||
if (write_ret < 0)
|
||
g_warning ("write() failed: %zd = write(STDOUT_FILENO, FLATPAK_ANSI_HIDE_CURSOR, %zu)",
|
||
write_ret, flatpak_hide_cursor_len);
|
||
}
|
||
|
||
void
|
||
flatpak_show_cursor (void)
|
||
{
|
||
const size_t flatpak_show_cursor_len = strlen (FLATPAK_ANSI_SHOW_CURSOR);
|
||
const ssize_t write_ret = write (STDOUT_FILENO, FLATPAK_ANSI_SHOW_CURSOR,
|
||
flatpak_show_cursor_len);
|
||
|
||
if (write_ret < 0)
|
||
g_warning ("write() failed: %zd = write(STDOUT_FILENO, FLATPAK_ANSI_SHOW_CURSOR, %zu)",
|
||
write_ret, flatpak_show_cursor_len);
|
||
}
|
||
|
||
void
|
||
flatpak_enable_raw_mode (void)
|
||
{
|
||
struct termios raw;
|
||
|
||
tcgetattr (STDIN_FILENO, &raw);
|
||
|
||
raw.c_lflag &= ~(ECHO | ICANON);
|
||
|
||
tcsetattr (STDIN_FILENO, TCSAFLUSH, &raw);
|
||
}
|
||
|
||
void
|
||
flatpak_disable_raw_mode (void)
|
||
{
|
||
struct termios raw;
|
||
|
||
tcgetattr (STDIN_FILENO, &raw);
|
||
|
||
raw.c_lflag |= (ECHO | ICANON);
|
||
|
||
tcsetattr (STDIN_FILENO, TCSAFLUSH, &raw);
|
||
}
|
||
|
||
/* Wrapper that uses ostree_repo_resolve_collection_ref() and on failure falls
|
||
* back to using ostree_repo_resolve_rev() for backwards compatibility. This
|
||
* means we support refs/heads/, refs/remotes/, and refs/mirrors/. */
|
||
gboolean
|
||
flatpak_repo_resolve_rev (OstreeRepo *repo,
|
||
const char *collection_id, /* nullable */
|
||
const char *remote_name, /* nullable */
|
||
const char *ref_name,
|
||
gboolean allow_noent,
|
||
char **out_rev,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
if (collection_id != NULL)
|
||
{
|
||
/* Do a version check to ensure we have these:
|
||
* https://github.com/ostreedev/ostree/pull/1821
|
||
* https://github.com/ostreedev/ostree/pull/1825 */
|
||
#if OSTREE_CHECK_VERSION (2019, 2)
|
||
const OstreeCollectionRef c_r =
|
||
{
|
||
.collection_id = (char *) collection_id,
|
||
.ref_name = (char *) ref_name,
|
||
};
|
||
OstreeRepoResolveRevExtFlags flags = remote_name == NULL ?
|
||
OSTREE_REPO_RESOLVE_REV_EXT_LOCAL_ONLY :
|
||
OSTREE_REPO_RESOLVE_REV_EXT_NONE;
|
||
if (ostree_repo_resolve_collection_ref (repo, &c_r,
|
||
allow_noent,
|
||
flags,
|
||
out_rev,
|
||
cancellable, NULL))
|
||
return TRUE;
|
||
#endif
|
||
}
|
||
|
||
/* There may be several remotes with the same branch (if we for
|
||
* instance changed the origin) so prepend the current origin to
|
||
* make sure we get the right one */
|
||
if (remote_name != NULL)
|
||
{
|
||
g_autofree char *refspec = g_strdup_printf ("%s:%s", remote_name, ref_name);
|
||
ostree_repo_resolve_rev (repo, refspec, allow_noent, out_rev, &local_error);
|
||
}
|
||
else
|
||
ostree_repo_resolve_rev_ext (repo, ref_name, allow_noent,
|
||
OSTREE_REPO_RESOLVE_REV_EXT_NONE, out_rev, &local_error);
|
||
|
||
if (local_error != NULL)
|
||
{
|
||
if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
|
||
flatpak_fail_error (error, FLATPAK_ERROR_REF_NOT_FOUND, "%s", local_error->message);
|
||
else
|
||
g_propagate_error (error, g_steal_pointer (&local_error));
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
|
||
#if !GLIB_CHECK_VERSION (2, 56, 0)
|
||
/* All this code is backported directly from glib */
|
||
|
||
static void
|
||
g_date_time_get_week_number (GDateTime *datetime,
|
||
gint *week_number,
|
||
gint *day_of_week,
|
||
gint *day_of_year)
|
||
{
|
||
gint a, b, c, d, e, f, g, n, s, month, day, year;
|
||
|
||
g_date_time_get_ymd (datetime, &year, &month, &day);
|
||
|
||
if (month <= 2)
|
||
{
|
||
a = g_date_time_get_year (datetime) - 1;
|
||
b = (a / 4) - (a / 100) + (a / 400);
|
||
c = ((a - 1) / 4) - ((a - 1) / 100) + ((a - 1) / 400);
|
||
s = b - c;
|
||
e = 0;
|
||
f = day - 1 + (31 * (month - 1));
|
||
}
|
||
else
|
||
{
|
||
a = year;
|
||
b = (a / 4) - (a / 100) + (a / 400);
|
||
c = ((a - 1) / 4) - ((a - 1) / 100) + ((a - 1) / 400);
|
||
s = b - c;
|
||
e = s + 1;
|
||
f = day + (((153 * (month - 3)) + 2) / 5) + 58 + s;
|
||
}
|
||
|
||
g = (a + b) % 7;
|
||
d = (f + g - e) % 7;
|
||
n = f + 3 - d;
|
||
|
||
if (week_number)
|
||
{
|
||
if (n < 0)
|
||
*week_number = 53 - ((g - s) / 5);
|
||
else if (n > 364 + s)
|
||
*week_number = 1;
|
||
else
|
||
*week_number = (n / 7) + 1;
|
||
}
|
||
|
||
if (day_of_week)
|
||
*day_of_week = d + 1;
|
||
|
||
if (day_of_year)
|
||
*day_of_year = f + 1;
|
||
}
|
||
|
||
#define GREGORIAN_LEAP(y) ((((y) % 4) == 0) && (!((((y) % 100) == 0) && (((y) % 400) != 0))))
|
||
|
||
/* Parse integers in the form d (week days), dd (hours etc), ddd (ordinal days) or dddd (years) */
|
||
static gboolean
|
||
get_iso8601_int (const gchar *text, gsize length, gint *value)
|
||
{
|
||
gint i, v = 0;
|
||
|
||
if (length < 1 || length > 4)
|
||
return FALSE;
|
||
|
||
for (i = 0; i < length; i++)
|
||
{
|
||
const gchar c = text[i];
|
||
if (c < '0' || c > '9')
|
||
return FALSE;
|
||
v = v * 10 + (c - '0');
|
||
}
|
||
|
||
*value = v;
|
||
return TRUE;
|
||
}
|
||
|
||
/* Parse seconds in the form ss or ss.sss (variable length decimal) */
|
||
static gboolean
|
||
get_iso8601_seconds (const gchar *text, gsize length, gdouble *value)
|
||
{
|
||
gint i;
|
||
gdouble divisor = 1, v = 0;
|
||
|
||
if (length < 2)
|
||
return FALSE;
|
||
|
||
for (i = 0; i < 2; i++)
|
||
{
|
||
const gchar c = text[i];
|
||
if (c < '0' || c > '9')
|
||
return FALSE;
|
||
v = v * 10 + (c - '0');
|
||
}
|
||
|
||
if (length > 2 && !(text[i] == '.' || text[i] == ','))
|
||
return FALSE;
|
||
i++;
|
||
if (i == length)
|
||
return FALSE;
|
||
|
||
for (; i < length; i++)
|
||
{
|
||
const gchar c = text[i];
|
||
if (c < '0' || c > '9')
|
||
return FALSE;
|
||
v = v * 10 + (c - '0');
|
||
divisor *= 10;
|
||
}
|
||
|
||
*value = v / divisor;
|
||
return TRUE;
|
||
}
|
||
|
||
static GDateTime *
|
||
g_date_time_new_ordinal (GTimeZone *tz, gint year, gint ordinal_day, gint hour, gint minute, gdouble seconds)
|
||
{
|
||
GDateTime *dt, *dt2;
|
||
|
||
if (ordinal_day < 1 || ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365))
|
||
return NULL;
|
||
|
||
dt = g_date_time_new (tz, year, 1, 1, hour, minute, seconds);
|
||
dt2 = g_date_time_add_days (dt, ordinal_day - 1);
|
||
g_date_time_unref (dt);
|
||
|
||
return dt2;
|
||
}
|
||
|
||
static GDateTime *
|
||
g_date_time_new_week (GTimeZone *tz, gint year, gint week, gint week_day, gint hour, gint minute, gdouble seconds)
|
||
{
|
||
gint64 p;
|
||
gint max_week, jan4_week_day, ordinal_day;
|
||
GDateTime *dt;
|
||
|
||
p = (year * 365 + (year / 4) - (year / 100) + (year / 400)) % 7;
|
||
max_week = p == 4 ? 53 : 52;
|
||
|
||
if (week < 1 || week > max_week || week_day < 1 || week_day > 7)
|
||
return NULL;
|
||
|
||
dt = g_date_time_new (tz, year, 1, 4, 0, 0, 0);
|
||
g_date_time_get_week_number (dt, NULL, &jan4_week_day, NULL);
|
||
g_date_time_unref (dt);
|
||
|
||
ordinal_day = (week * 7) + week_day - (jan4_week_day + 3);
|
||
if (ordinal_day < 0)
|
||
{
|
||
year--;
|
||
ordinal_day += GREGORIAN_LEAP (year) ? 366 : 365;
|
||
}
|
||
else if (ordinal_day > (GREGORIAN_LEAP (year) ? 366 : 365))
|
||
{
|
||
ordinal_day -= (GREGORIAN_LEAP (year) ? 366 : 365);
|
||
year++;
|
||
}
|
||
|
||
return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
|
||
}
|
||
|
||
static GDateTime *
|
||
parse_iso8601_date (const gchar *text, gsize length,
|
||
gint hour, gint minute, gdouble seconds, GTimeZone *tz)
|
||
{
|
||
/* YYYY-MM-DD */
|
||
if (length == 10 && text[4] == '-' && text[7] == '-')
|
||
{
|
||
int year, month, day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 5, 2, &month) ||
|
||
!get_iso8601_int (text + 8, 2, &day))
|
||
return NULL;
|
||
return g_date_time_new (tz, year, month, day, hour, minute, seconds);
|
||
}
|
||
/* YYYY-DDD */
|
||
else if (length == 8 && text[4] == '-')
|
||
{
|
||
gint year, ordinal_day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 5, 3, &ordinal_day))
|
||
return NULL;
|
||
return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
|
||
}
|
||
/* YYYY-Www-D */
|
||
else if (length == 10 && text[4] == '-' && text[5] == 'W' && text[8] == '-')
|
||
{
|
||
gint year, week, week_day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 6, 2, &week) ||
|
||
!get_iso8601_int (text + 9, 1, &week_day))
|
||
return NULL;
|
||
return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds);
|
||
}
|
||
/* YYYYWwwD */
|
||
else if (length == 8 && text[4] == 'W')
|
||
{
|
||
gint year, week, week_day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 5, 2, &week) ||
|
||
!get_iso8601_int (text + 7, 1, &week_day))
|
||
return NULL;
|
||
return g_date_time_new_week (tz, year, week, week_day, hour, minute, seconds);
|
||
}
|
||
/* YYYYMMDD */
|
||
else if (length == 8)
|
||
{
|
||
int year, month, day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 4, 2, &month) ||
|
||
!get_iso8601_int (text + 6, 2, &day))
|
||
return NULL;
|
||
return g_date_time_new (tz, year, month, day, hour, minute, seconds);
|
||
}
|
||
/* YYYYDDD */
|
||
else if (length == 7)
|
||
{
|
||
gint year, ordinal_day;
|
||
if (!get_iso8601_int (text, 4, &year) ||
|
||
!get_iso8601_int (text + 4, 3, &ordinal_day))
|
||
return NULL;
|
||
return g_date_time_new_ordinal (tz, year, ordinal_day, hour, minute, seconds);
|
||
}
|
||
else
|
||
return FALSE;
|
||
}
|
||
|
||
static GTimeZone *
|
||
parse_iso8601_timezone (const gchar *text, gsize length, gssize *tz_offset)
|
||
{
|
||
gint i, tz_length, offset_sign = 1, offset_hours, offset_minutes;
|
||
GTimeZone *tz;
|
||
|
||
/* UTC uses Z suffix */
|
||
if (length > 0 && text[length - 1] == 'Z')
|
||
{
|
||
*tz_offset = length - 1;
|
||
return g_time_zone_new_utc ();
|
||
}
|
||
|
||
/* Look for '+' or '-' of offset */
|
||
for (i = length - 1; i >= 0; i--)
|
||
if (text[i] == '+' || text[i] == '-')
|
||
{
|
||
offset_sign = text[i] == '-' ? -1 : 1;
|
||
break;
|
||
}
|
||
if (i < 0)
|
||
return NULL;
|
||
tz_length = length - i;
|
||
|
||
/* +hh:mm or -hh:mm */
|
||
if (tz_length == 6 && text[i + 3] == ':')
|
||
{
|
||
if (!get_iso8601_int (text + i + 1, 2, &offset_hours) ||
|
||
!get_iso8601_int (text + i + 4, 2, &offset_minutes))
|
||
return NULL;
|
||
}
|
||
/* +hhmm or -hhmm */
|
||
else if (tz_length == 5)
|
||
{
|
||
if (!get_iso8601_int (text + i + 1, 2, &offset_hours) ||
|
||
!get_iso8601_int (text + i + 3, 2, &offset_minutes))
|
||
return NULL;
|
||
}
|
||
/* +hh or -hh */
|
||
else if (tz_length == 3)
|
||
{
|
||
if (!get_iso8601_int (text + i + 1, 2, &offset_hours))
|
||
return NULL;
|
||
offset_minutes = 0;
|
||
}
|
||
else
|
||
return NULL;
|
||
|
||
*tz_offset = i;
|
||
tz = g_time_zone_new (text + i);
|
||
|
||
/* Double-check that the GTimeZone matches our interpretation of the timezone.
|
||
* Failure would indicate a bug either here of in the GTimeZone code. */
|
||
g_assert (g_time_zone_get_offset (tz, 0) == offset_sign * (offset_hours * 3600 + offset_minutes * 60));
|
||
|
||
return tz;
|
||
}
|
||
|
||
static gboolean
|
||
parse_iso8601_time (const gchar *text, gsize length,
|
||
gint *hour, gint *minute, gdouble *seconds, GTimeZone **tz)
|
||
{
|
||
gssize tz_offset = -1;
|
||
|
||
/* Check for timezone suffix */
|
||
*tz = parse_iso8601_timezone (text, length, &tz_offset);
|
||
if (tz_offset >= 0)
|
||
length = tz_offset;
|
||
|
||
/* hh:mm:ss(.sss) */
|
||
if (length >= 8 && text[2] == ':' && text[5] == ':')
|
||
{
|
||
return get_iso8601_int (text, 2, hour) &&
|
||
get_iso8601_int (text + 3, 2, minute) &&
|
||
get_iso8601_seconds (text + 6, length - 6, seconds);
|
||
}
|
||
/* hhmmss(.sss) */
|
||
else if (length >= 6)
|
||
{
|
||
return get_iso8601_int (text, 2, hour) &&
|
||
get_iso8601_int (text + 2, 2, minute) &&
|
||
get_iso8601_seconds (text + 4, length - 4, seconds);
|
||
}
|
||
else
|
||
return FALSE;
|
||
}
|
||
|
||
|
||
GDateTime *
|
||
flatpak_g_date_time_new_from_iso8601 (const gchar *text, GTimeZone *default_tz)
|
||
{
|
||
gint length, date_length = -1;
|
||
gint hour = 0, minute = 0;
|
||
gdouble seconds = 0.0;
|
||
GTimeZone *tz = NULL;
|
||
GDateTime *datetime = NULL;
|
||
|
||
g_return_val_if_fail (text != NULL, NULL);
|
||
|
||
/* Count length of string and find date / time separator ('T', 't', or ' ') */
|
||
for (length = 0; text[length] != '\0'; length++)
|
||
{
|
||
if (date_length < 0 && (text[length] == 'T' || text[length] == 't' || text[length] == ' '))
|
||
date_length = length;
|
||
}
|
||
|
||
if (date_length < 0)
|
||
return NULL;
|
||
|
||
if (!parse_iso8601_time (text + date_length + 1, length - (date_length + 1),
|
||
&hour, &minute, &seconds, &tz))
|
||
goto out;
|
||
if (tz == NULL && default_tz == NULL)
|
||
return NULL;
|
||
|
||
datetime = parse_iso8601_date (text, date_length, hour, minute, seconds, tz ? tz : default_tz);
|
||
|
||
out:
|
||
if (tz != NULL)
|
||
g_time_zone_unref (tz);
|
||
return datetime;
|
||
}
|
||
#endif
|
||
|
||
/* Convert an app id to a dconf path in the obvious way.
|
||
*/
|
||
char *
|
||
flatpak_dconf_path_for_app_id (const char *app_id)
|
||
{
|
||
GString *s;
|
||
const char *p;
|
||
|
||
s = g_string_new ("");
|
||
|
||
g_string_append_c (s, '/');
|
||
for (p = app_id; *p; p++)
|
||
{
|
||
if (*p == '.')
|
||
g_string_append_c (s, '/');
|
||
else
|
||
g_string_append_c (s, *p);
|
||
}
|
||
g_string_append_c (s, '/');
|
||
|
||
return g_string_free (s, FALSE);
|
||
}
|
||
|
||
/* Check if two dconf paths are 'similar enough', which
|
||
* for now is defined as equal except case differences
|
||
* and -/_
|
||
*/
|
||
gboolean
|
||
flatpak_dconf_path_is_similar (const char *path1,
|
||
const char *path2)
|
||
{
|
||
int i1, i2;
|
||
int num_components = -1;
|
||
|
||
for (i1 = i2 = 0; path1[i1] != '\0'; i1++, i2++)
|
||
{
|
||
if (path2[i2] == '\0')
|
||
break;
|
||
|
||
if (isupper(path2[i2]) &&
|
||
(path1[i1] == '-' || path1[i1] == '_'))
|
||
{
|
||
i1++;
|
||
if (path1[i1] == '\0')
|
||
break;
|
||
}
|
||
|
||
if (isupper(path1[i1]) &&
|
||
(path2[i2] == '-' || path2[i2] == '_'))
|
||
{
|
||
i2++;
|
||
if (path2[i2] == '\0')
|
||
break;
|
||
}
|
||
|
||
if (tolower (path1[i1]) == tolower (path2[i2]))
|
||
{
|
||
if (path1[i1] == '/')
|
||
num_components++;
|
||
continue;
|
||
}
|
||
|
||
if ((path1[i1] == '-' || path1[i1] == '_') &&
|
||
(path2[i2] == '-' || path2[i2] == '_'))
|
||
continue;
|
||
|
||
break;
|
||
}
|
||
|
||
/* Skip over any versioning if we have at least a TLD and
|
||
* domain name, so 2 components */
|
||
/* We need at least TLD, and domain name, so 2 components */
|
||
if (num_components >= 2)
|
||
{
|
||
while (isdigit (path1[i1]))
|
||
i1++;
|
||
while (isdigit (path2[i2]))
|
||
i2++;
|
||
}
|
||
|
||
if (path1[i1] != path2[i2])
|
||
return FALSE;
|
||
|
||
/* Both strings finished? */
|
||
if (path1[i1] == '\0')
|
||
return TRUE;
|
||
|
||
/* Maybe a trailing slash in both strings */
|
||
if (path1[i1] == '/')
|
||
{
|
||
i1++;
|
||
i2++;
|
||
}
|
||
|
||
if (path1[i1] != path2[i2])
|
||
return FALSE;
|
||
|
||
return (path1[i1] == '\0');
|
||
}
|
||
|
||
/**
|
||
* flatpak_envp_cmp:
|
||
* @p1: a `const char * const *`
|
||
* @p2: a `const char * const *`
|
||
*
|
||
* Compare two environment variables, given as pointers to pointers
|
||
* to the actual `KEY=value` string.
|
||
*
|
||
* In particular this is suitable for sorting a #GStrv using `qsort`.
|
||
*
|
||
* Returns: negative, 0 or positive if `*p1` compares before, equal to
|
||
* or after `*p2`
|
||
*/
|
||
int
|
||
flatpak_envp_cmp (const void *p1,
|
||
const void *p2)
|
||
{
|
||
const char * const * s1 = p1;
|
||
const char * const * s2 = p2;
|
||
size_t l1 = strlen (*s1);
|
||
size_t l2 = strlen (*s2);
|
||
size_t min;
|
||
const char *tmp;
|
||
int ret;
|
||
|
||
tmp = strchr (*s1, '=');
|
||
|
||
if (tmp != NULL)
|
||
l1 = tmp - *s1;
|
||
|
||
tmp = strchr (*s2, '=');
|
||
|
||
if (tmp != NULL)
|
||
l2 = tmp - *s2;
|
||
|
||
min = MIN (l1, l2);
|
||
ret = strncmp (*s1, *s2, min);
|
||
|
||
/* If they differ before the first '=' (if any) in either s1 or s2,
|
||
* then they are certainly different */
|
||
if (ret != 0)
|
||
return ret;
|
||
|
||
ret = strcmp (*s1, *s2);
|
||
|
||
/* If they do not differ at all, then they are equal */
|
||
if (ret == 0)
|
||
return ret;
|
||
|
||
/* FOO < FOO=..., and FOO < FOOBAR */
|
||
if ((*s1)[min] == '\0')
|
||
return -1;
|
||
|
||
/* FOO=... > FOO, and FOOBAR > FOO */
|
||
if ((*s2)[min] == '\0')
|
||
return 1;
|
||
|
||
/* FOO= < FOOBAR */
|
||
if ((*s1)[min] == '=' && (*s2)[min] != '=')
|
||
return -1;
|
||
|
||
/* FOOBAR > FOO= */
|
||
if ((*s2)[min] == '=' && (*s1)[min] != '=')
|
||
return 1;
|
||
|
||
/* Fall back to plain string comparison */
|
||
return ret;
|
||
}
|
||
|
||
/*
|
||
* Return %TRUE if @s consists of one or more digits.
|
||
* This is the same as Python bytes.isdigit().
|
||
*/
|
||
gboolean
|
||
flatpak_str_is_integer (const char *s)
|
||
{
|
||
if (s == NULL || *s == '\0')
|
||
return FALSE;
|
||
|
||
for (; *s != '\0'; s++)
|
||
{
|
||
if (!g_ascii_isdigit (*s))
|
||
return FALSE;
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
gboolean
|
||
flatpak_uri_equal (const char *uri1,
|
||
const char *uri2)
|
||
{
|
||
g_autofree char *uri1_norm = NULL;
|
||
g_autofree char *uri2_norm = NULL;
|
||
gsize uri1_len = strlen (uri1);
|
||
gsize uri2_len = strlen (uri2);
|
||
|
||
/* URIs handled by libostree are equivalent with or without a trailing slash,
|
||
* but this isn't otherwise guaranteed to be the case.
|
||
*/
|
||
if (g_str_has_prefix (uri1, "oci+") || g_str_has_prefix (uri2, "oci+"))
|
||
return g_strcmp0 (uri1, uri2) == 0;
|
||
|
||
if (g_str_has_suffix (uri1, "/"))
|
||
uri1_norm = g_strndup (uri1, uri1_len - 1);
|
||
else
|
||
uri1_norm = g_strdup (uri1);
|
||
|
||
if (g_str_has_suffix (uri2, "/"))
|
||
uri2_norm = g_strndup (uri2, uri2_len - 1);
|
||
else
|
||
uri2_norm = g_strdup (uri2);
|
||
|
||
return g_strcmp0 (uri1_norm, uri2_norm) == 0;
|
||
}
|
||
|
||
gboolean
|
||
running_under_sudo (void)
|
||
{
|
||
const char *sudo_command_env = g_getenv ("SUDO_COMMAND");
|
||
g_auto(GStrv) split_command = NULL;
|
||
|
||
if (!sudo_command_env)
|
||
return FALSE;
|
||
|
||
/* SUDO_COMMAND could be a value like `/usr/bin/flatpak run foo` */
|
||
split_command = g_strsplit (sudo_command_env, " ", 2);
|
||
if (g_str_has_suffix (split_command[0], "flatpak"))
|
||
return TRUE;
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
#if !GLIB_CHECK_VERSION (2, 62, 0)
|
||
void
|
||
g_ptr_array_extend (GPtrArray *array_to_extend,
|
||
GPtrArray *array,
|
||
GCopyFunc func,
|
||
gpointer user_data)
|
||
{
|
||
for (gsize i = 0; i < array->len; i++)
|
||
{
|
||
if (func)
|
||
g_ptr_array_add (array_to_extend, func (g_ptr_array_index (array, i), user_data));
|
||
else
|
||
g_ptr_array_add (array_to_extend, g_ptr_array_index (array, i));
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if !GLIB_CHECK_VERSION (2, 68, 0)
|
||
/* All this code is backported directly from glib */
|
||
guint
|
||
g_string_replace (GString *string,
|
||
const gchar *find,
|
||
const gchar *replace,
|
||
guint limit)
|
||
{
|
||
gsize f_len, r_len, pos;
|
||
gchar *cur, *next;
|
||
guint n = 0;
|
||
|
||
g_return_val_if_fail (string != NULL, 0);
|
||
g_return_val_if_fail (find != NULL, 0);
|
||
g_return_val_if_fail (replace != NULL, 0);
|
||
|
||
f_len = strlen (find);
|
||
r_len = strlen (replace);
|
||
cur = string->str;
|
||
|
||
while ((next = strstr (cur, find)) != NULL)
|
||
{
|
||
pos = next - string->str;
|
||
g_string_erase (string, pos, f_len);
|
||
g_string_insert (string, pos, replace);
|
||
cur = string->str + pos + r_len;
|
||
n++;
|
||
/* Only match the empty string once at any given position, to
|
||
* avoid infinite loops */
|
||
if (f_len == 0)
|
||
{
|
||
if (cur[0] == '\0')
|
||
break;
|
||
else
|
||
cur++;
|
||
}
|
||
if (n == limit)
|
||
break;
|
||
}
|
||
|
||
return n;
|
||
}
|
||
|
||
#endif /* GLIB_CHECK_VERSION (2, 68, 0) */
|