diff --git a/config.h.in b/config.h.in index 70ad448897c36f38befe386e559e9a494aa52871..6760d62a3e2f41936c2584e82789a9c264e6a3f3 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 0000000000000000000000000000000000000000..7f77b243556ba4345c7b80991b19339edecc5059 --- /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 0000000000000000000000000000000000000000..2773c979b200fc52bd0d49b77fea26c254887ec5 --- /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 0000000000000000000000000000000000000000..5cddc0db5dd358308106ed4764c16cee1e47cb07 --- /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 0000000000000000000000000000000000000000..d62bf8fa9c8811c54e2dec6c338315a544a537af --- /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 0000000000000000000000000000000000000000..6da3bef4b0714874090e745154b7a117a903519e --- /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 0000000000000000000000000000000000000000..eb072a21ed0928b92984f266727df5d6e4b3ca58 --- /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 24f1a8fd77b06fed406050c2cba0c7bb7dc44868..ddc9a406c535cfa313b06b0ae386e96332b146a0 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 94a20e25ee64917ddfe8323f8fd7086aff9db868..addd6e70786e31eb6d95bc3a674615db5ae7a60c 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 612a71e357ed3a0c0b712e34894edeb9c7658324..9e418e5a0c5c5cd464ef8bb84c518a96d8804e95 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 d796878008461472273a2f325a7540905fd6fb3c..c3b913de261995fb7e83afea1a49b566c54fe68c 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 ce4990ee15acbd95fbe213285a2e28c35370ea39..3a4694733a39a729ba1634674b036f148aa4ecfb 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 78f7fe164f4b3566c4a3b69ee79255144234b766..73a587bce98758cfdeb7e0ab4322810c57b16af6 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 27242f101364c88817a4708058217a7ca2f4bd72..2529a228306c95e01ff30000e661c0efb762c600 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',