From 936573fa0784e1638d41968189a1e41cbab6daca Mon Sep 17 00:00:00 2001 From: Simon McVittie <smcv@collabora.com> Date: Wed, 26 Aug 2020 14:47:10 +0100 Subject: [PATCH] Add flatpak-context, flatpak-exports from Flatpak A lot of the code added here is #if 0. Normally we don't commit commented-out or #ifdef'd out code, but in this case it helps tools like gvimdiff to have enough context to figure out which parts of the file in Flatpak correspond to which parts of the file in pressure-vessel. Signed-off-by: Simon McVittie <smcv@collabora.com> --- config.h.in | 8 + src/flatpak-common-types-private.h | 60 + src/flatpak-context-private.h | 165 + src/flatpak-context.c | 2477 ++++++++++ src/flatpak-error.h | 108 + src/flatpak-exports-private.h | 66 + src/flatpak-exports.c | 796 ++++ src/flatpak-run-private.h | 9 +- src/flatpak-run.c | 878 ++++ src/flatpak-utils-private.h | 45 + src/flatpak-utils.c | 7089 +++++++++++++++++++++++++++- src/glib-backports.c | 26 +- src/glib-backports.h | 7 + src/meson.build | 5 + 14 files changed, 11670 insertions(+), 69 deletions(-) create mode 100644 src/flatpak-common-types-private.h create mode 100644 src/flatpak-context-private.h create mode 100644 src/flatpak-context.c create mode 100644 src/flatpak-error.h create mode 100644 src/flatpak-exports-private.h create mode 100644 src/flatpak-exports.c diff --git a/config.h.in b/config.h.in index 70ad44889..6760d62a3 100644 --- a/config.h.in +++ b/config.h.in @@ -2,4 +2,12 @@ #define G_LOG_DOMAIN "pressure-vessel" #mesondefine VERSION +/* Allow using stuff from Flatpak with minimal modifications */ +#define FLATPAK_EXTERN extern +#define _(s) s +#define C_(context, s) s +#define N_(s) s +#define NC_(s) s +#define Q_(s) g_strip_context (s, s) + #include "subprojects/libglnx/config.h" diff --git a/src/flatpak-common-types-private.h b/src/flatpak-common-types-private.h new file mode 100644 index 000000000..7f77b2435 --- /dev/null +++ b/src/flatpak-common-types-private.h @@ -0,0 +1,60 @@ +/* + * Taken from Flatpak, last updated: 1.8.2 + * Copyright © 2015 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> + */ + +#ifndef __FLATPAK_COMMON_TYPES_H__ +#define __FLATPAK_COMMON_TYPES_H__ + +typedef enum { + FLATPAK_KINDS_APP = 1 << 0, + FLATPAK_KINDS_RUNTIME = 1 << 1, +} FlatpakKinds; + +typedef enum { + FLATPAK_RUN_FLAG_DEVEL = (1 << 0), + FLATPAK_RUN_FLAG_BACKGROUND = (1 << 1), + FLATPAK_RUN_FLAG_LOG_SESSION_BUS = (1 << 2), + FLATPAK_RUN_FLAG_LOG_SYSTEM_BUS = (1 << 3), + FLATPAK_RUN_FLAG_NO_SESSION_HELPER = (1 << 4), + FLATPAK_RUN_FLAG_MULTIARCH = (1 << 5), + FLATPAK_RUN_FLAG_WRITABLE_ETC = (1 << 6), + FLATPAK_RUN_FLAG_NO_SESSION_BUS_PROXY = (1 << 7), + FLATPAK_RUN_FLAG_NO_SYSTEM_BUS_PROXY = (1 << 8), + FLATPAK_RUN_FLAG_SET_PERSONALITY = (1 << 9), + FLATPAK_RUN_FLAG_FILE_FORWARDING = (1 << 10), + FLATPAK_RUN_FLAG_DIE_WITH_PARENT = (1 << 11), + FLATPAK_RUN_FLAG_LOG_A11Y_BUS = (1 << 12), + FLATPAK_RUN_FLAG_NO_A11Y_BUS_PROXY = (1 << 13), + FLATPAK_RUN_FLAG_SANDBOX = (1 << 14), + FLATPAK_RUN_FLAG_NO_DOCUMENTS_PORTAL = (1 << 15), + FLATPAK_RUN_FLAG_BLUETOOTH = (1 << 16), + FLATPAK_RUN_FLAG_CANBUS = (1 << 17), + FLATPAK_RUN_FLAG_DO_NOT_REAP = (1 << 18), + FLATPAK_RUN_FLAG_NO_PROC = (1 << 19), + FLATPAK_RUN_FLAG_PARENT_EXPOSE_PIDS = (1 << 20), +} FlatpakRunFlags; + +typedef struct FlatpakDir FlatpakDir; +typedef struct FlatpakDeploy FlatpakDeploy; +typedef struct FlatpakOciRegistry FlatpakOciRegistry; +typedef struct _FlatpakOciManifest FlatpakOciManifest; +typedef struct _FlatpakOciImage FlatpakOciImage; + +#endif /* __FLATPAK_COMMON_TYPES_H__ */ diff --git a/src/flatpak-context-private.h b/src/flatpak-context-private.h new file mode 100644 index 000000000..2773c979b --- /dev/null +++ b/src/flatpak-context-private.h @@ -0,0 +1,165 @@ +/* + * Taken from Flatpak, last updated: 1.9.x commit 1.8.0-74-g354b9a22 + * Modified to inline FlatpakPolicy instead of using the + * header file from xdg-dbus-proxy. + * + * Copyright © 2014-2018 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> + */ + +#ifndef __FLATPAK_CONTEXT_H__ +#define __FLATPAK_CONTEXT_H__ + +#include "libglnx/libglnx.h" + +typedef enum { + FLATPAK_POLICY_NONE, + FLATPAK_POLICY_SEE, + FLATPAK_POLICY_TALK, + FLATPAK_POLICY_OWN +} FlatpakPolicy; + +#include <flatpak-common-types-private.h> +#include "flatpak-exports-private.h" + +typedef struct FlatpakContext FlatpakContext; + +typedef enum { + FLATPAK_CONTEXT_SHARED_NETWORK = 1 << 0, + FLATPAK_CONTEXT_SHARED_IPC = 1 << 1, +} FlatpakContextShares; + +typedef enum { + FLATPAK_CONTEXT_SOCKET_X11 = 1 << 0, + FLATPAK_CONTEXT_SOCKET_WAYLAND = 1 << 1, + FLATPAK_CONTEXT_SOCKET_PULSEAUDIO = 1 << 2, + FLATPAK_CONTEXT_SOCKET_SESSION_BUS = 1 << 3, + FLATPAK_CONTEXT_SOCKET_SYSTEM_BUS = 1 << 4, + FLATPAK_CONTEXT_SOCKET_FALLBACK_X11 = 1 << 5, /* For backwards compat, also set SOCKET_X11 */ + FLATPAK_CONTEXT_SOCKET_SSH_AUTH = 1 << 6, + FLATPAK_CONTEXT_SOCKET_PCSC = 1 << 7, + FLATPAK_CONTEXT_SOCKET_CUPS = 1 << 8, +} FlatpakContextSockets; + +typedef enum { + FLATPAK_CONTEXT_DEVICE_DRI = 1 << 0, + FLATPAK_CONTEXT_DEVICE_ALL = 1 << 1, + FLATPAK_CONTEXT_DEVICE_KVM = 1 << 2, + FLATPAK_CONTEXT_DEVICE_SHM = 1 << 3, +} FlatpakContextDevices; + +typedef enum { + FLATPAK_CONTEXT_FEATURE_DEVEL = 1 << 0, + FLATPAK_CONTEXT_FEATURE_MULTIARCH = 1 << 1, + FLATPAK_CONTEXT_FEATURE_BLUETOOTH = 1 << 2, + FLATPAK_CONTEXT_FEATURE_CANBUS = 1 << 3, +} FlatpakContextFeatures; + +struct FlatpakContext +{ + FlatpakContextShares shares; + FlatpakContextShares shares_valid; + FlatpakContextSockets sockets; + FlatpakContextSockets sockets_valid; + FlatpakContextDevices devices; + FlatpakContextDevices devices_valid; + FlatpakContextFeatures features; + FlatpakContextFeatures features_valid; + GHashTable *env_vars; + GHashTable *persistent; + GHashTable *filesystems; + GHashTable *session_bus_policy; + GHashTable *system_bus_policy; + GHashTable *generic_policy; +}; + +extern const char *flatpak_context_sockets[]; +extern const char *flatpak_context_devices[]; +extern const char *flatpak_context_features[]; +extern const char *flatpak_context_shares[]; + +gboolean flatpak_context_parse_filesystem (const char *filesystem_and_mode, + char **filesystem_out, + FlatpakFilesystemMode *mode_out, + GError **error); + +FlatpakContext *flatpak_context_new (void); +void flatpak_context_free (FlatpakContext *context); +void flatpak_context_merge (FlatpakContext *context, + FlatpakContext *other); + +#if 0 + +GOptionEntry *flatpak_context_get_option_entries (void); +GOptionGroup *flatpak_context_get_options (FlatpakContext *context); +gboolean flatpak_context_load_metadata (FlatpakContext *context, + GKeyFile *metakey, + GError **error); +void flatpak_context_save_metadata (FlatpakContext *context, + gboolean flatten, + GKeyFile *metakey); + +#endif + +void flatpak_context_allow_host_fs (FlatpakContext *context); +void flatpak_context_set_session_bus_policy (FlatpakContext *context, + const char *name, + FlatpakPolicy policy); +GStrv flatpak_context_get_session_bus_policy_allowed_own_names (FlatpakContext *context); +void flatpak_context_set_system_bus_policy (FlatpakContext *context, + const char *name, + FlatpakPolicy policy); +void flatpak_context_to_args (FlatpakContext *context, + GPtrArray *args); +FlatpakRunFlags flatpak_context_get_run_flags (FlatpakContext *context); +void flatpak_context_add_bus_filters (FlatpakContext *context, + const char *app_id, + gboolean session_bus, + gboolean sandboxed, + FlatpakBwrap *bwrap); + +gboolean flatpak_context_get_needs_session_bus_proxy (FlatpakContext *context); +gboolean flatpak_context_get_needs_system_bus_proxy (FlatpakContext *context); +gboolean flatpak_context_adds_permissions (FlatpakContext *old_context, + FlatpakContext *new_context); + +void flatpak_context_reset_permissions (FlatpakContext *context); +void flatpak_context_reset_non_permissions (FlatpakContext *context); +void flatpak_context_make_sandboxed (FlatpakContext *context); + +gboolean flatpak_context_allows_features (FlatpakContext *context, + FlatpakContextFeatures features); + +FlatpakContext *flatpak_context_load_for_deploy (FlatpakDeploy *deploy, + GError **error); + +FlatpakExports *flatpak_context_get_exports (FlatpakContext *context, + const char *app_id); + +void flatpak_context_append_bwrap_filesystem (FlatpakContext *context, + FlatpakBwrap *bwrap, + const char *app_id, + GFile *app_id_dir, + GPtrArray *extra_app_id_dirs, + FlatpakExports **exports_out); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakContext, flatpak_context_free) + +extern const char *dont_mount_in_root[]; + +#endif /* __FLATPAK_CONTEXT_H__ */ diff --git a/src/flatpak-context.c b/src/flatpak-context.c new file mode 100644 index 000000000..5cddc0db5 --- /dev/null +++ b/src/flatpak-context.c @@ -0,0 +1,2477 @@ +/* + * Taken from Flatpak, last updated: 1.8.0-74-g354b9a22 + * + * Copyright © 2014-2018 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 <string.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/utsname.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/personality.h> +#include <grp.h> +#include <unistd.h> +#include <gio/gunixfdlist.h> + +#include <gio/gio.h> +#include "libglnx/libglnx.h" + +#include "flatpak-run-private.h" +#include "flatpak-utils-private.h" +#include "flatpak-error.h" + +/* Same order as enum */ +const char *flatpak_context_shares[] = { + "network", + "ipc", + NULL +}; + +/* Same order as enum */ +const char *flatpak_context_sockets[] = { + "x11", + "wayland", + "pulseaudio", + "session-bus", + "system-bus", + "fallback-x11", + "ssh-auth", + "pcsc", + "cups", + NULL +}; + +const char *flatpak_context_devices[] = { + "dri", + "all", + "kvm", + "shm", + NULL +}; + +const char *flatpak_context_features[] = { + "devel", + "multiarch", + "bluetooth", + "canbus", + NULL +}; + +const char *flatpak_context_special_filesystems[] = { + "home", + "host", + "host-etc", + "host-os", + NULL +}; + +FlatpakContext * +flatpak_context_new (void) +{ + FlatpakContext *context; + + context = g_slice_new0 (FlatpakContext); + context->env_vars = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + context->persistent = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + /* filename or special filesystem name => FlatpakFilesystemMode */ + context->filesystems = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + context->session_bus_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + context->system_bus_policy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + context->generic_policy = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_strfreev); + + return context; +} + +void +flatpak_context_free (FlatpakContext *context) +{ + g_hash_table_destroy (context->env_vars); + g_hash_table_destroy (context->persistent); + g_hash_table_destroy (context->filesystems); + g_hash_table_destroy (context->session_bus_policy); + g_hash_table_destroy (context->system_bus_policy); + g_hash_table_destroy (context->generic_policy); + g_slice_free (FlatpakContext, context); +} + +#if 0 + +static guint32 +flatpak_context_bitmask_from_string (const char *name, const char **names) +{ + guint32 i; + + for (i = 0; names[i] != NULL; i++) + { + if (strcmp (names[i], name) == 0) + return 1 << i; + } + + return 0; +} + +static char ** +flatpak_context_bitmask_to_string (guint32 enabled, guint32 valid, const char **names) +{ + guint32 i; + GPtrArray *array; + + array = g_ptr_array_new (); + + for (i = 0; names[i] != NULL; i++) + { + guint32 bitmask = 1 << i; + if (valid & bitmask) + { + if (enabled & bitmask) + g_ptr_array_add (array, g_strdup (names[i])); + else + g_ptr_array_add (array, g_strdup_printf ("!%s", names[i])); + } + } + + g_ptr_array_add (array, NULL); + return (char **) g_ptr_array_free (array, FALSE); +} + +#endif + +static void +flatpak_context_bitmask_to_args (guint32 enabled, guint32 valid, const char **names, + const char *enable_arg, const char *disable_arg, + GPtrArray *args) +{ + guint32 i; + + for (i = 0; names[i] != NULL; i++) + { + guint32 bitmask = 1 << i; + if (valid & bitmask) + { + if (enabled & bitmask) + g_ptr_array_add (args, g_strdup_printf ("%s=%s", enable_arg, names[i])); + else + g_ptr_array_add (args, g_strdup_printf ("%s=%s", disable_arg, names[i])); + } + } +} + +#if 0 + +static FlatpakContextShares +flatpak_context_share_from_string (const char *string, GError **error) +{ + FlatpakContextShares shares = flatpak_context_bitmask_from_string (string, flatpak_context_shares); + + if (shares == 0) + { + g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_shares); + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown share type %s, valid types are: %s"), string, values); + } + + return shares; +} + +static char ** +flatpak_context_shared_to_string (FlatpakContextShares shares, FlatpakContextShares valid) +{ + return flatpak_context_bitmask_to_string (shares, valid, flatpak_context_shares); +} + +#endif + +static void +flatpak_context_shared_to_args (FlatpakContextShares shares, + FlatpakContextShares valid, + GPtrArray *args) +{ + return flatpak_context_bitmask_to_args (shares, valid, flatpak_context_shares, "--share", "--unshare", args); +} + +#if 0 + +static FlatpakPolicy +flatpak_policy_from_string (const char *string, GError **error) +{ + const char *policies[] = { "none", "see", "talk", "own", NULL }; + int i; + g_autofree char *values = NULL; + + for (i = 0; policies[i]; i++) + { + if (strcmp (string, policies[i]) == 0) + return i; + } + + values = g_strjoinv (", ", (char **) policies); + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown policy type %s, valid types are: %s"), string, values); + + return -1; +} + +#endif + +static const char * +flatpak_policy_to_string (FlatpakPolicy policy) +{ + if (policy == FLATPAK_POLICY_SEE) + return "see"; + if (policy == FLATPAK_POLICY_TALK) + return "talk"; + if (policy == FLATPAK_POLICY_OWN) + return "own"; + + return "none"; +} + +#if 0 + +static gboolean +flatpak_verify_dbus_name (const char *name, GError **error) +{ + const char *name_part; + g_autofree char *tmp = NULL; + + if (g_str_has_suffix (name, ".*")) + { + tmp = g_strndup (name, strlen (name) - 2); + name_part = tmp; + } + else + { + name_part = name; + } + + if (g_dbus_is_name (name_part) && !g_dbus_is_unique_name (name_part)) + return TRUE; + + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Invalid dbus name %s"), name); + return FALSE; +} + +static FlatpakContextSockets +flatpak_context_socket_from_string (const char *string, GError **error) +{ + FlatpakContextSockets sockets = flatpak_context_bitmask_from_string (string, flatpak_context_sockets); + + if (sockets == 0) + { + g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_sockets); + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown socket type %s, valid types are: %s"), string, values); + } + + return sockets; +} + +static char ** +flatpak_context_sockets_to_string (FlatpakContextSockets sockets, FlatpakContextSockets valid) +{ + return flatpak_context_bitmask_to_string (sockets, valid, flatpak_context_sockets); +} + +#endif + +static void +flatpak_context_sockets_to_args (FlatpakContextSockets sockets, + FlatpakContextSockets valid, + GPtrArray *args) +{ + return flatpak_context_bitmask_to_args (sockets, valid, flatpak_context_sockets, "--socket", "--nosocket", args); +} + +#if 0 + +static FlatpakContextDevices +flatpak_context_device_from_string (const char *string, GError **error) +{ + FlatpakContextDevices devices = flatpak_context_bitmask_from_string (string, flatpak_context_devices); + + if (devices == 0) + { + g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_devices); + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown device type %s, valid types are: %s"), string, values); + } + return devices; +} + +static char ** +flatpak_context_devices_to_string (FlatpakContextDevices devices, FlatpakContextDevices valid) +{ + return flatpak_context_bitmask_to_string (devices, valid, flatpak_context_devices); +} + +#endif + +static void +flatpak_context_devices_to_args (FlatpakContextDevices devices, + FlatpakContextDevices valid, + GPtrArray *args) +{ + return flatpak_context_bitmask_to_args (devices, valid, flatpak_context_devices, "--device", "--nodevice", args); +} + +#if 0 + +static FlatpakContextFeatures +flatpak_context_feature_from_string (const char *string, GError **error) +{ + FlatpakContextFeatures feature = flatpak_context_bitmask_from_string (string, flatpak_context_features); + + if (feature == 0) + { + g_autofree char *values = g_strjoinv (", ", (char **) flatpak_context_features); + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown feature type %s, valid types are: %s"), string, values); + } + + return feature; +} + +static char ** +flatpak_context_features_to_string (FlatpakContextFeatures features, FlatpakContextFeatures valid) +{ + return flatpak_context_bitmask_to_string (features, valid, flatpak_context_features); +} + +#endif + +static void +flatpak_context_features_to_args (FlatpakContextFeatures features, + FlatpakContextFeatures valid, + GPtrArray *args) +{ + return flatpak_context_bitmask_to_args (features, valid, flatpak_context_features, "--allow", "--disallow", args); +} + +#if 0 + +static void +flatpak_context_add_shares (FlatpakContext *context, + FlatpakContextShares shares) +{ + context->shares_valid |= shares; + context->shares |= shares; +} + +static void +flatpak_context_remove_shares (FlatpakContext *context, + FlatpakContextShares shares) +{ + context->shares_valid |= shares; + context->shares &= ~shares; +} + +static void +flatpak_context_add_sockets (FlatpakContext *context, + FlatpakContextSockets sockets) +{ + context->sockets_valid |= sockets; + context->sockets |= sockets; +} + +static void +flatpak_context_remove_sockets (FlatpakContext *context, + FlatpakContextSockets sockets) +{ + context->sockets_valid |= sockets; + context->sockets &= ~sockets; +} + +static void +flatpak_context_add_devices (FlatpakContext *context, + FlatpakContextDevices devices) +{ + context->devices_valid |= devices; + context->devices |= devices; +} + +static void +flatpak_context_remove_devices (FlatpakContext *context, + FlatpakContextDevices devices) +{ + context->devices_valid |= devices; + context->devices &= ~devices; +} + +static void +flatpak_context_add_features (FlatpakContext *context, + FlatpakContextFeatures features) +{ + context->features_valid |= features; + context->features |= features; +} + +static void +flatpak_context_remove_features (FlatpakContext *context, + FlatpakContextFeatures features) +{ + context->features_valid |= features; + context->features &= ~features; +} + +static void +flatpak_context_set_env_var (FlatpakContext *context, + const char *name, + const char *value) +{ + g_hash_table_insert (context->env_vars, g_strdup (name), g_strdup (value)); +} + +#endif + +void +flatpak_context_set_session_bus_policy (FlatpakContext *context, + const char *name, + FlatpakPolicy policy) +{ + g_hash_table_insert (context->session_bus_policy, g_strdup (name), GINT_TO_POINTER (policy)); +} + +GStrv +flatpak_context_get_session_bus_policy_allowed_own_names (FlatpakContext *context) +{ + GHashTableIter iter; + gpointer key, value; + g_autoptr(GPtrArray) names = g_ptr_array_new_with_free_func (g_free); + + g_hash_table_iter_init (&iter, context->session_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + if (GPOINTER_TO_INT (value) == FLATPAK_POLICY_OWN) + g_ptr_array_add (names, g_strdup (key)); + + g_ptr_array_add (names, NULL); + return (GStrv) g_ptr_array_free (g_steal_pointer (&names), FALSE); +} + +void +flatpak_context_set_system_bus_policy (FlatpakContext *context, + const char *name, + FlatpakPolicy policy) +{ + g_hash_table_insert (context->system_bus_policy, g_strdup (name), GINT_TO_POINTER (policy)); +} + +static void +flatpak_context_apply_generic_policy (FlatpakContext *context, + const char *key, + const char *value) +{ + GPtrArray *new = g_ptr_array_new (); + const char **old_v; + int i; + + g_assert (strchr (key, '.') != NULL); + + old_v = g_hash_table_lookup (context->generic_policy, key); + for (i = 0; old_v != NULL && old_v[i] != NULL; i++) + { + const char *old = old_v[i]; + const char *cmp1 = old; + const char *cmp2 = value; + if (*cmp1 == '!') + cmp1++; + if (*cmp2 == '!') + cmp2++; + if (strcmp (cmp1, cmp2) != 0) + g_ptr_array_add (new, g_strdup (old)); + } + + g_ptr_array_add (new, g_strdup (value)); + g_ptr_array_add (new, NULL); + + g_hash_table_insert (context->generic_policy, g_strdup (key), + g_ptr_array_free (new, FALSE)); +} + +#if 0 + +static void +flatpak_context_set_persistent (FlatpakContext *context, + const char *path) +{ + g_hash_table_insert (context->persistent, g_strdup (path), GINT_TO_POINTER (1)); +} + +#endif + +static gboolean +get_xdg_dir_from_prefix (const char *prefix, + const char **where, + const char **dir) +{ + if (strcmp (prefix, "xdg-data") == 0) + { + if (where) + *where = "data"; + if (dir) + *dir = g_get_user_data_dir (); + return TRUE; + } + if (strcmp (prefix, "xdg-cache") == 0) + { + if (where) + *where = "cache"; + if (dir) + *dir = g_get_user_cache_dir (); + return TRUE; + } + if (strcmp (prefix, "xdg-config") == 0) + { + if (where) + *where = "config"; + if (dir) + *dir = g_get_user_config_dir (); + return TRUE; + } + return FALSE; +} + +/* This looks only in the xdg dirs (config, cache, data), not the user + definable ones */ +static char * +get_xdg_dir_from_string (const char *filesystem, + const char **suffix, + const char **where) +{ + char *slash; + const char *rest; + g_autofree char *prefix = NULL; + const char *dir = NULL; + gsize len; + + slash = strchr (filesystem, '/'); + + if (slash) + len = slash - filesystem; + else + len = strlen (filesystem); + + rest = filesystem + len; + while (*rest == '/') + rest++; + + if (suffix != NULL) + *suffix = rest; + + prefix = g_strndup (filesystem, len); + + if (get_xdg_dir_from_prefix (prefix, where, &dir)) + return g_build_filename (dir, rest, NULL); + + return NULL; +} + +static gboolean +get_xdg_user_dir_from_string (const char *filesystem, + const char **config_key, + const char **suffix, + const char **dir) +{ + char *slash; + const char *rest; + g_autofree char *prefix = NULL; + gsize len; + + slash = strchr (filesystem, '/'); + + if (slash) + len = slash - filesystem; + else + len = strlen (filesystem); + + rest = filesystem + len; + while (*rest == '/') + rest++; + + if (suffix) + *suffix = rest; + + prefix = g_strndup (filesystem, len); + + if (strcmp (prefix, "xdg-desktop") == 0) + { + if (config_key) + *config_key = "XDG_DESKTOP_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_DESKTOP); + return TRUE; + } + if (strcmp (prefix, "xdg-documents") == 0) + { + if (config_key) + *config_key = "XDG_DOCUMENTS_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_DOCUMENTS); + return TRUE; + } + if (strcmp (prefix, "xdg-download") == 0) + { + if (config_key) + *config_key = "XDG_DOWNLOAD_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_DOWNLOAD); + return TRUE; + } + if (strcmp (prefix, "xdg-music") == 0) + { + if (config_key) + *config_key = "XDG_MUSIC_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_MUSIC); + return TRUE; + } + if (strcmp (prefix, "xdg-pictures") == 0) + { + if (config_key) + *config_key = "XDG_PICTURES_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES); + return TRUE; + } + if (strcmp (prefix, "xdg-public-share") == 0) + { + if (config_key) + *config_key = "XDG_PUBLICSHARE_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_PUBLIC_SHARE); + return TRUE; + } + if (strcmp (prefix, "xdg-templates") == 0) + { + if (config_key) + *config_key = "XDG_TEMPLATES_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_TEMPLATES); + return TRUE; + } + if (strcmp (prefix, "xdg-videos") == 0) + { + if (config_key) + *config_key = "XDG_VIDEOS_DIR"; + if (dir) + *dir = g_get_user_special_dir (G_USER_DIRECTORY_VIDEOS); + return TRUE; + } + if (get_xdg_dir_from_prefix (prefix, NULL, dir)) + { + if (config_key) + *config_key = NULL; + return TRUE; + } + /* Don't support xdg-run without suffix, because that doesn't work */ + if (strcmp (prefix, "xdg-run") == 0 && + *rest != 0) + { + if (config_key) + *config_key = NULL; + if (dir) + *dir = flatpak_get_real_xdg_runtime_dir (); + return TRUE; + } + + return FALSE; +} + +static char * +unparse_filesystem_flags (const char *path, + FlatpakFilesystemMode mode) +{ + g_autoptr(GString) s = g_string_new (""); + const char *p; + + for (p = path; *p != 0; p++) + { + if (*p == ':') + g_string_append (s, "\\:"); + else if (*p == '\\') + g_string_append (s, "\\\\"); + else + g_string_append_c (s, *p); + } + + switch (mode) + { + case FLATPAK_FILESYSTEM_MODE_READ_ONLY: + g_string_append (s, ":ro"); + break; + + case FLATPAK_FILESYSTEM_MODE_CREATE: + g_string_append (s, ":create"); + break; + + case FLATPAK_FILESYSTEM_MODE_READ_WRITE: + break; + + case FLATPAK_FILESYSTEM_MODE_NONE: + default: + g_warning ("Unexpected filesystem mode %d", mode); + break; + } + + return g_string_free (g_steal_pointer (&s), FALSE); +} + +static char * +parse_filesystem_flags (const char *filesystem, + FlatpakFilesystemMode *mode_out) +{ + g_autoptr(GString) s = g_string_new (""); + const char *p, *suffix; + FlatpakFilesystemMode mode; + + p = filesystem; + while (*p != 0 && *p != ':') + { + if (*p == '\\') + { + p++; + if (*p != 0) + g_string_append_c (s, *p++); + } + else + g_string_append_c (s, *p++); + } + + mode = FLATPAK_FILESYSTEM_MODE_READ_WRITE; + + if (*p == ':') + { + suffix = p + 1; + + if (strcmp (suffix, "ro") == 0) + mode = FLATPAK_FILESYSTEM_MODE_READ_ONLY; + else if (strcmp (suffix, "rw") == 0) + mode = FLATPAK_FILESYSTEM_MODE_READ_WRITE; + else if (strcmp (suffix, "create") == 0) + mode = FLATPAK_FILESYSTEM_MODE_CREATE; + else if (*suffix != 0) + g_warning ("Unexpected filesystem suffix %s, ignoring", suffix); + } + + if (mode_out) + *mode_out = mode; + + return g_string_free (g_steal_pointer (&s), FALSE); +} + +gboolean +flatpak_context_parse_filesystem (const char *filesystem_and_mode, + char **filesystem_out, + FlatpakFilesystemMode *mode_out, + GError **error) +{ + g_autofree char *filesystem = parse_filesystem_flags (filesystem_and_mode, mode_out); + char *slash; + + slash = strchr (filesystem, '/'); + + /* Forbid /../ in paths */ + if (slash != NULL) + { + if (g_str_has_prefix (slash + 1, "../") || + g_str_has_suffix (slash + 1, "/..") || + strstr (slash + 1, "/../") != NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("Filesystem location \"%s\" contains \"..\""), + filesystem); + return FALSE; + } + + /* Convert "//" and "/./" to "/" */ + for (; slash != NULL; slash = strchr (slash + 1, '/')) + { + while (TRUE) + { + if (slash[1] == '/') + memmove (slash + 1, slash + 2, strlen (slash + 2) + 1); + else if (slash[1] == '.' && slash[2] == '/') + memmove (slash + 1, slash + 3, strlen (slash + 3) + 1); + else + break; + } + } + + /* Eliminate trailing "/." or "/". */ + while (TRUE) + { + slash = strrchr (filesystem, '/'); + + if (slash != NULL && + ((slash != filesystem && slash[1] == '\0') || + (slash[1] == '.' && slash[2] == '\0'))) + *slash = '\0'; + else + break; + } + + if (filesystem[0] == '/' && filesystem[1] == '\0') + { + /* We don't allow --filesystem=/ as equivalent to host, because + * it doesn't do what you'd think: --filesystem=host mounts some + * host directories in /run/host, not in the root. */ + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--filesystem=/ is not available, " + "use --filesystem=host for a similar result")); + return FALSE; + } + } + + if (g_strv_contains (flatpak_context_special_filesystems, filesystem) || + get_xdg_user_dir_from_string (filesystem, NULL, NULL, NULL) || + g_str_has_prefix (filesystem, "~/") || + g_str_has_prefix (filesystem, "/")) + { + if (filesystem_out != NULL) + *filesystem_out = g_steal_pointer (&filesystem); + + return TRUE; + } + + if (strcmp (filesystem, "~") == 0) + { + if (filesystem_out != NULL) + *filesystem_out = g_strdup ("home"); + + return TRUE; + } + + if (g_str_has_prefix (filesystem, "home/")) + { + if (filesystem_out != NULL) + *filesystem_out = g_strconcat ("~/", filesystem + 5, NULL); + + return TRUE; + } + + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Unknown filesystem location %s, valid locations are: host, host-os, host-etc, home, xdg-*[/…], ~/dir, /dir"), filesystem); + return FALSE; +} + +static void +flatpak_context_take_filesystem (FlatpakContext *context, + char *fs, + FlatpakFilesystemMode mode) +{ + g_hash_table_insert (context->filesystems, fs, GINT_TO_POINTER (mode)); +} + +void +flatpak_context_merge (FlatpakContext *context, + FlatpakContext *other) +{ + GHashTableIter iter; + gpointer key, value; + + context->shares &= ~other->shares_valid; + context->shares |= other->shares; + context->shares_valid |= other->shares_valid; + context->sockets &= ~other->sockets_valid; + context->sockets |= other->sockets; + context->sockets_valid |= other->sockets_valid; + context->devices &= ~other->devices_valid; + context->devices |= other->devices; + context->devices_valid |= other->devices_valid; + context->features &= ~other->features_valid; + context->features |= other->features; + context->features_valid |= other->features_valid; + + g_hash_table_iter_init (&iter, other->env_vars); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->env_vars, g_strdup (key), g_strdup (value)); + + g_hash_table_iter_init (&iter, other->persistent); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->persistent, g_strdup (key), value); + + g_hash_table_iter_init (&iter, other->filesystems); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->filesystems, g_strdup (key), value); + + g_hash_table_iter_init (&iter, other->session_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->session_bus_policy, g_strdup (key), value); + + g_hash_table_iter_init (&iter, other->system_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->system_bus_policy, g_strdup (key), value); + + g_hash_table_iter_init (&iter, other->system_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_hash_table_insert (context->system_bus_policy, g_strdup (key), value); + + g_hash_table_iter_init (&iter, other->generic_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char **policy_values = (const char **) value; + int i; + + for (i = 0; policy_values[i] != NULL; i++) + flatpak_context_apply_generic_policy (context, (char *) key, policy_values[i]); + } +} + +#if 0 + +static gboolean +option_share_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextShares share; + + share = flatpak_context_share_from_string (value, error); + if (share == 0) + return FALSE; + + flatpak_context_add_shares (context, share); + + return TRUE; +} + +static gboolean +option_unshare_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextShares share; + + share = flatpak_context_share_from_string (value, error); + if (share == 0) + return FALSE; + + flatpak_context_remove_shares (context, share); + + return TRUE; +} + +static gboolean +option_socket_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextSockets socket; + + socket = flatpak_context_socket_from_string (value, error); + if (socket == 0) + return FALSE; + + if (socket == FLATPAK_CONTEXT_SOCKET_FALLBACK_X11) + socket |= FLATPAK_CONTEXT_SOCKET_X11; + + flatpak_context_add_sockets (context, socket); + + return TRUE; +} + +static gboolean +option_nosocket_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextSockets socket; + + socket = flatpak_context_socket_from_string (value, error); + if (socket == 0) + return FALSE; + + if (socket == FLATPAK_CONTEXT_SOCKET_FALLBACK_X11) + socket |= FLATPAK_CONTEXT_SOCKET_X11; + + flatpak_context_remove_sockets (context, socket); + + return TRUE; +} + +static gboolean +option_device_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextDevices device; + + device = flatpak_context_device_from_string (value, error); + if (device == 0) + return FALSE; + + flatpak_context_add_devices (context, device); + + return TRUE; +} + +static gboolean +option_nodevice_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextDevices device; + + device = flatpak_context_device_from_string (value, error); + if (device == 0) + return FALSE; + + flatpak_context_remove_devices (context, device); + + return TRUE; +} + +static gboolean +option_allow_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextFeatures feature; + + feature = flatpak_context_feature_from_string (value, error); + if (feature == 0) + return FALSE; + + flatpak_context_add_features (context, feature); + + return TRUE; +} + +static gboolean +option_disallow_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + FlatpakContextFeatures feature; + + feature = flatpak_context_feature_from_string (value, error); + if (feature == 0) + return FALSE; + + flatpak_context_remove_features (context, feature); + + return TRUE; +} + +static gboolean +option_filesystem_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + g_autofree char *fs = NULL; + FlatpakFilesystemMode mode; + + if (!flatpak_context_parse_filesystem (value, &fs, &mode, error)) + return FALSE; + + flatpak_context_take_filesystem (context, g_steal_pointer (&fs), mode); + return TRUE; +} + +static gboolean +option_nofilesystem_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + g_autofree char *fs = NULL; + FlatpakFilesystemMode mode; + + if (!flatpak_context_parse_filesystem (value, &fs, &mode, error)) + return FALSE; + + flatpak_context_take_filesystem (context, g_steal_pointer (&fs), + FLATPAK_FILESYSTEM_MODE_NONE); + return TRUE; +} + +static gboolean +option_env_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + g_auto(GStrv) split = g_strsplit (value, "=", 2); + + if (split == NULL || split[0] == NULL || split[0][0] == 0 || split[1] == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, + _("Invalid env format %s"), value); + return FALSE; + } + + flatpak_context_set_env_var (context, split[0], split[1]); + return TRUE; +} + +static gboolean +option_own_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_OWN); + return TRUE; +} + +static gboolean +option_talk_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_TALK); + return TRUE; +} + +static gboolean +option_no_talk_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_session_bus_policy (context, value, FLATPAK_POLICY_NONE); + return TRUE; +} + +static gboolean +option_system_own_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_OWN); + return TRUE; +} + +static gboolean +option_system_talk_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_TALK); + return TRUE; +} + +static gboolean +option_system_no_talk_name_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + if (!flatpak_verify_dbus_name (value, error)) + return FALSE; + + flatpak_context_set_system_bus_policy (context, value, FLATPAK_POLICY_NONE); + return TRUE; +} + +static gboolean +option_add_generic_policy_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + char *t; + g_autofree char *key = NULL; + const char *policy_value; + + t = strchr (value, '='); + if (t == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--add-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); + return FALSE; + } + policy_value = t + 1; + key = g_strndup (value, t - value); + if (strchr (key, '.') == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--add-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); + return FALSE; + } + + if (policy_value[0] == '!') + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--add-policy values can't start with \"!\"")); + return FALSE; + } + + flatpak_context_apply_generic_policy (context, key, policy_value); + + return TRUE; +} + +static gboolean +option_remove_generic_policy_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + char *t; + g_autofree char *key = NULL; + const char *policy_value; + g_autofree char *extended_value = NULL; + + t = strchr (value, '='); + if (t == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); + return FALSE; + } + policy_value = t + 1; + key = g_strndup (value, t - value); + if (strchr (key, '.') == NULL) + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--remove-policy arguments must be in the form SUBSYSTEM.KEY=VALUE")); + return FALSE; + } + + if (policy_value[0] == '!') + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + _("--remove-policy values can't start with \"!\"")); + return FALSE; + } + + extended_value = g_strconcat ("!", policy_value, NULL); + + flatpak_context_apply_generic_policy (context, key, extended_value); + + return TRUE; +} + +static gboolean +option_persist_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + FlatpakContext *context = data; + + flatpak_context_set_persistent (context, value); + return TRUE; +} + +static gboolean option_no_desktop_deprecated; + +static GOptionEntry context_options[] = { + { "share", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_share_cb, N_("Share with host"), N_("SHARE") }, + { "unshare", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_unshare_cb, N_("Unshare with host"), N_("SHARE") }, + { "socket", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_socket_cb, N_("Expose socket to app"), N_("SOCKET") }, + { "nosocket", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nosocket_cb, N_("Don't expose socket to app"), N_("SOCKET") }, + { "device", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_device_cb, N_("Expose device to app"), N_("DEVICE") }, + { "nodevice", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nodevice_cb, N_("Don't expose device to app"), N_("DEVICE") }, + { "allow", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_allow_cb, N_("Allow feature"), N_("FEATURE") }, + { "disallow", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_disallow_cb, N_("Don't allow feature"), N_("FEATURE") }, + { "filesystem", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_filesystem_cb, N_("Expose filesystem to app (:ro for read-only)"), N_("FILESYSTEM[:ro]") }, + { "nofilesystem", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_nofilesystem_cb, N_("Don't expose filesystem to app"), N_("FILESYSTEM") }, + { "env", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_env_cb, N_("Set environment variable"), N_("VAR=VALUE") }, + { "own-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_own_name_cb, N_("Allow app to own name on the session bus"), N_("DBUS_NAME") }, + { "talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_talk_name_cb, N_("Allow app to talk to name on the session bus"), N_("DBUS_NAME") }, + { "no-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_no_talk_name_cb, N_("Don't allow app to talk to name on the session bus"), N_("DBUS_NAME") }, + { "system-own-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_own_name_cb, N_("Allow app to own name on the system bus"), N_("DBUS_NAME") }, + { "system-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_talk_name_cb, N_("Allow app to talk to name on the system bus"), N_("DBUS_NAME") }, + { "system-no-talk-name", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_system_no_talk_name_cb, N_("Don't allow app to talk to name on the system bus"), N_("DBUS_NAME") }, + { "add-policy", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_add_generic_policy_cb, N_("Add generic policy option"), N_("SUBSYSTEM.KEY=VALUE") }, + { "remove-policy", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_remove_generic_policy_cb, N_("Remove generic policy option"), N_("SUBSYSTEM.KEY=VALUE") }, + { "persist", 0, G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, &option_persist_cb, N_("Persist home directory subpath"), N_("FILENAME") }, + /* This is not needed/used anymore, so hidden, but we accept it for backwards compat */ + { "no-desktop", 0, G_OPTION_FLAG_IN_MAIN | G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &option_no_desktop_deprecated, N_("Don't require a running session (no cgroups creation)"), NULL }, + { NULL } +}; + +GOptionEntry * +flatpak_context_get_option_entries (void) +{ + return context_options; +} + +GOptionGroup * +flatpak_context_get_options (FlatpakContext *context) +{ + GOptionGroup *group; + + group = g_option_group_new ("environment", + "Runtime Environment", + "Runtime Environment", + context, + NULL); + g_option_group_set_translation_domain (group, GETTEXT_PACKAGE); + + g_option_group_add_entries (group, context_options); + + return group; +} + +static const char * +parse_negated (const char *option, gboolean *negated) +{ + if (option[0] == '!') + { + option++; + *negated = TRUE; + } + else + { + *negated = FALSE; + } + return option; +} + +/* + * Merge the FLATPAK_METADATA_GROUP_CONTEXT, + * FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, + * FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY and + * FLATPAK_METADATA_GROUP_ENVIRONMENT groups, and all groups starting + * with FLATPAK_METADATA_GROUP_PREFIX_POLICY, from metakey into context. + * + * This is a merge, not a replace! + */ +gboolean +flatpak_context_load_metadata (FlatpakContext *context, + GKeyFile *metakey, + GError **error) +{ + gboolean remove; + g_auto(GStrv) groups = NULL; + int i; + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SHARED, NULL)) + { + g_auto(GStrv) shares = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SHARED, NULL, error); + if (shares == NULL) + return FALSE; + + for (i = 0; shares[i] != NULL; i++) + { + FlatpakContextShares share; + + share = flatpak_context_share_from_string (parse_negated (shares[i], &remove), NULL); + if (share == 0) + g_debug ("Unknown share type %s", shares[i]); + else + { + if (remove) + flatpak_context_remove_shares (context, share); + else + flatpak_context_add_shares (context, share); + } + } + } + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_SOCKETS, NULL)) + { + g_auto(GStrv) sockets = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SOCKETS, NULL, error); + if (sockets == NULL) + return FALSE; + + for (i = 0; sockets[i] != NULL; i++) + { + FlatpakContextSockets socket = flatpak_context_socket_from_string (parse_negated (sockets[i], &remove), NULL); + if (socket == 0) + g_debug ("Unknown socket type %s", sockets[i]); + else + { + if (remove) + flatpak_context_remove_sockets (context, socket); + else + flatpak_context_add_sockets (context, socket); + } + } + } + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_DEVICES, NULL)) + { + g_auto(GStrv) devices = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_DEVICES, NULL, error); + if (devices == NULL) + return FALSE; + + + for (i = 0; devices[i] != NULL; i++) + { + FlatpakContextDevices device = flatpak_context_device_from_string (parse_negated (devices[i], &remove), NULL); + if (device == 0) + g_debug ("Unknown device type %s", devices[i]); + else + { + if (remove) + flatpak_context_remove_devices (context, device); + else + flatpak_context_add_devices (context, device); + } + } + } + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FEATURES, NULL)) + { + g_auto(GStrv) features = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FEATURES, NULL, error); + if (features == NULL) + return FALSE; + + + for (i = 0; features[i] != NULL; i++) + { + FlatpakContextFeatures feature = flatpak_context_feature_from_string (parse_negated (features[i], &remove), NULL); + if (feature == 0) + g_debug ("Unknown feature type %s", features[i]); + else + { + if (remove) + flatpak_context_remove_features (context, feature); + else + flatpak_context_add_features (context, feature); + } + } + } + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_FILESYSTEMS, NULL)) + { + g_auto(GStrv) filesystems = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FILESYSTEMS, NULL, error); + if (filesystems == NULL) + return FALSE; + + for (i = 0; filesystems[i] != NULL; i++) + { + const char *fs = parse_negated (filesystems[i], &remove); + g_autofree char *filesystem = NULL; + FlatpakFilesystemMode mode; + + if (!flatpak_context_parse_filesystem (fs, &filesystem, &mode, NULL)) + g_debug ("Unknown filesystem type %s", filesystems[i]); + else + { + if (remove) + flatpak_context_take_filesystem (context, g_steal_pointer (&filesystem), + FLATPAK_FILESYSTEM_MODE_NONE); + else + flatpak_context_take_filesystem (context, g_steal_pointer (&filesystem), mode); + } + } + } + + if (g_key_file_has_key (metakey, FLATPAK_METADATA_GROUP_CONTEXT, FLATPAK_METADATA_KEY_PERSISTENT, NULL)) + { + g_auto(GStrv) persistent = g_key_file_get_string_list (metakey, FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_PERSISTENT, NULL, error); + if (persistent == NULL) + return FALSE; + + for (i = 0; persistent[i] != NULL; i++) + flatpak_context_set_persistent (context, persistent[i]); + } + + if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY)) + { + g_auto(GStrv) keys = NULL; + gsize i, keys_count; + + keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, &keys_count, NULL); + for (i = 0; i < keys_count; i++) + { + const char *key = keys[i]; + g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, key, NULL); + FlatpakPolicy policy; + + if (!flatpak_verify_dbus_name (key, error)) + return FALSE; + + policy = flatpak_policy_from_string (value, NULL); + if ((int) policy != -1) + flatpak_context_set_session_bus_policy (context, key, policy); + } + } + + if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY)) + { + g_auto(GStrv) keys = NULL; + gsize i, keys_count; + + keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, &keys_count, NULL); + for (i = 0; i < keys_count; i++) + { + const char *key = keys[i]; + g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, key, NULL); + FlatpakPolicy policy; + + if (!flatpak_verify_dbus_name (key, error)) + return FALSE; + + policy = flatpak_policy_from_string (value, NULL); + if ((int) policy != -1) + flatpak_context_set_system_bus_policy (context, key, policy); + } + } + + if (g_key_file_has_group (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT)) + { + g_auto(GStrv) keys = NULL; + gsize i, keys_count; + + keys = g_key_file_get_keys (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, &keys_count, NULL); + for (i = 0; i < keys_count; i++) + { + const char *key = keys[i]; + g_autofree char *value = g_key_file_get_string (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, key, NULL); + + flatpak_context_set_env_var (context, key, value); + } + } + + groups = g_key_file_get_groups (metakey, NULL); + for (i = 0; groups[i] != NULL; i++) + { + const char *group = groups[i]; + const char *subsystem; + int j; + + if (g_str_has_prefix (group, FLATPAK_METADATA_GROUP_PREFIX_POLICY)) + { + g_auto(GStrv) keys = NULL; + subsystem = group + strlen (FLATPAK_METADATA_GROUP_PREFIX_POLICY); + keys = g_key_file_get_keys (metakey, group, NULL, NULL); + for (j = 0; keys != NULL && keys[j] != NULL; j++) + { + const char *key = keys[j]; + g_autofree char *policy_key = g_strdup_printf ("%s.%s", subsystem, key); + g_auto(GStrv) values = NULL; + int k; + + values = g_key_file_get_string_list (metakey, group, key, NULL, NULL); + for (k = 0; values != NULL && values[k] != NULL; k++) + flatpak_context_apply_generic_policy (context, policy_key, + values[k]); + } + } + } + + return TRUE; +} + +/* + * Save the FLATPAK_METADATA_GROUP_CONTEXT, + * FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, + * FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY and + * FLATPAK_METADATA_GROUP_ENVIRONMENT groups, and all groups starting + * with FLATPAK_METADATA_GROUP_PREFIX_POLICY, into metakey + */ +void +flatpak_context_save_metadata (FlatpakContext *context, + gboolean flatten, + GKeyFile *metakey) +{ + g_auto(GStrv) shared = NULL; + g_auto(GStrv) sockets = NULL; + g_auto(GStrv) devices = NULL; + g_auto(GStrv) features = NULL; + GHashTableIter iter; + gpointer key, value; + FlatpakContextShares shares_mask = context->shares; + FlatpakContextShares shares_valid = context->shares_valid; + FlatpakContextSockets sockets_mask = context->sockets; + FlatpakContextSockets sockets_valid = context->sockets_valid; + FlatpakContextDevices devices_mask = context->devices; + FlatpakContextDevices devices_valid = context->devices_valid; + FlatpakContextFeatures features_mask = context->features; + FlatpakContextFeatures features_valid = context->features_valid; + g_auto(GStrv) groups = NULL; + int i; + + if (flatten) + { + /* A flattened format means we don't expect this to be merged on top of + another context. In that case we never need to negate any flags. + We calculate this by removing the zero parts of the mask from the valid set. + */ + /* First we make sure only the valid parts of the mask are set, in case we + got some leftover */ + shares_mask &= shares_valid; + sockets_mask &= sockets_valid; + devices_mask &= devices_valid; + features_mask &= features_valid; + + /* Then just set the valid set to be the mask set */ + shares_valid = shares_mask; + sockets_valid = sockets_mask; + devices_valid = devices_mask; + features_valid = features_mask; + } + + shared = flatpak_context_shared_to_string (shares_mask, shares_valid); + sockets = flatpak_context_sockets_to_string (sockets_mask, sockets_valid); + devices = flatpak_context_devices_to_string (devices_mask, devices_valid); + features = flatpak_context_features_to_string (features_mask, features_valid); + + if (shared[0] != NULL) + { + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SHARED, + (const char * const *) shared, g_strv_length (shared)); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SHARED, + NULL); + } + + if (sockets[0] != NULL) + { + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SOCKETS, + (const char * const *) sockets, g_strv_length (sockets)); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_SOCKETS, + NULL); + } + + if (devices[0] != NULL) + { + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_DEVICES, + (const char * const *) devices, g_strv_length (devices)); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_DEVICES, + NULL); + } + + if (features[0] != NULL) + { + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FEATURES, + (const char * const *) features, g_strv_length (features)); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FEATURES, + NULL); + } + + if (g_hash_table_size (context->filesystems) > 0) + { + g_autoptr(GPtrArray) array = g_ptr_array_new_with_free_func (g_free); + + g_hash_table_iter_init (&iter, context->filesystems); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); + + if (mode != FLATPAK_FILESYSTEM_MODE_NONE) + g_ptr_array_add (array, unparse_filesystem_flags (key, mode)); + else + g_ptr_array_add (array, g_strconcat ("!", key, NULL)); + } + + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FILESYSTEMS, + (const char * const *) array->pdata, array->len); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_FILESYSTEMS, + NULL); + } + + if (g_hash_table_size (context->persistent) > 0) + { + g_autofree char **keys = (char **) g_hash_table_get_keys_as_array (context->persistent, NULL); + + g_key_file_set_string_list (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_PERSISTENT, + (const char * const *) keys, g_strv_length (keys)); + } + else + { + g_key_file_remove_key (metakey, + FLATPAK_METADATA_GROUP_CONTEXT, + FLATPAK_METADATA_KEY_PERSISTENT, + NULL); + } + + g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, NULL); + g_hash_table_iter_init (&iter, context->session_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + FlatpakPolicy policy = GPOINTER_TO_INT (value); + + if (flatten && (policy == 0)) + continue; + + g_key_file_set_string (metakey, + FLATPAK_METADATA_GROUP_SESSION_BUS_POLICY, + (char *) key, flatpak_policy_to_string (policy)); + } + + g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, NULL); + g_hash_table_iter_init (&iter, context->system_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + FlatpakPolicy policy = GPOINTER_TO_INT (value); + + if (flatten && (policy == 0)) + continue; + + g_key_file_set_string (metakey, + FLATPAK_METADATA_GROUP_SYSTEM_BUS_POLICY, + (char *) key, flatpak_policy_to_string (policy)); + } + + g_key_file_remove_group (metakey, FLATPAK_METADATA_GROUP_ENVIRONMENT, NULL); + g_hash_table_iter_init (&iter, context->env_vars); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + g_key_file_set_string (metakey, + FLATPAK_METADATA_GROUP_ENVIRONMENT, + (char *) key, (char *) value); + } + + + groups = g_key_file_get_groups (metakey, NULL); + for (i = 0; groups[i] != NULL; i++) + { + const char *group = groups[i]; + if (g_str_has_prefix (group, FLATPAK_METADATA_GROUP_PREFIX_POLICY)) + g_key_file_remove_group (metakey, group, NULL); + } + + g_hash_table_iter_init (&iter, context->generic_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + g_auto(GStrv) parts = g_strsplit ((const char *) key, ".", 2); + g_autofree char *group = NULL; + g_assert (parts[1] != NULL); + const char **policy_values = (const char **) value; + g_autoptr(GPtrArray) new = g_ptr_array_new (); + + for (i = 0; policy_values[i] != NULL; i++) + { + const char *policy_value = policy_values[i]; + + if (!flatten || policy_value[0] != '!') + g_ptr_array_add (new, (char *) policy_value); + } + + if (new->len > 0) + { + group = g_strconcat (FLATPAK_METADATA_GROUP_PREFIX_POLICY, + parts[0], NULL); + g_key_file_set_string_list (metakey, group, parts[1], + (const char * const *) new->pdata, + new->len); + } + } +} + +#endif + +void +flatpak_context_allow_host_fs (FlatpakContext *context) +{ + flatpak_context_take_filesystem (context, g_strdup ("host"), FLATPAK_FILESYSTEM_MODE_READ_WRITE); +} + +gboolean +flatpak_context_get_needs_session_bus_proxy (FlatpakContext *context) +{ + return g_hash_table_size (context->session_bus_policy) > 0; +} + +gboolean +flatpak_context_get_needs_system_bus_proxy (FlatpakContext *context) +{ + return g_hash_table_size (context->system_bus_policy) > 0; +} + +static gboolean +adds_flags (guint32 old_flags, guint32 new_flags) +{ + return (new_flags & ~old_flags) != 0; +} + +static gboolean +adds_bus_policy (GHashTable *old, GHashTable *new) +{ + GLNX_HASH_TABLE_FOREACH_KV (new, const char *, name, gpointer, _new_policy) + { + int new_policy = GPOINTER_TO_INT (_new_policy); + int old_policy = GPOINTER_TO_INT (g_hash_table_lookup (old, name)); + if (new_policy > old_policy) + return TRUE; + } + + return FALSE; +} + +static gboolean +adds_generic_policy (GHashTable *old, GHashTable *new) +{ + GLNX_HASH_TABLE_FOREACH_KV (new, const char *, key, GPtrArray *, new_values) + { + GPtrArray *old_values = g_hash_table_lookup (old, key); + int i; + + if (new_values == NULL || new_values->len == 0) + continue; + + if (old_values == NULL || old_values->len == 0) + return TRUE; + + for (i = 0; i < new_values->len; i++) + { + const char *new_value = g_ptr_array_index (new_values, i); + + if (!flatpak_g_ptr_array_contains_string (old_values, new_value)) + return TRUE; + } + } + + return FALSE; +} + +static gboolean +adds_filesystem_access (GHashTable *old, GHashTable *new) +{ + FlatpakFilesystemMode old_host_mode = GPOINTER_TO_INT (g_hash_table_lookup (old, "host")); + + GLNX_HASH_TABLE_FOREACH_KV (new, const char *, location, gpointer, _new_mode) + { + FlatpakFilesystemMode new_mode = GPOINTER_TO_INT (_new_mode); + FlatpakFilesystemMode old_mode = GPOINTER_TO_INT (g_hash_table_lookup (old, location)); + + /* Allow more limited access to the same thing */ + if (new_mode <= old_mode) + continue; + + /* Allow more limited access if we used to have access to everything */ + if (new_mode <= old_host_mode) + continue; + + /* For the remainder we have to be pessimistic, for instance even + if we have home access we can't allow adding access to ~/foo, + because foo might be a symlink outside home which didn't work + before but would work with an explicit access to that + particular file. */ + + return TRUE; + } + + return FALSE; +} + + +gboolean +flatpak_context_adds_permissions (FlatpakContext *old, + FlatpakContext *new) +{ + guint32 old_sockets; + + if (adds_flags (old->shares & old->shares_valid, + new->shares & new->shares_valid)) + return TRUE; + + old_sockets = old->sockets & old->sockets_valid; + + /* If we used to allow X11, also allow new fallback X11, + as that is actually less permissions */ + if (old_sockets & FLATPAK_CONTEXT_SOCKET_X11) + old_sockets |= FLATPAK_CONTEXT_SOCKET_FALLBACK_X11; + + if (adds_flags (old_sockets, + new->sockets & new->sockets_valid)) + return TRUE; + + if (adds_flags (old->devices & old->devices_valid, + new->devices & new->devices_valid)) + return TRUE; + + /* We allow upgrade to multiarch, that is really not a huge problem */ + if (adds_flags ((old->features & old->features_valid) | FLATPAK_CONTEXT_FEATURE_MULTIARCH, + new->features & new->features_valid)) + return TRUE; + + if (adds_bus_policy (old->session_bus_policy, new->session_bus_policy)) + return TRUE; + + if (adds_bus_policy (old->system_bus_policy, new->system_bus_policy)) + return TRUE; + + if (adds_generic_policy (old->generic_policy, new->generic_policy)) + return TRUE; + + if (adds_filesystem_access (old->filesystems, new->filesystems)) + return TRUE; + + return FALSE; +} + +gboolean +flatpak_context_allows_features (FlatpakContext *context, + FlatpakContextFeatures features) +{ + return (context->features & features) == features; +} + +void +flatpak_context_to_args (FlatpakContext *context, + GPtrArray *args) +{ + GHashTableIter iter; + gpointer key, value; + + flatpak_context_shared_to_args (context->shares, context->shares_valid, args); + flatpak_context_sockets_to_args (context->sockets, context->sockets_valid, args); + flatpak_context_devices_to_args (context->devices, context->devices_valid, args); + flatpak_context_features_to_args (context->features, context->features_valid, args); + + g_hash_table_iter_init (&iter, context->env_vars); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_ptr_array_add (args, g_strdup_printf ("--env=%s=%s", (char *) key, (char *) value)); + + g_hash_table_iter_init (&iter, context->persistent); + while (g_hash_table_iter_next (&iter, &key, &value)) + g_ptr_array_add (args, g_strdup_printf ("--persist=%s", (char *) key)); + + g_hash_table_iter_init (&iter, context->session_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *name = key; + FlatpakPolicy policy = GPOINTER_TO_INT (value); + + g_ptr_array_add (args, g_strdup_printf ("--%s-name=%s", flatpak_policy_to_string (policy), name)); + } + + g_hash_table_iter_init (&iter, context->system_bus_policy); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *name = key; + FlatpakPolicy policy = GPOINTER_TO_INT (value); + + g_ptr_array_add (args, g_strdup_printf ("--system-%s-name=%s", flatpak_policy_to_string (policy), name)); + } + + g_hash_table_iter_init (&iter, context->filesystems); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); + + if (mode != FLATPAK_FILESYSTEM_MODE_NONE) + { + g_autofree char *fs = unparse_filesystem_flags (key, mode); + g_ptr_array_add (args, g_strdup_printf ("--filesystem=%s", fs)); + } + else + g_ptr_array_add (args, g_strdup_printf ("--nofilesystem=%s", (char *) key)); + } +} + +void +flatpak_context_add_bus_filters (FlatpakContext *context, + const char *app_id, + gboolean session_bus, + gboolean sandboxed, + FlatpakBwrap *bwrap) +{ + GHashTable *ht; + GHashTableIter iter; + gpointer key, value; + + flatpak_bwrap_add_arg (bwrap, "--filter"); + if (app_id && session_bus) + { + if (!sandboxed) + { + flatpak_bwrap_add_arg_printf (bwrap, "--own=%s.*", app_id); + flatpak_bwrap_add_arg_printf (bwrap, "--own=org.mpris.MediaPlayer2.%s.*", app_id); + } + else + flatpak_bwrap_add_arg_printf (bwrap, "--own=%s.Sandboxed.*", app_id); + } + + if (session_bus) + ht = context->session_bus_policy; + else + ht = context->system_bus_policy; + + g_hash_table_iter_init (&iter, ht); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + FlatpakPolicy policy = GPOINTER_TO_INT (value); + + if (policy > 0) + flatpak_bwrap_add_arg_printf (bwrap, "--%s=%s", + flatpak_policy_to_string (policy), + (char *) key); + } +} + +void +flatpak_context_reset_non_permissions (FlatpakContext *context) +{ + g_hash_table_remove_all (context->env_vars); +} + +void +flatpak_context_reset_permissions (FlatpakContext *context) +{ + context->shares_valid = 0; + context->sockets_valid = 0; + context->devices_valid = 0; + context->features_valid = 0; + + context->shares = 0; + context->sockets = 0; + context->devices = 0; + context->features = 0; + + g_hash_table_remove_all (context->persistent); + g_hash_table_remove_all (context->filesystems); + g_hash_table_remove_all (context->session_bus_policy); + g_hash_table_remove_all (context->system_bus_policy); + g_hash_table_remove_all (context->generic_policy); +} + +void +flatpak_context_make_sandboxed (FlatpakContext *context) +{ + /* We drop almost everything from the app permission, except + * multiarch which is inherited, to make sure app code keeps + * running. */ + context->shares_valid &= 0; + context->sockets_valid &= 0; + context->devices_valid &= 0; + context->features_valid &= FLATPAK_CONTEXT_FEATURE_MULTIARCH; + + context->shares &= context->shares_valid; + context->sockets &= context->sockets_valid; + context->devices &= context->devices_valid; + context->features &= context->features_valid; + + g_hash_table_remove_all (context->persistent); + g_hash_table_remove_all (context->filesystems); + g_hash_table_remove_all (context->session_bus_policy); + g_hash_table_remove_all (context->system_bus_policy); + g_hash_table_remove_all (context->generic_policy); +} + +const char *dont_mount_in_root[] = { + ".", "..", "lib", "lib32", "lib64", "bin", "sbin", "usr", "boot", "root", + "tmp", "etc", "app", "run", "proc", "sys", "dev", "var", NULL +}; + +static void +flatpak_context_export (FlatpakContext *context, + FlatpakExports *exports, + GFile *app_id_dir, + GPtrArray *extra_app_id_dirs, + gboolean do_create, + GString *xdg_dirs_conf, + gboolean *home_access_out) +{ + gboolean home_access = FALSE; + FlatpakFilesystemMode fs_mode, os_mode, etc_mode, home_mode; + GHashTableIter iter; + gpointer key, value; + + fs_mode = (FlatpakFilesystemMode) g_hash_table_lookup (context->filesystems, "host"); + if (fs_mode != FLATPAK_FILESYSTEM_MODE_NONE) + { + DIR *dir; + struct dirent *dirent; + + g_debug ("Allowing host-fs access"); + home_access = TRUE; + + /* Bind mount most dirs in / into the new root */ + dir = opendir ("/"); + if (dir != NULL) + { + while ((dirent = readdir (dir))) + { + g_autofree char *path = NULL; + + if (g_strv_contains (dont_mount_in_root, dirent->d_name)) + continue; + + path = g_build_filename ("/", dirent->d_name, NULL); + flatpak_exports_add_path_expose (exports, fs_mode, path); + } + closedir (dir); + } + flatpak_exports_add_path_expose (exports, fs_mode, "/run/media"); + } + + os_mode = MAX ((FlatpakFilesystemMode) g_hash_table_lookup (context->filesystems, "host-os"), + fs_mode); + + if (os_mode != FLATPAK_FILESYSTEM_MODE_NONE) + flatpak_exports_add_host_os_expose (exports, os_mode); + + etc_mode = MAX ((FlatpakFilesystemMode) g_hash_table_lookup (context->filesystems, "host-etc"), + fs_mode); + + if (etc_mode != FLATPAK_FILESYSTEM_MODE_NONE) + flatpak_exports_add_host_etc_expose (exports, etc_mode); + + home_mode = (FlatpakFilesystemMode) g_hash_table_lookup (context->filesystems, "home"); + if (home_mode != FLATPAK_FILESYSTEM_MODE_NONE) + { + g_debug ("Allowing homedir access"); + home_access = TRUE; + + flatpak_exports_add_path_expose (exports, MAX (home_mode, fs_mode), g_get_home_dir ()); + } + + g_hash_table_iter_init (&iter, context->filesystems); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *filesystem = key; + FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); + + if (g_strv_contains (flatpak_context_special_filesystems, filesystem)) + continue; + + if (g_str_has_prefix (filesystem, "xdg-")) + { + const char *path, *rest = NULL; + const char *config_key = NULL; + g_autofree char *subpath = NULL; + + if (!get_xdg_user_dir_from_string (filesystem, &config_key, &rest, &path)) + { + g_warning ("Unsupported xdg dir %s", filesystem); + continue; + } + + if (path == NULL) + continue; /* Unconfigured, ignore */ + + if (strcmp (path, g_get_home_dir ()) == 0) + { + /* xdg-user-dirs sets disabled dirs to $HOME, and its in general not a good + idea to set full access to $HOME other than explicitly, so we ignore + these */ + g_debug ("Xdg dir %s is $HOME (i.e. disabled), ignoring", filesystem); + continue; + } + + subpath = g_build_filename (path, rest, NULL); + + if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) + g_mkdir_with_parents (subpath, 0755); + + if (g_file_test (subpath, G_FILE_TEST_EXISTS)) + { + if (config_key && xdg_dirs_conf) + g_string_append_printf (xdg_dirs_conf, "%s=\"%s\"\n", + config_key, path); + + flatpak_exports_add_path_expose_or_hide (exports, mode, subpath); + } + } + else if (g_str_has_prefix (filesystem, "~/")) + { + g_autofree char *path = NULL; + + path = g_build_filename (g_get_home_dir (), filesystem + 2, NULL); + + if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) + g_mkdir_with_parents (path, 0755); + + if (g_file_test (path, G_FILE_TEST_EXISTS)) + flatpak_exports_add_path_expose_or_hide (exports, mode, path); + } + else if (g_str_has_prefix (filesystem, "/")) + { + if (mode == FLATPAK_FILESYSTEM_MODE_CREATE && do_create) + g_mkdir_with_parents (filesystem, 0755); + + if (g_file_test (filesystem, G_FILE_TEST_EXISTS)) + flatpak_exports_add_path_expose_or_hide (exports, mode, filesystem); + } + else + { + g_warning ("Unexpected filesystem arg %s", filesystem); + } + } + + if (app_id_dir) + { + g_autoptr(GFile) apps_dir = g_file_get_parent (app_id_dir); + int i; + /* Hide the .var/app dir by default (unless explicitly made visible) */ + flatpak_exports_add_path_tmpfs (exports, flatpak_file_get_path_cached (apps_dir)); + /* But let the app write to the per-app dir in it */ + flatpak_exports_add_path_expose (exports, FLATPAK_FILESYSTEM_MODE_READ_WRITE, + flatpak_file_get_path_cached (app_id_dir)); + + if (extra_app_id_dirs != NULL) + { + for (i = 0; i < extra_app_id_dirs->len; i++) + { + GFile *extra_app_id_dir = g_ptr_array_index (extra_app_id_dirs, i); + flatpak_exports_add_path_expose (exports, FLATPAK_FILESYSTEM_MODE_READ_WRITE, + flatpak_file_get_path_cached (extra_app_id_dir)); + } + } + } + + if (home_access_out != NULL) + *home_access_out = home_access; +} + +FlatpakExports * +flatpak_context_get_exports (FlatpakContext *context, + const char *app_id) +{ + g_autoptr(FlatpakExports) exports = flatpak_exports_new (); + g_autoptr(GFile) app_id_dir = flatpak_get_data_dir (app_id); + + flatpak_context_export (context, exports, app_id_dir, NULL, FALSE, NULL, NULL); + return g_steal_pointer (&exports); +} + +FlatpakRunFlags +flatpak_context_get_run_flags (FlatpakContext *context) +{ + FlatpakRunFlags flags = 0; + + if (flatpak_context_allows_features (context, FLATPAK_CONTEXT_FEATURE_DEVEL)) + flags |= FLATPAK_RUN_FLAG_DEVEL; + + if (flatpak_context_allows_features (context, FLATPAK_CONTEXT_FEATURE_MULTIARCH)) + flags |= FLATPAK_RUN_FLAG_MULTIARCH; + + if (flatpak_context_allows_features (context, FLATPAK_CONTEXT_FEATURE_BLUETOOTH)) + flags |= FLATPAK_RUN_FLAG_BLUETOOTH; + + if (flatpak_context_allows_features (context, FLATPAK_CONTEXT_FEATURE_CANBUS)) + flags |= FLATPAK_RUN_FLAG_CANBUS; + + return flags; +} + +void +flatpak_context_append_bwrap_filesystem (FlatpakContext *context, + FlatpakBwrap *bwrap, + const char *app_id, + GFile *app_id_dir, + GPtrArray *extra_app_id_dirs, + FlatpakExports **exports_out) +{ + g_autoptr(FlatpakExports) exports = flatpak_exports_new (); + g_autoptr(GString) xdg_dirs_conf = g_string_new (""); +#if 0 + g_autoptr(GFile) user_flatpak_dir = NULL; +#endif + gboolean home_access = FALSE; + GHashTableIter iter; + gpointer key, value; + + flatpak_context_export (context, exports, app_id_dir, extra_app_id_dirs, TRUE, xdg_dirs_conf, &home_access); + if (app_id_dir != NULL) + flatpak_run_apply_env_appid (bwrap, app_id_dir); + + if (!home_access) + { + /* Enable persistent mapping only if no access to real home dir */ + + g_hash_table_iter_init (&iter, context->persistent); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + const char *persist = key; + g_autofree char *src = g_build_filename (g_get_home_dir (), ".var/app", app_id, persist, NULL); + g_autofree char *dest = g_build_filename (g_get_home_dir (), persist, NULL); + + g_mkdir_with_parents (src, 0755); + + flatpak_bwrap_add_bind_arg (bwrap, "--bind", src, dest); + } + } + + if (app_id_dir != NULL) + { + g_autofree char *user_runtime_dir = flatpak_get_real_xdg_runtime_dir (); + g_autofree char *run_user_app_dst = g_strdup_printf ("/run/user/%d/app/%s", getuid (), app_id); + g_autofree char *run_user_app_src = g_build_filename (user_runtime_dir, "app", app_id, NULL); + + if (glnx_shutil_mkdir_p_at (AT_FDCWD, + run_user_app_src, + 0700, + NULL, + NULL)) + flatpak_bwrap_add_args (bwrap, + "--bind", run_user_app_src, run_user_app_dst, + NULL); + } + +#if 0 + /* Hide the flatpak dir by default (unless explicitly made visible) */ + user_flatpak_dir = flatpak_get_user_base_dir_location (); + flatpak_exports_add_path_tmpfs (exports, flatpak_file_get_path_cached (user_flatpak_dir)); +#endif + + /* Ensure we always have a homedir */ + flatpak_exports_add_path_dir (exports, g_get_home_dir ()); + + /* This actually outputs the args for the hide/expose operations above */ + flatpak_exports_append_bwrap_args (exports, bwrap); + + /* Special case subdirectories of the cache, config and data xdg + * dirs. If these are accessible explicitly, then we bind-mount + * these in the app-id dir. This allows applications to explicitly + * opt out of keeping some config/cache/data in the app-specific + * directory. + */ + if (app_id_dir) + { + g_hash_table_iter_init (&iter, context->filesystems); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *filesystem = key; + FlatpakFilesystemMode mode = GPOINTER_TO_INT (value); + g_autofree char *xdg_path = NULL; + const char *rest, *where; + + xdg_path = get_xdg_dir_from_string (filesystem, &rest, &where); + + if (xdg_path != NULL && *rest != 0 && + mode >= FLATPAK_FILESYSTEM_MODE_READ_ONLY) + { + g_autoptr(GFile) app_version = g_file_get_child (app_id_dir, where); + g_autoptr(GFile) app_version_subdir = g_file_resolve_relative_path (app_version, rest); + + if (g_file_test (xdg_path, G_FILE_TEST_IS_DIR) || + g_file_test (xdg_path, G_FILE_TEST_IS_REGULAR)) + { + g_autofree char *xdg_path_in_app = g_file_get_path (app_version_subdir); + flatpak_bwrap_add_bind_arg (bwrap, + mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY ? "--ro-bind" : "--bind", + xdg_path, xdg_path_in_app); + } + } + } + } + + if (home_access && app_id_dir != NULL) + { + g_autofree char *src_path = g_build_filename (g_get_user_config_dir (), + "user-dirs.dirs", + NULL); + g_autofree char *path = g_build_filename (flatpak_file_get_path_cached (app_id_dir), + "config/user-dirs.dirs", NULL); + if (g_file_test (src_path, G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_bind_arg (bwrap, "--ro-bind", src_path, path); + } + else if (xdg_dirs_conf->len > 0 && app_id_dir != NULL) + { + g_autofree char *path = + g_build_filename (flatpak_file_get_path_cached (app_id_dir), + "config/user-dirs.dirs", NULL); + + flatpak_bwrap_add_args_data (bwrap, "xdg-config-dirs", + xdg_dirs_conf->str, xdg_dirs_conf->len, path, NULL); + } + + if (exports_out) + *exports_out = g_steal_pointer (&exports); +} diff --git a/src/flatpak-error.h b/src/flatpak-error.h new file mode 100644 index 000000000..d62bf8fa9 --- /dev/null +++ b/src/flatpak-error.h @@ -0,0 +1,108 @@ +/* flatpak-error.c + * Adapted from Flatpak, last update: 1.8.2 + * + * Copyright (C) 2015 Red Hat, Inc + * + * This file 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 of the + * License, or (at your option) any later version. + * + * This file 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 program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Alexander Larsson <alexl@redhat.com> + */ + +#ifndef FLATPAK_ERROR_H +#define FLATPAK_ERROR_H + +#include <glib.h> + +G_BEGIN_DECLS + +/* NOTE: If you add an error code below, also update the list in common/flatpak-utils.c */ +/** + * FlatpakError: + * @FLATPAK_ERROR_ALREADY_INSTALLED: App/runtime is already installed + * @FLATPAK_ERROR_NOT_INSTALLED: App/runtime is not installed + * @FLATPAK_ERROR_ONLY_PULLED: App/runtime was only pulled into the local + * repository but not installed. + * @FLATPAK_ERROR_DIFFERENT_REMOTE: The App/Runtime is already installed, but from a different remote. + * @FLATPAK_ERROR_ABORTED: The transaction was aborted (returned %TRUE in operation-error signal). + * @FLATPAK_ERROR_SKIPPED: The App/Runtime install was skipped due to earlier errors. + * @FLATPAK_ERROR_NEED_NEW_FLATPAK: The App/Runtime needs a more recent version of flatpak. + * @FLATPAK_ERROR_REMOTE_NOT_FOUND: The specified remote was not found. + * @FLATPAK_ERROR_RUNTIME_NOT_FOUND: A runtime needed for the app was not found. + * @FLATPAK_ERROR_DOWNGRADE: The pulled commit is a downgrade, and a downgrade wasn't + * specifically allowed. (Since: 1.0) + * @FLATPAK_ERROR_INVALID_REF: A ref could not be parsed. (Since: 1.0.3) + * @FLATPAK_ERROR_INVALID_DATA: Invalid data. (Since: 1.0.3) + * @FLATPAK_ERROR_UNTRUSTED: Missing GPG key or signature. (Since: 1.0.3) + * @FLATPAK_ERROR_SETUP_FAILED: Sandbox setup failed. (Since: 1.0.3) + * @FLATPAK_ERROR_EXPORT_FAILED: Exporting data failed. (Since: 1.0.3) + * @FLATPAK_ERROR_REMOTE_USED: Remote can't be uninstalled. (Since: 1.0.3) + * @FLATPAK_ERROR_RUNTIME_USED: Runtime can't be uninstalled. (Since: 1.0.3) + * @FLATPAK_ERROR_INVALID_NAME: Application, runtime or remote name is invalid. (Since: 1.0.3) + * @FLATPAK_ERROR_OUT_OF_SPACE: More disk space needed. (Since: 1.2.0) + * @FLATPAK_ERROR_WRONG_USER: An operation is being attempted by the wrong user (such as + * root operating on a user installation). (Since: 1.2.0) + * @FLATPAK_ERROR_NOT_CACHED: Cached data was requested, but it was not available. (Since: 1.4.0) + * @FLATPAK_ERROR_REF_NOT_FOUND: The specified ref was not found. (Since: 1.4.0) + * @FLATPAK_ERROR_PERMISSION_DENIED: An operation was not allowed by the administrative policy. + * For example, an app is not allowed to be installed due + * to not complying with the parental controls policy. (Since: 1.5.1) + * @FLATPAK_ERROR_AUTHENTICATION_FAILED: An authentication operation failed, for example, no + * correct password was supplied. (Since: 1.7.3) + * @FLATPAK_ERROR_NOT_AUTHORIZED: An operation tried to access a ref, or information about it that it + * was not authorized. For example, when succesfully authenticating with a + * server but the user doesn't have permissions for a private ref. (Since: 1.7.3) + * + * Error codes for library functions. + */ +typedef enum { + FLATPAK_ERROR_ALREADY_INSTALLED, + FLATPAK_ERROR_NOT_INSTALLED, + FLATPAK_ERROR_ONLY_PULLED, + FLATPAK_ERROR_DIFFERENT_REMOTE, + FLATPAK_ERROR_ABORTED, + FLATPAK_ERROR_SKIPPED, + FLATPAK_ERROR_NEED_NEW_FLATPAK, + FLATPAK_ERROR_REMOTE_NOT_FOUND, + FLATPAK_ERROR_RUNTIME_NOT_FOUND, + FLATPAK_ERROR_DOWNGRADE, + FLATPAK_ERROR_INVALID_REF, + FLATPAK_ERROR_INVALID_DATA, + FLATPAK_ERROR_UNTRUSTED, + FLATPAK_ERROR_SETUP_FAILED, + FLATPAK_ERROR_EXPORT_FAILED, + FLATPAK_ERROR_REMOTE_USED, + FLATPAK_ERROR_RUNTIME_USED, + FLATPAK_ERROR_INVALID_NAME, + FLATPAK_ERROR_OUT_OF_SPACE, + FLATPAK_ERROR_WRONG_USER, + FLATPAK_ERROR_NOT_CACHED, + FLATPAK_ERROR_REF_NOT_FOUND, + FLATPAK_ERROR_PERMISSION_DENIED, + FLATPAK_ERROR_AUTHENTICATION_FAILED, + FLATPAK_ERROR_NOT_AUTHORIZED, +} FlatpakError; + +/** + * FLATPAK_ERROR: + * + * The error domain for #FlatpakError errors. + */ +#define FLATPAK_ERROR flatpak_error_quark () + +FLATPAK_EXTERN GQuark flatpak_error_quark (void); + +G_END_DECLS + +#endif /* FLATPAK_ERROR_H */ diff --git a/src/flatpak-exports-private.h b/src/flatpak-exports-private.h new file mode 100644 index 000000000..6da3bef4b --- /dev/null +++ b/src/flatpak-exports-private.h @@ -0,0 +1,66 @@ +/* + * Taken from Flatpak, last updated: 1.9.x commit 1.8.0-74-g354b9a22 + * Copyright © 2014-2018 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> + */ + +#ifndef __FLATPAK_EXPORTS_H__ +#define __FLATPAK_EXPORTS_H__ + +#include "libglnx/libglnx.h" +#include "flatpak-bwrap-private.h" + +/* In numerical order of more privs */ +typedef enum { + FLATPAK_FILESYSTEM_MODE_NONE = 0, + FLATPAK_FILESYSTEM_MODE_READ_ONLY = 1, + FLATPAK_FILESYSTEM_MODE_READ_WRITE = 2, + FLATPAK_FILESYSTEM_MODE_CREATE = 3, + FLATPAK_FILESYSTEM_MODE_LAST = FLATPAK_FILESYSTEM_MODE_CREATE +} FlatpakFilesystemMode; + +typedef struct _FlatpakExports FlatpakExports; + +void flatpak_exports_free (FlatpakExports *exports); +FlatpakExports *flatpak_exports_new (void); +void flatpak_exports_append_bwrap_args (FlatpakExports *exports, + FlatpakBwrap *bwrap); +void flatpak_exports_add_host_etc_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode); +void flatpak_exports_add_host_os_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode); +void flatpak_exports_add_path_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode, + const char *path); +void flatpak_exports_add_path_tmpfs (FlatpakExports *exports, + const char *path); +void flatpak_exports_add_path_expose_or_hide (FlatpakExports *exports, + FlatpakFilesystemMode mode, + const char *path); +void flatpak_exports_add_path_dir (FlatpakExports *exports, + const char *path); + +gboolean flatpak_exports_path_is_visible (FlatpakExports *exports, + const char *path); +FlatpakFilesystemMode flatpak_exports_path_get_mode (FlatpakExports *exports, + const char *path); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (FlatpakExports, flatpak_exports_free); + + +#endif /* __FLATPAK_EXPORTS_H__ */ diff --git a/src/flatpak-exports.c b/src/flatpak-exports.c new file mode 100644 index 000000000..eb072a21e --- /dev/null +++ b/src/flatpak-exports.c @@ -0,0 +1,796 @@ +/* + * Taken from Flatpak, last updated: 1.9.x commit 1.8.0-74-g354b9a22 + * 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 <string.h> +#include <fcntl.h> +#include <stdio.h> +#include <unistd.h> +#include <sys/utsname.h> +#include <sys/socket.h> +#include <sys/ioctl.h> +#include <sys/vfs.h> +#include <sys/personality.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <grp.h> +#include <unistd.h> +#include <gio/gunixfdlist.h> + +#include <gio/gio.h> +#include "libglnx/libglnx.h" + +#include "flatpak-exports-private.h" +#include "flatpak-run-private.h" +#include "flatpak-utils-base-private.h" +#include "flatpak-utils-private.h" +#include "flatpak-error.h" + +/* To keep this more similar to the original file, we explicitly disable + * these warnings rather than fixing them */ +#if defined(__GNUC__) && __GNUC__ >= 8 +# pragma GCC diagnostic ignored "-Wcast-function-type" +#endif +#pragma GCC diagnostic ignored "-Wshadow" +#pragma GCC diagnostic ignored "-Wtype-limits" + +/* We don't want to export paths pointing into these, because they are readonly + (so we can't create mountpoints there) and don't match what's on the host anyway. + flatpak_abs_usrmerged_dirs get the same treatment without having to be listed + here. */ +const char *dont_export_in[] = { + "/usr", "/etc", "/app", "/dev", "/proc", NULL +}; + +static char * +make_relative (const char *base, const char *path) +{ + GString *s = g_string_new (""); + + while (*base != 0) + { + while (*base == '/') + base++; + + if (*base != 0) + g_string_append (s, "../"); + + while (*base != '/' && *base != 0) + base++; + } + + while (*path == '/') + path++; + + g_string_append (s, path); + + return g_string_free (s, FALSE); +} + +#define FAKE_MODE_DIR -1 /* Ensure a dir, either on tmpfs or mapped parent */ +#define FAKE_MODE_TMPFS FLATPAK_FILESYSTEM_MODE_NONE +#define FAKE_MODE_SYMLINK G_MAXINT + +static inline gboolean +is_export_mode (int mode) +{ + return ((mode >= FLATPAK_FILESYSTEM_MODE_NONE + && mode <= FLATPAK_FILESYSTEM_MODE_LAST) + || mode == FAKE_MODE_DIR + || mode == FAKE_MODE_SYMLINK); +} + +typedef struct +{ + char *path; + gint mode; +} ExportedPath; + +struct _FlatpakExports +{ + GHashTable *hash; + FlatpakFilesystemMode host_etc; + FlatpakFilesystemMode host_os; +}; + +static void +exported_path_free (ExportedPath *exported_path) +{ + g_free (exported_path->path); + g_free (exported_path); +} + +FlatpakExports * +flatpak_exports_new (void) +{ + FlatpakExports *exports = g_new0 (FlatpakExports, 1); + + exports->hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GFreeFunc) exported_path_free); + return exports; +} + +void +flatpak_exports_free (FlatpakExports *exports) +{ + g_hash_table_destroy (exports->hash); + g_free (exports); +} + +/* Returns TRUE if the location of this export + is not visible due to parents being exported */ +static gboolean +path_parent_is_mapped (const char **keys, + guint n_keys, + GHashTable *hash_table, + const char *path) +{ + guint i; + gboolean is_mapped = FALSE; + + /* The keys are sorted so shorter (i.e. parents) are first */ + for (i = 0; i < n_keys; i++) + { + const char *mounted_path = keys[i]; + ExportedPath *ep = g_hash_table_lookup (hash_table, mounted_path); + + g_assert (is_export_mode (ep->mode)); + + if (flatpak_has_path_prefix (path, mounted_path) && + (strcmp (path, mounted_path) != 0)) + { + /* FAKE_MODE_DIR has same mapped value as parent */ + if (ep->mode == FAKE_MODE_DIR) + continue; + + is_mapped = ep->mode != FAKE_MODE_TMPFS; + } + } + + return is_mapped; +} + +static gboolean +path_is_mapped (const char **keys, + guint n_keys, + GHashTable *hash_table, + const char *path, + gboolean *is_readonly_out) +{ + guint i; + gboolean is_mapped = FALSE; + gboolean is_readonly = FALSE; + + /* The keys are sorted so shorter (i.e. parents) are first */ + for (i = 0; i < n_keys; i++) + { + const char *mounted_path = keys[i]; + ExportedPath *ep = g_hash_table_lookup (hash_table, mounted_path); + + g_assert (is_export_mode (ep->mode)); + + if (flatpak_has_path_prefix (path, mounted_path)) + { + /* FAKE_MODE_DIR has same mapped value as parent */ + if (ep->mode == FAKE_MODE_DIR) + continue; + + if (ep->mode == FAKE_MODE_SYMLINK) + is_mapped = strcmp (path, mounted_path) == 0; + else + is_mapped = ep->mode != FAKE_MODE_TMPFS; + + if (is_mapped) + is_readonly = ep->mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY; + else + is_readonly = FALSE; + } + } + + *is_readonly_out = is_readonly; + return is_mapped; +} + +static gint +compare_eps (const ExportedPath *a, + const ExportedPath *b) +{ + return g_strcmp0 (a->path, b->path); +} + +/* This differs from g_file_test (path, G_FILE_TEST_IS_DIR) which + returns true if the path is a symlink to a dir */ +static gboolean +path_is_dir (const char *path) +{ + struct stat s; + + if (lstat (path, &s) != 0) + return FALSE; + + return S_ISDIR (s.st_mode); +} + +static gboolean +path_is_symlink (const char *path) +{ + struct stat s; + + if (lstat (path, &s) != 0) + return FALSE; + + return S_ISLNK (s.st_mode); +} + +/* + * @name: A file or directory below /etc + * @test: How we test whether it is suitable + * + * The paths in /etc that are required if we want to make use of the + * host /usr (and /lib, and so on). + */ +typedef struct +{ + const char *name; + GFileTest test; +} LibsNeedEtc; + +static const LibsNeedEtc libs_need_etc[] = +{ + /* glibc */ + { "ld.so.cache", G_FILE_TEST_IS_REGULAR }, + /* Used for executables and a few libraries on e.g. Debian */ + { "alternatives", G_FILE_TEST_IS_DIR } +}; + +void +flatpak_exports_append_bwrap_args (FlatpakExports *exports, + FlatpakBwrap *bwrap) +{ + guint n_keys; + g_autofree const char **keys = (const char **) g_hash_table_get_keys_as_array (exports->hash, &n_keys); + g_autoptr(GList) eps = NULL; + GList *l; + + eps = g_hash_table_get_values (exports->hash); + eps = g_list_sort (eps, (GCompareFunc) compare_eps); + + g_qsort_with_data (keys, n_keys, sizeof (char *), (GCompareDataFunc) flatpak_strcmp0_ptr, NULL); + + for (l = eps; l != NULL; l = l->next) + { + ExportedPath *ep = l->data; + const char *path = ep->path; + + g_assert (is_export_mode (ep->mode)); + + if (ep->mode == FAKE_MODE_SYMLINK) + { + if (!path_parent_is_mapped (keys, n_keys, exports->hash, path)) + { + g_autofree char *resolved = flatpak_resolve_link (path, NULL); + if (resolved) + { + g_autofree char *parent = g_path_get_dirname (path); + g_autofree char *relative = make_relative (parent, resolved); + flatpak_bwrap_add_args (bwrap, "--symlink", relative, path, NULL); + } + } + } + else if (ep->mode == FAKE_MODE_TMPFS) + { + /* Mount a tmpfs to hide the subdirectory, but only if there + is a pre-existing dir we can mount the path on. */ + if (path_is_dir (path)) + { + if (!path_parent_is_mapped (keys, n_keys, exports->hash, path)) + /* If the parent is not mapped, it will be a tmpfs, no need to mount another one */ + flatpak_bwrap_add_args (bwrap, "--dir", path, NULL); + else + flatpak_bwrap_add_args (bwrap, "--tmpfs", path, NULL); + } + } + else if (ep->mode == FAKE_MODE_DIR) + { + if (path_is_dir (path)) + flatpak_bwrap_add_args (bwrap, "--dir", path, NULL); + } + else + { + flatpak_bwrap_add_args (bwrap, + (ep->mode == FLATPAK_FILESYSTEM_MODE_READ_ONLY) ? "--ro-bind" : "--bind", + path, path, NULL); + } + } + + g_assert (exports->host_os >= FLATPAK_FILESYSTEM_MODE_NONE); + g_assert (exports->host_os <= FLATPAK_FILESYSTEM_MODE_LAST); + + if (exports->host_os != FLATPAK_FILESYSTEM_MODE_NONE) + { + const char *os_bind_mode = "--bind"; + int i; + + if (exports->host_os == FLATPAK_FILESYSTEM_MODE_READ_ONLY) + os_bind_mode = "--ro-bind"; + + if (g_file_test ("/usr", G_FILE_TEST_IS_DIR)) + flatpak_bwrap_add_args (bwrap, + os_bind_mode, "/usr", "/run/host/usr", NULL); + + for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++) + { + const char *subdir = flatpak_abs_usrmerged_dirs[i]; + g_autofree char *target = NULL; + g_autofree char *run_host_subdir = NULL; + + g_assert (subdir[0] == '/'); + /* e.g. /run/host/lib32 */ + run_host_subdir = g_strconcat ("/run/host", subdir, NULL); + target = glnx_readlinkat_malloc (-1, subdir, NULL, NULL); + + if (target != NULL && + g_str_has_prefix (target, "usr/")) + { + /* e.g. /lib32 is a relative symlink to usr/lib32, or + * on Arch Linux, /lib64 is a relative symlink to usr/lib; + * keep it relative */ + flatpak_bwrap_add_args (bwrap, + "--symlink", target, run_host_subdir, + NULL); + } + else if (target != NULL && + g_str_has_prefix (target, "/usr/")) + { + /* e.g. /lib32 is an absolute symlink to /usr/lib32; make + * it a relative symlink to usr/lib32 instead by skipping + * the '/' */ + flatpak_bwrap_add_args (bwrap, + "--symlink", target + 1, run_host_subdir, + NULL); + } + else if (g_file_test (subdir, G_FILE_TEST_IS_DIR)) + { + /* e.g. /lib32 is a symlink to /opt/compat/ia32/lib, + * or is a plain directory because the host OS has not + * undergone the /usr merge; bind-mount the directory instead */ + flatpak_bwrap_add_args (bwrap, + os_bind_mode, subdir, run_host_subdir, + NULL); + } + } + + if (exports->host_etc == FLATPAK_FILESYSTEM_MODE_NONE) + { + guint i; + + /* We are exposing the host /usr (and friends) but not the + * host /etc. Additionally expose just enough of /etc to make + * things that want to read /usr work as expected. + * + * (If exports->host_etc is nonzero, we'll do this as part of + * /etc instead.) */ + + for (i = 0; i < G_N_ELEMENTS (libs_need_etc); i++) + { + const LibsNeedEtc *item = &libs_need_etc[i]; + g_autofree gchar *host_path = g_strconcat ("/etc/", item->name, NULL); + + if (g_file_test (host_path, item->test)) + { + g_autofree gchar *run_host_path = g_strconcat ("/run/host/etc/", item->name, NULL); + + flatpak_bwrap_add_args (bwrap, + os_bind_mode, host_path, run_host_path, + NULL); + } + } + } + } + + g_assert (exports->host_etc >= FLATPAK_FILESYSTEM_MODE_NONE); + g_assert (exports->host_etc <= FLATPAK_FILESYSTEM_MODE_LAST); + + if (exports->host_etc != FLATPAK_FILESYSTEM_MODE_NONE) + { + const char *etc_bind_mode = "--bind"; + + if (exports->host_etc == FLATPAK_FILESYSTEM_MODE_READ_ONLY) + etc_bind_mode = "--ro-bind"; + + if (g_file_test ("/etc", G_FILE_TEST_IS_DIR)) + flatpak_bwrap_add_args (bwrap, + etc_bind_mode, "/etc", "/run/host/etc", NULL); + } + + /* As per the os-release specification https://www.freedesktop.org/software/systemd/man/os-release.html + * always read-only bind-mount /etc/os-release if it exists, or /usr/lib/os-release as a fallback from + * the host into the application's /run/host */ + if (g_file_test ("/etc/os-release", G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--ro-bind", "/etc/os-release", "/run/host/os-release", NULL); + else if (g_file_test ("/usr/lib/os-release", G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--ro-bind", "/usr/lib/os-release", "/run/host/os-release", NULL); +} + +/* Returns FLATPAK_FILESYSTEM_MODE_NONE if not visible */ +FlatpakFilesystemMode +flatpak_exports_path_get_mode (FlatpakExports *exports, + const char *path) +{ + guint n_keys; + g_autofree const char **keys = (const char **) g_hash_table_get_keys_as_array (exports->hash, &n_keys); + g_autofree char *canonical = NULL; + gboolean is_readonly = FALSE; + g_auto(GStrv) parts = NULL; + int i; + g_autoptr(GString) path_builder = g_string_new (""); + struct stat st; + + g_qsort_with_data (keys, n_keys, sizeof (char *), (GCompareDataFunc) flatpak_strcmp0_ptr, NULL); + + path = canonical = flatpak_canonicalize_filename (path); + + parts = g_strsplit (path + 1, "/", -1); + + /* A path is visible in the sandbox if no parent + * path element that is mapped in the sandbox is + * a symlink, and the final element is mapped. + * If any parent is a symlink we resolve that and + * continue with that instead. + */ + for (i = 0; parts[i] != NULL; i++) + { + g_string_append (path_builder, "/"); + g_string_append (path_builder, parts[i]); + + if (path_is_mapped (keys, n_keys, exports->hash, path_builder->str, &is_readonly)) + { + if (lstat (path_builder->str, &st) != 0) + { + if (errno == ENOENT && parts[i + 1] == NULL && !is_readonly) + { + /* Last element was mapped but isn't there, this is + * OK (used for the save case) if we the parent is + * mapped and writable, as the app can then create + * the file here. + */ + break; + } + + return FLATPAK_FILESYSTEM_MODE_NONE; + } + + if (S_ISLNK (st.st_mode)) + { + g_autofree char *resolved = flatpak_resolve_link (path_builder->str, NULL); + g_autoptr(GString) path2_builder = NULL; + int j; + + if (resolved == NULL) + return FLATPAK_FILESYSTEM_MODE_NONE; + + path2_builder = g_string_new (resolved); + + for (j = i + 1; parts[j] != NULL; j++) + { + g_string_append (path2_builder, "/"); + g_string_append (path2_builder, parts[j]); + } + + return flatpak_exports_path_get_mode (exports, path2_builder->str); + } + } + else if (parts[i + 1] == NULL) + return FLATPAK_FILESYSTEM_MODE_NONE; /* Last part was not mapped */ + } + + if (is_readonly) + return FLATPAK_FILESYSTEM_MODE_READ_ONLY; + + return FLATPAK_FILESYSTEM_MODE_READ_WRITE; +} + +gboolean +flatpak_exports_path_is_visible (FlatpakExports *exports, + const char *path) +{ + return flatpak_exports_path_get_mode (exports, path) > FLATPAK_FILESYSTEM_MODE_NONE; +} + +static gboolean +never_export_as_symlink (const char *path) +{ + /* Don't export /tmp as a symlink even if it is on the host, because + that will fail with the pre-existing directory we created for /tmp, + and anyway, it being a symlink is not useful in the sandbox */ + if (strcmp (path, "/tmp") == 0) + return TRUE; + + return FALSE; +} + +static void +do_export_path (FlatpakExports *exports, + const char *path, + gint mode) +{ + ExportedPath *old_ep = g_hash_table_lookup (exports->hash, path); + ExportedPath *ep; + + g_return_if_fail (is_export_mode (mode)); + + ep = g_new0 (ExportedPath, 1); + ep->path = g_strdup (path); + + if (old_ep != NULL) + ep->mode = MAX (old_ep->mode, mode); + else + ep->mode = mode; + + g_hash_table_replace (exports->hash, ep->path, ep); +} + +/* AUTOFS mounts are tricky, as using them as a source in a bind mount + * causes the mount to trigger, which can take a long time (or forever) + * waiting for a device or network mount. We try to open the directory + * but time out after a while, ignoring the mount. Unfortunately we + * have to mess with forks and stuff to be able to handle the timeout. + */ +static gboolean +check_if_autofs_works (const char *path) +{ + int selfpipe[2]; + struct timeval timeout; + pid_t pid; + fd_set rfds; + int res; + int wstatus; + + if (pipe2 (selfpipe, O_CLOEXEC) == -1) + return FALSE; + + fcntl (selfpipe[0], F_SETFL, fcntl (selfpipe[0], F_GETFL) | O_NONBLOCK); + fcntl (selfpipe[1], F_SETFL, fcntl (selfpipe[1], F_GETFL) | O_NONBLOCK); + + pid = fork (); + if (pid == -1) + { + close (selfpipe[0]); + close (selfpipe[1]); + return FALSE; + } + + if (pid == 0) + { + /* Note: open, close and _exit are signal-async-safe, so it is ok to call in the child after fork */ + + close (selfpipe[0]); /* Close unused read end */ + int dir_fd = open (path, O_RDONLY | O_NONBLOCK | O_CLOEXEC | O_DIRECTORY); + _exit (dir_fd == -1 ? 1 : 0); + } + + /* Parent */ + close (selfpipe[1]); /* Close unused write end */ + + /* 200 msec timeout*/ + timeout.tv_sec = 0; + timeout.tv_usec = 200 * 1000; + + FD_ZERO (&rfds); + FD_SET (selfpipe[0], &rfds); + res = select (selfpipe[0] + 1, &rfds, NULL, NULL, &timeout); + + close (selfpipe[0]); + + if (res == -1 /* Error */ || res == 0) /* Timeout */ + { + /* Kill, but then waitpid to avoid zombie */ + kill (pid, SIGKILL); + } + + if (waitpid (pid, &wstatus, 0) != pid) + return FALSE; + + if (res == -1 /* Error */ || res == 0) /* Timeout */ + return FALSE; + + if (!WIFEXITED (wstatus) || WEXITSTATUS (wstatus) != 0) + return FALSE; + + return TRUE; +} + +/* We use level to avoid infinite recursion */ +static gboolean +_exports_path_expose (FlatpakExports *exports, + int mode, + const char *path, + int level) +{ + g_autofree char *canonical = NULL; + struct stat st; + struct statfs stfs; + char *slash; + int i; + glnx_autofd int o_path_fd = -1; + + g_return_val_if_fail (is_export_mode (mode), FALSE); + + if (level > 40) /* 40 is the current kernel ELOOP check */ + { + g_debug ("Expose too deep, bail"); + return FALSE; + } + + if (!g_path_is_absolute (path)) + { + g_debug ("Not exposing relative path %s", path); + return FALSE; + } + + /* Check if it exists at all */ + o_path_fd = open (path, O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (o_path_fd == -1) + return FALSE; + + if (fstat (o_path_fd, &st) != 0) + return FALSE; + + /* Don't expose weird things */ + if (!(S_ISDIR (st.st_mode) || + S_ISREG (st.st_mode) || + S_ISLNK (st.st_mode) || + S_ISSOCK (st.st_mode))) + return FALSE; + + /* O_PATH + fstatfs is the magic that we need to statfs without automounting the target */ + if (fstatfs (o_path_fd, &stfs) != 0) + return FALSE; + + if (stfs.f_type == AUTOFS_SUPER_MAGIC) + { + if (!check_if_autofs_works (path)) + { + g_debug ("ignoring blocking autofs path %s", path); + return FALSE; + } + } + + path = canonical = flatpak_canonicalize_filename (path); + + for (i = 0; dont_export_in[i] != NULL; i++) + { + /* Don't expose files in non-mounted dirs like /app or /usr, as + they are not the same as on the host, and we generally can't + create the parents for them anyway */ + if (flatpak_has_path_prefix (path, dont_export_in[i])) + { + g_debug ("skipping export for path %s", path); + return FALSE; + } + } + + for (i = 0; flatpak_abs_usrmerged_dirs[i] != NULL; i++) + { + /* Same as /usr, but for the directories that get merged into /usr */ + if (flatpak_has_path_prefix (path, flatpak_abs_usrmerged_dirs[i])) + { + g_debug ("skipping export for path %s", path); + return FALSE; + } + } + + /* Handle any symlinks prior to the target itself. This includes path itself, + because we expose the target of the symlink. */ + slash = canonical; + do + { + slash = strchr (slash + 1, '/'); + if (slash) + *slash = 0; + + if (path_is_symlink (path) && !never_export_as_symlink (path)) + { + g_autofree char *resolved = flatpak_resolve_link (path, NULL); + g_autofree char *new_target = NULL; + + if (resolved) + { + if (slash) + new_target = g_build_filename (resolved, slash + 1, NULL); + else + new_target = g_strdup (resolved); + + if (_exports_path_expose (exports, mode, new_target, level + 1)) + { + do_export_path (exports, path, FAKE_MODE_SYMLINK); + return TRUE; + } + } + + return FALSE; + } + if (slash) + *slash = '/'; + } + while (slash != NULL); + + do_export_path (exports, path, mode); + return TRUE; +} + +void +flatpak_exports_add_path_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode, + const char *path) +{ + g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE); + g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); + _exports_path_expose (exports, mode, path, 0); +} + +void +flatpak_exports_add_path_tmpfs (FlatpakExports *exports, + const char *path) +{ + _exports_path_expose (exports, FAKE_MODE_TMPFS, path, 0); +} + +void +flatpak_exports_add_path_expose_or_hide (FlatpakExports *exports, + FlatpakFilesystemMode mode, + const char *path) +{ + g_return_if_fail (mode >= FLATPAK_FILESYSTEM_MODE_NONE); + g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); + + if (mode == FLATPAK_FILESYSTEM_MODE_NONE) + flatpak_exports_add_path_tmpfs (exports, path); + else + flatpak_exports_add_path_expose (exports, mode, path); +} + +void +flatpak_exports_add_path_dir (FlatpakExports *exports, + const char *path) +{ + _exports_path_expose (exports, FAKE_MODE_DIR, path, 0); +} + +void +flatpak_exports_add_host_etc_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode) +{ + g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE); + g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); + + exports->host_etc = mode; +} + +void +flatpak_exports_add_host_os_expose (FlatpakExports *exports, + FlatpakFilesystemMode mode) +{ + g_return_if_fail (mode > FLATPAK_FILESYSTEM_MODE_NONE); + g_return_if_fail (mode <= FLATPAK_FILESYSTEM_MODE_LAST); + + exports->host_os = mode; +} diff --git a/src/flatpak-run-private.h b/src/flatpak-run-private.h index 24f1a8fd7..ddc9a406c 100644 --- a/src/flatpak-run-private.h +++ b/src/flatpak-run-private.h @@ -1,6 +1,6 @@ /* * Cut-down version of common/flatpak-run-private.h from Flatpak - * Last updated: Flatpak 1.6.1 + * Last updated: Flatpak 1.8.2 * * Copyright © 2017-2019 Collabora Ltd. * Copyright © 2014-2019 Red Hat, Inc @@ -32,6 +32,8 @@ #include "libglnx/libglnx.h" +#include "flatpak-common-types-private.h" +#include "flatpak-context-private.h" #include "flatpak-bwrap-private.h" #include "flatpak-utils-private.h" #include "glib-backports.h" @@ -42,4 +44,9 @@ gboolean flatpak_run_add_wayland_args (FlatpakBwrap *bwrap); void flatpak_run_add_pulseaudio_args (FlatpakBwrap *bwrap); gboolean flatpak_run_add_system_dbus_args (FlatpakBwrap *app_bwrap); gboolean flatpak_run_add_session_dbus_args (FlatpakBwrap *app_bwrap); +void flatpak_run_apply_env_appid (FlatpakBwrap *bwrap, + GFile *app_dir); +GFile *flatpak_get_data_dir (const char *app_id); + +extern const char * const *flatpak_abs_usrmerged_dirs; #endif /* __FLATPAK_RUN_H__ */ diff --git a/src/flatpak-run.c b/src/flatpak-run.c index 94a20e25e..addd6e707 100644 --- a/src/flatpak-run.c +++ b/src/flatpak-run.c @@ -40,6 +40,17 @@ /* In Flatpak this is optional, in pressure-vessel not so much */ #define ENABLE_XAUTH +const char * const abs_usrmerged_dirs[] = +{ + "/bin", + "/lib", + "/lib32", + "/lib64", + "/sbin", + NULL +}; +const char * const *flatpak_abs_usrmerged_dirs = abs_usrmerged_dirs; + static char * extract_unix_path_from_dbus_address (const char *address) { @@ -393,6 +404,14 @@ flatpak_run_add_pulseaudio_args (FlatpakBwrap *bwrap) } else g_debug ("Could not find pulseaudio socket"); + + /* Also allow ALSA access. This was added in 1.8, and is not ideally named. However, + * since the practical permission of ALSA and PulseAudio are essentially the same, and + * since we don't want to add more permissions for something we plan to replace with + * portals/pipewire going forward we reinterpret pulseaudio to also mean ALSA. + */ + if (g_file_test ("/dev/snd", G_FILE_TEST_IS_DIR)) + flatpak_bwrap_add_args (bwrap, "--dev-bind", "/dev/snd", "/dev/snd", NULL); } /* Simplified from Flatpak: we never restrict access to the D-Bus system bus */ @@ -458,3 +477,862 @@ flatpak_run_add_session_dbus_args (FlatpakBwrap *app_bwrap) return FALSE; } + +#if 0 + +static gboolean +flatpak_run_add_a11y_dbus_args (FlatpakBwrap *app_bwrap, + FlatpakBwrap *proxy_arg_bwrap, + FlatpakContext *context, + FlatpakRunFlags flags) +{ + g_autoptr(GDBusConnection) session_bus = NULL; + g_autofree char *a11y_address = NULL; + g_autoptr(GError) local_error = NULL; + g_autoptr(GDBusMessage) reply = NULL; + g_autoptr(GDBusMessage) msg = NULL; + g_autofree char *proxy_socket = NULL; + + if ((flags & FLATPAK_RUN_FLAG_NO_A11Y_BUS_PROXY) != 0) + return FALSE; + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + if (session_bus == NULL) + return FALSE; + + msg = g_dbus_message_new_method_call ("org.a11y.Bus", "/org/a11y/bus", "org.a11y.Bus", "GetAddress"); + g_dbus_message_set_body (msg, g_variant_new ("()")); + reply = + g_dbus_connection_send_message_with_reply_sync (session_bus, msg, + G_DBUS_SEND_MESSAGE_FLAGS_NONE, + 30000, + NULL, + NULL, + NULL); + if (reply) + { + if (g_dbus_message_to_gerror (reply, &local_error)) + { + if (!g_error_matches (local_error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + g_message ("Can't find a11y bus: %s", local_error->message); + } + else + { + g_variant_get (g_dbus_message_get_body (reply), + "(s)", &a11y_address); + } + } + + if (!a11y_address) + return FALSE; + + proxy_socket = create_proxy_socket ("a11y-bus-proxy-XXXXXX"); + if (proxy_socket == NULL) + return FALSE; + + g_autofree char *sandbox_socket_path = g_strdup_printf ("/run/user/%d/at-spi-bus", getuid ()); + g_autofree char *sandbox_dbus_address = g_strdup_printf ("unix:path=/run/user/%d/at-spi-bus", getuid ()); + + flatpak_bwrap_add_args (proxy_arg_bwrap, + a11y_address, + proxy_socket, "--filter", "--sloppy-names", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Embed@/org/a11y/atspi/accessible/root", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.Socket.Unembed@/org/a11y/atspi/accessible/root", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.Registry.GetRegisteredEvents@/org/a11y/atspi/registry", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetKeystrokeListeners@/org/a11y/atspi/registry/deviceeventcontroller", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.GetDeviceEventListeners@/org/a11y/atspi/registry/deviceeventcontroller", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersSync@/org/a11y/atspi/registry/deviceeventcontroller", + "--call=org.a11y.atspi.Registry=org.a11y.atspi.DeviceEventController.NotifyListenersAsync@/org/a11y/atspi/registry/deviceeventcontroller", + NULL); + + if ((flags & FLATPAK_RUN_FLAG_LOG_A11Y_BUS) != 0) + flatpak_bwrap_add_args (proxy_arg_bwrap, "--log", NULL); + + flatpak_bwrap_add_args (app_bwrap, + "--ro-bind", proxy_socket, sandbox_socket_path, + NULL); + flatpak_bwrap_set_env (app_bwrap, "AT_SPI_BUS_ADDRESS", sandbox_dbus_address, TRUE); + + return TRUE; +} + +/* This wraps the argv in a bwrap call, primary to allow the + command to be run with a proper /.flatpak-info with data + taken from app_info_path */ +static gboolean +add_bwrap_wrapper (FlatpakBwrap *bwrap, + const char *app_info_path, + GError **error) +{ + glnx_autofd int app_info_fd = -1; + g_auto(GLnxDirFdIterator) dir_iter = { 0 }; + struct dirent *dent; + g_autofree char *user_runtime_dir = flatpak_get_real_xdg_runtime_dir (); + g_autofree char *proxy_socket_dir = g_build_filename (user_runtime_dir, ".dbus-proxy/", NULL); + + app_info_fd = open (app_info_path, O_RDONLY | O_CLOEXEC); + if (app_info_fd == -1) + return glnx_throw_errno_prefix (error, _("Failed to open app info file")); + + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, "/", FALSE, &dir_iter, error)) + return FALSE; + + flatpak_bwrap_add_arg (bwrap, flatpak_get_bwrap ()); + + while (TRUE) + { + glnx_autofd int o_path_fd = -1; + struct statfs stfs; + + if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&dir_iter, &dent, NULL, error)) + return FALSE; + + if (dent == NULL) + break; + + if (strcmp (dent->d_name, ".flatpak-info") == 0) + continue; + + /* O_PATH + fstatfs is the magic that we need to statfs without automounting the target */ + o_path_fd = openat (dir_iter.fd, dent->d_name, O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (o_path_fd == -1 || fstatfs (o_path_fd, &stfs) != 0 || stfs.f_type == AUTOFS_SUPER_MAGIC) + continue; /* AUTOFS mounts are risky and can cause us to block (see issue #1633), so ignore it. Its unlikely the proxy needs such a directory. */ + + if (dent->d_type == DT_DIR) + { + if (strcmp (dent->d_name, "tmp") == 0 || + strcmp (dent->d_name, "var") == 0 || + strcmp (dent->d_name, "run") == 0) + flatpak_bwrap_add_arg (bwrap, "--bind"); + else + flatpak_bwrap_add_arg (bwrap, "--ro-bind"); + + flatpak_bwrap_add_arg_printf (bwrap, "/%s", dent->d_name); + flatpak_bwrap_add_arg_printf (bwrap, "/%s", dent->d_name); + } + else if (dent->d_type == DT_LNK) + { + g_autofree gchar *target = NULL; + + target = glnx_readlinkat_malloc (dir_iter.fd, dent->d_name, + NULL, error); + if (target == NULL) + return FALSE; + flatpak_bwrap_add_args (bwrap, "--symlink", target, NULL); + flatpak_bwrap_add_arg_printf (bwrap, "/%s", dent->d_name); + } + } + + flatpak_bwrap_add_args (bwrap, "--bind", proxy_socket_dir, proxy_socket_dir, NULL); + + /* This is a file rather than a bind mount, because it will then + not be unmounted from the namespace when the namespace dies. */ + flatpak_bwrap_add_args_data_fd (bwrap, "--file", glnx_steal_fd (&app_info_fd), "/.flatpak-info"); + + if (!flatpak_bwrap_bundle_args (bwrap, 1, -1, FALSE, error)) + return FALSE; + + return TRUE; +} + +static gboolean +start_dbus_proxy (FlatpakBwrap *app_bwrap, + FlatpakBwrap *proxy_arg_bwrap, + const char *app_info_path, + GError **error) +{ + char x = 'x'; + const char *proxy; + g_autofree char *commandline = NULL; + g_autoptr(FlatpakBwrap) proxy_bwrap = NULL; + int sync_fds[2] = {-1, -1}; + int proxy_start_index; + g_auto(GStrv) minimal_envp = NULL; + + minimal_envp = flatpak_run_get_minimal_env (FALSE, FALSE); + proxy_bwrap = flatpak_bwrap_new (NULL); + + if (!add_bwrap_wrapper (proxy_bwrap, app_info_path, error)) + return FALSE; + + proxy = g_getenv ("FLATPAK_DBUSPROXY"); + if (proxy == NULL) + proxy = DBUSPROXY; + + flatpak_bwrap_add_arg (proxy_bwrap, proxy); + + proxy_start_index = proxy_bwrap->argv->len; + + if (pipe2 (sync_fds, O_CLOEXEC) < 0) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errno), + _("Unable to create sync pipe")); + return FALSE; + } + + /* read end goes to app */ + flatpak_bwrap_add_args_data_fd (app_bwrap, "--sync-fd", sync_fds[0], NULL); + + /* write end goes to proxy */ + flatpak_bwrap_add_fd (proxy_bwrap, sync_fds[1]); + flatpak_bwrap_add_arg_printf (proxy_bwrap, "--fd=%d", sync_fds[1]); + + /* Note: This steals the fds from proxy_arg_bwrap */ + flatpak_bwrap_append_bwrap (proxy_bwrap, proxy_arg_bwrap); + + if (!flatpak_bwrap_bundle_args (proxy_bwrap, proxy_start_index, -1, TRUE, error)) + return FALSE; + + flatpak_bwrap_finish (proxy_bwrap); + + commandline = flatpak_quote_argv ((const char **) proxy_bwrap->argv->pdata, -1); + g_debug ("Running '%s'", commandline); + + /* We use LEAVE_DESCRIPTORS_OPEN to work around dead-lock, see flatpak_close_fds_workaround */ + if (!g_spawn_async (NULL, + (char **) proxy_bwrap->argv->pdata, + NULL, + G_SPAWN_SEARCH_PATH | G_SPAWN_LEAVE_DESCRIPTORS_OPEN, + flatpak_bwrap_child_setup_cb, proxy_bwrap->fds, + NULL, error)) + return FALSE; + + /* The write end can be closed now, otherwise the read below will hang of xdg-dbus-proxy + fails to start. */ + g_clear_pointer (&proxy_bwrap, flatpak_bwrap_free); + + /* Sync with proxy, i.e. wait until its listening on the sockets */ + if (read (sync_fds[0], &x, 1) != 1) + { + g_set_error_literal (error, G_IO_ERROR, g_io_error_from_errno (errno), + _("Failed to sync with dbus proxy")); + return FALSE; + } + + return TRUE; +} + +static int +flatpak_extension_compare_by_path (gconstpointer _a, + gconstpointer _b) +{ + const FlatpakExtension *a = _a; + const FlatpakExtension *b = _b; + + return g_strcmp0 (a->directory, b->directory); +} + +gboolean +flatpak_run_add_extension_args (FlatpakBwrap *bwrap, + GKeyFile *metakey, + const char *full_ref, + gboolean use_ld_so_cache, + char **extensions_out, + GCancellable *cancellable, + GError **error) +{ + g_auto(GStrv) parts = NULL; + g_autoptr(GString) used_extensions = g_string_new (""); + gboolean is_app; + GList *extensions, *path_sorted_extensions, *l; + g_autoptr(GString) ld_library_path = g_string_new (""); + int count = 0; + g_autoptr(GHashTable) mounted_tmpfs = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + g_autoptr(GHashTable) created_symlink = + g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + parts = g_strsplit (full_ref, "/", 0); + if (g_strv_length (parts) != 4) + return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Failed to determine parts from ref: %s"), full_ref); + + is_app = strcmp (parts[0], "app") == 0; + + extensions = flatpak_list_extensions (metakey, + parts[2], parts[3]); + + /* First we apply all the bindings, they are sorted alphabetically in order for parent directory + to be mounted before child directories */ + path_sorted_extensions = g_list_copy (extensions); + path_sorted_extensions = g_list_sort (path_sorted_extensions, flatpak_extension_compare_by_path); + + for (l = path_sorted_extensions; l != NULL; l = l->next) + { + FlatpakExtension *ext = l->data; + g_autofree char *directory = g_build_filename (is_app ? "/app" : "/usr", ext->directory, NULL); + g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL); + g_autofree char *ref = g_build_filename (full_directory, ".ref", NULL); + g_autofree char *real_ref = g_build_filename (ext->files_path, ext->directory, ".ref", NULL); + + if (ext->needs_tmpfs) + { + g_autofree char *parent = g_path_get_dirname (directory); + + if (g_hash_table_lookup (mounted_tmpfs, parent) == NULL) + { + flatpak_bwrap_add_args (bwrap, + "--tmpfs", parent, + NULL); + g_hash_table_insert (mounted_tmpfs, g_steal_pointer (&parent), "mounted"); + } + } + + flatpak_bwrap_add_args (bwrap, + "--ro-bind", ext->files_path, full_directory, + NULL); + + if (g_file_test (real_ref, G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, + "--lock-file", ref, + NULL); + } + + g_list_free (path_sorted_extensions); + + /* Then apply library directories and file merging, in extension prio order */ + + for (l = extensions; l != NULL; l = l->next) + { + FlatpakExtension *ext = l->data; + g_autofree char *directory = g_build_filename (is_app ? "/app" : "/usr", ext->directory, NULL); + g_autofree char *full_directory = g_build_filename (directory, ext->subdir_suffix, NULL); + int i; + + if (used_extensions->len > 0) + g_string_append (used_extensions, ";"); + g_string_append (used_extensions, ext->installed_id); + g_string_append (used_extensions, "="); + if (ext->commit != NULL) + g_string_append (used_extensions, ext->commit); + else + g_string_append (used_extensions, "local"); + + if (ext->add_ld_path) + { + g_autofree char *ld_path = g_build_filename (full_directory, ext->add_ld_path, NULL); + + if (use_ld_so_cache) + { + g_autofree char *contents = g_strconcat (ld_path, "\n", NULL); + /* We prepend app or runtime and a counter in order to get the include order correct for the conf files */ + g_autofree char *ld_so_conf_file = g_strdup_printf ("%s-%03d-%s.conf", parts[0], ++count, ext->installed_id); + g_autofree char *ld_so_conf_file_path = g_build_filename ("/run/flatpak/ld.so.conf.d", ld_so_conf_file, NULL); + + if (!flatpak_bwrap_add_args_data (bwrap, "ld-so-conf", + contents, -1, ld_so_conf_file_path, error)) + return FALSE; + } + else + { + if (ld_library_path->len != 0) + g_string_append (ld_library_path, ":"); + g_string_append (ld_library_path, ld_path); + } + } + + for (i = 0; ext->merge_dirs != NULL && ext->merge_dirs[i] != NULL; i++) + { + g_autofree char *parent = g_path_get_dirname (directory); + g_autofree char *merge_dir = g_build_filename (parent, ext->merge_dirs[i], NULL); + g_autofree char *source_dir = g_build_filename (ext->files_path, ext->merge_dirs[i], NULL); + g_auto(GLnxDirFdIterator) source_iter = { 0 }; + struct dirent *dent; + + if (glnx_dirfd_iterator_init_at (AT_FDCWD, source_dir, TRUE, &source_iter, NULL)) + { + while (glnx_dirfd_iterator_next_dent (&source_iter, &dent, NULL, NULL) && dent != NULL) + { + g_autofree char *symlink_path = g_build_filename (merge_dir, dent->d_name, NULL); + /* Only create the first, because extensions are listed in prio order */ + if (g_hash_table_lookup (created_symlink, symlink_path) == NULL) + { + g_autofree char *symlink = g_build_filename (directory, ext->merge_dirs[i], dent->d_name, NULL); + flatpak_bwrap_add_args (bwrap, + "--symlink", symlink, symlink_path, + NULL); + g_hash_table_insert (created_symlink, g_steal_pointer (&symlink_path), "created"); + } + } + } + } + } + + g_list_free_full (extensions, (GDestroyNotify) flatpak_extension_free); + + if (ld_library_path->len != 0) + { + const gchar *old_ld_path = g_environ_getenv (bwrap->envp, "LD_LIBRARY_PATH"); + + if (old_ld_path != NULL && *old_ld_path != 0) + { + if (is_app) + { + g_string_append (ld_library_path, ":"); + g_string_append (ld_library_path, old_ld_path); + } + else + { + g_string_prepend (ld_library_path, ":"); + g_string_prepend (ld_library_path, old_ld_path); + } + } + + flatpak_bwrap_set_env (bwrap, "LD_LIBRARY_PATH", ld_library_path->str, TRUE); + } + + if (extensions_out) + *extensions_out = g_string_free (g_steal_pointer (&used_extensions), FALSE); + + return TRUE; +} + +gboolean +flatpak_run_add_environment_args (FlatpakBwrap *bwrap, + const char *app_info_path, + FlatpakRunFlags flags, + const char *app_id, + FlatpakContext *context, + GFile *app_id_dir, + GPtrArray *previous_app_id_dirs, + FlatpakExports **exports_out, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GError) my_error = NULL; + g_autoptr(FlatpakExports) exports = NULL; + g_autoptr(FlatpakBwrap) proxy_arg_bwrap = flatpak_bwrap_new (flatpak_bwrap_empty_env); + gboolean has_wayland = FALSE; + gboolean allow_x11 = FALSE; + + if ((context->shares & FLATPAK_CONTEXT_SHARED_IPC) == 0) + { + g_debug ("Disallowing ipc access"); + flatpak_bwrap_add_args (bwrap, "--unshare-ipc", NULL); + } + + if ((context->shares & FLATPAK_CONTEXT_SHARED_NETWORK) == 0) + { + g_debug ("Disallowing network access"); + flatpak_bwrap_add_args (bwrap, "--unshare-net", NULL); + } + + if (context->devices & FLATPAK_CONTEXT_DEVICE_ALL) + { + flatpak_bwrap_add_args (bwrap, + "--dev-bind", "/dev", "/dev", + NULL); + /* Don't expose the host /dev/shm, just the device nodes, unless explicitly allowed */ + if (g_file_test ("/dev/shm", G_FILE_TEST_IS_DIR)) + { + if ((context->devices & FLATPAK_CONTEXT_DEVICE_SHM) == 0) + flatpak_bwrap_add_args (bwrap, + "--tmpfs", "/dev/shm", + NULL); + } + else if (g_file_test ("/dev/shm", G_FILE_TEST_IS_SYMLINK)) + { + g_autofree char *link = flatpak_readlink ("/dev/shm", NULL); + + /* On debian (with sysv init) the host /dev/shm is a symlink to /run/shm, so we can't + mount on top of it. */ + if (g_strcmp0 (link, "/run/shm") == 0) + { + if (context->devices & FLATPAK_CONTEXT_DEVICE_SHM && + g_file_test ("/run/shm", G_FILE_TEST_IS_DIR)) + flatpak_bwrap_add_args (bwrap, + "--bind", "/run/shm", "/run/shm", + NULL); + else + flatpak_bwrap_add_args (bwrap, + "--dir", "/run/shm", + NULL); + } + else + g_warning ("Unexpected /dev/shm symlink %s", link); + } + } + else + { + flatpak_bwrap_add_args (bwrap, + "--dev", "/dev", + NULL); + if (context->devices & FLATPAK_CONTEXT_DEVICE_DRI) + { + g_debug ("Allowing dri access"); + int i; + char *dri_devices[] = { + "/dev/dri", + /* mali */ + "/dev/mali", + "/dev/mali0", + "/dev/umplock", + /* nvidia */ + "/dev/nvidiactl", + "/dev/nvidia-modeset", + /* nvidia OpenCL/CUDA */ + "/dev/nvidia-uvm", + "/dev/nvidia-uvm-tools", + }; + + for (i = 0; i < G_N_ELEMENTS (dri_devices); i++) + { + if (g_file_test (dri_devices[i], G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--dev-bind", dri_devices[i], dri_devices[i], NULL); + } + + /* Each Nvidia card gets its own device. + This is a fairly arbitrary limit but ASUS sells mining boards supporting 20 in theory. */ + char nvidia_dev[14]; /* /dev/nvidia plus up to 2 digits */ + for (i = 0; i < 20; i++) + { + g_snprintf (nvidia_dev, sizeof (nvidia_dev), "/dev/nvidia%d", i); + if (g_file_test (nvidia_dev, G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--dev-bind", nvidia_dev, nvidia_dev, NULL); + } + } + + if (context->devices & FLATPAK_CONTEXT_DEVICE_KVM) + { + g_debug ("Allowing kvm access"); + if (g_file_test ("/dev/kvm", G_FILE_TEST_EXISTS)) + flatpak_bwrap_add_args (bwrap, "--dev-bind", "/dev/kvm", "/dev/kvm", NULL); + } + + if (context->devices & FLATPAK_CONTEXT_DEVICE_SHM) + { + /* This is a symlink to /run/shm on debian, so bind to real target */ + g_autofree char *real_dev_shm = realpath ("/dev/shm", NULL); + + g_debug ("Allowing /dev/shm access (as %s)", real_dev_shm); + if (real_dev_shm != NULL) + flatpak_bwrap_add_args (bwrap, "--bind", real_dev_shm, "/dev/shm", NULL); + } + } + + flatpak_context_append_bwrap_filesystem (context, bwrap, app_id, app_id_dir, previous_app_id_dirs, &exports); + + if (context->sockets & FLATPAK_CONTEXT_SOCKET_WAYLAND) + { + g_debug ("Allowing wayland access"); + has_wayland = flatpak_run_add_wayland_args (bwrap); + } + + if ((context->sockets & FLATPAK_CONTEXT_SOCKET_FALLBACK_X11) != 0) + allow_x11 = !has_wayland; + else + allow_x11 = (context->sockets & FLATPAK_CONTEXT_SOCKET_X11) != 0; + + flatpak_run_add_x11_args (bwrap, allow_x11); + + if (context->sockets & FLATPAK_CONTEXT_SOCKET_SSH_AUTH) + { + flatpak_run_add_ssh_args (bwrap); + } + + if (context->sockets & FLATPAK_CONTEXT_SOCKET_PULSEAUDIO) + { + g_debug ("Allowing pulseaudio access"); + flatpak_run_add_pulseaudio_args (bwrap); + } + + if (context->sockets & FLATPAK_CONTEXT_SOCKET_PCSC) + { + flatpak_run_add_pcsc_args (bwrap); + } + + if (context->sockets & FLATPAK_CONTEXT_SOCKET_CUPS) + { + flatpak_run_add_cups_args (bwrap); + } + + flatpak_run_add_session_dbus_args (bwrap, proxy_arg_bwrap, context, flags, app_id); + flatpak_run_add_system_dbus_args (bwrap, proxy_arg_bwrap, context, flags); + flatpak_run_add_a11y_dbus_args (bwrap, proxy_arg_bwrap, context, flags); + + if (g_environ_getenv (bwrap->envp, "LD_LIBRARY_PATH") != NULL) + { + /* LD_LIBRARY_PATH is overridden for setuid helper, so pass it as cmdline arg */ + flatpak_bwrap_add_args (bwrap, + "--setenv", "LD_LIBRARY_PATH", g_environ_getenv (bwrap->envp, "LD_LIBRARY_PATH"), + NULL); + flatpak_bwrap_unset_env (bwrap, "LD_LIBRARY_PATH"); + } + + if (g_environ_getenv (bwrap->envp, "TMPDIR") != NULL) + { + /* TMPDIR is overridden for setuid helper, so pass it as cmdline arg */ + flatpak_bwrap_add_args (bwrap, + "--setenv", "TMPDIR", g_environ_getenv (bwrap->envp, "TMPDIR"), + NULL); + flatpak_bwrap_unset_env (bwrap, "TMPDIR"); + } + + /* Must run this before spawning the dbus proxy, to ensure it + ends up in the app cgroup */ + if (!flatpak_run_in_transient_unit (app_id, &my_error)) + { + /* We still run along even if we don't get a cgroup, as nothing + really depends on it. Its just nice to have */ + g_debug ("Failed to run in transient scope: %s", my_error->message); + g_clear_error (&my_error); + } + + if (!flatpak_bwrap_is_empty (proxy_arg_bwrap) && + !start_dbus_proxy (bwrap, proxy_arg_bwrap, app_info_path, error)) + return FALSE; + + if (exports_out) + *exports_out = g_steal_pointer (&exports); + + return TRUE; +} + +typedef struct +{ + const char *env; + const char *val; +} ExportData; + +static const ExportData default_exports[] = { + {"PATH", "/app/bin:/usr/bin"}, + /* We always want to unset LD_LIBRARY_PATH to avoid inheriting weird + * dependencies from the host. But if not using ld.so.cache this is + * later set. */ + {"LD_LIBRARY_PATH", NULL}, + {"XDG_CONFIG_DIRS", "/app/etc/xdg:/etc/xdg"}, + {"XDG_DATA_DIRS", "/app/share:/usr/share"}, + {"SHELL", "/bin/sh"}, + {"TMPDIR", NULL}, /* Unset TMPDIR as it may not exist in the sandbox */ + + /* Some env vars are common enough and will affect the sandbox badly + if set on the host. We clear these always. */ + {"PYTHONPATH", NULL}, + {"PERLLIB", NULL}, + {"PERL5LIB", NULL}, + {"XCURSOR_PATH", NULL}, +}; + +static const ExportData no_ld_so_cache_exports[] = { + {"LD_LIBRARY_PATH", "/app/lib"}, +}; + +static const ExportData devel_exports[] = { + {"ACLOCAL_PATH", "/app/share/aclocal"}, + {"C_INCLUDE_PATH", "/app/include"}, + {"CPLUS_INCLUDE_PATH", "/app/include"}, + {"LDFLAGS", "-L/app/lib "}, + {"PKG_CONFIG_PATH", "/app/lib/pkgconfig:/app/share/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig"}, + {"LC_ALL", "en_US.utf8"}, +}; + +static void +add_exports (GPtrArray *env_array, + const ExportData *exports, + gsize n_exports) +{ + int i; + + for (i = 0; i < n_exports; i++) + { + if (exports[i].val) + g_ptr_array_add (env_array, g_strdup_printf ("%s=%s", exports[i].env, exports[i].val)); + } +} + +char ** +flatpak_run_get_minimal_env (gboolean devel, gboolean use_ld_so_cache) +{ + GPtrArray *env_array; + static const char * const copy[] = { + "PWD", + "GDMSESSION", + "XDG_CURRENT_DESKTOP", + "XDG_SESSION_DESKTOP", + "DESKTOP_SESSION", + "EMAIL_ADDRESS", + "HOME", + "HOSTNAME", + "LOGNAME", + "REAL_NAME", + "TERM", + "USER", + "USERNAME", + }; + static const char * const copy_nodevel[] = { + "LANG", + "LANGUAGE", + "LC_ALL", + "LC_ADDRESS", + "LC_COLLATE", + "LC_CTYPE", + "LC_IDENTIFICATION", + "LC_MEASUREMENT", + "LC_MESSAGES", + "LC_MONETARY", + "LC_NAME", + "LC_NUMERIC", + "LC_PAPER", + "LC_TELEPHONE", + "LC_TIME", + }; + int i; + + env_array = g_ptr_array_new_with_free_func (g_free); + + add_exports (env_array, default_exports, G_N_ELEMENTS (default_exports)); + + if (!use_ld_so_cache) + add_exports (env_array, no_ld_so_cache_exports, G_N_ELEMENTS (no_ld_so_cache_exports)); + + if (devel) + add_exports (env_array, devel_exports, G_N_ELEMENTS (devel_exports)); + + for (i = 0; i < G_N_ELEMENTS (copy); i++) + { + const char *current = g_getenv (copy[i]); + if (current) + g_ptr_array_add (env_array, g_strdup_printf ("%s=%s", copy[i], current)); + } + + if (!devel) + { + for (i = 0; i < G_N_ELEMENTS (copy_nodevel); i++) + { + const char *current = g_getenv (copy_nodevel[i]); + if (current) + g_ptr_array_add (env_array, g_strdup_printf ("%s=%s", copy_nodevel[i], current)); + } + } + + g_ptr_array_add (env_array, NULL); + return (char **) g_ptr_array_free (env_array, FALSE); +} + +static char ** +apply_exports (char **envp, + const ExportData *exports, + gsize n_exports) +{ + int i; + + for (i = 0; i < n_exports; i++) + { + const char *value = exports[i].val; + + if (value) + envp = g_environ_setenv (envp, exports[i].env, value, TRUE); + else + envp = g_environ_unsetenv (envp, exports[i].env); + } + + return envp; +} + +void +flatpak_run_apply_env_default (FlatpakBwrap *bwrap, gboolean use_ld_so_cache) +{ + bwrap->envp = apply_exports (bwrap->envp, default_exports, G_N_ELEMENTS (default_exports)); + + if (!use_ld_so_cache) + bwrap->envp = apply_exports (bwrap->envp, no_ld_so_cache_exports, G_N_ELEMENTS (no_ld_so_cache_exports)); +} + +static void +flatpak_run_apply_env_prompt (FlatpakBwrap *bwrap, const char *app_id) +{ + /* A custom shell prompt. FLATPAK_ID is always set. + * PS1 can be overwritten by runtime metadata or by --env overrides + */ + flatpak_bwrap_set_env (bwrap, "FLATPAK_ID", app_id, TRUE); + flatpak_bwrap_set_env (bwrap, "PS1", "[📦 $FLATPAK_ID \\W]\\$ ", FALSE); +} + +#endif + +void +flatpak_run_apply_env_appid (FlatpakBwrap *bwrap, + GFile *app_dir) +{ + g_autoptr(GFile) app_dir_data = NULL; + g_autoptr(GFile) app_dir_config = NULL; + g_autoptr(GFile) app_dir_cache = NULL; + + app_dir_data = g_file_get_child (app_dir, "data"); + app_dir_config = g_file_get_child (app_dir, "config"); + app_dir_cache = g_file_get_child (app_dir, "cache"); + flatpak_bwrap_set_env (bwrap, "XDG_DATA_HOME", flatpak_file_get_path_cached (app_dir_data), TRUE); + flatpak_bwrap_set_env (bwrap, "XDG_CONFIG_HOME", flatpak_file_get_path_cached (app_dir_config), TRUE); + flatpak_bwrap_set_env (bwrap, "XDG_CACHE_HOME", flatpak_file_get_path_cached (app_dir_cache), TRUE); + + if (g_getenv ("XDG_DATA_HOME")) + flatpak_bwrap_set_env (bwrap, "HOST_XDG_DATA_HOME", g_getenv ("XDG_DATA_HOME"), TRUE); + if (g_getenv ("XDG_CONFIG_HOME")) + flatpak_bwrap_set_env (bwrap, "HOST_XDG_CONFIG_HOME", g_getenv ("XDG_CONFIG_HOME"), TRUE); + if (g_getenv ("XDG_CACHE_HOME")) + flatpak_bwrap_set_env (bwrap, "HOST_XDG_CACHE_HOME", g_getenv ("XDG_CACHE_HOME"), TRUE); +} + +#if 0 + +void +flatpak_run_apply_env_vars (FlatpakBwrap *bwrap, FlatpakContext *context) +{ + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, context->env_vars); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *var = key; + const char *val = value; + + if (val && val[0] != 0) + flatpak_bwrap_set_env (bwrap, var, val, TRUE); + else + flatpak_bwrap_unset_env (bwrap, var); + } +} + +#endif + +GFile * +flatpak_get_data_dir (const char *app_id) +{ + g_autoptr(GFile) home = g_file_new_for_path (g_get_home_dir ()); + g_autoptr(GFile) var_app = g_file_resolve_relative_path (home, ".var/app"); + + return g_file_get_child (var_app, app_id); +} + +#if 0 + +gboolean +flatpak_ensure_data_dir (GFile *app_id_dir, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GFile) data_dir = g_file_get_child (app_id_dir, "data"); + g_autoptr(GFile) cache_dir = g_file_get_child (app_id_dir, "cache"); + g_autoptr(GFile) fontconfig_cache_dir = g_file_get_child (cache_dir, "fontconfig"); + g_autoptr(GFile) tmp_dir = g_file_get_child (cache_dir, "tmp"); + g_autoptr(GFile) config_dir = g_file_get_child (app_id_dir, "config"); + + if (!flatpak_mkdir_p (data_dir, cancellable, error)) + return FALSE; + + if (!flatpak_mkdir_p (cache_dir, cancellable, error)) + return FALSE; + + if (!flatpak_mkdir_p (fontconfig_cache_dir, cancellable, error)) + return FALSE; + + if (!flatpak_mkdir_p (tmp_dir, cancellable, error)) + return FALSE; + + if (!flatpak_mkdir_p (config_dir, cancellable, error)) + return FALSE; + + return TRUE; +} + +#endif diff --git a/src/flatpak-utils-private.h b/src/flatpak-utils-private.h index 612a71e35..9e418e5a0 100644 --- a/src/flatpak-utils-private.h +++ b/src/flatpak-utils-private.h @@ -24,6 +24,22 @@ #define __FLATPAK_UTILS_H__ #include "libglnx/libglnx.h" +#include <flatpak-common-types-private.h> +#include <gio/gio.h> +#include <gio/gunixfdlist.h> +#include "flatpak-context-private.h" +#include "flatpak-error.h" + +#define AUTOFS_SUPER_MAGIC 0x0187 + +/* https://bugzilla.gnome.org/show_bug.cgi?id=766370 */ +#if !GLIB_CHECK_VERSION (2, 49, 3) +#define FLATPAK_VARIANT_BUILDER_INITIALIZER {{0, }} +#define FLATPAK_VARIANT_DICT_INITIALIZER {{0, }} +#else +#define FLATPAK_VARIANT_BUILDER_INITIALIZER {{{0, }}} +#define FLATPAK_VARIANT_DICT_INITIALIZER {{{0, }}} +#endif /* https://github.com/GNOME/libglnx/pull/38 * Note by using #define rather than wrapping via a static inline, we @@ -31,20 +47,49 @@ */ #define flatpak_fail glnx_throw +gboolean flatpak_fail_error (GError **error, + FlatpakError code, + const char *fmt, + ...) G_GNUC_PRINTF (3, 4); + #define flatpak_debug2 g_debug +gint flatpak_strcmp0_ptr (gconstpointer a, + gconstpointer 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); +gboolean flatpak_has_path_prefix (const char *str, + const char *prefix); + + +gboolean flatpak_g_ptr_array_contains_string (GPtrArray *array, + const char *str); char * flatpak_quote_argv (const char *argv[], gssize len); +const char *flatpak_file_get_path_cached (GFile *file); + + +gboolean flatpak_mkdir_p (GFile *dir, + GCancellable *cancellable, + GError **error); gboolean flatpak_buffer_to_sealed_memfd_or_tmpfile (GLnxTmpfile *tmpf, const char *name, const char *str, size_t len, GError **error); +#if !GLIB_CHECK_VERSION (2, 43, 4) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GUnixFDList, g_object_unref) +#endif + +static inline void +null_safe_g_ptr_array_unref (gpointer data) +{ + g_clear_pointer (&data, g_ptr_array_unref); +} #endif /* __FLATPAK_UTILS_H__ */ diff --git a/src/flatpak-utils.c b/src/flatpak-utils.c index d79687800..c3b913de2 100644 --- a/src/flatpak-utils.c +++ b/src/flatpak-utils.c @@ -39,8 +39,10 @@ #include <termios.h> #include <glib.h> +#include "flatpak-error.h" #include "flatpak-utils-base-private.h" #include "flatpak-utils-private.h" +#include "libglnx/libglnx.h" /* To keep this more similar to the original file, we explicitly disable * this warning rather than fixing it */ @@ -48,6 +50,95 @@ #define RUNNING_ON_VALGRIND 0 +/* 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 */ +}; + +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; +} + + +#if 0 + +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)); +} + +#endif + +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 * @@ -56,95 +147,6963 @@ flatpak_get_real_xdg_runtime_dir (void) return realpath (g_get_user_runtime_dir (), NULL); } -static gboolean -needs_quoting (const char *arg) +/* 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 (*arg != 0) + while (TRUE) { - char c = *arg; - if (!g_ascii_isalnum (c) && - !(c == '-' || c == '/' || c == '~' || - c == ':' || c == '.' || c == '_' || - c == '=' || c == '@')) + /* 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; - arg++; + + /* 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; } - return FALSE; } -char * -flatpak_quote_argv (const char *argv[], - gssize len) +#if 0 + +/* Returns end of matching path prefix, or NULL if no match */ +const char * +flatpak_path_match_prefix (const char *pattern, + const char *string) { - GString *res = g_string_new (""); - int i; + char c, test; + const char *tmp; - if (len == -1) - len = g_strv_length ((char **) argv); + while (*pattern == '/') + pattern++; - for (i = 0; i < len; i++) - { - if (i != 0) - g_string_append_c (res, ' '); + while (*string == '/') + string++; - if (needs_quoting (argv[i])) + while (TRUE) + { + switch (c = *pattern++) { - g_autofree char *quoted = g_shell_quote (argv[i]); - g_string_append (res, quoted); + 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) + { + char *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; } - else - g_string_append (res, argv[i]); } + return NULL; /* Should not be reached */ +} - return g_string_free (res, FALSE); +const char * +flatpak_get_bwrap (void) +{ + const char *e = g_getenv ("FLATPAK_BWRAP"); + + if (e != NULL) + return e; + return HELPER; } -/* 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. +static gboolean +is_valid_initial_name_character (gint c, gboolean allow_dash) +{ + return + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == '_') || (allow_dash && c == '-'); +} + +static gboolean +is_valid_name_character (gint c, gboolean allow_dash) +{ + return + is_valid_initial_name_character (c, allow_dash) || + (c >= '0' && c <= '9'); +} + +/** + * flatpak_is_valid_name: + * @string: The string to check + * @error: Return location for an error + * + * Checks if @string is a valid application name. + * + * App names are composed of 3 or more elements separated by a period + * ('.') character. All elements must contain at least one character. + * + * Each element must only contain the ASCII characters + * "[A-Z][a-z][0-9]_-". Elements may not begin with a digit. + * Additionally "-" is only allowed in the last element. + * + * App names must not begin with a '.' (period) character. + * + * App names must not exceed 255 characters in length. + * + * The above means that any app name is also a valid DBus well known + * bus name, but not all DBus names are valid app names. The difference are: + * 1) DBus name elements may contain '-' in the non-last element. + * 2) DBus names require only two elements + * + * Returns: %TRUE if valid, %FALSE otherwise. + * + * Since: 2.26 */ gboolean -flatpak_buffer_to_sealed_memfd_or_tmpfile (GLnxTmpfile *tmpf, - const char *name, - const char *str, - size_t len, - GError **error) +flatpak_is_valid_name (const char *string, + 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) + guint len; + gboolean ret; + const gchar *s; + const gchar *end; + const gchar *last_dot; + int dot_count; + gboolean last_element; + + g_return_val_if_fail (string != NULL, FALSE); + + ret = FALSE; + + len = strlen (string); + if (G_UNLIKELY (len == 0)) { - fd = memfd; + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't be empty")); + goto out; } - else + + if (G_UNLIKELY (len > 255)) { - /* 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; + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't be longer than 255 characters")); + goto out; } - 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) + + end = string + len; + + last_dot = strrchr (string, '.'); + last_element = FALSE; + + s = string; + if (G_UNLIKELY (*s == '.')) { - /* 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; + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't start with a period")); + goto out; + } + else if (G_UNLIKELY (!is_valid_initial_name_character (*s, last_element))) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't start with %c"), *s); + goto out; + } + + s += 1; + dot_count = 0; + while (s != end) + { + if (*s == '.') + { + if (s == last_dot) + last_element = TRUE; + s += 1; + if (G_UNLIKELY (s == end)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't end with a period")); + goto out; + } + if (!is_valid_initial_name_character (*s, last_element)) + { + if (*s == '-') + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Only last name segment can contain -")); + else + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name segment can't start with %c"), *s); + goto out; + } + dot_count++; + } + else if (G_UNLIKELY (!is_valid_name_character (*s, last_element))) + { + if (*s == '-') + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Only last name segment can contain -")); + else + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Name can't contain %c"), *s); + goto out; + } + s += 1; + } + + if (G_UNLIKELY (dot_count < 2)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Names must contain at least 2 periods")); + goto out; + } + + ret = TRUE; + +out: + return ret; +} + +gboolean +flatpak_has_name_prefix (const char *string, + const char *name) +{ + const char *rest; + + if (!g_str_has_prefix (string, name)) + return FALSE; + + rest = string + strlen (name); + return + *rest == 0 || + *rest == '.' || + !is_valid_name_character (*rest, FALSE); +} + +gboolean +flatpak_name_matches_one_wildcard_prefix (const char *name, + const char * const *wildcarded_prefixes, + gboolean require_exact_match) +{ + const char * const *iter = wildcarded_prefixes; + const char *remainder; + gsize longest_match_len = 0; + + /* Find longest valid match */ + for (; *iter != NULL; ++iter) + { + const char *prefix = *iter; + gsize prefix_len = strlen (prefix); + gsize match_len = strlen (prefix); + gboolean has_wildcard = FALSE; + const char *end_of_match; + + if (g_str_has_suffix (prefix, ".*")) + { + has_wildcard = TRUE; + prefix_len -= 2; + } + + if (strncmp (name, prefix, prefix_len) != 0) + continue; + + end_of_match = name + prefix_len; + + if (has_wildcard && + end_of_match[0] == '.' && + is_valid_initial_name_character (end_of_match[1], TRUE)) + { + end_of_match += 2; + while (*end_of_match != 0 && + (is_valid_name_character (*end_of_match, TRUE) || + (end_of_match[0] == '.' && + is_valid_initial_name_character (end_of_match[1], TRUE)))) + end_of_match++; + } + + match_len = end_of_match - name; + + if (match_len > longest_match_len) + longest_match_len = match_len; + } + + if (longest_match_len == 0) + return FALSE; + + if (require_exact_match) + return name[longest_match_len] == 0; + + /* non-exact matches can be exact, or can be followed by characters that would make + * not be part of the last element in the matched prefix, due to being invalid or + * a new element. As a special case we explicitly disallow dash here, even though + * it iss typically allowed in the final element of a name, this allows you too sloppily + * match org.the.App with org.the.App-symbolic[.png] or org.the.App-settings[.desktop]. + */ + remainder = name + longest_match_len; + return + *remainder == 0 || + *remainder == '.' || + !is_valid_name_character (*remainder, FALSE); +} + +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 + 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 gboolean +is_valid_initial_branch_character (gint c) +{ + return + (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || + (c >= 'a' && c <= 'z') || + (c == '_') || + (c == '-'); +} + +static gboolean +is_valid_branch_character (gint c) +{ + return + is_valid_initial_branch_character (c) || + (c == '.'); +} + +/** + * flatpak_is_valid_branch: + * @string: The string to check + * @error: return location for an error + * + * Checks if @string is a valid branch name. + * + * Branch names must only contain the ASCII characters + * "[A-Z][a-z][0-9]_-.". + * Branch names may not begin with a period. + * Branch names must contain at least one character. + * + * Returns: %TRUE if valid, %FALSE otherwise. + * + * Since: 2.26 + */ +gboolean +flatpak_is_valid_branch (const char *string, + GError **error) +{ + guint len; + gboolean ret; + const gchar *s; + const gchar *end; + + g_return_val_if_fail (string != NULL, FALSE); + + ret = FALSE; + + len = strlen (string); + if (G_UNLIKELY (len == 0)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Branch can't be empty")); + goto out; + } + + end = string + len; + + s = string; + if (G_UNLIKELY (!is_valid_initial_branch_character (*s))) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Branch can't start with %c"), *s); + goto out; + } + + s += 1; + while (s != end) + { + if (G_UNLIKELY (!is_valid_branch_character (*s))) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_NAME, + _("Branch can't contain %c"), *s); + goto out; + } + s += 1; + } + + ret = TRUE; + +out: + return ret; +} + +/* Dashes are only valid in the last part of the app id, so + we replace them with underscore so we can suffix the id */ +char * +flatpak_make_valid_id_prefix (const char *orig_id) +{ + char *id, *t; + + id = g_strdup (orig_id); + t = id; + while (*t != 0 && *t != '/') + { + if (*t == '-') + *t = '_'; + + t++; + } + + return id; +} + +gboolean +flatpak_id_has_subref_suffix (const char *id) +{ + return + g_str_has_suffix (id, ".Locale") || + g_str_has_suffix (id, ".Debug") || + g_str_has_suffix (id, ".Sources"); +} + + +static const char * +skip_segment (const char *s) +{ + const char *slash; + + slash = strchr (s, '/'); + if (slash) + return slash + 1; + return s + strlen (s); +} + +static int +compare_segment (const char *s1, const char *s2) +{ + gint c1, c2; + + while (*s1 && *s1 != '/' && + *s2 && *s2 != '/') + { + c1 = *s1; + c2 = *s2; + if (c1 != c2) + return c1 - c2; + s1++; + s2++; + } + + c1 = *s1; + if (c1 == '/') + c1 = 0; + c2 = *s2; + if (c2 == '/') + c2 = 0; + + return c1 - c2; +} + +int +flatpak_compare_ref (const char *ref1, const char *ref2) +{ + int res; + int i; + + /* Skip first element and do per-segment compares for rest */ + for (i = 0; i < 3; i++) + { + ref1 = skip_segment (ref1); + ref2 = skip_segment (ref2); + + res = compare_segment (ref1, ref2); + if (res != 0) + return res; + } + return 0; +} + +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, GError **error) +{ + g_autoptr(GString) regexp = g_string_new (""); + int parts = 1; + gboolean empty_part; + + if (g_str_has_prefix (glob, "app/")) + { + 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 + 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, 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_decompose_ref (const char *full_ref, + GError **error) +{ + g_auto(GStrv) parts = NULL; + g_autoptr(GError) local_error = NULL; + + parts = g_strsplit (full_ref, "/", 0); + if (g_strv_length (parts) != 4) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Wrong number of components in %s"), full_ref); + return NULL; + } + + if (strcmp (parts[0], "app") != 0 && strcmp (parts[0], "runtime") != 0) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("%s is not application or runtime"), full_ref); + return NULL; + } + + if (!flatpak_is_valid_name (parts[1], &local_error)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid name %s: %s"), parts[1], local_error->message); + return NULL; + } + + if (strlen (parts[2]) == 0) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid arch %s"), parts[2]); + return NULL; + } + + if (!flatpak_is_valid_branch (parts[3], &local_error)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid branch %s: %s"), parts[3], local_error->message); + return NULL; + } + + return g_steal_pointer (&parts); +} + +static const char * +next_element (const char **partial_ref) +{ + const char *slash; + const char *end; + + slash = (const char *) strchr (*partial_ref, '/'); + if (slash != NULL) + { + end = slash; + *partial_ref = slash + 1; + } + else + { + end = *partial_ref + strlen (*partial_ref); + *partial_ref = end; + } + + return end; +} + +FlatpakKinds +flatpak_kinds_from_bools (gboolean app, gboolean runtime) +{ + FlatpakKinds kinds = 0; + + if (app) + kinds |= FLATPAK_KINDS_APP; + + if (runtime) + kinds |= FLATPAK_KINDS_RUNTIME; + + if (kinds == 0) + kinds = FLATPAK_KINDS_APP | FLATPAK_KINDS_RUNTIME; + + return kinds; +} + +static gboolean +_flatpak_split_partial_ref_arg (const char *partial_ref, + gboolean validate, + FlatpakKinds default_kinds, + const char *default_arch, + const char *default_branch, + FlatpakKinds *out_kinds, + char **out_id, + char **out_arch, + char **out_branch, + GError **error) +{ + const char *id_start = NULL; + const char *id_end = NULL; + g_autofree char *id = NULL; + const char *arch_start = NULL; + const char *arch_end = NULL; + g_autofree char *arch = NULL; + const char *branch_start = NULL; + const char *branch_end = NULL; + g_autofree char *branch = NULL; + g_autoptr(GError) local_error = NULL; + FlatpakKinds kinds = 0; + + if (g_str_has_prefix (partial_ref, "app/")) + { + partial_ref += strlen ("app/"); + kinds = FLATPAK_KINDS_APP; + } + else if (g_str_has_prefix (partial_ref, "runtime/")) + { + partial_ref += strlen ("runtime/"); + kinds = FLATPAK_KINDS_RUNTIME; + } + else + kinds = default_kinds; + + id_start = partial_ref; + id_end = next_element (&partial_ref); + id = g_strndup (id_start, id_end - id_start); + + if (validate && !flatpak_is_valid_name (id, &local_error)) + return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid id %s: %s"), id, local_error->message); + + arch_start = partial_ref; + arch_end = next_element (&partial_ref); + if (arch_end != arch_start) + arch = g_strndup (arch_start, arch_end - arch_start); + else + arch = g_strdup (default_arch); + + branch_start = partial_ref; + branch_end = next_element (&partial_ref); + if (branch_end != branch_start) + branch = g_strndup (branch_start, branch_end - branch_start); + else + branch = g_strdup (default_branch); + + if (validate && branch != NULL && !flatpak_is_valid_branch (branch, &local_error)) + return flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("Invalid branch %s: %s"), branch, local_error->message); + + if (out_kinds) + *out_kinds = kinds; + if (out_id != NULL) + *out_id = g_steal_pointer (&id); + if (out_arch != NULL) + *out_arch = g_steal_pointer (&arch); + if (out_branch != NULL) + *out_branch = g_steal_pointer (&branch); + + return TRUE; +} + +gboolean +flatpak_split_partial_ref_arg (const char *partial_ref, + FlatpakKinds default_kinds, + const char *default_arch, + const char *default_branch, + FlatpakKinds *out_kinds, + char **out_id, + char **out_arch, + char **out_branch, + GError **error) +{ + return _flatpak_split_partial_ref_arg (partial_ref, + TRUE, + default_kinds, + default_arch, + default_branch, + out_kinds, + out_id, + out_arch, + out_branch, + error); +} + +gboolean +flatpak_split_partial_ref_arg_novalidate (const char *partial_ref, + FlatpakKinds default_kinds, + const char *default_arch, + const char *default_branch, + FlatpakKinds *out_kinds, + char **out_id, + char **out_arch, + char **out_branch) +{ + return _flatpak_split_partial_ref_arg (partial_ref, + FALSE, + default_kinds, + default_arch, + default_branch, + out_kinds, + out_id, + out_arch, + out_branch, + NULL); +} + + +char * +flatpak_compose_ref (gboolean app, + const char *name, + const char *branch, + const char *arch, + GError **error) +{ + g_autoptr(GError) local_error = NULL; + + if (!flatpak_is_valid_name (name, &local_error)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("'%s' is not a valid name: %s"), name, local_error->message); + return NULL; + } + + if (branch && !flatpak_is_valid_branch (branch, &local_error)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_REF, _("'%s' is not a valid branch name: %s"), branch, local_error->message); + return NULL; + } + + if (app) + return flatpak_build_app_ref (name, branch, arch); + else + return flatpak_build_runtime_ref (name, branch, arch); +} + +char * +flatpak_build_untyped_ref (const char *runtime, + const char *branch, + const char *arch) +{ + if (arch == NULL) + arch = flatpak_get_arch (); + + return g_build_filename (runtime, arch, branch, NULL); +} + +char * +flatpak_build_runtime_ref (const char *runtime, + const char *branch, + const char *arch) +{ + if (branch == NULL) + branch = "master"; + + if (arch == NULL) + arch = flatpak_get_arch (); + + return g_build_filename ("runtime", runtime, arch, branch, NULL); +} + +char * +flatpak_build_app_ref (const char *app, + const char *branch, + const char *arch) +{ + if (branch == NULL) + branch = "master"; + + if (arch == NULL) + arch = flatpak_get_arch (); + + return g_build_filename ("app", app, arch, branch, NULL); +} + +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; + const char *key; + GHashTableIter iter; + int i; + + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, 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 (); + 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; + +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 (const char *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"), ref); + return NULL; + } + + if (dir_out) + *dir_out = g_object_ref (dir); + return g_steal_pointer (&deploy); +} + +GFile * +flatpak_find_files_dir_for_ref (const char *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); +} + +char * +flatpak_find_current_ref (const char *app_id, + GCancellable *cancellable, + GError **error) +{ + g_autofree char *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, + const char *commit, + GCancellable *cancellable, + GError **error) +{ + FlatpakDeploy *deploy = NULL; + int i; + g_autoptr(GError) my_error = 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, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GPtrArray) dirs = NULL; + + dirs = flatpak_dir_get_system_list (cancellable, error); + if (dirs == NULL) + return NULL; + + 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"); +} + +#endif + +static gboolean +needs_quoting (const char *arg) +{ + 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 (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); +} + +#if 0 + +/* 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; +} + +#endif + +#if !GLIB_CHECK_VERSION(2, 34, 0) +G_LOCK_DEFINE_STATIC (path_cached); +#endif + +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"); + +#if GLIB_CHECK_VERSION(2, 34, 0) + 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); +#else + G_LOCK (path_cached); + { + path = g_object_get_qdata ((GObject *) file, _file_path_quark); + + if (path == NULL) + { + g_autofree char *new_path = g_file_get_path (file); + + path = new_path; + + if (new_path != NULL) + g_object_set_qdata_full ((GObject *) file, _file_path_quark, + g_steal_pointer (&new_path), g_free); + } + } + G_UNLOCK (path_cached); +#endif + + return path; +} + +#if 0 + +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); +} + +#endif + +/* 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); +} + +#if 0 + +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; +} + +#endif + +/* 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; +} + +#if 0 + +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 */ +static 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 */ +char ** +flatpak_summary_match_subrefs (GVariant *summary_v, + const char *collection_id, + const char *ref) +{ + GPtrArray *res = g_ptr_array_new (); + gsize n, i; + g_auto(GStrv) parts = NULL; + 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. */ + parts = g_strsplit (ref, "/", 0); + parts_prefix = g_strconcat (parts[1], ".", NULL); + + ref_prefix = g_strconcat (parts[0], "/", NULL); + ref_suffix = g_strconcat ("/", parts[2], "/", parts[3], 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; + + 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; + + /* But only prefix of id */ + if (!g_str_has_prefix (id_start + 1, parts_prefix)) + continue; + + g_ptr_array_add (res, g_strdup (cur)); + } + } + + g_ptr_array_add (res, NULL); + return (char **) g_ptr_array_free (res, FALSE); +} + +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; +} + +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 *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); + + title = g_key_file_get_locale_string (keyfile, source_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 */ + { + 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); + } + + collection_id = g_key_file_get_string (keyfile, source_group, + FLATPAK_REPO_DEPLOY_COLLECTION_ID_KEY, NULL); + if (collection_id == NULL || *collection_id == '\0') + collection_id = g_key_file_get_string (keyfile, source_group, + FLATPAK_REPO_COLLECTION_ID_KEY, NULL); + 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; +} + +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 a 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)); +} + +typedef struct +{ + guint64 installed_size; + guint64 download_size; + char *metadata_contents; + GVariant *sparse_data; +} CommitData; + +static void +commit_data_free (gpointer data) +{ + CommitData *rev_data = data; + + g_free (rev_data->metadata_contents); + if (rev_data->sparse_data) + g_variant_unref (rev_data->sparse_data); + g_free (rev_data); +} + +/* For all the refs listed in @cache_v (an xa.cache value) which exist in the + * @summary, insert their data into @commit_data_cache if it isn’t already there. */ +static void +populate_commit_data_cache (GVariant *metadata, + GVariant *summary, + GHashTable *commit_data_cache /* (element-type utf8 CommitData) */) +{ + g_autoptr(GVariant) cache_v = NULL; + g_autoptr(GVariant) cache = NULL; + g_autoptr(GVariant) sparse_cache = NULL; + gsize n, i; + guint32 cache_version = 0; + const char *old_collection_id; + + if (!g_variant_lookup (metadata, "ostree.summary.collection-id", "&s", &old_collection_id)) + old_collection_id = NULL; + + cache_v = g_variant_lookup_value (metadata, "xa.cache", NULL); + if (cache_v == NULL) + return; + + if (g_variant_lookup (metadata, "xa.cache-version", "u", &cache_version)) + cache_version = GUINT32_FROM_LE (cache_version); + else + cache_version = 0; + + if (cache_version < FLATPAK_XA_CACHE_VERSION) + return; /* We need to rebuild the cache with the current version */ + + cache = g_variant_get_child_value (cache_v, 0); + + sparse_cache = g_variant_lookup_value (metadata, "xa.sparse-cache", NULL); + + n = g_variant_n_children (cache); + for (i = 0; i < n; i++) + { + g_autoptr(GVariant) old_element = g_variant_get_child_value (cache, i); + g_autoptr(GVariant) old_ref_v = g_variant_get_child_value (old_element, 0); + const char *old_ref = g_variant_get_string (old_ref_v, NULL); + g_autofree char *old_rev = NULL; + g_autoptr(GVariant) old_commit_data_v = g_variant_get_child_value (old_element, 1); + CommitData *old_rev_data; + + if (flatpak_summary_lookup_ref (summary, old_collection_id, old_ref, &old_rev, NULL)) + { + guint64 old_installed_size, old_download_size; + g_autofree char *old_metadata = NULL; + + /* See if we already have the info on this revision */ + if (g_hash_table_lookup (commit_data_cache, old_rev)) + continue; + + g_variant_get_child (old_commit_data_v, 0, "t", &old_installed_size); + old_installed_size = GUINT64_FROM_BE (old_installed_size); + g_variant_get_child (old_commit_data_v, 1, "t", &old_download_size); + old_download_size = GUINT64_FROM_BE (old_download_size); + g_variant_get_child (old_commit_data_v, 2, "s", &old_metadata); + + old_rev_data = g_new0 (CommitData, 1); + old_rev_data->installed_size = old_installed_size; + old_rev_data->download_size = old_download_size; + old_rev_data->metadata_contents = g_steal_pointer (&old_metadata); + + if (sparse_cache) + old_rev_data->sparse_data = g_variant_lookup_value (sparse_cache, old_ref, G_VARIANT_TYPE_VARDICT); + + g_hash_table_insert (commit_data_cache, g_steal_pointer (&old_rev), old_rev_data); + } + } +} + +/* 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, + const char **gpg_key_ids, + const char *gpg_homedir, + GCancellable *cancellable, + GError **error) +{ + g_autoptr(GVariantBuilder) builder = g_variant_builder_new (G_VARIANT_TYPE_VARDICT); + g_autoptr(GVariantBuilder) commits_builder = g_variant_builder_new (G_VARIANT_TYPE ("aay")); + 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}}")); + 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 *authenticator_name = NULL; + g_autofree char *gpg_keys = NULL; + g_auto(GStrv) config_keys = NULL; + int authenticator_install = -1; + g_autoptr(GVariant) old_summary = NULL; + g_autoptr(GVariant) new_summary = NULL; + g_autoptr(GHashTable) refs = NULL; + const char *prefixes[] = { "appstream", "appstream2", "app", "runtime", NULL }; + const char **prefix; + g_autoptr(GList) ordered_keys = NULL; + GList *l = NULL; + g_autoptr(GHashTable) commit_data_cache = NULL; + const char *collection_id; + g_autofree char *old_ostree_metadata_checksum = NULL; + g_autoptr(GVariant) old_ostree_metadata_v = NULL; + gboolean deploy_collection_id = FALSE; + gboolean deploy_sideload_collection_id = FALSE; + + config = ostree_repo_get_config (repo); + + if (config) + { + 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); + + if (title) + g_variant_builder_add (builder, "{sv}", "xa.title", + g_variant_new_string (title)); + + if (comment) + g_variant_builder_add (builder, "{sv}", "xa.comment", + g_variant_new_string (comment)); + + if (description) + g_variant_builder_add (builder, "{sv}", "xa.description", + g_variant_new_string (description)); + + if (homepage) + g_variant_builder_add (builder, "{sv}", "xa.homepage", + g_variant_new_string (homepage)); + + if (icon) + g_variant_builder_add (builder, "{sv}", "xa.icon", + g_variant_new_string (icon)); + + if (redirect_url) + g_variant_builder_add (builder, "{sv}", "xa.redirect-url", + g_variant_new_string (redirect_url)); + + if (default_branch) + g_variant_builder_add (builder, "{sv}", "xa.default-branch", + g_variant_new_string (default_branch)); + + if (deploy_collection_id && collection_id != NULL) + g_variant_builder_add (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 (builder, "{sv}", "xa.deploy-collection-id", + g_variant_new_string (collection_id)); + else if (deploy_collection_id) + g_debug ("Ignoring deploy-collection-id=true because no collection ID is set."); + + if (authenticator_name) + g_variant_builder_add (builder, "{sv}", "xa.authenticator-name", + g_variant_new_string (authenticator_name)); + + if (authenticator_install != -1) + g_variant_builder_add (builder, "{sv}", "xa.authenticator-install", + g_variant_new_boolean (authenticator_install)); + + 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 (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 (builder, "{sv}", "xa.gpg-keys", + g_variant_new_from_data (G_VARIANT_TYPE ("ay"), decoded, decoded_len, + TRUE, (GDestroyNotify) g_free, decoded)); + } + + /* Only operate on flatpak relevant refs */ + refs = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + for (prefix = prefixes; *prefix != NULL; prefix++) + { + g_autoptr(GHashTable) prefix_refs = NULL; + GHashTableIter hashiter; + gpointer key, value; + + if (!ostree_repo_list_refs_ext (repo, *prefix, &prefix_refs, + OSTREE_REPO_LIST_REFS_EXT_NONE, + cancellable, error)) + return FALSE; + + /* Merge the prefix refs to the full refs table */ + g_hash_table_iter_init (&hashiter, prefix_refs); + while (g_hash_table_iter_next (&hashiter, &key, &value)) + { + char *ref = g_strdup (key); + char *rev = g_strdup (value); + g_hash_table_replace (refs, ref, rev); + } + } + + commit_data_cache = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, commit_data_free); + + old_summary = flatpak_repo_load_summary (repo, NULL); + + if (!flatpak_repo_resolve_rev (repo, collection_id, NULL, OSTREE_REPO_METADATA_REF, + TRUE, &old_ostree_metadata_checksum, cancellable, error)) + return FALSE; + + if (old_summary != NULL && + old_ostree_metadata_checksum != NULL && + ostree_repo_load_commit (repo, old_ostree_metadata_checksum, &old_ostree_metadata_v, NULL, NULL)) + { + g_autoptr(GVariant) metadata = g_variant_get_child_value (old_ostree_metadata_v, 0); + + populate_commit_data_cache (metadata, old_summary, commit_data_cache); + } + else if (old_summary != NULL) + { + g_autoptr(GVariant) extensions = g_variant_get_child_value (old_summary, 1); + + populate_commit_data_cache (extensions, old_summary, commit_data_cache); + } + + ordered_keys = g_hash_table_get_keys (refs); + ordered_keys = g_list_sort (ordered_keys, (GCompareFunc) strcmp); + for (l = ordered_keys; l; l = l->next) + { + const char *ref = l->data; + const char *rev = g_hash_table_lookup (refs, ref); + 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; + 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; + + /* See if we already have the info on this revision */ + if (g_hash_table_lookup (commit_data_cache, rev)) + continue; + + if (!ostree_repo_read_commit (repo, rev, &root, &commit, NULL, error)) + return FALSE; + + if (!ostree_repo_load_commit (repo, commit, &commit_v, NULL, error)) + return FALSE; + + 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 FALSE; + } + + 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); + + 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 download_size; + flatpak_repo_parse_extra_data_sources (extra_data_sources, i, + NULL, + &download_size, + NULL, + NULL, + NULL); + total_extra_data_download_size += 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)); + } + + g_hash_table_insert (commit_data_cache, g_strdup (rev), rev_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 = g_hash_table_lookup (commit_data_cache, + rev); + + 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); + g_variant_builder_add (commits_builder, "@ay", ostree_checksum_to_bytes_v (rev)); + } + + /* 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 (builder, "{sv}", "xa.cache", + g_variant_new_variant (g_variant_builder_end (ref_data_builder))); + g_variant_builder_add (builder, "{sv}", "xa.cache-version", + g_variant_new_uint32 (GUINT32_TO_LE (FLATPAK_XA_CACHE_VERSION))); + + g_variant_builder_add (builder, "{sv}", "xa.sparse-cache", + g_variant_builder_end (ref_sparse_data_builder)); + + new_summary = g_variant_ref_sink (g_variant_builder_end (builder)); + + /* Write out a new metadata commit for the repository. */ + if (collection_id != NULL) + { + OstreeCollectionRef collection_ref = { (gchar *) collection_id, (gchar *) OSTREE_REPO_METADATA_REF }; + g_autofree gchar *new_ostree_metadata_checksum = NULL; + g_autoptr(OstreeMutableTree) mtree = NULL; + g_autoptr(OstreeRepoFile) repo_file = NULL; + g_autoptr(GVariantDict) new_summary_commit_dict = NULL; + g_autoptr(GVariant) new_summary_commit = NULL; + + /* Add bindings to the metadata. */ + new_summary_commit_dict = g_variant_dict_new (new_summary); + g_variant_dict_insert_value (new_summary_commit_dict, "xa.commits", + g_variant_builder_end (commits_builder)); + g_variant_dict_insert (new_summary_commit_dict, "ostree.collection-binding", + "s", collection_ref.collection_id); + g_variant_dict_insert_value (new_summary_commit_dict, "ostree.ref-binding", + g_variant_new_strv ((const gchar * const *) &collection_ref.ref_name, 1)); + new_summary_commit = g_variant_ref_sink (g_variant_dict_end (new_summary_commit_dict)); + + if (!ostree_repo_prepare_transaction (repo, NULL, cancellable, error)) + goto out; + + /* Set up an empty mtree. */ + mtree = ostree_mutable_tree_new (); + if (!flatpak_mtree_ensure_dir_metadata (repo, mtree, cancellable, error)) + goto out; + if (!ostree_repo_write_mtree (repo, mtree, (GFile **) &repo_file, NULL, error)) + goto out; + + if (!ostree_repo_write_commit (repo, old_ostree_metadata_checksum, + NULL /* subject */, NULL /* body */, + new_summary_commit, repo_file, &new_ostree_metadata_checksum, + NULL, error)) + goto out; + + if (gpg_key_ids != NULL) + { + const char * const *iter; + + for (iter = gpg_key_ids; iter != NULL && *iter != NULL; iter++) + { + const char *key_id = *iter; + + if (!ostree_repo_sign_commit (repo, + new_ostree_metadata_checksum, + key_id, + gpg_homedir, + cancellable, + error)) + goto out; + } + } + + ostree_repo_transaction_set_collection_ref (repo, &collection_ref, + new_ostree_metadata_checksum); + + if (!ostree_repo_commit_transaction (repo, NULL, cancellable, error)) + goto out; + } + + /* Regenerate and re-sign the summary file. */ + if (!ostree_repo_regenerate_summary (repo, new_summary, 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; + } + + return TRUE; + +out: + if (repo != NULL) + ostree_repo_abort_transaction (repo, cancellable, NULL); + return FALSE; +} + +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 (component, bundle)); + + 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_debug ("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, + const char *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, 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; + } + + app_info_dir = g_file_resolve_relative_path (root, "files/share/app-info"); + + xmls_dir = g_file_resolve_relative_path (app_info_dir, "xmls"); + icons_dir = g_file_resolve_relative_path (app_info_dir, "icons/flatpak"); + + appstream_basename = g_strconcat (id, ".xml.gz", NULL); + appstream_file = g_file_get_child (xmls_dir, appstream_basename); + + 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, + 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); + } + } + } +} + + +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_autofree const char **all_refs_keys = NULL; + guint n_keys; + gsize i; + g_autoptr(GHashTable) arches = NULL; /* (element-type utf8 utf8) */ + const char *collection_id; + + arches = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + collection_id = ostree_repo_get_collection_id (repo); + + if (!ostree_repo_list_refs (repo, + NULL, + &all_refs, + cancellable, + error)) + return FALSE; + + all_refs_keys = (const char **) 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 (char *), (GCompareDataFunc) flatpak_strcmp0_ptr, NULL); + + for (i = 0; i < n_keys; i++) + { + const char *ref = all_refs_keys[i]; + g_auto(GStrv) split = NULL; + const char *arch; + + split = flatpak_decompose_ref (ref, NULL); + if (!split) + continue; + + arch = split[2]; + if (!g_hash_table_contains (arches, arch)) + { + const char *reverse_compat_arch; + g_hash_table_add (arches, g_strdup (arch)); + + /* If repo contains e.g. i386, also generated x86-64 appdata */ + reverse_compat_arch = flatpak_get_compat_arch_reverse (arch); + if (reverse_compat_arch) + g_hash_table_add (arches, g_strdup (reverse_compat_arch)); + } + } + + GLNX_HASH_TABLE_FOREACH (arches, const char *, arch) + { + OstreeRepoTransactionStats stats; + 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; + g_autoptr(FlatpakRepoTransaction) transaction = NULL; + compat_arch = flatpak_get_compat_arch (arch); + const char *branch_names[] = { "appstream", "appstream2" }; + + 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 (i = 0; i < n_keys; i++) + { + const char *ref = all_refs_keys[i]; + const char *commit; + g_autoptr(GVariant) commit_v = NULL; + g_autoptr(GVariant) commit_metadata = NULL; + g_auto(GStrv) split = NULL; + g_autoptr(GError) my_error = NULL; + const char *eol = NULL; + const char *eol_rebase = NULL; + + split = flatpak_decompose_ref (ref, NULL); + if (!split) + continue; + + if (strcmp (split[2], arch) != 0) + { + g_autofree char *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 (g_strcmp0 (split[2], compat_arch) == 0) + main_ref = g_strdup_printf ("%s/%s/%s/%s", + split[0], split[1], arch, split[3]); + if (main_ref == NULL || + g_hash_table_lookup (all_refs, main_ref)) + continue; + } + + commit = g_hash_table_lookup (all_refs, ref); + + if (!ostree_repo_load_variant (repo, OSTREE_OBJECT_TYPE_COMMIT, commit, + &commit_v, NULL)) + { + g_warning ("Couldn't load commit %s (ref %s)", commit, ref); + continue; + } + + commit_metadata = g_variant_get_child_value (commit_v, 0); + 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 (eol || eol_rebase) + { + g_print (_("%s is end-of-life, ignoring\n"), ref); + continue; + } + + if (!extract_appstream (repo, appstream_root, + ref, split[1], size1_mtree, size2_mtree, + cancellable, &my_error)) + { + if (g_str_has_prefix (ref, "app/")) + g_print (_("No appstream data for %s: %s\n"), ref, my_error->message); + continue; + } + } + + if (!flatpak_appstream_xml_root_to_data (appstream_root, &xml_data, &xml_gz_data, error)) + return FALSE; + + transaction = flatpak_repo_transaction_start (repo, cancellable, error); + if (transaction == NULL) + return FALSE; + + for (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; + + 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_debug ("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) + { + int i; + + for (i = 0; gpg_key_ids[i] != NULL; i++) + { + const char *keyid = gpg_key_ids[i]; + + if (!ostree_repo_sign_commit (repo, + commit_checksum, + keyid, + gpg_homedir, + cancellable, + error)) + return FALSE; + } + } + + 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); + } + } + } + + 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); + g_free (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, + const char *ref, + const char *directory, + const char *add_ld_path, + const char *subdir_suffix, + char **merge_dirs, + GFile *files, + GFile *deploy_dir, + gboolean is_unmaintained) +{ + 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 = g_strdup (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; + + if (deploy_dir) + { + deploy_data = flatpak_load_deploy_data (deploy_dir, ref, 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, "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_autofree char *ref = NULL; + gboolean is_unmaintained = FALSE; + g_autoptr(GFile) files = NULL; + g_autoptr(GFile) deploy_dir = NULL; + + if (directory == NULL) + return res; + + ref = g_build_filename ("runtime", extension, arch, branch, NULL); + + files = flatpak_find_unmaintained_extension_dir_if_exists (extension, arch, branch, NULL); + + if (files == NULL) + { + deploy_dir = flatpak_find_deploy_dir_for_ref (ref, NULL, 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); + 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) refs = NULL; + g_auto(GStrv) unmaintained_refs = NULL; + int j; + + refs = flatpak_list_deployed_refs ("runtime", prefix, arch, branch, + NULL, NULL); + for (j = 0; refs != NULL && refs[j] != NULL; j++) + { + g_autofree char *extended_dir = g_build_filename (directory, refs[j] + strlen (prefix), NULL); + g_autofree char *dir_ref = g_build_filename ("runtime", refs[j], arch, branch, NULL); + g_autoptr(GFile) subdir_deploy_dir = NULL; + g_autoptr(GFile) subdir_files = NULL; + subdir_deploy_dir = flatpak_find_deploy_dir_for_ref (dir_ref, NULL, NULL, NULL); + if (subdir_deploy_dir) + subdir_files = g_file_get_child (subdir_deploy_dir, "files"); + + if (subdir_files && flatpak_extension_matches_reason (refs[j], enable_if, TRUE)) + { + ext = flatpak_extension_new (extension, refs[j], dir_ref, extended_dir, add_ld_path, subdir_suffix, merge_dirs, subdir_files, subdir_deploy_dir, FALSE); + 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_autofree char *dir_ref = g_build_filename ("runtime", unmaintained_refs[j], arch, branch, NULL); + g_autoptr(GFile) subdir_files = flatpak_find_unmaintained_extension_dir_if_exists (unmaintained_refs[j], arch, branch, NULL); + + 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); + 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, + char **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) + { + if (!g_variant_lookup (metadata, "ref", "s", ref)) + { + flatpak_fail_error (error, FLATPAK_ERROR_INVALID_DATA, _("Invalid bundle, no ref in metadata")); + return NULL; + } + } + + 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) +{ + 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 (!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; + + /* Null terminate */ + g_output_stream_write (G_OUTPUT_STREAM (data_stream), "\0", 1, NULL, NULL); + + metadata_valid = + metadata_contents != NULL && + strcmp (metadata_contents, g_memory_output_stream_get_data (data_stream)) == 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_debug ("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_debug ("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; +} + +char * +flatpak_prompt (gboolean allow_empty, + const char *prompt, ...) +{ + char buf[512]; + va_list var_args; + g_autofree char *s = NULL; + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + va_start (var_args, prompt); + s = g_strdup_vprintf (prompt, var_args); + va_end (var_args); +#pragma GCC diagnostic pop + + 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; + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + va_start (var_args, prompt); + s = g_strdup_vprintf (prompt, var_args); + va_end (var_args); +#pragma GCC diagnostic pop + + 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; + + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-nonliteral" + va_start (var_args, prompt); + s = g_strdup_vprintf (prompt, var_args); + va_end (var_args); +#pragma GCC diagnostic pop + + 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"); +} + +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; +} + +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); +} + +#endif + +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 0 + +char ** +flatpak_get_current_locale_langs (void) +{ + const gchar * const *locales = g_get_language_names (); + GPtrArray *langs = g_ptr_array_new (); + int i; + + 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_debug ("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, const char *t) +{ + int ls = strlen (s); + int lt = strlen (t); + int i, j; + int *d; + + 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) +{ + write (STDOUT_FILENO, FLATPAK_ANSI_HIDE_CURSOR, strlen (FLATPAK_ANSI_HIDE_CURSOR)); +} + +void +flatpak_show_cursor (void) +{ + write (STDOUT_FILENO, FLATPAK_ANSI_SHOW_CURSOR, strlen (FLATPAK_ANSI_SHOW_CURSOR)); +} + +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 i, i1, i2; + int num_components = -1; + + for (i = 0; path1[i] != '\0'; i++) + { + if (path2[i] == '\0') + break; + + if (tolower (path1[i]) == tolower (path2[i])) + { + if (path1[i] == '/') + num_components++; + continue; + } + + if ((path1[i] == '-' || path1[i] == '_') && + (path2[i] == '-' || path2[i] == '_')) + 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 */ + i1 = i2 = i; + 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'); +} + +#endif diff --git a/src/glib-backports.c b/src/glib-backports.c index ce4990ee1..3a4694733 100644 --- a/src/glib-backports.c +++ b/src/glib-backports.c @@ -33,9 +33,6 @@ #include <glib-object.h> -/* We have no internationalization */ -#define _(x) x - #if !GLIB_CHECK_VERSION (2, 34, 0) G_DEFINE_QUARK (g-spawn-exit-error-quark, my_g_spawn_exit_error) #endif @@ -490,3 +487,26 @@ my_g_canonicalize_filename (const gchar *filename, return canon; } #endif + +#if !GLIB_CHECK_VERSION(2, 40, 0) +gpointer * +my_g_hash_table_get_keys_as_array (GHashTable *hash, + guint *len) +{ + GPtrArray *arr = g_ptr_array_sized_new (g_hash_table_size (hash)); + GHashTableIter iter; + gpointer k; + + g_hash_table_iter_init (&iter, hash); + + while (g_hash_table_iter_next (&iter, &k, NULL)) + g_ptr_array_add (arr, k); + + if (len != NULL) + *len = arr->len; + + g_ptr_array_add (arr, NULL); + + return g_ptr_array_free (arr, FALSE); +} +#endif diff --git a/src/glib-backports.h b/src/glib-backports.h index 78f7fe164..73a587bce 100644 --- a/src/glib-backports.h +++ b/src/glib-backports.h @@ -100,3 +100,10 @@ GSource *my_g_unix_fd_source_new (int fd, gchar *my_g_canonicalize_filename (const gchar *filename, const gchar *relative_to); #endif + +#if !GLIB_CHECK_VERSION(2, 40, 0) +#define g_hash_table_get_keys_as_array(h, l) \ + my_g_hash_table_get_keys_as_array (h, l) +gpointer *my_g_hash_table_get_keys_as_array (GHashTable *hash, + guint *len); +#endif diff --git a/src/meson.build b/src/meson.build index 27242f101..2529a2283 100644 --- a/src/meson.build +++ b/src/meson.build @@ -148,6 +148,11 @@ executable( 'bwrap.h', 'flatpak-bwrap.c', 'flatpak-bwrap-private.h', + 'flatpak-common-types-private.h', + 'flatpak-context.c', + 'flatpak-context-private.h', + 'flatpak-exports.c', + 'flatpak-exports-private.h', 'flatpak-run.c', 'flatpak-run-private.h', 'runtime.c', -- GitLab